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:
Beth Rennie 2024-10-18 16:30:05 -04:00 коммит произвёл GitHub
Родитель eda2402004
Коммит be67f0b808
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: B5690EEEBB952194
16 изменённых файлов: 1673 добавлений и 53 удалений

Просмотреть файл

@ -427,7 +427,7 @@ jobs:
paths:
- tests/integration/ios_recipe.json
- tests/integration/fenix_recipe.json
deploy_experimenter:
working_directory: ~/experimenter
machine:
@ -517,10 +517,6 @@ jobs:
echo "No changes in package version. Skipping mozilla-nimbus-schemas deployment."
circleci-agent step halt
fi
- run:
name: Create the distribution files
command: |
make schemas_dist
- run:
name: Upload to PyPI
command: |
@ -620,7 +616,7 @@ jobs:
else
echo "No config changes, skipping"
fi
check_external_firefox_integrations:
machine:
image: ubuntu-2004:2023.10.1
@ -676,7 +672,7 @@ jobs:
steps:
- checkout
- check_file_paths:
paths: "experimenter/tests/firefox-fenix-build.env"
paths: "experimenter/tests/firefox-fenix-build.env"
- docker_login:
username: $DOCKER_USER
password: $DOCKER_PASS
@ -718,7 +714,7 @@ workflows:
jobs:
- update_external_configs
- update_application_services
check_firefox_integrations:
triggers:
- schedule:

3
.gitignore поставляемый
Просмотреть файл

@ -120,3 +120,6 @@ cirrus/server/app/__pycache__
# Application Services for processing FML files
experimenter/experimenter/features/manifests/application-services/
# Schemas
schemas/mozilla_nimbus_schemas/schemas/

Просмотреть файл

