diff --git a/.github/labels.yml b/.github/labels.yml index 83989042c..bc189cd77 100644 --- a/.github/labels.yml +++ b/.github/labels.yml @@ -26,6 +26,15 @@ - name: breaking-change description: for breaking changes in the changelog. color: ff0000 +- name: security + description: for security fixes and improvements in the changelog. + color: AA1111 +- name: community-contribution + description: contributions from the community. + color: 22ee47 +- name: hotfix + description: urgent fixes for production issues. + color: ff0000 - name: ignore-for-release description: PRs you do not want to render in the changelog color: 7b8eac diff --git a/.github/release.yml b/.github/release.yml index a2318fa64..5bf07fc08 100644 --- a/.github/release.yml +++ b/.github/release.yml @@ -9,6 +9,9 @@ changelog: - title: ⚠️ Breaking Change labels: - breaking-change + - title: 🔒 Security Fixes + labels: + - security - title: 🐛 Bug Fixes labels: - bugfix diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 55fb91289..d65acf9e6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: uses: actions/checkout@v7 - name: setup python 3 - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.x' @@ -62,7 +62,7 @@ jobs: python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] steps: - uses: actions/checkout@v7 - - uses: actions/setup-python@v6 + - uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Run tests diff --git a/.github/workflows/e2e-test-pr.yml b/.github/workflows/e2e-test-pr.yml index f470c3a6e..d89cda3b1 100644 --- a/.github/workflows/e2e-test-pr.yml +++ b/.github/workflows/e2e-test-pr.yml @@ -96,7 +96,7 @@ jobs: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.x' @@ -112,9 +112,13 @@ jobs: run: | timestamp=$(date +'%Y%m%d%H%M') report_filename="${timestamp}_sdk_test_report.xml" - make test-int RUN_DB_FORK_TESTS=${{ github.event.inputs.run_db_fork_tests }} RUN_DB_TESTS=${{ github.event.inputs.run_db_tests }} RUN_ACLP_LOGS_STREAM_TESTS=${{ github.event.inputs.run_aclp_logs_stream_tests }} TEST_ARGS="--junitxml=${report_filename}" TEST_SUITE="${{ github.event.inputs.test_suite }}" + make test-int RUN_DB_FORK_TESTS="$RUN_DB_FORK_TESTS" RUN_DB_TESTS="$RUN_DB_TESTS" RUN_ACLP_LOGS_STREAM_TESTS="$RUN_ACLP_LOGS_STREAM_TESTS" TEST_ARGS="--junitxml=${report_filename}" TEST_SUITE="$TEST_SUITE" env: LINODE_TOKEN: ${{ secrets.LINODE_TOKEN }} + RUN_DB_FORK_TESTS: ${{ github.event.inputs.run_db_fork_tests }} + RUN_DB_TESTS: ${{ github.event.inputs.run_db_tests }} + RUN_ACLP_LOGS_STREAM_TESTS: ${{ github.event.inputs.run_aclp_logs_stream_tests }} + TEST_SUITE: ${{ github.event.inputs.test_suite }} - name: Upload test results if: always() && github.repository == 'linode/linode_api4-python' && (github.event_name == 'pull_request' || (github.event_name == 'workflow_dispatch' && inputs.test_report_upload == 'true')) @@ -192,7 +196,7 @@ jobs: steps: - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.x' diff --git a/.github/workflows/e2e-test.yml b/.github/workflows/e2e-test.yml index aea007e54..46742cbae 100644 --- a/.github/workflows/e2e-test.yml +++ b/.github/workflows/e2e-test.yml @@ -87,7 +87,7 @@ jobs: submodules: 'recursive' - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ inputs.run-eol-python-version == 'true' && env.EOL_PYTHON_VERSION || inputs.python-version || env.DEFAULT_PYTHON_VERSION }} @@ -107,9 +107,13 @@ jobs: run: | timestamp=$(date +'%Y%m%d%H%M') report_filename="${timestamp}_sdk_test_report.xml" - make test-int RUN_DB_FORK_TESTS=${{ github.event.inputs.run_db_fork_tests }} RUN_DB_TESTS=${{ github.event.inputs.run_db_tests }} RUN_ACLP_LOGS_STREAM_TESTS=${{ github.event.inputs.run_aclp_logs_stream_tests }} TEST_SUITE="${{ github.event.inputs.test_suite }}" TEST_ARGS="--junitxml=${report_filename}" + make test-int RUN_DB_FORK_TESTS="$RUN_DB_FORK_TESTS" RUN_DB_TESTS="$RUN_DB_TESTS" RUN_ACLP_LOGS_STREAM_TESTS="$RUN_ACLP_LOGS_STREAM_TESTS" TEST_SUITE="$TEST_SUITE" TEST_ARGS="--junitxml=${report_filename}" env: LINODE_TOKEN: ${{ env.LINODE_TOKEN }} + RUN_DB_FORK_TESTS: ${{ github.event.inputs.run_db_fork_tests }} + RUN_DB_TESTS: ${{ github.event.inputs.run_db_tests }} + RUN_ACLP_LOGS_STREAM_TESTS: ${{ github.event.inputs.run_aclp_logs_stream_tests }} + TEST_SUITE: ${{ github.event.inputs.test_suite }} - name: Upload Test Report as Artifact if: always() @@ -157,7 +161,7 @@ jobs: steps: - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.x' @@ -206,7 +210,7 @@ jobs: name: test-report-file - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.x' @@ -249,7 +253,7 @@ jobs: steps: - name: Notify Slack id: main_message - uses: slackapi/slack-github-action@v3 + uses: slackapi/slack-github-action@v4 with: method: chat.postMessage token: ${{ secrets.SLACK_BOT_TOKEN }} @@ -281,7 +285,7 @@ jobs: - name: Test summary thread if: success() - uses: slackapi/slack-github-action@v3 + uses: slackapi/slack-github-action@v4 with: method: chat.postMessage token: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/nightly-smoke-tests.yml b/.github/workflows/nightly-smoke-tests.yml index 42d153d89..e36a0b8af 100644 --- a/.github/workflows/nightly-smoke-tests.yml +++ b/.github/workflows/nightly-smoke-tests.yml @@ -24,7 +24,7 @@ jobs: ref: dev - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.x' @@ -45,7 +45,7 @@ jobs: - name: Notify Slack if: always() && github.repository == 'linode/linode_api4-python' - uses: slackapi/slack-github-action@v3 + uses: slackapi/slack-github-action@v4 with: method: chat.postMessage token: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/.github/workflows/publish-pypi.yaml b/.github/workflows/publish-pypi.yaml index 089b665ef..fb69f9f7b 100644 --- a/.github/workflows/publish-pypi.yaml +++ b/.github/workflows/publish-pypi.yaml @@ -15,7 +15,7 @@ jobs: uses: actions/checkout@v7 - name: Setup Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.x' @@ -28,4 +28,4 @@ jobs: LINODE_SDK_VERSION: ${{ github.event.release.tag_name }} - name: Publish the release artifacts to PyPI - uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # pin@release/v1.14.0 + uses: pypa/gh-action-pypi-publish@ba38be9e461d3875417946c167d0b5f3d385a247 # pin@release/v1.14.1 diff --git a/.github/workflows/release-cross-repo-test.yml b/.github/workflows/release-cross-repo-test.yml index 9a3470e58..be2eaba68 100644 --- a/.github/workflows/release-cross-repo-test.yml +++ b/.github/workflows/release-cross-repo-test.yml @@ -25,7 +25,7 @@ jobs: run: sudo apt-get install -y build-essential - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: '3.10' diff --git a/.github/workflows/release-notify-slack.yml b/.github/workflows/release-notify-slack.yml index aa21d80e5..e6ba97c1b 100644 --- a/.github/workflows/release-notify-slack.yml +++ b/.github/workflows/release-notify-slack.yml @@ -11,7 +11,7 @@ jobs: steps: - name: Notify Slack - Main Message id: main_message - uses: slackapi/slack-github-action@v3 + uses: slackapi/slack-github-action@v4 with: method: chat.postMessage token: ${{ secrets.SLACK_BOT_TOKEN }} diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..8cc3170c5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,71 @@ +# Agent Guide + +## Source of Truth + +- This is the official synchronous Python SDK for the Linode API. Supported + Python versions come from [pyproject.toml](pyproject.toml) (`>=3.10`); do not + infer support from the older environment names still present in [tox.ini](tox.ini). +- Make changes only in canonical source trees such as `linode_api4/`, `test/`, + `docs/`, `examples/`, and `.github/`. Do not edit generated copies or output in + `build/`, `dist/`, `docs/build/`, `docs/_build/`, or `linode_api4.egg-info/`. +- `linode_api4/version.py` is generated by `make create-version` and `make build`. + Do not hand-edit it for ordinary feature work. +- Use [README.rst](README.rst) for setup and test details and + [CONTRIBUTING.md](CONTRIBUTING.md) for contribution policy. Keep this file + limited to guidance that is easy to miss while navigating the repository. + +## Architecture and Implementation + +- `linode_api4/linode_client.py` owns HTTP behavior and wires API groups onto + `LinodeClient`; `linode_api4/groups/` exposes operations; `linode_api4/objects/` + defines resource models; `linode_api4/paginated_list.py` handles collection + pagination. +- Follow the nearest existing group method, model, and unit test before adding a + new pattern. Collection methods normally use `client._get_and_filter(...)`; + create/update/delete methods use the client's request helpers and return model + objects where appropriate. +- Models are lazy-loaded, and reading an unknown or stale property may issue an + API request. Read [docs/guides/core_concepts.rst](docs/guides/core_concepts.rst) + before changing model properties, relationships, filtering, or pagination. +- New public models must be exported from `linode_api4/objects/__init__.py`. New + groups must be exported from `linode_api4/groups/__init__.py` and wired into + `LinodeClient`. Preserve public API compatibility unless the task explicitly + requires a breaking change. +- Public behavior belongs in Sphinx-compatible docstrings and, when needed, the + source files under `docs/`. Never edit rendered documentation. + +## Tests + +- Start with the narrowest relevant unit test, for example + `python -m pytest test/unit/objects/linode_test.py -k test_name`. Run + `make test-unit` for the full mocked unit suite. +- Unit tests normally extend `test.unit.base.ClientBaseCase`. GET requests are + resolved from JSON under `test/fixtures/`; use `mock_post`, `mock_put`, and + `mock_delete` for other verbs and assert the captured URL, body, or headers. +- Fixture names encode endpoint paths: a single underscore becomes `/`, while a + doubled underscore becomes a literal `_`. Paginated fixtures containing + `results` also provide per-ID responses automatically when items include an + `id` field. See + `test/unit/fixtures.py` before inventing custom request mocking. +- `make test-int` and `make test-smoke` call the live Linode API, require + `LINODE_TOKEN`, and may create or delete real resources. Run them only when the + change requires live validation and the environment is intentionally configured. + +## Formatting and Validation + +- Install development dependencies with `make dev-install`. +- Formatting is Black + isort + autoflake with settings in [pyproject.toml](pyproject.toml). + `make format` rewrites all of `linode_api4/` and `test/`, so review the worktree + before and after using it for a focused change. +- `make lint` is the broad CI-equivalent check: it builds distributions, checks + formatting/imports, runs pylint, and validates package metadata. Prefer focused + tests first because this target regenerates build artifacts and version metadata. + +## Pull Requests + +- Target the `dev` branch unless the task specifies otherwise. +- PR titles must use `TPT-1234: Description`; CI exempts only the labels documented + in [.github/workflows/ci.yml](.github/workflows/ci.yml). +- Keep changes focused, add tests for behavior changes, and use + [.github/pull_request_template.md](.github/pull_request_template.md) when drafting + the PR description. diff --git a/test/integration/conftest.py b/test/integration/conftest.py index a210fc20f..d81360b97 100644 --- a/test/integration/conftest.py +++ b/test/integration/conftest.py @@ -286,6 +286,7 @@ def test_linode_client(): token, base_url=api_url, ca_path=api_ca_file, + retry_statuses=[504], ) return client diff --git a/test/integration/models/linode/test_linode.py b/test/integration/models/linode/test_linode.py index 1ef129bf1..55ae68880 100644 --- a/test/integration/models/linode/test_linode.py +++ b/test/integration/models/linode/test_linode.py @@ -23,6 +23,7 @@ Type, ) from linode_api4.objects.linode import InstanceDiskEncryptionType, MigrationType +from linode_api4.objects.region import RegionAvailabilityEntry @pytest.fixture(scope="session") @@ -119,12 +120,23 @@ def linode_and_vpc_for_legacy_interface_tests_offline( @pytest.fixture(scope="session") def linode_for_vpu_tests(test_linode_client, e2e_test_firewall): client = test_linode_client - region = "us-lax" + vpu_type = "g1-accelerated-netint-vpu-t1u1-s" + + availability = client.regions.availability( + RegionAvailabilityEntry.filters.plan == vpu_type + ) + + region = next( + (entry.region for entry in availability if entry.available), None + ) + + if region is None: + pytest.skip("No VPU capacity is currently available") label = get_test_label(length=8) linode_instance = client.linode.instance_create( - "g1-accelerated-netint-vpu-t1u1-s", + vpu_type, region, image="linode/debian12", label=label,