Update ci

This commit is contained in:
Xuwznln
2025-10-01 17:07:31 +08:00
parent e0a1e69fa8
commit a9fc374d31
37 changed files with 872 additions and 1140 deletions

10
.bumpversion.cfg Normal file
View File

@@ -0,0 +1,10 @@
[bumpversion]
current_version = 0.0.1
commit = True
tag = True
tag_name = v{new_version}
message = Bump version: {current_version} → {new_version}
[bumpversion:file:elevator_saga/__init__.py]
search = __version__ = "{current_version}"
replace = __version__ = "{new_version}"

View File

@@ -18,13 +18,13 @@ updates:
commit-message:
prefix: "deps"
include: "scope"
# GitHub Actions
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"
day: "monday"
day: "monday"
time: "06:00"
open-pull-requests-limit: 5
reviewers:
@@ -35,4 +35,3 @@ updates:
commit-message:
prefix: "ci"
include: "scope"

View File

@@ -1,203 +1,181 @@
name: CI
# This workflow will install Python dependencies, run tests and lint with a variety of Python versions
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python
name: Python package
on:
push:
branches: [ main, develop ]
branches: ["main", "dev"]
pull_request:
branches: [ main, develop ]
schedule:
# Run tests daily at 6 AM UTC
- cron: '0 6 * * *'
branches: ["main", "dev"]
jobs:
test:
name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest]
python-version: ['3.12']
steps:
- uses: actions/checkout@v4
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v4
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ hashFiles('**/pyproject.toml', '**/setup.py') }}
restore-keys: |
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Check dependencies
run: |
python run_all_tests.py --check-deps
- name: Run linting
run: |
black --check elevator_saga tests
isort --check-only elevator_saga tests
- name: Run type checking
run: |
mypy elevator_saga
- name: Run tests with coverage
run: |
python -m pytest --cov=elevator_saga --cov-report=xml --cov-report=term-missing
- name: Upload coverage to Codecov
if: matrix.python-version == '3.12' && matrix.os == 'ubuntu-latest'
uses: codecov/codecov-action@v3
with:
file: ./coverage.xml
flags: unittests
name: codecov-umbrella
fail_ci_if_error: false
test-examples:
name: Test examples
# Step 1: Code formatting and pre-commit validation (fast failure)
code-format:
name: Code formatting and pre-commit validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Run example tests
run: |
python run_all_tests.py --type examples
build:
name: Build and check package
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10" # Use minimum version for consistency
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files
# Step 2: Basic build and test with minimum Python version (3.10)
basic-build:
name: Basic build (Python 3.10, Ubuntu)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine check-manifest
- name: Check manifest
run: check-manifest
- name: Build package
run: python -m build
- name: Check package
run: twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v3
with:
name: dist
path: dist/
needs: [code-format] # Only run after code formatting passes
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.10
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ubuntu-pip-3.10-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
ubuntu-pip-3.10-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
pip install -e .[dev]
- name: Test with pytest
run: |
pytest -v
# Step 3: Security scan
security:
name: Security scan
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
- name: Install security tools
run: |
python -m pip install --upgrade pip
pip install bandit safety
- name: Run bandit security scan
run: bandit -r elevator_saga/ -f json -o bandit-report.json
continue-on-error: true
- name: Run safety security scan
run: safety check --json --output safety-report.json
continue-on-error: true
- name: Upload security reports
uses: actions/upload-artifact@v3
with:
name: security-reports
path: |
bandit-report.json
safety-report.json
if: always()
needs: [basic-build] # Run in parallel with other tests after basic build
docs:
name: Build documentation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev][docs]
# 为将来的文档构建预留
- name: Check documentation
run: |
echo "Documentation build placeholder"
# sphinx-build -b html docs docs/_build/html
performance:
name: Performance benchmarks
runs-on: ubuntu-latest
if: github.event_name == 'push' && github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
pip install pytest-benchmark
# 为将来的性能测试预留
- name: Run benchmarks
run: |
echo "Performance benchmarks placeholder"
# python -m pytest tests/benchmarks/ --benchmark-json=benchmark.json
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Run Safety CLI to check for vulnerabilities
uses: pyupio/safety-action@v1
with:
api-key: ${{ secrets.SAFETY_CHECK }}
output-format: json
args: --detailed-output --output-format json
continue-on-error: true
- name: Upload security reports
uses: actions/upload-artifact@v4
with:
name: security-reports
path: |
safety-report.json
if: always()
# Step 4: Package build check
package-build:
name: Package build check
runs-on: ubuntu-latest
needs: [basic-build] # Run in parallel with other checks
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10" # Use minimum version for consistency
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine
- name: Build package
run: python -m build
- name: Check package
run: twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist
path: dist/
# Step 5: Full matrix build (only after all basic checks pass)
full-matrix-build:
name: Test Python ${{ matrix.python-version }} on ${{ matrix.os }}
runs-on: ${{ matrix.os }}
needs: [security, package-build] # Wait for all prerequisite checks
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, windows-latest, macos-latest]
python-version: ["3.10", "3.11", "3.12", "3.13"]
exclude:
# Skip the combination we already tested in basic-build
- os: ubuntu-latest
python-version: "3.10"
steps:
- uses: actions/checkout@v5
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v6
with:
python-version: ${{ matrix.python-version }}
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-${{ matrix.python-version }}-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-${{ matrix.python-version }}-
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install flake8 pytest
pip install -e .[dev]
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings
flake8 . --count --exit-zero --max-line-length=200 --extend-ignore=E203,W503,F401,E402,E721,F841 --statistics
- name: Type checking with mypy
run: |
mypy elevator_saga --disable-error-code=unused-ignore
continue-on-error: true
- name: Test with pytest
run: |
pytest

View File

@@ -1,269 +0,0 @@
name: Code Quality
on:
push:
branches: [ main, develop ]
pull_request:
branches: [ main, develop ]
schedule:
# Run weekly code quality checks
- cron: '0 2 * * 1'
jobs:
lint:
name: Linting and formatting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Cache pip dependencies
uses: actions/cache@v3
with:
path: ~/.cache/pip
key: ${{ runner.os }}-pip-lint-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
${{ runner.os }}-pip-lint-
${{ runner.os }}-pip-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install flake8 pylint
- name: Run Black formatting check
run: |
black --check --diff elevator_saga tests
- name: Run isort import sorting check
run: |
isort --check-only --diff elevator_saga tests
- name: Run flake8
run: |
flake8 elevator_saga tests --count --select=E9,F63,F7,F82 --show-source --statistics
flake8 elevator_saga tests --count --exit-zero --max-complexity=10 --max-line-length=88 --statistics
- name: Run pylint
run: |
pylint elevator_saga --exit-zero --output-format=parseable --reports=no | tee pylint-report.txt
- name: Upload lint reports
uses: actions/upload-artifact@v3
with:
name: lint-reports
path: |
pylint-report.txt
if: always()
type-check:
name: Type checking
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
- name: Run mypy type checking
run: |
mypy elevator_saga --html-report mypy-report --txt-report mypy-report
- name: Upload type check reports
uses: actions/upload-artifact@v3
with:
name: type-check-reports
path: mypy-report/
if: always()
complexity:
name: Code complexity analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install radon xenon
- name: Run cyclomatic complexity check
run: |
radon cc elevator_saga --min B --total-average
- name: Run maintainability index
run: |
radon mi elevator_saga --min B
- name: Run complexity with xenon
run: |
xenon --max-absolute B --max-modules B --max-average A elevator_saga
continue-on-error: true
dependencies:
name: Dependency analysis
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pip-audit pipdeptree
- name: Generate dependency tree
run: |
pipdeptree --freeze > requirements-freeze.txt
pipdeptree --graph-output png > dependency-graph.png
- name: Check for known vulnerabilities
run: |
pip-audit --format=json --output=vulnerability-report.json
continue-on-error: true
- name: Upload dependency reports
uses: actions/upload-artifact@v3
with:
name: dependency-reports
path: |
requirements-freeze.txt
dependency-graph.png
vulnerability-report.json
if: always()
documentation:
name: Documentation quality
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .
pip install pydocstyle interrogate
- name: Check docstring style
run: |
pydocstyle elevator_saga --count --explain --source
continue-on-error: true
- name: Check docstring coverage
run: |
interrogate elevator_saga --ignore-init-method --ignore-magic --ignore-module --ignore-nested-functions --fail-under=70
continue-on-error: true
pre-commit:
name: Pre-commit hooks
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
- name: Install pre-commit
run: |
python -m pip install --upgrade pip
pip install pre-commit
- name: Cache pre-commit
uses: actions/cache@v3
with:
path: ~/.cache/pre-commit
key: pre-commit-${{ hashFiles('.pre-commit-config.yaml') }}
- name: Run pre-commit hooks
run: |
pre-commit run --all-files --show-diff-on-failure
continue-on-error: true
summary:
name: Quality summary
runs-on: ubuntu-latest
needs: [lint, type-check, complexity, dependencies, documentation, pre-commit]
if: always()
steps:
- name: Create quality report
run: |
echo "## 📊 Code Quality Report" >> $GITHUB_STEP_SUMMARY
echo "| Check | Status |" >> $GITHUB_STEP_SUMMARY
echo "|-------|--------|" >> $GITHUB_STEP_SUMMARY
# Format results
if [ "${{ needs.lint.result }}" = "success" ]; then
echo "| Linting | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| Linting | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.type-check.result }}" = "success" ]; then
echo "| Type Check | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| Type Check | ❌ Failed |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.complexity.result }}" = "success" ]; then
echo "| Complexity | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| Complexity | ⚠️ Warning |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.dependencies.result }}" = "success" ]; then
echo "| Dependencies | ✅ Secure |" >> $GITHUB_STEP_SUMMARY
else
echo "| Dependencies | ⚠️ Check needed |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.documentation.result }}" = "success" ]; then
echo "| Documentation | ✅ Good |" >> $GITHUB_STEP_SUMMARY
else
echo "| Documentation | ⚠️ Needs improvement |" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ needs.pre-commit.result }}" = "success" ]; then
echo "| Pre-commit | ✅ Passed |" >> $GITHUB_STEP_SUMMARY
else
echo "| Pre-commit | ⚠️ Some hooks failed |" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "📈 **Overall Quality:** ${{ needs.lint.result == 'success' && needs.type-check.result == 'success' && 'Good' || 'Needs attention' }}" >> $GITHUB_STEP_SUMMARY

