diff --git a/monitoring/benchmarker/.gitignore b/monitoring/benchmarker/.gitignore new file mode 100644 index 0000000000..ea1472ec1f --- /dev/null +++ b/monitoring/benchmarker/.gitignore @@ -0,0 +1 @@ +output/ diff --git a/monitoring/benchmarker/artifacts/__init__.py b/monitoring/benchmarker/artifacts/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/benchmarker/artifacts/generation.py b/monitoring/benchmarker/artifacts/generation.py new file mode 100644 index 0000000000..03a74865c3 --- /dev/null +++ b/monitoring/benchmarker/artifacts/generation.py @@ -0,0 +1,26 @@ +import os + +from monitoring.benchmarker.artifacts.matplotlib.matplotlib_figure import ( + generate_matplotlib_figure, +) +from monitoring.benchmarker.artifacts.raw_report import generate_raw_report +from monitoring.benchmarker.configurations.artifacts.artifact import ( + ArtifactSpecification, +) +from monitoring.benchmarker.reports.report import BenchmarkRunReport + + +def generate_artifacts( + artifacts_specs: list[ArtifactSpecification], + report: BenchmarkRunReport, + output_dir: str, +) -> None: + """Generate and save all configured artifacts.""" + os.makedirs(output_dir, exist_ok=True) + + for spec in artifacts_specs: + if "raw_report" in spec and spec.raw_report is not None: + generate_raw_report(report, spec.raw_report, output_dir) + + if "matplotlib_figure" in spec and spec.matplotlib_figure is not None: + generate_matplotlib_figure(report, spec.matplotlib_figure, output_dir) diff --git a/monitoring/benchmarker/artifacts/matplotlib/__init__.py b/monitoring/benchmarker/artifacts/matplotlib/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/benchmarker/artifacts/matplotlib/matplotlib_figure.py b/monitoring/benchmarker/artifacts/matplotlib/matplotlib_figure.py new file mode 100644 index 0000000000..e2e1f68df6 --- /dev/null +++ b/monitoring/benchmarker/artifacts/matplotlib/matplotlib_figure.py @@ -0,0 +1,36 @@ +import os + +import matplotlib +from loguru import logger + +matplotlib.use("Agg") # Non-interactive backend suitable for headless benchmarking +import matplotlib.pyplot as plt + +from monitoring.benchmarker.configurations.artifacts.matplotlib_figure import ( + MatplotlibFigureSpecification, +) +from monitoring.benchmarker.reports.report import BenchmarkRunReport + + +def generate_matplotlib_figure( + report: BenchmarkRunReport, fig_spec: MatplotlibFigureSpecification, output_dir: str +) -> None: + filename = ( + fig_spec.name + if fig_spec.name.lower().endswith(".png") + else f"{fig_spec.name}.png" + ) + out_path = os.path.join(output_dir, filename) + logger.info( + f"Generating Matplotlib figure artifact '{fig_spec.name}' -> {out_path}" + ) + + fig = plt.figure( + figsize=(8 * fig_spec.n_subfigure_cols, 5 * fig_spec.n_subfigure_rows) + ) + + # TODO: Populate graph + + plt.tight_layout() + fig.savefig(out_path, bbox_inches="tight") + plt.close(fig) diff --git a/monitoring/benchmarker/artifacts/raw_report.py b/monitoring/benchmarker/artifacts/raw_report.py new file mode 100644 index 0000000000..7447476af7 --- /dev/null +++ b/monitoring/benchmarker/artifacts/raw_report.py @@ -0,0 +1,23 @@ +import json +import os + +from loguru import logger + +from monitoring.benchmarker.configurations.artifacts.raw_report import ( + RawReportSpecification, +) +from monitoring.benchmarker.reports.report import BenchmarkRunReport + + +def generate_raw_report( + report: BenchmarkRunReport, raw_spec: RawReportSpecification, output_dir: str +) -> None: + filename = ( + raw_spec.name + if raw_spec.name.lower().endswith(".json") + else f"{raw_spec.name}.json" + ) + out_path = os.path.join(output_dir, filename) + logger.info(f"Writing raw report artifact to {out_path}") + with open(out_path, "w") as f: + json.dump(report, f, sort_keys=True) diff --git a/monitoring/benchmarker/benchmark.py b/monitoring/benchmarker/benchmark.py index addd267899..717f4ce934 100755 --- a/monitoring/benchmarker/benchmark.py +++ b/monitoring/benchmarker/benchmark.py @@ -1,18 +1,14 @@ #!env/bin/python3 import argparse -import json import os import sys -from implicitdict import ImplicitDict from loguru import logger -from monitoring.benchmarker.configurations.configuration import BenchmarkConfiguration -from monitoring.benchmarker.validation import validate_config -from monitoring.uss_qualifier.fileio import ( - load_dict_with_references, -) # TODO: factor fileio out of uss_qualifier into monitorlib +from monitoring.benchmarker.artifacts.generation import generate_artifacts +from monitoring.benchmarker.engine.engine import run_benchmark +from monitoring.benchmarker.validation import load_config def parseArgs() -> argparse.Namespace: @@ -24,6 +20,15 @@ def parseArgs() -> argparse.Namespace: required=True, ) + parser.add_argument( + "-o", + "--output", + "--output-dir", + dest="output_dir", + default=os.path.join(os.path.dirname(__file__), "output"), + help="Folder to which output artifacts should be written (default: monitoring/benchmarker/output)", + ) + parser.add_argument( "--skip-validation", action="store_true", @@ -36,23 +41,18 @@ def parseArgs() -> argparse.Namespace: def run_config( config_name: str, skip_validation: bool, + output_dir: str, ) -> int: - config_src = load_dict_with_references(config_name) - - if not skip_validation: - logger.info("Validating configuration...") - validation_errors = validate_config(config_src) - if validation_errors: - for e in validation_errors: - logger.error("[{}]: {}", e.json_path, e.message) - raise ValueError( - f"{len(validation_errors)} benchmark configuration validation errors indicated above. Hint: resolve the clearest error first and then rerun validation." - ) + config = load_config(config_name, skip_validation) - whole_config = ImplicitDict.parse(config_src, BenchmarkConfiguration) + run_report = run_benchmark(config) - # Print the resolved configuration to console as JSON - print(json.dumps(whole_config, indent=2, sort_keys=True)) + if "artifacts" in config and config.artifacts: + logger.info("Generating configured artifacts...") + generate_artifacts(config.artifacts, run_report, output_dir) + logger.info("Artifact generation complete.") + else: + logger.warning("No artifacts specified in configuration.") return os.EX_OK @@ -66,6 +66,7 @@ def main() -> int: exit_code = run_config( args.config, args.skip_validation, + args.output_dir, ) if exit_code != os.EX_OK: return exit_code diff --git a/monitoring/benchmarker/engine/__init__.py b/monitoring/benchmarker/engine/__init__.py new file mode 100644 index 0000000000..74798ef546 --- /dev/null +++ b/monitoring/benchmarker/engine/__init__.py @@ -0,0 +1 @@ +"""Benchmarker execution engine and supporting modules.""" diff --git a/monitoring/benchmarker/engine/actions/__init__.py b/monitoring/benchmarker/engine/actions/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/benchmarker/engine/actions/actions.py b/monitoring/benchmarker/engine/actions/actions.py new file mode 100644 index 0000000000..81856254c6 --- /dev/null +++ b/monitoring/benchmarker/engine/actions/actions.py @@ -0,0 +1,28 @@ +from monitoring.benchmarker.configurations.actions import ( + BenchmarkActionName, + BenchmarkActionSpecification, +) +from monitoring.benchmarker.engine.actions.run_command import run_command + + +def run_scenario_actions( + action_names: list[BenchmarkActionName] | None, + action_specs: dict[BenchmarkActionName, BenchmarkActionSpecification], +) -> None: + """Run a sequence of scenario setup or teardown actions by name.""" + if not action_names: + return + + for action_name in action_names: + if action_name not in action_specs: + raise ValueError( + f"Scenario action '{action_name}' not defined in configuration.actions" + ) + action_spec = action_specs[action_name] + + if "run_command" in action_spec and action_spec.run_command is not None: + run_command(action_spec.run_command) + else: + raise NotImplementedError( + f"Action '{action_name}' has no recognized action specification implemented in this version of benchmarker" + ) diff --git a/monitoring/benchmarker/engine/actions/run_command.py b/monitoring/benchmarker/engine/actions/run_command.py new file mode 100644 index 0000000000..85e88ff60d --- /dev/null +++ b/monitoring/benchmarker/engine/actions/run_command.py @@ -0,0 +1,20 @@ +import os +import subprocess + +from loguru import logger + +from monitoring.benchmarker.configurations.actions import RunCommandActionSpecification +from monitoring.monitorlib.versioning import get_repo_root + + +def run_command(cmd_spec: RunCommandActionSpecification) -> None: + repo_root = get_repo_root() + cwd = cmd_spec.path.replace("$REPO_ROOT", repo_root) + env = os.environ.copy() + if "env" in cmd_spec and cmd_spec.env: + for k, v in cmd_spec.env.items(): + env[k] = str(v) + + logger.info(f"Action: Running command `{cmd_spec.command}` in {cwd}") + + subprocess.run(cmd_spec.command, shell=True, cwd=cwd, env=env, check=True) diff --git a/monitoring/benchmarker/engine/engine.py b/monitoring/benchmarker/engine/engine.py new file mode 100644 index 0000000000..16217a4f9f --- /dev/null +++ b/monitoring/benchmarker/engine/engine.py @@ -0,0 +1,104 @@ +import asyncio +from concurrent.futures import ThreadPoolExecutor + +from loguru import logger + +from monitoring.benchmarker.configurations.configuration import BenchmarkConfiguration +from monitoring.benchmarker.engine.actions.actions import run_scenario_actions +from monitoring.benchmarker.engine.loads.loads import run_scenario_load +from monitoring.benchmarker.engine.operations import ( + group_operations, +) +from monitoring.benchmarker.engine.resources import instantiate_resources +from monitoring.benchmarker.reports.report import ( + BenchmarkReport, + BenchmarkRunReport, + BenchmarkScenarioReport, +) +from monitoring.monitorlib.versioning import get_code_version, get_commit_hash + + +async def _run_benchmark_async( + config: BenchmarkConfiguration, + codebase_version: str, + commit_hash: str, +) -> BenchmarkRunReport: + logger.info("Instantiating declared resources...") + resource_pool = instantiate_resources(config) + + user_specs_map = {u.name: u for u in config.user_types} + loads_map = {load.name: load for load in config.loads} + + max_io_threads = 400 + executor = ThreadPoolExecutor( + max_workers=max_io_threads, thread_name_prefix="benchmarker_io_" + ) + + scenarios_reports: list[BenchmarkScenarioReport] = [] + + action_list = config.actions if "actions" in config and config.actions else [] + action_specs = {action_spec.name: action_spec for action_spec in action_list} + + try: + for scenario_spec in config.scenarios: + logger.info( + f"========== Starting Scenario '{scenario_spec.name}' ==========" + ) + + # Run setup actions + setup_actions = scenario_spec.setup if "setup" in scenario_spec else None + run_scenario_actions(setup_actions, action_specs) + + # Run load + if scenario_spec.load not in loads_map: + raise ValueError( + f"Scenario load '{scenario_spec.load}' not defined in configuration.loads" + ) + load_spec = loads_map[scenario_spec.load] + scenario_ops, scenario_steps = await run_scenario_load( + load_spec, + user_specs_map, + resource_pool, + executor, + ) + + # Run teardown actions + teardown_actions = ( + scenario_spec.teardown if "teardown" in scenario_spec else None + ) + run_scenario_actions(teardown_actions, action_specs) + + # Generate and record scenario report + scenario_report = BenchmarkScenarioReport( + operations=group_operations(scenario_ops), + steps=scenario_steps, + ) + if "metadata" in scenario_spec: + scenario_report.metadata = ( + dict(scenario_spec.metadata) + if scenario_spec.metadata is not None + else None + ) + scenarios_reports.append(scenario_report) + + logger.info( + f"========== Completed Scenario '{scenario_spec.name}' ==========" + ) + finally: + executor.shutdown(wait=True) + + run_report = BenchmarkRunReport( + codebase_version=codebase_version, + commit_hash=commit_hash, + configuration=config, + report=BenchmarkReport(scenarios=scenarios_reports), + ) + + return run_report + + +def run_benchmark(config: BenchmarkConfiguration) -> BenchmarkRunReport: + """Execute the benchmarker engine for the provided configuration and return the resulting report.""" + codebase_version = get_code_version() + commit_hash = get_commit_hash() + return asyncio.run(_run_benchmark_async(config, codebase_version, commit_hash)) diff --git a/monitoring/benchmarker/engine/loads/__init__.py b/monitoring/benchmarker/engine/loads/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/benchmarker/engine/loads/criteria.py b/monitoring/benchmarker/engine/loads/criteria.py new file mode 100644 index 0000000000..2f2b38b32f --- /dev/null +++ b/monitoring/benchmarker/engine/loads/criteria.py @@ -0,0 +1,197 @@ +from datetime import datetime + +from monitoring.benchmarker.configurations.loads import ( + LoadCompletionCriteria, + StepCompletionCriteria, + ThroughputStabilityCriteria, +) +from monitoring.benchmarker.engine.loads.status import throughput_of_step_ops +from monitoring.benchmarker.engine.operations import ExecutedOperation +from monitoring.benchmarker.engine.users.framework import VirtualUser +from monitoring.benchmarker.reports.report import BenchmarkScenarioStepReport + + +def check_stability_criteria( + criteria: ThroughputStabilityCriteria, + operations: list[ExecutedOperation], + virtual_users: list[VirtualUser], + step_start_time: datetime, +) -> bool: + if ( + "each_user_completed_at_least" in criteria + and criteria.each_user_completed_at_least + ): + req_count = criteria.each_user_completed_at_least.count + req_ops = set(criteria.each_user_completed_at_least.operations) + + user_counts: dict[str, int] = {vu.user_id: 0 for vu in virtual_users} + for op in operations: + if op.initiated_at.datetime < step_start_time: + continue + if op.successful and str(op.type) in req_ops and op.origin in user_counts: + user_counts[op.origin] += 1 + + return all(c >= req_count for c in user_counts.values()) + else: + raise NotImplementedError( + "No supported criteria specified in ThroughputStabilityCriteria" + ) + + +def check_step_completion_criteria( + criteria: StepCompletionCriteria, + step_start_time: datetime, + stability_time: datetime, + now: datetime, + operations: list[ExecutedOperation], +) -> bool: + has_any_of = "any_of" in criteria and criteria.any_of + has_duration = ( + "sampling_duration_at_least" in criteria and criteria.sampling_duration_at_least + ) + has_completed_ops = "completed_at_least" in criteria and criteria.completed_at_least + has_min_average = ( + "average_duration_more_than" in criteria and criteria.average_duration_more_than + ) + has_stability_duration = ( + "throughput_stability_took_longer_than" in criteria + and criteria.throughput_stability_took_longer_than + ) + + if ( + not has_any_of + and not has_duration + and not has_completed_ops + and not has_min_average + and not has_stability_duration + ): + raise NotImplementedError("StepCompletionCriteria has no specified conditions") + + if has_any_of and criteria.any_of: + if not any( + check_step_completion_criteria( + child, + step_start_time, + stability_time, + now, + operations, + ) + for child in criteria.any_of + ): + return False + + if has_duration and criteria.sampling_duration_at_least: + required_duration = criteria.sampling_duration_at_least.timedelta + if now - stability_time < required_duration: + return False + + if has_completed_ops and criteria.completed_at_least: + req_count = criteria.completed_at_least.count + relevant_ops = set(criteria.completed_at_least.operations) + completed = sum( + 1 + for op in operations + if op.successful + and op.completed_at.datetime >= stability_time + and op.type in relevant_ops + and op.completed_at.datetime <= now + ) + if completed < req_count: + return False + + if has_min_average and criteria.average_duration_more_than: + required_duration_s = ( + criteria.average_duration_more_than.duration.timedelta.total_seconds() + ) + relevant_ops = set(criteria.average_duration_more_than.operations) + matching_ops = [ + op + for op in operations + if op.successful + and op.completed_at.datetime >= stability_time + and op.type in relevant_ops + and stability_time <= op.completed_at.datetime <= now + ] + if not matching_ops: + return False + average_duration_s = sum( + (op.completed_at.datetime - op.initiated_at.datetime).total_seconds() + for op in matching_ops + ) / len(matching_ops) + if average_duration_s <= required_duration_s: + return False + + if has_stability_duration and criteria.throughput_stability_took_longer_than: + required_duration = criteria.throughput_stability_took_longer_than.timedelta + if stability_time - step_start_time <= required_duration: + return False + + return True + + +def check_load_completion_criteria( + criteria: LoadCompletionCriteria, + steps: list[BenchmarkScenarioStepReport], + operations: list[ExecutedOperation], +) -> bool: + has_any_of = "any_of" in criteria and criteria.any_of + has_throughput = ( + "throughput_lower_than_peak" in criteria and criteria.throughput_lower_than_peak + ) + has_failures = "failures_more_than" in criteria and criteria.failures_more_than + has_most_recent_step = "most_recent_step" in criteria and criteria.most_recent_step + + if ( + not has_any_of + and not has_throughput + and not has_failures + and not has_most_recent_step + ): + raise NotImplementedError( + "LoadCompletionCriteria specified no supported conditions" + ) + + if has_any_of and criteria.any_of: + if not any( + check_load_completion_criteria(child, steps, operations) + for child in criteria.any_of + ): + return False + + if has_throughput and criteria.throughput_lower_than_peak: + if len(steps) < 2: + return False + op_types = set(criteria.throughput_lower_than_peak.operations) + last_step_idx = len(steps) - 1 + last_tp = throughput_of_step_ops(steps[last_step_idx], operations, op_types) + peak_tp = max( + throughput_of_step_ops(steps[idx], operations, op_types) + for idx in range(last_step_idx) + ) + if last_tp >= peak_tp * criteria.throughput_lower_than_peak.fraction_of_peak: + return False + + if has_failures and criteria.failures_more_than: + req_count = criteria.failures_more_than.count + op_types = set(criteria.failures_more_than.operations) + fails = sum(1 for op in operations if not op.successful and op.type in op_types) + if fails <= req_count: + return False + + if has_most_recent_step and criteria.most_recent_step: + if not steps: + return False + last_step = steps[-1] + step_start_time = last_step.start_time.datetime + stability_time = last_step.throughput_stability_time.datetime + now = last_step.end_time.datetime + if not check_step_completion_criteria( + criteria.most_recent_step, + step_start_time, + stability_time, + now, + operations, + ): + return False + + return True diff --git a/monitoring/benchmarker/engine/loads/loads.py b/monitoring/benchmarker/engine/loads/loads.py new file mode 100644 index 0000000000..1ff82f9f5d --- /dev/null +++ b/monitoring/benchmarker/engine/loads/loads.py @@ -0,0 +1,34 @@ +from concurrent.futures import ThreadPoolExecutor +from typing import Any + +from monitoring.benchmarker.configurations.loads import ( + BenchmarkLoadSpecification, +) +from monitoring.benchmarker.configurations.users import ( + BenchmarkUserName, + BenchmarkUserSpecification, +) +from monitoring.benchmarker.engine.loads.user_ramp.user_ramp import run_user_ramp_load +from monitoring.benchmarker.engine.operations import ExecutedOperation +from monitoring.benchmarker.reports.report import BenchmarkScenarioStepReport +from monitoring.uss_qualifier.resources.definitions import ResourceID + + +async def run_scenario_load( + load_spec: BenchmarkLoadSpecification, + user_specs_map: dict[BenchmarkUserName, BenchmarkUserSpecification], + resource_pool: dict[ResourceID, Any], + executor: ThreadPoolExecutor, +) -> tuple[list[ExecutedOperation], list[BenchmarkScenarioStepReport]]: + """Execute a scenario load.""" + if "user_ramp" in load_spec and load_spec.user_ramp: + return await run_user_ramp_load( + load_spec.user_ramp, + user_specs_map, + resource_pool, + executor, + ) + else: + raise NotImplementedError( + f"Load specification '{load_spec.name}' does not specify any supported load type" + ) diff --git a/monitoring/benchmarker/engine/loads/status.py b/monitoring/benchmarker/engine/loads/status.py new file mode 100644 index 0000000000..71979b0c41 --- /dev/null +++ b/monitoring/benchmarker/engine/loads/status.py @@ -0,0 +1,131 @@ +from datetime import datetime + +from monitoring.benchmarker.configurations.loads import ( + OperationType, + StepCompletionCriteria, +) +from monitoring.benchmarker.engine.operations import ExecutedOperation +from monitoring.benchmarker.reports.report import BenchmarkScenarioStepReport + + +def throughput_of_step_ops( + step: BenchmarkScenarioStepReport, + operations: list[ExecutedOperation], + op_types: set[str] | set[OperationType], +) -> float: + start_time = step.throughput_stability_time.datetime + end_time = step.end_time.datetime + count = sum( + 1 + for op in operations + if op.successful + and op.type in op_types + and op.completed_at.datetime >= start_time + and op.completed_at.datetime <= end_time + ) + dur = (end_time - start_time).total_seconds() + return count / dur if dur > 0 else 0.0 + + +def format_step_completion_progress( + criteria: StepCompletionCriteria, + step_start_time: datetime, + stability_time: datetime, + now: datetime, + operations: list[ExecutedOperation], +) -> list[str]: + parts: list[str] = [] + if "sampling_duration_at_least" in criteria and criteria.sampling_duration_at_least: + req_dur = criteria.sampling_duration_at_least.timedelta.total_seconds() + cur_dur = (now - stability_time).total_seconds() + parts.append( + f"sampling_duration_at_least (threshold: {req_dur:.1f}s, current: {cur_dur:.1f}s)" + ) + + if "completed_at_least" in criteria and criteria.completed_at_least: + req_count = criteria.completed_at_least.count + req_ops = set(criteria.completed_at_least.operations) + completed = sum( + 1 + for op in operations + if op.successful + and op.completed_at.datetime >= stability_time + and op.type in req_ops + and op.completed_at.datetime <= now + ) + ops_str = ", ".join(sorted(req_ops)) + parts.append( + f"completed_at_least (threshold: {req_count} of [{ops_str}], current: {completed})" + ) + + if "average_duration_more_than" in criteria and criteria.average_duration_more_than: + req_dur = criteria.average_duration_more_than.duration.timedelta.total_seconds() + req_ops = set(criteria.average_duration_more_than.operations) + matching_ops = [ + op + for op in operations + if op.successful + and op.completed_at.datetime >= step_start_time + and op.type in req_ops + and stability_time <= op.completed_at.datetime <= now + ] + if matching_ops: + cur_dur = sum( + (op.completed_at.datetime - op.initiated_at.datetime).total_seconds() + for op in matching_ops + ) / len(matching_ops) + cur_str = f"{cur_dur:.1f}s" + else: + cur_str = "N/A" + ops_str = ", ".join(sorted(req_ops)) + parts.append( + f"average_duration_more_than (threshold: {req_dur:.1f}s of [{ops_str}], current: {cur_str})" + ) + + if ( + "throughput_stability_took_longer_than" in criteria + and criteria.throughput_stability_took_longer_than + ): + req_dur = ( + criteria.throughput_stability_took_longer_than.timedelta.total_seconds() + ) + cur_dur = (stability_time - step_start_time).total_seconds() + parts.append( + f"throughput_stability_took_longer_than (threshold: {req_dur:.1f}s, current: {cur_dur:.1f}s)" + ) + + if "any_of" in criteria and criteria.any_of is not None: + child_parts = [] + for child in criteria.any_of: + sub = format_step_completion_progress( + child, + step_start_time, + stability_time, + now, + operations, + ) + if sub: + child_parts.append("(" + " AND ".join(sub) + ")") + if child_parts: + parts.append(" OR ".join(child_parts)) + return parts + + +def get_operations_of_interest( + criteria: StepCompletionCriteria, + all_step_ops: list[ExecutedOperation], + with_defaults: bool = False, +) -> set[OperationType]: + ops: set[OperationType] = set() + if "completed_at_least" in criteria and criteria.completed_at_least: + for o in criteria.completed_at_least.operations: + ops.add(o) + if "average_duration_more_than" in criteria and criteria.average_duration_more_than: + for o in criteria.average_duration_more_than.operations: + ops.add(o) + if "any_of" in criteria and criteria.any_of is not None: + for child in criteria.any_of: + ops.update(get_operations_of_interest(child, all_step_ops)) + if not ops and with_defaults: + ops = {op.type for op in all_step_ops} + return ops diff --git a/monitoring/benchmarker/engine/loads/user_ramp/__init__.py b/monitoring/benchmarker/engine/loads/user_ramp/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/benchmarker/engine/loads/user_ramp/status.py b/monitoring/benchmarker/engine/loads/user_ramp/status.py new file mode 100644 index 0000000000..0524233881 --- /dev/null +++ b/monitoring/benchmarker/engine/loads/user_ramp/status.py @@ -0,0 +1,63 @@ +from datetime import UTC, datetime + +from monitoring.benchmarker.configurations.loads import UserRampLoad +from monitoring.benchmarker.engine.loads.status import format_step_completion_progress +from monitoring.benchmarker.engine.operations import ExecutedOperation +from monitoring.benchmarker.engine.users.framework import VirtualUser + + +def format_waiting_status( + ramp: UserRampLoad, + step_index: int, + operations: list[ExecutedOperation], + step_start_time: datetime | None, + stability_time: datetime | None, + virtual_users: list[VirtualUser], +) -> str: + if step_start_time is None: + return f"[Step {step_index} waiting to start]" + + if stability_time is None: + if ( + "each_user_completed_at_least" in ramp.throughput_stability_criteria + and ramp.throughput_stability_criteria.each_user_completed_at_least + ): + crit = ramp.throughput_stability_criteria.each_user_completed_at_least + req_count = crit.count + req_ops = set(crit.operations) + user_counts: dict[str, int] = {vu.user_id: 0 for vu in virtual_users} + for op in operations: + if ( + op.successful + and op.completed_at.datetime >= step_start_time + and op.type in req_ops + and op.origin in user_counts + ): + user_counts[op.origin] += 1 + counts = list(user_counts.values()) + met_users = sum(1 for c in counts if c >= req_count) + max_c = max(counts) if counts else 0 + min_c = min(counts) if counts else 0 + ops_str = ", ".join(sorted(req_ops)) + return f"[Step {step_index} waiting for throughput stability] each_user_completed_at_least (threshold: {req_count} of [{ops_str}]): {met_users}/{len(virtual_users)} users met threshold | most advanced user: {max_c} completed, least advanced user: {min_c} completed" + else: + return f"[Step {step_index} waiting for throughput stability] (evaluating stability criteria)" + else: + now = datetime.now(UTC) + progress = ( + format_step_completion_progress( + ramp.step_completion_criteria, + step_start_time, + stability_time, + now, + operations, + ) + if step_start_time is not None + else [] + ) + cond_str = ( + " AND ".join(progress) + if progress + else "(evaluating step completion criteria)" + ) + return f"[Step {step_index} waiting for step completion] {cond_str}" diff --git a/monitoring/benchmarker/engine/loads/user_ramp/user_ramp.py b/monitoring/benchmarker/engine/loads/user_ramp/user_ramp.py new file mode 100644 index 0000000000..518db7285d --- /dev/null +++ b/monitoring/benchmarker/engine/loads/user_ramp/user_ramp.py @@ -0,0 +1,255 @@ +import asyncio +import time +from concurrent.futures import ThreadPoolExecutor +from datetime import UTC, datetime +from typing import Any + +from implicitdict import StringBasedDateTime +from loguru import logger + +from monitoring.benchmarker.configurations.loads import ( + UserRampLoad, +) +from monitoring.benchmarker.configurations.users import ( + BenchmarkUserName, + BenchmarkUserSpecification, +) +from monitoring.benchmarker.engine.loads.criteria import ( + check_load_completion_criteria, + check_stability_criteria, + check_step_completion_criteria, +) +from monitoring.benchmarker.engine.loads.status import get_operations_of_interest +from monitoring.benchmarker.engine.loads.user_ramp.status import format_waiting_status +from monitoring.benchmarker.engine.operations import ExecutedOperation, record_operation +from monitoring.benchmarker.engine.users.creation import create_virtual_user +from monitoring.benchmarker.engine.users.framework import VirtualUser +from monitoring.benchmarker.reports.report import BenchmarkScenarioStepReport +from monitoring.uss_qualifier.resources.definitions import ResourceID + +PERIODIC_STATUS_PERIOD_S = 30.0 + + +async def run_user_ramp_load( + ramp: UserRampLoad, + user_specs_map: dict[BenchmarkUserName, BenchmarkUserSpecification], + resource_pool: dict[ResourceID, Any], + executor: ThreadPoolExecutor, +) -> tuple[list[ExecutedOperation], list[BenchmarkScenarioStepReport]]: + """Apply a load by driving virtual user workflows and monitoring step criteria.""" + ramp_user_type = ramp.user_type + if ramp_user_type not in user_specs_map: + raise ValueError( + f"User type '{ramp_user_type}' for UserRampLoad.user_type not found in configuration.user_types" + ) + user_spec = user_specs_map[ramp_user_type] + + active_tasks: list[asyncio.Task] = [] + virtual_users: list[VirtualUser] = [] + stop_event = asyncio.Event() + + operations: list[ExecutedOperation] = [] + steps: list[BenchmarkScenarioStepReport] = [] + + current_load_factor = ramp.initial_users + step_index = 0 + step_start_time: datetime | None = None + stability_time: datetime | None = None + + last_status_time = [time.monotonic()] + + def update_status_time() -> None: + last_status_time[0] = time.monotonic() + + def wrapped_record_op(op: ExecutedOperation) -> None: + update_status_time() + record_operation(op, operations) + + logger.info(f"Starting user_ramp load with initial_users={current_load_factor}") + update_status_time() + + # Print status periodically if no other status updates have happened recently + async def _periodic_summary_logger() -> None: + while not stop_event.is_set(): + now_t = time.monotonic() + dt_s = last_status_time[0] + PERIODIC_STATUS_PERIOD_S - now_t + if dt_s > 0: + await asyncio.sleep(dt_s) + elif not stop_event.is_set(): + last_status_time[0] = time.monotonic() + msg = format_waiting_status( + ramp, + step_index, + operations, + step_start_time, + stability_time, + virtual_users, + ) + logger.info(msg) + + periodic_summary_task = asyncio.create_task(_periodic_summary_logger()) + + try: + while not stop_event.is_set(): + # Start new step + step_start_time = datetime.now(UTC) + + # Spawn new users for step + first_user_spawned = None + last_user_spawned = None + while len(virtual_users) < current_load_factor: + user_id = f"{user_spec.name}_{len(virtual_users) + 1}" + if first_user_spawned is None: + first_user_spawned = user_id + last_user_spawned = user_id + vu = create_virtual_user( + user_id, + user_spec, + resource_pool, + executor, + wrapped_record_op, + ) + virtual_users.append(vu) + active_tasks.append(asyncio.create_task(vu.run_workflow(stop_event))) + if first_user_spawned and last_user_spawned: + logger.info( + f"Spawned virtual users '{first_user_spawned}' to '{last_user_spawned}'" + ) + update_status_time() + + # Wait for throughput to become stable for this step + stability_time = None + while not stop_event.is_set(): + if check_stability_criteria( + ramp.throughput_stability_criteria, + operations, + virtual_users, + step_start_time, + ): + stability_time = datetime.now(UTC) + logger.info( + f"Step {step_index} reached throughput stability after {(stability_time - step_start_time).total_seconds():.1f}s" + ) + update_status_time() + break + await asyncio.sleep(0.5) + + if stop_event.is_set() or stability_time is None: + break + + step_end_time = datetime.now(UTC) + + # Wait for step to complete + while not stop_event.is_set(): + now = datetime.now(UTC) + if check_step_completion_criteria( + ramp.step_completion_criteria, + step_start_time, + stability_time, + now, + operations, + ): + step_end_time = now + break + await asyncio.sleep(0.5) + + # Summarize activity during step + step_ops = [ + op + for op in operations + if op.completed_at.datetime >= step_start_time + and op.completed_at.datetime <= step_end_time + ] + ops_of_interest = get_operations_of_interest( + ramp.step_completion_criteria, step_ops, True + ) + throughput_duration_s = ( + (step_end_time - stability_time).total_seconds() + if stability_time + else 0.0 + ) + step_duration_s = (step_end_time - step_start_time).total_seconds() + + valid_count = sum( + 1 + for op in step_ops + if op.completed_at.datetime >= stability_time + and op.type in ops_of_interest + and op.successful + ) + tp_valid = ( + valid_count / throughput_duration_s + if throughput_duration_s > 0 + else 0.0 + ) + + step_count = sum( + 1 for op in step_ops if op.type in ops_of_interest and op.successful + ) + + fails_by_type: dict[str, int] = {} + for op in step_ops: + if not op.successful: + t_str = str(op.type) + fails_by_type[t_str] = fails_by_type.get(t_str, 0) + 1 + + failures_str = ( + ", ".join(f"{k}: {v}" for k, v in sorted(fails_by_type.items())) + if fails_by_type + else "0 failures" + ) + ops_interest_str = ( + ", ".join(sorted(ops_of_interest)) + if ops_of_interest + else "all operations" + ) + + logger.info( + f"Step {step_index} completed (load_factor={current_load_factor}, operations of interest: [{ops_interest_str}]):\n" + f" • Validity Period Throughput: {tp_valid:.2f} ops/s across validity duration ({throughput_duration_s:.1f}s)\n" + f" • Operations of Interest Completed: {valid_count} in validity period ({throughput_duration_s:.1f}s), {step_count} started since step began; full step duration ({step_duration_s:.1f}s)\n" + f" • Failures during step: {failures_str}" + ) + update_status_time() + + if stop_event.is_set(): + break + + # Report step + step_report = BenchmarkScenarioStepReport( + load_factor=float(current_load_factor), + start_time=StringBasedDateTime(step_start_time), + throughput_stability_time=StringBasedDateTime(stability_time), + end_time=StringBasedDateTime(step_end_time), + ) + steps.append(step_report) + + # Check if load is complete + if check_load_completion_criteria( + ramp.load_completion_criteria, steps, operations + ): + logger.info( + f"Load completion criteria met after step {step_index}. Stopping load." + ) + update_status_time() + stop_event.set() + break + + # Move to next step + step_index += 1 + current_load_factor += ramp.additional_users_per_step + logger.info( + f"Advancing to step {step_index} with load_factor={current_load_factor}" + ) + update_status_time() + finally: + periodic_summary_task.cancel() + if not stop_event.is_set(): + stop_event.set() + logger.info( + f"Waiting for {len(active_tasks)} active virtual users to wind down gracefully..." + ) + await asyncio.gather(*active_tasks, return_exceptions=True) + logger.info("All virtual users have finished.") + + return operations, steps diff --git a/monitoring/benchmarker/engine/operations.py b/monitoring/benchmarker/engine/operations.py new file mode 100644 index 0000000000..bae6cfda8e --- /dev/null +++ b/monitoring/benchmarker/engine/operations.py @@ -0,0 +1,111 @@ +from implicitdict import StringBasedDateTime, dataclass +from loguru import logger + +from monitoring.benchmarker.configurations.loads import OperationType +from monitoring.benchmarker.reports.report import ( + BenchmarkOperation, + OperationsByOrigin, + OperationsByOutcome, + OperationsByType, +) +from monitoring.monitorlib.fetch import Query + + +@dataclass +class ExecutedOperation: + """Record of an operation executed during a benchmark run, including type, origin, and outcome.""" + + type: OperationType + """The type of operation described by this record.""" + + origin: str + """Source/originator of this operation; e.g., the user that initiated this operation.""" + + initiated_at: StringBasedDateTime + """Time this operation was started/initiated.""" + + completed_at: StringBasedDateTime + """Time this operation completed, either successfully or in failure.""" + + successful: bool + """Whether this operation was successful (false for errors).""" + + query: Query | None = None + """The query details for this operation, if this was a query operation and query details are being recorded.""" + + +def group_operations(executed_ops: list[ExecutedOperation]) -> list[OperationsByType]: + """Convert/group a flat list of ExecutedOperation into hierarchical OperationsByType structure for reports or analysis.""" + ops_by_type_origin: dict[tuple[OperationType, str], list[ExecutedOperation]] = {} + type_order: list[OperationType] = [] + origin_order_by_type: dict[OperationType, list[str]] = {} + + for op in executed_ops: + if op.type not in type_order: + type_order.append(op.type) + origin_order_by_type[op.type] = [] + if op.origin not in origin_order_by_type[op.type]: + origin_order_by_type[op.type].append(op.origin) + + key = (op.type, op.origin) + if key not in ops_by_type_origin: + ops_by_type_origin[key] = [] + ops_by_type_origin[key].append(op) + + result: list[OperationsByType] = [] + for op_type in type_order: + origins_list: list[OperationsByOrigin] = [] + for origin in origin_order_by_type[op_type]: + ops = ops_by_type_origin[(op_type, origin)] + successful_ops: list[BenchmarkOperation] = [] + unsuccessful_ops: list[BenchmarkOperation] = [] + for op in ops: + clean_op = BenchmarkOperation( + t0=op.initiated_at, + t1=op.completed_at, + ) + if op.query: + clean_op.query = op.query + if op.successful: + successful_ops.append(clean_op) + else: + unsuccessful_ops.append(clean_op) + + kwargs = {} + if successful_ops: + kwargs["successful"] = successful_ops + if unsuccessful_ops: + kwargs["unsuccessful"] = unsuccessful_ops + outcome_record = OperationsByOutcome(**kwargs) + origins_list.append( + OperationsByOrigin( + origin=origin, + outcomes=[outcome_record], + ) + ) + result.append( + OperationsByType( + type=op_type, + origins=origins_list, + ) + ) + return result + + +def record_operation( + op: ExecutedOperation, operations: list[ExecutedOperation] +) -> None: + operations.append(op) + if not op.successful: + if op.query is not None: + details = ( + op.query.failure_details + or op.query.error_message + or op.query.response.failure + or f"HTTP {op.query.status_code}" + ) + logger.warning( + f"Operation '{op.type}' from origin '{op.origin}' failed on {op.query.request.method} {op.query.request.url} ({op.query.status_code}): {details}" + ) + else: + logger.warning(f"Operation '{op.type}' from origin '{op.origin}' failed.") diff --git a/monitoring/benchmarker/engine/resources.py b/monitoring/benchmarker/engine/resources.py new file mode 100644 index 0000000000..dda68b6399 --- /dev/null +++ b/monitoring/benchmarker/engine/resources.py @@ -0,0 +1,21 @@ +from monitoring.benchmarker.configurations.configuration import BenchmarkConfiguration +from monitoring.uss_qualifier.resources.definitions import ResourceID +from monitoring.uss_qualifier.resources.resource import ResourceType, create_resources + + +def instantiate_resources( + config: BenchmarkConfiguration, +) -> dict[ResourceID, ResourceType]: + """Instantiate all resources declared in the benchmark configuration.""" + if ( + "resources" not in config + or not config.resources + or "resource_declarations" not in config.resources + or not config.resources.resource_declarations + ): + return {} + return create_resources( + config.resources.resource_declarations, + resource_source="benchmark configuration", + stop_when_not_created=True, + ) diff --git a/monitoring/benchmarker/engine/users/__init__.py b/monitoring/benchmarker/engine/users/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/benchmarker/engine/users/creation.py b/monitoring/benchmarker/engine/users/creation.py new file mode 100644 index 0000000000..610ce3dc78 --- /dev/null +++ b/monitoring/benchmarker/engine/users/creation.py @@ -0,0 +1,30 @@ +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +from typing import Any + +from monitoring.benchmarker.configurations.users import ( + BenchmarkUserSpecification, +) +from monitoring.benchmarker.engine.operations import ExecutedOperation +from monitoring.benchmarker.engine.users.flight_planner.flight_planner import ( + FlightPlannerUser, +) +from monitoring.benchmarker.engine.users.framework import VirtualUser +from monitoring.uss_qualifier.resources.definitions import ResourceID + + +def create_virtual_user( + user_id: str, + user_spec: BenchmarkUserSpecification, + resource_pool: dict[ResourceID, Any], + executor: ThreadPoolExecutor, + record_operation: Callable[[ExecutedOperation], None], +) -> VirtualUser: + if "flight_planner" in user_spec and user_spec.flight_planner is not None: + return FlightPlannerUser( + user_id, user_spec, resource_pool, executor, record_operation + ) + else: + raise NotImplementedError( + f"User type '{user_spec.name}' does not specify any implemented behavior" + ) diff --git a/monitoring/benchmarker/engine/users/flight_planner/__init__.py b/monitoring/benchmarker/engine/users/flight_planner/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/monitoring/benchmarker/engine/users/flight_planner/flight_planner.py b/monitoring/benchmarker/engine/users/flight_planner/flight_planner.py new file mode 100644 index 0000000000..7ddbe77625 --- /dev/null +++ b/monitoring/benchmarker/engine/users/flight_planner/flight_planner.py @@ -0,0 +1,67 @@ +import asyncio +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor +from datetime import UTC, datetime +from typing import Any + +from implicitdict import StringBasedDateTime + +from monitoring.benchmarker.configurations.loads import OperationType, WorkflowType +from monitoring.benchmarker.configurations.users import ( + BenchmarkUserSpecification, +) +from monitoring.benchmarker.engine.operations import ExecutedOperation +from monitoring.benchmarker.engine.users.framework import VirtualUser +from monitoring.uss_qualifier.resources.definitions import ResourceID + + +class FlightPlannerUser(VirtualUser): + """Virtual user performing flight planning.""" + + def __init__( + self, + user_id: str, + user_spec: BenchmarkUserSpecification, + resource_pool: dict[ResourceID, Any], + executor: ThreadPoolExecutor, + record_operation: Callable[[ExecutedOperation], None], + ): + super().__init__(user_id, user_spec.name, executor, record_operation) + + self.record_operation = record_operation + + # TODO: Implement + + async def _sleep_interruptible( + self, seconds: float, stop_event: asyncio.Event + ) -> None: + """Sleep in short intervals to wake up quickly if stop_event is set.""" + elapsed = 0.0 + while elapsed < seconds and not stop_event.is_set(): + slice_dur = min(0.5, seconds - elapsed) + await asyncio.sleep(slice_dur) + elapsed += slice_dur + + async def run_workflow(self, stop_event: asyncio.Event) -> None: + duration_ms = 200 + duration_ms_dt = 200 + while not stop_event.is_set(): + t_start = datetime.now(UTC) + + # TODO: Perform real flight planning work + await self._sleep_interruptible(duration_ms * 0.001, stop_event) + duration_ms += duration_ms_dt + + t_end = datetime.now(UTC) + + flight_op = ExecutedOperation( + type=OperationType(WorkflowType.FlightPlannerFlight), + origin=self.user_id, + initiated_at=StringBasedDateTime(t_start), + completed_at=StringBasedDateTime(t_end), + successful=True, + ) + self.record_operation(flight_op) + + if stop_event.is_set(): + break diff --git a/monitoring/benchmarker/engine/users/framework.py b/monitoring/benchmarker/engine/users/framework.py new file mode 100644 index 0000000000..42bbd7a70a --- /dev/null +++ b/monitoring/benchmarker/engine/users/framework.py @@ -0,0 +1,40 @@ +import asyncio +from abc import ABC, abstractmethod +from collections.abc import Callable +from concurrent.futures import ThreadPoolExecutor + +from monitoring.benchmarker.configurations.users import ( + BenchmarkUserName, +) +from monitoring.benchmarker.engine.operations import ExecutedOperation + + +class VirtualUser(ABC): + """Base class for virtual users generating load.""" + + user_id: str + """Identity of the user for origin identification, etc.""" + + user_type_name: BenchmarkUserName + + executor: ThreadPoolExecutor + """Means by which to perform synchronous work in the async context.""" + + record_operation: Callable[[ExecutedOperation], None] + """Means by which to record an operation completed by the virtual user.""" + + def __init__( + self, + user_id: str, + user_type_name: BenchmarkUserName, + executor: ThreadPoolExecutor, + record_operation: Callable[[ExecutedOperation], None], + ): + self.user_id = user_id + self.user_type_name = user_type_name + self.executor = executor + self.record_operation = record_operation + + @abstractmethod + async def run_workflow(self, stop_event: asyncio.Event) -> None: + raise NotImplementedError() diff --git a/monitoring/benchmarker/reports/report.py b/monitoring/benchmarker/reports/report.py index 90d9319b96..1b6db795ad 100644 --- a/monitoring/benchmarker/reports/report.py +++ b/monitoring/benchmarker/reports/report.py @@ -71,7 +71,7 @@ class BenchmarkScenarioReport(ImplicitDict): steps: list[BenchmarkScenarioStepReport] """Boundaries of steps within this scenario.""" - metadata: dict = {} + metadata: Optional[dict] """Arbitrary metadata copied from the scenario specification.""" diff --git a/monitoring/benchmarker/validation.py b/monitoring/benchmarker/validation.py index 90a3c3b251..00ee2e5d71 100644 --- a/monitoring/benchmarker/validation.py +++ b/monitoring/benchmarker/validation.py @@ -1,3 +1,6 @@ +from implicitdict import ImplicitDict +from loguru import logger + from monitoring.benchmarker.configurations.configuration import ( BenchmarkConfiguration, ) @@ -5,6 +8,7 @@ ValidationError, validate_implicitdict_object, ) +from monitoring.uss_qualifier.fileio import load_dict_with_references from monitoring.uss_qualifier.validation import validate_resource_declarations @@ -24,3 +28,29 @@ def validate_config(config: dict) -> list[ValidationError]: ) return result + + +def load_config( + config_name: str, skip_validation: bool = False +) -> BenchmarkConfiguration: + """Load, optionally validate, and parse a benchmark configuration file reference. + + Args: + config_name: Configuration string indicating file reference (e.g. file://path/to/config.jsonnet) + skip_validation: Whether to skip schema validation of the configuration. + + Returns: Parsed BenchmarkConfiguration instance. + """ + config_src = load_dict_with_references(config_name) + + if not skip_validation: + logger.info("Validating configuration...") + validation_errors = validate_config(config_src) + if validation_errors: + for e in validation_errors: + logger.error("[{}]: {}", e.json_path, e.message) + raise ValueError( + f"{len(validation_errors)} benchmark configuration validation errors indicated above. Hint: resolve the clearest error first and then rerun validation." + ) + + return ImplicitDict.parse(config_src, BenchmarkConfiguration) diff --git a/monitoring/monitorlib/expressions/evaluation.py b/monitoring/monitorlib/expressions/evaluation.py new file mode 100644 index 0000000000..bdb78f45f8 --- /dev/null +++ b/monitoring/monitorlib/expressions/evaluation.py @@ -0,0 +1,70 @@ +from typing import Any + +from asteval import Interpreter, make_symbol_table +from loguru import logger + +from monitoring.monitorlib.expressions.types import ASTExpression, SymbolExpression + + +def _truncate(s: str) -> str: + max_len = 80 + if len(s) <= max_len: + return s + return f"{s[0 : max_len - 3]}..." + + +def _abbreviate(s: str) -> str: + max_len = 500 + if len(s) <= max_len: + return s + snippset_len = int(max_len / 2) - 4 + return f"{s[0:snippset_len]} [...] {s[-snippset_len:]}" + + +def _raise_evaluation_error(msg: str, interpreter: Interpreter) -> None: + err = interpreter.error[0].get_error() + + detail = "\n".join(_abbreviate(line) for line in err[1].split("\n")) + + default_symbols = make_symbol_table() + custom_symbols = {} + for k, v in interpreter.symtable.items(): + if k not in default_symbols or default_symbols[k] != v: + custom_symbols[k] = v + + logger.debug( + "asteval interpreter symbol table:\n" + + "\n".join(f" {k}: {_truncate(str(v))}" for k, v in custom_symbols.items()) + ) + raise ValueError(f"{msg}:\n{detail}") + + +def evaluate_expression( + expression: ASTExpression, name: str, interpreter: Interpreter +) -> Any: + """Evaluate `expression` for a variable of `name` using `interpreter`. + + Enhanced information is logged for failures.""" + result = interpreter.eval(expression) + if isinstance(interpreter.error, list) and len(interpreter.error) > 0: + _raise_evaluation_error(f"Error evaluating {name} '{expression}'", interpreter) + return result + + +def get_updated_context( + existing_symbols: dict[str, Any], new_expressions: list[SymbolExpression] +) -> tuple[dict[str, Any], Interpreter]: + """Starting from a context of `existing_symbols` and their values, evaluate the `new_expressions`. + + Returns: + * Updated context of new symbols and their values. + * Interpreter ready to evaluate more expressions with the updated context. + """ + updated_symbols = dict(existing_symbols) + interpreter = Interpreter(user_symbols=updated_symbols) + for sym_expr in new_expressions: + value = evaluate_expression(sym_expr.value, sym_expr.name, interpreter) + interpreter.symtable[sym_expr.name] = value + updated_symbols[sym_expr.name] = value + + return updated_symbols, interpreter diff --git a/pyproject.toml b/pyproject.toml index 31f8105cfe..31a30ec82b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -10,6 +10,7 @@ requires-python = ">=3.13" dependencies = [ "aiohttp", "arrow", + "asteval", "bc-jsonpath-ng", "cryptography", "deprecation", @@ -30,6 +31,7 @@ dependencies = [ "loguru", "lxml", "marko", + "matplotlib", "numpy", "oauthlib", "pem", @@ -73,6 +75,7 @@ extend-exclude = [ "monitoring/prober/output/*", "monitoring/mock_uss/output/*", "monitoring/uss_qualifier/output/*", + "monitoring/benchmarker/output/*", ] line-length = 88 @@ -96,6 +99,7 @@ exclude = [ "monitoring/prober/output/*", "monitoring/mock_uss/output/*", "monitoring/uss_qualifier/output/*", + "monitoring/benchmarker/output/*", ] [build-system] diff --git a/uv.lock b/uv.lock index 1a54ac8e90..53823b202d 100644 --- a/uv.lock +++ b/uv.lock @@ -132,6 +132,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ed/c9/d7977eaacb9df673210491da99e6a247e93df98c715fc43fd136ce1d3d33/arrow-1.4.0-py3-none-any.whl", hash = "sha256:749f0769958ebdc79c173ff0b0670d59051a535fa26e8eba02953dc19eb43205", size = 68797, upload-time = "2025-10-18T17:46:45.663Z" }, ] +[[package]] +name = "asteval" +version = "1.0.9" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b1/71/bec6758ab41ada3b6870cb194ea16d2e76bb530562c8a8cd7d2167012bea/asteval-1.0.9.tar.gz", hash = "sha256:4f7323d63d9fbf89d7e1d5f8bb59b6c093970c15bb5926e6e67d0195858aa230", size = 55237, upload-time = "2026-06-11T18:40:36.086Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fe/d7/a71b12b625c44cfcb95d41397752edccc0a731903081609cffd6720f671d/asteval-1.0.9-py3-none-any.whl", hash = "sha256:60513ea5d608d27bdb0cf3785dc993a6c3adeaee45f1bded0953a09e904e461f", size = 23320, upload-time = "2026-06-11T18:40:35.068Z" }, +] + [[package]] name = "attrs" version = "26.1.0" @@ -380,6 +389,61 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/fe/19/3ba5e1b0bcc7b91aeab6c258afd70e4907d220fed3972febe38feb40db30/configargparse-1.7.5-py3-none-any.whl", hash = "sha256:1e63fdffedf94da9cd435fc13a1cd24777e76879dd2343912c1f871d4ac8c592", size = 27692, upload-time = "2026-03-11T02:19:36.442Z" }, ] +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + [[package]] name = "cryptography" version = "49.0.0" @@ -439,6 +503,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/20/0c/7bb51e3acfafd16c48875bf3db03607674df16f5b6ef8d056586af7e2b8b/cssselect-1.4.0-py3-none-any.whl", hash = "sha256:c0ec5c0191c8ee39fcc8afc1540331d8b55b0183478c50e9c8a79d44dbceb1d8", size = 18540, upload-time = "2026-01-29T07:00:24.994Z" }, ] +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + [[package]] name = "decorator" version = "5.2.1" @@ -538,6 +611,39 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/59/f5/67e9cc5c2036f58115f9fe0f00d203cf6780c3ff8ae0e705e7a9d9e8ff9e/Flask_Login-0.6.3-py3-none-any.whl", hash = "sha256:849b25b82a436bf830a054e74214074af59097171562ab10bfa999e6b78aae5d", size = 17303, upload-time = "2023-10-30T14:53:19.636Z" }, ] +[[package]] +name = "fonttools" +version = "4.63.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/84/69/c97f2c18e0db87d2c7b15da1974dace76ae938f1cfa22e2727a648b7ed43/fonttools-4.63.0.tar.gz", hash = "sha256:caeb583deeb5168e694b65cda8b4ee62abedfa66cf88488734466f2366b9c4e0", size = 3597189, upload-time = "2026-05-14T12:04:30.958Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/8d/d8fec3dcde2963f8c908fb315e5ff2cd0ac34f82394bbbf73a2aa5145ce3/fonttools-4.63.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:cd7e9857e5e63738b9d9fd707bc1f59c8b09e5177726d23664db393c59bb08bd", size = 2876062, upload-time = "2026-05-14T12:03:32.554Z" }, + { url = "https://files.pythonhosted.org/packages/ef/71/d935dc54e4ff121bfdd11e08702db63a7e6f25af21d8a3d7b7212df53641/fonttools-4.63.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c2a2a42198b696a6f48fad91709afb55176e66a5e566131219dba372fb7f8c59", size = 2424594, upload-time = "2026-05-14T12:03:34.86Z" }, + { url = "https://files.pythonhosted.org/packages/8e/40/e76320afa1df918e146155ef239b1719ee266092e96f5423bfd075affba1/fonttools-4.63.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1e874792a8212b44583ea02189d9e693906b2f78b261f372f95d6c563210ac1d", size = 5024840, upload-time = "2026-05-14T12:03:36.745Z" }, + { url = "https://files.pythonhosted.org/packages/ce/36/0b805d8c485f872f65a509cbe3b58a5d0d17bee855333b54a150c79d3061/fonttools-4.63.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:22135da48a348785c5e2d5d2d9d6bec5ed44adacbaeb9db12d9493bf6c6bfa68", size = 4975801, upload-time = "2026-05-14T12:03:38.833Z" }, + { url = "https://files.pythonhosted.org/packages/c8/26/2cee03d0aa083ab022da5c07aff9ed3f689da1defb81ad6917c9627896da/fonttools-4.63.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ccf41f2efdf56994d22d73bef4ced1052161958169428d06ba9724ea9e9a64be", size = 4965009, upload-time = "2026-05-14T12:03:41.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/48/cc4b66d9058c0d0982c833fad10127c4b0e9324606aafa41382295ca4102/fonttools-4.63.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9ced0bd02ac751dd6319b0da88aaef24414e3b0dbc32bb4f24944821a3741a27", size = 5105892, upload-time = "2026-05-14T12:03:43.525Z" }, + { url = "https://files.pythonhosted.org/packages/d8/1f/a98a30a814b9ddef3a2e706025f90b9e0bc94890e6cb15254bc86547d11a/fonttools-4.63.0-cp313-cp313-win32.whl", hash = "sha256:85be818f5506e8a7753153def2c9550178f0ecae6a47b5e0e8dbb23f7cc90380", size = 2291313, upload-time = "2026-05-14T12:03:45.594Z" }, + { url = "https://files.pythonhosted.org/packages/92/46/5177b01f3b4abfdd4409f31cca4ab279c9343a26efbe9ec78c97fc612e02/fonttools-4.63.0-cp313-cp313-win_amd64.whl", hash = "sha256:ba04cb5891d4c0c21b6da95eda8d7b090021508a294fff33464fc7d241e0856b", size = 2342299, upload-time = "2026-05-14T12:03:47.414Z" }, + { url = "https://files.pythonhosted.org/packages/27/d2/23d25e3f247b328be58d04a4c9f894178a0d1eda7d42867cfb388adaf416/fonttools-4.63.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:fd1e3094f42d806d3d7c79162fc59e5910fcbe3a7360c385b8da969bc4493745", size = 2875338, upload-time = "2026-05-14T12:03:50.052Z" }, + { url = "https://files.pythonhosted.org/packages/cd/58/7dfa0c761cb3b2964e2a84c4dc986c926a87de0cb9fb60d5b28ded3f2914/fonttools-4.63.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:6e528da43bc3791085f8cb6141b1d13e459226790240340fcbb4625649238b03", size = 2422661, upload-time = "2026-05-14T12:03:52.154Z" }, + { url = "https://files.pythonhosted.org/packages/dd/87/64cfa18a7a1621d17b7f4502b2b0ed8a135a90c3db51ea590ee99043e76b/fonttools-4.63.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6b2248c5decb223562f7902ff6325077a073f608ee8e33e88ad88db734eb9f49", size = 5010526, upload-time = "2026-05-14T12:03:54.647Z" }, + { url = "https://files.pythonhosted.org/packages/36/e1/a8933a72c45a87177fbde2696e0d0755c8c9062f8c077a961c6215fa27b1/fonttools-4.63.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:308f957cdeaf8abe4e5f2f124902ef405448af92c90f80e302a3b771c2e6116b", size = 4923946, upload-time = "2026-05-14T12:03:56.984Z" }, + { url = "https://files.pythonhosted.org/packages/27/60/872e6e233b8c5e8b41413796ff18b7fe479661bd40147e071b450dfad7a1/fonttools-4.63.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:bf00f21eb5fb721dbaf73d1e9da6d02a1af7768f2ebcf9798be98beab8ba90f6", size = 4962489, upload-time = "2026-05-14T12:03:59.443Z" }, + { url = "https://files.pythonhosted.org/packages/30/c4/83c24f2ec38b90cfda84bf4b1a1f49df80e84a1db4e7ac6e0d41bf23bc39/fonttools-4.63.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c1aaa4b9c75798400ac043ce04d74e7830376c85095a5a6ed7cba2f17a266bf4", size = 5071870, upload-time = "2026-05-14T12:04:02.122Z" }, + { url = "https://files.pythonhosted.org/packages/de/40/3ae22b60ff1d41ce0bd044b31238cdc72cef99f28b976f1e128ebd618c9b/fonttools-4.63.0-cp314-cp314-win32.whl", hash = "sha256:22693918177bd9ceabec4736d338045f357769416fc6b0b2508eefef75b08616", size = 2295026, upload-time = "2026-05-14T12:04:04.47Z" }, + { url = "https://files.pythonhosted.org/packages/c3/d4/98078064ccc76b45cb0f6c002452011e93c4bd26f6850344f0951cc1fe89/fonttools-4.63.0-cp314-cp314-win_amd64.whl", hash = "sha256:7d782fac32985914c351556f68ac0855391572bcd87de50e05970d3cd4c96fc5", size = 2347454, upload-time = "2026-05-14T12:04:06.752Z" }, + { url = "https://files.pythonhosted.org/packages/49/4e/652d1580c5f4e39f7d103b0c793e4773129ad633dce4addd0cf4dfebde02/fonttools-4.63.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:6db5140a60a5d731d21ec076745b40a310607731b0a565b50776393188649001", size = 2958152, upload-time = "2026-05-14T12:04:08.706Z" }, + { url = "https://files.pythonhosted.org/packages/0e/55/ad864c9a9b219f552eb46b32cd7906c466e5a578ba0c3abfcc0fe7413eb6/fonttools-4.63.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:7d76edbff9014094dbf03bd2d074709dfa6ec7aba13d838c937a2b33d2d6a86e", size = 2460809, upload-time = "2026-05-14T12:04:10.783Z" }, + { url = "https://files.pythonhosted.org/packages/ea/2b/0aa8db70f18cf52e49b4ed5ecec68547f981160bf5ded3b5aed6faa0a6f9/fonttools-4.63.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0eac00b9118c3c2f87d272e45341871c5b3066baa3c86897fa634a7c3fb59096", size = 5148649, upload-time = "2026-05-14T12:04:12.747Z" }, + { url = "https://files.pythonhosted.org/packages/7f/63/18e4369c25043096f1048e0c9915951adc4f842bd81c6b18155824d6fa99/fonttools-4.63.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:51394295f1a51de8b5f30bdb1e1b9a4231536c7064ef5c6e211eec19fa36036f", size = 4932147, upload-time = "2026-05-14T12:04:14.806Z" }, + { url = "https://files.pythonhosted.org/packages/a1/3f/67f3eac2ffd8a98446c5022f8ed3864eac878a5ff7af8df4c8286dba16cc/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:9e12f105d2b6342c559c298afb674006bb2893afc7102dcf8a1b55b0486b4e40", size = 5027237, upload-time = "2026-05-14T12:04:17.675Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ba/4e6214cb38a7b04779e97bb7636de9a5c7f20af7018d03dee0b64c08510a/fonttools-4.63.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:796f27556dbe094c4824f75ca85267e4df776c79036c8441469a4df37038c196", size = 5053933, upload-time = "2026-05-14T12:04:20.818Z" }, + { url = "https://files.pythonhosted.org/packages/34/3b/214dcc19ee31d3d38fb5ad2755c11ef0514e5dc300bbaf41c0b69f393799/fonttools-4.63.0-cp314-cp314t-win32.whl", hash = "sha256:948428a275741f0b64b113c955425a953314f4b9ab9997f73a72c83e68e569c8", size = 2359326, upload-time = "2026-05-14T12:04:24.22Z" }, + { url = "https://files.pythonhosted.org/packages/dd/1e/3ff1a9b523058c2eeb6a9d50f5574e2a738200d0d94107d5bc4105e8da3f/fonttools-4.63.0-cp314-cp314t-win_amd64.whl", hash = "sha256:6d4741eb179121cab9eea4cb2393d24492373a260d7945006358c08cfbf45419", size = 2425829, upload-time = "2026-05-14T12:04:26.829Z" }, + { url = "https://files.pythonhosted.org/packages/2c/47/c99d5268f354002ce80f8d029cd9d7d872969da1de8b93d32de4dc56d6f4/fonttools-4.63.0-py3-none-any.whl", hash = "sha256:445af2eab030a16b9171ea8bdda7ebf7d96bda2df88ee182a464252f6e05e20d", size = 1164562, upload-time = "2026-05-14T12:04:29.092Z" }, +] + [[package]] name = "frozenlist" version = "1.8.0" @@ -930,6 +1036,73 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/6e/7c/f87c5db042c4f7b4476346fc0d22f1025a777fee08eebde3720d3b9c4eb5/jwcrypto-1.5.8-py3-none-any.whl", hash = "sha256:85aeb475f808d56bbc2f2ed1f6f73e6a317c4011a4321505f02f0aed695a3742", size = 96133, upload-time = "2026-06-24T19:36:49.519Z" }, ] +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, +] + [[package]] name = "kubernetes" version = "36.0.2" @@ -1113,12 +1286,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, ] +[[package]] +name = "matplotlib" +version = "3.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1f/24/080c99d223d158d3a8902769269ab6da5b50f7a0e6e072513907e02b7a6c/matplotlib-3.11.0.tar.gz", hash = "sha256:68c0c7be01b30dcca3638934f7f591df73401235cbdbf0d1ab1c71e7db7f8b57", size = 33251176, upload-time = "2026-06-12T02:29:15.508Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/55/41/aa47f156b061d14c98b906f76c428507397708ec63ff94f410ae1752b426/matplotlib-3.11.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:6ce3b839b34ae1f430b4616893a2945a2999debaa7e94e7e29a2a8bbf286f7b5", size = 9450532, upload-time = "2026-06-12T02:28:06.769Z" }, + { url = "https://files.pythonhosted.org/packages/8c/4f/5a9eb0375e81413953febf8af7b012a6b6357f53438a15c4f5ad86c6bbb5/matplotlib-3.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:373db8f91214e8ccaf35ac833cc1dd59dd961e148bbd55dd027141591dde1313", size = 9279760, upload-time = "2026-06-12T02:28:09.152Z" }, + { url = "https://files.pythonhosted.org/packages/a4/c0/1117d53077e3ac3152503a84e9cf7a5c239576805ee71276e80c2aaa7471/matplotlib-3.11.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:be152b7570324dc8d01574cc9474dd2d803237acf528bcbb5b211fa347461a09", size = 10031623, upload-time = "2026-06-12T02:28:11.26Z" }, + { url = "https://files.pythonhosted.org/packages/92/7e/e937138daffad65b71bf831a377809dcbc830fb4f31a31e067dc1faa2575/matplotlib-3.11.0-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:126f256df600652d7e4b394cf3164ff75210a00038f287c95a012a6f58d0e83f", size = 10839372, upload-time = "2026-06-12T02:28:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/1d/c2/438ecc197ffb8023b6b9922915542f2172f5fd45b76703b0b4fc47322243/matplotlib-3.11.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:03acfeddf87b0dddb11b081ef7740ad445a3ca8bcb6b8e3011b08f2cf802b75c", size = 10924099, upload-time = "2026-06-12T02:28:16.383Z" }, + { url = "https://files.pythonhosted.org/packages/40/2e/395883da416f378b3ed2c9f3e843ac477eae1ce731b671b79adaa6f0bacd/matplotlib-3.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:ab3722f04f3ff34c23b5012c5873d2894174e06c3822fcdac3610965a5ac7d06", size = 9329727, upload-time = "2026-06-12T02:28:18.581Z" }, + { url = "https://files.pythonhosted.org/packages/61/82/2c388956abf8bf392dfb5b8917c502f1082df6a941b781ab8c8e5ba2474b/matplotlib-3.11.0-cp313-cp313-win_arm64.whl", hash = "sha256:c945824670fb8915b4ac879e5e61f3c58e0913022f70a0de4c082b17372f8771", size = 9003506, upload-time = "2026-06-12T02:28:20.474Z" }, + { url = "https://files.pythonhosted.org/packages/c8/c1/34454baa44da7975ada82e9aea37105ec47059514dc967d3be14426ba8dc/matplotlib-3.11.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:3489c3dc487669b4a980bc3068f87856de7a1564248d3f6c629efb2a58b03f24", size = 9499838, upload-time = "2026-06-12T02:28:22.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/c3/98fe79a398cf232219f090163a7fa7e6766e9f2e0ad26df54d6f8934d8ee/matplotlib-3.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:6a98f5476ce784a50ce09998f4ae1e6a9f25043cef8a480c98949902eda74620", size = 9332298, upload-time = "2026-06-12T02:28:24.796Z" }, + { url = "https://files.pythonhosted.org/packages/95/e4/b4b7c33151e74e5c802f3cde1ba807ebfc38401e329b44e215a5888dd76d/matplotlib-3.11.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:565af866fd63e4bd3f987d580afe27c44c2552a3b3305f4ecbb85133601ea6f3", size = 10045491, upload-time = "2026-06-12T02:28:27.141Z" }, + { url = "https://files.pythonhosted.org/packages/71/28/394548efd68354110c1a1be11fe6b6e559e06d1a23da35908a0e316c55a9/matplotlib-3.11.0-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e6b3e64dea5062c570f04358e2711859f3531b459f29516274fbad889079e4f3", size = 10857059, upload-time = "2026-06-12T02:28:29.222Z" }, + { url = "https://files.pythonhosted.org/packages/c8/44/e7922e6e2a4d63bdfbc9dc4a53e3850ab438d46cf42e6779bb15ec92c948/matplotlib-3.11.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:942b37c5db1899610bd1543ce8e13e4ecff9a4633e7f63bb6aa9205d2644ebd1", size = 10939576, upload-time = "2026-06-12T02:28:31.66Z" }, + { url = "https://files.pythonhosted.org/packages/3d/be/b1ca96003a441d619b727fee21d671fdff7a5ce2f1bb797b2521aa2f679a/matplotlib-3.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:c08e649a6313e1291e713623b97a38e5bb4aa580b2a100a94a3309bc6b9c8eb3", size = 9379519, upload-time = "2026-06-12T02:28:33.888Z" }, + { url = "https://files.pythonhosted.org/packages/e3/72/4bf3b91821c34596dd6a7bdac5836d94f744144c8208939ef49d8ec43f7e/matplotlib-3.11.0-cp313-cp313t-win_arm64.whl", hash = "sha256:2746cd2c113742ff6ce37a864c5ac5fd7aa644568f445e66166e457ac78e40e0", size = 9055456, upload-time = "2026-06-12T02:28:35.878Z" }, + { url = "https://files.pythonhosted.org/packages/57/52/a94102ac99eb78e2fe9b826674f9ef9ee23327110ea6ab4776c1b4eb6209/matplotlib-3.11.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:3338e3e3de128cf50d0d2fb92a122815daf9c755bd882a474343c05f8fd7ec79", size = 9452137, upload-time = "2026-06-12T02:28:37.93Z" }, + { url = "https://files.pythonhosted.org/packages/7c/03/b8cdb625a21f710dfa11bbca1f48fb4057d2c0286975f8b415bf80942c99/matplotlib-3.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:25c2e5455efd8d99f41fb79871a31feb7d301569642e332ec58d72cfe9282bc3", size = 9281514, upload-time = "2026-06-12T02:28:40.028Z" }, + { url = "https://files.pythonhosted.org/packages/b7/2d/4e1240ea82ee197dfb3851e71f71c87eeeb975f1753b56a0588e4e80739a/matplotlib-3.11.0-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d9695457a467ff86d23f35037a43deb6f1134dd6d3e2ac8ce1e2087cff09ffb9", size = 10843005, upload-time = "2026-06-12T02:28:42.39Z" }, + { url = "https://files.pythonhosted.org/packages/29/dc/6377ecfaa5fef79430f74a1a16638b4e2aa30d4692bae2c19f9d76fe3b01/matplotlib-3.11.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:19c16c61dea63b3582918503e6b294193961261d9daa806d4ae2151f1ad05430", size = 11127459, upload-time = "2026-06-12T02:28:44.483Z" }, + { url = "https://files.pythonhosted.org/packages/6f/41/795c405aa7560443a3b01309424cde4a1113b85c90b8a63417444a749617/matplotlib-3.11.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2d72ea8b7924f3cb955e61518d21e43b3df1e6c8a793b480a0c1214f185d30ba", size = 10925160, upload-time = "2026-06-12T02:28:46.564Z" }, + { url = "https://files.pythonhosted.org/packages/1a/f7/3a9e6389a7cfaeff76c56e40c2dabcb13110e21e82f837228c834ebe748c/matplotlib-3.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:1c02da0a629dfa9debf52725ea06866b74c1fb70a895bae05e4493d34074f9f2", size = 9485186, upload-time = "2026-06-12T02:28:49.344Z" }, + { url = "https://files.pythonhosted.org/packages/8b/c0/396478ee7cf2091d182db8b4a8695f6a37f1ddb978989cf9dbb84cd5c123/matplotlib-3.11.0-cp314-cp314-win_arm64.whl", hash = "sha256:aa55d73b3117d4b07f959cd9eb6f69b375d8df3414139c479388e551aa5d999d", size = 9160349, upload-time = "2026-06-12T02:28:51.382Z" }, + { url = "https://files.pythonhosted.org/packages/c5/6f/1c3bd51bb2b34eaacdcf3c3d859dbb357f952fc8020c617dc118ad7c9e38/matplotlib-3.11.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:a9d8c6e7cd2f0ddf11d8d92e520dd1d9d2abb0cf6ac8831e338666c81e905847", size = 9500921, upload-time = "2026-06-12T02:28:53.443Z" }, + { url = "https://files.pythonhosted.org/packages/e0/0d/4d861d0121840cb1a3fd4a10deb211efd6fccd481ed23e553f31f4f4da4a/matplotlib-3.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:be050fcf32f729eda99f7f75a80bf67612ce16ab9ac1c23a387dcaede95cb70e", size = 9332190, upload-time = "2026-06-12T02:28:55.623Z" }, + { url = "https://files.pythonhosted.org/packages/4b/cb/22f6bc35711a0b5639a784e74e653e77c86210bd4304449dd399a482f74e/matplotlib-3.11.0-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dfabef0230d0697aa0d717385194dd41162e00207a68bf4abf94c2bf4c27dca0", size = 10854181, upload-time = "2026-06-12T02:28:57.856Z" }, + { url = "https://files.pythonhosted.org/packages/3f/7e/9a9eaca731a2939589da520f0ebe8fd8753d0f51fca98c7d20af6dbe261a/matplotlib-3.11.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1644db30e759199443493ac5e5caec24fdb775a8f6123021f85ba47c4133c3cb", size = 11137715, upload-time = "2026-06-12T02:29:00.555Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f9/9b030b6088354acb0296871bb624b25befc1c42509d3c6cd17420c83a5b8/matplotlib-3.11.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:15b0d160079cb10699a0e98b5989c70677b2df7cacdc62af67c30f2facec46d9", size = 10939427, upload-time = "2026-06-12T02:29:02.527Z" }, + { url = "https://files.pythonhosted.org/packages/59/94/6b273eaee4ee250863567d100865da61a5c1527fa67f527b7ed22e0dd29c/matplotlib-3.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:446307e6b04b57b1f1239e228a1ec2af0d589a1008cebc3dfa3f5441d095cfb6", size = 9535809, upload-time = "2026-06-12T02:29:04.994Z" }, + { url = "https://files.pythonhosted.org/packages/60/95/1d36bddf2b7e2692c1540e78a6e5bc88bc1496b137e3e35a611f91b65ac3/matplotlib-3.11.0-cp314-cp314t-win_arm64.whl", hash = "sha256:652fb5696271d4c50f196d22a5ff4f8e4444c74f847423570d7dc0aa2bbd0159", size = 9209226, upload-time = "2026-06-12T02:29:07.033Z" }, +] + [[package]] name = "monitoring" source = { editable = "." } dependencies = [ { name = "aiohttp" }, { name = "arrow" }, + { name = "asteval" }, { name = "bc-jsonpath-ng" }, { name = "cryptography" }, { name = "deprecation" }, @@ -1139,6 +1360,7 @@ dependencies = [ { name = "loguru" }, { name = "lxml" }, { name = "marko" }, + { name = "matplotlib" }, { name = "numpy" }, { name = "oauthlib" }, { name = "pem" }, @@ -1174,6 +1396,7 @@ dev = [ requires-dist = [ { name = "aiohttp" }, { name = "arrow" }, + { name = "asteval" }, { name = "bc-jsonpath-ng" }, { name = "cryptography" }, { name = "deprecation" }, @@ -1194,6 +1417,7 @@ requires-dist = [ { name = "loguru" }, { name = "lxml" }, { name = "marko" }, + { name = "matplotlib" }, { name = "numpy" }, { name = "oauthlib" }, { name = "pem" }, @@ -1474,6 +1698,68 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c9/97/8299a481ae6c08494b5d53511e6a4746775d8a354c685c69d8796b2ed482/pem-23.1.0-py3-none-any.whl", hash = "sha256:78bbb1e75b737891350cb9499cbba31da5d59545f360f44163c0bc751cad55d3", size = 9195, upload-time = "2023-06-21T10:24:39.164Z" }, ] +[[package]] +name = "pillow" +version = "12.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1c/3d/bb7fca845737cf9d7dbde16ed1843984665ff2e0a518f5db43e77ec540b9/pillow-12.3.0.tar.gz", hash = "sha256:3b8182a766685eaa002637e28b4ec8d6b18819a0c71f579bf0dbaa5830297cce", size = 47025035, upload-time = "2026-07-01T11:56:38.965Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9d/ac/31fb64e1e7efb5a4b50cd3d92049ba89ac6e4d8d3bb6a74e15048ca3353e/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:21900ce7ba264168cd50defae43cd75d25c833ad4ad6e73ffc5596d12e25ac89", size = 4161684, upload-time = "2026-07-01T11:54:25.934Z" }, + { url = "https://files.pythonhosted.org/packages/87/b4/9805e23d2b4d77842b468513841fda254ee42f0289d25088340e4ff46e2d/pillow-12.3.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:4e8c2a84d977f50b9daed6eeaf3baef67d00d5d74d932288f02cb94518ee3ace", size = 4255487, upload-time = "2026-07-01T11:54:27.935Z" }, + { url = "https://files.pythonhosted.org/packages/df/39/ecf519435a200c693fe053a6ee4d835b41cf963a4dfc2551c4e637cb2a71/pillow-12.3.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:ae26d61dfa7a47befdc7572b521024e8745f3d809bd95ca9505a7bba9ef849ec", size = 3696433, upload-time = "2026-07-01T11:54:29.813Z" }, + { url = "https://files.pythonhosted.org/packages/42/92/2fc3ffad878ae8dd5469ec1bc8eb83b71f48e13efdf68f02709003982a32/pillow-12.3.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:7a743ff716f746fc19a9557f60dab1600d4613255f8a7aeb3cdde4db7eb15a66", size = 5345889, upload-time = "2026-07-01T11:54:31.97Z" }, + { url = "https://files.pythonhosted.org/packages/10/76/8803c13605b763d33d156c4678fc77f8443389c0c51c8aef707bb02015f4/pillow-12.3.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d69141514cc30b774ceea5e3ed3a6635c8d8a96edf664689b890f4089111fb35", size = 4780109, upload-time = "2026-07-01T11:54:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/e18aff37cb0b4aac47ac90f016d347a49aca667ef97f190b06ac2aabc928/pillow-12.3.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f7401aebd7f581d7f83a439d87d474999317ee099218e5ad25d125290990ba65", size = 6263736, upload-time = "2026-07-01T11:54:36.131Z" }, + { url = "https://files.pythonhosted.org/packages/f7/62/de5bdd77d935331f4f802edc11e4d82950f642caad6cb2f949837b8560e2/pillow-12.3.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0847a763afefb695bc912d7c131e7e0632d4edc1d8698f58ddabec8e46b8b6d3", size = 6937129, upload-time = "2026-07-01T11:54:38.216Z" }, + { url = "https://files.pythonhosted.org/packages/70/4d/105627a13300c5e0df1d174230b32fd1273062c96f7745fd552b945d1e1d/pillow-12.3.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:571b9fcb07b97ef3a492028fb3d2dc0993ca23a06138b0315286566d29ef718a", size = 6339562, upload-time = "2026-07-01T11:54:40.354Z" }, + { url = "https://files.pythonhosted.org/packages/6b/1d/f13de01a553988ab895ba1c722e06cf3144d4f57656fd5b81b6d881f1179/pillow-12.3.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:756c768d0c9c2955feb7a56c37ea24aea2e369f8d36a88da270b6a9f19e62b5e", size = 7049439, upload-time = "2026-07-01T11:54:42.489Z" }, + { url = "https://files.pythonhosted.org/packages/c9/f9/066794cca041b969964f779ee5fa66a9498bbf34248ac39c5d7954e4198f/pillow-12.3.0-cp313-cp313-win32.whl", hash = "sha256:a876864214e136f0eb367788dbd7df045f4806801518e2cfe9e13229cfe06d8f", size = 6473287, upload-time = "2026-07-01T11:54:44.9Z" }, + { url = "https://files.pythonhosted.org/packages/a6/9b/7a58e61d62be561da3a356fe2384d4059a6345fc130e23ef1c36a5b81d24/pillow-12.3.0-cp313-cp313-win_amd64.whl", hash = "sha256:1cca606cd25738df4ed873d5ad46bbdb3d83b5cbca291f6b4ff13a4df6b0bbe8", size = 7239691, upload-time = "2026-07-01T11:54:47.141Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b0/c4ed4f0ef8f8fa5ee8351537db6650bb8189f7e118842978dd6589065692/pillow-12.3.0-cp313-cp313-win_arm64.whl", hash = "sha256:b629de27fda84b42cde7edef0d85f13b958b47f6e9bbcbba9b673c562a89bd8b", size = 2568185, upload-time = "2026-07-01T11:54:49.137Z" }, + { url = "https://files.pythonhosted.org/packages/dc/01/001f65b68192f0228cc1dbbc8d2530ab5d58b61037ba0587f946fea607cd/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:9cf95fe4d0f84c82d282745d9bb08ad9f926efa00be4697e767b814ce40d4330", size = 4161736, upload-time = "2026-07-01T11:54:51.156Z" }, + { url = "https://files.pythonhosted.org/packages/1a/d2/0219746d0fd16fc8a84498e79452375be3797d3ce4044596ce565164b84f/pillow-12.3.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:8728f216dcdb6e6d555cf971cb34076139ad74b31fc2c14da4fafc741c5f6217", size = 4255435, upload-time = "2026-07-01T11:54:53.414Z" }, + { url = "https://files.pythonhosted.org/packages/c8/02/8d0bc62ef0302318c46ff2a512822d2610e81c7aa46c9b3abe6cbaca5ad0/pillow-12.3.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:a45650e8ce7fafffd731db8550230db6b0d306d181a90b67d3e6bca2f1990930", size = 3696262, upload-time = "2026-07-01T11:54:55.739Z" }, + { url = "https://files.pythonhosted.org/packages/85/e2/73c77d218410b14f5f2d565e8a998d5317b7b9c75368d29985139f7a46f0/pillow-12.3.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ba54cfebe86920a559a7c4d6b9050791c20513650a1952ebe3368c7dc70306f8", size = 5350344, upload-time = "2026-07-01T11:54:57.657Z" }, + { url = "https://files.pythonhosted.org/packages/c7/da/32c752228ae345f489e3a42499d817b6c3996da7e8a3bc7a04fc806b243b/pillow-12.3.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e158cb00350dc278f3b91551101aa7d12415a66ebf2c91d8d5ac14e56ddd3ad0", size = 4780131, upload-time = "2026-07-01T11:54:59.713Z" }, + { url = "https://files.pythonhosted.org/packages/b1/9d/8b2c807dbef61a5197c047afe99823787eb66f63daf9fb2432f91d6f0462/pillow-12.3.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e9aeb04d6aef139de265b29683e119b638208f88cf73cdd1658aa07221165321", size = 6263757, upload-time = "2026-07-01T11:55:01.778Z" }, + { url = "https://files.pythonhosted.org/packages/5c/44/c85361f65dbe00eea8576ee467c768d25129989efb76e94f205e9ca9bb46/pillow-12.3.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:251bf95b67017e27b13d82f5b326234ca62d70f9cf4c2b9032de2358a3b12c7b", size = 6936962, upload-time = "2026-07-01T11:55:03.93Z" }, + { url = "https://files.pythonhosted.org/packages/18/7e/e483414b35800b86b6f08dbbc7803fb5cd52c4d6f897f47d53ea2c7e6f65/pillow-12.3.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fe3cca2e4e8a592be0f269a1ca4835c25199d9f3ce815c8491048f785b0a0198", size = 6339171, upload-time = "2026-07-01T11:55:05.989Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f4/68c491844841ede6bed70189546b3ee9731cf9f2cbad396faff5e1ccba45/pillow-12.3.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:23aceaa007d6172b02c277f0cd359c79492bbb14f7072b4ede9fbcaf20648130", size = 7048116, upload-time = "2026-07-01T11:55:08.131Z" }, + { url = "https://files.pythonhosted.org/packages/a3/34/77f3f793fed8efc7d243f21b33c5a3f0d1c97ee70346d3db855587e155ff/pillow-12.3.0-cp314-cp314-win32.whl", hash = "sha256:af8d94b0db561cf68b88a267c5c44b49e134f525d0dc2cb7ed413a66bc23559a", size = 6467209, upload-time = "2026-07-01T11:55:10.408Z" }, + { url = "https://files.pythonhosted.org/packages/f1/e0/492879f69d94f91f60fc8cd05ba03650e9520afebb2fb7aa12777d7c7f38/pillow-12.3.0-cp314-cp314-win_amd64.whl", hash = "sha256:fdafc9cce40277e0f7a0feabce0ee50dd2fa1800f3b38015e51296b5e814048d", size = 7237707, upload-time = "2026-07-01T11:55:12.745Z" }, + { url = "https://files.pythonhosted.org/packages/c9/ac/6b11f2875f1c2ac040d84e1bbf9cf22a88038f901ca1037898b280b38365/pillow-12.3.0-cp314-cp314-win_arm64.whl", hash = "sha256:e91206ee562682b51b98ef4b26a6ef48fd84e15fd4c4bc5ec768eb641d206838", size = 2565995, upload-time = "2026-07-01T11:55:14.736Z" }, + { url = "https://files.pythonhosted.org/packages/52/69/c2208e56af9bfc1913afb24020297a691eb1d4ef688474c8a04913f65e04/pillow-12.3.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:164b31cd1a0490ab6efae01aa5df49da7061be0af1b30e035b6e9a1bfe34ee6e", size = 5352503, upload-time = "2026-07-01T11:55:17.076Z" }, + { url = "https://files.pythonhosted.org/packages/07/70/e5686d753e898a45d778ff1718dba8516ead6ab6b95d85fc8c4b70650cf2/pillow-12.3.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5afb51d599ea772b8365ae807ae557f18bccfe46ab261fd1c2a9ed700fc6eb17", size = 4782956, upload-time = "2026-07-01T11:55:19.448Z" }, + { url = "https://files.pythonhosted.org/packages/d5/37/25c6692f06927ee973ff18c8d9ee98ad0b4d84ee67a09610c2dd1447958e/pillow-12.3.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3edce1d53195db527e0191f84b71d02022de0540bf43a16ed734ed7537b07385", size = 6322855, upload-time = "2026-07-01T11:55:21.613Z" }, + { url = "https://files.pythonhosted.org/packages/cc/91/420637fcb8f1bc11029e403b4538e6694744428d8246118e45719f944556/pillow-12.3.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bf16ba1b4d0b6b7c8e534936632270cf70eb00dbe09005bc345b2677b726855c", size = 6989642, upload-time = "2026-07-01T11:55:24.006Z" }, + { url = "https://files.pythonhosted.org/packages/10/08/b94d7811281ccf0d143a1cf768d1c49e1e54af63e7b708ab2ee3eb87face/pillow-12.3.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:24870b09b224f7ae3c39ed07d10e819d06f8720bc551847b1d623832b5b0e28d", size = 6391281, upload-time = "2026-07-01T11:55:26.252Z" }, + { url = "https://files.pythonhosted.org/packages/d2/87/24233f785f55474dc02ce3e739c5528a77e3a862e9333d1dd7a25cc31f70/pillow-12.3.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:30f2aa603c41533cc25c05acd0da21636e84a315768feb631c937177db558931", size = 7096716, upload-time = "2026-07-01T11:55:28.318Z" }, + { url = "https://files.pythonhosted.org/packages/23/26/fcb2f6e37175b04f53570b59937867e2b80ee1685e744023153028fc14f9/pillow-12.3.0-cp314-cp314t-win32.whl", hash = "sha256:4b0a7fe987b14c31ebda6083f74f22b561fd3739bc0ac51e019622e3d72668c7", size = 6474125, upload-time = "2026-07-01T11:55:30.956Z" }, + { url = "https://files.pythonhosted.org/packages/90/de/3634abee5f1c9e13c56787b7d5517b0ba8d6de51700b95578cf338349c9f/pillow-12.3.0-cp314-cp314t-win_amd64.whl", hash = "sha256:962864dc93511324d51ddbb5b9f8731bf71675b93ca612a07441896f4688fb8c", size = 7242939, upload-time = "2026-07-01T11:55:34.044Z" }, + { url = "https://files.pythonhosted.org/packages/ce/2a/fd13f8eb24de5714a6eb444a3d67e2842c6c576e159a43793adf23051351/pillow-12.3.0-cp314-cp314t-win_arm64.whl", hash = "sha256:0740a512dc522224c77d9aa5a8d70d8b7d73fb91f2c21125d8d025d3b8990e45", size = 2567506, upload-time = "2026-07-01T11:55:35.988Z" }, + { url = "https://files.pythonhosted.org/packages/5d/dc/8fdce34ec725a33c81c6ba122b904d6b9024e50ea9ac7bede62fab54506c/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphoneos.whl", hash = "sha256:0feb2e9d6ad6c9e3c06effe9d00f3f1e618a6643273576b016f591e9315a7139", size = 4162063, upload-time = "2026-07-01T11:55:37.941Z" }, + { url = "https://files.pythonhosted.org/packages/76/66/2044b9a63d3b84ff048228dfcb7cd9bf0df983e8470971bf7d4c57b693de/pillow-12.3.0-cp315-cp315-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:9e881fca225083806662a5c43d627d215f258ff43c890f831966c7d7ba9c7402", size = 4255549, upload-time = "2026-07-01T11:55:40.022Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/1f67e6f4ece6b582ee4b539decbcc9f848dc245a93ed8cd7338bafef72f1/pillow-12.3.0-cp315-cp315-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:4998562bf62a445225f22e07c896bb04b35b1b1f2eb6d760584c9c51d7a5f78c", size = 3696331, upload-time = "2026-07-01T11:55:41.98Z" }, + { url = "https://files.pythonhosted.org/packages/12/40/d306fc2c8e4d45d7f175c77edca7063be7b86fe7fe6e68f4353bf71d808c/pillow-12.3.0-cp315-cp315-macosx_10_15_x86_64.whl", hash = "sha256:dc624f6bc473dacdf7ef7eb8678d0d08edf15cd94fad6ae5c7d6cc67a4e4902f", size = 5350370, upload-time = "2026-07-01T11:55:44.028Z" }, + { url = "https://files.pythonhosted.org/packages/dd/44/668fb1437e8ce420f62d6106eb66e44a5971602a4d794615bdf79315d82d/pillow-12.3.0-cp315-cp315-macosx_11_0_arm64.whl", hash = "sha256:71d6097b330eea8fd15097780c8e89cb1a8ce7838669f48c5bacd6f663dd4701", size = 4780147, upload-time = "2026-07-01T11:55:46.073Z" }, + { url = "https://files.pythonhosted.org/packages/0c/08/93fa2e70e30a2d81547e481b6ee2bb9522117221fb1e0ce4b5df70967677/pillow-12.3.0-cp315-cp315-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:28ce87c5ab450a9dd970b52e5aca5fe63ed432d18a2eaddd1979a00a1ba24ace", size = 6273659, upload-time = "2026-07-01T11:55:48.264Z" }, + { url = "https://files.pythonhosted.org/packages/f8/6d/043e96ff814fc31a33077e4cba86082167db520c93632afdf2042febbb0c/pillow-12.3.0-cp315-cp315-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6b02afb9b97f65fbca5f31db6a2a3ba21aa93030225f150fa3f249717e938fb4", size = 6947439, upload-time = "2026-07-01T11:55:50.503Z" }, + { url = "https://files.pythonhosted.org/packages/af/92/ba71d2ee2ac0edf3fa33bd9d5ee9ee080da70b1766f3ca3934f9938ddac9/pillow-12.3.0-cp315-cp315-musllinux_1_2_aarch64.whl", hash = "sha256:1182d52bc2d5e5d7d0949503aa7e36d12f42205dc287e4883f407b1988820d39", size = 6353577, upload-time = "2026-07-01T11:55:52.697Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ce/e63064e2122923ff687c8ad792d0d736a7b3920a56a46982e81a7fdd25d6/pillow-12.3.0-cp315-cp315-musllinux_1_2_x86_64.whl", hash = "sha256:e795b7eb908249c4e43c7c99fac7c2c75dab0c43566e37db472a355f63693d71", size = 7060394, upload-time = "2026-07-01T11:55:55.149Z" }, + { url = "https://files.pythonhosted.org/packages/54/76/a09cc3ccc8d773a7283d34c38bec1708f9e3cc932093cbc4c5e71ac4060b/pillow-12.3.0-cp315-cp315-win32.whl", hash = "sha256:57b3d78c95ba9059768b10e28b813002261d3f3dfc55cc48b0c988f625175827", size = 6467375, upload-time = "2026-07-01T11:55:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/3e/03/1846c49ba3b1d5550392a4bbd06d6fb4578e1cd91a803198b5c90f5f7d53/pillow-12.3.0-cp315-cp315-win_amd64.whl", hash = "sha256:fa4ecea169a355be7a3ade2c783e2ed12f0e40d2c5621cda8b3297faf7fbb9f5", size = 7237048, upload-time = "2026-07-01T11:55:59.975Z" }, + { url = "https://files.pythonhosted.org/packages/fb/bb/89f35dcc79610423f9f195504d7def7f0d1416a711541b42867e25fe3412/pillow-12.3.0-cp315-cp315-win_arm64.whl", hash = "sha256:877c3f311ff35410f690861c4409e7ccbf0cd2f878e50628a28e5a0bb689e658", size = 2566006, upload-time = "2026-07-01T11:56:02.143Z" }, + { url = "https://files.pythonhosted.org/packages/30/88/707027ba09942dfa2c28759b5c222d769290a41c6d20ea60ec250801941f/pillow-12.3.0-cp315-cp315t-macosx_10_15_x86_64.whl", hash = "sha256:e9871b1ffbfa9656b60aeee92ed5136a5742696006fa322b29ea3d8da0ecc9cf", size = 5352509, upload-time = "2026-07-01T11:56:04.2Z" }, + { url = "https://files.pythonhosted.org/packages/b0/6d/00352fa25332c2569cd387851f568cc5a4b75a9adbfb37ac4fbce4c02eec/pillow-12.3.0-cp315-cp315t-macosx_11_0_arm64.whl", hash = "sha256:53aa02d20d10c3d814d536aa4e5ac9b84ca0ff5a88377963b085ad6822f93e64", size = 4783167, upload-time = "2026-07-01T11:56:06.631Z" }, + { url = "https://files.pythonhosted.org/packages/13/4f/9e049dfa21af7c22427275720e2490267ba8138120add5c4c574deb69782/pillow-12.3.0-cp315-cp315t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:446c34dcc4324b084a53b705127dc15717b22c5e140ae0a3c38349d4efec071e", size = 6329237, upload-time = "2026-07-01T11:56:08.868Z" }, + { url = "https://files.pythonhosted.org/packages/36/16/cf6eeaae8d0fce8dd390a33437cf68c5d5bd73834a2bc6e2f14efda0ab45/pillow-12.3.0-cp315-cp315t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:cf1845d02ad822a369a49f2bb9345b1614744267682e7a03527dc3bf6eea1777", size = 6997047, upload-time = "2026-07-01T11:56:11.379Z" }, + { url = "https://files.pythonhosted.org/packages/1e/69/dbf769bdd55f48bf5733cac28edc6364ffaa072ec9ba336266e4fe66be55/pillow-12.3.0-cp315-cp315t-musllinux_1_2_aarch64.whl", hash = "sha256:186941b6aef820ad110fb01fb06eb925374dc3a21b17e37ec9a53b250c6fe2d1", size = 6400440, upload-time = "2026-07-01T11:56:13.908Z" }, + { url = "https://files.pythonhosted.org/packages/a0/e1/ffc9cfc2eea0d178da8018e18e959301ad9d6bc9f3edb7181e748a474b97/pillow-12.3.0-cp315-cp315t-musllinux_1_2_x86_64.whl", hash = "sha256:f13c32a3abd6079a66d9526e18dad9b6d280384d49d7c54040cd57b6424041d9", size = 7105895, upload-time = "2026-07-01T11:56:16.575Z" }, + { url = "https://files.pythonhosted.org/packages/18/f0/a5595c1e8c3ae44b9828cb2f0fa8155e5095ef04d6327b8f61cf44a3df85/pillow-12.3.0-cp315-cp315t-win32.whl", hash = "sha256:1657923d2d45afb66526e5b933e5b3052e6bdea196c90d3abb2424e18c77dae8", size = 6474384, upload-time = "2026-07-01T11:56:18.855Z" }, + { url = "https://files.pythonhosted.org/packages/e4/04/62bcd9f844984c5938d3b05264a61d797a29d3e0812341a8204af70bbdee/pillow-12.3.0-cp315-cp315t-win_amd64.whl", hash = "sha256:8cd2f7bdda092d99c9fc2fb7391354f306d01443d22785d0cbfafa2e2c8bb418", size = 7243537, upload-time = "2026-07-01T11:56:21.214Z" }, + { url = "https://files.pythonhosted.org/packages/3d/68/1f3066acedf37673694a7141381d8f811ae97f30d34413d236abe7d489f1/pillow-12.3.0-cp315-cp315t-win_arm64.whl", hash = "sha256:06ff022112bc9cbf83b60f8e028d94ad87b60621706487e65f673de61610ab59", size = 2567491, upload-time = "2026-07-01T11:56:23.506Z" }, +] + [[package]] name = "pip" version = "26.1.1" @@ -1750,6 +2036,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/18/1dd71c9b43192ab83f1d531ad6002dc81108ac36c475f79fb7a295abe2f4/pyopenssl-26.3.0-py3-none-any.whl", hash = "sha256:46367f8f66b92271e6d218da9c87607e1ef5a0bc5c8dea5bb3db82f395c385a3", size = 56008, upload-time = "2026-06-12T20:28:05.999Z" }, ] +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pyproj" version = "3.7.2"