Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 6 additions & 7 deletions docs/concepts/transforms.rst
Original file line number Diff line number Diff line change
Expand Up @@ -105,17 +105,16 @@ about the state of the tasks at given points. Here is an example:

.. code-block:: python

from voluptuous import Optional, Required

from typing import Optional
from taskgraph.transforms.base import TransformSequence
from taskgraph.util.schema import Schema

my_schema = Schema({
Required("foo"): str,
Optional("bar"): bool,
})
class MySchema(Schema):
foo: str # Required field
bar: Optional[bool] = None # Optional field

transforms.add_validate(my_schema)
transforms = TransformSequence()
transforms.add_validate(MySchema)

In the above example, we can be sure that every task dict has a string field
called ``foo``, and may or may not have a boolean field called ``bar``.
Expand Down
16 changes: 8 additions & 8 deletions docs/tutorials/creating-a-task-graph.rst
Original file line number Diff line number Diff line change
Expand Up @@ -136,16 +136,16 @@ comments for explanations):

.. code-block:: python

from voluptuous import Optional, Required

from taskgraph.transforms.base import TransformSequence
from typing import Optional
from taskgraph.util.schema import Schema
from taskgraph.transforms.base import TransformSequence

# Define the schema using Schema base class.
class HelloDescriptionSchema(Schema):
text: str # Required field
description: Optional[str] = None # Optional field

# Define the schema. We use the `voluptuous` package to handle validation.
hello_description_schema = Schema({
Required("text"): str,
Optional("description"): str,
})
hello_description_schema = HelloDescriptionSchema

# Create a 'TransformSequence' instance. This class collects transform
# functions to run later.
Expand Down
2 changes: 1 addition & 1 deletion pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,13 @@ dependencies = [
"cookiecutter~=2.1",
"json-e>=2.7",
"mozilla-repo-urls",
"msgspec>=0.18.6",
"PyYAML>=5.3.1",
"redo>=2.0",
"requests>=2.25",
"slugid>=2.0",
"taskcluster>=55.0",
"taskcluster-urls>=11.0",
"voluptuous>=0.12.1",
]

[project.optional-dependencies]
Expand Down
188 changes: 94 additions & 94 deletions src/taskgraph/config.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,113 +2,112 @@
# 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 logging
import os
import sys
from dataclasses import dataclass
from pathlib import Path
from typing import Dict

from voluptuous import ALLOW_EXTRA, All, Any, Extra, Length, Optional, Required
from typing import Dict, List, Literal, Optional, Union

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 Schema, TaskPriority, optionally_keyed_by, validate_schema
from .util.vcs import get_repository
from .util.yaml import load_yaml

logger = logging.getLogger(__name__)

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


class WorkerAliasSchema(Schema):
"""Worker alias configuration."""

provisioner: optionally_keyed_by("level", str) # type: ignore
implementation: str
os: str
worker_type: optionally_keyed_by("level", str) # type: ignore


class WorkersSchema(Schema, rename=None):
"""Workers configuration."""

aliases: Dict[str, WorkerAliasSchema]


class Repository(Schema, forbid_unknown_fields=False):
"""Repository configuration.

This schema allows extra fields for repository-specific configuration.
"""

# Required fields first
name: str

# Optional fields
project_regex: Optional[str] = None # Maps from "project-regex"
ssh_secret_name: Optional[str] = None # Maps from "ssh-secret-name"


class RunConfig(Schema):
"""Run transforms configuration."""

# List of caches to enable, or a boolean to enable/disable all of them.
use_caches: Optional[Union[bool, List[str]]] = None # Maps from "use-caches"

def __post_init__(self):
"""Validate that cache names are valid."""
if isinstance(self.use_caches, list):
invalid = set(self.use_caches) - set(CACHES.keys())
if invalid:
raise ValueError(
f"Invalid cache names: {invalid}. "
f"Valid names are: {list(CACHES.keys())}"
)


class TaskGraphSchema(Schema):
"""Taskgraph specific configuration."""

# Required fields first
repositories: Dict[str, Repository]

# Optional fields
# Python function to call to register extensions.
register: Optional[str] = None
decision_parameters: Optional[str] = None # Maps from "decision-parameters"
# The taskcluster index prefix to use for caching tasks. Defaults to `trust-domain`.
cached_task_prefix: Optional[str] = None # Maps from "cached-task-prefix"
# Should tasks from pull requests populate the cache
cache_pull_requests: Optional[bool] = None # Maps from "cache-pull-requests"
# Regular expressions matching index paths to be summarized.
index_path_regexes: Optional[List[str]] = None # Maps from "index-path-regexes"
# Configuration related to the 'run' transforms.
run: Optional[RunConfig] = None