View File

@@ -1,34 +0,0 @@
name: Dependabot Auto-merge
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
dependabot:
name: Auto-merge Dependabot PRs
runs-on: ubuntu-latest
if: github.actor == 'dependabot[bot]'
steps:
- name: Dependabot metadata
id: metadata
uses: dependabot/fetch-metadata@v1
with:
github-token: "${{ secrets.GITHUB_TOKEN }}"
- name: Enable auto-merge for Dependabot PRs
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
run: gh pr merge --auto --merge "$PR_URL"
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Comment on major version updates
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
run: |
gh pr comment "$PR_URL" --body "🚨 **Major version update detected!** Please review this PR carefully before merging."
env:
PR_URL: ${{ github.event.pull_request.html_url }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}

87
.github/workflows/docs.yml vendored Normal file
View File

@@ -0,0 +1,87 @@
name: Build and Deploy Documentation
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
branch:
description: "要部署文档的分支"
required: false
default: "main"
type: string
deploy_to_pages:
description: "是否部署到 GitHub Pages"
required: false
default: true
type: boolean
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
permissions:
contents: read
pages: write
id-token: write
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
concurrency:
group: "pages"
cancel-in-progress: false
jobs:
# Build documentation
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v5
with:
ref: ${{ github.event.inputs.branch || github.ref }}
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Install dependencies
run: |
python -m pip install --upgrade pip
# Install package in development mode to get version info
pip install -e .
# Install documentation dependencies
pip install -e .[docs]
- name: Setup Pages
id: pages
uses: actions/configure-pages@v5
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
- name: Build Sphinx documentation
run: |
# Create docs directory if it doesn't exist
mkdir -p docs
# Placeholder for Sphinx build
echo "Documentation build placeholder - configure Sphinx in docs/"
# cd docs
# make html
- name: Upload artifact
uses: actions/upload-pages-artifact@v4
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
with:
path: docs/_build/html
# Deploy to GitHub Pages
deploy:
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
runs-on: ubuntu-latest
needs: build
steps:
- name: Deploy to GitHub Pages
id: deployment
uses: actions/deploy-pages@v4

View File

