From 5aaf492c50cfbee9775793a20e0dbbd28736477d Mon Sep 17 00:00:00 2001 From: Abhijeet Prasad Date: Mon, 10 Aug 2026 12:01:37 -0400 Subject: [PATCH] ref(git): replace GitPython with direct git commands Preserve experiment git metadata and ancestor selection while removing the GitPython dependency. Resolve git to an absolute executable path and cover the Braintrust API payload with VCR-backed regression tests. --- integrations/adk-py/uv.lock | 35 - integrations/langchain-py/uv.lock | 35 - py/pyproject.toml | 1 - ...adata_is_preserved_in_braintrust_data.yaml | 662 ++++++++++++++++++ py/src/braintrust/gitutil.py | 140 ++-- py/src/braintrust/test_git_metadata_vcr.py | 197 ++++++ py/src/braintrust/test_gitutil.py | 65 ++ py/uv.lock | 35 - 8 files changed, 1014 insertions(+), 156 deletions(-) create mode 100644 py/src/braintrust/cassettes/test_git_metadata_is_preserved_in_braintrust_data.yaml create mode 100644 py/src/braintrust/test_git_metadata_vcr.py create mode 100644 py/src/braintrust/test_gitutil.py diff --git a/integrations/adk-py/uv.lock b/integrations/adk-py/uv.lock index 77cc98af7..3bae84919 100644 --- a/integrations/adk-py/uv.lock +++ b/integrations/adk-py/uv.lock @@ -83,7 +83,6 @@ source = { editable = "../../py" } dependencies = [ { name = "chevron" }, { name = "exceptiongroup" }, - { name = "gitpython" }, { name = "jsonschema" }, { name = "packaging" }, { name = "python-slugify" }, @@ -100,7 +99,6 @@ requires-dist = [ { name = "boto3", marker = "extra == 'cli'" }, { name = "chevron" }, { name = "exceptiongroup", specifier = ">=1.2.0" }, - { name = "gitpython", specifier = ">=3.1.50" }, { name = "jsonschema" }, { name = "openai-agents", marker = "extra == 'all'" }, { name = "openai-agents", marker = "extra == 'openai-agents'" }, @@ -676,30 +674,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5a/ff/2e4eca3ade2c22fe1dea7043b8ee9dabe47753349eb1b56a202de8af6349/fastapi-0.136.1-py3-none-any.whl", hash = "sha256:a6e9d7eeada96c93a4d69cb03836b44fa34e2854accb7244a1ece36cd4781c3f", size = 117683, upload-time = "2026-04-23T16:49:42.437Z" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.59" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/dc/126b28e76b24a9268ba931ad3e012f71ebdadf62fd9f17758f7074bb0b20/gitpython-3.1.59.tar.gz", hash = "sha256:0a1475cfdc38a5bfba1a3e9a4a9da52a39749ecec322b772915c019f94e5b7e4", size = 230445, upload-time = "2026-08-10T12:03:20.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/ed/ae57eb7d344f43f87b74b3a281ead6ec7d6394eef72a7b1dcb28dd089550/gitpython-3.1.59-py3-none-any.whl", hash = "sha256:67a82f537384578643624c8b2c531938a9b82be431663e575dcf638526631d4c", size = 220996, upload-time = "2026-08-10T12:03:18.804Z" }, -] - [[package]] name = "google-adk" version = "2.2.0" @@ -1421,15 +1395,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c0/98/6beb4b351e472e5f4c4613f7c35a5290b8be2497e183825310c4c3a3984b/ruff-0.15.12-py3-none-win_arm64.whl", hash = "sha256:a538f7a82d061cee7be55542aca1d86d1393d55d81d4fcc314370f4340930d4f", size = 11120821, upload-time = "2026-04-24T18:16:57.979Z" }, ] -[[package]] -name = "smmap" -version = "5.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, -] - [[package]] name = "sniffio" version = "1.3.1" diff --git a/integrations/langchain-py/uv.lock b/integrations/langchain-py/uv.lock index 3082717a4..278a730c6 100644 --- a/integrations/langchain-py/uv.lock +++ b/integrations/langchain-py/uv.lock @@ -77,7 +77,6 @@ source = { editable = "../../py" } dependencies = [ { name = "chevron" }, { name = "exceptiongroup" }, - { name = "gitpython" }, { name = "jsonschema" }, { name = "packaging" }, { name = "python-slugify" }, @@ -94,7 +93,6 @@ requires-dist = [ { name = "boto3", marker = "extra == 'cli'" }, { name = "chevron" }, { name = "exceptiongroup", specifier = ">=1.2.0" }, - { name = "gitpython", specifier = ">=3.1.50" }, { name = "jsonschema" }, { name = "openai-agents", marker = "extra == 'all'" }, { name = "openai-agents", marker = "extra == 'openai-agents'" }, @@ -644,30 +642,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.59" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/dc/126b28e76b24a9268ba931ad3e012f71ebdadf62fd9f17758f7074bb0b20/gitpython-3.1.59.tar.gz", hash = "sha256:0a1475cfdc38a5bfba1a3e9a4a9da52a39749ecec322b772915c019f94e5b7e4", size = 230445, upload-time = "2026-08-10T12:03:20.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/ed/ae57eb7d344f43f87b74b3a281ead6ec7d6394eef72a7b1dcb28dd089550/gitpython-3.1.59-py3-none-any.whl", hash = "sha256:67a82f537384578643624c8b2c531938a9b82be431663e575dcf638526631d4c", size = 220996, upload-time = "2026-08-10T12:03:18.804Z" }, -] - [[package]] name = "h11" version = "0.16.0" @@ -2055,15 +2029,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/46/f5af3402b579fd5e11573ce652019a67074317e18c1935cc0b4ba9b35552/secretstorage-3.5.0-py3-none-any.whl", hash = "sha256:0ce65888c0725fcb2c5bc0fdb8e5438eece02c523557ea40ce0703c266248137", size = 15554, upload-time = "2025-11-23T19:02:51.545Z" }, ] -[[package]] -name = "smmap" -version = "5.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, -] - [[package]] name = "sniffio" version = "1.3.1" diff --git a/py/pyproject.toml b/py/pyproject.toml index d5207ecc5..b7fb53da0 100644 --- a/py/pyproject.toml +++ b/py/pyproject.toml @@ -18,7 +18,6 @@ classifiers = [ "Operating System :: OS Independent", ] dependencies = [ - "GitPython>=3.1.50", "requests", "chevron", "tqdm", diff --git a/py/src/braintrust/cassettes/test_git_metadata_is_preserved_in_braintrust_data.yaml b/py/src/braintrust/cassettes/test_git_metadata_is_preserved_in_braintrust_data.yaml new file mode 100644 index 000000000..e47b9e7a7 --- /dev/null +++ b/py/src/braintrust/cassettes/test_git_metadata_is_preserved_in_braintrust_data.yaml @@ -0,0 +1,662 @@ +interactions: +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '0' + User-Agent: + - python-requests/2.34.2 + method: POST + uri: https://www.braintrust.dev/api/apikey/login + response: + body: + string: '{"org_info":[{"id":"5abfae3a-7aa7-4653-a9c8-b3efcb18f584","name":"Braintrust + SDKs","api_url":"https://api.braintrust.dev","git_metadata":{"collect":"some","fields":["commit","branch","tag","dirty","author_name","author_email","commit_message","commit_time"]},"is_universal_api":null,"proxy_url":"https://api.braintrust.dev","realtime_url":"wss://realtime.braintrustapi.com"}]}' + headers: + Access-Control-Allow-Credentials: + - 'true' + Access-Control-Allow-Headers: + - X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, + Content-Type, Date, X-Api-Version + Access-Control-Allow-Methods: + - GET,OPTIONS,PATCH,DELETE,POST,PUT + Access-Control-Allow-Origin: + - '*' + Cache-Control: + - public, max-age=0, must-revalidate + Content-Length: + - '376' + Content-Security-Policy: + - 'script-src ''self'' ''unsafe-eval'' ''wasm-unsafe-eval'' ''strict-dynamic'' + ''nonce-YjY0MzQwZDEtZWFkZC00YjlhLWJhNDMtYjQxYmI3NWY1MDhj'' *.js.stripe.com + js.stripe.com maps.googleapis.com ; style-src ''self'' ''unsafe-inline'' *.braintrust.dev + btcm6qilbbhv4yi1.public.blob.vercel-storage.com fonts.googleapis.com www.gstatic.com + d4tuoctqmanu0.cloudfront.net; font-src ''self'' data: fonts.gstatic.com btcm6qilbbhv4yi1.public.blob.vercel-storage.com + cdn.jsdelivr.net d4tuoctqmanu0.cloudfront.net fonts.googleapis.com mintlify-assets.b-cdn.net + fonts.cdnfonts.com; object-src ''none''; base-uri ''self''; form-action ''self'' + https://www.facebook.com; frame-ancestors ''self''; worker-src ''self'' blob:; + report-uri https://o4507221741076480.ingest.us.sentry.io/api/4507221754380288/security/?sentry_key=27fa5ac907cf7c6ce4a1ab2a03f805b4&sentry_environment=production&sentry_release=16; + report-to csp-endpoint-0' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 10 Aug 2026 15:14:16 GMT + Etag: + - '"13vsc5ye8flag"' + Reporting-Endpoints: + - csp-endpoint-0="https://o4507221741076480.ingest.us.sentry.io/api/4507221754380288/security/?sentry_key=27fa5ac907cf7c6ce4a1ab2a03f805b4&sentry_environment=production&sentry_release=16" + Server: + - Vercel + Strict-Transport-Security: + - max-age=63072000 + X-Bt-Was-Udf-Cached: + - 'true' + X-Clerk-Auth-Message: + - Invalid JWT form. A JWT consists of three parts separated by dots. (reason=token-invalid, + token-carrier=header) + X-Clerk-Auth-Reason: + - token-invalid + X-Clerk-Auth-Status: + - signed-out + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Matched-Path: + - /api/apikey/login + X-Nonce: + - YjY0MzQwZDEtZWFkZC00YjlhLWJhNDMtYjQxYmI3NWY1MDhj + X-Vercel-Cache: + - MISS + X-Vercel-Id: + - yul1::iad1::9q69z-1786374856109-0775da92bc39 + status: + code: 200 + message: OK +- request: + body: '{"ancestor_commits":["af25aa5dc6b2f7daf031f1f1d26d98ae70871c45"],"experiment_name":"test-git-metadata-base-v2","org_id":"5abfae3a-7aa7-4653-a9c8-b3efcb18f584","project_id":null,"project_name":"python-sdk-vcr-tests","public":false,"repo_info":{"author_email":"git-regression@braintrust.dev","author_name":"Braintrust + Git Regression","branch":"main","commit":"e3feac8b65efbdfc911e83cf3e6204853b6a2d8c","commit_message":"Deterministic + base commit","commit_time":"2024-02-01T12:00:00+00:00","dirty":false,"git_diff":null,"tag":"git-regression-base"},"update":true}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '592' + Content-Type: + - application/json + User-Agent: + - python-requests/2.34.2 + method: POST + uri: https://www.braintrust.dev/api/experiment/register + response: + body: + string: '{"project":{"id":"8b2277cd-df50-4245-b4a0-057a6ce318a9","org_id":"5abfae3a-7aa7-4653-a9c8-b3efcb18f584","name":"python-sdk-vcr-tests","description":null,"created":"2026-08-10T15:11:32.568Z","deleted_at":null,"user_id":"c1f71e19-b3ce-4f59-89a9-055901f7755b","settings":null},"experiment":{"id":"21a09505-aa26-44f3-b233-fe7ee9f38add","project_id":"8b2277cd-df50-4245-b4a0-057a6ce318a9","name":"test-git-metadata-base-v2","description":null,"created":"2026-08-10T15:12:33.800Z","repo_info":{"commit":"e3feac8b65efbdfc911e83cf3e6204853b6a2d8c","branch":"main","tag":"git-regression-base","dirty":false,"author_name":"Braintrust + Git Regression","author_email":"git-regression@braintrust.dev","commit_message":"Deterministic + base commit","commit_time":"2024-02-01T12:00:00+00:00"},"commit":"e3feac8b65efbdfc911e83cf3e6204853b6a2d8c","base_exp_id":null,"deleted_at":null,"dataset_id":null,"dataset_version":null,"internal_metadata":null,"parameters_id":null,"parameters_version":null,"public":false,"user_id":"c1f71e19-b3ce-4f59-89a9-055901f7755b","metadata":null,"tags":null}}' + headers: + Cache-Control: + - public, max-age=0, must-revalidate + Content-Security-Policy: + - 'script-src ''self'' ''unsafe-eval'' ''wasm-unsafe-eval'' ''strict-dynamic'' + ''nonce-NDVhMzRjOGMtZGFjNi00NTg0LWI3NWQtZGY1MDA4MzUwYmQz'' *.js.stripe.com + js.stripe.com maps.googleapis.com ; style-src ''self'' ''unsafe-inline'' *.braintrust.dev + btcm6qilbbhv4yi1.public.blob.vercel-storage.com fonts.googleapis.com www.gstatic.com + d4tuoctqmanu0.cloudfront.net; font-src ''self'' data: fonts.gstatic.com btcm6qilbbhv4yi1.public.blob.vercel-storage.com + cdn.jsdelivr.net d4tuoctqmanu0.cloudfront.net fonts.googleapis.com mintlify-assets.b-cdn.net + fonts.cdnfonts.com; object-src ''none''; base-uri ''self''; form-action ''self'' + https://www.facebook.com; frame-ancestors ''self''; worker-src ''self'' blob:; + report-uri https://o4507221741076480.ingest.us.sentry.io/api/4507221754380288/security/?sentry_key=27fa5ac907cf7c6ce4a1ab2a03f805b4&sentry_environment=production&sentry_release=16; + report-to csp-endpoint-0' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 10 Aug 2026 15:14:16 GMT + Etag: + - W/"cag931vw7jtq" + Reporting-Endpoints: + - csp-endpoint-0="https://o4507221741076480.ingest.us.sentry.io/api/4507221754380288/security/?sentry_key=27fa5ac907cf7c6ce4a1ab2a03f805b4&sentry_environment=production&sentry_release=16" + Server: + - Vercel + Strict-Transport-Security: + - max-age=63072000 + Transfer-Encoding: + - chunked + X-Clerk-Auth-Message: + - Invalid JWT form. A JWT consists of three parts separated by dots. (reason=token-invalid, + token-carrier=header) + X-Clerk-Auth-Reason: + - token-invalid + X-Clerk-Auth-Status: + - signed-out + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Matched-Path: + - /api/experiment/register + X-Nonce: + - NDVhMzRjOGMtZGFjNi00NTg0LWI3NWQtZGY1MDA4MzUwYmQz + X-Vercel-Cache: + - MISS + X-Vercel-Id: + - yul1::iad1::mmzkq-1786374856625-be06ebfc2f2f + content-length: + - '1070' + status: + code: 200 + message: OK +- request: + body: null + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + User-Agent: + - python-requests/2.34.2 + method: GET + uri: https://api.braintrust.dev/version + response: + body: + string: '{"version":"2.10.0","date_version":"20260808","ff_version":44,"commit":"84828c4e229f601fc6e91542ed217ff194f184fa","deployment_mode":"lambda","deployment_type":"custom","brainstore_default":"force","brainstore_can_contain_row_refs":true,"skip_pg_config":"all","has_realtime_wal_bucket":true,"brainstore_wal_footer_version":"v3","brainstore_wal_use_efficient_format":true,"has_logs2":true,"brainstore_export_enabled":true,"js":true,"universal":true,"code_execution":true,"logs3_payload_max_bytes":5242880,"control_plane_telemetry":["status","memprof","usage"]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 10 Aug 2026 15:14:16 GMT + Via: + - 1.1 7f51f788f893911f0cd6b09d9ab21c4a.cloudfront.net (CloudFront), 1.1 a7af18c87ffc07d74544efce5f2b0f9c.cloudfront.net + (CloudFront) + X-Amz-Cf-Id: + - 7EFy58deuw4TrZSmze6EhBAu6oKStH3Phwwk09jWnqfiWcyBwYH-yg== + X-Amz-Cf-Pop: + - YTO53-P2 + - YTO50-P2 + X-Amzn-Trace-Id: + - Root=1-6a79eac8-021bdac46025f62533259380;Parent=06b20d091dbab419;Sampled=0;Lineage=1:24be3d11:0 + X-Cache: + - Miss from cloudfront + access-control-allow-credentials: + - 'true' + access-control-expose-headers: + - x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms,x-bt-internal-trace-id,x-bt-error-origin,x-bt-used-endpoint,x-bt-overflow-url + cache-control: + - no-store, no-cache, must-revalidate, proxy-revalidate + content-length: + - '558' + etag: + - W/"22e-GdcVWuguVZQBIHdVwatLfQwEG2o" + expires: + - '0' + surrogate-control: + - no-store + vary: + - Origin + x-amz-apigw-id: + - B5GfdHf3oAMEqug= + x-amzn-Remapped-content-length: + - '558' + x-amzn-RequestId: + - f4ab591a-d904-48e5-b2a9-f4179c93fcde + x-bt-internal-trace-id: + - 6a79eac8000000006301f846d092ad79 + status: + code: 200 + message: OK +- request: + body: '{"api_version":2,"rows":[{"experiment_id":"21a09505-aa26-44f3-b233-fe7ee9f38add","id":"10000000-0000-4000-8000-000000000001","input":{"case":"base"},"metadata":{"git_regression_case":"base"},"metrics":{},"output":{"status":"recorded"},"span_attributes":{"exec_counter":1,"name":"git-metadata-regression","type":"eval"},"span_parents":null}]}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '943' + User-Agent: + - python-requests/2.34.2 + method: POST + uri: https://api.braintrust.dev/logs3 + response: + body: + string: '{"ids":["10000000-0000-4000-8000-000000000001"],"xact_id":"1000197664115603136"}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 10 Aug 2026 15:14:17 GMT + Via: + - 1.1 b8bc0bb1dae1ba3e17c1d9845e9f70c4.cloudfront.net (CloudFront), 1.1 bc9d715161855640c4738aa7390d934e.cloudfront.net + (CloudFront) + X-Amz-Cf-Id: + - aOHKYfGK48jyWZ3lWeHN69t-lLicNdfkyocDc9dejxr5kcTve2qJLg== + X-Amz-Cf-Pop: + - YTO53-P2 + - YTO50-P2 + X-Amzn-Trace-Id: + - Root=1-6a79eac9-07a32f452f7ef7a45aa465a8;Parent=569e7c50a817eda0;Sampled=0;Lineage=1:24be3d11:0 + X-Cache: + - Miss from cloudfront + access-control-allow-credentials: + - 'true' + access-control-expose-headers: + - x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms,x-bt-internal-trace-id,x-bt-error-origin,x-bt-used-endpoint,x-bt-overflow-url + cache-control: + - no-store, no-cache, must-revalidate, proxy-revalidate + content-length: + - '80' + etag: + - W/"50-8QtL3rdkofnaieJXZ6uF51awGyU" + expires: + - '0' + surrogate-control: + - no-store + vary: + - Origin, Accept-Encoding + x-amz-apigw-id: + - B5GffGI6IAMEXxw= + x-amzn-RequestId: + - cb65b241-e776-4a0b-abb5-94a60d85c69e + x-bt-internal-trace-id: + - 6a79eac9000000000658b07fd4608542 + status: + code: 200 + message: OK +- request: + body: '{"ancestor_commits":["e3feac8b65efbdfc911e83cf3e6204853b6a2d8c","af25aa5dc6b2f7daf031f1f1d26d98ae70871c45"],"experiment_name":"test-git-metadata-feature-v2","org_id":"5abfae3a-7aa7-4653-a9c8-b3efcb18f584","project_id":null,"project_name":"python-sdk-vcr-tests","public":false,"repo_info":{"author_email":"git-regression@braintrust.dev","author_name":"Braintrust + Git Regression","branch":"feature/git-metadata-regression","commit":"52fc9a56dafc7af027641fb796d9a888e012066b","commit_message":"Deterministic + feature commit","commit_time":"2024-02-02T12:00:00+00:00","dirty":true,"git_diff":null,"tag":"git-regression-feature"},"update":true}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '671' + Content-Type: + - application/json + User-Agent: + - python-requests/2.34.2 + method: POST + uri: https://www.braintrust.dev/api/experiment/register + response: + body: + string: '{"project":{"id":"8b2277cd-df50-4245-b4a0-057a6ce318a9","org_id":"5abfae3a-7aa7-4653-a9c8-b3efcb18f584","name":"python-sdk-vcr-tests","description":null,"created":"2026-08-10T15:11:32.568Z","deleted_at":null,"user_id":"c1f71e19-b3ce-4f59-89a9-055901f7755b","settings":null},"experiment":{"id":"7b05ec26-59e6-431d-b6d9-6909482a35ab","project_id":"8b2277cd-df50-4245-b4a0-057a6ce318a9","name":"test-git-metadata-feature-v2","description":null,"created":"2026-08-10T15:12:35.019Z","repo_info":{"commit":"52fc9a56dafc7af027641fb796d9a888e012066b","branch":"feature/git-metadata-regression","tag":"git-regression-feature","dirty":true,"author_name":"Braintrust + Git Regression","author_email":"git-regression@braintrust.dev","commit_message":"Deterministic + feature commit","commit_time":"2024-02-02T12:00:00+00:00"},"commit":"52fc9a56dafc7af027641fb796d9a888e012066b","base_exp_id":"21a09505-aa26-44f3-b233-fe7ee9f38add","deleted_at":null,"dataset_id":null,"dataset_version":null,"internal_metadata":null,"parameters_id":null,"parameters_version":null,"public":false,"user_id":"c1f71e19-b3ce-4f59-89a9-055901f7755b","metadata":null,"tags":null}}' + headers: + Cache-Control: + - public, max-age=0, must-revalidate + Content-Security-Policy: + - 'script-src ''self'' ''unsafe-eval'' ''wasm-unsafe-eval'' ''strict-dynamic'' + ''nonce-NzcyZTZiNmQtMGE5OC00YWRmLTkzMzgtYzM5NTQzYWQwMDFm'' *.js.stripe.com + js.stripe.com maps.googleapis.com ; style-src ''self'' ''unsafe-inline'' *.braintrust.dev + btcm6qilbbhv4yi1.public.blob.vercel-storage.com fonts.googleapis.com www.gstatic.com + d4tuoctqmanu0.cloudfront.net; font-src ''self'' data: fonts.gstatic.com btcm6qilbbhv4yi1.public.blob.vercel-storage.com + cdn.jsdelivr.net d4tuoctqmanu0.cloudfront.net fonts.googleapis.com mintlify-assets.b-cdn.net + fonts.cdnfonts.com; object-src ''none''; base-uri ''self''; form-action ''self'' + https://www.facebook.com; frame-ancestors ''self''; worker-src ''self'' blob:; + report-uri https://o4507221741076480.ingest.us.sentry.io/api/4507221754380288/security/?sentry_key=27fa5ac907cf7c6ce4a1ab2a03f805b4&sentry_environment=production&sentry_release=16; + report-to csp-endpoint-0' + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 10 Aug 2026 15:14:17 GMT + Etag: + - W/"j5275fm1f6vn" + Reporting-Endpoints: + - csp-endpoint-0="https://o4507221741076480.ingest.us.sentry.io/api/4507221754380288/security/?sentry_key=27fa5ac907cf7c6ce4a1ab2a03f805b4&sentry_environment=production&sentry_release=16" + Server: + - Vercel + Strict-Transport-Security: + - max-age=63072000 + Transfer-Encoding: + - chunked + X-Clerk-Auth-Message: + - Invalid JWT form. A JWT consists of three parts separated by dots. (reason=token-invalid, + token-carrier=header) + X-Clerk-Auth-Reason: + - token-invalid + X-Clerk-Auth-Status: + - signed-out + X-Content-Type-Options: + - nosniff + X-Frame-Options: + - SAMEORIGIN + X-Matched-Path: + - /api/experiment/register + X-Nonce: + - NzcyZTZiNmQtMGE5OC00YWRmLTkzMzgtYzM5NTQzYWQwMDFm + X-Vercel-Cache: + - MISS + X-Vercel-Id: + - yul1::iad1::j2mvk-1786374857798-d8add06fcf07 + content-length: + - '1139' + status: + code: 200 + message: OK +- request: + body: '{"api_version":2,"rows":[{"experiment_id":"7b05ec26-59e6-431d-b6d9-6909482a35ab","id":"10000000-0000-4000-8000-000000000002","input":{"case":"feature-dirty"},"metadata":{"git_regression_case":"feature-dirty"},"metrics":{},"output":{"status":"recorded"},"span_attributes":{"exec_counter":2,"name":"git-metadata-regression","type":"eval"},"span_parents":null}]}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip, deflate, zstd + Connection: + - keep-alive + Content-Length: + - '961' + User-Agent: + - python-requests/2.34.2 + method: POST + uri: https://api.braintrust.dev/logs3 + response: + body: + string: '{"ids":["10000000-0000-4000-8000-000000000002"],"xact_id":"1000197664115673194"}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json; charset=utf-8 + Date: + - Mon, 10 Aug 2026 15:14:18 GMT + Via: + - 1.1 76283331d5eee8cee95c8c29a2095f48.cloudfront.net (CloudFront), 1.1 70fd8dd903406754b301439f9111e256.cloudfront.net + (CloudFront) + X-Amz-Cf-Id: + - KXksJjKa7YyH1eU4Y16StI_rw4zPCIG6efjxSXwXm1PFcOhJ7BeGhQ== + X-Amz-Cf-Pop: + - YTO53-P2 + - YTO50-P2 + X-Amzn-Trace-Id: + - Root=1-6a79eaca-3c07bd5e034fc31665d51641;Parent=1ba5bd6b13204bfb;Sampled=0;Lineage=1:24be3d11:0 + X-Cache: + - Miss from cloudfront + access-control-allow-credentials: + - 'true' + access-control-expose-headers: + - x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms,x-bt-internal-trace-id,x-bt-error-origin,x-bt-used-endpoint,x-bt-overflow-url + cache-control: + - no-store, no-cache, must-revalidate, proxy-revalidate + content-length: + - '80' + etag: + - W/"50-s87k1m2YoL255wuKo1rv2ETMVwM" + expires: + - '0' + surrogate-control: + - no-store + vary: + - Origin, Accept-Encoding + x-amz-apigw-id: + - B5GfpFQ8IAMEBxw= + x-amzn-RequestId: + - b6c3c3f4-9830-4f9c-9c18-a001f3bda038 + x-bt-internal-trace-id: + - 6a79eaca000000007350e63e04d9e93a + status: + code: 200 + message: OK +- request: + body: '{"brainstore_realtime":true,"query":{"cursor":null,"from":{"args":[{"op":"literal","value":"7b05ec26-59e6-431d-b6d9-6909482a35ab"}],"name":{"name":["experiment"],"op":"ident"},"op":"function"},"limit":1000,"select":[{"op":"star"}]},"query_source":"py_sdk_object_fetcher_experiment","use_columnstore":false}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip + Connection: + - keep-alive + Content-Length: + - '332' + Content-Type: + - application/json + User-Agent: + - python-requests/2.34.2 + method: POST + uri: https://api.braintrust.dev/btql + response: + body: + string: '{"data":[{"_pagination_key":"p07672421594721550336","_xact_id":"1000197664115673194","audit_data":[{"_xact_id":"1000197664108932421","audit_data":{"action":"upsert"},"metadata":{},"source":"api"},{"_xact_id":"1000197664115673194","audit_data":{"action":"upsert"},"metadata":{},"source":"api"}],"classifications":null,"comments":null,"context":{"caller_filename":"/Users/abhijeetprasad/.superset/worktrees/698022ff-26ce-4bd5-ac6d-5978cac9c633/abhi-git-python-dep/py/.nox/test_core/lib/python3.14/site-packages/_pytest/python.py","caller_functionname":"pytest_pyfunc_call","caller_lineno":167,"span_origin":{"instrumentation":{"name":"braintrust-python-logger"},"name":"braintrust.sdk.python","version":"0.32.0"}},"created":"2026-08-10T15:14:17.543Z","error":null,"expected":null,"experiment_id":"7b05ec26-59e6-431d-b6d9-6909482a35ab","facets":null,"id":"10000000-0000-4000-8000-000000000002","input":{"case":"feature-dirty"},"is_root":true,"metadata":{"git_regression_case":"feature-dirty"},"metrics":{"end":1786374857.5446882,"start":1786374857.54395},"origin":null,"output":{"status":"recorded"},"project_id":"8b2277cd-df50-4245-b4a0-057a6ce318a9","root_span_id":"30b2567a7f669f951986feaeb954696f","scores":null,"span_attributes":{"created_by_api_key_id":"607d8072-e7a7-48d9-a326-de588dc07bf0","created_by_user_id":"c1f71e19-b3ce-4f59-89a9-055901f7755b","exec_counter":2,"name":"git-metadata-regression","type":"eval"},"span_id":"c8635e1f15a33d62","span_parents":null,"tags":null}],"schema":{"type":"array","items":{"type":"object","properties":{"_pagination_key":{"description":"A + stable, time-ordered key that can be used to paginate over experiment events. + This field is auto-generated by Braintrust and only exists in Brainstore.","type":["string","null"]},"_xact_id":{"description":"The + transaction id of an event is unique to the network operation that processed + the event insertion. Transaction ids are monotonically increasing over time + and can be used to retrieve a versioned snapshot of the experiment (see the + `version` parameter)","type":"string"},"audit_data":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}]},"classifications":{"anyOf":[{"additionalProperties":{"items":{"additionalProperties":false,"properties":{"confidence":{"description":"Optional + confidence score for the classification","type":["number","null"]},"id":{"description":"Stable + classification identifier","type":"string"},"label":{"description":"Original + label of the classification item, which is useful for search and indexing + purposes","type":"string"},"metadata":{"anyOf":[{"additionalProperties":{},"type":"object"},{"type":"null"}],"description":"Optional + metadata associated with the classification"},"source":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"id":{"type":"string"},"type":{"const":"function","type":"string"},"version":{"description":"The + version of the function","type":"string"}},"required":["type","id"],"type":"object"},{"additionalProperties":false,"properties":{"function_type":{"default":"scorer","description":"The + type of global function. Defaults to ''scorer''.","enum":["llm","scorer","task","tool","custom_view","preprocessor","facet","classifier","tag","parameters","sandbox"],"type":"string"},"name":{"type":"string"},"type":{"const":"global","type":"string"}},"required":["type","name"],"type":"object"}]},{"type":"null"}],"description":"Optional + function identifier that produced the classification"}},"required":["id"],"type":"object"},"type":"array"},"properties":{},"type":"object"},{"type":"null"}]},"comments":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}]},"context":{"anyOf":[{"additionalProperties":{},"properties":{"caller_filename":{"description":"Name + of the file in code where the experiment event was created","type":["string","null"]},"caller_functionname":{"description":"The + function in code which created the experiment event","type":["string","null"]},"caller_lineno":{"anyOf":[{"type":"integer"},{"type":"null"}]}},"type":"object"},{"type":"null"}]},"created":{"description":"The + timestamp the experiment event was created","format":"date-time","type":"string"},"error":{"description":"The + error that occurred, if any."},"expected":{"description":"The ground truth + value (an arbitrary, JSON serializable object) that you''d compare to `output` + to determine if your `output` value is correct or not. Braintrust currently + does not compare `output` to `expected` for you, since there are so many different + ways to do that correctly. Instead, these values are just used to help you + navigate your experiments while digging into analyses. However, we may later + use these values to re-score outputs or fine-tune your models"},"experiment_id":{"description":"Unique + identifier for the experiment","format":"uuid","type":"string"},"facets":{"anyOf":[{"additionalProperties":{"type":["string","null"]},"properties":{},"type":"object"},{"type":"null"}]},"id":{"description":"A + unique identifier for the experiment event. If you don''t provide one, Braintrust + will generate one for you","type":"string"},"input":{"description":"The arguments + that uniquely define a test case (an arbitrary, JSON serializable object). + Later on, Braintrust will use the `input` to know whether two test cases are + the same between experiments, so they should not contain experiment-specific + state. A simple rule of thumb is that if you run the same experiment twice, + the `input` should be identical"},"is_root":{"description":"Whether this span + is a root span","type":["boolean","null"]},"metadata":{"anyOf":[{"additionalProperties":{},"properties":{"model":{"description":"The + model used for this example","type":["string","null"]}},"type":"object"},{"type":"null"}]},"metrics":{"anyOf":[{"additionalProperties":{"type":"number"},"properties":{"caller_filename":{"description":"This + metric is deprecated"},"caller_functionname":{"description":"This metric is + deprecated"},"caller_lineno":{"description":"This metric is deprecated"},"completion_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}]},"end":{"description":"A + unix timestamp recording when the section of code which produced the experiment + event finished","type":["number","null"]},"prompt_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}]},"start":{"description":"A + unix timestamp recording when the section of code which produced the experiment + event started","type":["number","null"]},"tokens":{"anyOf":[{"type":"integer"},{"type":"null"}]}},"type":"object"},{"type":"null"}]},"origin":{"anyOf":[{"description":"Reference + to the original object and event this was copied from.","properties":{"_xact_id":{"description":"Transaction + ID of the original event.","type":["string","null"]},"created":{"description":"Created + timestamp of the original event. Used to help sort in the UI","type":["string","null"]},"id":{"description":"ID + of the original event.","type":"string"},"object_id":{"description":"ID of + the object the event is originating from.","format":"uuid","type":"string"},"object_type":{"description":"Type + of the object the event is originating from.","enum":["project_logs","experiment","dataset","prompt","function","prompt_session"],"type":"string"}},"required":["object_type","object_id","id"],"type":"object"},{"type":"null"}]},"output":{"description":"The + output of your application, including post-processing (an arbitrary, JSON + serializable object), that allows you to determine whether the result is correct + or not. For example, in an app that generates SQL queries, the `output` should + be the _result_ of the SQL query generated by the model, not the query itself, + because there may be multiple valid queries that answer a single question"},"project_id":{"description":"Unique + identifier for the project that the experiment belongs under","format":"uuid","type":"string"},"root_span_id":{"description":"A + unique identifier for the trace this experiment event belongs to","type":"string"},"scores":{"anyOf":[{"additionalProperties":{"anyOf":[{"maximum":1,"minimum":0,"type":"number"},{"type":"null"}]},"properties":{},"type":"object"},{"type":"null"}]},"span_attributes":{"anyOf":[{"additionalProperties":{},"description":"Human-identifying + attributes of the span, such as name, type, etc.","properties":{"name":{"description":"Name + of the span, for display purposes only","type":["string","null"]},"purpose":{"anyOf":[{"enum":["scorer"],"type":"string"},{"type":"null"}]},"type":{"anyOf":[{"enum":["llm","score","function","eval","task","tool","automation","facet","preprocessor","classifier","review"],"type":"string"},{"type":"null"}]}},"type":"object"},{"type":"null"}]},"span_id":{"description":"A + unique identifier used to link different experiment events together as part + of a full trace. See the [tracing guide](https://www.braintrust.dev/docs/instrument) + for full details on tracing","type":"string"},"span_parents":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}]},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}]}}}},"cursor":"annqykhqAAA","realtime_state":{"type":"on","minimum_xact_id":null,"read_bytes":2330,"actual_xact_id":"1000197664115673194"},"freshness_state":{"last_processed_xact_id":"1000197664108932421","last_considered_xact_id":"1000197664115673194"},"warnings":[]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Mon, 10 Aug 2026 15:14:18 GMT + Via: + - 1.1 7f51f788f893911f0cd6b09d9ab21c4a.cloudfront.net (CloudFront), 1.1 4f3eaee3896fb5ad2377261bd0d773c8.cloudfront.net + (CloudFront) + X-Amz-Cf-Id: + - LfIA4zVZxsPTc95tdW7IpsuOz-VKiq8OeJmHa8EcxE73qeChOKMaaA== + X-Amz-Cf-Pop: + - YTO53-P2 + - YTO50-P2 + X-Amzn-Trace-Id: + - Root=1-6a79eaca-125e559661b5bc0647b2ff6f;Parent=13eadb93e54ff07a;Sampled=0;Lineage=1:24be3d11:0 + X-Cache: + - Miss from cloudfront + access-control-allow-credentials: + - 'true' + access-control-expose-headers: + - x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms,x-bt-internal-trace-id,x-bt-error-origin,x-bt-used-endpoint,x-bt-overflow-url + cache-control: + - private, no-cache + content-length: + - '9266' + vary: + - Origin + x-amz-apigw-id: + - B5GfrGyNIAMEEhg= + x-amzn-RequestId: + - 1871d17c-f861-4fe0-87b0-bfbf2fe4a96e + x-bt-api-duration-ms: + - '204' + x-bt-brainstore-duration-ms: + - '155' + x-bt-cursor: + - annqykhqAAA + x-bt-internal-trace-id: + - 6a79eaca000000002fec996d6eb6decf + status: + code: 200 + message: OK +- request: + body: '{"brainstore_realtime":true,"query":{"cursor":"annqykhqAAA","from":{"args":[{"op":"literal","value":"7b05ec26-59e6-431d-b6d9-6909482a35ab"}],"name":{"name":["experiment"],"op":"ident"},"op":"function"},"limit":1000,"select":[{"op":"star"}]},"query_source":"py_sdk_object_fetcher_experiment","use_columnstore":false}' + headers: + Accept: + - '*/*' + Accept-Encoding: + - gzip + Connection: + - keep-alive + Content-Length: + - '341' + Content-Type: + - application/json + User-Agent: + - python-requests/2.34.2 + method: POST + uri: https://api.braintrust.dev/btql + response: + body: + string: '{"data":[],"schema":{"type":"array","items":{"type":"object","properties":{"_pagination_key":{"description":"A + stable, time-ordered key that can be used to paginate over experiment events. + This field is auto-generated by Braintrust and only exists in Brainstore.","type":["string","null"]},"_xact_id":{"description":"The + transaction id of an event is unique to the network operation that processed + the event insertion. Transaction ids are monotonically increasing over time + and can be used to retrieve a versioned snapshot of the experiment (see the + `version` parameter)","type":"string"},"audit_data":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}]},"classifications":{"anyOf":[{"additionalProperties":{"items":{"additionalProperties":false,"properties":{"confidence":{"description":"Optional + confidence score for the classification","type":["number","null"]},"id":{"description":"Stable + classification identifier","type":"string"},"label":{"description":"Original + label of the classification item, which is useful for search and indexing + purposes","type":"string"},"metadata":{"anyOf":[{"additionalProperties":{},"type":"object"},{"type":"null"}],"description":"Optional + metadata associated with the classification"},"source":{"anyOf":[{"anyOf":[{"additionalProperties":false,"properties":{"id":{"type":"string"},"type":{"const":"function","type":"string"},"version":{"description":"The + version of the function","type":"string"}},"required":["type","id"],"type":"object"},{"additionalProperties":false,"properties":{"function_type":{"default":"scorer","description":"The + type of global function. Defaults to ''scorer''.","enum":["llm","scorer","task","tool","custom_view","preprocessor","facet","classifier","tag","parameters","sandbox"],"type":"string"},"name":{"type":"string"},"type":{"const":"global","type":"string"}},"required":["type","name"],"type":"object"}]},{"type":"null"}],"description":"Optional + function identifier that produced the classification"}},"required":["id"],"type":"object"},"type":"array"},"properties":{},"type":"object"},{"type":"null"}]},"comments":{"anyOf":[{"items":{},"type":"array"},{"type":"null"}]},"context":{"anyOf":[{"additionalProperties":{},"properties":{"caller_filename":{"description":"Name + of the file in code where the experiment event was created","type":["string","null"]},"caller_functionname":{"description":"The + function in code which created the experiment event","type":["string","null"]},"caller_lineno":{"anyOf":[{"type":"integer"},{"type":"null"}]}},"type":"object"},{"type":"null"}]},"created":{"description":"The + timestamp the experiment event was created","format":"date-time","type":"string"},"error":{"description":"The + error that occurred, if any."},"expected":{"description":"The ground truth + value (an arbitrary, JSON serializable object) that you''d compare to `output` + to determine if your `output` value is correct or not. Braintrust currently + does not compare `output` to `expected` for you, since there are so many different + ways to do that correctly. Instead, these values are just used to help you + navigate your experiments while digging into analyses. However, we may later + use these values to re-score outputs or fine-tune your models"},"experiment_id":{"description":"Unique + identifier for the experiment","format":"uuid","type":"string"},"facets":{"anyOf":[{"additionalProperties":{"type":["string","null"]},"properties":{},"type":"object"},{"type":"null"}]},"id":{"description":"A + unique identifier for the experiment event. If you don''t provide one, Braintrust + will generate one for you","type":"string"},"input":{"description":"The arguments + that uniquely define a test case (an arbitrary, JSON serializable object). + Later on, Braintrust will use the `input` to know whether two test cases are + the same between experiments, so they should not contain experiment-specific + state. A simple rule of thumb is that if you run the same experiment twice, + the `input` should be identical"},"is_root":{"description":"Whether this span + is a root span","type":["boolean","null"]},"metadata":{"anyOf":[{"additionalProperties":{},"properties":{"model":{"description":"The + model used for this example","type":["string","null"]}},"type":"object"},{"type":"null"}]},"metrics":{"anyOf":[{"additionalProperties":{"type":"number"},"properties":{"caller_filename":{"description":"This + metric is deprecated"},"caller_functionname":{"description":"This metric is + deprecated"},"caller_lineno":{"description":"This metric is deprecated"},"completion_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}]},"end":{"description":"A + unix timestamp recording when the section of code which produced the experiment + event finished","type":["number","null"]},"prompt_tokens":{"anyOf":[{"type":"integer"},{"type":"null"}]},"start":{"description":"A + unix timestamp recording when the section of code which produced the experiment + event started","type":["number","null"]},"tokens":{"anyOf":[{"type":"integer"},{"type":"null"}]}},"type":"object"},{"type":"null"}]},"origin":{"anyOf":[{"description":"Reference + to the original object and event this was copied from.","properties":{"_xact_id":{"description":"Transaction + ID of the original event.","type":["string","null"]},"created":{"description":"Created + timestamp of the original event. Used to help sort in the UI","type":["string","null"]},"id":{"description":"ID + of the original event.","type":"string"},"object_id":{"description":"ID of + the object the event is originating from.","format":"uuid","type":"string"},"object_type":{"description":"Type + of the object the event is originating from.","enum":["project_logs","experiment","dataset","prompt","function","prompt_session"],"type":"string"}},"required":["object_type","object_id","id"],"type":"object"},{"type":"null"}]},"output":{"description":"The + output of your application, including post-processing (an arbitrary, JSON + serializable object), that allows you to determine whether the result is correct + or not. For example, in an app that generates SQL queries, the `output` should + be the _result_ of the SQL query generated by the model, not the query itself, + because there may be multiple valid queries that answer a single question"},"project_id":{"description":"Unique + identifier for the project that the experiment belongs under","format":"uuid","type":"string"},"root_span_id":{"description":"A + unique identifier for the trace this experiment event belongs to","type":"string"},"scores":{"anyOf":[{"additionalProperties":{"anyOf":[{"maximum":1,"minimum":0,"type":"number"},{"type":"null"}]},"properties":{},"type":"object"},{"type":"null"}]},"span_attributes":{"anyOf":[{"additionalProperties":{},"description":"Human-identifying + attributes of the span, such as name, type, etc.","properties":{"name":{"description":"Name + of the span, for display purposes only","type":["string","null"]},"purpose":{"anyOf":[{"enum":["scorer"],"type":"string"},{"type":"null"}]},"type":{"anyOf":[{"enum":["llm","score","function","eval","task","tool","automation","facet","preprocessor","classifier","review"],"type":"string"},{"type":"null"}]}},"type":"object"},{"type":"null"}]},"span_id":{"description":"A + unique identifier used to link different experiment events together as part + of a full trace. See the [tracing guide](https://www.braintrust.dev/docs/instrument) + for full details on tracing","type":"string"},"span_parents":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}]},"tags":{"anyOf":[{"items":{"type":"string"},"type":"array"},{"type":"null"}]}}}},"realtime_state":{"type":"on","minimum_xact_id":null,"read_bytes":2330,"actual_xact_id":"1000197664115673194"},"freshness_state":{"last_processed_xact_id":"1000197664108932421","last_considered_xact_id":"1000197664115673194"},"warnings":[]}' + headers: + Connection: + - keep-alive + Content-Type: + - application/json + Date: + - Mon, 10 Aug 2026 15:14:18 GMT + Via: + - 1.1 76283331d5eee8cee95c8c29a2095f48.cloudfront.net (CloudFront), 1.1 bc9d715161855640c4738aa7390d934e.cloudfront.net + (CloudFront) + X-Amz-Cf-Id: + - VxqYgBxn9TeK-IPw66N27vRtbQA-ics2iHApaeO833j4qB8Hs7DaCg== + X-Amz-Cf-Pop: + - YTO53-P2 + - YTO50-P2 + X-Amzn-Trace-Id: + - Root=1-6a79eaca-561b52663497aa626244bb33;Parent=454bb788ddf17bb4;Sampled=0;Lineage=1:24be3d11:0 + X-Cache: + - Miss from cloudfront + access-control-allow-credentials: + - 'true' + access-control-expose-headers: + - x-bt-cursor,x-bt-found-existing,x-bt-query-plan,x-bt-api-duration-ms,x-bt-brainstore-duration-ms,x-bt-internal-trace-id,x-bt-error-origin,x-bt-used-endpoint,x-bt-overflow-url + cache-control: + - private, no-cache + content-length: + - '7771' + vary: + - Origin + x-amz-apigw-id: + - B5GfvHNAIAMEhVw= + x-amzn-RequestId: + - d1bb8ed4-c0d7-4494-964d-74168f17e900 + x-bt-api-duration-ms: + - '264' + x-bt-brainstore-duration-ms: + - '194' + x-bt-internal-trace-id: + - 6a79eaca00000000638990b0bfc546e6 + status: + code: 200 + message: OK +version: 1 diff --git a/py/src/braintrust/gitutil.py b/py/src/braintrust/gitutil.py index bc96206c7..69887b6be 100644 --- a/py/src/braintrust/gitutil.py +++ b/py/src/braintrust/gitutil.py @@ -1,6 +1,7 @@ import logging import os import re +import shutil import subprocess import threading from functools import lru_cache as _cache @@ -8,33 +9,75 @@ from .git_fields import GitMetadataSettings, RepoInfo -# https://stackoverflow.com/questions/48399498/git-executable-not-found-in-python -os.environ["GIT_PYTHON_REFRESH"] = "quiet" -try: - import git -except ImportError: - git = None - _logger = logging.getLogger("braintrust.gitutil") _gitlock = threading.RLock() @_cache(1) -def _current_repo(): - if git is None: - # If the git module is not available, we can't do anything. +def _git_executable(): + """Resolve `git` from PATH, ignoring any copy in the current directory. + + Handing subprocess an absolute path avoids the current-directory-first executable + search Windows performs at spawn time, where a `git.exe` committed to a repository + would otherwise shadow the real one. `shutil.which()` searches the current + directory on Windows as well, so a relative hit is rejected rather than used. + """ + resolved = shutil.which("git") + if resolved is not None and not os.path.isabs(resolved): + _logger.warning("Ignoring 'git' resolved from the current directory: %s", resolved) return None + return resolved + + +def _git_output(args, cwd=None): + git = _git_executable() + if git is None: + raise FileNotFoundError("Could not find a 'git' executable on PATH") + + result = subprocess.run( + [git, *args], + cwd=cwd, + check=True, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + ) + output = result.stdout + if output.endswith(b"\n"): + output = output[:-1] + return output.decode("utf-8", errors="surrogateescape") + +def _normalize_git_iso_datetime(value): + return value[:-1] + "+00:00" if value.endswith("Z") else value + + +def _is_dirty(repo_path): + diff_args = ["--abbrev=40", "--full-index", "--raw"] + if _git_output(["diff", "--cached", *diff_args], cwd=repo_path): + return True + return bool(_git_output(["diff", *diff_args], cwd=repo_path)) + + +@_cache(1) +def _current_repo(): try: - return git.Repo(search_parent_directories=True) - except git.exc.InvalidGitRepositoryError: + return _git_output(["rev-parse", "--show-toplevel"]).strip() + except (OSError, subprocess.CalledProcessError): + try: + if _git_output(["rev-parse", "--is-bare-repository"]).strip() == "true": + return _git_output(["rev-parse", "--absolute-git-dir"]).strip() + except (OSError, subprocess.CalledProcessError): + pass return None @_cache(1) def _get_base_branch(remote=None): - repo = _current_repo() - remote = repo.remote(**({} if remote is None else {"name": remote})).name + repo_path = _current_repo() + remote = remote or "origin" + remotes = set(_git_output(["remote"], cwd=repo_path).splitlines()) + if remote not in remotes: + raise ValueError(f"Remote named '{remote}' didn't exist") # NOTE: This should potentially be configuration that we derive from the project, # instead of spending a second or two computing it each time we run an experiment. @@ -42,7 +85,9 @@ def _get_base_branch(remote=None): # To speed this up in the short term, we pick from a list of common names # and only fall back to the remote origin if required. COMMON_BASE_BRANCHES = ["main", "master", "develop"] - repo_branches = {b.name for b in repo.branches} + repo_branches = set( + _git_output(["for-each-ref", "--format=%(refname:short)", "refs/heads/"], cwd=repo_path).splitlines() + ) if sum(b in repo_branches for b in COMMON_BASE_BRANCHES) == 1: for b in COMMON_BASE_BRANCHES: if b in repo_branches: @@ -50,7 +95,7 @@ def _get_base_branch(remote=None): raise RuntimeError("Impossible") try: - s = subprocess.check_output(["git", "remote", "show", "origin"]).decode() + s = _git_output(["remote", "show", "origin"], cwd=repo_path) match = re.search(r"\s*HEAD branch:\s*(.*)$", s, re.MULTILINE) if match is None: raise RuntimeError("Could not find HEAD branch in remote " + remote) @@ -71,46 +116,35 @@ def _get_base_branch_ancestor(remote=None): return None try: - head = "HEAD" if _current_repo().is_dirty() else "HEAD^" - return subprocess.check_output(["git", "merge-base", head, f"{remote_name}/{base_branch}"]).decode().strip() - except (subprocess.CalledProcessError, git.GitCommandError) as e: + repo_path = _current_repo() + head = "HEAD" if _is_dirty(repo_path) else "HEAD^" + return _git_output(["merge-base", head, f"{remote_name}/{base_branch}"], cwd=repo_path).strip() + except subprocess.CalledProcessError as e: # _logger.warning(f"Could not find a common ancestor with {remote_name}/{base_branch}") return None def get_past_n_ancestors(n=1000, remote=None): with _gitlock: - repo = _current_repo() - if repo is None: + repo_path = _current_repo() + if repo_path is None or n <= 0: return ancestor_output = _get_base_branch_ancestor() if ancestor_output is None: return - ancestor = repo.commit(ancestor_output) - count = 0 - for _ in range(n): - if count >= n: - break - yield ancestor.hexsha - count += 1 - try: - if ancestor.parents: - ancestor = ancestor.parents[0] - else: - break - except ValueError: - # Since parents are fetched on-demand, this can happen if the - # downloaded repo does not have information for this commit's - # parent. - break + try: + output = _git_output(["rev-list", "--first-parent", f"--max-count={n}", ancestor_output], cwd=repo_path) + except subprocess.CalledProcessError: + return + yield from output.splitlines() def attempt(op): try: return op() # OSError covers FileNotFoundError, FileExistsError, etc. - except (TypeError, ValueError, OSError, git.GitCommandError): + except (TypeError, ValueError, OSError, subprocess.CalledProcessError): return None @@ -137,8 +171,8 @@ def get_repo_info(settings: GitMetadataSettings | None = None): def repo_info(): with _gitlock: - repo = _current_repo() - if repo is None: + repo_path = _current_repo() + if repo_path is None: return None commit = None @@ -150,19 +184,25 @@ def repo_info(): branch = None git_diff = None - dirty = attempt(lambda: repo.is_dirty()) + dirty = attempt(lambda: _is_dirty(repo_path)) - commit = attempt(lambda: repo.head.commit.hexsha.strip()) - commit_message = attempt(lambda: repo.head.commit.message.strip()) - commit_time = attempt(lambda: repo.head.commit.committed_datetime.isoformat()) - author_name = attempt(lambda: repo.head.commit.author.name.strip()) - author_email = attempt(lambda: repo.head.commit.author.email.strip()) - tag = attempt(lambda: repo.git.describe("--tags", "--exact-match", "--always")) + commit = attempt(lambda: _git_output(["rev-parse", "HEAD"], cwd=repo_path).strip()) + commit_message = attempt(lambda: _git_output(["log", "-1", "--format=%B", "HEAD"], cwd=repo_path).strip()) + commit_time = attempt( + lambda: _normalize_git_iso_datetime( + _git_output(["log", "-1", "--format=%cI", "HEAD"], cwd=repo_path).strip() + ) + ) + author_name = attempt(lambda: _git_output(["log", "-1", "--format=%an", "HEAD"], cwd=repo_path).strip()) + author_email = attempt(lambda: _git_output(["log", "-1", "--format=%ae", "HEAD"], cwd=repo_path).strip()) + tag = attempt(lambda: _git_output(["describe", "--tags", "--exact-match", "--always"], cwd=repo_path).strip()) - branch = attempt(lambda: repo.active_branch.name) + branch = attempt(lambda: _git_output(["symbolic-ref", "--quiet", "--short", "HEAD"], cwd=repo_path).strip()) if dirty: - git_diff = attempt(lambda: truncate_to_byte_limit(repo.git.diff("HEAD", no_ext_diff=True))) + git_diff = attempt( + lambda: truncate_to_byte_limit(_git_output(["diff", "--no-ext-diff", "HEAD"], cwd=repo_path)) + ) return RepoInfo( commit=commit, diff --git a/py/src/braintrust/test_git_metadata_vcr.py b/py/src/braintrust/test_git_metadata_vcr.py new file mode 100644 index 000000000..5b5a3c3a1 --- /dev/null +++ b/py/src/braintrust/test_git_metadata_vcr.py @@ -0,0 +1,197 @@ +import json +import os +import subprocess +from pathlib import Path +from urllib.parse import urlparse + +import pytest +from braintrust import gitutil +from braintrust.conftest import get_vcr_config +from braintrust.git_fields import GitMetadataSettings +from braintrust.logger import init + + +PROJECT_NAME = "python-sdk-vcr-tests" +BASE_EXPERIMENT_NAME = "test-git-metadata-base-v2" +FEATURE_EXPERIMENT_NAME = "test-git-metadata-feature-v2" +BASE_SPAN_ID = "10000000-0000-4000-8000-000000000001" +FEATURE_SPAN_ID = "10000000-0000-4000-8000-000000000002" +GIT_METADATA_FIELDS = [ + "commit", + "branch", + "tag", + "dirty", + "author_name", + "author_email", + "commit_message", + "commit_time", +] + + +def _git(repo: Path, *args: str, env: dict[str, str] | None = None) -> str: + result = subprocess.run( + ["git", *args], + cwd=repo, + check=True, + capture_output=True, + text=True, + env=env, + ) + return result.stdout.strip() + + +def _commit(repo: Path, message: str, timestamp: str) -> str: + env = os.environ.copy() + env.update( + { + "GIT_AUTHOR_DATE": timestamp, + "GIT_COMMITTER_DATE": timestamp, + } + ) + _git(repo, "add", ".") + _git(repo, "commit", "-m", message, env=env) + return _git(repo, "rev-parse", "HEAD") + + +def _initialize_repository(tmp_path: Path) -> tuple[Path, str]: + repo = tmp_path / "repo" + repo.mkdir() + _git(repo, "init") + _git(repo, "symbolic-ref", "HEAD", "refs/heads/main") + _git(repo, "config", "user.name", "Braintrust Git Regression") + _git(repo, "config", "user.email", "git-regression@braintrust.dev") + (repo / "application.py").write_text('print("seed")\n') + _commit(repo, "Deterministic seed commit", "2024-01-31T12:00:00+00:00") + (repo / "application.py").write_text('print("base")\n') + base_commit = _commit(repo, "Deterministic base commit", "2024-02-01T12:00:00+00:00") + _git(repo, "tag", "git-regression-base") + + remote = tmp_path / "remote.git" + remote.mkdir() + _git(remote, "init", "--bare") + _git(remote, "symbolic-ref", "HEAD", "refs/heads/main") + _git(repo, "remote", "add", "origin", str(remote)) + _git(repo, "push", "--set-upstream", "origin", "main") + return repo, base_commit + + +def _api_key() -> str: + return os.environ.get("BRAINTRUST_API_KEY", "sk-dummy-for-vcr-replay") + + +def _git_metadata_settings() -> GitMetadataSettings: + return GitMetadataSettings(collect="some", fields=GIT_METADATA_FIELDS) + + +def _normalize_vcr_request(request): + if not request.body: + return request + + try: + payload = json.loads(request.body) + except (TypeError, ValueError): + return request + + if urlparse(request.uri).path == "/logs3": + for row in payload.get("rows", []): + row.pop("context", None) + row.pop("created", None) + row.pop("root_span_id", None) + row.pop("span_id", None) + metrics = row.get("metrics", {}) + metrics.pop("start", None) + metrics.pop("end", None) + + request.body = json.dumps(payload, sort_keys=True, separators=(",", ":")).encode() + return request + + +@pytest.fixture(scope="module") +def vcr_config(): + config = get_vcr_config() + config["before_record_request"] = _normalize_vcr_request + config["match_on"] = ["method", "scheme", "host", "port", "path", "query", "body"] + return config + + +def _log_regression_span(experiment, span_id: str, case: str) -> None: + with experiment.start_span( + id=span_id, + name="git-metadata-regression", + input={"case": case}, + metadata={"git_regression_case": case}, + ) as span: + span.log(output={"status": "recorded"}) + experiment.flush() + + +@pytest.fixture(autouse=True) +def clear_gitutil_caches(): + gitutil._current_repo.cache_clear() + gitutil._get_base_branch.cache_clear() + yield + gitutil._current_repo.cache_clear() + gitutil._get_base_branch.cache_clear() + + +@pytest.mark.vcr +def test_git_metadata_is_preserved_in_braintrust_data(tmp_path: Path, monkeypatch: pytest.MonkeyPatch): + repo, base_commit = _initialize_repository(tmp_path) + monkeypatch.chdir(repo) + + base_experiment = init( + project=PROJECT_NAME, + experiment=BASE_EXPERIMENT_NAME, + api_key=_api_key(), + git_metadata_settings=_git_metadata_settings(), + update=True, + set_current=False, + ) + _log_regression_span(base_experiment, BASE_SPAN_ID, "base") + + assert base_experiment.data["repo_info"] == { + "commit": base_commit, + "branch": "main", + "tag": "git-regression-base", + "dirty": False, + "author_name": "Braintrust Git Regression", + "author_email": "git-regression@braintrust.dev", + "commit_message": "Deterministic base commit", + "commit_time": "2024-02-01T12:00:00+00:00", + } + assert base_experiment.data["commit"] == base_commit + + _git(repo, "checkout", "-b", "feature/git-metadata-regression") + (repo / "application.py").write_text('print("feature")\n') + feature_commit = _commit(repo, "Deterministic feature commit", "2024-02-02T12:00:00+00:00") + _git(repo, "tag", "git-regression-feature") + (repo / "application.py").write_text('print("dirty feature")\n') + + feature_experiment = init( + project=PROJECT_NAME, + experiment=FEATURE_EXPERIMENT_NAME, + api_key=_api_key(), + git_metadata_settings=_git_metadata_settings(), + update=True, + set_current=False, + ) + _log_regression_span(feature_experiment, FEATURE_SPAN_ID, "feature-dirty") + + assert feature_experiment.data["repo_info"] == { + "commit": feature_commit, + "branch": "feature/git-metadata-regression", + "tag": "git-regression-feature", + "dirty": True, + "author_name": "Braintrust Git Regression", + "author_email": "git-regression@braintrust.dev", + "commit_message": "Deterministic feature commit", + "commit_time": "2024-02-02T12:00:00+00:00", + } + assert feature_experiment.data["commit"] == feature_commit + assert feature_experiment.data["base_exp_id"] == base_experiment.id + + fetched_span = next(row for row in feature_experiment.fetch() if row["id"] == FEATURE_SPAN_ID) + assert fetched_span["input"] == {"case": "feature-dirty"} + assert fetched_span["output"] == {"status": "recorded"} + assert fetched_span["metadata"]["git_regression_case"] == "feature-dirty" + assert "repo_info" not in fetched_span diff --git a/py/src/braintrust/test_gitutil.py b/py/src/braintrust/test_gitutil.py new file mode 100644 index 000000000..2cb0eee10 --- /dev/null +++ b/py/src/braintrust/test_gitutil.py @@ -0,0 +1,65 @@ +import os +import subprocess + +import pytest +from braintrust import gitutil + + +@pytest.fixture(autouse=True) +def clear_git_executable_cache(): + gitutil._git_executable.cache_clear() + gitutil._current_repo.cache_clear() + yield + gitutil._git_executable.cache_clear() + gitutil._current_repo.cache_clear() + + +def test_git_executable_resolves_to_an_absolute_path(): + resolved = gitutil._git_executable() + assert resolved is not None + assert os.path.isabs(resolved) + + +def test_git_output_spawns_the_resolved_absolute_path(monkeypatch: pytest.MonkeyPatch): + """git must be spawned by absolute path, never by bare name. + + Windows' CreateProcess searches the current directory before PATH, so spawning + "git" from inside an untrusted repository would run a committed git.exe. + """ + spawned = [] + + def fake_run(args, **kwargs): + spawned.append(args) + return subprocess.CompletedProcess(args, 0, stdout=b"ok\n", stderr=b"") + + monkeypatch.setattr(gitutil.subprocess, "run", fake_run) + + assert gitutil._git_output(["rev-parse", "HEAD"]) == "ok" + assert len(spawned) == 1 + assert spawned[0][0] == gitutil._git_executable() + assert os.path.isabs(spawned[0][0]) + assert spawned[0][1:] == ["rev-parse", "HEAD"] + + +def test_git_executable_rejects_a_current_directory_hit(monkeypatch: pytest.MonkeyPatch): + """shutil.which() searches the current directory on Windows; that hit must be dropped.""" + monkeypatch.setattr(gitutil.shutil, "which", lambda cmd: os.path.join(os.curdir, "git.exe")) + + assert gitutil._git_executable() is None + + +def test_git_output_raises_when_git_cannot_be_resolved(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(gitutil.shutil, "which", lambda cmd: None) + + # FileNotFoundError is an OSError, which the callers below already treat as + # "no git metadata available". + with pytest.raises(FileNotFoundError): + gitutil._git_output(["rev-parse", "HEAD"]) + + +def test_repo_info_returns_none_when_git_cannot_be_resolved(monkeypatch: pytest.MonkeyPatch): + monkeypatch.setattr(gitutil.shutil, "which", lambda cmd: None) + + assert gitutil._current_repo() is None + assert gitutil.repo_info() is None + assert list(gitutil.get_past_n_ancestors()) == [] diff --git a/py/uv.lock b/py/uv.lock index 4124f0319..711e6d1e3 100644 --- a/py/uv.lock +++ b/py/uv.lock @@ -668,7 +668,6 @@ source = { editable = "." } dependencies = [ { name = "chevron" }, { name = "exceptiongroup" }, - { name = "gitpython" }, { name = "jsonschema" }, { name = "packaging" }, { name = "python-slugify" }, @@ -912,7 +911,6 @@ requires-dist = [ { name = "boto3", marker = "extra == 'cli'" }, { name = "chevron" }, { name = "exceptiongroup", specifier = ">=1.2.0" }, - { name = "gitpython", specifier = ">=3.1.50" }, { name = "jsonschema" }, { name = "openai-agents", marker = "extra == 'all'" }, { name = "openai-agents", marker = "extra == 'openai-agents'" }, @@ -2235,30 +2233,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/dc/b7/8c72dedbb950d88a6f64588fcbc590d2a21e2b9f19b36aa6c5016c54ec75/gepa-0.1.1-py3-none-any.whl", hash = "sha256:71ead7c591eafcc727b83509cdc4182f20264800a6ddf8520d61419daeb47466", size = 244246, upload-time = "2026-03-16T10:17:51.922Z" }, ] -[[package]] -name = "gitdb" -version = "4.0.12" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "smmap" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/72/94/63b0fc47eb32792c7ba1fe1b694daec9a63620db1e313033d18140c2320a/gitdb-4.0.12.tar.gz", hash = "sha256:5ef71f855d191a3326fcfbc0d5da835f26b13fbcba60c32c21091c349ffdb571", size = 394684, upload-time = "2025-01-02T07:20:46.413Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/a0/61/5c78b91c3143ed5c14207f463aecfc8f9dbb5092fb2869baf37c273b2705/gitdb-4.0.12-py3-none-any.whl", hash = "sha256:67073e15955400952c6565cc3e707c554a4eea2e428946f7a4c162fab9bd9bcf", size = 62794, upload-time = "2025-01-02T07:20:43.624Z" }, -] - -[[package]] -name = "gitpython" -version = "3.1.59" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "gitdb" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/ca/dc/126b28e76b24a9268ba931ad3e012f71ebdadf62fd9f17758f7074bb0b20/gitpython-3.1.59.tar.gz", hash = "sha256:0a1475cfdc38a5bfba1a3e9a4a9da52a39749ecec322b772915c019f94e5b7e4", size = 230445, upload-time = "2026-08-10T12:03:20.271Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ef/ed/ae57eb7d344f43f87b74b3a281ead6ec7d6394eef72a7b1dcb28dd089550/gitpython-3.1.59-py3-none-any.whl", hash = "sha256:67a82f537384578643624c8b2c531938a9b82be431663e575dcf638526631d4c", size = 220996, upload-time = "2026-08-10T12:03:18.804Z" }, -] - [[package]] name = "google-adk" version = "2.6.2" @@ -7013,15 +6987,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, ] -[[package]] -name = "smmap" -version = "5.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/1f/ea/49c993d6dfdd7338c9b1000a0f36817ed7ec84577ae2e52f890d1a4ff909/smmap-5.0.3.tar.gz", hash = "sha256:4d9debb8b99007ae47165abc08670bd74cb74b5227dda7f643eccc4e9eb5642c", size = 22506, upload-time = "2026-03-09T03:43:26.1Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c1/d4/59e74daffcb57a07668852eeeb6035af9f32cbfd7a1d2511f17d2fe6a738/smmap-5.0.3-py3-none-any.whl", hash = "sha256:c106e05d5a61449cf6ba9a1e650227ecfb141590d2a98412103ff35d89fc7b2f", size = 24390, upload-time = "2026-03-09T03:43:24.361Z" }, -] - [[package]] name = "sniffio" version = "1.3.1"