feat(schemas): generate .schema.json files for Experimenter and Feature Manifest schemas (#11519)
Because: - we need the .schema.json files in Desktop; - they are not currently packaged into either the NPM or PyPI packages; and - the generated schemas do not include the custom validation logic expressed by Pydantic validators This commit: - adds JSON schema validation via Pydantic's json_schema_extra hook to give the JSON schemas validation parity with the Pydantic models and existing JSON schemas in Desktop; - generates the JSON schemas and commits them (in schemas/schemas); - packages the produced schemas into the NPM package; - packages the produced schemas into the PyPI package (but uncommitted, since they are duplicates); - revises the Makefile build steps for schema package generation; and - bumps the schema package version. Fixes #11518
This commit is contained in:
Родитель
eda2402004
Коммит
be67f0b808
|
@ -517,10 +517,6 @@ jobs:
|
||||||
echo "No changes in package version. Skipping mozilla-nimbus-schemas deployment."
|
echo "No changes in package version. Skipping mozilla-nimbus-schemas deployment."
|
||||||
circleci-agent step halt
|
circleci-agent step halt
|
||||||
fi
|
fi
|
||||||
- run:
|
|
||||||
name: Create the distribution files
|
|
||||||
command: |
|
|
||||||
make schemas_dist
|
|
||||||
- run:
|
- run:
|
||||||
name: Upload to PyPI
|
name: Upload to PyPI
|
||||||
command: |
|
command: |
|
||||||
|
|
|
@ -120,3 +120,6 @@ cirrus/server/app/__pycache__
|
||||||
|
|
||||||
# Application Services for processing FML files
|
# Application Services for processing FML files
|
||||||
experimenter/experimenter/features/manifests/application-services/
|
experimenter/experimenter/features/manifests/application-services/
|
||||||
|
|
||||||
|
# Schemas
|
||||||
|
schemas/mozilla_nimbus_schemas/schemas/
|
||||||
|
|
18
Makefile
18
Makefile
|
@ -292,8 +292,8 @@ SCHEMAS_DIFF_PYDANTIC = \
|
||||||
echo 'Done. No problems found in schemas.'
|
echo 'Done. No problems found in schemas.'
|
||||||
SCHEMAS_TEST = pytest
|
SCHEMAS_TEST = pytest
|
||||||
SCHEMAS_FORMAT = ruff check --fix . && black .
|
SCHEMAS_FORMAT = ruff check --fix . && black .
|
||||||
|
SCHEMAS_GENERATE = poetry run python generate_json_schema.py
|
||||||
SCHEMAS_DIST_PYPI = poetry build
|
SCHEMAS_DIST_PYPI = poetry build
|
||||||
SCHEMAS_DIST_NPM = poetry run python generate_json_schema.py --output index.d.ts
|
|
||||||
SCHEMAS_DEPLOY_PYPI = twine upload --skip-existing dist/*;
|
SCHEMAS_DEPLOY_PYPI = twine upload --skip-existing dist/*;
|
||||||
SCHEMAS_DEPLOY_NPM = echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc;yarn publish --new-version ${SCHEMAS_VERSION} --access public;
|
SCHEMAS_DEPLOY_NPM = echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc;yarn publish --new-version ${SCHEMAS_VERSION} --access public;
|
||||||
SCHEMAS_VERSION_PYPI = poetry version ${SCHEMAS_VERSION};
|
SCHEMAS_VERSION_PYPI = poetry version ${SCHEMAS_VERSION};
|
||||||
|
@ -302,7 +302,8 @@ SCHEMAS_VERSION_NPM = npm version --allow-same-version ${SCHEMAS_VERSION};
|
||||||
schemas_docker_build: ## Build schemas docker image
|
schemas_docker_build: ## Build schemas docker image
|
||||||
$(DOCKER_BUILD) --target dev -f schemas/Dockerfile -t schemas:dev schemas/
|
$(DOCKER_BUILD) --target dev -f schemas/Dockerfile -t schemas:dev schemas/
|
||||||
|
|
||||||
schemas_build: schemas_docker_build schemas_dist ## Build schemas
|
schemas_build: schemas_docker_build ## Build the mozilla_nimbus_schemas packages.
|
||||||
|
$(SCHEMAS_RUN) "$(SCHEMAS_GENERATE) && $(SCHEMAS_DIST_PYPI)"
|
||||||
|
|
||||||
schemas_bash: schemas_docker_build
|
schemas_bash: schemas_docker_build
|
||||||
$(SCHEMAS_RUN) "bash"
|
$(SCHEMAS_RUN) "bash"
|
||||||
|
@ -312,20 +313,17 @@ schemas_format: schemas_docker_build ## Format schemas source tree
|
||||||
|
|
||||||
schemas_lint: schemas_docker_build ## Lint schemas source tree
|
schemas_lint: schemas_docker_build ## Lint schemas source tree
|
||||||
$(SCHEMAS_RUN) "$(SCHEMAS_BLACK)&&$(SCHEMAS_RUFF)&&$(SCHEMAS_DIFF_PYDANTIC)&&$(SCHEMAS_TEST)"
|
$(SCHEMAS_RUN) "$(SCHEMAS_BLACK)&&$(SCHEMAS_RUFF)&&$(SCHEMAS_DIFF_PYDANTIC)&&$(SCHEMAS_TEST)"
|
||||||
|
|
||||||
schemas_check: schemas_lint
|
schemas_check: schemas_lint
|
||||||
|
|
||||||
schemas_dist_pypi: schemas_docker_build
|
schemas_generate: schemas_docker_build
|
||||||
$(SCHEMAS_RUN) "$(SCHEMAS_DIST_PYPI)"
|
$(SCHEMAS_RUN) "$(SCHEMAS_GENERATE)"
|
||||||
|
|
||||||
schemas_dist_npm: schemas_docker_build schemas_dist_pypi
|
|
||||||
$(SCHEMAS_RUN) "$(SCHEMAS_DIST_NPM)"
|
|
||||||
|
|
||||||
schemas_dist: schemas_docker_build schemas_dist_pypi schemas_dist_npm
|
schemas_deploy_pypi: schemas_build
|
||||||
|
|
||||||
schemas_deploy_pypi: schemas_docker_build
|
|
||||||
$(SCHEMAS_RUN) "$(SCHEMAS_DEPLOY_PYPI)"
|
$(SCHEMAS_RUN) "$(SCHEMAS_DEPLOY_PYPI)"
|
||||||
|
|
||||||
schemas_deploy_npm: schemas_docker_build
|
schemas_deploy_npm: schemas_build
|
||||||
$(SCHEMAS_RUN) "$(SCHEMAS_DEPLOY_NPM)"
|
$(SCHEMAS_RUN) "$(SCHEMAS_DEPLOY_NPM)"
|
||||||
|
|
||||||
schemas_version_pypi: schemas_docker_build
|
schemas_version_pypi: schemas_docker_build
|
||||||
|
|
|
@ -1 +1 @@
|
||||||
2024.9.3
|
2024.10.1
|
||||||
|
|
|
@ -4,10 +4,12 @@ https://github.com/Darius-Labs/pydantic-to-typescript2/blob/main/pydantic2ts/cli
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import json
|
import json
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
import subprocess
|
import subprocess
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from tempfile import TemporaryDirectory
|
from tempfile import TemporaryDirectory
|
||||||
from typing import Any
|
from typing import Any, Iterable
|
||||||
|
|
||||||
import click
|
import click
|
||||||
from polyfactory.factories.pydantic_factory import ModelFactory
|
from polyfactory.factories.pydantic_factory import ModelFactory
|
||||||
|
@ -15,6 +17,8 @@ from pydantic import BaseModel, create_model
|
||||||
|
|
||||||
from mozilla_nimbus_schemas import experiments, jetstream
|
from mozilla_nimbus_schemas import experiments, jetstream
|
||||||
|
|
||||||
|
NEWLINES_RE = re.compile("\n+")
|
||||||
|
|
||||||
|
|
||||||
def clean_output_file(ts_path: Path) -> None:
|
def clean_output_file(ts_path: Path) -> None:
|
||||||
"""Clean up the output file typescript definitions were written to by:
|
"""Clean up the output file typescript definitions were written to by:
|
||||||
|
@ -99,6 +103,123 @@ def iterate_models() -> dict[str, Any]:
|
||||||
return schema
|
return schema
|
||||||
|
|
||||||
|
|
||||||
|
def prettify_json_schema(schema: dict[str, Any]) -> dict[str, Any]:
|
||||||
|
# Add a $schema field.
|
||||||
|
pretty_schema = {
|
||||||
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
|
}
|
||||||
|
|
||||||
|
# Re-order the properties in the dict so that they are in a sensible order
|
||||||
|
# for humans consuming these schemas.
|
||||||
|
|
||||||
|
# Use this order for top-level keys.
|
||||||
|
key_order = [
|
||||||
|
"title",
|
||||||
|
"description",
|
||||||
|
"type",
|
||||||
|
"properties",
|
||||||
|
"required",
|
||||||
|
"additionalProperties",
|
||||||
|
"if",
|
||||||
|
"then",
|
||||||
|
"$defs",
|
||||||
|
]
|
||||||
|
|
||||||
|
# If there are any other keys not listed above, splice them in before $defs.
|
||||||
|
key_order = [
|
||||||
|
*key_order[:-1],
|
||||||
|
*(set(schema.keys()) - set(key_order)),
|
||||||
|
key_order[-1],
|
||||||
|
]
|
||||||
|
|
||||||
|
pretty_schema.update({key: schema[key] for key in key_order if key in schema})
|
||||||
|
|
||||||
|
# Assert that the schemas have not structurally changed.
|
||||||
|
#
|
||||||
|
# We have to add the $schema field back to the original schema for comparison.
|
||||||
|
schema["$schema"] = pretty_schema["$schema"]
|
||||||
|
assert schema == pretty_schema
|
||||||
|
|
||||||
|
# Next, lets walk the schema and remove attributes we don't care about.
|
||||||
|
def _walk_objects(objs: Iterable[dict[str, Any]]):
|
||||||
|
for obj in objs:
|
||||||
|
_walk_object(obj)
|
||||||
|
|
||||||
|
def _walk_object(obj: dict[str, Any], top_level: bool = False):
|
||||||
|
# All but the top-level title will be auto-generated base on field names. They are
|
||||||
|
# not useful.
|
||||||
|
if not top_level:
|
||||||
|
obj.pop("title", None)
|
||||||
|
|
||||||
|
# We don't support defaults.
|
||||||
|
obj.pop("default", None)
|
||||||
|
|
||||||
|
# This is an OpenAPI extension and it leads to incorrect code generation in our
|
||||||
|
# case (due to using a boolean discriminator).
|
||||||
|
obj.pop("discriminator", None)
|
||||||
|
|
||||||
|
# Strip newlines from descriptions.
|
||||||
|
if description := obj.get("description"):
|
||||||
|
obj["description"] = NEWLINES_RE.sub(" ", description)
|
||||||
|
|
||||||
|
# Remove redundant enum entries for constants.
|
||||||
|
if obj.get("const") is not None:
|
||||||
|
obj.pop("enum", None)
|
||||||
|
|
||||||
|
match obj.get("type"):
|
||||||
|
case "object":
|
||||||
|
if properties := obj.get("properties"):
|
||||||
|
_walk_objects(properties.values())
|
||||||
|
|
||||||
|
case "array":
|
||||||
|
if items := obj.get("items"):
|
||||||
|
_walk_object(items)
|
||||||
|
|
||||||
|
for group_key in ("allOf", "anyOf", "oneOf"):
|
||||||
|
if group := obj.get(group_key):
|
||||||
|
_walk_objects(group)
|
||||||
|
|
||||||
|
_walk_object(pretty_schema, top_level=True)
|
||||||
|
if defs := pretty_schema.get("$defs"):
|
||||||
|
_walk_objects(defs.values())
|
||||||
|
|
||||||
|
return pretty_schema
|
||||||
|
|
||||||
|
|
||||||
|
def write_json_schemas(json_schemas_path: Path, python_package_dir: Path):
|
||||||
|
json_schemas_path.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
models = {
|
||||||
|
model_name: getattr(experiments, model_name)
|
||||||
|
for model_name in experiments.__all__
|
||||||
|
if issubclass(getattr(experiments, model_name), BaseModel)
|
||||||
|
}
|
||||||
|
|
||||||
|
written_paths = set()
|
||||||
|
|
||||||
|
for model_name, model in models.items():
|
||||||
|
model_schema_path = json_schemas_path / f"{model_name}.schema.json"
|
||||||
|
written_paths.add(model_schema_path)
|
||||||
|
|
||||||
|
json_schema = prettify_json_schema(model.model_json_schema())
|
||||||
|
with model_schema_path.open("w") as f:
|
||||||
|
json.dump(json_schema, f, indent=2)
|
||||||
|
f.write("\n")
|
||||||
|
|
||||||
|
# Ensure we don't include any files in schemas/ that we did not generate (e.g., if a
|
||||||
|
# model gets removed).
|
||||||
|
for path in list(json_schemas_path.iterdir()):
|
||||||
|
if path not in written_paths:
|
||||||
|
path.unlink()
|
||||||
|
|
||||||
|
# Copy schemas into the python package.
|
||||||
|
schemas_dist_dir = python_package_dir / "schemas"
|
||||||
|
if schemas_dist_dir.exists():
|
||||||
|
shutil.rmtree(schemas_dist_dir)
|
||||||
|
|
||||||
|
shutil.copytree(json_schemas_path, schemas_dist_dir)
|
||||||
|
|
||||||
|
|
||||||
@click.command()
|
@click.command()
|
||||||
@click.option(
|
@click.option(
|
||||||
"--output",
|
"--output",
|
||||||
|
@ -107,7 +228,25 @@ def iterate_models() -> dict[str, Any]:
|
||||||
default=Path("index.d.ts"),
|
default=Path("index.d.ts"),
|
||||||
help="Output typescript file.",
|
help="Output typescript file.",
|
||||||
)
|
)
|
||||||
def main(*, ts_output_path: Path):
|
@click.option(
|
||||||
|
"--json-schemas",
|
||||||
|
"json_schemas_path",
|
||||||
|
type=Path,
|
||||||
|
default=Path("schemas"),
|
||||||
|
help="Output JSON Schemas to this directory.",
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
"--python-package-dir",
|
||||||
|
"python_package_dir",
|
||||||
|
type=Path,
|
||||||
|
default=Path("mozilla_nimbus_schemas"),
|
||||||
|
help=(
|
||||||
|
"The directory to the mozilla-nimbus-schemas python package.\n"
|
||||||
|
"\n"
|
||||||
|
"Schemas will be installed inside this package at the schemas dir."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
def main(*, ts_output_path: Path, json_schemas_path: Path, python_package_dir: Path):
|
||||||
json_schema = iterate_models()
|
json_schema = iterate_models()
|
||||||
|
|
||||||
with TemporaryDirectory() as tmp_dir:
|
with TemporaryDirectory() as tmp_dir:
|
||||||
|
@ -133,6 +272,8 @@ def main(*, ts_output_path: Path):
|
||||||
|
|
||||||
clean_output_file(ts_output_path)
|
clean_output_file(ts_output_path)
|
||||||
|
|
||||||
|
write_json_schemas(json_schemas_path, python_package_dir)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
main()
|
main()
|
||||||
|
|
|
@ -32,15 +32,6 @@ export type SizingMetricName = "active_hours" | "search_count" | "days_of_use" |
|
||||||
export type StatisticIngestEnum = "percentage" | "binomial" | "mean" | "count";
|
export type StatisticIngestEnum = "percentage" | "binomial" | "mean" | "count";
|
||||||
export type Statistics = Statistic[];
|
export type Statistics = Statistic[];
|
||||||
|
|
||||||
/**
|
|
||||||
* The Firefox Desktop-specific feature manifest.
|
|
||||||
*
|
|
||||||
* Firefox Desktop requires different fields for its features compared to the general
|
|
||||||
* Nimbus feature manifest.
|
|
||||||
*/
|
|
||||||
export interface DesktopFeatureManifest {
|
|
||||||
[k: string]: DesktopFeature;
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* A feature.
|
* A feature.
|
||||||
*/
|
*/
|
||||||
|
@ -133,6 +124,15 @@ export interface NimbusFeatureSchema {
|
||||||
*/
|
*/
|
||||||
path: string;
|
path: string;
|
||||||
}
|
}
|
||||||
|
/**
|
||||||
|
* The Firefox Desktop-specific feature manifest.
|
||||||
|
*
|
||||||
|
* Firefox Desktop requires different fields for its features compared to the general
|
||||||
|
* Nimbus feature manifest.
|
||||||
|
*/
|
||||||
|
export interface DesktopFeatureManifest {
|
||||||
|
[k: string]: DesktopFeature;
|
||||||
|
}
|
||||||
/**
|
/**
|
||||||
* The experiment definition accessible to:
|
* The experiment definition accessible to:
|
||||||
*
|
*
|
||||||
|
|
|
@ -1,7 +1,8 @@
|
||||||
from .experiments import NimbusExperiment, RandomizationUnit
|
from .experiments import NimbusExperiment, RandomizationUnit
|
||||||
from .feature_manifests import DesktopFeatureManifest, SdkFeatureManifest
|
from .feature_manifests import DesktopFeature, DesktopFeatureManifest, SdkFeatureManifest
|
||||||
|
|
||||||
__all__ = (
|
__all__ = (
|
||||||
|
"DesktopFeature",
|
||||||
"DesktopFeatureManifest",
|
"DesktopFeatureManifest",
|
||||||
"NimbusExperiment",
|
"NimbusExperiment",
|
||||||
"RandomizationUnit",
|
"RandomizationUnit",
|
||||||
|
|
|
@ -2,6 +2,7 @@ from enum import Enum
|
||||||
|
|
||||||
from pydantic import (
|
from pydantic import (
|
||||||
BaseModel,
|
BaseModel,
|
||||||
|
ConfigDict,
|
||||||
Field,
|
Field,
|
||||||
RootModel,
|
RootModel,
|
||||||
model_validator,
|
model_validator,
|
||||||
|
@ -56,6 +57,18 @@ class SdkFeatureVariable(BaseFeatureVariable):
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"dependentSchemas": {
|
||||||
|
"enum": {
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": FeatureVariableType.STRING.value},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_enum(cls, data: Self) -> Self:
|
def validate_enum(cls, data: Self) -> Self:
|
||||||
|
@ -102,6 +115,87 @@ class DesktopFeatureVariable(BaseFeatureVariable):
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"dependentSchemas": {
|
||||||
|
# This is the equivalent of the `validate_enums` validator.
|
||||||
|
#
|
||||||
|
# This could also be done declaratively by specializing FeatureVariable
|
||||||
|
# into specifically typed child classes and using a union in the parent
|
||||||
|
# class, but that is much more verbose and generates a bunch of
|
||||||
|
# boilerplate types.
|
||||||
|
#
|
||||||
|
# From a JSON Schema perspective, don't have to tuck this away in
|
||||||
|
# dependentSchemas and the allOf clause could live at the top-level, but
|
||||||
|
# then json-schema-to-typescript gets confused and generates an empty type
|
||||||
|
# for `DesktopFeatureVariable`.
|
||||||
|
"enum": {
|
||||||
|
"allOf": [
|
||||||
|
*(
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": ty},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {
|
||||||
|
"items": {"type": json_schema_ty},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for ty, json_schema_ty in (
|
||||||
|
(FeatureVariableType.STRING, "string"),
|
||||||
|
(FeatureVariableType.INT, "integer"),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
*(
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {"const": ty},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {"const": None},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
for ty in (
|
||||||
|
FeatureVariableType.BOOLEAN,
|
||||||
|
FeatureVariableType.JSON,
|
||||||
|
)
|
||||||
|
),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
# These are the the equivalent of the
|
||||||
|
# `validate_set_pref_fallback_pref_mutually_exclusive` validator.
|
||||||
|
#
|
||||||
|
# Pydantic does not have a way to encode this relationship outside custom
|
||||||
|
# validation.
|
||||||
|
"fallbackPref": {
|
||||||
|
"description": "setPref is mutually exclusive with fallbackPref",
|
||||||
|
"properties": {
|
||||||
|
"setPref": {
|
||||||
|
"const": None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"setPref": {
|
||||||
|
"description": "fallbackPref is mutually exclusive with setPref",
|
||||||
|
"properties": {
|
||||||
|
"fallbackPref": {
|
||||||
|
"const": None,
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_set_pref_fallback_pref_mutually_exclusive(cls, data: Self) -> Self:
|
def validate_set_pref_fallback_pref_mutually_exclusive(cls, data: Self) -> Self:
|
||||||
|
@ -170,6 +264,21 @@ class BaseFeature(BaseModel):
|
||||||
default=None,
|
default=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
model_config = ConfigDict(
|
||||||
|
json_schema_extra={
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"hasExposure": {
|
||||||
|
"const": True,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": ["exposureDescription"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
@model_validator(mode="after")
|
@model_validator(mode="after")
|
||||||
@classmethod
|
@classmethod
|
||||||
def validate_exposure_description(cls, data: Self) -> Self:
|
def validate_exposure_description(cls, data: Self) -> Self:
|
||||||
|
|
|
@ -1,9 +1,14 @@
|
||||||
|
import importlib.resources
|
||||||
|
import json
|
||||||
|
from functools import cache
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import pydantic
|
import pydantic
|
||||||
import pytest
|
import pytest
|
||||||
import yaml
|
import yaml
|
||||||
|
from jsonschema.protocols import Validator
|
||||||
|
from jsonschema.validators import validator_for
|
||||||
|
|
||||||
from mozilla_nimbus_schemas.experiments.feature_manifests import (
|
from mozilla_nimbus_schemas.experiments.feature_manifests import (
|
||||||
DesktopFeatureManifest,
|
DesktopFeatureManifest,
|
||||||
|
@ -14,22 +19,108 @@ from mozilla_nimbus_schemas.experiments.feature_manifests import (
|
||||||
|
|
||||||
FIXTURE_DIR = Path(__file__).parent / "fixtures" / "feature_manifests"
|
FIXTURE_DIR = Path(__file__).parent / "fixtures" / "feature_manifests"
|
||||||
|
|
||||||
|
PACKAGE_DIR = importlib.resources.files("mozilla_nimbus_schemas")
|
||||||
|
SCHEMAS_DIR = PACKAGE_DIR / "schemas"
|
||||||
|
|
||||||
|
|
||||||
|
def load_schema(name: str) -> Validator:
|
||||||
|
with SCHEMAS_DIR.joinpath(name).open() as f:
|
||||||
|
schema = json.load(f)
|
||||||
|
|
||||||
|
validator = validator_for(schema)
|
||||||
|
validator.check_schema(schema)
|
||||||
|
|
||||||
|
return validator(schema)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
@cache
|
||||||
|
def desktop_feature_schema_validator() -> Validator:
|
||||||
|
return load_schema("DesktopFeature.schema.json")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
@cache
|
||||||
|
def desktop_feature_manifest_schema_validator() -> Validator:
|
||||||
|
return load_schema("DesktopFeatureManifest.schema.json")
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
@cache
|
||||||
|
def sdk_feature_manifest_schema_validator() -> Validator:
|
||||||
|
return load_schema("SdkFeatureManifest.schema.json")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("manifest_file", FIXTURE_DIR.joinpath("desktop").iterdir())
|
@pytest.mark.parametrize("manifest_file", FIXTURE_DIR.joinpath("desktop").iterdir())
|
||||||
def test_desktop_manifest_fixtures_are_valid(manifest_file):
|
def test_desktop_manifest_fixtures_are_valid(
|
||||||
|
manifest_file, desktop_feature_manifest_schema_validator
|
||||||
|
):
|
||||||
with manifest_file.open() as f:
|
with manifest_file.open() as f:
|
||||||
contents = yaml.safe_load(f)
|
contents = yaml.safe_load(f)
|
||||||
|
|
||||||
DesktopFeatureManifest.model_validate(contents)
|
DesktopFeatureManifest.model_validate(contents)
|
||||||
|
|
||||||
|
assert desktop_feature_manifest_schema_validator.is_valid(contents)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize("manifest_file", FIXTURE_DIR.joinpath("sdk").iterdir())
|
@pytest.mark.parametrize("manifest_file", FIXTURE_DIR.joinpath("sdk").iterdir())
|
||||||
def test_sdk_manifest_fixtures_are_valid(manifest_file):
|
def test_sdk_manifest_fixtures_are_valid(
|
||||||
|
manifest_file, sdk_feature_manifest_schema_validator
|
||||||
|
):
|
||||||
with manifest_file.open() as f:
|
with manifest_file.open() as f:
|
||||||
contents = yaml.safe_load(f)
|
contents = yaml.safe_load(f)
|
||||||
|
|
||||||
SdkFeatureManifest.model_validate(contents)
|
SdkFeatureManifest.model_validate(contents)
|
||||||
|
|
||||||
|
assert sdk_feature_manifest_schema_validator.is_valid(contents)
|
||||||
|
|
||||||
|
|
||||||
|
def test_desktop_feature_exposure_description_conditionally_required(
|
||||||
|
desktop_feature_schema_validator,
|
||||||
|
):
|
||||||
|
assert desktop_feature_schema_validator.is_valid(
|
||||||
|
{
|
||||||
|
"owner": "owner@example.com",
|
||||||
|
"description": "placeholder",
|
||||||
|
"hasExposure": False,
|
||||||
|
"variables": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
errors = list(
|
||||||
|
desktop_feature_schema_validator.iter_errors(
|
||||||
|
{
|
||||||
|
"owner": "owner@example.com",
|
||||||
|
"description": "placeholder",
|
||||||
|
"hasExposure": True,
|
||||||
|
"variables": {},
|
||||||
|
}
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [e.message for e in errors] == ["'exposureDescription' is a required property"]
|
||||||
|
assert tuple(errors[0].path) == ()
|
||||||
|
|
||||||
|
|
||||||
|
def test_sdk_feature_manifest_feature_exposure_description_conditionally_required(
|
||||||
|
sdk_feature_manifest_schema_validator,
|
||||||
|
):
|
||||||
|
manifest = {
|
||||||
|
"feature": {
|
||||||
|
"description": "placeholder",
|
||||||
|
"hasExposure": False,
|
||||||
|
"variables": {},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert sdk_feature_manifest_schema_validator.is_valid(manifest)
|
||||||
|
|
||||||
|
manifest["feature"]["hasExposure"] = True
|
||||||
|
|
||||||
|
errors = list(sdk_feature_manifest_schema_validator.iter_errors(manifest))
|
||||||
|
|
||||||
|
assert [e.message for e in errors] == ["'exposureDescription' is a required property"]
|
||||||
|
assert tuple(errors[0].path) == ("feature",)
|
||||||
|
|
||||||
|
|
||||||
def test_sdk_feature_variable_valid_enum():
|
def test_sdk_feature_variable_valid_enum():
|
||||||
SdkFeatureVariable.model_validate(
|
SdkFeatureVariable.model_validate(
|
||||||
|
@ -63,7 +154,7 @@ def test_sdk_feature_variable_invalid_enum_unsupported_type(model_json):
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"model_json,expected_pydantic_error",
|
"model_json,expected_pydantic_error,expected_jsonschema_error,expected_jsonschema_error_path",
|
||||||
[
|
[
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
@ -72,6 +163,8 @@ def test_sdk_feature_variable_invalid_enum_unsupported_type(model_json):
|
||||||
"enum": ["hello"],
|
"enum": ["hello"],
|
||||||
},
|
},
|
||||||
"only string enums are supported",
|
"only string enums are supported",
|
||||||
|
"'string' was expected",
|
||||||
|
("feature", "variables", "variable", "type"),
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
{
|
{
|
||||||
|
@ -80,16 +173,35 @@ def test_sdk_feature_variable_invalid_enum_unsupported_type(model_json):
|
||||||
"enum": [1],
|
"enum": [1],
|
||||||
},
|
},
|
||||||
"Input should be a valid string",
|
"Input should be a valid string",
|
||||||
|
"1 is not of type 'string'",
|
||||||
|
("feature", "variables", "variable", "enum", 0),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_sdk_feature_variable_invalid_enum_type_mismatch(
|
def test_sdk_feature_variable_invalid_enum_type_mismatch(
|
||||||
model_json,
|
model_json,
|
||||||
expected_pydantic_error,
|
expected_pydantic_error,
|
||||||
|
expected_jsonschema_error,
|
||||||
|
expected_jsonschema_error_path,
|
||||||
|
sdk_feature_manifest_schema_validator,
|
||||||
):
|
):
|
||||||
with pytest.raises(pydantic.ValidationError, match=expected_pydantic_error):
|
with pytest.raises(pydantic.ValidationError, match=expected_pydantic_error):
|
||||||
SdkFeatureVariable.model_validate(model_json)
|
SdkFeatureVariable.model_validate(model_json)
|
||||||
|
|
||||||
|
manifest = {
|
||||||
|
"feature": {
|
||||||
|
"description": "description",
|
||||||
|
"hasExposure": False,
|
||||||
|
"variables": {
|
||||||
|
"variable": model_json,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
errors = list(sdk_feature_manifest_schema_validator.iter_errors(manifest))
|
||||||
|
assert [e.message for e in errors] == [expected_jsonschema_error]
|
||||||
|
assert tuple(errors[0].path) == expected_jsonschema_error_path
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"model_json",
|
"model_json",
|
||||||
|
@ -136,34 +248,68 @@ def _desktop_feature_with_variable(variable: dict[str, Any]) -> dict[str, Any]:
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_desktop_feature_variable_invalid_enum_types(model_json):
|
def test_desktop_feature_variable_invalid_enum_types(
|
||||||
|
model_json, desktop_feature_schema_validator
|
||||||
|
):
|
||||||
with pytest.raises(pydantic.ValidationError):
|
with pytest.raises(pydantic.ValidationError):
|
||||||
DesktopFeatureVariable.model_validate(model_json)
|
DesktopFeatureVariable.model_validate(model_json)
|
||||||
|
|
||||||
|
errors = list(
|
||||||
|
desktop_feature_schema_validator.iter_errors(
|
||||||
|
_desktop_feature_with_variable(model_json)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [e.message for e in errors] == [
|
||||||
|
"None was expected",
|
||||||
|
f"{str(model_json['enum'])} is not valid under any of the given schemas",
|
||||||
|
]
|
||||||
|
|
||||||
|
assert tuple(errors[0].path) == ("variables", "variable", "enum")
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.parametrize(
|
@pytest.mark.parametrize(
|
||||||
"model_json",
|
"model_json,expected_error",
|
||||||
[
|
[
|
||||||
|
(
|
||||||
{
|
{
|
||||||
"description": "invalid enum (string options for int type)",
|
"description": "invalid enum (string options for int type)",
|
||||||
"type": "int",
|
"type": "int",
|
||||||
"enum": ["hello"],
|
"enum": ["hello"],
|
||||||
},
|
},
|
||||||
|
"'hello' is not of type 'integer'",
|
||||||
|
),
|
||||||
|
(
|
||||||
{
|
{
|
||||||
"description": "invalid enum (int options for string type)",
|
"description": "invalid enum (int options for string type)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
"enum": [1],
|
"enum": [1],
|
||||||
},
|
},
|
||||||
|
"1 is not of type 'string'",
|
||||||
|
),
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_desktop_feature_variable_invalid_enum_type_mismatch(model_json):
|
def test_desktop_feature_variable_invalid_enum_type_mismatch(
|
||||||
|
model_json, expected_error, desktop_feature_schema_validator
|
||||||
|
):
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
pydantic.ValidationError, match="enum values do not match variable type"
|
pydantic.ValidationError, match="enum values do not match variable type"
|
||||||
):
|
):
|
||||||
DesktopFeatureVariable.model_validate(model_json)
|
DesktopFeatureVariable.model_validate(model_json)
|
||||||
|
|
||||||
|
errors = list(
|
||||||
|
desktop_feature_schema_validator.iter_errors(
|
||||||
|
_desktop_feature_with_variable(model_json)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
def test_desktop_feature_variable_invalid_fallback_pref_set_pref_mutually_exclusive():
|
assert [e.message for e in errors] == [expected_error]
|
||||||
|
assert tuple(errors[0].path) == ("variables", "variable", "enum", 0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_desktop_feature_variable_invalid_fallback_pref_set_pref_mutually_exclusive(
|
||||||
|
desktop_feature_schema_validator,
|
||||||
|
):
|
||||||
model_json = {
|
model_json = {
|
||||||
"description": "invalid variable (fallbackPref and setPref mutually exclusive)",
|
"description": "invalid variable (fallbackPref and setPref mutually exclusive)",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
|
@ -179,3 +325,13 @@ def test_desktop_feature_variable_invalid_fallback_pref_set_pref_mutually_exclus
|
||||||
match="fallback_pref and set_pref are mutually exclusive",
|
match="fallback_pref and set_pref are mutually exclusive",
|
||||||
):
|
):
|
||||||
DesktopFeatureVariable.model_validate(model_json)
|
DesktopFeatureVariable.model_validate(model_json)
|
||||||
|
|
||||||
|
errors = list(
|
||||||
|
desktop_feature_schema_validator.iter_errors(
|
||||||
|
_desktop_feature_with_variable(model_json)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert [e.message for e in errors] == ["None was expected", "None was expected"]
|
||||||
|
assert tuple(errors[0].path) == ("variables", "variable", "setPref")
|
||||||
|
assert tuple(errors[1].path) == ("variables", "variable", "fallbackPref")
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
{
|
{
|
||||||
"name": "@mozilla/nimbus-schemas",
|
"name": "@mozilla/nimbus-schemas",
|
||||||
"version": "2024.9.3",
|
"version": "2024.10.1",
|
||||||
"description": "Schemas used by Mozilla Nimbus and related projects.",
|
"description": "Schemas used by Mozilla Nimbus and related projects.",
|
||||||
"main": "index.d.ts",
|
"main": "index.d.ts",
|
||||||
"repository": {
|
"repository": {
|
||||||
|
@ -18,6 +18,7 @@
|
||||||
"typescript": "^5.1.6"
|
"typescript": "^5.1.6"
|
||||||
},
|
},
|
||||||
"files": [
|
"files": [
|
||||||
"index.d.ts"
|
"index.d.ts",
|
||||||
|
"schemas"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# This file is automatically @generated by Poetry 1.8.3 and should not be changed by hand.
|
# This file is automatically @generated by Poetry 1.6.0 and should not be changed by hand.
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "annotated-types"
|
name = "annotated-types"
|
||||||
|
@ -11,6 +11,25 @@ files = [
|
||||||
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
{file = "annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89"},
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "attrs"
|
||||||
|
version = "24.2.0"
|
||||||
|
description = "Classes Without Boilerplate"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.7"
|
||||||
|
files = [
|
||||||
|
{file = "attrs-24.2.0-py3-none-any.whl", hash = "sha256:81921eb96de3191c8258c199618104dd27ac608d9366f5e35d011eae1867ede2"},
|
||||||
|
{file = "attrs-24.2.0.tar.gz", hash = "sha256:5cfb1b9148b5b086569baec03f20d7b6bf3bcacc9a42bebf87ffaaca362f6346"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||||
|
cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||||
|
dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||||
|
docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier (<24.7)"]
|
||||||
|
tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"]
|
||||||
|
tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "backports-tarfile"
|
name = "backports-tarfile"
|
||||||
version = "1.2.0"
|
version = "1.2.0"
|
||||||
|
@ -484,6 +503,41 @@ files = [
|
||||||
test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"]
|
test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "trio"]
|
||||||
trio = ["async_generator", "trio"]
|
trio = ["async_generator", "trio"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonschema"
|
||||||
|
version = "4.23.0"
|
||||||
|
description = "An implementation of JSON Schema validation for Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "jsonschema-4.23.0-py3-none-any.whl", hash = "sha256:fbadb6f8b144a8f8cf9f0b89ba94501d143e50411a1278633f56a7acf7fd5566"},
|
||||||
|
{file = "jsonschema-4.23.0.tar.gz", hash = "sha256:d71497fef26351a33265337fa77ffeb82423f3ea21283cd9467bb03999266bc4"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
attrs = ">=22.2.0"
|
||||||
|
jsonschema-specifications = ">=2023.03.6"
|
||||||
|
referencing = ">=0.28.4"
|
||||||
|
rpds-py = ">=0.7.1"
|
||||||
|
|
||||||
|
[package.extras]
|
||||||
|
format = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3987", "uri-template", "webcolors (>=1.11)"]
|
||||||
|
format-nongpl = ["fqdn", "idna", "isoduration", "jsonpointer (>1.13)", "rfc3339-validator", "rfc3986-validator (>0.1.0)", "uri-template", "webcolors (>=24.6.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "jsonschema-specifications"
|
||||||
|
version = "2024.10.1"
|
||||||
|
description = "The JSON Schema meta-schemas and vocabularies, exposed as a Registry"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.9"
|
||||||
|
files = [
|
||||||
|
{file = "jsonschema_specifications-2024.10.1-py3-none-any.whl", hash = "sha256:a09a0680616357d9a0ecf05c12ad234479f549239d0f5b55f3deea67475da9bf"},
|
||||||
|
{file = "jsonschema_specifications-2024.10.1.tar.gz", hash = "sha256:0f38b83639958ce1152d02a7f062902c41c8fd20d558b0c34344292d417ae272"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
referencing = ">=0.31.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "keyring"
|
name = "keyring"
|
||||||
version = "25.3.0"
|
version = "25.3.0"
|
||||||
|
@ -959,6 +1013,21 @@ Pygments = ">=2.5.1"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
md = ["cmarkgfm (>=0.8.0)"]
|
md = ["cmarkgfm (>=0.8.0)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "referencing"
|
||||||
|
version = "0.35.1"
|
||||||
|
description = "JSON Referencing + Python"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "referencing-0.35.1-py3-none-any.whl", hash = "sha256:eda6d3234d62814d1c64e305c1331c9a3a6132da475ab6382eaa997b21ee75de"},
|
||||||
|
{file = "referencing-0.35.1.tar.gz", hash = "sha256:25b42124a6c8b632a425174f24087783efb348a6f1e0008e63cd4466fedf703c"},
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dependencies]
|
||||||
|
attrs = ">=22.2.0"
|
||||||
|
rpds-py = ">=0.7.0"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.3"
|
version = "2.32.3"
|
||||||
|
@ -1026,6 +1095,118 @@ pygments = ">=2.13.0,<3.0.0"
|
||||||
[package.extras]
|
[package.extras]
|
||||||
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
jupyter = ["ipywidgets (>=7.5.1,<9)"]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "rpds-py"
|
||||||
|
version = "0.20.0"
|
||||||
|
description = "Python bindings to Rust's persistent data structures (rpds)"
|
||||||
|
optional = false
|
||||||
|
python-versions = ">=3.8"
|
||||||
|
files = [
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:3ad0fda1635f8439cde85c700f964b23ed5fc2d28016b32b9ee5fe30da5c84e2"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9bb4a0d90fdb03437c109a17eade42dfbf6190408f29b2744114d11586611d6f"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:c6377e647bbfd0a0b159fe557f2c6c602c159fc752fa316572f012fc0bf67150"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:eb851b7df9dda52dc1415ebee12362047ce771fc36914586b2e9fcbd7d293b3e"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1e0f80b739e5a8f54837be5d5c924483996b603d5502bfff79bf33da06164ee2"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5a8c94dad2e45324fc74dce25e1645d4d14df9a4e54a30fa0ae8bad9a63928e3"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f8e604fe73ba048c06085beaf51147eaec7df856824bfe7b98657cf436623daf"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:df3de6b7726b52966edf29663e57306b23ef775faf0ac01a3e9f4012a24a4140"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:cf258ede5bc22a45c8e726b29835b9303c285ab46fc7c3a4cc770736b5304c9f"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:55fea87029cded5df854ca7e192ec7bdb7ecd1d9a3f63d5c4eb09148acf4a7ce"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:ae94bd0b2f02c28e199e9bc51485d0c5601f58780636185660f86bf80c89af94"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-none-win32.whl", hash = "sha256:28527c685f237c05445efec62426d285e47a58fb05ba0090a4340b73ecda6dee"},
|
||||||
|
{file = "rpds_py-0.20.0-cp310-none-win_amd64.whl", hash = "sha256:238a2d5b1cad28cdc6ed15faf93a998336eb041c4e440dd7f902528b8891b399"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:ac2f4f7a98934c2ed6505aead07b979e6f999389f16b714448fb39bbaa86a489"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:220002c1b846db9afd83371d08d239fdc865e8f8c5795bbaec20916a76db3318"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8d7919548df3f25374a1f5d01fbcd38dacab338ef5f33e044744b5c36729c8db"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:758406267907b3781beee0f0edfe4a179fbd97c0be2e9b1154d7f0a1279cf8e5"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3d61339e9f84a3f0767b1995adfb171a0d00a1185192718a17af6e124728e0f5"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:1259c7b3705ac0a0bd38197565a5d603218591d3f6cee6e614e380b6ba61c6f6"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:5c1dc0f53856b9cc9a0ccca0a7cc61d3d20a7088201c0937f3f4048c1718a209"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:7e60cb630f674a31f0368ed32b2a6b4331b8350d67de53c0359992444b116dd3"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:dbe982f38565bb50cb7fb061ebf762c2f254ca3d8c20d4006878766e84266272"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:514b3293b64187172bc77c8fb0cdae26981618021053b30d8371c3a902d4d5ad"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:d0a26ffe9d4dd35e4dfdd1e71f46401cff0181c75ac174711ccff0459135fa58"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-none-win32.whl", hash = "sha256:89c19a494bf3ad08c1da49445cc5d13d8fefc265f48ee7e7556839acdacf69d0"},
|
||||||
|
{file = "rpds_py-0.20.0-cp311-none-win_amd64.whl", hash = "sha256:c638144ce971df84650d3ed0096e2ae7af8e62ecbbb7b201c8935c370df00a2c"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:a84ab91cbe7aab97f7446652d0ed37d35b68a465aeef8fc41932a9d7eee2c1a6"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:56e27147a5a4c2c21633ff8475d185734c0e4befd1c989b5b95a5d0db699b21b"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2580b0c34583b85efec8c5c5ec9edf2dfe817330cc882ee972ae650e7b5ef739"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:b80d4a7900cf6b66bb9cee5c352b2d708e29e5a37fe9bf784fa97fc11504bf6c"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50eccbf054e62a7b2209b28dc7a22d6254860209d6753e6b78cfaeb0075d7bee"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:49a8063ea4296b3a7e81a5dfb8f7b2d73f0b1c20c2af401fb0cdf22e14711a96"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ea438162a9fcbee3ecf36c23e6c68237479f89f962f82dae83dc15feeceb37e4"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:18d7585c463087bddcfa74c2ba267339f14f2515158ac4db30b1f9cbdb62c8ef"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d4c7d1a051eeb39f5c9547e82ea27cbcc28338482242e3e0b7768033cb083821"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:e4df1e3b3bec320790f699890d41c59d250f6beda159ea3c44c3f5bac1976940"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2cf126d33a91ee6eedc7f3197b53e87a2acdac63602c0f03a02dd69e4b138174"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-none-win32.whl", hash = "sha256:8bc7690f7caee50b04a79bf017a8d020c1f48c2a1077ffe172abec59870f1139"},
|
||||||
|
{file = "rpds_py-0.20.0-cp312-none-win_amd64.whl", hash = "sha256:0e13e6952ef264c40587d510ad676a988df19adea20444c2b295e536457bc585"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:aa9a0521aeca7d4941499a73ad7d4f8ffa3d1affc50b9ea11d992cd7eff18a29"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:4a1f1d51eccb7e6c32ae89243cb352389228ea62f89cd80823ea7dd1b98e0b91"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8a86a9b96070674fc88b6f9f71a97d2c1d3e5165574615d1f9168ecba4cecb24"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:6c8ef2ebf76df43f5750b46851ed1cdf8f109d7787ca40035fe19fbdc1acc5a7"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b74b25f024b421d5859d156750ea9a65651793d51b76a2e9238c05c9d5f203a9"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:57eb94a8c16ab08fef6404301c38318e2c5a32216bf5de453e2714c964c125c8"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e1940dae14e715e2e02dfd5b0f64a52e8374a517a1e531ad9412319dc3ac7879"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:d20277fd62e1b992a50c43f13fbe13277a31f8c9f70d59759c88f644d66c619f"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:06db23d43f26478303e954c34c75182356ca9aa7797d22c5345b16871ab9c45c"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:b2a5db5397d82fa847e4c624b0c98fe59d2d9b7cf0ce6de09e4d2e80f8f5b3f2"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:5a35df9f5548fd79cb2f52d27182108c3e6641a4feb0f39067911bf2adaa3e57"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-none-win32.whl", hash = "sha256:fd2d84f40633bc475ef2d5490b9c19543fbf18596dcb1b291e3a12ea5d722f7a"},
|
||||||
|
{file = "rpds_py-0.20.0-cp313-none-win_amd64.whl", hash = "sha256:9bc2d153989e3216b0559251b0c260cfd168ec78b1fac33dd485750a228db5a2"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-macosx_10_12_x86_64.whl", hash = "sha256:f2fbf7db2012d4876fb0d66b5b9ba6591197b0f165db8d99371d976546472a24"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-macosx_11_0_arm64.whl", hash = "sha256:1e5f3cd7397c8f86c8cc72d5a791071431c108edd79872cdd96e00abd8497d29"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce9845054c13696f7af7f2b353e6b4f676dab1b4b215d7fe5e05c6f8bb06f965"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c3e130fd0ec56cb76eb49ef52faead8ff09d13f4527e9b0c400307ff72b408e1"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:4b16aa0107ecb512b568244ef461f27697164d9a68d8b35090e9b0c1c8b27752"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:aa7f429242aae2947246587d2964fad750b79e8c233a2367f71b554e9447949c"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:af0fc424a5842a11e28956e69395fbbeab2c97c42253169d87e90aac2886d751"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b8c00a3b1e70c1d3891f0db1b05292747f0dbcfb49c43f9244d04c70fbc40eb8"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:40ce74fc86ee4645d0a225498d091d8bc61f39b709ebef8204cb8b5a464d3c0e"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:4fe84294c7019456e56d93e8ababdad5a329cd25975be749c3f5f558abb48253"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:338ca4539aad4ce70a656e5187a3a31c5204f261aef9f6ab50e50bcdffaf050a"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-none-win32.whl", hash = "sha256:54b43a2b07db18314669092bb2de584524d1ef414588780261e31e85846c26a5"},
|
||||||
|
{file = "rpds_py-0.20.0-cp38-none-win_amd64.whl", hash = "sha256:a1862d2d7ce1674cffa6d186d53ca95c6e17ed2b06b3f4c476173565c862d232"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-macosx_10_12_x86_64.whl", hash = "sha256:3fde368e9140312b6e8b6c09fb9f8c8c2f00999d1823403ae90cc00480221b22"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:9824fb430c9cf9af743cf7aaf6707bf14323fb51ee74425c380f4c846ea70789"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:11ef6ce74616342888b69878d45e9f779b95d4bd48b382a229fe624a409b72c5"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c52d3f2f82b763a24ef52f5d24358553e8403ce05f893b5347098014f2d9eff2"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d35cef91e59ebbeaa45214861874bc6f19eb35de96db73e467a8358d701a96c"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d72278a30111e5b5525c1dd96120d9e958464316f55adb030433ea905866f4de"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:b4c29cbbba378759ac5786730d1c3cb4ec6f8ababf5c42a9ce303dc4b3d08cda"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:6632f2d04f15d1bd6fe0eedd3b86d9061b836ddca4c03d5cf5c7e9e6b7c14580"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:d0b67d87bb45ed1cd020e8fbf2307d449b68abc45402fe1a4ac9e46c3c8b192b"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:ec31a99ca63bf3cd7f1a5ac9fe95c5e2d060d3c768a09bc1d16e235840861420"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:22e6c9976e38f4d8c4a63bd8a8edac5307dffd3ee7e6026d97f3cc3a2dc02a0b"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-none-win32.whl", hash = "sha256:569b3ea770c2717b730b61998b6c54996adee3cef69fc28d444f3e7920313cf7"},
|
||||||
|
{file = "rpds_py-0.20.0-cp39-none-win_amd64.whl", hash = "sha256:e6900ecdd50ce0facf703f7a00df12374b74bbc8ad9fe0f6559947fb20f82364"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:617c7357272c67696fd052811e352ac54ed1d9b49ab370261a80d3b6ce385045"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9426133526f69fcaba6e42146b4e12d6bc6c839b8b555097020e2b78ce908dcc"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:deb62214c42a261cb3eb04d474f7155279c1a8a8c30ac89b7dcb1721d92c3c02"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:fcaeb7b57f1a1e071ebd748984359fef83ecb026325b9d4ca847c95bc7311c92"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d454b8749b4bd70dd0a79f428731ee263fa6995f83ccb8bada706e8d1d3ff89d"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d807dc2051abe041b6649681dce568f8e10668e3c1c6543ebae58f2d7e617855"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c3c20f0ddeb6e29126d45f89206b8291352b8c5b44384e78a6499d68b52ae511"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:b7f19250ceef892adf27f0399b9e5afad019288e9be756d6919cb58892129f51"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:4f1ed4749a08379555cebf4650453f14452eaa9c43d0a95c49db50c18b7da075"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_i686.whl", hash = "sha256:dcedf0b42bcb4cfff4101d7771a10532415a6106062f005ab97d1d0ab5681c60"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:39ed0d010457a78f54090fafb5d108501b5aa5604cc22408fc1c0c77eac14344"},
|
||||||
|
{file = "rpds_py-0.20.0-pp310-pypy310_pp73-win_amd64.whl", hash = "sha256:bb273176be34a746bdac0b0d7e4e2c467323d13640b736c4c477881a3220a989"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_10_12_x86_64.whl", hash = "sha256:f918a1a130a6dfe1d7fe0f105064141342e7dd1611f2e6a21cd2f5c8cb1cfb3e"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-macosx_11_0_arm64.whl", hash = "sha256:f60012a73aa396be721558caa3a6fd49b3dd0033d1675c6d59c4502e870fcf0c"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3d2b1ad682a3dfda2a4e8ad8572f3100f95fad98cb99faf37ff0ddfe9cbf9d03"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:614fdafe9f5f19c63ea02817fa4861c606a59a604a77c8cdef5aa01d28b97921"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fa518bcd7600c584bf42e6617ee8132869e877db2f76bcdc281ec6a4113a53ab"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f0475242f447cc6cb8a9dd486d68b2ef7fbee84427124c232bff5f63b1fe11e5"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f90a4cd061914a60bd51c68bcb4357086991bd0bb93d8aa66a6da7701370708f"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:def7400461c3a3f26e49078302e1c1b38f6752342c77e3cf72ce91ca69fb1bc1"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_aarch64.whl", hash = "sha256:65794e4048ee837494aea3c21a28ad5fc080994dfba5b036cf84de37f7ad5074"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_i686.whl", hash = "sha256:faefcc78f53a88f3076b7f8be0a8f8d35133a3ecf7f3770895c25f8813460f08"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-musllinux_1_2_x86_64.whl", hash = "sha256:5b4f105deeffa28bbcdff6c49b34e74903139afa690e35d2d9e3c2c2fba18cec"},
|
||||||
|
{file = "rpds_py-0.20.0-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:fdfc3a892927458d98f3d55428ae46b921d1f7543b89382fdb483f5640daaec8"},
|
||||||
|
{file = "rpds_py-0.20.0.tar.gz", hash = "sha256:d72a210824facfdaf8768cf2d7ca25a042c30320b3020de2fa04640920d4e121"},
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ruff"
|
name = "ruff"
|
||||||
version = "0.6.1"
|
version = "0.6.1"
|
||||||
|
@ -1173,4 +1354,4 @@ type = ["pytest-mypy"]
|
||||||
[metadata]
|
[metadata]
|
||||||
lock-version = "2.0"
|
lock-version = "2.0"
|
||||||
python-versions = "^3.10"
|
python-versions = "^3.10"
|
||||||
content-hash = "9204c714a20e1d21cc566ac6df2dc43bdf410d72b5dae44afef822dda2dddd66"
|
content-hash = "0c9c045c39541914844c794ec731c3f52efba328be29b68d50056668cee3de37"
|
||||||
|
|
|
@ -1,16 +1,19 @@
|
||||||
[tool.poetry]
|
[tool.poetry]
|
||||||
name = "mozilla-nimbus-schemas"
|
name = "mozilla-nimbus-schemas"
|
||||||
version = "2024.9.3"
|
version = "2024.10.1"
|
||||||
description = "Schemas used by Mozilla Nimbus and related projects."
|
description = "Schemas used by Mozilla Nimbus and related projects."
|
||||||
authors = ["mikewilli"]
|
authors = ["mikewilli"]
|
||||||
license = "MPL 2.0"
|
license = "MPL 2.0"
|
||||||
readme = "README.md"
|
readme = "README.md"
|
||||||
packages = [{ include = "mozilla_nimbus_schemas" }]
|
packages = [{ include = "mozilla_nimbus_schemas" }]
|
||||||
|
include = [{ path = "mozilla_nimbus_schemas/schemas", format = ["sdist", "wheel"] }]
|
||||||
|
|
||||||
[tool.poetry.dependencies]
|
[tool.poetry.dependencies]
|
||||||
python = "^3.10"
|
python = "^3.10"
|
||||||
pydantic = "^2"
|
pydantic = "^2"
|
||||||
polyfactory = "^2.7.2"
|
polyfactory = "^2.7.2"
|
||||||
|
typing-extensions = ">=4.0.1" # Required until Python 3.11
|
||||||
|
jsonschema = "^4.23.0"
|
||||||
|
|
||||||
[tool.poetry.group.dev.dependencies]
|
[tool.poetry.group.dev.dependencies]
|
||||||
ruff = ">=0.5.0,<0.6.2"
|
ruff = ">=0.5.0,<0.6.2"
|
||||||
|
@ -19,6 +22,7 @@ pytest = "^7.3.1"
|
||||||
twine = "^5.1.1"
|
twine = "^5.1.1"
|
||||||
PyYAML = "^6.0"
|
PyYAML = "^6.0"
|
||||||
|
|
||||||
|
|
||||||
[build-system]
|
[build-system]
|
||||||
requires = ["poetry-core"]
|
requires = ["poetry-core"]
|
||||||
build-backend = "poetry.core.masonry.api"
|
build-backend = "poetry.core.masonry.api"
|
||||||
|
|
|
@ -0,0 +1,265 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
|
"title": "DesktopFeature",
|
||||||
|
"description": "A feature.",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"description": "The description of the feature.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"hasExposure": {
|
||||||
|
"description": "Whether or not this feature records exposure telemetry.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"exposureDescription": {
|
||||||
|
"description": "A description of the exposure telemetry collected by this feature. Only required if hasExposure is true.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"description": "The owner of the feature.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isEarlyStartup": {
|
||||||
|
"description": "If true, the feature values will be cached in prefs so that they can be read before Nimbus is initialized during Firefox startup.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"applications": {
|
||||||
|
"description": "The applications that can enroll in experiments for this feature. Defaults to \"firefox-desktop\".",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/DesktopApplication"
|
||||||
|
},
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/DesktopFeatureVariable"
|
||||||
|
},
|
||||||
|
"description": "The variables that this feature can set.",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/$defs/NimbusFeatureSchema",
|
||||||
|
"description": "An optional JSON schema that describes the feature variables."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"description",
|
||||||
|
"hasExposure",
|
||||||
|
"owner",
|
||||||
|
"variables"
|
||||||
|
],
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"hasExposure": {
|
||||||
|
"const": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"exposureDescription"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"DesktopApplication": {
|
||||||
|
"enum": [
|
||||||
|
"firefox-desktop",
|
||||||
|
"firefox-desktop-background-task"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"DesktopFeatureVariable": {
|
||||||
|
"dependentSchemas": {
|
||||||
|
"enum": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {
|
||||||
|
"const": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {
|
||||||
|
"const": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fallbackPref": {
|
||||||
|
"description": "setPref is mutually exclusive with fallbackPref",
|
||||||
|
"properties": {
|
||||||
|
"setPref": {
|
||||||
|
"const": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setPref": {
|
||||||
|
"description": "fallbackPref is mutually exclusive with setPref",
|
||||||
|
"properties": {
|
||||||
|
"fallbackPref": {
|
||||||
|
"const": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "A feature variable.",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"description": "A description of the feature.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/$defs/FeatureVariableType",
|
||||||
|
"description": "The field type."
|
||||||
|
},
|
||||||
|
"enum": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An optional list of possible string or integer values. Only allowed when type is string or int. The types in the enum must match the type of the field."
|
||||||
|
},
|
||||||
|
"fallbackPref": {
|
||||||
|
"description": "A pref that provides the default value for a feature when none is present.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"setPref": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/SetPref"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A pref that should be set to the value of this variable when enrolling in experiments. Using a string is deprecated and unsupported in Firefox 124+."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"description",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"FeatureVariableType": {
|
||||||
|
"enum": [
|
||||||
|
"int",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"NimbusFeatureSchema": {
|
||||||
|
"description": "Information about a JSON schema.",
|
||||||
|
"properties": {
|
||||||
|
"uri": {
|
||||||
|
"description": "The resource:// or chrome:// URI that can be loaded at runtime within Firefox. Required by Firefox so that Nimbus can import the schema for validation.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"description": "The path to the schema file in the source checkout. Required by Experimenter so that it can find schema files in source checkouts.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"uri",
|
||||||
|
"path"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"PrefBranch": {
|
||||||
|
"enum": [
|
||||||
|
"default",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"SetPref": {
|
||||||
|
"properties": {
|
||||||
|
"branch": {
|
||||||
|
"$ref": "#/$defs/PrefBranch",
|
||||||
|
"description": "The branch the pref will be set on. Prefs set on the user branch persists through restarts."
|
||||||
|
},
|
||||||
|
"pref": {
|
||||||
|
"description": "The name of the pref to set.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"branch",
|
||||||
|
"pref"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,272 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
|
"title": "DesktopFeatureManifest",
|
||||||
|
"description": "The Firefox Desktop-specific feature manifest. Firefox Desktop requires different fields for its features compared to the general Nimbus feature manifest.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/DesktopFeature"
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"DesktopApplication": {
|
||||||
|
"enum": [
|
||||||
|
"firefox-desktop",
|
||||||
|
"firefox-desktop-background-task"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"DesktopFeature": {
|
||||||
|
"description": "A feature.",
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"hasExposure": {
|
||||||
|
"const": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"description": "The description of the feature.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"hasExposure": {
|
||||||
|
"description": "Whether or not this feature records exposure telemetry.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"exposureDescription": {
|
||||||
|
"description": "A description of the exposure telemetry collected by this feature. Only required if hasExposure is true.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"owner": {
|
||||||
|
"description": "The owner of the feature.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isEarlyStartup": {
|
||||||
|
"description": "If true, the feature values will be cached in prefs so that they can be read before Nimbus is initialized during Firefox startup.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"applications": {
|
||||||
|
"description": "The applications that can enroll in experiments for this feature. Defaults to \"firefox-desktop\".",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/DesktopApplication"
|
||||||
|
},
|
||||||
|
"minLength": 1,
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/DesktopFeatureVariable"
|
||||||
|
},
|
||||||
|
"description": "The variables that this feature can set.",
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"schema": {
|
||||||
|
"$ref": "#/$defs/NimbusFeatureSchema",
|
||||||
|
"description": "An optional JSON schema that describes the feature variables."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"description",
|
||||||
|
"hasExposure",
|
||||||
|
"owner",
|
||||||
|
"variables"
|
||||||
|
],
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"exposureDescription"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"DesktopFeatureVariable": {
|
||||||
|
"dependentSchemas": {
|
||||||
|
"enum": {
|
||||||
|
"allOf": [
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "int"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "boolean"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {
|
||||||
|
"const": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "json"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"then": {
|
||||||
|
"properties": {
|
||||||
|
"enum": {
|
||||||
|
"const": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"fallbackPref": {
|
||||||
|
"description": "setPref is mutually exclusive with fallbackPref",
|
||||||
|
"properties": {
|
||||||
|
"setPref": {
|
||||||
|
"const": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"setPref": {
|
||||||
|
"description": "fallbackPref is mutually exclusive with setPref",
|
||||||
|
"properties": {
|
||||||
|
"fallbackPref": {
|
||||||
|
"const": null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "A feature variable.",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"description": "A description of the feature.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/$defs/FeatureVariableType",
|
||||||
|
"description": "The field type."
|
||||||
|
},
|
||||||
|
"enum": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "An optional list of possible string or integer values. Only allowed when type is string or int. The types in the enum must match the type of the field."
|
||||||
|
},
|
||||||
|
"fallbackPref": {
|
||||||
|
"description": "A pref that provides the default value for a feature when none is present.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"setPref": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"$ref": "#/$defs/SetPref"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A pref that should be set to the value of this variable when enrolling in experiments. Using a string is deprecated and unsupported in Firefox 124+."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"description",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"FeatureVariableType": {
|
||||||
|
"enum": [
|
||||||
|
"int",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"NimbusFeatureSchema": {
|
||||||
|
"description": "Information about a JSON schema.",
|
||||||
|
"properties": {
|
||||||
|
"uri": {
|
||||||
|
"description": "The resource:// or chrome:// URI that can be loaded at runtime within Firefox. Required by Firefox so that Nimbus can import the schema for validation.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"path": {
|
||||||
|
"description": "The path to the schema file in the source checkout. Required by Experimenter so that it can find schema files in source checkouts.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"uri",
|
||||||
|
"path"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"PrefBranch": {
|
||||||
|
"enum": [
|
||||||
|
"default",
|
||||||
|
"user"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"SetPref": {
|
||||||
|
"properties": {
|
||||||
|
"branch": {
|
||||||
|
"$ref": "#/$defs/PrefBranch",
|
||||||
|
"description": "The branch the pref will be set on. Prefs set on the user branch persists through restarts."
|
||||||
|
},
|
||||||
|
"pref": {
|
||||||
|
"description": "The name of the pref to set.",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"branch",
|
||||||
|
"pref"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,397 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
|
"title": "NimbusExperiment",
|
||||||
|
"description": "The experiment definition accessible to: 1. The Nimbus SDK via Remote Settings 2. Jetstream via the Experimenter API",
|
||||||
|
"type": "object",
|
||||||
|
"properties": {
|
||||||
|
"schemaVersion": {
|
||||||
|
"description": "Version of the NimbusExperiment schema this experiment refers to",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"slug": {
|
||||||
|
"description": "Unique identifier for the experiment",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"id": {
|
||||||
|
"description": "Unique identifier for the experiiment. This is a duplicate of slug, but is required field for all Remote Settings records.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"appName": {
|
||||||
|
"description": "A slug identifying the targeted product of this experiment. It should be a lowercased_with_underscores name that is short and unambiguous and it should match the app_name found in https://probeinfo.telemetry.mozilla.org/glean/repositories. Examples are \"fenix\" and \"firefox_desktop\".",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"appId": {
|
||||||
|
"description": "The platform identifier for the targeted app. This should match app's identifier exactly as it appears in the relevant app store listing (for relevant platforms) or the app's Glean initialization (for other platforms). Examples are \"org.mozilla.firefox_beta\" and \"firefox-desktop\".",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"channel": {
|
||||||
|
"description": "A specific channel of an application such as \"nightly\", \"beta\", or \"release\".",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userFacingName": {
|
||||||
|
"description": "Public name of the experiment that will be displayed on \"about:studies\".",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"userFacingDescription": {
|
||||||
|
"description": "Short public description of the experiment that will be displayed on \"about:studies\".",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"isEnrollmentPaused": {
|
||||||
|
"description": "When this property is set to true, the SDK should not enroll new users into the experiment that have not already been enrolled.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"isRollout": {
|
||||||
|
"description": "When this property is set to true, treat this experiment as a rollout. Rollouts are currently handled as single-branch experiments separated from the bucketing namespace for normal experiments. See-also: https://mozilla-hub.atlassian.net/browse/SDK-405",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"bucketConfig": {
|
||||||
|
"$ref": "#/$defs/ExperimentBucketConfig",
|
||||||
|
"description": "Bucketing configuration."
|
||||||
|
},
|
||||||
|
"outcomes": {
|
||||||
|
"description": "A list of outcomes relevant to the experiment analysis.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/ExperimentOutcome"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"featureIds": {
|
||||||
|
"description": "A list of featureIds the experiment contains configurations for.",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"branches": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/ExperimentSingleFeatureBranch"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/ExperimentMultiFeatureDesktopBranch"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/ExperimentMultiFeatureMobileBranch"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Branch configuration for the experiment."
|
||||||
|
},
|
||||||
|
"targeting": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "A JEXL targeting expression used to filter out experiments."
|
||||||
|
},
|
||||||
|
"startDate": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"format": "date",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Actual publish date of the experiment. Note that this value is expected to be null in Remote Settings."
|
||||||
|
},
|
||||||
|
"enrollmentEndDate": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"format": "date",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Actual enrollment end date of the experiment. Note that this value is expected to be null in Remote Settings."
|
||||||
|
},
|
||||||
|
"endDate": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"format": "date",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Actual end date of this experiment. Note that this field is expected to be null in Remote Settings."
|
||||||
|
},
|
||||||
|
"proposedDuration": {
|
||||||
|
"description": "Duration of the experiment from the start date in days. Note that this property is only used during the analysis phase (i.e., not by the SDK).",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"proposedEnrollment": {
|
||||||
|
"description": "This represents the number of days that we expect to enroll new users. Note that this property is only used during the analysis phase (i.e., not by the SDK).",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"referenceBranch": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The slug of the reference branch (i.e., the branch we consider \"control\")."
|
||||||
|
},
|
||||||
|
"featureValidationOptOut": {
|
||||||
|
"description": "Opt out of feature schema validation. Only supported on desktop.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"localizations": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"additionalProperties": {
|
||||||
|
"additionalProperties": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "Per-locale localization substitutions. The top level key is the locale (e.g., \"en-US\" or \"fr\"). Each entry is a mapping of string IDs to their localized equivalents. Only supported on desktop."
|
||||||
|
},
|
||||||
|
"locales": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The list of locale codes (e.g., \"en-US\" or \"fr\") that this experiment is targeting. If null, all locales are targeted."
|
||||||
|
},
|
||||||
|
"publishedDate": {
|
||||||
|
"anyOf": [
|
||||||
|
{
|
||||||
|
"format": "date-time",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "null"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"description": "The date that this experiment was first published to Remote Settings. If null, it has not yet been published."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"schemaVersion",
|
||||||
|
"slug",
|
||||||
|
"id",
|
||||||
|
"appName",
|
||||||
|
"appId",
|
||||||
|
"channel",
|
||||||
|
"userFacingName",
|
||||||
|
"userFacingDescription",
|
||||||
|
"isEnrollmentPaused",
|
||||||
|
"bucketConfig",
|
||||||
|
"branches",
|
||||||
|
"startDate",
|
||||||
|
"endDate",
|
||||||
|
"proposedEnrollment",
|
||||||
|
"referenceBranch"
|
||||||
|
],
|
||||||
|
"$defs": {
|
||||||
|
"DesktopTombstoneFeatureConfig": {
|
||||||
|
"properties": {
|
||||||
|
"featureId": {
|
||||||
|
"const": "unused-feature-id-for-legacy-support",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"enabled": {
|
||||||
|
"const": false,
|
||||||
|
"type": "boolean"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"featureId",
|
||||||
|
"value",
|
||||||
|
"enabled"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ExperimentBucketConfig": {
|
||||||
|
"properties": {
|
||||||
|
"randomizationUnit": {
|
||||||
|
"$ref": "#/$defs/RandomizationUnit"
|
||||||
|
},
|
||||||
|
"namespace": {
|
||||||
|
"description": "Additional inputs to the hashing function.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"start": {
|
||||||
|
"description": "Index of the starting bucket of the range.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"count": {
|
||||||
|
"description": "Number of buckets in the range.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"total": {
|
||||||
|
"description": "The total number of buckets. You can assume this will always be 10000",
|
||||||
|
"type": "integer"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"randomizationUnit",
|
||||||
|
"namespace",
|
||||||
|
"start",
|
||||||
|
"count",
|
||||||
|
"total"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ExperimentFeatureConfig": {
|
||||||
|
"properties": {
|
||||||
|
"featureId": {
|
||||||
|
"description": "The identifier for the feature flag.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"value": {
|
||||||
|
"description": "The values that define the feature configuration. This should be validated against a schema.",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"featureId",
|
||||||
|
"value"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ExperimentMultiFeatureDesktopBranch": {
|
||||||
|
"description": "The branch definition supported on Firefox Desktop 95+.",
|
||||||
|
"properties": {
|
||||||
|
"slug": {
|
||||||
|
"description": "Identifier for the branch.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ratio": {
|
||||||
|
"description": "Relative ratio of population for the branch. e.g., if branch A=1 and branch B=3, then branch A would get 25% of the population.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"description": "An array of feature configurations.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/ExperimentFeatureConfig"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"$ref": "#/$defs/DesktopTombstoneFeatureConfig",
|
||||||
|
"description": "The feature key must be provided with values to prevent crashes if the is encountered by Desktop clients earlier than version 95."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"slug",
|
||||||
|
"ratio",
|
||||||
|
"features",
|
||||||
|
"feature"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ExperimentMultiFeatureMobileBranch": {
|
||||||
|
"description": "The branch definition for mobile browsers. Supported on Firefox for Android 96+ and Firefox for iOS 39+.",
|
||||||
|
"properties": {
|
||||||
|
"slug": {
|
||||||
|
"description": "Identifier for the branch.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ratio": {
|
||||||
|
"description": "Relative ratio of population for the branch. e.g., if branch A=1 and branch B=3, then branch A would get 25% of the population.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"features": {
|
||||||
|
"description": "An array of feature configurations.",
|
||||||
|
"items": {
|
||||||
|
"$ref": "#/$defs/ExperimentFeatureConfig"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"slug",
|
||||||
|
"ratio",
|
||||||
|
"features"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ExperimentOutcome": {
|
||||||
|
"properties": {
|
||||||
|
"slug": {
|
||||||
|
"description": "Identifier for the outcome.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"priority": {
|
||||||
|
"description": "e.g., \"primary\" or \"secondary\".",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"slug",
|
||||||
|
"priority"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"ExperimentSingleFeatureBranch": {
|
||||||
|
"description": "A single-feature branch definition. Supported by Firefox Desktop for versions before 95, Firefox for Android for versions before 96, and Firefox for iOS for versions before 39.",
|
||||||
|
"properties": {
|
||||||
|
"slug": {
|
||||||
|
"description": "Identifier for the branch.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"ratio": {
|
||||||
|
"description": "Relative ratio of population for the branch. e.g., if branch A=1 and branch B=3, then branch A would get 25% of the population.",
|
||||||
|
"type": "integer"
|
||||||
|
},
|
||||||
|
"feature": {
|
||||||
|
"$ref": "#/$defs/ExperimentFeatureConfig",
|
||||||
|
"description": "A single feature configuration."
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"slug",
|
||||||
|
"ratio",
|
||||||
|
"feature"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"RandomizationUnit": {
|
||||||
|
"description": "A unique, stable indentifier for the user used as an input to bucket hashing.",
|
||||||
|
"enum": [
|
||||||
|
"normandy_id",
|
||||||
|
"nimbus_id",
|
||||||
|
"user_id",
|
||||||
|
"group_id"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,96 @@
|
||||||
|
{
|
||||||
|
"$schema": "https://json-schema.org/draft/2019-09/schema",
|
||||||
|
"title": "SdkFeatureManifest",
|
||||||
|
"description": "The SDK-specific feature manifest.",
|
||||||
|
"type": "object",
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/SdkFeature"
|
||||||
|
},
|
||||||
|
"$defs": {
|
||||||
|
"FeatureVariableType": {
|
||||||
|
"enum": [
|
||||||
|
"int",
|
||||||
|
"string",
|
||||||
|
"boolean",
|
||||||
|
"json"
|
||||||
|
],
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"SdkFeature": {
|
||||||
|
"description": "A feature.",
|
||||||
|
"if": {
|
||||||
|
"properties": {
|
||||||
|
"hasExposure": {
|
||||||
|
"const": true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"description": "The description of the feature.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"hasExposure": {
|
||||||
|
"description": "Whether or not this feature records exposure telemetry.",
|
||||||
|
"type": "boolean"
|
||||||
|
},
|
||||||
|
"exposureDescription": {
|
||||||
|
"description": "A description of the exposure telemetry collected by this feature. Only required if hasExposure is true.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"variables": {
|
||||||
|
"additionalProperties": {
|
||||||
|
"$ref": "#/$defs/SdkFeatureVariable"
|
||||||
|
},
|
||||||
|
"description": "The variables that this feature can set.",
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"description",
|
||||||
|
"hasExposure",
|
||||||
|
"variables"
|
||||||
|
],
|
||||||
|
"then": {
|
||||||
|
"required": [
|
||||||
|
"exposureDescription"
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "object"
|
||||||
|
},
|
||||||
|
"SdkFeatureVariable": {
|
||||||
|
"dependentSchemas": {
|
||||||
|
"enum": {
|
||||||
|
"properties": {
|
||||||
|
"type": {
|
||||||
|
"const": "string"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"description": "A feature variable.",
|
||||||
|
"properties": {
|
||||||
|
"description": {
|
||||||
|
"description": "A description of the feature.",
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": {
|
||||||
|
"$ref": "#/$defs/FeatureVariableType",
|
||||||
|
"description": "The field type."
|
||||||
|
},
|
||||||
|
"enum": {
|
||||||
|
"description": "An optional list of possible string values. Only allowed when type is string.",
|
||||||
|
"items": {
|
||||||
|
"type": "string"
|
||||||
|
},
|
||||||
|
"type": "array"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"required": [
|
||||||
|
"description",
|
||||||
|
"type"
|
||||||
|
],
|
||||||
|
"type": "object"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Загрузка…
Ссылка в новой задаче