From a90174499438936a25410b0a77c84dca120ae73d Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Mon, 5 May 2025 11:27:03 +0200 Subject: [PATCH 01/18] Test struct to dict --- betterproto2/tests/test_struct.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 betterproto2/tests/test_struct.py diff --git a/betterproto2/tests/test_struct.py b/betterproto2/tests/test_struct.py new file mode 100644 index 00000000..c8f51011 --- /dev/null +++ b/betterproto2/tests/test_struct.py @@ -0,0 +1,22 @@ +def test_struct_to_dict(): + from tests.output_betterproto_pydantic.google.protobuf import ListValue, NullValue, Struct, Value + + struct = Struct( + fields={ + "null_field": Value(null_value=NullValue._), # TODO fix the name + "number_field": Value(number_value=12), + "string_field": Value(string_value="test"), + "bool_field": Value(bool_value=True), + "struct_field": Value(struct_value=Struct(fields={"x": Value(string_value="abc")})), + "list_field": Value(list_value=ListValue(values=[Value(number_value=42), Value(bool_value=False)])), + } + ) + + assert struct.to_dict() == { + "null_field": None, + "number_field": 12, + "string_field": "test", + "bool_field": True, + "struct_field": {"x": "abc"}, + "list_field": [42, False], + } From b443e5d13049855453711c13eab012aa5aaa98e7 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Wed, 20 Aug 2025 10:32:37 +0200 Subject: [PATCH 02/18] Fix test struct --- betterproto2/tests/test_struct.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/betterproto2/tests/test_struct.py b/betterproto2/tests/test_struct.py index c8f51011..33933413 100644 --- a/betterproto2/tests/test_struct.py +++ b/betterproto2/tests/test_struct.py @@ -1,9 +1,9 @@ def test_struct_to_dict(): - from tests.output_betterproto_pydantic.google.protobuf import ListValue, NullValue, Struct, Value + from tests.outputs.google.google.protobuf import ListValue, NullValue, Struct, Value struct = Struct( fields={ - "null_field": Value(null_value=NullValue._), # TODO fix the name + "null_field": Value(null_value=NullValue.NULL_VALUE), "number_field": Value(number_value=12), "string_field": Value(string_value="test"), "bool_field": Value(bool_value=True), From 04fa18bfb043f12cd3cc06048dc8f28ec4e175b4 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Wed, 20 Aug 2025 11:53:33 +0200 Subject: [PATCH 03/18] Struct support --- betterproto2/src/betterproto2/__init__.py | 3 + betterproto2_compiler/pyproject.toml | 6 +- .../known_types/__init__.py | 22 ++ betterproto2_compiler/uv.lock | 222 +++++++++++------- 4 files changed, 161 insertions(+), 92 deletions(-) diff --git a/betterproto2/src/betterproto2/__init__.py b/betterproto2/src/betterproto2/__init__.py index e1f101cd..3cad9011 100644 --- a/betterproto2/src/betterproto2/__init__.py +++ b/betterproto2/src/betterproto2/__init__.py @@ -138,6 +138,9 @@ NEG_INFINITY = "-Infinity" NAN = "NaN" +# For Struct support +JSON = int | float | bool | str | list["JSON"] | dict[str, "JSON"] | None + class Casing(builtin_enum.Enum): """Casing constants for serialization.""" diff --git a/betterproto2_compiler/pyproject.toml b/betterproto2_compiler/pyproject.toml index d78aa655..0c9daca1 100644 --- a/betterproto2_compiler/pyproject.toml +++ b/betterproto2_compiler/pyproject.toml @@ -15,13 +15,17 @@ keywords = [ requires-python = ">=3.10,<4.0" dependencies = [ # TODO use the version from the current repo? - "betterproto2[grpclib]>=0.7.0,<0.8", + # "betterproto2[grpclib]>=0.7.0,<0.8", + "betterproto2[grpclib]", "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" } + [project.urls] Documentation = "https://betterproto.github.io/python-betterproto2/" Repository = "https://github.com/betterproto/python-betterproto2" diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py index f6accdd6..81c160a6 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py @@ -13,6 +13,7 @@ UInt32Value, UInt64Value, ) +from .struct import ListValue, Struct, Value from .timestamp import Timestamp # For each (package, message name), lists the methods that should be added to the message definition. @@ -92,6 +93,24 @@ BytesValue.from_wrapped, BytesValue.to_wrapped, ], + ("google.protobuf", "Struct"): [ + Struct.from_dict, + Struct.to_dict, + Struct.from_wrapped, + Struct.to_wrapped, + ], + ("google.protobuf", "ListValue"): [ + ListValue.from_dict, + ListValue.to_dict, + ListValue.from_wrapped, + ListValue.to_wrapped, + ], + ("google.protobuf", "Value"): [ + Value.from_dict, + Value.to_dict, + Value.from_wrapped, + Value.to_wrapped, + ], } # A wrapped type is the type of a message that is automatically replaced by a known Python type. @@ -107,4 +126,7 @@ ("google.protobuf", "BytesValue"): "bytes", ("google.protobuf", "Timestamp"): "datetime.datetime", ("google.protobuf", "Duration"): "datetime.timedelta", + ("google.protobuf", "Struct"): "dict[str, betterproto2.JSON]", + ("google.protobuf", "ListValue"): "list[betterproto2.JSON]", + ("google.protobuf", "Value"): "betterproto2.JSON", } diff --git a/betterproto2_compiler/uv.lock b/betterproto2_compiler/uv.lock index e174ae81..52222551 100644 --- a/betterproto2_compiler/uv.lock +++ b/betterproto2_compiler/uv.lock @@ -26,22 +26,52 @@ wheels = [ [[package]] name = "betterproto2" -version = "0.7.0" -source = { registry = "https://pypi.org/simple" } +version = "0.7.1" +source = { directory = "../betterproto2" } dependencies = [ { name = "python-dateutil" }, { name = "typing-extensions" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/35/ad/4ada4789b83b653df8de8a4b0b4c6e57e50715d2591809dd4c09e5c99c30/betterproto2-0.7.0.tar.gz", hash = "sha256:517d6dd0e97ad64532f7e3ad5facce3f0d40bd0ecdbad8f70694418bf2f39e73", size = 142446, upload-time = "2025-06-25T14:41:07.583Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/3b/27/7ee873420f0cbfbfea0f29a3d8c4deb96fbacb9790775ba2a3ad7ad54293/betterproto2-0.7.0-py3-none-any.whl", hash = "sha256:6575f07089084b285f712bfd6edcbbdf8ee2ab3f388e2fcb7e7316256f2fc99d", size = 18930, upload-time = "2025-06-25T14:41:06.322Z" }, -] [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]] name = "betterproto2-compiler" version = "0.7.1" @@ -69,7 +99,7 @@ test = [ [package.metadata] requires-dist = [ - { name = "betterproto2", extras = ["grpclib"], specifier = ">=0.7.0,<0.8" }, + { name = "betterproto2", extras = ["grpclib"], 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" }, @@ -625,94 +655,104 @@ wheels = [ [[package]] name = "multidict" -version = "6.5.1" +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/5c/43/2d90c414d9efc4587d6e7cebae9f2c2d8001bcb4f89ed514ae837e9dcbe6/multidict-6.5.1.tar.gz", hash = "sha256:a835ea8103f4723915d7d621529c80ef48db48ae0c818afcabe0f95aa1febc3a", size = 98690, upload-time = "2025-06-24T22:16:05.117Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d5/29/db869cd110db3b91cbb6a353031e1e7964487403c086fb18fa0fdf5ec48a/multidict-6.5.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7b7d75cb5b90fa55700edbbdca12cd31f6b19c919e98712933c7a1c3c6c71b73", size = 74668, upload-time = "2025-06-24T22:13:46.269Z" }, - { url = "https://files.pythonhosted.org/packages/37/82/2520af39f62a2eb989aaaf84059e95e337ea7aeb464d9feccd53738d5c37/multidict-6.5.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:ad32e43e028276612bf5bab762677e7d131d2df00106b53de2efb2b8a28d5bce", size = 43479, upload-time = "2025-06-24T22:13:48.053Z" }, - { url = "https://files.pythonhosted.org/packages/71/1a/e4f4822d2a0597d7f9e2222178ca4f06ce979ea81c2a6747cc4e0e675cb3/multidict-6.5.1-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0499cbc67c1b02ba333781798560c5b1e7cd03e9273b678c92c6de1b1657fac9", size = 43511, upload-time = "2025-06-24T22:13:49.131Z" }, - { url = "https://files.pythonhosted.org/packages/b3/5e/f9e0b97f235a31de5db2a6859aa057bb459c767a7991b5d24549ca123aa6/multidict-6.5.1-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3c78fc6bc1dd7a139dab7ee9046f79a2082dce9360e3899b762615d564e2e857", size = 250040, upload-time = "2025-06-24T22:13:51.151Z" }, - { url = "https://files.pythonhosted.org/packages/c1/ac/2b3058deb743ce99421f8d4072f18eee30a81c86030f9f196a9bdb42978c/multidict-6.5.1-cp310-cp310-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f369d6619b24da4df4a02455fea8641fe8324fc0100a3e0dcebc5bf55fa903f3", size = 222481, upload-time = "2025-06-24T22:13:53.189Z" }, - { url = "https://files.pythonhosted.org/packages/39/81/b38cf2c7717a286f40876cdb3f2ecaa251512a34da1a3a7c4a7b4e0a226e/multidict-6.5.1-cp310-cp310-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:719af50a44ce9cf9ab15d829bf8cf146de486b4816284c17c3c9b9c9735abb8f", size = 260376, upload-time = "2025-06-24T22:13:54.763Z" }, - { url = "https://files.pythonhosted.org/packages/61/02/ce56214478629279941c5c20af2a865bbfa6bb9fd59ef1c2250bed8d077a/multidict-6.5.1-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:199a0a9b3de8bbeb6881460d32b857dc7abec94448aeb6d607c336628c53580a", size = 256845, upload-time = "2025-06-24T22:13:56.001Z" }, - { url = "https://files.pythonhosted.org/packages/ed/7d/e8a57728a6c0fd421e3c7637621bb0cd9e20bdd6cd07bb8c068ea9b0bd4c/multidict-6.5.1-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fe09318a28b00c6f43180d0d889df1535e98fb2d93d25955d46945f8d5410d87", size = 247834, upload-time = "2025-06-24T22:13:57.22Z" }, - { url = "https://files.pythonhosted.org/packages/cc/30/7f3abcf04755d0265123ceee96d9560c6994a90cd5b28f5c25ec83e43824/multidict-6.5.1-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:ab94923ae54385ed480e4ab19f10269ee60f3eabd0b35e2a5d1ba6dbf3b0cc27", size = 245233, upload-time = "2025-06-24T22:13:59.004Z" }, - { url = "https://files.pythonhosted.org/packages/f6/7a/86057ac640fb13205a489b671281211c85fd9a2b4d4274401484b5e3f1cb/multidict-6.5.1-cp310-cp310-musllinux_1_2_armv7l.whl", hash = "sha256:de2b253a3a90e1fa55eef5f9b3146bb5c722bd3400747112c9963404a2f5b9cf", size = 236867, upload-time = "2025-06-24T22:14:00.225Z" }, - { url = "https://files.pythonhosted.org/packages/e2/3e/de3c63ab6690d4ab4bd7656264b8bec6d3426e8b6b9a280723bb538861fc/multidict-6.5.1-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:b3bd88c1bc1f749db6a1e1f01696c3498bc25596136eceebb45766d24a320b27", size = 257058, upload-time = "2025-06-24T22:14:01.79Z" }, - { url = "https://files.pythonhosted.org/packages/13/1f/46782124968479edd8dc8bc6d770b5b0e4b3fcd00275ee26198f025a6184/multidict-6.5.1-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:0ce8f0ea49e8f54203f7d80e083a7aa017dbcb6f2d76d674273e25144c8aa3d7", size = 246787, upload-time = "2025-06-24T22:14:03.079Z" }, - { url = "https://files.pythonhosted.org/packages/a2/b6/c8cd1ece2855296cc64f4c9db21a5252f1c61535ef6e008d984b47ba31e2/multidict-6.5.1-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:dc62c8ac1b73ec704ed1a05be0267358fd5c99d1952f30448db1637336635cf8", size = 243538, upload-time = "2025-06-24T22:14:04.362Z" }, - { url = "https://files.pythonhosted.org/packages/c4/c4/41fad83305ca4a3b4b0a1cf8fec3a6d044a7eb4d9484ee574851fde0b182/multidict-6.5.1-cp310-cp310-win32.whl", hash = "sha256:7a365a579fb3e067943d0278474e14c2244c252f460b401ccbf49f962e7b70fa", size = 40742, upload-time = "2025-06-24T22:14:05.516Z" }, - { url = "https://files.pythonhosted.org/packages/6d/cf/cd82de0db6469376dfe40b56c96768d422077e71e76b4783384864975882/multidict-6.5.1-cp310-cp310-win_amd64.whl", hash = "sha256:4b299a2ffed33ad0733a9d47805b538d59465f8439bfea44df542cfb285c4db2", size = 44715, upload-time = "2025-06-24T22:14:06.686Z" }, - { url = "https://files.pythonhosted.org/packages/1d/9b/5bb9a84cb264f4717a4be7be8ce0536172fb587f8aa8868889408384f8cd/multidict-6.5.1-cp310-cp310-win_arm64.whl", hash = "sha256:ed98ac527278372251fbc8f5c6c41bdf64ded1db0e6e86f9b9622744306060f6", size = 41922, upload-time = "2025-06-24T22:14:07.762Z" }, - { url = "https://files.pythonhosted.org/packages/d5/65/439c3f595f68ee60d2c7abd14f36829b936b49c4939e35f24e65950b59b2/multidict-6.5.1-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:153d7ff738d9b67b94418b112dc5a662d89d2fc26846a9e942f039089048c804", size = 74129, upload-time = "2025-06-24T22:14:08.859Z" }, - { url = "https://files.pythonhosted.org/packages/8a/7a/88b474366126ef7cd427dca84ea6692d81e6e8ebb46f810a565e60716951/multidict-6.5.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:1d784c0a1974f00d87f632d0fb6b1078baf7e15d2d2d1408af92f54d120f136e", size = 43248, upload-time = "2025-06-24T22:14:10.017Z" }, - { url = "https://files.pythonhosted.org/packages/aa/8f/c45ff8980c2f2d1ed8f4f0c682953861fbb840adc318da1b26145587e443/multidict-6.5.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:dedf667cded1cdac5bfd3f3c2ff30010f484faccae4e871cc8a9316d2dc27363", size = 43250, upload-time = "2025-06-24T22:14:11.107Z" }, - { url = "https://files.pythonhosted.org/packages/ac/71/795e729385ecd8994d2033731ced3a80959e9c3c279766613565f5dcc7e1/multidict-6.5.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7cbf407313236a79ce9b8af11808c29756cfb9c9a49a7f24bb1324537eec174b", size = 254313, upload-time = "2025-06-24T22:14:12.216Z" }, - { url = "https://files.pythonhosted.org/packages/de/5a/36e8dd1306f8f6e5b252d6341e919c4a776745e2c38f86bc27d0640d3379/multidict-6.5.1-cp311-cp311-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2bf0068fe9abb0ebed1436a4e415117386951cf598eb8146ded4baf8e1ff6d1e", size = 227162, upload-time = "2025-06-24T22:14:13.549Z" }, - { url = "https://files.pythonhosted.org/packages/f0/c2/4e68fb3a8ef5b23bbf3d82a19f4ff71de8289b696c662572a6cb094eabf6/multidict-6.5.1-cp311-cp311-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:195882f2f6272dacc88194ecd4de3608ad0ee29b161e541403b781a5f5dd346f", size = 265552, upload-time = "2025-06-24T22:14:14.846Z" }, - { url = "https://files.pythonhosted.org/packages/51/5b/b9ee059e39cd3fec2e1fe9ecb57165fba0518d79323a6f355275ed9ec956/multidict-6.5.1-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5776f9d2c3a1053f022f744af5f467c2f65b40d4cc00082bcf70e8c462c7dbad", size = 260935, upload-time = "2025-06-24T22:14:16.209Z" }, - { url = "https://files.pythonhosted.org/packages/4c/0a/ea655a79d2d89dedb33f423b5dd3a733d97b1765a5e2155da883060fb48f/multidict-6.5.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a266373c604e49552d295d9f8ec4fd59bd364f2dd73eb18e7d36d5533b88f45", size = 251778, upload-time = "2025-06-24T22:14:17.963Z" }, - { url = "https://files.pythonhosted.org/packages/3f/58/8ff6b032f6c8956c8beb93a7191c80e4a6f385e9ffbe4a38c1cd758a7445/multidict-6.5.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:79101d58094419b6e8d07e24946eba440136b9095590271cd6ccc4a90674a57d", size = 249837, upload-time = "2025-06-24T22:14:19.344Z" }, - { url = "https://files.pythonhosted.org/packages/de/be/2fcdfd358ebc1be2ac3922a594daf660f99a23740f5177ba8b2fb6a66feb/multidict-6.5.1-cp311-cp311-musllinux_1_2_armv7l.whl", hash = "sha256:62eb76be8c20d9017a82b74965db93ddcf472b929b6b2b78c56972c73bacf2e4", size = 240831, upload-time = "2025-06-24T22:14:20.647Z" }, - { url = "https://files.pythonhosted.org/packages/e3/e0/1d3a4bb4ce34f314b919f4cb0da26430a6d88758f6d20b1c4f236a569085/multidict-6.5.1-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:70c742357dd6207be30922207f8d59c91e2776ddbefa23830c55c09020e59f8a", size = 262110, upload-time = "2025-06-24T22:14:21.919Z" }, - { url = "https://files.pythonhosted.org/packages/f0/5a/4cabf6661aa18e43dca54d00de06ef287740ad6ddbba34be53b3a554a6ee/multidict-6.5.1-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:29eff1c9a905e298e9cd29f856f77485e58e59355f0ee323ac748203e002bbd3", size = 250845, upload-time = "2025-06-24T22:14:23.276Z" }, - { url = "https://files.pythonhosted.org/packages/66/ad/44c44312d48423327d22be8c7058f9da8e2a527c9230d89b582670327efd/multidict-6.5.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:090e0b37fde199b58ea050c472c21dc8a3fbf285f42b862fe1ff02aab8942239", size = 247351, upload-time = "2025-06-24T22:14:24.523Z" }, - { url = "https://files.pythonhosted.org/packages/21/30/a12bbd76222be44c4f2d540c0d9cd1f932ab97e84a06098749f29b2908f5/multidict-6.5.1-cp311-cp311-win32.whl", hash = "sha256:6037beca8cb481307fb586ee0b73fae976a3e00d8f6ad7eb8af94a878a4893f0", size = 40644, upload-time = "2025-06-24T22:14:26.139Z" }, - { url = "https://files.pythonhosted.org/packages/90/58/2ce479dcb4611212eaa4808881d9a66a4362c48cd9f7b525b24a5d45764f/multidict-6.5.1-cp311-cp311-win_amd64.whl", hash = "sha256:b632c1e4a2ff0bb4c1367d6c23871aa95dbd616bf4a847034732a142bb6eea94", size = 44693, upload-time = "2025-06-24T22:14:27.265Z" }, - { url = "https://files.pythonhosted.org/packages/cc/d1/466a6cf48dcef796f2d75ba51af4475ac96c6ea33ef4dbf4cea1caf99532/multidict-6.5.1-cp311-cp311-win_arm64.whl", hash = "sha256:2ec3aa63f0c668f591d43195f8e555f803826dee34208c29ade9d63355f9e095", size = 41822, upload-time = "2025-06-24T22:14:28.387Z" }, - { url = "https://files.pythonhosted.org/packages/33/36/225fb9b890607d740f61957febf622f5c9cd9e641a93502c7877934d57ef/multidict-6.5.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:48f95fe064f63d9601ef7a3dce2fc2a437d5fcc11bca960bc8be720330b13b6a", size = 74287, upload-time = "2025-06-24T22:14:29.456Z" }, - { url = "https://files.pythonhosted.org/packages/70/e5/c9eabb16ecf77275664413263527ab169e08371dfa6b168025d8f67261fd/multidict-6.5.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7b7b6e1ce9b61f721417c68eeeb37599b769f3b631e6b25c21f50f8f619420b9", size = 44092, upload-time = "2025-06-24T22:14:30.686Z" }, - { url = "https://files.pythonhosted.org/packages/df/0b/dd9322a432c477a2e6d089bbb53acb68ed25515b8292dbc60f27e7e45d70/multidict-6.5.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8b83b055889bda09fc866c0a652cdb6c36eeeafc2858259c9a7171fe82df5773", size = 42565, upload-time = "2025-06-24T22:14:31.8Z" }, - { url = "https://files.pythonhosted.org/packages/f9/ac/22f5b4e55a4bc99f9622de280f7da366c1d7f29ec4eec9d339cb2ba62019/multidict-6.5.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b7bd4d655dc460c7aebb73b58ed1c074e85f7286105b012556cf0f25c6d1dba3", size = 254896, upload-time = "2025-06-24T22:14:32.865Z" }, - { url = "https://files.pythonhosted.org/packages/09/dc/2f6d96d4a80ec731579cb69532fac33cbbda2a838079ae0c47c6e8f5545b/multidict-6.5.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:aa6dcf25ced31cdce10f004506dbc26129f28a911b32ed10e54453a0842a6173", size = 236854, upload-time = "2025-06-24T22:14:34.185Z" }, - { url = "https://files.pythonhosted.org/packages/4a/cb/ef38a69ee75e8b72e5cff9ed4cff92379eadd057a99eaf4893494bf6ab64/multidict-6.5.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:059fb556c3e6ce1a168496f92ef139ad839a47f898eaa512b1d43e5e05d78c6b", size = 265131, upload-time = "2025-06-24T22:14:35.534Z" }, - { url = "https://files.pythonhosted.org/packages/c0/9e/85d9fe9e658e0edf566c02181248fa2aaf5e53134df0c80f7231ce5fc689/multidict-6.5.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f97680c839dd9fa208e9584b1c2a5f1224bd01d31961f7f7d94984408c4a6b9e", size = 262187, upload-time = "2025-06-24T22:14:36.891Z" }, - { url = "https://files.pythonhosted.org/packages/2b/1c/b46ec1dd78c3faa55bffb354410c48fadd81029a144cd056828c82ca15b4/multidict-6.5.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7710c716243525cc05cd038c6e09f1807ee0fef2510a6e484450712c389c8d7f", size = 251220, upload-time = "2025-06-24T22:14:38.584Z" }, - { url = "https://files.pythonhosted.org/packages/6b/6b/481ec5179ddc7da8b05077ebae2dd51da3df3ae3e5842020fbfa939167c1/multidict-6.5.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:83eb172b4856ffff2814bdcf9c7792c0439302faab1b31376817b067b26cd8f5", size = 249949, upload-time = "2025-06-24T22:14:40.033Z" }, - { url = "https://files.pythonhosted.org/packages/00/e3/642f63e12c1b8e6662c23626a98e9d764fe5a63c3a6cb59002f6fdcb920f/multidict-6.5.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:562d4714fa43f6ebc043a657535e4575e7d6141a818c9b3055f0868d29a1a41b", size = 244438, upload-time = "2025-06-24T22:14:41.464Z" }, - { url = "https://files.pythonhosted.org/packages/dc/cf/797397f6d38b011912504aef213a4be43ef4ec134859caa47f94d810bad8/multidict-6.5.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:2d7def2fc47695c46a427b8f298fb5ace03d635c1fb17f30d6192c9a8fb69e70", size = 259921, upload-time = "2025-06-24T22:14:43.248Z" }, - { url = "https://files.pythonhosted.org/packages/82/b2/ae914a2d84eba21e956fa3727060248ca23ed4a5bf1beb057df0d10f9de3/multidict-6.5.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:77bc8ab5c6bfe696eff564824e73a451fdeca22f3b960261750836cee02bcbfa", size = 252691, upload-time = "2025-06-24T22:14:45.57Z" }, - { url = "https://files.pythonhosted.org/packages/01/fa/1ab4d79a236b871cfd40d36a1f9942906c630bd2b7822287bd3927addb62/multidict-6.5.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:9eec51891d3c210948ead894ec1483d48748abec08db5ce9af52cc13fef37aee", size = 246224, upload-time = "2025-06-24T22:14:47.316Z" }, - { url = "https://files.pythonhosted.org/packages/78/dd/bf002fe04e952db73cad8ce10a5b5347358d0d17221aef156e050aff690b/multidict-6.5.1-cp312-cp312-win32.whl", hash = "sha256:189f0c2bd1c0ae5509e453707d0e187e030c9e873a0116d1f32d1c870d0fc347", size = 41354, upload-time = "2025-06-24T22:14:48.567Z" }, - { url = "https://files.pythonhosted.org/packages/95/ce/508a8487d98fdc3e693755bc19c543a2af293f5ce96da398bd1974efb802/multidict-6.5.1-cp312-cp312-win_amd64.whl", hash = "sha256:e81f23b4b6f2a588f15d5cb554b2d8b482bb6044223d64b86bc7079cae9ebaad", size = 45072, upload-time = "2025-06-24T22:14:50.898Z" }, - { url = "https://files.pythonhosted.org/packages/ae/da/4782cf2f274d0d56fff6c07fc5cc5a14acf821dec08350c17d66d0207a05/multidict-6.5.1-cp312-cp312-win_arm64.whl", hash = "sha256:79d13e06d5241f9c8479dfeaf0f7cce8f453a4a302c9a0b1fa9b1a6869ff7757", size = 42149, upload-time = "2025-06-24T22:14:53.138Z" }, - { url = "https://files.pythonhosted.org/packages/19/3f/c2e07031111d2513d260157933a8697ad52a935d8a2a2b8b7b317ddd9a96/multidict-6.5.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:98011312f36d1e496f15454a95578d1212bc2ffc25650a8484752b06d304fd9b", size = 73588, upload-time = "2025-06-24T22:14:54.332Z" }, - { url = "https://files.pythonhosted.org/packages/95/bb/f47aa21827202a9f889fd66de9a1db33d0e4bbaaa2567156e4efb3cc0e5e/multidict-6.5.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bae589fb902b47bd94e6f539b34eefe55a1736099f616f614ec1544a43f95b05", size = 43756, upload-time = "2025-06-24T22:14:55.748Z" }, - { url = "https://files.pythonhosted.org/packages/9f/ec/24549de092c9b0bc3167e0beb31a11be58e8595dbcfed2b7821795bb3923/multidict-6.5.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6eb3bf26cd94eb306e4bc776d0964cc67a7967e4ad9299309f0ff5beec3c62be", size = 42222, upload-time = "2025-06-24T22:14:57.418Z" }, - { url = "https://files.pythonhosted.org/packages/13/45/54452027ebc0ba660667aab67ae11afb9aaba91f4b5d63cddef045279d94/multidict-6.5.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e5e1a5a99c72d1531501406fcc06b6bf699ebd079dacd6807bb43fc0ff260e5c", size = 253014, upload-time = "2025-06-24T22:14:58.738Z" }, - { url = "https://files.pythonhosted.org/packages/97/3c/76e7b4c0ce3a8bb43efca679674fba421333fbc8429134072db80e13dcb8/multidict-6.5.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:38755bcba18720cb2338bea23a5afcff234445ee75fa11518f6130e22f2ab970", size = 235939, upload-time = "2025-06-24T22:15:00.138Z" }, - { url = "https://files.pythonhosted.org/packages/86/ce/48e3123a9af61ff2f60e3764b0b15cf4fca22b1299aac281252ac3a590d6/multidict-6.5.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f42fef9bcba3c32fd4e4a23c5757fc807d218b249573aaffa8634879f95feb73", size = 262940, upload-time = "2025-06-24T22:15:01.52Z" }, - { url = "https://files.pythonhosted.org/packages/b3/ab/bccd739faf87051b55df619a0967c8545b4d4a4b90258c5f564ab1752f15/multidict-6.5.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:071b962f4cc87469cda90c7cc1c077b76496878b39851d7417a3d994e27fe2c6", size = 260652, upload-time = "2025-06-24T22:15:02.988Z" }, - { url = "https://files.pythonhosted.org/packages/9a/9c/01f654aad28a5d0d74f2678c1541ae15e711f99603fd84c780078205966e/multidict-6.5.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:627ba4b7ce7c0115981f0fd91921f5d101dfb9972622178aeef84ccce1c2bbf3", size = 250011, upload-time = "2025-06-24T22:15:04.317Z" }, - { url = "https://files.pythonhosted.org/packages/5c/bc/edf08906e1db7385c6bf36e4179957307f50c44a889493e9b251255be79c/multidict-6.5.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:05dcaed3e5e54f0d0f99a39762b0195274b75016cbf246f600900305581cf1a2", size = 248242, upload-time = "2025-06-24T22:15:06.035Z" }, - { url = "https://files.pythonhosted.org/packages/b7/c3/1ad054b88b889fda8b62ea9634ac7082567e8dc42b9b794a2c565ef102ab/multidict-6.5.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:11f5ecf3e741a18c578d118ad257c5588ca33cc7c46d51c0487d7ae76f072c32", size = 244683, upload-time = "2025-06-24T22:15:07.731Z" }, - { url = "https://files.pythonhosted.org/packages/57/63/119a76b2095e1bb765816175cafeac7b520f564691abef2572fb80f4f246/multidict-6.5.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:b948eb625411c20b15088fca862c51a39140b9cf7875b5fb47a72bb249fa2f42", size = 257626, upload-time = "2025-06-24T22:15:09.013Z" }, - { url = "https://files.pythonhosted.org/packages/26/a9/b91a76af5ff49bd088ee76d11eb6134227f5ea50bcd5f6738443b2fe8e05/multidict-6.5.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc993a96dfc8300befd03d03df46efdb1d8d5a46911b014e956a4443035f470d", size = 251077, upload-time = "2025-06-24T22:15:10.366Z" }, - { url = "https://files.pythonhosted.org/packages/2a/fe/b1dc57aaa4de9f5a27543e28bd1f8bff00a316888b7344b5d33258b14b0a/multidict-6.5.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ee2d333380f22d35a56c6461f4579cfe186e143cd0b010b9524ac027de2a34cd", size = 244715, upload-time = "2025-06-24T22:15:11.76Z" }, - { url = "https://files.pythonhosted.org/packages/51/55/47a82690f71d0141eea49a623bbcc00a4d28770efc7cba8ead75602c9b90/multidict-6.5.1-cp313-cp313-win32.whl", hash = "sha256:5891e3327e6a426ddd443c87339b967c84feb8c022dd425e0c025fa0fcd71e68", size = 41156, upload-time = "2025-06-24T22:15:13.139Z" }, - { url = "https://files.pythonhosted.org/packages/25/b3/43306e4d7d3a9898574d1dc156b9607540dad581b1d767c992030751b82d/multidict-6.5.1-cp313-cp313-win_amd64.whl", hash = "sha256:fcdaa72261bff25fad93e7cb9bd7112bd4bac209148e698e380426489d8ed8a9", size = 44933, upload-time = "2025-06-24T22:15:14.639Z" }, - { url = "https://files.pythonhosted.org/packages/30/e2/34cb83c8a4e01b28e2abf30dc90178aa63c9db042be22fa02472cb744b86/multidict-6.5.1-cp313-cp313-win_arm64.whl", hash = "sha256:84292145303f354a35558e601c665cdf87059d87b12777417e2e57ba3eb98903", size = 41967, upload-time = "2025-06-24T22:15:15.856Z" }, - { url = "https://files.pythonhosted.org/packages/64/08/17d2de9cf749ea9589ecfb7532ab4988e8b113b7624826dba6b7527a58f3/multidict-6.5.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f8316e58db799a1972afbc46770dfaaf20b0847003ab80de6fcb9861194faa3f", size = 80513, upload-time = "2025-06-24T22:15:16.946Z" }, - { url = "https://files.pythonhosted.org/packages/3e/b9/c9392465a21f7dff164633348b4cf66eef55c4ee48bdcdc00f0a71792779/multidict-6.5.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:d3468f0db187aca59eb56e0aa9f7c8c5427bcb844ad1c86557b4886aeb4484d8", size = 46854, upload-time = "2025-06-24T22:15:18.116Z" }, - { url = "https://files.pythonhosted.org/packages/2e/24/d79cbed5d0573304bc907dff0e5ad8788a4de891eec832809812b319930e/multidict-6.5.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:228533a5f99f1248cd79f6470779c424d63bc3e10d47c82511c65cc294458445", size = 45724, upload-time = "2025-06-24T22:15:19.241Z" }, - { url = "https://files.pythonhosted.org/packages/ec/22/232be6c077183719c78131f0e3c3d7134eb2d839e6e50e1c1e69e5ef5965/multidict-6.5.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:527076fdf5854901b1246c589af9a8a18b4a308375acb0020b585f696a10c794", size = 251895, upload-time = "2025-06-24T22:15:20.564Z" }, - { url = "https://files.pythonhosted.org/packages/57/80/85985e1441864b946e79538355b7b47f36206bf6bbaa2fa6d74d8232f2ab/multidict-6.5.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:9a17a17bad5c22f43e6a6b285dd9c16b1e8f8428202cd9bc22adaac68d0bbfed", size = 229357, upload-time = "2025-06-24T22:15:21.949Z" }, - { url = "https://files.pythonhosted.org/packages/b1/14/0024d1428b05aedaeea211da232aa6b6ad5c556a8a38b0942df1e54e1fa5/multidict-6.5.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:efd1951edab4a6cb65108d411867811f2b283f4b972337fb4269e40142f7f6a6", size = 259262, upload-time = "2025-06-24T22:15:23.455Z" }, - { url = "https://files.pythonhosted.org/packages/b1/cc/3fe63d61ffc9a48d62f36249e228e330144d990ac01f61169b615a3be471/multidict-6.5.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:c07d5f38b39acb4f8f61a7aa4166d140ed628245ff0441630df15340532e3b3c", size = 257998, upload-time = "2025-06-24T22:15:24.907Z" }, - { url = "https://files.pythonhosted.org/packages/e8/e4/46b38b9a565ccc5d86f55787090670582d51ab0a0d37cfeaf4313b053f7b/multidict-6.5.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8a6605dc74cd333be279e1fcb568ea24f7bdf1cf09f83a77360ce4dd32d67f14", size = 247951, upload-time = "2025-06-24T22:15:26.274Z" }, - { url = "https://files.pythonhosted.org/packages/af/78/58a9bc0674401f1f26418cd58a5ebf35ce91ead76a22b578908acfe0f4e2/multidict-6.5.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:8d64e30ae9ba66ce303a567548a06d64455d97c5dff7052fe428d154274d7174", size = 246786, upload-time = "2025-06-24T22:15:27.695Z" }, - { url = "https://files.pythonhosted.org/packages/66/24/51142ccee295992e22881cccc54b291308423bbcc836fcf4d2edef1a88d0/multidict-6.5.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:2fb5dde79a7f6d98ac5e26a4c9de77ccd2c5224a7ce89aeac6d99df7bbe06464", size = 235030, upload-time = "2025-06-24T22:15:29.391Z" }, - { url = "https://files.pythonhosted.org/packages/4b/9a/a6f7b75460d3e35b16bf7745c9e3ebb3293324a4295e586563bf50d361f4/multidict-6.5.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:8a0d22e8b07cf620e9aeb1582340d00f0031e6a1f3e39d9c2dcbefa8691443b4", size = 253964, upload-time = "2025-06-24T22:15:31.689Z" }, - { url = "https://files.pythonhosted.org/packages/3d/f8/0b690674bf8f78604eb0a2b0a85d1380ff3003f270440d40def2a3de8cf4/multidict-6.5.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:0120ed5cff2082c7a0ed62a8f80f4f6ac266010c722381816462f279bfa19487", size = 247370, upload-time = "2025-06-24T22:15:33.114Z" }, - { url = "https://files.pythonhosted.org/packages/7f/7d/ca55049d1041c517f294c1755c786539cb7a8dc5033361f20ce3a3d817be/multidict-6.5.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:3dea06ba27401c4b54317aa04791182dc9295e7aa623732dd459071a0e0f65db", size = 242920, upload-time = "2025-06-24T22:15:34.669Z" }, - { url = "https://files.pythonhosted.org/packages/1e/65/f4afa14f0921751864bb3ef80267f15ecae423483e8da9bc5d3757632bfa/multidict-6.5.1-cp313-cp313t-win32.whl", hash = "sha256:93b21be44f3cfee3be68ed5cd8848a3c0420d76dbd12d74f7776bde6b29e5f33", size = 46968, upload-time = "2025-06-24T22:15:36.023Z" }, - { url = "https://files.pythonhosted.org/packages/00/0a/13d08be1ca1523df515fb4efd3cf10f153e62d533f55c53f543cd73041e8/multidict-6.5.1-cp313-cp313t-win_amd64.whl", hash = "sha256:c5c18f8646a520cc34d00f65f9f6f77782b8a8c59fd8de10713e0de7f470b5d0", size = 52353, upload-time = "2025-06-24T22:15:37.247Z" }, - { url = "https://files.pythonhosted.org/packages/4b/dd/84aaf725b236677597a9570d8c1c99af0ba03712149852347969e014d826/multidict-6.5.1-cp313-cp313t-win_arm64.whl", hash = "sha256:eb27128141474a1d545f0531b496c7c2f1c4beff50cb5a828f36eb62fef16c67", size = 44500, upload-time = "2025-06-24T22:15:38.445Z" }, - { url = "https://files.pythonhosted.org/packages/07/9f/d4719ce55a1d8bf6619e8bb92f1e2e7399026ea85ae0c324ec77ee06c050/multidict-6.5.1-py3-none-any.whl", hash = "sha256:895354f4a38f53a1df2cc3fa2223fa714cff2b079a9f018a76cad35e7f0f044c", size = 12185, upload-time = "2025-06-24T22:16:03.816Z" }, +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]] From 3b1cbbca4df71f036f371210879ea40a651ebbc6 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Wed, 20 Aug 2025 11:55:38 +0200 Subject: [PATCH 04/18] Add missing file --- .../known_types/struct.py | 131 ++++++++++++++++++ 1 file changed, 131 insertions(+) create mode 100644 betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py new file mode 100644 index 00000000..829da482 --- /dev/null +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py @@ -0,0 +1,131 @@ +import typing + +import betterproto2 + +from betterproto2_compiler.lib.google.protobuf import ( + ListValue as VanillaListValue, + NullValue, + Struct as VanillaStruct, + Value as VanillaValue, +) + + +class Struct(VanillaStruct): + # TODO typing + @classmethod + def from_dict(cls, value, *, ignore_unknown_fields: bool = False): + assert isinstance(value, dict) + + fields: dict[str, Value] = {} + + for key, val in value.items(): + fields[key] = Value.from_dict(val, ignore_unknown_fields=ignore_unknown_fields) + + return cls(fields=fields) + + # TODO typing + def to_dict( + self, + *, + output_format: betterproto2.OutputFormat = betterproto2.OutputFormat.PROTO_JSON, + casing: betterproto2.Casing = betterproto2.Casing.CAMEL, + include_default_values: bool = False, + ) -> dict[str, typing.Any] | typing.Any: + # If the output format is PYTHON, we should have kept the wrapped type without building the real class + assert output_format == betterproto2.OutputFormat.PROTO_JSON + + return { + key: value.to_dict( + output_format=output_format, casing=casing, include_default_values=include_default_values + ) + for key, value in self.fields.items() + } + + @staticmethod + def from_wrapped(wrapped: betterproto2.JSON) -> "Struct": + return Struct.from_dict(wrapped) + + def to_wrapped(self) -> betterproto2.JSON: + return self.to_dict() + + +class Value(VanillaValue): + # TODO typing + @classmethod + def from_dict(cls, value, *, ignore_unknown_fields: bool = False): + match value: + case bool() as b: + return cls(bool_value=b) + case int() | float() as num: + return cls(number_value=num) + case str() as s: + return cls(string_value=s) + case list() as l: + return cls(list_value=ListValue.from_dict(l)) + case dict() as d: + return cls(struct_value=Struct.from_dict(d)) + case None: + return cls(null_value=NullValue.NULL_VALUE) + case _: + raise ValueError(f"Unknown value type: {type(value)}") + + # TODO typing + def to_dict( + self, + *, + output_format: betterproto2.OutputFormat = betterproto2.OutputFormat.PROTO_JSON, + casing: betterproto2.Casing = betterproto2.Casing.CAMEL, + include_default_values: bool = False, + ) -> dict[str, typing.Any] | typing.Any: + # If the output format is PYTHON, we should have kept the wrapped type without building the real class + assert output_format == betterproto2.OutputFormat.PROTO_JSON + + match self: + case Value(null_value=NullValue.NULL_VALUE): + return None + case Value(bool_value=bool(b)): + return b + case Value(number_value=int(num)) | Value(number_value=float(num)): + return num + case Value(string_value=str(s)): + return s + case Value(list_value=ListValue(values=l)): + return [v.to_dict() for v in l] + case Value(struct_value=Struct(fields=f)): + return {k: v.to_dict() for k, v in f.items()} + + raise ValueError("Invalid value") + + @staticmethod + def from_wrapped(wrapped: betterproto2.JSON) -> "Value": + return Value.from_dict(wrapped) + + def to_wrapped(self) -> betterproto2.JSON: + return self.to_dict() + + +class ListValue(VanillaListValue): + # TODO typing + @classmethod + def from_dict(cls, value, *, ignore_unknown_fields: bool = False): + return cls(values=[Value.from_dict(v) for v in value]) + + # TODO typing + def to_dict( + self, + *, + output_format: betterproto2.OutputFormat = betterproto2.OutputFormat.PROTO_JSON, + casing: betterproto2.Casing = betterproto2.Casing.CAMEL, + include_default_values: bool = False, + ) -> dict[str, typing.Any] | typing.Any: + # If the output format is PYTHON, we should have kept the wrapped type without building the real class + assert output_format == betterproto2.OutputFormat.PROTO_JSON + + return [value.to_dict() for value in self.values] + + @staticmethod + def from_wrapped(wrapped: list[betterproto2.JSON]) -> "ListValue": + return ListValue.from_dict(wrapped) + + def to_wrapped(self) -> list[betterproto2.JSON]: + return self.to_dict() From 65138fcc9a88bb6b4c895efa7294c2583772875a Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Wed, 20 Aug 2025 13:56:38 +0200 Subject: [PATCH 05/18] Make Any's pack a class method --- betterproto2/tests/test_any.py | 13 ++++--------- .../src/betterproto2_compiler/known_types/any.py | 9 ++++++--- 2 files changed, 10 insertions(+), 12 deletions(-) diff --git a/betterproto2/tests/test_any.py b/betterproto2/tests/test_any.py index d25b05d3..b641f4d1 100644 --- a/betterproto2/tests/test_any.py +++ b/betterproto2/tests/test_any.py @@ -1,12 +1,10 @@ def test_any() -> None: - # TODO using a custom message pool will no longer be necessary when the well-known types will be compiled as well from tests.outputs.any.any import Person from tests.outputs.any.google.protobuf import Any person = Person(first_name="John", last_name="Smith") - any = Any() - any.pack(person) + any = Any.pack(person) new_any = Any.parse(bytes(any)) @@ -19,13 +17,11 @@ def test_any_to_dict() -> None: person = Person(first_name="John", last_name="Smith") - any = Any() - # TODO test with include defautl value - assert any.to_dict() == {"@type": ""} + assert Any().to_dict() == {"@type": ""} # Pack an object inside - any.pack(person) + any = Any.pack(person) assert any.to_dict() == { "@type": "type.googleapis.com/any.Person", @@ -34,8 +30,7 @@ def test_any_to_dict() -> None: } # Pack again in another Any - any2 = Any() - any2.pack(any) + any2 = Any.pack(any) assert any2.to_dict() == { "@type": "type.googleapis.com/google.protobuf.Any", diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py index 325c6d11..37d9780c 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py @@ -8,7 +8,8 @@ class Any(VanillaAny): - def pack(self, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> None: + @classmethod + def pack(cls, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> typing.Self: """ Pack the given message in the `Any` object. @@ -17,8 +18,10 @@ def pack(self, message: betterproto2.Message, message_pool: "betterproto2.Messag """ message_pool = message_pool or default_message_pool - self.type_url = message_pool.type_to_url[type(message)] - self.value = bytes(message) + type_url = message_pool.type_to_url[type(message)] + value = bytes(message) + + return cls(type_url=type_url, value=value) def unpack(self, message_pool: "betterproto2.MessagePool | None" = None) -> betterproto2.Message | None: """ From d60be350494315e2beafe23bd0414f130f2c0fde Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Wed, 20 Aug 2025 15:01:12 +0200 Subject: [PATCH 06/18] Fix Any.from_dict --- betterproto2/tests/test_any.py | 4 ++++ .../betterproto2_compiler/known_types/__init__.py | 2 +- .../src/betterproto2_compiler/known_types/any.py | 15 +++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/betterproto2/tests/test_any.py b/betterproto2/tests/test_any.py index b641f4d1..c8d2f77a 100644 --- a/betterproto2/tests/test_any.py +++ b/betterproto2/tests/test_any.py @@ -29,6 +29,8 @@ def test_any_to_dict() -> None: "lastName": "Smith", } + assert Any.from_dict(any.to_dict()) == any + # Pack again in another Any any2 = Any.pack(any) @@ -36,3 +38,5 @@ def test_any_to_dict() -> None: "@type": "type.googleapis.com/google.protobuf.Any", "value": {"@type": "type.googleapis.com/any.Person", "firstName": "John", "lastName": "Smith"}, } + + assert Any.from_dict(any2.to_dict()) == any2 diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py index 81c160a6..fe452110 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py @@ -20,7 +20,7 @@ # The source code of the method is read from the `known_types` folder. If imports are needed, they can be directly added # to the template file: they will automatically be removed if not necessary. KNOWN_METHODS: dict[tuple[str, str], list[Callable]] = { - ("google.protobuf", "Any"): [Any.pack, Any.unpack, Any.to_dict], + ("google.protobuf", "Any"): [Any.pack, Any.unpack, Any.to_dict, Any.from_dict], ("google.protobuf", "Timestamp"): [ Timestamp.from_datetime, Timestamp.to_datetime, diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py index 37d9780c..6a48bc67 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py @@ -57,3 +57,18 @@ def to_dict(self, **kwargs) -> dict[str, typing.Any]: output["value"] = value.to_dict(**kwargs) return output + + # TODO typing + @classmethod + def from_dict(cls, value, *, ignore_unknown_fields: bool = False): + value = dict(value) # Make a copy + + type_url = value.pop("@type", None) + msg_cls = default_message_pool.url_to_type.get(type_url, None) + + if not msg_cls: + raise TypeError(f"Can't unpack unregistered type: {type_url}") + + return cls( + type_url=type_url, value=bytes(msg_cls.from_dict(value, ignore_unknown_fields=ignore_unknown_fields)) + ) From db3ad92ecf6d32f895b72cc663495a3fb4b24013 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 22 Aug 2025 09:57:04 +0200 Subject: [PATCH 07/18] Fix remaining problems --- betterproto2/src/betterproto2/__init__.py | 17 ++++---- betterproto2/tests/test_any.py | 2 + betterproto2/tests/test_pickling.py | 30 ++++--------- betterproto2/tests/test_struct.py | 42 +++++++++++++++---- .../betterproto2_compiler/known_types/any.py | 5 ++- .../known_types/struct.py | 38 ++++------------- 6 files changed, 66 insertions(+), 68 deletions(-) diff --git a/betterproto2/src/betterproto2/__init__.py b/betterproto2/src/betterproto2/__init__.py index 3cad9011..de1a9406 100644 --- a/betterproto2/src/betterproto2/__init__.py +++ b/betterproto2/src/betterproto2/__init__.py @@ -24,7 +24,7 @@ from enum import IntEnum from io import BytesIO from itertools import count -from typing import TYPE_CHECKING, Any, ClassVar, get_type_hints +from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias, get_type_hints from typing_extensions import Self @@ -139,7 +139,7 @@ NAN = "NaN" # For Struct support -JSON = int | float | bool | str | list["JSON"] | dict[str, "JSON"] | None +JSON: TypeAlias = int | float | bool | str | list[Any] | dict[str, Any] | None class Casing(builtin_enum.Enum): @@ -782,7 +782,7 @@ def __bytes__(self) -> bytes: # Default (zero) values are not serialized. continue - if isinstance(value, list): + if meta.repeated: if meta.proto_type in PACKED_TYPES: # Packed lists look like a length-delimited field. First, # preprocess/encode each value into a buffer and then @@ -805,9 +805,8 @@ def __bytes__(self) -> bytes: or b"\n\x00" ) - elif isinstance(value, dict): + elif meta.map_meta: for k, v in value.items(): - assert meta.map_meta sk = _serialize_single(1, meta.map_meta[0].proto_type, k) sv = _serialize_single(2, meta.map_meta[1].proto_type, v, unwrap=meta.map_meta[1].unwrap) stream.write(_serialize_single(meta.number, meta.proto_type, sk + sv)) @@ -947,8 +946,10 @@ def load( meta = proto_meta.meta_by_field_name[field_name] + is_packed_repeated = parsed.wire_type == WIRE_LEN_DELIM and meta.proto_type in PACKED_TYPES + value: Any - if parsed.wire_type == WIRE_LEN_DELIM and meta.proto_type in PACKED_TYPES: + if is_packed_repeated: # This is a packed repeated field. pos = 0 value = [] @@ -972,8 +973,8 @@ def load( if meta.proto_type == TYPE_MAP: # Value represents a single key/value pair entry in the map. current[value.key] = value.value - elif isinstance(current, list): - if isinstance(value, list): + elif meta.repeated: + if is_packed_repeated: current.extend(value) else: current.append(value) diff --git a/betterproto2/tests/test_any.py b/betterproto2/tests/test_any.py index c8d2f77a..00910261 100644 --- a/betterproto2/tests/test_any.py +++ b/betterproto2/tests/test_any.py @@ -30,6 +30,7 @@ def test_any_to_dict() -> None: } assert Any.from_dict(any.to_dict()) == any + assert Any.parse(bytes(any)) == any # Pack again in another Any any2 = Any.pack(any) @@ -40,3 +41,4 @@ def test_any_to_dict() -> None: } assert Any.from_dict(any2.to_dict()) == any2 + assert Any.parse(bytes(any2)) == any2 diff --git a/betterproto2/tests/test_pickling.py b/betterproto2/tests/test_pickling.py index aba2cf54..01fa4246 100644 --- a/betterproto2/tests/test_pickling.py +++ b/betterproto2/tests/test_pickling.py @@ -17,19 +17,13 @@ def complex_msg(): fe=Fe(abc="1"), nested_data=NestedData( struct_foo={ - "foo": google.Struct( - fields={ - "hello": google.Value(list_value=google.ListValue(values=[google.Value(string_value="world")])) - } - ), - }, - map_str_any_bar={ - "key": google.Any(value=b"value"), + "foo": { + "hello": [["world"]], + } }, ), mapping={ - "message": google.Any(value=bytes(Fi(abc="hi"))), - "string": google.Any(value=b"howdy"), + "message": google.Any.pack(Fi(abc="hi")), }, ) @@ -40,9 +34,8 @@ def test_pickling_complex_message(): assert msg == deser assert msg.fe.abc == "1" assert msg.is_set("fi") is not True - assert msg.mapping["message"] == google.Any(value=bytes(Fi(abc="hi"))) - assert msg.mapping["string"].value.decode() == "howdy" - assert msg.nested_data.struct_foo["foo"].fields["hello"].list_value.values[0].string_value == "world" + assert msg.mapping["message"] == google.Any.pack(Fi(abc="hi")) + assert msg.nested_data.struct_foo["foo"]["hello"][0][0] == "world" def test_recursive_message_defaults(): @@ -51,11 +44,7 @@ def test_recursive_message_defaults(): msg = RecursiveMessage(name="bob", intermediate=Intermediate(42)) msg = unpickled(msg) - # set values are as expected assert msg == RecursiveMessage(name="bob", intermediate=Intermediate(42)) - - # lazy initialized works modifies the message - assert msg != RecursiveMessage(name="bob", intermediate=Intermediate(42), child=RecursiveMessage(name="jude")) msg.child = RecursiveMessage(child=RecursiveMessage(name="jude")) assert msg == RecursiveMessage( name="bob", @@ -104,7 +93,6 @@ def use_cache(): msg = use_cache() assert use_cache.calls == 1 # The message is only ever built once assert msg.fe.abc == "1" - assert msg.is_set("fi") is not True - assert msg.mapping["message"] == google.Any(value=bytes(Fi(abc="hi"))) - assert msg.mapping["string"].value.decode() == "howdy" - assert msg.nested_data.struct_foo["foo"].fields["hello"].list_value.values[0].string_value == "world" + assert not msg.is_set("fi") + assert msg.mapping["message"] == google.Any.pack(Fi(abc="hi")) + assert msg.nested_data.struct_foo["foo"]["hello"][0][0] == "world" diff --git a/betterproto2/tests/test_struct.py b/betterproto2/tests/test_struct.py index 33933413..d7446183 100644 --- a/betterproto2/tests/test_struct.py +++ b/betterproto2/tests/test_struct.py @@ -1,14 +1,14 @@ def test_struct_to_dict(): - from tests.outputs.google.google.protobuf import ListValue, NullValue, Struct, Value + from tests.outputs.google.google.protobuf import Struct struct = Struct( fields={ - "null_field": Value(null_value=NullValue.NULL_VALUE), - "number_field": Value(number_value=12), - "string_field": Value(string_value="test"), - "bool_field": Value(bool_value=True), - "struct_field": Value(struct_value=Struct(fields={"x": Value(string_value="abc")})), - "list_field": Value(list_value=ListValue(values=[Value(number_value=42), Value(bool_value=False)])), + "null_field": None, + "number_field": 12, + "string_field": "test", + "bool_field": True, + "struct_field": {"x": "abc"}, + "list_field": [42, False], } ) @@ -20,3 +20,31 @@ def test_struct_to_dict(): "struct_field": {"x": "abc"}, "list_field": [42, False], } + + assert Struct.from_dict(struct.to_dict()) == struct + + +def test_listvalue_to_dict(): + from tests.outputs.google.google.protobuf import ListValue + + list_value = ListValue(values=[42, False, {}]) + + assert list_value.to_dict() == [42, False, {}] + assert ListValue.from_dict(list_value.to_dict()) == list_value + + +def test_nullvalue(): + from tests.outputs.google.google.protobuf import NullValue, Value + + null_value = NullValue.NULL_VALUE + + assert bytes(Value(null_value=null_value)) == b"\x08" + + +def test_value_to_dict(): + from tests.outputs.google.google.protobuf import Value + + value = Value(list_value=[1, 2, False]) + + assert value.to_dict() == [1, 2, False] + assert Value.from_dict(value.to_dict()) == value diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py index 6a48bc67..aed19f38 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py @@ -51,7 +51,7 @@ def to_dict(self, **kwargs) -> dict[str, typing.Any]: if value is None: return output - if type(value).to_dict == betterproto2.Message.to_dict: + if type(value).to_dict is betterproto2.Message.to_dict: output.update(value.to_dict(**kwargs)) else: output["value"] = value.to_dict(**kwargs) @@ -69,6 +69,9 @@ def from_dict(cls, value, *, ignore_unknown_fields: bool = False): if not msg_cls: raise TypeError(f"Can't unpack unregistered type: {type_url}") + if msg_cls.to_dict is not betterproto2.Message.to_dict: + value = value["value"] + return cls( type_url=type_url, value=bytes(msg_cls.from_dict(value, ignore_unknown_fields=ignore_unknown_fields)) ) diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py index 829da482..27331f79 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py @@ -16,12 +16,7 @@ class Struct(VanillaStruct): def from_dict(cls, value, *, ignore_unknown_fields: bool = False): assert isinstance(value, dict) - fields: dict[str, Value] = {} - - for key, val in value.items(): - fields[key] = Value.from_dict(val, ignore_unknown_fields=ignore_unknown_fields) - - return cls(fields=fields) + return cls(fields=value) # TODO typing def to_dict( @@ -34,12 +29,7 @@ def to_dict( # If the output format is PYTHON, we should have kept the wrapped type without building the real class assert output_format == betterproto2.OutputFormat.PROTO_JSON - return { - key: value.to_dict( - output_format=output_format, casing=casing, include_default_values=include_default_values - ) - for key, value in self.fields.items() - } + return self.fields @staticmethod def from_wrapped(wrapped: betterproto2.JSON) -> "Struct": @@ -61,9 +51,9 @@ def from_dict(cls, value, *, ignore_unknown_fields: bool = False): case str() as s: return cls(string_value=s) case list() as l: - return cls(list_value=ListValue.from_dict(l)) + return cls(list_value=list(l)) case dict() as d: - return cls(struct_value=Struct.from_dict(d)) + return cls(struct_value=dict(d)) case None: return cls(null_value=NullValue.NULL_VALUE) case _: @@ -80,21 +70,7 @@ def to_dict( # If the output format is PYTHON, we should have kept the wrapped type without building the real class assert output_format == betterproto2.OutputFormat.PROTO_JSON - match self: - case Value(null_value=NullValue.NULL_VALUE): - return None - case Value(bool_value=bool(b)): - return b - case Value(number_value=int(num)) | Value(number_value=float(num)): - return num - case Value(string_value=str(s)): - return s - case Value(list_value=ListValue(values=l)): - return [v.to_dict() for v in l] - case Value(struct_value=Struct(fields=f)): - return {k: v.to_dict() for k, v in f.items()} - - raise ValueError("Invalid value") + return betterproto2.which_one_of(self, "kind")[1] @staticmethod def from_wrapped(wrapped: betterproto2.JSON) -> "Value": @@ -108,7 +84,7 @@ class ListValue(VanillaListValue): # TODO typing @classmethod def from_dict(cls, value, *, ignore_unknown_fields: bool = False): - return cls(values=[Value.from_dict(v) for v in value]) + return cls(values=list(value)) # TODO typing def to_dict( @@ -121,7 +97,7 @@ def to_dict( # If the output format is PYTHON, we should have kept the wrapped type without building the real class assert output_format == betterproto2.OutputFormat.PROTO_JSON - return [value.to_dict() for value in self.values] + return self.values @staticmethod def from_wrapped(wrapped: list[betterproto2.JSON]) -> "ListValue": From b1e711f85bb071d55278073af5b662a04a234418 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 22 Aug 2025 09:58:37 +0200 Subject: [PATCH 08/18] Update compiler lib --- .../lib/google/protobuf/__init__.py | 131 ++++++++++++++++-- 1 file changed, 122 insertions(+), 9 deletions(-) diff --git a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py index a0108907..8da737c7 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py @@ -866,7 +866,8 @@ class Any(betterproto2.Message): Must be a valid serialized protocol buffer of the above specified type. """ - def pack(self, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> None: + @classmethod + def pack(cls, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> typing.Self: """ Pack the given message in the `Any` object. @@ -875,8 +876,10 @@ def pack(self, message: betterproto2.Message, message_pool: "betterproto2.Messag """ message_pool = message_pool or default_message_pool - self.type_url = message_pool.type_to_url[type(message)] - self.value = bytes(message) + type_url = message_pool.type_to_url[type(message)] + value = bytes(message) + + return cls(type_url=type_url, value=value) def unpack(self, message_pool: "betterproto2.MessagePool | None" = None) -> betterproto2.Message | None: """ @@ -906,13 +909,30 @@ def to_dict(self, **kwargs) -> dict[str, typing.Any]: if value is None: return output - if type(value).to_dict == betterproto2.Message.to_dict: + if type(value).to_dict is betterproto2.Message.to_dict: output.update(value.to_dict(**kwargs)) else: output["value"] = value.to_dict(**kwargs) return output + @classmethod + def from_dict(cls, value, *, ignore_unknown_fields: bool = False): + value = dict(value) # Make a copy + + type_url = value.pop("@type", None) + msg_cls = default_message_pool.url_to_type.get(type_url, None) + + if not msg_cls: + raise TypeError(f"Can't unpack unregistered type: {type_url}") + + if msg_cls.to_dict is not betterproto2.Message.to_dict: + value = value["value"] + + return cls( + type_url=type_url, value=bytes(msg_cls.from_dict(value, ignore_unknown_fields=ignore_unknown_fields)) + ) + default_message_pool.register_message("google.protobuf", "Any", Any) @@ -2742,11 +2762,36 @@ class ListValue(betterproto2.Message): The JSON representation for `ListValue` is JSON array. """ - values: "list[Value]" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, repeated=True) + values: "list[betterproto2.JSON]" = betterproto2.field( + 1, betterproto2.TYPE_MESSAGE, unwrap=lambda: Value, repeated=True + ) """ Repeated field of dynamically typed values. """ + @classmethod + def from_dict(cls, value, *, ignore_unknown_fields: bool = False): + return cls(values=list(value)) + + def to_dict( + self, + *, + output_format: betterproto2.OutputFormat = betterproto2.OutputFormat.PROTO_JSON, + casing: betterproto2.Casing = betterproto2.Casing.CAMEL, + include_default_values: bool = False, + ) -> dict[str, typing.Any] | typing.Any: + # If the output format is PYTHON, we should have kept the wrapped type without building the real class + assert output_format == betterproto2.OutputFormat.PROTO_JSON + + return self.values + + @staticmethod + def from_wrapped(wrapped: list[betterproto2.JSON]) -> "ListValue": + return ListValue.from_dict(wrapped) + + def to_wrapped(self) -> list[betterproto2.JSON]: + return self.to_dict() + default_message_pool.register_message("google.protobuf", "ListValue", ListValue) @@ -3393,13 +3438,40 @@ class Struct(betterproto2.Message): The JSON representation for `Struct` is JSON object. """ - fields: "dict[str, Value]" = betterproto2.field( - 1, betterproto2.TYPE_MAP, map_meta=betterproto2.map_meta(betterproto2.TYPE_STRING, betterproto2.TYPE_MESSAGE) + fields: "dict[str, betterproto2.JSON]" = betterproto2.field( + 1, + betterproto2.TYPE_MAP, + map_meta=betterproto2.map_meta(betterproto2.TYPE_STRING, betterproto2.TYPE_MESSAGE, unwrap_2=lambda: Value), ) """ Unordered map of dynamically typed values. """ + @classmethod + def from_dict(cls, value, *, ignore_unknown_fields: bool = False): + assert isinstance(value, dict) + + return cls(fields=value) + + def to_dict( + self, + *, + output_format: betterproto2.OutputFormat = betterproto2.OutputFormat.PROTO_JSON, + casing: betterproto2.Casing = betterproto2.Casing.CAMEL, + include_default_values: bool = False, + ) -> dict[str, typing.Any] | typing.Any: + # If the output format is PYTHON, we should have kept the wrapped type without building the real class + assert output_format == betterproto2.OutputFormat.PROTO_JSON + + return self.fields + + @staticmethod + def from_wrapped(wrapped: betterproto2.JSON) -> "Struct": + return Struct.from_dict(wrapped) + + def to_wrapped(self) -> betterproto2.JSON: + return self.to_dict() + default_message_pool.register_message("google.protobuf", "Struct", Struct) @@ -3798,15 +3870,56 @@ class Value(betterproto2.Message): Represents a boolean value. """ - struct_value: "Struct | None" = betterproto2.field(5, betterproto2.TYPE_MESSAGE, optional=True, group="kind") + struct_value: "dict[str, betterproto2.JSON] | None" = betterproto2.field( + 5, betterproto2.TYPE_MESSAGE, unwrap=lambda: Struct, optional=True, group="kind" + ) """ Represents a structured value. """ - list_value: "ListValue | None" = betterproto2.field(6, betterproto2.TYPE_MESSAGE, optional=True, group="kind") + list_value: "list[betterproto2.JSON] | None" = betterproto2.field( + 6, betterproto2.TYPE_MESSAGE, unwrap=lambda: ListValue, optional=True, group="kind" + ) """ Represents a repeated `Value`. """ + @classmethod + def from_dict(cls, value, *, ignore_unknown_fields: bool = False): + match value: + case bool() as b: + return cls(bool_value=b) + case int() | float() as num: + return cls(number_value=num) + case str() as s: + return cls(string_value=s) + case list() as l: + return cls(list_value=list(l)) + case dict() as d: + return cls(struct_value=dict(d)) + case None: + return cls(null_value=NullValue.NULL_VALUE) + case _: + raise ValueError(f"Unknown value type: {type(value)}") + + def to_dict( + self, + *, + output_format: betterproto2.OutputFormat = betterproto2.OutputFormat.PROTO_JSON, + casing: betterproto2.Casing = betterproto2.Casing.CAMEL, + include_default_values: bool = False, + ) -> dict[str, typing.Any] | typing.Any: + # If the output format is PYTHON, we should have kept the wrapped type without building the real class + assert output_format == betterproto2.OutputFormat.PROTO_JSON + + return betterproto2.which_one_of(self, "kind")[1] + + @staticmethod + def from_wrapped(wrapped: betterproto2.JSON) -> "Value": + return Value.from_dict(wrapped) + + def to_wrapped(self) -> betterproto2.JSON: + return self.to_dict() + default_message_pool.register_message("google.protobuf", "Value", Value) From 1f61d522510a3b13588aabe5a7665fef0d44c57c Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 22 Aug 2025 10:09:27 +0200 Subject: [PATCH 09/18] Fix test --- betterproto2/tests/test_struct.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/betterproto2/tests/test_struct.py b/betterproto2/tests/test_struct.py index d7446183..c6987c21 100644 --- a/betterproto2/tests/test_struct.py +++ b/betterproto2/tests/test_struct.py @@ -38,7 +38,7 @@ def test_nullvalue(): null_value = NullValue.NULL_VALUE - assert bytes(Value(null_value=null_value)) == b"\x08" + assert bytes(Value(null_value=null_value)) == b"\x08\x00" def test_value_to_dict(): From 63fb3627cfecfbfb0b611b6d21dee80551694723 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 22 Aug 2025 10:10:52 +0200 Subject: [PATCH 10/18] Fix typechecking --- .../src/betterproto2_compiler/known_types/struct.py | 2 +- .../src/betterproto2_compiler/lib/google/protobuf/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py index 27331f79..aefceb59 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py @@ -104,4 +104,4 @@ def from_wrapped(wrapped: list[betterproto2.JSON]) -> "ListValue": return ListValue.from_dict(wrapped) def to_wrapped(self) -> list[betterproto2.JSON]: - return self.to_dict() + return self.values diff --git a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py index 8da737c7..7bc1ff35 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py @@ -2790,7 +2790,7 @@ def from_wrapped(wrapped: list[betterproto2.JSON]) -> "ListValue": return ListValue.from_dict(wrapped) def to_wrapped(self) -> list[betterproto2.JSON]: - return self.to_dict() + return self.values default_message_pool.register_message("google.protobuf", "ListValue", ListValue) From 25b22b94ae6c964283ea053ba1417ae15cd76f63 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 22 Aug 2025 10:12:39 +0200 Subject: [PATCH 11/18] Remove typing.Self --- .../src/betterproto2_compiler/known_types/any.py | 2 +- .../src/betterproto2_compiler/lib/google/protobuf/__init__.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py index aed19f38..14d179a2 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py @@ -9,7 +9,7 @@ class Any(VanillaAny): @classmethod - def pack(cls, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> typing.Self: + def pack(cls, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> "Any": """ Pack the given message in the `Any` object. diff --git a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py index 7bc1ff35..b99074d0 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py @@ -867,7 +867,7 @@ class Any(betterproto2.Message): """ @classmethod - def pack(cls, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> typing.Self: + def pack(cls, message: betterproto2.Message, message_pool: "betterproto2.MessagePool | None" = None) -> "Any": """ Pack the given message in the `Any` object. From ffb8a91b8e4f8079a070308fbc80e1ac99d3fb46 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 22 Aug 2025 21:53:39 +0200 Subject: [PATCH 12/18] Add more tests --- betterproto2/tests/test_inputs.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/betterproto2/tests/test_inputs.py b/betterproto2/tests/test_inputs.py index 6db984c9..787319d8 100644 --- a/betterproto2/tests/test_inputs.py +++ b/betterproto2/tests/test_inputs.py @@ -107,13 +107,11 @@ def reset_sys_path(): ["googletypes_struct/googletypes_struct.json"], "googletypes_struct.googletypes_struct", "googletypes_struct_reference.googletypes_struct_pb2", - xfail=True, ), TestCase( ["googletypes_value/googletypes_value.json"], "googletypes_value.googletypes_value", "googletypes_value_reference.googletypes_value_pb2", - xfail=True, ), TestCase(["int32/int32.json"], "int32.int32", "int32_reference.int32_pb2"), TestCase(["map/map.json"], "map.map", "map_reference.map_pb2"), From 20e5ae352a1d38048dd3b885372cefc4b7449e47 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 22 Aug 2025 21:55:10 +0200 Subject: [PATCH 13/18] Remove useless file --- betterproto2/tests/inputs/config.py | 21 --------------------- 1 file changed, 21 deletions(-) delete mode 100644 betterproto2/tests/inputs/config.py diff --git a/betterproto2/tests/inputs/config.py b/betterproto2/tests/inputs/config.py deleted file mode 100644 index cd5c11c9..00000000 --- a/betterproto2/tests/inputs/config.py +++ /dev/null @@ -1,21 +0,0 @@ -# Test cases that are expected to fail, e.g. unimplemented features or bug-fixes. -# Remove from list when fixed. -xfail = { - "namespace_keywords", # 70 - "googletypes_struct", # 9 - "googletypes_value", # 9 -} - -services = { - "googletypes_request", - "googletypes_response", - "googletypes_response_embedded", - "service", - "service_separate_packages", - "import_service_input_message", - "googletypes_service_returns_empty", - "googletypes_service_returns_googletype", - "example_service", - "empty_service", - "service_uppercase", -} From 8a73739d0f8c774c04408e9133f1a3f9ad1fde52 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 22 Aug 2025 23:20:20 +0200 Subject: [PATCH 14/18] Remove wrapping for structs --- betterproto2/src/betterproto2/__init__.py | 7 +- betterproto2/tests/test_pickling.py | 12 +-- betterproto2/tests/test_struct.py | 12 +-- .../known_types/__init__.py | 9 --- .../known_types/struct.py | 62 +++++++------- .../lib/google/protobuf/__init__.py | 80 +++++++++---------- 6 files changed, 89 insertions(+), 93 deletions(-) diff --git a/betterproto2/src/betterproto2/__init__.py b/betterproto2/src/betterproto2/__init__.py index de1a9406..3bae10fd 100644 --- a/betterproto2/src/betterproto2/__init__.py +++ b/betterproto2/src/betterproto2/__init__.py @@ -1146,7 +1146,12 @@ def _from_dict_init(cls, mapping: Mapping[str, Any] | Any, *, ignore_unknown_fie raise KeyError(f"Unknown field '{field_name}' in message {cls.__name__}.") from None if value is None: - continue + name, module = field_cls.__name__, field_cls.__module__ + + # Edge case: None shouldn't be ignored for google.protobuf.Value + # See https://protobuf.dev/programming-guides/json/ + if not (module.endswith("google.protobuf") and name == "Value"): + continue if meta.proto_type == TYPE_MESSAGE: if meta.repeated: diff --git a/betterproto2/tests/test_pickling.py b/betterproto2/tests/test_pickling.py index 01fa4246..4f8ece88 100644 --- a/betterproto2/tests/test_pickling.py +++ b/betterproto2/tests/test_pickling.py @@ -17,9 +17,11 @@ def complex_msg(): fe=Fe(abc="1"), nested_data=NestedData( struct_foo={ - "foo": { - "hello": [["world"]], - } + "foo": google.Struct.from_dict( + { + "hello": [["world"]], + } + ), }, ), mapping={ @@ -35,7 +37,7 @@ def test_pickling_complex_message(): assert msg.fe.abc == "1" assert msg.is_set("fi") is not True assert msg.mapping["message"] == google.Any.pack(Fi(abc="hi")) - assert msg.nested_data.struct_foo["foo"]["hello"][0][0] == "world" + assert msg.nested_data.struct_foo["foo"].to_dict()["hello"][0][0] == "world" def test_recursive_message_defaults(): @@ -95,4 +97,4 @@ def use_cache(): assert msg.fe.abc == "1" assert not msg.is_set("fi") assert msg.mapping["message"] == google.Any.pack(Fi(abc="hi")) - assert msg.nested_data.struct_foo["foo"]["hello"][0][0] == "world" + assert msg.nested_data.struct_foo["foo"].to_dict()["hello"][0][0] == "world" diff --git a/betterproto2/tests/test_struct.py b/betterproto2/tests/test_struct.py index c6987c21..5eecfd27 100644 --- a/betterproto2/tests/test_struct.py +++ b/betterproto2/tests/test_struct.py @@ -1,14 +1,14 @@ def test_struct_to_dict(): from tests.outputs.google.google.protobuf import Struct - struct = Struct( - fields={ + struct = Struct.from_dict( + { "null_field": None, "number_field": 12, "string_field": "test", "bool_field": True, "struct_field": {"x": "abc"}, - "list_field": [42, False], + "list_field": [42, False, None], } ) @@ -18,7 +18,7 @@ def test_struct_to_dict(): "string_field": "test", "bool_field": True, "struct_field": {"x": "abc"}, - "list_field": [42, False], + "list_field": [42, False, None], } assert Struct.from_dict(struct.to_dict()) == struct @@ -27,7 +27,7 @@ def test_struct_to_dict(): def test_listvalue_to_dict(): from tests.outputs.google.google.protobuf import ListValue - list_value = ListValue(values=[42, False, {}]) + list_value = ListValue.from_dict([42, False, {}]) assert list_value.to_dict() == [42, False, {}] assert ListValue.from_dict(list_value.to_dict()) == list_value @@ -44,7 +44,7 @@ def test_nullvalue(): def test_value_to_dict(): from tests.outputs.google.google.protobuf import Value - value = Value(list_value=[1, 2, False]) + value = Value.from_dict([1, 2, False]) assert value.to_dict() == [1, 2, False] assert Value.from_dict(value.to_dict()) == value diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py index fe452110..5ca6e9e4 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/__init__.py @@ -96,20 +96,14 @@ ("google.protobuf", "Struct"): [ Struct.from_dict, Struct.to_dict, - Struct.from_wrapped, - Struct.to_wrapped, ], ("google.protobuf", "ListValue"): [ ListValue.from_dict, ListValue.to_dict, - ListValue.from_wrapped, - ListValue.to_wrapped, ], ("google.protobuf", "Value"): [ Value.from_dict, Value.to_dict, - Value.from_wrapped, - Value.to_wrapped, ], } @@ -126,7 +120,4 @@ ("google.protobuf", "BytesValue"): "bytes", ("google.protobuf", "Timestamp"): "datetime.datetime", ("google.protobuf", "Duration"): "datetime.timedelta", - ("google.protobuf", "Struct"): "dict[str, betterproto2.JSON]", - ("google.protobuf", "ListValue"): "list[betterproto2.JSON]", - ("google.protobuf", "Value"): "betterproto2.JSON", } diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py index aefceb59..2cf93398 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py @@ -16,7 +16,12 @@ class Struct(VanillaStruct): def from_dict(cls, value, *, ignore_unknown_fields: bool = False): assert isinstance(value, dict) - return cls(fields=value) + fields: dict[str, betterproto2.JSON] = {} + + for key, val in value.items(): + fields[key] = Value.from_dict(val) + + return cls(fields=fields) # TODO typing def to_dict( @@ -29,16 +34,16 @@ def to_dict( # If the output format is PYTHON, we should have kept the wrapped type without building the real class assert output_format == betterproto2.OutputFormat.PROTO_JSON - return self.fields - - @staticmethod - def from_wrapped(wrapped: betterproto2.JSON) -> "Struct": - return Struct.from_dict(wrapped) - - def to_wrapped(self) -> betterproto2.JSON: - return self.to_dict() + return { + key: value.to_dict( + output_format=output_format, casing=casing, include_default_values=include_default_values + ) + for key, value in self.fields.items() + } +# We can't use the unwrap mechanism to support directly using a Python object due to the None case: it would then be +# impossible to distinguish between the absence of the message and a None value. class Value(VanillaValue): # TODO typing @classmethod @@ -51,13 +56,12 @@ def from_dict(cls, value, *, ignore_unknown_fields: bool = False): case str() as s: return cls(string_value=s) case list() as l: - return cls(list_value=list(l)) + return cls(list_value=ListValue.from_dict(l)) case dict() as d: - return cls(struct_value=dict(d)) + return cls(struct_value=Struct.from_dict(d)) case None: return cls(null_value=NullValue.NULL_VALUE) - case _: - raise ValueError(f"Unknown value type: {type(value)}") + raise ValueError(f"Unknown value type: {type(value)}") # TODO typing def to_dict( @@ -70,21 +74,28 @@ def to_dict( # If the output format is PYTHON, we should have kept the wrapped type without building the real class assert output_format == betterproto2.OutputFormat.PROTO_JSON - return betterproto2.which_one_of(self, "kind")[1] + match self: + case Value(null_value=NullValue.NULL_VALUE): + return None + case Value(bool_value=bool(b)): + return b + case Value(number_value=int(num)) | Value(number_value=float(num)): + return num + case Value(string_value=str(s)): + return s + case Value(list_value=ListValue(values=l)): + return [v.to_dict() for v in l] + case Value(struct_value=Struct(fields=f)): + return {k: v.to_dict() for k, v in f.items()} - @staticmethod - def from_wrapped(wrapped: betterproto2.JSON) -> "Value": - return Value.from_dict(wrapped) - - def to_wrapped(self) -> betterproto2.JSON: - return self.to_dict() + raise ValueError("Invalid value") class ListValue(VanillaListValue): # TODO typing @classmethod def from_dict(cls, value, *, ignore_unknown_fields: bool = False): - return cls(values=list(value)) + return cls(values=[Value.from_dict(v) for v in value]) # TODO typing def to_dict( @@ -97,11 +108,4 @@ def to_dict( # If the output format is PYTHON, we should have kept the wrapped type without building the real class assert output_format == betterproto2.OutputFormat.PROTO_JSON - return self.values - - @staticmethod - def from_wrapped(wrapped: list[betterproto2.JSON]) -> "ListValue": - return ListValue.from_dict(wrapped) - - def to_wrapped(self) -> list[betterproto2.JSON]: - return self.values + return [value.to_dict() for value in self.values] diff --git a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py index b99074d0..2e0192a4 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py @@ -2762,16 +2762,14 @@ class ListValue(betterproto2.Message): The JSON representation for `ListValue` is JSON array. """ - values: "list[betterproto2.JSON]" = betterproto2.field( - 1, betterproto2.TYPE_MESSAGE, unwrap=lambda: Value, repeated=True - ) + values: "list[Value]" = betterproto2.field(1, betterproto2.TYPE_MESSAGE, repeated=True) """ Repeated field of dynamically typed values. """ @classmethod def from_dict(cls, value, *, ignore_unknown_fields: bool = False): - return cls(values=list(value)) + return cls(values=[Value.from_dict(v) for v in value]) def to_dict( self, @@ -2783,14 +2781,7 @@ def to_dict( # If the output format is PYTHON, we should have kept the wrapped type without building the real class assert output_format == betterproto2.OutputFormat.PROTO_JSON - return self.values - - @staticmethod - def from_wrapped(wrapped: list[betterproto2.JSON]) -> "ListValue": - return ListValue.from_dict(wrapped) - - def to_wrapped(self) -> list[betterproto2.JSON]: - return self.values + return [value.to_dict() for value in self.values] default_message_pool.register_message("google.protobuf", "ListValue", ListValue) @@ -3438,10 +3429,8 @@ class Struct(betterproto2.Message): The JSON representation for `Struct` is JSON object. """ - fields: "dict[str, betterproto2.JSON]" = betterproto2.field( - 1, - betterproto2.TYPE_MAP, - map_meta=betterproto2.map_meta(betterproto2.TYPE_STRING, betterproto2.TYPE_MESSAGE, unwrap_2=lambda: Value), + fields: "dict[str, Value]" = betterproto2.field( + 1, betterproto2.TYPE_MAP, map_meta=betterproto2.map_meta(betterproto2.TYPE_STRING, betterproto2.TYPE_MESSAGE) ) """ Unordered map of dynamically typed values. @@ -3451,7 +3440,12 @@ class Struct(betterproto2.Message): def from_dict(cls, value, *, ignore_unknown_fields: bool = False): assert isinstance(value, dict) - return cls(fields=value) + fields: dict[str, betterproto2.JSON] = {} + + for key, val in value.items(): + fields[key] = Value.from_dict(val) + + return cls(fields=fields) def to_dict( self, @@ -3463,14 +3457,12 @@ def to_dict( # If the output format is PYTHON, we should have kept the wrapped type without building the real class assert output_format == betterproto2.OutputFormat.PROTO_JSON - return self.fields - - @staticmethod - def from_wrapped(wrapped: betterproto2.JSON) -> "Struct": - return Struct.from_dict(wrapped) - - def to_wrapped(self) -> betterproto2.JSON: - return self.to_dict() + return { + key: value.to_dict( + output_format=output_format, casing=casing, include_default_values=include_default_values + ) + for key, value in self.fields.items() + } default_message_pool.register_message("google.protobuf", "Struct", Struct) @@ -3870,16 +3862,12 @@ class Value(betterproto2.Message): Represents a boolean value. """ - struct_value: "dict[str, betterproto2.JSON] | None" = betterproto2.field( - 5, betterproto2.TYPE_MESSAGE, unwrap=lambda: Struct, optional=True, group="kind" - ) + struct_value: "Struct | None" = betterproto2.field(5, betterproto2.TYPE_MESSAGE, optional=True, group="kind") """ Represents a structured value. """ - list_value: "list[betterproto2.JSON] | None" = betterproto2.field( - 6, betterproto2.TYPE_MESSAGE, unwrap=lambda: ListValue, optional=True, group="kind" - ) + list_value: "ListValue | None" = betterproto2.field(6, betterproto2.TYPE_MESSAGE, optional=True, group="kind") """ Represents a repeated `Value`. """ @@ -3894,13 +3882,12 @@ def from_dict(cls, value, *, ignore_unknown_fields: bool = False): case str() as s: return cls(string_value=s) case list() as l: - return cls(list_value=list(l)) + return cls(list_value=ListValue.from_dict(l)) case dict() as d: - return cls(struct_value=dict(d)) + return cls(struct_value=Struct.from_dict(d)) case None: return cls(null_value=NullValue.NULL_VALUE) - case _: - raise ValueError(f"Unknown value type: {type(value)}") + raise ValueError(f"Unknown value type: {type(value)}") def to_dict( self, @@ -3912,14 +3899,21 @@ def to_dict( # If the output format is PYTHON, we should have kept the wrapped type without building the real class assert output_format == betterproto2.OutputFormat.PROTO_JSON - return betterproto2.which_one_of(self, "kind")[1] - - @staticmethod - def from_wrapped(wrapped: betterproto2.JSON) -> "Value": - return Value.from_dict(wrapped) - - def to_wrapped(self) -> betterproto2.JSON: - return self.to_dict() + match self: + case Value(null_value=NullValue.NULL_VALUE): + return None + case Value(bool_value=bool(b)): + return b + case Value(number_value=int(num)) | Value(number_value=float(num)): + return num + case Value(string_value=str(s)): + return s + case Value(list_value=ListValue(values=l)): + return [v.to_dict() for v in l] + case Value(struct_value=Struct(fields=f)): + return {k: v.to_dict() for k, v in f.items()} + + raise ValueError("Invalid value") default_message_pool.register_message("google.protobuf", "Value", Value) From 0cf01b5db6571781b4dc391b9283ccfe5e6e33bd Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 22 Aug 2025 23:25:38 +0200 Subject: [PATCH 15/18] Fix typechecking --- .../src/betterproto2_compiler/known_types/struct.py | 4 ++-- .../src/betterproto2_compiler/lib/google/protobuf/__init__.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py index 2cf93398..6bcffadc 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/struct.py @@ -16,12 +16,12 @@ class Struct(VanillaStruct): def from_dict(cls, value, *, ignore_unknown_fields: bool = False): assert isinstance(value, dict) - fields: dict[str, betterproto2.JSON] = {} + fields: dict[str, Value] = {} for key, val in value.items(): fields[key] = Value.from_dict(val) - return cls(fields=fields) + return cls(fields=fields) # type: ignore[reportArgumentType] # TODO typing def to_dict( diff --git a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py index 2e0192a4..d20cfc54 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py @@ -3440,7 +3440,7 @@ class Struct(betterproto2.Message): def from_dict(cls, value, *, ignore_unknown_fields: bool = False): assert isinstance(value, dict) - fields: dict[str, betterproto2.JSON] = {} + fields: dict[str, Value] = {} for key, val in value.items(): fields[key] = Value.from_dict(val) From e6a5cf0cdb17b4c788de27533a16fb59b3881aaa Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Sat, 23 Aug 2025 13:59:42 +0200 Subject: [PATCH 16/18] Remove JSON --- betterproto2/src/betterproto2/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/betterproto2/src/betterproto2/__init__.py b/betterproto2/src/betterproto2/__init__.py index 3bae10fd..9114b885 100644 --- a/betterproto2/src/betterproto2/__init__.py +++ b/betterproto2/src/betterproto2/__init__.py @@ -24,7 +24,7 @@ from enum import IntEnum from io import BytesIO from itertools import count -from typing import TYPE_CHECKING, Any, ClassVar, TypeAlias, get_type_hints +from typing import TYPE_CHECKING, Any, ClassVar, get_type_hints from typing_extensions import Self @@ -138,9 +138,6 @@ NEG_INFINITY = "-Infinity" NAN = "NaN" -# For Struct support -JSON: TypeAlias = int | float | bool | str | list[Any] | dict[str, Any] | None - class Casing(builtin_enum.Enum): """Casing constants for serialization.""" From 03b731d84bde166ced803e436698eac1f8884099 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 29 Aug 2025 19:01:20 +0200 Subject: [PATCH 17/18] Switch from is to == --- .../src/betterproto2_compiler/known_types/any.py | 4 ++-- .../src/betterproto2_compiler/lib/google/protobuf/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py index 14d179a2..57a23a97 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py +++ b/betterproto2_compiler/src/betterproto2_compiler/known_types/any.py @@ -51,7 +51,7 @@ def to_dict(self, **kwargs) -> dict[str, typing.Any]: if value is None: return output - if type(value).to_dict is betterproto2.Message.to_dict: + if type(value).to_dict == betterproto2.Message.to_dict: output.update(value.to_dict(**kwargs)) else: output["value"] = value.to_dict(**kwargs) @@ -69,7 +69,7 @@ def from_dict(cls, value, *, ignore_unknown_fields: bool = False): if not msg_cls: raise TypeError(f"Can't unpack unregistered type: {type_url}") - if msg_cls.to_dict is not betterproto2.Message.to_dict: + if not msg_cls.to_dict == betterproto2.Message.to_dict: value = value["value"] return cls( diff --git a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py index d20cfc54..9898580d 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py @@ -909,7 +909,7 @@ def to_dict(self, **kwargs) -> dict[str, typing.Any]: if value is None: return output - if type(value).to_dict is betterproto2.Message.to_dict: + if type(value).to_dict == betterproto2.Message.to_dict: output.update(value.to_dict(**kwargs)) else: output["value"] = value.to_dict(**kwargs) @@ -926,7 +926,7 @@ def from_dict(cls, value, *, ignore_unknown_fields: bool = False): if not msg_cls: raise TypeError(f"Can't unpack unregistered type: {type_url}") - if msg_cls.to_dict is not betterproto2.Message.to_dict: + if msg_cls.to_dict == not betterproto2.Message.to_dict: value = value["value"] return cls( From e959203dbb2c67327e40c98e07a595fd91d1a901 Mon Sep 17 00:00:00 2001 From: Adrien Vannson Date: Fri, 29 Aug 2025 19:03:56 +0200 Subject: [PATCH 18/18] Fix comparaison --- .../src/betterproto2_compiler/lib/google/protobuf/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py index 9898580d..3d52ed58 100644 --- a/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py +++ b/betterproto2_compiler/src/betterproto2_compiler/lib/google/protobuf/__init__.py @@ -926,7 +926,7 @@ def from_dict(cls, value, *, ignore_unknown_fields: bool = False): if not msg_cls: raise TypeError(f"Can't unpack unregistered type: {type_url}") - if msg_cls.to_dict == not betterproto2.Message.to_dict: + if not msg_cls.to_dict == betterproto2.Message.to_dict: value = value["value"] return cls(