@ -292,8 +292,8 @@ SCHEMAS_DIFF_PYDANTIC = \
echo 'Done. No problems found in schemas.'
SCHEMAS_TEST = pytest
SCHEMAS_FORMAT = ruff check --fix . && black .
SCHEMAS_GENERATE = poetry run python generate_json_schema.py
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_NPM = echo "//registry.npmjs.org/:_authToken=${NPM_TOKEN}" > .npmrc;yarn publish --new-version ${SCHEMAS_VERSION} --access public;
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
$(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_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_RUN) "$(SCHEMAS_BLACK)&&$(SCHEMAS_RUFF)&&$(SCHEMAS_DIFF_PYDANTIC)&&$(SCHEMAS_TEST)"
schemas_check: schemas_lint
schemas_dist_pypi: schemas_docker_build
$(SCHEMAS_RUN) "$(SCHEMAS_DIST_PYPI)"
schemas_generate: schemas_docker_build
$(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_docker_build
schemas_deploy_pypi: schemas_build
$(SCHEMAS_RUN) "$(SCHEMAS_DEPLOY_PYPI)"
schemas_deploy_npm: schemas_docker_build
schemas_deploy_npm: schemas_build
$(SCHEMAS_RUN) "$(SCHEMAS_DEPLOY_NPM)"
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 re
import shutil
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
from typing import Any
from typing import Any, Iterable
import click
from polyfactory.factories.pydantic_factory import ModelFactory
@ -15,6 +17,8 @@ from pydantic import BaseModel, create_model
from mozilla_nimbus_schemas import experiments, jetstream
NEWLINES_RE = re.compile("\n+")
def clean_output_file(ts_path: Path) -> None:
"""Clean up the output file typescript definitions were written to by:
@ -99,6 +103,123 @@ def iterate_models() -> dict[str, Any]:
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.option(
"--output",
@ -107,7 +228,25 @@ def iterate_models() -> dict[str, Any]:
default=Path("index.d.ts"),
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()
with TemporaryDirectory() as tmp_dir:
@ -133,6 +272,8 @@ def main(*, ts_output_path: Path):
clean_output_file(ts_output_path)
write_json_schemas(json_schemas_path, python_package_dir)
if __name__ == "__main__":
main()

18
schemas/index.d.ts поставляемый
Просмотреть файл

@ -32,15 +32,6 @@ export type SizingMetricName = "active_hours" | "search_count" | "days_of_use" |
export type StatisticIngestEnum = "percentage" | "binomial" | "mean" | "count";
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.
*/
@ -133,6 +124,15 @@ export interface NimbusFeatureSchema {
*/
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:
*

Просмотреть файл

@ -1,7 +1,8 @@
from .experiments import NimbusExperiment, RandomizationUnit
from .feature_manifests import DesktopFeatureManifest, SdkFeatureManifest
from .feature_manifests import DesktopFeature, DesktopFeatureManifest, SdkFeatureManifest
__all__ = (
"DesktopFeature",
"DesktopFeatureManifest",
"NimbusExperiment",
"RandomizationUnit",

Просмотреть файл

@ -2,6 +2,7 @@ from enum import Enum
from pydantic import (
BaseModel,
ConfigDict,
Field,
RootModel,
model_validator,
@ -56,6 +57,18 @@ class SdkFeatureVariable(BaseFeatureVariable):
default=None,
)
model_config = ConfigDict(
json_schema_extra={
"dependentSchemas": {
"enum": {
"properties": {
"type": {"const": FeatureVariableType.STRING.value},
}
}
}
}
)
@model_validator(mode="after")
@classmethod
def validate_enum(cls, data: Self) -> Self:
@ -102,6 +115,87 @@ class DesktopFeatureVariable(BaseFeatureVariable):
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")
@classmethod
def validate_set_pref_fallback_pref_mutually_exclusive(cls, data: Self) -> Self:
@ -170,6 +264,21 @@ class BaseFeature(BaseModel):
default=None,
)
model_config = ConfigDict(
json_schema_extra={
"if": {
"properties": {
"hasExposure": {
"const": True,
},
},
},
"then": {
"required": ["exposureDescription"],
},
}
)
@model_validator(mode="after")
@classmethod
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 typing import Any
import pydantic
import pytest
import yaml
from jsonschema.protocols import Validator
from jsonschema.validators import validator_for
from mozilla_nimbus_schemas.experiments.feature_manifests import (
DesktopFeatureManifest,
@ -14,22 +19,108 @@ from mozilla_nimbus_schemas.experiments.feature_manifests import (
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())
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:
contents = yaml.safe_load(f)
DesktopFeatureManifest.model_validate(contents)
assert desktop_feature_manifest_schema_validator.is_valid(contents)
@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:
contents = yaml.safe_load(f)
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():
SdkFeatureVariable.model_validate(
@ -63,7 +154,7 @@ def test_sdk_feature_variable_invalid_enum_unsupported_type(model_json):
@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"],
},
"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],
},
"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(
model_json,
expected_pydantic_error,
expected_jsonschema_error,
expected_jsonschema_error_path,
sdk_feature_manifest_schema_validator,
):
with pytest.raises(pydantic.ValidationError, match=expected_pydantic_error):
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(
"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):
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(
"model_json",
"model_json,expected_error",
[
{
"description": "invalid enum (string options for int type)",
"type": "int",
"enum": ["hello"],
},
{
"description": "invalid enum (int options for string type)",
"type": "string",
"enum": [1],
},
(
{
"description": "invalid enum (string options for int type)",
"type": "int",
"enum": ["hello"],
},
"'hello' is not of type 'integer'",
),
(
{
"description": "invalid enum (int options for string type)",
"type": "string",
"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(
pydantic.ValidationError, match="enum values do not match variable type"
):
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 = {
"description": "invalid variable (fallbackPref and setPref mutually exclusive)",
"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",
):
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",
"version": "2024.9.3",
"version": "2024.10.1",
"description": "Schemas used by Mozilla Nimbus and related projects.",
"main": "index.d.ts",
"repository": {
@ -18,6 +18,7 @@
"typescript": "^5.1.6"
},
"files": [
"index.d.ts"
"index.d.ts",
"schemas"
]
}

185
schemas/poetry.lock сгенерированный
Просмотреть файл

@ -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]]
name = "annotated-types"
@ -11,6 +11,25 @@ files = [
{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]]
name = "backports-tarfile"
version = "1.2.0"
@ -484,6 +503,41 @@ files = [
test = ["async-timeout", "pytest", "pytest-asyncio (>=0.17)", "pytest-trio", "testpath", "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]]
name = "keyring"
version = "25.3.0"
@ -959,6 +1013,21 @@ Pygments = ">=2.5.1"
[package.extras]
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]]
name = "requests"
version = "2.32.3"
@ -1026,6 +1095,118 @@ pygments = ">=2.13.0,<3.0.0"
[package.extras]
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]]
name = "ruff"
version = "0.6.1"
@ -1173,4 +1354,4 @@ type = ["pytest-mypy"]
[metadata]
lock-version = "2.0"
python-versions = "^3.10"
content-hash = "9204c714a20e1d21cc566ac6df2dc43bdf410d72b5dae44afef822dda2dddd66"
content-hash = "0c9c045c39541914844c794ec731c3f52efba328be29b68d50056668cee3de37"

Просмотреть файл

@ -1,16 +1,19 @@
[tool.poetry]
name = "mozilla-nimbus-schemas"
version = "2024.9.3"
version = "2024.10.1"
description = "Schemas used by Mozilla Nimbus and related projects."
authors = ["mikewilli"]
license = "MPL 2.0"
readme = "README.md"
packages = [{ include = "mozilla_nimbus_schemas" }]
include = [{ path = "mozilla_nimbus_schemas/schemas", format = ["sdist", "wheel"] }]
[tool.poetry.dependencies]
python = "^3.10"
pydantic = "^2"
polyfactory = "^2.7.2"
typing-extensions = ">=4.0.1" # Required until Python 3.11
jsonschema = "^4.23.0"
[tool.poetry.group.dev.dependencies]
ruff = ">=0.5.0,<0.6.2"
@ -19,6 +22,7 @@ pytest = "^7.3.1"
twine = "^5.1.1"
PyYAML = "^6.0"
[build-system]
requires = ["poetry-core"]
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"
}
}
}