diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 306ce479d..57555d57a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -78,7 +78,7 @@ jobs: - name: "Check out repository code" uses: "actions/checkout@v7" - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.12" - name: Install UV @@ -141,7 +141,7 @@ jobs: - name: "Check out repository code" uses: "actions/checkout@v7" - name: "Set up Python" - uses: "actions/setup-python@v6" + uses: "actions/setup-python@v7" with: python-version: "3.12" - name: "Install uv" @@ -169,7 +169,7 @@ jobs: with: submodules: true - name: Install NodeJS - uses: actions/setup-node@v5 + uses: actions/setup-node@v7 with: node-version: 24 cache: "npm" @@ -177,7 +177,7 @@ jobs: - name: "Install dependencies" run: npm install - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.12" - name: Install UV @@ -204,7 +204,7 @@ jobs: with: submodules: true - name: Install NodeJS - uses: actions/setup-node@v5 + uses: actions/setup-node@v7 with: node-version: 24 cache: "npm" @@ -213,7 +213,7 @@ jobs: run: npm install working-directory: ./docs - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.12" - name: Install UV @@ -277,7 +277,7 @@ jobs: - name: "Check out repository code" uses: "actions/checkout@v7" - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: ${{ matrix.python-version }} - name: Install UV @@ -325,7 +325,7 @@ jobs: - name: "Check out repository code" uses: "actions/checkout@v7" - name: Set up Python - uses: actions/setup-python@v6 + uses: actions/setup-python@v7 with: python-version: "3.12" - name: "Set environment variables" @@ -376,7 +376,7 @@ jobs: # submodules: true # - name: Set up Python - # uses: actions/setup-python@v6 + # uses: actions/setup-python@v7 # with: # python-version: "3.12" diff --git a/.github/workflows/labeler.yml b/.github/workflows/labeler.yml index 601a3d5a9..2988cd057 100644 --- a/.github/workflows/labeler.yml +++ b/.github/workflows/labeler.yml @@ -12,7 +12,7 @@ jobs: pull-requests: write runs-on: ubuntu-latest steps: - - uses: actions/labeler@v6.2.0 + - uses: actions/labeler@v7.0.0 with: repo-token: "${{ secrets.GITHUB_TOKEN }}" sync-labels: true diff --git a/.github/workflows/publish-pypi.yml b/.github/workflows/publish-pypi.yml index 1f3414b3b..e26033738 100644 --- a/.github/workflows/publish-pypi.yml +++ b/.github/workflows/publish-pypi.yml @@ -37,7 +37,7 @@ jobs: needs: prepare-environment steps: - name: "Set up Python" - uses: "actions/setup-python@v6" + uses: "actions/setup-python@v7" with: python-version: "3.12" diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c9261442b..44de92c21 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -26,7 +26,7 @@ jobs: submodules: true - name: "Set up Python" - uses: "actions/setup-python@v6" + uses: "actions/setup-python@v7" with: python-version: "3.12" diff --git a/CHANGELOG.md b/CHANGELOG.md index eb423641d..2704ac369 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,22 @@ This project uses [*towncrier*](https://towncrier.readthedocs.io/) and the chang +## [1.22.2](https://github.com/opsmill/infrahub-sdk-python/tree/v1.22.2) - 2026-07-27 + +### Added + +- Added a `--dependencies` flag to `infrahubctl marketplace get`. When downloading a schema or a collection, it now also resolves and downloads the schemas they depend on, via the marketplace API. Dependencies are grouped by the collection they belong to: prerequisite collections (and, for a single schema, dependencies that are members of a collection) are placed in their own `/` directory, while dependencies that belong to no collection land in the output root. Referenced kinds the marketplace cannot resolve are reported as unresolved dependencies. A schema that already exists in the output directory is reconciled to a single file rather than duplicated across directories — kept by default, or overwritten with the new `-y`/`--yes` flag. ([#1117](https://github.com/opsmill/infrahub-sdk-python/issues/1117)) +- Added `infrahubctl marketplace list`, `search`, and `show` commands for browsing schemas and collections on the Infrahub Marketplace. + +### Fixed + +- Branch names containing URL-significant characters (such as `#` or `/`) are now percent-encoded in the GraphQL URL, so requests against those branches resolve correctly instead of returning a 404. ([#1209](https://github.com/opsmill/infrahub-sdk-python/issues/1209)) +- Fixed several cases where `InfrahubClientSync` had drifted from `InfrahubClient`: sync `branch.merge` now applies the same 120-second minimum timeout floor as the async client, and sync `create(allow_upsert=True)` now excludes the `hfid` from the mutation payload to avoid server-side upsert overhead. Also corrected an incorrect "feature not supported" message on async artifact fetch and a resource-pool error message typo. + +### Housekeeping + +- Replaced `markdownlint-cli2` with [rumdl](https://github.com/rvben/rumdl) for markdown linting. This removes the Node.js/npm dependency for the markdown check, speeds up linting, and consolidates the configuration into `pyproject.toml` under `[tool.rumdl]`. ([#1138](https://github.com/opsmill/infrahub-sdk-python/issues/1138)) + ## [1.22.1](https://github.com/opsmill/infrahub-sdk-python/tree/v1.22.1) - 2026-07-03 ### Added diff --git a/changelog/+async-sync-twin-drift.fixed.md b/changelog/+async-sync-twin-drift.fixed.md deleted file mode 100644 index 4fb0d9fe7..000000000 --- a/changelog/+async-sync-twin-drift.fixed.md +++ /dev/null @@ -1 +0,0 @@ -Fixed several cases where `InfrahubClientSync` had drifted from `InfrahubClient`: sync `branch.merge` now applies the same 120-second minimum timeout floor as the async client, and sync `create(allow_upsert=True)` now excludes the `hfid` from the mutation payload to avoid server-side upsert overhead. Also corrected an incorrect "feature not supported" message on async artifact fetch and a resource-pool error message typo. diff --git a/changelog/+marketplace-browsing.added.md b/changelog/+marketplace-browsing.added.md deleted file mode 100644 index b7a7c62d6..000000000 --- a/changelog/+marketplace-browsing.added.md +++ /dev/null @@ -1 +0,0 @@ -Added `infrahubctl marketplace list`, `search`, and `show` commands for browsing schemas and collections on the Infrahub Marketplace. diff --git a/changelog/1117.added.md b/changelog/1117.added.md deleted file mode 100644 index 89c4e94b7..000000000 --- a/changelog/1117.added.md +++ /dev/null @@ -1 +0,0 @@ -Added a `--dependencies` flag to `infrahubctl marketplace get`. When downloading a schema or a collection, it now also resolves and downloads the schemas they depend on, via the marketplace API. Dependencies are grouped by the collection they belong to: prerequisite collections (and, for a single schema, dependencies that are members of a collection) are placed in their own `/` directory, while dependencies that belong to no collection land in the output root. Referenced kinds the marketplace cannot resolve are reported as unresolved dependencies. A schema that already exists in the output directory is reconciled to a single file rather than duplicated across directories — kept by default, or overwritten with the new `-y`/`--yes` flag. diff --git a/changelog/1138.housekeeping.md b/changelog/1138.housekeeping.md deleted file mode 100644 index 4764d9fa8..000000000 --- a/changelog/1138.housekeeping.md +++ /dev/null @@ -1 +0,0 @@ -Replaced `markdownlint-cli2` with [rumdl](https://github.com/rvben/rumdl) for markdown linting. This removes the Node.js/npm dependency for the markdown check, speeds up linting, and consolidates the configuration into `pyproject.toml` under `[tool.rumdl]`. diff --git a/changelog/1231.fixed.md b/changelog/1231.fixed.md new file mode 100644 index 000000000..c845fa9f4 --- /dev/null +++ b/changelog/1231.fixed.md @@ -0,0 +1 @@ +Register the `infrahub_integration` pytest marker under its real name. It was registered as `infrahub_integraton`, so integration tests raised a `PytestUnknownMarkWarning` on every run and failed to collect under `--strict-markers`. diff --git a/docs/docs/python-sdk/reference/compatibility.mdx b/docs/docs/python-sdk/reference/compatibility.mdx index 586e5f9ec..2e86977f8 100644 --- a/docs/docs/python-sdk/reference/compatibility.mdx +++ b/docs/docs/python-sdk/reference/compatibility.mdx @@ -13,6 +13,7 @@ Each Infrahub release pins a specific SDK version. Using the matching SDK versio | Infrahub | Required SDK | Release date | | --- | --- | --- | +| 1.10.x | >= 1.22.0 | June 2026 | | 1.9.x | >= 1.20.0 | April 2026 | | 1.8.x | >= 1.19.0 | March 2026 | | 1.7.x | >= 1.18.1 | January 2026 | @@ -35,10 +36,25 @@ The table below shows the exact SDK version pinned to each Infrahub release. | Infrahub | SDK version | Infrahub release date | | --- | --- | --- | +| 1.10.6 | 1.22.2 | 2026-07-28 | +| 1.10.5 | 1.22.1 | 2026-07-15 | +| 1.10.4 | 1.22.1 | 2026-07-13 | +| 1.10.3 | 1.22.1 | 2026-07-08 | +| 1.10.2 | 1.22.0 | 2026-07-03 | +| 1.10.1 | 1.22.0 | 2026-07-01 | +| 1.10.0 | 1.22.0 | 2026-06-23 | +| 1.9.10 | 1.20.1 | 2026-07-07 | +| 1.9.9 | 1.20.1 | 2026-06-23 | +| 1.9.8 | 1.20.1 | 2026-06-09 | +| 1.9.7 | 1.20.1 | 2026-06-03 | +| 1.9.6 | 1.20.1 | 2026-05-20 | +| 1.9.5 | 1.20.0 | 2026-05-18 | +| 1.9.4 | 1.20.0 | 2026-05-13 | | 1.9.3 | 1.20.0 | 2026-05-05 | | 1.9.2 | 1.20.0 | 2026-04-30 | | 1.9.1 | 1.20.0 | 2026-04-29 | | 1.9.0 | 1.20.0 | 2026-04-24 | +| 1.8.7 | 1.19.0 | 2026-06-04 | | 1.8.6 | 1.19.0 | 2026-04-21 | | 1.8.5 | 1.19.0 | 2026-04-17 | | 1.8.4 | 1.19.0 | 2026-04-02 | diff --git a/docs/docs_generation/compatibility.py b/docs/docs_generation/compatibility.py index 0f9c6f253..8c1f2974b 100644 --- a/docs/docs_generation/compatibility.py +++ b/docs/docs_generation/compatibility.py @@ -74,6 +74,7 @@ class FeatureRequirement: # Mapping of Infrahub minor version series to minimum SDK versions. # Auto-updated by update_compatibility.py. VERSION_RANGES: list[VersionRange] = [ + VersionRange(infrahub="1.10.x", min_sdk="1.22.0", date="June 2026"), VersionRange(infrahub="1.9.x", min_sdk="1.20.0", date="April 2026"), VersionRange(infrahub="1.8.x", min_sdk="1.19.0", date="March 2026"), VersionRange(infrahub="1.7.x", min_sdk="1.18.1", date="January 2026"), @@ -90,10 +91,25 @@ class FeatureRequirement: # Detailed mapping of every Infrahub release to its pinned SDK version. # Auto-updated by update_compatibility.py. RELEASE_MAPPINGS: list[ReleaseMapping] = [ + ReleaseMapping(infrahub="1.10.6", sdk="1.22.2", date="2026-07-28"), + ReleaseMapping(infrahub="1.10.5", sdk="1.22.1", date="2026-07-15"), + ReleaseMapping(infrahub="1.10.4", sdk="1.22.1", date="2026-07-13"), + ReleaseMapping(infrahub="1.10.3", sdk="1.22.1", date="2026-07-08"), + ReleaseMapping(infrahub="1.10.2", sdk="1.22.0", date="2026-07-03"), + ReleaseMapping(infrahub="1.10.1", sdk="1.22.0", date="2026-07-01"), + ReleaseMapping(infrahub="1.10.0", sdk="1.22.0", date="2026-06-23"), + ReleaseMapping(infrahub="1.9.10", sdk="1.20.1", date="2026-07-07"), + ReleaseMapping(infrahub="1.9.9", sdk="1.20.1", date="2026-06-23"), + ReleaseMapping(infrahub="1.9.8", sdk="1.20.1", date="2026-06-09"), + ReleaseMapping(infrahub="1.9.7", sdk="1.20.1", date="2026-06-03"), + ReleaseMapping(infrahub="1.9.6", sdk="1.20.1", date="2026-05-20"), + ReleaseMapping(infrahub="1.9.5", sdk="1.20.0", date="2026-05-18"), + ReleaseMapping(infrahub="1.9.4", sdk="1.20.0", date="2026-05-13"), ReleaseMapping(infrahub="1.9.3", sdk="1.20.0", date="2026-05-05"), ReleaseMapping(infrahub="1.9.2", sdk="1.20.0", date="2026-04-30"), ReleaseMapping(infrahub="1.9.1", sdk="1.20.0", date="2026-04-29"), ReleaseMapping(infrahub="1.9.0", sdk="1.20.0", date="2026-04-24"), + ReleaseMapping(infrahub="1.8.7", sdk="1.19.0", date="2026-06-04"), ReleaseMapping(infrahub="1.8.6", sdk="1.19.0", date="2026-04-21"), ReleaseMapping(infrahub="1.8.5", sdk="1.19.0", date="2026-04-17"), ReleaseMapping(infrahub="1.8.4", sdk="1.19.0", date="2026-04-02"), diff --git a/docs/package-lock.json b/docs/package-lock.json index a445e87b4..cb248a797 100644 --- a/docs/package-lock.json +++ b/docs/package-lock.json @@ -7028,9 +7028,9 @@ } }, "node_modules/body-parser": { - "version": "1.20.5", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.5.tgz", - "integrity": "sha512-3grm+/2tUOvu2cjJkvsIxrv/wVpfXQW4PsQHYm7yk4vfpu7Ekl6nEsYBoJUL6qDwZUx8wUhQ8tR2qz+ad9c9OA==", + "version": "1.20.6", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.6.tgz", + "integrity": "sha512-p5tAzS57i5MV9fZFDj9LeIiTZEufbSe2eDozP+ElheSUq1m74CRq1jI4mYNDdVs9vQztXFLuk/Gd6BWTdwRJ5g==", "license": "MIT", "dependencies": { "bytes": "~3.1.2", @@ -7114,9 +7114,9 @@ } }, "node_modules/brace-expansion": { - "version": "1.1.14", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.14.tgz", - "integrity": "sha512-MWPGfDxnyzKU7rNOW9SP/c50vi3xrmrua/+6hfPbCS2ABNWfx24vPidzvC7krjU/RTo235sV776ymlsMtGKj8g==", + "version": "1.1.18", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", + "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -9419,21 +9419,6 @@ "integrity": "sha512-A/AGNMFN3c8bOlvV9RreMdrv7jsmF9XIfDeCd87+I8RNg6s78BhJxMu69NEMHBSJFxKidViTEdruRwEk/WIKqA==", "license": "MIT" }, - "node_modules/express/node_modules/qs": { - "version": "6.14.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.14.2.tgz", - "integrity": "sha512-V/yCWTTF7VJ9hIh18Ugr2zhJMP01MY7c5kh4J870L7imm6/DIzBsNLTXzMwUA3yZ5b/KBqLx8Kp3uRvd7xSe3Q==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.1.0" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, "node_modules/express/node_modules/range-parser": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", @@ -9490,9 +9475,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", - "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", + "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", "funding": [ { "type": "github", @@ -14156,9 +14141,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.12", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.12.tgz", - "integrity": "sha512-ZB9RH/39qpq5Vu6Y+NmUaFhQR6pp+M2Xt76XBnEwDaGcVAqhlvxrl3B2bKS5D3NH3QR76v3aSrKaF/Kiy7lEtQ==", + "version": "3.3.16", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.16.tgz", + "integrity": "sha512-bzlKTyNJ7+LdGIIwy8ijFpIqEQIvafahV7eYykJ8Cvh42EdJeODoJ6gUJXpQJvej1BddH8OqTXZNE/KfbWAu8Q==", "funding": [ { "type": "github", @@ -14855,9 +14840,9 @@ } }, "node_modules/postcss": { - "version": "8.5.14", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.14.tgz", - "integrity": "sha512-SoSL4+OSEtR99LHFZQiJLkT59C5B1amGO1NzTwj7TT1qCUgUO6hxOvzkOYxD+vMrXBM3XJIKzokoERdqQq/Zmg==", + "version": "8.5.23", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.23.tgz", + "integrity": "sha512-g50586zr4bZmwFiTlflMu8E0bDTb5I5gertgwAKmsdUlTQIhZtunzUlD1WSzwcVWPoAVpsrA6vlfCD7oXvRwgg==", "funding": [ { "type": "opencollective", @@ -14874,7 +14859,7 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.16", "picocolors": "^1.1.1", "source-map-js": "^1.2.1" }, @@ -17904,9 +17889,9 @@ } }, "node_modules/shell-quote": { - "version": "1.8.4", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.4.tgz", - "integrity": "sha512-VsC6n6vz1ihYYyZZwX7YZSF5l5x36ca17OC+a69h94YqB7X6XLwf+5MOgynYir2SLFUbl8gIYvBo8K8RoNQ6bQ==", + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.10.0.tgz", + "integrity": "sha512-w1aiOKwKuRgtwAReIIj89puqg+I7GvX4IbLrvmhXbzQsj1+Zwi4VO3+fa6ZF91TWSjIxoEkKnMeHcLEODK5ZXA==", "license": "MIT", "engines": { "node": ">= 0.4" @@ -18423,9 +18408,9 @@ "license": "MIT" }, "node_modules/svgo": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.3.tgz", - "integrity": "sha512-+wn7I4p7YgJhHs38k2TNjy1vCfPIfLIJWR5MnCStsN8WuuTcBnRKcMHQLMM2ijxGZmDoZwNv8ipl5aTTen62ng==", + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.4.tgz", + "integrity": "sha512-GsNRis4e8jxn2Y9ENz/8lbJ93CstG8svtMnuRaHbiF2LTJ5tK0/q3t/URPq9Zc7zVWBJnNnJMIp6bevK7bSmNg==", "license": "MIT", "dependencies": { "commander": "^7.2.0", @@ -19765,9 +19750,9 @@ } }, "node_modules/webpack-dev-server": { - "version": "5.2.5", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.5.tgz", - "integrity": "sha512-4wZtCquSuv9CKX8oybo+mqxtxZqWz47uM1Ch94lxowBztOhWCbhqvRbfC/mODOwxgV2brY+JGZpHq58/SuVFYg==", + "version": "5.2.6", + "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-5.2.6.tgz", + "integrity": "sha512-HNLRmamRvVavZQ+avceZifmv8hmdUjg43t6MI4SqJDwFdW7RPQwH5vzGhDRZSX59SgfbeHhLnq3g+uooWo7pVw==", "license": "MIT", "dependencies": { "@types/bonjour": "^3.5.13", @@ -19788,7 +19773,7 @@ "graceful-fs": "^4.2.6", "http-proxy-middleware": "^2.0.9", "ipaddr.js": "^2.1.0", - "launch-editor": "^2.6.1", + "launch-editor": "^2.14.1", "open": "^10.0.3", "p-retry": "^6.2.0", "schema-utils": "^4.2.0", @@ -19938,9 +19923,9 @@ "license": "MIT" }, "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.5.tgz", + "integrity": "sha512-ZL2+3c7kMBdIRCMz6l8jQMHyGVxj+UL+xVk74Ombiciboca8rHa15L86B19E5oh1pL9Ii/uj54gtsIrZGMo6zA==", "license": "Apache-2.0", "dependencies": { "http-parser-js": ">=0.5.1", diff --git a/infrahub_sdk/client.py b/infrahub_sdk/client.py index 0b09bdcce..f39458531 100644 --- a/infrahub_sdk/client.py +++ b/infrahub_sdk/client.py @@ -11,7 +11,7 @@ from functools import wraps from time import sleep from typing import TYPE_CHECKING, Any, BinaryIO, Literal, TypedDict, TypeVar, overload -from urllib.parse import urlencode +from urllib.parse import quote, urlencode import httpx import ujson @@ -278,7 +278,7 @@ def _graphql_url( ) -> str: url = f"{self.config.address}/graphql" if branch_name: - url += f"/{branch_name}" + url += f"/{quote(branch_name, safe='')}" url_params = {} if at: diff --git a/infrahub_sdk/pytest_plugin/plugin.py b/infrahub_sdk/pytest_plugin/plugin.py index 258e7f9c8..aa5f79824 100644 --- a/infrahub_sdk/pytest_plugin/plugin.py +++ b/infrahub_sdk/pytest_plugin/plugin.py @@ -100,7 +100,7 @@ def pytest_configure(config: pytest.Config) -> None: config.addinivalue_line("markers", "infrahub_unit: Unit test for an Infrahub resource, works without dependencies") config.addinivalue_line( "markers", - "infrahub_integraton: Integation test for an Infrahub resource, depends on an Infrahub running instance", + "infrahub_integration: Integration test for an Infrahub resource, depends on an Infrahub running instance", ) config.addinivalue_line("markers", "infrahub_check: Test related to an Infrahub Check") config.addinivalue_line("markers", "infrahub_graphql_query: Test related to an Infrahub GraphQL query") diff --git a/pyproject.toml b/pyproject.toml index 0722829a1..0bd7f27e2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [project] name = "infrahub-sdk" -version = "1.22.1" +version = "1.22.2" description = "Python Client to interact with Infrahub" authors = [ {name = "OpsMill", email = "info@opsmill.com"} @@ -117,7 +117,7 @@ filterwarnings = [ "ignore:Module already imported so cannot be rewritten", "ignore:Deprecated call to", ] -addopts = "-vs --cov-report term-missing --cov-report xml --dist loadscope" +addopts = "-vs --strict-markers --cov-report term-missing --cov-report xml --dist loadscope" [tool.ty] diff --git a/tests/unit/pytest_plugin/test_plugin.py b/tests/unit/pytest_plugin/test_plugin.py index 01fa38ac7..7a1b5db4c 100644 --- a/tests/unit/pytest_plugin/test_plugin.py +++ b/tests/unit/pytest_plugin/test_plugin.py @@ -1,5 +1,7 @@ import pytest +from infrahub_sdk.pytest_plugin.loader import MARKER_MAPPING + def test_help_message(pytester: pytest.Pytester) -> None: """Make sure that the plugin is loaded by capturing an option it adds in the help message.""" @@ -28,6 +30,72 @@ def test_emptyconfig(pytester: pytest.Pytester) -> None: result.assert_outcomes() +def test_resource_markers_are_registered(pytester: pytest.Pytester) -> None: + """The resource markers are built when the loader is imported, before a config exists. + + `--strict-markers` only validates a marker created once a config is attached, so it never sees + these. Compare them against the registered list instead. + """ + result = pytester.runpytest("--markers") + + registered = { + line.removeprefix("@pytest.mark.").split(":")[0].split("(")[0] + for line in result.stdout.lines + if line.startswith("@pytest.mark.") + } + missing = {mark.markname for mark in MARKER_MAPPING.values()} - registered + + assert not missing, f"markers applied by the loader but never registered: {sorted(missing)}" + + +def test_type_markers_are_registered(pytester: pytest.Pytester) -> None: + """The type markers are applied during collection, so --strict-markers rejects an unregistered one.""" + pytester.makefile( + ".yml", + test_markers=""" + --- + version: "1.0" + infrahub_tests: + - resource: "Jinja2Transform" + resource_name: "bgp_config" + tests: + - name: "smoke" + spec: + kind: "jinja2-transform-smoke" + - name: "unit" + spec: + kind: "jinja2-transform-unit-render" + - name: "integration" + spec: + kind: "jinja2-transform-integration" + variables: {} + """, + ) + pytester.makefile( + ".yml", + infrahub_config=""" + --- + jinja2_transforms: + - name: bgp_config + description: "Template for BGP config base" + query: "bgp_sessions" + template_path: "templates/bgp_config.j2" + """, + ) + pytester.makefile(".json", input="{}") + + result = pytester.runpytest("--infrahub-repo-config=infrahub_config.yml", "--strict-markers", "--collect-only") + + assert result.ret == pytest.ExitCode.OK + result.stdout.fnmatch_lines( + [ + "*infrahub_jinja2_transform__bgp_config__smoke*", + "*infrahub_jinja2_transform__bgp_config__unit*", + "*infrahub_jinja2_transform__bgp_config__integration*", + ] + ) + + def test_jinja2_transform_config_missing_directory(pytester: pytest.Pytester) -> None: """Make sure tests raise errors if directories are not found.""" pytester.makefile( diff --git a/tests/unit/sdk/test_client.py b/tests/unit/sdk/test_client.py index cf7a7988b..7b44e0d4b 100644 --- a/tests/unit/sdk/test_client.py +++ b/tests/unit/sdk/test_client.py @@ -3,6 +3,7 @@ import inspect import json import ssl +from dataclasses import dataclass from pathlib import Path from typing import TYPE_CHECKING @@ -872,3 +873,24 @@ async def test_execute_graphql_omits_operation_name_when_unset( assert len(post_requests) == 1 payload = json.loads(post_requests[0].content) assert "operationName" not in payload + + +@dataclass +class GraphQLURLCase: + name: str + branch_name: str + expected_url: str + + +GRAPHQL_URL_CASES = [ + GraphQLURLCase(name="hash", branch_name="feature#123", expected_url="http://mock/graphql/feature%23123"), + GraphQLURLCase(name="slash", branch_name="feature/foo", expected_url="http://mock/graphql/feature%2Ffoo"), + GraphQLURLCase(name="plain", branch_name="main", expected_url="http://mock/graphql/main"), +] + + +@pytest.mark.parametrize("client_type", client_types) +@pytest.mark.parametrize("case", [pytest.param(tc, id=tc.name) for tc in GRAPHQL_URL_CASES]) +async def test_graphql_url_encodes_branch_name(clients: BothClients, client_type: str, case: GraphQLURLCase) -> None: + client = clients.standard if client_type == "standard" else clients.sync + assert client._graphql_url(branch_name=case.branch_name) == case.expected_url diff --git a/uv.lock b/uv.lock index d376f6b5d..b1f4bc711 100644 --- a/uv.lock +++ b/uv.lock @@ -676,7 +676,7 @@ wheels = [ [[package]] name = "infrahub-sdk" -version = "1.22.1" +version = "1.22.2" source = { editable = "." } dependencies = [ { name = "dulwich" }, @@ -849,7 +849,7 @@ types = [ [[package]] name = "infrahub-testcontainers" -version = "1.10.5" +version = "1.10.6" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "httpx" }, @@ -859,9 +859,9 @@ dependencies = [ { name = "pytest" }, { name = "testcontainers" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/e8/36/13e1bfff33a28da3fb221532ba5029b825a761b79b988024ba536785f026/infrahub_testcontainers-1.10.5.tar.gz", hash = "sha256:9a6cf400fba71e52bcc36b7ea80f41c2c84bcc1c6e60c9aacda11af9cd21eb6f", size = 17940, upload-time = "2026-07-15T18:52:27.798Z" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/c2/4ef3e3248c8a59dce88f2efd6a2a3fa7f8efc17d6f380ae861ad6d5e674b/infrahub_testcontainers-1.10.6.tar.gz", hash = "sha256:56cd3a9855743b05402275bfe21a863ce22134de849863cd157d3da0df1d453f", size = 17943, upload-time = "2026-07-28T10:57:46.856Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/ad/8c/8eb3618611494790f7641b94b8456a534b8837e7214c460faaee66f02d2d/infrahub_testcontainers-1.10.5-py3-none-any.whl", hash = "sha256:36bba14a1c0e76df3a95fe0742d3b3c3df69a283dc07e65b5d2765cb7d48a062", size = 24210, upload-time = "2026-07-15T18:52:26.777Z" }, + { url = "https://files.pythonhosted.org/packages/76/8f/8cf61d25a3a8b8e8f643d12c059cbe6c941297adee2cc7a211935806a03b/infrahub_testcontainers-1.10.6-py3-none-any.whl", hash = "sha256:949e5c30b2049710ad0e22a5a5423743ce69366ac015c98656a4abf850805400", size = 24215, upload-time = "2026-07-28T10:57:45.609Z" }, ] [[package]]