From 6f5a3fe8aff4919bd7fc7cebe5d908c8ed642600 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Wed, 29 Oct 2025 20:11:21 +0200 Subject: [PATCH 01/15] WIP --- .../_internal/exporters/processor_wrapper.py | 38 +++++++++++++++++++ logfire/_internal/utils.py | 6 ++- 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index deadeb9aa..4829ef6b4 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -1,8 +1,10 @@ from __future__ import annotations +import base64 import json from contextlib import suppress from dataclasses import dataclass +from io import BytesIO from typing import Any, cast from urllib.parse import parse_qs, urlparse @@ -30,6 +32,7 @@ handle_internal_errors, is_asgi_send_receive_span_name, is_instrumentation_suppressed, + sha256_bytes, span_to_dict, truncate_string, ) @@ -86,11 +89,46 @@ def on_end(self, span: ReadableSpan) -> None: _transform_google_genai_span(span_dict) _transform_litellm_span(span_dict) _default_gen_ai_response_model(span_dict) + _upload_gen_ai_blobs(span_dict) self.scrubber.scrub_span(span_dict) span = ReadableSpan(**span_dict) super().on_end(span) +def _upload_gen_ai_blobs(span: ReadableSpanDict) -> None: + # TODO: + # other attributes + # error handling + if 'pydantic_ai.all_messages' not in span['attributes']: + return + messages = json.loads(span['attributes']['pydantic_ai.all_messages']) + for message in messages: + parts = message.get('parts', []) + for i, part in enumerate(parts): + # TODO otel semantic type + if part.get('type') != 'binary' or 'content' not in part: + continue + data = part['content'] + if not isinstance(data, str): + continue + + value = base64.b64decode(data) # TODO handle errors + # TODO date + key = sha256_bytes(value) + + # todo move to config + from google.cloud import storage + + storage_client = storage.Client() + bucket = storage_client.bucket('alexmojaki-test') + blob = bucket.blob(key) + # TODO media type in hash + blob.upload_from_file(BytesIO(value), content_type=part.get('media_type', 'application/octet-stream')) + # TODO keep part, remove content, add new key, make frontend work + parts[i] = dict(type='image-url', url=f'https://storage.cloud.google.com/alexmojaki-test/{key}') + span['attributes'] = {**span['attributes'], 'pydantic_ai.all_messages': json.dumps(messages)} + + def _set_error_level_and_status(span: ReadableSpanDict) -> None: """Default the log level to error if the status code is error, and vice versa. diff --git a/logfire/_internal/utils.py b/logfire/_internal/utils.py index 3905cfcb3..27c98e96b 100644 --- a/logfire/_internal/utils.py +++ b/logfire/_internal/utils.py @@ -499,6 +499,10 @@ def canonicalize_exception_traceback(exc: BaseException, seen: set[int] | None = def sha256_string(s: str) -> str: + return sha256_bytes(s.encode('utf-8')) + + +def sha256_bytes(b: bytes) -> str: hasher = hashlib.sha256() - hasher.update(s.encode('utf-8')) + hasher.update(b) return hasher.hexdigest() From 2345d7b77ea3308efc8ef2e48bfdb91da88dc791 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 17:38:05 +0200 Subject: [PATCH 02/15] WIP --- .../_internal/exporters/processor_wrapper.py | 28 ++-- logfire/experimental/uploaders/gcs.py | 17 +++ pyproject.toml | 1 + uv.lock | 127 +++++++++++++++++- 4 files changed, 160 insertions(+), 13 deletions(-) create mode 100644 logfire/experimental/uploaders/gcs.py diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index 4829ef6b4..e28a9fe10 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -4,7 +4,6 @@ import json from contextlib import suppress from dataclasses import dataclass -from io import BytesIO from typing import Any, cast from urllib.parse import parse_qs, urlparse @@ -33,6 +32,7 @@ is_asgi_send_receive_span_name, is_instrumentation_suppressed, sha256_bytes, + sha256_string, span_to_dict, truncate_string, ) @@ -99,9 +99,14 @@ def _upload_gen_ai_blobs(span: ReadableSpanDict) -> None: # TODO: # other attributes # error handling - if 'pydantic_ai.all_messages' not in span['attributes']: + attr_name = 'pydantic_ai.all_messages' + attr_value = span['attributes'].get(attr_name) + if not (attr_value and isinstance(attr_value, str)): + return + try: + messages = json.loads(attr_value) + except json.JSONDecodeError: return - messages = json.loads(span['attributes']['pydantic_ai.all_messages']) for message in messages: parts = message.get('parts', []) for i, part in enumerate(parts): @@ -114,19 +119,18 @@ def _upload_gen_ai_blobs(span: ReadableSpanDict) -> None: value = base64.b64decode(data) # TODO handle errors # TODO date - key = sha256_bytes(value) + media_type = part.get('media_type', 'application/octet-stream') + key = sha256_string(sha256_bytes(value) + media_type) # todo move to config - from google.cloud import storage + from logfire.experimental.uploaders.gcs import GcsUploader + + uploader = GcsUploader('alexmojaki-test') + uploader.upload(key, value, media_type) - storage_client = storage.Client() - bucket = storage_client.bucket('alexmojaki-test') - blob = bucket.blob(key) - # TODO media type in hash - blob.upload_from_file(BytesIO(value), content_type=part.get('media_type', 'application/octet-stream')) # TODO keep part, remove content, add new key, make frontend work - parts[i] = dict(type='image-url', url=f'https://storage.cloud.google.com/alexmojaki-test/{key}') - span['attributes'] = {**span['attributes'], 'pydantic_ai.all_messages': json.dumps(messages)} + parts[i] = dict(type='image-url', url=uploader.get_attribute_value(key)) + span['attributes'] = {**span['attributes'], attr_name: json.dumps(messages)} def _set_error_level_and_status(span: ReadableSpanDict) -> None: diff --git a/logfire/experimental/uploaders/gcs.py b/logfire/experimental/uploaders/gcs.py new file mode 100644 index 000000000..ca77399bf --- /dev/null +++ b/logfire/experimental/uploaders/gcs.py @@ -0,0 +1,17 @@ +from io import BytesIO + +from google.cloud import storage + + +class GcsUploader: # noqa: D101 TODO + def __init__(self, bucket_name: str): + self.bucket_name = bucket_name + self.storage_client = storage.Client() + self.bucket: storage.Bucket = self.storage_client.bucket(bucket_name) # pyright: ignore [reportUnknownMemberType] + + def upload(self, key: str, value: bytes, media_type: str): # noqa: D102 TODO + blob: storage.Blob = self.bucket.blob(key) # pyright: ignore [reportUnknownMemberType] + blob.upload_from_file(BytesIO(value), content_type=media_type) # pyright: ignore [reportUnknownMemberType] + + def get_attribute_value(self, key: str): # noqa: D102 TODO + return f'https://storage.cloud.google.com/{self.bucket_name}/{key}' diff --git a/pyproject.toml b/pyproject.toml index a61f4f5a0..2a4e3e570 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -182,6 +182,7 @@ dev = [ "openinference-instrumentation-litellm >= 0", "litellm >= 0", "pip >= 0", + "google-cloud-storage>=3.4.1", ] docs = [ "black>=23.12.0", diff --git a/uv.lock b/uv.lock index 51a5777ea..2e4c02047 100644 --- a/uv.lock +++ b/uv.lock @@ -544,7 +544,7 @@ name = "cffi" version = "2.0.0" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "pycparser", marker = "implementation_name != 'PyPy'" }, + { name = "pycparser", marker = "implementation_name != 'PyPy' and platform_python_implementation != 'PyPy'" }, ] sdist = { url = "https://files.pythonhosted.org/packages/eb/56/b1ba7935a17738ae8453301356628e8147c79dbb825bcbc73dc7401f9846/cffi-2.0.0.tar.gz", hash = "sha256:44d1b5909021139fe36001ae048dbdde8214afa20200eda0f64c068cac5d5529", size = 523588, upload-time = "2025-09-08T23:24:04.541Z" } wheels = [ @@ -1617,6 +1617,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f7/ec/67fbef5d497f86283db54c22eec6f6140243aae73265799baaaa19cd17fb/ghp_import-2.1.0-py3-none-any.whl", hash = "sha256:8337dd7b50877f163d4c0289bc1f1c7f127550241988d568c1db512c4324a619", size = 11034, upload-time = "2022-05-02T15:47:14.552Z" }, ] +[[package]] +name = "google-api-core" +version = "2.28.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-auth" }, + { name = "googleapis-common-protos" }, + { name = "proto-plus" }, + { name = "protobuf" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/da/83d7043169ac2c8c7469f0e375610d78ae2160134bf1b80634c482fa079c/google_api_core-2.28.1.tar.gz", hash = "sha256:2b405df02d68e68ce0fbc138559e6036559e685159d148ae5861013dc201baf8", size = 176759, upload-time = "2025-10-28T21:34:51.529Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ed/d4/90197b416cb61cefd316964fd9e7bd8324bcbafabf40eef14a9f20b81974/google_api_core-2.28.1-py3-none-any.whl", hash = "sha256:4021b0f8ceb77a6fb4de6fde4502cecab45062e66ff4f2895169e0b35bc9466c", size = 173706, upload-time = "2025-10-28T21:34:50.151Z" }, +] + [[package]] name = "google-auth" version = "2.41.1" @@ -1631,6 +1647,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/be/a4/7319a2a8add4cc352be9e3efeff5e2aacee917c85ca2fa1647e29089983c/google_auth-2.41.1-py2.py3-none-any.whl", hash = "sha256:754843be95575b9a19c604a848a41be03f7f2afd8c019f716dc1f51ee41c639d", size = 221302, upload-time = "2025-09-30T22:51:24.212Z" }, ] +[[package]] +name = "google-cloud-core" +version = "2.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a6/03/ef0bc99d0e0faf4fdbe67ac445e18cdaa74824fd93cd069e7bb6548cb52d/google_cloud_core-2.5.0.tar.gz", hash = "sha256:7c1b7ef5c92311717bd05301aa1a91ffbc565673d3b0b4163a52d8413a186963", size = 36027, upload-time = "2025-10-29T23:17:39.513Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/89/20/bfa472e327c8edee00f04beecc80baeddd2ab33ee0e86fd7654da49d45e9/google_cloud_core-2.5.0-py3-none-any.whl", hash = "sha256:67d977b41ae6c7211ee830c7912e41003ea8194bff15ae7d72fd6f51e57acabc", size = 29469, upload-time = "2025-10-29T23:17:38.548Z" }, +] + +[[package]] +name = "google-cloud-storage" +version = "3.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-api-core" }, + { name = "google-auth" }, + { name = "google-cloud-core" }, + { name = "google-crc32c" }, + { name = "google-resumable-media" }, + { name = "requests" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bd/ef/7cefdca67a6c8b3af0ec38612f9e78e5a9f6179dd91352772ae1a9849246/google_cloud_storage-3.4.1.tar.gz", hash = "sha256:6f041a297e23a4b485fad8c305a7a6e6831855c208bcbe74d00332a909f82268", size = 17238203, upload-time = "2025-10-08T18:43:39.665Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/6e/b47d83d3a35231c6232566341b0355cce78fd4e6988a7343725408547b2c/google_cloud_storage-3.4.1-py3-none-any.whl", hash = "sha256:972764cc0392aa097be8f49a5354e22eb47c3f62370067fb1571ffff4a1c1189", size = 290142, upload-time = "2025-10-08T18:43:37.524Z" }, +] + +[[package]] +name = "google-crc32c" +version = "1.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/19/ae/87802e6d9f9d69adfaedfcfd599266bf386a54d0be058b532d04c794f76d/google_crc32c-1.7.1.tar.gz", hash = "sha256:2bff2305f98846f3e825dbeec9ee406f89da7962accdb29356e4eadc251bd472", size = 14495, upload-time = "2025-03-26T14:29:13.32Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/eb/69/b1b05cf415df0d86691d6a8b4b7e60ab3a6fb6efb783ee5cd3ed1382bfd3/google_crc32c-1.7.1-cp310-cp310-macosx_12_0_arm64.whl", hash = "sha256:b07d48faf8292b4db7c3d64ab86f950c2e94e93a11fd47271c28ba458e4a0d76", size = 30467, upload-time = "2025-03-26T14:31:11.92Z" }, + { url = "https://files.pythonhosted.org/packages/44/3d/92f8928ecd671bd5b071756596971c79d252d09b835cdca5a44177fa87aa/google_crc32c-1.7.1-cp310-cp310-macosx_12_0_x86_64.whl", hash = "sha256:7cc81b3a2fbd932a4313eb53cc7d9dde424088ca3a0337160f35d91826880c1d", size = 30311, upload-time = "2025-03-26T14:53:14.161Z" }, + { url = "https://files.pythonhosted.org/packages/33/42/c2d15a73df79d45ed6b430b9e801d0bd8e28ac139a9012d7d58af50a385d/google_crc32c-1.7.1-cp310-cp310-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:1c67ca0a1f5b56162951a9dae987988679a7db682d6f97ce0f6381ebf0fbea4c", size = 37889, upload-time = "2025-03-26T14:41:27.83Z" }, + { url = "https://files.pythonhosted.org/packages/57/ea/ac59c86a3c694afd117bb669bde32aaf17d0de4305d01d706495f09cbf19/google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fc5319db92daa516b653600794d5b9f9439a9a121f3e162f94b0e1891c7933cb", size = 33028, upload-time = "2025-03-26T14:41:29.141Z" }, + { url = "https://files.pythonhosted.org/packages/60/44/87e77e8476767a4a93f6cf271157c6d948eacec63688c093580af13b04be/google_crc32c-1.7.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dcdf5a64adb747610140572ed18d011896e3b9ae5195f2514b7ff678c80f1603", size = 38026, upload-time = "2025-03-26T14:41:29.921Z" }, + { url = "https://files.pythonhosted.org/packages/c8/bf/21ac7bb305cd7c1a6de9c52f71db0868e104a5b573a4977cd9d0ff830f82/google_crc32c-1.7.1-cp310-cp310-win_amd64.whl", hash = "sha256:754561c6c66e89d55754106739e22fdaa93fafa8da7221b29c8b8e8270c6ec8a", size = 33476, upload-time = "2025-03-26T14:29:09.086Z" }, + { url = "https://files.pythonhosted.org/packages/f7/94/220139ea87822b6fdfdab4fb9ba81b3fff7ea2c82e2af34adc726085bffc/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_arm64.whl", hash = "sha256:6fbab4b935989e2c3610371963ba1b86afb09537fd0c633049be82afe153ac06", size = 30468, upload-time = "2025-03-26T14:32:52.215Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/789b23bdeeb9d15dc2904660463ad539d0318286d7633fe2760c10ed0c1c/google_crc32c-1.7.1-cp311-cp311-macosx_12_0_x86_64.whl", hash = "sha256:ed66cbe1ed9cbaaad9392b5259b3eba4a9e565420d734e6238813c428c3336c9", size = 30313, upload-time = "2025-03-26T14:57:38.758Z" }, + { url = "https://files.pythonhosted.org/packages/81/b8/976a2b843610c211e7ccb3e248996a61e87dbb2c09b1499847e295080aec/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ee6547b657621b6cbed3562ea7826c3e11cab01cd33b74e1f677690652883e77", size = 33048, upload-time = "2025-03-26T14:41:30.679Z" }, + { url = "https://files.pythonhosted.org/packages/c9/16/a3842c2cf591093b111d4a5e2bfb478ac6692d02f1b386d2a33283a19dc9/google_crc32c-1.7.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d68e17bad8f7dd9a49181a1f5a8f4b251c6dbc8cc96fb79f1d321dfd57d66f53", size = 32669, upload-time = "2025-03-26T14:41:31.432Z" }, + { url = "https://files.pythonhosted.org/packages/04/17/ed9aba495916fcf5fe4ecb2267ceb851fc5f273c4e4625ae453350cfd564/google_crc32c-1.7.1-cp311-cp311-win_amd64.whl", hash = "sha256:6335de12921f06e1f774d0dd1fbea6bf610abe0887a1638f64d694013138be5d", size = 33476, upload-time = "2025-03-26T14:29:10.211Z" }, + { url = "https://files.pythonhosted.org/packages/dd/b7/787e2453cf8639c94b3d06c9d61f512234a82e1d12d13d18584bd3049904/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:2d73a68a653c57281401871dd4aeebbb6af3191dcac751a76ce430df4d403194", size = 30470, upload-time = "2025-03-26T14:34:31.655Z" }, + { url = "https://files.pythonhosted.org/packages/ed/b4/6042c2b0cbac3ec3a69bb4c49b28d2f517b7a0f4a0232603c42c58e22b44/google_crc32c-1.7.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:22beacf83baaf59f9d3ab2bbb4db0fb018da8e5aebdce07ef9f09fce8220285e", size = 30315, upload-time = "2025-03-26T15:01:54.634Z" }, + { url = "https://files.pythonhosted.org/packages/29/ad/01e7a61a5d059bc57b702d9ff6a18b2585ad97f720bd0a0dbe215df1ab0e/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19eafa0e4af11b0a4eb3974483d55d2d77ad1911e6cf6f832e1574f6781fd337", size = 33180, upload-time = "2025-03-26T14:41:32.168Z" }, + { url = "https://files.pythonhosted.org/packages/3b/a5/7279055cf004561894ed3a7bfdf5bf90a53f28fadd01af7cd166e88ddf16/google_crc32c-1.7.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b6d86616faaea68101195c6bdc40c494e4d76f41e07a37ffdef270879c15fb65", size = 32794, upload-time = "2025-03-26T14:41:33.264Z" }, + { url = "https://files.pythonhosted.org/packages/0f/d6/77060dbd140c624e42ae3ece3df53b9d811000729a5c821b9fd671ceaac6/google_crc32c-1.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:b7491bdc0c7564fcf48c0179d2048ab2f7c7ba36b84ccd3a3e1c3f7a72d3bba6", size = 33477, upload-time = "2025-03-26T14:29:10.94Z" }, + { url = "https://files.pythonhosted.org/packages/8b/72/b8d785e9184ba6297a8620c8a37cf6e39b81a8ca01bb0796d7cbb28b3386/google_crc32c-1.7.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:df8b38bdaf1629d62d51be8bdd04888f37c451564c2042d36e5812da9eff3c35", size = 30467, upload-time = "2025-03-26T14:36:06.909Z" }, + { url = "https://files.pythonhosted.org/packages/34/25/5f18076968212067c4e8ea95bf3b69669f9fc698476e5f5eb97d5b37999f/google_crc32c-1.7.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:e42e20a83a29aa2709a0cf271c7f8aefaa23b7ab52e53b322585297bb94d4638", size = 30309, upload-time = "2025-03-26T15:06:15.318Z" }, + { url = "https://files.pythonhosted.org/packages/92/83/9228fe65bf70e93e419f38bdf6c5ca5083fc6d32886ee79b450ceefd1dbd/google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:905a385140bf492ac300026717af339790921f411c0dfd9aa5a9e69a08ed32eb", size = 33133, upload-time = "2025-03-26T14:41:34.388Z" }, + { url = "https://files.pythonhosted.org/packages/c3/ca/1ea2fd13ff9f8955b85e7956872fdb7050c4ace8a2306a6d177edb9cf7fe/google_crc32c-1.7.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6b211ddaf20f7ebeec5c333448582c224a7c90a9d98826fbab82c0ddc11348e6", size = 32773, upload-time = "2025-03-26T14:41:35.19Z" }, + { url = "https://files.pythonhosted.org/packages/89/32/a22a281806e3ef21b72db16f948cad22ec68e4bdd384139291e00ff82fe2/google_crc32c-1.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:0f99eaa09a9a7e642a61e06742856eec8b19fc0037832e03f941fe7cf0c8e4db", size = 33475, upload-time = "2025-03-26T14:29:11.771Z" }, + { url = "https://files.pythonhosted.org/packages/b8/c5/002975aff514e57fc084ba155697a049b3f9b52225ec3bc0f542871dd524/google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:32d1da0d74ec5634a05f53ef7df18fc646666a25efaaca9fc7dcfd4caf1d98c3", size = 33243, upload-time = "2025-03-26T14:41:35.975Z" }, + { url = "https://files.pythonhosted.org/packages/61/cb/c585282a03a0cea70fcaa1bf55d5d702d0f2351094d663ec3be1c6c67c52/google_crc32c-1.7.1-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e10554d4abc5238823112c2ad7e4560f96c7bf3820b202660373d769d9e6e4c9", size = 32870, upload-time = "2025-03-26T14:41:37.08Z" }, + { url = "https://files.pythonhosted.org/packages/e3/89/940d170a9f24e6e711666a7c5596561358243023b4060869d9adae97a762/google_crc32c-1.7.1-cp39-cp39-macosx_12_0_arm64.whl", hash = "sha256:9fc196f0b8d8bd2789352c6a522db03f89e83a0ed6b64315923c396d7a932315", size = 30462, upload-time = "2025-03-26T14:29:25.969Z" }, + { url = "https://files.pythonhosted.org/packages/42/0c/22bebe2517368e914a63e5378aab74e2b6357eb739d94b6bc0e830979a37/google_crc32c-1.7.1-cp39-cp39-macosx_12_0_x86_64.whl", hash = "sha256:bb5e35dcd8552f76eed9461a23de1030920a3c953c1982f324be8f97946e7127", size = 30304, upload-time = "2025-03-26T14:49:16.642Z" }, + { url = "https://files.pythonhosted.org/packages/36/32/2daf4c46f875aaa3a057ecc8569406979cb29fb1e2389e4f2570d8ed6a5c/google_crc32c-1.7.1-cp39-cp39-manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f2226b6a8da04f1d9e61d3e357f2460b9551c5e6950071437e122c958a18ae14", size = 37734, upload-time = "2025-03-26T14:41:37.88Z" }, + { url = "https://files.pythonhosted.org/packages/76/b5/b3e220b68d5d265c4aacd2878301fdb2df72715c45ba49acc19f310d4555/google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f2b3522222746fff0e04a9bd0a23ea003ba3cccc8cf21385c564deb1f223242", size = 32869, upload-time = "2025-03-26T14:41:38.965Z" }, + { url = "https://files.pythonhosted.org/packages/0a/90/2931c3c8d2de1e7cde89945d3ceb2c4258a1f23f0c22c3c1c921c3c026a6/google_crc32c-1.7.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3bda0fcb632d390e3ea8b6b07bf6b4f4a66c9d02dcd6fbf7ba00a197c143f582", size = 37875, upload-time = "2025-03-26T14:41:41.732Z" }, + { url = "https://files.pythonhosted.org/packages/30/9e/0aaed8a209ea6fa4b50f66fed2d977f05c6c799e10bb509f5523a5a5c90c/google_crc32c-1.7.1-cp39-cp39-win_amd64.whl", hash = "sha256:713121af19f1a617054c41f952294764e0c5443d5a5d9034b2cd60f5dd7e0349", size = 33471, upload-time = "2025-03-26T14:29:12.578Z" }, + { url = "https://files.pythonhosted.org/packages/0b/43/31e57ce04530794917dfe25243860ec141de9fadf4aa9783dffe7dac7c39/google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a8e9afc74168b0b2232fb32dd202c93e46b7d5e4bf03e66ba5dc273bb3559589", size = 28242, upload-time = "2025-03-26T14:41:42.858Z" }, + { url = "https://files.pythonhosted.org/packages/eb/f3/8b84cd4e0ad111e63e30eb89453f8dd308e3ad36f42305cf8c202461cdf0/google_crc32c-1.7.1-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fa8136cc14dd27f34a3221c0f16fd42d8a40e4778273e61a3c19aedaa44daf6b", size = 28049, upload-time = "2025-03-26T14:41:44.651Z" }, + { url = "https://files.pythonhosted.org/packages/16/1b/1693372bf423ada422f80fd88260dbfd140754adb15cbc4d7e9a68b1cb8e/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:85fef7fae11494e747c9fd1359a527e5970fc9603c90764843caabd3a16a0a48", size = 28241, upload-time = "2025-03-26T14:41:45.898Z" }, + { url = "https://files.pythonhosted.org/packages/fd/3c/2a19a60a473de48717b4efb19398c3f914795b64a96cf3fbe82588044f78/google_crc32c-1.7.1-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6efb97eb4369d52593ad6f75e7e10d053cf00c48983f7a973105bc70b0ac4d82", size = 28048, upload-time = "2025-03-26T14:41:46.696Z" }, +] + [[package]] name = "google-genai" version = "1.46.0" @@ -1650,6 +1737,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/79/8993ec6cbf56e5c8f88c165380e55de34ec74f7b928bc302ff5c370f9c4e/google_genai-1.46.0-py3-none-any.whl", hash = "sha256:879c4a260d630db0dcedb5cc84a9d7b47acd29e43e9dc63541b511b757ea7296", size = 239445, upload-time = "2025-10-21T22:55:03.072Z" }, ] +[[package]] +name = "google-resumable-media" +version = "2.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "google-crc32c" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/5a/0efdc02665dca14e0837b62c8a1a93132c264bd02054a15abb2218afe0ae/google_resumable_media-2.7.2.tar.gz", hash = "sha256:5280aed4629f2b60b847b0d42f9857fd4935c11af266744df33d8074cae92fe0", size = 2163099, upload-time = "2024-08-07T22:20:38.555Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/35/b8d3baf8c46695858cb9d8835a53baa1eeb9906ddaf2f728a5f5b640fd1e/google_resumable_media-2.7.2-py2.py3-none-any.whl", hash = "sha256:3ce7551e9fe6d99e9a126101d2536612bb73486721951e9562fee0f90c6ababa", size = 81251, upload-time = "2024-08-07T22:20:36.409Z" }, +] + [[package]] name = "googleapis-common-protos" version = "1.71.0" @@ -2593,6 +2692,7 @@ dev = [ { name = "eval-type-backport" }, { name = "fastapi" }, { name = "flask" }, + { name = "google-cloud-storage" }, { name = "google-genai" }, { name = "greenlet" }, { name = "httpx" }, @@ -2736,6 +2836,7 @@ dev = [ { name = "eval-type-backport", specifier = ">=0.2.0" }, { name = "fastapi", specifier = ">=0.115.0" }, { name = "flask", specifier = ">=3.0.3" }, + { name = "google-cloud-storage", specifier = ">=3.4.1" }, { name = "google-genai", specifier = ">=0" }, { name = "greenlet", specifier = ">=3.1.1" }, { name = "httpx", specifier = ">=0.27.2" }, @@ -4687,6 +4788,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, ] +[[package]] +name = "proto-plus" +version = "1.26.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "protobuf" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f4/ac/87285f15f7cce6d4a008f33f1757fb5a13611ea8914eb58c3d0d26243468/proto_plus-1.26.1.tar.gz", hash = "sha256:21a515a4c4c0088a773899e23c7bbade3d18f9c66c73edd4c7ee3816bc96a012", size = 56142, upload-time = "2025-03-10T15:54:38.843Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4e/6d/280c4c2ce28b1593a19ad5239c8b826871fc6ec275c21afc8e1820108039/proto_plus-1.26.1-py3-none-any.whl", hash = "sha256:13285478c2dcf2abb829db158e1047e2f1e8d63a077d94263c2b88b043c75a66", size = 50163, upload-time = "2025-03-10T15:54:37.335Z" }, +] + [[package]] name = "protobuf" version = "6.33.0" @@ -4820,8 +4933,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8c/df/16848771155e7c419c60afeb24950b8aaa3ab09c0a091ec3ccca26a574d0/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c47676e5b485393f069b4d7a811267d3168ce46f988fa602658b8bb901e9e64d", size = 4410873, upload-time = "2025-10-10T11:10:38.951Z" }, { url = "https://files.pythonhosted.org/packages/43/79/5ef5f32621abd5a541b89b04231fe959a9b327c874a1d41156041c75494b/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:a28d8c01a7b27a1e3265b11250ba7557e5f72b5ee9e5f3a2fa8d2949c29bf5d2", size = 4468016, upload-time = "2025-10-10T11:10:43.319Z" }, { url = "https://files.pythonhosted.org/packages/f0/9b/d7542d0f7ad78f57385971f426704776d7b310f5219ed58da5d605b1892e/psycopg2_binary-2.9.11-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5f3f2732cf504a1aa9e9609d02f79bea1067d99edf844ab92c247bbca143303b", size = 4164996, upload-time = "2025-10-10T11:10:46.705Z" }, + { url = "https://files.pythonhosted.org/packages/14/ed/e409388b537fa7414330687936917c522f6a77a13474e4238219fcfd9a84/psycopg2_binary-2.9.11-cp310-cp310-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:865f9945ed1b3950d968ec4690ce68c55019d79e4497366d36e090327ce7db14", size = 3981881, upload-time = "2025-10-30T02:54:57.182Z" }, { url = "https://files.pythonhosted.org/packages/bf/30/50e330e63bb05efc6fa7c1447df3e08954894025ca3dcb396ecc6739bc26/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:91537a8df2bde69b1c1db01d6d944c831ca793952e4f57892600e96cee95f2cd", size = 3650857, upload-time = "2025-10-10T11:10:50.112Z" }, { url = "https://files.pythonhosted.org/packages/f0/e0/4026e4c12bb49dd028756c5b0bc4c572319f2d8f1c9008e0dad8cc9addd7/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:4dca1f356a67ecb68c81a7bc7809f1569ad9e152ce7fd02c2f2036862ca9f66b", size = 3296063, upload-time = "2025-10-10T11:10:54.089Z" }, + { url = "https://files.pythonhosted.org/packages/2c/34/eb172be293c886fef5299fe5c3fcf180a05478be89856067881007934a7c/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_riscv64.whl", hash = "sha256:0da4de5c1ac69d94ed4364b6cbe7190c1a70d325f112ba783d83f8440285f152", size = 3043464, upload-time = "2025-10-30T02:55:02.483Z" }, { url = "https://files.pythonhosted.org/packages/18/1c/532c5d2cb11986372f14b798a95f2eaafe5779334f6a80589a68b5fcf769/psycopg2_binary-2.9.11-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:37d8412565a7267f7d79e29ab66876e55cb5e8e7b3bbf94f8206f6795f8f7e7e", size = 3345378, upload-time = "2025-10-10T11:11:01.039Z" }, { url = "https://files.pythonhosted.org/packages/70/e7/de420e1cf16f838e1fa17b1120e83afff374c7c0130d088dba6286fcf8ea/psycopg2_binary-2.9.11-cp310-cp310-win_amd64.whl", hash = "sha256:c665f01ec8ab273a61c62beeb8cce3014c214429ced8a308ca1fc410ecac3a39", size = 2713904, upload-time = "2025-10-10T11:11:04.81Z" }, { url = "https://files.pythonhosted.org/packages/c7/ae/8d8266f6dd183ab4d48b95b9674034e1b482a3f8619b33a0d86438694577/psycopg2_binary-2.9.11-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:0e8480afd62362d0a6a27dd09e4ca2def6fa50ed3a4e7c09165266106b2ffa10", size = 3756452, upload-time = "2025-10-10T11:11:11.583Z" }, @@ -4829,8 +4944,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/48/89/3fdb5902bdab8868bbedc1c6e6023a4e08112ceac5db97fc2012060e0c9a/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2e164359396576a3cc701ba8af4751ae68a07235d7a380c631184a611220d9a4", size = 4410955, upload-time = "2025-10-10T11:11:21.21Z" }, { url = "https://files.pythonhosted.org/packages/ce/24/e18339c407a13c72b336e0d9013fbbbde77b6fd13e853979019a1269519c/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:d57c9c387660b8893093459738b6abddbb30a7eab058b77b0d0d1c7d521ddfd7", size = 4468007, upload-time = "2025-10-10T11:11:24.831Z" }, { url = "https://files.pythonhosted.org/packages/91/7e/b8441e831a0f16c159b5381698f9f7f7ed54b77d57bc9c5f99144cc78232/psycopg2_binary-2.9.11-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2c226ef95eb2250974bf6fa7a842082b31f68385c4f3268370e3f3870e7859ee", size = 4165012, upload-time = "2025-10-10T11:11:29.51Z" }, + { url = "https://files.pythonhosted.org/packages/0d/61/4aa89eeb6d751f05178a13da95516c036e27468c5d4d2509bb1e15341c81/psycopg2_binary-2.9.11-cp311-cp311-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a311f1edc9967723d3511ea7d2708e2c3592e3405677bf53d5c7246753591fbb", size = 3981881, upload-time = "2025-10-30T02:55:07.332Z" }, { url = "https://files.pythonhosted.org/packages/76/a1/2f5841cae4c635a9459fe7aca8ed771336e9383b6429e05c01267b0774cf/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:ebb415404821b6d1c47353ebe9c8645967a5235e6d88f914147e7fd411419e6f", size = 3650985, upload-time = "2025-10-10T11:11:34.975Z" }, { url = "https://files.pythonhosted.org/packages/84/74/4defcac9d002bca5709951b975173c8c2fa968e1a95dc713f61b3a8d3b6a/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:f07c9c4a5093258a03b28fab9b4f151aa376989e7f35f855088234e656ee6a94", size = 3296039, upload-time = "2025-10-10T11:11:40.432Z" }, + { url = "https://files.pythonhosted.org/packages/6d/c2/782a3c64403d8ce35b5c50e1b684412cf94f171dc18111be8c976abd2de1/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_riscv64.whl", hash = "sha256:00ce1830d971f43b667abe4a56e42c1e2d594b32da4802e44a73bacacb25535f", size = 3043477, upload-time = "2025-10-30T02:55:11.182Z" }, { url = "https://files.pythonhosted.org/packages/c8/31/36a1d8e702aa35c38fc117c2b8be3f182613faa25d794b8aeaab948d4c03/psycopg2_binary-2.9.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:cffe9d7697ae7456649617e8bb8d7a45afb71cd13f7ab22af3e5c61f04840908", size = 3345842, upload-time = "2025-10-10T11:11:45.366Z" }, { url = "https://files.pythonhosted.org/packages/6e/b4/a5375cda5b54cb95ee9b836930fea30ae5a8f14aa97da7821722323d979b/psycopg2_binary-2.9.11-cp311-cp311-win_amd64.whl", hash = "sha256:304fd7b7f97eef30e91b8f7e720b3db75fee010b520e434ea35ed1ff22501d03", size = 2713894, upload-time = "2025-10-10T11:11:48.775Z" }, { url = "https://files.pythonhosted.org/packages/d8/91/f870a02f51be4a65987b45a7de4c2e1897dd0d01051e2b559a38fa634e3e/psycopg2_binary-2.9.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:be9b840ac0525a283a96b556616f5b4820e0526addb8dcf6525a0fa162730be4", size = 3756603, upload-time = "2025-10-10T11:11:52.213Z" }, @@ -4838,8 +4955,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2d/75/364847b879eb630b3ac8293798e380e441a957c53657995053c5ec39a316/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ab8905b5dcb05bf3fb22e0cf90e10f469563486ffb6a96569e51f897c750a76a", size = 4411159, upload-time = "2025-10-10T11:12:00.49Z" }, { url = "https://files.pythonhosted.org/packages/6f/a0/567f7ea38b6e1c62aafd58375665a547c00c608a471620c0edc364733e13/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:bf940cd7e7fec19181fdbc29d76911741153d51cab52e5c21165f3262125685e", size = 4468234, upload-time = "2025-10-10T11:12:04.892Z" }, { url = "https://files.pythonhosted.org/packages/30/da/4e42788fb811bbbfd7b7f045570c062f49e350e1d1f3df056c3fb5763353/psycopg2_binary-2.9.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fa0f693d3c68ae925966f0b14b8edda71696608039f4ed61b1fe9ffa468d16db", size = 4166236, upload-time = "2025-10-10T11:12:11.674Z" }, + { url = "https://files.pythonhosted.org/packages/3c/94/c1777c355bc560992af848d98216148be5f1be001af06e06fc49cbded578/psycopg2_binary-2.9.11-cp312-cp312-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:a1cf393f1cdaf6a9b57c0a719a1068ba1069f022a59b8b1fe44b006745b59757", size = 3983083, upload-time = "2025-10-30T02:55:15.73Z" }, { url = "https://files.pythonhosted.org/packages/bd/42/c9a21edf0e3daa7825ed04a4a8588686c6c14904344344a039556d78aa58/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:ef7a6beb4beaa62f88592ccc65df20328029d721db309cb3250b0aae0fa146c3", size = 3652281, upload-time = "2025-10-10T11:12:17.713Z" }, { url = "https://files.pythonhosted.org/packages/12/22/dedfbcfa97917982301496b6b5e5e6c5531d1f35dd2b488b08d1ebc52482/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:31b32c457a6025e74d233957cc9736742ac5a6cb196c6b68499f6bb51390bd6a", size = 3298010, upload-time = "2025-10-10T11:12:22.671Z" }, + { url = "https://files.pythonhosted.org/packages/66/ea/d3390e6696276078bd01b2ece417deac954dfdd552d2edc3d03204416c0c/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:edcb3aeb11cb4bf13a2af3c53a15b3d612edeb6409047ea0b5d6a21a9d744b34", size = 3044641, upload-time = "2025-10-30T02:55:19.929Z" }, { url = "https://files.pythonhosted.org/packages/12/9a/0402ded6cbd321da0c0ba7d34dc12b29b14f5764c2fc10750daa38e825fc/psycopg2_binary-2.9.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:62b6d93d7c0b61a1dd6197d208ab613eb7dcfdcca0a49c42ceb082257991de9d", size = 3347940, upload-time = "2025-10-10T11:12:26.529Z" }, { url = "https://files.pythonhosted.org/packages/b1/d2/99b55e85832ccde77b211738ff3925a5d73ad183c0b37bcbbe5a8ff04978/psycopg2_binary-2.9.11-cp312-cp312-win_amd64.whl", hash = "sha256:b33fabeb1fde21180479b2d4667e994de7bbf0eec22832ba5d9b5e4cf65b6c6d", size = 2714147, upload-time = "2025-10-10T11:12:29.535Z" }, { url = "https://files.pythonhosted.org/packages/ff/a8/a2709681b3ac11b0b1786def10006b8995125ba268c9a54bea6f5ae8bd3e/psycopg2_binary-2.9.11-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b8fb3db325435d34235b044b199e56cdf9ff41223a4b9752e8576465170bb38c", size = 3756572, upload-time = "2025-10-10T11:12:32.873Z" }, @@ -4847,8 +4966,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/11/32/b2ffe8f3853c181e88f0a157c5fb4e383102238d73c52ac6d93a5c8bffe6/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8c55b385daa2f92cb64b12ec4536c66954ac53654c7f15a203578da4e78105c0", size = 4411242, upload-time = "2025-10-10T11:12:42.388Z" }, { url = "https://files.pythonhosted.org/packages/10/04/6ca7477e6160ae258dc96f67c371157776564679aefd247b66f4661501a2/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:c0377174bf1dd416993d16edc15357f6eb17ac998244cca19bc67cdc0e2e5766", size = 4468258, upload-time = "2025-10-10T11:12:48.654Z" }, { url = "https://files.pythonhosted.org/packages/3c/7e/6a1a38f86412df101435809f225d57c1a021307dd0689f7a5e7fe83588b1/psycopg2_binary-2.9.11-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:5c6ff3335ce08c75afaed19e08699e8aacf95d4a260b495a4a8545244fe2ceb3", size = 4166295, upload-time = "2025-10-10T11:12:52.525Z" }, + { url = "https://files.pythonhosted.org/packages/f2/7d/c07374c501b45f3579a9eb761cbf2604ddef3d96ad48679112c2c5aa9c25/psycopg2_binary-2.9.11-cp313-cp313-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84011ba3109e06ac412f95399b704d3d6950e386b7994475b231cf61eec2fc1f", size = 3983133, upload-time = "2025-10-30T02:55:24.329Z" }, { url = "https://files.pythonhosted.org/packages/82/56/993b7104cb8345ad7d4516538ccf8f0d0ac640b1ebd8c754a7b024e76878/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ba34475ceb08cccbdd98f6b46916917ae6eeb92b5ae111df10b544c3a4621dc4", size = 3652383, upload-time = "2025-10-10T11:12:56.387Z" }, { url = "https://files.pythonhosted.org/packages/2d/ac/eaeb6029362fd8d454a27374d84c6866c82c33bfc24587b4face5a8e43ef/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b31e90fdd0f968c2de3b26ab014314fe814225b6c324f770952f7d38abf17e3c", size = 3298168, upload-time = "2025-10-10T11:13:00.403Z" }, + { url = "https://files.pythonhosted.org/packages/2b/39/50c3facc66bded9ada5cbc0de867499a703dc6bca6be03070b4e3b65da6c/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:d526864e0f67f74937a8fce859bd56c979f5e2ec57ca7c627f5f1071ef7fee60", size = 3044712, upload-time = "2025-10-30T02:55:27.975Z" }, { url = "https://files.pythonhosted.org/packages/9c/8e/b7de019a1f562f72ada81081a12823d3c1590bedc48d7d2559410a2763fe/psycopg2_binary-2.9.11-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:04195548662fa544626c8ea0f06561eb6203f1984ba5b4562764fbeb4c3d14b1", size = 3347549, upload-time = "2025-10-10T11:13:03.971Z" }, { url = "https://files.pythonhosted.org/packages/80/2d/1bb683f64737bbb1f86c82b7359db1eb2be4e2c0c13b947f80efefa7d3e5/psycopg2_binary-2.9.11-cp313-cp313-win_amd64.whl", hash = "sha256:efff12b432179443f54e230fdf60de1f6cc726b6c832db8701227d089310e8aa", size = 2714215, upload-time = "2025-10-10T11:13:07.14Z" }, { url = "https://files.pythonhosted.org/packages/64/12/93ef0098590cf51d9732b4f139533732565704f45bdc1ffa741b7c95fb54/psycopg2_binary-2.9.11-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:92e3b669236327083a2e33ccfa0d320dd01b9803b3e14dd986a4fc54aa00f4e1", size = 3756567, upload-time = "2025-10-10T11:13:11.885Z" }, @@ -4856,8 +4977,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/13/1e/98874ce72fd29cbde93209977b196a2edae03f8490d1bd8158e7f1daf3a0/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9b52a3f9bb540a3e4ec0f6ba6d31339727b2950c9772850d6545b7eae0b9d7c5", size = 4411646, upload-time = "2025-10-10T11:13:24.432Z" }, { url = "https://files.pythonhosted.org/packages/5a/bd/a335ce6645334fb8d758cc358810defca14a1d19ffbc8a10bd38a2328565/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:db4fd476874ccfdbb630a54426964959e58da4c61c9feba73e6094d51303d7d8", size = 4468701, upload-time = "2025-10-10T11:13:29.266Z" }, { url = "https://files.pythonhosted.org/packages/44/d6/c8b4f53f34e295e45709b7568bf9b9407a612ea30387d35eb9fa84f269b4/psycopg2_binary-2.9.11-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:47f212c1d3be608a12937cc131bd85502954398aaa1320cb4c14421a0ffccf4c", size = 4166293, upload-time = "2025-10-10T11:13:33.336Z" }, + { url = "https://files.pythonhosted.org/packages/4b/e0/f8cc36eadd1b716ab36bb290618a3292e009867e5c97ce4aba908cb99644/psycopg2_binary-2.9.11-cp314-cp314-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e35b7abae2b0adab776add56111df1735ccc71406e56203515e228a8dc07089f", size = 3983184, upload-time = "2025-10-30T02:55:32.483Z" }, { url = "https://files.pythonhosted.org/packages/53/3e/2a8fe18a4e61cfb3417da67b6318e12691772c0696d79434184a511906dc/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fcf21be3ce5f5659daefd2b3b3b6e4727b028221ddc94e6c1523425579664747", size = 3652650, upload-time = "2025-10-10T11:13:38.181Z" }, { url = "https://files.pythonhosted.org/packages/76/36/03801461b31b29fe58d228c24388f999fe814dfc302856e0d17f97d7c54d/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:9bd81e64e8de111237737b29d68039b9c813bdf520156af36d26819c9a979e5f", size = 3298663, upload-time = "2025-10-10T11:13:44.878Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/21b0ea2e1a73aa5fa9222b2a6b8ba325c43c3a8d54272839c991f2345656/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:32770a4d666fbdafab017086655bcddab791d7cb260a16679cc5a7338b64343b", size = 3044737, upload-time = "2025-10-30T02:55:35.69Z" }, { url = "https://files.pythonhosted.org/packages/67/69/f36abe5f118c1dca6d3726ceae164b9356985805480731ac6712a63f24f0/psycopg2_binary-2.9.11-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c3cb3a676873d7506825221045bd70e0427c905b9c8ee8d6acd70cfcbd6e576d", size = 3347643, upload-time = "2025-10-10T11:13:53.499Z" }, { url = "https://files.pythonhosted.org/packages/e1/36/9c0c326fe3a4227953dfb29f5d0c8ae3b8eb8c1cd2967aa569f50cb3c61f/psycopg2_binary-2.9.11-cp314-cp314-win_amd64.whl", hash = "sha256:4012c9c954dfaccd28f94e84ab9f94e12df76b4afb22331b1f0d3154893a6316", size = 2803913, upload-time = "2025-10-10T11:13:57.058Z" }, { url = "https://files.pythonhosted.org/packages/b2/41/cb36a61146b3afed03e980477f6dd29c0263f15e4b4844660501a774dc0b/psycopg2_binary-2.9.11-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:20e7fb94e20b03dcc783f76c0865f9da39559dcc0c28dd1a3fce0d01902a6b9c", size = 3756418, upload-time = "2025-10-10T11:14:00.728Z" }, @@ -4865,8 +4988,10 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bc/86/ec3682dc3550c65eff80384f603a6a55b798e1b86ccef262d454d19f96eb/psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9d3a9edcfbe77a3ed4bc72836d466dfce4174beb79eda79ea155cc77237ed9e8", size = 4410882, upload-time = "2025-10-10T11:14:09.552Z" }, { url = "https://files.pythonhosted.org/packages/41/af/540ee7d56fb33408c57240d55904c95e4a30952c096f5e1542769cadc787/psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:44fc5c2b8fa871ce7f0023f619f1349a0aa03a0857f2c96fbc01c657dcbbdb49", size = 4468062, upload-time = "2025-10-10T11:14:15.225Z" }, { url = "https://files.pythonhosted.org/packages/b4/d5/b95d47b2e67b2adfaba517c803a99a1ac41e84c8201d0f3b29d77b56e357/psycopg2_binary-2.9.11-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:9c55460033867b4622cda1b6872edf445809535144152e5d14941ef591980edf", size = 4165036, upload-time = "2025-10-10T11:14:21.209Z" }, + { url = "https://files.pythonhosted.org/packages/af/f9/99e39882b70d9b0cfdcbad33bea2e5823843c3a7839c1aaf89fc1337c05c/psycopg2_binary-2.9.11-cp39-cp39-manylinux_2_38_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2d11098a83cca92deaeaed3d58cfd150d49b3b06ee0d0852be466bf87596899e", size = 3981901, upload-time = "2025-10-30T02:55:39.325Z" }, { url = "https://files.pythonhosted.org/packages/de/c3/8d2c97f1dfddedf5a06c6ad2eda83fba48555a7bc525c3150aedc6f2bedc/psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:691c807d94aecfbc76a14e1408847d59ff5b5906a04a23e12a89007672b9e819", size = 3650995, upload-time = "2025-10-10T11:14:27.733Z" }, { url = "https://files.pythonhosted.org/packages/9c/89/afdf59b44b84ebb28111652485fab608429389f4051d22bc5a7bb43d5208/psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:8b81627b691f29c4c30a8f322546ad039c40c328373b11dff7490a3e1b517855", size = 3296106, upload-time = "2025-10-10T11:14:33.312Z" }, + { url = "https://files.pythonhosted.org/packages/94/21/851c9ecf0e9a699907d1c455dbbde7ef9b11dba28e7b7b132c7bb28391f2/psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_riscv64.whl", hash = "sha256:b637d6d941209e8d96a072d7977238eea128046effbf37d1d8b2c0764750017d", size = 3043491, upload-time = "2025-10-30T02:55:42.228Z" }, { url = "https://files.pythonhosted.org/packages/9c/de/50f6eced439e7a131b268276c4b68cf8800fd55d8cef7b37109c44bf957a/psycopg2_binary-2.9.11-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:41360b01c140c2a03d346cec3280cf8a71aa07d94f3b1509fa0161c366af66b4", size = 3345816, upload-time = "2025-10-10T11:14:38.648Z" }, { url = "https://files.pythonhosted.org/packages/45/3b/e0506f199dc8a90ff3b462f261f45d15c0703bb8c59f0da1add5f0c11a30/psycopg2_binary-2.9.11-cp39-cp39-win_amd64.whl", hash = "sha256:875039274f8a2361e5207857899706da840768e2a775bf8c65e82f60b197df02", size = 2714968, upload-time = "2025-10-10T11:14:43.24Z" }, ] From 3cf850d2f1aa475536ad526290963f955f7f3aeb Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 17:49:25 +0200 Subject: [PATCH 03/15] BaseUploader --- .../_internal/exporters/processor_wrapper.py | 8 +++--- logfire/experimental/uploaders/__init__.py | 26 +++++++++++++++++++ logfire/experimental/uploaders/gcs.py | 16 ++++++++---- 3 files changed, 42 insertions(+), 8 deletions(-) create mode 100644 logfire/experimental/uploaders/__init__.py diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index e28a9fe10..68b009cd7 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -14,6 +14,7 @@ import logfire +from ...experimental.uploaders import UploadItem from ..constants import ( ATTRIBUTES_JSON_SCHEMA_KEY, ATTRIBUTES_LOG_LEVEL_NUM_KEY, @@ -119,14 +120,15 @@ def _upload_gen_ai_blobs(span: ReadableSpanDict) -> None: value = base64.b64decode(data) # TODO handle errors # TODO date - media_type = part.get('media_type', 'application/octet-stream') - key = sha256_string(sha256_bytes(value) + media_type) + media_type = part.get('media_type') + key = sha256_string(sha256_bytes(value) + str(media_type)) + upload_item = UploadItem(key=key, value=value, media_type=media_type) # todo move to config from logfire.experimental.uploaders.gcs import GcsUploader uploader = GcsUploader('alexmojaki-test') - uploader.upload(key, value, media_type) + uploader.upload(upload_item) # TODO keep part, remove content, add new key, make frontend work parts[i] = dict(type='image-url', url=uploader.get_attribute_value(key)) diff --git a/logfire/experimental/uploaders/__init__.py b/logfire/experimental/uploaders/__init__.py new file mode 100644 index 000000000..df83c898b --- /dev/null +++ b/logfire/experimental/uploaders/__init__.py @@ -0,0 +1,26 @@ +from __future__ import annotations + +from abc import ABC, abstractmethod +from dataclasses import dataclass +from typing import Any + + +@dataclass +class UploadItem: + """An item to upload.""" + + key: str + value: bytes + media_type: str | None = None + + +class BaseUploader(ABC): + """Abstract base class for uploaders.""" + + @abstractmethod + def upload(self, item: UploadItem) -> None: + """Upload the given item.""" + + @abstractmethod + def get_attribute_value(self, key: str) -> Any: + """Return a reference to the uploaded item, e.g. a URL or path.""" diff --git a/logfire/experimental/uploaders/gcs.py b/logfire/experimental/uploaders/gcs.py index ca77399bf..31213be4d 100644 --- a/logfire/experimental/uploaders/gcs.py +++ b/logfire/experimental/uploaders/gcs.py @@ -2,16 +2,22 @@ from google.cloud import storage +from logfire.experimental.uploaders import BaseUploader, UploadItem + + +class GcsUploader(BaseUploader): + """Google Cloud Storage uploader.""" -class GcsUploader: # noqa: D101 TODO def __init__(self, bucket_name: str): self.bucket_name = bucket_name self.storage_client = storage.Client() self.bucket: storage.Bucket = self.storage_client.bucket(bucket_name) # pyright: ignore [reportUnknownMemberType] - def upload(self, key: str, value: bytes, media_type: str): # noqa: D102 TODO - blob: storage.Blob = self.bucket.blob(key) # pyright: ignore [reportUnknownMemberType] - blob.upload_from_file(BytesIO(value), content_type=media_type) # pyright: ignore [reportUnknownMemberType] + def upload(self, item: UploadItem): + """Upload the given item to GCS.""" + blob: storage.Blob = self.bucket.blob(item.key) # pyright: ignore [reportUnknownMemberType] + blob.upload_from_file(BytesIO(item.value), content_type=item.media_type) # pyright: ignore [reportUnknownMemberType] - def get_attribute_value(self, key: str): # noqa: D102 TODO + def get_attribute_value(self, key: str): + """Return the GCS authenticated URL for the uploaded item.""" return f'https://storage.cloud.google.com/{self.bucket_name}/{key}' From 912d5e9fade1b80a7d94b31ad6123cda61c58b96 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 18:00:52 +0200 Subject: [PATCH 04/15] UploadItem.create --- logfire/_internal/exporters/processor_wrapper.py | 7 ++----- logfire/experimental/uploaders/__init__.py | 16 +++++++++++++++- logfire/experimental/uploaders/gcs.py | 4 ++-- 3 files changed, 19 insertions(+), 8 deletions(-) diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index 68b009cd7..82cc8ca27 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -32,8 +32,6 @@ handle_internal_errors, is_asgi_send_receive_span_name, is_instrumentation_suppressed, - sha256_bytes, - sha256_string, span_to_dict, truncate_string, ) @@ -121,8 +119,7 @@ def _upload_gen_ai_blobs(span: ReadableSpanDict) -> None: value = base64.b64decode(data) # TODO handle errors # TODO date media_type = part.get('media_type') - key = sha256_string(sha256_bytes(value) + str(media_type)) - upload_item = UploadItem(key=key, value=value, media_type=media_type) + upload_item = UploadItem.create(value, media_type) # todo move to config from logfire.experimental.uploaders.gcs import GcsUploader @@ -131,7 +128,7 @@ def _upload_gen_ai_blobs(span: ReadableSpanDict) -> None: uploader.upload(upload_item) # TODO keep part, remove content, add new key, make frontend work - parts[i] = dict(type='image-url', url=uploader.get_attribute_value(key)) + parts[i] = dict(type='image-url', url=uploader.get_attribute_value(upload_item)) span['attributes'] = {**span['attributes'], attr_name: json.dumps(messages)} diff --git a/logfire/experimental/uploaders/__init__.py b/logfire/experimental/uploaders/__init__.py index df83c898b..9b1326b6e 100644 --- a/logfire/experimental/uploaders/__init__.py +++ b/logfire/experimental/uploaders/__init__.py @@ -4,6 +4,8 @@ from dataclasses import dataclass from typing import Any +from logfire._internal.utils import sha256_bytes + @dataclass class UploadItem: @@ -13,6 +15,18 @@ class UploadItem: value: bytes media_type: str | None = None + @classmethod + def create(cls, value: bytes, media_type: str | None = None) -> UploadItem: + """Create an UploadItem with a generated key. + + Use this instead of constructing directly. + """ + parts = [sha256_bytes(value)] + if media_type: + parts.insert(0, media_type) + key = '/'.join(parts) + return cls(key=key, value=value, media_type=media_type) + class BaseUploader(ABC): """Abstract base class for uploaders.""" @@ -22,5 +36,5 @@ def upload(self, item: UploadItem) -> None: """Upload the given item.""" @abstractmethod - def get_attribute_value(self, key: str) -> Any: + def get_attribute_value(self, item: UploadItem) -> Any: """Return a reference to the uploaded item, e.g. a URL or path.""" diff --git a/logfire/experimental/uploaders/gcs.py b/logfire/experimental/uploaders/gcs.py index 31213be4d..2482d7b58 100644 --- a/logfire/experimental/uploaders/gcs.py +++ b/logfire/experimental/uploaders/gcs.py @@ -18,6 +18,6 @@ def upload(self, item: UploadItem): blob: storage.Blob = self.bucket.blob(item.key) # pyright: ignore [reportUnknownMemberType] blob.upload_from_file(BytesIO(item.value), content_type=item.media_type) # pyright: ignore [reportUnknownMemberType] - def get_attribute_value(self, key: str): + def get_attribute_value(self, item: UploadItem): """Return the GCS authenticated URL for the uploaded item.""" - return f'https://storage.cloud.google.com/{self.bucket_name}/{key}' + return f'https://storage.cloud.google.com/{self.bucket_name}/{item.key}' From 6e0bd6d6a9167d0dc58e7567f6dbc17e594c34ce Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 18:07:41 +0200 Subject: [PATCH 05/15] timestamp, base64 error --- logfire/_internal/exporters/processor_wrapper.py | 10 +++++++--- logfire/experimental/uploaders/__init__.py | 15 ++++++++++++--- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index 82cc8ca27..082e8dcf0 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -1,6 +1,7 @@ from __future__ import annotations import base64 +import binascii import json from contextlib import suppress from dataclasses import dataclass @@ -116,10 +117,13 @@ def _upload_gen_ai_blobs(span: ReadableSpanDict) -> None: if not isinstance(data, str): continue - value = base64.b64decode(data) # TODO handle errors - # TODO date + try: + value = base64.b64decode(data, validate=True) + except binascii.Error: + value = data.encode() + media_type = part.get('media_type') - upload_item = UploadItem.create(value, media_type) + upload_item = UploadItem.create(value, timestamp=span['start_time'], media_type=media_type) # todo move to config from logfire.experimental.uploaders.gcs import GcsUploader diff --git a/logfire/experimental/uploaders/__init__.py b/logfire/experimental/uploaders/__init__.py index 9b1326b6e..6a2153f5c 100644 --- a/logfire/experimental/uploaders/__init__.py +++ b/logfire/experimental/uploaders/__init__.py @@ -1,5 +1,6 @@ from __future__ import annotations +import datetime from abc import ABC, abstractmethod from dataclasses import dataclass from typing import Any @@ -16,15 +17,23 @@ class UploadItem: media_type: str | None = None @classmethod - def create(cls, value: bytes, media_type: str | None = None) -> UploadItem: + def create(cls, value: bytes, *, timestamp: int | None, media_type: str | None = None) -> UploadItem: """Create an UploadItem with a generated key. Use this instead of constructing directly. """ parts = [sha256_bytes(value)] + if media_type: - parts.insert(0, media_type) - key = '/'.join(parts) + parts.append(media_type) + + if timestamp is None: + date = datetime.date.today() + else: + date = datetime.datetime.fromtimestamp(timestamp).date() + parts.append(date.isoformat()) + + key = '/'.join(parts[::-1]) return cls(key=key, value=value, media_type=media_type) From 2076ea516730a32f9cff8983480d49e88f6889ea Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 18:10:30 +0200 Subject: [PATCH 06/15] JsonValue --- logfire/experimental/uploaders/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/logfire/experimental/uploaders/__init__.py b/logfire/experimental/uploaders/__init__.py index 6a2153f5c..cb3f713eb 100644 --- a/logfire/experimental/uploaders/__init__.py +++ b/logfire/experimental/uploaders/__init__.py @@ -3,9 +3,8 @@ import datetime from abc import ABC, abstractmethod from dataclasses import dataclass -from typing import Any -from logfire._internal.utils import sha256_bytes +from logfire._internal.utils import JsonValue, sha256_bytes @dataclass @@ -45,5 +44,5 @@ def upload(self, item: UploadItem) -> None: """Upload the given item.""" @abstractmethod - def get_attribute_value(self, item: UploadItem) -> Any: + def get_attribute_value(self, item: UploadItem) -> JsonValue: """Return a reference to the uploaded item, e.g. a URL or path.""" From 3650b242845681d3ebaad5990251f08b5dbdc01d Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 18:15:07 +0200 Subject: [PATCH 07/15] other attrs --- .../_internal/exporters/processor_wrapper.py | 63 +++++++++---------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index 082e8dcf0..2dd6df26e 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -96,44 +96,41 @@ def on_end(self, span: ReadableSpan) -> None: def _upload_gen_ai_blobs(span: ReadableSpanDict) -> None: - # TODO: - # other attributes - # error handling - attr_name = 'pydantic_ai.all_messages' - attr_value = span['attributes'].get(attr_name) - if not (attr_value and isinstance(attr_value, str)): - return - try: - messages = json.loads(attr_value) - except json.JSONDecodeError: - return - for message in messages: - parts = message.get('parts', []) - for i, part in enumerate(parts): - # TODO otel semantic type - if part.get('type') != 'binary' or 'content' not in part: - continue - data = part['content'] - if not isinstance(data, str): - continue + for attr_name in ['pydantic_ai.all_messages', 'gen_ai.input.messages', 'gen_ai.output.messages']: + attr_value = span['attributes'].get(attr_name) + if not (attr_value and isinstance(attr_value, str)): + continue + try: + messages = json.loads(attr_value) + except json.JSONDecodeError: + continue + for message in messages: + parts = message.get('parts', []) + for i, part in enumerate(parts): + # TODO otel semantic type + if part.get('type') != 'binary' or 'content' not in part: + continue + data = part['content'] + if not isinstance(data, str): + continue - try: - value = base64.b64decode(data, validate=True) - except binascii.Error: - value = data.encode() + try: + value = base64.b64decode(data, validate=True) + except binascii.Error: + value = data.encode() - media_type = part.get('media_type') - upload_item = UploadItem.create(value, timestamp=span['start_time'], media_type=media_type) + media_type = part.get('media_type') + upload_item = UploadItem.create(value, timestamp=span['start_time'], media_type=media_type) - # todo move to config - from logfire.experimental.uploaders.gcs import GcsUploader + # todo move to config + from logfire.experimental.uploaders.gcs import GcsUploader - uploader = GcsUploader('alexmojaki-test') - uploader.upload(upload_item) + uploader = GcsUploader('alexmojaki-test') + uploader.upload(upload_item) - # TODO keep part, remove content, add new key, make frontend work - parts[i] = dict(type='image-url', url=uploader.get_attribute_value(upload_item)) - span['attributes'] = {**span['attributes'], attr_name: json.dumps(messages)} + # TODO keep part, remove content, add new key, make frontend work + parts[i] = dict(type='image-url', url=uploader.get_attribute_value(upload_item)) + span['attributes'] = {**span['attributes'], attr_name: json.dumps(messages)} def _set_error_level_and_status(span: ReadableSpanDict) -> None: From 65abd409cee8f56d285b9ae9e189982cb4019997 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 18:17:47 +0200 Subject: [PATCH 08/15] ONE_SECOND_IN_NANOSECONDS --- logfire/experimental/uploaders/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/logfire/experimental/uploaders/__init__.py b/logfire/experimental/uploaders/__init__.py index cb3f713eb..93ffc371c 100644 --- a/logfire/experimental/uploaders/__init__.py +++ b/logfire/experimental/uploaders/__init__.py @@ -4,6 +4,7 @@ from abc import ABC, abstractmethod from dataclasses import dataclass +from logfire._internal.constants import ONE_SECOND_IN_NANOSECONDS from logfire._internal.utils import JsonValue, sha256_bytes @@ -29,7 +30,7 @@ def create(cls, value: bytes, *, timestamp: int | None, media_type: str | None = if timestamp is None: date = datetime.date.today() else: - date = datetime.datetime.fromtimestamp(timestamp).date() + date = datetime.datetime.fromtimestamp(timestamp / ONE_SECOND_IN_NANOSECONDS).date() parts.append(date.isoformat()) key = '/'.join(parts[::-1]) From d72ce152ef0bf02ae68302babcc882857204dbe8 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 18:24:28 +0200 Subject: [PATCH 09/15] config --- logfire/_internal/config.py | 9 ++- .../_internal/exporters/processor_wrapper.py | 75 +++++++++---------- 2 files changed, 44 insertions(+), 40 deletions(-) diff --git a/logfire/_internal/config.py b/logfire/_internal/config.py index cda6b17c7..5f10b1315 100644 --- a/logfire/_internal/config.py +++ b/logfire/_internal/config.py @@ -65,6 +65,7 @@ from logfire.sampling._tail_sampling import TailSamplingProcessor from logfire.version import VERSION +from ..experimental.uploaders import BaseUploader from ..propagate import NoExtractTraceContextPropagator, WarnOnExtractTraceContextPropagator from ..types import ExceptionCallback from .client import InvalidProjectName, LogfireClient, ProjectAlreadyExists @@ -192,6 +193,9 @@ class AdvancedOptions: This is experimental and may be modified or removed.""" + uploader: BaseUploader | None = None + """Very experimental blob storage uploader.""" + def generate_base_url(self, token: str) -> str: if self.base_url is not None: return self.base_url @@ -862,7 +866,7 @@ def _initialize(self) -> None: root_processor = TailSamplingProcessor(root_processor, self.sampling.tail) tracer_provider.add_span_processor( CheckSuppressInstrumentationProcessorWrapper( - MainSpanProcessorWrapper(root_processor, self.scrubber), + MainSpanProcessorWrapper(root_processor, self.scrubber, self.advanced.uploader), ) ) @@ -1030,7 +1034,8 @@ def check_token(): main_multiprocessor.add_span_processor( PendingSpanProcessor( - self.advanced.id_generator, MainSpanProcessorWrapper(pending_multiprocessor, self.scrubber) + self.advanced.id_generator, + MainSpanProcessorWrapper(pending_multiprocessor, self.scrubber, self.advanced.uploader), ) ) diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index 2dd6df26e..e22ddf96a 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -15,7 +15,7 @@ import logfire -from ...experimental.uploaders import UploadItem +from ...experimental.uploaders import BaseUploader, UploadItem from ..constants import ( ATTRIBUTES_JSON_SCHEMA_KEY, ATTRIBUTES_LOG_LEVEL_NUM_KEY, @@ -67,6 +67,7 @@ class MainSpanProcessorWrapper(WrapperSpanProcessor): """ scrubber: BaseScrubber + uploader: BaseUploader | None def on_start( self, @@ -89,48 +90,46 @@ def on_end(self, span: ReadableSpan) -> None: _transform_google_genai_span(span_dict) _transform_litellm_span(span_dict) _default_gen_ai_response_model(span_dict) - _upload_gen_ai_blobs(span_dict) + self._upload_gen_ai_blobs(span_dict) self.scrubber.scrub_span(span_dict) span = ReadableSpan(**span_dict) super().on_end(span) + def _upload_gen_ai_blobs(self, span: ReadableSpanDict) -> None: + if not self.uploader: + return -def _upload_gen_ai_blobs(span: ReadableSpanDict) -> None: - for attr_name in ['pydantic_ai.all_messages', 'gen_ai.input.messages', 'gen_ai.output.messages']: - attr_value = span['attributes'].get(attr_name) - if not (attr_value and isinstance(attr_value, str)): - continue - try: - messages = json.loads(attr_value) - except json.JSONDecodeError: - continue - for message in messages: - parts = message.get('parts', []) - for i, part in enumerate(parts): - # TODO otel semantic type - if part.get('type') != 'binary' or 'content' not in part: - continue - data = part['content'] - if not isinstance(data, str): - continue - - try: - value = base64.b64decode(data, validate=True) - except binascii.Error: - value = data.encode() - - media_type = part.get('media_type') - upload_item = UploadItem.create(value, timestamp=span['start_time'], media_type=media_type) - - # todo move to config - from logfire.experimental.uploaders.gcs import GcsUploader - - uploader = GcsUploader('alexmojaki-test') - uploader.upload(upload_item) - - # TODO keep part, remove content, add new key, make frontend work - parts[i] = dict(type='image-url', url=uploader.get_attribute_value(upload_item)) - span['attributes'] = {**span['attributes'], attr_name: json.dumps(messages)} + for attr_name in ['pydantic_ai.all_messages', 'gen_ai.input.messages', 'gen_ai.output.messages']: + attr_value = span['attributes'].get(attr_name) + if not (attr_value and isinstance(attr_value, str)): + continue + try: + messages = json.loads(attr_value) + except json.JSONDecodeError: + continue + for message in messages: + parts = message.get('parts', []) + for i, part in enumerate(parts): + # TODO otel semantic type + if part.get('type') != 'binary' or 'content' not in part: + continue + data = part['content'] + if not isinstance(data, str): + continue + + try: + value = base64.b64decode(data, validate=True) + except binascii.Error: + value = data.encode() + + media_type = part.get('media_type') + upload_item = UploadItem.create(value, timestamp=span['start_time'], media_type=media_type) + + self.uploader.upload(upload_item) + + # TODO keep part, remove content, add new key, make frontend work + parts[i] = dict(type='image-url', url=self.uploader.get_attribute_value(upload_item)) + span['attributes'] = {**span['attributes'], attr_name: json.dumps(messages)} def _set_error_level_and_status(span: ReadableSpanDict) -> None: From 4343c0ad44898c8ad54540f3104392b061bf3bb9 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 19:05:24 +0200 Subject: [PATCH 10/15] test_pydantic_ai_gcs_upload --- logfire/experimental/uploaders/gcs.py | 8 +- tests/otel_integrations/test_pydantic_ai.py | 143 +++++++++++++++++- .../otel_integrations/test_pydantic_ai_mcp.py | 2 +- 3 files changed, 147 insertions(+), 6 deletions(-) diff --git a/logfire/experimental/uploaders/gcs.py b/logfire/experimental/uploaders/gcs.py index 2482d7b58..504bc74a4 100644 --- a/logfire/experimental/uploaders/gcs.py +++ b/logfire/experimental/uploaders/gcs.py @@ -1,3 +1,5 @@ +from __future__ import annotations + from io import BytesIO from google.cloud import storage @@ -8,10 +10,10 @@ class GcsUploader(BaseUploader): """Google Cloud Storage uploader.""" - def __init__(self, bucket_name: str): + def __init__(self, bucket_name: str, client: storage.Client | None = None): self.bucket_name = bucket_name - self.storage_client = storage.Client() - self.bucket: storage.Bucket = self.storage_client.bucket(bucket_name) # pyright: ignore [reportUnknownMemberType] + self.client = client or storage.Client() + self.bucket: storage.Bucket = self.client.bucket(bucket_name) # pyright: ignore [reportUnknownMemberType] def upload(self, item: UploadItem): """Upload the given item to GCS.""" diff --git a/tests/otel_integrations/test_pydantic_ai.py b/tests/otel_integrations/test_pydantic_ai.py index 6e89d2db1..a370afdc5 100644 --- a/tests/otel_integrations/test_pydantic_ai.py +++ b/tests/otel_integrations/test_pydantic_ai.py @@ -1,15 +1,19 @@ import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any +from unittest.mock import Mock import pydantic import pytest +from dirty_equals import IsPartialDict +from inline_snapshot import snapshot import logfire +from logfire._internal.exporters.test import TestExporter from logfire._internal.tracer import _ProxyTracer # type: ignore from logfire._internal.utils import get_version try: - from pydantic_ai import Agent + from pydantic_ai import Agent, BinaryContent from pydantic_ai.models.instrumented import InstrumentationSettings, InstrumentedModel from pydantic_ai.models.test import TestModel @@ -97,3 +101,138 @@ def get_model(a: Agent): def test_invalid_instrument_pydantic_ai(): with pytest.raises(TypeError): logfire.instrument_pydantic_ai(42) # type: ignore + + +@pytest.mark.vcr() +@pytest.mark.anyio +async def test_pydantic_ai_gcs_upload(exporter: TestExporter, config_kwargs: dict[str, Any]): + with pytest.warns(ImportWarning): + from logfire.experimental.uploaders.gcs import GcsUploader + + bucket_name = 'test-bucket' + data = b'\x89PNG\r\n\x1a\n\x00\x00\x00\rIHDR\x00\x00\x00\x01\x00\x00\x00\x01\x08\x06\x00\x00\x00\x1f\x15\xc4\x89\x00\x00\x00\nIDATx\x9cc\x00\x01\x00\x00\x05\x00\x01\r\n-\xb4\x00\x00\x00\x00IEND\xaeB`\x82' + media_type = 'image/png' + key = f'1970-01-01/{media_type}/ebf4f635a17d10d6eb46ba680b70142419aa3220f228001a036d311a22ee9d2a' + image_url = f'https://storage.cloud.google.com/{bucket_name}/{key}' + + mock_client = Mock() + uploader = GcsUploader(bucket_name, client=mock_client) + assert uploader.bucket is mock_client.bucket(bucket_name) + assert isinstance(uploader.bucket, Mock) + + config_kwargs['advanced'].uploader = uploader + logfire.configure(**config_kwargs) + + agent = Agent('openai:gpt-4o') + logfire.instrument_pydantic_ai(agent, version=3) + + await agent.run(['What is this?', BinaryContent(data=data, media_type=media_type)]) + + blob = uploader.bucket.blob + blob.assert_called_with(key) + calls = blob(key).upload_from_file.call_args_list + assert len(calls) == 2 + for call in calls: + assert call.args[0].read() == data + assert call.kwargs['content_type'] == media_type + + assert exporter.exported_spans_as_dict(parse_json_attributes=True) == snapshot( + [ + { + 'name': 'chat gpt-4o', + 'context': {'trace_id': 1, 'span_id': 3, 'is_remote': False}, + 'parent': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, + 'start_time': 2000000000, + 'end_time': 3000000000, + 'attributes': { + 'gen_ai.operation.name': 'chat', + 'gen_ai.system': 'openai', + 'gen_ai.request.model': 'gpt-4o', + 'server.address': 'api.openai.com', + 'model_request_parameters': { + 'function_tools': [], + 'builtin_tools': [], + 'output_mode': 'text', + 'output_object': None, + 'output_tools': [], + 'allow_text_output': True, + 'allow_image_output': False, + }, + 'logfire.span_type': 'span', + 'logfire.msg': 'chat gpt-4o', + 'gen_ai.input.messages': [ + { + 'role': 'user', + 'parts': [ + {'type': 'text', 'content': 'What is this?'}, + { + 'type': 'image-url', + 'url': image_url, + }, + ], + } + ], + 'gen_ai.output.messages': [ + { + 'role': 'assistant', + 'parts': [ + { + 'type': 'text', + 'content': 'The image shows the lowercase Greek letter "pi" (π). π is a mathematical constant representing the ratio of a circle\'s circumference to its diameter, approximately equal to 3.14159. It\'s widely used in mathematics and science.', + } + ], + 'finish_reason': 'stop', + } + ], + 'logfire.json_schema': IsPartialDict(), + 'gen_ai.usage.input_tokens': 266, + 'gen_ai.usage.output_tokens': 47, + 'gen_ai.response.model': 'gpt-4o-2024-08-06', + 'operation.cost': 0.001135, + 'gen_ai.response.id': 'chatcmpl-CWmRLk5TMkoir3x9mJHUlwsq7oart', + 'gen_ai.response.finish_reasons': ('stop',), + }, + }, + { + 'name': 'invoke_agent agent', + 'context': {'trace_id': 1, 'span_id': 1, 'is_remote': False}, + 'parent': None, + 'start_time': 1000000000, + 'end_time': 4000000000, + 'attributes': { + 'model_name': 'gpt-4o', + 'agent_name': 'agent', + 'gen_ai.agent.name': 'agent', + 'logfire.msg': 'agent run', + 'logfire.span_type': 'span', + 'final_result': 'The image shows the lowercase Greek letter "pi" (π). π is a mathematical constant representing the ratio of a circle\'s circumference to its diameter, approximately equal to 3.14159. It\'s widely used in mathematics and science.', + 'gen_ai.usage.input_tokens': 266, + 'gen_ai.usage.output_tokens': 47, + 'pydantic_ai.all_messages': [ + { + 'role': 'user', + 'parts': [ + {'type': 'text', 'content': 'What is this?'}, + { + 'type': 'image-url', + 'url': 'https://storage.cloud.google.com/test-bucket/1970-01-01/image/png/ebf4f635a17d10d6eb46ba680b70142419aa3220f228001a036d311a22ee9d2a', + }, + ], + }, + { + 'role': 'assistant', + 'parts': [ + { + 'type': 'text', + 'content': 'The image shows the lowercase Greek letter "pi" (π). π is a mathematical constant representing the ratio of a circle\'s circumference to its diameter, approximately equal to 3.14159. It\'s widely used in mathematics and science.', + } + ], + 'finish_reason': 'stop', + }, + ], + 'logfire.json_schema': IsPartialDict(), + 'logfire.metrics': IsPartialDict(), + }, + }, + ] + ) diff --git a/tests/otel_integrations/test_pydantic_ai_mcp.py b/tests/otel_integrations/test_pydantic_ai_mcp.py index 40cf2841b..d1dcf2717 100644 --- a/tests/otel_integrations/test_pydantic_ai_mcp.py +++ b/tests/otel_integrations/test_pydantic_ai_mcp.py @@ -10,6 +10,7 @@ import pydantic import pytest from dirty_equals import IsPartialDict +from inline_snapshot import snapshot import logfire from logfire._internal.exporters.test import TestExporter @@ -17,7 +18,6 @@ from tests.otel_integrations.test_openai_agents import simplify_spans try: - from inline_snapshot import snapshot from mcp.server.fastmcp import Context, FastMCP from mcp.shared.memory import create_client_server_memory_streams from pydantic_ai import Agent From 0ea3c84d5a87ae1be1b5a565b3638f817f38441f Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 19:05:42 +0200 Subject: [PATCH 11/15] test_pydantic_ai_gcs_upload --- .../test_pydantic_ai_gcs_upload.yaml | 104 ++++++++++++++++++ 1 file changed, 104 insertions(+) create mode 100644 tests/otel_integrations/cassettes/test_pydantic_ai/test_pydantic_ai_gcs_upload.yaml diff --git a/tests/otel_integrations/cassettes/test_pydantic_ai/test_pydantic_ai_gcs_upload.yaml b/tests/otel_integrations/cassettes/test_pydantic_ai/test_pydantic_ai_gcs_upload.yaml new file mode 100644 index 000000000..5874ef26a --- /dev/null +++ b/tests/otel_integrations/cassettes/test_pydantic_ai/test_pydantic_ai_gcs_upload.yaml @@ -0,0 +1,104 @@ +interactions: +- request: + body: '{"messages":[{"role":"user","content":[{"text":"What is this?","type":"text"},{"image_url":{"url":"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAACklEQVR4nGMAAQAABQABDQottAAAAABJRU5ErkJggg=="},"type":"image_url"}]}],"model":"gpt-4o","stream":false}' + headers: + accept: + - application/json + accept-encoding: + - gzip, deflate, zstd + connection: + - keep-alive + content-length: + - '271' + content-type: + - application/json + host: + - api.openai.com + user-agent: + - pydantic-ai/1.6.0 + x-stainless-arch: + - arm64 + x-stainless-async: + - async:asyncio + x-stainless-lang: + - python + x-stainless-os: + - MacOS + x-stainless-package-version: + - 2.6.1 + x-stainless-read-timeout: + - '600' + x-stainless-retry-count: + - '0' + x-stainless-runtime: + - CPython + x-stainless-runtime-version: + - 3.12.7 + method: POST + uri: https://api.openai.com/v1/chat/completions + response: + body: + string: !!binary | + H4sIAAAAAAAAA4xTyW7bQAy9+yuIuaQFbMO7E1976IL2UqTooQkEZkRJrGfLDFUnCALkD/NLxchO + 5LQp0Isg8PG9edzuBgCKS7UBpRsUbYMZvftuv37eLs+/bD3H+c2Z/fThm9ml67XHKGqYGf7qJ2l5 + Yo21t8GQsHd7WEdCoaw6Xa+mZ7PT9XzZAdaXZDKtDjJa+NFsMluMJqejyepAbDxrSmoDPwYAAHfd + N1t0Jd2oDUyGTxFLKWFNavOcBKCiNzmiMCVOgm5v9wBq74Rc5/q8IWCLNUFq/C6BNATG7yhqTATv + I9EWDIlQhAsV+ELBm8eHt2N4fABOgGBRGrIorNGA9q57CyKFSImcsKs7yYjCHnwFCJqjNnSSup/W + VhTJaQLxwJKgZLQkFIeAIUR/wxaFzC3QdYsmJ83H08V0eTaGj3KSYMdlRttEJbA7cpMAXQlJcxYf + HxcfqWoT5t671pgjAJ3zkn26ru2XB+T+udHG1yH6q/QHVVXsODVFJEze5aYm8UF16P0A4LIbaPti + RipEb4MU4rfUPTdbrfZ6ql+hHl2sD6B4QdPH59P58BW9oiRBNuloJZRG3VDZU/v9wbZkfwQMjqr+ + 281r2vvK2dX/I98DWlMQKosQqWT9suI+LVK+sH+lPXe5M6wSxV+sqRCmmCdRUoWtOdxquk1CtqjY + 1RRD5P0FVKHQV9V0fbpcrtZqcD/4DQAA//8DADK3eBAKBAAA + headers: + Connection: + - keep-alive + Content-Encoding: + - gzip + Content-Type: + - application/json + Date: + - Fri, 31 Oct 2025 16:38:57 GMT + Server: + - cloudflare + Strict-Transport-Security: + - max-age=31536000; includeSubDomains; preload + Transfer-Encoding: + - chunked + X-Content-Type-Options: + - nosniff + access-control-expose-headers: + - X-Request-ID + alt-svc: + - h3=":443"; ma=86400 + cf-cache-status: + - DYNAMIC + openai-processing-ms: + - '1643' + openai-version: + - '2020-10-01' + x-envoy-upstream-service-time: + - '1823' + x-openai-proxy-wasm: + - v0.1 + x-ratelimit-limit-input-images: + - '50000' + x-ratelimit-limit-requests: + - '10000' + x-ratelimit-limit-tokens: + - '2000000' + x-ratelimit-remaining-input-images: + - '49999' + x-ratelimit-remaining-requests: + - '9999' + x-ratelimit-remaining-tokens: + - '1999229' + x-ratelimit-reset-input-images: + - 1ms + x-ratelimit-reset-requests: + - 6ms + x-ratelimit-reset-tokens: + - 23ms + status: + code: 200 + message: OK +version: 1 From dfb98cf242ddb2246a2fb47e709540feb5f5b714 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 19:06:28 +0200 Subject: [PATCH 12/15] pragma --- logfire/_internal/exporters/processor_wrapper.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/logfire/_internal/exporters/processor_wrapper.py b/logfire/_internal/exporters/processor_wrapper.py index e22ddf96a..9320d5f4c 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -105,7 +105,7 @@ def _upload_gen_ai_blobs(self, span: ReadableSpanDict) -> None: continue try: messages = json.loads(attr_value) - except json.JSONDecodeError: + except json.JSONDecodeError: # pragma: no cover continue for message in messages: parts = message.get('parts', []) @@ -114,12 +114,12 @@ def _upload_gen_ai_blobs(self, span: ReadableSpanDict) -> None: if part.get('type') != 'binary' or 'content' not in part: continue data = part['content'] - if not isinstance(data, str): + if not isinstance(data, str): # pragma: no cover continue try: value = base64.b64decode(data, validate=True) - except binascii.Error: + except binascii.Error: # pragma: no cover value = data.encode() media_type = part.get('media_type') From dc0570cb3f28ea9b1b42b70e23aa23909d6af045 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 19:09:06 +0200 Subject: [PATCH 13/15] warnings.filterwarnings --- tests/otel_integrations/test_pydantic_ai.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/otel_integrations/test_pydantic_ai.py b/tests/otel_integrations/test_pydantic_ai.py index a370afdc5..1f3295654 100644 --- a/tests/otel_integrations/test_pydantic_ai.py +++ b/tests/otel_integrations/test_pydantic_ai.py @@ -1,4 +1,5 @@ import sys +import warnings from typing import TYPE_CHECKING, Any from unittest.mock import Mock @@ -106,7 +107,9 @@ def test_invalid_instrument_pydantic_ai(): @pytest.mark.vcr() @pytest.mark.anyio async def test_pydantic_ai_gcs_upload(exporter: TestExporter, config_kwargs: dict[str, Any]): - with pytest.warns(ImportWarning): + with warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=ImportWarning) + from logfire.experimental.uploaders.gcs import GcsUploader bucket_name = 'test-bucket' From 5c688d24e8ddcee5dc9af9604130a4077ce810cd Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 19:17:35 +0200 Subject: [PATCH 14/15] warnings.filterwarnings --- tests/otel_integrations/test_pydantic_ai.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/otel_integrations/test_pydantic_ai.py b/tests/otel_integrations/test_pydantic_ai.py index 1f3295654..ee5fe7c75 100644 --- a/tests/otel_integrations/test_pydantic_ai.py +++ b/tests/otel_integrations/test_pydantic_ai.py @@ -109,6 +109,7 @@ def test_invalid_instrument_pydantic_ai(): async def test_pydantic_ai_gcs_upload(exporter: TestExporter, config_kwargs: dict[str, Any]): with warnings.catch_warnings(): warnings.filterwarnings('ignore', category=ImportWarning) + warnings.filterwarnings('ignore', category=FutureWarning) from logfire.experimental.uploaders.gcs import GcsUploader From ed84249068ce377bb7783301b0eddca19c75ecf9 Mon Sep 17 00:00:00 2001 From: Alex Hall Date: Fri, 31 Oct 2025 19:53:01 +0200 Subject: [PATCH 15/15] pragma --- logfire/experimental/uploaders/__init__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/logfire/experimental/uploaders/__init__.py b/logfire/experimental/uploaders/__init__.py index 93ffc371c..95341cea0 100644 --- a/logfire/experimental/uploaders/__init__.py +++ b/logfire/experimental/uploaders/__init__.py @@ -24,10 +24,10 @@ def create(cls, value: bytes, *, timestamp: int | None, media_type: str | None = """ parts = [sha256_bytes(value)] - if media_type: + if media_type: # pragma: no branch parts.append(media_type) - if timestamp is None: + if timestamp is None: # pragma: no cover date = datetime.date.today() else: date = datetime.datetime.fromtimestamp(timestamp / ONE_SECOND_IN_NANOSECONDS).date()