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 deadeb9aa..9320d5f4c 100644 --- a/logfire/_internal/exporters/processor_wrapper.py +++ b/logfire/_internal/exporters/processor_wrapper.py @@ -1,5 +1,7 @@ from __future__ import annotations +import base64 +import binascii import json from contextlib import suppress from dataclasses import dataclass @@ -13,6 +15,7 @@ import logfire +from ...experimental.uploaders import BaseUploader, UploadItem from ..constants import ( ATTRIBUTES_JSON_SCHEMA_KEY, ATTRIBUTES_LOG_LEVEL_NUM_KEY, @@ -64,6 +67,7 @@ class MainSpanProcessorWrapper(WrapperSpanProcessor): """ scrubber: BaseScrubber + uploader: BaseUploader | None def on_start( self, @@ -86,10 +90,47 @@ 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) + 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 + + 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: # pragma: no cover + 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): # pragma: no cover + continue + + try: + value = base64.b64decode(data, validate=True) + except binascii.Error: # pragma: no cover + 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: """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() diff --git a/logfire/experimental/uploaders/__init__.py b/logfire/experimental/uploaders/__init__.py new file mode 100644 index 000000000..95341cea0 --- /dev/null +++ b/logfire/experimental/uploaders/__init__.py @@ -0,0 +1,49 @@ +from __future__ import annotations + +import datetime +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 + + +@dataclass +class UploadItem: + """An item to upload.""" + + key: str + value: bytes + media_type: str | None = None + + @classmethod + 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: # pragma: no branch + parts.append(media_type) + + if timestamp is None: # pragma: no cover + date = datetime.date.today() + else: + date = datetime.datetime.fromtimestamp(timestamp / ONE_SECOND_IN_NANOSECONDS).date() + parts.append(date.isoformat()) + + key = '/'.join(parts[::-1]) + return cls(key=key, value=value, media_type=media_type) + + +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, item: UploadItem) -> JsonValue: + """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 new file mode 100644 index 000000000..504bc74a4 --- /dev/null +++ b/logfire/experimental/uploaders/gcs.py @@ -0,0 +1,25 @@ +from __future__ import annotations + +from io import BytesIO + +from google.cloud import storage + +from logfire.experimental.uploaders import BaseUploader, UploadItem + + +class GcsUploader(BaseUploader): + """Google Cloud Storage uploader.""" + + def __init__(self, bucket_name: str, client: storage.Client | None = None): + self.bucket_name = bucket_name + 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.""" + 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, item: UploadItem): + """Return the GCS authenticated URL for the uploaded item.""" + return f'https://storage.cloud.google.com/{self.bucket_name}/{item.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/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 diff --git a/tests/otel_integrations/test_pydantic_ai.py b/tests/otel_integrations/test_pydantic_ai.py index 6e89d2db1..ee5fe7c75 100644 --- a/tests/otel_integrations/test_pydantic_ai.py +++ b/tests/otel_integrations/test_pydantic_ai.py @@ -1,15 +1,20 @@ import sys -from typing import TYPE_CHECKING +import warnings +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 +102,141 @@ 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 warnings.catch_warnings(): + warnings.filterwarnings('ignore', category=ImportWarning) + warnings.filterwarnings('ignore', category=FutureWarning) + + 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 diff --git a/uv.lock b/uv.lock index 76776c93c..e2cc904c3 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.42.0" @@ -1631,6 +1647,77 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/87/24/ec82aee6ba1a076288818fe5cc5125f4d93fffdc68bb7b381c68286c8aaa/google_auth-2.42.0-py2.py3-none-any.whl", hash = "sha256:f8f944bcb9723339b0ef58a73840f3c61bc91b69bf7368464906120b55804473", size = 222550, upload-time = "2025-10-28T17:38:05.496Z" }, ] +[[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.47.0" @@ -1650,6 +1737,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/89/ef/e080e8d67c270ea320956bb911a9359664fc46d3b87d1f029decd33e5c4c/google_genai-1.47.0-py3-none-any.whl", hash = "sha256:e3851237556cbdec96007d8028b4b1f2425cdc5c099a8dc36b72a57e42821b60", size = 241506, upload-time = "2025-10-29T22:01:00.982Z" }, ] +[[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"