Skip to content

Commit 4f129da

Browse files
feat: update schema from voluptuous to msgspec
1 parent 928330f commit 4f129da

File tree

25 files changed

+2538
-2275
lines changed

25 files changed

+2538
-2275
lines changed

docs/concepts/transforms.rst

Lines changed: 6 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -105,17 +105,16 @@ about the state of the tasks at given points. Here is an example:
105105

106106
.. code-block:: python
107107
108-
from voluptuous import Optional, Required
109-
108+
from typing import Optional
110109
from taskgraph.transforms.base import TransformSequence
111110
from taskgraph.util.schema import Schema
112111
113-
my_schema = Schema({
114-
Required("foo"): str,
115-
Optional("bar"): bool,
116-
})
112+
class MySchema(Schema):
113+
foo: str # Required field
114+
bar: Optional[bool] = None # Optional field
117115
118-
transforms.add_validate(my_schema)
116+
transforms = TransformSequence()
117+
transforms.add_validate(MySchema)
119118
120119
In the above example, we can be sure that every task dict has a string field
121120
called ``foo``, and may or may not have a boolean field called ``bar``.

docs/tutorials/creating-a-task-graph.rst

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,16 +136,16 @@ comments for explanations):
136136

137137
.. code-block:: python
138138
139-
from voluptuous import Optional, Required
140-
141-
from taskgraph.transforms.base import TransformSequence
139+
from typing import Optional
142140
from taskgraph.util.schema import Schema
141+
from taskgraph.transforms.base import TransformSequence
142+
143+
# Define the schema using Schema base class.
144+
class HelloDescriptionSchema(Schema):
145+
text: str # Required field
146+
description: Optional[str] = None # Optional field
143147
144-
# Define the schema. We use the `voluptuous` package to handle validation.
145-
hello_description_schema = Schema({
146-
Required("text"): str,
147-
Optional("description"): str,
148-
})
148+
hello_description_schema = HelloDescriptionSchema
149149
150150
# Create a 'TransformSequence' instance. This class collects transform
151151
# functions to run later.

pyproject.toml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@ dependencies = [
2525
"cookiecutter~=2.1",
2626
"json-e>=2.7",
2727
"mozilla-repo-urls",
28+
"msgspec>=0.18.6",
2829
"PyYAML>=5.3.1",
2930
"redo>=2.0",
3031
"requests>=2.25",
3132
"slugid>=2.0",
3233
"taskcluster>=91.0",
3334
"taskcluster-urls>=11.0",
34-
"voluptuous>=0.12.1",
35+
"voluptuous>=0.14.2",
3536
]
3637

3738
[dependency-groups]

src/taskgraph/config.py

Lines changed: 103 additions & 93 deletions
Original file line numberDiff line numberDiff line change
@@ -2,112 +2,121 @@
22
# License, v. 2.0. If a copy of the MPL was not distributed with this
33
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
44

5-
65
import logging
76
import os
87
import sys
98
from dataclasses import dataclass
109
from pathlib import Path
11-
12-
from voluptuous import ALLOW_EXTRA, All, Any, Extra, Length, Optional, Required
10+
from typing import Dict, List, Literal, Optional, Union
1311

1412
from .util.caches import CACHES
1513
from .util.python_path import find_object
16-
from .util.schema import Schema, optionally_keyed_by, validate_schema
14+
from .util.schema import (
15+
Struct,
16+
TaskPriority,
17+
optionally_keyed_by,
18+
validate_schema,
19+
)
1720
from .util.vcs import get_repository
1821
from .util.yaml import load_yaml
1922

2023
logger = logging.getLogger(__name__)
2124

25+
# CacheName type for valid cache names
26+
CacheName = Literal[tuple(CACHES.keys())]
2227

