diff --git a/.bumpversion.cfg b/.bumpversion.cfg
new file mode 100644
index 0000000..af37c4b
--- /dev/null
+++ b/.bumpversion.cfg
@@ -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}"
diff --git a/.github/dependabot.yml b/.github/dependabot.yml
index 2b6e82e..c2407bf 100644
--- a/.github/dependabot.yml
+++ b/.github/dependabot.yml
@@ -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"
-
diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index e9edfff..3b85184 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -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
diff --git a/.github/workflows/code-quality.yml b/.github/workflows/code-quality.yml
deleted file mode 100644
index f5cbf6e..0000000
--- a/.github/workflows/code-quality.yml
+++ /dev/null
@@ -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
-
diff --git a/.github/workflows/dependabot.yml b/.github/workflows/dependabot.yml
deleted file mode 100644
index 3b309d2..0000000
--- a/.github/workflows/dependabot.yml
+++ /dev/null
@@ -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 }}
-
diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml
new file mode 100644
index 0000000..44401be
--- /dev/null
+++ b/.github/workflows/docs.yml
@@ -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
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index ea5c34c..38d9ed8 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -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
diff --git a/.gitignore b/.gitignore
index 1e80c4d..cec6d0c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,5 +1,60 @@
-.vscode
-.idea
-__pycache__
-.mypy_cache
-elevator_saga.egg-info
\ No newline at end of file
+# ================================
+# 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
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a98f731..d3af09d 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -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
-
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..98a5dea
--- /dev/null
+++ b/LICENSE
@@ -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.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..bec28b6
--- /dev/null
+++ b/README.md
@@ -0,0 +1,84 @@
+# Elevator Saga
+
+
+
+[](https://badge.fury.io/py/elevatorpy)
+[](https://pypi.org/project/elevatorpy/)
+[](https://github.com/ZGCA-Forge/Elevator/actions)
+[](https://zgca-forge.github.io/Elevator/)
+
+[](https://github.com/ZGCA-Forge/Elevator)
+[](https://github.com/ZGCA-Forge/Elevator/fork)
+[](https://github.com/ZGCA-Forge/Elevator/issues)
+[](https://github.com/ZGCA-Forge/Elevator/blob/main/LICENSE)
+
+
+
+---
+
+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
+
+[](https://star-history.com/#ZGCA-Forge/Elevator&Date)
+
+## License
+
+This project is licensed under MIT License - see the [LICENSE](LICENSE) file for details.
+
+---
+
+
+
+Made with ❤️ by the ZGCA-Forge Team
+
+
diff --git a/elevator_saga/__init__.py b/elevator_saga/__init__.py
index 7a454c9..7b1112c 100644
--- a/elevator_saga/__init__.py
+++ b/elevator_saga/__init__.py
@@ -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"
diff --git a/elevator_saga/client/api_client.py b/elevator_saga/client/api_client.py
index 31d0a17..92bcd5a 100644
--- a/elevator_saga/client/api_client.py
+++ b/elevator_saga/client/api_client.py
@@ -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:
diff --git a/elevator_saga/client/base_controller.py b/elevator_saga/client/base_controller.py
index 1f5e7b7..4284468 100644
--- a/elevator_saga/client/base_controller.py
+++ b/elevator_saga/client/base_controller.py
@@ -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:
# 重置服务器状态
diff --git a/elevator_saga/client/proxy_models.py b/elevator_saga/client/proxy_models.py
index edc8a9a..aedfdcb 100644
--- a/elevator_saga/client/proxy_models.py
+++ b/elevator_saga/client/proxy_models.py
@@ -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:
diff --git a/README_CN.md b/elevator_saga/client_examples/__init__.py
similarity index 100%
rename from README_CN.md
rename to elevator_saga/client_examples/__init__.py
diff --git a/test_example.py b/elevator_saga/client_examples/bus_example.py
similarity index 84%
rename from test_example.py
rename to elevator_saga/client_examples/bus_example.py
index fd5781a..72bf6b3 100644
--- a/test_example.py
+++ b/elevator_saga/client_examples/bus_example.py
@@ -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()
diff --git a/simple_example.py b/elevator_saga/client_examples/simple_example.py
similarity index 93%
rename from simple_example.py
rename to elevator_saga/client_examples/simple_example.py
index ef9071a..09a726b 100644
--- a/simple_example.py
+++ b/elevator_saga/client_examples/simple_example.py
@@ -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()
diff --git a/elevator_saga/core/models.py b/elevator_saga/core/models.py
index 7b29103..7ea3e0a 100644
--- a/elevator_saga/core/models.py
+++ b/elevator_saga/core/models.py
@@ -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)
diff --git a/elevator_saga/scripts/client_examples/simple_example.py b/elevator_saga/scripts/client_examples/simple_example.py
index 02b51f7..1b0d9ae 100644
--- a/elevator_saga/scripts/client_examples/simple_example.py
+++ b/elevator_saga/scripts/client_examples/simple_example.py
@@ -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()
diff --git a/elevator_saga/server/simulator.py b/elevator_saga/server/simulator.py
index 56a2911..62cf690 100644
--- a/elevator_saga/server/simulator.py
+++ b/elevator_saga/server/simulator.py
@@ -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为单位)"""
diff --git a/elevator_saga/traffic/down_peak.json b/elevator_saga/traffic/down_peak.json
index 8e847f5..480ed37 100644
--- a/elevator_saga/traffic/down_peak.json
+++ b/elevator_saga/traffic/down_peak.json
@@ -455,4 +455,4 @@
"tick": 196
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/fire_evacuation.json b/elevator_saga/traffic/fire_evacuation.json
index 6338912..8c4d3be 100644
--- a/elevator_saga/traffic/fire_evacuation.json
+++ b/elevator_saga/traffic/fire_evacuation.json
@@ -179,4 +179,4 @@
"tick": 74
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/generators.py b/elevator_saga/traffic/generators.py
index 2735fcf..3309a2b 100644
--- a/elevator_saga/traffic/generators.py
+++ b/elevator_saga/traffic/generators.py
@@ -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
diff --git a/elevator_saga/traffic/high_density.json b/elevator_saga/traffic/high_density.json
index c4e821a..0eaa602 100644
--- a/elevator_saga/traffic/high_density.json
+++ b/elevator_saga/traffic/high_density.json
@@ -407,4 +407,4 @@
"tick": 197
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/inter_floor.json b/elevator_saga/traffic/inter_floor.json
index ba90f9b..d7c3480 100644
--- a/elevator_saga/traffic/inter_floor.json
+++ b/elevator_saga/traffic/inter_floor.json
@@ -371,4 +371,4 @@
"tick": 197
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/lunch_rush.json b/elevator_saga/traffic/lunch_rush.json
index 60f8ae2..c3f6712 100644
--- a/elevator_saga/traffic/lunch_rush.json
+++ b/elevator_saga/traffic/lunch_rush.json
@@ -161,4 +161,4 @@
"tick": 192
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/medical.json b/elevator_saga/traffic/medical.json
index c755eb7..c99326d 100644
--- a/elevator_saga/traffic/medical.json
+++ b/elevator_saga/traffic/medical.json
@@ -491,4 +491,4 @@
"tick": 138
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/meeting_event.json b/elevator_saga/traffic/meeting_event.json
index 72d5e01..55fdb54 100644
--- a/elevator_saga/traffic/meeting_event.json
+++ b/elevator_saga/traffic/meeting_event.json
@@ -311,4 +311,4 @@
"tick": 49
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/mixed_scenario.json b/elevator_saga/traffic/mixed_scenario.json
index 8680bc9..01eb876 100644
--- a/elevator_saga/traffic/mixed_scenario.json
+++ b/elevator_saga/traffic/mixed_scenario.json
@@ -611,4 +611,4 @@
"tick": 190
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/progressive_test.json b/elevator_saga/traffic/progressive_test.json
index 4d70d35..e861569 100644
--- a/elevator_saga/traffic/progressive_test.json
+++ b/elevator_saga/traffic/progressive_test.json
@@ -587,4 +587,4 @@
"tick": 199
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/random.json b/elevator_saga/traffic/random.json
index 9f37078..63ea508 100644
--- a/elevator_saga/traffic/random.json
+++ b/elevator_saga/traffic/random.json
@@ -479,4 +479,4 @@
"tick": 196
}
]
-}
\ No newline at end of file
+}
diff --git a/elevator_saga/traffic/up_peak.json b/elevator_saga/traffic/up_peak.json
index 2daf779..9122654 100644
--- a/elevator_saga/traffic/up_peak.json
+++ b/elevator_saga/traffic/up_peak.json
@@ -491,4 +491,4 @@
"tick": 146
}
]
-}
\ No newline at end of file
+}
diff --git a/pyproject.toml b/pyproject.toml
index e765fd7..45ba865 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -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"]
diff --git a/pyrightconfig.json b/pyrightconfig.json
index 54348aa..d756a76 100644
--- a/pyrightconfig.json
+++ b/pyrightconfig.json
@@ -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
- }
-
\ No newline at end of file
+ "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
+}
diff --git a/run_all_tests.py b/run_all_tests.py
deleted file mode 100644
index bd891ee..0000000
--- a/run_all_tests.py
+++ /dev/null
@@ -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())
diff --git a/setup.py b/setup.py
deleted file mode 100644
index c91b433..0000000
--- a/setup.py
+++ /dev/null
@@ -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,
-)