Loading ci/sdk-consumer.md +19 −11 Changes for ci/sdk-consumer.md: 19 added lines, 11 removed lines. Original line number Diff line number Diff line Loading @@ -2,19 +2,27 @@ An SDK pipeline can select an SRM branch or open MR before either source change merges. It sends one wheel's build job, source revision and checksum. SRM checks its own checkout against `EXPECTED_SRM_SHA` before resolving anything. A changed its own checkout against `EXPECTED_SRM_SHA` before installing anything. A changed branch head requires a new run. The `sdk-consumer` job downloads that exact artifact and copies SRM's project and lock into `.cache/sdk-consumer/project`. `uv add --no-sync` selects the local wheel, including a new or same-version candidate, and resolves its dependencies. `uv sync --locked --extra dev --no-install-project` creates the test environment from this candidate lock. SRM's committed `pyproject.toml` and `uv.lock` stay unchanged. Resolution conflicts fail this job; there are no package exceptions. Before selection, the SRM MR must explicitly adopt the candidate. Use the SDK pipeline's `candidate-package.json`: set the exact SDK version in SRM's project requirements and its content-addressed URL under `[tool.uv.sources]`. Run `uv lock` as a developer, inspect the changes and commit both dependency files. The lock must select that version, URL and wheel checksum. A dependency change is therefore visible in the SRM MR before any qualification starts. The `sdk-consumer` job downloads the checked artifact and copies the committed `pyproject.toml` and `uv.lock` unchanged into `.cache/sdk-consumer/project`. It rejects an undeclared candidate before installation, then uses `uv sync --locked --extra dev --no-install-project`. No `uv add`, replacement lock or dependency resolution is performed by this job. Both the source and copied dependency inputs must retain their original hashes after installation. The job checks installed SDK files against the wheel, runs dependency checks, and executes the offline unit/bridge and CI tests without skips. Its artifacts retain the wheel, candidate project/lock, dependency changes and qualification. retain the wheel, committed project/lock hashes and qualification. Dependency changes belong in the MR; this job records no implicit dependency changes. These checks alone do not establish cluster, NEF or OEG acceptance. With `SDK_BUILD_IMAGE=1`, `sdk-consumer-image` builds through the existing Loading @@ -22,7 +30,7 @@ Dockerfile using that tested project/lock and the pinned test Python image. It checks the installed wheel, runtime dependency versions and package metadata before pushing a unique `sdk-candidate-p<PIPELINE>-<SDK-SHA>` image. The image receipt records its immutable digest, both source identities, wheel checksum, lock hash and actual dependencies. Ansible deploys that finished image; it project/lock hashes, unchanged-input marker and actual dependencies. Ansible deploys that finished image; it performs no further package installation. Normal MR pipelines also build images after their usual checks, using unique Loading @@ -34,8 +42,8 @@ For local offline consumer verification, use Python 3.12 and uv 0.11.26. Set `SDK_WHEEL_FILE`, `SDK_MANIFEST_FILE`, `SDK_SOURCE_SHA`, `SDK_WHEEL_NAME` and `SDK_WHEEL_SHA256`, then run `python ci/sdk_consumer.py` in a clean checkout. Use a disk-backed `TMPDIR` and a fresh `.cache/sdk-consumer` directory. The resolver creates its own isolated environment; an existing SDK version need not be published first. Supply the matching pinned `CI_JOB_IMAGE` if preparing locked installation creates its own isolated environment from the committed candidate URL; stable SDK publication is not needed first. Supply the matching pinned `CI_JOB_IMAGE` if preparing inputs for the CI image build. SDK must admit SRM job tokens for exact build-artifact downloads. SRM admits the Loading ci/sdk_consumer.py +49 −16 Changes for ci/sdk_consumer.py: 49 added lines, 16 removed lines. Original line number Diff line number Diff line Loading @@ -20,6 +20,9 @@ OUT = ROOT / ".cache/sdk-consumer" SHA = re.compile(r"[a-f0-9]{40}") DIGEST = re.compile(r"[a-f0-9]{64}") REGISTRY = "labs.etsi.org:5050/oop/code/service-resource-manager" CANDIDATE_REGISTRY = ( "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates" ) def digest(path): Loading Loading @@ -116,12 +119,35 @@ def prepare(root=ROOT, out=OUT, environment=None, call=subprocess.run, fetch=url project = out / "project" for name in ("pyproject.toml", "uv.lock", "README.md"): shutil.copyfile(root / name, project / name) # uv owns resolution and rewrites only this disposable project, never SRM source. call( ["uv", "add", "--no-sync", "--python", sys.executable, "wheels/" + sdk["filename"]], cwd=project, check=True, # The SRM MR owns adoption. CI only materializes its committed inputs. declaration = tomllib.loads((root / "pyproject.toml").read_text()) lock = tomllib.loads((root / "uv.lock").read_text()) packages = [p for p in lock["package"] if p["name"] == "openop-tfsdk"] require(len(packages) == 1, "The committed lock must select exactly one SDK") package = packages[0] expected_url = f"{CANDIDATE_REGISTRY}/{sdk['sha256']}/{sdk['filename']}" declared_url = ( declaration.get("tool", {}) .get("uv", {}) .get("sources", {}) .get("openop-tfsdk", {}) .get("url") ) require( package.get("version") == sdk["version"] and any( re.fullmatch( r"openop[-_]tfsdk\s*==\s*" + re.escape(sdk["version"]), item, flags=re.IGNORECASE ) for item in declaration.get("project", {}).get("dependencies", []) ) and declared_url == expected_url and package.get("source", {}).get("url") == expected_url and any(p.get("hash") == "sha256:" + sdk["sha256"] for p in package.get("wheels", [])), "SDK candidate is not pinned by the committed SRM dependency and lock; " "update them in an SRM MR before qualification", ) locked_inputs = {name: digest(root / name) for name in ("pyproject.toml", "uv.lock")} call( [ "uv", Loading @@ -144,16 +170,16 @@ def prepare(root=ROOT, out=OUT, environment=None, call=subprocess.run, fetch=url "project_sha256": digest(project / "pyproject.toml"), } def versions(path): rows = tomllib.loads(path.read_text())["package"] return {item["name"]: item.get("version") for item in rows} before, after = versions(root / "uv.lock"), versions(project / "uv.lock") proof["dependency_changes"] = [ {"name": name, "before": before.get(name), "after": after.get(name)} for name in sorted(before.keys() | after.keys()) if before.get(name) != after.get(name) ] require( all( digest(project / name) == value == digest(root / name) for name, value in locked_inputs.items() ), "Strict consumer installation changed committed dependency inputs", ) proof["dependency_changes"] = [] proof["source_inputs_unchanged"] = True proof["sdk_dependency_url"] = expected_url write(out / "candidate.json", proof) return proof Loading Loading @@ -234,8 +260,12 @@ def verify_inputs(root, out): ) project = out / "project" require( digest(project / "uv.lock") == proof["lock_sha256"] (root / "uv.lock").is_file() and (root / "pyproject.toml").is_file() and digest(project / "uv.lock") == proof["lock_sha256"] and digest(project / "pyproject.toml") == proof["project_sha256"] and digest(root / "uv.lock") == proof["lock_sha256"] and digest(root / "pyproject.toml") == proof["project_sha256"] and digest(project / "wheels" / proof["sdk"]["filename"]) == proof["sdk"]["sha256"], "Candidate inputs changed after consumer tests", ) Loading Loading @@ -349,6 +379,9 @@ def build_image(root=ROOT, out=OUT, call=subprocess.run): "lock_sha256": proof["lock_sha256"], "python_image": proof["python_image"], "dependency_changes": proof["dependency_changes"], "source_inputs_unchanged": proof["source_inputs_unchanged"], "sdk_dependency_url": proof["sdk_dependency_url"], "project_sha256": proof["project_sha256"], "runtime_dependencies": inventory["dependencies"], "consumer_checks": {"job_id": proof["qualification_job_id"], "status": "passed"}, } Loading ci/tests/test_sdk_consumer.py +57 −0 Changes for ci/tests/test_sdk_consumer.py: 57 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -130,3 +130,60 @@ def test_normal_image_receipt_requires_a_real_digest(): assert result["source_sha"] == "a" * 40 and result["image"].endswith("b" * 64) with pytest.raises(ValueError, match="digest"): image_receipt.receipt({}, env) def committed_project(root, wheel, *, old=False): _, _, env = wheel url = f"{consumer.CANDIDATE_REGISTRY}/{env['SDK_WHEEL_SHA256']}/candidate.whl" version = "1.0.0" if old else "2.0.0" (root / "README.md").write_text("consumer") (root / "pyproject.toml").write_text( '[project]\ndependencies = ["openop-tfsdk==' + version + '"]\n' '[tool.uv.sources]\nopenop-tfsdk = {url = "' + url + '"}\n' ) (root / "uv.lock").write_text( '[[package]]\nname = "openop-tfsdk"\nversion = "' + version + '"\n' 'source = {url = "' + url + '"}\n' 'wheels = [{hash = "sha256:' + env["SDK_WHEEL_SHA256"] + '"}]\n' ) return env def test_strict_install_uses_committed_inputs_without_resolution(wheel, tmp_path, monkeypatch): root = tmp_path / "srm" root.mkdir() env = committed_project(root, wheel) original = {n: (root / n).read_bytes() for n in ("pyproject.toml", "uv.lock")} monkeypatch.setattr(consumer, "revision", lambda *_: "a" * 40) calls = [] proof = consumer.prepare(root, root / "out", env, call=lambda args, **kw: calls.append(args)) assert len(calls) == 1 and calls[0][:3] == ["uv", "sync", "--locked"] assert all((root / n).read_bytes() == value for n, value in original.items()) assert all((root / "out/project" / n).read_bytes() == value for n, value in original.items()) assert proof["dependency_changes"] == [] and proof["source_inputs_unchanged"] is True def test_incompatible_committed_lock_fails_before_install(wheel, tmp_path, monkeypatch): root = tmp_path / "srm" root.mkdir() env = committed_project(root, wheel, old=True) original = (root / "uv.lock").read_bytes() monkeypatch.setattr(consumer, "revision", lambda *_: "a" * 40) with pytest.raises(ValueError, match="not pinned"): consumer.prepare( root, root / "out", env, call=lambda *_a, **_k: pytest.fail("No install or resolver") ) assert (root / "uv.lock").read_bytes() == original def test_install_cannot_rewrite_the_copied_lock(wheel, tmp_path, monkeypatch): root = tmp_path / "srm" root.mkdir() env = committed_project(root, wheel) monkeypatch.setattr(consumer, "revision", lambda *_: "a" * 40) def changing_install(args, **kwargs): (kwargs["cwd"] / "uv.lock").write_text("resolved a different lock") with pytest.raises(ValueError, match="changed committed"): consumer.prepare(root, root / "out", env, call=changing_install) pyproject.toml +2 −2 Changes for pyproject.toml: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -21,7 +21,7 @@ dependencies = [ "sqlalchemy>=2.0.48", "asyncpg>=0.31.0", "nats-py>=2.10.0", "openop-tfsdk==2.0.0", "openop-tfsdk==2.0.1", "apscheduler<4.0.0", ] Loading @@ -39,7 +39,7 @@ dev = [ ] [tool.uv.sources] openop-tfsdk = { index = "etsi-tf-sdk" } openop-tfsdk = { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates/54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b/openop_tfsdk-2.0.1-py3-none-any.whl" } [[tool.uv.index]] name = "etsi-tf-sdk" Loading uv.lock +34 −20 Changes for uv.lock: 34 added lines, 20 removed lines. Original line number Diff line number Diff line Loading @@ -730,12 +730,13 @@ wheels = [ [[package]] name = "openop-tfsdk" version = "2.0.0" source = { registry = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/simple" } version = "2.0.1" source = { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates/54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b/openop_tfsdk-2.0.1-py3-none-any.whl" } dependencies = [ { name = "auto-mix-prep" }, { name = "colorlog" }, { name = "kubernetes" }, { name = "pyasn1" }, { name = "pydantic" }, { name = "pydantic-extra-types" }, { name = "pymongo" }, Loading @@ -743,9 +744,22 @@ dependencies = [ { name = "requests" }, { name = "structlog" }, ] sdist = { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/files/229082cbb098e089dd8f8a7f90395ee7d18ea2a72e91a975940f3fa1ebcaa973/openop_tfsdk-2.0.0.tar.gz", hash = "sha256:229082cbb098e089dd8f8a7f90395ee7d18ea2a72e91a975940f3fa1ebcaa973" } wheels = [ { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/files/6431c6da12b664e69a7a27ea3b35c84520c17ec42acba7caa8fdde5f6899da55/openop_tfsdk-2.0.0-py3-none-any.whl", hash = "sha256:6431c6da12b664e69a7a27ea3b35c84520c17ec42acba7caa8fdde5f6899da55" }, { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates/54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b/openop_tfsdk-2.0.1-py3-none-any.whl", hash = "sha256:54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b" }, ] [package.metadata] requires-dist = [ { name = "auto-mix-prep", specifier = "==0.2.0" }, { name = "colorlog", specifier = "==6.8.2" }, { name = "kubernetes", specifier = "==33.1.0" }, { name = "pyasn1", specifier = "==0.6.5" }, { name = "pydantic", specifier = "==2.11.3" }, { name = "pydantic-extra-types", specifier = "==2.10.3" }, { name = "pymongo", specifier = "==4.18.3" }, { name = "pyyaml", specifier = "==6.0.3" }, { name = "requests", specifier = "==2.33.0" }, { name = "structlog", specifier = ">=25.5.0" }, ] [[package]] Loading Loading @@ -802,11 +816,11 @@ wheels = [ [[package]] name = "pyasn1" version = "0.6.4" version = "0.6.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } sdist = { url = "https://files.pythonhosted.org/packages/88/7e/3c0da41061a2021cac7cd524fd57092adbe5b515e8bf9005702e366802e2/pyasn1-0.6.5.tar.gz", hash = "sha256:6b11616a6bcf3a40e635f082bb232fa57b5e0dd7f517bd7ddd69ec3794508a47", size = 155152, upload-time = "2026-10-27T06:32:49.125Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, { url = "https://files.pythonhosted.org/packages/19/1f/3eefd5c84c35cae44549ce1c7f7e58f7945b34f5a6bce07a135438522073/pyasn1-0.6.5-py3-none-any.whl", hash = "sha256:4017b0fce125096b8573b39c989fdb1a4b19cf761e683398184550fea0e66587", size = 84807, upload-time = "2026-10-27T06:32:48.045Z" }, ] [[package]] Loading Loading @@ -922,23 +936,23 @@ wheels = [ [[package]] name = "pymongo" version = "4.18.2" version = "4.18.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/04/455039b475bb8afa5af9b8c363fb773a45d898016f04ee40c1c75003a3f9/pymongo-4.18.2.tar.gz", hash = "sha256:1bd4321cc63c06954953eae9eb8fb5d9075296b96c0e3ed34ccb91a0dcce31a5", size = 2746691, upload-time = "2026-09-24T16:59:59.472Z" } sdist = { url = "https://files.pythonhosted.org/packages/42/d8/2421a5ae0d6dcdaad2a0fb75d4071eaede9f764e73b829c62b6185c3ee6b/pymongo-4.18.3.tar.gz", hash = "sha256:5dd6e659b6014288a1c53458929402a58f44a032e6f29bcef44e7477c5268e48", size = 2747872, upload-time = "2026-10-08T19:44:08.343Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/eb/995ec978741cfc56a29223ec0806976218b95d79a516d9b719c984ead130/pymongo-4.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78a3b324407ba7228a57f207e175efc40341c1312d1e7df2d4ad8141ff6d8e00", size = 819057, upload-time = "2026-09-24T16:58:35.844Z" }, { url = "https://files.pythonhosted.org/packages/40/e4/5a166bbac7f6e620cfb75b0640e793dd3ce69423a1f89cf6a0bfc588c107/pymongo-4.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:498187b5171d5efa87291b30e4cbf1e9849929c02bc436e95a3d01ff19e49f42", size = 819448, upload-time = "2026-09-24T16:58:37.221Z" }, { url = "https://files.pythonhosted.org/packages/c9/c8/82194d9a1387a56cac996901f92ea6a25ae817622ee32d15c986638b263f/pymongo-4.18.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c003646d3fa5db4e835483fc29018eb06d8c637cf69f676ffbb3f0734005220", size = 1039602, upload-time = "2026-09-24T16:58:38.821Z" }, { url = "https://files.pythonhosted.org/packages/cc/06/628f93f15f269d8a1ea20a024083e67871f722876a33992aec5525a6c6c4/pymongo-4.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c743347143298252948e038f768ea6f71b71ef8a85b7cf0dc13b734e072f74da", size = 1050746, upload-time = "2026-09-24T16:58:40.322Z" }, { url = "https://files.pythonhosted.org/packages/bf/93/58ea76803128651ec9c732b59b8c1eb73d511c085c2e5786da3289d4015c/pymongo-4.18.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:09e6afa68001433c42925a258b57f2ceccb95c73fe8920452e147a82d90c1ae9", size = 1075681, upload-time = "2026-09-24T16:58:41.722Z" }, { url = "https://files.pythonhosted.org/packages/7c/6d/702ee4593420e2a8ceb169ece6394f0bb9fc2e8e8d8101e99f2468ed6d0d/pymongo-4.18.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d3445caeeb47dc64bf416dc03e0b3bfcf3b60a109dec355a563aa62c135cb33", size = 1068235, upload-time = "2026-09-24T16:58:43.228Z" }, { url = "https://files.pythonhosted.org/packages/a9/92/a02a4108ca4faf05bca394d65dcebc7b84b0248cff23aa89927c81a0d5dd/pymongo-4.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecde9e735e1f86559e0a71a5bbec671e59a35549bfd8c60968d9f032b05013de", size = 1049896, upload-time = "2026-09-24T16:58:44.914Z" }, { url = "https://files.pythonhosted.org/packages/6e/93/65ef17eecdd86c153f7907b2d490d85eb655d3a5eb35a8494e7e09376353/pymongo-4.18.2-cp312-cp312-win32.whl", hash = "sha256:a463c3fc072655b7fe9e9ded4601876bef7f1df98f6f3faeed07fa7f11309fff", size = 815124, upload-time = "2026-09-24T16:58:46.39Z" }, { url = "https://files.pythonhosted.org/packages/3c/cc/fd66aa9d8956e8269ea63b2977a7fd38fbec45795d38b8067bfafc98992f/pymongo-4.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:cbb67901ba9b6334fa94643d66587a30ffe5be05061af287364e6b5f7d239dc5", size = 821996, upload-time = "2026-09-24T16:58:47.769Z" }, { url = "https://files.pythonhosted.org/packages/06/21/983d340cfe226c987014f1caf9568b176cbd7f25dd87536379a609e6371e/pymongo-4.18.2-cp312-cp312-win_arm64.whl", hash = "sha256:94c5e0fe9af4c05f382ae7c4addd0255a8ddcc91b3d2a6bc09b34230cb28290b", size = 815707, upload-time = "2026-09-24T16:58:49.493Z" }, { url = "https://files.pythonhosted.org/packages/05/d5/4775a2891396ad125545e23b3024adae4bfac9553b70c924f1f372269dbf/pymongo-4.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ea78719dd05de3a919a52b94bec790c0d0cb7d07d2f7271711832664502a0782", size = 819374, upload-time = "2026-10-08T19:42:26.931Z" }, { url = "https://files.pythonhosted.org/packages/e0/0b/89ad56f43c3da6cbde100699f6b99528e78eba3c6740d8dad4ea2516aa45/pymongo-4.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6029d14761ba7243e6c5e464592013b519ad4dd3e4cfb75ddec39f4b5910711b", size = 819694, upload-time = "2026-10-08T19:42:28.76Z" }, { url = "https://files.pythonhosted.org/packages/84/b4/b68ffc205441b0a6d36d6299e35e063a5d0d3264fd685428920e1f82b634/pymongo-4.18.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9536fb3820f721290f03ad07472ec2266d8f364f91de628679a7146c9c1dbe35", size = 1040136, upload-time = "2026-10-08T19:42:30.852Z" }, { url = "https://files.pythonhosted.org/packages/c1/40/e779ff3d9165316c35a2f9742a42b9c3e3a678e9e2a9f6fe4128b7c551eb/pymongo-4.18.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e461bfca4861057929efa4215730b28b93b2adb4d07828d0b65475755bbf63f5", size = 1051430, upload-time = "2026-10-08T19:42:32.533Z" }, { url = "https://files.pythonhosted.org/packages/07/9b/443ee038a739cc65a75f2078c9ef725c1cb4881545d2e9d7941c46f64a6c/pymongo-4.18.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1fef248623ed5e7406902a68d49dc0b1db434f19489f8d2fc9fe512c3c08bb1", size = 1075464, upload-time = "2026-10-08T19:42:34.325Z" }, { url = "https://files.pythonhosted.org/packages/36/4b/d80518f675cd4c1215b770444bb83002454574dae0e69760af10703ed1e8/pymongo-4.18.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:213eaed8fc4f2b0f9c84323a229dea699e01e18b8fb39723f430123b6ee77813", size = 1069046, upload-time = "2026-10-08T19:42:36.105Z" }, { url = "https://files.pythonhosted.org/packages/e5/77/f2e9648c62e423c3b9dab1e16491a6c33250487c819e5c75784b35d16047/pymongo-4.18.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa6f363ff648bf061335d2190dd580cbf465b1308a7e6acb992d128d6a16a3bd", size = 1050321, upload-time = "2026-10-08T19:42:38.052Z" }, { url = "https://files.pythonhosted.org/packages/a6/e4/3236e3a87b29fc4c502ad7dd1521c20d4a29faf1db6fde6ae94d05c5ec27/pymongo-4.18.3-cp312-cp312-win32.whl", hash = "sha256:28ba8cae86ea02d7ffdf0eea81be69be80d35d6a4a3eba4dc436d3194341805a", size = 815300, upload-time = "2026-10-08T19:42:40.062Z" }, { url = "https://files.pythonhosted.org/packages/1e/18/3fa9d86ba32386c02ea991f10875a6a066dd5e5d80790243be3c141b0e73/pymongo-4.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:dc8ccf72b76c99a6b9fd05f8b89fe4a693128c5cfdba70f70e5792a6a563f6b0", size = 822079, upload-time = "2026-10-08T19:42:42.089Z" }, { url = "https://files.pythonhosted.org/packages/03/50/65a7cefd3891b77994841992b2c5b59394667df64ef21377b8ac7ecdef47/pymongo-4.18.3-cp312-cp312-win_arm64.whl", hash = "sha256:4a1f7c7dc1d554449a1695d897eb42b6080a2f1e9ccd81385dfa00204979c54d", size = 817935, upload-time = "2026-10-08T19:42:43.98Z" }, ] [[package]] Loading Loading @@ -1208,7 +1222,7 @@ requires-dist = [ { name = "import-linter", marker = "extra == 'dev'", specifier = ">=2.11" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.19.1" }, { name = "nats-py", specifier = ">=2.10.0" }, { name = "openop-tfsdk", specifier = "==2.0.0", index = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/simple" }, { name = "openop-tfsdk", url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates/54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b/openop_tfsdk-2.0.1-py3-none-any.whl" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.5.1" }, { name = "pydantic-settings", specifier = ">=2.13.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, Loading Loading
ci/sdk-consumer.md +19 −11 Changes for ci/sdk-consumer.md: 19 added lines, 11 removed lines. Original line number Diff line number Diff line Loading @@ -2,19 +2,27 @@ An SDK pipeline can select an SRM branch or open MR before either source change merges. It sends one wheel's build job, source revision and checksum. SRM checks its own checkout against `EXPECTED_SRM_SHA` before resolving anything. A changed its own checkout against `EXPECTED_SRM_SHA` before installing anything. A changed branch head requires a new run. The `sdk-consumer` job downloads that exact artifact and copies SRM's project and lock into `.cache/sdk-consumer/project`. `uv add --no-sync` selects the local wheel, including a new or same-version candidate, and resolves its dependencies. `uv sync --locked --extra dev --no-install-project` creates the test environment from this candidate lock. SRM's committed `pyproject.toml` and `uv.lock` stay unchanged. Resolution conflicts fail this job; there are no package exceptions. Before selection, the SRM MR must explicitly adopt the candidate. Use the SDK pipeline's `candidate-package.json`: set the exact SDK version in SRM's project requirements and its content-addressed URL under `[tool.uv.sources]`. Run `uv lock` as a developer, inspect the changes and commit both dependency files. The lock must select that version, URL and wheel checksum. A dependency change is therefore visible in the SRM MR before any qualification starts. The `sdk-consumer` job downloads the checked artifact and copies the committed `pyproject.toml` and `uv.lock` unchanged into `.cache/sdk-consumer/project`. It rejects an undeclared candidate before installation, then uses `uv sync --locked --extra dev --no-install-project`. No `uv add`, replacement lock or dependency resolution is performed by this job. Both the source and copied dependency inputs must retain their original hashes after installation. The job checks installed SDK files against the wheel, runs dependency checks, and executes the offline unit/bridge and CI tests without skips. Its artifacts retain the wheel, candidate project/lock, dependency changes and qualification. retain the wheel, committed project/lock hashes and qualification. Dependency changes belong in the MR; this job records no implicit dependency changes. These checks alone do not establish cluster, NEF or OEG acceptance. With `SDK_BUILD_IMAGE=1`, `sdk-consumer-image` builds through the existing Loading @@ -22,7 +30,7 @@ Dockerfile using that tested project/lock and the pinned test Python image. It checks the installed wheel, runtime dependency versions and package metadata before pushing a unique `sdk-candidate-p<PIPELINE>-<SDK-SHA>` image. The image receipt records its immutable digest, both source identities, wheel checksum, lock hash and actual dependencies. Ansible deploys that finished image; it project/lock hashes, unchanged-input marker and actual dependencies. Ansible deploys that finished image; it performs no further package installation. Normal MR pipelines also build images after their usual checks, using unique Loading @@ -34,8 +42,8 @@ For local offline consumer verification, use Python 3.12 and uv 0.11.26. Set `SDK_WHEEL_FILE`, `SDK_MANIFEST_FILE`, `SDK_SOURCE_SHA`, `SDK_WHEEL_NAME` and `SDK_WHEEL_SHA256`, then run `python ci/sdk_consumer.py` in a clean checkout. Use a disk-backed `TMPDIR` and a fresh `.cache/sdk-consumer` directory. The resolver creates its own isolated environment; an existing SDK version need not be published first. Supply the matching pinned `CI_JOB_IMAGE` if preparing locked installation creates its own isolated environment from the committed candidate URL; stable SDK publication is not needed first. Supply the matching pinned `CI_JOB_IMAGE` if preparing inputs for the CI image build. SDK must admit SRM job tokens for exact build-artifact downloads. SRM admits the Loading
ci/sdk_consumer.py +49 −16 Changes for ci/sdk_consumer.py: 49 added lines, 16 removed lines. Original line number Diff line number Diff line Loading @@ -20,6 +20,9 @@ OUT = ROOT / ".cache/sdk-consumer" SHA = re.compile(r"[a-f0-9]{40}") DIGEST = re.compile(r"[a-f0-9]{64}") REGISTRY = "labs.etsi.org:5050/oop/code/service-resource-manager" CANDIDATE_REGISTRY = ( "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates" ) def digest(path): Loading Loading @@ -116,12 +119,35 @@ def prepare(root=ROOT, out=OUT, environment=None, call=subprocess.run, fetch=url project = out / "project" for name in ("pyproject.toml", "uv.lock", "README.md"): shutil.copyfile(root / name, project / name) # uv owns resolution and rewrites only this disposable project, never SRM source. call( ["uv", "add", "--no-sync", "--python", sys.executable, "wheels/" + sdk["filename"]], cwd=project, check=True, # The SRM MR owns adoption. CI only materializes its committed inputs. declaration = tomllib.loads((root / "pyproject.toml").read_text()) lock = tomllib.loads((root / "uv.lock").read_text()) packages = [p for p in lock["package"] if p["name"] == "openop-tfsdk"] require(len(packages) == 1, "The committed lock must select exactly one SDK") package = packages[0] expected_url = f"{CANDIDATE_REGISTRY}/{sdk['sha256']}/{sdk['filename']}" declared_url = ( declaration.get("tool", {}) .get("uv", {}) .get("sources", {}) .get("openop-tfsdk", {}) .get("url") ) require( package.get("version") == sdk["version"] and any( re.fullmatch( r"openop[-_]tfsdk\s*==\s*" + re.escape(sdk["version"]), item, flags=re.IGNORECASE ) for item in declaration.get("project", {}).get("dependencies", []) ) and declared_url == expected_url and package.get("source", {}).get("url") == expected_url and any(p.get("hash") == "sha256:" + sdk["sha256"] for p in package.get("wheels", [])), "SDK candidate is not pinned by the committed SRM dependency and lock; " "update them in an SRM MR before qualification", ) locked_inputs = {name: digest(root / name) for name in ("pyproject.toml", "uv.lock")} call( [ "uv", Loading @@ -144,16 +170,16 @@ def prepare(root=ROOT, out=OUT, environment=None, call=subprocess.run, fetch=url "project_sha256": digest(project / "pyproject.toml"), } def versions(path): rows = tomllib.loads(path.read_text())["package"] return {item["name"]: item.get("version") for item in rows} before, after = versions(root / "uv.lock"), versions(project / "uv.lock") proof["dependency_changes"] = [ {"name": name, "before": before.get(name), "after": after.get(name)} for name in sorted(before.keys() | after.keys()) if before.get(name) != after.get(name) ] require( all( digest(project / name) == value == digest(root / name) for name, value in locked_inputs.items() ), "Strict consumer installation changed committed dependency inputs", ) proof["dependency_changes"] = [] proof["source_inputs_unchanged"] = True proof["sdk_dependency_url"] = expected_url write(out / "candidate.json", proof) return proof Loading Loading @@ -234,8 +260,12 @@ def verify_inputs(root, out): ) project = out / "project" require( digest(project / "uv.lock") == proof["lock_sha256"] (root / "uv.lock").is_file() and (root / "pyproject.toml").is_file() and digest(project / "uv.lock") == proof["lock_sha256"] and digest(project / "pyproject.toml") == proof["project_sha256"] and digest(root / "uv.lock") == proof["lock_sha256"] and digest(root / "pyproject.toml") == proof["project_sha256"] and digest(project / "wheels" / proof["sdk"]["filename"]) == proof["sdk"]["sha256"], "Candidate inputs changed after consumer tests", ) Loading Loading @@ -349,6 +379,9 @@ def build_image(root=ROOT, out=OUT, call=subprocess.run): "lock_sha256": proof["lock_sha256"], "python_image": proof["python_image"], "dependency_changes": proof["dependency_changes"], "source_inputs_unchanged": proof["source_inputs_unchanged"], "sdk_dependency_url": proof["sdk_dependency_url"], "project_sha256": proof["project_sha256"], "runtime_dependencies": inventory["dependencies"], "consumer_checks": {"job_id": proof["qualification_job_id"], "status": "passed"}, } Loading
ci/tests/test_sdk_consumer.py +57 −0 Changes for ci/tests/test_sdk_consumer.py: 57 added lines, 0 removed lines. Original line number Diff line number Diff line Loading @@ -130,3 +130,60 @@ def test_normal_image_receipt_requires_a_real_digest(): assert result["source_sha"] == "a" * 40 and result["image"].endswith("b" * 64) with pytest.raises(ValueError, match="digest"): image_receipt.receipt({}, env) def committed_project(root, wheel, *, old=False): _, _, env = wheel url = f"{consumer.CANDIDATE_REGISTRY}/{env['SDK_WHEEL_SHA256']}/candidate.whl" version = "1.0.0" if old else "2.0.0" (root / "README.md").write_text("consumer") (root / "pyproject.toml").write_text( '[project]\ndependencies = ["openop-tfsdk==' + version + '"]\n' '[tool.uv.sources]\nopenop-tfsdk = {url = "' + url + '"}\n' ) (root / "uv.lock").write_text( '[[package]]\nname = "openop-tfsdk"\nversion = "' + version + '"\n' 'source = {url = "' + url + '"}\n' 'wheels = [{hash = "sha256:' + env["SDK_WHEEL_SHA256"] + '"}]\n' ) return env def test_strict_install_uses_committed_inputs_without_resolution(wheel, tmp_path, monkeypatch): root = tmp_path / "srm" root.mkdir() env = committed_project(root, wheel) original = {n: (root / n).read_bytes() for n in ("pyproject.toml", "uv.lock")} monkeypatch.setattr(consumer, "revision", lambda *_: "a" * 40) calls = [] proof = consumer.prepare(root, root / "out", env, call=lambda args, **kw: calls.append(args)) assert len(calls) == 1 and calls[0][:3] == ["uv", "sync", "--locked"] assert all((root / n).read_bytes() == value for n, value in original.items()) assert all((root / "out/project" / n).read_bytes() == value for n, value in original.items()) assert proof["dependency_changes"] == [] and proof["source_inputs_unchanged"] is True def test_incompatible_committed_lock_fails_before_install(wheel, tmp_path, monkeypatch): root = tmp_path / "srm" root.mkdir() env = committed_project(root, wheel, old=True) original = (root / "uv.lock").read_bytes() monkeypatch.setattr(consumer, "revision", lambda *_: "a" * 40) with pytest.raises(ValueError, match="not pinned"): consumer.prepare( root, root / "out", env, call=lambda *_a, **_k: pytest.fail("No install or resolver") ) assert (root / "uv.lock").read_bytes() == original def test_install_cannot_rewrite_the_copied_lock(wheel, tmp_path, monkeypatch): root = tmp_path / "srm" root.mkdir() env = committed_project(root, wheel) monkeypatch.setattr(consumer, "revision", lambda *_: "a" * 40) def changing_install(args, **kwargs): (kwargs["cwd"] / "uv.lock").write_text("resolved a different lock") with pytest.raises(ValueError, match="changed committed"): consumer.prepare(root, root / "out", env, call=changing_install)
pyproject.toml +2 −2 Changes for pyproject.toml: 2 added lines, 2 removed lines. Original line number Diff line number Diff line Loading @@ -21,7 +21,7 @@ dependencies = [ "sqlalchemy>=2.0.48", "asyncpg>=0.31.0", "nats-py>=2.10.0", "openop-tfsdk==2.0.0", "openop-tfsdk==2.0.1", "apscheduler<4.0.0", ] Loading @@ -39,7 +39,7 @@ dev = [ ] [tool.uv.sources] openop-tfsdk = { index = "etsi-tf-sdk" } openop-tfsdk = { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates/54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b/openop_tfsdk-2.0.1-py3-none-any.whl" } [[tool.uv.index]] name = "etsi-tf-sdk" Loading
uv.lock +34 −20 Changes for uv.lock: 34 added lines, 20 removed lines. Original line number Diff line number Diff line Loading @@ -730,12 +730,13 @@ wheels = [ [[package]] name = "openop-tfsdk" version = "2.0.0" source = { registry = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/simple" } version = "2.0.1" source = { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates/54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b/openop_tfsdk-2.0.1-py3-none-any.whl" } dependencies = [ { name = "auto-mix-prep" }, { name = "colorlog" }, { name = "kubernetes" }, { name = "pyasn1" }, { name = "pydantic" }, { name = "pydantic-extra-types" }, { name = "pymongo" }, Loading @@ -743,9 +744,22 @@ dependencies = [ { name = "requests" }, { name = "structlog" }, ] sdist = { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/files/229082cbb098e089dd8f8a7f90395ee7d18ea2a72e91a975940f3fa1ebcaa973/openop_tfsdk-2.0.0.tar.gz", hash = "sha256:229082cbb098e089dd8f8a7f90395ee7d18ea2a72e91a975940f3fa1ebcaa973" } wheels = [ { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/files/6431c6da12b664e69a7a27ea3b35c84520c17ec42acba7caa8fdde5f6899da55/openop_tfsdk-2.0.0-py3-none-any.whl", hash = "sha256:6431c6da12b664e69a7a27ea3b35c84520c17ec42acba7caa8fdde5f6899da55" }, { url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates/54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b/openop_tfsdk-2.0.1-py3-none-any.whl", hash = "sha256:54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b" }, ] [package.metadata] requires-dist = [ { name = "auto-mix-prep", specifier = "==0.2.0" }, { name = "colorlog", specifier = "==6.8.2" }, { name = "kubernetes", specifier = "==33.1.0" }, { name = "pyasn1", specifier = "==0.6.5" }, { name = "pydantic", specifier = "==2.11.3" }, { name = "pydantic-extra-types", specifier = "==2.10.3" }, { name = "pymongo", specifier = "==4.18.3" }, { name = "pyyaml", specifier = "==6.0.3" }, { name = "requests", specifier = "==2.33.0" }, { name = "structlog", specifier = ">=25.5.0" }, ] [[package]] Loading Loading @@ -802,11 +816,11 @@ wheels = [ [[package]] name = "pyasn1" version = "0.6.4" version = "0.6.5" source = { registry = "https://pypi.org/simple" } sdist = { url = "https://files.pythonhosted.org/packages/a4/9a/23310166d960def5897e91fe20e5b724601b02a22e84ba1f94232c0b7f67/pyasn1-0.6.4.tar.gz", hash = "sha256:9c447d8431c947fe4c8febc4ed9e760bc29011a5b01e5c74b67025bd9fb8ce81", size = 151262, upload-time = "2026-07-09T01:12:33.988Z" } sdist = { url = "https://files.pythonhosted.org/packages/88/7e/3c0da41061a2021cac7cd524fd57092adbe5b515e8bf9005702e366802e2/pyasn1-0.6.5.tar.gz", hash = "sha256:6b11616a6bcf3a40e635f082bb232fa57b5e0dd7f517bd7ddd69ec3794508a47", size = 155152, upload-time = "2026-10-27T06:32:49.125Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3b/6163796d69c3977d1e4287bea4a6979161cbbdd170ebb430511e8e1999ce/pyasn1-0.6.4-py3-none-any.whl", hash = "sha256:deda9277cfd454080ec40b207fb6df82206a3a2688735233cdcd8d3d565f088b", size = 84410, upload-time = "2026-07-09T01:12:32.92Z" }, { url = "https://files.pythonhosted.org/packages/19/1f/3eefd5c84c35cae44549ce1c7f7e58f7945b34f5a6bce07a135438522073/pyasn1-0.6.5-py3-none-any.whl", hash = "sha256:4017b0fce125096b8573b39c989fdb1a4b19cf761e683398184550fea0e66587", size = 84807, upload-time = "2026-10-27T06:32:48.045Z" }, ] [[package]] Loading Loading @@ -922,23 +936,23 @@ wheels = [ [[package]] name = "pymongo" version = "4.18.2" version = "4.18.3" source = { registry = "https://pypi.org/simple" } dependencies = [ { name = "dnspython" }, ] sdist = { url = "https://files.pythonhosted.org/packages/dc/04/455039b475bb8afa5af9b8c363fb773a45d898016f04ee40c1c75003a3f9/pymongo-4.18.2.tar.gz", hash = "sha256:1bd4321cc63c06954953eae9eb8fb5d9075296b96c0e3ed34ccb91a0dcce31a5", size = 2746691, upload-time = "2026-09-24T16:59:59.472Z" } sdist = { url = "https://files.pythonhosted.org/packages/42/d8/2421a5ae0d6dcdaad2a0fb75d4071eaede9f764e73b829c62b6185c3ee6b/pymongo-4.18.3.tar.gz", hash = "sha256:5dd6e659b6014288a1c53458929402a58f44a032e6f29bcef44e7477c5268e48", size = 2747872, upload-time = "2026-10-08T19:44:08.343Z" } wheels = [ { url = "https://files.pythonhosted.org/packages/10/eb/995ec978741cfc56a29223ec0806976218b95d79a516d9b719c984ead130/pymongo-4.18.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:78a3b324407ba7228a57f207e175efc40341c1312d1e7df2d4ad8141ff6d8e00", size = 819057, upload-time = "2026-09-24T16:58:35.844Z" }, { url = "https://files.pythonhosted.org/packages/40/e4/5a166bbac7f6e620cfb75b0640e793dd3ce69423a1f89cf6a0bfc588c107/pymongo-4.18.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:498187b5171d5efa87291b30e4cbf1e9849929c02bc436e95a3d01ff19e49f42", size = 819448, upload-time = "2026-09-24T16:58:37.221Z" }, { url = "https://files.pythonhosted.org/packages/c9/c8/82194d9a1387a56cac996901f92ea6a25ae817622ee32d15c986638b263f/pymongo-4.18.2-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2c003646d3fa5db4e835483fc29018eb06d8c637cf69f676ffbb3f0734005220", size = 1039602, upload-time = "2026-09-24T16:58:38.821Z" }, { url = "https://files.pythonhosted.org/packages/cc/06/628f93f15f269d8a1ea20a024083e67871f722876a33992aec5525a6c6c4/pymongo-4.18.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c743347143298252948e038f768ea6f71b71ef8a85b7cf0dc13b734e072f74da", size = 1050746, upload-time = "2026-09-24T16:58:40.322Z" }, { url = "https://files.pythonhosted.org/packages/bf/93/58ea76803128651ec9c732b59b8c1eb73d511c085c2e5786da3289d4015c/pymongo-4.18.2-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:09e6afa68001433c42925a258b57f2ceccb95c73fe8920452e147a82d90c1ae9", size = 1075681, upload-time = "2026-09-24T16:58:41.722Z" }, { url = "https://files.pythonhosted.org/packages/7c/6d/702ee4593420e2a8ceb169ece6394f0bb9fc2e8e8d8101e99f2468ed6d0d/pymongo-4.18.2-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:8d3445caeeb47dc64bf416dc03e0b3bfcf3b60a109dec355a563aa62c135cb33", size = 1068235, upload-time = "2026-09-24T16:58:43.228Z" }, { url = "https://files.pythonhosted.org/packages/a9/92/a02a4108ca4faf05bca394d65dcebc7b84b0248cff23aa89927c81a0d5dd/pymongo-4.18.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ecde9e735e1f86559e0a71a5bbec671e59a35549bfd8c60968d9f032b05013de", size = 1049896, upload-time = "2026-09-24T16:58:44.914Z" }, { url = "https://files.pythonhosted.org/packages/6e/93/65ef17eecdd86c153f7907b2d490d85eb655d3a5eb35a8494e7e09376353/pymongo-4.18.2-cp312-cp312-win32.whl", hash = "sha256:a463c3fc072655b7fe9e9ded4601876bef7f1df98f6f3faeed07fa7f11309fff", size = 815124, upload-time = "2026-09-24T16:58:46.39Z" }, { url = "https://files.pythonhosted.org/packages/3c/cc/fd66aa9d8956e8269ea63b2977a7fd38fbec45795d38b8067bfafc98992f/pymongo-4.18.2-cp312-cp312-win_amd64.whl", hash = "sha256:cbb67901ba9b6334fa94643d66587a30ffe5be05061af287364e6b5f7d239dc5", size = 821996, upload-time = "2026-09-24T16:58:47.769Z" }, { url = "https://files.pythonhosted.org/packages/06/21/983d340cfe226c987014f1caf9568b176cbd7f25dd87536379a609e6371e/pymongo-4.18.2-cp312-cp312-win_arm64.whl", hash = "sha256:94c5e0fe9af4c05f382ae7c4addd0255a8ddcc91b3d2a6bc09b34230cb28290b", size = 815707, upload-time = "2026-09-24T16:58:49.493Z" }, { url = "https://files.pythonhosted.org/packages/05/d5/4775a2891396ad125545e23b3024adae4bfac9553b70c924f1f372269dbf/pymongo-4.18.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ea78719dd05de3a919a52b94bec790c0d0cb7d07d2f7271711832664502a0782", size = 819374, upload-time = "2026-10-08T19:42:26.931Z" }, { url = "https://files.pythonhosted.org/packages/e0/0b/89ad56f43c3da6cbde100699f6b99528e78eba3c6740d8dad4ea2516aa45/pymongo-4.18.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6029d14761ba7243e6c5e464592013b519ad4dd3e4cfb75ddec39f4b5910711b", size = 819694, upload-time = "2026-10-08T19:42:28.76Z" }, { url = "https://files.pythonhosted.org/packages/84/b4/b68ffc205441b0a6d36d6299e35e063a5d0d3264fd685428920e1f82b634/pymongo-4.18.3-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:9536fb3820f721290f03ad07472ec2266d8f364f91de628679a7146c9c1dbe35", size = 1040136, upload-time = "2026-10-08T19:42:30.852Z" }, { url = "https://files.pythonhosted.org/packages/c1/40/e779ff3d9165316c35a2f9742a42b9c3e3a678e9e2a9f6fe4128b7c551eb/pymongo-4.18.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e461bfca4861057929efa4215730b28b93b2adb4d07828d0b65475755bbf63f5", size = 1051430, upload-time = "2026-10-08T19:42:32.533Z" }, { url = "https://files.pythonhosted.org/packages/07/9b/443ee038a739cc65a75f2078c9ef725c1cb4881545d2e9d7941c46f64a6c/pymongo-4.18.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1fef248623ed5e7406902a68d49dc0b1db434f19489f8d2fc9fe512c3c08bb1", size = 1075464, upload-time = "2026-10-08T19:42:34.325Z" }, { url = "https://files.pythonhosted.org/packages/36/4b/d80518f675cd4c1215b770444bb83002454574dae0e69760af10703ed1e8/pymongo-4.18.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:213eaed8fc4f2b0f9c84323a229dea699e01e18b8fb39723f430123b6ee77813", size = 1069046, upload-time = "2026-10-08T19:42:36.105Z" }, { url = "https://files.pythonhosted.org/packages/e5/77/f2e9648c62e423c3b9dab1e16491a6c33250487c819e5c75784b35d16047/pymongo-4.18.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:aa6f363ff648bf061335d2190dd580cbf465b1308a7e6acb992d128d6a16a3bd", size = 1050321, upload-time = "2026-10-08T19:42:38.052Z" }, { url = "https://files.pythonhosted.org/packages/a6/e4/3236e3a87b29fc4c502ad7dd1521c20d4a29faf1db6fde6ae94d05c5ec27/pymongo-4.18.3-cp312-cp312-win32.whl", hash = "sha256:28ba8cae86ea02d7ffdf0eea81be69be80d35d6a4a3eba4dc436d3194341805a", size = 815300, upload-time = "2026-10-08T19:42:40.062Z" }, { url = "https://files.pythonhosted.org/packages/1e/18/3fa9d86ba32386c02ea991f10875a6a066dd5e5d80790243be3c141b0e73/pymongo-4.18.3-cp312-cp312-win_amd64.whl", hash = "sha256:dc8ccf72b76c99a6b9fd05f8b89fe4a693128c5cfdba70f70e5792a6a563f6b0", size = 822079, upload-time = "2026-10-08T19:42:42.089Z" }, { url = "https://files.pythonhosted.org/packages/03/50/65a7cefd3891b77994841992b2c5b59394667df64ef21377b8ac7ecdef47/pymongo-4.18.3-cp312-cp312-win_arm64.whl", hash = "sha256:4a1f7c7dc1d554449a1695d897eb42b6080a2f1e9ccd81385dfa00204979c54d", size = 817935, upload-time = "2026-10-08T19:42:43.98Z" }, ] [[package]] Loading Loading @@ -1208,7 +1222,7 @@ requires-dist = [ { name = "import-linter", marker = "extra == 'dev'", specifier = ">=2.11" }, { name = "mypy", marker = "extra == 'dev'", specifier = ">=1.19.1" }, { name = "nats-py", specifier = ">=2.10.0" }, { name = "openop-tfsdk", specifier = "==2.0.0", index = "https://labs.etsi.org/rep/api/v4/projects/396/packages/pypi/simple" }, { name = "openop-tfsdk", url = "https://labs.etsi.org/rep/api/v4/projects/396/packages/generic/openop-tfsdk-candidates/54f5822e813572b88d32bc55bc53107828b9b4b09b99f1fbee9d164b68e47d7b/openop_tfsdk-2.0.1-py3-none-any.whl" }, { name = "pre-commit", marker = "extra == 'dev'", specifier = ">=4.5.1" }, { name = "pydantic-settings", specifier = ">=2.13.1" }, { name = "pytest", marker = "extra == 'dev'", specifier = ">=9.0.2" }, Loading