Commit eadd1ff2 authored by Konstantinos Togias's avatar Konstantinos Togias
Browse files

ci: qualify the SDK from explicit committed consumer inputs

parent c9fbc9ba
Loading
Loading
Loading
Loading
+19 −11
Changes for ci/sdk-consumer.md: 19 added lines, 11 removed lines.
Original line number Diff line number Diff line
@@ -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
@@ -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
@@ -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
+49 −16
Changes for ci/sdk_consumer.py: 49 added lines, 16 removed lines.
Original line number Diff line number Diff line
@@ -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):
@@ -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",
@@ -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

@@ -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",
    )
@@ -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"},
    }
+57 −0
Changes for ci/tests/test_sdk_consumer.py: 57 added lines, 0 removed lines.
Original line number Diff line number Diff line
@@ -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)
+2 −2
Changes for pyproject.toml: 2 added lines, 2 removed lines.
Original line number Diff line number Diff line
@@ -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",
]

@@ -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"
+34 −20
Changes for uv.lock: 34 added lines, 20 removed lines.
Original line number Diff line number Diff line
@@ -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" },
@@ -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]]
@@ -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]]
@@ -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]]
@@ -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" },