class GraphConfigSchema(Schema, forbid_unknown_fields=False):
"""Main graph configuration schema.

This schema allows extra fields for flexibility in graph configuration.
"""

# Required fields first
# The trust-domain for this graph.
# (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain)
trust_domain: str # Maps from "trust-domain"
task_priority: optionally_keyed_by("project", "level", TaskPriority) # type: ignore
workers: WorkersSchema
taskgraph: TaskGraphSchema

#: Schema for the graph config
graph_config_schema = Schema(
{
# The trust-domain for this graph.
# (See https://firefox-source-docs.mozilla.org/taskcluster/taskcluster/taskgraph.html#taskgraph-trust-domain) # noqa
Required("trust-domain"): str,
Optional(
"docker-image-kind",
description="Name of the docker image kind (default: docker-image)",
): str,
Required("task-priority"): optionally_keyed_by(
"project",
"level",
Any(
"highest",
"very-high",
"high",
"medium",
"low",
"very-low",
"lowest",
),
),
Optional(
"task-deadline-after",
description="Default 'deadline' for tasks, in relative date format. "
"Eg: '1 week'",
): optionally_keyed_by("project", str),
Optional(
"task-expires-after",
description="Default 'expires-after' for level 1 tasks, in relative date format. "
"Eg: '90 days'",
): str,
Required("workers"): {
Required("aliases"): {
str: {
Required("provisioner"): optionally_keyed_by("level", str),
Required("implementation"): str,
Required("os"): str,
Required("worker-type"): optionally_keyed_by("level", str),
}
},
},
Required("taskgraph"): {
Optional(
"register",
description="Python function to call to register extensions.",
): str,
Optional("decision-parameters"): str,
Optional(
"cached-task-prefix",
description="The taskcluster index prefix to use for caching tasks. "
"Defaults to `trust-domain`.",
): str,
Optional(
"cache-pull-requests",
description="Should tasks from pull requests populate the cache",
): bool,
Optional(
"index-path-regexes",
description="Regular expressions matching index paths to be summarized.",
): [str],
Optional(
"run",
description="Configuration related to the 'run' transforms.",
): {
Optional(
"use-caches",
description="List of caches to enable, or a boolean to "
"enable/disable all of them.",
): Any(bool, list(CACHES.keys())),
},
Required("repositories"): All(
{
str: {
Required("name"): str,
Optional("project-regex"): str,
Optional("ssh-secret-name"): str,
# FIXME
Extra: str,
}
},
Length(min=1),
),
},
},
extra=ALLOW_EXTRA,
)
# Optional fields
# Name of the docker image kind (default: docker-image)
docker_image_kind: Optional[str] = None # Maps from "docker-image-kind"
# Default 'deadline' for tasks, in relative date format. Eg: '1 week'
task_deadline_after: Optional[optionally_keyed_by("project", str)] = None # type: ignore
# Default 'expires-after' for level 1 tasks, in relative date format. Eg: '90 days'
task_expires_after: Optional[str] = None # Maps from "task-expires-after"


@dataclass(frozen=True, eq=False)
Expand Down Expand Up @@ -179,7 +178,8 @@ def kinds_dir(self):


def validate_graph_config(config):
validate_schema(graph_config_schema, config, "Invalid graph configuration:")
"""Validate graph configuration using msgspec."""
validate_schema(GraphConfigSchema, config, "Invalid graph configuration:")


def load_graph_config(root_dir):
Expand Down
12 changes: 5 additions & 7 deletions src/taskgraph/decision.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,9 @@
import shutil
import time
from pathlib import Path
from typing import Any, Dict, Optional

import yaml
from voluptuous import Optional

from taskgraph.actions import render_actions_json
from taskgraph.create import create_tasks
Expand Down Expand Up @@ -40,11 +40,9 @@


#: Schema for try_task_config.json version 2
try_task_config_schema_v2 = Schema(
{
Optional("parameters"): {str: object},
}
)
class TryTaskConfigSchemaV2(Schema):
# All fields are optional
parameters: Optional[Dict[str, Any]] = None


def full_task_graph_to_runnable_tasks(full_task_json):
Expand Down Expand Up @@ -354,7 +352,7 @@ def set_try_config(parameters, task_config_file):
task_config_version = task_config.pop("version")
if task_config_version == 2:
validate_schema(
try_task_config_schema_v2,
TryTaskConfigSchemaV2,
task_config,
"Invalid v2 `try_task_config.json`.",
)
Expand Down
Loading
Loading