diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index cd35baf1..49686a6b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -12,6 +12,111 @@ permissions: contents: read jobs: + test-dependencies: + name: Check running without dependencies + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install uv with Python 3.12 + uses: astral-sh/setup-uv@v5 + with: + version: "0.7.5" + python-version: "3.12" + + - name: Install betterproto2 + working-directory: ./betterproto2 + run: uv sync --locked + + - name: Install betterproto2_compiler + working-directory: ./betterproto2_compiler + run: uv sync --locked + + - name: Compile test files + working-directory: ./betterproto2_compiler + shell: bash + run: uv run poe generate + + - name: Move compiled files to betterproto2 + shell: bash + run: cp -r betterproto2_compiler/tests/outputs betterproto2/tests + + - name: Execute test suite + working-directory: ./betterproto2 + run: uv run poe test + + + - name: Add Pydantic + working-directory: ./betterproto2 + run: uv sync --locked --extra=pydantic + + - name: Execute test suite + working-directory: ./betterproto2 + run: uv run poe test + + + - name: Add all but Pydantic + working-directory: ./betterproto2 + run: uv sync --locked --extra=grpclib --extra=grpcio --extra=protobuf + + - name: Execute test suite + working-directory: ./betterproto2 + run: uv run poe test + + + - name: Add grpclib + working-directory: ./betterproto2 + run: uv sync --locked --extra=grpclib + + - name: Execute test suite + working-directory: ./betterproto2 + run: uv run poe test + + + - name: Add all but grpclib + working-directory: ./betterproto2 + run: uv sync --locked --extra=pydantic --extra=grpcio --extra=protobuf + + - name: Execute test suite + working-directory: ./betterproto2 + run: uv run poe test + + + - name: Add grpcio + working-directory: ./betterproto2 + run: uv sync --locked --extra=grpcio + + - name: Execute test suite + working-directory: ./betterproto2 + run: uv run poe test + + + - name: Add all but grpcio + working-directory: ./betterproto2 + run: uv sync --locked --extra=pydantic --extra=grpclib --extra=protobuf + + - name: Execute test suite + working-directory: ./betterproto2 + run: uv run poe test + + + - name: Add protobuf + working-directory: ./betterproto2 + run: uv sync --locked --extra=protobuf + + - name: Execute test suite + working-directory: ./betterproto2 + run: uv run poe test + + + - name: Add all but protobuf + working-directory: ./betterproto2 + run: uv sync --locked --extra=pydantic --extra=grpclib --extra=grpcio + + - name: Execute test suite + working-directory: ./betterproto2 + run: uv run poe test + tests: name: ${{ matrix.os }} / ${{ matrix.python-version }} runs-on: ${{ matrix.os }}-latest diff --git a/betterproto2/src/betterproto2/__init__.py b/betterproto2/src/betterproto2/__init__.py index d7174c1a..5c8d1b3c 100644 --- a/betterproto2/src/betterproto2/__init__.py +++ b/betterproto2/src/betterproto2/__init__.py @@ -43,7 +43,6 @@ from ._version import __version__, check_compiler_version from .casing import camel_case, safe_snake_case, snake_case from .enum_ import Enum as Enum -from .grpc.grpclib_client import ServiceStub as ServiceStub from .utils import classproperty, staticproperty if TYPE_CHECKING: diff --git a/betterproto2/src/betterproto2/grpc/__init__.py b/betterproto2/src/betterproto2/grpc/__init__.py deleted file mode 100644 index e69de29b..00000000 diff --git a/betterproto2/src/betterproto2/grpclib/__init__.py b/betterproto2/src/betterproto2/grpclib/__init__.py new file mode 100644 index 00000000..23d7c063 --- /dev/null +++ b/betterproto2/src/betterproto2/grpclib/__init__.py @@ -0,0 +1,2 @@ +from .grpclib_client import ServiceStub as ServiceStub +from .grpclib_server import ServiceBase as ServiceBase diff --git a/betterproto2/src/betterproto2/grpc/grpclib_client.py b/betterproto2/src/betterproto2/grpclib/grpclib_client.py similarity index 100% rename from betterproto2/src/betterproto2/grpc/grpclib_client.py rename to betterproto2/src/betterproto2/grpclib/grpclib_client.py diff --git a/betterproto2/src/betterproto2/grpc/grpclib_server.py b/betterproto2/src/betterproto2/grpclib/grpclib_server.py similarity index 100% rename from betterproto2/src/betterproto2/grpc/grpclib_server.py rename to betterproto2/src/betterproto2/grpclib/grpclib_server.py diff --git a/betterproto2/tests/grpc/test_grpclib_client.py b/betterproto2/tests/grpc/test_grpclib_client.py index af45a654..0391d281 100644 --- a/betterproto2/tests/grpc/test_grpclib_client.py +++ b/betterproto2/tests/grpc/test_grpclib_client.py @@ -1,25 +1,18 @@ import asyncio import uuid +from typing import TYPE_CHECKING -import grpclib -import grpclib.client -import grpclib.metadata -import grpclib.server import pytest -from grpclib.testing import ChannelFor -from tests.grpc.async_channel import AsyncChannel -from tests.outputs.service.service import ( - DoThingRequest, - DoThingResponse, - GetThingRequest, - TestStub as ThingServiceClient, -) +from tests.util import requires_grpcio, requires_grpclib # noqa: F401 -from .thing_service import ThingService +if TYPE_CHECKING: + from tests.outputs.service.service import TestStub as ThingServiceClient -async def _test_client(client: ThingServiceClient, name="clean room", **kwargs): +async def _test_client(client: "ThingServiceClient", name="clean room", **kwargs): + from tests.outputs.service.service import DoThingRequest + response = await client.do_thing(DoThingRequest(name=name), **kwargs) assert response.names == [name] @@ -38,6 +31,9 @@ def server_side_test(stream): @pytest.fixture def handler_trailer_only_unauthenticated(): + import grpclib + import grpclib.server + async def handler(stream: grpclib.server.Stream): await stream.recv_message() await stream.send_initial_metadata() @@ -47,13 +43,28 @@ async def handler(stream: grpclib.server.Stream): @pytest.mark.asyncio -async def test_simple_service_call(): +async def test_simple_service_call(requires_grpclib, requires_grpcio): + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import TestStub as ThingServiceClient + + from .thing_service import ThingService + async with ChannelFor([ThingService()]) as channel: await _test_client(ThingServiceClient(channel)) @pytest.mark.asyncio -async def test_trailer_only_error_unary_unary(mocker, handler_trailer_only_unauthenticated): +async def test_trailer_only_error_unary_unary( + mocker, requires_grpclib, requires_grpcio, handler_trailer_only_unauthenticated +): + import grpclib + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import DoThingRequest, TestStub as ThingServiceClient + + from .thing_service import ThingService + service = ThingService() mocker.patch.object( service, @@ -68,7 +79,19 @@ async def test_trailer_only_error_unary_unary(mocker, handler_trailer_only_unaut @pytest.mark.asyncio -async def test_trailer_only_error_stream_unary(mocker, handler_trailer_only_unauthenticated): +async def test_trailer_only_error_stream_unary( + mocker, requires_grpclib, requires_grpcio, handler_trailer_only_unauthenticated +): + import grpclib + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import ( + DoThingRequest, + TestStub as ThingServiceClient, + ) + + from .thing_service import ThingService + service = ThingService() mocker.patch.object( service, @@ -84,7 +107,13 @@ async def test_trailer_only_error_stream_unary(mocker, handler_trailer_only_unau @pytest.mark.asyncio -async def test_service_call_mutable_defaults(mocker): +async def test_service_call_mutable_defaults(mocker, requires_grpclib, requires_grpcio): + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import TestStub as ThingServiceClient + + from .thing_service import ThingService + async with ChannelFor([ThingService()]) as channel: client = ThingServiceClient(channel) spy = mocker.spy(client, "_unary_unary") @@ -95,7 +124,15 @@ async def test_service_call_mutable_defaults(mocker): @pytest.mark.asyncio -async def test_service_call_with_upfront_request_params(): +async def test_service_call_with_upfront_request_params(requires_grpclib, requires_grpcio): + import grpclib + import grpclib.metadata + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import TestStub as ThingServiceClient + + from .thing_service import ThingService + # Setting deadline deadline = grpclib.metadata.Deadline.from_timeout(22) metadata = {"authorization": "12345"} @@ -111,7 +148,19 @@ async def test_service_call_with_upfront_request_params(): @pytest.mark.asyncio -async def test_service_call_lower_level_with_overrides(): +async def test_service_call_lower_level_with_overrides(requires_grpclib, requires_grpcio): + import grpclib + import grpclib.metadata + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import ( + DoThingRequest, + DoThingResponse, + TestStub as ThingServiceClient, + ) + + from .thing_service import ThingService + THING_TO_DO = "get milk" # Setting deadline @@ -156,54 +205,69 @@ async def test_service_call_lower_level_with_overrides(): @pytest.mark.asyncio -@pytest.mark.parametrize( - ("overrides_gen",), - [ - (lambda: dict(timeout=10),), - (lambda: dict(deadline=grpclib.metadata.Deadline.from_timeout(10)),), - (lambda: dict(metadata={"authorization": str(uuid.uuid4())}),), - (lambda: dict(timeout=20, metadata={"authorization": str(uuid.uuid4())}),), - ], -) -async def test_service_call_high_level_with_overrides(mocker, overrides_gen): - overrides = overrides_gen() - request_spy = mocker.spy(grpclib.client.Channel, "request") - name = str(uuid.uuid4()) - defaults = dict( - timeout=99, - deadline=grpclib.metadata.Deadline.from_timeout(99), - metadata={"authorization": name}, - ) +async def test_service_call_high_level_with_overrides(mocker, requires_grpclib, requires_grpcio): + import grpclib + import grpclib.client + import grpclib.metadata + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import TestStub as ThingServiceClient + + from .thing_service import ThingService + + overrides = [ + dict(timeout=10), + dict(deadline=grpclib.metadata.Deadline.from_timeout(10)), + dict(metadata={"authorization": str(uuid.uuid4())}), + dict(timeout=20, metadata={"authorization": str(uuid.uuid4())}), + ] + + for override in overrides: + request_spy = mocker.spy(grpclib.client.Channel, "request") + name = str(uuid.uuid4()) + defaults = dict( + timeout=99, + deadline=grpclib.metadata.Deadline.from_timeout(99), + metadata={"authorization": name}, + ) - async with ChannelFor( - [ - ThingService( - test_hook=_assert_request_meta_received( - deadline=grpclib.metadata.Deadline.from_timeout(overrides.get("timeout", 99)), - metadata=overrides.get("metadata", defaults.get("metadata")), + async with ChannelFor( + [ + ThingService( + test_hook=_assert_request_meta_received( + deadline=grpclib.metadata.Deadline.from_timeout(override.get("timeout", 99)), + metadata=override.get("metadata", defaults.get("metadata")), + ) ) - ) - ] - ) as channel: - client = ThingServiceClient(channel, **defaults) - await _test_client(client, name=name, **overrides) - assert request_spy.call_count == 1 + ] + ) as channel: + client = ThingServiceClient(channel, **defaults) + await _test_client(client, name=name, **override) + assert request_spy.call_count == 1 + + request_spy_call_kwargs = request_spy.call_args.kwargs - request_spy_call_kwargs = request_spy.call_args.kwargs + # ensure all overrides were successful + for key, value in override.items(): + assert key in request_spy_call_kwargs + assert request_spy_call_kwargs[key] == value - # ensure all overrides were successful - for key, value in overrides.items(): - assert key in request_spy_call_kwargs - assert request_spy_call_kwargs[key] == value + # ensure default values were retained + for key in set(defaults.keys()) - set(override.keys()): + assert key in request_spy_call_kwargs + assert request_spy_call_kwargs[key] == defaults[key] - # ensure default values were retained - for key in set(defaults.keys()) - set(overrides.keys()): - assert key in request_spy_call_kwargs - assert request_spy_call_kwargs[key] == defaults[key] + mocker.stop(request_spy) @pytest.mark.asyncio -async def test_async_gen_for_unary_stream_request(): +async def test_async_gen_for_unary_stream_request(requires_grpclib, requires_grpcio): + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import GetThingRequest, TestStub as ThingServiceClient + + from .thing_service import ThingService + thing_name = "my milkshakes" async with ChannelFor([ThingService()]) as channel: @@ -215,7 +279,14 @@ async def test_async_gen_for_unary_stream_request(): @pytest.mark.asyncio -async def test_async_gen_for_stream_stream_request(): +async def test_async_gen_for_stream_stream_request(requires_grpclib, requires_grpcio): + from grpclib.testing import ChannelFor + + from tests.grpc.async_channel import AsyncChannel + from tests.outputs.service.service import GetThingRequest, TestStub as ThingServiceClient + + from .thing_service import ThingService + some_things = ["cake", "cricket", "coral reef"] more_things = ["ball", "that", "56kmodem", "liberal humanism", "cheesesticks"] expected_things = (*some_things, *more_things) @@ -248,7 +319,13 @@ async def test_async_gen_for_stream_stream_request(): @pytest.mark.asyncio -async def test_stream_unary_with_empty_iterable(): +async def test_stream_unary_with_empty_iterable(requires_grpclib, requires_grpcio): + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import DoThingRequest, TestStub as ThingServiceClient + + from .thing_service import ThingService + things = [] # empty async with ChannelFor([ThingService()]) as channel: @@ -259,7 +336,13 @@ async def test_stream_unary_with_empty_iterable(): @pytest.mark.asyncio -async def test_stream_stream_with_empty_iterable(): +async def test_stream_stream_with_empty_iterable(requires_grpclib, requires_grpcio): + from grpclib.testing import ChannelFor + + from tests.outputs.service.service import GetThingRequest, TestStub as ThingServiceClient + + from .thing_service import ThingService + things = [] # empty async with ChannelFor([ThingService()]) as channel: diff --git a/betterproto2/tests/grpc/test_grpclib_reflection.py b/betterproto2/tests/grpc/test_grpclib_reflection.py index b7507255..437c3a7e 100644 --- a/betterproto2/tests/grpc/test_grpclib_reflection.py +++ b/betterproto2/tests/grpc/test_grpclib_reflection.py @@ -2,57 +2,57 @@ from typing import Generic, TypeVar import pytest -from google.protobuf import descriptor_pb2 -from grpclib.reflection.service import ServerReflection -from grpclib.reflection.v1.reflection_grpc import ServerReflectionBase as ServerReflectionBaseV1 -from grpclib.reflection.v1alpha.reflection_grpc import ServerReflectionBase as ServerReflectionBaseV1Alpha -from grpclib.testing import ChannelFor -from tests.outputs.grpclib_reflection.example_service import TestBase -from tests.outputs.grpclib_reflection.grpc.reflection.v1 import ( - ErrorResponse, - ListServiceResponse, - ServerReflectionRequest, - ServerReflectionStub, - ServiceResponse, -) -from tests.outputs.grpclib_reflection_descriptors.google_proto_descriptor_pool import ( - default_google_proto_descriptor_pool, -) - - -class TestService(TestBase): - pass - - -T = TypeVar("T") - - -class AsyncIterableQueue(Generic[T]): - CLOSED_SENTINEL = object() - - def __init__(self): - self._queue = asyncio.Queue() - self._done = asyncio.Event() - - def put(self, item: T): - self._queue.put_nowait(item) - - def close(self): - self._queue.put_nowait(self.CLOSED_SENTINEL) - - def __aiter__(self): - return self - - async def __anext__(self) -> T: - val = await self._queue.get() - if val is self.CLOSED_SENTINEL: - raise StopAsyncIteration - return val +from tests.util import requires_grpclib, requires_protobuf # noqa: F401 @pytest.mark.asyncio -async def test_grpclib_reflection(): +async def test_grpclib_reflection(requires_grpclib, requires_protobuf): + from google.protobuf import descriptor_pb2 + from grpclib.reflection.service import ServerReflection + from grpclib.reflection.v1.reflection_grpc import ServerReflectionBase as ServerReflectionBaseV1 + from grpclib.reflection.v1alpha.reflection_grpc import ServerReflectionBase as ServerReflectionBaseV1Alpha + from grpclib.testing import ChannelFor + + from tests.outputs.grpclib_reflection.example_service import TestBase + from tests.outputs.grpclib_reflection.grpc.reflection.v1 import ( + ErrorResponse, + ListServiceResponse, + ServerReflectionRequest, + ServerReflectionStub, + ServiceResponse, + ) + from tests.outputs.grpclib_reflection_descriptors.google_proto_descriptor_pool import ( + default_google_proto_descriptor_pool, + ) + + class TestService(TestBase): + pass + + T = TypeVar("T") + + class AsyncIterableQueue(Generic[T]): + CLOSED_SENTINEL = object() + + def __init__(self): + self._queue = asyncio.Queue() + self._done = asyncio.Event() + + def put(self, item: T): + self._queue.put_nowait(item) + + def close(self): + self._queue.put_nowait(self.CLOSED_SENTINEL) + + def __aiter__(self): + return self + + async def __anext__(self) -> T: + val = await self._queue.get() + if val is self.CLOSED_SENTINEL: + raise StopAsyncIteration + return val + service = TestService() services = ServerReflection.extend([service]) for service in services: diff --git a/betterproto2/tests/grpc/test_message_enum_descriptors.py b/betterproto2/tests/grpc/test_message_enum_descriptors.py index 3f71425d..51b62ed6 100644 --- a/betterproto2/tests/grpc/test_message_enum_descriptors.py +++ b/betterproto2/tests/grpc/test_message_enum_descriptors.py @@ -1,17 +1,19 @@ import pytest -from tests.outputs.import_cousin_package_same_name.import_cousin_package_same_name.test.subpackage import Test +from tests.util import requires_protobuf # noqa: F401 -# importing the cousin should cause no descriptor pool errors since the subpackage imports it once already -from tests.outputs.import_cousin_package_same_name_descriptors.import_cousin_package_same_name.cousin.subpackage import ( # noqa: E501 - CousinMessage, -) -from tests.outputs.import_cousin_package_same_name_descriptors.import_cousin_package_same_name.test.subpackage import ( - Test as TestWithDesc, -) +def test_message_enum_descriptors(requires_protobuf): + from tests.outputs.import_cousin_package_same_name.import_cousin_package_same_name.test.subpackage import Test + + # importing the cousin should cause no descriptor pool errors since the subpackage imports it once already + from tests.outputs.import_cousin_package_same_name_descriptors.import_cousin_package_same_name.cousin.subpackage import ( # noqa: E501 + CousinMessage, + ) + from tests.outputs.import_cousin_package_same_name_descriptors.import_cousin_package_same_name.test.subpackage import ( # noqa: E501 + Test as TestWithDesc, + ) -def test_message_enum_descriptors(): # Normally descriptors are not available as they require protobuf support # to inteoperate with other libraries. with pytest.raises(AttributeError): diff --git a/betterproto2/tests/inputs/bool/test_bool.py b/betterproto2/tests/inputs/bool/test_bool.py index aa553e37..fb29ac7f 100644 --- a/betterproto2/tests/inputs/bool/test_bool.py +++ b/betterproto2/tests/inputs/bool/test_bool.py @@ -1,5 +1,7 @@ import pytest +from tests.util import requires_pydantic # noqa: F401 + def test_value(): from tests.outputs.bool.bool import Test @@ -8,21 +10,21 @@ def test_value(): assert not message.value, "Boolean is False by default" -def test_pydantic_no_value(): +def test_pydantic_no_value(requires_pydantic): from tests.outputs.bool_pydantic.bool import Test as TestPyd message = TestPyd() assert not message.value, "Boolean is False by default" -def test_pydantic_value(): +def test_pydantic_value(requires_pydantic): from tests.outputs.bool_pydantic.bool import Test as TestPyd message = TestPyd(value=False) assert not message.value -def test_pydantic_bad_value(): +def test_pydantic_bad_value(requires_pydantic): from tests.outputs.bool_pydantic.bool import Test as TestPyd with pytest.raises(ValueError): diff --git a/betterproto2/tests/inputs/example_service/test_example_service.py b/betterproto2/tests/inputs/example_service/test_example_service.py index a465b7c2..71b1a8b5 100644 --- a/betterproto2/tests/inputs/example_service/test_example_service.py +++ b/betterproto2/tests/inputs/example_service/test_example_service.py @@ -1,53 +1,55 @@ from collections.abc import AsyncIterator import pytest -from grpclib.testing import ChannelFor - -from tests.outputs.example_service.example_service import ( - ExampleRequest, - ExampleResponse, - TestBase, - TestStub, -) - - -class ExampleService(TestBase): - async def example_unary_unary(self, example_request: ExampleRequest) -> "ExampleResponse": - return ExampleResponse( - example_string=example_request.example_string, - example_integer=example_request.example_integer, - ) - - async def example_unary_stream(self, example_request: ExampleRequest) -> AsyncIterator["ExampleResponse"]: - response = ExampleResponse( - example_string=example_request.example_string, - example_integer=example_request.example_integer, - ) - yield response - yield response - yield response - - async def example_stream_unary( - self, example_request_iterator: AsyncIterator["ExampleRequest"] - ) -> "ExampleResponse": - async for example_request in example_request_iterator: + +from tests.util import requires_grpclib # noqa: F401 + + +@pytest.mark.asyncio +async def test_calls_with_different_cardinalities(requires_grpclib): + from grpclib.testing import ChannelFor + + from tests.outputs.example_service.example_service import ( + ExampleRequest, + ExampleResponse, + TestBase, + TestStub, + ) + + class ExampleService(TestBase): + async def example_unary_unary(self, example_request: ExampleRequest) -> "ExampleResponse": return ExampleResponse( example_string=example_request.example_string, example_integer=example_request.example_integer, ) - async def example_stream_stream( - self, example_request_iterator: AsyncIterator["ExampleRequest"] - ) -> AsyncIterator["ExampleResponse"]: - async for example_request in example_request_iterator: - yield ExampleResponse( + async def example_unary_stream(self, example_request: ExampleRequest) -> AsyncIterator["ExampleResponse"]: + response = ExampleResponse( example_string=example_request.example_string, example_integer=example_request.example_integer, ) + yield response + yield response + yield response + async def example_stream_unary( + self, example_request_iterator: AsyncIterator["ExampleRequest"] + ) -> "ExampleResponse": + async for example_request in example_request_iterator: + return ExampleResponse( + example_string=example_request.example_string, + example_integer=example_request.example_integer, + ) + + async def example_stream_stream( + self, example_request_iterator: AsyncIterator["ExampleRequest"] + ) -> AsyncIterator["ExampleResponse"]: + async for example_request in example_request_iterator: + yield ExampleResponse( + example_string=example_request.example_string, + example_integer=example_request.example_integer, + ) -@pytest.mark.asyncio -async def test_calls_with_different_cardinalities(): example_request = ExampleRequest("test string", 42) async with ChannelFor([ExampleService()]) as channel: diff --git a/betterproto2/tests/inputs/google_impl_behavior_equivalence/test_google_impl_behavior_equivalence.py b/betterproto2/tests/inputs/google_impl_behavior_equivalence/test_google_impl_behavior_equivalence.py index 81ec5003..ad714259 100644 --- a/betterproto2/tests/inputs/google_impl_behavior_equivalence/test_google_impl_behavior_equivalence.py +++ b/betterproto2/tests/inputs/google_impl_behavior_equivalence/test_google_impl_behavior_equivalence.py @@ -1,11 +1,6 @@ -from datetime import ( - datetime, - timezone, -) +from datetime import datetime, timezone import pytest -from google.protobuf import json_format -from google.protobuf.timestamp_pb2 import Timestamp from tests.outputs.google_impl_behavior_equivalence.google_impl_behavior_equivalence import ( Empty, @@ -14,16 +9,17 @@ Spam, Test, ) -from tests.outputs.google_impl_behavior_equivalence_reference.google_impl_behavior_equivalence_pb2 import ( - Empty as ReferenceEmpty, - Foo as ReferenceFoo, - Request as ReferenceRequest, - Spam as ReferenceSpam, - Test as ReferenceTest, -) +from tests.util import requires_protobuf # noqa: F401 + + +def test_oneof_serializes_similar_to_google_oneof(requires_protobuf): + from google.protobuf import json_format + from tests.outputs.google_impl_behavior_equivalence_reference.google_impl_behavior_equivalence_pb2 import ( + Foo as ReferenceFoo, + Test as ReferenceTest, + ) -def test_oneof_serializes_similar_to_google_oneof(): tests = [ (Test(string="abc"), ReferenceTest(string="abc")), (Test(integer=2), ReferenceTest(integer=2)), @@ -39,7 +35,12 @@ def test_oneof_serializes_similar_to_google_oneof(): assert message.to_dict() == json_format.MessageToDict(message_reference) -def test_bytes_are_the_same_for_oneof(): +def test_bytes_are_the_same_for_oneof(requires_protobuf): + from tests.outputs.google_impl_behavior_equivalence_reference.google_impl_behavior_equivalence_pb2 import ( + Foo as ReferenceFoo, + Test as ReferenceTest, + ) + message = Test(string="") message_reference = ReferenceTest(string="") @@ -65,7 +66,13 @@ def test_bytes_are_the_same_for_oneof(): @pytest.mark.parametrize("dt", (datetime.min.replace(tzinfo=timezone.utc),)) -def test_datetime_clamping(dt): # see #407 +def test_datetime_clamping(dt, requires_protobuf): # see #407 + from google.protobuf.timestamp_pb2 import Timestamp + + from tests.outputs.google_impl_behavior_equivalence_reference.google_impl_behavior_equivalence_pb2 import ( + Spam as ReferenceSpam, + ) + ts = Timestamp() ts.FromDatetime(dt) assert bytes(Spam(dt)) == ReferenceSpam(ts=ts).SerializeToString() @@ -74,7 +81,12 @@ def test_datetime_clamping(dt): # see #407 assert Spam.parse(message_bytes).ts.timestamp() == ReferenceSpam.FromString(message_bytes).ts.seconds -def test_empty_message_field(): +def test_empty_message_field(requires_protobuf): + from tests.outputs.google_impl_behavior_equivalence_reference.google_impl_behavior_equivalence_pb2 import ( + Empty as ReferenceEmpty, + Request as ReferenceRequest, + ) + message = Request() reference_message = ReferenceRequest() diff --git a/betterproto2/tests/inputs/googletypes_request/test_googletypes_request.py b/betterproto2/tests/inputs/googletypes_request/test_googletypes_request.py index 05cc905e..16986995 100644 --- a/betterproto2/tests/inputs/googletypes_request/test_googletypes_request.py +++ b/betterproto2/tests/inputs/googletypes_request/test_googletypes_request.py @@ -1,43 +1,36 @@ -from collections.abc import Callable -from datetime import ( - datetime, - timedelta, -) -from typing import ( - Any, -) +from datetime import datetime, timedelta import pytest import tests.outputs.googletypes_request.google.protobuf as protobuf -from tests.mocks import MockChannel -from tests.outputs.googletypes_request.googletypes_request import Input, TestStub - -test_cases = [ - (TestStub.send_double, protobuf.DoubleValue, 2.5), - (TestStub.send_float, protobuf.FloatValue, 2.5), - (TestStub.send_int_64, protobuf.Int64Value, -64), - (TestStub.send_u_int_64, protobuf.UInt64Value, 64), - (TestStub.send_int_32, protobuf.Int32Value, -32), - (TestStub.send_u_int_32, protobuf.UInt32Value, 32), - (TestStub.send_bool, protobuf.BoolValue, True), - (TestStub.send_string, protobuf.StringValue, "string"), - (TestStub.send_bytes, protobuf.BytesValue, bytes(0xFF)[0:4]), - (TestStub.send_datetime, protobuf.Timestamp, datetime(2038, 1, 19, 3, 14, 8)), - (TestStub.send_timedelta, protobuf.Duration, timedelta(seconds=123456)), -] +from tests.util import requires_grpclib # noqa: F401 @pytest.mark.asyncio -@pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases) -async def test_channel_receives_wrapped_type( - service_method: Callable[[TestStub, Input], Any], wrapper_class: Callable, value -): - wrapped_value = wrapper_class() - wrapped_value.value = value - channel = MockChannel(responses=[Input()]) - service = TestStub(channel) - - await service_method(service, wrapped_value) - - assert channel.requests[0]["request"] == type(wrapped_value) +async def test_channel_receives_wrapped_type(requires_grpclib): + from tests.mocks import MockChannel + from tests.outputs.googletypes_request.googletypes_request import Input, TestStub + + test_cases = [ + (TestStub.send_double, protobuf.DoubleValue, 2.5), + (TestStub.send_float, protobuf.FloatValue, 2.5), + (TestStub.send_int_64, protobuf.Int64Value, -64), + (TestStub.send_u_int_64, protobuf.UInt64Value, 64), + (TestStub.send_int_32, protobuf.Int32Value, -32), + (TestStub.send_u_int_32, protobuf.UInt32Value, 32), + (TestStub.send_bool, protobuf.BoolValue, True), + (TestStub.send_string, protobuf.StringValue, "string"), + (TestStub.send_bytes, protobuf.BytesValue, bytes(0xFF)[0:4]), + (TestStub.send_datetime, protobuf.Timestamp, datetime(2038, 1, 19, 3, 14, 8)), + (TestStub.send_timedelta, protobuf.Duration, timedelta(seconds=123456)), + ] + + for service_method, wrapper_class, value in test_cases: + wrapped_value = wrapper_class() + wrapped_value.value = value + channel = MockChannel(responses=[Input()]) + service = TestStub(channel) + + await service_method(service, wrapped_value) + + assert channel.requests[0]["request"] == type(wrapped_value) diff --git a/betterproto2/tests/inputs/googletypes_response/test_googletypes_response.py b/betterproto2/tests/inputs/googletypes_response/test_googletypes_response.py index 14f49c0a..8a826c6b 100644 --- a/betterproto2/tests/inputs/googletypes_response/test_googletypes_response.py +++ b/betterproto2/tests/inputs/googletypes_response/test_googletypes_response.py @@ -1,59 +1,65 @@ from collections.abc import Callable -from typing import ( - Any, -) +from typing import TYPE_CHECKING, Any import pytest -import tests.outputs.googletypes_response.google.protobuf as protobuf -from tests.mocks import MockChannel -from tests.outputs.googletypes_response.googletypes_response import Input, TestStub +from tests.util import requires_grpclib # noqa: F401 -test_cases = [ - (TestStub.get_double, protobuf.DoubleValue, 2.5), - (TestStub.get_float, protobuf.FloatValue, 2.5), - (TestStub.get_int_64, protobuf.Int64Value, -64), - (TestStub.get_u_int_64, protobuf.UInt64Value, 64), - (TestStub.get_int_32, protobuf.Int32Value, -32), - (TestStub.get_u_int_32, protobuf.UInt32Value, 32), - (TestStub.get_bool, protobuf.BoolValue, True), - (TestStub.get_string, protobuf.StringValue, "string"), - (TestStub.get_bytes, protobuf.BytesValue, bytes(0xFF)[0:4]), -] +if TYPE_CHECKING: + from tests.outputs.googletypes_response.googletypes_response import Input, TestStub + + +def get_test_cases() -> list[tuple[Callable[["TestStub", "Input"], Any], Callable, Any]]: + import tests.outputs.googletypes_response.google.protobuf as protobuf + from tests.outputs.googletypes_response.googletypes_response import TestStub + + return [ + (TestStub.get_double, protobuf.DoubleValue, 2.5), + (TestStub.get_float, protobuf.FloatValue, 2.5), + (TestStub.get_int_64, protobuf.Int64Value, -64), + (TestStub.get_u_int_64, protobuf.UInt64Value, 64), + (TestStub.get_int_32, protobuf.Int32Value, -32), + (TestStub.get_u_int_32, protobuf.UInt32Value, 32), + (TestStub.get_bool, protobuf.BoolValue, True), + (TestStub.get_string, protobuf.StringValue, "string"), + (TestStub.get_bytes, protobuf.BytesValue, bytes(0xFF)[0:4]), + ] @pytest.mark.asyncio -@pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases) -async def test_channel_receives_wrapped_type( - service_method: Callable[[TestStub, Input], Any], wrapper_class: Callable, value -): - wrapped_value = wrapper_class() - wrapped_value.value = value - channel = MockChannel(responses=[wrapped_value]) - service = TestStub(channel) - method_param = Input() +async def test_channel_receives_wrapped_type(requires_grpclib): + from tests.mocks import MockChannel + from tests.outputs.googletypes_response.googletypes_response import Input, TestStub - await service_method(service, method_param) + for service_method, wrapper_class, value in get_test_cases(): + wrapped_value = wrapper_class() + wrapped_value.value = value + channel = MockChannel(responses=[wrapped_value]) + service = TestStub(channel) + method_param = Input() - assert channel.requests[0]["response_type"] != type(value) | None - assert channel.requests[0]["response_type"] == type(wrapped_value) + await service_method(service, method_param) + + assert channel.requests[0]["response_type"] != type(value) | None + assert channel.requests[0]["response_type"] == type(wrapped_value) @pytest.mark.asyncio @pytest.mark.xfail -@pytest.mark.parametrize(["service_method", "wrapper_class", "value"], test_cases) -async def test_service_unwraps_response( - service_method: Callable[[TestStub, Input], Any], wrapper_class: Callable, value -): +async def test_service_unwraps_response(requires_grpclib): """ grpclib does not unwrap wrapper values returned by services """ - wrapped_value = wrapper_class() - wrapped_value.value = value - service = TestStub(MockChannel(responses=[wrapped_value])) - method_param = Input() + from tests.mocks import MockChannel + from tests.outputs.googletypes_response.googletypes_response import Input, TestStub + + for service_method, wrapper_class, value in get_test_cases(): + wrapped_value = wrapper_class() + wrapped_value.value = value + service = TestStub(MockChannel(responses=[wrapped_value])) + method_param = Input() - response_value = await service_method(service, method_param) + response_value = await service_method(service, method_param) - assert response_value == value - assert type(response_value) == type(value) + assert response_value == value + assert type(response_value) == type(value) diff --git a/betterproto2/tests/inputs/googletypes_response_embedded/test_googletypes_response_embedded.py b/betterproto2/tests/inputs/googletypes_response_embedded/test_googletypes_response_embedded.py index 74b5cda6..3ca3e677 100644 --- a/betterproto2/tests/inputs/googletypes_response_embedded/test_googletypes_response_embedded.py +++ b/betterproto2/tests/inputs/googletypes_response_embedded/test_googletypes_response_embedded.py @@ -1,19 +1,17 @@ import pytest -from tests.mocks import MockChannel -from tests.outputs.googletypes_response_embedded.googletypes_response_embedded import ( - Input, - Output, - TestStub, -) +from tests.util import requires_grpclib # noqa: F401 @pytest.mark.asyncio -async def test_service_passes_through_unwrapped_values_embedded_in_response(): +async def test_service_passes_through_unwrapped_values_embedded_in_response(requires_grpclib): """ We do not not need to implement value unwrapping for embedded well-known types, as this is already handled by grpclib. This test merely shows that this is the case. """ + from tests.mocks import MockChannel + from tests.outputs.googletypes_response_embedded.googletypes_response_embedded import Input, Output, TestStub + output = Output( double_value=10.0, float_value=12.0, diff --git a/betterproto2/tests/inputs/import_service_input_message/test_import_service_input_message.py b/betterproto2/tests/inputs/import_service_input_message/test_import_service_input_message.py index 5c7cd6cc..df7bf673 100644 --- a/betterproto2/tests/inputs/import_service_input_message/test_import_service_input_message.py +++ b/betterproto2/tests/inputs/import_service_input_message/test_import_service_input_message.py @@ -1,19 +1,17 @@ import pytest -from tests.mocks import MockChannel -from tests.outputs.import_service_input_message.import_service_input_message import ( - NestedRequestMessage, - RequestMessage, - RequestResponse, - TestStub, -) -from tests.outputs.import_service_input_message.import_service_input_message.child import ( - ChildRequestMessage, -) +from tests.util import requires_grpclib # noqa: F401 @pytest.mark.asyncio -async def test_service_correctly_imports_reference_message(): +async def test_service_correctly_imports_reference_message(requires_grpclib): + from tests.mocks import MockChannel + from tests.outputs.import_service_input_message.import_service_input_message import ( + RequestMessage, + RequestResponse, + TestStub, + ) + mock_response = RequestResponse(value=10) service = TestStub(MockChannel([mock_response])) response = await service.do_thing(RequestMessage(1)) @@ -21,7 +19,16 @@ async def test_service_correctly_imports_reference_message(): @pytest.mark.asyncio -async def test_service_correctly_imports_reference_message_from_child_package(): +async def test_service_correctly_imports_reference_message_from_child_package(requires_grpclib): + from tests.mocks import MockChannel + from tests.outputs.import_service_input_message.import_service_input_message import ( + RequestResponse, + TestStub, + ) + from tests.outputs.import_service_input_message.import_service_input_message.child import ( + ChildRequestMessage, + ) + mock_response = RequestResponse(value=10) service = TestStub(MockChannel([mock_response])) response = await service.do_thing_2(ChildRequestMessage(1)) @@ -29,7 +36,14 @@ async def test_service_correctly_imports_reference_message_from_child_package(): @pytest.mark.asyncio -async def test_service_correctly_imports_nested_reference(): +async def test_service_correctly_imports_nested_reference(requires_grpclib): + from tests.mocks import MockChannel + from tests.outputs.import_service_input_message.import_service_input_message import ( + NestedRequestMessage, + RequestResponse, + TestStub, + ) + mock_response = RequestResponse(value=10) service = TestStub(MockChannel([mock_response])) response = await service.do_thing_3(NestedRequestMessage(1)) diff --git a/betterproto2/tests/inputs/invalid_field/test_invalid_field.py b/betterproto2/tests/inputs/invalid_field/test_invalid_field.py index aefead03..a7ec6b83 100644 --- a/betterproto2/tests/inputs/invalid_field/test_invalid_field.py +++ b/betterproto2/tests/inputs/invalid_field/test_invalid_field.py @@ -1,5 +1,7 @@ import pytest +from tests.util import requires_pydantic # noqa: F401 + def test_invalid_field(): from tests.outputs.invalid_field.invalid_field import Test @@ -8,7 +10,7 @@ def test_invalid_field(): Test(unknown_field=12) -def test_invalid_field_pydantic(): +def test_invalid_field_pydantic(requires_pydantic): from pydantic import ValidationError from tests.outputs.invalid_field_pydantic.invalid_field import Test diff --git a/betterproto2/tests/inputs/oneof/test_oneof.py b/betterproto2/tests/inputs/oneof/test_oneof.py index a703cf62..71944506 100644 --- a/betterproto2/tests/inputs/oneof/test_oneof.py +++ b/betterproto2/tests/inputs/oneof/test_oneof.py @@ -1,5 +1,5 @@ import betterproto2 -from tests.util import get_test_case_json_data +from tests.util import get_test_case_json_data, requires_pydantic # noqa: F401 def test_which_count(): @@ -16,7 +16,7 @@ def test_which_name(): assert betterproto2.which_one_of(message, "foo") == ("pitier", "Mr. T") -def test_which_count_pyd(): +def test_which_count_pyd(requires_pydantic): from tests.outputs.oneof_pydantic.oneof import Test message = Test(pitier="Mr. T", just_a_regular_field=2, bar_name="a_bar") diff --git a/betterproto2/tests/inputs/rpc_empty_input_message/test_rpc_empty_input_message.py b/betterproto2/tests/inputs/rpc_empty_input_message/test_rpc_empty_input_message.py index 29a90a21..39a68832 100644 --- a/betterproto2/tests/inputs/rpc_empty_input_message/test_rpc_empty_input_message.py +++ b/betterproto2/tests/inputs/rpc_empty_input_message/test_rpc_empty_input_message.py @@ -1,9 +1,12 @@ import pytest -from grpclib.testing import ChannelFor + +from tests.util import requires_grpclib # noqa: F401 @pytest.mark.asyncio -async def test_rpc_input_message(): +async def test_rpc_input_message(requires_grpclib): + from grpclib.testing import ChannelFor + from tests.outputs.rpc_empty_input_message.rpc_empty_input_message import ( Response, ServiceBase, diff --git a/betterproto2/tests/inputs/service_uppercase/test_service.py b/betterproto2/tests/inputs/service_uppercase/test_service.py index cbf50f4c..e254f3f6 100644 --- a/betterproto2/tests/inputs/service_uppercase/test_service.py +++ b/betterproto2/tests/inputs/service_uppercase/test_service.py @@ -1,8 +1,10 @@ import inspect -from tests.outputs.service_uppercase.service_uppercase import TestStub +from tests.util import requires_grpclib # noqa: F401 -def test_parameters(): +def test_parameters(requires_grpclib): + from tests.outputs.service_uppercase.service_uppercase import TestStub + sig = inspect.signature(TestStub.do_thing) assert len(sig.parameters) == 5, "Expected 5 parameters" diff --git a/betterproto2/tests/test_all_definition.py b/betterproto2/tests/test_all_definition.py index ce22ae7c..4cabbbf1 100644 --- a/betterproto2/tests/test_all_definition.py +++ b/betterproto2/tests/test_all_definition.py @@ -1,4 +1,7 @@ -def test_all_definition(): +from tests.util import requires_grpcio, requires_grpclib # noqa: F401 + + +def test_all_definition(requires_grpclib, requires_grpcio): """ Check that a compiled module defines __all__ with the right value. diff --git a/betterproto2/tests/test_deprecated.py b/betterproto2/tests/test_deprecated.py index f77da7ee..311ecb1b 100644 --- a/betterproto2/tests/test_deprecated.py +++ b/betterproto2/tests/test_deprecated.py @@ -2,24 +2,21 @@ import pytest -from tests.mocks import MockChannel -from tests.outputs.deprecated.deprecated import ( - Empty, - Message, - Test, - TestNested, - TestServiceStub, -) +from tests.util import requires_grpclib # noqa: F401 @pytest.fixture def message(): + from tests.outputs.deprecated.deprecated import Message + with warnings.catch_warnings(): warnings.filterwarnings("ignore", category=DeprecationWarning) return Message(value="hello") -def test_deprecated_message(): +def test_deprecated_message(requires_grpclib): + from tests.outputs.deprecated.deprecated import Message + with pytest.warns(DeprecationWarning) as record: Message(value="hello") @@ -27,7 +24,9 @@ def test_deprecated_message(): assert str(record[0].message) == f"{Message.__name__} is deprecated" -def test_deprecated_nested_message_field(): +def test_deprecated_nested_message_field(requires_grpclib): + from tests.outputs.deprecated.deprecated import TestNested + with pytest.warns(DeprecationWarning) as record: TestNested(nested_value="hello") @@ -35,7 +34,9 @@ def test_deprecated_nested_message_field(): assert str(record[0].message) == f"TestNested.nested_value is deprecated" -def test_message_with_deprecated_field(message): +def test_message_with_deprecated_field(requires_grpclib, message): + from tests.outputs.deprecated.deprecated import Test + with pytest.warns(DeprecationWarning) as record: Test(message=message, value=10) @@ -43,20 +44,27 @@ def test_message_with_deprecated_field(message): assert str(record[0].message) == f"{Test.__name__}.message is deprecated" -def test_message_with_deprecated_field_not_set(message): +def test_message_with_deprecated_field_not_set(requires_grpclib, message): + from tests.outputs.deprecated.deprecated import Test + with warnings.catch_warnings(): warnings.simplefilter("error") Test(value=10) -def test_message_with_deprecated_field_not_set_default(message): +def test_message_with_deprecated_field_not_set_default(requires_grpclib, message): + from tests.outputs.deprecated.deprecated import Test + with warnings.catch_warnings(): warnings.simplefilter("error") _ = Test(value=10).message @pytest.mark.asyncio -async def test_service_with_deprecated_method(): +async def test_service_with_deprecated_method(requires_grpclib): + from tests.mocks import MockChannel + from tests.outputs.deprecated.deprecated import Empty, TestServiceStub + stub = TestServiceStub(MockChannel([Empty(), Empty()])) with pytest.warns(DeprecationWarning) as record: diff --git a/betterproto2/tests/test_documentation.py b/betterproto2/tests/test_documentation.py index 86332722..e051bb00 100644 --- a/betterproto2/tests/test_documentation.py +++ b/betterproto2/tests/test_documentation.py @@ -1,6 +1,8 @@ import ast import inspect +from tests.util import requires_grpclib # noqa: F401 + def check(generated_doc: str, type: str) -> None: assert f"Documentation of {type} 1" in generated_doc @@ -10,7 +12,7 @@ def check(generated_doc: str, type: str) -> None: assert f"Documentation of {type} 3" in generated_doc -def test_documentation() -> None: +def test_documentation(requires_grpclib) -> None: from .outputs.documentation.documentation import ( Enum, ServiceBase, @@ -38,7 +40,7 @@ def test_documentation() -> None: check(ServiceStub.get.__doc__, "method") -def test_escaping() -> None: +def test_escaping(requires_grpclib) -> None: from .outputs.documentation.documentation import ComplexDocumentation ComplexDocumentation.__doc__ == """ diff --git a/betterproto2/tests/test_features.py b/betterproto2/tests/test_features.py index bbb37844..11058cd7 100644 --- a/betterproto2/tests/test_features.py +++ b/betterproto2/tests/test_features.py @@ -1,19 +1,13 @@ import json -from datetime import ( - datetime, - timedelta, - timezone, -) -from inspect import ( - Parameter, - signature, -) +from datetime import datetime, timedelta, timezone +from inspect import Parameter, signature from unittest.mock import ANY import pytest import betterproto2 from betterproto2 import OutputFormat +from tests.util import requires_grpcio, requires_grpclib # noqa: F401 def test_class_init(): @@ -416,7 +410,7 @@ def test_iso_datetime_list(): assert all([isinstance(item, datetime) for item in msg.timestamps]) -def test_service_argument__expected_parameter(): +def test_service_argument__expected_parameter(requires_grpclib, requires_grpcio): from tests.outputs.service.service import TestStub sig = signature(TestStub.do_thing) diff --git a/betterproto2/tests/test_inputs.py b/betterproto2/tests/test_inputs.py index 787319d8..e56cd04f 100644 --- a/betterproto2/tests/test_inputs.py +++ b/betterproto2/tests/test_inputs.py @@ -11,13 +11,12 @@ import pytest import betterproto2 +from tests.util import requires_grpcio, requires_grpclib, requires_protobuf, requires_pydantic # noqa: F401 # Force pure-python implementation instead of C++, otherwise imports # break things because we can't properly reset the symbol database. os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" -from google.protobuf.json_format import Parse - @dataclass class TestCase: @@ -190,7 +189,7 @@ def reset_sys_path(): @pytest.mark.parametrize("test_case", TEST_CASES, ids=lambda x: x.plugin_package) -def test_message_json(test_case: TestCase) -> None: +def test_message_json(test_case: TestCase, requires_pydantic, requires_grpcio, requires_grpclib) -> None: if test_case.xfail: pytest.xfail(f"Test case {test_case.plugin_package} is expected to fail.") @@ -209,7 +208,11 @@ def test_message_json(test_case: TestCase) -> None: @pytest.mark.parametrize("test_case", TEST_CASES, ids=lambda x: x.plugin_package) -def test_binary_compatibility(test_case: TestCase, reset_sys_path) -> None: +def test_binary_compatibility( + test_case: TestCase, reset_sys_path, requires_grpcio, requires_protobuf, requires_grpclib +) -> None: + from google.protobuf.json_format import Parse + if test_case.xfail: pytest.xfail(f"Test case {test_case.plugin_package} is expected to fail.") diff --git a/betterproto2/tests/test_manual_validation.py b/betterproto2/tests/test_manual_validation.py index 342ec685..552c31b0 100644 --- a/betterproto2/tests/test_manual_validation.py +++ b/betterproto2/tests/test_manual_validation.py @@ -1,8 +1,11 @@ -import pydantic import pytest +from tests.util import requires_pydantic # noqa: F401 + + +def test_manual_validation(requires_pydantic): + import pydantic -def test_manual_validation(): from tests.outputs.manual_validation_pydantic.manual_validation import Msg msg = Msg() diff --git a/betterproto2/tests/test_map.py b/betterproto2/tests/test_map.py new file mode 100644 index 00000000..7c7d6042 --- /dev/null +++ b/betterproto2/tests/test_map.py @@ -0,0 +1,12 @@ +# from tests.output_betterproto.map import Enum, Test + + +def test_map(): + # msg = Test(counts={"a": 1, "b": 2}) + # assert msg == Test.parse(bytes(msg)) + # assert msg == Test.from_dict(msg.to_dict()) + + pass + # msg = Test(map_enum={1: Enum.ONE, 2: Enum.TWO}) + # assert msg == Test.parse(bytes(msg)) + # assert msg == Test.from_dict(msg.to_dict()) diff --git a/betterproto2/tests/test_sync_client.py b/betterproto2/tests/test_sync_client.py index ec0611ba..ed3ab78f 100644 --- a/betterproto2/tests/test_sync_client.py +++ b/betterproto2/tests/test_sync_client.py @@ -2,34 +2,36 @@ import threading from collections.abc import AsyncIterator -import grpc import pytest -from grpclib.server import Server -from tests.outputs.simple_service.simple_service import Request, Response, SimpleServiceBase, SimpleServiceSyncStub +from tests.util import requires_grpcio, requires_grpclib # noqa: F401 -class SimpleService(SimpleServiceBase): - async def get_unary_unary(self, message: "Request") -> "Response": - return Response(message=f"Hello {message.value}") +@pytest.mark.asyncio +async def test_sync_client(requires_grpcio, requires_grpclib): + import grpc + from grpclib.server import Server - async def get_unary_stream(self, message: "Request") -> "AsyncIterator[Response]": - for i in range(5): - yield Response(message=f"Hello {message.value} {i}") + from tests.outputs.simple_service.simple_service import Request, Response, SimpleServiceBase, SimpleServiceSyncStub - async def get_stream_unary(self, messages: "AsyncIterator[Request]") -> "Response": - s = 0 - async for m in messages: - s += m.value - return Response(message=f"Hello {s}") + class SimpleService(SimpleServiceBase): + async def get_unary_unary(self, message: "Request") -> "Response": + return Response(message=f"Hello {message.value}") - async def get_stream_stream(self, messages: "AsyncIterator[Request]") -> "AsyncIterator[Response]": - async for message in messages: - yield Response(message=f"Hello {message.value}") + async def get_unary_stream(self, message: "Request") -> "AsyncIterator[Response]": + for i in range(5): + yield Response(message=f"Hello {message.value} {i}") + async def get_stream_unary(self, messages: "AsyncIterator[Request]") -> "Response": + s = 0 + async for m in messages: + s += m.value + return Response(message=f"Hello {s}") + + async def get_stream_stream(self, messages: "AsyncIterator[Request]") -> "AsyncIterator[Response]": + async for message in messages: + yield Response(message=f"Hello {message.value}") -@pytest.mark.asyncio -async def test_sync_client(): start_server_event = threading.Event() close_server_event = asyncio.Event() diff --git a/betterproto2/tests/test_validation.py b/betterproto2/tests/test_validation.py index fa267013..f3e49411 100644 --- a/betterproto2/tests/test_validation.py +++ b/betterproto2/tests/test_validation.py @@ -1,8 +1,11 @@ -import pydantic import pytest +from tests.util import requires_pydantic # noqa: F401 + + +def test_int32_validation(requires_pydantic): + import pydantic -def test_int32_validation(): from .outputs.validation_pydantic.validation import Message # Test valid values @@ -17,7 +20,9 @@ def test_int32_validation(): Message(int32_value=-(2**31) - 1) -def test_int64_validation(): +def test_int64_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -32,7 +37,9 @@ def test_int64_validation(): Message(int64_value=-(2**63) - 1) -def test_uint32_validation(): +def test_uint32_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -46,7 +53,9 @@ def test_uint32_validation(): Message(uint32_value=2**32) -def test_uint64_validation(): +def test_uint64_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -60,7 +69,9 @@ def test_uint64_validation(): Message(uint64_value=2**64) -def test_sint32_validation(): +def test_sint32_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -75,7 +86,9 @@ def test_sint32_validation(): Message(sint32_value=-(2**31) - 1) -def test_sint64_validation(): +def test_sint64_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -90,7 +103,9 @@ def test_sint64_validation(): Message(sint64_value=-(2**63) - 1) -def test_fixed32_validation(): +def test_fixed32_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -104,7 +119,9 @@ def test_fixed32_validation(): Message(fixed32_value=2**32) -def test_fixed64_validation(): +def test_fixed64_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -118,7 +135,9 @@ def test_fixed64_validation(): Message(fixed64_value=2**64) -def test_sfixed32_validation(): +def test_sfixed32_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -133,7 +152,9 @@ def test_sfixed32_validation(): Message(sfixed32_value=-(2**31) - 1) -def test_sfixed64_validation(): +def test_sfixed64_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -148,7 +169,9 @@ def test_sfixed64_validation(): Message(sfixed64_value=-(2**63) - 1) -def test_float_validation(): +def test_float_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values @@ -160,7 +183,9 @@ def test_float_validation(): Message(float_value=3.5e38) -def test_string_validation(): +def test_string_validation(requires_pydantic): + import pydantic + from .outputs.validation_pydantic.validation import Message # Test valid values diff --git a/betterproto2/tests/util.py b/betterproto2/tests/util.py index c4ad0c54..0f62f832 100644 --- a/betterproto2/tests/util.py +++ b/betterproto2/tests/util.py @@ -5,6 +5,8 @@ from pathlib import Path from types import ModuleType +import pytest + os.environ["PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION"] = "python" root_path = Path(__file__).resolve().parent @@ -75,3 +77,35 @@ def find_module(module: ModuleType, predicate: Callable[[ModuleType], bool]) -> return sub_module return None + + +@pytest.fixture +def requires_pydantic(): + try: + import pydantic # noqa: F401 + except ImportError: + pytest.skip("pydantic is not installed") + + +@pytest.fixture +def requires_grpclib(): + try: + import grpclib # noqa: F401 + except ImportError: + pytest.skip("grpclib is not installed") + + +@pytest.fixture +def requires_grpcio(): + try: + import grpc # noqa: F401 + except ImportError: + pytest.skip("grpcio is not installed") + + +@pytest.fixture +def requires_protobuf(): + try: + import google.protobuf # noqa: F401 + except ImportError: + pytest.skip("protobuf is not installed") diff --git a/betterproto2_compiler/pyproject.toml b/betterproto2_compiler/pyproject.toml index 0e7aec87..0372ace8 100644 --- a/betterproto2_compiler/pyproject.toml +++ b/betterproto2_compiler/pyproject.toml @@ -15,16 +15,16 @@ keywords = [ requires-python = ">=3.10,<4.0" dependencies = [ # TODO use the version from the current repo? - "betterproto2[grpclib]>=0.8.0,<0.9", - # "betterproto2[grpclib]", + # "betterproto2>=0.8.0,<0.9", + "betterproto2", "ruff~=0.9.3", "jinja2>=3.0.3", "typing-extensions>=4.7.1,<5", "strenum>=0.4.15,<0.5 ; python_version == '3.10'", ] -# [tool.uv.sources] -# "betterproto2" = { path = "../betterproto2" } +[tool.uv.sources] +betterproto2 = { path = "../betterproto2" } [project.urls] Documentation = "https://betterproto.github.io/python-betterproto2/" diff --git a/betterproto2_compiler/src/betterproto2_compiler/templates/header.py.j2 b/betterproto2_compiler/src/betterproto2_compiler/templates/header.py.j2 index aa348bf7..4cf7262c 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/templates/header.py.j2 +++ b/betterproto2_compiler/src/betterproto2_compiler/templates/header.py.j2 @@ -29,7 +29,7 @@ from dataclasses import dataclass {% endif %} import betterproto2 -from betterproto2.grpc.grpclib_server import ServiceBase +from betterproto2 import grpclib as betterproto2_grpclib import grpc import grpclib from google.protobuf.descriptor import Descriptor, EnumDescriptor diff --git a/betterproto2_compiler/src/betterproto2_compiler/templates/service_stub_async.py.j2 b/betterproto2_compiler/src/betterproto2_compiler/templates/service_stub_async.py.j2 index 191d6a94..bf4c50c0 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/templates/service_stub_async.py.j2 +++ b/betterproto2_compiler/src/betterproto2_compiler/templates/service_stub_async.py.j2 @@ -2,7 +2,7 @@ {# Class definition #} {% block class_name %}{{ service.py_name }}{% if output_file.settings.client_generation.is_async_prefixed %}Async{% endif %}Stub{% endblock %} -{% block inherit_from %}betterproto2.ServiceStub{% endblock %} +{% block inherit_from %}betterproto2_grpclib.ServiceStub{% endblock %} {# Methods definition #} {% block method_definition %} diff --git a/betterproto2_compiler/src/betterproto2_compiler/templates/template.py.j2 b/betterproto2_compiler/src/betterproto2_compiler/templates/template.py.j2 index e2eb1e40..b9761766 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/templates/template.py.j2 +++ b/betterproto2_compiler/src/betterproto2_compiler/templates/template.py.j2 @@ -139,7 +139,7 @@ default_message_pool.register_message("{{ output_file.package }}", "{{ message.p {% if output_file.settings.server_generation == "async" %} {% for _, service in output_file.services|dictsort(by="key") %} -class {{ (service.py_name + "Base") | add_to_all }}(ServiceBase): +class {{ (service.py_name + "Base") | add_to_all }}(betterproto2_grpclib.ServiceBase): {% if service.comment %} """ {{ service.comment | indent(4) }} diff --git a/betterproto2_compiler/tests/generate.py b/betterproto2_compiler/tests/generate.py index 54a2e87c..b0ebd660 100644 --- a/betterproto2_compiler/tests/generate.py +++ b/betterproto2_compiler/tests/generate.py @@ -17,6 +17,7 @@ async def generate_test( reference: bool = False, pydantic: bool = False, descriptors: bool = False, + client_generation: str = "async_sync", ): await semaphore.acquire() @@ -41,6 +42,7 @@ async def generate_test( reference=reference, pydantic_dataclasses=pydantic, google_protobuf_descriptors=descriptors, + client_generation=client_generation, ) if options: @@ -74,14 +76,14 @@ async def main_async(): generate_test("casing", semaphore), generate_test("compiler_lib", semaphore), generate_test("deprecated", semaphore, reference=True), - generate_test("deprecated", semaphore), - generate_test("documentation", semaphore), + generate_test("deprecated", semaphore, client_generation="async"), + generate_test("documentation", semaphore, client_generation="async"), generate_test("double", semaphore, reference=True), generate_test("double", semaphore), generate_test("encoding_decoding", semaphore), generate_test("enum", semaphore, reference=True), generate_test("enum", semaphore), - generate_test("example_service", semaphore), + generate_test("example_service", semaphore, client_generation="async"), generate_test("features", semaphore), generate_test("field_name_identical_to_type", semaphore, reference=True), generate_test("field_name_identical_to_type", semaphore), @@ -92,20 +94,20 @@ async def main_async(): generate_test("google_impl_behavior_equivalence", semaphore, reference=True), generate_test("google_impl_behavior_equivalence", semaphore), generate_test("google", semaphore), - generate_test("googletypes_request", semaphore), - generate_test("googletypes_response_embedded", semaphore), - generate_test("googletypes_response", semaphore), + generate_test("googletypes_request", semaphore, client_generation="async"), + generate_test("googletypes_response_embedded", semaphore, client_generation="async"), + generate_test("googletypes_response", semaphore, client_generation="async"), generate_test("googletypes_struct", semaphore, reference=True), generate_test("googletypes_struct", semaphore), generate_test("googletypes_value", semaphore, reference=True), generate_test("googletypes_value", semaphore), generate_test("googletypes", semaphore, reference=True), generate_test("googletypes", semaphore), - generate_test("grpclib_reflection", semaphore, descriptors=True), - generate_test("grpclib_reflection", semaphore), + generate_test("grpclib_reflection", semaphore, descriptors=True, client_generation="async"), + generate_test("grpclib_reflection", semaphore, client_generation="async"), generate_test("import_cousin_package_same_name", semaphore, descriptors=True), generate_test("import_cousin_package_same_name", semaphore), - generate_test("import_service_input_message", semaphore), + generate_test("import_service_input_message", semaphore, client_generation="async"), generate_test("int32", semaphore, reference=True), generate_test("int32", semaphore), generate_test("invalid_field", semaphore, pydantic=True), @@ -152,8 +154,8 @@ async def main_async(): generate_test("repeatedmessage", semaphore), generate_test("repeatedpacked", semaphore, reference=True), generate_test("repeatedpacked", semaphore), - generate_test("rpc_empty_input_message", semaphore), - generate_test("service_uppercase", semaphore), + generate_test("rpc_empty_input_message", semaphore, client_generation="async"), + generate_test("service_uppercase", semaphore, client_generation="async"), generate_test("service", semaphore), generate_test("signed", semaphore, reference=True), generate_test("signed", semaphore), diff --git a/betterproto2_compiler/tests/util.py b/betterproto2_compiler/tests/util.py index b837bbd5..e320ea6b 100644 --- a/betterproto2_compiler/tests/util.py +++ b/betterproto2_compiler/tests/util.py @@ -12,6 +12,7 @@ async def protoc( reference: bool = False, pydantic_dataclasses: bool = False, google_protobuf_descriptors: bool = False, + client_generation: str = "async_sync", ): resolved_path: Path = Path(path).resolve() resolved_output_dir: Path = Path(output_dir).resolve() @@ -28,7 +29,7 @@ async def protoc( if not reference: command.insert(3, "--python_betterproto2_opt=server_generation=async") - command.insert(3, "--python_betterproto2_opt=client_generation=async_sync") + command.insert(3, f"--python_betterproto2_opt=client_generation={client_generation}") if pydantic_dataclasses: command.insert(3, "--python_betterproto2_opt=pydantic_dataclasses") diff --git a/betterproto2_compiler/uv.lock b/betterproto2_compiler/uv.lock index 83014804..8080938c 100644 --- a/betterproto2_compiler/uv.lock +++ b/betterproto2_compiler/uv.lock @@ -27,19 +27,44 @@ wheels = [ [[package]] name = "betterproto2" version = "0.8.0" -source = { registry = "https://pypi.org/simple" } +source = { directory = "../betterproto2" } dependencies = [ { name = "python-dateutil" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/04/8d/99f8ca87dbac19fba70574a420b091fbc8324b114cda283ed3aaa37ad820/betterproto2-0.8.0.tar.gz", hash = "sha256:75f8a46d056e22bef18e61ec62c1b071091f9df64a081a2c305f6a8011c8e8f5", size = 143140, upload-time = "2025-08-31T20:03:23.962Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/62/2f/d98f986d59c35e2c14f3b37157badf9b7df57c1c680ccf7ae7800d21ec5b/betterproto2-0.8.0-py3-none-any.whl", hash = "sha256:b79dc910453ee5153c32d74c896e423234aaa2bb5c0bd73f020a7db63e72ee24", size = 19469, upload-time = "2025-08-31T20:03:22.348Z" }, -] -[package.optional-dependencies] -grpclib = [ - { name = "grpclib" }, +[package.metadata] +requires-dist = [ + { name = "grpcio", marker = "extra == 'all'", specifier = ">=1.72.1" }, + { name = "grpcio", marker = "extra == 'grpcio'", specifier = ">=1.72.1" }, + { name = "grpclib", marker = "extra == 'all'", specifier = ">=0.4.8" }, + { name = "grpclib", marker = "extra == 'grpclib'", specifier = ">=0.4.8" }, + { name = "protobuf", marker = "extra == 'all'", specifier = ">=5.29.3" }, + { name = "protobuf", marker = "extra == 'protobuf'", specifier = ">=5.29.3" }, + { name = "pydantic", marker = "extra == 'all'", specifier = ">=2.11.5" }, + { name = "pydantic", marker = "extra == 'pydantic'", specifier = ">=2.11.5" }, + { name = "python-dateutil", specifier = ">=2.9.0.post0" }, + { name = "typing-extensions", specifier = ">=4.14.0" }, +] +provides-extras = ["grpcio", "grpclib", "pydantic", "protobuf", "all"] + +[package.metadata.requires-dev] +dev = [ + { name = "ipykernel", specifier = ">=6.29.5" }, + { name = "mkdocs-material", specifier = ">=9.6.14" }, + { name = "mkdocstrings", extras = ["python"], specifier = ">=0.29.1" }, + { name = "mypy", specifier = ">=1.16.0" }, + { name = "pre-commit", specifier = ">=4.2.0" }, + { name = "pyright", specifier = ">=1.1.401" }, + { name = "ruff", specifier = "==0.9.3" }, +] +test = [ + { name = "cachelib", specifier = ">=0.13.0" }, + { name = "poethepoet", specifier = ">=0.34.0" }, + { name = "pytest", specifier = ">=8.4.0" }, + { name = "pytest-asyncio", specifier = ">=1.0.0" }, + { name = "pytest-cov", specifier = ">=6.1.1" }, + { name = "pytest-mock", specifier = ">=3.14.1" }, ] [[package]] @@ -47,7 +72,7 @@ name = "betterproto2-compiler" version = "0.8.0" source = { editable = "." } dependencies = [ - { name = "betterproto2", extra = ["grpclib"] }, + { name = "betterproto2" }, { name = "jinja2" }, { name = "ruff" }, { name = "strenum", marker = "python_full_version < '3.11'" }, @@ -69,7 +94,7 @@ test = [ [package.metadata] requires-dist = [ - { name = "betterproto2", extras = ["grpclib"], specifier = ">=0.8.0,<0.9" }, + { name = "betterproto2", directory = "../betterproto2" }, { name = "jinja2", specifier = ">=3.0.3" }, { name = "ruff", specifier = "~=0.9.3" }, { name = "strenum", marker = "python_full_version == '3.10.*'", specifier = ">=0.4.15,<0.5" }, @@ -350,50 +375,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/bb/14/ab131a39187bfea950280b2277a82d2033469fe8c86f73b10b19f53cc5ca/grpcio_tools-1.71.0-cp313-cp313-win_amd64.whl", hash = "sha256:ffff9bc5eacb34dd26b487194f7d44a3e64e752fc2cf049d798021bf25053b87", size = 1119649, upload-time = "2025-03-10T19:28:23.679Z" }, ] -[[package]] -name = "grpclib" -version = "0.4.8" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "h2" }, - { name = "multidict" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/19/75/0f0d3524b38b35e5cd07334b754aa9bd0570140ad982131b04ebfa3b0374/grpclib-0.4.8.tar.gz", hash = "sha256:d8823763780ef94fed8b2c562f7485cf0bbee15fc7d065a640673667f7719c9a", size = 62793, upload-time = "2025-05-04T16:27:30.051Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/03/8b/ad381ec1b8195fa4a9a693cb8087e031b99530c0d6b8ad036dcb99e144c4/grpclib-0.4.8-py3-none-any.whl", hash = "sha256:a5047733a7acc1c1cee6abf3c841c7c6fab67d2844a45a853b113fa2e6cd2654", size = 76311, upload-time = "2025-05-04T16:27:22.818Z" }, -] - -[[package]] -name = "h2" -version = "4.2.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "hpack" }, - { name = "hyperframe" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/1b/38/d7f80fd13e6582fb8e0df8c9a653dcc02b03ca34f4d72f34869298c5baf8/h2-4.2.0.tar.gz", hash = "sha256:c8a52129695e88b1a0578d8d2cc6842bbd79128ac685463b887ee278126ad01f", size = 2150682, upload-time = "2025-02-02T07:43:51.815Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d0/9e/984486f2d0a0bd2b024bf4bc1c62688fcafa9e61991f041fb0e2def4a982/h2-4.2.0-py3-none-any.whl", hash = "sha256:479a53ad425bb29af087f3458a61d30780bc818e4ebcf01f0b536ba916462ed0", size = 60957, upload-time = "2025-02-01T11:02:26.481Z" }, -] - -[[package]] -name = "hpack" -version = "4.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/2c/48/71de9ed269fdae9c8057e5a4c0aa7402e8bb16f2c6e90b3aa53327b113f8/hpack-4.1.0.tar.gz", hash = "sha256:ec5eca154f7056aa06f196a557655c5b009b382873ac8d1e66e79e87535f1dca", size = 51276, upload-time = "2025-01-22T21:44:58.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/07/c6/80c95b1b2b94682a72cbdbfb85b81ae2daffa4291fbfa1b1464502ede10d/hpack-4.1.0-py3-none-any.whl", hash = "sha256:157ac792668d995c657d93111f46b4535ed114f0c9c8d672271bbec7eae1b496", size = 34357, upload-time = "2025-01-22T21:44:56.92Z" }, -] - -[[package]] -name = "hyperframe" -version = "6.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/02/e7/94f8232d4a74cc99514c13a9f995811485a6903d48e5d952771ef6322e30/hyperframe-6.1.0.tar.gz", hash = "sha256:f630908a00854a7adeabd6382b43923a4c4cd4b821fcb527e6ab9e15382a3b08", size = 26566, upload-time = "2025-01-22T21:41:49.302Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/48/30/47d0bf6072f7252e6521f3447ccfa40b421b6824517f82854703d0f5a98b/hyperframe-6.1.0-py3-none-any.whl", hash = "sha256:b03380493a519fce58ea5af42e4a42317bf9bd425596f7a0835ffce80f1a42e5", size = 13007, upload-time = "2025-01-22T21:41:47.295Z" }, -] - [[package]] name = "identify" version = "2.6.10" @@ -623,108 +604,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/8f/8e/9ad090d3553c280a8060fbf6e24dc1c0c29704ee7d1c372f0c174aa59285/matplotlib_inline-0.1.7-py3-none-any.whl", hash = "sha256:df192d39a4ff8f21b1895d72e6a13f5fcc5099f00fa84384e0ea28c2cc0653ca", size = 9899, upload-time = "2024-04-15T13:44:43.265Z" }, ] -[[package]] -name = "multidict" -version = "6.6.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "typing-extensions", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/3d/2c/5dad12e82fbdf7470f29bff2171484bf07cb3b16ada60a6589af8f376440/multidict-6.6.3.tar.gz", hash = "sha256:798a9eb12dab0a6c2e29c1de6f3468af5cb2da6053a20dfa3344907eed0937cc", size = 101006, upload-time = "2025-06-30T15:53:46.929Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0b/67/414933982bce2efce7cbcb3169eaaf901e0f25baec69432b4874dfb1f297/multidict-6.6.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:a2be5b7b35271f7fff1397204ba6708365e3d773579fe2a30625e16c4b4ce817", size = 77017, upload-time = "2025-06-30T15:50:58.931Z" }, - { url = "https://files.pythonhosted.org/packages/8a/fe/d8a3ee1fad37dc2ef4f75488b0d9d4f25bf204aad8306cbab63d97bff64a/multidict-6.6.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:12f4581d2930840295c461764b9a65732ec01250b46c6b2c510d7ee68872b140", size = 44897, upload-time = "2025-06-30T15:51:00.999Z" }, - { url = "https://files.pythonhosted.org/packages/1f/e0/265d89af8c98240265d82b8cbcf35897f83b76cd59ee3ab3879050fd8c45/multidict-6.6.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:dd7793bab517e706c9ed9d7310b06c8672fd0aeee5781bfad612f56b8e0f7d14", size = 44574, upload-time = "2025-06-30T15:51:02.449Z" }, - { url = "https://files.pythonhosted.org/packages/e6/05/6b759379f7e8e04ccc97cfb2a5dcc5cdbd44a97f072b2272dc51281e6a40/multidict-6.6.3-cp310-cp310-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:72d8815f2cd3cf3df0f83cac3f3ef801d908b2d90409ae28102e0553af85545a", size = 225729, upload-time = "2025-06-30T15:51:03.794Z" }, - { url = "https://files.pythonhosted.org/packages/4e/f5/8d5a15488edd9a91fa4aad97228d785df208ed6298580883aa3d9def1959/multidict-6.6.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:531e331a2ee53543ab32b16334e2deb26f4e6b9b28e41f8e0c87e99a6c8e2d69", size = 242515, upload-time = "2025-06-30T15:51:05.002Z" }, - { url = "https://files.pythonhosted.org/packages/6e/b5/a8f317d47d0ac5bb746d6d8325885c8967c2a8ce0bb57be5399e3642cccb/multidict-6.6.3-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:42ca5aa9329a63be8dc49040f63817d1ac980e02eeddba763a9ae5b4027b9c9c", size = 222224, upload-time = "2025-06-30T15:51:06.148Z" }, - { url = "https://files.pythonhosted.org/packages/76/88/18b2a0d5e80515fa22716556061189c2853ecf2aa2133081ebbe85ebea38/multidict-6.6.3-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:208b9b9757060b9faa6f11ab4bc52846e4f3c2fb8b14d5680c8aac80af3dc751", size = 253124, upload-time = "2025-06-30T15:51:07.375Z" }, - { url = "https://files.pythonhosted.org/packages/62/bf/ebfcfd6b55a1b05ef16d0775ae34c0fe15e8dab570d69ca9941073b969e7/multidict-6.6.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:acf6b97bd0884891af6a8b43d0f586ab2fcf8e717cbd47ab4bdddc09e20652d8", size = 251529, upload-time = "2025-06-30T15:51:08.691Z" }, - { url = "https://files.pythonhosted.org/packages/44/11/780615a98fd3775fc309d0234d563941af69ade2df0bb82c91dda6ddaea1/multidict-6.6.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:68e9e12ed00e2089725669bdc88602b0b6f8d23c0c95e52b95f0bc69f7fe9b55", size = 241627, upload-time = "2025-06-30T15:51:10.605Z" }, - { url = "https://files.pythonhosted.org/packages/28/3d/35f33045e21034b388686213752cabc3a1b9d03e20969e6fa8f1b1d82db1/multidict-6.6.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:05db2f66c9addb10cfa226e1acb363450fab2ff8a6df73c622fefe2f5af6d4e7", size = 239351, upload-time = "2025-06-30T15:51:12.18Z" }, - { url = "https://files.pythonhosted.org/packages/6e/cc/ff84c03b95b430015d2166d9aae775a3985d757b94f6635010d0038d9241/multidict-6.6.3-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:0db58da8eafb514db832a1b44f8fa7906fdd102f7d982025f816a93ba45e3dcb", size = 233429, upload-time = "2025-06-30T15:51:13.533Z" }, - { url = "https://files.pythonhosted.org/packages/2e/f0/8cd49a0b37bdea673a4b793c2093f2f4ba8e7c9d6d7c9bd672fd6d38cd11/multidict-6.6.3-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:14117a41c8fdb3ee19c743b1c027da0736fdb79584d61a766da53d399b71176c", size = 243094, upload-time = "2025-06-30T15:51:14.815Z" }, - { url = "https://files.pythonhosted.org/packages/96/19/5d9a0cfdafe65d82b616a45ae950975820289069f885328e8185e64283c2/multidict-6.6.3-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:877443eaaabcd0b74ff32ebeed6f6176c71850feb7d6a1d2db65945256ea535c", size = 248957, upload-time = "2025-06-30T15:51:16.076Z" }, - { url = "https://files.pythonhosted.org/packages/e6/dc/c90066151da87d1e489f147b9b4327927241e65f1876702fafec6729c014/multidict-6.6.3-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:70b72e749a4f6e7ed8fb334fa8d8496384840319512746a5f42fa0aec79f4d61", size = 243590, upload-time = "2025-06-30T15:51:17.413Z" }, - { url = "https://files.pythonhosted.org/packages/ec/39/458afb0cccbb0ee9164365273be3e039efddcfcb94ef35924b7dbdb05db0/multidict-6.6.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:43571f785b86afd02b3855c5ac8e86ec921b760298d6f82ff2a61daf5a35330b", size = 237487, upload-time = "2025-06-30T15:51:19.039Z" }, - { url = "https://files.pythonhosted.org/packages/35/38/0016adac3990426610a081787011177e661875546b434f50a26319dc8372/multidict-6.6.3-cp310-cp310-win32.whl", hash = "sha256:20c5a0c3c13a15fd5ea86c42311859f970070e4e24de5a550e99d7c271d76318", size = 41390, upload-time = "2025-06-30T15:51:20.362Z" }, - { url = "https://files.pythonhosted.org/packages/f3/d2/17897a8f3f2c5363d969b4c635aa40375fe1f09168dc09a7826780bfb2a4/multidict-6.6.3-cp310-cp310-win_amd64.whl", hash = "sha256:ab0a34a007704c625e25a9116c6770b4d3617a071c8a7c30cd338dfbadfe6485", size = 45954, upload-time = "2025-06-30T15:51:21.383Z" }, - { url = "https://files.pythonhosted.org/packages/2d/5f/d4a717c1e457fe44072e33fa400d2b93eb0f2819c4d669381f925b7cba1f/multidict-6.6.3-cp310-cp310-win_arm64.whl", hash = "sha256:769841d70ca8bdd140a715746199fc6473414bd02efd678d75681d2d6a8986c5", size = 42981, upload-time = "2025-06-30T15:51:22.809Z" }, - { url = "https://files.pythonhosted.org/packages/08/f0/1a39863ced51f639c81a5463fbfa9eb4df59c20d1a8769ab9ef4ca57ae04/multidict-6.6.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:18f4eba0cbac3546b8ae31e0bbc55b02c801ae3cbaf80c247fcdd89b456ff58c", size = 76445, upload-time = "2025-06-30T15:51:24.01Z" }, - { url = "https://files.pythonhosted.org/packages/c9/0e/a7cfa451c7b0365cd844e90b41e21fab32edaa1e42fc0c9f68461ce44ed7/multidict-6.6.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef43b5dd842382329e4797c46f10748d8c2b6e0614f46b4afe4aee9ac33159df", size = 44610, upload-time = "2025-06-30T15:51:25.158Z" }, - { url = "https://files.pythonhosted.org/packages/c6/bb/a14a4efc5ee748cc1904b0748be278c31b9295ce5f4d2ef66526f410b94d/multidict-6.6.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:bf9bd1fd5eec01494e0f2e8e446a74a85d5e49afb63d75a9934e4a5423dba21d", size = 44267, upload-time = "2025-06-30T15:51:26.326Z" }, - { url = "https://files.pythonhosted.org/packages/c2/f8/410677d563c2d55e063ef74fe578f9d53fe6b0a51649597a5861f83ffa15/multidict-6.6.3-cp311-cp311-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:5bd8d6f793a787153956cd35e24f60485bf0651c238e207b9a54f7458b16d539", size = 230004, upload-time = "2025-06-30T15:51:27.491Z" }, - { url = "https://files.pythonhosted.org/packages/fd/df/2b787f80059314a98e1ec6a4cc7576244986df3e56b3c755e6fc7c99e038/multidict-6.6.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1bf99b4daf908c73856bd87ee0a2499c3c9a3d19bb04b9c6025e66af3fd07462", size = 247196, upload-time = "2025-06-30T15:51:28.762Z" }, - { url = "https://files.pythonhosted.org/packages/05/f2/f9117089151b9a8ab39f9019620d10d9718eec2ac89e7ca9d30f3ec78e96/multidict-6.6.3-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b9e59946b49dafaf990fd9c17ceafa62976e8471a14952163d10a7a630413a9", size = 225337, upload-time = "2025-06-30T15:51:30.025Z" }, - { url = "https://files.pythonhosted.org/packages/93/2d/7115300ec5b699faa152c56799b089a53ed69e399c3c2d528251f0aeda1a/multidict-6.6.3-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e2db616467070d0533832d204c54eea6836a5e628f2cb1e6dfd8cd6ba7277cb7", size = 257079, upload-time = "2025-06-30T15:51:31.716Z" }, - { url = "https://files.pythonhosted.org/packages/15/ea/ff4bab367623e39c20d3b07637225c7688d79e4f3cc1f3b9f89867677f9a/multidict-6.6.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7394888236621f61dcdd25189b2768ae5cc280f041029a5bcf1122ac63df79f9", size = 255461, upload-time = "2025-06-30T15:51:33.029Z" }, - { url = "https://files.pythonhosted.org/packages/74/07/2c9246cda322dfe08be85f1b8739646f2c4c5113a1422d7a407763422ec4/multidict-6.6.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f114d8478733ca7388e7c7e0ab34b72547476b97009d643644ac33d4d3fe1821", size = 246611, upload-time = "2025-06-30T15:51:34.47Z" }, - { url = "https://files.pythonhosted.org/packages/a8/62/279c13d584207d5697a752a66ffc9bb19355a95f7659140cb1b3cf82180e/multidict-6.6.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:cdf22e4db76d323bcdc733514bf732e9fb349707c98d341d40ebcc6e9318ef3d", size = 243102, upload-time = "2025-06-30T15:51:36.525Z" }, - { url = "https://files.pythonhosted.org/packages/69/cc/e06636f48c6d51e724a8bc8d9e1db5f136fe1df066d7cafe37ef4000f86a/multidict-6.6.3-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:e995a34c3d44ab511bfc11aa26869b9d66c2d8c799fa0e74b28a473a692532d6", size = 238693, upload-time = "2025-06-30T15:51:38.278Z" }, - { url = "https://files.pythonhosted.org/packages/89/a4/66c9d8fb9acf3b226cdd468ed009537ac65b520aebdc1703dd6908b19d33/multidict-6.6.3-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:766a4a5996f54361d8d5a9050140aa5362fe48ce51c755a50c0bc3706460c430", size = 246582, upload-time = "2025-06-30T15:51:39.709Z" }, - { url = "https://files.pythonhosted.org/packages/cf/01/c69e0317be556e46257826d5449feb4e6aa0d18573e567a48a2c14156f1f/multidict-6.6.3-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:3893a0d7d28a7fe6ca7a1f760593bc13038d1d35daf52199d431b61d2660602b", size = 253355, upload-time = "2025-06-30T15:51:41.013Z" }, - { url = "https://files.pythonhosted.org/packages/c0/da/9cc1da0299762d20e626fe0042e71b5694f9f72d7d3f9678397cbaa71b2b/multidict-6.6.3-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:934796c81ea996e61914ba58064920d6cad5d99140ac3167901eb932150e2e56", size = 247774, upload-time = "2025-06-30T15:51:42.291Z" }, - { url = "https://files.pythonhosted.org/packages/e6/91/b22756afec99cc31105ddd4a52f95ab32b1a4a58f4d417979c570c4a922e/multidict-6.6.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9ed948328aec2072bc00f05d961ceadfd3e9bfc2966c1319aeaf7b7c21219183", size = 242275, upload-time = "2025-06-30T15:51:43.642Z" }, - { url = "https://files.pythonhosted.org/packages/be/f1/adcc185b878036a20399d5be5228f3cbe7f823d78985d101d425af35c800/multidict-6.6.3-cp311-cp311-win32.whl", hash = "sha256:9f5b28c074c76afc3e4c610c488e3493976fe0e596dd3db6c8ddfbb0134dcac5", size = 41290, upload-time = "2025-06-30T15:51:45.264Z" }, - { url = "https://files.pythonhosted.org/packages/e0/d4/27652c1c6526ea6b4f5ddd397e93f4232ff5de42bea71d339bc6a6cc497f/multidict-6.6.3-cp311-cp311-win_amd64.whl", hash = "sha256:bc7f6fbc61b1c16050a389c630da0b32fc6d4a3d191394ab78972bf5edc568c2", size = 45942, upload-time = "2025-06-30T15:51:46.377Z" }, - { url = "https://files.pythonhosted.org/packages/16/18/23f4932019804e56d3c2413e237f866444b774b0263bcb81df2fdecaf593/multidict-6.6.3-cp311-cp311-win_arm64.whl", hash = "sha256:d4e47d8faffaae822fb5cba20937c048d4f734f43572e7079298a6c39fb172cb", size = 42880, upload-time = "2025-06-30T15:51:47.561Z" }, - { url = "https://files.pythonhosted.org/packages/0e/a0/6b57988ea102da0623ea814160ed78d45a2645e4bbb499c2896d12833a70/multidict-6.6.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:056bebbeda16b2e38642d75e9e5310c484b7c24e3841dc0fb943206a72ec89d6", size = 76514, upload-time = "2025-06-30T15:51:48.728Z" }, - { url = "https://files.pythonhosted.org/packages/07/7a/d1e92665b0850c6c0508f101f9cf0410c1afa24973e1115fe9c6a185ebf7/multidict-6.6.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e5f481cccb3c5c5e5de5d00b5141dc589c1047e60d07e85bbd7dea3d4580d63f", size = 45394, upload-time = "2025-06-30T15:51:49.986Z" }, - { url = "https://files.pythonhosted.org/packages/52/6f/dd104490e01be6ef8bf9573705d8572f8c2d2c561f06e3826b081d9e6591/multidict-6.6.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10bea2ee839a759ee368b5a6e47787f399b41e70cf0c20d90dfaf4158dfb4e55", size = 43590, upload-time = "2025-06-30T15:51:51.331Z" }, - { url = "https://files.pythonhosted.org/packages/44/fe/06e0e01b1b0611e6581b7fd5a85b43dacc08b6cea3034f902f383b0873e5/multidict-6.6.3-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:2334cfb0fa9549d6ce2c21af2bfbcd3ac4ec3646b1b1581c88e3e2b1779ec92b", size = 237292, upload-time = "2025-06-30T15:51:52.584Z" }, - { url = "https://files.pythonhosted.org/packages/ce/71/4f0e558fb77696b89c233c1ee2d92f3e1d5459070a0e89153c9e9e804186/multidict-6.6.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b8fee016722550a2276ca2cb5bb624480e0ed2bd49125b2b73b7010b9090e888", size = 258385, upload-time = "2025-06-30T15:51:53.913Z" }, - { url = "https://files.pythonhosted.org/packages/e3/25/cca0e68228addad24903801ed1ab42e21307a1b4b6dd2cf63da5d3ae082a/multidict-6.6.3-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5511cb35f5c50a2db21047c875eb42f308c5583edf96bd8ebf7d770a9d68f6d", size = 242328, upload-time = "2025-06-30T15:51:55.672Z" }, - { url = "https://files.pythonhosted.org/packages/6e/a3/46f2d420d86bbcb8fe660b26a10a219871a0fbf4d43cb846a4031533f3e0/multidict-6.6.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:712b348f7f449948e0a6c4564a21c7db965af900973a67db432d724619b3c680", size = 268057, upload-time = "2025-06-30T15:51:57.037Z" }, - { url = "https://files.pythonhosted.org/packages/9e/73/1c743542fe00794a2ec7466abd3f312ccb8fad8dff9f36d42e18fb1ec33e/multidict-6.6.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e4e15d2138ee2694e038e33b7c3da70e6b0ad8868b9f8094a72e1414aeda9c1a", size = 269341, upload-time = "2025-06-30T15:51:59.111Z" }, - { url = "https://files.pythonhosted.org/packages/a4/11/6ec9dcbe2264b92778eeb85407d1df18812248bf3506a5a1754bc035db0c/multidict-6.6.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8df25594989aebff8a130f7899fa03cbfcc5d2b5f4a461cf2518236fe6f15961", size = 256081, upload-time = "2025-06-30T15:52:00.533Z" }, - { url = "https://files.pythonhosted.org/packages/9b/2b/631b1e2afeb5f1696846d747d36cda075bfdc0bc7245d6ba5c319278d6c4/multidict-6.6.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:159ca68bfd284a8860f8d8112cf0521113bffd9c17568579e4d13d1f1dc76b65", size = 253581, upload-time = "2025-06-30T15:52:02.43Z" }, - { url = "https://files.pythonhosted.org/packages/bf/0e/7e3b93f79efeb6111d3bf9a1a69e555ba1d07ad1c11bceb56b7310d0d7ee/multidict-6.6.3-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:e098c17856a8c9ade81b4810888c5ad1914099657226283cab3062c0540b0643", size = 250750, upload-time = "2025-06-30T15:52:04.26Z" }, - { url = "https://files.pythonhosted.org/packages/ad/9e/086846c1d6601948e7de556ee464a2d4c85e33883e749f46b9547d7b0704/multidict-6.6.3-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:67c92ed673049dec52d7ed39f8cf9ebbadf5032c774058b4406d18c8f8fe7063", size = 251548, upload-time = "2025-06-30T15:52:06.002Z" }, - { url = "https://files.pythonhosted.org/packages/8c/7b/86ec260118e522f1a31550e87b23542294880c97cfbf6fb18cc67b044c66/multidict-6.6.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:bd0578596e3a835ef451784053cfd327d607fc39ea1a14812139339a18a0dbc3", size = 262718, upload-time = "2025-06-30T15:52:07.707Z" }, - { url = "https://files.pythonhosted.org/packages/8c/bd/22ce8f47abb0be04692c9fc4638508b8340987b18691aa7775d927b73f72/multidict-6.6.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:346055630a2df2115cd23ae271910b4cae40f4e336773550dca4889b12916e75", size = 259603, upload-time = "2025-06-30T15:52:09.58Z" }, - { url = "https://files.pythonhosted.org/packages/07/9c/91b7ac1691be95cd1f4a26e36a74b97cda6aa9820632d31aab4410f46ebd/multidict-6.6.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:555ff55a359302b79de97e0468e9ee80637b0de1fce77721639f7cd9440b3a10", size = 251351, upload-time = "2025-06-30T15:52:10.947Z" }, - { url = "https://files.pythonhosted.org/packages/6f/5c/4d7adc739884f7a9fbe00d1eac8c034023ef8bad71f2ebe12823ca2e3649/multidict-6.6.3-cp312-cp312-win32.whl", hash = "sha256:73ab034fb8d58ff85c2bcbadc470efc3fafeea8affcf8722855fb94557f14cc5", size = 41860, upload-time = "2025-06-30T15:52:12.334Z" }, - { url = "https://files.pythonhosted.org/packages/6a/a3/0fbc7afdf7cb1aa12a086b02959307848eb6bcc8f66fcb66c0cb57e2a2c1/multidict-6.6.3-cp312-cp312-win_amd64.whl", hash = "sha256:04cbcce84f63b9af41bad04a54d4cc4e60e90c35b9e6ccb130be2d75b71f8c17", size = 45982, upload-time = "2025-06-30T15:52:13.6Z" }, - { url = "https://files.pythonhosted.org/packages/b8/95/8c825bd70ff9b02462dc18d1295dd08d3e9e4eb66856d292ffa62cfe1920/multidict-6.6.3-cp312-cp312-win_arm64.whl", hash = "sha256:0f1130b896ecb52d2a1e615260f3ea2af55fa7dc3d7c3003ba0c3121a759b18b", size = 43210, upload-time = "2025-06-30T15:52:14.893Z" }, - { url = "https://files.pythonhosted.org/packages/52/1d/0bebcbbb4f000751fbd09957257903d6e002943fc668d841a4cf2fb7f872/multidict-6.6.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:540d3c06d48507357a7d57721e5094b4f7093399a0106c211f33540fdc374d55", size = 75843, upload-time = "2025-06-30T15:52:16.155Z" }, - { url = "https://files.pythonhosted.org/packages/07/8f/cbe241b0434cfe257f65c2b1bcf9e8d5fb52bc708c5061fb29b0fed22bdf/multidict-6.6.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c19cea2a690f04247d43f366d03e4eb110a0dc4cd1bbeee4d445435428ed35b", size = 45053, upload-time = "2025-06-30T15:52:17.429Z" }, - { url = "https://files.pythonhosted.org/packages/32/d2/0b3b23f9dbad5b270b22a3ac3ea73ed0a50ef2d9a390447061178ed6bdb8/multidict-6.6.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7af039820cfd00effec86bda5d8debef711a3e86a1d3772e85bea0f243a4bd65", size = 43273, upload-time = "2025-06-30T15:52:19.346Z" }, - { url = "https://files.pythonhosted.org/packages/fd/fe/6eb68927e823999e3683bc49678eb20374ba9615097d085298fd5b386564/multidict-6.6.3-cp313-cp313-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:500b84f51654fdc3944e936f2922114349bf8fdcac77c3092b03449f0e5bc2b3", size = 237124, upload-time = "2025-06-30T15:52:20.773Z" }, - { url = "https://files.pythonhosted.org/packages/e7/ab/320d8507e7726c460cb77117848b3834ea0d59e769f36fdae495f7669929/multidict-6.6.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3fc723ab8a5c5ed6c50418e9bfcd8e6dceba6c271cee6728a10a4ed8561520c", size = 256892, upload-time = "2025-06-30T15:52:22.242Z" }, - { url = "https://files.pythonhosted.org/packages/76/60/38ee422db515ac69834e60142a1a69111ac96026e76e8e9aa347fd2e4591/multidict-6.6.3-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:94c47ea3ade005b5976789baaed66d4de4480d0a0bf31cef6edaa41c1e7b56a6", size = 240547, upload-time = "2025-06-30T15:52:23.736Z" }, - { url = "https://files.pythonhosted.org/packages/27/fb/905224fde2dff042b030c27ad95a7ae744325cf54b890b443d30a789b80e/multidict-6.6.3-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dbc7cf464cc6d67e83e136c9f55726da3a30176f020a36ead246eceed87f1cd8", size = 266223, upload-time = "2025-06-30T15:52:25.185Z" }, - { url = "https://files.pythonhosted.org/packages/76/35/dc38ab361051beae08d1a53965e3e1a418752fc5be4d3fb983c5582d8784/multidict-6.6.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:900eb9f9da25ada070f8ee4a23f884e0ee66fe4e1a38c3af644256a508ad81ca", size = 267262, upload-time = "2025-06-30T15:52:26.969Z" }, - { url = "https://files.pythonhosted.org/packages/1f/a3/0a485b7f36e422421b17e2bbb5a81c1af10eac1d4476f2ff92927c730479/multidict-6.6.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7c6df517cf177da5d47ab15407143a89cd1a23f8b335f3a28d57e8b0a3dbb884", size = 254345, upload-time = "2025-06-30T15:52:28.467Z" }, - { url = "https://files.pythonhosted.org/packages/b4/59/bcdd52c1dab7c0e0d75ff19cac751fbd5f850d1fc39172ce809a74aa9ea4/multidict-6.6.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4ef421045f13879e21c994b36e728d8e7d126c91a64b9185810ab51d474f27e7", size = 252248, upload-time = "2025-06-30T15:52:29.938Z" }, - { url = "https://files.pythonhosted.org/packages/bb/a4/2d96aaa6eae8067ce108d4acee6f45ced5728beda55c0f02ae1072c730d1/multidict-6.6.3-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:6c1e61bb4f80895c081790b6b09fa49e13566df8fbff817da3f85b3a8192e36b", size = 250115, upload-time = "2025-06-30T15:52:31.416Z" }, - { url = "https://files.pythonhosted.org/packages/25/d2/ed9f847fa5c7d0677d4f02ea2c163d5e48573de3f57bacf5670e43a5ffaa/multidict-6.6.3-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:e5e8523bb12d7623cd8300dbd91b9e439a46a028cd078ca695eb66ba31adee3c", size = 249649, upload-time = "2025-06-30T15:52:32.996Z" }, - { url = "https://files.pythonhosted.org/packages/1f/af/9155850372563fc550803d3f25373308aa70f59b52cff25854086ecb4a79/multidict-6.6.3-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:ef58340cc896219e4e653dade08fea5c55c6df41bcc68122e3be3e9d873d9a7b", size = 261203, upload-time = "2025-06-30T15:52:34.521Z" }, - { url = "https://files.pythonhosted.org/packages/36/2f/c6a728f699896252cf309769089568a33c6439626648843f78743660709d/multidict-6.6.3-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc9dc435ec8699e7b602b94fe0cd4703e69273a01cbc34409af29e7820f777f1", size = 258051, upload-time = "2025-06-30T15:52:35.999Z" }, - { url = "https://files.pythonhosted.org/packages/d0/60/689880776d6b18fa2b70f6cc74ff87dd6c6b9b47bd9cf74c16fecfaa6ad9/multidict-6.6.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9e864486ef4ab07db5e9cb997bad2b681514158d6954dd1958dfb163b83d53e6", size = 249601, upload-time = "2025-06-30T15:52:37.473Z" }, - { url = "https://files.pythonhosted.org/packages/75/5e/325b11f2222a549019cf2ef879c1f81f94a0d40ace3ef55cf529915ba6cc/multidict-6.6.3-cp313-cp313-win32.whl", hash = "sha256:5633a82fba8e841bc5c5c06b16e21529573cd654f67fd833650a215520a6210e", size = 41683, upload-time = "2025-06-30T15:52:38.927Z" }, - { url = "https://files.pythonhosted.org/packages/b1/ad/cf46e73f5d6e3c775cabd2a05976547f3f18b39bee06260369a42501f053/multidict-6.6.3-cp313-cp313-win_amd64.whl", hash = "sha256:e93089c1570a4ad54c3714a12c2cef549dc9d58e97bcded193d928649cab78e9", size = 45811, upload-time = "2025-06-30T15:52:40.207Z" }, - { url = "https://files.pythonhosted.org/packages/c5/c9/2e3fe950db28fb7c62e1a5f46e1e38759b072e2089209bc033c2798bb5ec/multidict-6.6.3-cp313-cp313-win_arm64.whl", hash = "sha256:c60b401f192e79caec61f166da9c924e9f8bc65548d4246842df91651e83d600", size = 43056, upload-time = "2025-06-30T15:52:41.575Z" }, - { url = "https://files.pythonhosted.org/packages/3a/58/aaf8114cf34966e084a8cc9517771288adb53465188843d5a19862cb6dc3/multidict-6.6.3-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:02fd8f32d403a6ff13864b0851f1f523d4c988051eea0471d4f1fd8010f11134", size = 82811, upload-time = "2025-06-30T15:52:43.281Z" }, - { url = "https://files.pythonhosted.org/packages/71/af/5402e7b58a1f5b987a07ad98f2501fdba2a4f4b4c30cf114e3ce8db64c87/multidict-6.6.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:f3aa090106b1543f3f87b2041eef3c156c8da2aed90c63a2fbed62d875c49c37", size = 48304, upload-time = "2025-06-30T15:52:45.026Z" }, - { url = "https://files.pythonhosted.org/packages/39/65/ab3c8cafe21adb45b24a50266fd747147dec7847425bc2a0f6934b3ae9ce/multidict-6.6.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e924fb978615a5e33ff644cc42e6aa241effcf4f3322c09d4f8cebde95aff5f8", size = 46775, upload-time = "2025-06-30T15:52:46.459Z" }, - { url = "https://files.pythonhosted.org/packages/49/ba/9fcc1b332f67cc0c0c8079e263bfab6660f87fe4e28a35921771ff3eea0d/multidict-6.6.3-cp313-cp313t-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:b9fe5a0e57c6dbd0e2ce81ca66272282c32cd11d31658ee9553849d91289e1c1", size = 229773, upload-time = "2025-06-30T15:52:47.88Z" }, - { url = "https://files.pythonhosted.org/packages/a4/14/0145a251f555f7c754ce2dcbcd012939bbd1f34f066fa5d28a50e722a054/multidict-6.6.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b24576f208793ebae00280c59927c3b7c2a3b1655e443a25f753c4611bc1c373", size = 250083, upload-time = "2025-06-30T15:52:49.366Z" }, - { url = "https://files.pythonhosted.org/packages/9e/d4/d5c0bd2bbb173b586c249a151a26d2fb3ec7d53c96e42091c9fef4e1f10c/multidict-6.6.3-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:135631cb6c58eac37d7ac0df380294fecdc026b28837fa07c02e459c7fb9c54e", size = 228980, upload-time = "2025-06-30T15:52:50.903Z" }, - { url = "https://files.pythonhosted.org/packages/21/32/c9a2d8444a50ec48c4733ccc67254100c10e1c8ae8e40c7a2d2183b59b97/multidict-6.6.3-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:274d416b0df887aef98f19f21578653982cfb8a05b4e187d4a17103322eeaf8f", size = 257776, upload-time = "2025-06-30T15:52:52.764Z" }, - { url = "https://files.pythonhosted.org/packages/68/d0/14fa1699f4ef629eae08ad6201c6b476098f5efb051b296f4c26be7a9fdf/multidict-6.6.3-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:e252017a817fad7ce05cafbe5711ed40faeb580e63b16755a3a24e66fa1d87c0", size = 256882, upload-time = "2025-06-30T15:52:54.596Z" }, - { url = "https://files.pythonhosted.org/packages/da/88/84a27570fbe303c65607d517a5f147cd2fc046c2d1da02b84b17b9bdc2aa/multidict-6.6.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:2e4cc8d848cd4fe1cdee28c13ea79ab0ed37fc2e89dd77bac86a2e7959a8c3bc", size = 247816, upload-time = "2025-06-30T15:52:56.175Z" }, - { url = "https://files.pythonhosted.org/packages/1c/60/dca352a0c999ce96a5d8b8ee0b2b9f729dcad2e0b0c195f8286269a2074c/multidict-6.6.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9e236a7094b9c4c1b7585f6b9cca34b9d833cf079f7e4c49e6a4a6ec9bfdc68f", size = 245341, upload-time = "2025-06-30T15:52:57.752Z" }, - { url = "https://files.pythonhosted.org/packages/50/ef/433fa3ed06028f03946f3993223dada70fb700f763f70c00079533c34578/multidict-6.6.3-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:e0cb0ab69915c55627c933f0b555a943d98ba71b4d1c57bc0d0a66e2567c7471", size = 235854, upload-time = "2025-06-30T15:52:59.74Z" }, - { url = "https://files.pythonhosted.org/packages/1b/1f/487612ab56fbe35715320905215a57fede20de7db40a261759690dc80471/multidict-6.6.3-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:81ef2f64593aba09c5212a3d0f8c906a0d38d710a011f2f42759704d4557d3f2", size = 243432, upload-time = "2025-06-30T15:53:01.602Z" }, - { url = "https://files.pythonhosted.org/packages/da/6f/ce8b79de16cd885c6f9052c96a3671373d00c59b3ee635ea93e6e81b8ccf/multidict-6.6.3-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:b9cbc60010de3562545fa198bfc6d3825df430ea96d2cc509c39bd71e2e7d648", size = 252731, upload-time = "2025-06-30T15:53:03.517Z" }, - { url = "https://files.pythonhosted.org/packages/bb/fe/a2514a6aba78e5abefa1624ca85ae18f542d95ac5cde2e3815a9fbf369aa/multidict-6.6.3-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70d974eaaa37211390cd02ef93b7e938de564bbffa866f0b08d07e5e65da783d", size = 247086, upload-time = "2025-06-30T15:53:05.48Z" }, - { url = "https://files.pythonhosted.org/packages/8c/22/b788718d63bb3cce752d107a57c85fcd1a212c6c778628567c9713f9345a/multidict-6.6.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3713303e4a6663c6d01d648a68f2848701001f3390a030edaaf3fc949c90bf7c", size = 243338, upload-time = "2025-06-30T15:53:07.522Z" }, - { url = "https://files.pythonhosted.org/packages/22/d6/fdb3d0670819f2228f3f7d9af613d5e652c15d170c83e5f1c94fbc55a25b/multidict-6.6.3-cp313-cp313t-win32.whl", hash = "sha256:639ecc9fe7cd73f2495f62c213e964843826f44505a3e5d82805aa85cac6f89e", size = 47812, upload-time = "2025-06-30T15:53:09.263Z" }, - { url = "https://files.pythonhosted.org/packages/b6/d6/a9d2c808f2c489ad199723197419207ecbfbc1776f6e155e1ecea9c883aa/multidict-6.6.3-cp313-cp313t-win_amd64.whl", hash = "sha256:9f97e181f344a0ef3881b573d31de8542cc0dbc559ec68c8f8b5ce2c2e91646d", size = 53011, upload-time = "2025-06-30T15:53:11.038Z" }, - { url = "https://files.pythonhosted.org/packages/f2/40/b68001cba8188dd267590a111f9661b6256debc327137667e832bf5d66e8/multidict-6.6.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ce8b7693da41a3c4fde5871c738a81490cea5496c671d74374c8ab889e1834fb", size = 45254, upload-time = "2025-06-30T15:53:12.421Z" }, - { url = "https://files.pythonhosted.org/packages/d8/30/9aec301e9772b098c1f5c0ca0279237c9766d94b97802e9888010c64b0ed/multidict-6.6.3-py3-none-any.whl", hash = "sha256:8db10f29c7541fc5da4defd8cd697e1ca429db743fa716325f236079b96f775a", size = 12313, upload-time = "2025-06-30T15:53:45.437Z" }, -] - [[package]] name = "nest-asyncio" version = "1.6.0"