@@ -1,189 +1,258 @@
name: Publish to PyPI
# This workflow will upload a Python Package to PyPI when a release is created
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
# This workflow uses actions that are not certified by GitHub.
# They are provided by a third-party and are governed by
# separate terms of service, privacy policy, and support
# documentation.
name: Upload PyPI package
on:
release:
types: [published]
types: [published, edited]
workflow_dispatch:
inputs:
test_pypi:
description: 'Publish to Test PyPI instead of PyPI'
description: "Publish to Test PyPI instead of PyPI"
required: false
default: 'false'
default: false
type: boolean
permissions:
contents: read
jobs:
test:
name: Run tests before publish
# Step 1: Code formatting and pre-commit validation (fast failure)
code-format:
name: Code formatting and pre-commit validation
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
# - name: Install dependencies
# run: |
# python -m pip install --upgrade pip
# pip install -e .
# - name: Run comprehensive tests
# run: |
# python -m pytest --cov=msgcenterpy --cov-fail-under=80
# - name: Run linting
# run: |
# black --check msgcenterpy tests
# isort --check-only msgcenterpy tests
# mypy msgcenterpy
build:
name: Build package
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10" # Use minimum version for consistency
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -e .[dev]
- name: Run pre-commit hooks
uses: pre-commit/action@v3.0.1
with:
extra_args: --all-files
# Step 2: Basic build and test with minimum Python version (3.10)
basic-build:
name: Basic build (Python 3.10, Ubuntu)
runs-on: ubuntu-latest
needs: test
steps:
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.12'
# - name: Install build dependencies
# run: |
# python -m pip install --upgrade pip
# pip install build twine check-manifest
# - name: Verify version consistency
# run: |
# # 检查版本号一致性
# VERSION=$(python -c "import elevator_saga; print(elevator_saga.__version__)" 2>/dev/null || echo "unknown")
# TAG_VERSION="${GITHUB_REF#refs/tags/v}"
# if [ "$GITHUB_EVENT_NAME" = "release" ]; then
# if [ "$VERSION" != "$TAG_VERSION" ]; then
# echo "Version mismatch: package=$VERSION, tag=$TAG_VERSION"
# exit 1
# fi
# fi
# - name: Check manifest
# run: check-manifest
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
pip install build twine check-manifest
- name: Build package
run: |
python -m build
# - name: Check package
# run: |
# twine check dist/*
- name: Upload build artifacts
uses: actions/upload-artifact@v4
with:
name: dist-${{ github.run_number }}
path: dist/
retention-days: 30
needs: [code-format] # Only run after code formatting passes
publish-test:
name: Publish to Test PyPI
steps:
- uses: actions/checkout@v5
- name: Set up Python 3.10
uses: actions/setup-python@v6
with:
python-version: "3.10"
- name: Cache pip dependencies
uses: actions/cache@v4
with:
path: ~/.cache/pip
key: ubuntu-pip-3.10-${{ hashFiles('**/pyproject.toml') }}
restore-keys: |
ubuntu-pip-3.10-
- name: Install dependencies
run: |
python -m pip install --upgrade pip
python -m pip install pytest
pip install -e .[dev]
- name: Test with pytest
run: |
pytest -v
# Step 3: Security scan
security:
name: Security scan
runs-on: ubuntu-latest
needs: build
if: github.event.inputs.test_pypi == 'true' || (github.event_name == 'release' && github.event.release.prerelease)
environment:
name: test-pypi
url: https://test.pypi.org/p/elevator-saga
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist-${{ github.run_number }}
path: dist/
- name: Publish to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
verbose: true
needs: [basic-build] # Run in parallel with other tests after basic build
publish-pypi:
steps:
- uses: actions/checkout@v5
- name: Run Safety CLI to check for vulnerabilities
uses: pyupio/safety-action@v1
with:
api-key: ${{ secrets.SAFETY_CHECK }}
output-format: json
args: --detailed-output --output-format json
continue-on-error: true
- name: Upload security reports
uses: actions/upload-artifact@v4
with:
name: security-reports
path: |
safety-report.json
if: always()
release-build:
name: Build release distributions
runs-on: ubuntu-latest
needs: [basic-build]
steps:
- uses: actions/checkout@v5
- name: Set up Python
uses: actions/setup-python@v6
with:
python-version: "3.10" # Use minimum version for consistency
- name: Install build dependencies
run: |
python -m pip install --upgrade pip
python -m pip install build twine
- name: Verify version consistency
if: github.event_name == 'release' && (github.event.action == 'published' || (github.event.action == 'edited' && !github.event.release.prerelease))
run: |
# Install package first
pip install -e .
# Get package version (fail fast if not available)
VERSION=$(python -c "import elevator_saga; print(elevator_saga.__version__)")
# Handle both v0.0.3 and 0.0.3 tag formats
RAW_TAG="${GITHUB_REF#refs/tags/}"
if [[ "$RAW_TAG" == v* ]]; then
TAG_VERSION="${RAW_TAG#v}"
else
TAG_VERSION="$RAW_TAG"
fi
echo "Package version: $VERSION"
echo "Tag version: $TAG_VERSION"
if [ "$VERSION" != "$TAG_VERSION" ]; then
echo "❌ Version mismatch: package=$VERSION, tag=$TAG_VERSION"
echo "Please ensure the package version matches the git tag"
exit 1
fi
echo "✅ Version verification passed: $VERSION"
- name: Build release distributions
run: |
python -m build
- name: Check package
run: |
twine check dist/*
- name: Upload distributions
uses: actions/upload-artifact@v4
with:
name: release-dists
path: dist/
pypi-publish:
name: Publish to PyPI
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release' && !github.event.release.prerelease && github.event.inputs.test_pypi != 'true'
environment:
name: pypi
url: https://pypi.org/p/elevator-saga
needs:
- release-build
if: github.event_name == 'release' && !github.event.release.prerelease && github.event.inputs.test_pypi != 'true' && (github.event.action == 'published' || github.event.action == 'edited')
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
# Note: For enhanced security, consider configuring deployment environments
# in your GitHub repository settings with protection rules
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist-${{ github.run_number }}
path: dist/
- name: Publish to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
password: ${{ secrets.PYPI_API_TOKEN }}
verbose: true
- name: Retrieve release distributions
uses: actions/download-artifact@v5
with:
name: release-dists
path: dist/
- name: Publish release distributions to PyPI
uses: pypa/gh-action-pypi-publish@release/v1
test-pypi-publish:
name: Publish to Test PyPI
runs-on: ubuntu-latest
needs:
- release-build
if: github.event.inputs.test_pypi == 'true' || (github.event_name == 'release' && github.event.release.prerelease && (github.event.action == 'published' || github.event.action == 'edited'))
permissions:
# IMPORTANT: this permission is mandatory for trusted publishing
id-token: write
# Note: For enhanced security, consider configuring deployment environments
# in your GitHub repository settings with protection rules
steps:
- name: Retrieve release distributions
uses: actions/download-artifact@v5
with:
name: release-dists
path: dist/
- name: Publish release distributions to Test PyPI
uses: pypa/gh-action-pypi-publish@release/v1
with:
repository-url: https://test.pypi.org/legacy/
create-github-release-assets:
name: Add assets to GitHub release
runs-on: ubuntu-latest
needs: build
if: github.event_name == 'release'
needs: release-build
if: github.event_name == 'release' && (github.event.action == 'published' || github.event.action == 'edited')
permissions:
contents: write # Need write access to upload release assets
steps:
- name: Download build artifacts
uses: actions/download-artifact@v3
with:
name: dist-${{ github.run_number }}
path: dist/
- name: Upload release assets
uses: softprops/action-gh-release@v1
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Retrieve release distributions
uses: actions/download-artifact@v5
with:
name: release-dists
path: dist/
- name: Upload release assets
uses: softprops/action-gh-release@v2
with:
files: dist/*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
post-publish:
name: Post-publish tasks
runs-on: ubuntu-latest
needs: [publish-pypi, publish-test]
if: always() && (needs.publish-pypi.result == 'success' || needs.publish-test.result == 'success')
steps:
- uses: actions/checkout@v4
- name: Create deployment summary
run: |
echo "## 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "| Item | Status |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.publish-pypi.result }}" = "success" ]; then
echo "| PyPI | ✅ Published |" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.publish-test.result }}" = "success" ]; then
echo "| Test PyPI | ✅ Published |" >> $GITHUB_STEP_SUMMARY
fi
echo "| GitHub Release | ✅ Assets uploaded |" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${{ github.event.release.tag_name || 'test' }} |" >> $GITHUB_STEP_SUMMARY
# 为将来的通知预留
- name: Notify team
run: |
echo "Package published successfully!"
# 可以添加 Slack、Discord 等通知
needs: [pypi-publish, test-pypi-publish]
if: always() && (needs.pypi-publish.result == 'success' || needs.test-pypi-publish.result == 'success')
steps:
- uses: actions/checkout@v5
- name: Create deployment summary
run: |
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
echo "| Item | Status |" >> $GITHUB_STEP_SUMMARY
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.pypi-publish.result }}" = "success" ]; then
echo "| PyPI | Published |" >> $GITHUB_STEP_SUMMARY
elif [ "${{ needs.test-pypi-publish.result }}" = "success" ]; then
echo "| Test PyPI | Published |" >> $GITHUB_STEP_SUMMARY
fi
echo "| GitHub Release | Assets uploaded |" >> $GITHUB_STEP_SUMMARY
echo "| Version | ${{ github.event.release.tag_name || 'test' }} |" >> $GITHUB_STEP_SUMMARY

65
.gitignore vendored
View File

@@ -1,5 +1,60 @@
.vscode
.idea
__pycache__
.mypy_cache
elevator_saga.egg-info
# ================================
# Python-related files
# ================================
# Compiled Python files
__pycache__/
*.py[cod]
*$py.class
# Distribution / packaging
.Python
build/
dist/
*.egg-info/
.installed.cfg
*.egg
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Type checking
.mypy_cache/
# Documentation
docs/_build/
docs/_static/
docs/_templates/
docs/_static/
# ================================
# IDE and Editor files
# ================================
# PyCharm
.idea/
# Visual Studio Code
.vscode/
.cursor/
.cursorignore
pyrightconfig.json
# ================================
# Operating System files
# ================================
# macOS
.DS_Store

View File

@@ -5,33 +5,34 @@ repos:
hooks:
- id: black
language_version: python3
args: ["--line-length=88"]
args: ["--line-length=120"]
# Import sorting
- repo: https://github.com/pycqa/isort
rev: 5.13.2
hooks:
- id: isort
args: ["--profile", "black", "--multi-line", "3"]
args:
["--profile", "black", "--multi-line", "3", "--line-length", "120"]
# Linting
- repo: https://github.com/pycqa/flake8
rev: 7.0.0
hooks:
- id: flake8
args:
- "--max-line-length=88"
- "--extend-ignore=E203,W503,F401"
- "--exclude=build,dist,.eggs"
args:
- "--max-line-length=200" # Allow longer lines after black formatting
- "--extend-ignore=E203,W503,F401,E402,E721,F841"
- "--exclude=build,dist,__pycache__,.mypy_cache,.pytest_cache,htmlcov,.idea,.vscode,docs/_build,elevatorpy.egg-info,elevator_saga.egg-info,.eggs"
# Type checking
- repo: https://github.com/pre-commit/mirrors-mypy
rev: v1.8.0
hooks:
- id: mypy
additional_dependencies: [types-PyYAML]
args: ["--ignore-missing-imports", "--scripts-are-modules"]
exclude: "^(tests/|examples/)"
additional_dependencies: [types-PyYAML, types-jsonschema]
args: ["--ignore-missing-imports", "--disable-error-code=unused-ignore"]
files: "^(elevator_saga/)" # Check both source code
# General pre-commit hooks
- repo: https://github.com/pre-commit/pre-commit-hooks
@@ -45,18 +46,16 @@ repos:
- id: check-json
- id: check-toml
- id: check-xml
# Security
- id: check-merge-conflict
- id: check-case-conflict
- id: check-symlinks
- id: check-added-large-files
args: ["--maxkb=1000"]
# Python specific
- id: check-ast
- id: check-builtin-literals
- id: check-docstring-first
- id: debug-statements
- id: name-tests-test
args: ["--django"]
@@ -67,47 +66,17 @@ repos:
hooks:
- id: bandit
args: ["-c", "pyproject.toml"]
additional_dependencies: ["bandit[toml]"]
additional_dependencies: ["bandit[toml]", "pbr"]
exclude: "^tests/"
# Documentation
- repo: https://github.com/pycqa/pydocstyle
rev: 6.3.0
hooks:
- id: pydocstyle
args: ["--convention=google"]
exclude: "^(tests/|examples/)"
# YAML/JSON formatting
- repo: https://github.com/pre-commit/mirrors-prettier
rev: v4.0.0-alpha.8
hooks:
- id: prettier
types_or: [yaml, json, markdown]
exclude: "^(.github/)"
# Spell checking (optional)
- repo: https://github.com/codespell-project/codespell
rev: v2.2.6
hooks:
- id: codespell
args: ["--write-changes"]
exclude: "^(.*\\.po|.*\\.pot|CHANGELOG\\.md)$"
exclude: "^(build/|dist/|__pycache__/|\\.mypy_cache/|\\.pytest_cache/|htmlcov/|\\.idea/|\\.vscode/|docs/_build/|elevatorpy\\.egg-info/|elevator_saga\\.egg-info/)"
# Global settings
default_stages: [commit, push]
default_stages: [pre-commit, pre-push]
fail_fast: false
# CI settings
ci:
autofix_commit_msg: |
[pre-commit.ci] auto fixes from pre-commit.com hooks
for more information, see https://pre-commit.ci
autofix_prs: true
autoupdate_branch: ''
autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate'
autoupdate_schedule: weekly
skip: []
submodules: false

21
LICENSE Normal file
View File

@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2025 Elevator Saga Team
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

84
README.md Normal file
View File

@@ -0,0 +1,84 @@
# Elevator Saga
<div align="center">
[![PyPI version](https://badge.fury.io/py/elevatorpy.svg)](https://badge.fury.io/py/elevatorpy)
[![Python versions](https://img.shields.io/pypi/pyversions/elevatorpy.svg)](https://pypi.org/project/elevatorpy/)
[![Build Status](https://github.com/ZGCA-Forge/Elevator/actions/workflows/ci.yml/badge.svg)](https://github.com/ZGCA-Forge/Elevator/actions)
[![Documentation](https://img.shields.io/badge/docs-GitHub%20Pages-brightgreen)](https://zgca-forge.github.io/Elevator/)
[![GitHub stars](https://img.shields.io/github/stars/ZGCA-Forge/Elevator.svg?style=social&label=Star)](https://github.com/ZGCA-Forge/Elevator)
[![GitHub forks](https://img.shields.io/github/forks/ZGCA-Forge/Elevator.svg?style=social&label=Fork)](https://github.com/ZGCA-Forge/Elevator/fork)
[![GitHub issues](https://img.shields.io/github/issues/ZGCA-Forge/Elevator.svg)](https://github.com/ZGCA-Forge/Elevator/issues)
[![License](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/ZGCA-Forge/Elevator/blob/main/LICENSE)
</div>
---
Elevator Saga is a Python implementation of an elevator [simulation game](https://play.elevatorsaga.com/) with a event-driven architecture Design and optimize elevator control algorithms to efficiently transport passengers in buildings.
### Features
- 🏢 **Realistic Simulation**: Physics-based elevator movement with acceleration, deceleration, and realistic timing
## Installation
### Basic Installation
```bash
pip install elevatorpy
```
### With Development Dependencies
```bash
pip install elevatorpy[dev]
```
### From Source
```bash
git clone https://github.com/ZGCA-Forge/Elevator.git
cd Elevator
pip install -e .[dev]
```
## Quick Start
### Running the Game
```bash
# Start the backend simulator (Terminal #1)
python -m elevator_saga.server.simulator
```
```bash
# Start your own client (Terminal #2)
# Example:
python -m elevator_saga.client_examples.bus_example
```
## Documentation
For detailed documentation, please visit: [https://zgca-forge.github.io/Elevator/](https://zgca-forge.github.io/Elevator/)
## Contributing
Contributions are welcome! Please feel free to submit a Pull Request.
## Star History
[![Star History Chart](https://api.star-history.com/svg?repos=ZGCA-Forge/Elevator&type=Date)](https://star-history.com/#ZGCA-Forge/Elevator&Date)
## License
This project is licensed under MIT License - see the [LICENSE](LICENSE) file for details.
---
<div align="center">
Made with ❤️ by the ZGCA-Forge Team
</div>

View File

@@ -6,5 +6,5 @@ A Python implementation of the Elevator Saga game with event-driven architecture
realistic elevator dispatch algorithm development and testing.
"""
__version__ = "1.0.0"
__version__ = "0.0.1"
__author__ = "ZGCA Team"

View File

@@ -134,12 +134,14 @@ class ElevatorAPIClient:
def send_elevator_command(self, command: Union[GoToFloorCommand]) -> bool:
"""发送电梯命令"""
endpoint = self._get_elevator_endpoint(command)
debug_log(f"Sending elevator command: {command.command_type} to elevator {command.elevator_id} To:F{command.floor}")
debug_log(
f"Sending elevator command: {command.command_type} to elevator {command.elevator_id} To:F{command.floor}"
)
response_data = self._send_post_request(endpoint, command.parameters)
if response_data.get("success"):
return response_data["success"]
return bool(response_data["success"])
else:
raise RuntimeError(f"Command failed: {response_data.get('error_message')}")
@@ -168,7 +170,7 @@ class ElevatorAPIClient:
try:
with urllib.request.urlopen(url, timeout=60) as response:
data = json.loads(response.read().decode("utf-8"))
data: Dict[str, Any] = json.loads(response.read().decode("utf-8"))
# debug_log(f"GET {url} -> {response.status}")
return data
except urllib.error.URLError as e:
@@ -178,7 +180,7 @@ class ElevatorAPIClient:
"""重置模拟"""
try:
response_data = self._send_post_request("/api/reset", {})
success = response_data.get("success", False)
success = bool(response_data.get("success", False))
if success:
# 清空缓存,因为状态已重置
self._cached_state = None
@@ -190,11 +192,11 @@ class ElevatorAPIClient:
debug_log(f"Reset failed: {e}")
return False
def next_traffic_round(self, full_reset = False) -> bool:
def next_traffic_round(self, full_reset: bool = False) -> bool:
"""切换到下一个流量文件"""
try:
response_data = self._send_post_request("/api/traffic/next", {"full_reset": full_reset})
success = response_data.get("success", False)
success = bool(response_data.get("success", False))
if success:
# 清空缓存,因为流量文件已切换,状态会改变
self._cached_state = None
@@ -230,7 +232,7 @@ class ElevatorAPIClient:
try:
with urllib.request.urlopen(req, timeout=600) as response:
response_data = json.loads(response.read().decode("utf-8"))
response_data: Dict[str, Any] = json.loads(response.read().decode("utf-8"))
# debug_log(f"POST {url} -> {response.status}")
return response_data
except urllib.error.URLError as e:

View File

@@ -44,7 +44,7 @@ class ElevatorController(ABC):
self.api_client = ElevatorAPIClient(server_url)
@abstractmethod
def on_init(self, elevators: List[Any], floors: List[Any]):
def on_init(self, elevators: List[Any], floors: List[Any]) -> None:
"""
算法初始化方法 - 必须由子类实现
@@ -55,7 +55,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_event_execute_start(self, tick: int, events: List[Any], elevators: List[Any], floors: List[Any]):
def on_event_execute_start(self, tick: int, events: List[Any], elevators: List[Any], floors: List[Any]) -> None:
"""
事件执行前的回调 - 必须由子类实现
@@ -68,7 +68,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_event_execute_end(self, tick: int, events: List[Any], elevators: List[Any], floors: List[Any]):
def on_event_execute_end(self, tick: int, events: List[Any], elevators: List[Any], floors: List[Any]) -> None:
"""
事件执行后的回调 - 必须由子类实现
@@ -80,20 +80,20 @@ class ElevatorController(ABC):
"""
pass
def on_start(self):
def on_start(self) -> None:
"""
算法启动前的回调 - 可选实现
"""
print(f"启动 {self.__class__.__name__} 算法")
def on_stop(self):
def on_stop(self) -> None:
"""
算法停止后的回调 - 可选实现
"""
print(f"停止 {self.__class__.__name__} 算法")
@abstractmethod
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str):
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
"""
乘客呼叫时的回调 - 可选实现
@@ -104,7 +104,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_elevator_idle(self, elevator: ProxyElevator):
def on_elevator_idle(self, elevator: ProxyElevator) -> None:
"""
电梯空闲时的回调 - 可选实现
@@ -114,7 +114,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor):
def on_elevator_stopped(self, elevator: ProxyElevator, floor: ProxyFloor) -> None:
"""
电梯停靠时的回调 - 可选实现
@@ -125,7 +125,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_passenger_board(self, elevator: ProxyElevator, passenger: ProxyPassenger):
def on_passenger_board(self, elevator: ProxyElevator, passenger: ProxyPassenger) -> None:
"""
乘客上梯时的回调 - 可选实现
@@ -136,7 +136,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor):
def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor) -> None:
"""
乘客下车时的回调 - 可选实现
@@ -148,7 +148,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_elevator_passing_floor(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str):
def on_elevator_passing_floor(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
"""
电梯经过楼层时的回调 - 可选实现
@@ -160,7 +160,7 @@ class ElevatorController(ABC):
pass
@abstractmethod
def on_elevator_approaching(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str):
def on_elevator_approaching(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
"""
电梯即将到达时的回调 - 可选实现
@@ -171,7 +171,7 @@ class ElevatorController(ABC):
"""
pass
def _internal_init(self, elevators: List[Any], floors: List[Any]):
def _internal_init(self, elevators: List[Any], floors: List[Any]) -> None:
"""内部初始化方法"""
self.elevators = elevators
self.floors = floors
@@ -180,7 +180,7 @@ class ElevatorController(ABC):
# 调用用户的初始化方法
self.on_init(elevators, floors)
def start(self):
def start(self) -> None:
"""
启动控制器
"""
@@ -198,12 +198,12 @@ class ElevatorController(ABC):
self.is_running = False
self.on_stop()
def stop(self):
def stop(self) -> None:
"""停止控制器"""
self.is_running = False
print(f"停止 {self.__class__.__name__}")
def on_simulation_complete(self, final_state: Dict[str, Any]):
def on_simulation_complete(self, final_state: Dict[str, Any]) -> None:
"""
模拟完成时的回调 - 可选实现
@@ -212,7 +212,7 @@ class ElevatorController(ABC):
"""
pass
def _run_event_driven_simulation(self):
def _run_event_driven_simulation(self) -> None:
"""运行事件驱动的模拟"""
try:
# 获取初始状态并初始化默认从0开始
@@ -304,7 +304,7 @@ class ElevatorController(ABC):
try:
traffic_info = self.api_client.get_traffic_info()
if traffic_info:
self.current_traffic_max_tick = traffic_info["max_tick"]
self.current_traffic_max_tick = int(traffic_info["max_tick"])
debug_log(f"Updated traffic info - max_tick: {self.current_traffic_max_tick}")
else:
debug_log("Failed to get traffic info")
@@ -313,7 +313,7 @@ class ElevatorController(ABC):
debug_log(f"Error updating traffic info: {e}")
self.current_traffic_max_tick = 0
def _handle_single_event(self, event: SimulationEvent):
def _handle_single_event(self, event: SimulationEvent) -> None:
"""处理单个事件"""
if event.type == EventType.UP_BUTTON_PRESSED:
floor_id = event.data["floor"]
@@ -385,7 +385,7 @@ class ElevatorController(ABC):
floor_proxy = ProxyFloor(floor_id, self.api_client)
self.on_passenger_alight(elevator_proxy, passenger_proxy, floor_proxy)
def _reset_and_reinit(self):
def _reset_and_reinit(self) -> None:
"""重置并重新初始化"""
try:
# 重置服务器状态

View File

@@ -22,6 +22,8 @@ class ProxyFloor(FloorState):
"""获取 FloorState 实例"""
state = self._api_client.get_state()
floor_data = next((f for f in state.floors if f.floor == self._floor_id), None)
if floor_data is None:
raise ValueError(f"Floor {self._floor_id} not found in state")
return floor_data
def __getattribute__(self, name: str) -> Any:
@@ -66,6 +68,8 @@ class ProxyElevator(ElevatorState):
# 获取当前状态
state = self._api_client.get_state()
elevator_data = next((e for e in state.elevators if e.id == self._elevator_id), None)
if elevator_data is None:
raise ValueError(f"Elevator {self._elevator_id} not found in state")
return elevator_data
def __getattribute__(self, name: str) -> Any:
@@ -113,6 +117,8 @@ class ProxyPassenger(PassengerInfo):
"""获取 PassengerInfo 实例"""
state = self._api_client.get_state()
passenger_data = state.passengers.get(self._passenger_id)
if passenger_data is None:
raise ValueError(f"Passenger {self._passenger_id} not found in state")
return passenger_data
def __getattribute__(self, name: str) -> Any:

View File

@@ -3,11 +3,11 @@ from typing import List
from elevator_saga.client.base_controller import ElevatorController
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
from elevator_saga.core.models import SimulationEvent, Direction
from elevator_saga.core.models import Direction, SimulationEvent
class SingleElevatorBusController(ElevatorController):
def __init__(self):
class ElevatorBusExampleController(ElevatorController):
def __init__(self) -> None:
super().__init__("http://127.0.0.1:8000", True)
self.all_passengers: List[ProxyPassenger] = []
self.max_floor = 0
@@ -27,7 +27,11 @@ class SingleElevatorBusController(ElevatorController):
) -> None:
print(f"Tick {tick}: 即将处理 {len(events)} 个事件 {[e.type.value for e in events]}")
for i in elevators:
print(f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]" + "👦" * len(i.passengers), end="")
print(
f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]"
+ "👦" * len(i.passengers),
end="",
)
print()
def on_event_execute_end(
@@ -35,7 +39,7 @@ class SingleElevatorBusController(ElevatorController):
) -> None:
pass
def on_passenger_call(self, passenger:ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
self.all_passengers.append(passenger)
pass
@@ -67,6 +71,7 @@ class SingleElevatorBusController(ElevatorController):
def on_elevator_approaching(self, elevator: ProxyElevator, floor: ProxyFloor, direction: str) -> None:
pass
if __name__ == "__main__":
algorithm = SingleElevatorBusController()
algorithm = ElevatorBusExampleController()
algorithm.start()

View File

@@ -7,7 +7,7 @@ from typing import Dict, List
from elevator_saga.client.base_controller import ElevatorController
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
from elevator_saga.core.models import SimulationEvent, Direction
from elevator_saga.core.models import Direction, SimulationEvent
class ElevatorBusController(ElevatorController):
@@ -45,7 +45,11 @@ class ElevatorBusController(ElevatorController):
"""事件执行前的回调"""
print(f"Tick {tick}: 即将处理 {len(events)} 个事件 {[e.type.value for e in events]}")
for i in elevators:
print(f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]" + "👦" * len(i.passengers), end="")
print(
f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]"
+ "👦" * len(i.passengers),
end="",
)
print()
def on_event_execute_end(
@@ -55,7 +59,7 @@ class ElevatorBusController(ElevatorController):
# print(f"✅ Tick {tick}: 已处理 {len(events)} 个事件")
pass
def on_passenger_call(self, passenger:ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
"""
乘客呼叫时的回调
公交车模式下电梯已经在循环运行无需特别响应呼叫
@@ -108,9 +112,7 @@ class ElevatorBusController(ElevatorController):
乘客上梯时的回调
打印乘客上梯信息
"""
print(
f" 乘客{passenger.id} E{elevator.id}⬆️ F{elevator.current_floor} -> F{passenger.destination}"
)
print(f" 乘客{passenger.id} E{elevator.id}⬆️ F{elevator.current_floor} -> F{passenger.destination}")
def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor) -> None:
"""
@@ -137,6 +139,7 @@ class ElevatorBusController(ElevatorController):
elevator.go_to_floor(elevator.target_floor + 1, immediate=True)
print(f" 不让0号电梯上行停站设定新目标楼层 {elevator.target_floor + 1}")
if __name__ == "__main__":
algorithm = ElevatorBusController(debug=True)
algorithm.start()

View File

@@ -85,7 +85,6 @@ class SerializableModel:
setattr(instance, k, v.__class__(value))
return instance
@classmethod
def from_json(cls: Type[T], json_str: str) -> T:
"""从JSON字符串创建实例"""
@@ -113,10 +112,10 @@ class Position(SerializableModel):
floor_up_position: int = 0
@property
def current_floor_float(self):
def current_floor_float(self) -> float:
return self.current_floor + self.floor_up_position / 10
def floor_up_position_add(self, num: int):
def floor_up_position_add(self, num: int) -> int:
self.floor_up_position += num
# 处理向上楼层跨越
@@ -139,7 +138,7 @@ class ElevatorIndicators(SerializableModel):
up: bool = False
down: bool = False
def set_direction(self, direction: Direction):
def set_direction(self, direction: Direction) -> None:
"""根据方向设置指示灯"""
if direction == Direction.UP:
self.up = True
@@ -202,13 +201,13 @@ class ElevatorState(SerializableModel):
id: int
position: Position
next_target_floor: Optional[int] = None
passengers: List[int] = field(default_factory=list) # type: ignore[reportUnknownVariableType] 乘客ID列表
passengers: List[int] = field(default_factory=list) # 乘客ID列表
max_capacity: int = 10
speed_pre_tick: float = 0.5
run_status: ElevatorStatus = ElevatorStatus.STOPPED
last_tick_direction: Direction = Direction.STOPPED
indicators: ElevatorIndicators = field(default_factory=ElevatorIndicators)
passenger_destinations: Dict[int, int] = field(default_factory=dict) # type: ignore[reportUnknownVariableType] 乘客ID -> 目的地楼层映射
passenger_destinations: Dict[int, int] = field(default_factory=dict) # 乘客ID -> 目的地楼层映射
energy_consumed: float = 0.0
last_update_tick: int = 0
@@ -223,7 +222,7 @@ class ElevatorState(SerializableModel):
def current_floor_float(self) -> float:
"""当前楼层"""
if isinstance(self.position, dict):
self.position = Position.from_dict(self.position)
self.position = Position.from_dict(self.position) # type: ignore[arg-type]
return self.position.current_floor_float
@property
@@ -269,7 +268,7 @@ class ElevatorState(SerializableModel):
"""按下的楼层(基于当前乘客的目的地动态计算)"""
return sorted(list(set(self.passenger_destinations.values())))
def clear_destinations(self):
def clear_destinations(self) -> None:
"""清空目标队列"""
self.next_target_floor = None
@@ -279,8 +278,8 @@ class FloorState(SerializableModel):
"""楼层状态"""
floor: int
up_queue: List[int] = field(default_factory=list) # type: ignore[reportUnknownVariableType] 等待上行的乘客ID
down_queue: List[int] = field(default_factory=list) # type: ignore[reportUnknownVariableType] 等待下行的乘客ID
up_queue: List[int] = field(default_factory=list) # 等待上行的乘客ID
down_queue: List[int] = field(default_factory=list) # 等待下行的乘客ID
@property
def has_waiting_passengers(self) -> bool:
@@ -292,7 +291,7 @@ class FloorState(SerializableModel):
"""总等待人数"""
return len(self.up_queue) + len(self.down_queue)
def add_waiting_passenger(self, passenger_id: int, direction: Direction):
def add_waiting_passenger(self, passenger_id: int, direction: Direction) -> None:
"""添加等待乘客"""
if direction == Direction.UP:
if passenger_id not in self.up_queue:
@@ -321,7 +320,7 @@ class SimulationEvent(SerializableModel):
data: Dict[str, Any]
timestamp: Optional[str] = None
def __post_init__(self):
def __post_init__(self) -> None:
if self.timestamp is None:
self.timestamp = datetime.now().isoformat()
@@ -360,9 +359,9 @@ class SimulationState(SerializableModel):
tick: int
elevators: List[ElevatorState]
floors: List[FloorState]
passengers: Dict[int, PassengerInfo] = field(default_factory=dict) # type: ignore[reportUnknownVariableType]
passengers: Dict[int, PassengerInfo] = field(default_factory=dict)
metrics: PerformanceMetrics = field(default_factory=PerformanceMetrics)
events: List[SimulationEvent] = field(default_factory=list) # type: ignore[reportUnknownVariableType]
events: List[SimulationEvent] = field(default_factory=list)
def get_elevator_by_id(self, elevator_id: int) -> Optional[ElevatorState]:
"""根据ID获取电梯"""
@@ -382,7 +381,7 @@ class SimulationState(SerializableModel):
"""根据状态获取乘客"""
return [p for p in self.passengers.values() if p.status == status]
def add_event(self, event_type: EventType, data: Dict[str, Any]):
def add_event(self, event_type: EventType, data: Dict[str, Any]) -> None:
"""添加事件"""
event = SimulationEvent(tick=self.tick, type=event_type, data=data)
self.events.append(event)
@@ -422,7 +421,7 @@ class StepResponse(SerializableModel):
success: bool
tick: int
events: List[SimulationEvent] = field(default_factory=list) # type: ignore[reportUnknownVariableType]
events: List[SimulationEvent] = field(default_factory=list)
request_id: Optional[str] = None
error_message: Optional[str] = None
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
@@ -443,7 +442,7 @@ class ElevatorCommand(SerializableModel):
elevator_id: int
command_type: str # "go_to_floor", "stop"
parameters: Dict[str, Any] = field(default_factory=dict) # type: ignore[reportUnknownVariableType]
parameters: Dict[str, Any] = field(default_factory=dict)
request_id: str = field(default_factory=lambda: str(uuid.uuid4()))
timestamp: str = field(default_factory=lambda: datetime.now().isoformat())
@@ -494,7 +493,7 @@ class TrafficPattern(SerializableModel):
entries: List[TrafficEntry] = field(default_factory=list)
metadata: Dict[str, Any] = field(default_factory=dict)
def add_entry(self, entry: TrafficEntry):
def add_entry(self, entry: TrafficEntry) -> None:
"""添加流量条目"""
self.entries.append(entry)

View File

@@ -7,7 +7,7 @@ from typing import Dict, List
from elevator_saga.client.base_controller import ElevatorController
from elevator_saga.client.proxy_models import ProxyElevator, ProxyFloor, ProxyPassenger
from elevator_saga.core.models import SimulationEvent, Direction
from elevator_saga.core.models import Direction, SimulationEvent
class ElevatorBusController(ElevatorController):
@@ -45,7 +45,11 @@ class ElevatorBusController(ElevatorController):
"""事件执行前的回调"""
print(f"Tick {tick}: 即将处理 {len(events)} 个事件 {[e.type.value for e in events]}")
for i in elevators:
print(f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]" + "👦" * len(i.passengers), end="")
print(
f"\t{i.id}[{i.target_floor_direction.value},{i.current_floor_float}/{i.target_floor}]"
+ "👦" * len(i.passengers),
end="",
)
print()
def on_event_execute_end(
@@ -54,7 +58,7 @@ class ElevatorBusController(ElevatorController):
"""事件执行后的回调"""
pass
def on_passenger_call(self, passenger:ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
def on_passenger_call(self, passenger: ProxyPassenger, floor: ProxyFloor, direction: str) -> None:
"""
乘客呼叫时的回调
公交车模式下,电梯已经在循环运行,无需特别响应呼叫
@@ -107,9 +111,7 @@ class ElevatorBusController(ElevatorController):
乘客上梯时的回调
打印乘客上梯信息
"""
print(
f" 乘客{passenger.id} E{elevator.id}⬆️ F{elevator.current_floor} -> F{passenger.destination}"
)
print(f" 乘客{passenger.id} E{elevator.id}⬆️ F{elevator.current_floor} -> F{passenger.destination}")
def on_passenger_alight(self, elevator: ProxyElevator, passenger: ProxyPassenger, floor: ProxyFloor) -> None:
"""
@@ -136,6 +138,7 @@ class ElevatorBusController(ElevatorController):
elevator.go_to_floor(elevator.target_floor + 1, immediate=True)
print(f" 不让0号电梯上行停站设定新目标楼层 {elevator.target_floor + 1}")
if __name__ == "__main__":
algorithm = ElevatorBusController(debug=True)
algorithm.start()

View File

@@ -33,13 +33,13 @@ from elevator_saga.core.models import (
_SERVER_DEBUG_MODE = False
def set_server_debug_mode(enabled: bool):
def set_server_debug_mode(enabled: bool) -> None:
"""Enable or disable server debug logging"""
global _SERVER_DEBUG_MODE
globals()["_SERVER_DEBUG_MODE"] = enabled
def server_debug_log(message: str):
def server_debug_log(message: str) -> None:
"""Print server debug message if debug mode is enabled"""
if _SERVER_DEBUG_MODE:
print(f"[SERVER-DEBUG] {message}", flush=True)
@@ -360,9 +360,7 @@ class ElevatorSimulation:
destination=traffic_entry.destination,
arrive_tick=self.tick,
)
assert (
traffic_entry.origin != traffic_entry.destination
), f"乘客{passenger.id}目的地和起始地{traffic_entry.origin}重复"
assert traffic_entry.origin != traffic_entry.destination, f"乘客{passenger.id}目的地和起始地{traffic_entry.origin}重复"
self.passengers[passenger.id] = passenger
server_debug_log(f"乘客 {passenger.id:4} 创建 | {passenger}")
if passenger.destination > passenger.origin:
@@ -434,7 +432,9 @@ class ElevatorSimulation:
if target_floor == new_floor and elevator.position.floor_up_position == 0:
elevator.run_status = ElevatorStatus.STOPPED
# 刚进入Stopped状态可以通过last_direction识别
self._emit_event(EventType.STOPPED_AT_FLOOR, {"elevator": elevator.id, "floor": new_floor, "reason": "move_reached"})
self._emit_event(
EventType.STOPPED_AT_FLOOR, {"elevator": elevator.id, "floor": new_floor, "reason": "move_reached"}
)
# elevator.energy_consumed += abs(direction * elevator.speed_pre_tick) * 0.5
def _process_elevator_stops(self) -> None:
@@ -471,7 +471,7 @@ class ElevatorSimulation:
self._set_elevator_target_floor(elevator, elevator.next_target_floor)
elevator.next_target_floor = None
def _set_elevator_target_floor(self, elevator: ElevatorState, floor: int):
def _set_elevator_target_floor(self, elevator: ElevatorState, floor: int) -> None:
"""
同一个tick内提示
[SERVER-DEBUG] 电梯 E0 下一目的地设定为 F1
@@ -492,9 +492,7 @@ class ElevatorSimulation:
server_debug_log(f"电梯 E{elevator.id} 被设定为减速")
if elevator.current_floor != floor or elevator.position.floor_up_position != 0:
old_status = elevator.run_status.value
server_debug_log(
f"电梯{elevator.id} 状态:{old_status}->{elevator.run_status.value}"
)
server_debug_log(f"电梯{elevator.id} 状态:{old_status}->{elevator.run_status.value}")
def _calculate_distance_to_target(self, elevator: ElevatorState) -> float:
"""计算到目标楼层的距离以floor_up_position为单位"""

View File

@@ -455,4 +455,4 @@
"tick": 196
}
]
}
}

View File

@@ -179,4 +179,4 @@
"tick": 74
}
]
}
}

View File

@@ -9,7 +9,7 @@ import math
import os.path
import random
from pathlib import Path
from typing import Any, Dict, List, Optional
from typing import Any, Callable, Dict, List, Optional
# 建筑规模配置
BUILDING_SCALES = {
@@ -294,9 +294,7 @@ def generate_fire_evacuation_traffic(
# 在10个tick内陆续到达模拟疏散的紧急性
arrival_tick = alarm_tick + random.randint(0, min(10, duration - alarm_tick - 1))
if arrival_tick < duration:
traffic.append(
{"id": passenger_id, "origin": floor, "destination": 0, "tick": arrival_tick} # 疏散到大厅
)
traffic.append({"id": passenger_id, "origin": floor, "destination": 0, "tick": arrival_tick}) # 疏散到大厅
passenger_id += 1
return limit_traffic_count(traffic, max_people)
@@ -738,12 +736,12 @@ def determine_building_scale(floors: int, elevators: int) -> str:
return "large"
def generate_traffic_file(scenario: str, output_file: str, scale: Optional[str] = None, **kwargs) -> int:
def generate_traffic_file(scenario: str, output_file: str, scale: Optional[str] = None, **kwargs: Any) -> int:
"""生成单个流量文件,支持规模化配置"""
if scenario not in TRAFFIC_SCENARIOS:
raise ValueError(f"Unknown scenario: {scenario}. Available: {list(TRAFFIC_SCENARIOS.keys())}")
config = TRAFFIC_SCENARIOS[scenario]
config: Dict[str, Any] = TRAFFIC_SCENARIOS[scenario]
# 确定建筑规模
if scale is None:
@@ -766,6 +764,7 @@ def generate_traffic_file(scenario: str, output_file: str, scale: Optional[str]
scale_params = config["scales"].get(scale, {})
# 合并参数kwargs > scale_params > building_scale_defaults
assert scale is not None # scale should be determined by this point
building_scale = BUILDING_SCALES[scale]
params = {}
@@ -786,9 +785,10 @@ def generate_traffic_file(scenario: str, output_file: str, scale: Optional[str]
# 生成流量数据 - 只传递生成器函数需要的参数
import inspect
generator_signature = inspect.signature(config["generator"])
generator_func: Callable[..., List[Dict[str, Any]]] = config["generator"]
generator_signature = inspect.signature(generator_func)
generator_params = {k: v for k, v in params.items() if k in generator_signature.parameters}
traffic_data = config["generator"](**generator_params)
traffic_data = generator_func(**generator_params)
# 准备building配置
building_config = {
@@ -819,7 +819,7 @@ def generate_scaled_traffic_files(
seed: int = 42,
generate_all_scales: bool = False,
custom_building: Optional[Dict[str, Any]] = None,
):
) -> None:
"""生成按规模分类的流量文件"""
output_path = Path(output_dir)
output_path.mkdir(exist_ok=True)
@@ -848,7 +848,7 @@ def generate_scaled_traffic_files(
def _generate_files_for_scale(
output_path: Path, scale: str, seed: int, custom_building: Optional[Dict[str, Any]] = None
):
) -> None:
"""为指定规模生成所有适合的场景文件"""
building_config = BUILDING_SCALES[scale]
total_passengers = 0
@@ -868,9 +868,10 @@ def _generate_files_for_scale(
print(f"\nGenerating {scale} scale traffic files:")
print(f"Building: {floors} floors, {elevators} elevators, capacity {elevator_capacity}")
for scenario_name, config in TRAFFIC_SCENARIOS.items():
for scenario_name, scenario_config in TRAFFIC_SCENARIOS.items():
# 检查场景是否适合该规模
if scale not in config["suitable_scales"]:
config_dict: Dict[str, Any] = scenario_config
if scale not in config_dict["suitable_scales"]:
continue
filename = f"{scenario_name}.json"
@@ -905,7 +906,7 @@ def generate_all_traffic_files(
elevators: int = 2,
elevator_capacity: int = 8,
seed: int = 42,
):
) -> None:
"""生成所有场景的流量文件 - 保持向后兼容"""
scale = determine_building_scale(floors, elevators)
custom_building = {"floors": floors, "elevators": elevators, "capacity": elevator_capacity}
@@ -913,7 +914,7 @@ def generate_all_traffic_files(
generate_scaled_traffic_files(output_dir=output_dir, scale=scale, seed=seed, custom_building=custom_building)
def main():
def main() -> None:
"""主函数 - 命令行接口"""
import argparse

View File

@@ -407,4 +407,4 @@
"tick": 197
}
]
}
}

View File

@@ -371,4 +371,4 @@
"tick": 197
}
]
}
}

View File

@@ -161,4 +161,4 @@
"tick": 192
}
]
}
}

View File

@@ -491,4 +491,4 @@
"tick": 138
}
]
}
}

View File

@@ -311,4 +311,4 @@
"tick": 49
}
]
}
}

View File

@@ -611,4 +611,4 @@
"tick": 190
}
]
}
}

View File

@@ -587,4 +587,4 @@
"tick": 199
}
]
}
}

View File

@@ -479,4 +479,4 @@
"tick": 196
}
]
}
}

View File

@@ -491,4 +491,4 @@
"tick": 146
}
]
}
}

View File

@@ -1,45 +1,56 @@
[build-system]
requires = ["setuptools>=45", "wheel", "setuptools_scm[toml]>=6.2"]
requires = ["setuptools>=61.0", "wheel"]
build-backend = "setuptools.build_meta"
[project]
name = "elevator-saga"
name = "elevatorpy"
dynamic = ["version"]
description = "Python implementation of Elevator Saga game with PyEE event system"
readme = "README_CN.md"
license = {text = "MIT"}
description = "Python implementation of Elevator Saga game with event system"
readme = "README.md"
requires-python = ">=3.10"
license = {file = "LICENSE"}
authors = [
{name = "Elevator Saga Team"},
{name = "ZGCA-Forge Team", email = "zgca@zgca.com"}
]
keywords = ["elevator", "simulation", "game", "event-driven", "optimization"]
classifiers = [
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"Intended Audience :: Education",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Topic :: Games/Entertainment :: Simulation",
"Topic :: Software Development :: Libraries :: Python Modules",
]
requires-python = ">=3.12"
dependencies = [
"pyee>=11.0.0",
"numpy>=1.20.0",
"matplotlib>=3.5.0",
"seaborn>=0.11.0",
"pandas>=1.3.0",
"flask",
"flask>=2.0.0",
]
[project.optional-dependencies]
dev = [
"pytest>=6.0",
"pytest-cov",
"black",
"flake8",
"isort",
"mypy",
"pytest>=7.0.0",
"pytest-cov>=4.0.0",
"black>=22.0.0",
"isort>=5.0.0",
"mypy>=1.0.0",
"flake8>=6.0.0",
"pre-commit>=2.20.0",
"bump2version>=1.0.0",
]
docs = [
"sphinx>=5.0.0",
"sphinx-rtd-theme>=1.0.0",
"myst-parser>=0.18.0",
]
all = [
"elevatorpy[dev,docs]",
]
[project.scripts]
@@ -50,66 +61,38 @@ elevator-grader = "elevator_saga.grader.grader:main"
elevator-batch-test = "elevator_saga.grader.batch_runner:main"
[project.urls]
Repository = "https://github.com/yourusername/elevator-saga"
Issues = "https://github.com/yourusername/elevator-saga/issues"
[tool.setuptools_scm]
write_to = "elevator_saga/_version.py"
Homepage = "https://github.com/ZGCA-Forge/Elevator"
Documentation = "https://zgca-forge.github.io/Elevator/"
Repository = "https://github.com/ZGCA-Forge/Elevator"
Issues = "https://github.com/ZGCA-Forge/Elevator/issues"
[tool.setuptools.packages.find]
where = ["."]
include = ["elevator_saga*"]
[tool.setuptools.dynamic]
version = {attr = "elevator_saga.__version__"}
[tool.black]
line-length = 120
target-version = ['py312']
include = '\.pyi?$'
extend-exclude = '''
/(
# directories
\.eggs
| \.git
| \.hg
| \.mypy_cache
| \.tox
| \.venv
| _build
| buck-out
| build
| dist
)/
'''
target-version = ['py310', 'py311', 'py312']
[tool.isort]
profile = "black"
line_length = 120
multi_line_output = 3
include_trailing_comma = true
force_grid_wrap = 0
use_parentheses = true
ensure_newline_before_comments = true
[tool.mypy]
python_version = "3.12"
python_version = "3.10"
warn_return_any = true
warn_unused_configs = true
disallow_untyped_defs = true
disallow_incomplete_defs = true
check_untyped_defs = true
disallow_untyped_decorators = true
no_implicit_optional = true
warn_redundant_casts = true
warn_unused_ignores = true
warn_no_return = true
warn_unreachable = true
strict_equality = true
[[tool.mypy.overrides]]
module = [
"pyee.*",
"matplotlib.*",
"seaborn.*",
"pandas.*",
"numpy.*",
"flask.*",
]
@@ -117,38 +100,20 @@ ignore_missing_imports = true
[tool.pytest.ini_options]
testpaths = ["tests"]
python_files = ["test_*.py", "*_test.py"]
python_classes = ["Test*"]
python_functions = ["test_*"]
addopts = [
"--strict-markers",
"--strict-config",
"--verbose",
]
markers = [
"slow: marks tests as slow (deselect with '-m \"not slow\"')",
"integration: marks tests as integration tests",
python_files = "test_*.py"
python_classes = "Test*"
python_functions = "test_*"
addopts = "-v --tb=short --strict-markers --strict-config -ra --color=yes"
filterwarnings = [
"ignore::DeprecationWarning",
"ignore::PendingDeprecationWarning",
]
[tool.coverage.run]
source = ["elevator_saga"]
omit = [
"*/tests/*",
"*/test_*",
"*/__pycache__/*",
"*/.*",
]
[tool.coverage.report]
exclude_lines = [
"pragma: no cover",
"def __repr__",
"if self.debug:",
"if settings.DEBUG",
"raise AssertionError",
"raise NotImplementedError",
"if 0:",
"if __name__ == .__main__.:",
"class .*\\bProtocol\\):",
"@(abc\\.)?abstractmethod",
]
[tool.bandit]
exclude_dirs = ["tests", "test_*.py", "*_test.py", ".venv", "venv", "build", "dist"]
# B101: assert语句用于类型检查和开发时验证不是安全问题
# B601: shell命令参数化在受控环境中使用
# B310: urllib.urlopen用于连接受控的API服务器URL来源可信
# B311: random模块用于生成电梯流量模拟数据非加密用途
skips = ["B101", "B601", "B310", "B311"]

