diff --git a/pyproject.toml b/pyproject.toml index 49ff09226..cc1da7ad2 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,7 @@ dependencies = [ "cookiecutter~=2.1", "json-e>=2.7", "mozilla-repo-urls", + "msgspec>=0.19.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 074907894..d0f945146 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 669bcd812..80419b8e2 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 6702ddc10..2103733a2 100644 --- a/src/taskgraph/transforms/task.py +++ b/src/taskgraph/transforms/task.py @@ -25,8 +25,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, @@ -50,7 +50,7 @@ def _run_task_suffix(): #: Schema for the task transforms -task_description_schema = Schema( +task_description_schema = LegacySchema( { Required( "label", @@ -432,14 +432,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 39b57319e..29582dfe9 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,49 @@ 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.19.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cf/9b/95d8ce458462b8b71b8a70fa94563b2498b89933689f3a7b8911edfae3d7/msgspec-0.19.0.tar.gz", hash = "sha256:604037e7cd475345848116e89c553aa9a233259733ab51986ac924ab1b976f8e", size = 216934, upload-time = "2024-12-27T17:40:28.597Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/13/40/817282b42f58399762267b30deb8ac011d8db373f8da0c212c85fbe62b8f/msgspec-0.19.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d8dd848ee7ca7c8153462557655570156c2be94e79acec3561cf379581343259", size = 190019, upload-time = "2024-12-27T17:39:13.803Z" }, + { url = "https://files.pythonhosted.org/packages/92/99/bd7ed738c00f223a8119928661167a89124140792af18af513e6519b0d54/msgspec-0.19.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:0553bbc77662e5708fe66aa75e7bd3e4b0f209709c48b299afd791d711a93c36", size = 183680, upload-time = "2024-12-27T17:39:17.847Z" }, + { url = "https://files.pythonhosted.org/packages/e5/27/322badde18eb234e36d4a14122b89edd4e2973cdbc3da61ca7edf40a1ccd/msgspec-0.19.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:fe2c4bf29bf4e89790b3117470dea2c20b59932772483082c468b990d45fb947", size = 209334, upload-time = "2024-12-27T17:39:19.065Z" }, + { url = "https://files.pythonhosted.org/packages/c6/65/080509c5774a1592b2779d902a70b5fe008532759927e011f068145a16cb/msgspec-0.19.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00e87ecfa9795ee5214861eab8326b0e75475c2e68a384002aa135ea2a27d909", size = 211551, upload-time = "2024-12-27T17:39:21.767Z" }, + { url = "https://files.pythonhosted.org/packages/6f/2e/1c23c6b4ca6f4285c30a39def1054e2bee281389e4b681b5e3711bd5a8c9/msgspec-0.19.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3c4ec642689da44618f68c90855a10edbc6ac3ff7c1d94395446c65a776e712a", size = 215099, upload-time = "2024-12-27T17:39:24.71Z" }, + { url = "https://files.pythonhosted.org/packages/83/fe/95f9654518879f3359d1e76bc41189113aa9102452170ab7c9a9a4ee52f6/msgspec-0.19.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:2719647625320b60e2d8af06b35f5b12d4f4d281db30a15a1df22adb2295f633", size = 218211, upload-time = "2024-12-27T17:39:27.396Z" }, + { url = "https://files.pythonhosted.org/packages/79/f6/71ca7e87a1fb34dfe5efea8156c9ef59dd55613aeda2ca562f122cd22012/msgspec-0.19.0-cp310-cp310-win_amd64.whl", hash = "sha256:695b832d0091edd86eeb535cd39e45f3919f48d997685f7ac31acb15e0a2ed90", size = 186174, upload-time = "2024-12-27T17:39:29.647Z" }, + { url = "https://files.pythonhosted.org/packages/24/d4/2ec2567ac30dab072cce3e91fb17803c52f0a37aab6b0c24375d2b20a581/msgspec-0.19.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:aa77046904db764b0462036bc63ef71f02b75b8f72e9c9dd4c447d6da1ed8f8e", size = 187939, upload-time = "2024-12-27T17:39:32.347Z" }, + { url = "https://files.pythonhosted.org/packages/2b/c0/18226e4328897f4f19875cb62bb9259fe47e901eade9d9376ab5f251a929/msgspec-0.19.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:047cfa8675eb3bad68722cfe95c60e7afabf84d1bd8938979dd2b92e9e4a9551", size = 182202, upload-time = "2024-12-27T17:39:33.633Z" }, + { url = "https://files.pythonhosted.org/packages/81/25/3a4b24d468203d8af90d1d351b77ea3cffb96b29492855cf83078f16bfe4/msgspec-0.19.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e78f46ff39a427e10b4a61614a2777ad69559cc8d603a7c05681f5a595ea98f7", size = 209029, upload-time = "2024-12-27T17:39:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/85/2e/db7e189b57901955239f7689b5dcd6ae9458637a9c66747326726c650523/msgspec-0.19.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c7adf191e4bd3be0e9231c3b6dc20cf1199ada2af523885efc2ed218eafd011", size = 210682, upload-time = "2024-12-27T17:39:36.384Z" }, + { url = "https://files.pythonhosted.org/packages/03/97/7c8895c9074a97052d7e4a1cc1230b7b6e2ca2486714eb12c3f08bb9d284/msgspec-0.19.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:f04cad4385e20be7c7176bb8ae3dca54a08e9756cfc97bcdb4f18560c3042063", size = 214003, upload-time = "2024-12-27T17:39:39.097Z" }, + { url = "https://files.pythonhosted.org/packages/61/61/e892997bcaa289559b4d5869f066a8021b79f4bf8e955f831b095f47a4cd/msgspec-0.19.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45c8fb410670b3b7eb884d44a75589377c341ec1392b778311acdbfa55187716", size = 216833, upload-time = "2024-12-27T17:39:41.203Z" }, + { url = "https://files.pythonhosted.org/packages/ce/3d/71b2dffd3a1c743ffe13296ff701ee503feaebc3f04d0e75613b6563c374/msgspec-0.19.0-cp311-cp311-win_amd64.whl", hash = "sha256:70eaef4934b87193a27d802534dc466778ad8d536e296ae2f9334e182ac27b6c", size = 186184, upload-time = "2024-12-27T17:39:43.702Z" }, + { url = "https://files.pythonhosted.org/packages/b2/5f/a70c24f075e3e7af2fae5414c7048b0e11389685b7f717bb55ba282a34a7/msgspec-0.19.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f98bd8962ad549c27d63845b50af3f53ec468b6318400c9f1adfe8b092d7b62f", size = 190485, upload-time = "2024-12-27T17:39:44.974Z" }, + { url = "https://files.pythonhosted.org/packages/89/b0/1b9763938cfae12acf14b682fcf05c92855974d921a5a985ecc197d1c672/msgspec-0.19.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:43bbb237feab761b815ed9df43b266114203f53596f9b6e6f00ebd79d178cdf2", size = 183910, upload-time = "2024-12-27T17:39:46.401Z" }, + { url = "https://files.pythonhosted.org/packages/87/81/0c8c93f0b92c97e326b279795f9c5b956c5a97af28ca0fbb9fd86c83737a/msgspec-0.19.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4cfc033c02c3e0aec52b71710d7f84cb3ca5eb407ab2ad23d75631153fdb1f12", size = 210633, upload-time = "2024-12-27T17:39:49.099Z" }, + { url = "https://files.pythonhosted.org/packages/d0/ef/c5422ce8af73928d194a6606f8ae36e93a52fd5e8df5abd366903a5ca8da/msgspec-0.19.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:d911c442571605e17658ca2b416fd8579c5050ac9adc5e00c2cb3126c97f73bc", size = 213594, upload-time = "2024-12-27T17:39:51.204Z" }, + { url = "https://files.pythonhosted.org/packages/19/2b/4137bc2ed45660444842d042be2cf5b18aa06efd2cda107cff18253b9653/msgspec-0.19.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:757b501fa57e24896cf40a831442b19a864f56d253679f34f260dcb002524a6c", size = 214053, upload-time = "2024-12-27T17:39:52.866Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e6/8ad51bdc806aac1dc501e8fe43f759f9ed7284043d722b53323ea421c360/msgspec-0.19.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5f0f65f29b45e2816d8bded36e6b837a4bf5fb60ec4bc3c625fa2c6da4124537", size = 219081, upload-time = "2024-12-27T17:39:55.142Z" }, + { url = "https://files.pythonhosted.org/packages/b1/ef/27dd35a7049c9a4f4211c6cd6a8c9db0a50647546f003a5867827ec45391/msgspec-0.19.0-cp312-cp312-win_amd64.whl", hash = "sha256:067f0de1c33cfa0b6a8206562efdf6be5985b988b53dd244a8e06f993f27c8c0", size = 187467, upload-time = "2024-12-27T17:39:56.531Z" }, + { url = "https://files.pythonhosted.org/packages/3c/cb/2842c312bbe618d8fefc8b9cedce37f773cdc8fa453306546dba2c21fd98/msgspec-0.19.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f12d30dd6266557aaaf0aa0f9580a9a8fbeadfa83699c487713e355ec5f0bd86", size = 190498, upload-time = "2024-12-27T17:40:00.427Z" }, + { url = "https://files.pythonhosted.org/packages/58/95/c40b01b93465e1a5f3b6c7d91b10fb574818163740cc3acbe722d1e0e7e4/msgspec-0.19.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82b2c42c1b9ebc89e822e7e13bbe9d17ede0c23c187469fdd9505afd5a481314", size = 183950, upload-time = "2024-12-27T17:40:04.219Z" }, + { url = "https://files.pythonhosted.org/packages/e8/f0/5b764e066ce9aba4b70d1db8b087ea66098c7c27d59b9dd8a3532774d48f/msgspec-0.19.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:19746b50be214a54239aab822964f2ac81e38b0055cca94808359d779338c10e", size = 210647, upload-time = "2024-12-27T17:40:05.606Z" }, + { url = "https://files.pythonhosted.org/packages/9d/87/bc14f49bc95c4cb0dd0a8c56028a67c014ee7e6818ccdce74a4862af259b/msgspec-0.19.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:60ef4bdb0ec8e4ad62e5a1f95230c08efb1f64f32e6e8dd2ced685bcc73858b5", size = 213563, upload-time = "2024-12-27T17:40:10.516Z" }, + { url = "https://files.pythonhosted.org/packages/53/2f/2b1c2b056894fbaa975f68f81e3014bb447516a8b010f1bed3fb0e016ed7/msgspec-0.19.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac7f7c377c122b649f7545810c6cd1b47586e3aa3059126ce3516ac7ccc6a6a9", size = 213996, upload-time = "2024-12-27T17:40:12.244Z" }, + { url = "https://files.pythonhosted.org/packages/aa/5a/4cd408d90d1417e8d2ce6a22b98a6853c1b4d7cb7669153e4424d60087f6/msgspec-0.19.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a5bc1472223a643f5ffb5bf46ccdede7f9795078194f14edd69e3aab7020d327", size = 219087, upload-time = "2024-12-27T17:40:14.881Z" }, + { url = "https://files.pythonhosted.org/packages/23/d8/f15b40611c2d5753d1abb0ca0da0c75348daf1252220e5dda2867bd81062/msgspec-0.19.0-cp313-cp313-win_amd64.whl", hash = "sha256:317050bc0f7739cb30d257ff09152ca309bf5a369854bbf1e57dffc310c1f20f", size = 187432, upload-time = "2024-12-27T17:40:16.256Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d0/323f867eaec1f2236ba30adf613777b1c97a7e8698e2e881656b21871fa4/msgspec-0.19.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:15c1e86fff77184c20a2932cd9742bf33fe23125fa3fcf332df9ad2f7d483044", size = 189926, upload-time = "2024-12-27T17:40:18.939Z" }, + { url = "https://files.pythonhosted.org/packages/a8/37/c3e1b39bdae90a7258d77959f5f5e36ad44b40e2be91cff83eea33c54d43/msgspec-0.19.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:3b5541b2b3294e5ffabe31a09d604e23a88533ace36ac288fa32a420aa38d229", size = 183873, upload-time = "2024-12-27T17:40:20.214Z" }, + { url = "https://files.pythonhosted.org/packages/cb/a2/48f2c15c7644668e51f4dce99d5f709bd55314e47acb02e90682f5880f35/msgspec-0.19.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0f5c043ace7962ef188746e83b99faaa9e3e699ab857ca3f367b309c8e2c6b12", size = 209272, upload-time = "2024-12-27T17:40:21.534Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/aa339cf08b990c3f07e67b229a3a8aa31bf129ed974b35e5daa0df7d9d56/msgspec-0.19.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ca06aa08e39bf57e39a258e1996474f84d0dd8130d486c00bec26d797b8c5446", size = 211396, upload-time = "2024-12-27T17:40:22.897Z" }, + { url = "https://files.pythonhosted.org/packages/c7/00/c7fb9d524327c558b2803973cc3f988c5100a1708879970a9e377bdf6f4f/msgspec-0.19.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:e695dad6897896e9384cf5e2687d9ae9feaef50e802f93602d35458e20d1fb19", size = 215002, upload-time = "2024-12-27T17:40:24.341Z" }, + { url = "https://files.pythonhosted.org/packages/3f/bf/d9f9fff026c1248cde84a5ce62b3742e8a63a3c4e811f99f00c8babf7615/msgspec-0.19.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:3be5c02e1fee57b54130316a08fe40cca53af92999a302a6054cd451700ea7db", size = 218132, upload-time = "2024-12-27T17:40:25.744Z" }, + { url = "https://files.pythonhosted.org/packages/00/03/b92011210f79794958167a3a3ea64a71135d9a2034cfb7597b545a42606d/msgspec-0.19.0-cp39-cp39-win_amd64.whl", hash = "sha256:0684573a821be3c749912acf5848cce78af4298345cb2d7a8b8948a0a5a27cfe", size = 186301, upload-time = "2024-12-27T17:40:27.076Z" }, +] + [[package]] name = "multidict" version = "6.7.0" @@ -2028,6 +2071,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 +2118,7 @@ requires-dist = [ { name = "cookiecutter", specifier = "~=2.1" }, { name = "json-e", specifier = ">=2.7" }, { name = "mozilla-repo-urls" }, + { name = "msgspec", specifier = ">=0.19.0" }, { name = "orjson", marker = "extra == 'orjson'" }, { name = "pyyaml", specifier = ">=5.3.1" }, { name = "redo", specifier = ">=2.0" },