mirror of
https://github.com/ZGCA-Forge/Elevator.git
synced 2025-12-14 04:54:50 +00:00
Update ci
This commit is contained in:
10
.bumpversion.cfg
Normal file
10
.bumpversion.cfg
Normal file
@@ -0,0 +1,10 @@
|
||||
[bumpversion]
|
||||
current_version = 0.0.1
|
||||
commit = True
|
||||
tag = True
|
||||
tag_name = v{new_version}
|
||||
message = Bump version: {current_version} → {new_version}
|
||||
|
||||
[bumpversion:file:elevator_saga/__init__.py]
|
||||
search = __version__ = "{current_version}"
|
||||
replace = __version__ = "{new_version}"
|
||||
5
.github/dependabot.yml
vendored
5
.github/dependabot.yml
vendored
@@ -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"
|
||||
|
||||
|
||||
352
.github/workflows/ci.yml
vendored
352
.github/workflows/ci.yml
vendored
@@ -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
|
||||
|
||||
269
.github/workflows/code-quality.yml
vendored
269
.github/workflows/code-quality.yml
vendored
@@ -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
|
||||
|
||||
34
.github/workflows/dependabot.yml
vendored
34
.github/workflows/dependabot.yml
vendored
@@ -1,34 +0,0 @@
|
||||
name: Dependabot Auto-merge
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
jobs:
|
||||
dependabot:
|
||||
name: Auto-merge Dependabot PRs
|
||||
runs-on: ubuntu-latest
|
||||
if: github.actor == 'dependabot[bot]'
|
||||
|
||||
steps:
|
||||
- name: Dependabot metadata
|
||||
id: metadata
|
||||
uses: dependabot/fetch-metadata@v1
|
||||
with:
|
||||
github-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
|
||||
- name: Enable auto-merge for Dependabot PRs
|
||||
if: steps.metadata.outputs.update-type == 'version-update:semver-patch' || steps.metadata.outputs.update-type == 'version-update:semver-minor'
|
||||
run: gh pr merge --auto --merge "$PR_URL"
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Comment on major version updates
|
||||
if: steps.metadata.outputs.update-type == 'version-update:semver-major'
|
||||
run: |
|
||||
gh pr comment "$PR_URL" --body "🚨 **Major version update detected!** Please review this PR carefully before merging."
|
||||
env:
|
||||
PR_URL: ${{ github.event.pull_request.html_url }}
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
87
.github/workflows/docs.yml
vendored
Normal file
87
.github/workflows/docs.yml
vendored
Normal file
@@ -0,0 +1,87 @@
|
||||
name: Build and Deploy Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
branches: [main]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
branch:
|
||||
description: "要部署文档的分支"
|
||||
required: false
|
||||
default: "main"
|
||||
type: string
|
||||
deploy_to_pages:
|
||||
description: "是否部署到 GitHub Pages"
|
||||
required: false
|
||||
default: true
|
||||
type: boolean
|
||||
|
||||
# Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages
|
||||
permissions:
|
||||
contents: read
|
||||
pages: write
|
||||
id-token: write
|
||||
|
||||
# Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued.
|
||||
# However, do NOT cancel in-progress runs as we want to allow these production deployments to complete.
|
||||
concurrency:
|
||||
group: "pages"
|
||||
cancel-in-progress: false
|
||||
|
||||
jobs:
|
||||
# Build documentation
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v5
|
||||
with:
|
||||
ref: ${{ github.event.inputs.branch || github.ref }}
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
# Install package in development mode to get version info
|
||||
pip install -e .
|
||||
# Install documentation dependencies
|
||||
pip install -e .[docs]
|
||||
|
||||
- name: Setup Pages
|
||||
id: pages
|
||||
uses: actions/configure-pages@v5
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
|
||||
- name: Build Sphinx documentation
|
||||
run: |
|
||||
# Create docs directory if it doesn't exist
|
||||
mkdir -p docs
|
||||
# Placeholder for Sphinx build
|
||||
echo "Documentation build placeholder - configure Sphinx in docs/"
|
||||
# cd docs
|
||||
# make html
|
||||
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-pages-artifact@v4
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
with:
|
||||
path: docs/_build/html
|
||||
|
||||
# Deploy to GitHub Pages
|
||||
deploy:
|
||||
if: github.ref == 'refs/heads/main' || (github.event_name == 'workflow_dispatch' && github.event.inputs.deploy_to_pages == 'true')
|
||||
environment:
|
||||
name: github-pages
|
||||
url: ${{ steps.deployment.outputs.page_url }}
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
steps:
|
||||
- name: Deploy to GitHub Pages
|
||||
id: deployment
|
||||
uses: actions/deploy-pages@v4
|
||||
389
.github/workflows/publish.yml
vendored
389
.github/workflows/publish.yml
vendored
@@ -1,189 +1,258 @@
|
||||
name: Publish to PyPI
|
||||
# This workflow will upload a Python Package to PyPI when a release is created
|
||||
# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-python#publishing-to-package-registries
|
||||
|
||||
# This workflow uses actions that are not certified by GitHub.
|
||||
# They are provided by a third-party and are governed by
|
||||
# separate terms of service, privacy policy, and support
|
||||
# documentation.
|
||||
|
||||
name: Upload PyPI package
|
||||
|
||||
on:
|
||||
release:
|
||||
types: [published]
|
||||
types: [published, edited]
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
test_pypi:
|
||||
description: 'Publish to Test PyPI instead of PyPI'
|
||||
description: "Publish to Test PyPI instead of PyPI"
|
||||
required: false
|
||||
default: 'false'
|
||||
default: false
|
||||
type: boolean
|
||||
|
||||
permissions:
|
||||
contents: read
|
||||
|
||||
jobs:
|
||||
test:
|
||||
name: Run tests before publish
|
||||
# Step 1: Code formatting and pre-commit validation (fast failure)
|
||||
code-format:
|
||||
name: Code formatting and pre-commit validation
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
# - name: Install dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install -e .
|
||||
|
||||
# - name: Run comprehensive tests
|
||||
# run: |
|
||||
# python -m pytest --cov=msgcenterpy --cov-fail-under=80
|
||||
|
||||
# - name: Run linting
|
||||
# run: |
|
||||
# black --check msgcenterpy tests
|
||||
# isort --check-only msgcenterpy tests
|
||||
# mypy msgcenterpy
|
||||
|
||||
build:
|
||||
name: Build package
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10" # Use minimum version for consistency
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install -e .[dev]
|
||||
|
||||
- name: Run pre-commit hooks
|
||||
uses: pre-commit/action@v3.0.1
|
||||
with:
|
||||
extra_args: --all-files
|
||||
|
||||
# Step 2: Basic build and test with minimum Python version (3.10)
|
||||
basic-build:
|
||||
name: Basic build (Python 3.10, Ubuntu)
|
||||
runs-on: ubuntu-latest
|
||||
needs: test
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.12'
|
||||
|
||||
# - name: Install build dependencies
|
||||
# run: |
|
||||
# python -m pip install --upgrade pip
|
||||
# pip install build twine check-manifest
|
||||
|
||||
# - name: Verify version consistency
|
||||
# run: |
|
||||
# # 检查版本号一致性
|
||||
# VERSION=$(python -c "import elevator_saga; print(elevator_saga.__version__)" 2>/dev/null || echo "unknown")
|
||||
# TAG_VERSION="${GITHUB_REF#refs/tags/v}"
|
||||
# if [ "$GITHUB_EVENT_NAME" = "release" ]; then
|
||||
# if [ "$VERSION" != "$TAG_VERSION" ]; then
|
||||
# echo "Version mismatch: package=$VERSION, tag=$TAG_VERSION"
|
||||
# exit 1
|
||||
# fi
|
||||
# fi
|
||||
|
||||
# - name: Check manifest
|
||||
# run: check-manifest
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install build twine check-manifest
|
||||
|
||||
- name: Build package
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
# - name: Check package
|
||||
# run: |
|
||||
# twine check dist/*
|
||||
|
||||
- name: Upload build artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dist-${{ github.run_number }}
|
||||
path: dist/
|
||||
retention-days: 30
|
||||
needs: [code-format] # Only run after code formatting passes
|
||||
|
||||
publish-test:
|
||||
name: Publish to Test PyPI
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python 3.10
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10"
|
||||
|
||||
- name: Cache pip dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: ~/.cache/pip
|
||||
key: ubuntu-pip-3.10-${{ hashFiles('**/pyproject.toml') }}
|
||||
restore-keys: |
|
||||
ubuntu-pip-3.10-
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install pytest
|
||||
pip install -e .[dev]
|
||||
|
||||
- name: Test with pytest
|
||||
run: |
|
||||
pytest -v
|
||||
|
||||
# Step 3: Security scan
|
||||
security:
|
||||
name: Security scan
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: github.event.inputs.test_pypi == 'true' || (github.event_name == 'release' && github.event.release.prerelease)
|
||||
environment:
|
||||
name: test-pypi
|
||||
url: https://test.pypi.org/p/elevator-saga
|
||||
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist-${{ github.run_number }}
|
||||
path: dist/
|
||||
|
||||
- name: Publish to Test PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
password: ${{ secrets.TEST_PYPI_API_TOKEN }}
|
||||
verbose: true
|
||||
needs: [basic-build] # Run in parallel with other tests after basic build
|
||||
|
||||
publish-pypi:
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Run Safety CLI to check for vulnerabilities
|
||||
uses: pyupio/safety-action@v1
|
||||
with:
|
||||
api-key: ${{ secrets.SAFETY_CHECK }}
|
||||
output-format: json
|
||||
args: --detailed-output --output-format json
|
||||
continue-on-error: true
|
||||
|
||||
- name: Upload security reports
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: security-reports
|
||||
path: |
|
||||
safety-report.json
|
||||
if: always()
|
||||
|
||||
release-build:
|
||||
name: Build release distributions
|
||||
runs-on: ubuntu-latest
|
||||
needs: [basic-build]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v6
|
||||
with:
|
||||
python-version: "3.10" # Use minimum version for consistency
|
||||
|
||||
- name: Install build dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
python -m pip install build twine
|
||||
|
||||
- name: Verify version consistency
|
||||
if: github.event_name == 'release' && (github.event.action == 'published' || (github.event.action == 'edited' && !github.event.release.prerelease))
|
||||
run: |
|
||||
# Install package first
|
||||
pip install -e .
|
||||
|
||||
# Get package version (fail fast if not available)
|
||||
VERSION=$(python -c "import elevator_saga; print(elevator_saga.__version__)")
|
||||
|
||||
# Handle both v0.0.3 and 0.0.3 tag formats
|
||||
RAW_TAG="${GITHUB_REF#refs/tags/}"
|
||||
if [[ "$RAW_TAG" == v* ]]; then
|
||||
TAG_VERSION="${RAW_TAG#v}"
|
||||
else
|
||||
TAG_VERSION="$RAW_TAG"
|
||||
fi
|
||||
|
||||
echo "Package version: $VERSION"
|
||||
echo "Tag version: $TAG_VERSION"
|
||||
|
||||
if [ "$VERSION" != "$TAG_VERSION" ]; then
|
||||
echo "❌ Version mismatch: package=$VERSION, tag=$TAG_VERSION"
|
||||
echo "Please ensure the package version matches the git tag"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "✅ Version verification passed: $VERSION"
|
||||
|
||||
- name: Build release distributions
|
||||
run: |
|
||||
python -m build
|
||||
|
||||
- name: Check package
|
||||
run: |
|
||||
twine check dist/*
|
||||
|
||||
- name: Upload distributions
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
|
||||
pypi-publish:
|
||||
name: Publish to PyPI
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: github.event_name == 'release' && !github.event.release.prerelease && github.event.inputs.test_pypi != 'true'
|
||||
environment:
|
||||
name: pypi
|
||||
url: https://pypi.org/p/elevator-saga
|
||||
|
||||
needs:
|
||||
- release-build
|
||||
if: github.event_name == 'release' && !github.event.release.prerelease && github.event.inputs.test_pypi != 'true' && (github.event.action == 'published' || github.event.action == 'edited')
|
||||
permissions:
|
||||
# IMPORTANT: this permission is mandatory for trusted publishing
|
||||
id-token: write
|
||||
|
||||
# Note: For enhanced security, consider configuring deployment environments
|
||||
# in your GitHub repository settings with protection rules
|
||||
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist-${{ github.run_number }}
|
||||
path: dist/
|
||||
|
||||
- name: Publish to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
password: ${{ secrets.PYPI_API_TOKEN }}
|
||||
verbose: true
|
||||
- name: Retrieve release distributions
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
|
||||
- name: Publish release distributions to PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
|
||||
test-pypi-publish:
|
||||
name: Publish to Test PyPI
|
||||
runs-on: ubuntu-latest
|
||||
needs:
|
||||
- release-build
|
||||
if: github.event.inputs.test_pypi == 'true' || (github.event_name == 'release' && github.event.release.prerelease && (github.event.action == 'published' || github.event.action == 'edited'))
|
||||
permissions:
|
||||
# IMPORTANT: this permission is mandatory for trusted publishing
|
||||
id-token: write
|
||||
|
||||
# Note: For enhanced security, consider configuring deployment environments
|
||||
# in your GitHub repository settings with protection rules
|
||||
|
||||
steps:
|
||||
- name: Retrieve release distributions
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
|
||||
- name: Publish release distributions to Test PyPI
|
||||
uses: pypa/gh-action-pypi-publish@release/v1
|
||||
with:
|
||||
repository-url: https://test.pypi.org/legacy/
|
||||
|
||||
create-github-release-assets:
|
||||
name: Add assets to GitHub release
|
||||
runs-on: ubuntu-latest
|
||||
needs: build
|
||||
if: github.event_name == 'release'
|
||||
|
||||
needs: release-build
|
||||
if: github.event_name == 'release' && (github.event.action == 'published' || github.event.action == 'edited')
|
||||
permissions:
|
||||
contents: write # Need write access to upload release assets
|
||||
|
||||
steps:
|
||||
- name: Download build artifacts
|
||||
uses: actions/download-artifact@v3
|
||||
with:
|
||||
name: dist-${{ github.run_number }}
|
||||
path: dist/
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v1
|
||||
with:
|
||||
files: dist/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Retrieve release distributions
|
||||
uses: actions/download-artifact@v5
|
||||
with:
|
||||
name: release-dists
|
||||
path: dist/
|
||||
|
||||
- name: Upload release assets
|
||||
uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
files: dist/*
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
post-publish:
|
||||
name: Post-publish tasks
|
||||
runs-on: ubuntu-latest
|
||||
needs: [publish-pypi, publish-test]
|
||||
if: always() && (needs.publish-pypi.result == 'success' || needs.publish-test.result == 'success')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Create deployment summary
|
||||
run: |
|
||||
echo "## 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Item | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ needs.publish-pypi.result }}" = "success" ]; then
|
||||
echo "| PyPI | ✅ Published |" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ needs.publish-test.result }}" = "success" ]; then
|
||||
echo "| Test PyPI | ✅ Published |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "| GitHub Release | ✅ Assets uploaded |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | ${{ github.event.release.tag_name || 'test' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
# 为将来的通知预留
|
||||
- name: Notify team
|
||||
run: |
|
||||
echo "Package published successfully!"
|
||||
# 可以添加 Slack、Discord 等通知
|
||||
needs: [pypi-publish, test-pypi-publish]
|
||||
if: always() && (needs.pypi-publish.result == 'success' || needs.test-pypi-publish.result == 'success')
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v5
|
||||
|
||||
- name: Create deployment summary
|
||||
run: |
|
||||
echo "## Deployment Summary" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Item | Status |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "|------|--------|" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
if [ "${{ needs.pypi-publish.result }}" = "success" ]; then
|
||||
echo "| PyPI | Published |" >> $GITHUB_STEP_SUMMARY
|
||||
elif [ "${{ needs.test-pypi-publish.result }}" = "success" ]; then
|
||||
echo "| Test PyPI | Published |" >> $GITHUB_STEP_SUMMARY
|
||||
fi
|
||||
|
||||
echo "| GitHub Release | Assets uploaded |" >> $GITHUB_STEP_SUMMARY
|
||||
echo "| Version | ${{ github.event.release.tag_name || 'test' }} |" >> $GITHUB_STEP_SUMMARY
|
||||
|
||||
65
.gitignore
vendored
65
.gitignore
vendored
@@ -1,5 +1,60 @@
|
||||
.vscode
|
||||
.idea
|
||||
__pycache__
|
||||
.mypy_cache
|
||||
elevator_saga.egg-info
|
||||
# ================================
|
||||
# Python-related files
|
||||
# ================================
|
||||
|
||||
# Compiled Python files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
dist/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
|
||||
# Type checking
|
||||
.mypy_cache/
|
||||
|
||||
# Documentation
|
||||
docs/_build/
|
||||
docs/_static/
|
||||
docs/_templates/
|
||||
docs/_static/
|
||||
|
||||
# ================================
|
||||
# IDE and Editor files
|
||||
# ================================
|
||||
|
||||
# PyCharm
|
||||
.idea/
|
||||
|
||||
# Visual Studio Code
|
||||
.vscode/
|
||||
.cursor/
|
||||
.cursorignore
|
||||
pyrightconfig.json
|
||||
|
||||
# ================================
|
||||
# Operating System files
|
||||
# ================================
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
|
||||
@@ -5,33 +5,34 @@ repos:
|
||||
hooks:
|
||||
- id: black
|
||||
language_version: python3
|
||||
args: ["--line-length=88"]
|
||||
args: ["--line-length=120"]
|
||||
|
||||
# Import sorting
|
||||
- repo: https://github.com/pycqa/isort
|
||||
rev: 5.13.2
|
||||
hooks:
|
||||
- id: isort
|
||||
args: ["--profile", "black", "--multi-line", "3"]
|
||||
args:
|
||||
["--profile", "black", "--multi-line", "3", "--line-length", "120"]
|
||||
|
||||
# Linting
|
||||
- repo: https://github.com/pycqa/flake8
|
||||
rev: 7.0.0
|
||||
hooks:
|
||||
- id: flake8
|
||||
args:
|
||||
- "--max-line-length=88"
|
||||
- "--extend-ignore=E203,W503,F401"
|
||||
- "--exclude=build,dist,.eggs"
|
||||
args:
|
||||
- "--max-line-length=200" # Allow longer lines after black formatting
|
||||
- "--extend-ignore=E203,W503,F401,E402,E721,F841"
|
||||
- "--exclude=build,dist,__pycache__,.mypy_cache,.pytest_cache,htmlcov,.idea,.vscode,docs/_build,elevatorpy.egg-info,elevator_saga.egg-info,.eggs"
|
||||
|
||||
# Type checking
|
||||
- repo: https://github.com/pre-commit/mirrors-mypy
|
||||
rev: v1.8.0
|
||||
hooks:
|
||||
- id: mypy
|
||||
additional_dependencies: [types-PyYAML]
|
||||
args: ["--ignore-missing-imports", "--scripts-are-modules"]
|
||||
exclude: "^(tests/|examples/)"
|
||||
additional_dependencies: [types-PyYAML, types-jsonschema]
|
||||
args: ["--ignore-missing-imports", "--disable-error-code=unused-ignore"]
|
||||
files: "^(elevator_saga/)" # Check both source code
|
||||
|
||||
# General pre-commit hooks
|
||||
- repo: https://github.com/pre-commit/pre-commit-hooks
|
||||
@@ -45,18 +46,16 @@ repos:
|
||||
- id: check-json
|
||||
- id: check-toml
|
||||
- id: check-xml
|
||||
|
||||
|
||||
# Security
|
||||
- id: check-merge-conflict
|
||||
- id: check-case-conflict
|
||||
- id: check-symlinks
|
||||
- id: check-added-large-files
|
||||
args: ["--maxkb=1000"]
|
||||
|
||||
|
||||
# Python specific
|
||||
- id: check-ast
|
||||
- id: check-builtin-literals
|
||||
- id: check-docstring-first
|
||||
- id: debug-statements
|
||||
- id: name-tests-test
|
||||
args: ["--django"]
|
||||
@@ -67,47 +66,17 @@ repos:
|
||||
hooks:
|
||||
- id: bandit
|
||||
args: ["-c", "pyproject.toml"]
|
||||
additional_dependencies: ["bandit[toml]"]
|
||||
additional_dependencies: ["bandit[toml]", "pbr"]
|
||||
exclude: "^tests/"
|
||||
|
||||
# Documentation
|
||||
- repo: https://github.com/pycqa/pydocstyle
|
||||
rev: 6.3.0
|
||||
hooks:
|
||||
- id: pydocstyle
|
||||
args: ["--convention=google"]
|
||||
exclude: "^(tests/|examples/)"
|
||||
|
||||
# YAML/JSON formatting
|
||||
- repo: https://github.com/pre-commit/mirrors-prettier
|
||||
rev: v4.0.0-alpha.8
|
||||
hooks:
|
||||
- id: prettier
|
||||
types_or: [yaml, json, markdown]
|
||||
exclude: "^(.github/)"
|
||||
|
||||
# Spell checking (optional)
|
||||
- repo: https://github.com/codespell-project/codespell
|
||||
rev: v2.2.6
|
||||
hooks:
|
||||
- id: codespell
|
||||
args: ["--write-changes"]
|
||||
exclude: "^(.*\\.po|.*\\.pot|CHANGELOG\\.md)$"
|
||||
exclude: "^(build/|dist/|__pycache__/|\\.mypy_cache/|\\.pytest_cache/|htmlcov/|\\.idea/|\\.vscode/|docs/_build/|elevatorpy\\.egg-info/|elevator_saga\\.egg-info/)"
|
||||
|
||||
# Global settings
|
||||
default_stages: [commit, push]
|
||||
default_stages: [pre-commit, pre-push]
|
||||
fail_fast: false
|
||||
|
||||
# CI settings
|
||||
ci:
|
||||
autofix_commit_msg: |
|
||||
[pre-commit.ci] auto fixes from pre-commit.com hooks
|
||||
|
||||
for more information, see https://pre-commit.ci
|
||||
autofix_prs: true
|
||||
autoupdate_branch: ''
|
||||
autoupdate_commit_msg: '[pre-commit.ci] pre-commit autoupdate'
|
||||
autoupdate_schedule: weekly
|
||||
skip: []
|
||||
submodules: false
|
||||
|
||||
|
||||
21
LICENSE
Normal file
21
LICENSE
Normal file
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2025 Elevator Saga Team
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
84
README.md
Normal file
84
README.md
Normal file
@@ -0,0 +1,84 @@
|
||||
# Elevator Saga
|
||||
|
||||
<div align="center">
|
||||
|
||||
[](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)
|
||||
|
||||
</div>
|
||||
|
||||
---
|
||||
|
||||
Elevator Saga is a Python implementation of an elevator [simulation game](https://play.elevatorsaga.com/) with a event-driven architecture Design and optimize elevator control algorithms to efficiently transport passengers in buildings.
|
||||
|
||||
### Features
|
||||
|
||||
- 🏢 **Realistic Simulation**: Physics-based elevator movement with acceleration, deceleration, and realistic timing
|
||||
|
||||
## Installation
|
||||
|
||||
### Basic Installation
|
||||
|
||||
```bash
|
||||
pip install elevatorpy
|
||||
```
|
||||
|
||||
### With Development Dependencies
|
||||
|
||||
```bash
|
||||
pip install elevatorpy[dev]
|
||||
```
|
||||
|
||||
### From Source
|
||||
|
||||
```bash
|
||||
git clone https://github.com/ZGCA-Forge/Elevator.git
|
||||
cd Elevator
|
||||
pip install -e .[dev]
|
||||
```
|
||||
|
||||
## Quick Start
|
||||
|
||||
### Running the Game
|
||||
|
||||
```bash
|
||||
# Start the backend simulator (Terminal #1)
|
||||
python -m elevator_saga.server.simulator
|
||||
```
|
||||
|
||||
```bash
|
||||
# Start your own client (Terminal #2)
|
||||
# Example:
|
||||
python -m elevator_saga.client_examples.bus_example
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
For detailed documentation, please visit: [https://zgca-forge.github.io/Elevator/](https://zgca-forge.github.io/Elevator/)
|
||||
|
||||
## Contributing
|
||||
|
||||
Contributions are welcome! Please feel free to submit a Pull Request.
|
||||
|
||||
## Star History
|
||||
|
||||
[](https://star-history.com/#ZGCA-Forge/Elevator&Date)
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under MIT License - see the [LICENSE](LICENSE) file for details.
|
||||
|
||||
---
|
||||
|
||||
<div align="center">
|
||||
|
||||
Made with ❤️ by the ZGCA-Forge Team
|
||||
|
||||
</div>
|
||||
@@ -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"
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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:
|
||||
# 重置服务器状态
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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为单位)"""
|
||||
|
||||
@@ -455,4 +455,4 @@
|
||||
"tick": 196
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -179,4 +179,4 @@
|
||||
"tick": 74
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -407,4 +407,4 @@
|
||||
"tick": 197
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -371,4 +371,4 @@
|
||||
"tick": 197
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,4 +161,4 @@
|
||||
"tick": 192
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,4 +491,4 @@
|
||||
"tick": 138
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -311,4 +311,4 @@
|
||||
"tick": 49
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -611,4 +611,4 @@
|
||||
"tick": 190
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -587,4 +587,4 @@
|
||||
"tick": 199
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -479,4 +479,4 @@
|
||||
"tick": 196
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,4 +491,4 @@
|
||||
"tick": 146
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
143
pyproject.toml
143
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"]
|
||||
|
||||
@@ -1,33 +1,30 @@
|
||||
{
|
||||
"include": ["."],
|
||||
"exclude": [
|
||||
"build",
|
||||
"dist",
|
||||
"__pycache__",
|
||||
".mypy_cache",
|
||||
".pytest_cache",
|
||||
"htmlcov",
|
||||
".idea",
|
||||
".vscode",
|
||||
"docs/_build",
|
||||
"elevator_saga.egg-info"
|
||||
],
|
||||
"pythonVersion": "3.10",
|
||||
"typeCheckingMode": "strict",
|
||||
"executionEnvironments": [
|
||||
{
|
||||
"root": ".",
|
||||
"extraPaths": ["elevator_saga"]
|
||||
}
|
||||
],
|
||||
"reportMissingImports": "none",
|
||||
"reportUnusedImport": "warning",
|
||||
"reportUnusedVariable": "warning",
|
||||
"reportUnknownArgumentType": "warning",
|
||||
"reportUnknownMemberType": "warning",
|
||||
"reportUnknownVariableType": "warning",
|
||||
"reportUnknownParameterType": "warning",
|
||||
"reportPrivateUsage": "warning",
|
||||
"reportMissingTypeStubs": false
|
||||
}
|
||||
|
||||
"include": ["."],
|
||||
"exclude": [
|
||||
"build",
|
||||
"dist",
|
||||
"__pycache__",
|
||||
".mypy_cache",
|
||||
".pytest_cache",
|
||||
"htmlcov",
|
||||
".idea",
|
||||
".vscode",
|
||||
"docs/_build",
|
||||
"elevatorpy.egg-info",
|
||||
"elevator_saga.egg-info",
|
||||
".eggs",
|
||||
"MsgCenterPy"
|
||||
],
|
||||
"pythonVersion": "3.10",
|
||||
"typeCheckingMode": "basic",
|
||||
"executionEnvironments": [
|
||||
{
|
||||
"root": ".",
|
||||
"extraPaths": ["elevator_saga"]
|
||||
}
|
||||
],
|
||||
"reportMissingImports": "warning",
|
||||
"reportUnusedImport": "warning",
|
||||
"reportUnusedVariable": "warning",
|
||||
"reportMissingTypeStubs": false
|
||||
}
|
||||
|
||||
157
run_all_tests.py
157
run_all_tests.py
@@ -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())
|
||||
59
setup.py
59
setup.py
@@ -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,
|
||||
)
|
||||
Reference in New Issue
Block a user