View File

@@ -1,33 +1,30 @@
{
"include": ["."],
"exclude": [
"build",
"dist",
"__pycache__",
".mypy_cache",
".pytest_cache",
"htmlcov",
".idea",
".vscode",
"docs/_build",
"elevator_saga.egg-info"
],
"pythonVersion": "3.10",
"typeCheckingMode": "strict",
"executionEnvironments": [
{
"root": ".",
"extraPaths": ["elevator_saga"]
}
],
"reportMissingImports": "none",
"reportUnusedImport": "warning",
"reportUnusedVariable": "warning",
"reportUnknownArgumentType": "warning",
"reportUnknownMemberType": "warning",
"reportUnknownVariableType": "warning",
"reportUnknownParameterType": "warning",
"reportPrivateUsage": "warning",
"reportMissingTypeStubs": false
}
"include": ["."],
"exclude": [
"build",
"dist",
"__pycache__",
".mypy_cache",
".pytest_cache",
"htmlcov",
".idea",
".vscode",
"docs/_build",
"elevatorpy.egg-info",
"elevator_saga.egg-info",
".eggs",
"MsgCenterPy"
],
"pythonVersion": "3.10",
"typeCheckingMode": "basic",
"executionEnvironments": [
{
"root": ".",
"extraPaths": ["elevator_saga"]
}
],
"reportMissingImports": "warning",
"reportUnusedImport": "warning",
"reportUnusedVariable": "warning",
"reportMissingTypeStubs": false
}

