diff --git a/.github/workflows/ci-test.yml b/.github/workflows/ci-test.yml index f460be7c..db6a57c7 100644 --- a/.github/workflows/ci-test.yml +++ b/.github/workflows/ci-test.yml @@ -11,11 +11,11 @@ permissions: jobs: # Lint, type-check and security run once on a single pinned interpreter - # (Python 3.10, matching the mypy/ruff target-version in pyproject). These + # (Python 3.11, matching the mypy/ruff target-version in pyproject). These # checks are interpreter-agnostic, so fanning them across the matrix would # only duplicate work. quality: - name: Lint, type-check & security (Py 3.10) + name: Lint, type-check & security (Py 3.11) runs-on: ubuntu-latest steps: - name: Checkout code @@ -24,10 +24,10 @@ jobs: # A PR can branch before the base tip; its merge-base needs full history. fetch-depth: 0 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.10" + python-version: "3.11" - name: Install package with dev extras run: | @@ -65,19 +65,19 @@ jobs: fail-fast: false matrix: # Run the full suite on every declared Python version. The package's - # environment markers resolve azure-functions 1.x on 3.10-3.12 and + # environment markers resolve azure-functions 1.x on 3.11-3.12 and # permit 2.x on 3.13-3.14; the dedicated wheel lanes below separately # prove that the built distribution works against the 2.x line. - python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14"] # 'latest' resolves whatever satisfies the pyproject.toml pin at CI time. azure-functions-version: ["latest"] include: # Extra SDK-version coverage on the lowest supported Python. # Keep in sync with the SDK compatibility matrix in README.md and # the version pin in pyproject.toml. - - python-version: "3.10" + - python-version: "3.11" azure-functions-version: "1.21.0" - - python-version: "3.10" + - python-version: "3.11" azure-functions-version: "1.24.0" steps: @@ -91,7 +91,7 @@ jobs: - name: Assert the runtime interpreter matches the matrix cell # Tests must run on the interpreter this cell claims to exercise. Before - # #554 the whole matrix ran through the Python-3.10-pinned hatch env, so + # #554 the whole matrix ran through the default pinned Hatch env, so # 3.11/3.12 were never actually tested. This guard fails loudly if the # running interpreter ever drifts from matrix.python-version again. env: @@ -134,13 +134,13 @@ jobs: - name: Run tests on the matrix interpreter # Raw pytest on the interpreter provisioned above (NOT through the - # Python-3.10-pinned hatch env). The repo pytest config enables --cov - # with fail_under=95, but only the 3.10 x latest cell collects the full + # default pinned Hatch env). The repo pytest config enables --cov + # with fail_under=95, but only the 3.11 x latest cell collects the full # suite. Enforce the coverage gate on the - # canonical 3.10 x latest cell and run the rest with --no-cov so a + # canonical 3.11 x latest cell and run the rest with --no-cov so a # version-variant cell cannot fail the build on a spurious coverage dip. env: - IS_COVERAGE_CELL: ${{ matrix.python-version == '3.10' && matrix.azure-functions-version == 'latest' }} + IS_COVERAGE_CELL: ${{ matrix.python-version == '3.11' && matrix.azure-functions-version == 'latest' }} run: | if [ "$IS_COVERAGE_CELL" = "true" ]; then pytest tests/ @@ -149,11 +149,11 @@ jobs: fi - name: Verify coverage output - if: matrix.python-version == '3.10' && matrix.azure-functions-version == 'latest' + if: matrix.python-version == '3.11' && matrix.azure-functions-version == 'latest' run: ls -lh coverage.xml - name: Upload coverage to Codecov - if: matrix.python-version == '3.10' && matrix.azure-functions-version == 'latest' && github.actor != 'dependabot[bot]' + if: matrix.python-version == '3.11' && matrix.azure-functions-version == 'latest' && github.actor != 'dependabot[bot]' uses: codecov/codecov-action@303a32d7a59b442fa8d48b6a1cc6825c09c847a5 # v7.1.1 with: token: ${{ secrets.CODECOV_TOKEN }} @@ -175,7 +175,15 @@ jobs: # reports, and a skipped or cancelled dependency is not success, so it cannot # pass by being skipped. ci-required: - needs: [quality, test, azure-functions-2x, host-smoke] + needs: + - quality + - test + - minimum-dependencies + - artifact-build + - artifact-python310-negative + - artifact-python311 + - azure-functions-2x + - host-smoke if: always() runs-on: ubuntu-latest steps: @@ -183,17 +191,29 @@ jobs: env: QUALITY_RESULT: ${{ needs.quality.result }} TEST_RESULT: ${{ needs.test.result }} + MINIMUM_DEPENDENCIES_RESULT: ${{ needs.minimum-dependencies.result }} + ARTIFACT_BUILD_RESULT: ${{ needs.artifact-build.result }} + ARTIFACT_PY310_RESULT: ${{ needs.artifact-python310-negative.result }} + ARTIFACT_PY311_RESULT: ${{ needs.artifact-python311.result }} AF2X_RESULT: ${{ needs.azure-functions-2x.result }} HOST_SMOKE_RESULT: ${{ needs.host-smoke.result }} run: | set -euo pipefail echo "quality result: ${QUALITY_RESULT}" echo "test result: ${TEST_RESULT}" + echo "minimum deps: ${MINIMUM_DEPENDENCIES_RESULT}" + echo "artifact build: ${ARTIFACT_BUILD_RESULT}" + echo "artifact 3.10: ${ARTIFACT_PY310_RESULT}" + echo "artifact 3.11: ${ARTIFACT_PY311_RESULT}" echo "af 2.x result: ${AF2X_RESULT}" echo "host smoke: ${HOST_SMOKE_RESULT}" if [ "${QUALITY_RESULT}" = "success" ] \ && [ "${TEST_RESULT}" = "success" ] \ + && [ "${MINIMUM_DEPENDENCIES_RESULT}" = "success" ] \ + && [ "${ARTIFACT_BUILD_RESULT}" = "success" ] \ + && [ "${ARTIFACT_PY310_RESULT}" = "success" ] \ + && [ "${ARTIFACT_PY311_RESULT}" = "success" ] \ && [ "${AF2X_RESULT}" = "success" ] \ && [ "${HOST_SMOKE_RESULT}" = "success" ]; then echo "Quality, test matrix, azure-functions 2.x and host smoke all succeeded. Passing." @@ -203,18 +223,161 @@ jobs: echo "::error::Required checks did not all succeed -- quality='${QUALITY_RESULT}' test='${TEST_RESULT}' af2x='${AF2X_RESULT}' host-smoke='${HOST_SMOKE_RESULT}'" exit 1 + minimum-dependencies: + name: Minimum dependencies (Py 3.11) + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Python 3.11 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.11" + + - name: Install minimum runtime and development dependencies + run: | + python -m pip install --upgrade pip + pip install "azure-functions==1.21.0" + pip install -e ".[dev]" + + - name: Assert the declared azure-functions floor is resolved + run: | + pip show azure-functions azure-functions-openapi + python - <<'PY' + from importlib.metadata import version + + resolved = version("azure-functions") + print(f"resolved azure-functions={resolved}") + assert resolved == "1.21.0" + PY + + - name: Run full suite against minimum runtime dependencies + run: pytest tests/ --no-cov + + artifact-build: + name: Build compatibility artifacts + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + + - name: Set up Python 3.11 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.11" + + - name: Build wheel and source distribution + run: | + python -m pip install --upgrade pip build + python -m build + + - name: Upload compatibility artifacts + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4 + with: + name: compatibility-dist + path: dist/ + if-no-files-found: error + + artifact-python310-negative: + name: Wheel install rejection (Py 3.10, negative test) + needs: artifact-build + runs-on: ubuntu-latest + steps: + - name: Set up Python 3.10 for negative install test + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.10" + + - name: Download built artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: compatibility-dist + path: compatibility-dist + + - name: Require wheel rejection by Requires-Python + run: | + wheel=$(find compatibility-dist -maxdepth 1 -name '*.whl' -print -quit) + test -n "$wheel" + if output=$(python -m pip install --no-deps "$wheel" 2>&1); then + echo "::error ::Python 3.10 unexpectedly installed the candidate wheel" + exit 1 + fi + printf '%s\n' "$output" + grep -F "requires a different Python" <<<"$output" + grep -F "3.10" <<<"$output" + grep -F ">=3.11" <<<"$output" + + artifact-python311: + name: Wheel install and smoke (Py 3.11) + needs: artifact-build + runs-on: ubuntu-latest + steps: + - name: Set up Python 3.11 + uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 + with: + python-version: "3.11" + + - name: Download built artifacts + uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 + with: + name: compatibility-dist + path: compatibility-dist + + - name: Install wheel with runtime dependencies + run: | + wheel=$(find compatibility-dist -maxdepth 1 -name '*.whl' -print -quit) + test -n "$wheel" + python -m pip install "$wheel" + + - name: Check installed wheel outside the checkout + working-directory: /tmp + env: + CHECKOUT: ${{ github.workspace }} + run: | + python -m pip check + python - <<'PY' + import os + from pathlib import Path + + import azure.functions as func + import azure_functions_openapi + from azure_functions_openapi import generate_openapi_spec, openapi, scan_endpoint_metadata + + module_path = Path(azure_functions_openapi.__file__).resolve() + checkout = Path(os.environ["CHECKOUT"]).resolve() + print(f"imported module: {module_path}") + assert not module_path.is_relative_to(checkout) + + app = func.FunctionApp() + + @openapi(summary="Artifact smoke", responses={200: {"description": "OK"}}) + @app.route(route="artifact-smoke", methods=["GET"]) + def artifact_smoke(req: func.HttpRequest) -> func.HttpResponse: + return func.HttpResponse("ok") + + scan_endpoint_metadata(app) + spec = generate_openapi_spec() + assert "/api/artifact-smoke" in spec["paths"] + assert spec["paths"]["/api/artifact-smoke"]["get"] + PY + host-smoke: - name: Real Functions host smoke (Py 3.12) + name: Real Functions host smoke (Py ${{ matrix.python-version }}) runs-on: ubuntu-latest timeout-minutes: 5 + strategy: + fail-fast: false + matrix: + python-version: ["3.11", "3.12"] steps: - name: Checkout code uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Set up Python 3.12 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.12" + python-version: ${{ matrix.python-version }} - name: Install Functions Core Tools v4 and smoke app run: | @@ -279,7 +442,7 @@ jobs: # # Why a wheel + dedicated job (not the main matrix): azure-functions 2.x # declares Requires-Python >=3.13, so it is only installable on 3.13/3.14 -- - # interpreters the main `test` matrix (3.10-3.12) never covers. This lane + # interpreters the main `test` matrix proves independently. This lane # installs the built wheel directly (no hatch env) on real 3.13 AND 3.14 # # Gating: this lane runs on every PR as a VISIBLE compatibility signal. The @@ -309,7 +472,7 @@ jobs: # tree, mirroring the sibling proof lanes (azure-functions-validation # #351 / azure-functions-langgraph #350). Building the distribution # artifact also avoids hatch's default env, which is pinned to Python - # 3.10 in pyproject and cannot install azure-functions 2.x (Requires- + # 3.11 in pyproject and cannot install azure-functions 2.x (Requires- # Python >=3.13). run: | python -m pip install --upgrade pip build diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index b7e8fc34..178dd1d8 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -22,7 +22,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.10" + python-version: "3.11" - name: Lint Mermaid diagram labels run: python scripts/lint_mermaid.py diff --git a/.github/workflows/e2e-azure.yml b/.github/workflows/e2e-azure.yml index 5e2b9b96..84e2cc05 100644 --- a/.github/workflows/e2e-azure.yml +++ b/.github/workflows/e2e-azure.yml @@ -18,9 +18,9 @@ on: required: false type: string python_version: - description: Python runtime version for the Azure Function App (e.g. 3.10, 3.13) + description: Python runtime version for the Azure Function App (e.g. 3.12, 3.13) required: false - default: "3.10" + default: "3.12" type: string workflow_call: inputs: @@ -33,9 +33,9 @@ on: required: false type: string python_version: - description: Python runtime version for the Azure Function App (e.g. 3.10, 3.13) + description: Python runtime version for the Azure Function App (e.g. 3.12, 3.13) required: false - default: "3.10" + default: "3.12" type: string secrets: AZURE_CLIENT_ID: @@ -104,13 +104,13 @@ jobs: location="$AZURE_LOCATION" \ functionAppName="${{ steps.names.outputs.app }}" \ storageName="${{ steps.names.outputs.st }}" \ - pythonVersion="${{ inputs.python_version || '3.10' }}" \ + pythonVersion="${{ inputs.python_version || '3.12' }}" \ enableAppInsights=false - - name: Set up Python ${{ inputs.python_version || '3.10' }} + - name: Set up Python ${{ inputs.python_version || '3.12' }} uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "${{ inputs.python_version || '3.10' }}" + python-version: "${{ inputs.python_version || '3.12' }}" - name: Install Azure Functions Core Tools run: | diff --git a/.github/workflows/maintenance.yml b/.github/workflows/maintenance.yml index aa31ee41..ed03456c 100644 --- a/.github/workflows/maintenance.yml +++ b/.github/workflows/maintenance.yml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.12" + python-version: "3.11" - name: Install Hatch and dependencies via Makefile run: | diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 711f8b7b..705b9a07 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -54,7 +54,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.12" + python-version: "3.11" - name: Install build dependencies run: | @@ -101,7 +101,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.12" + python-version: "3.11" - name: Install Hatch run: pip install hatch @@ -123,7 +123,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.10" + python-version: "3.11" - name: Download candidate wheel uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4 @@ -174,7 +174,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.10" + python-version: "3.11" - name: Set up Node.js (func Core Tools + Azurite) uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/screenshots.yml b/.github/workflows/screenshots.yml index 40a62252..8abc036b 100644 --- a/.github/workflows/screenshots.yml +++ b/.github/workflows/screenshots.yml @@ -23,10 +23,10 @@ jobs: steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.10" + python-version: "3.11" - name: Install checker dependencies run: pip install pyyaml diff --git a/.github/workflows/security.yml b/.github/workflows/security.yml index 33419754..6f76dbd3 100644 --- a/.github/workflows/security.yml +++ b/.github/workflows/security.yml @@ -21,7 +21,7 @@ jobs: - name: Set up Python uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: "3.12" + python-version: "3.11" - name: Install Hatch and dependencies via Makefile run: | diff --git a/AGENTS.md b/AGENTS.md index d622447a..ef4e382c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -8,7 +8,7 @@ - Project: `azure-functions-openapi` - Project type: Python library - Runtime scope: Azure Functions Python v2 programming model -- Minimum supported Python: `3.10` +- Minimum supported Python: `3.11` - Packaging: `pyproject.toml` with Hatch ## Root vs Docs @@ -266,7 +266,7 @@ Use Makefile entry points only. Do not bypass the Makefile in CI or contributor ## Compatibility Rules -- Runtime code must remain compatible with Python `3.10`. +- Runtime code must remain compatible with Python `3.11`. - Public APIs must be fully typed. - Avoid silent behavior changes. - Breaking changes require explicit documentation and versioning discussion. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 24b600aa..24b88229 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -34,9 +34,9 @@ Recommended branch prefixes: ## Local development quickstart -The package supports Python `>=3.10,<3.15` (currently Python 3.10 through 3.14). +The package supports Python `>=3.11,<3.15` (currently Python 3.11 through 3.14). The default Hatch development environment is intentionally provisioned with -Python 3.10, so `make install` may download or select Python 3.10 even when +Python 3.11, so `make install` may download or select Python 3.11 even when your machine's default interpreter is newer. From the repository root, run: ```bash diff --git a/DESIGN.md b/DESIGN.md index a2429503..bc85c26d 100644 --- a/DESIGN.md +++ b/DESIGN.md @@ -61,7 +61,7 @@ Note: `render_swagger_ui()` does not embed or generate the OpenAPI spec. It retu ## Compatibility Policy -- Minimum supported Python version: `3.10` +- Minimum supported Python version: `3.11` - Supported runtime target: Azure Functions Python v2 programming model - Public APIs follow semantic versioning expectations diff --git a/README.ja.md b/README.ja.md index 5650188a..2ad1d90b 100644 --- a/README.ja.md +++ b/README.ja.md @@ -4,7 +4,7 @@ [![PyPI](https://img.shields.io/pypi/v/azure-functions-openapi.svg)](https://pypi.org/project/azure-functions-openapi/) [![Downloads](https://static.pepy.tech/badge/azure-functions-openapi/month)](https://pepy.tech/project/azure-functions-openapi) -[![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-openapi/) +[![Python Version](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-openapi/) [![CI](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/ci-test.yml) [![Release](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/publish-pypi.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/publish-pypi.yml) [![Security Scans](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/security.yml) @@ -140,12 +140,12 @@ azure-functions-openapi このパッケージは、`azure-functions` SDK からルート、メソッド、ハンドラーを 1 つの分離されたアダプター(`azure_functions_openapi.adapters`)を介して検出します。検出は **公開 API 優先** で、公開された冪等な `FunctionBuilder.build()` で列挙し、その他はすべて公開 `Function` アクセサー(`get_function_name` / `get_user_function` / `get_bindings` / `is_http_function`)を介して読み取ります。アダプターは冪等でない `FunctionApp.get_functions()` を決して呼び出しません。列挙に公開の代替手段がない **唯一** の非公開トークン `app._function_builders` のみがアダプター内部に分離され、必須のガードテストで保護されます。CI で明示的なマトリックスで検証します。経緯は [イシュー #258](https://github.com/yeongseon/azure-functions-openapi-python/issues/258) と [イシュー #327](https://github.com/yeongseon/azure-functions-openapi-python/issues/327) を参照してください。 -| `azure-functions` | Python 3.10 | Python 3.11 | Python 3.12 | Python 3.13 | Python 3.14 | -| ----------------- | :---------: | :---------: | :---------: | :---------: | :---------: | -| `1.21.0`(下限) | ✅ 検証済 | | | | | -| `1.24.0` | ✅ 検証済 | | | | | -| `latest 1.x` | ✅ 検証済 | ✅ 検証済 | ✅ 検証済 | | | -| `2.x` (`>=2,<3`) | | | | ✅ 検証済 | ✅ 検証済 | +| `azure-functions` | Python 3.11 | Python 3.12 | Python 3.13 | Python 3.14 | +| ----------------- | :---------: | :---------: | :---------: | :---------: | +| `1.21.0`(下限) | ✅ 検証済 | | | | +| `1.24.0` | ✅ 検証済 | | | | +| `latest 1.x` | ✅ 検証済 | ✅ 検証済 | | | +| `2.x` (`>=2,<3`) | | | ✅ 検証済 | ✅ 検証済 | `pyproject.toml` のバージョンピンはインタプリタに依存します: Python < 3.13 では `azure-functions>=1.21.0,<2.0.0`、Python 3.13+ では `azure-functions>=1.21.0,<3.0.0`(未認証の `azure-functions` 3.x を除外)です。下限が `1.21.0` なのは、それ以前のリリースが `FunctionBuilder.__call__` から `None` を返すためです(テストと CLI 抽出でデコレートされたハンドラーの直接呼び出しが壊れる)。分割している理由は、`azure-functions` 2.x が Python < 3.13 のサポートを廃止するため、2.x ラインは Python 3.13+ でのみインストール・提供されるからです。2.x パスは CI の専用 wheel ベース互換性マトリックス(実際の Python 3.13 および 3.14 インタプリタ)と、実際の Azure 認証 — koreacentral の Flex Consumption プランにデプロイされた Python 3.13 Function App — によって実証されています。上限解除の作業は [イシュー #528](https://github.com/yeongseon/azure-functions-openapi-python/issues/528) と [イシュー #488](https://github.com/yeongseon/azure-functions-openapi-python/issues/488) を参照してください。 diff --git a/README.ko.md b/README.ko.md index 7e53b87c..22684d69 100644 --- a/README.ko.md +++ b/README.ko.md @@ -4,7 +4,7 @@ [![PyPI](https://img.shields.io/pypi/v/azure-functions-openapi.svg)](https://pypi.org/project/azure-functions-openapi/) [![Downloads](https://static.pepy.tech/badge/azure-functions-openapi/month)](https://pepy.tech/project/azure-functions-openapi) -[![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-openapi/) +[![Python Version](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-openapi/) [![CI](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/ci-test.yml) [![Release](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/publish-pypi.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/publish-pypi.yml) [![Security Scans](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/security.yml) @@ -140,12 +140,12 @@ azure-functions-openapi 이 패키지는 `azure-functions` SDK에서 라우트, 메서드, 핸들러를 하나의 격리된 어댑터(`azure_functions_openapi.adapters`)를 통해 검색합니다. 검색은 **공개 API 우선**으로, 공개적이고 멱등한 `FunctionBuilder.build()`로 열거하며 나머지는 모두 공개 `Function` 접근자(`get_function_name` / `get_user_function` / `get_bindings` / `is_http_function`)를 통해 읽습니다. 어댑터는 멱등하지 않은 `FunctionApp.get_functions()`를 절대 호출하지 않습니다. 열거에 공개 대체 수단이 없는 **유일한** 비공개 토큰 `app._function_builders`만 어댑터 내부에 격리되어 있으며 필수 가드 테스트로 보호됩니다. CI에서 명시적 매트릭스로 검증합니다. 배경은 [이슈 #258](https://github.com/yeongseon/azure-functions-openapi-python/issues/258)과 [이슈 #327](https://github.com/yeongseon/azure-functions-openapi-python/issues/327)을 참고하세요. -| `azure-functions` | Python 3.10 | Python 3.11 | Python 3.12 | Python 3.13 | Python 3.14 | -| ----------------- | :---------: | :---------: | :---------: | :---------: | :---------: | -| `1.21.0` (최소) | ✅ 테스트됨 | | | | | -| `1.24.0` | ✅ 테스트됨 | | | | | -| `latest 1.x` | ✅ 테스트됨 | ✅ 테스트됨 | ✅ 테스트됨 | | | -| `2.x` (`>=2,<3`) | | | | ✅ 테스트됨 | ✅ 테스트됨 | +| `azure-functions` | Python 3.11 | Python 3.12 | Python 3.13 | Python 3.14 | +| ----------------- | :---------: | :---------: | :---------: | :---------: | +| `1.21.0` (최소) | ✅ 테스트됨 | | | | +| `1.24.0` | ✅ 테스트됨 | | | | +| `latest 1.x` | ✅ 테스트됨 | ✅ 테스트됨 | | | +| `2.x` (`>=2,<3`) | | | ✅ 테스트됨 | ✅ 테스트됨 | `pyproject.toml`의 버전 핀은 인터프리터에 따라 다릅니다: Python < 3.13에서는 `azure-functions>=1.21.0,<2.0.0`, Python 3.13+에서는 `azure-functions>=1.21.0,<3.0.0`(미인증 `azure-functions` 3.x 차단)입니다. 최소 버전이 `1.21.0`인 이유는 그 이전 릴리스가 `FunctionBuilder.__call__`에서 `None`을 반환하기 때문입니다(테스트와 CLI 추출에서 데코레이트된 핸들러의 직접 호출이 깨짐). 이렇게 나눈 이유는 `azure-functions` 2.x가 Python < 3.13 지원을 중단하므로, 2.x 라인은 Python 3.13+에서만 설치 가능하고 제공되기 때문입니다. 2.x 경로는 CI의 전용 wheel 기반 호환성 매트릭스(실제 Python 3.13 및 3.14 인터프리터)와 실제 Azure 인증 — koreacentral의 Flex Consumption 요금제에 배포된 Python 3.13 Function App — 으로 검증되었습니다. 상한 해제 작업은 [이슈 #528](https://github.com/yeongseon/azure-functions-openapi-python/issues/528)과 [이슈 #488](https://github.com/yeongseon/azure-functions-openapi-python/issues/488)을 참고하세요. diff --git a/README.md b/README.md index de9d9562..def5b77c 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ [![PyPI](https://img.shields.io/pypi/v/azure-functions-openapi.svg)](https://pypi.org/project/azure-functions-openapi/) [![Downloads](https://static.pepy.tech/badge/azure-functions-openapi/month)](https://pepy.tech/project/azure-functions-openapi) -[![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-openapi/) +[![Python Version](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-openapi/) [![CI](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/ci-test.yml) [![Release](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/publish-pypi.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/publish-pypi.yml) [![Security Scans](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/security.yml) @@ -17,8 +17,6 @@ Read this in: [한국어](README.ko.md) | [日本語](README.ja.md) | [简体中 OpenAPI (Swagger) documentation and Swagger UI for the **Azure Functions Python v2 programming model**. -> **Python 3.10 is deprecated.** Support ends in the next minor release — Python 3.10 reaches end of life in October 2026. Importing the package on Python 3.10 emits a `FutureWarning`; upgrade to Python 3.11 or newer. - ## Why this exists Azure Functions Python v2 has no built-in API documentation story: @@ -179,12 +177,12 @@ azure-functions-openapi This package discovers routes, methods, and handlers from the `azure-functions` SDK through a single isolated adapter (`azure_functions_openapi.adapters`). Discovery is **public-API-first**: it enumerates via the public, idempotent `FunctionBuilder.build()` and reads everything else through public `Function` accessors (`get_function_name` / `get_user_function` / `get_bindings` / `is_http_function`). The adapter never calls the non-idempotent `FunctionApp.get_functions()`. The **one** unavoidable private token — `app._function_builders`, which has no public substitute for enumeration — lives exclusively in the adapter and is covered by a mandatory guard test. We validate the package against an explicit matrix in CI. See [issue #258](https://github.com/yeongseon/azure-functions-openapi-python/issues/258) and [issue #327](https://github.com/yeongseon/azure-functions-openapi-python/issues/327) for background. -| `azure-functions` | Python 3.10 | Python 3.11 | Python 3.12 | Python 3.13 | Python 3.14 | -| ------------------ | :---------: | :---------: | :---------: | :---------: | :---------: | -| `1.21.0` (floor) | ✅ tested | | | | | -| `1.24.0` | ✅ tested | | | | | -| `latest 1.x` | ✅ tested | ✅ tested | ✅ tested | | | -| `2.x` (`>=2,<3`) | | | | ✅ tested | ✅ tested | +| `azure-functions` | Python 3.11 | Python 3.12 | Python 3.13 | Python 3.14 | +| ------------------ | :---------: | :---------: | :---------: | :---------: | +| `1.21.0` (floor) | ✅ tested | | | | +| `1.24.0` | ✅ tested | | | | +| `latest 1.x` | ✅ tested | ✅ tested | | | +| `2.x` (`>=2,<3`) | | | ✅ tested | ✅ tested | The version pin in `pyproject.toml` is interpreter-aware: `azure-functions>=1.21.0,<2.0.0` on Python < 3.13, and `azure-functions>=1.21.0,<3.0.0` diff --git a/README.zh-CN.md b/README.zh-CN.md index 634d32a6..a08a75e7 100644 --- a/README.zh-CN.md +++ b/README.zh-CN.md @@ -4,7 +4,7 @@ [![PyPI](https://img.shields.io/pypi/v/azure-functions-openapi.svg)](https://pypi.org/project/azure-functions-openapi/) [![Downloads](https://static.pepy.tech/badge/azure-functions-openapi/month)](https://pepy.tech/project/azure-functions-openapi) -[![Python Version](https://img.shields.io/badge/python-3.10%20%7C%203.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-openapi/) +[![Python Version](https://img.shields.io/badge/python-3.11%20%7C%203.12%20%7C%203.13%20%7C%203.14-blue)](https://pypi.org/project/azure-functions-openapi/) [![CI](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/ci-test.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/ci-test.yml) [![Release](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/publish-pypi.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/publish-pypi.yml) [![Security Scans](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/security.yml/badge.svg)](https://github.com/yeongseon/azure-functions-openapi-python/actions/workflows/security.yml) @@ -140,12 +140,12 @@ azure-functions-openapi 本包通过单一隔离适配器(`azure_functions_openapi.adapters`)从 `azure-functions` SDK 发现路由、方法和处理器。发现采用 **公共 API 优先** 策略:通过公共、幂等的 `FunctionBuilder.build()` 枚举,其余内容均通过公共 `Function` 访问器(`get_function_name` / `get_user_function` / `get_bindings` / `is_http_function`)读取。适配器绝不调用非幂等的 `FunctionApp.get_functions()`。枚举时没有公共替代方案的 **唯一** 非公开令牌 `app._function_builders` 仅驻留在适配器内部,并由强制守卫测试覆盖。我们在 CI 中使用明确的矩阵进行验证。背景请参阅 [问题 #258](https://github.com/yeongseon/azure-functions-openapi-python/issues/258) 和 [问题 #327](https://github.com/yeongseon/azure-functions-openapi-python/issues/327)。 -| `azure-functions` | Python 3.10 | Python 3.11 | Python 3.12 | Python 3.13 | Python 3.14 | -| ----------------- | :---------: | :---------: | :---------: | :---------: | :---------: | -| `1.21.0`(下限) | ✅ 已测试 | | | | | -| `1.24.0` | ✅ 已测试 | | | | | -| `latest 1.x` | ✅ 已测试 | ✅ 已测试 | ✅ 已测试 | | | -| `2.x` (`>=2,<3`) | | | | ✅ 已测试 | ✅ 已测试 | +| `azure-functions` | Python 3.11 | Python 3.12 | Python 3.13 | Python 3.14 | +| ----------------- | :---------: | :---------: | :---------: | :---------: | +| `1.21.0`(下限) | ✅ 已测试 | | | | +| `1.24.0` | ✅ 已测试 | | | | +| `latest 1.x` | ✅ 已测试 | ✅ 已测试 | | | +| `2.x` (`>=2,<3`) | | | ✅ 已测试 | ✅ 已测试 | `pyproject.toml` 中的版本锁定与解释器相关:在 Python < 3.13 上为 `azure-functions>=1.21.0,<2.0.0`,在 Python 3.13+ 上为 `azure-functions>=1.21.0,<3.0.0`(挡住未认证的 `azure-functions` 3.x)。下限为 `1.21.0` 是因为更早的版本会从 `FunctionBuilder.__call__` 返回 `None`(会破坏测试和 CLI 提取中对装饰处理器的直接调用)。之所以拆分,是因为 `azure-functions` 2.x 放弃了对 Python < 3.13 的支持,因此 2.x 系列仅在 Python 3.13+ 上可安装和提供。2.x 路径已通过 CI 中专用的基于 wheel 的兼容性矩阵(真实的 Python 3.13 和 3.14 解释器)以及真实 Azure 认证 —— 部署在 koreacentral Flex Consumption 计划上的 Python 3.13 Function App —— 得到验证。解除上限的相关工作请参阅 [问题 #528](https://github.com/yeongseon/azure-functions-openapi-python/issues/528) 和 [问题 #488](https://github.com/yeongseon/azure-functions-openapi-python/issues/488)。 diff --git a/SUPPORT.md b/SUPPORT.md index f3a81fcf..86c8bd14 100644 --- a/SUPPORT.md +++ b/SUPPORT.md @@ -2,7 +2,7 @@ ## Supported Environment -* Python: 3.10 and above +* Python: 3.11 and above * Runtime: Azure Functions (Python) ## Support Scope diff --git a/docs/assets/screenshots.yml b/docs/assets/screenshots.yml index b22f0918..e867f8fe 100644 --- a/docs/assets/screenshots.yml +++ b/docs/assets/screenshots.yml @@ -10,7 +10,7 @@ screenshots: source: inputs: - examples/report_jobs/function_app.py - hash: sha256:06a1bef69c8635a8825562e48302c694b7f9be4b037259e0d417a22e82f90d9d + hash: sha256:dcf095b8a97e9e01cf944cd9f3f60f4303056a52c30cc52f50ad523c890aa574 output: hash: sha256:aec622c63df0425f60a47b36551fc79853df805ff77732594340058119d78b72 - id: deployment_report_jobs_swagger_ui @@ -23,7 +23,7 @@ screenshots: source: inputs: - examples/report_jobs/function_app.py - hash: sha256:06a1bef69c8635a8825562e48302c694b7f9be4b037259e0d417a22e82f90d9d + hash: sha256:dcf095b8a97e9e01cf944cd9f3f60f4303056a52c30cc52f50ad523c890aa574 output: hash: sha256:579ee7357acd04644926b7861e7129775553df4ffb1b67b9149b46384d0b4296 - id: notification_request_swagger_ui @@ -36,7 +36,7 @@ screenshots: source: inputs: - examples/notification_request/function_app.py - hash: sha256:fb2c2957bb56f0d199a2ca17c24923bfa8cd3844c79968d7c46ca9fc99225fa7 + hash: sha256:805b60b099a34b4d097bea88768d9582957878ae9661ee85bec559357561296a output: hash: sha256:f8ef5429b57db07e9c222de5d978c114f76f345c84d19774ba2b8e74a016c522 - id: partner_import_bridge_swagger_ui @@ -49,7 +49,7 @@ screenshots: source: inputs: - examples/partner_import_bridge/function_app.py - hash: sha256:c8319c9917e1018b10e925c98a0943789cda4ba2bd577c4d1387388287aa10cf + hash: sha256:937af8dbe86f743008c734295b80e5ca9854f5c8c8cd19cb16bc78b7aa7ddee6 output: hash: sha256:9f9f204a5e5028e875e4446fdd738b83e04de7a7cb17b880d69a9ab4122d0986 - id: webhook_receiver_openapi_spec_preview @@ -62,7 +62,7 @@ screenshots: source: inputs: - examples/webhook_receiver/function_app.py - hash: sha256:e3b582c65610a9c235c8da3584ae2efd1848aee25d7056b6672742d7b1068615 + hash: sha256:826e24b8ac3f42e7d61e0a40c5b53d40f604460e298e2e34ee2b471d92a95106 output: hash: sha256:ecb270e3c9dd74fc55c99b67776e2c5b48e65f6be019cb6265715cd54cd03900 - id: webhook_receiver_openapi_swagger_ui_preview @@ -75,7 +75,7 @@ screenshots: source: inputs: - examples/webhook_receiver/function_app.py - hash: sha256:e3b582c65610a9c235c8da3584ae2efd1848aee25d7056b6672742d7b1068615 + hash: sha256:826e24b8ac3f42e7d61e0a40c5b53d40f604460e298e2e34ee2b471d92a95106 output: hash: sha256:42658bfeeffab7caa4815a4d1588361f39ae4975434a4214ef5af5d3a281a0c8 - id: webhook_receiver_swagger_ui @@ -88,7 +88,7 @@ screenshots: source: inputs: - examples/webhook_receiver/function_app.py - hash: sha256:e3b582c65610a9c235c8da3584ae2efd1848aee25d7056b6672742d7b1068615 + hash: sha256:826e24b8ac3f42e7d61e0a40c5b53d40f604460e298e2e34ee2b471d92a95106 output: hash: sha256:5bdb7901a647ce5e15fc9ad439742160c04a5c46f655c627106b5e2856a644f0 - id: e2e_app_swagger_ui diff --git a/docs/contributing.md b/docs/contributing.md index 48401aab..f6618045 100644 --- a/docs/contributing.md +++ b/docs/contributing.md @@ -83,7 +83,7 @@ Describe what changed and why. Reference related issues with `Fixes #N` or `Clos ### Review process - At least one approval is required before merging. -- CI must pass on all Python versions (3.10 -- 3.14). +- CI must pass on all Python versions (3.11 -- 3.14). - Merge with "Squash and merge" to keep the commit history clean. ## Code Style @@ -97,7 +97,7 @@ Describe what changed and why. Reference related issues with `Fixes #N` or `Clos ### Type annotations - All public functions must have complete type annotations. -- Use Python 3.10+ syntax (PEP 604 `X | Y`, PEP 585 `list[T]`). +- Use Python 3.11+ syntax. - `mypy` strict mode is enforced in CI. ### Naming conventions diff --git a/docs/deployment.md b/docs/deployment.md index faee469d..0ecd500f 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -45,7 +45,7 @@ In this guide, you deploy example apps that provide: | Azure account | [portal.azure.com](https://portal.azure.com) | [Create free account](https://azure.microsoft.com/free/) | | Azure CLI | `az --version` | [Install Azure CLI](https://learn.microsoft.com/cli/azure/install-azure-cli) | | Azure Functions Core Tools v4 | `func --version` | [Install Core Tools](https://learn.microsoft.com/azure/azure-functions/functions-run-local#install-the-azure-functions-core-tools) | -| Python 3.10-3.13 | `python3 --version` | [python.org](https://www.python.org/downloads/) | +| Python 3.11-3.14 | `python3 --version` | [python.org](https://www.python.org/downloads/) | | Package examples available | `ls examples` | Clone this repo again if missing | | Local example works | `func start` + local `curl` | See [README](https://github.com/yeongseon/azure-functions-openapi-python/blob/main/README.md) | diff --git a/docs/development.md b/docs/development.md index fcdae1dc..2869b289 100644 --- a/docs/development.md +++ b/docs/development.md @@ -6,7 +6,7 @@ This guide covers how to set up a local development environment, run tests, and ## Prerequisites -- **Python 3.10+** installed on your system +- **Python 3.11+** installed on your system - **Git** for version control - **Hatch** as the build and environment manager (installed via `pip install hatch`) - **Make** for running the provided Makefile targets @@ -144,7 +144,7 @@ Use these as the **golden commands** for local validation and CI parity. Prefer ## Tips -- Ensure you're using Python 3.10+. +- Ensure you're using Python 3.11+. - Use `make check-all` before committing to validate your changes. - Prefer `make` commands to ensure consistent dev experience across platforms. - Follow [Conventional Commits](https://www.conventionalcommits.org/) for proper changelog generation. diff --git a/docs/getting-started.md b/docs/getting-started.md index d61a2bb9..db1e0454 100644 --- a/docs/getting-started.md +++ b/docs/getting-started.md @@ -10,7 +10,7 @@ This quickstart takes you from zero to a working Azure Functions API with: Before you begin, make sure you have: -- Python 3.10+ +- Python 3.11+ - Azure Functions Core Tools v4 - An Azure Functions Python v2 app (`func.FunctionApp`) diff --git a/docs/index.md b/docs/index.md index 6f8de54d..c5f95c52 100644 --- a/docs/index.md +++ b/docs/index.md @@ -128,7 +128,7 @@ Generate programmatically with: | Component | Supported | | --- | --- | -| Python | 3.10 to 3.14 | +| Python | 3.11 to 3.14 | | Azure Functions model | Python v2 (`func.FunctionApp`) | | OpenAPI versions | 3.0.0, 3.1.0, and 3.2.0 | | Pydantic | v2 (≥2.0) | diff --git a/docs/installation.md b/docs/installation.md index f67b95c5..2ab670b8 100644 --- a/docs/installation.md +++ b/docs/installation.md @@ -4,7 +4,7 @@ | Dependency | Version | | --- | --- | -| Python | 3.10 -- 3.14 | +| Python | 3.11 -- 3.14 | | Azure Functions Core Tools | Latest stable | | Azure Functions programming model | v2 (`func.FunctionApp` with decorators) | diff --git a/docs/testing.md b/docs/testing.md index b27a3023..3597cc1c 100644 --- a/docs/testing.md +++ b/docs/testing.md @@ -51,11 +51,11 @@ hatch run pytest -k "security" -v ## Coverage Policy -- CI runs tests on Python **3.10 through 3.14**. +- CI runs tests on Python **3.11 through 3.14**. - All matrix versions are **required** -- no allow-fail entries. - Python 3.14 is treated as stable support, not preview. - Coverage report is generated as `coverage.xml`. -- Codecov upload runs in CI for the Python 3.10 job. +- Codecov upload runs in CI for the Python 3.11 job. ### Coverage thresholds @@ -233,7 +233,7 @@ The CI workflow runs: 4. `make security` -- Bandit security scan All four steps must pass for a PR to be mergeable. The matrix covers -Python 3.10, 3.11, 3.12, 3.13, and 3.14. +Python 3.11, 3.12, 3.13, and 3.14. ## Real Azure E2E Tests diff --git a/examples/notification_request/function_app.py b/examples/notification_request/function_app.py index 054154a5..f21e8d2d 100644 --- a/examples/notification_request/function_app.py +++ b/examples/notification_request/function_app.py @@ -9,7 +9,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import logging import uuid @@ -98,7 +98,7 @@ def send_notification( entry = { "notification_id": notification_id, "status": "queued", - "queued_at": datetime.now(timezone.utc).isoformat(), + "queued_at": datetime.now(UTC).isoformat(), } _notifications[notification_id] = entry diff --git a/examples/partner_import_bridge/function_app.py b/examples/partner_import_bridge/function_app.py index 725d03a7..c376bcf2 100644 --- a/examples/partner_import_bridge/function_app.py +++ b/examples/partner_import_bridge/function_app.py @@ -10,7 +10,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import json import logging from typing import Any @@ -103,7 +103,7 @@ def import_partners(req: func.HttpRequest, body: ImportBatchRequest) -> ImportBa "imported": imported, "skipped": skipped, "status": "dry_run" if body.dry_run else "completed", - "created_at": datetime.now(timezone.utc).isoformat(), + "created_at": datetime.now(UTC).isoformat(), } _import_history.append(entry) diff --git a/examples/report_jobs/function_app.py b/examples/report_jobs/function_app.py index abcd1c8a..8217fb2f 100644 --- a/examples/report_jobs/function_app.py +++ b/examples/report_jobs/function_app.py @@ -11,8 +11,8 @@ from __future__ import annotations -from datetime import datetime, timezone -from enum import Enum +from datetime import UTC, datetime +from enum import StrEnum import json import logging from typing import Any @@ -39,7 +39,7 @@ # --------------------------------------------------------------------------- -class ReportFormat(str, Enum): +class ReportFormat(StrEnum): csv = "csv" pdf = "pdf" xlsx = "xlsx" @@ -149,7 +149,7 @@ def submit_report(req: func.HttpRequest) -> func.HttpResponse: job = { "job_id": job_id, "status": "queued", - "created_at": datetime.now(timezone.utc).isoformat(), + "created_at": datetime.now(UTC).isoformat(), "request": body, "progress_pct": 0, "download_url": None, diff --git a/examples/webhook_receiver/function_app.py b/examples/webhook_receiver/function_app.py index fb3c7117..87f70248 100644 --- a/examples/webhook_receiver/function_app.py +++ b/examples/webhook_receiver/function_app.py @@ -9,7 +9,7 @@ from __future__ import annotations -from datetime import datetime, timezone +from datetime import UTC, datetime import hashlib import hmac import json @@ -129,7 +129,7 @@ def receive_order_webhook(req: func.HttpRequest) -> func.HttpResponse: mimetype="application/json", status_code=401, ) - age = (datetime.now(timezone.utc) - ts).total_seconds() + age = (datetime.now(UTC) - ts).total_seconds() if abs(age) > _MAX_WEBHOOK_AGE_SECONDS: logger.warning("Webhook timestamp too old: %s (age=%.0fs)", timestamp, age) return func.HttpResponse( @@ -186,7 +186,7 @@ def receive_order_webhook(req: func.HttpRequest) -> func.HttpResponse: entry = { "delivery_id": f"dlv_{uuid.uuid4().hex[:12]}", "status": "accepted", - "received_at": datetime.now(timezone.utc).isoformat(), + "received_at": datetime.now(UTC).isoformat(), } _recent_deliveries.append(entry) diff --git a/infra/main.bicep b/infra/main.bicep index 0ad44029..51325fae 100644 --- a/infra/main.bicep +++ b/infra/main.bicep @@ -1,7 +1,7 @@ // infra/main.bicep // Minimal Azure resources for e2e testing. // Creates: Storage Account + Function App on a Flex Consumption (FC1) plan, -// Linux/Python, version selectable via the pythonVersion parameter (default 3.11). +// Linux/Python, version selectable via the pythonVersion parameter (default 3.12). // Optionally creates Application Insights (enableAppInsights=true). // // Flex Consumption (not the classic Y1 Consumption plan) is required because @@ -29,7 +29,7 @@ param enableAppInsights bool = false param appInsightsName string = '${functionAppName}-ai' @description('Python runtime version for the Flex Consumption Function App (e.g. 3.11, 3.13).') -param pythonVersion string = '3.11' +param pythonVersion string = '3.12' @description('Per-instance memory (MB) for the Flex Consumption app.') param instanceMemoryMB int = 2048 diff --git a/llms-full.txt b/llms-full.txt index 5f405af4..b01528cc 100644 --- a/llms-full.txt +++ b/llms-full.txt @@ -6,7 +6,7 @@ - PyPI: `pip install azure-functions-openapi` - Version: 0.27.0 -- Python: >=3.10, <3.15 +- Python: >=3.11, <3.15 - License: MIT - Docs: https://yeongseon.dev/azure-functions-python/openapi/ - Repository: https://github.com/yeongseon/azure-functions-openapi-python diff --git a/llms.txt b/llms.txt index cdda6502..09655a85 100644 --- a/llms.txt +++ b/llms.txt @@ -6,7 +6,7 @@ - PyPI: `pip install azure-functions-openapi` - Version: 0.27.0 -- Python: >=3.10, <3.15 +- Python: >=3.11, <3.15 - License: MIT - Docs: https://yeongseon.dev/azure-functions-python/openapi/ - Repository: https://github.com/yeongseon/azure-functions-openapi-python diff --git a/pyproject.toml b/pyproject.toml index b3c14fad..74265f18 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,14 +5,13 @@ keywords = ["azure-functions", "azure", "python", "v2", "openapi", "swagger", "r authors = [{ name = "Yeongseon Choe", email = "yeongseon.choe@gmail.com" }] license = { text = "MIT" } readme = "README.md" -requires-python = ">=3.10,<3.15" +requires-python = ">=3.11,<3.15" dynamic = ["version"] classifiers = [ "Development Status :: 4 - Beta", "Intended Audience :: Developers", "License :: OSI Approved :: MIT License", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", @@ -98,7 +97,7 @@ url = "https://upload.pypi.org/legacy/" url = "https://test.pypi.org/legacy/" [tool.hatch.envs.default] -python = "3.10" +python = "3.11" features = ["dev", "docs"] [tool.hatch.envs.default.scripts] @@ -117,12 +116,12 @@ security = "python -m bandit -r src" [tool.ruff] line-length = 100 -target-version = "py310" +target-version = "py311" [tool.ruff.lint] # C4 + UP keep the codebase on modern syntax (PEP 585/604 generics and -# unions, comprehension forms) instead of the pre-3.10 `typing.List` / -# `Optional[...]` spellings. target-version = "py310" bounds every fix to +# unions, comprehension forms) instead of the legacy `typing.List` / +# `Optional[...]` spellings. target-version = "py311" bounds every fix to # syntax the declared floor supports. select = ["C4", "E", "F", "I", "UP"] fixable = ["ALL"] @@ -133,7 +132,7 @@ force-sort-within-sections = true section-order = ["future", "standard-library", "third-party", "first-party", "local-folder"] [tool.mypy] -python_version = "3.10" +python_version = "3.11" strict = true ignore_missing_imports = true show_error_codes = true diff --git a/scripts/check_screenshots.py b/scripts/check_screenshots.py index b5afd1bc..da3fb5c2 100644 --- a/scripts/check_screenshots.py +++ b/scripts/check_screenshots.py @@ -45,7 +45,7 @@ # artifact or its manifest. Kept deliberately narrow to avoid false positives on # ordinary docs content; reviewed, known-safe matches can be exempted via the # manifest-level ``secret_scan_allow`` list (substring match). -_SECRET_PATTERNS: tuple[tuple[str, "re.Pattern[str]"], ...] = ( +_SECRET_PATTERNS: tuple[tuple[str, re.Pattern[str]], ...] = ( ( "azure subscription id", re.compile( @@ -102,7 +102,8 @@ def _combined_source_hash(inputs: list[str]) -> str: def _image_hash(rel: str) -> str: - return "sha256:" + hashlib.sha256((REPO_ROOT / rel).read_bytes()).hexdigest() + with (REPO_ROOT / rel).open("rb") as image: + return "sha256:" + hashlib.file_digest(image, "sha256").hexdigest() def _load_manifest(path: Path) -> dict[str, Any]: diff --git a/src/azure_functions_openapi/__init__.py b/src/azure_functions_openapi/__init__.py index 6f81b067..2fb24e99 100644 --- a/src/azure_functions_openapi/__init__.py +++ b/src/azure_functions_openapi/__init__.py @@ -1,7 +1,4 @@ # src/azure_functions_openapi/__init__.py -import sys -import warnings - from azure_functions_openapi._warnings import SpecWarning, WarningCode import azure_functions_openapi.bridge as _bridge from azure_functions_openapi.decorator import ( @@ -51,13 +48,3 @@ "scan_endpoint_metadata", "scan_validation_metadata", ] - - -if sys.version_info < (3, 11): - warnings.warn( - "azure-functions-openapi will drop support for Python 3.10 in its next minor release. " - "Python 3.10 reaches end of life in October 2026; upgrade to Python 3.11 " - "or newer to keep receiving updates.", - FutureWarning, - stacklevel=2, - ) diff --git a/src/azure_functions_openapi/_warnings.py b/src/azure_functions_openapi/_warnings.py index 8aedeb53..5415e59d 100644 --- a/src/azure_functions_openapi/_warnings.py +++ b/src/azure_functions_openapi/_warnings.py @@ -16,14 +16,13 @@ from __future__ import annotations from dataclasses import dataclass -from enum import Enum +from enum import StrEnum -class WarningCode(str, Enum): +class WarningCode(StrEnum): """Stable identifiers for structured spec-generation warnings. - Inherits from ``str`` (rather than :class:`enum.StrEnum`, which is 3.11+) so - the codes serialise as plain strings on Python 3.10+. + Codes serialise as plain strings on Python 3.11+. """ VERSION_SKEW = "version-skew" diff --git a/tests/test_hello_openapi_function_app.py b/tests/test_hello_openapi_function_app.py index 0a7edd9a..cb3161fb 100644 --- a/tests/test_hello_openapi_function_app.py +++ b/tests/test_hello_openapi_function_app.py @@ -1,7 +1,7 @@ # tests/test_webhook_receiver_example.py # (file kept as test_hello_openapi_function_app.py so CI mapping stays stable) -from datetime import datetime, timedelta, timezone +from datetime import UTC, datetime, timedelta import hashlib import hmac import importlib @@ -115,7 +115,7 @@ def test_receive_webhook_signature_valid() -> None: } ).encode("utf-8") secret = "test-secret" - timestamp = datetime.now(timezone.utc).isoformat() + timestamp = datetime.now(UTC).isoformat() sig = _make_signature(payload, timestamp, secret) req = func.HttpRequest( @@ -138,7 +138,7 @@ def test_receive_webhook_signature_valid() -> None: def test_receive_webhook_signature_invalid() -> None: fa = _load_example_module() - timestamp = datetime.now(timezone.utc).isoformat() + timestamp = datetime.now(UTC).isoformat() req = func.HttpRequest( method="POST", url="/api/webhooks/orders", @@ -194,7 +194,7 @@ def test_receive_webhook_stale_timestamp() -> None: """Webhooks older than 5 minutes are rejected.""" fa = _load_example_module() secret = "test-secret" - old_timestamp = (datetime.now(timezone.utc) - timedelta(minutes=10)).isoformat() + old_timestamp = (datetime.now(UTC) - timedelta(minutes=10)).isoformat() payload = json.dumps( { "event_type": "order.completed", diff --git a/tests/test_python_310_deprecation.py b/tests/test_python_310_deprecation.py deleted file mode 100644 index 3b725203..00000000 --- a/tests/test_python_310_deprecation.py +++ /dev/null @@ -1,24 +0,0 @@ -"""Python 3.10 is deprecated ahead of its removal in the next minor release.""" - -from __future__ import annotations - -import importlib -import sys -import warnings - -import pytest - -import azure_functions_openapi - - -def test_warns_on_python_310(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(sys, "version_info", (3, 10, 0, "final", 0)) - with pytest.warns(FutureWarning, match="drop support for Python 3.10"): - importlib.reload(azure_functions_openapi) - - -def test_silent_on_python_311(monkeypatch: pytest.MonkeyPatch) -> None: - monkeypatch.setattr(sys, "version_info", (3, 11, 0, "final", 0)) - with warnings.catch_warnings(): - warnings.simplefilter("error", FutureWarning) - importlib.reload(azure_functions_openapi) diff --git a/tests/test_python_version_metadata.py b/tests/test_python_version_metadata.py new file mode 100644 index 00000000..02067caf --- /dev/null +++ b/tests/test_python_version_metadata.py @@ -0,0 +1,104 @@ +from __future__ import annotations + +from email.parser import BytesParser +from pathlib import Path +import subprocess +import sys +import tarfile +import tomllib +from zipfile import ZipFile + +from packaging.specifiers import SpecifierSet +import pytest + +PROJECT_ROOT = Path(__file__).resolve().parents[1] +SUPPORTED_PYTHON = SpecifierSet(">=3.11,<3.15") + + +@pytest.fixture(scope="module") +def built_metadata(tmp_path_factory: pytest.TempPathFactory) -> tuple[str, str]: + # Given isolated output for both distribution formats + output = tmp_path_factory.mktemp("dist") + + # When the project builds its wheel and source distribution + subprocess.run( + [sys.executable, "-m", "build", "--outdir", str(output)], + cwd=PROJECT_ROOT, + check=True, + ) + + # Then expose the metadata embedded in each artifact + wheel = next(output.glob("*.whl")) + with ZipFile(wheel) as archive: + wheel_metadata_name = next( + name for name in archive.namelist() if name.endswith(".dist-info/METADATA") + ) + wheel_metadata = archive.read(wheel_metadata_name).decode() + sdist = next(output.glob("*.tar.gz")) + with tarfile.open(sdist) as archive: + pkg_info = next( + member for member in archive.getmembers() if member.name.endswith("/PKG-INFO") + ) + extracted = archive.extractfile(pkg_info) + assert extracted is not None + sdist_metadata = extracted.read().decode() + return wheel_metadata, sdist_metadata + + +def test_requires_python_declares_supported_range() -> None: + # Given the source package metadata + project = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))[ + "project" + ] + + # When the declared Python range is parsed semantically + declared = SpecifierSet(project["requires-python"]) + + # Then the complete normalized range is exactly the supported range + assert declared == SUPPORTED_PYTHON + + +@pytest.mark.parametrize( + "mutant", + [">=3.11,<3.14.1", ">=3.11,<3.15,!=3.12.5"], + ids=["narrow-upper-bound", "excluded-patch"], +) +def test_supported_range_rejects_semantically_different_mutants(mutant: str) -> None: + # Given a range that agrees at each supported minor's .0 release + mutated = SpecifierSet(mutant) + + # When the complete normalized range is compared + # Then hidden boundary and interior exclusions are rejected + assert mutated != SUPPORTED_PYTHON + + +def test_classifiers_match_supported_python_minors() -> None: + # Given the source package metadata + project = tomllib.loads((PROJECT_ROOT / "pyproject.toml").read_text(encoding="utf-8"))[ + "project" + ] + + # When version-specific Python classifiers are selected + prefix = "Programming Language :: Python :: 3." + classified_minors = { + classifier.removeprefix(prefix) + for classifier in project["classifiers"] + if classifier.startswith(prefix) + } + + # Then every and only supported minor version is advertised + assert classified_minors == {"11", "12", "13", "14"} + + +@pytest.mark.parametrize("artifact_index", [0, 1], ids=["wheel", "sdist"]) +def test_built_artifact_requires_python_matches_supported_range( + built_metadata: tuple[str, str], artifact_index: int +) -> None: + # Given metadata from a built wheel or source distribution + metadata = BytesParser().parsebytes(built_metadata[artifact_index].encode()) + + # When its Python requirement is parsed semantically + declared = SpecifierSet(metadata["Requires-Python"]) + + # Then the complete normalized range is exactly the supported range + assert declared == SUPPORTED_PYTHON diff --git a/tests/test_sdk_compat_adapter.py b/tests/test_sdk_compat_adapter.py index fb3d1d3c..fbd049bc 100644 --- a/tests/test_sdk_compat_adapter.py +++ b/tests/test_sdk_compat_adapter.py @@ -4,7 +4,7 @@ through :mod:`azure_functions_openapi.adapters.azure_functions`. This module pins that contract against the *installed* SDK so the CI matrix proves the package works on every supported line: ``azure-functions`` 1.21 → latest 1.x -on Python 3.10–3.12, and the 2.x line on Python 3.13+ (the wheel-based compat +on Python 3.11–3.12, and the 2.x line on Python 3.13+ (the wheel-based compat matrix in ci-test.yml). Verified support matrix (enforced by CI; see the interpreter-aware @@ -12,9 +12,9 @@ azure-functions | Python --------------- | ---------------------------- - 1.21.0 (floor) | 3.10 - 1.24.0 | 3.10 - latest (<2.0) | 3.10, 3.11, 3.12 +1.21.0 (floor) | 3.11 +1.24.0 | 3.11 +latest (<2.0) | 3.11, 3.12 2.x (>=2,<3) | 3.13, 3.14 Three guarantees are asserted here: diff --git a/tests/test_spec_warnings.py b/tests/test_spec_warnings.py index 0b1c2183..c63a67a1 100644 --- a/tests/test_spec_warnings.py +++ b/tests/test_spec_warnings.py @@ -128,9 +128,18 @@ def _isolate_registry() -> Any: class TestSpecWarning: - def test_warning_code_serialises_as_plain_string(self) -> None: + def test_warning_code_value_is_stable(self) -> None: assert WarningCode.VERSION_SKEW.value == "version-skew" + def test_warning_code_string_forms_are_stable(self) -> None: + assert str(WarningCode.VERSION_SKEW) == "version-skew" + assert f"{WarningCode.VERSION_SKEW}" == "version-skew" + + def test_warning_code_remains_string_compatible(self) -> None: + code: str = WarningCode.VERSION_SKEW + assert code == "version-skew" + assert json.dumps({"code": WarningCode.VERSION_SKEW}) == '{"code": "version-skew"}' + def test_to_dict_is_json_serialisable(self) -> None: warning = SpecWarning( code=WarningCode.VERSION_SKEW, diff --git a/tests/test_typed_parameters.py b/tests/test_typed_parameters.py index 66e385cc..58d83688 100644 --- a/tests/test_typed_parameters.py +++ b/tests/test_typed_parameters.py @@ -1,7 +1,7 @@ from __future__ import annotations from datetime import datetime -from enum import Enum +from enum import StrEnum import json from typing import Literal @@ -20,7 +20,7 @@ from azure_functions_openapi.spec import generate_openapi_spec -class Color(str, Enum): +class Color(StrEnum): red = "red" blue = "blue"