From a9fc374d314568cc6d83341e4d295aa42453fd2e Mon Sep 17 00:00:00 2001 From: Xuwznln <18435084+Xuwznln@users.noreply.github.com> Date: Wed, 1 Oct 2025 17:07:31 +0800 Subject: [PATCH] Update ci --- .bumpversion.cfg | 10 + .github/dependabot.yml | 5 +- .github/workflows/ci.yml | 352 ++++++++-------- .github/workflows/code-quality.yml | 269 ------------ .github/workflows/dependabot.yml | 34 -- .github/workflows/docs.yml | 87 ++++ .github/workflows/publish.yml | 389 +++++++++++------- .gitignore | 65 ++- .pre-commit-config.yaml | 61 +-- LICENSE | 21 + README.md | 84 ++++ elevator_saga/__init__.py | 2 +- elevator_saga/client/api_client.py | 16 +- elevator_saga/client/base_controller.py | 40 +- elevator_saga/client/proxy_models.py | 6 + .../client_examples/__init__.py | 0 .../client_examples/bus_example.py | 17 +- .../client_examples/simple_example.py | 15 +- elevator_saga/core/models.py | 35 +- .../scripts/client_examples/simple_example.py | 15 +- elevator_saga/server/simulator.py | 18 +- elevator_saga/traffic/down_peak.json | 2 +- elevator_saga/traffic/fire_evacuation.json | 2 +- elevator_saga/traffic/generators.py | 29 +- elevator_saga/traffic/high_density.json | 2 +- elevator_saga/traffic/inter_floor.json | 2 +- elevator_saga/traffic/lunch_rush.json | 2 +- elevator_saga/traffic/medical.json | 2 +- elevator_saga/traffic/meeting_event.json | 2 +- elevator_saga/traffic/mixed_scenario.json | 2 +- elevator_saga/traffic/progressive_test.json | 2 +- elevator_saga/traffic/random.json | 2 +- elevator_saga/traffic/up_peak.json | 2 +- pyproject.toml | 143 +++---- pyrightconfig.json | 61 ++- run_all_tests.py | 157 ------- setup.py | 59 --- 37 files changed, 872 insertions(+), 1140 deletions(-) create mode 100644 .bumpversion.cfg delete mode 100644 .github/workflows/code-quality.yml delete mode 100644 .github/workflows/dependabot.yml create mode 100644 .github/workflows/docs.yml create mode 100644 LICENSE create mode 100644 README.md rename README_CN.md => elevator_saga/client_examples/__init__.py (100%) rename test_example.py => elevator_saga/client_examples/bus_example.py (84%) rename simple_example.py => elevator_saga/client_examples/simple_example.py (93%) delete mode 100644 run_all_tests.py delete mode 100644 setup.py 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 + +
+ +[![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) + +
+ +--- + +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. + +--- + +
+ +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, -)