View File

@@ -1,157 +0,0 @@
#!/usr/bin/env python3
"""
Test runner for Elevator Saga project
"""
import argparse
import subprocess
import sys
import os
from pathlib import Path
def check_dependencies():
"""Check if all dependencies are installed correctly"""
print("🔍 Checking dependencies...")
try:
import elevator_saga
print(f"✅ elevator_saga version: {getattr(elevator_saga, '__version__', 'unknown')}")
# Check main dependencies
dependencies = ["pyee", "numpy", "matplotlib", "seaborn", "pandas", "flask"]
for dep in dependencies:
try:
__import__(dep)
print(f"{dep}: installed")
except ImportError:
print(f"{dep}: missing")
return False
print("✅ All dependencies are correctly installed")
return True
except ImportError as e:
print(f"❌ Error importing elevator_saga: {e}")
return False
def run_unit_tests():
"""Run unit tests"""
print("🧪 Running unit tests...")
# Check if tests directory exists
tests_dir = Path("tests")
if not tests_dir.exists():
print(" No tests directory found, creating basic test structure...")
tests_dir.mkdir()
(tests_dir / "__init__.py").touch()
# Create a basic test file
basic_test = tests_dir / "test_basic.py"
basic_test.write_text(
'''"""Basic tests for elevator_saga"""
import unittest
from elevator_saga.core.models import Direction, SimulationEvent
class TestBasic(unittest.TestCase):
"""Basic functionality tests"""
def test_direction_enum(self):
"""Test Direction enum"""
self.assertEqual(Direction.UP.value, "up")
self.assertEqual(Direction.DOWN.value, "down")
self.assertEqual(Direction.NONE.value, "none")
def test_import(self):
"""Test that main modules can be imported"""
import elevator_saga.client.base_controller
import elevator_saga.core.models
import elevator_saga.server.simulator
if __name__ == '__main__':
unittest.main()
'''
)
# Run pytest if available, otherwise unittest
try:
result = subprocess.run([sys.executable, "-m", "pytest", "tests/", "-v"], capture_output=True, text=True)
print(result.stdout)
if result.stderr:
print(result.stderr)
return result.returncode == 0
except FileNotFoundError:
print("pytest not found, using unittest...")
result = subprocess.run([sys.executable, "-m", "unittest", "discover", "tests"], capture_output=True, text=True)
print(result.stdout)
if result.stderr:
print(result.stderr)
return result.returncode == 0
def run_example_tests():
"""Run example files to ensure they work"""
print("🚀 Running example tests...")
example_files = ["simple_example.py", "test_example.py"]
for example_file in example_files:
if os.path.exists(example_file):
print(f"Testing {example_file}...")
# Just check if the file can be imported without errors
try:
result = subprocess.run(
[sys.executable, "-c", f"import {example_file[:-3]}"], capture_output=True, text=True, timeout=10
)
if result.returncode == 0:
print(f"{example_file}: import successful")
else:
print(f"{example_file}: import failed")
print(result.stderr)
return False
except subprocess.TimeoutExpired:
print(f"{example_file}: timeout (probably waiting for server)")
# This is expected for examples that try to connect to server
print(f"{example_file}: import successful (with server connection)")
except Exception as e:
print(f"{example_file}: error - {e}")
return False
return True
def main():
parser = argparse.ArgumentParser(description="Run tests for Elevator Saga")
parser.add_argument("--check-deps", action="store_true", help="Check dependencies only")
parser.add_argument("--type", choices=["unit", "examples", "all"], default="all", help="Type of tests to run")
args = parser.parse_args()
success = True
if args.check_deps:
success = check_dependencies()
else:
# Always check dependencies first
if not check_dependencies():
print("❌ Dependency check failed")
return 1
if args.type in ["unit", "all"]:
if not run_unit_tests():
success = False
if args.type in ["examples", "all"]:
if not run_example_tests():
success = False
if success:
print("🎉 All tests passed!")
return 0
else:
print("💥 Some tests failed!")
return 1
if __name__ == "__main__":
sys.exit(main())

