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"