diff --git a/.pyrit_conf_example b/.pyrit_conf_example index 4a900b4e75..8bc95546eb 100644 --- a/.pyrit_conf_example +++ b/.pyrit_conf_example @@ -30,16 +30,27 @@ memory_db_type: sqlite # # Each initializer can be specified as: # - A simple string (name only) -# - A dictionary with 'name' and optional 'args' for constructor arguments +# - A dictionary with 'name' and optional 'args' for parameters +# +# Parameters are lists of strings. Use the CLI command +# `pyrit_scan --list-initializers` to see available parameters. # # Example: # initializers: # - simple -# - name: airt +# - name: target # args: -# some_param: value +# tags: +# - default +# - scorer initializers: - - simple + - name: simple + - name: scorer + - name: target + args: + tags: + - default + - scorer # Operator and Operation Labels # ------------------------------ diff --git a/build_scripts/evaluate_scorers.py b/build_scripts/evaluate_scorers.py index c303184a16..2b79ea74cf 100644 --- a/build_scripts/evaluate_scorers.py +++ b/build_scripts/evaluate_scorers.py @@ -35,9 +35,11 @@ async def evaluate_scorers() -> None: 5. Save results to scorer_evals directory """ print("Initializing PyRIT...") + target_init = TargetInitializer() + target_init.params = {"tags": ["default", "scorer"]} await initialize_pyrit_async( memory_db_type=IN_MEMORY, - initializers=[TargetInitializer(tags=["default", "scorer"]), ScorerInitializer()], + initializers=[target_init, ScorerInitializer()], ) registry = ScorerRegistry.get_registry_singleton() diff --git a/doc/code/setup/pyrit_initializer.ipynb b/doc/code/setup/pyrit_initializer.ipynb index 80b2a579cb..d0a81f1498 100644 --- a/doc/code/setup/pyrit_initializer.ipynb +++ b/doc/code/setup/pyrit_initializer.ipynb @@ -61,7 +61,7 @@ " def execution_order(self) -> int:\n", " return 2 # Lower numbers run first (default is 1)\n", "\n", - " async def initialize_async(self) -> None:\n", + " async def initialize_async(self, *, params=None) -> None:\n", " set_default_value(class_type=OpenAIChatTarget, parameter_name=\"temperature\", value=0.9)\n", "\n", " @property\n", @@ -159,7 +159,7 @@ " def execution_order(self) -> int:\n", " return 2 # Lower numbers run first (default is 1)\n", "\n", - " async def initialize_async(self) -> None:\n", + " async def initialize_async(self, *, params=None) -> None:\n", " set_default_value(class_type=OpenAIChatTarget, parameter_name=\"temperature\", value=0.9)\n", "\n", " @property\n", diff --git a/doc/code/setup/pyrit_initializer.py b/doc/code/setup/pyrit_initializer.py index c8a7d0590f..ccf2a94e8f 100644 --- a/doc/code/setup/pyrit_initializer.py +++ b/doc/code/setup/pyrit_initializer.py @@ -43,7 +43,7 @@ def name(self) -> str: def execution_order(self) -> int: return 2 # Lower numbers run first (default is 1) - async def initialize_async(self) -> None: + async def initialize_async(self, *, params=None) -> None: set_default_value(class_type=OpenAIChatTarget, parameter_name="temperature", value=0.9) @property @@ -107,7 +107,7 @@ def name(self) -> str: def execution_order(self) -> int: return 2 # Lower numbers run first (default is 1) - async def initialize_async(self) -> None: + async def initialize_async(self, *, params=None) -> None: set_default_value(class_type=OpenAIChatTarget, parameter_name="temperature", value=0.9) @property diff --git a/doc/setup/pyrit_conf.md b/doc/setup/pyrit_conf.md index 09ce09f77f..199e1a2ed0 100644 --- a/doc/setup/pyrit_conf.md +++ b/doc/setup/pyrit_conf.md @@ -44,7 +44,7 @@ A list of built-in initializers to run during PyRIT initialization. Initializers Each entry can be: - **A simple string** — just the initializer name -- **A dictionary** — with `name` and optional `args` for constructor arguments +- **A dictionary** — with `name` and optional `args` (each arg is a list of strings passed to `initialize_async`) Example: @@ -53,7 +53,9 @@ initializers: - simple - name: airt args: - some_param: value + tags: + - default + - scorer ``` Use `pyrit list initializers` in the CLI to see all registered initializers. See the [initializer documentation notebook](../code/setup/pyrit_initializer.ipynb) for reference. diff --git a/docker/README.md b/docker/README.md index 4f5a6b1112..daea76b606 100644 --- a/docker/README.md +++ b/docker/README.md @@ -183,5 +183,5 @@ The JupyterLab instance is configured to run without authentication by default f - 📖 **[Docker Installation Guide](./../doc/setup/1b_install_docker.md)** - Complete user-friendly installation instructions - 🚀 **[PyRIT Documentation](https://azure.github.io/PyRIT/)** - Full documentation site -- 🔧 **[Contributing Guide](https://azure.github.io/PyRIT/contributing/README.html)** - For developers and contributors +- 🔧 **[Contributing Guide](https://azure.github.io/PyRIT/contributing/readme/)** - For developers and contributors - 🐛 **[Issues](https://github.com/Azure/PyRIT/issues)** - Report bugs or request features diff --git a/pyrit/cli/frontend_core.py b/pyrit/cli/frontend_core.py index 20365ae720..dce11cb467 100644 --- a/pyrit/cli/frontend_core.py +++ b/pyrit/cli/frontend_core.py @@ -77,7 +77,7 @@ def __init__( config_file: Optional[Path] = None, database: Optional[str] = None, initialization_scripts: Optional[list[Path]] = None, - initializer_names: Optional[list[str]] = None, + initializer_names: Optional[list[Any]] = None, env_files: Optional[list[Path]] = None, log_level: Optional[int] = None, ): @@ -94,7 +94,9 @@ def __init__( The file uses .pyrit_conf extension but is YAML format. database: Database type (InMemory, SQLite, or AzureSQL). initialization_scripts: Optional list of initialization script paths. - initializer_names: Optional list of built-in initializer names to run. + initializer_names: Optional list of initializer entries. Each entry can be + a string name (e.g., "simple") or a dict with 'name' and optional 'args' + (e.g., {"name": "target", "args": {"tags": "default,scorer"}}). env_files: Optional list of environment file paths to load in order. log_level: Logging level constant (e.g., logging.WARNING). Defaults to logging.WARNING. @@ -130,9 +132,7 @@ def __init__( # Use canonical mapping from configuration_loader self._database = _MEMORY_DB_TYPE_MAP[config.memory_db_type] self._initialization_scripts = config._resolve_initialization_scripts() - self._initializer_names = ( - [ic.name for ic in config._initializer_configs] if config._initializer_configs else None - ) + self._initializer_configs = config._initializer_configs if config._initializer_configs else None self._env_files = config._resolve_env_files() self._operator = config.operator self._operation = config.operation @@ -289,15 +289,20 @@ async def run_scenario_async( # Run initializers before scenario initializer_instances = None - if context._initializer_names: - print(f"Running {len(context._initializer_names)} initializer(s)...") + if context._initializer_configs: + print(f"Running {len(context._initializer_configs)} initializer(s)...") sys.stdout.flush() initializer_instances = [] - for name in context._initializer_names: - initializer_class = context.initializer_registry.get_class(name) - initializer_instances.append(initializer_class()) + for config in context._initializer_configs: + initializer_class = context.initializer_registry.get_class(config.name) + instance = initializer_class() + if config.args: + instance.params = { + k: [str(i) for i in v] if isinstance(v, list) else [str(v)] for k, v in config.args.items() + } + initializer_instances.append(instance) # Re-initialize PyRIT with the scenario-specific initializers # This resets memory and applies initializer defaults @@ -479,6 +484,13 @@ def format_initializer_metadata(*, initializer_metadata: InitializerMetadata) -> else: print(" Required Environment Variables: None") + if initializer_metadata.supported_parameters: + print(" Supported Parameters:") + for param_name, param_desc, param_required, param_default in initializer_metadata.supported_parameters: + req_str = " (required)" if param_required else "" + default_str = f" [default: {param_default}]" if param_default else "" + print(f" - {param_name}{req_str}{default_str}: {param_desc}") + if initializer_metadata.class_description: print(" Description:") print(_format_wrapped_text(text=initializer_metadata.class_description, indent=" ")) @@ -775,7 +787,11 @@ async def print_initializers_list_async(*, context: FrontendCore, discovery_path "initialization scripts, and env files. CLI arguments override config file values. " "If not specified, ~/.pyrit/.pyrit_conf is loaded if it exists." ), - "initializers": "Built-in initializer names to run before the scenario (e.g., openai_objective_target)", + "initializers": ( + "Built-in initializer names to run before the scenario. " + "Supports optional params with name:key=val syntax " + "(e.g., target:tags=default,scorer dataset:mode=strict)" + ), "initialization_scripts": "Paths to custom Python initialization scripts to run before the scenario", "env_files": "Paths to environment files to load in order (e.g., .env.production .env.local). Later files " "override earlier ones.", @@ -792,6 +808,51 @@ async def print_initializers_list_async(*, context: FrontendCore, discovery_path } +def _parse_initializer_arg(arg: str) -> dict[str, Any]: + """ + Parse an initializer CLI argument into a dict for ConfigurationLoader. + + Supports two formats: + - Simple name: "simple" → {"name": "simple"} + - Name with params: "target:tags=default,scorer" → {"name": "target", "args": {"tags": "default,scorer"}} + + For multiple params on one initializer, separate with semicolons: "name:key1=val1;key2=val2" + For multiple initializers with params, space-separate them: "target:tags=a,b dataset:mode=strict" + + Args: + arg: The CLI argument string. + + Returns: + dict: A dict with 'name' and optionally 'args' keys. + + Raises: + ValueError: If the argument format is invalid. + """ + if ":" not in arg: + return arg # type: ignore[return-value] + + name, params_str = arg.split(":", 1) + if not name: + raise ValueError(f"Invalid initializer argument '{arg}': missing name before ':'") + + args: dict[str, list[str]] = {} + for pair in params_str.split(";"): + pair = pair.strip() + if not pair: + continue + if "=" not in pair: + raise ValueError(f"Invalid initializer parameter '{pair}' in '{arg}': expected key=value format") + key, value = pair.split("=", 1) + key = key.strip() + if not key: + raise ValueError(f"Invalid initializer parameter in '{arg}': empty key") + args[key] = [v.strip() for v in value.split(",")] + + if args: + return {"name": name, "args": args} + return name # type: ignore[return-value] + + def parse_run_arguments(*, args_string: str) -> dict[str, Any]: """ Parse run command arguments from a string (for shell mode). @@ -839,11 +900,11 @@ def parse_run_arguments(*, args_string: str) -> dict[str, Any]: i = 1 while i < len(parts): if parts[i] == "--initializers": - # Collect initializers until next flag + # Collect initializers until next flag, parsing name:key=val syntax result["initializers"] = [] i += 1 while i < len(parts) and not parts[i].startswith("--"): - result["initializers"].append(parts[i]) + result["initializers"].append(_parse_initializer_arg(parts[i])) i += 1 elif parts[i] == "--initialization-scripts": # Collect script paths until next flag diff --git a/pyrit/cli/pyrit_backend.py b/pyrit/cli/pyrit_backend.py index 41674d2388..63dee34f20 100644 --- a/pyrit/cli/pyrit_backend.py +++ b/pyrit/cli/pyrit_backend.py @@ -92,7 +92,7 @@ def parse_args(*, args: Optional[list[str]] = None) -> Namespace: parser.add_argument( "--initializers", - type=str, + type=frontend_core._parse_initializer_arg, nargs="+", help=frontend_core.ARG_HELP["initializers"], ) @@ -165,12 +165,17 @@ async def initialize_and_run_async(*, parsed_args: Namespace) -> int: # Run initializers up-front (backend runs them once at startup, not per-scenario) initializer_instances = None - if context._initializer_names: - print(f"Running {len(context._initializer_names)} initializer(s)...") + if context._initializer_configs: + print(f"Running {len(context._initializer_configs)} initializer(s)...") initializer_instances = [] - for name in context._initializer_names: - initializer_class = context.initializer_registry.get_class(name) - initializer_instances.append(initializer_class()) + for config in context._initializer_configs: + initializer_class = context.initializer_registry.get_class(config.name) + instance = initializer_class() + if config.args: + instance.params = { + k: [str(i) for i in v] if isinstance(v, list) else [str(v)] for k, v in config.args.items() + } + initializer_instances.append(instance) # Re-initialize with initializers applied await initialize_pyrit_async( @@ -196,14 +201,14 @@ async def initialize_and_run_async(*, parsed_args: Namespace) -> int: print(f"🚀 Starting PyRIT backend on http://{parsed_args.host}:{parsed_args.port}") print(f" API Docs: http://{parsed_args.host}:{parsed_args.port}/docs") - config = uvicorn.Config( + uvicorn_config = uvicorn.Config( "pyrit.backend.main:app", host=parsed_args.host, port=parsed_args.port, log_level=parsed_args.log_level, reload=parsed_args.reload, ) - server = uvicorn.Server(config) + server = uvicorn.Server(uvicorn_config) await server.serve() return 0 diff --git a/pyrit/cli/pyrit_scan.py b/pyrit/cli/pyrit_scan.py index e801fe6adb..c4dcb971da 100644 --- a/pyrit/cli/pyrit_scan.py +++ b/pyrit/cli/pyrit_scan.py @@ -95,7 +95,7 @@ def parse_args(args: Optional[list[str]] = None) -> Namespace: parser.add_argument( "--initializers", - type=str, + type=frontend_core._parse_initializer_arg, nargs="+", help=frontend_core.ARG_HELP["initializers"], ) diff --git a/pyrit/cli/pyrit_shell.py b/pyrit/cli/pyrit_shell.py index c07857bcaa..7a4de89363 100644 --- a/pyrit/cli/pyrit_shell.py +++ b/pyrit/cli/pyrit_shell.py @@ -44,7 +44,7 @@ class PyRITShell(cmd.Cmd): --no-animation Disable the animated startup banner Run Command Options: - --initializers ... Built-in initializers to run before the scenario + --initializers ... Built-in initializers (supports name:key=val1,val2 syntax) --initialization-scripts <...> Custom Python scripts to run before the scenario --env-files ... Environment files to load in order (overrides startup default) --strategies, -s ... Strategy names to use @@ -135,7 +135,7 @@ def do_run(self, line: str) -> None: run [options] Options: - --initializers ... Built-in initializers to run before the scenario + --initializers ... Built-in initializers (supports name:key=val1,val2 syntax) --initialization-scripts <...> Custom Python scripts to run before the scenario --env-files ... Environment files to load in order --strategies, -s ... Strategy names to use @@ -150,6 +150,8 @@ def do_run(self, line: str) -> None: load_default_datasets run garak.encoding --initializers custom_target \ load_default_datasets --strategies base64 rot13 + run foundry --initializers target:tags=default,scorer \ + dataset:mode=strict --strategies base64 run foundry --initializers openai_objective_target \ load_default_datasets --max-concurrency 10 --max-retries 3 run garak.encoding --initializers custom_target \ @@ -360,6 +362,10 @@ def do_help(self, arg: str) -> None: print(f" {frontend_core.ARG_HELP['initializers']}") print(" Every scenario requires at least one initializer") print(" Example: run foundry --initializers openai_objective_target load_default_datasets") + print(" With params: run foundry --initializers target:tags=default,scorer") + print( + " Multiple with params: run foundry --initializers target:tags=default,scorer dataset:mode=strict" + ) print() print(" --initialization-scripts [ ...] (Alternative to --initializers)") print(f" {frontend_core.ARG_HELP['initialization_scripts']}") diff --git a/pyrit/registry/class_registries/initializer_registry.py b/pyrit/registry/class_registries/initializer_registry.py index 510daae204..cea7e16203 100644 --- a/pyrit/registry/class_registries/initializer_registry.py +++ b/pyrit/registry/class_registries/initializer_registry.py @@ -50,6 +50,9 @@ class InitializerMetadata(ClassRegistryEntry): # Execution order priority (lower = earlier). execution_order: int = field(kw_only=True) + # Supported parameters as tuples of (name, description, required, default). + supported_parameters: tuple[tuple[str, str, bool, Optional[list[str]]], ...] = field(kw_only=True, default=()) + class InitializerRegistry(BaseClassRegistry["PyRITInitializer", InitializerMetadata]): """ @@ -223,6 +226,9 @@ def _build_metadata(self, name: str, entry: ClassEntry[PyRITInitializer]) -> Ini display_name=instance.name, required_env_vars=tuple(instance.required_env_vars), execution_order=instance.execution_order, + supported_parameters=tuple( + (p.name, p.description, p.required, p.default) for p in instance.supported_parameters + ), ) except Exception as e: logger.warning(f"Failed to get metadata for {name}: {e}") diff --git a/pyrit/setup/configuration_loader.py b/pyrit/setup/configuration_loader.py index d96f7a7ca4..58f251e2c1 100644 --- a/pyrit/setup/configuration_loader.py +++ b/pyrit/setup/configuration_loader.py @@ -325,8 +325,14 @@ def _resolve_initializers(self) -> Sequence["PyRITInitializer"]: f"Initializer '{config.name}' not found in registry.\nAvailable initializers: {available}" ) - # Instantiate with args if provided - instance = initializer_class(**config.args) if config.args else initializer_class() + # Instantiate and set params if provided + instance = initializer_class() + if config.args: + instance.params = { + k: [str(i) for i in v] if isinstance(v, list) else [str(v)] for k, v in config.args.items() + } + # Validate params early against supported_parameters to fail fast + instance._validate_params(params=instance.params) resolved.append(instance) diff --git a/pyrit/setup/initializers/__init__.py b/pyrit/setup/initializers/__init__.py index 1df84c897b..d27fc41c2a 100644 --- a/pyrit/setup/initializers/__init__.py +++ b/pyrit/setup/initializers/__init__.py @@ -6,13 +6,14 @@ from pyrit.setup.initializers.airt import AIRTInitializer from pyrit.setup.initializers.components.scorers import ScorerInitializer from pyrit.setup.initializers.components.targets import TargetInitializer -from pyrit.setup.initializers.pyrit_initializer import PyRITInitializer +from pyrit.setup.initializers.pyrit_initializer import InitializerParameter, PyRITInitializer from pyrit.setup.initializers.scenarios.load_default_datasets import LoadDefaultDatasets from pyrit.setup.initializers.scenarios.objective_list import ScenarioObjectiveListInitializer from pyrit.setup.initializers.scenarios.openai_objective_target import ScenarioObjectiveTargetInitializer from pyrit.setup.initializers.simple import SimpleInitializer __all__ = [ + "InitializerParameter", "PyRITInitializer", "AIRTInitializer", "ScorerInitializer", diff --git a/pyrit/setup/initializers/components/scorers.py b/pyrit/setup/initializers/components/scorers.py index d7bc220037..06b304ebc6 100644 --- a/pyrit/setup/initializers/components/scorers.py +++ b/pyrit/setup/initializers/components/scorers.py @@ -31,7 +31,7 @@ TrueFalseQuestionPaths, TrueFalseScoreAggregator, ) -from pyrit.setup.initializers.pyrit_initializer import PyRITInitializer +from pyrit.setup.initializers.pyrit_initializer import InitializerParameter, PyRITInitializer if TYPE_CHECKING: from pyrit.prompt_target.common.prompt_chat_target import PromptChatTarget @@ -77,6 +77,9 @@ class ScorerInitializer(PyRITInitializer): so this initializer must run after the target initializer (enforced via execution_order). Scorers that fail to initialize (e.g., due to missing targets) are skipped with a warning. + Supported Parameters: + tags: Tags for filtering scorers. Defaults to ["default"]. + Example: initializer = ScorerInitializer() await initializer.initialize_async() @@ -84,15 +87,16 @@ class ScorerInitializer(PyRITInitializer): refusal = registry.get_instance_by_name(REFUSAL_GPT4O) """ - def __init__(self, *, tags: list[ScorerTag] | None = None) -> None: - """ - Initialize the Scorer Initializer. - - Args: - tags (list[ScorerTag] | None): Tags for future filtering. Defaults to ["default"]. - """ - super().__init__() - self._tags = tags if tags is not None else ["default"] + @property + def supported_parameters(self) -> list[InitializerParameter]: + """Get the list of parameters this initializer accepts.""" + return [ + InitializerParameter( + name="tags", + description="Tags for filtering (e.g., ['default'])", + default=["default"], + ), + ] @property def name(self) -> str: @@ -134,6 +138,8 @@ async def initialize_async(self) -> None: Raises: RuntimeError: If the TargetRegistry is empty or hasn't been initialized. """ + tags = self.params.get("tags", ["default"]) + target_registry = TargetRegistry.get_registry_singleton() if len(target_registry) == 0: diff --git a/pyrit/setup/initializers/components/targets.py b/pyrit/setup/initializers/components/targets.py index 3cc42de3ee..5e2f88bfb3 100644 --- a/pyrit/setup/initializers/components/targets.py +++ b/pyrit/setup/initializers/components/targets.py @@ -31,13 +31,15 @@ RealtimeTarget, ) from pyrit.registry import TargetRegistry -from pyrit.setup.initializers.pyrit_initializer import PyRITInitializer +from pyrit.setup.initializers.pyrit_initializer import InitializerParameter, PyRITInitializer logger = logging.getLogger(__name__) # Literal type for target tags -TargetTag = Literal["default", "scorer"] +TargetTag = Literal["default", "scorer", "all"] + +ALL_TARGET_TAGS: list[str] = ["default", "scorer"] @dataclass @@ -366,11 +368,11 @@ class TargetInitializer(PyRITInitializer): the corresponding targets into the TargetRegistry. Targets can be filtered by tags to control which targets are registered. - Args: - tags: List of tags to filter which targets to register. + Supported Parameters: + tags: Target tags to register (list of strings). "default" registers the base environment targets. "scorer" registers scorer-specific temperature variant targets. - Pass multiple tags to register targets matching any tag. + "all" registers all targets regardless of tag. If not provided, only "default" targets are registered. Supported Endpoints by Category: @@ -426,20 +428,20 @@ class TargetInitializer(PyRITInitializer): await initializer.initialize_async() # Register scorer temperature variants too - initializer = TargetInitializer(tags=["default", "scorer"]) + initializer.params = {"tags": ["default", "scorer"]} await initializer.initialize_async() """ - def __init__(self, *, tags: list[TargetTag] | None = None) -> None: - """ - Initialize the Target Initializer. - - Args: - tags (list[TargetTag] | None): Tags to filter which targets to register. - If None, only "default" targets are registered. - """ - super().__init__() - self._tags = tags if tags is not None else ["default"] + @property + def supported_parameters(self) -> list[InitializerParameter]: + """Get the list of parameters this initializer accepts.""" + return [ + InitializerParameter( + name="tags", + description="Target tags to register (e.g., ['default'], ['default', 'scorer'], or ['all'])", + default=["default"], + ), + ] @property def name(self) -> str: @@ -477,8 +479,12 @@ async def initialize_async(self) -> None: corresponding targets into the TargetRegistry. Only targets with tags matching the configured tags are registered. """ + tags = self.params.get("tags", ["default"]) + if "all" in tags: + tags = ALL_TARGET_TAGS + for config in TARGET_CONFIGS: - if not any(tag in self._tags for tag in config.tags): + if not any(tag in tags for tag in config.tags): continue self._register_target(config) diff --git a/pyrit/setup/initializers/pyrit_initializer.py b/pyrit/setup/initializers/pyrit_initializer.py index e4bff8c6aa..2dd9850c5e 100644 --- a/pyrit/setup/initializers/pyrit_initializer.py +++ b/pyrit/setup/initializers/pyrit_initializer.py @@ -12,11 +12,33 @@ from abc import ABC, abstractmethod from collections.abc import Iterator from contextlib import contextmanager, suppress -from typing import Any +from dataclasses import dataclass +from typing import Any, Optional from pyrit.common.apply_defaults import get_global_default_values +@dataclass(frozen=True) +class InitializerParameter: + """ + Describes a parameter that an initializer accepts. + + Each parameter value is a list of strings, which works naturally with + CLI (comma-separated), YAML (lists), and programmatic APIs. + + Args: + name: The parameter name (used as key in the params dict). + description: Human-readable description of the parameter. + required: Whether the parameter must be provided. Defaults to False. + default: Default value if not provided. Defaults to None. + """ + + name: str + description: str + required: bool = False + default: Optional[list[str]] = None + + class PyRITInitializer(ABC): """ Abstract base class for PyRIT configuration initializers. @@ -32,6 +54,7 @@ class PyRITInitializer(ABC): def __init__(self) -> None: # noqa: B027 """Initialize the PyRIT initializer with no parameters.""" + self.params: dict[str, list[str]] = {} @property @abstractmethod @@ -89,6 +112,19 @@ def execution_order(self) -> int: """ return 1 + @property + def supported_parameters(self) -> list[InitializerParameter]: + """ + Get the list of parameters this initializer accepts. + + Override this property to declare what parameters the initializer + supports. Parameters are set on self.params before initialize_async() is called. + + Returns: + list[InitializerParameter]: List of supported parameters. Defaults to empty list. + """ + return [] + @abstractmethod async def initialize_async(self) -> None: """ @@ -97,17 +133,22 @@ async def initialize_async(self) -> None: This method should contain all the configuration logic, including calls to set_default_value() and set_global_variable() as needed. All initializers must implement this as an async method. + + Subclasses that accept parameters should read them from self.params, + which is populated before this method is called. """ def validate(self) -> None: """ Validate the initializer configuration before execution. - This method checks that all required environment variables are set. + This method checks that all required environment variables are set + and validates any configured parameters against supported_parameters. Subclasses should not override this method. Raises: - ValueError: If required environment variables are not set. + ValueError: If required environment variables are not set or + if configured parameters are invalid. """ import os @@ -118,6 +159,41 @@ def validate(self) -> None: f"{', '.join(missing_vars)}" ) + # Validate configured params + if self.params: + self._validate_params(params=self.params) + + def _validate_params(self, *, params: dict[str, list[str]]) -> None: + """ + Validate parameters against supported_parameters. + + Checks that all provided params are declared in supported_parameters + and that all required params are present. + + Args: + params: The parameters to validate. + + Raises: + ValueError: If unknown parameters are provided or required parameters are missing. + """ + supported = {p.name: p for p in self.supported_parameters} + supported_names = set(supported.keys()) + + # Check for unknown params + unknown = set(params.keys()) - supported_names + if unknown: + raise ValueError( + f"Initializer '{self.name}' received unknown parameter(s): {', '.join(sorted(unknown))}. " + f"Supported parameters: {', '.join(sorted(supported_names)) if supported_names else 'none'}" + ) + + # Check for missing required params + for param_def in self.supported_parameters: + if param_def.required and param_def.name not in params: + raise ValueError( + f"Initializer '{self.name}' requires parameter '{param_def.name}': {param_def.description}" + ) + async def initialize_with_tracking_async(self) -> None: """ Execute initialization while tracking what changes are made. @@ -265,6 +341,18 @@ async def get_info_async(cls) -> dict[str, Any]: "execution_order": instance.execution_order, } + # Add supported parameters if any are declared + if instance.supported_parameters: + base_info["supported_parameters"] = [ + { + "name": p.name, + "description": p.description, + "required": p.required, + "default": p.default, + } + for p in instance.supported_parameters + ] + # Add required environment variables if any are defined if instance.required_env_vars: base_info["required_env_vars"] = instance.required_env_vars diff --git a/tests/unit/cli/test_frontend_core.py b/tests/unit/cli/test_frontend_core.py index 35faea45c6..f7c5242dc2 100644 --- a/tests/unit/cli/test_frontend_core.py +++ b/tests/unit/cli/test_frontend_core.py @@ -25,7 +25,7 @@ def test_init_with_defaults(self): assert context._database == frontend_core.SQLITE assert context._initialization_scripts is None - assert context._initializer_names is None + assert context._initializer_configs is None assert context._log_level == logging.WARNING assert context._initialized is False @@ -46,7 +46,8 @@ def test_init_with_all_parameters(self): assert context._initialization_scripts is not None assert len(context._initialization_scripts) == 1 assert context._initialization_scripts[0].parts[-2:] == ("test", "script.py") - assert context._initializer_names == initializers + assert context._initializer_configs is not None + assert [ic.name for ic in context._initializer_configs] == initializers assert context._log_level == logging.DEBUG def test_init_with_invalid_database(self): @@ -517,6 +518,49 @@ def test_format_initializer_metadata_with_description(self, capsys) -> None: assert "Test description" in captured.out +class TestParseInitializerArg: + """Tests for _parse_initializer_arg function.""" + + def test_simple_name_returns_string(self) -> None: + """Test that a plain name without ':' returns the string as-is.""" + assert frontend_core._parse_initializer_arg("simple") == "simple" + + def test_name_with_single_param(self) -> None: + """Test name:key=value parsing.""" + result = frontend_core._parse_initializer_arg("target:tags=default") + assert result == {"name": "target", "args": {"tags": ["default"]}} + + def test_name_with_comma_separated_values(self) -> None: + """Test that comma-separated values are split into a list.""" + result = frontend_core._parse_initializer_arg("target:tags=default,scorer") + assert result == {"name": "target", "args": {"tags": ["default", "scorer"]}} + + def test_name_with_multiple_params(self) -> None: + """Test semicolon-separated multiple params.""" + result = frontend_core._parse_initializer_arg("target:tags=default;mode=strict") + assert result == {"name": "target", "args": {"tags": ["default"], "mode": ["strict"]}} + + def test_missing_name_before_colon_raises(self) -> None: + """Test that ':key=val' with no name raises ValueError.""" + with pytest.raises(ValueError, match="missing name before ':'"): + frontend_core._parse_initializer_arg(":tags=default") + + def test_missing_equals_in_param_raises(self) -> None: + """Test that 'name:badparam' without '=' raises ValueError.""" + with pytest.raises(ValueError, match="expected key=value format"): + frontend_core._parse_initializer_arg("target:badparam") + + def test_empty_key_raises(self) -> None: + """Test that 'name:=value' with empty key raises ValueError.""" + with pytest.raises(ValueError, match="empty key"): + frontend_core._parse_initializer_arg("target:=value") + + def test_colon_but_no_params_returns_string(self) -> None: + """Test that 'name:' with trailing colon but no params returns the name string.""" + result = frontend_core._parse_initializer_arg("target:") + assert result == "target" + + class TestParseRunArguments: """Tests for parse_run_arguments function.""" @@ -535,6 +579,31 @@ def test_parse_run_arguments_with_initializers(self): assert result["scenario_name"] == "test_scenario" assert result["initializers"] == ["init1", "init2"] + def test_parse_run_arguments_with_initializer_params(self): + """Test parsing initializers with key=value params.""" + result = frontend_core.parse_run_arguments( + args_string="test_scenario --initializers simple target:tags=default" + ) + + assert result["initializers"][0] == "simple" + assert result["initializers"][1] == {"name": "target", "args": {"tags": ["default"]}} + + def test_parse_run_arguments_with_initializer_multiple_params(self): + """Test parsing initializers with multiple key=value params separated by semicolons.""" + result = frontend_core.parse_run_arguments( + args_string="test_scenario --initializers target:tags=default;mode=strict" + ) + + assert result["initializers"][0] == {"name": "target", "args": {"tags": ["default"], "mode": ["strict"]}} + + def test_parse_run_arguments_with_initializer_comma_list(self): + """Test parsing initializer params with comma-separated values into lists.""" + result = frontend_core.parse_run_arguments( + args_string="test_scenario --initializers target:tags=default,scorer" + ) + + assert result["initializers"][0] == {"name": "target", "args": {"tags": ["default", "scorer"]}} + def test_parse_run_arguments_with_strategies(self): """Test parsing with strategies.""" result = frontend_core.parse_run_arguments(args_string="test_scenario --strategies s1 s2") diff --git a/tests/unit/cli/test_pyrit_backend.py b/tests/unit/cli/test_pyrit_backend.py index d99744f138..a66e0e2422 100644 --- a/tests/unit/cli/test_pyrit_backend.py +++ b/tests/unit/cli/test_pyrit_backend.py @@ -42,7 +42,7 @@ async def test_initialize_and_run_passes_config_file_to_frontend_core(self) -> N ): mock_core = MagicMock() mock_core.initialize_async = AsyncMock() - mock_core._initializer_names = None + mock_core._initializer_configs = None mock_core_class.return_value = mock_core mock_server = MagicMock() diff --git a/tests/unit/setup/test_pyrit_initializer.py b/tests/unit/setup/test_pyrit_initializer.py index d4c22d82d0..28f36be052 100644 --- a/tests/unit/setup/test_pyrit_initializer.py +++ b/tests/unit/setup/test_pyrit_initializer.py @@ -10,7 +10,7 @@ set_default_value, set_global_variable, ) -from pyrit.setup.initializers import PyRITInitializer +from pyrit.setup.initializers import InitializerParameter, PyRITInitializer class TestPyRITInitializerBase: @@ -672,3 +672,163 @@ async def initialize_async(self) -> None: # Should return helpful messages assert "await initialize_pyrit_async()" in str(info["default_values"]) assert "await initialize_pyrit_async()" in str(info["global_variables"]) + + +class TestSupportedParameters: + """Tests for the parameter system on PyRITInitializer.""" + + def test_default_supported_parameters_is_empty(self) -> None: + """Test that base class returns empty supported_parameters.""" + + class NoParamsInit(PyRITInitializer): + @property + def name(self) -> str: + return "no_params" + + async def initialize_async(self) -> None: + pass + + init = NoParamsInit() + assert init.supported_parameters == [] + + def test_supported_parameters_declared(self) -> None: + """Test that subclass can declare supported_parameters.""" + + class WithParamsInit(PyRITInitializer): + @property + def name(self) -> str: + return "with_params" + + @property + def supported_parameters(self) -> list: + return [ + InitializerParameter(name="mode", description="Operation mode", default="fast"), + InitializerParameter(name="count", description="Item count", required=True), + ] + + async def initialize_async(self) -> None: + pass + + init = WithParamsInit() + assert len(init.supported_parameters) == 2 + assert init.supported_parameters[0].name == "mode" + assert init.supported_parameters[1].required is True + + def test_validate_params_raises_on_unknown(self) -> None: + """Test that unknown params raise ValueError.""" + + class StrictInit(PyRITInitializer): + @property + def name(self) -> str: + return "strict" + + @property + def supported_parameters(self) -> list: + return [InitializerParameter(name="level", description="Level")] + + async def initialize_async(self) -> None: + pass + + init = StrictInit() + with pytest.raises(ValueError, match="unknown parameter"): + init._validate_params(params={"bogus": ["value"]}) + + def test_validate_params_raises_on_missing_required(self) -> None: + """Test that missing required params raise ValueError.""" + + class RequiredInit(PyRITInitializer): + @property + def name(self) -> str: + return "required" + + @property + def supported_parameters(self) -> list: + return [InitializerParameter(name="key", description="API key", required=True)] + + async def initialize_async(self) -> None: + pass + + init = RequiredInit() + with pytest.raises(ValueError, match="requires parameter 'key'"): + init._validate_params(params={}) + + def test_validate_params_accepts_valid(self) -> None: + """Test that valid params pass validation.""" + + class ValidInit(PyRITInitializer): + @property + def name(self) -> str: + return "valid" + + @property + def supported_parameters(self) -> list: + return [ + InitializerParameter(name="mode", description="Mode", default="fast"), + InitializerParameter(name="key", description="Key", required=True), + ] + + async def initialize_async(self) -> None: + pass + + init = ValidInit() + # Should not raise + init._validate_params(params={"key": ["abc"], "mode": ["slow"]}) + + def test_validate_checks_params_on_instance(self) -> None: + """Test that validate() checks self.params.""" + + class ParamInit(PyRITInitializer): + @property + def name(self) -> str: + return "param_init" + + @property + def supported_parameters(self) -> list: + return [InitializerParameter(name="x", description="X")] + + async def initialize_async(self) -> None: + pass + + init = ParamInit() + init.params = {"unknown_key": ["val"]} + with pytest.raises(ValueError, match="unknown parameter"): + init.validate() + + @pytest.mark.asyncio + async def test_params_available_in_initialize_async(self) -> None: + """Test that self.params is available inside initialize_async.""" + + received_params = {} + + class TrackingInit(PyRITInitializer): + @property + def name(self) -> str: + return "tracking" + + async def initialize_async(self) -> None: + received_params.update(self.params) + + init = TrackingInit() + init.params = {"tags": ["default", "scorer"]} + await init.initialize_with_tracking_async() + + assert received_params == {"tags": ["default", "scorer"]} + + @pytest.mark.asyncio + async def test_empty_params_remains_empty_dict(self) -> None: + """Test that self.params is empty dict when not set.""" + + received = {"params": "not_set"} + + class EmptyParamsInit(PyRITInitializer): + @property + def name(self) -> str: + return "empty" + + async def initialize_async(self) -> None: + received["params"] = dict(self.params) + + init = EmptyParamsInit() + await init.initialize_with_tracking_async() + + assert received["params"] == {} diff --git a/tests/unit/setup/test_scorer_initializer.py b/tests/unit/setup/test_scorer_initializer.py index 7c8846e127..a59057e325 100644 --- a/tests/unit/setup/test_scorer_initializer.py +++ b/tests/unit/setup/test_scorer_initializer.py @@ -195,6 +195,19 @@ async def test_gracefully_skips_scorers_with_missing_target(self) -> None: assert registry.get_instance_by_name("inverted_refusal_gpt4o_unsafe_temp9") is None assert registry.get_instance_by_name("refusal_gpt4o") is not None + @pytest.mark.asyncio + async def test_default_tag_registers_all_current_scorers(self) -> None: + """Test that tags=['default'] registers all current scorers (all are tagged default).""" + self._register_all_scorer_targets() + os.environ.update(self.CONTENT_SAFETY_ENV_VARS) + + init = ScorerInitializer() + init.params = {"tags": ["default"]} + await init.initialize_async() + + registry = ScorerRegistry.get_registry_singleton() + assert len(registry) == 24 + class TestScorerInitializerGetInfo: """Tests for ScorerInitializer.get_info_async method.""" diff --git a/tests/unit/setup/test_targets_initializer.py b/tests/unit/setup/test_targets_initializer.py index 213268ccc4..fc91f54c6e 100644 --- a/tests/unit/setup/test_targets_initializer.py +++ b/tests/unit/setup/test_targets_initializer.py @@ -286,7 +286,8 @@ async def test_default_tag_excludes_scorer_targets(self) -> None: os.environ["AZURE_OPENAI_GPT4O_KEY"] = "test_key" os.environ["AZURE_OPENAI_GPT4O_MODEL"] = "gpt-4o" - init = TargetInitializer(tags=["default"]) + init = TargetInitializer() + init.params = {"tags": ["default"]} await init.initialize_async() registry = TargetRegistry.get_registry_singleton() @@ -305,7 +306,8 @@ async def test_scorer_tag_only_registers_scorer_targets(self) -> None: os.environ["AZURE_OPENAI_GPT4O_KEY"] = "test_key" os.environ["AZURE_OPENAI_GPT4O_MODEL"] = "gpt-4o" - init = TargetInitializer(tags=["scorer"]) + init = TargetInitializer() + init.params = {"tags": ["scorer"]} await init.initialize_async() registry = TargetRegistry.get_registry_singleton() @@ -324,7 +326,28 @@ async def test_multiple_tags_registers_matching(self) -> None: os.environ["AZURE_OPENAI_GPT4O_KEY"] = "test_key" os.environ["AZURE_OPENAI_GPT4O_MODEL"] = "gpt-4o" - init = TargetInitializer(tags=["default", "scorer"]) + init = TargetInitializer() + init.params = {"tags": ["default", "scorer"]} + await init.initialize_async() + + registry = TargetRegistry.get_registry_singleton() + assert registry.get_instance_by_name("azure_openai_gpt4o") is not None + assert registry.get_instance_by_name("azure_openai_gpt4o_temp9") is not None + + # Clean up + del os.environ["AZURE_OPENAI_GPT4O_ENDPOINT"] + del os.environ["AZURE_OPENAI_GPT4O_KEY"] + del os.environ["AZURE_OPENAI_GPT4O_MODEL"] + + @pytest.mark.asyncio + async def test_all_tag_registers_all_targets(self) -> None: + """Test that tags=['all'] registers both default and scorer targets.""" + os.environ["AZURE_OPENAI_GPT4O_ENDPOINT"] = "https://test.openai.azure.com" + os.environ["AZURE_OPENAI_GPT4O_KEY"] = "test_key" + os.environ["AZURE_OPENAI_GPT4O_MODEL"] = "gpt-4o" + + init = TargetInitializer() + init.params = {"tags": ["all"]} await init.initialize_async() registry = TargetRegistry.get_registry_singleton() diff --git a/uv.lock b/uv.lock index 916bec1e4b..c27a32692a 100644 --- a/uv.lock +++ b/uv.lock @@ -51,18 +51,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9f/d2/c581486aa6c4fbd7394c23c47b83fa1a919d34194e16944241daf9e762dd/accelerate-1.12.0-py3-none-any.whl", hash = "sha256:3e2091cd341423207e2f084a6654b1efcd250dc326f2a37d6dde446e07cabb11", size = 380935, upload-time = "2025-11-21T11:27:44.522Z" }, ] -[[package]] -name = "accessible-pygments" -version = "0.0.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pygments" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/bc/c1/bbac6a50d02774f91572938964c582fff4270eee73ab822a4aeea4d8b11b/accessible_pygments-0.0.5.tar.gz", hash = "sha256:40918d3e6a2b619ad424cb91e556bd3bd8865443d9f22f1dcdf79e33c8046872", size = 1377899, upload-time = "2024-05-10T11:23:10.216Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/8d/3f/95338030883d8c8b91223b4e21744b04d11b161a3ef117295d8241f50ab4/accessible_pygments-0.0.5-py3-none-any.whl", hash = "sha256:88ae3211e68a1d0b011504b2ffc1691feafce124b845bd072ab6f9f66f34d4b7", size = 1395903, upload-time = "2024-05-10T11:23:08.421Z" }, -] - [[package]] name = "aiofiles" version = "24.1.0" @@ -180,15 +168,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, ] -[[package]] -name = "alabaster" -version = "0.7.16" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/c9/3e/13dd8e5ed9094e734ac430b5d0eb4f2bb001708a8b7856cbf8e084e001ba/alabaster-0.7.16.tar.gz", hash = "sha256:75a8b99c28a5dad50dd7f8ccdd447a121ddb3892da9e53d1ca5cca3106d58d65", size = 23776, upload-time = "2024-01-10T00:56:10.189Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/32/34/d4e1c02d3bee589efb5dfa17f88ea08bdb3e3eac12bc475462aec52ed223/alabaster-0.7.16-py3-none-any.whl", hash = "sha256:b46733c07dce03ae4e150330b975c75737fa60f0a7c591b6c8bf4928a28e2c92", size = 13511, upload-time = "2024-01-10T00:56:08.388Z" }, -] - [[package]] name = "alembic" version = "1.17.2" @@ -2099,6 +2078,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7e/71/ba21c3fb8c5dce83b8c01f458a42e99ffdb1963aeec08fff5a18588d8fd7/greenlet-3.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:9ee1942ea19550094033c35d25d20726e4f1c40d59545815e1128ac58d416d38", size = 301833, upload-time = "2025-12-04T14:32:23.929Z" }, ] +[[package]] +name = "griffe" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "griffecli" }, + { name = "griffelib" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/94/ee21d41e7eb4f823b94603b9d40f86d3c7fde80eacc2c3c71845476dddaa/griffe-2.0.0-py3-none-any.whl", hash = "sha256:5418081135a391c3e6e757a7f3f156f1a1a746cc7b4023868ff7d5e2f9a980aa", size = 5214, upload-time = "2026-02-09T19:09:44.105Z" }, +] + +[[package]] +name = "griffecli" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama" }, + { name = "griffelib" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ed/d93f7a447bbf7a935d8868e9617cbe1cadf9ee9ee6bd275d3040fbf93d60/griffecli-2.0.0-py3-none-any.whl", hash = "sha256:9f7cd9ee9b21d55e91689358978d2385ae65c22f307a63fb3269acf3f21e643d", size = 9345, upload-time = "2026-02-09T19:09:42.554Z" }, +] + +[[package]] +name = "griffelib" +version = "2.0.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/51/c936033e16d12b627ea334aaaaf42229c37620d0f15593456ab69ab48161/griffelib-2.0.0-py3-none-any.whl", hash = "sha256:01284878c966508b6d6f1dbff9b6fa607bc062d8261c5c7253cb285b06422a7f", size = 142004, upload-time = "2026-02-09T19:09:40.561Z" }, +] + [[package]] name = "groovy" version = "0.1.2" @@ -2306,15 +2317,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0e/61/66938bbb5fc52dbdf84594873d5b51fb1f7c7794e9c0f5bd885f30bc507b/idna-3.11-py3-none-any.whl", hash = "sha256:771a87f49d9defaf64091e6e6fe9c18d4833f140bd19464795bc32d966ca37ea", size = 71008, upload-time = "2025-10-12T14:55:18.883Z" }, ] -[[package]] -name = "imagesize" -version = "1.4.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/84/62473fb57d61e31fef6e36d64a179c8781605429fd927b5dd608c997be31/imagesize-1.4.1.tar.gz", hash = "sha256:69150444affb9cb0d5cc5a92b3676f0b2fb7cd9ae39e947a5e11a36b4497cd4a", size = 1280026, upload-time = "2022-07-01T12:21:05.687Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ff/62/85c4c919272577931d407be5ba5d71c20f0b616d31a0befe0ae45bb79abd/imagesize-1.4.1-py2.py3-none-any.whl", hash = "sha256:0d8d18d08f840c19d0ee7ca1fd82490fdc3729b7ac93f49870406ddde8ef8d8b", size = 8769, upload-time = "2022-07-01T12:21:02.467Z" }, -] - [[package]] name = "importlib-metadata" version = "8.7.1" @@ -2733,50 +2735,18 @@ wheels = [ [[package]] name = "jupyter-book" -version = "1.0.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "jinja2" }, - { name = "jsonschema" }, - { name = "linkify-it-py" }, - { name = "myst-nb" }, - { name = "myst-parser" }, - { name = "pyyaml" }, - { name = "sphinx" }, - { name = "sphinx-book-theme" }, - { name = "sphinx-comments" }, - { name = "sphinx-copybutton" }, - { name = "sphinx-design" }, - { name = "sphinx-external-toc" }, - { name = "sphinx-jupyterbook-latex" }, - { name = "sphinx-multitoc-numbering" }, - { name = "sphinx-thebe" }, - { name = "sphinx-togglebutton" }, - { name = "sphinxcontrib-bibtex" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5c/52/506a4d67dfb9c3229e579ea9e4e42405f32f2d4854cc5729ad6a4affa80f/jupyter_book-1.0.4.tar.gz", hash = "sha256:143d7cf5adab5f370f3998c51e3495fc92d8d2a78631483adc0cb46a34e3240c", size = 67218, upload-time = "2025-02-25T10:37:02.785Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/58/7aa4413eec49a01a8746867f6f5e3bf2a2f5439459bafb44fc41df89574b/jupyter_book-1.0.4-py3-none-any.whl", hash = "sha256:c4a52d4af2a6518871ecc044bac08e0055025724baf3ec06663c2b0229b8b3ba", size = 44930, upload-time = "2025-02-25T10:37:00.899Z" }, -] - -[[package]] -name = "jupyter-cache" -version = "1.0.1" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } dependencies = [ - { name = "attrs" }, - { name = "click" }, - { name = "importlib-metadata" }, - { name = "nbclient" }, - { name = "nbformat" }, - { name = "pyyaml" }, - { name = "sqlalchemy" }, - { name = "tabulate" }, + { name = "ipykernel" }, + { name = "jupyter-core" }, + { name = "jupyter-server" }, + { name = "nodeenv" }, + { name = "platformdirs" }, ] -sdist = { url = "https://files.pythonhosted.org/packages/bb/f7/3627358075f183956e8c4974603232b03afd4ddc7baf72c2bc9fff522291/jupyter_cache-1.0.1.tar.gz", hash = "sha256:16e808eb19e3fb67a223db906e131ea6e01f03aa27f49a7214ce6a5fec186fb9", size = 32048, upload-time = "2024-11-15T16:03:55.322Z" } +sdist = { url = "https://files.pythonhosted.org/packages/68/39/8643f6629002e9ea35109219d09dfbe076c338c03fb3fd170c8d5e38bc73/jupyter_book-2.1.2.tar.gz", hash = "sha256:94bb8e63ef191e88cb6e7ab8a1ac66c7dc354ac228de1f558e5beb0381d38f8a", size = 3198230, upload-time = "2026-02-01T22:55:19.724Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/64/6b/67b87da9d36bff9df7d0efbd1a325fa372a43be7158effaf43ed7b22341d/jupyter_cache-1.0.1-py3-none-any.whl", hash = "sha256:9c3cafd825ba7da8b5830485343091143dff903e4d8c69db9349b728b140abf6", size = 33907, upload-time = "2024-11-15T16:03:54.021Z" }, + { url = "https://files.pythonhosted.org/packages/7c/25/6fe2dfc3d830ec614c5f83f88fc7472c4ed892b7f7f496367d31de4110c4/jupyter_book-2.1.2-py3-none-any.whl", hash = "sha256:1e92850680782ca777452780f9dee0550b52af338e6fe8c115961142ebf30a0e", size = 2606017, upload-time = "2026-02-01T22:55:16.714Z" }, ] [[package]] @@ -3090,15 +3060,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/82/3d/14ce75ef66813643812f3093ab17e46d3a206942ce7376d31ec2d36229e7/lark-1.3.1-py3-none-any.whl", hash = "sha256:c629b661023a014c37da873b4ff58a817398d12635d3bbb2c5a03be7fe5d1e12", size = 113151, upload-time = "2025-10-27T18:25:54.882Z" }, ] -[[package]] -name = "latexcodec" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/27/dd/4270b2c5e2ee49316c3859e62293bd2ea8e382339d63ab7bbe9f39c0ec3b/latexcodec-3.0.1.tar.gz", hash = "sha256:e78a6911cd72f9dec35031c6ec23584de6842bfbc4610a9678868d14cdfb0357", size = 31222, upload-time = "2025-06-17T18:47:34.051Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/b5/40/23569737873cc9637fd488606347e9dd92b9fa37ba4fcda1f98ee5219a97/latexcodec-3.0.1-py3-none-any.whl", hash = "sha256:a9eb8200bff693f0437a69581f7579eb6bca25c4193515c09900ce76451e452e", size = 18532, upload-time = "2025-06-17T18:47:30.726Z" }, -] - [[package]] name = "librt" version = "0.7.7" @@ -3150,18 +3111,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1c/7a/aab5f0fb122822e2acbc776addf8b9abfb4944a9056c00c393e46e543177/librt-0.7.7-cp313-cp313-win_arm64.whl", hash = "sha256:0996c83b1cb43c00e8c87835a284f9057bc647abd42b5871e5f941d30010c832", size = 42828, upload-time = "2026-01-01T23:51:41.731Z" }, ] -[[package]] -name = "linkify-it-py" -version = "2.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "uc-micro-py" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2a/ae/bb56c6828e4797ba5a4821eec7c43b8bf40f69cda4d4f5f8c8a2810ec96a/linkify-it-py-2.0.3.tar.gz", hash = "sha256:68cda27e162e9215c17d786649d1da0021a451bdc436ef9e0fa0ba5234b9b048", size = 27946, upload-time = "2024-02-04T14:48:04.179Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/04/1e/b832de447dee8b582cac175871d2f6c3d5077cc56d5575cadba1fd1cccfa/linkify_it_py-2.0.3-py3-none-any.whl", hash = "sha256:6bcbc417b0ac14323382aef5c5192c0075bf8a9d6b41820a2b66371eac6b6d79", size = 19820, upload-time = "2024-02-04T14:48:02.496Z" }, -] - [[package]] name = "lupa" version = "2.6" @@ -4044,45 +3993,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, ] -[[package]] -name = "myst-nb" -version = "1.3.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "importlib-metadata" }, - { name = "ipykernel" }, - { name = "ipython", version = "8.38.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "ipython", version = "9.9.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, - { name = "jupyter-cache" }, - { name = "myst-parser" }, - { name = "nbclient" }, - { name = "nbformat" }, - { name = "pyyaml" }, - { name = "sphinx" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/21/83/a894bd8dea7a6e9f053502ee8413484dcbf75a219013d6a72e971c0fecfd/myst_nb-1.3.0.tar.gz", hash = "sha256:df3cd4680f51a5af673fd46b38b562be3559aef1475e906ed0f2e66e4587ce4b", size = 81963, upload-time = "2025-07-13T22:49:38.493Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/69/a6/03d410c114b8c4856579b3d294dafc27626a7690a552625eec42b16dfa41/myst_nb-1.3.0-py3-none-any.whl", hash = "sha256:1f36af3c19964960ec4e51ac30949b6ed6df220356ffa8d60dd410885e132d7d", size = 82396, upload-time = "2025-07-13T22:49:37.019Z" }, -] - -[[package]] -name = "myst-parser" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "jinja2" }, - { name = "markdown-it-py" }, - { name = "mdit-py-plugins" }, - { name = "pyyaml" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/49/64/e2f13dac02f599980798c01156393b781aec983b52a6e4057ee58f07c43a/myst_parser-3.0.1.tar.gz", hash = "sha256:88f0cb406cb363b077d176b51c476f62d60604d68a8dcdf4832e080441301a87", size = 92392, upload-time = "2024-04-28T20:22:42.116Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e2/de/21aa8394f16add8f7427f0a1326ccd2b3a2a8a3245c9252bc5ac034c6155/myst_parser-3.0.1-py3-none-any.whl", hash = "sha256:6457aaa33a5d474aca678b8ead9b3dc298e89c68e67012e73146ea6fd54babf1", size = 83163, upload-time = "2024-04-28T20:22:39.985Z" }, -] - [[package]] name = "nbclient" version = "0.10.4" @@ -5454,32 +5364,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/47/8d/d529b5d697919ba8c11ad626e835d4039be708a35b0d22de83a269a6682c/pyasn1_modules-0.4.2-py3-none-any.whl", hash = "sha256:29253a9207ce32b64c3ac6600edc75368f98473906e8fd1043bd6b5b1de2c14a", size = 181259, upload-time = "2025-03-28T02:41:19.028Z" }, ] -[[package]] -name = "pybtex" -version = "0.25.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "latexcodec" }, - { name = "pyyaml" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5f/bc/c2be05ca72f8c103670e983df8be26d1e288bc6556f487fa8cccaa27779f/pybtex-0.25.1.tar.gz", hash = "sha256:9eaf90267c7e83e225af89fea65c370afbf65f458220d3946a9e3049e1eca491", size = 406157, upload-time = "2025-06-26T13:27:41.903Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/25/68/ceb5d6679baa326261f5d3e5113d9cfed6efef2810afd9f18bffb8ed312b/pybtex-0.25.1-py2.py3-none-any.whl", hash = "sha256:9053b0d619409a0a83f38abad5d9921de5f7b3ede00742beafcd9f10ad0d8c5c", size = 127437, upload-time = "2025-06-26T13:27:43.585Z" }, -] - -[[package]] -name = "pybtex-docutils" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "pybtex" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/7e/84/796ea94d26188a853660f81bded39f8de4cfe595130aef0dea1088705a11/pybtex-docutils-1.0.3.tar.gz", hash = "sha256:3a7ebdf92b593e00e8c1c538aa9a20bca5d92d84231124715acc964d51d93c6b", size = 18348, upload-time = "2023-08-22T18:47:54.833Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/11/b1/ce1f4596211efb5410e178a803f08e59b20bedb66837dcf41e21c54f9ec1/pybtex_docutils-1.0.3-py3-none-any.whl", hash = "sha256:8fd290d2ae48e32fcb54d86b0efb8d573198653c7e2447d5bec5847095f430b9", size = 6385, upload-time = "2023-08-22T06:43:20.513Z" }, -] - [[package]] name = "pycparser" version = "2.23" @@ -5625,25 +5509,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2c/86/e74c978800131c657fc5145f2c1c63e0cea01a49b6216f729cf77a2e1edf/pydash-8.0.5-py3-none-any.whl", hash = "sha256:b2625f8981862e19911daa07f80ed47b315ce20d9b5eb57aaf97aaf570c3892f", size = 102077, upload-time = "2025-01-17T16:08:47.91Z" }, ] -[[package]] -name = "pydata-sphinx-theme" -version = "0.15.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "accessible-pygments" }, - { name = "babel" }, - { name = "beautifulsoup4" }, - { name = "docutils" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "sphinx" }, - { name = "typing-extensions" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/67/ea/3ab478cccacc2e8ef69892c42c44ae547bae089f356c4b47caf61730958d/pydata_sphinx_theme-0.15.4.tar.gz", hash = "sha256:7762ec0ac59df3acecf49fd2f889e1b4565dbce8b88b2e29ee06fdd90645a06d", size = 2400673, upload-time = "2024-06-25T19:28:45.041Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e7/d3/c622950d87a2ffd1654208733b5bd1c5645930014abed8f4c0d74863988b/pydata_sphinx_theme-0.15.4-py3-none-any.whl", hash = "sha256:2136ad0e9500d0949f96167e63f3e298620040aea8f9c74621959eda5d4cf8e6", size = 4640157, upload-time = "2024-06-25T19:28:42.383Z" }, -] - [[package]] name = "pydocket" version = "0.16.3" @@ -5949,6 +5814,7 @@ all = [ ] dev = [ { name = "feedgen" }, + { name = "griffe" }, { name = "ipykernel" }, { name = "jupyter" }, { name = "jupyter-book" }, @@ -5965,7 +5831,6 @@ dev = [ { name = "pytest-xdist" }, { name = "respx" }, { name = "ruff" }, - { name = "sphinxcontrib-mermaid" }, { name = "types-aiofiles" }, { name = "types-cachetools" }, { name = "types-decorator" }, @@ -6041,13 +5906,14 @@ requires-dist = [ { name = "flask", marker = "extra == 'playwright'", specifier = ">=3.1.3" }, { name = "gradio", marker = "extra == 'all'", specifier = ">=6.7.0" }, { name = "gradio", marker = "extra == 'gradio'", specifier = ">=6.7.0" }, + { name = "griffe", marker = "extra == 'dev'", specifier = ">=2.0.0" }, { name = "httpx", extras = ["http2"], specifier = ">=0.27.2" }, { name = "ipykernel", marker = "extra == 'all'", specifier = ">=6.29.5" }, { name = "ipykernel", marker = "extra == 'dev'", specifier = ">=6.29.5" }, { name = "jinja2", specifier = ">=3.1.6" }, { name = "jupyter", marker = "extra == 'all'", specifier = ">=1.1.1" }, { name = "jupyter", marker = "extra == 'dev'", specifier = ">=1.1.1" }, - { name = "jupyter-book", marker = "extra == 'dev'", specifier = "==1.0.4" }, + { name = "jupyter-book", marker = "extra == 'dev'", specifier = ">=2.0.0" }, { name = "jupytext", marker = "extra == 'dev'", specifier = ">=1.17.1" }, { name = "matplotlib", marker = "extra == 'dev'", specifier = ">=3.10.0" }, { name = "ml-collections", marker = "extra == 'all'", specifier = ">=1.1.0" }, @@ -6090,7 +5956,6 @@ requires-dist = [ { name = "sentencepiece", marker = "extra == 'gcg'", specifier = ">=0.2.0" }, { name = "spacy", marker = "extra == 'all'", specifier = ">=3.8.7" }, { name = "spacy", marker = "extra == 'fairness-bias'", specifier = ">=3.8.7" }, - { name = "sphinxcontrib-mermaid", marker = "extra == 'dev'", specifier = ">=1.0.0" }, { name = "sqlalchemy", specifier = ">=2.0.41" }, { name = "tenacity", specifier = ">=9.1.2" }, { name = "termcolor", specifier = ">=2.4.0" }, @@ -7236,15 +7101,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/e9/44/75a9c9421471a6c4805dbf2356f7c181a29c1879239abab1ea2cc8f38b40/sniffio-1.3.1-py3-none-any.whl", hash = "sha256:2f6da418d1f1e0fddd844478f41680e794e6051915791a034ff65e5f100525a2", size = 10235, upload-time = "2024-02-25T23:20:01.196Z" }, ] -[[package]] -name = "snowballstemmer" -version = "3.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/75/a7/9810d872919697c9d01295633f5d574fb416d47e535f258272ca1f01f447/snowballstemmer-3.0.1.tar.gz", hash = "sha256:6d5eeeec8e9f84d4d56b847692bacf79bc2c8e90c7f80ca4444ff8b6f2e52895", size = 105575, upload-time = "2025-05-09T16:34:51.843Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c8/78/3565d011c61f5a43488987ee32b6f3f656e7f107ac2782dd57bdd7d91d9a/snowballstemmer-3.0.1-py3-none-any.whl", hash = "sha256:6cd7b3897da8d6c9ffb968a6781fa6532dce9c3618a4b127d920dab764a19064", size = 103274, upload-time = "2025-05-09T16:34:50.371Z" }, -] - [[package]] name = "sortedcontainers" version = "2.4.0" @@ -7340,231 +7196,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/33/78/d1a1a026ef3af911159398c939b1509d5c36fe524c7b644f34a5146c4e16/spacy_loggers-1.0.5-py3-none-any.whl", hash = "sha256:196284c9c446cc0cdb944005384270d775fdeaf4f494d8e269466cfa497ef645", size = 22343, upload-time = "2023-09-11T12:26:50.586Z" }, ] -[[package]] -name = "sphinx" -version = "7.4.7" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "alabaster" }, - { name = "babel" }, - { name = "colorama", marker = "sys_platform == 'win32'" }, - { name = "docutils" }, - { name = "imagesize" }, - { name = "jinja2" }, - { name = "packaging" }, - { name = "pygments" }, - { name = "requests" }, - { name = "snowballstemmer" }, - { name = "sphinxcontrib-applehelp" }, - { name = "sphinxcontrib-devhelp" }, - { name = "sphinxcontrib-htmlhelp" }, - { name = "sphinxcontrib-jsmath" }, - { name = "sphinxcontrib-qthelp" }, - { name = "sphinxcontrib-serializinghtml" }, - { name = "tomli", marker = "python_full_version < '3.11'" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/5b/be/50e50cb4f2eff47df05673d361095cafd95521d2a22521b920c67a372dcb/sphinx-7.4.7.tar.gz", hash = "sha256:242f92a7ea7e6c5b406fdc2615413890ba9f699114a9c09192d7dfead2ee9cfe", size = 8067911, upload-time = "2024-07-20T14:46:56.059Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0d/ef/153f6803c5d5f8917dbb7f7fcf6d34a871ede3296fa89c2c703f5f8a6c8e/sphinx-7.4.7-py3-none-any.whl", hash = "sha256:c2419e2135d11f1951cd994d6eb18a1835bd8fdd8429f9ca375dc1f3281bd239", size = 3401624, upload-time = "2024-07-20T14:46:52.142Z" }, -] - -[[package]] -name = "sphinx-book-theme" -version = "1.1.4" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pydata-sphinx-theme" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/45/19/d002ed96bdc7738c15847c730e1e88282d738263deac705d5713b4d8fa94/sphinx_book_theme-1.1.4.tar.gz", hash = "sha256:73efe28af871d0a89bd05856d300e61edce0d5b2fbb7984e84454be0fedfe9ed", size = 439188, upload-time = "2025-02-20T16:32:32.581Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/51/9e/c41d68be04eef5b6202b468e0f90faf0c469f3a03353f2a218fd78279710/sphinx_book_theme-1.1.4-py3-none-any.whl", hash = "sha256:843b3f5c8684640f4a2d01abd298beb66452d1b2394cd9ef5be5ebd5640ea0e1", size = 433952, upload-time = "2025-02-20T16:32:31.009Z" }, -] - -[[package]] -name = "sphinx-comments" -version = "0.0.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/c0/75/5bbf29e83eaf79843180cf424d0d550bda14a1792ca51dcf79daa065ba93/sphinx-comments-0.0.3.tar.gz", hash = "sha256:00170afff27019fad08e421da1ae49c681831fb2759786f07c826e89ac94cf21", size = 7960, upload-time = "2020-08-12T00:07:31.183Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/26/97/a5c39f619375d4f81d5422377fb027075898efa6b6202c1ccf1e5bb38a32/sphinx_comments-0.0.3-py3-none-any.whl", hash = "sha256:1e879b4e9bfa641467f83e3441ac4629225fc57c29995177d043252530c21d00", size = 4591, upload-time = "2020-08-12T00:07:30.297Z" }, -] - -[[package]] -name = "sphinx-copybutton" -version = "0.5.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/fc/2b/a964715e7f5295f77509e59309959f4125122d648f86b4fe7d70ca1d882c/sphinx-copybutton-0.5.2.tar.gz", hash = "sha256:4cf17c82fb9646d1bc9ca92ac280813a3b605d8c421225fd9913154103ee1fbd", size = 23039, upload-time = "2023-04-14T08:10:22.998Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/48/1ea60e74949eecb12cdd6ac43987f9fd331156388dcc2319b45e2ebb81bf/sphinx_copybutton-0.5.2-py3-none-any.whl", hash = "sha256:fb543fd386d917746c9a2c50360c7905b605726b9355cd26e9974857afeae06e", size = 13343, upload-time = "2023-04-14T08:10:20.844Z" }, -] - -[[package]] -name = "sphinx-design" -version = "0.6.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/2b/69/b34e0cb5336f09c6866d53b4a19d76c227cdec1bbc7ac4de63ca7d58c9c7/sphinx_design-0.6.1.tar.gz", hash = "sha256:b44eea3719386d04d765c1a8257caca2b3e6f8421d7b3a5e742c0fd45f84e632", size = 2193689, upload-time = "2024-08-02T13:48:44.277Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c6/43/65c0acbd8cc6f50195a3a1fc195c404988b15c67090e73c7a41a9f57d6bd/sphinx_design-0.6.1-py3-none-any.whl", hash = "sha256:b11f37db1a802a183d61b159d9a202314d4d2fe29c163437001324fe2f19549c", size = 2215338, upload-time = "2024-08-02T13:48:42.106Z" }, -] - -[[package]] -name = "sphinx-external-toc" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "click" }, - { name = "pyyaml" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/d7/b3/e900bcbb9d0071b928991e00ea70b3e6c6dd774dcf906c043c500e61584c/sphinx_external_toc-1.0.1.tar.gz", hash = "sha256:a7d2c63cc47ec688546443b28bc4ef466121827ef3dc7bb509de354bad4ea2e0", size = 32755, upload-time = "2023-12-12T10:26:53.951Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5f/9a/cb412957424012869b43a5aa3d95ccebcac737dafc5a545ce15bb8037f6e/sphinx_external_toc-1.0.1-py3-none-any.whl", hash = "sha256:d9e02d50731dee9697c1887e4f8b361e7b86d38241f0e66bd5a9f4096779646f", size = 26677, upload-time = "2023-12-12T10:26:52.017Z" }, -] - -[[package]] -name = "sphinx-jupyterbook-latex" -version = "1.0.0" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/80/29/18a1fc30e9315e72f068637079169525069a7c0b2fbe51cf689af0576214/sphinx_jupyterbook_latex-1.0.0.tar.gz", hash = "sha256:f54c6674c13f1616f9a93443e98b9b5353f9fdda8e39b6ec552ccf0b3e5ffb62", size = 11945, upload-time = "2023-12-11T15:37:25.034Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/70/1f/1d4ecaf58b17fe61497644655f40b04d84a88348e41a6f0c6392394d95e4/sphinx_jupyterbook_latex-1.0.0-py3-none-any.whl", hash = "sha256:e0cd3e9e1c5af69136434e21a533343fdf013475c410a414d5b7b4922b4f3891", size = 13319, upload-time = "2023-12-11T15:37:23.25Z" }, -] - -[[package]] -name = "sphinx-multitoc-numbering" -version = "0.1.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/37/1e/577bae038372885ebc34bd8c0f290295785a0250cac6528eb6d50e4b92d5/sphinx-multitoc-numbering-0.1.3.tar.gz", hash = "sha256:c9607671ac511236fa5d61a7491c1031e700e8d498c9d2418e6c61d1251209ae", size = 4542, upload-time = "2021-03-15T12:01:43.758Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ec/9f/902f2030674cd9473fdbe5a2c2dec2618c27ec853484c35f82cf8df40ece/sphinx_multitoc_numbering-0.1.3-py3-none-any.whl", hash = "sha256:33d2e707a9b2b8ad636b3d4302e658a008025106fe0474046c651144c26d8514", size = 4616, upload-time = "2021-03-15T12:01:42.419Z" }, -] - -[[package]] -name = "sphinx-thebe" -version = "0.3.1" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/da/fd/926ba4af1eb2708b1ac0fa4376e4bfb11d9a32b2a00e3614137a569c1ddf/sphinx_thebe-0.3.1.tar.gz", hash = "sha256:576047f45560e82f64aa5f15200b1eb094dcfe1c5b8f531a8a65bd208e25a493", size = 20789, upload-time = "2024-02-07T13:31:57.002Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/ca/7c/a53bdb465fd364bc3d255d96d5d70e6ba5183cfb4e45b8aa91c59b099124/sphinx_thebe-0.3.1-py3-none-any.whl", hash = "sha256:e7e7edee9f0d601c76bc70156c471e114939484b111dd8e74fe47ac88baffc52", size = 9030, upload-time = "2024-02-07T13:31:55.286Z" }, -] - -[[package]] -name = "sphinx-togglebutton" -version = "0.3.2" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "setuptools" }, - { name = "sphinx" }, - { name = "wheel" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f0/df/d151dfbbe588116e450ca7e898750cb218dca6b2e557ced8de6f9bd7242b/sphinx-togglebutton-0.3.2.tar.gz", hash = "sha256:ab0c8b366427b01e4c89802d5d078472c427fa6e9d12d521c34fa0442559dc7a", size = 8324, upload-time = "2022-07-15T12:08:50.286Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/e9/18/267ce39f29d26cdc7177231428ba823fe5ca94db8c56d1bed69033b364c8/sphinx_togglebutton-0.3.2-py3-none-any.whl", hash = "sha256:9647ba7874b7d1e2d43413d8497153a85edc6ac95a3fea9a75ef9c1e08aaae2b", size = 8249, upload-time = "2022-07-15T12:08:48.8Z" }, -] - -[[package]] -name = "sphinxcontrib-applehelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ba/6e/b837e84a1a704953c62ef8776d45c3e8d759876b4a84fe14eba2859106fe/sphinxcontrib_applehelp-2.0.0.tar.gz", hash = "sha256:2f29ef331735ce958efa4734873f084941970894c6090408b079c61b2e1c06d1", size = 20053, upload-time = "2024-07-29T01:09:00.465Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/5d/85/9ebeae2f76e9e77b952f4b274c27238156eae7979c5421fba91a28f4970d/sphinxcontrib_applehelp-2.0.0-py3-none-any.whl", hash = "sha256:4cd3f0ec4ac5dd9c17ec65e9ab272c9b867ea77425228e68ecf08d6b28ddbdb5", size = 119300, upload-time = "2024-07-29T01:08:58.99Z" }, -] - -[[package]] -name = "sphinxcontrib-bibtex" -version = "2.6.5" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "docutils" }, - { name = "pybtex" }, - { name = "pybtex-docutils" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/de/83/1488c9879f2fa3c2cbd6f666c7a3a42a1fa9e08462bec73281fa6c092cba/sphinxcontrib_bibtex-2.6.5.tar.gz", hash = "sha256:9b3224dd6fece9268ebd8c905dc0a83ff2f6c54148a9235fe70e9d1e9ff149c0", size = 118462, upload-time = "2025-06-27T10:40:14.061Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/9e/a0/3a612da94f828f26cabb247817393e79472c32b12c49222bf85fb6d7b6c8/sphinxcontrib_bibtex-2.6.5-py3-none-any.whl", hash = "sha256:455ea4509642ea0b28ede3721550273626f85af65af01f161bfd8e19dc1edd7d", size = 40410, upload-time = "2025-06-27T10:40:12.274Z" }, -] - -[[package]] -name = "sphinxcontrib-devhelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/f6/d2/5beee64d3e4e747f316bae86b55943f51e82bb86ecd325883ef65741e7da/sphinxcontrib_devhelp-2.0.0.tar.gz", hash = "sha256:411f5d96d445d1d73bb5d52133377b4248ec79db5c793ce7dbe59e074b4dd1ad", size = 12967, upload-time = "2024-07-29T01:09:23.417Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/35/7a/987e583882f985fe4d7323774889ec58049171828b58c2217e7f79cdf44e/sphinxcontrib_devhelp-2.0.0-py3-none-any.whl", hash = "sha256:aefb8b83854e4b0998877524d1029fd3e6879210422ee3780459e28a1f03a8a2", size = 82530, upload-time = "2024-07-29T01:09:21.945Z" }, -] - -[[package]] -name = "sphinxcontrib-htmlhelp" -version = "2.1.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/43/93/983afd9aa001e5201eab16b5a444ed5b9b0a7a010541e0ddfbbfd0b2470c/sphinxcontrib_htmlhelp-2.1.0.tar.gz", hash = "sha256:c9e2916ace8aad64cc13a0d233ee22317f2b9025b9cf3295249fa985cc7082e9", size = 22617, upload-time = "2024-07-29T01:09:37.889Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/0a/7b/18a8c0bcec9182c05a0b3ec2a776bba4ead82750a55ff798e8d406dae604/sphinxcontrib_htmlhelp-2.1.0-py3-none-any.whl", hash = "sha256:166759820b47002d22914d64a075ce08f4c46818e17cfc9470a9786b759b19f8", size = 98705, upload-time = "2024-07-29T01:09:36.407Z" }, -] - -[[package]] -name = "sphinxcontrib-jsmath" -version = "1.0.1" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/b2/e8/9ed3830aeed71f17c026a07a5097edcf44b692850ef215b161b8ad875729/sphinxcontrib-jsmath-1.0.1.tar.gz", hash = "sha256:a9925e4a4587247ed2191a22df5f6970656cb8ca2bd6284309578f2153e0c4b8", size = 5787, upload-time = "2019-01-21T16:10:16.347Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/c2/42/4c8646762ee83602e3fb3fbe774c2fac12f317deb0b5dbeeedd2d3ba4b77/sphinxcontrib_jsmath-1.0.1-py2.py3-none-any.whl", hash = "sha256:2ec2eaebfb78f3f2078e73666b1415417a116cc848b72e5172e596c871103178", size = 5071, upload-time = "2019-01-21T16:10:14.333Z" }, -] - -[[package]] -name = "sphinxcontrib-mermaid" -version = "1.2.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "pyyaml" }, - { name = "sphinx" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/f5/49/c6ddfe709a4ab76ac6e5a00e696f73626b2c189dc1e1965a361ec102e6cc/sphinxcontrib_mermaid-1.2.3.tar.gz", hash = "sha256:358699d0ec924ef679b41873d9edd97d0773446daf9760c75e18dc0adfd91371", size = 18885, upload-time = "2025-11-26T04:18:32.43Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/d1/39/8b54299ffa00e597d3b0b4d042241a0a0b22cb429ad007ccfb9c1745b4d1/sphinxcontrib_mermaid-1.2.3-py3-none-any.whl", hash = "sha256:5be782b27026bef97bfb15ccb2f7868b674a1afc0982b54cb149702cfc25aa02", size = 13413, upload-time = "2025-11-26T04:18:31.269Z" }, -] - -[[package]] -name = "sphinxcontrib-qthelp" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/68/bc/9104308fc285eb3e0b31b67688235db556cd5b0ef31d96f30e45f2e51cae/sphinxcontrib_qthelp-2.0.0.tar.gz", hash = "sha256:4fe7d0ac8fc171045be623aba3e2a8f613f8682731f9153bb2e40ece16b9bbab", size = 17165, upload-time = "2024-07-29T01:09:56.435Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/27/83/859ecdd180cacc13b1f7e857abf8582a64552ea7a061057a6c716e790fce/sphinxcontrib_qthelp-2.0.0-py3-none-any.whl", hash = "sha256:b18a828cdba941ccd6ee8445dbe72ffa3ef8cbe7505d8cd1fa0d42d3f2d5f3eb", size = 88743, upload-time = "2024-07-29T01:09:54.885Z" }, -] - -[[package]] -name = "sphinxcontrib-serializinghtml" -version = "2.0.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/3b/44/6716b257b0aa6bfd51a1b31665d1c205fb12cb5ad56de752dfa15657de2f/sphinxcontrib_serializinghtml-2.0.0.tar.gz", hash = "sha256:e9d912827f872c029017a53f0ef2180b327c3f7fd23c87229f7a8e8b70031d4d", size = 16080, upload-time = "2024-07-29T01:10:09.332Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/52/a7/d2782e4e3f77c8450f727ba74a8f12756d5ba823d81b941f1b04da9d033a/sphinxcontrib_serializinghtml-2.0.0-py3-none-any.whl", hash = "sha256:6e2cb0eef194e10c27ec0023bfeb25badbbb5868244cf5bc5bdc04e4464bf331", size = 92072, upload-time = "2024-07-29T01:10:08.203Z" }, -] - [[package]] name = "sqlalchemy" version = "2.0.45" @@ -7718,15 +7349,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, ] -[[package]] -name = "tabulate" -version = "0.9.0" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/ec/fe/802052aecb21e3797b8f7902564ab6ea0d60ff8ca23952079064155d1ae1/tabulate-0.9.0.tar.gz", hash = "sha256:0095b12bf5966de529c0feb1fa08671671b3368eec77d7ef7ab114be2c068b3c", size = 81090, upload-time = "2022-10-06T17:21:48.54Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/40/44/4a5f08c96eb108af5cb50b41f76142f0afa346dfa99d5296fe7202a11854/tabulate-0.9.0-py3-none-any.whl", hash = "sha256:024ca478df22e9340661486f85298cff5f6dcdba14f3813e8830015b9ed1948f", size = 35252, upload-time = "2022-10-06T17:21:44.262Z" }, -] - [[package]] name = "tenacity" version = "9.1.2" @@ -8222,15 +7844,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c7/b0/003792df09decd6849a5e39c28b513c06e84436a54440380862b5aeff25d/tzdata-2025.3-py2.py3-none-any.whl", hash = "sha256:06a47e5700f3081aab02b2e513160914ff0694bce9947d6b76ebd6bf57cfc5d1", size = 348521, upload-time = "2025-12-13T17:45:33.889Z" }, ] -[[package]] -name = "uc-micro-py" -version = "1.0.3" -source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/91/7a/146a99696aee0609e3712f2b44c6274566bc368dfe8375191278045186b8/uc-micro-py-1.0.3.tar.gz", hash = "sha256:d321b92cff673ec58027c04015fcaa8bb1e005478643ff4a500882eaab88c48a", size = 6043, upload-time = "2024-02-09T16:52:01.654Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/37/87/1f677586e8ac487e29672e4b17455758fce261de06a0d086167bb760361a/uc_micro_py-1.0.3-py3-none-any.whl", hash = "sha256:db1dffff340817673d7b466ec86114a9dc0e9d4d9b5ba229d9d60e5c12600cd5", size = 6229, upload-time = "2024-02-09T16:52:00.371Z" }, -] - [[package]] name = "uri-template" version = "1.3.0" @@ -8549,18 +8162,6 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/4d/ec/d58832f89ede95652fd01f4f24236af7d32b70cab2196dfcc2d2fd13c5c2/werkzeug-3.1.6-py3-none-any.whl", hash = "sha256:7ddf3357bb9564e407607f988f683d72038551200c704012bb9a4c523d42f131", size = 225166, upload-time = "2026-02-19T15:17:17.475Z" }, ] -[[package]] -name = "wheel" -version = "0.46.3" -source = { registry = "https://pypi.org/simple" } -dependencies = [ - { name = "packaging" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/89/24/a2eb353a6edac9a0303977c4cb048134959dd2a51b48a269dfc9dde00c8a/wheel-0.46.3.tar.gz", hash = "sha256:e3e79874b07d776c40bd6033f8ddf76a7dad46a7b8aa1b2787a83083519a1803", size = 60605, upload-time = "2026-01-22T12:39:49.136Z" } -wheels = [ - { url = "https://files.pythonhosted.org/packages/87/22/b76d483683216dde3d67cba61fb2444be8d5be289bf628c13fc0fd90e5f9/wheel-0.46.3-py3-none-any.whl", hash = "sha256:4b399d56c9d9338230118d705d9737a2a468ccca63d5e813e2a4fc7815d8bc4d", size = 30557, upload-time = "2026-01-22T12:39:48.099Z" }, -] - [[package]] name = "widgetsnbextension" version = "4.0.15"