From 235b4da5439f61c7a3969dcc4b2bfb6dde57cbeb Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Thu, 23 Jul 2026 15:53:48 -0400 Subject: [PATCH 1/4] fix: source edx-platform requirements from uv.lock when present openedx-platform is migrating requirements/edx/*.txt and requirements/edx-sandbox/base.txt from pip-compile output to pyproject.toml + uv.lock, and plans to remove the .txt compat exports once known consumers stop reading them directly (openedx/public-engineering#552). master picks up the migration first; named releases won't for a while, if ever. Branch install_deps, _python_only_env, build_platform's test requirements, and codejail's sandbox install on whether the checkout has grown a uv.lock, rather than hardcoding release_name == "master". That keeps both tracks working through the transition and picks up master's migration automatically the moment it lands, without another lehrer change. --- src/lehrer/core/codejail.py | 54 ++++++++++++++++++++----- src/lehrer/core/platform.py | 79 ++++++++++++++++++++++++++++++------- 2 files changed, 109 insertions(+), 24 deletions(-) diff --git a/src/lehrer/core/codejail.py b/src/lehrer/core/codejail.py index 4f764e37..8b91b43a 100644 --- a/src/lehrer/core/codejail.py +++ b/src/lehrer/core/codejail.py @@ -1,5 +1,7 @@ """Generic codejail service build for Open edX operators.""" +import shlex + import dagger from dagger import dag, function, object_type @@ -95,6 +97,7 @@ async def _build( "-y", "--no-install-recommends", "build-essential", + "curl", "python3-virtualenv", "python3-pip", "git", @@ -172,22 +175,53 @@ async def _build( ["pip", "install", "--no-cache-dir", "-r", "requirements/base.txt"] ).with_exec(["pip", "install", "--no-cache-dir", "gunicorn"]) - # Install edx-platform sandbox requirements in virtualenv - # The URL pattern differs based on whether it's a release or master - sandbox_req_url = ( - f"https://raw.githubusercontent.com/openedx/edx-platform/master/requirements/edx-sandbox/releases/{release_name}.txt" - if release_name != "master" - else "https://raw.githubusercontent.com/openedx/edx-platform/master/requirements/edx-sandbox/base.txt" - ) - - import shlex + # Install edx-platform sandbox requirements in virtualenv. + # + # Named releases haven't picked up the pip-compile -> uv migration + # and keep publishing a per-release export; read it directly as + # before. master's sandbox is now its own standalone uv project + # (requirements/edx-sandbox/{pyproject.toml,uv.lock}), with + # requirements/edx-sandbox/base.txt kept only as a temporary + # machine-generated compat export slated for removal once known + # consumers (including this one) stop reading it -- see + # openedx/public-engineering#552. Try the compat export first (fast, + # no uv needed) and fall back to a uv export of the sub-project once + # it's gone, so this keeps working across that removal without a + # lehrer change. + if release_name != "master": + sandbox_req_url = ( + "https://raw.githubusercontent.com/openedx/edx-platform/master/" + f"requirements/edx-sandbox/releases/{release_name}.txt" + ) + install_sandbox_reqs = ( + f"pip install --no-cache-dir -r {shlex.quote(sandbox_req_url)}" + ) + else: + sandbox_base_url = ( + "https://raw.githubusercontent.com/openedx/edx-platform/master/" + "requirements/edx-sandbox" + ) + install_sandbox_reqs = ( + "set -eu && " + "if curl -fsSL -o /tmp/edx_sandbox.txt " + f"{shlex.quote(sandbox_base_url + '/base.txt')}; then :; else " + "mkdir -p /tmp/edx-sandbox-uv && cd /tmp/edx-sandbox-uv && " + "curl -fsSL -o pyproject.toml " + f"{shlex.quote(sandbox_base_url + '/pyproject.toml')} && " + f"curl -fsSL -o uv.lock {shlex.quote(sandbox_base_url + '/uv.lock')} && " + "pip install --quiet uv && " + "uv export --frozen --no-hashes --no-emit-project " + "-o /tmp/edx_sandbox.txt; " + "fi && " + "pip install --no-cache-dir -r /tmp/edx_sandbox.txt" + ) container = container.with_exec( [ "bash", "-c", f"source /sandbox/venv/bin/activate && " - f"pip install --no-cache-dir -r {shlex.quote(sandbox_req_url)} && " + f"{install_sandbox_reqs} && " f"deactivate", ] ) diff --git a/src/lehrer/core/platform.py b/src/lehrer/core/platform.py index edc164c4..ad133c6f 100644 --- a/src/lehrer/core/platform.py +++ b/src/lehrer/core/platform.py @@ -457,6 +457,53 @@ def _derive_test_settings(service: str) -> str: ) +def _edx_base_requirements_script(*, base_dest: str, assets_dest: str) -> str: + """Shell script that sources edx-platform's base + assets requirements. + + openedx-platform is mid-migration from pip-compile (``requirements/edx/ + {base,assets}.txt``) to ``pyproject.toml``/``uv.lock`` as the source of + truth; the ``.txt`` compat exports are slated for removal once known + consumers move off them (openedx/public-engineering#552). Named releases + haven't picked up the migration and won't grow a ``uv.lock`` until they + do, so branching on ``uv.lock``'s presence -- rather than hardcoding + ``release_name == "master"`` -- keeps both tracks working, and picks up + master's migration automatically without a lehrer change once it lands. + + Must run with cwd (or an explicit ``cd``) at the edx-platform checkout. + """ + return ( + "set -eu\n" + "cd /openedx/edx-platform\n" + "if [ -f uv.lock ]; then\n" + " uv export --frozen --no-hashes --no-emit-project" + f" --no-default-groups -o {base_dest}\n" + " uv export --frozen --no-hashes --no-emit-project" + f" --no-default-groups --only-group assets -o {assets_dest}\n" + "else\n" + f" cp requirements/edx/base.txt {base_dest}\n" + f" cp requirements/edx/assets.txt {assets_dest}\n" + "fi\n" + ) + + +def _edx_testing_requirements_script(dest: str) -> str: + """Shell script that sources edx-platform's test-suite requirements. + + Same uv.lock-presence branch as ``_edx_base_requirements_script`` — see + its docstring and openedx/public-engineering#552. + """ + return ( + "set -eu\n" + "cd /openedx/edx-platform\n" + "if [ -f uv.lock ]; then\n" + " uv export --frozen --no-hashes --no-emit-project" + f" --no-default-groups --only-group testing -o {dest}\n" + "else\n" + f" cp requirements/edx/testing.txt {dest}\n" + "fi\n" + ) + + @object_type class OpenedxPlatform: """Generic edx-platform build pipeline. @@ -723,19 +770,16 @@ def install_deps( .with_mounted_directory( "/root/pip_package_overrides", pip_package_overrides ) - # Copy base requirements from edx-platform + # Source base + assets requirements from edx-platform (uv.lock + # export or legacy pip-compile .txt, whichever the checkout has). .with_exec( [ "sh", "-c", - "cp /openedx/edx-platform/requirements/edx/base.txt /root/pip_package_lists/edx_base.txt", - ] - ) - .with_exec( - [ - "sh", - "-c", - "cp /openedx/edx-platform/requirements/edx/assets.txt /root/pip_package_lists/edx_assets.txt", + _edx_base_requirements_script( + base_dest="/root/pip_package_lists/edx_base.txt", + assets_dest="/root/pip_package_lists/edx_assets.txt", + ), ] ) # Install base Python dependencies using uv (much faster than pip) @@ -1585,10 +1629,10 @@ def _python_only_env( [ "sh", "-c", - "cp /openedx/edx-platform/requirements/edx/base.txt" - " /root/pip_package_lists/edx_base.txt" - " && cp /openedx/edx-platform/requirements/edx/assets.txt" - " /root/pip_package_lists/edx_assets.txt", + _edx_base_requirements_script( + base_dest="/root/pip_package_lists/edx_base.txt", + assets_dest="/root/pip_package_lists/edx_assets.txt", + ), ] ) ) @@ -2294,7 +2338,14 @@ async def test( # noqa: PLR0913 container = ( container.with_workdir("/openedx/edx-platform") .with_exec(["uv", "pip", "install", "-e", "."]) - .with_exec(["uv", "pip", "install", "-r", "requirements/edx/testing.txt"]) + .with_exec( + [ + "sh", + "-c", + _edx_testing_requirements_script("/tmp/edx_testing.txt") + + "uv pip install -r /tmp/edx_testing.txt\n", + ] + ) ) # When folding the plugins' own suites into this run, derive the plugin From da5269d9e106ad37786ed49239fca85ba3bbe3d9 Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Thu, 23 Jul 2026 16:13:37 -0400 Subject: [PATCH 2/4] fix: install edx-platform deps via uv sync, add a dev target Switch from # This file was autogenerated by uv via the following command: # uv export -e . annotated-types==0.7.0 \ --hash=sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53 \ --hash=sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89 # via pydantic anyio==4.14.1 \ --hash=sha256:4e5533c5b8ff0a24f5d7a176cbe6877129cd183893f66b537f8f227d10527d72 \ --hash=sha256:8d648a3544c1a700e3ff78615cd679e4c5c3f149904287e73687b2596963629e # via # dagger-io # gql # httpx attrs==26.1.0 \ --hash=sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309 \ --hash=sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32 # via # cattrs # cyclopts backoff==2.2.1 \ --hash=sha256:03f829f5bb1923180821643f8753b0502c3b682293992485b0eef2807afa5cba \ --hash=sha256:63579f9a0628e06278f7e47b7d7d5b6ce20dc65c5e96a6f3ca99a6adca0396e8 # via gql beartype==0.22.9 \ --hash=sha256:8f82b54aa723a2848a56008d18875f91c1db02c32ef6a62319a002e3e25a975f \ --hash=sha256:d16c9bbc61ea14637596c5f6fbff2ee99cbe3573e46a716401734ef50c3060c2 # via dagger-io cattrs==26.1.0 \ --hash=sha256:d1e0804c42639494d469d08d4f26d6b9de9b8ab26b446db7b5f8c2e97f7c3096 \ --hash=sha256:fa239e0f0ec0715ba34852ce813986dfed1e12117e209b816ab87401271cdd40 # via dagger-io certifi==2026.6.17 \ --hash=sha256:024c88eeec92ca068db80f02b8b07c9cef7b9fe261d1d535abfd5abd6f6af432 \ --hash=sha256:2227dcbaafe0d2f59279d1762ddddc37783ed4354594f194ffc31d20f41fc3db # via # httpcore # httpx # requests cfgv==3.5.0 \ --hash=sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0 \ --hash=sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132 # via pre-commit charset-normalizer==3.4.9 \ --hash=sha256:0327fcd59a935777d83410750c50600ee9571af2846f71ce40f25b13da1ef380 \ --hash=sha256:0d861473f743244d349b50f850d10eb87aeb22bbdcc8e64f79273c94af5a8226 \ --hash=sha256:16b65ea0f2465b6fb52aa22de5eca612aa964ddfec00a912e26f4656cbef890b \ --hash=sha256:19ac87f93086ce37b86e098888555c4b4bc48102279bae3350098c0ed664b501 \ --hash=sha256:21e764fd1e70b6a3e205a0e46f3051701f98a8cb3fad66eeb80e48bb502f8698 \ --hash=sha256:32286a2c8d167e897177b673176c1e3e00d4057caf5d2b64eef9a3666b03018e \ --hash=sha256:33bdcc2a32c0a0e861f60841a512c8acc658c87c2ac59d89e3a46dacf7d866e4 \ --hash=sha256:40a126142a56b2dfc0aacbad1de8310cbf60da7656db0e6b16eebd48e3e93519 \ --hash=sha256:416c229f77e5ea25b3dfd4b582f8d73d7e43c22320302b9ab128a2d3a0b38efe \ --hash=sha256:440eede837960000d74978f0eba527be106b5b9aee0daf779d395276ed0b0614 \ --hash=sha256:4d1c96a7a18b9690a4d46df09e3e3382406ae3213727cd1019ebade1c4a81917 \ --hash=sha256:51307f5c71007673a2bf8232ad973483d281e74cb99c8c5a990af1eefa6277d9 \ --hash=sha256:51447e9aa2684679af07ca5021c3db526e0284347ebf4ffcec1154c3350cfe32 \ --hash=sha256:5b10cd92fc5c498b35a8635df6d5a100207f88b63a4dc1de7ef9a548e1e2cd63 \ --hash=sha256:609b3ba8fcc0fb5ab7af00719d0fb6ad0cb518e48e7712d12fd68f1327951198 \ --hash=sha256:611057cc5d5c0afc743ba8be6bd828c17e0aaa8643f9d0a9b9bb7dea80eb8012 \ --hash=sha256:673611bbd43f0810bec0b0f028ddeaaa501190339cac411f347ac76917c3ae7b \ --hash=sha256:68e5f26a1ad57ded6d1cfb85331d1c1a195314756471d97758c48498bb4dcdf5 \ --hash=sha256:69b157c5d3292bcd443faca052f3096f637f1e074b98212a933c074ae23dc3b8 \ --hash=sha256:75286256590a6320cf106a0d28970d3560aad9ee09aa7b34fb40524792436d35 \ --hash=sha256:83aed2c10721ddd90f68140685391b50811a880af20654c59af6b6c66c40513c \ --hash=sha256:84fd18bcc17526fc2b3c1af7d2b9217d32c9c04448c16ec693b9b4f1985c3d33 \ --hash=sha256:898f0e9068ca27d37f8e83a5b962821df851532e6c4a7d615c1c033f9da6eedf \ --hash=sha256:8a79d9f4d8001473a30c163556b3c3bfebec837495a412dde78b51672f6134f9 \ --hash=sha256:9b8e0f3107e2200b76f6054de99016eac3ee6762713587b36baaa7e4bd2ae177 \ --hash=sha256:a4cfde78a9f2880208d16a93b795726a3017d5977e08d1e162a7a31322479c41 \ --hash=sha256:a4fbdde9dd4a9ce5fd52c2b3a347bb50cc89483ef783f1cb00d408c13f7a96c0 \ --hash=sha256:bd47ba7fc3ca94896759ea0109775132d3e7ab921fbf54038e1bab2e46c313c9 \ --hash=sha256:c1c948747b03be832dceed96ca815cef7360de9aa19d37c730f8e3f6101aca48 \ --hash=sha256:c25fe15c70c59eb7c5ce8c06a1f3fa1da0ecc5ea1e7a5922c40fd2fa9b0d5046 \ --hash=sha256:cc1b0fff8ead343dae06305f954eb8468ba0ec1a97881f42489d198e4ce3c632 \ --hash=sha256:cd6c3d4b783c556fa00bf540854e42f135e2f256abd29669fcd0da0f2dec79c2 \ --hash=sha256:d4d6fcde76f94f5cb9e43e9e9a61f16dacefd228cbbf6f1a09bd9b219a92f1a1 \ --hash=sha256:df115d4d83168fdf2cae48ef1ff6d1cb4c466364e30861b37121de0f3bf1b990 \ --hash=sha256:e4fd89cc178bced6ad29cb3e6dd4aa63fa5017c3524dbd0b25998fb64a87cc8b \ --hash=sha256:ee2f2a527e3c1a6e6411eb4209642e138b544a2d72fe5d0d76daf77b24063534 \ --hash=sha256:f7fb7d750cfa0a070d2c24e831fd3481019a60dd317ea2b39acbcebc08b6ed81 \ --hash=sha256:f840ed6d8ecba8255df8c42b87fadeda98ddfc6eeec05e2dc66e26d46dd6f58a \ --hash=sha256:f86c6358749bd4fda175388691e3ba8c46e24c5347d0afd20f9b7edfc9faf07d \ --hash=sha256:fa36ec09ef71d158186bc79e359ff5fdd6e7996fe8ab638f00d6b93139ba4fcf \ --hash=sha256:fe2c7201c642b7c308f1675355ad7ff7b66acfe3541625efe5a3ad38f29d6115 # via requests colorama==0.4.6 ; sys_platform == 'win32' \ --hash=sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44 \ --hash=sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6 # via pytest cyclopts==4.21.0 \ --hash=sha256:477c18c791c924cca4836f79fce000a7bae45f551e340d9e1654e102c6d9ab9d \ --hash=sha256:ded3ddb15b0c815f44d245011fc4cdd5a4809a3bb8202869e9e02195a87c0e18 # via lehrer dagger-io==0.21.7 \ --hash=sha256:06b9213a5ea45234f9cd511feda6cc13dc5ceebecf412476097ae4a63ba85698 \ --hash=sha256:18e825f3c703d84b88bbd90f05bcbea699fac27e93ed689cd369460d3f67db43 # via lehrer distlib==0.4.3 \ --hash=sha256:4b0ce306c966eb73bc3a7b6abad017c556dadd92c44701562cd528ac7fde4d5b \ --hash=sha256:f152097224a0ae24be5a0f6bae1b9359af82133bce63f98a95f86cae1aede9ed # via virtualenv docstring-parser==0.18.0 \ --hash=sha256:292510982205c12b1248696f44959db3cdd1740237a968ea1e2e7a900eeb2015 \ --hash=sha256:b3fcbed555c47d8479be0796ef7e19c2670d428d72e96da63f3a40122860374b # via cyclopts exceptiongroup==1.3.1 \ --hash=sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219 \ --hash=sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598 # via dagger-io filelock==3.29.7 \ --hash=sha256:5b481979797ae69e72f0b389d89a80bdd585c260c5b3f1fb9c0a5ba9bb3f195d \ --hash=sha256:987db6f789a3a2a59f55081801b2b3697cb97e2a736b5f1a9e99b559285fbc51 # via # python-discovery # virtualenv googleapis-common-protos==1.75.0 \ --hash=sha256:53a062ff3c32552fbd62c11fe23768b78e4ddf0494d5e5fd97d3f4689c75fbbd \ --hash=sha256:961ed60399c457ceb0ee8f285a84c870aabc9c6a832b9d37bb281b5bebde43ed # via opentelemetry-exporter-otlp-proto-http gql==4.0.0 \ --hash=sha256:f22980844eb6a7c0266ffc70f111b9c7e7c7c13da38c3b439afc7eab3d7c9c8e \ --hash=sha256:f3beed7c531218eb24d97cb7df031b4a84fdb462f4a2beb86e2633d395937479 # via dagger-io graphql-core==3.2.11 \ --hash=sha256:0b3e35ff41e9adba53021ab0cef475eb18f57c7f53f0f2ca55567fbf3c537ea0 \ --hash=sha256:e7e156d10beb127cab5c89ff0da71416fc73d27c484a4757d3b2d35633774802 # via gql h11==0.16.0 \ --hash=sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1 \ --hash=sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86 # via httpcore httpcore==1.0.9 \ --hash=sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55 \ --hash=sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8 # via # dagger-io # httpx httpx==0.28.1 \ --hash=sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc \ --hash=sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad # via gql identify==2.6.19 \ --hash=sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a \ --hash=sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842 # via pre-commit idna==3.18 \ --hash=sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2 \ --hash=sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848 # via # anyio # httpx # requests # yarl iniconfig==2.3.0 \ --hash=sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730 \ --hash=sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12 # via pytest librt==0.13.0 ; platform_python_implementation != 'PyPy' \ --hash=sha256:0d38604854e8d22faadf683ec6c02bb0f886e2ba56ef981a1c36ee275f21ea22 \ --hash=sha256:109b84a9edf69ad89dc1f66358659e14a031baca95e3e5b0060bd903ede8efd6 \ --hash=sha256:1304368a3e7ffc3e9db986796cc5326fdb5943a3567ecc137cff318e4240c0e7 \ --hash=sha256:1ce61b3746545029d4f5c17d6bd74b676254ad98433086c846ffb5e8fa73f007 \ --hash=sha256:1d2a610c14ac0d0750ee0a3ab8548e83155258387891caaca04def4bf7289781 \ --hash=sha256:22034924f5b42d5a56371cf271771bfeaabf235a7a8b6264bef2d20013f786c6 \ --hash=sha256:25218d94b1d2cbc0ba1d8a3f9dc9af578d9646e5ed16443a70cde1dfdcce6d71 \ --hash=sha256:260c33e92263fa629b4f6d3c51967a1c2158fe6c33237aaa3ebeac586b085259 \ --hash=sha256:2f281549a4c52ac7bb97997f14353f8bd0e53a34ca0dad1c905cfd0b4a58ae99 \ --hash=sha256:301067672387902c55f94b51d5022304b36c966ea9fe1f21caab99a9bef487c9 \ --hash=sha256:32c26893cd085c1efe83219e78d866da23fb20a066101b8f68210004361d224c \ --hash=sha256:371f7ce73026815dafd51c50ce38416e91428b28c4b2ec97cd39271164b0045c \ --hash=sha256:375f5af8f99cbaa99dd293af986e3d57caabc9ba81a5d3f021603764854197a1 \ --hash=sha256:387e2f1d27e89bffe0d3f520f0da0662c973fd607ca16c1808f8a5085419485e \ --hash=sha256:3aaedf52171bee90860704c560bc798fe83b76247df47568e0197e9b13c735a0 \ --hash=sha256:4000d961ff9598ac6ea603c6c836a5ed49bc205ade5fc378b998dfe1e2c36628 \ --hash=sha256:46c330e82565962c761dbce7941be2cff7db674ee807455a8d0cadc5f9b759b0 \ --hash=sha256:4f6db193d2e5e0ed60359b9a5a682cd67205d0d3b1e459a867dd4b5c4e7eaa7a \ --hash=sha256:531b2df3e9fe96b1fcf73a6d165921e4656be5f58d631d384ebce344298368db \ --hash=sha256:54dab44a847d5ad1acd05c8a83fe518ae685516ecf4d3f7cc6e3df2a66767650 \ --hash=sha256:5929da1981a46bcf4b28b1b9499905f0ff58e2419da402a048234e9783acbc4b \ --hash=sha256:5fdcf34f86de8fb66d7dc7589f96ba91c4aa46671200d400e6fd6f109a483f18 \ --hash=sha256:68a5faee4bba381cb93b5961f684a514cf0053cb92308ff9c792c2fea0b174c6 \ --hash=sha256:6bf6a559ffe4a93bbea6cf31ddf01a7fd9ba342ef51f27beb178e318b74acd61 \ --hash=sha256:79e44cff71750d299d61a678e49995b0d5935a9cda238c2574daeca3ba536927 \ --hash=sha256:9320d34c3376ae204b2cd176e8d4883a013934e0aef822f1aed9c536490c275d \ --hash=sha256:94b85d664d777bab6c0d709416cb42938251fda9e221b79e3a2215d85df5f4f9 \ --hash=sha256:96bad8725a4f196a798366c25ce075d1f7543a4ec045ffc13e6a7ec095cdab04 \ --hash=sha256:9af313c66157a69dc69ea0059a66961692250e0dc95af9c385a48ffb770a0d16 \ --hash=sha256:9f836c37478f167a81200d8c8b2c920a22224564bed2c23d7aeec760965c367a \ --hash=sha256:a38fb81d8376dfa2f8963b265fec07637802b0d01e2a127c19c66cb070fb24f5 \ --hash=sha256:a3dfe4edf10e8ed7e55b026a8bfc2c2a8704218b659cd4bffdf604fab966dc39 \ --hash=sha256:a4517d47b2b8af26975a406fba7d314de9696d864252e0257c6ea90238cfe27f \ --hash=sha256:a468951af16155824e88bdd8326ebe5bdb371f3ec0ac04642994b98201d914f3 \ --hash=sha256:ae01d8512cc17079e53425635327dbf3f7ff57a42c00dec348bf79791c56444c \ --hash=sha256:c7897db4e95e22468bdda33d8e012ceacd0182abf001e6389d763f0def6286b9 \ --hash=sha256:d4c8d9bd5abce34b2e75edb3bf37ab0f34e49b1f915a40ae8468eb7c85bc5b46 \ --hash=sha256:d4cb6fbfdf874340ab5e51450753c0f817b6958a3621125ee695bbc3de866566 \ --hash=sha256:e4f9b472e7d308d94b62c801982065661158c6ed02790d6c7ddb4337cea0f9c1 \ --hash=sha256:f19e181de5b3a1148bb3420b8c4b0b0ea0fce6950099724ad151d6cea5acc180 \ --hash=sha256:f26629539d4893c2957a16c41bb058e1e135c1f150f6a2e25ed047f64cf3f5c6 \ --hash=sha256:f2a7253458e34f33543551394ae4fe104b497ec2a65ac266074de64c1df82e37 # via mypy markdown-it-py==4.2.0 \ --hash=sha256:04a21681d6fbb623de53f6f364d352309d4094dd4194040a10fd51833e418d49 \ --hash=sha256:9f7ebbcd14fe59494226453aed97c1070d83f8d24b6fc3a3bcf9a38092641c4a # via rich mdurl==0.1.2 \ --hash=sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8 \ --hash=sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba # via markdown-it-py multidict==6.7.1 \ --hash=sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9 \ --hash=sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581 \ --hash=sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43 \ --hash=sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1 \ --hash=sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6 \ --hash=sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c \ --hash=sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262 \ --hash=sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd \ --hash=sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d \ --hash=sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d \ --hash=sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3 \ --hash=sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601 \ --hash=sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292 \ --hash=sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed \ --hash=sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362 \ --hash=sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23 \ --hash=sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2 \ --hash=sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb \ --hash=sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d \ --hash=sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65 \ --hash=sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d \ --hash=sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108 \ --hash=sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177 \ --hash=sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5 \ --hash=sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd \ --hash=sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5 \ --hash=sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060 \ --hash=sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37 \ --hash=sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56 \ --hash=sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df \ --hash=sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963 \ --hash=sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118 \ --hash=sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84 \ --hash=sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f \ --hash=sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889 \ --hash=sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7 \ --hash=sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048 \ --hash=sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59 \ --hash=sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709 \ --hash=sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c \ --hash=sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2 \ --hash=sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee \ --hash=sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609 \ --hash=sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c \ --hash=sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445 \ --hash=sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1 \ --hash=sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5 \ --hash=sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31 \ --hash=sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33 \ --hash=sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7 \ --hash=sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca \ --hash=sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429 \ --hash=sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9 \ --hash=sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4 \ --hash=sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2 \ --hash=sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52 \ --hash=sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c \ --hash=sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2 \ --hash=sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a \ --hash=sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1 \ --hash=sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c \ --hash=sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e \ --hash=sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8 \ --hash=sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32 \ --hash=sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3 \ --hash=sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489 \ --hash=sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23 \ --hash=sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34 \ --hash=sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8 \ --hash=sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d \ --hash=sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b \ --hash=sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4 \ --hash=sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d \ --hash=sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0 # via yarl mypy==1.19.1 \ --hash=sha256:016f2246209095e8eda7538944daa1d60e1e8134d98983b9fc1e92c1fc0cb8dd \ --hash=sha256:06e6170bd5836770e8104c8fdd58e5e725cfeb309f0a6c681a811f557e97eac1 \ --hash=sha256:19d88bb05303fe63f71dd2c6270daca27cb9401c4ca8255fe50d1d920e0eb9ba \ --hash=sha256:21761006a7f497cb0d4de3d8ef4ca70532256688b0523eee02baf9eec895e27b \ --hash=sha256:28902ee51f12e0f19e1e16fbe2f8f06b6637f482c459dd393efddd0ec7f82045 \ --hash=sha256:481daf36a4c443332e2ae9c137dfee878fcea781a2e3f895d54bd3002a900957 \ --hash=sha256:804bd67b8054a85447c8954215a906d6eff9cabeabe493fb6334b24f4bfff718 \ --hash=sha256:8bb5c6f6d043655e055be9b542aa5f3bdd30e4f3589163e85f93f3640060509f \ --hash=sha256:bdb12f69bcc02700c2b47e070238f42cb87f18c0bc1fc4cdb4fb2bc5fd7a3b8b \ --hash=sha256:c9a6538e0415310aad77cb94004ca6482330fece18036b5f360b62c45814c4ef \ --hash=sha256:da4869fc5e7f62a88f3fe0b5c919d1d9f7ea3cef92d3689de2823fd27e40aa75 \ --hash=sha256:e3157c7594ff2ef1634ee058aafc56a82db665c9438fd41b390f3bde1ab12250 \ --hash=sha256:f1235f5ea01b7db5468d53ece6aaddf1ad0b88d9e7462b86ef96fe04995d7247 \ --hash=sha256:f859fb09d9583a985be9a493d5cfc5515b56b08f7447759a0c5deaf68d80506e mypy-extensions==1.1.0 \ --hash=sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505 \ --hash=sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558 # via mypy nodeenv==1.10.0 \ --hash=sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827 \ --hash=sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb # via pre-commit opentelemetry-api==1.43.0 \ --hash=sha256:107d0d03857ea8fc7c5fcbbbd83f800c281f0d560553d61c1d675fccfd1761c1 \ --hash=sha256:20acf45e9b21851926835292e4045d290acade1edd2ff3de86d2f069687ba1fd # via # opentelemetry-exporter-otlp-proto-http # opentelemetry-instrumentation # opentelemetry-instrumentation-logging # opentelemetry-sdk # opentelemetry-semantic-conventions opentelemetry-exporter-otlp-proto-common==1.43.0 \ --hash=sha256:123c3f9cc87218562490c63b36f497bf3a722faf174a515d1443f31ababa6264 \ --hash=sha256:c4e32ba6d6b13bdb2b8f6764c4fd28d00192826561aa04f6d14eedfce7ac076f # via opentelemetry-exporter-otlp-proto-http opentelemetry-exporter-otlp-proto-http==1.43.0 \ --hash=sha256:647f603aa8efdbdb4dbff842e0729d0406a6fff26b295a72d3d60e7d963b2610 \ --hash=sha256:fa8a42bb7d00ee5391f4c0b04d8e6a46c03caa437903296ab73a81dc11ba118f # via dagger-io opentelemetry-instrumentation==0.64b0 \ --hash=sha256:133ab7ffca796557aec059bf6be3190a34b6dea987f25be3d9409e230cbdad8b \ --hash=sha256:b47d528dead6271d7743114417eb67fc915bd9258111c48dbf9a4951d2efa88d # via opentelemetry-instrumentation-logging opentelemetry-instrumentation-logging==0.64b0 \ --hash=sha256:6435b4d215bf8183e15f7e3416a10ebe1c411810d3411fbfc62667daae3abacb \ --hash=sha256:9b56f19648798ddb331c206b74d36c5f2a71e5c6b771e96298f167b2b590e40d # via dagger-io opentelemetry-proto==1.43.0 \ --hash=sha256:224778df17e1f3fafeaaa21d874236ca5f6ffc2f86e0899298ec7351aac27924 \ --hash=sha256:c58f1f7ef84bc7dc2834016c0c37fe0081dde7ca9f6339be1970fbf9cdaaa90d # via # opentelemetry-exporter-otlp-proto-common # opentelemetry-exporter-otlp-proto-http opentelemetry-sdk==1.43.0 \ --hash=sha256:d1323a547c1ce69d6a069a17a44b7da82bb8b332051ecb074041f87642c86823 \ --hash=sha256:d8187c81c162df9913e4003dd6485f7390d9a24fc17026ec7387b8b8218b08e9 # via # dagger-io # opentelemetry-exporter-otlp-proto-http opentelemetry-semantic-conventions==0.64b0 \ --hash=sha256:72f76fb2d1582d9d033dd1fcd84532e961e6ff3d90d24ba6fabc72975a83864c \ --hash=sha256:ea77e85e354b8f604ddbe5f3d9135216f982fa4d77e5859ac30f6d8a50505aa6 # via # opentelemetry-instrumentation # opentelemetry-instrumentation-logging # opentelemetry-sdk packaging==26.2 \ --hash=sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e \ --hash=sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661 # via # opentelemetry-instrumentation # pytest pathspec==1.1.1 \ --hash=sha256:17db5ecd524104a120e173814c90367a96a98d07c45b2e10c2f3919fff91bf5a \ --hash=sha256:a00ce642f577bf7f473932318056212bc4f8bfdf53128c78bbd5af0b9b20b189 # via mypy platformdirs==4.10.0 \ --hash=sha256:31e761a6a0ca04faf7353ea759bdba55652be214725111e5aac52dfa29d4bef7 \ --hash=sha256:fb516cdb12eb0d857d0cd85a7c57cea4d060bee4578d6cf5a14dfdf8cbf8784a # via # dagger-io # python-discovery # virtualenv pluggy==1.6.0 \ --hash=sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3 \ --hash=sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746 # via pytest pre-commit==4.6.0 \ --hash=sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9 \ --hash=sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b propcache==0.5.2 \ --hash=sha256:01c4fc7480cd0598bb4b57022df55b9ca296da7fc5a8760bd8451a7e63a7d427 \ --hash=sha256:06187263ddad280d05b4d8a8b3bb7d164cbebd469236544a42e6d9b28ac6a4fa \ --hash=sha256:0958834041a0166d343b8d2cedcd8bcbaeb4fdbe0cf08320c5379f143c3be6e7 \ --hash=sha256:099aaf4b4d1a02265b92a977edf00b5c4f63b3b17ac6de39b0d637c9cac0188a \ --hash=sha256:0d2c9bf8528f135dbb805ce027567e09164f7efa51a2be07458a2c0420f292d0 \ --hash=sha256:10734b5484ea113152ee25a91dccedf81631791805d2c9ccb054958e51842c94 \ --hash=sha256:13fef48778b5a2a756523fdb781326b028ca75e32858b04f2cdd19f394564917 \ --hash=sha256:196913dea116aeb5a2ba95af4ddcb7ea85559ae07d8eee8751688310d09168c3 \ --hash=sha256:1b31822f4474c4036bae62de9402710051d431a606d6a0f907fec79935a071aa \ --hash=sha256:1dbcf7675229b35d31abb6547d8ebc8c27a830ac3f9a794edff6254873ec7c0a \ --hash=sha256:2d7aa89ebca5acc98cba9d1472d976e394782f587bad6661003602a619fd1821 \ --hash=sha256:2f22cbbac9e26a8e864c0985ff1268d5d939d53d9d9411a9824279097e03a2cb \ --hash=sha256:3115559b8effafd63b142ea5ed53d63a16ea6469cbc63dce4ee194b42db5d853 \ --hash=sha256:32775082acd2d807ee3db715c7770d38767b817870acfa08c29e057f3c4d5b56 \ --hash=sha256:3430bb2bfe1331885c427745a751e774ee679fd4344f80b97bf879815fe8fa55 \ --hash=sha256:40314bca9ac559716fe374094fc81c11dcc34b64fd6c585360f5775690505704 \ --hash=sha256:452b5065457eb9991ec5eb38ff41d6cd4c991c9ac7c531c4d5849ae473a9a13f \ --hash=sha256:46088abff4cba581dea21ae0467a480526cb25aa5f3c269e909f800328bc3999 \ --hash=sha256:4bc8ff1feffc6a61c7002ffe84634c41b822e104990ae009f44a0834430070bb \ --hash=sha256:4db0ba63d693afd40d249bd93f842b5f144f8fcbb83de05660373bcf30517b1d \ --hash=sha256:51f96d685ab16e88cab128cd37a52c5da540809c8b879fa047731bfcb4ad35a4 \ --hash=sha256:5538d2c13d93e4698af7e092b57bc7298fd35d1d58e656ae18f23ee0d0378e03 \ --hash=sha256:5570dbcc97571c15f68068e529c92715a12f8d54030e272d264b377e22bd17a5 \ --hash=sha256:583c19759d9eec1e5b69e2fbef36a7d9c326041be9746cb822d335c8cedc2979 \ --hash=sha256:6041d31504dc1779d700e1edcfb08eea334b357620b06681a4eabb57a74e574e \ --hash=sha256:68ce1c44c7a813a7f71ea04315a8c7b330b63db99d059a797a4651bb6f69f117 \ --hash=sha256:6a997d0489e9668a384fcfd5061b857aa5361de73191cac204d04b889cfbbafa \ --hash=sha256:6de8bd93ddde9b992cf2b2e0d796d501a19026b5b9fd87356d7d0779531a8d96 \ --hash=sha256:6e7b8719005dd1175be4ab1cd25e9b98659a5e0347331506ec6760d2773a7fb5 \ --hash=sha256:72d61e16dd78228b58c5d47be830ff3da7e5f139abdf0aef9d86cde1c5cf2191 \ --hash=sha256:79aa3ff0a9b566633b642fa9caf7e21ed1c13d6feca718187873f199e1514078 \ --hash=sha256:7afa37062e6650640e932e4cc9297d81f9f42d9944029cc386b8247dea4da837 \ --hash=sha256:81e3a30b0bb60caa22033dd0f8a3618d1d67356212514f62c57db75cb0ef410c \ --hash=sha256:8a90efd5777e996e42d568db9ac740b944d691e565cbfd31b2f7832f9184b2b8 \ --hash=sha256:8b73ab70f1a3351fbc71f663b3e645af6dd0329100c353081cf69c37433fc6fe \ --hash=sha256:9282fb1a3bccd038da9f768b927b24a0c753e466c086b7c4f3c6982851eefb2d \ --hash=sha256:97797ebb098e670a2f92dd66f32897e30d7615b14e7f59711de23e30a9072539 \ --hash=sha256:a473b3440261e0c60706e732b2ed2f517857344fc21bf48fdfe211e2d98eb285 \ --hash=sha256:a592f5f3da71c8691c788c13cb6734b6d17663d2e1cb8caddf0673d01ef8847d \ --hash=sha256:a6ae2198be502c10f09b2516e7b5d019816924bc3183a43ce792a7bd6625e6f4 \ --hash=sha256:a6ddc6ac9e25de626c1f129c1b467d7ecd33ce2237d3fd0c4e429feef0a7ee1f \ --hash=sha256:acd2c8edba48e31e58a363b8cf4e5c7db3b04b3f9e371f601df30d9b0d244836 \ --hash=sha256:ba338430e87ceb9c8f0cf754de38a9860560261e56c00376debd628698a7364f \ --hash=sha256:ba57fffe4ac99c5d30076161b5866336d97600769bad35cc68f7774b15298a4e \ --hash=sha256:be1ddfcbb376e3de5d2e2db1d58d6d67463e6b4f9f040c000de8e300295465fe \ --hash=sha256:c60462af8e6dc30c35407c7237ea908d777b22862bbee27bc4699c0d8bcdc45a \ --hash=sha256:c66afea89b1e43725731d2004732a046fe6fe955d51f952c3e95a7314a284a39 \ --hash=sha256:cafca7e56c12bb02ae16d283742bef25a61122e9dab2b5b3f2ccbe589ce32164 \ --hash=sha256:cc49723e2f60d6b32a0f0b08a3fd6d13203c07f1cd9566cfce0f12a917c967a2 \ --hash=sha256:cc6fc3cc62e8501d3ed62894425040d2728ecddb1ed072737a5c70bd537aa9f0 \ --hash=sha256:cd645f03898405cabe694fb8bc35241e3a9c332ec85627584fe3de201452b335 \ --hash=sha256:cef6cea3922890dd6c9654971001fa797b526c16ab5e1e46c05fd6f877be7568 \ --hash=sha256:cfa21e036ce1e1db2be04ba3b85d2df1bb1702fa01932d984c5464c665228ff4 \ --hash=sha256:d0326e2e5e1f3163fa306c834e48e8d490e5fae607a097a40c0648109b47ba80 \ --hash=sha256:d310c013aad2c72f1c3f2f8dd3279d460a858c551f97aeb8c63e4693cca7b4d2 \ --hash=sha256:d447bb0b3054be5818458fbb171208b1d9ff11eba14e18ca18b90cbb45767370 \ --hash=sha256:d4dc37dec6c6cdad0b57881a5658fd14fbf53e333b1a86cf86559f190e1d9ec4 \ --hash=sha256:dfed59d0a5aeb01e242e66ff0300bc4a265a7c05f612d30016f0b60b1017d757 \ --hash=sha256:e00820e192c8dbebcafb383ebbf99030895f09905e7a0eb2e0340a0bcc2bc825 \ --hash=sha256:f064f8d2b59177878b7615df1735cd8fe3462ed6be8c7b217d17a276489c2b7f \ --hash=sha256:f156a3529f38063b6dbaf356e15602a7f95f8055b1295a438433a6386f10463d \ --hash=sha256:f19bb891234d72535764d703bfed1153cc34f4214d5bd7150aee1eec9e8f4366 \ --hash=sha256:f7467da8a9822bf1a55336f877340c5bcbd3c482afc43a99771169f74a26dedc \ --hash=sha256:f78abfa8dfc32376fd1aacf597b2f2fbbe0ea751419aee718af5d4f82537ef8c \ --hash=sha256:f7eabc04151c78a9f4d5bbb5f1faf571e4defeb4b585e0fe95b60ff2dbe4d3d7 \ --hash=sha256:f814362777a9f841adddb200ecdf8f5cb1e5a3c4b7a86378edbd6ccb26edd702 \ --hash=sha256:fc299c129490f55f254cd90be0deca4764e36e9a7c08b4aa588479a3bbed3098 \ --hash=sha256:fc76378c62a0f04d0cd82fbb1a2cd2d7e28fcb40d5873f28a6c44e388aaa2751 \ --hash=sha256:fc88b26f08d634f7bc819a7852e5214f5802641ab8d9fd5326892292eee1993e \ --hash=sha256:fe67a3d11cd9b4efabfa45c3d00ffba2b26811442a73a581a94b67c2b5faccf6 # via yarl protobuf==7.35.1 \ --hash=sha256:11d6b0ec246892d85215b0a13ca6e0233cf5284b68f0ac02646427f4ff88a799 \ --hash=sha256:230a75ddfc2de4806e56696ce9640c1cdfdb6543b7cfce98d42a4c0a0e7bdb87 \ --hash=sha256:24f857477359a85c0c235261b8ba905fd51b2562f4a64ca1df5473f29850cbf6 \ --hash=sha256:353652e4efd0bca5b5fc2656abf8307ef351f0cf938c9eba09f0e09c20a25c30 \ --hash=sha256:4bc97768d8fe4ad6743c8a19403e314511ed9f6d13205b687e52421c023ac1b9 \ --hash=sha256:74758715c53d7158fb76caf4f0cfdacc5329a4b1bb994f865d6cf302d413a1c4 \ --hash=sha256:b73f9489a4b8b1c9cb1f8ed951c736392592edb24b9d6819f36d2e10b171d5b4 \ --hash=sha256:ce115a26fe0c39a2c29973d914d327e516a6455464489fe3cd1e51a1b354f81a # via # googleapis-common-protos # opentelemetry-proto pydantic==2.13.4 \ --hash=sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba \ --hash=sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6 # via # lehrer # pydantic-settings pydantic-core==2.46.4 \ --hash=sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262 \ --hash=sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e \ --hash=sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d \ --hash=sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb \ --hash=sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5 \ --hash=sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462 \ --hash=sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4 \ --hash=sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b \ --hash=sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519 \ --hash=sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6 \ --hash=sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565 \ --hash=sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898 \ --hash=sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb \ --hash=sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596 \ --hash=sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e \ --hash=sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d \ --hash=sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008 \ --hash=sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd \ --hash=sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1 \ --hash=sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be \ --hash=sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292 \ --hash=sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3 \ --hash=sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4 \ --hash=sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b \ --hash=sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac \ --hash=sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d \ --hash=sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4 \ --hash=sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914 \ --hash=sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a \ --hash=sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424 \ --hash=sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76 \ --hash=sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb \ --hash=sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0 \ --hash=sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e \ --hash=sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a \ --hash=sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c \ --hash=sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000 \ --hash=sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4 \ --hash=sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd \ --hash=sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28 \ --hash=sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9 \ --hash=sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3 \ --hash=sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02 \ --hash=sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76 \ --hash=sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7 \ --hash=sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff # via pydantic pydantic-settings==2.14.2 \ --hash=sha256:a20c97b37910b6550d5ea50fbcc2d4187defe58cd57070b73863d069419c9440 \ --hash=sha256:c19dd64b19097f1de80184f0cc7b0272a13ae6e170cbf240a3e27e381ed14a5f pygments==2.20.0 \ --hash=sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f \ --hash=sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176 # via # pytest # rich # rich-rst pytest==9.1.1 \ --hash=sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313 \ --hash=sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c python-discovery==1.4.4 \ --hash=sha256:5cad33982d412c1f3ffb8f9ca4ea292c9680bca3942451d30b69c37fce53a4a3 \ --hash=sha256:abebe9120b43453b68c908acfb1e72a19d1a959ed2cb620ad38fc57d08056dbe # via virtualenv python-dotenv==1.2.2 \ --hash=sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a \ --hash=sha256:2c371a91fbd7ba082c2c1dc1f8bf89ca22564a087c2c287cd9b662adde799cf3 # via pydantic-settings pyyaml==6.0.3 \ --hash=sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c \ --hash=sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3 \ --hash=sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6 \ --hash=sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65 \ --hash=sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1 \ --hash=sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310 \ --hash=sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac \ --hash=sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9 \ --hash=sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7 \ --hash=sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35 \ --hash=sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb \ --hash=sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065 \ --hash=sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c \ --hash=sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c \ --hash=sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764 \ --hash=sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac \ --hash=sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8 \ --hash=sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3 \ --hash=sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5 \ --hash=sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702 \ --hash=sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788 \ --hash=sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba \ --hash=sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5 \ --hash=sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26 \ --hash=sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f \ --hash=sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b \ --hash=sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be \ --hash=sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c \ --hash=sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6 # via # lehrer # pre-commit # pydantic-settings requests==2.34.2 \ --hash=sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0 \ --hash=sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed # via opentelemetry-exporter-otlp-proto-http rich==15.0.0 \ --hash=sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb \ --hash=sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36 # via # cyclopts # dagger-io # rich-rst rich-rst==2.1.0 \ --hash=sha256:7ecd1343ee12c879d0e7ae74c3eb6d263b023d2929c6d114212eb1fd91057255 \ --hash=sha256:f4d117b49697f338769759fa5cacf5197da4888b347b9fda2e50aef5cd8d93bd # via cyclopts ruff==0.15.20 \ --hash=sha256:00e188c53e499c3c1637f73c91dcf2fb56d576cab76ce1be50a27c4e80e37078 \ --hash=sha256:01cc00dd58f0df339d0e902219dd53990ea99996a0344e5d9cc8d45d5307e460 \ --hash=sha256:1416eb04349192646b54de98f146c4f59afe37d0decfc02c3cbbf396f3a28566 \ --hash=sha256:2d2374caa2f2c2f9e2b7da0a50802cfb8b79f55a9b5e49379f564544fbf56487 \ --hash=sha256:2f5b2a6d614e8700388806a14996c40fab2c47b819ef57d790a34878858ed9ca \ --hash=sha256:309809086c2acb67624950a3c8133e80f32d0d3e27106c0cd60ff26657c9f24b \ --hash=sha256:3413bb3c3d2ca6a8208f1f4809cd2dca3c6de6d0b491c0e70847672bde6e6efd \ --hash=sha256:5b9c0c367ad8e5d0d5b5b8537864c469a0a0e55417aadfbeca41fa61333be9f4 \ --hash=sha256:9ebd1fd9b9c95fc0bd7b2761aebec1f030013d2e193a2901b224af68fe47251b \ --hash=sha256:a1ed17b65293e0c2f22fc387bc13198a5de94bf4429589b0ff6946b0feaf21a3 \ --hash=sha256:a525c81c70fb0380344dd1d8745d8cc1c890b7fc94a58d5a07bd8eb9557b8415 \ --hash=sha256:b6df3b1e4610432f0386dba04d853b5f08cbbc903410c6fcc02f620f05aff53c \ --hash=sha256:bd7ec42b3bb3da066488db093308a69c4ac5ee6d2af333a86ba6e2eb2e7dd44b \ --hash=sha256:c5b16cdd67ca108185cd36dce98c576350c03b1660a751de725fb049193a0632 \ --hash=sha256:e1a36ad0eb77fba9aabfb69ede54de6f376d04ac18ebea022847046d340a8267 \ --hash=sha256:e89f198a1ea6ef0d727c1cf16088bc91a6cb0ab947dedc966715691647186eae \ --hash=sha256:ed65ef510e43a137207e0f01cfcf998aeddb1aeeda5c9d35023e910284d7cf21 \ --hash=sha256:f701305e66b38ea6c91882490eb73459796808e4c6362a1b765255e0cdcd4053 types-pyyaml==6.0.12.20260518 \ --hash=sha256:d2150f75a231c9fe9c7463bd29487d93e60bac90400287351384bc2284eba7cd \ --hash=sha256:d917f83fb38462550338c1297faedd860b3ec83912b96b1e3d73255f7473e466 types-requests==2.33.0.20260518 \ --hash=sha256:626d697d1adaaff76e2044dc8c5c051d8f21abc157bdfe204a75558076fe0bf0 \ --hash=sha256:df7bd3bfe0ca8402dfb841e7d9be714bb5578203283d66d7dc4ef69343449a5e typing-extensions==4.16.0 \ --hash=sha256:481caa481374e813c1b176ada14e97f1f67a4539ce9cfeb3f350d78d6370c2e8 \ --hash=sha256:dc983d19a509c94dba722ee6abd33940f7c05a89e243c47e907eb4db6f1a43e5 # via # cattrs # dagger-io # mypy # opentelemetry-api # opentelemetry-exporter-otlp-proto-http # opentelemetry-sdk # opentelemetry-semantic-conventions # pydantic # pydantic-core # typing-inspection typing-inspection==0.4.2 \ --hash=sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7 \ --hash=sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464 # via # pydantic # pydantic-settings urllib3==2.7.0 \ --hash=sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c \ --hash=sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897 # via # requests # types-requests virtualenv==21.6.0 \ --hash=sha256:bce9d097950fef9d81129b333babfb7767072850c2f1acce0ec536708401bfd1 \ --hash=sha256:e18a4d750f2b64dea73e72ffde3922f3c52365fabdc8157ebd3da20d031c4734 # via pre-commit wrapt==2.2.2 \ --hash=sha256:0065a3b657cec06813b4241d2462ccec287f6863103d7445b725fb3a889736f9 \ --hash=sha256:0788e321027c999bf221b667bd4a54aaefd1a36283749a860ac3eb77daed0302 \ --hash=sha256:07fdcb012821859168641acf68afad61ef9783cf37100af85f152550e9677194 \ --hash=sha256:09f811d43f6f33ec7515f0be76b159569f4057ab54d3e079c3204dddb90afa2a \ --hash=sha256:0abfd648dac9ac9c5b3aa9b523d27f1789046640b58dcd5652a720ddb325e1fc \ --hash=sha256:0ad520e6daa9bbf136f14de735474dbec7dcc0891f718e1d274ce8dc92e645af \ --hash=sha256:0e64826f920c42d9d9f87e8cc09ffae66c51ede12d59061a5a426deb9aa71745 \ --hash=sha256:11d95fc2fbad3163596c39d440e6f21ca9fccece74b56e30a37ac2fca786a07c \ --hash=sha256:173e5bb5ca350a6e0abab60b7ec7cdd7992a814cb14b4de670a28f067f105663 \ --hash=sha256:1fc7691f070220215cccb2a20836b9adbaecb8ff22ad47abe63de5f110994fac \ --hash=sha256:2252f77663651b89255895f58cc6ac08fcb206d4371813e5af61bb62d4f7689c \ --hash=sha256:25904acb9475f46c24fe0423dbc8fda8cc5fbc282ab3dc6e72e919748c53f4e9 \ --hash=sha256:26fc73a1b15e0946d2942b9a4426d162b51676338327dc067ccd8d2d76385f94 \ --hash=sha256:2cb07f414fab25dbe6b5c7398e1491423a5c81a6209533639969a6c928d474a4 \ --hash=sha256:2cd7181ab1c31192ff5219269830744b5a62020b3a6d433588c4f1c95b8f8bff \ --hash=sha256:305d4c247d61c4115794a169141823c62f719525ddb90b23aa332741c77d2c28 \ --hash=sha256:30f7424af5c5c345b7f26490e097f74a2ef45b3d08b664dc33571aee3bd3b56c \ --hash=sha256:385cb1866f20479e83299af585375bfa0a4b0c6c9907a981483ea782ea8ae406 \ --hash=sha256:3c4095803491f6ef72128914c28ec05bbad9758433bb35f6715a3e9c8e46fb2d \ --hash=sha256:4250b43d1a129d947e083c4dc6baf333c9bb34edd26f912d5b0457841fc858ab \ --hash=sha256:45c2f2768e790c9f8db90f239ef23a2af8e7570f25a35619ef902df4a738447f \ --hash=sha256:4b481fb0c40d9fd90a5809911208da700987d373a20a4709dc9e3944af7a6bec \ --hash=sha256:58f9f8d637c9a6e245c6ef5b109b67ec187d2faed23d1405656b51d96e0a5b56 \ --hash=sha256:5bad217350f19ce99ca5b5e71d406765ea86fe541628426772b657375ee1c048 \ --hash=sha256:62415fd095bc590b842b6d092f2b5d9ccbaeb7e0b28535c03dcea2718b48636b \ --hash=sha256:6fe35fd51b74867d8b80174c277bd6bbf6a73e443f908129dc531c4b688a20d5 \ --hash=sha256:8ffbeaea6771a6eba6e6eeb09767864995726bc8240bb54baf88a9bb1db34d5c \ --hash=sha256:94b00b00f806eb3ef2abe9049ed45994a81ee9284884d96e6b8314927c6cea3d \ --hash=sha256:9e8b648270c613720a202d9a45ebabc33261b22c3a839b115ac5bce8c0bb0d69 \ --hash=sha256:a41e758d80dc0ab8c210f641ac892009d356cf1f955d97db544c8dd317b4d14c \ --hash=sha256:a795d3c06e5fbf9ea2f13196180b77aeab1b4685917256ee0d014cc163d90063 \ --hash=sha256:aa14b01804bce36c6d63d7b6a4f55df390f29f8648cc13a1f40b166f4d54680d \ --hash=sha256:b84cd4058001c9727b0e9980b7a9e66325b5ca748b1b578e822cade1bc6b304f \ --hash=sha256:bbf00ee0cb55ec24e2b0995a71942b85b21a066db8f3f46e1dbfdb9433ffba81 \ --hash=sha256:c20279cd1a29800815d7b2d6338b60a6c6e78263f9d6e62e0eda251ba9cae2d0 \ --hash=sha256:c427c9d06d859848a69f0d928fe28b5c33a941b2265d10a0e1f15cd244f1ee33 \ --hash=sha256:c5d7825491bfa2d08b97e9557768987952c7b9ae687d06c3320b40a37ccb7f20 \ --hash=sha256:d09db0f7e8357060d3c38fc22a018aba683a796bf184360fd1a58f6fc180dc77 \ --hash=sha256:d8a15813215f33fa83667bfc978b300e35669ea8bb424e970a1426bcb7bc6cca \ --hash=sha256:dcaa5e1451bd8751d7bd1568dfa3321c78092a52a7ecb5d1a0f18a5791e1fd00 \ --hash=sha256:e6fb7e94e8fe3e4c3067bb1653a91cce7c5e83acc119fdd41501b1bf74654617 \ --hash=sha256:ec8f83949028366531383603139403cac7a826e4011955813cdd640017845ce5 \ --hash=sha256:f32fe639c39561ccc187bcae17e9271be0eb45f1c2952510d2f29b33ab577347 \ --hash=sha256:f4bfd8d1eb438153eff8b8cfe87f032ba65731e1ce06138b5090f745a33f6f95 \ --hash=sha256:f90038ab58fafb584801ca62d72384d7d5225d93c76f7b773c22fae545bd8066 \ --hash=sha256:fb18fc51e813df0d9c98049e3bf2298a5495a648602040e21fa3c7329371159e # via opentelemetry-instrumentation yarl==1.24.2 \ --hash=sha256:0063adad533e57171b79db3943b229d40dfafeeee579767f96541f106bac5f1b \ --hash=sha256:081c2bf54efe03774d0311172bc04fedf9ca01e644d4cd8c805688e527209bdc \ --hash=sha256:0c3063e5c0a8e8e62fae6c2596fa01da1561e4cd1da6fec5789f5cf99a8aefd8 \ --hash=sha256:1e831894be7c2954240e49791fa4b50c05a0dc881de2552cfe3ffd8631c7f461 \ --hash=sha256:204e7a61ce99919c0de1bf904ab5d7aa188a129ea8f690a8f76cfb6e2844dc44 \ --hash=sha256:221ce1dd921ac4f603957f17d7c18c5cc0797fbb52f156941f92e04605d1d67b \ --hash=sha256:2783d9226db8797636cd6896e4de81feed252d1db72265686c9558d97a4d94b9 \ --hash=sha256:297a2fe352ecf858b30a98f87948746ec16f001d279f84aebdbd3bd965e2f1bd \ --hash=sha256:2a263e76b97bc42bdcd7c5f4953dec1f7cd62a1112fa7f869e57255229390d67 \ --hash=sha256:2d07d21d0bc4b17558e8de0b02fbfdf1e347d3bb3699edd00bb92e7c57925420 \ --hash=sha256:310fc687f7b2044ec54e372c8cbe923bb88f5c37bded0d3079e5791c2fc3cf50 \ --hash=sha256:33a29b5d00ccbf3219bb3e351d7875739c19481e030779f48cc46a7a71681a9b \ --hash=sha256:374423f70754a2c96942ede36a29d37dc6b0cb8f92f8d009ddf3ed78d3da5488 \ --hash=sha256:491ac9141decf49ee8030199e1ee251cdff0e131f25678817ff6aa5f837a3536 \ --hash=sha256:4b156914620f0b9d78dc1adb3751141daee561cfec796088abb89ed49d220f1a \ --hash=sha256:4da31a5512ed1729ca8d8aacde3f7faeb8843cde3165d6bcf7f88f74f17bb8aa \ --hash=sha256:4fb1ac3fc5fecd8ae7453ea237e4d22b49befa70266dfe1629924245c21a0c7f \ --hash=sha256:533ded4dceb5f1f3da7906244f4e82cf46cfd40d84c69a1faf5ac506aa65ecbe \ --hash=sha256:5f3224db28173a00d7afacdee07045cc4673dfab2b15492c7ae10deddbece761 \ --hash=sha256:68cf6eacd6028ef1142bc4b48376b81566385ca6f9e7dde3b0fa91be08ffcb57 \ --hash=sha256:73e68edf6dfd5f73f9ca127d84e2a6f9213c65bdffb736bda19524c0564fcd14 \ --hash=sha256:7b3a85525f6e7eeabcfdd372862b21ee1915db1b498a04e8bf0e389b607ff0bd \ --hash=sha256:7d37fb7c38f2b6edab0f845c4f85148d4c44204f52bc127021bd2bc9fdbf1656 \ --hash=sha256:810e19b685c8c3c5862f6a38160a1f4e4c0916c9390024ec347b6157a45a0992 \ --hash=sha256:822519b64cf0b474f1a0aaef1dc621438ea46bb77c94df97a5b4d213a7d8a8b1 \ --hash=sha256:8372a2b976cf70654b2be6619ab6068acabb35f724c0fda7b277fbf53d66a5cf \ --hash=sha256:84f9670b89f34db07f81e53aee83e0b938a3412329d51c8f922488be7fcc4024 \ --hash=sha256:863297ddede92ee49024e9a9b11ecb59f310ca85b60d8537f56bed9bbb5b1986 \ --hash=sha256:86746bef442aa479107fe28132e1277237f9c24c2f00b0b0cf22b3ee0904f2bb \ --hash=sha256:8cec2a38d70edc10e0e856ceda886af5327a017ccbde8e1de1bd44d300357543 \ --hash=sha256:904065e6e85b1fa54d0d87438bd58c14c0bad97aad654ad1077fd9d87e8478ed \ --hash=sha256:91e72cf093fd833483a97ee648e0c053c7c629f51ff4a0e7edd84f806b0c5617 \ --hash=sha256:9ac374123c6fd7abf64d1fec93962b0bd4ee2c19751755a762a72dd96c0378f8 \ --hash=sha256:a296ca617f2d25fbceafb962b88750d627e5984e75732c712154d058ae8d79a3 \ --hash=sha256:a46d1ab4ba4d32e6dc80daf8a28ce0bd83d08df52fbc32f3e288663427734535 \ --hash=sha256:a4f4d6cd615823bfc7fb7e9b5987c3f41666371d870d51058f77e2680fbe9630 \ --hash=sha256:a7624b1ca46ca5d7b864ef0d2f8efe3091454085ee1855b4e992314529972215 \ --hash=sha256:a9532c57211730c515341af11fef6e9b61d157487272a096d0c04da445642592 \ --hash=sha256:abb2759733d63a28b4956500a5dd57140f26486c92b2caedfb964ab7d9b79dbf \ --hash=sha256:afb00d7fd8e0f285ca29a44cc50df2d622ff2f7a6d933fa641577b5f9d5f3db0 \ --hash=sha256:b3177bc0a768ef3bacceb4f272632990b7bea352f1b2f1eee9d6d6ff16516f92 \ --hash=sha256:b6067060d9dc594899ba83e6db6c48c68d1e494a6dab158156ed86977ca7bcb1 \ --hash=sha256:c557165320d6244ebe3a02431b2a201a20080e02f41f0cfa0ccc47a183765da8 \ --hash=sha256:cdfcce633b4a4bb8281913c57fcafd4b5933fbc19111a5e3930bbd299d6102f1 \ --hash=sha256:e196952aacaf3b232e265ff02980b64d483dc0972bd49bcb061171ff22ac203a \ --hash=sha256:e434a45ce2e7a947f951fc5a8944c8cc080b7e59f9c50ae80fd39107cf88126d \ --hash=sha256:e51b2cf5ec89a8b8470177641ed62a3ba22d74e1e898e06ad53aa77972487208 \ --hash=sha256:e7484b9361ed222ee1ca5b4337aa4cbdcc4618ce5aff57d9ef1582fd95893fc0 \ --hash=sha256:e89418f65eda18f99030386305bd44d7d504e328a7945db1ead514fbe03a0607 \ --hash=sha256:ee8e3fb34513e8dc082b586ef4910c98335d43a6fab688cd44d4851bacfce3e8 \ --hash=sha256:f9312b3c02d9b3d23840f67952913c9c8721d7f1b7db305289faefa878f364c2 \ --hash=sha256:f9a1e9b622ca284143aab5d885848686dcd85453bb1ca9abcdb7503e64dc0056 \ --hash=sha256:fecd17873a096036c1c87ab3486f1aef7f269ada7f23f7f856f93b1cc7744f14 # via # dagger-io # gql (generate a flat requirements.txt, then `uv pip install -r` it) to `uv sync --locked` directly against openedx-platform's uv.lock when present -- fewer moving parts, and lets uv's own dependency-group selection do the group filtering instead of reimplementing it with --only-group exports. --inexact is required on every sync call here: the environment already has (or will have) packages sync doesn't know about -- a locally editable django-aqueduct checkout installed ahead of it, the deployment's own pinned package list layered after it, plugin distributions from a prior install_deps run. A plain sync would prune all of that back out to exactly what's in edx-platform's own lock. Also add an include_dev_dependencies flag (install_deps, build_platform) that pulls in the 'dev' dependency group -- mypy, tox, django-debug-toolbar, etc., or requirements/edx/development.txt pre-migration -- for building local-dev images. Production builds leave it off by default. --- src/lehrer/core/codejail.py | 12 +-- src/lehrer/core/platform.py | 143 ++++++++++++++++++++---------------- 2 files changed, 84 insertions(+), 71 deletions(-) diff --git a/src/lehrer/core/codejail.py b/src/lehrer/core/codejail.py index 8b91b43a..b01251f5 100644 --- a/src/lehrer/core/codejail.py +++ b/src/lehrer/core/codejail.py @@ -185,7 +185,7 @@ async def _build( # machine-generated compat export slated for removal once known # consumers (including this one) stop reading it -- see # openedx/public-engineering#552. Try the compat export first (fast, - # no uv needed) and fall back to a uv export of the sub-project once + # no uv needed) and fall back to a uv sync of the sub-project once # it's gone, so this keeps working across that removal without a # lehrer change. if release_name != "master": @@ -204,16 +204,16 @@ async def _build( install_sandbox_reqs = ( "set -eu && " "if curl -fsSL -o /tmp/edx_sandbox.txt " - f"{shlex.quote(sandbox_base_url + '/base.txt')}; then :; else " + f"{shlex.quote(sandbox_base_url + '/base.txt')}; then " + "pip install --no-cache-dir -r /tmp/edx_sandbox.txt; " + "else " "mkdir -p /tmp/edx-sandbox-uv && cd /tmp/edx-sandbox-uv && " "curl -fsSL -o pyproject.toml " f"{shlex.quote(sandbox_base_url + '/pyproject.toml')} && " f"curl -fsSL -o uv.lock {shlex.quote(sandbox_base_url + '/uv.lock')} && " "pip install --quiet uv && " - "uv export --frozen --no-hashes --no-emit-project " - "-o /tmp/edx_sandbox.txt; " - "fi && " - "pip install --no-cache-dir -r /tmp/edx_sandbox.txt" + "uv sync --locked --active --no-install-project; " + "fi" ) container = container.with_exec( diff --git a/src/lehrer/core/platform.py b/src/lehrer/core/platform.py index ad133c6f..65f38700 100644 --- a/src/lehrer/core/platform.py +++ b/src/lehrer/core/platform.py @@ -457,49 +457,74 @@ def _derive_test_settings(service: str) -> str: ) -def _edx_base_requirements_script(*, base_dest: str, assets_dest: str) -> str: - """Shell script that sources edx-platform's base + assets requirements. +def _edx_base_deps_script(*, include_dev: bool = False) -> str: + """Shell script that installs edx-platform's base + assets (+ dev) deps. openedx-platform is mid-migration from pip-compile (``requirements/edx/ - {base,assets}.txt``) to ``pyproject.toml``/``uv.lock`` as the source of - truth; the ``.txt`` compat exports are slated for removal once known - consumers move off them (openedx/public-engineering#552). Named releases - haven't picked up the migration and won't grow a ``uv.lock`` until they - do, so branching on ``uv.lock``'s presence -- rather than hardcoding - ``release_name == "master"`` -- keeps both tracks working, and picks up - master's migration automatically without a lehrer change once it lands. - - Must run with cwd (or an explicit ``cd``) at the edx-platform checkout. + {base,assets,development}.txt``) to ``pyproject.toml``/``uv.lock`` as the + source of truth; the ``.txt`` compat exports are slated for removal once + known consumers move off them (openedx/public-engineering#552). Named + releases haven't picked up the migration and won't grow a ``uv.lock`` + until they do, so branching on ``uv.lock``'s presence -- rather than + hardcoding ``release_name == "master"`` -- keeps both tracks working, and + picks up master's migration automatically without a lehrer change once it + lands. + + ``--inexact`` on the ``uv sync`` path is load-bearing: callers layer their + own editable installs (e.g. a local django-aqueduct checkout) and the + deployment's own pinned package list on top of this, before or after — + a plain ``uv sync`` prunes anything not in edx-platform's own lock + resolution, which would strip those back out. + + ``include_dev`` pulls in openedx-platform's ``dev`` dependency group + (mypy, tox, django-debug-toolbar, ...) — or, pre-migration, + ``requirements/edx/development.txt`` — for local-dev builds; production + builds leave it out (the default). + + Must run with cwd (or an explicit ``cd``) at the edx-platform checkout, + and with ``VIRTUAL_ENV`` set to the target environment. """ + sync_groups = "--group assets" + (" --group dev" if include_dev else "") + legacy_cp = ( + "cp requirements/edx/base.txt /tmp/edx_base.txt\n" + " cp requirements/edx/assets.txt /tmp/edx_assets.txt\n" + ) + legacy_files = "/tmp/edx_base.txt /tmp/edx_assets.txt" + if include_dev: + legacy_cp += " cp requirements/edx/development.txt /tmp/edx_development.txt\n" + legacy_files += " /tmp/edx_development.txt" + legacy_install = " ".join(f"-r {path}" for path in legacy_files.split()) return ( "set -eu\n" "cd /openedx/edx-platform\n" "if [ -f uv.lock ]; then\n" - " uv export --frozen --no-hashes --no-emit-project" - f" --no-default-groups -o {base_dest}\n" - " uv export --frozen --no-hashes --no-emit-project" - f" --no-default-groups --only-group assets -o {assets_dest}\n" + " uv sync --locked --active --no-install-project --inexact" + f" --no-default-groups {sync_groups}\n" "else\n" - f" cp requirements/edx/base.txt {base_dest}\n" - f" cp requirements/edx/assets.txt {assets_dest}\n" + f" {legacy_cp}" + f" uv pip install {legacy_install}\n" "fi\n" ) -def _edx_testing_requirements_script(dest: str) -> str: - """Shell script that sources edx-platform's test-suite requirements. +def _edx_testing_deps_script() -> str: + """Shell script that installs edx-platform's test-suite deps. - Same uv.lock-presence branch as ``_edx_base_requirements_script`` — see - its docstring and openedx/public-engineering#552. + Same ``uv.lock``-presence branch and ``--inexact`` rationale as + ``_edx_base_deps_script`` — see its docstring and + openedx/public-engineering#552. Runs after ``install_deps`` has already + layered the deployment's own packages into the environment, so pruning + those back out on sync is not an option. """ return ( "set -eu\n" "cd /openedx/edx-platform\n" "if [ -f uv.lock ]; then\n" - " uv export --frozen --no-hashes --no-emit-project" - f" --no-default-groups --only-group testing -o {dest}\n" + " uv sync --locked --active --no-install-project --inexact" + " --no-default-groups --group testing\n" "else\n" - f" cp requirements/edx/testing.txt {dest}\n" + " cp requirements/edx/testing.txt /tmp/edx_testing.txt\n" + " uv pip install -r /tmp/edx_testing.txt\n" "fi\n" ) @@ -731,6 +756,7 @@ def install_deps( packages_to_remove: list[str] | None = None, extra_npm_packages: list[str] | None = None, install_node: bool = True, + include_dev_dependencies: bool = False, # noqa: FBT001, FBT002 ) -> dagger.Container: """Install Python and Node.js dependencies using uv @@ -754,6 +780,11 @@ def install_deps( ``False`` for Python-only consumers (e.g. plugin import checks, settings regeneration) that never build webpack assets — the Python environment above is complete without it. + include_dev_dependencies: Also install edx-platform's ``dev`` + dependency group (mypy, tox, django-debug-toolbar, ...) — or, + pre-migration, ``requirements/edx/development.txt``. Default + ``False`` for production builds; set ``True`` for local-dev + images. Returns: Container with all dependencies installed @@ -770,30 +801,23 @@ def install_deps( .with_mounted_directory( "/root/pip_package_overrides", pip_package_overrides ) - # Source base + assets requirements from edx-platform (uv.lock - # export or legacy pip-compile .txt, whichever the checkout has). + # Install base + assets (+ dev) deps from edx-platform (uv sync + # against uv.lock, or the legacy pip-compile .txt, whichever the + # checkout has), then layer the deployment's own pinned package + # list on top. .with_exec( [ "sh", "-c", - _edx_base_requirements_script( - base_dest="/root/pip_package_lists/edx_base.txt", - assets_dest="/root/pip_package_lists/edx_assets.txt", - ), + _edx_base_deps_script(include_dev=include_dev_dependencies), ] ) - # Install base Python dependencies using uv (much faster than pip) - # uv automatically uses the VIRTUAL_ENV set in apt_base .with_exec( [ "uv", "pip", "install", "-r", - "/root/pip_package_lists/edx_base.txt", - "-r", - "/root/pip_package_lists/edx_assets.txt", - "-r", f"/root/pip_package_lists/{release_name}/{deployment_name}.txt", ] ) @@ -1618,40 +1642,29 @@ def _python_only_env( edx_platform_git_repo=cell.platform_repo, edx_platform_git_branch=cell.platform_branch, ) - container = ( - container.with_mounted_directory( - "/root/pip_package_lists", cell.pip_package_lists - ) - .with_mounted_directory( - "/root/pip_package_overrides", cell.pip_package_overrides - ) - .with_exec( - [ - "sh", - "-c", - _edx_base_requirements_script( - base_dest="/root/pip_package_lists/edx_base.txt", - assets_dest="/root/pip_package_lists/edx_assets.txt", - ), - ] - ) + container = container.with_mounted_directory( + "/root/pip_package_lists", cell.pip_package_lists + ).with_mounted_directory( + "/root/pip_package_overrides", cell.pip_package_overrides ) + # Installed *before* the base deps sync/install below so its version + # satisfies the pinned django-aqueduct== constraint and uv skips the + # PyPI fetch. The base step's --inexact flag is what keeps this from + # being pruned back out. if aqueduct_source is not None: container = container.with_mounted_directory( "/root/django-aqueduct", aqueduct_source ).with_exec(["uv", "pip", "install", "-e", "/root/django-aqueduct"]) container = container.with_exec( + ["sh", "-c", _edx_base_deps_script()] + ).with_exec( [ "uv", "pip", "install", "-r", - "/root/pip_package_lists/edx_base.txt", - "-r", - "/root/pip_package_lists/edx_assets.txt", - "-r", f"/root/pip_package_lists/{release_name}/{deployment_name}.txt", ] ) @@ -1712,6 +1725,7 @@ async def build_platform( extra_npm_packages: list[str] | None = None, verify_boot: bool = True, # noqa: FBT001, FBT002 strict_translations: bool = False, # noqa: FBT001, FBT002 + include_dev_dependencies: bool = False, # noqa: FBT001, FBT002 ) -> dagger.Container: """Build a complete openedx-platform image @@ -1761,6 +1775,11 @@ async def build_platform( pull/compile step fails, instead of warning (default: False — a missing plugin translation is normal, so this is opt-in until the per-step baseline is known). + include_dev_dependencies: Also install edx-platform's ``dev`` + dependency group (mypy, tox, django-debug-toolbar, ...) — or, + pre-migration, ``requirements/edx/development.txt``. Default + ``False`` for production builds; set ``True`` to build a + local-dev image. Returns: Container ready to be deployed @@ -1872,6 +1891,7 @@ async def build_platform( node_version=node_version, packages_to_remove=packages_to_remove, extra_npm_packages=extra_npm_packages, + include_dev_dependencies=include_dev_dependencies, ) # ── Clean base ──────────────────────────────────────────────────────── @@ -2338,14 +2358,7 @@ async def test( # noqa: PLR0913 container = ( container.with_workdir("/openedx/edx-platform") .with_exec(["uv", "pip", "install", "-e", "."]) - .with_exec( - [ - "sh", - "-c", - _edx_testing_requirements_script("/tmp/edx_testing.txt") - + "uv pip install -r /tmp/edx_testing.txt\n", - ] - ) + .with_exec(["sh", "-c", _edx_testing_deps_script()]) ) # When folding the plugins' own suites into this run, derive the plugin From 8c1b9c9ec03d216a7d08dc7ca744adf4debbc87d Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Thu, 23 Jul 2026 16:27:42 -0400 Subject: [PATCH 3/4] fix: preserve codejail service deps through sandbox uv sync fallback Add --inexact to the sandbox's uv sync fallback. Without it, a plain sync prunes anything not in requirements/edx-sandbox's own uv.lock -- including codejailservice's own requirements/base.txt and gunicorn, both already installed into the same /sandbox/venv. The image would build successfully but lose its gunicorn entrypoint and fail at startup, only once the base.txt compat file this fallback guards against is actually removed. Also add pytest coverage for _edx_base_deps_script and _edx_testing_deps_script -- the uv.lock-vs-legacy branching, include_dev on/off, and that --inexact/--group testing are present -- so a future edit to these generators fails fast in CI instead of only surfacing in the Dagger canary. --- src/lehrer/core/codejail.py | 8 +++- tests/core/test_platform_test_helpers.py | 55 +++++++++++++++++++++++- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/src/lehrer/core/codejail.py b/src/lehrer/core/codejail.py index b01251f5..32bb65d4 100644 --- a/src/lehrer/core/codejail.py +++ b/src/lehrer/core/codejail.py @@ -187,7 +187,11 @@ async def _build( # openedx/public-engineering#552. Try the compat export first (fast, # no uv needed) and fall back to a uv sync of the sub-project once # it's gone, so this keeps working across that removal without a - # lehrer change. + # lehrer change. --inexact on that sync is required: codejailservice's + # own requirements/base.txt and gunicorn (installed above, into this + # same /sandbox/venv) aren't part of the sandbox project's uv.lock, and + # a plain sync would prune them back out, leaving the image without + # its gunicorn entrypoint. if release_name != "master": sandbox_req_url = ( "https://raw.githubusercontent.com/openedx/edx-platform/master/" @@ -212,7 +216,7 @@ async def _build( f"{shlex.quote(sandbox_base_url + '/pyproject.toml')} && " f"curl -fsSL -o uv.lock {shlex.quote(sandbox_base_url + '/uv.lock')} && " "pip install --quiet uv && " - "uv sync --locked --active --no-install-project; " + "uv sync --locked --active --no-install-project --inexact; " "fi" ) diff --git a/tests/core/test_platform_test_helpers.py b/tests/core/test_platform_test_helpers.py index 6993cfec..52f7f461 100644 --- a/tests/core/test_platform_test_helpers.py +++ b/tests/core/test_platform_test_helpers.py @@ -4,7 +4,12 @@ import pytest -from lehrer.core.platform import _derive_test_settings, _test_paths +from lehrer.core.platform import ( + _derive_test_settings, + _edx_base_deps_script, + _edx_testing_deps_script, + _test_paths, +) @pytest.mark.parametrize("service", ["lms", "cms"]) @@ -44,3 +49,51 @@ def test_derive_test_settings_is_valid_python(service: str) -> None: def test_derive_test_settings_rejects_unknown_service() -> None: with pytest.raises(ValueError, match="service must be one of"): _derive_test_settings("workers") + + +def test_edx_base_deps_script_branches_on_uv_lock() -> None: + script = _edx_base_deps_script() + assert "cd /openedx/edx-platform" in script + assert "if [ -f uv.lock ]; then" in script + # uv.lock branch: a direct sync, no intermediate requirements file. + assert "uv sync --locked --active --no-install-project" in script + # legacy branch: same base+assets requirements the pre-migration path used. + assert "cp requirements/edx/base.txt /tmp/edx_base.txt" in script + assert "cp requirements/edx/assets.txt /tmp/edx_assets.txt" in script + assert "uv pip install -r /tmp/edx_base.txt -r /tmp/edx_assets.txt" in script + + +def test_edx_base_deps_script_sync_is_inexact() -> None: + # Load-bearing: callers layer an editable django-aqueduct install and the + # deployment's own pinned package list on top of this sync, before or + # after. A plain (exact) sync prunes anything not in edx-platform's own + # lock resolution, which would silently strip those back out. + script = _edx_base_deps_script() + assert "--inexact" in script + + +def test_edx_base_deps_script_excludes_dev_group_by_default() -> None: + script = _edx_base_deps_script() + assert "--group assets" in script + assert "--group dev" not in script + assert "development.txt" not in script + + +def test_edx_base_deps_script_include_dev() -> None: + script = _edx_base_deps_script(include_dev=True) + assert "--group assets" in script + assert "--group dev" in script + # legacy branch also needs the pre-migration development.txt equivalent. + assert "cp requirements/edx/development.txt /tmp/edx_development.txt" in script + assert "-r /tmp/edx_development.txt" in script + + +def test_edx_testing_deps_script() -> None: + script = _edx_testing_deps_script() + assert "cd /openedx/edx-platform" in script + assert "if [ -f uv.lock ]; then" in script + assert "--inexact" in script + assert "--group testing" in script + # legacy branch: same testing.txt the pre-migration path installed. + assert "cp requirements/edx/testing.txt /tmp/edx_testing.txt" in script + assert "uv pip install -r /tmp/edx_testing.txt" in script From d05c893e1d4f522f1b52128cd415b9db292709ed Mon Sep 17 00:00:00 2001 From: Tobias Macey Date: Thu, 23 Jul 2026 16:50:38 -0400 Subject: [PATCH 4/4] refactor: make the uv.lock/pip-compile bridge default for every python build Extract the uv.lock-vs-legacy-requirements branching (uv sync --locked --active --no-install-project --inexact, or pip install -r the pip-compile .txt) into a shared python_deps_install_script helper in a new pip_compile_bridge module. platform.py's two script generators become thin wrappers around it. Apply it as the default for every Python service build in this repo, not just edx-platform: - codejail.py: codejailservice's own requirements/base.txt install (previously a plain pip install with no migration awareness). - notes.py: edx-notes-api's own requirements/base.txt install. This one needed an actual venv first (/opt/notes-venv) -- it previously installed straight into system site-packages as root, which uv sync --active has no way to target. Verified against the real repo: the legacy branch installs the exact same set (django, gunicorn, elasticsearch, ...) as before, just into the venv now. Neither codejailservice nor edx-notes-api has a uv.lock today (both verified against their live repos), so this is presently dead code on both -- the branch always takes the legacy arm, same as edx-platform's own named releases. It costs nothing to keep there and picks up either repo's migration automatically without a lehrer change if one ever lands. Added tests/core/test_pip_compile_bridge.py for the shared helper directly (branching, --inexact, group selection, installer/ensure_uv overrides), and updated the existing platform.py helper tests for the now-simpler legacy-branch output (a direct requirements/edx/*.txt reference instead of a /tmp copy, which was never necessary). --- src/lehrer/core/codejail.py | 24 +++++-- src/lehrer/core/notes.py | 33 +++++++-- src/lehrer/core/pip_compile_bridge.py | 90 ++++++++++++++++++++++++ src/lehrer/core/platform.py | 78 ++++++++------------ tests/core/test_pip_compile_bridge.py | 67 ++++++++++++++++++ tests/core/test_platform_test_helpers.py | 13 ++-- 6 files changed, 238 insertions(+), 67 deletions(-) create mode 100644 src/lehrer/core/pip_compile_bridge.py create mode 100644 tests/core/test_pip_compile_bridge.py diff --git a/src/lehrer/core/codejail.py b/src/lehrer/core/codejail.py index 32bb65d4..69292299 100644 --- a/src/lehrer/core/codejail.py +++ b/src/lehrer/core/codejail.py @@ -5,6 +5,8 @@ import dagger from dagger import dag, function, object_type +from lehrer.core.pip_compile_bridge import python_deps_install_script + @object_type class OpenedxCodejail: @@ -147,10 +149,12 @@ async def _build( .with_exec(["chown", "-R", "sandbox:sandbox", "/sandbox/venv"]) ) - # Update PATH to use virtualenv + # Update PATH to use virtualenv. VIRTUAL_ENV is also needed by any + # `uv sync --active` call below (python_deps_install_script's + # uv.lock path) -- it targets that env var, not just PATH. container = container.with_env_variable( "PATH", "/sandbox/venv/bin:/usr/local/bin:/usr/bin:/bin" - ) + ).with_env_variable("VIRTUAL_ENV", "/sandbox/venv") # Clone codejail service container = container.with_workdir("/codejail").with_exec( @@ -170,9 +174,21 @@ async def _build( sudoers_file = codejail_config.file("01-sandbox") container = container.with_file("/etc/sudoers.d/01-sandbox", sudoers_file) - # Install dependencies + # Install dependencies. codejailservice has no uv.lock today (plain + # pip-compile requirements/), but sourcing it through the same + # uv.lock-vs-legacy bridge as every other Python build here means + # this keeps working with no lehrer change if that ever changes -- + # see python_deps_install_script. container = container.with_exec( - ["pip", "install", "--no-cache-dir", "-r", "requirements/base.txt"] + [ + "sh", + "-c", + python_deps_install_script( + workdir="/codejail", + legacy_requirements=["requirements/base.txt"], + ensure_uv=True, + ), + ] ).with_exec(["pip", "install", "--no-cache-dir", "gunicorn"]) # Install edx-platform sandbox requirements in virtualenv. diff --git a/src/lehrer/core/notes.py b/src/lehrer/core/notes.py index 3c02ccc2..d2cfd623 100644 --- a/src/lehrer/core/notes.py +++ b/src/lehrer/core/notes.py @@ -3,6 +3,8 @@ import dagger from dagger import dag, function, object_type +from lehrer.core.pip_compile_bridge import python_deps_install_script + @object_type class OpenedxNotes: @@ -106,9 +108,18 @@ async def _build( ] ) - # Set working directory and PATH - container = container.with_workdir("/app/edx-notes-api").with_env_variable( - "PATH", "/app/.local/bin:/usr/local/bin:/usr/bin:/bin" + # Set working directory and PATH. /opt/notes-venv holds the app's own + # deps -- a real venv (not just system site-packages) is what + # `uv sync --active` needs a target for, should edx-notes-api's own + # requirements ever migrate to uv.lock. See python_deps_install_script. + container = ( + container.with_workdir("/app/edx-notes-api") + .with_exec(["python3", "-m", "venv", "/opt/notes-venv"]) + .with_env_variable( + "PATH", + "/opt/notes-venv/bin:/app/.local/bin:/usr/local/bin:/usr/bin:/bin", + ) + .with_env_variable("VIRTUAL_ENV", "/opt/notes-venv") ) # Get edx-notes-api source from local directory or Git @@ -130,12 +141,22 @@ async def _build( else: raise ValueError("Must provide either notes_code or notes_repo") - # Install Python dependencies as root (system site-packages), then fix ownership + # Install Python dependencies (uv sync against a uv.lock, or the + # legacy pip-compile requirements/base.txt, whichever the checkout + # has -- see python_deps_install_script), then fix ownership. container = ( container.with_exec( - ["pip", "install", "--no-cache-dir", "-r", "requirements/base.txt"] + [ + "sh", + "-c", + python_deps_install_script( + workdir="/app/edx-notes-api", + legacy_requirements=["requirements/base.txt"], + ensure_uv=True, + ), + ] ) - .with_exec(["chown", "-R", "app:app", "/app"]) + .with_exec(["chown", "-R", "app:app", "/app", "/opt/notes-venv"]) .with_user("1000") ) diff --git a/src/lehrer/core/pip_compile_bridge.py b/src/lehrer/core/pip_compile_bridge.py new file mode 100644 index 00000000..703fd716 --- /dev/null +++ b/src/lehrer/core/pip_compile_bridge.py @@ -0,0 +1,90 @@ +"""Shared pip-compile -> uv.lock bridge for Python service builds. + +Several Open edX services are (or may eventually be) migrating their Python +dependency management from pip-compile-generated ``requirements/*.txt`` +files to ``pyproject.toml`` + ``uv.lock`` as the source of truth -- +openedx-platform's own migration is tracked in +openedx/public-engineering#552. Every Python build in lehrer sources its +dependencies through :func:`python_deps_install_script`, which detects the +track from the checkout itself (``uv.lock`` present or not) rather than +hardcoding which release or service has migrated. That keeps a build +working across a service's migration landing without a lehrer change -- +and costs nothing for a checkout that never migrates: the branch just +always takes the legacy arm. +""" + +import shlex + + +def python_deps_install_script( + *, + workdir: str, + legacy_requirements: list[str], + sync_groups: list[str] | None = None, + legacy_installer: list[str] | None = None, + ensure_uv: bool = False, +) -> str: + """Shell script installing a checkout's Python deps. + + Runs ``uv sync --locked`` against a committed ``uv.lock`` when present, + otherwise falls back to installing the legacy pip-compile + ``requirements/*.txt`` files. + + ``--inexact`` is always passed on the sync path: callers commonly layer + other installs into the same environment around this call -- a + deployment's own pinned package list, another service's base + requirements, an editable framework checkout installed just before it. + A plain (exact) sync prunes anything not in the checkout's own lock + resolution, which would silently strip those back out. + + Args: + workdir: Directory containing the checkout (``pyproject.toml``/ + ``uv.lock`` or the legacy ``requirements/`` tree). + legacy_requirements: Requirement file paths (relative to + ``workdir``) to install when no ``uv.lock`` is present. + sync_groups: Dependency groups to select when ``uv.lock`` IS + present, via ``--no-default-groups --group `` for each. + Omit for just the project's own default dependencies. + legacy_installer: Command prefix for the legacy branch. Default + ``["pip", "install", "--no-cache-dir"]``. Pass + ``["uv", "pip", "install"]`` for a container where uv is + already the installer of record. + ensure_uv: Install uv via pip before the sync branch runs. Default + ``False`` for containers (e.g. the edx-platform build) that + already have uv on PATH; set ``True`` for containers (e.g. + codejail, notes) that only have plain pip. + + Must run with ``VIRTUAL_ENV`` set to the target environment -- the sync + branch uses ``--active``. + """ + installer = list(legacy_installer or ["pip", "install", "--no-cache-dir"]) + legacy_args: list[str] = [] + for req in legacy_requirements: + legacy_args += ["-r", req] + legacy_cmd = " ".join(shlex.quote(part) for part in [*installer, *legacy_args]) + + sync_parts = [ + "uv", + "sync", + "--locked", + "--active", + "--no-install-project", + "--inexact", + ] + if sync_groups: + sync_parts.append("--no-default-groups") + for group in sync_groups: + sync_parts += ["--group", group] + sync_cmd = " ".join(shlex.quote(part) for part in sync_parts) + if ensure_uv: + sync_cmd = f"pip install --quiet uv && {sync_cmd}" + + return ( + "set -eu\n" + f"cd {shlex.quote(workdir)}\n" + "if [ -f uv.lock ]; then\n" + f" {sync_cmd}\n" + "else\n" + f" {legacy_cmd}\n" + "fi\n" + ) diff --git a/src/lehrer/core/platform.py b/src/lehrer/core/platform.py index 65f38700..d84034b9 100644 --- a/src/lehrer/core/platform.py +++ b/src/lehrer/core/platform.py @@ -18,6 +18,7 @@ from dagger import dag, function, object_type from lehrer.core.build_manifest import BuildManifest, Cell +from lehrer.core.pip_compile_bridge import python_deps_install_script from lehrer.core.plugin_imports import plugin_distributions from lehrer.core.plugin_tests import ( combined_pytest_script, @@ -460,72 +461,49 @@ def _derive_test_settings(service: str) -> str: def _edx_base_deps_script(*, include_dev: bool = False) -> str: """Shell script that installs edx-platform's base + assets (+ dev) deps. - openedx-platform is mid-migration from pip-compile (``requirements/edx/ - {base,assets,development}.txt``) to ``pyproject.toml``/``uv.lock`` as the - source of truth; the ``.txt`` compat exports are slated for removal once - known consumers move off them (openedx/public-engineering#552). Named - releases haven't picked up the migration and won't grow a ``uv.lock`` - until they do, so branching on ``uv.lock``'s presence -- rather than - hardcoding ``release_name == "master"`` -- keeps both tracks working, and - picks up master's migration automatically without a lehrer change once it + Delegates to :func:`python_deps_install_script` -- see its docstring for + the ``uv.lock``-presence branch and ``--inexact`` rationale, and + openedx/public-engineering#552 for openedx-platform's own migration. + Named releases haven't picked up the migration and won't grow a + ``uv.lock`` until they do, so this keeps both tracks working, and picks + up master's migration automatically without a lehrer change once it lands. - ``--inexact`` on the ``uv sync`` path is load-bearing: callers layer their - own editable installs (e.g. a local django-aqueduct checkout) and the - deployment's own pinned package list on top of this, before or after — - a plain ``uv sync`` prunes anything not in edx-platform's own lock - resolution, which would strip those back out. - ``include_dev`` pulls in openedx-platform's ``dev`` dependency group (mypy, tox, django-debug-toolbar, ...) — or, pre-migration, ``requirements/edx/development.txt`` — for local-dev builds; production builds leave it out (the default). - Must run with cwd (or an explicit ``cd``) at the edx-platform checkout, - and with ``VIRTUAL_ENV`` set to the target environment. + Must run with ``VIRTUAL_ENV`` set to the target environment. """ - sync_groups = "--group assets" + (" --group dev" if include_dev else "") - legacy_cp = ( - "cp requirements/edx/base.txt /tmp/edx_base.txt\n" - " cp requirements/edx/assets.txt /tmp/edx_assets.txt\n" - ) - legacy_files = "/tmp/edx_base.txt /tmp/edx_assets.txt" + legacy_requirements = [ + "requirements/edx/base.txt", + "requirements/edx/assets.txt", + ] + sync_groups = ["assets"] if include_dev: - legacy_cp += " cp requirements/edx/development.txt /tmp/edx_development.txt\n" - legacy_files += " /tmp/edx_development.txt" - legacy_install = " ".join(f"-r {path}" for path in legacy_files.split()) - return ( - "set -eu\n" - "cd /openedx/edx-platform\n" - "if [ -f uv.lock ]; then\n" - " uv sync --locked --active --no-install-project --inexact" - f" --no-default-groups {sync_groups}\n" - "else\n" - f" {legacy_cp}" - f" uv pip install {legacy_install}\n" - "fi\n" + legacy_requirements.append("requirements/edx/development.txt") + sync_groups.append("dev") + return python_deps_install_script( + workdir="/openedx/edx-platform", + legacy_requirements=legacy_requirements, + sync_groups=sync_groups, + legacy_installer=["uv", "pip", "install"], ) def _edx_testing_deps_script() -> str: """Shell script that installs edx-platform's test-suite deps. - Same ``uv.lock``-presence branch and ``--inexact`` rationale as - ``_edx_base_deps_script`` — see its docstring and - openedx/public-engineering#552. Runs after ``install_deps`` has already - layered the deployment's own packages into the environment, so pruning - those back out on sync is not an option. + Runs after ``install_deps`` has already layered the deployment's own + packages into the environment, so ``python_deps_install_script``'s + ``--inexact`` sync is what keeps those from being pruned back out. """ - return ( - "set -eu\n" - "cd /openedx/edx-platform\n" - "if [ -f uv.lock ]; then\n" - " uv sync --locked --active --no-install-project --inexact" - " --no-default-groups --group testing\n" - "else\n" - " cp requirements/edx/testing.txt /tmp/edx_testing.txt\n" - " uv pip install -r /tmp/edx_testing.txt\n" - "fi\n" + return python_deps_install_script( + workdir="/openedx/edx-platform", + legacy_requirements=["requirements/edx/testing.txt"], + sync_groups=["testing"], + legacy_installer=["uv", "pip", "install"], ) diff --git a/tests/core/test_pip_compile_bridge.py b/tests/core/test_pip_compile_bridge.py new file mode 100644 index 00000000..35b7f942 --- /dev/null +++ b/tests/core/test_pip_compile_bridge.py @@ -0,0 +1,67 @@ +from __future__ import annotations + +from lehrer.core.pip_compile_bridge import python_deps_install_script + + +def test_branches_on_uv_lock_presence() -> None: + script = python_deps_install_script( + workdir="/app/service", + legacy_requirements=["requirements/base.txt"], + ) + assert "cd /app/service" in script + assert "if [ -f uv.lock ]; then" in script + assert "uv sync --locked --active --no-install-project --inexact" in script + assert "pip install --no-cache-dir -r requirements/base.txt" in script + + +def test_multiple_legacy_requirements_each_get_their_own_flag() -> None: + script = python_deps_install_script( + workdir=".", + legacy_requirements=["requirements/base.txt", "requirements/assets.txt"], + ) + assert ( + "pip install --no-cache-dir -r requirements/base.txt" + " -r requirements/assets.txt" in script + ) + + +def test_sync_is_always_inexact() -> None: + # Load-bearing: callers commonly layer other installs (a deployment's own + # package list, an editable framework checkout, another service's base + # requirements) into the same environment around this call. A plain + # (exact) sync prunes anything not in the checkout's own lock resolution, + # which would silently strip those back out. + script = python_deps_install_script(workdir=".", legacy_requirements=[]) + assert "--inexact" in script + + +def test_no_sync_groups_by_default() -> None: + script = python_deps_install_script(workdir=".", legacy_requirements=[]) + assert "--group" not in script + assert "--no-default-groups" not in script + + +def test_sync_groups_use_no_default_groups() -> None: + script = python_deps_install_script( + workdir=".", legacy_requirements=[], sync_groups=["assets", "dev"] + ) + assert "--no-default-groups --group assets --group dev" in script + + +def test_legacy_installer_override() -> None: + script = python_deps_install_script( + workdir=".", + legacy_requirements=["requirements/base.txt"], + legacy_installer=["uv", "pip", "install"], + ) + assert "uv pip install -r requirements/base.txt" in script + assert "pip install --no-cache-dir" not in script + + +def test_ensure_uv_installs_uv_before_sync() -> None: + without = python_deps_install_script(workdir=".", legacy_requirements=[]) + with_ensure = python_deps_install_script( + workdir=".", legacy_requirements=[], ensure_uv=True + ) + assert "pip install --quiet uv" not in without + assert "pip install --quiet uv && uv sync" in with_ensure diff --git a/tests/core/test_platform_test_helpers.py b/tests/core/test_platform_test_helpers.py index 52f7f461..0811c43b 100644 --- a/tests/core/test_platform_test_helpers.py +++ b/tests/core/test_platform_test_helpers.py @@ -58,9 +58,10 @@ def test_edx_base_deps_script_branches_on_uv_lock() -> None: # uv.lock branch: a direct sync, no intermediate requirements file. assert "uv sync --locked --active --no-install-project" in script # legacy branch: same base+assets requirements the pre-migration path used. - assert "cp requirements/edx/base.txt /tmp/edx_base.txt" in script - assert "cp requirements/edx/assets.txt /tmp/edx_assets.txt" in script - assert "uv pip install -r /tmp/edx_base.txt -r /tmp/edx_assets.txt" in script + assert ( + "uv pip install -r requirements/edx/base.txt" + " -r requirements/edx/assets.txt" in script + ) def test_edx_base_deps_script_sync_is_inexact() -> None: @@ -84,8 +85,7 @@ def test_edx_base_deps_script_include_dev() -> None: assert "--group assets" in script assert "--group dev" in script # legacy branch also needs the pre-migration development.txt equivalent. - assert "cp requirements/edx/development.txt /tmp/edx_development.txt" in script - assert "-r /tmp/edx_development.txt" in script + assert "-r requirements/edx/development.txt" in script def test_edx_testing_deps_script() -> None: @@ -95,5 +95,4 @@ def test_edx_testing_deps_script() -> None: assert "--inexact" in script assert "--group testing" in script # legacy branch: same testing.txt the pre-migration path installed. - assert "cp requirements/edx/testing.txt /tmp/edx_testing.txt" in script - assert "uv pip install -r /tmp/edx_testing.txt" in script + assert "uv pip install -r requirements/edx/testing.txt" in script