23-
#: Schema for the graph config
24-
graph_config_schema = Schema(
25-
{
26-
# The trust-domain for this graph.
27-
# (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain) # noqa
28-
Required("trust-domain"): str,
29-
Optional(
30-
"docker-image-kind",
31-
description="Name of the docker image kind (default: docker-image)",
32-
): str,
33-
Required("task-priority"): optionally_keyed_by(
34-
"project",
35-
"level",
36-
Any(
37-
"highest",
38-
"very-high",
39-
"high",
40-
"medium",
41-
"low",
42-
"very-low",
43-
"lowest",
44-
),
45-
),
46-
Optional(
47-
"task-deadline-after",
48-
description="Default 'deadline' for tasks, in relative date format. "
49-
"Eg: '1 week'",
50-
): optionally_keyed_by("project", str),
51-
Optional(
52-
"task-expires-after",
53-
description="Default 'expires-after' for level 1 tasks, in relative date format. "
54-
"Eg: '90 days'",
55-
): str,
56-
Required("workers"): {
57-
Required("aliases"): {
58-
str: {
59-
Required("provisioner"): optionally_keyed_by("level", str),
60-
Required("implementation"): str,
61-
Required("os"): str,
62-
Required("worker-type"): optionally_keyed_by("level", str),
63-
}
64-
},
65-
},
66-
Required("taskgraph"): {
67-
Optional(
68-
"register",
69-
description="Python function to call to register extensions.",
70-
): str,
71-
Optional("decision-parameters"): str,
72-
Optional(
73-
"cached-task-prefix",
74-
description="The taskcluster index prefix to use for caching tasks. "
75-
"Defaults to `trust-domain`.",
76-
): str,
77-
Optional(
78-
"cache-pull-requests",
79-
description="Should tasks from pull requests populate the cache",
80-
): bool,
81-
Optional(
82-
"index-path-regexes",
83-
description="Regular expressions matching index paths to be summarized.",
84-
): [str],
85-
Optional(
86-
"run",
87-
description="Configuration related to the 'run' transforms.",
88-
): {
89-
Optional(
90-
"use-caches",
91-
description="List of caches to enable, or a boolean to "
92-
"enable/disable all of them.",
93-
): Any(bool, list(CACHES.keys())),
94-
},
95-
Required("repositories"): All(
96-
{
97-
str: {
98-
Required("name"): str,
99-
Optional("project-regex"): str,
100-
Optional("ssh-secret-name"): str,
101-
# FIXME
102-
Extra: str,
103-
}
104-
},
105-
Length(min=1),
106-
),
107-
},
108-
},
109-
extra=ALLOW_EXTRA,
110-
)
28+
29+
class WorkerAliasStruct(Struct):
30+
"""Worker alias configuration."""
31+
32+
provisioner: optionally_keyed_by("level", str, use_msgspec=True) # type: ignore
33+
implementation: str
34+
os: str
35+
worker_type: optionally_keyed_by("level", str, use_msgspec=True) # type: ignore
36+
37+
38+
class WorkersStruct(Struct, rename=None):
39+
"""Workers configuration."""
40+
41+
aliases: Dict[str, WorkerAliasStruct]
42+
43+
44+
class Repository(Struct, forbid_unknown_fields=False):
45+
"""Repository configuration.
46+
47+
This schema allows extra fields for repository-specific configuration.
48+
"""
49+
50+
# Required fields first
51+
name: str
52+
53+
# Optional fields
54+
project_regex: Optional[str] = None # Maps from "project-regex"
55+
ssh_secret_name: Optional[str] = None # Maps from "ssh-secret-name"
56+
57+
58+
class RunConfig(Struct):
59+
"""Run transforms configuration."""
60+
61+
# List of caches to enable, or a boolean to enable/disable all of them.
62+
use_caches: Optional[Union[bool, List[str]]] = None # Maps from "use-caches"
63+
64+
def __post_init__(self):
65+
"""Validate that cache names are valid."""
66+
if isinstance(self.use_caches, list):
67+
invalid = set(self.use_caches) - set(CACHES.keys())
68+
if invalid:
69+
raise ValueError(
70+
f"Invalid cache names: {invalid}. "
71+
f"Valid names are: {list(CACHES.keys())}"
72+
)
73+
74+
75+
class TaskGraphStruct(Struct):
76+
"""Taskgraph specific configuration."""
77+
78+
# Required fields first
79+
repositories: Dict[str, Repository]
80+
81+
# Optional fields
82+
# Python function to call to register extensions.
83+
register: Optional[str] = None
84+
decision_parameters: Optional[str] = None # Maps from "decision-parameters"
85+
# The taskcluster index prefix to use for caching tasks. Defaults to `trust-domain`.
86+
cached_task_prefix: Optional[str] = None # Maps from "cached-task-prefix"
87+
# Should tasks from pull requests populate the cache
88+
cache_pull_requests: Optional[bool] = None # Maps from "cache-pull-requests"
89+
# Regular expressions matching index paths to be summarized.
90+
index_path_regexes: Optional[List[str]] = None # Maps from "index-path-regexes"
91+
# Configuration related to the 'run' transforms.
92+
run: Optional[RunConfig] = None
93+
94+
95+
class GraphConfigStruct(Struct, forbid_unknown_fields=False):
96+
"""Main graph configuration schema.
97+
98+
This schema allows extra fields for flexibility in graph configuration.
99+
"""
100+
101+
# Required fields first
102+
# The trust-domain for this graph.
103+
# (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain)
104+
trust_domain: str # Maps from "trust-domain"
105+
task_priority: optionally_keyed_by(
106+
"project", "level", TaskPriority, use_msgspec=True
107+
) # type: ignore
108+
workers: WorkersStruct
109+
taskgraph: TaskGraphStruct
110+
111+
# Optional fields
112+
# Name of the docker image kind (default: docker-image)
113+
docker_image_kind: Optional[str] = None # Maps from "docker-image-kind"
114+
# Default 'deadline' for tasks, in relative date format. Eg: '1 week'
115+
task_deadline_after: Optional[
116+
optionally_keyed_by("project", str, use_msgspec=True)
117+
] = None # type: ignore
118+
# Default 'expires-after' for level 1 tasks, in relative date format. Eg: '90 days'
119+
task_expires_after: Optional[str] = None # Maps from "task-expires-after"
111120

