diff --git a/pyproject.toml b/pyproject.toml index 2ed3a8632..9c473d7ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "cookiecutter~=2.1", "json-e>=2.7", "mozilla-repo-urls", + "msgspec>=0.20.0", "PyYAML>=5.3.1", "redo>=2.0", "requests>=2.25", diff --git a/src/taskgraph/config.py b/src/taskgraph/config.py index d0b0f7ce6..b9a2b57d2 100644 --- a/src/taskgraph/config.py +++ b/src/taskgraph/config.py @@ -13,7 +13,7 @@ from .util.caches import CACHES from .util.python_path import find_object -from .util.schema import Schema, optionally_keyed_by, validate_schema +from .util.schema import LegacySchema, optionally_keyed_by, validate_schema from .util.vcs import get_repository from .util.yaml import load_yaml @@ -21,7 +21,7 @@ #: Schema for the graph config -graph_config_schema = Schema( +graph_config_schema = LegacySchema( { # The trust-domain for this graph. # (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain) # noqa diff --git a/src/taskgraph/decision.py b/src/taskgraph/decision.py index a0c5381a8..888ad12fe 100644 --- a/src/taskgraph/decision.py +++ b/src/taskgraph/decision.py @@ -20,7 +20,7 @@ from taskgraph.taskgraph import TaskGraph from taskgraph.util import json from taskgraph.util.python_path import find_object -from taskgraph.util.schema import Schema, validate_schema +from taskgraph.util.schema import LegacySchema, validate_schema from taskgraph.util.vcs import get_repository from taskgraph.util.yaml import load_yaml @@ -40,7 +40,7 @@ #: Schema for try_task_config.json version 2 -try_task_config_schema_v2 = Schema( +try_task_config_schema_v2 = LegacySchema( { Optional("parameters"): {str: object}, } diff --git a/src/taskgraph/transforms/base.py b/src/taskgraph/transforms/base.py index 42705c3c5..a1d50ea4f 100644 --- a/src/taskgraph/transforms/base.py +++ b/src/taskgraph/transforms/base.py @@ -12,7 +12,7 @@ from ..config import GraphConfig from ..parameters import Parameters -from ..util.schema import Schema, validate_schema +from ..util.schema import LegacySchema, validate_schema @dataclass(frozen=True) @@ -138,7 +138,7 @@ def add_validate(self, schema): @dataclass class ValidateSchema: - schema: Schema + schema: LegacySchema def __call__(self, config, tasks): for task in tasks: diff --git a/src/taskgraph/transforms/chunking.py b/src/taskgraph/transforms/chunking.py index d8ad89dd2..59818337b 100644 --- a/src/taskgraph/transforms/chunking.py +++ b/src/taskgraph/transforms/chunking.py @@ -7,11 +7,11 @@ from voluptuous import ALLOW_EXTRA, Optional, Required from taskgraph.transforms.base import TransformSequence -from taskgraph.util.schema import Schema +from taskgraph.util.schema import LegacySchema from taskgraph.util.templates import substitute #: Schema for chunking transforms -CHUNK_SCHEMA = Schema( +CHUNK_SCHEMA = LegacySchema( { # Optional, so it can be used for a subset of tasks in a kind Optional( diff --git a/src/taskgraph/transforms/docker_image.py b/src/taskgraph/transforms/docker_image.py index 8ded711ac..0898cc79d 100644 --- a/src/taskgraph/transforms/docker_image.py +++ b/src/taskgraph/transforms/docker_image.py @@ -13,7 +13,7 @@ from taskgraph.transforms.base import TransformSequence from taskgraph.util import json from taskgraph.util.docker import create_context_tar, generate_context_hash -from taskgraph.util.schema import Schema +from taskgraph.util.schema import LegacySchema from .task import task_description_schema @@ -32,7 +32,7 @@ transforms = TransformSequence() #: Schema for docker_image transforms -docker_image_schema = Schema( +docker_image_schema = LegacySchema( { Required( "name", diff --git a/src/taskgraph/transforms/fetch.py b/src/taskgraph/transforms/fetch.py index 797ab71e2..e165eec31 100644 --- a/src/taskgraph/transforms/fetch.py +++ b/src/taskgraph/transforms/fetch.py @@ -18,14 +18,14 @@ from ..util import path from ..util.cached_tasks import add_optimization -from ..util.schema import Schema, validate_schema +from ..util.schema import LegacySchema, validate_schema from ..util.treeherder import join_symbol from .base import TransformSequence CACHE_TYPE = "content.v1" #: Schema for fetch transforms -FETCH_SCHEMA = Schema( +FETCH_SCHEMA = LegacySchema( { Required( "name", @@ -87,12 +87,12 @@ @dataclass(frozen=True) class FetchBuilder: - schema: Schema + schema: LegacySchema builder: Callable def fetch_builder(name, schema): - schema = Schema({Required("type"): name}).extend(schema) + schema = LegacySchema({Required("type"): name}).extend(schema) def wrap(func): fetch_builders[name] = FetchBuilder(schema, func) # type: ignore diff --git a/src/taskgraph/transforms/from_deps.py b/src/taskgraph/transforms/from_deps.py index 6bf5c6ec9..c03148c99 100644 --- a/src/taskgraph/transforms/from_deps.py +++ b/src/taskgraph/transforms/from_deps.py @@ -20,11 +20,11 @@ from taskgraph.transforms.run import fetches_schema from taskgraph.util.attributes import attrmatch from taskgraph.util.dependencies import GROUP_BY_MAP, get_dependencies -from taskgraph.util.schema import Schema, validate_schema +from taskgraph.util.schema import LegacySchema, validate_schema from taskgraph.util.set_name import SET_NAME_MAP #: Schema for from_deps transforms -FROM_DEPS_SCHEMA = Schema( +FROM_DEPS_SCHEMA = LegacySchema( { Required("from-deps"): { Optional( diff --git a/src/taskgraph/transforms/matrix.py b/src/taskgraph/transforms/matrix.py index 476507284..855bffa41 100644 --- a/src/taskgraph/transforms/matrix.py +++ b/src/taskgraph/transforms/matrix.py @@ -13,11 +13,11 @@ from voluptuous import ALLOW_EXTRA, Extra, Optional, Required from taskgraph.transforms.base import TransformSequence -from taskgraph.util.schema import Schema +from taskgraph.util.schema import LegacySchema from taskgraph.util.templates import substitute_task_fields #: Schema for matrix transforms -MATRIX_SCHEMA = Schema( +MATRIX_SCHEMA = LegacySchema( { Required("name"): str, Optional("matrix"): { diff --git a/src/taskgraph/transforms/notify.py b/src/taskgraph/transforms/notify.py index 9c0152dad..a7d118f10 100644 --- a/src/taskgraph/transforms/notify.py +++ b/src/taskgraph/transforms/notify.py @@ -11,7 +11,7 @@ from voluptuous import ALLOW_EXTRA, Any, Exclusive, Optional, Required from taskgraph.transforms.base import TransformSequence -from taskgraph.util.schema import Schema, optionally_keyed_by, resolve_keyed_by +from taskgraph.util.schema import LegacySchema, optionally_keyed_by, resolve_keyed_by _status_type = Any( "on-completed", @@ -55,7 +55,7 @@ """Map each type to its primary key that will be used in the route.""" #: Schema for notify transforms -NOTIFY_SCHEMA = Schema( +NOTIFY_SCHEMA = LegacySchema( { Exclusive("notify", "config"): { Required("recipients"): [Any(*_recipients)], diff --git a/src/taskgraph/transforms/run/__init__.py b/src/taskgraph/transforms/run/__init__.py index 29406e7cd..ed3d7bf02 100644 --- a/src/taskgraph/transforms/run/__init__.py +++ b/src/taskgraph/transforms/run/__init__.py @@ -21,7 +21,7 @@ from taskgraph.util import json from taskgraph.util import path as mozpath from taskgraph.util.python_path import import_sibling_modules -from taskgraph.util.schema import Schema, validate_schema +from taskgraph.util.schema import LegacySchema, validate_schema from taskgraph.util.taskcluster import get_artifact_prefix from taskgraph.util.workertypes import worker_type_implementation @@ -38,7 +38,7 @@ } #: Schema for a run transforms -run_description_schema = Schema( +run_description_schema = LegacySchema( { Optional( "name", @@ -457,7 +457,7 @@ def wrap(func): @run_task_using( - "always-optimized", "always-optimized", Schema({"using": "always-optimized"}) + "always-optimized", "always-optimized", LegacySchema({"using": "always-optimized"}) ) def always_optimized(config, task, taskdesc): pass diff --git a/src/taskgraph/transforms/run/index_search.py b/src/taskgraph/transforms/run/index_search.py index 7436f010f..d5c0c6109 100644 --- a/src/taskgraph/transforms/run/index_search.py +++ b/src/taskgraph/transforms/run/index_search.py @@ -12,13 +12,13 @@ from taskgraph.transforms.base import TransformSequence from taskgraph.transforms.run import run_task_using -from taskgraph.util.schema import Schema +from taskgraph.util.schema import LegacySchema transforms = TransformSequence() #: Schema for run.using index-search -run_task_schema = Schema( +run_task_schema = LegacySchema( { Required("using"): "index-search", Required( diff --git a/src/taskgraph/transforms/run/run_task.py b/src/taskgraph/transforms/run/run_task.py index 9d76380d8..ce81d7d23 100644 --- a/src/taskgraph/transforms/run/run_task.py +++ b/src/taskgraph/transforms/run/run_task.py @@ -19,7 +19,7 @@ from taskgraph.transforms.task import taskref_or_string from taskgraph.util import path, taskcluster from taskgraph.util.caches import CACHES -from taskgraph.util.schema import Schema +from taskgraph.util.schema import LegacySchema EXEC_COMMANDS = { "bash": ["bash", "-cx"], @@ -28,7 +28,7 @@ #: Schema for run.using run_task -run_task_schema = Schema( +run_task_schema = LegacySchema( { Required( "using", diff --git a/src/taskgraph/transforms/run/toolchain.py b/src/taskgraph/transforms/run/toolchain.py index 58d318043..77406ad61 100644 --- a/src/taskgraph/transforms/run/toolchain.py +++ b/src/taskgraph/transforms/run/toolchain.py @@ -18,13 +18,13 @@ ) from taskgraph.util import path as mozpath from taskgraph.util.hash import hash_paths -from taskgraph.util.schema import Schema +from taskgraph.util.schema import LegacySchema from taskgraph.util.shell import quote as shell_quote CACHE_TYPE = "toolchains.v3" #: Schema for run.using toolchain -toolchain_run_schema = Schema( +toolchain_run_schema = LegacySchema( { Required( "using", diff --git a/src/taskgraph/transforms/task.py b/src/taskgraph/transforms/task.py index 7c834dcc7..59efbd8f3 100644 --- a/src/taskgraph/transforms/task.py +++ b/src/taskgraph/transforms/task.py @@ -23,8 +23,8 @@ from taskgraph.util.hash import hash_path from taskgraph.util.keyed_by import evaluate_keyed_by from taskgraph.util.schema import ( + LegacySchema, OptimizationSchema, - Schema, optionally_keyed_by, resolve_keyed_by, taskref_or_string, @@ -48,7 +48,7 @@ def run_task_suffix(): #: Schema for the task transforms -task_description_schema = Schema( +task_description_schema = LegacySchema( { Required( "label", @@ -430,14 +430,14 @@ def get_default_deadline(graph_config, project): @dataclass(frozen=True) class PayloadBuilder: - schema: Schema + schema: LegacySchema builder: Callable def payload_builder(name, schema): - schema = Schema({Required("implementation"): name, Optional("os"): str}).extend( - schema - ) + schema = LegacySchema( + {Required("implementation"): name, Optional("os"): str} + ).extend(schema) def wrap(func): assert name not in payload_builders, f"duplicate payload builder name {name}" diff --git a/src/taskgraph/transforms/task_context.py b/src/taskgraph/transforms/task_context.py index 815c582de..e38648cd3 100644 --- a/src/taskgraph/transforms/task_context.py +++ b/src/taskgraph/transforms/task_context.py @@ -3,12 +3,12 @@ from voluptuous import ALLOW_EXTRA, Any, Optional, Required from taskgraph.transforms.base import TransformSequence -from taskgraph.util.schema import Schema +from taskgraph.util.schema import LegacySchema from taskgraph.util.templates import deep_get, substitute_task_fields from taskgraph.util.yaml import load_yaml #: Schema for the task_context transforms -SCHEMA = Schema( +SCHEMA = LegacySchema( { Optional("name"): str, Optional( diff --git a/src/taskgraph/util/dependencies.py b/src/taskgraph/util/dependencies.py index 94d47b8d5..17c41d732 100644 --- a/src/taskgraph/util/dependencies.py +++ b/src/taskgraph/util/dependencies.py @@ -7,7 +7,7 @@ from taskgraph.task import Task from taskgraph.transforms.base import TransformConfig -from taskgraph.util.schema import Schema +from taskgraph.util.schema import LegacySchema # Define a collection of group_by functions GROUP_BY_MAP = {} @@ -36,7 +36,7 @@ def group_by_all(config, tasks): return [[task for task in tasks]] -@group_by("attribute", schema=Schema(str)) +@group_by("attribute", schema=LegacySchema(str)) def group_by_attribute(config, tasks, attr): groups = {} for task in tasks: diff --git a/src/taskgraph/util/schema.py b/src/taskgraph/util/schema.py index 3c5f4c955..9f256f69d 100644 --- a/src/taskgraph/util/schema.py +++ b/src/taskgraph/util/schema.py @@ -2,67 +2,109 @@ # License, v. 2.0. If a copy of the MPL was not distributed with this # file, You can obtain one at http://mozilla.org/MPL/2.0/. - import pprint import re from collections.abc import Mapping +from functools import reduce +from typing import Literal, Optional, Union +import msgspec import voluptuous import taskgraph from taskgraph.util.keyed_by import evaluate_keyed_by, iter_dot_path +# Common type definitions that are used across multiple schemas +TaskPriority = Literal[ + "highest", "very-high", "high", "medium", "low", "very-low", "lowest" +] + def validate_schema(schema, obj, msg_prefix): """ Validate that object satisfies schema. If not, generate a useful exception beginning with msg_prefix. + + Args: + schema: A voluptuous.Schema or msgspec-based StructSchema type + obj: Object to validate + msg_prefix: Prefix for error messages """ if taskgraph.fast: return + try: - schema(obj) - except voluptuous.MultipleInvalid as exc: - msg = [msg_prefix] - for error in exc.errors: - msg.append(str(error)) - raise Exception("\n".join(msg) + "\n" + pprint.pformat(obj)) + # Handle voluptuous Schema + if isinstance(schema, voluptuous.Schema): + schema(obj) + # Handle msgspec-based schemas (StructSchema and its subclasses) + elif isinstance(schema, type) and issubclass(schema, msgspec.Struct): + # Check if it's our Struct subclass with validate method + if issubclass(schema, Schema): + schema.validate(obj) + else: + # Fall back to msgspec.convert for validation + msgspec.convert(obj, schema) + else: + raise TypeError(f"Unsupported schema type: {type(schema)}") + except ( + voluptuous.MultipleInvalid, + msgspec.ValidationError, + msgspec.DecodeError, + ) as exc: + if isinstance(exc, voluptuous.MultipleInvalid): + msg = [msg_prefix] + for error in exc.errors: + msg.append(str(error)) + raise Exception("\n".join(msg) + "\n" + pprint.pformat(obj)) + else: + raise Exception(f"{msg_prefix}\n{str(exc)}\n{pprint.pformat(obj)}") + + +def UnionTypes(*types): + """Use `functools.reduce` to simulate `Union[*allowed_types]` on older + Python versions. + """ + return reduce(lambda a, b: Union[a, b], types) -def optionally_keyed_by(*arguments): +def optionally_keyed_by(*arguments, use_msgspec=False): """ - Mark a schema value as optionally keyed by any of a number of fields. The - schema is the last argument, and the remaining fields are taken to be the - field names. For example: - - 'some-value': optionally_keyed_by( - 'test-platform', 'build-platform', - Any('a', 'b', 'c')) + Mark a schema value as optionally keyed by any of a number of fields. - The resulting schema will allow nesting of `by-test-platform` and - `by-build-platform` in either order. + Args: + *arguments: Field names followed by the schema + use_msgspec: If True, return msgspec type hints; if False, return voluptuous validator """ - schema = arguments[-1] - fields = arguments[:-1] - - def validator(obj): - if isinstance(obj, dict) and len(obj) == 1: - k, v = list(obj.items())[0] - if k.startswith("by-") and k[len("by-") :] in fields: - res = {} - for kk, vv in v.items(): - try: - res[kk] = validator(vv) - except voluptuous.Invalid as e: - e.prepend([k, kk]) - raise - return res - return Schema(schema)(obj) - - # set to assist autodoc - setattr(validator, "schema", schema) - setattr(validator, "fields", fields) - return validator + if use_msgspec: + # msgspec implementation - return type hints + _type = arguments[-1] + fields = arguments[:-1] + bykeys = [Literal[f"by-{field}"] for field in fields] + return Union[_type, dict[UnionTypes(*bykeys), dict[str, _type]]] + else: + # voluptuous implementation - return validator function + schema = arguments[-1] + fields = arguments[:-1] + + def validator(obj): + if isinstance(obj, dict) and len(obj) == 1: + k, v = list(obj.items())[0] + if k.startswith("by-") and k[len("by-") :] in fields: + res = {} + for kk, vv in v.items(): + try: + res[kk] = validator(vv) + except voluptuous.Invalid as e: + e.prepend([k, kk]) + raise + return res + return LegacySchema(schema)(obj) + + # set to assist autodoc + setattr(validator, "schema", schema) + setattr(validator, "fields", fields) + return validator def resolve_keyed_by( @@ -199,7 +241,7 @@ def check_identifier(path, k): iter("schema", schema.schema) -class Schema(voluptuous.Schema): +class LegacySchema(voluptuous.Schema): """ Operates identically to voluptuous.Schema, but applying some taskgraph-specific checks in the process. @@ -218,7 +260,7 @@ def extend(self, *args, **kwargs): if self.check: check_schema(schema) # We want twice extend schema to be checked too. - schema.__class__ = Schema + schema.__class__ = LegacySchema return schema def _compile(self, schema): @@ -230,6 +272,103 @@ def __getitem__(self, item): return self.schema[item] # type: ignore +class Schema( + msgspec.Struct, + kw_only=True, + omit_defaults=True, + rename="kebab", + forbid_unknown_fields=True, +): + """ + Base schema class that extends msgspec.Struct. + + This allows schemas to be defined directly as: + + class MySchema(Schema): + foo: str + bar: int = 10 + + Instead of wrapping msgspec.Struct types. + Most schemas use kebab-case renaming by default. + + By default, forbid_unknown_fields is True, meaning extra fields + will cause validation errors. Child classes can override this by + setting forbid_unknown_fields=False in their class definition: + + class MySchema(Schema, forbid_unknown_fields=False): + foo: str + """ + + @classmethod + def validate(cls, data): + """Validate data against this schema.""" + if taskgraph.fast: + return data + + try: + return msgspec.convert(data, cls) + except (msgspec.ValidationError, msgspec.DecodeError) as e: + raise msgspec.ValidationError(str(e)) + + +class IndexSearchOptimizationSchema(Schema): + """Search the index for the given index namespaces.""" + + index_search: list[str] + + +class SkipUnlessChangedOptimizationSchema(Schema): + """Skip this task if none of the given file patterns match.""" + + skip_unless_changed: list[str] + + +# Create a class for optimization types to avoid dict union issues +class OptimizationTypeSchema(Schema, forbid_unknown_fields=False): + """Schema that accepts various optimization configurations.""" + + index_search: Optional[list[str]] = None + skip_unless_changed: Optional[list[str]] = None + + def __post_init__(self): + """Ensure at least one optimization type is provided.""" + if not self.index_search and not self.skip_unless_changed: + # Allow empty schema for other dict-based optimizations + pass + + +OptimizationType = Union[None, OptimizationTypeSchema] + + +# Task reference types using msgspec +class TaskReferenceSchema(Schema): + """Reference to another task (msgspec version).""" + + task_reference: str + + +class ArtifactReferenceSchema(Schema): + """Reference to a task artifact (msgspec version).""" + + artifact_reference: str + + +class TaskRefTypeSchema(Schema, forbid_unknown_fields=False): + """Schema that accepts either task-reference or artifact-reference (msgspec version).""" + + task_reference: Optional[str] = None + artifact_reference: Optional[str] = None + + def __post_init__(self): + """Ensure exactly one reference type is provided.""" + if self.task_reference and self.artifact_reference: + raise ValueError("Cannot have both task-reference and artifact-reference") + if not self.task_reference and not self.artifact_reference: + raise ValueError("Must have either task-reference or artifact-reference") + + +taskref_or_string_msgspec = Union[str, TaskRefTypeSchema] + OptimizationSchema = voluptuous.Any( # always run this task (default) None, diff --git a/template/{{cookiecutter.project_name}}/taskcluster/{{cookiecutter.project_slug}}_taskgraph/transforms/hello.py b/template/{{cookiecutter.project_name}}/taskcluster/{{cookiecutter.project_slug}}_taskgraph/transforms/hello.py index 6729f2f57..f63b5feab 100644 --- a/template/{{cookiecutter.project_name}}/taskcluster/{{cookiecutter.project_slug}}_taskgraph/transforms/hello.py +++ b/template/{{cookiecutter.project_name}}/taskcluster/{{cookiecutter.project_slug}}_taskgraph/transforms/hello.py @@ -1,9 +1,9 @@ from voluptuous import ALLOW_EXTRA, Required from taskgraph.transforms.base import TransformSequence -from taskgraph.util.schema import Schema +from taskgraph.util.schema import LegacySchema -HELLO_SCHEMA = Schema( +HELLO_SCHEMA = LegacySchema( { Required("noun"): str, }, diff --git a/test/test_transforms_run_run_task.py b/test/test_transforms_run_run_task.py index dec10dc0a..2ef25b9f2 100644 --- a/test/test_transforms_run_run_task.py +++ b/test/test_transforms_run_run_task.py @@ -10,7 +10,7 @@ from taskgraph.transforms.run import make_task_description from taskgraph.transforms.task import payload_builders, set_defaults from taskgraph.util.caches import CACHES -from taskgraph.util.schema import Schema, validate_schema +from taskgraph.util.schema import LegacySchema, validate_schema from taskgraph.util.taskcluster import get_root_url from taskgraph.util.templates import merge @@ -258,7 +258,7 @@ def inner(task, **kwargs): pprint(caches, indent=2) # Create a new schema object with just the part relevant to caches. - partial_schema = Schema(payload_builders[impl].schema.schema[key]) + partial_schema = LegacySchema(payload_builders[impl].schema.schema[key]) validate_schema(partial_schema, caches, "validation error") return caches diff --git a/test/test_util_schema.py b/test/test_util_schema.py index 59c354e6a..3364b2813 100644 --- a/test/test_util_schema.py +++ b/test/test_util_schema.py @@ -9,13 +9,13 @@ import taskgraph from taskgraph.util.schema import ( - Schema, + LegacySchema, optionally_keyed_by, resolve_keyed_by, validate_schema, ) -schema = Schema( +schema = LegacySchema( { "x": int, "y": str, @@ -39,26 +39,26 @@ class TestCheckSchema(unittest.TestCase): def test_schema(self): "Creating a schema applies taskgraph checks." with self.assertRaises(Exception): - Schema({"camelCase": int}) + LegacySchema({"camelCase": int}) def test_extend_schema(self): "Extending a schema applies taskgraph checks." with self.assertRaises(Exception): - Schema({"kebab-case": int}).extend({"camelCase": int}) + LegacySchema({"kebab-case": int}).extend({"camelCase": int}) def test_extend_schema_twice(self): "Extending a schema twice applies taskgraph checks." with self.assertRaises(Exception): - Schema({"kebab-case": int}).extend({"more-kebab": int}).extend( + LegacySchema({"kebab-case": int}).extend({"more-kebab": int}).extend( {"camelCase": int} ) def test_check_skipped(monkeypatch): """Schema not validated if 'check=False' or taskgraph.fast is unset.""" - Schema({"camelCase": int}, check=False) # assert no exception + LegacySchema({"camelCase": int}, check=False) # assert no exception monkeypatch.setattr(taskgraph, "fast", True) - Schema({"camelCase": int}) # assert no exception + LegacySchema({"camelCase": int}) # assert no exception class TestResolveKeyedBy(unittest.TestCase): diff --git a/uv.lock b/uv.lock index c0efc03ba..54a5b5495 100644 --- a/uv.lock +++ b/uv.lock @@ -1,5 +1,5 @@ version = 1 -revision = 3 +revision = 2 requires-python = ">=3.9" resolution-markers = [ "python_full_version >= '3.11'", @@ -1023,6 +1023,70 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/98/2f/836d81783089672cc92d0b75c3ffb96a7d0318f071e56b78b4220810e027/mozilla_repo_urls-0.2.2-py3-none-any.whl", hash = "sha256:161ab84cac58c0bef2c9ce0c414aaec5483fc9593fc5616c5b186e0cccff4984", size = 9906, upload-time = "2025-06-03T13:13:03.304Z" }, ] +[[package]] +name = "msgspec" +version = "0.20.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ea/9c/bfbd12955a49180cbd234c5d29ec6f74fe641698f0cd9df154a854fc8a15/msgspec-0.20.0.tar.gz", hash = "sha256:692349e588fde322875f8d3025ac01689fead5901e7fb18d6870a44519d62a29", size = 317862, upload-time = "2025-11-24T03:56:28.934Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/5e/151883ba2047cca9db8ed2f86186b054ad200bc231352df15b0c1dd75b1f/msgspec-0.20.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:23a6ec2a3b5038c233b04740a545856a068bc5cb8db184ff493a58e08c994fbf", size = 195191, upload-time = "2025-11-24T03:55:08.549Z" }, + { url = "https://files.pythonhosted.org/packages/50/88/a795647672f547c983eff0823b82aaa35db922c767e1b3693e2dcf96678d/msgspec-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:cde2c41ed3eaaef6146365cb0d69580078a19f974c6cb8165cc5dcd5734f573e", size = 188513, upload-time = "2025-11-24T03:55:10.008Z" }, + { url = "https://files.pythonhosted.org/packages/4b/91/eb0abb0e0de142066cebfe546dc9140c5972ea824aa6ff507ad0b6a126ac/msgspec-0.20.0-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5da0daa782f95d364f0d95962faed01e218732aa1aa6cad56b25a5d2092e75a4", size = 216370, upload-time = "2025-11-24T03:55:11.566Z" }, + { url = "https://files.pythonhosted.org/packages/15/2a/48e41d9ef0a24b1c6e67cbd94a676799e0561bfbc163be1aaaff5ca853f5/msgspec-0.20.0-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9369d5266144bef91be2940a3821e03e51a93c9080fde3ef72728c3f0a3a8bb7", size = 222653, upload-time = "2025-11-24T03:55:13.159Z" }, + { url = "https://files.pythonhosted.org/packages/90/c9/14b825df203d980f82a623450d5f39e7f7a09e6e256c52b498ea8f29d923/msgspec-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:90fb865b306ca92c03964a5f3d0cd9eb1adda14f7e5ac7943efd159719ea9f10", size = 222337, upload-time = "2025-11-24T03:55:14.777Z" }, + { url = "https://files.pythonhosted.org/packages/8b/d7/39a5c3ddd294f587d6fb8efccc8361b6aa5089974015054071e665c9d24b/msgspec-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:e8112cd48b67dfc0cfa49fc812b6ce7eb37499e1d95b9575061683f3428975d3", size = 225565, upload-time = "2025-11-24T03:55:16.4Z" }, + { url = "https://files.pythonhosted.org/packages/98/bd/5db3c14d675ee12842afb9b70c94c64f2c873f31198c46cbfcd7dffafab0/msgspec-0.20.0-cp310-cp310-win_amd64.whl", hash = "sha256:666b966d503df5dc27287675f525a56b6e66a2b8e8ccd2877b0c01328f19ae6c", size = 188412, upload-time = "2025-11-24T03:55:17.747Z" }, + { url = "https://files.pythonhosted.org/packages/76/c7/06cc218bc0c86f0c6c6f34f7eeea6cfb8b835070e8031e3b0ef00f6c7c69/msgspec-0.20.0-cp310-cp310-win_arm64.whl", hash = "sha256:099e3e85cd5b238f2669621be65f0728169b8c7cb7ab07f6137b02dc7feea781", size = 173951, upload-time = "2025-11-24T03:55:19.335Z" }, + { url = "https://files.pythonhosted.org/packages/03/59/fdcb3af72f750a8de2bcf39d62ada70b5eb17b06d7f63860e0a679cb656b/msgspec-0.20.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:09e0efbf1ac641fedb1d5496c59507c2f0dc62a052189ee62c763e0aae217520", size = 193345, upload-time = "2025-11-24T03:55:20.613Z" }, + { url = "https://files.pythonhosted.org/packages/5a/15/3c225610da9f02505d37d69a77f4a2e7daae2a125f99d638df211ba84e59/msgspec-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:23ee3787142e48f5ee746b2909ce1b76e2949fbe0f97f9f6e70879f06c218b54", size = 186867, upload-time = "2025-11-24T03:55:22.4Z" }, + { url = "https://files.pythonhosted.org/packages/81/36/13ab0c547e283bf172f45491edfdea0e2cecb26ae61e3a7b1ae6058b326d/msgspec-0.20.0-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:81f4ac6f0363407ac0465eff5c7d4d18f26870e00674f8fcb336d898a1e36854", size = 215351, upload-time = "2025-11-24T03:55:23.958Z" }, + { url = "https://files.pythonhosted.org/packages/6b/96/5c095b940de3aa6b43a71ec76275ac3537b21bd45c7499b5a17a429110fa/msgspec-0.20.0-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb4d873f24ae18cd1334f4e37a178ed46c9d186437733351267e0a269bdf7e53", size = 219896, upload-time = "2025-11-24T03:55:25.356Z" }, + { url = "https://files.pythonhosted.org/packages/98/7a/81a7b5f01af300761087b114dafa20fb97aed7184d33aab64d48874eb187/msgspec-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:b92b8334427b8393b520c24ff53b70f326f79acf5f74adb94fd361bcff8a1d4e", size = 220389, upload-time = "2025-11-24T03:55:26.99Z" }, + { url = "https://files.pythonhosted.org/packages/70/c0/3d0cce27db9a9912421273d49eab79ce01ecd2fed1a2f1b74af9b445f33c/msgspec-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:562c44b047c05cc0384e006fae7a5e715740215c799429e0d7e3e5adf324285a", size = 223348, upload-time = "2025-11-24T03:55:28.311Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/406b7d578926b68790e390d83a1165a9bfc2d95612a1a9c1c4d5c72ea815/msgspec-0.20.0-cp311-cp311-win_amd64.whl", hash = "sha256:d1dcc93a3ce3d3195985bfff18a48274d0b5ffbc96fa1c5b89da6f0d9af81b29", size = 188713, upload-time = "2025-11-24T03:55:29.553Z" }, + { url = "https://files.pythonhosted.org/packages/47/87/14fe2316624ceedf76a9e94d714d194cbcb699720b210ff189f89ca4efd7/msgspec-0.20.0-cp311-cp311-win_arm64.whl", hash = "sha256:aa387aa330d2e4bd69995f66ea8fdc87099ddeedf6fdb232993c6a67711e7520", size = 174229, upload-time = "2025-11-24T03:55:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/d9/6f/1e25eee957e58e3afb2a44b94fa95e06cebc4c236193ed0de3012fff1e19/msgspec-0.20.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2aba22e2e302e9231e85edc24f27ba1f524d43c223ef5765bd8624c7df9ec0a5", size = 196391, upload-time = "2025-11-24T03:55:32.677Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/af51d090ada641d4b264992a486435ba3ef5b5634bc27e6eb002f71cef7d/msgspec-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:716284f898ab2547fedd72a93bb940375de9fbfe77538f05779632dc34afdfde", size = 188644, upload-time = "2025-11-24T03:55:33.934Z" }, + { url = "https://files.pythonhosted.org/packages/49/d6/9709ee093b7742362c2934bfb1bbe791a1e09bed3ea5d8a18ce552fbfd73/msgspec-0.20.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:558ed73315efa51b1538fa8f1d3b22c8c5ff6d9a2a62eff87d25829b94fc5054", size = 218852, upload-time = "2025-11-24T03:55:35.575Z" }, + { url = "https://files.pythonhosted.org/packages/5c/a2/488517a43ccf5a4b6b6eca6dd4ede0bd82b043d1539dd6bb908a19f8efd3/msgspec-0.20.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:509ac1362a1d53aa66798c9b9fd76872d7faa30fcf89b2fba3bcbfd559d56eb0", size = 224937, upload-time = "2025-11-24T03:55:36.859Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e8/49b832808aa23b85d4f090d1d2e48a4e3834871415031ed7c5fe48723156/msgspec-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:1353c2c93423602e7dea1aa4c92f3391fdfc25ff40e0bacf81d34dbc68adb870", size = 222858, upload-time = "2025-11-24T03:55:38.187Z" }, + { url = "https://files.pythonhosted.org/packages/9f/56/1dc2fa53685dca9c3f243a6cbecd34e856858354e455b77f47ebd76cf5bf/msgspec-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:cb33b5eb5adb3c33d749684471c6a165468395d7aa02d8867c15103b81e1da3e", size = 227248, upload-time = "2025-11-24T03:55:39.496Z" }, + { url = "https://files.pythonhosted.org/packages/5a/51/aba940212c23b32eedce752896205912c2668472ed5b205fc33da28a6509/msgspec-0.20.0-cp312-cp312-win_amd64.whl", hash = "sha256:fb1d934e435dd3a2b8cf4bbf47a8757100b4a1cfdc2afdf227541199885cdacb", size = 190024, upload-time = "2025-11-24T03:55:40.829Z" }, + { url = "https://files.pythonhosted.org/packages/41/ad/3b9f259d94f183daa9764fef33fdc7010f7ecffc29af977044fa47440a83/msgspec-0.20.0-cp312-cp312-win_arm64.whl", hash = "sha256:00648b1e19cf01b2be45444ba9dc961bd4c056ffb15706651e64e5d6ec6197b7", size = 175390, upload-time = "2025-11-24T03:55:42.05Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d1/b902d38b6e5ba3bdddbec469bba388d647f960aeed7b5b3623a8debe8a76/msgspec-0.20.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:9c1ff8db03be7598b50dd4b4a478d6fe93faae3bd54f4f17aa004d0e46c14c46", size = 196463, upload-time = "2025-11-24T03:55:43.405Z" }, + { url = "https://files.pythonhosted.org/packages/57/b6/eff0305961a1d9447ec2b02f8c73c8946f22564d302a504185b730c9a761/msgspec-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f6532369ece217fd37c5ebcfd7e981f2615628c21121b7b2df9d3adcf2fd69b8", size = 188650, upload-time = "2025-11-24T03:55:44.761Z" }, + { url = "https://files.pythonhosted.org/packages/99/93/f2ec1ae1de51d3fdee998a1ede6b2c089453a2ee82b5c1b361ed9095064a/msgspec-0.20.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9a1697da2f85a751ac3cc6a97fceb8e937fc670947183fb2268edaf4016d1ee", size = 218834, upload-time = "2025-11-24T03:55:46.441Z" }, + { url = "https://files.pythonhosted.org/packages/28/83/36557b04cfdc317ed8a525c4993b23e43a8fbcddaddd78619112ca07138c/msgspec-0.20.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7fac7e9c92eddcd24c19d9e5f6249760941485dff97802461ae7c995a2450111", size = 224917, upload-time = "2025-11-24T03:55:48.06Z" }, + { url = "https://files.pythonhosted.org/packages/8f/56/362037a1ed5be0b88aced59272442c4b40065c659700f4b195a7f4d0ac88/msgspec-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f953a66f2a3eb8d5ea64768445e2bb301d97609db052628c3e1bcb7d87192a9f", size = 222821, upload-time = "2025-11-24T03:55:49.388Z" }, + { url = "https://files.pythonhosted.org/packages/92/75/fa2370ec341cedf663731ab7042e177b3742645c5dd4f64dc96bd9f18a6b/msgspec-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:247af0313ae64a066d3aea7ba98840f6681ccbf5c90ba9c7d17f3e39dbba679c", size = 227227, upload-time = "2025-11-24T03:55:51.125Z" }, + { url = "https://files.pythonhosted.org/packages/f1/25/5e8080fe0117f799b1b68008dc29a65862077296b92550632de015128579/msgspec-0.20.0-cp313-cp313-win_amd64.whl", hash = "sha256:67d5e4dfad52832017018d30a462604c80561aa62a9d548fc2bd4e430b66a352", size = 189966, upload-time = "2025-11-24T03:55:52.458Z" }, + { url = "https://files.pythonhosted.org/packages/79/b6/63363422153937d40e1cb349c5081338401f8529a5a4e216865decd981bf/msgspec-0.20.0-cp313-cp313-win_arm64.whl", hash = "sha256:91a52578226708b63a9a13de287b1ec3ed1123e4a088b198143860c087770458", size = 175378, upload-time = "2025-11-24T03:55:53.721Z" }, + { url = "https://files.pythonhosted.org/packages/bb/18/62dc13ab0260c7d741dda8dc7f481495b93ac9168cd887dda5929880eef8/msgspec-0.20.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:eead16538db1b3f7ec6e3ed1f6f7c5dec67e90f76e76b610e1ffb5671815633a", size = 196407, upload-time = "2025-11-24T03:55:55.001Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1d/b9949e4ad6953e9f9a142c7997b2f7390c81e03e93570c7c33caf65d27e1/msgspec-0.20.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:703c3bb47bf47801627fb1438f106adbfa2998fe586696d1324586a375fca238", size = 188889, upload-time = "2025-11-24T03:55:56.311Z" }, + { url = "https://files.pythonhosted.org/packages/1e/19/f8bb2dc0f1bfe46cc7d2b6b61c5e9b5a46c62298e8f4d03bbe499c926180/msgspec-0.20.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6cdb227dc585fb109305cee0fd304c2896f02af93ecf50a9c84ee54ee67dbb42", size = 219691, upload-time = "2025-11-24T03:55:57.908Z" }, + { url = "https://files.pythonhosted.org/packages/b8/8e/6b17e43f6eb9369d9858ee32c97959fcd515628a1df376af96c11606cf70/msgspec-0.20.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27d35044dd8818ac1bd0fedb2feb4fbdff4e3508dd7c5d14316a12a2d96a0de0", size = 224918, upload-time = "2025-11-24T03:55:59.322Z" }, + { url = "https://files.pythonhosted.org/packages/1c/db/0e833a177db1a4484797adba7f429d4242585980b90882cc38709e1b62df/msgspec-0.20.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b4296393a29ee42dd25947981c65506fd4ad39beaf816f614146fa0c5a6c91ae", size = 223436, upload-time = "2025-11-24T03:56:00.716Z" }, + { url = "https://files.pythonhosted.org/packages/c3/30/d2ee787f4c918fd2b123441d49a7707ae9015e0e8e1ab51aa7967a97b90e/msgspec-0.20.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:205fbdadd0d8d861d71c8f3399fe1a82a2caf4467bc8ff9a626df34c12176980", size = 227190, upload-time = "2025-11-24T03:56:02.371Z" }, + { url = "https://files.pythonhosted.org/packages/ff/37/9c4b58ff11d890d788e700b827db2366f4d11b3313bf136780da7017278b/msgspec-0.20.0-cp314-cp314-win_amd64.whl", hash = "sha256:7dfebc94fe7d3feec6bc6c9df4f7e9eccc1160bb5b811fbf3e3a56899e398a6b", size = 193950, upload-time = "2025-11-24T03:56:03.668Z" }, + { url = "https://files.pythonhosted.org/packages/e9/4e/cab707bf2fa57408e2934e5197fc3560079db34a1e3cd2675ff2e47e07de/msgspec-0.20.0-cp314-cp314-win_arm64.whl", hash = "sha256:2ad6ae36e4a602b24b4bf4eaf8ab5a441fec03e1f1b5931beca8ebda68f53fc0", size = 179018, upload-time = "2025-11-24T03:56:05.038Z" }, + { url = "https://files.pythonhosted.org/packages/4c/06/3da3fc9aaa55618a8f43eb9052453cfe01f82930bca3af8cea63a89f3a11/msgspec-0.20.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:f84703e0e6ef025663dd1de828ca028774797b8155e070e795c548f76dde65d5", size = 200389, upload-time = "2025-11-24T03:56:06.375Z" }, + { url = "https://files.pythonhosted.org/packages/83/3b/cc4270a5ceab40dfe1d1745856951b0a24fd16ac8539a66ed3004a60c91e/msgspec-0.20.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7c83fc24dd09cf1275934ff300e3951b3adc5573f0657a643515cc16c7dee131", size = 193198, upload-time = "2025-11-24T03:56:07.742Z" }, + { url = "https://files.pythonhosted.org/packages/cd/ae/4c7905ac53830c8e3c06fdd60e3cdcfedc0bbc993872d1549b84ea21a1bd/msgspec-0.20.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5f13ccb1c335a124e80c4562573b9b90f01ea9521a1a87f7576c2e281d547f56", size = 225973, upload-time = "2025-11-24T03:56:09.18Z" }, + { url = "https://files.pythonhosted.org/packages/d9/da/032abac1de4d0678d99eaeadb1323bd9d247f4711c012404ba77ed6f15ca/msgspec-0.20.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17c2b5ca19f19306fc83c96d85e606d2cc107e0caeea85066b5389f664e04846", size = 229509, upload-time = "2025-11-24T03:56:10.898Z" }, + { url = "https://files.pythonhosted.org/packages/69/52/fdc7bdb7057a166f309e0b44929e584319e625aaba4771b60912a9321ccd/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d931709355edabf66c2dd1a756b2d658593e79882bc81aae5964969d5a291b63", size = 230434, upload-time = "2025-11-24T03:56:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/cb/fe/1dfd5f512b26b53043884e4f34710c73e294e7cc54278c3fe28380e42c37/msgspec-0.20.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:565f915d2e540e8a0c93a01ff67f50aebe1f7e22798c6a25873f9fda8d1325f8", size = 231758, upload-time = "2025-11-24T03:56:13.765Z" }, + { url = "https://files.pythonhosted.org/packages/97/f6/9ba7121b8e0c4e0beee49575d1dbc804e2e72467692f0428cf39ceba1ea5/msgspec-0.20.0-cp314-cp314t-win_amd64.whl", hash = "sha256:726f3e6c3c323f283f6021ebb6c8ccf58d7cd7baa67b93d73bfbe9a15c34ab8d", size = 206540, upload-time = "2025-11-24T03:56:15.029Z" }, + { url = "https://files.pythonhosted.org/packages/c8/3e/c5187de84bb2c2ca334ab163fcacf19a23ebb1d876c837f81a1b324a15bf/msgspec-0.20.0-cp314-cp314t-win_arm64.whl", hash = "sha256:93f23528edc51d9f686808a361728e903d6f2be55c901d6f5c92e44c6d546bfc", size = 183011, upload-time = "2025-11-24T03:56:16.442Z" }, + { url = "https://files.pythonhosted.org/packages/b2/30/55eb8645bf11ea84bc1dafa670d068348b08b84660c4c9240ff05296e707/msgspec-0.20.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:eee56472ced14602245ac47516e179d08c6c892d944228796f239e983de7449c", size = 195293, upload-time = "2025-11-24T03:56:17.763Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c2/78c66d69beb45c311ba6ad0021f31ddfe6f19fe1b46cf295175fbb41430d/msgspec-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:19395e9a08cc5bd0e336909b3e13b4ae5ee5e47b82e98f8b7801d5a13806bb6f", size = 188572, upload-time = "2025-11-24T03:56:19.431Z" }, + { url = "https://files.pythonhosted.org/packages/44/14/9d6f685a277e4d3417f103c4d228cb7ea83fdd776c739570f233917f5fd2/msgspec-0.20.0-cp39-cp39-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d5bb7ce84fe32f6ce9f62aa7e7109cb230ad542cc5bc9c46e587f1dac4afc48e", size = 216219, upload-time = "2025-11-24T03:56:20.823Z" }, + { url = "https://files.pythonhosted.org/packages/98/24/e50ea4080656a711bee9fe3d846de3b0e74f03c1dc620284b82e1757fdb0/msgspec-0.20.0-cp39-cp39-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8c6da9ae2d76d11181fbb0ea598f6e1d558ef597d07ec46d689d17f68133769f", size = 222573, upload-time = "2025-11-24T03:56:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/d1/4b/2d9415a935ebd6e5f34fd5cad7be6b8525d8353bf5ed6eb77e706863f3b0/msgspec-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:84d88bd27d906c471a5ca232028671db734111996ed1160e37171a8d1f07a599", size = 222097, upload-time = "2025-11-24T03:56:23.553Z" }, + { url = "https://files.pythonhosted.org/packages/b3/56/2cc277def0d43625dd14ab6ee0e3a5198175725198122d707fa139ebbdd1/msgspec-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:03907bf733f94092a6b4c5285b274f79947cad330bd8a9d8b45c0369e1a3c7f0", size = 225419, upload-time = "2025-11-24T03:56:24.953Z" }, + { url = "https://files.pythonhosted.org/packages/42/1d/e9401b352aa399af5efa35f1f130651698e65f919ecb9221b925b2236948/msgspec-0.20.0-cp39-cp39-win_amd64.whl", hash = "sha256:9fbcb660632a2f5c247c0dc820212bf3a423357ac6241ff6dc6cfc6f72584016", size = 188527, upload-time = "2025-11-24T03:56:26.193Z" }, + { url = "https://files.pythonhosted.org/packages/02/59/079f33cd092ee42c9b97a59daa2115e7550a7eba98781ef6657e3d710d56/msgspec-0.20.0-cp39-cp39-win_arm64.whl", hash = "sha256:f7cd0e89b86a16005745cb99bd1858e8050fc17f63de571504492b267bca188a", size = 173927, upload-time = "2025-11-24T03:56:27.52Z" }, +] + [[package]] name = "multidict" version = "6.7.0" @@ -2028,6 +2092,7 @@ dependencies = [ { name = "json-e" }, { name = "mozilla-repo-urls", version = "0.1.1", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, { name = "mozilla-repo-urls", version = "0.2.2", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, + { name = "msgspec" }, { name = "pyyaml" }, { name = "redo" }, { name = "requests" }, @@ -2074,6 +2139,7 @@ requires-dist = [ { name = "cookiecutter", specifier = "~=2.1" }, { name = "json-e", specifier = ">=2.7" }, { name = "mozilla-repo-urls" }, + { name = "msgspec", specifier = ">=0.20.0" }, { name = "orjson", marker = "extra == 'orjson'" }, { name = "pyyaml", specifier = ">=5.3.1" }, { name = "redo", specifier = ">=2.0" },