View File

@@ -1,59 +0,0 @@
#!/usr/bin/env python3
"""
Setup script for Elevator Saga Python Package
"""
from setuptools import setup, find_packages
with open("README_CN.md", "r", encoding="utf-8") as fh:
long_description = fh.read()
setup(
name="elevator-saga",
version="1.0.0",
author="Elevator Saga Team",
description="Python implementation of Elevator Saga game with PyEE event system",
long_description=long_description,
long_description_content_type="text/markdown",
packages=find_packages(),
classifiers=[
"Development Status :: 4 - Beta",
"Intended Audience :: Developers",
"Intended Audience :: Education",
"License :: OSI Approved :: MIT License",
"Operating System :: OS Independent",
"Programming Language :: Python :: 3",
"Programming Language :: Python :: 3.12",
"Topic :: Games/Entertainment :: Simulation",
"Topic :: Software Development :: Libraries :: Python Modules",
],
python_requires=">=3.12",
install_requires=[
"pyee>=11.0.0",
"numpy>=1.20.0",
"matplotlib>=3.5.0",
"seaborn>=0.11.0",
"pandas>=1.3.0",
"flask",
],
extras_require={
"dev": [
"pytest>=6.0",
"pytest-cov",
"black",
"flake8",
"isort",
"mypy",
],
},
entry_points={
"console_scripts": [
"elevator-saga=elevator_saga.cli.main:main",
"elevator-server=elevator_saga.cli.main:server_main",
"elevator-client=elevator_saga.cli.main:client_main",
"elevator-grader=elevator_saga.grader.grader:main",
"elevator-batch-test=elevator_saga.grader.batch_runner:main",
],
},
include_package_data=True,
zip_safe=False,
)