112121

113122
@dataclass(frozen=True, eq=False)
@@ -178,7 +187,8 @@ def kinds_dir(self):
178187

179188

180189
def validate_graph_config(config):
181-
validate_schema(graph_config_schema, config, "Invalid graph configuration:")
190+
"""Validate graph configuration using msgspec."""
191+
validate_schema(GraphConfigStruct, config, "Invalid graph configuration:")
182192

183193

184194
def load_graph_config(root_dir):

src/taskgraph/decision.py

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@
99
import shutil
1010
import time
1111
from pathlib import Path
12+
from typing import Any, Dict, Optional
1213

1314
import yaml
14-
from voluptuous import Optional
1515

1616
from taskgraph.actions import render_actions_json
1717
from taskgraph.create import create_tasks
@@ -20,8 +20,8 @@
2020
from taskgraph.taskgraph import TaskGraph
2121
from taskgraph.util import json
2222
from taskgraph.util.python_path import find_object
23-
from taskgraph.util.schema import Schema, validate_schema
24-
from taskgraph.util.vcs import get_repository
23+
from taskgraph.util.schema import Struct, validate_schema
24+
from taskgraph.util.vcs import Repository, get_repository
2525
from taskgraph.util.yaml import load_yaml
2626

2727
logger = logging.getLogger(__name__)
@@ -40,11 +40,9 @@
4040

4141

4242
#: Schema for try_task_config.json version 2
43-
try_task_config_schema_v2 = Schema(
44-
{
45-
Optional("parameters"): {str: object},
46-
}
47-
)
43+
class TryTaskConfigSchemaV2(Struct):
44+
# All fields are optional
45+
parameters: Optional[Dict[str, Any]] = None
4846

4947

5048
def full_task_graph_to_runnable_tasks(full_task_json):
@@ -277,7 +275,7 @@ def set_try_config(parameters, task_config_file):
277275
task_config_version = task_config.pop("version")
278276
if task_config_version == 2:
279277
validate_schema(
280-
try_task_config_schema_v2,
278+
TryTaskConfigSchemaV2,
281279
task_config,
282280
"Invalid v2 `try_task_config.json`.",
283281
)

0 commit comments

Comments
 (0)