diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..6cd2d96 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,13 @@ +repos: + - repo: https://github.com/astral-sh/ruff-pre-commit + rev: v0.9.0 + hooks: + - id: ruff + args: [ --fix ] + - id: ruff-format + + - repo: https://github.com/pre-commit/mirrors-mypy + rev: v1.14.0 + hooks: + - id: mypy + additional_dependencies: [pydantic>=2.13.0] diff --git a/Makefile b/Makefile index 47aee0b..6a8aedf 100644 --- a/Makefile +++ b/Makefile @@ -2,7 +2,25 @@ IMAGE_NAME = extractor-agent PORT = 3000 -.PHONY: build run stop clean test start shell logs +.PHONY: build run stop clean test start shell logs lint format lint-fix typecheck install-hooks + +# Pre-commit hooks +install-hooks: + uv run pre-commit install + +# Linting and Formatting +lint: + uv run ruff check . + +format: + uv run ruff format . + +lint-fix: + uv run ruff check --fix . + uv run ruff format . + +typecheck: + uv run mypy . # Build and run the container start: build run diff --git a/README.md b/README.md index b99bea7..0687f03 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,35 @@ docker run -p 3000:3000 -t govuk-ai-graph-tools-app --- +--- + +## Development and Code Quality + +### 1. Manual Checks +You can run the full suite of checks using the `Makefile`: + +```bash +# Run all checks +make lint && make format && make typecheck + +# Run individual checks +make lint +make format +make typecheck +``` + +### 2. Pre-commit Hooks +The project is configured with `pre-commit` to automatically run these checks before every `git commit`. + +To install the hooks in your local repository: +```bash +make install-hooks +``` + +Once installed, your code will be automatically linted and type-checked whenever you commit. If you need to skip the hooks (e.g., for an urgent WIP commit), you can use `git commit --no-verify`. + +--- + ## Tests ```bash diff --git a/app.py b/app.py index 924b00c..010095c 100644 --- a/app.py +++ b/app.py @@ -1,25 +1,27 @@ import asyncio -import fsspec -import json import logging import os -import re import time -import uuid + from asgiref.wsgi import WsgiToAsgi from dotenv import load_dotenv -from flask import Flask, request, jsonify, render_template -from src.visualiser_graph_generator import generate_graph, generate_output_path -from src.visualiser_graph_loader import load_json_file, extract_path_parts, visualiser_graph_file_path +from flask import Flask, jsonify, render_template, request +from werkzeug.exceptions import BadRequest + from src.utils import ( - update_job_status, - read_job_status, - get_job_id_for_path, - get_active_job_status, background_run_extraction, - resume_interrupted_jobs + get_active_job_status, + get_job_id_for_path, + read_job_status, + resume_interrupted_jobs, + update_job_status, +) +from src.visualiser_graph_generator import generate_output_path +from src.visualiser_graph_loader import ( + extract_path_parts, + load_json_file, + visualiser_graph_file_path, ) -from werkzeug.exceptions import BadRequest load_dotenv() @@ -27,93 +29,102 @@ # Configure logging logging.basicConfig( level=logging.INFO, - format='%(asctime)s - %(name)s - %(levelname)s - %(message)s', - handlers=[ - logging.StreamHandler() - ] + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[logging.StreamHandler()], ) logger = logging.getLogger(__name__) + def create_app(): app = Flask(__name__) - @app.route('/graph', methods=['GET']) + @app.route("/graph", methods=["GET"]) def graph_page(): """Serve the Cytoscape graph viewer page.""" - source_path_param = request.args.get('source_path') + source_path_param = request.args.get("source_path") # Validate the source_path format if source_path_param: extract_path_parts(source_path_param) - return render_template('graph.html', source_path=source_path_param or '') + return render_template("graph.html", source_path=source_path_param or "") - @app.route('/graph-viewmodel', methods=['GET']) + @app.route("/graph-viewmodel", methods=["GET"]) async def graph_viewmodel(): """Serve the graph data as JSON for the frontend.""" try: - source_path_param = request.args.get('source_path') - + source_path_param = request.args.get("source_path") + graph_filepath = visualiser_graph_file_path(source_path_param) graph_data = load_json_file(graph_filepath) - logger.info('Graph data loaded successfully.') + logger.info("Graph data loaded successfully.") return jsonify(graph_data), 200 except Exception as e: app.logger.error(f"Error loading graph data: {str(e)}") return jsonify({"error": "Error loading graph data."}), 500 - @app.route('/healthcheck/ready', methods=['GET']) + @app.route("/healthcheck/ready", methods=["GET"]) def health_check(): """Simple health check endpoint.""" return "Application OK", 200 - @app.route('/extract', methods=['GET']) + @app.route("/extract", methods=["GET"]) async def extract_quotes(): """ Endpoint that runs the Cytoscape graph generation logic based on graph.json. """ try: - source_path = request.args.get('source_path') + source_path = request.args.get("source_path") if not source_path: return jsonify({"error": "Missing 'source_path' query parameter"}), 400 input_path, output_path = generate_output_path(source_path) job_id = get_job_id_for_path(source_path) - + active_status = get_active_job_status(job_id) if active_status: - logger.info(f"Duplicate request for {source_path}. Job {job_id} is already in progress.") - return jsonify({ - 'job_id': job_id, - 'status': 'already_running', - 'message': f'A graph generation job is already in progress for {source_path}', - 'output_path': output_path - }), 202 + logger.info( + f"Duplicate request for {source_path}. Job {job_id} is already in progress." + ) + return jsonify( + { + "job_id": job_id, + "status": "already_running", + "message": ( + f"A graph generation job is already in progress for {source_path}" + ), + "output_path": output_path, + } + ), 202 initial_status = { "job_id": job_id, "status": "pending", "source_path": source_path, - "created_at": time.time() + "created_at": time.time(), } update_job_status(job_id, initial_status) - asyncio.create_task(background_run_extraction(job_id, input_path, output_path, initial_status)) + asyncio.create_task( + background_run_extraction(job_id, input_path, output_path, initial_status) + ) - return jsonify({ - 'job_id': job_id, - 'status': 'accepted', - 'message': f'Graph generation started in background for {source_path}', - 'output_path': output_path - }), 202 + return jsonify( + { + "job_id": job_id, + "status": "accepted", + "message": f"Graph generation started in background for {source_path}", + "output_path": output_path, + } + ), 202 except Exception as e: app.logger.error(f"Error starting background task: {str(e)}") return jsonify({"error": str(e)}), 500 - @app.route('/status/', methods=['GET']) + @app.route("/status/", methods=["GET"]) def get_status(job_id): """Check the status of a background job from S3.""" status_info = read_job_status(job_id) @@ -127,8 +138,10 @@ def handle_bad_request(e): return app + class LifespanMiddleware: """ASGI middleware to handle startup and shutdown events.""" + def __init__(self, app): self.app = app @@ -146,14 +159,17 @@ async def __call__(self, scope, receive, send): return return await self.app(scope, receive, send) + def create_asgi_app(): flask_app = create_app() asgi_app = WsgiToAsgi(flask_app) return LifespanMiddleware(asgi_app) + if __name__ == "__main__": asgi_app = create_asgi_app() import uvicorn + port = int(os.getenv("PORT", 3000)) logger.info(f"Starting Uvicorn server on port {port}...") - uvicorn.run(asgi_app, host='0.0.0.0', port=port) + uvicorn.run(asgi_app, host="0.0.0.0", port=port) diff --git a/pyproject.toml b/pyproject.toml index db4eec3..7ec0102 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,4 +22,34 @@ dev = [ "pytest>=9.0.3", "pytest-asyncio>=1.3.0", "pytest-mock>=3.15.1", + "ruff>=0.9.0", + "mypy>=1.14.0", + "pre-commit>=4.0.0", ] + +[tool.mypy] +plugins = ["pydantic.mypy"] +follow_imports = "silent" +warn_redundant_casts = true +warn_unused_ignores = true +disallow_any_generics = false +check_untyped_defs = true +no_implicit_reexport = true +ignore_missing_imports = true + +[tool.pydantic-mypy] +init_forbid_extra = true +init_typed = true +warn_required_dynamic_aliases = true + +[tool.ruff] +line-length = 100 +target-version = "py312" + +[tool.ruff.lint] +select = ["E", "F", "I", "W"] +ignore = [] + +[tool.ruff.lint.isort] +combine-as-imports = true +lines-after-imports = 2 diff --git a/src/content_extractor/base.py b/src/content_extractor/base.py index 8fcb160..41bf48e 100644 --- a/src/content_extractor/base.py +++ b/src/content_extractor/base.py @@ -1,56 +1,75 @@ -import os -import boto3 import json -from typing import List, Dict, Optional, Any +import os from dataclasses import dataclass, field from pprint import pprint -from pydantic import BaseModel, Field, RootModel +from typing import Dict, List, Optional + +import boto3 +from pydantic import BaseModel, Field from pydantic_ai import Agent from pydantic_ai.models.bedrock import BedrockConverseModel from pydantic_ai.providers.bedrock import BedrockProvider + # --- Shared Models --- + class AgentQuote(BaseModel): """Simple quote model for the agent to return.""" + content: str = Field(description="The exact sentence or phrase found in the document.") keyword_matched: str = Field(description="The keyword or phrase that triggered this match.") + class AgentQuoteExtraction(BaseModel): """Collection of quotes returned by the agent for a single document.""" + quotes: List[AgentQuote] + class Finding(BaseModel): """A single unique finding within a keyword group.""" + content: str source_documents: List[str] -class FinalQuoteExtraction(BaseModel): # Changed to BaseModel as RootModel is often unnecessary for simple dict roots + +class FinalQuoteExtraction( + BaseModel +): # Changed to BaseModel as RootModel is often unnecessary for simple dict roots """The final collection of quotes as a dictionary of keyword -> list of findings.""" + root: Dict[str, List[Finding]] + # --- Shared Configuration --- + @dataclass class BaseExtractorConfig: keywords: List[str] s3_documents: List[str] model_id: str = "eu.anthropic.claude-sonnet-4-6" - region: str = field(default_factory=lambda: os.getenv("AWS_REGION", os.getenv("AWS_DEFAULT_REGION", "eu-west-2"))) + region: str = field( + default_factory=lambda: os.getenv( + "AWS_REGION", os.getenv("AWS_DEFAULT_REGION", "eu-west-2") + ) + ) chunk_max_chars: int = 6000 secret_id: Optional[str] = None + # --- Base Extractor Class --- + class BaseQuoteExtractor: def __init__(self, config: BaseExtractorConfig): self.config = config self.s3_client = boto3.client("s3", region_name=self.config.region) - + # Initialize Bedrock Agent model = BedrockConverseModel( - self.config.model_id, - provider=BedrockProvider(region_name=self.config.region) + self.config.model_id, provider=BedrockProvider(region_name=self.config.region) ) self.agent = Agent( model, @@ -62,16 +81,18 @@ def __init__(self, config: BaseExtractorConfig): "For each match, return:\n" "1. the EXACT sentence as 'content' (do not paraphrase)\n" "2. the keyword that was matched as 'keyword_matched'\n\n" - "If a keyword appears multiple times in different sentences, extract each unique sentence. " + "If a keyword appears multiple times in different sentences, " + "extract each unique sentence. " "If no matches are found, return an empty list of quotes.\n" - """IMPORTANT: The source is a Markdown file. You MUST return 'content' that matches the RENDERED text, not the raw source. " - Apply these cleaning rules to every extracted quote: - - Remove Markdown link syntax: change [link text](url) to just link text. - - Strip formatting: remove all **, *, __, _, and ` symbols. - - Strip list markers: remove leading '* ', '- ', '+ ', or '1. ' types of bullet points. - - Strip headers: remove leading '#' symbols.""" - - ) + "IMPORTANT: The source is a Markdown file. You MUST return 'content' that " + "matches the RENDERED text, not the raw source. " + "Apply these cleaning rules to every extracted quote:\n" + "- Remove Markdown link syntax: change [link text](url) to just link text.\n" + "- Strip formatting: remove all **, *, __, _, and ` symbols.\n" + "- Strip list markers: remove leading '* ', '- ', '+ ', or '1. ' " + "types of bullet points.\n" + "- Strip headers: remove leading '#' symbols." + ), ) def get_aws_secret(self, secret_id: str) -> dict: @@ -92,7 +113,7 @@ def fetch_s3_content(self, s3_uri: str) -> str: try: if not s3_uri.startswith("s3://"): raise ValueError(f"Invalid S3 URI: {s3_uri}") - + parts = s3_uri.replace("s3://", "").split("/", 1) bucket, key = parts response = self.s3_client.get_object(Bucket=bucket, Key=key) @@ -105,11 +126,12 @@ def chunk_content(self, text: str) -> List[str]: """Splits text into chunks respecting paragraph boundaries.""" if not text: return [] - + paragraphs = text.split("\n\n") - chunks, current_chunk = [], [] + chunks: List[str] = [] + current_chunk: List[str] = [] current_length = 0 - + for para in paragraphs: para_len = len(para) if current_length + para_len > self.config.chunk_max_chars and current_chunk: @@ -117,8 +139,8 @@ def chunk_content(self, text: str) -> List[str]: current_chunk, current_length = [para], para_len else: current_chunk.append(para) - current_length += para_len + 2 # +2 for \n\n separators - + current_length += para_len + 2 # +2 for \n\n separators + if current_chunk: chunks.append("\n\n".join(current_chunk)) return chunks diff --git a/src/content_extractor/highlighter.py b/src/content_extractor/highlighter.py index 2007b72..49f1386 100644 --- a/src/content_extractor/highlighter.py +++ b/src/content_extractor/highlighter.py @@ -1,13 +1,14 @@ import re + def highlight_occurrence(text: str, keyword: str) -> str: if not keyword or not text: return text - + escaped_keyword = re.escape(keyword) - + pattern = re.compile(f"({escaped_keyword})", re.IGNORECASE) - + highlighted = pattern.sub(r'\1', text, count=1) - + return highlighted diff --git a/src/content_extractor/opensearch.py b/src/content_extractor/opensearch.py index 496f156..2450573 100644 --- a/src/content_extractor/opensearch.py +++ b/src/content_extractor/opensearch.py @@ -1,13 +1,15 @@ -import asyncio -from typing import List from dataclasses import dataclass -from .base import BaseQuoteExtractor, BaseExtractorConfig + +from .base import BaseExtractorConfig, BaseQuoteExtractor + # --- Configuration --- + @dataclass class OpenSearchConfig(BaseExtractorConfig): """Placeholder for OpenSearch specialized configuration.""" + index_name: str = "document_chunks" @@ -17,5 +19,7 @@ def __init__(self, config: OpenSearchConfig): super().__init__(config) self.config = config - async def run(self, perform_indexing: bool = False, output_file: str = "outputs/extracted_quotes_os.json"): + async def run( + self, perform_indexing: bool = False, output_file: str = "outputs/extracted_quotes_os.json" + ): return None diff --git a/src/content_extractor/s3_sequential.py b/src/content_extractor/s3_sequential.py index fb09284..2bc448d 100644 --- a/src/content_extractor/s3_sequential.py +++ b/src/content_extractor/s3_sequential.py @@ -1,17 +1,25 @@ import asyncio -import logging import json -from typing import List, Dict, Any +import logging from collections import defaultdict -from .base import BaseQuoteExtractor, Finding, FinalQuoteExtraction, BaseExtractorConfig +from typing import Any, Dict, List, Optional, Set + from src.url.generator import generate_url_fragement, s3_to_govuk_url +from .base import ( + BaseExtractorConfig, + BaseQuoteExtractor, + FinalQuoteExtraction, + Finding, +) + + logger = logging.getLogger(__name__) class S3QuoteExtractor(BaseQuoteExtractor): """Processes documents sequentially by fetching from S3 and chunking.""" - + def __init__(self, config: BaseExtractorConfig): super().__init__(config) self.url_map: Dict[str, str] = {} @@ -28,7 +36,7 @@ def _fetch_url_map(self, s3_uris: List[str]): for uri in s3_uris: if uri in self.url_map: continue - + if "/input/" in uri: sources_uri = uri.split("/input/")[0] + "/input/sources.json" else: @@ -37,7 +45,7 @@ def _fetch_url_map(self, s3_uris: List[str]): for sources_uri in sources_locations: logger.info(f"Attempting to fetch sources map from {sources_uri}...") - content = self.fetch_s3_content(sources_uri) + content: Optional[str] = self.fetch_s3_content(sources_uri) if content: try: new_map = json.loads(content) @@ -47,7 +55,7 @@ def _fetch_url_map(self, s3_uris: List[str]): logger.error(f"Failed to parse {sources_uri}: {e}") else: logger.warning(f"No sources.json found at {sources_uri}.") - + if self.url_map: logger.info(f"Total URL mappings loaded: {len(self.url_map)}") else: @@ -56,7 +64,8 @@ def _fetch_url_map(self, s3_uris: List[str]): async def process_document(self, s3_uri: str, keywords: List[str], results_list: list): """Processes a single document for a specific set of keywords.""" content = self.fetch_s3_content(s3_uri) - if not content: return + if not content: + return chunks = self.chunk_content(content) if len(chunks) > 1: @@ -66,29 +75,30 @@ async def process_document(self, s3_uri: str, keywords: List[str], results_list: for i, chunk in enumerate(chunks, 1): prompt = ( - f"Keywords: {', '.join(keywords)}\n\n" - f"Content (Chunk {i}/{len(chunks)}):\n{chunk}" + f"Keywords: {', '.join(keywords)}\n\nContent (Chunk {i}/{len(chunks)}):\n{chunk}" ) try: result = await self.agent.run(prompt) for q in result.output.quotes: - results_list.append({ - "content": q.content, - "keyword_matched": q.keyword_matched, - "source": s3_uri, - "link": generate_url_fragement(base_govuk_url, q.content) - }) + results_list.append( + { + "content": q.content, + "keyword_matched": q.keyword_matched, + "source": s3_uri, + "link": generate_url_fragement(base_govuk_url, q.content), + } + ) except Exception as e: logger.error(f" Error in {s3_uri} chunk {i}: {e}") async def run_mapping(self, doc_to_keywords: Dict[str, List[str]]): """Processes documents based on a mapping of {s3_uri: [keywords]}.""" - raw_findings = [] - + raw_findings: List[Dict[str, Any]] = [] + self._fetch_url_map(list(doc_to_keywords.keys())) - + tasks = [ - self.process_document(uri, keywords, raw_findings) + self.process_document(uri, keywords, raw_findings) for uri, keywords in doc_to_keywords.items() ] await asyncio.gather(*tasks) @@ -99,15 +109,17 @@ async def run(self): doc_to_keywords = {uri: self.config.keywords for uri in self.config.s3_documents} raw_findings = await self.run_mapping(doc_to_keywords) - keyword_map = defaultdict(lambda: defaultdict(set)) + keyword_map: Dict[str, Dict[str, Set[str]]] = defaultdict(lambda: defaultdict(set)) for f in raw_findings: keyword_map[f["keyword_matched"]][f["content"]].add(f["source"]) final_data = { - kw: [Finding(content=txt, source_documents=sorted(list(srcs))) - for txt, srcs in content_map.items()] + kw: [ + Finding(content=txt, source_documents=sorted(list(srcs))) + for txt, srcs in content_map.items() + ] for kw, content_map in keyword_map.items() } extraction = FinalQuoteExtraction(root=final_data) - return extraction \ No newline at end of file + return extraction diff --git a/src/models/graph_models.py b/src/models/graph_models.py index c6d15c7..475e8f1 100644 --- a/src/models/graph_models.py +++ b/src/models/graph_models.py @@ -1,10 +1,13 @@ -from pydantic import BaseModel, Field, ConfigDict -from typing import List, Optional, Dict, Union, Any, Literal +from typing import Any, Dict, List, Literal, Optional + +from pydantic import BaseModel, ConfigDict, Field + class Alias(BaseModel): name: str source_files: List[str] = Field(default_factory=list) + class Entity(BaseModel): id: str canonical_key: str @@ -16,32 +19,39 @@ class Entity(BaseModel): model_config = ConfigDict(extra="allow") + class GraphInput(BaseModel): entities: List[Entity] - + model_config = ConfigDict(extra="allow") + class Occurrence(BaseModel): link: str context: str + class NodeData(BaseModel): id: str label: str type: Literal["entity", "alias"] occurrences: Optional[List[Occurrence]] = None + class Node(BaseModel): data: NodeData + class EdgeData(BaseModel): source: str target: str label: str + class Edge(BaseModel): data: EdgeData + class GraphOutput(BaseModel): nodes: List[Node] edges: List[Edge] diff --git a/src/url/generator.py b/src/url/generator.py index 92a21ce..6cf106f 100644 --- a/src/url/generator.py +++ b/src/url/generator.py @@ -1,22 +1,24 @@ import urllib.parse from typing import Optional -def convert_string_to_url_query_format(text: str)-> str: - quoted = urllib.parse.quote(text, safe='') - quoted= (quoted - .replace('-', '%2D') - .replace('.', '%2E') - .replace('~', '%7E') - .replace('_', '%5F')) + +def convert_string_to_url_query_format(text: str) -> str: + quoted = urllib.parse.quote(text, safe="") + quoted = quoted.replace("-", "%2D").replace(".", "%2E").replace("~", "%7E").replace("_", "%5F") return quoted + def generate_url_fragement(base_url: str, content: str): encoded_content = convert_string_to_url_query_format(content) url = f"{base_url}#:~:text={encoded_content}" return url + def s3_to_govuk_url(s3_uri: str, url_map: Optional[dict] = None) -> str: - """Derives a GOV.UK URL from an S3 URI, using url_map if provided, otherwise using fallback logic.""" + """ + Derives a GOV.UK URL from an S3 URI, using url_map if provided, + otherwise using fallback logic. + """ if url_map and s3_uri in url_map: return url_map[s3_uri] @@ -24,8 +26,6 @@ def s3_to_govuk_url(s3_uri: str, url_map: Optional[dict] = None) -> str: path = s3_uri.split("/input/")[-1] else: path = s3_uri.split("/")[-1] - + path = path.replace(".md", "") return f"https://www.gov.uk/{path}" - - diff --git a/src/utils/__init__.py b/src/utils/__init__.py index a6e051a..c66bc8c 100644 --- a/src/utils/__init__.py +++ b/src/utils/__init__.py @@ -1,9 +1,20 @@ from .job_tracker import ( - update_job_status, - read_job_status, - get_status_path, - get_job_id_for_path, - get_active_job_status, background_run_extraction, - resume_interrupted_jobs + get_active_job_status, + get_job_id_for_path, + get_status_path, + read_job_status, + resume_interrupted_jobs, + update_job_status, ) + + +__all__ = [ + "background_run_extraction", + "get_active_job_status", + "get_job_id_for_path", + "get_status_path", + "read_job_status", + "resume_interrupted_jobs", + "update_job_status", +] diff --git a/src/utils/job_tracker.py b/src/utils/job_tracker.py index c2b107a..ae4f6fe 100644 --- a/src/utils/job_tracker.py +++ b/src/utils/job_tracker.py @@ -1,20 +1,25 @@ +import asyncio +import hashlib import json import logging -import fsspec -import hashlib import time -import asyncio from typing import Optional + +import fsspec + from src.visualiser_graph_generator import generate_graph, generate_output_path + logger = logging.getLogger(__name__) STATUS_ROOT = "s3://govuk-ai-accelerator-data-integration/graph_tools/job_statuses" + def get_status_path(job_id: str) -> str: """Returns the S3 path for a given job ID.""" return f"{STATUS_ROOT}/{job_id}.json" + def update_job_status(job_id: str, status_data: dict): """Updates the job status in S3.""" path = get_status_path(job_id) @@ -26,7 +31,8 @@ def update_job_status(job_id: str, status_data: dict): except Exception as e: logger.error(f"Failed to update job status in S3 for job {job_id}: {str(e)}") -def read_job_status(job_id: str) -> dict| None: + +def read_job_status(job_id: str) -> dict | None: """Reads the job status from S3.""" path = get_status_path(job_id) fs = fsspec.filesystem("s3") @@ -43,72 +49,78 @@ def read_job_status(job_id: str) -> dict| None: logger.error(f"Failed to read job status from S3 for job {job_id}: {str(e)}") return None + def get_job_id_for_path(source_path: str) -> str: """Generates a predictable job_id based on a hash of the source_path.""" return hashlib.sha256(source_path.encode()).hexdigest() + def get_active_job_status(job_id: str, timeout_hours: int = 24) -> Optional[dict]: """ - Returns the job status ONLY if it is currently active (pending/running) + Returns the job status ONLY if it is currently active (pending/running) and has not exceeded the timeout. Returns None if the job is stale or not active. """ status = read_job_status(job_id) if not status: return None - + if status.get("status") in ["pending", "running"]: created_at = status.get("created_at", 0) is_stale = (time.time() - created_at) > (timeout_hours * 3600) - + if not is_stale: return status - + return None + async def background_run_extraction(job_id: str, input_path: str, output_path: str, status: dict): """Background task for graph generation and status tracking.""" try: - logger.info(f'Starting background graph generation for {input_path} (Job: {job_id})...') + logger.info(f"Starting background graph generation for {input_path} (Job: {job_id})...") status["status"] = "running" update_job_status(job_id, status) - + await generate_graph(input_path, output_path) - + status["status"] = "completed" status["output_path"] = output_path status["completed_at"] = time.time() update_job_status(job_id, status) - logger.info(f'Graph generation completed successfully for {output_path}') + logger.info(f"Graph generation completed successfully for {output_path}") except Exception as e: logger.error(f"Background graph generation failed for job {job_id}: {str(e)}") status["status"] = "failed" status["error"] = str(e) update_job_status(job_id, status) + async def resume_interrupted_jobs(): """Scans for jobs stuck in 'running' state and restarts them if they are fresh (<24h).""" logger.info("Scanning for interrupted jobs to resume...") fs = fsspec.filesystem("s3") - + try: # List all status files status_files = fs.glob(f"{STATUS_ROOT}/*.json") - + for file_path in status_files: job_id = file_path.split("/")[-1].replace(".json", "") status = read_job_status(job_id) - + if status and status.get("status") == "running": created_at = status.get("created_at", 0) is_fresh = (time.time() - created_at) < (24 * 3600) - + if is_fresh: source_path = status.get("source_path") if source_path: try: input_path, output_path = generate_output_path(source_path) logger.info(f"Resuming interrupted job {job_id} for {source_path}") - asyncio.create_task(background_run_extraction(job_id, input_path, output_path, status)) + asyncio.create_task( + background_run_extraction(job_id, input_path, output_path, status) + ) except Exception as e: logger.error(f"Failed to prepare resumption for job {job_id}: {str(e)}") else: diff --git a/src/visualiser_graph_generator.py b/src/visualiser_graph_generator.py index 0110c2a..f5949d6 100644 --- a/src/visualiser_graph_generator.py +++ b/src/visualiser_graph_generator.py @@ -1,31 +1,41 @@ -import json import asyncio -import os -import re +import json import logging -from typing import List, Dict, Any, Set, Tuple, Optional, Union +import re from collections import defaultdict -from src.content_extractor.s3_sequential import S3QuoteExtractor +from typing import Any, Dict, List, Optional, Tuple, Union + +import fsspec + from src.content_extractor.base import BaseExtractorConfig from src.content_extractor.highlighter import highlight_occurrence -import fsspec +from src.content_extractor.s3_sequential import S3QuoteExtractor from src.models.graph_models import ( - GraphInput, GraphOutput, Node, NodeData, Edge, EdgeData, Occurrence, Entity + Edge, + EdgeData, + Entity, + GraphInput, + GraphOutput, + Node, + NodeData, + Occurrence, ) logger = logging.getLogger(__name__) + def slugify(text: str) -> str: """Simple slugify for node IDs.""" text = text.lower() - text = re.sub(r'[^a-z0-9]+', '_', text) - return text.strip('_') + text = re.sub(r"[^a-z0-9]+", "_", text) + return text.strip("_") + def build_registries(entities: List[Entity]) -> Dict[str, Any]: """Parses entities to map s3_uris to keywords and metadata based on structured aliases.""" - registry = defaultdict(lambda: {"keywords": set(), "entities": []}) - + registry: Dict[str, Dict[str, Any]] = defaultdict(lambda: {"keywords": set(), "entities": []}) + for ent in entities: for alias in ent.aliases: for uri in alias.source_files: @@ -35,20 +45,19 @@ def build_registries(entities: List[Entity]) -> Dict[str, Any]: # Ensure each entity is only added once per unique URI if ent not in registry[uri]["entities"]: registry[uri]["entities"].append(ent) - + return registry + async def fetch_extraction_findings(registry: Dict[str, Any]) -> List[Dict[str, Any]]: """Runs the extractor over unique S3 documents.""" config = BaseExtractorConfig(keywords=[], s3_documents=[]) extractor = S3QuoteExtractor(config) - doc_to_keywords = { - uri: list(data["keywords"]) - for uri, data in registry.items() - if data["keywords"] + doc_to_keywords: Dict[str, List[str]] = { + uri: list(data["keywords"]) for uri, data in registry.items() if data["keywords"] } - + if not doc_to_keywords: logger.warning("No documents or aliases found to extract.") return [] @@ -56,26 +65,30 @@ async def fetch_extraction_findings(registry: Dict[str, Any]) -> List[Dict[str, logger.info(f"Starting extraction for {len(doc_to_keywords)} documents...") return await extractor.run_mapping(doc_to_keywords) -def map_findings_to_entities(raw_findings: List[Dict[str, Any]], registry: Dict[str, Any]) -> Dict[str, Any]: + +def map_findings_to_entities( + raw_findings: List[Dict[str, Any]], registry: Dict[str, Any] +) -> Dict[str, Dict[str, List[Occurrence]]]: """Groups findings by entity and alias with highlighting and links.""" - results = defaultdict(lambda: defaultdict(list)) - + results: Dict[str, Dict[str, List[Occurrence]]] = defaultdict(lambda: defaultdict(list)) + for finding in raw_findings: uri = finding["source"] keyword = finding["keyword_matched"] content = finding["content"] - link = finding["link"] - - for ent in registry[uri]["entities"]: - if any(a.name == keyword for a in ent.aliases): - occurrence = Occurrence( - link=link, - context=highlight_occurrence(content, keyword) - ) - results[ent.canonical_key][keyword].append(occurrence) - + link = finding["link"] + + if uri in registry: + for ent in registry[uri]["entities"]: + if any(a.name == keyword for a in ent.aliases): + occurrence = Occurrence( + link=link, context=highlight_occurrence(content, keyword) + ) + results[ent.canonical_key][keyword].append(occurrence) + return results + def build_node_structure(entities: List[Entity], entity_results: Dict[str, Any]) -> GraphOutput: """Constructs the final list of nodes and edges.""" nodes, edges = [], [] @@ -84,45 +97,46 @@ def build_node_structure(entities: List[Entity], entity_results: Dict[str, Any]) ent_id = ent.canonical_key human_label = ent.label or ent_id.replace("_", " ").title() nodes.append(Node(data=NodeData(id=ent_id, label=human_label, type="entity"))) - + # Use a dict to accumulate alias nodes by their slugified ID to avoid duplicates alias_map = {} - + for alias_obj in ent.aliases: alias = alias_obj.name occurrences = entity_results[ent_id].get(alias, []) alias_id = f"{ent_id}__{slugify(alias)}" - + if alias_id not in alias_map: alias_map[alias_id] = NodeData( - id=alias_id, - label=alias, - type="alias", - occurrences=[] + id=alias_id, label=alias, type="alias", occurrences=[] ) - - if occurrences: - alias_map[alias_id].occurrences.extend(occurrences) - + + occ = alias_map[alias_id].occurrences + if occurrences and occ is not None: + occ.extend(occurrences) + # Add the deduplicated alias nodes and their edges for alias_id, node_data in alias_map.items(): # If no occurrences, clear the list (Pydantic will handle Optional) if not node_data.occurrences: node_data.occurrences = None - + nodes.append(Node(data=node_data)) - + count = len(node_data.occurrences) if node_data.occurrences else 0 - edges.append(Edge( - data=EdgeData( - source=ent_id, - target=alias_id, - label=f"Alias ({count})" if count > 0 else "Alias" + edges.append( + Edge( + data=EdgeData( + source=ent_id, + target=alias_id, + label=f"Alias ({count})" if count > 0 else "Alias", + ) ) - )) + ) return GraphOutput(nodes=nodes, edges=edges) + async def generate_graph(input_data: Union[str, Dict[str, Any]], output_path: Optional[str] = None): """Main orchestration function. Can take a file path (str) or a dictionary.""" if isinstance(input_data, str): @@ -134,7 +148,7 @@ async def generate_graph(input_data: Union[str, Dict[str, Any]], output_path: Op raise else: graph_data = input_data - + # Validate input try: validated_input = GraphInput.model_validate(graph_data) @@ -144,38 +158,37 @@ async def generate_graph(input_data: Union[str, Dict[str, Any]], output_path: Op raise registry = build_registries(entities) - + raw_findings = await fetch_extraction_findings(registry) entity_results = map_findings_to_entities(raw_findings, registry) - + cy_graph = build_node_structure(entities, entity_results) cy_json = cy_graph.model_dump(exclude_none=True) - + if output_path: with fsspec.open(output_path, "w", auto_mkdir=True) as f: json.dump(cy_json, f, indent=4) logger.info(f"Graph saved to {output_path}") - - return cy_json + return cy_json def generate_output_path(source_path: str) -> Tuple[str, str]: """Generates the output path for the graph JSON file.""" - - #TODO: make input from user be relative without the bucketname applied - match = re.search(r'(?P[^/]+)/(?Prun-\d+-\d+)', source_path) - s3_bucket_uri = 's3://govuk-ai-accelerator-data-integration' + + # TODO: make input from user be relative without the bucketname applied + match = re.search(r"(?P[^/]+)/(?Prun-\d+-\d+)", source_path) + s3_bucket_uri = "s3://govuk-ai-accelerator-data-integration" if match: - domain_name = match.group('domain_name') - run_id = match.group('run') - output_path =f"{s3_bucket_uri}/graph_tools/{domain_name}/{run_id}/graphNode.json" - input_path= f"{s3_bucket_uri}/{source_path}" + domain_name = match.group("domain_name") + run_id = match.group("run") + output_path = f"{s3_bucket_uri}/graph_tools/{domain_name}/{run_id}/graphNode.json" + input_path = f"{s3_bucket_uri}/{source_path}" return input_path, output_path else: logger.error(f"Invalid input path: {source_path}") raise ValueError(f"Invalid input path: {source_path}") - - + + if __name__ == "__main__": asyncio.run(generate_graph("graph.json", "outputs/graphNode.json")) diff --git a/src/visualiser_graph_loader.py b/src/visualiser_graph_loader.py index f98efde..957ed12 100644 --- a/src/visualiser_graph_loader.py +++ b/src/visualiser_graph_loader.py @@ -1,20 +1,23 @@ -import fsspec import json import logging -import os import re -from typing import Dict, Any +from typing import Any, Dict + +import fsspec from werkzeug.exceptions import BadRequest + logger = logging.getLogger(__name__) -ONTOLOGY_RUN_PATH_PATTERN = r'(?P[a-zA-Z0-9_-]+)/(?Prun-\d+-\d+)' +ONTOLOGY_RUN_PATH_PATTERN = r"(?P[a-zA-Z0-9_-]+)/(?Prun-\d+-\d+)" S3_BUCKET_NAME = "govuk-ai-accelerator-data-integration" + def load_json_file(file_path: str) -> Dict[str, Any]: with fsspec.open(file_path, "r") as f: return json.load(f) + def visualiser_graph_file_path(source_path: str | None) -> str: if source_path: domain_name, run_id = extract_path_parts(source_path) @@ -22,15 +25,16 @@ def visualiser_graph_file_path(source_path: str | None) -> str: logger.info(f"Loading graph data from: '{filename}'") else: filename = "graph-viewmodel.json" - logger.info('Loading default example graph data for viewmodel endpoint...') + logger.info("Loading default example graph data for viewmodel endpoint...") return filename + def extract_path_parts(path: str) -> tuple[str, str]: match = re.fullmatch(ONTOLOGY_RUN_PATH_PATTERN, path) if not match: logger.warning(f"Invalid 'source_path' format: '{path}'") raise BadRequest(f"Invalid 'source_path' format: '{path}'") - domain_name = match.group('domain_name') - run_id = match.group('run') + domain_name = match.group("domain_name") + run_id = match.group("run") return domain_name, run_id diff --git a/tests/test_graph_validation.py b/tests/test_graph_validation.py index 5377dcf..fc0f838 100644 --- a/tests/test_graph_validation.py +++ b/tests/test_graph_validation.py @@ -1,7 +1,9 @@ import pytest + from src.models.graph_models import GraphInput, GraphOutput from src.visualiser_graph_generator import slugify + def test_graph_input_validation(): data = { "entities": [ @@ -10,7 +12,7 @@ def test_graph_input_validation(): "canonical_key": "test_entity", "label": "Test Entity", "aliases": [{"name": "alias1"}, {"name": "alias2"}], - "properties": {"sourceUrls": "s3://bucket/key"} + "properties": {"sourceUrls": "s3://bucket/key"}, } ] } @@ -18,43 +20,30 @@ def test_graph_input_validation(): assert len(validated.entities) == 1 assert validated.entities[0].id == "e1" + def test_graph_input_invalid(): data = { "entities": [ { "id": "e1", # missing canonical_key - "label": "Test Entity" + "label": "Test Entity", } ] } with pytest.raises(Exception): GraphInput.model_validate(data) + def test_slugify(): assert slugify("Hello World!") == "hello_world" assert slugify("Test-123") == "test_123" + def test_graph_output_validation(): data = { - "nodes": [ - { - "data": { - "id": "e1", - "label": "Entity 1", - "type": "entity" - } - } - ], - "edges": [ - { - "data": { - "source": "e1", - "target": "a1", - "label": "Alias" - } - } - ] + "nodes": [{"data": {"id": "e1", "label": "Entity 1", "type": "entity"}}], + "edges": [{"data": {"source": "e1", "target": "a1", "label": "Alias"}}], } validated = GraphOutput.model_validate(data) assert len(validated.nodes) == 1 diff --git a/tests/test_routes.py b/tests/test_routes.py index 354e41c..cc38306 100644 --- a/tests/test_routes.py +++ b/tests/test_routes.py @@ -1,9 +1,9 @@ -import pytest import importlib -import sys + from werkzeug.test import Client from werkzeug.wrappers import Response + def _app_module(): return importlib.import_module("app") @@ -11,9 +11,10 @@ def _app_module(): def _client(): return Client(_app_module().create_app(), Response) + def test_healthcheck_ready_route(): response = _client().get("/healthcheck/ready") assert response.status_code == 200 body = response.get_data(as_text=True) - assert 'Application OK' in body + assert "Application OK" in body diff --git a/uv.lock b/uv.lock index b978467..c2c6e5b 100644 --- a/uv.lock +++ b/uv.lock @@ -1,6 +1,10 @@ version = 1 revision = 3 requires-python = ">=3.12" +resolution-markers = [ + "python_full_version >= '3.15'", + "python_full_version < '3.15'", +] [[package]] name = "aiobotocore" @@ -222,6 +226,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/9a/3c/c17fb3ca2d9c3acff52e30b309f538586f9f5b9c9cf454f3845fc9af4881/certifi-2026.2.25-py3-none-any.whl", hash = "sha256:027692e4402ad994f1c42e52a4997a9763c646b73e4096e4d5d6db8af1d6f0fa", size = 153684, upload-time = "2026-02-25T02:54:15.766Z" }, ] +[[package]] +name = "cfgv" +version = "3.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/4e/b5/721b8799b04bf9afe054a3899c6cf4e880fcf8563cc71c15610242490a0c/cfgv-3.5.0.tar.gz", hash = "sha256:d5b1034354820651caa73ede66a6294d6e95c1b00acc5e9b098e917404669132", size = 7334, upload-time = "2025-11-19T20:55:51.612Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/db/3c/33bac158f8ab7f89b2e59426d5fe2e4f63f7ed25df84c036890172b412b5/cfgv-3.5.0-py2.py3-none-any.whl", hash = "sha256:a8dc6b26ad22ff227d2634a65cb388215ce6cc96bbcc5cfde7641ae87e8dacc0", size = 7445, upload-time = "2025-11-19T20:55:50.744Z" }, +] + [[package]] name = "charset-normalizer" version = "3.4.7" @@ -316,6 +329,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "distlib" +version = "0.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/96/8e/709914eb2b5749865801041647dc7f4e6d00b549cfe88b65ca192995f07c/distlib-0.4.0.tar.gz", hash = "sha256:feec40075be03a04501a973d81f633735b4b69f98b05450592310c0f401a4e0d", size = 614605, upload-time = "2025-07-17T16:52:00.465Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/33/6b/e0547afaf41bf2c42e52430072fa5658766e3d65bd4b03a563d1b6336f57/distlib-0.4.0-py2.py3-none-any.whl", hash = "sha256:9659f7d87e46584a30b5780e43ac7a2143098441670ff0a49d5f9034c54a6c16", size = 469047, upload-time = "2025-07-17T16:51:58.613Z" }, +] + [[package]] name = "events" version = "0.5" @@ -343,9 +365,12 @@ dependencies = [ [package.dev-dependencies] dev = [ + { name = "mypy" }, + { name = "pre-commit" }, { name = "pytest" }, { name = "pytest-asyncio" }, { name = "pytest-mock" }, + { name = "ruff" }, ] [package.metadata] @@ -364,9 +389,21 @@ requires-dist = [ [package.metadata.requires-dev] dev = [ + { name = "mypy", specifier = ">=1.14.0" }, + { name = "pre-commit", specifier = ">=4.0.0" }, { name = "pytest", specifier = ">=9.0.3" }, { name = "pytest-asyncio", specifier = ">=1.3.0" }, { name = "pytest-mock", specifier = ">=3.15.1" }, + { name = "ruff", specifier = ">=0.9.0" }, +] + +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, ] [[package]] @@ -589,6 +626,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "identify" +version = "2.6.19" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/52/63/51723b5f116cc04b061cb6f5a561790abf249d25931d515cd375e063e0f4/identify-2.6.19.tar.gz", hash = "sha256:6be5020c38fcb07da56c53733538a3081ea5aa70d36a156f83044bfbf9173842", size = 99567, upload-time = "2026-04-17T18:39:50.265Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/84/d9273cd09688070a6523c4aee4663a8538721b2b755c4962aafae0011e72/identify-2.6.19-py2.py3-none-any.whl", hash = "sha256:20e6a87f786f768c092a721ad107fc9df0eb89347be9396cadf3f4abbd1fb78a", size = 99397, upload-time = "2026-04-17T18:39:49.221Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -649,6 +695,66 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/14/2f/967ba146e6d58cf6a652da73885f52fc68001525b4197effc174321d70b4/jmespath-1.1.0-py3-none-any.whl", hash = "sha256:a5663118de4908c91729bea0acadca56526eb2698e83de10cd116ae0f4e97c64", size = 20419, upload-time = "2026-01-22T16:35:24.919Z" }, ] +[[package]] +name = "librt" +version = "0.9.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/eb/6b/3d5c13fb3e3c4f43206c8f9dfed13778c2ed4f000bacaa0b7ce3c402a265/librt-0.9.0.tar.gz", hash = "sha256:a0951822531e7aee6e0dfb556b30d5ee36bbe234faf60c20a16c01be3530869d", size = 184368, upload-time = "2026-04-09T16:06:26.173Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/bf/90/89ddba8e1c20b0922783cd93ed8e64f34dc05ab59c38a9c7e313632e20ff/librt-0.9.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:9b3e3bc363f71bda1639a4ee593cb78f7fbfeacc73411ec0d4c92f00730010a4", size = 68332, upload-time = "2026-04-09T16:05:00.09Z" }, + { url = "https://files.pythonhosted.org/packages/a8/40/7aa4da1fb08bdeeb540cb07bfc8207cb32c5c41642f2594dbd0098a0662d/librt-0.9.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0a09c2f5869649101738653a9b7ab70cf045a1105ac66cbb8f4055e61df78f2d", size = 70581, upload-time = "2026-04-09T16:05:01.213Z" }, + { url = "https://files.pythonhosted.org/packages/48/ac/73a2187e1031041e93b7e3a25aae37aa6f13b838c550f7e0f06f66766212/librt-0.9.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:5ca8e133d799c948db2ab1afc081c333a825b5540475164726dcbf73537e5c2f", size = 203984, upload-time = "2026-04-09T16:05:02.542Z" }, + { url = "https://files.pythonhosted.org/packages/5e/3d/23460d571e9cbddb405b017681df04c142fb1b04cbfce77c54b08e28b108/librt-0.9.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:603138ee838ee1583f1b960b62d5d0007845c5c423feb68e44648b1359014e27", size = 215762, upload-time = "2026-04-09T16:05:04.127Z" }, + { url = "https://files.pythonhosted.org/packages/de/1e/42dc7f8ab63e65b20640d058e63e97fd3e482c1edbda3570d813b4d0b927/librt-0.9.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4003f70c56a5addd6aa0897f200dd59afd3bf7bcd5b3cce46dd21f925743bc2", size = 230288, upload-time = "2026-04-09T16:05:05.883Z" }, + { url = "https://files.pythonhosted.org/packages/dc/08/ca812b6d8259ad9ece703397f8ad5c03af5b5fedfce64279693d3ce4087c/librt-0.9.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:78042f6facfd98ecb25e9829c7e37cce23363d9d7c83bc5f72702c5059eb082b", size = 224103, upload-time = "2026-04-09T16:05:07.148Z" }, + { url = "https://files.pythonhosted.org/packages/b6/3f/620490fb2fa66ffd44e7f900254bc110ebec8dac6c1b7514d64662570e6f/librt-0.9.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a361c9434a64d70a7dbb771d1de302c0cc9f13c0bffe1cf7e642152814b35265", size = 232122, upload-time = "2026-04-09T16:05:08.386Z" }, + { url = "https://files.pythonhosted.org/packages/e9/83/12864700a1b6a8be458cf5d05db209b0d8e94ae281e7ec261dbe616597b4/librt-0.9.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:dd2c7e082b0b92e1baa4da28163a808672485617bc855cc22a2fd06978fa9084", size = 225045, upload-time = "2026-04-09T16:05:09.707Z" }, + { url = "https://files.pythonhosted.org/packages/fd/1b/845d339c29dc7dbc87a2e992a1ba8d28d25d0e0372f9a0a2ecebde298186/librt-0.9.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:7e6274fd33fc5b2a14d41c9119629d3ff395849d8bcbc80cf637d9e8d2034da8", size = 227372, upload-time = "2026-04-09T16:05:10.942Z" }, + { url = "https://files.pythonhosted.org/packages/8d/fe/277985610269d926a64c606f761d58d3db67b956dbbf40024921e95e7fcb/librt-0.9.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5093043afb226ecfa1400120d1ebd4442b4f99977783e4f4f7248879009b227f", size = 248224, upload-time = "2026-04-09T16:05:12.254Z" }, + { url = "https://files.pythonhosted.org/packages/92/1b/ee486d244b8de6b8b5dbaefabe6bfdd4a72e08f6353edf7d16d27114da8d/librt-0.9.0-cp312-cp312-win32.whl", hash = "sha256:9edcc35d1cae9fd5320171b1a838c7da8a5c968af31e82ecc3dff30b4be0957f", size = 55986, upload-time = "2026-04-09T16:05:13.529Z" }, + { url = "https://files.pythonhosted.org/packages/89/7a/ba1737012308c17dc6d5516143b5dce9a2c7ba3474afd54e11f44a4d1ef3/librt-0.9.0-cp312-cp312-win_amd64.whl", hash = "sha256:3cc2917258e131ae5f958a4d872e07555b51cb7466a43433218061c74ef33745", size = 63260, upload-time = "2026-04-09T16:05:14.68Z" }, + { url = "https://files.pythonhosted.org/packages/36/e4/01752c113da15127f18f7bf11142f5640038f062407a611c059d0036c6aa/librt-0.9.0-cp312-cp312-win_arm64.whl", hash = "sha256:90e6d5420fc8a300518d4d2288154ff45005e920425c22cbbfe8330f3f754bd9", size = 53694, upload-time = "2026-04-09T16:05:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/5f/d7/1b3e26fffde1452d82f5666164858a81c26ebe808e7ae8c9c88628981540/librt-0.9.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f29b68cd9714531672db62cc54f6e8ff981900f824d13fa0e00749189e13778e", size = 68367, upload-time = "2026-04-09T16:05:17.243Z" }, + { url = "https://files.pythonhosted.org/packages/a5/5b/c61b043ad2e091fbe1f2d35d14795e545d0b56b03edaa390fa1dcee3d160/librt-0.9.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7d5c8a5929ac325729f6119802070b561f4db793dffc45e9ac750992a4ed4d22", size = 70595, upload-time = "2026-04-09T16:05:18.471Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/2448471196d8a73370aa2f23445455dc42712c21404081fcd7a03b9e0749/librt-0.9.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:756775d25ec8345b837ab52effee3ad2f3b2dfd6bbee3e3f029c517bd5d8f05a", size = 204354, upload-time = "2026-04-09T16:05:19.593Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5e/39fc4b153c78cfd2c8a2dcb32700f2d41d2312aa1050513183be4540930d/librt-0.9.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b8f5d00b49818f4e2b1667db994488b045835e0ac16fe2f924f3871bd2b8ac5", size = 216238, upload-time = "2026-04-09T16:05:20.868Z" }, + { url = "https://files.pythonhosted.org/packages/d7/42/bc2d02d0fa7badfa63aa8d6dcd8793a9f7ef5a94396801684a51ed8d8287/librt-0.9.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c81aef782380f0f13ead670aae01825eb653b44b046aa0e5ebbb79f76ed4aa11", size = 230589, upload-time = "2026-04-09T16:05:22.305Z" }, + { url = "https://files.pythonhosted.org/packages/c8/7b/e2d95cc513866373692aa5edf98080d5602dd07cabfb9e5d2f70df2f25f7/librt-0.9.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:66b58fed90a545328e80d575467244de3741e088c1af928f0b489ebec3ef3858", size = 224610, upload-time = "2026-04-09T16:05:23.647Z" }, + { url = "https://files.pythonhosted.org/packages/31/d5/6cec4607e998eaba57564d06a1295c21b0a0c8de76e4e74d699e627bd98c/librt-0.9.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:e78fb7419e07d98c2af4b8567b72b3eaf8cb05caad642e9963465569c8b2d87e", size = 232558, upload-time = "2026-04-09T16:05:25.025Z" }, + { url = "https://files.pythonhosted.org/packages/95/8c/27f1d8d3aaf079d3eb26439bf0b32f1482340c3552e324f7db9dca858671/librt-0.9.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:2c3786f0f4490a5cd87f1ed6cefae833ad6b1060d52044ce0434a2e85893afd0", size = 225521, upload-time = "2026-04-09T16:05:26.311Z" }, + { url = "https://files.pythonhosted.org/packages/6b/d8/1e0d43b1c329b416017619469b3c3801a25a6a4ef4a1c68332aeaa6f72ca/librt-0.9.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:8494cfc61e03542f2d381e71804990b3931175a29b9278fdb4a5459948778dc2", size = 227789, upload-time = "2026-04-09T16:05:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/2c/b4/d3d842e88610fcd4c8eec7067b0c23ef2d7d3bff31496eded6a83b0f99be/librt-0.9.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:07cf11f769831186eeac424376e6189f20ace4f7263e2134bdb9757340d84d4d", size = 248616, upload-time = "2026-04-09T16:05:29.181Z" }, + { url = "https://files.pythonhosted.org/packages/ec/28/527df8ad0d1eb6c8bdfa82fc190f1f7c4cca5a1b6d7b36aeabf95b52d74d/librt-0.9.0-cp313-cp313-win32.whl", hash = "sha256:850d6d03177e52700af605fd60db7f37dcb89782049a149674d1a9649c2138fd", size = 56039, upload-time = "2026-04-09T16:05:30.709Z" }, + { url = "https://files.pythonhosted.org/packages/f3/a7/413652ad0d92273ee5e30c000fc494b361171177c83e57c060ecd3c21538/librt-0.9.0-cp313-cp313-win_amd64.whl", hash = "sha256:a5af136bfba820d592f86c67affcef9b3ff4d4360ac3255e341e964489b48519", size = 63264, upload-time = "2026-04-09T16:05:31.881Z" }, + { url = "https://files.pythonhosted.org/packages/a4/0a/92c244309b774e290ddb15e93363846ae7aa753d9586b8aad511c5e6145b/librt-0.9.0-cp313-cp313-win_arm64.whl", hash = "sha256:4c4d0440a3a8e31d962340c3e1cc3fc9ee7febd34c8d8f770d06adb947779ea5", size = 53728, upload-time = "2026-04-09T16:05:33.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c1/184e539543f06ea2912f4b92a5ffaede4f9b392689e3f00acbf8134bee92/librt-0.9.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:3f05d145df35dca5056a8bc3838e940efebd893a54b3e19b2dda39ceaa299bcb", size = 67830, upload-time = "2026-04-09T16:05:34.517Z" }, + { url = "https://files.pythonhosted.org/packages/f3/ad/23399bdcb7afca819acacdef31b37ee59de261bd66b503a7995c03c4b0dc/librt-0.9.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1c587494461ebd42229d0f1739f3aa34237dd9980623ecf1be8d3bcba79f4499", size = 70280, upload-time = "2026-04-09T16:05:35.649Z" }, + { url = "https://files.pythonhosted.org/packages/9f/0b/4542dc5a2b8772dbf92cafb9194701230157e73c14b017b6961a23598b03/librt-0.9.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:b0a2040f801406b93657a70b72fa12311063a319fee72ce98e1524da7200171f", size = 201925, upload-time = "2026-04-09T16:05:36.739Z" }, + { url = "https://files.pythonhosted.org/packages/31/d4/8ee7358b08fd0cfce051ef96695380f09b3c2c11b77c9bfbc367c921cce5/librt-0.9.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f38bc489037eca88d6ebefc9c4d41a4e07c8e8b4de5188a9e6d290273ad7ebb1", size = 212381, upload-time = "2026-04-09T16:05:38.043Z" }, + { url = "https://files.pythonhosted.org/packages/f2/94/a2025fe442abedf8b038038dab3dba942009ad42b38ea064a1a9e6094241/librt-0.9.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3fd278f5e6bf7c75ccd6d12344eb686cc020712683363b66f46ac79d37c799f", size = 227065, upload-time = "2026-04-09T16:05:39.394Z" }, + { url = "https://files.pythonhosted.org/packages/7c/e9/b9fcf6afa909f957cfbbf918802f9dada1bd5d3c1da43d722fd6a310dc3f/librt-0.9.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fcbdf2a9ca24e87bbebb47f1fe34e531ef06f104f98c9ccfc953a3f3344c567a", size = 221333, upload-time = "2026-04-09T16:05:40.999Z" }, + { url = "https://files.pythonhosted.org/packages/ac/7c/ba54cd6aa6a3c8cd12757a6870e0c79a64b1e6327f5248dcff98423f4d43/librt-0.9.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e306d956cfa027fe041585f02a1602c32bfa6bb8ebea4899d373383295a6c62f", size = 229051, upload-time = "2026-04-09T16:05:42.605Z" }, + { url = "https://files.pythonhosted.org/packages/4b/4b/8cfdbad314c8677a0148bf0b70591d6d18587f9884d930276098a235461b/librt-0.9.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:465814ab157986acb9dfa5ccd7df944be5eefc0d08d31ec6e8d88bc71251d845", size = 222492, upload-time = "2026-04-09T16:05:43.842Z" }, + { url = "https://files.pythonhosted.org/packages/1f/d1/2eda69563a1a88706808decdce035e4b32755dbfbb0d05e1a65db9547ed1/librt-0.9.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:703f4ae36d6240bfe24f542bac784c7e4194ec49c3ba5a994d02891649e2d85b", size = 223849, upload-time = "2026-04-09T16:05:45.054Z" }, + { url = "https://files.pythonhosted.org/packages/04/44/b2ed37df6be5b3d42cfe36318e0598e80843d5c6308dd63d0bf4e0ce5028/librt-0.9.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:3be322a15ee5e70b93b7a59cfd074614f22cc8c9ff18bd27f474e79137ea8d3b", size = 245001, upload-time = "2026-04-09T16:05:46.34Z" }, + { url = "https://files.pythonhosted.org/packages/47/e7/617e412426df89169dd2a9ed0cc8752d5763336252c65dbf945199915119/librt-0.9.0-cp314-cp314-win32.whl", hash = "sha256:b8da9f8035bb417770b1e1610526d87ad4fc58a2804dc4d79c53f6d2cf5a6eb9", size = 51799, upload-time = "2026-04-09T16:05:47.738Z" }, + { url = "https://files.pythonhosted.org/packages/24/ed/c22ca4db0ca3cbc285e4d9206108746beda561a9792289c3c31281d7e9df/librt-0.9.0-cp314-cp314-win_amd64.whl", hash = "sha256:b8bd70d5d816566a580d193326912f4a76ec2d28a97dc4cd4cc831c0af8e330e", size = 59165, upload-time = "2026-04-09T16:05:49.198Z" }, + { url = "https://files.pythonhosted.org/packages/24/56/875398fafa4cbc8f15b89366fc3287304ddd3314d861f182a4b87595ace0/librt-0.9.0-cp314-cp314-win_arm64.whl", hash = "sha256:fc5758e2b7a56532dc33e3c544d78cbaa9ecf0a0f2a2da2df882c1d6b99a317f", size = 49292, upload-time = "2026-04-09T16:05:50.362Z" }, + { url = "https://files.pythonhosted.org/packages/4c/61/bc448ecbf9b2d69c5cff88fe41496b19ab2a1cbda0065e47d4d0d51c0867/librt-0.9.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:f24b90b0e0c8cc9491fb1693ae91fe17cb7963153a1946395acdbdd5818429a4", size = 70175, upload-time = "2026-04-09T16:05:51.564Z" }, + { url = "https://files.pythonhosted.org/packages/60/f2/c47bb71069a73e2f04e70acbd196c1e5cc411578ac99039a224b98920fd4/librt-0.9.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:3fe56e80badb66fdcde06bef81bbaa5bfcf6fbd7aefb86222d9e369c38c6b228", size = 72951, upload-time = "2026-04-09T16:05:52.699Z" }, + { url = "https://files.pythonhosted.org/packages/29/19/0549df59060631732df758e8886d92088da5fdbedb35b80e4643664e8412/librt-0.9.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:527b5b820b47a09e09829051452bb0d1dd2122261254e2a6f674d12f1d793d54", size = 225864, upload-time = "2026-04-09T16:05:53.895Z" }, + { url = "https://files.pythonhosted.org/packages/9d/f8/3b144396d302ac08e50f89e64452c38db84bc7b23f6c60479c5d3abd303c/librt-0.9.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7d429bdd4ac0ab17c8e4a8af0ed2a7440b16eba474909ab357131018fe8c7e71", size = 241155, upload-time = "2026-04-09T16:05:55.191Z" }, + { url = "https://files.pythonhosted.org/packages/7a/ce/ee67ec14581de4043e61d05786d2aed6c9b5338816b7859bcf07455c6a9f/librt-0.9.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7202bdcac47d3a708271c4304a474a8605a4a9a4a709e954bf2d3241140aa938", size = 252235, upload-time = "2026-04-09T16:05:56.549Z" }, + { url = "https://files.pythonhosted.org/packages/8a/fa/0ead15daa2b293a54101550b08d4bafe387b7d4a9fc6d2b985602bae69b6/librt-0.9.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c0d620e74897f8c2613b3c4e2e9c1e422eb46d2ddd07df540784d44117836af3", size = 244963, upload-time = "2026-04-09T16:05:57.858Z" }, + { url = "https://files.pythonhosted.org/packages/29/68/9fbf9a9aa704ba87689e40017e720aced8d9a4d2b46b82451d8142f91ec9/librt-0.9.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d69fc39e627908f4c03297d5a88d9284b73f4d90b424461e32e8c2485e21c283", size = 257364, upload-time = "2026-04-09T16:05:59.686Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8d/9d60869f1b6716c762e45f66ed945b1e5dd649f7377684c3b176ae424648/librt-0.9.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:c2640e23d2b7c98796f123ffd95cf2022c7777aa8a4a3b98b36c570d37e85eee", size = 247661, upload-time = "2026-04-09T16:06:00.938Z" }, + { url = "https://files.pythonhosted.org/packages/70/ff/a5c365093962310bfdb4f6af256f191085078ffb529b3f0cbebb5b33ebe2/librt-0.9.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:451daa98463b7695b0a30aa56bf637831ea559e7b8101ac2ef6382e8eb15e29c", size = 248238, upload-time = "2026-04-09T16:06:02.537Z" }, + { url = "https://files.pythonhosted.org/packages/a0/3c/2d34365177f412c9e19c0a29f969d70f5343f27634b76b765a54d8b27705/librt-0.9.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:928bd06eca2c2bbf4349e5b817f837509b0604342e65a502de1d50a7570afd15", size = 269457, upload-time = "2026-04-09T16:06:03.833Z" }, + { url = "https://files.pythonhosted.org/packages/bc/cd/de45b239ea3bdf626f982a00c14bfcf2e12d261c510ba7db62c5969a27cd/librt-0.9.0-cp314-cp314t-win32.whl", hash = "sha256:a9c63e04d003bc0fb6a03b348018b9a3002f98268200e22cc80f146beac5dc40", size = 52453, upload-time = "2026-04-09T16:06:05.229Z" }, + { url = "https://files.pythonhosted.org/packages/7f/f9/bfb32ae428aa75c0c533915622176f0a17d6da7b72b5a3c6363685914f70/librt-0.9.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f162af66a2ed3f7d1d161a82ca584efd15acd9c1cff190a373458c32f7d42118", size = 60044, upload-time = "2026-04-09T16:06:06.398Z" }, + { url = "https://files.pythonhosted.org/packages/aa/47/7d70414bcdbb3bc1f458a8d10558f00bbfdb24e5a11740fc8197e12c3255/librt-0.9.0-cp314-cp314t-win_arm64.whl", hash = "sha256:a4b25c6c25cac5d0d9d6d6da855195b254e0021e513e0249f0e3b444dc6e0e61", size = 50009, upload-time = "2026-04-09T16:06:07.995Z" }, +] + [[package]] name = "logfire-api" version = "4.32.0" @@ -820,6 +926,67 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "mypy" +version = "1.20.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "librt", marker = "platform_python_implementation != 'PyPy'" }, + { name = "mypy-extensions" }, + { name = "pathspec" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/af/e3d4b3e9ec91a0ff9aabfdb38692952acf49bbb899c2e4c29acb3a6da3ae/mypy-1.20.2.tar.gz", hash = "sha256:e8222c26daaafd9e8626dec58ae36029f82585890589576f769a650dd20fd665", size = 3817349, upload-time = "2026-04-21T17:12:28.473Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/4e/7560e4528db9e9b147e4c0f22660466bf30a0a1fe3d63d1b9d3b0fd354ee/mypy-1.20.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4dbfcf869f6b0517f70cf0030ba6ea1d6645e132337a7d5204a18d8d5636c02b", size = 14539393, upload-time = "2026-04-21T17:07:12.52Z" }, + { url = "https://files.pythonhosted.org/packages/32/d9/34a5efed8124f5a9234f55ac6a4ced4201e2c5b81e1109c49ad23190ec8c/mypy-1.20.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b6481b228d072315b053210b01ac320e1be243dc17f9e5887ef167f23f5fae4", size = 13361642, upload-time = "2026-04-21T17:06:53.742Z" }, + { url = "https://files.pythonhosted.org/packages/d1/14/eb377acf78c03c92d566a1510cda8137348215b5335085ef662ab82ecd3a/mypy-1.20.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:34397cdced6b90b836e38182076049fdb41424322e0b0728c946b0939ebdf9f6", size = 13740347, upload-time = "2026-04-21T17:12:04.73Z" }, + { url = "https://files.pythonhosted.org/packages/b9/94/7e4634a32b641aa1c112422eed1bbece61ee16205f674190e8b536f884de/mypy-1.20.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a5da6976f20cae27059ea8d0c86e7cef3de720e04c4bb9ee18e3690fdb792066", size = 14734042, upload-time = "2026-04-21T17:07:43.16Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f3/f7e62395cb7f434541b4491a01149a4439e28ace4c0c632bbf5431e92d1f/mypy-1.20.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:56908d7e08318d39f85b1f0c6cfd47b0cac1a130da677630dac0de3e0623e102", size = 14964958, upload-time = "2026-04-21T17:11:00.665Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0d/47e3c3a0ec2a876e35aeac365df3cac7776c36bbd4ed18cc521e1b9d255b/mypy-1.20.2-cp312-cp312-win_amd64.whl", hash = "sha256:d52ad8d78522da1d308789df651ee5379088e77c76cb1994858d40a426b343b9", size = 10911340, upload-time = "2026-04-21T17:10:49.179Z" }, + { url = "https://files.pythonhosted.org/packages/d6/b2/6c852d72e0ea8b01f49da817fb52539993cde327e7d010e0103dc12d0dac/mypy-1.20.2-cp312-cp312-win_arm64.whl", hash = "sha256:785b08db19c9f214dc37d65f7c165d19a30fcecb48abfa30f31b01b5acaabb58", size = 9833947, upload-time = "2026-04-21T17:09:05.267Z" }, + { url = "https://files.pythonhosted.org/packages/5b/c4/b93812d3a192c9bcf5df405bd2f30277cd0e48106a14d1023c7f6ed6e39b/mypy-1.20.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:edfbfca868cdd6bd8d974a60f8a3682f5565d3f5c99b327640cedd24c4264026", size = 14524670, upload-time = "2026-04-21T17:10:30.737Z" }, + { url = "https://files.pythonhosted.org/packages/f3/47/42c122501bff18eaf1e8f457f5c017933452d8acdc52918a9f59f6812955/mypy-1.20.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e2877a02380adfcdbc69071a0f74d6e9dbbf593c0dc9d174e1f223ffd5281943", size = 13336218, upload-time = "2026-04-21T17:08:44.069Z" }, + { url = "https://files.pythonhosted.org/packages/92/8f/75bbc92f41725fbd585fb17b440b1119b576105df1013622983e18640a93/mypy-1.20.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7488448de6007cd5177c6cea0517ac33b4c0f5ee9b5e9f2be51ce75511a85517", size = 13724906, upload-time = "2026-04-21T17:08:01.02Z" }, + { url = "https://files.pythonhosted.org/packages/a1/32/4c49da27a606167391ff0c39aa955707a00edc500572e562f7c36c08a71f/mypy-1.20.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bb9c2fa06887e21d6a3a868762acb82aec34e2c6fd0174064f27c93ede68ad15", size = 14726046, upload-time = "2026-04-21T17:11:22.354Z" }, + { url = "https://files.pythonhosted.org/packages/7f/fc/4e354a1bd70216359deb0c9c54847ee6b32ef78dfb09f5131ff99b494078/mypy-1.20.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9d56a78b646f2e3daa865bc70cd5ec5a46c50045801ca8ff17a0c43abc97e3ee", size = 14955587, upload-time = "2026-04-21T17:12:16.033Z" }, + { url = "https://files.pythonhosted.org/packages/62/b2/c0f2056e9eb8f08c62cafd9715e4584b89132bdc832fcf85d27d07b5f3e5/mypy-1.20.2-cp313-cp313-win_amd64.whl", hash = "sha256:2a4102b03bb7481d9a91a6da8d174740c9c8c4401024684b9ca3b7cc5e49852f", size = 10922681, upload-time = "2026-04-21T17:06:35.842Z" }, + { url = "https://files.pythonhosted.org/packages/e5/14/065e333721f05de8ef683d0aa804c23026bcc287446b61cac657b902ccac/mypy-1.20.2-cp313-cp313-win_arm64.whl", hash = "sha256:a95a9248b0c6fd933a442c03c3b113c3b61320086b88e2c444676d3fd1ca3330", size = 9830560, upload-time = "2026-04-21T17:07:51.023Z" }, + { url = "https://files.pythonhosted.org/packages/ae/d1/b4ec96b0ecc620a4443570c6e95c867903428cfcde4206518eafdd5880c3/mypy-1.20.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:419413398fe250aae057fd2fe50166b61077083c9b82754c341cf4fd73038f30", size = 14524561, upload-time = "2026-04-21T17:06:27.325Z" }, + { url = "https://files.pythonhosted.org/packages/3a/63/d2c2ff4fa66bc49477d32dfa26e8a167ba803ea6a69c5efb416036909d30/mypy-1.20.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:e73c07f23009962885c197ccb9b41356a30cc0e5a1d0c2ea8fd8fb1362d7f924", size = 13363883, upload-time = "2026-04-21T17:11:11.239Z" }, + { url = "https://files.pythonhosted.org/packages/2a/56/983916806bf4eddeaaa2c9230903c3669c6718552a921154e1c5182c701f/mypy-1.20.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c64e5973df366b747646fc98da921f9d6eba9716d57d1db94a83c026a08e0fb", size = 13742945, upload-time = "2026-04-21T17:08:34.181Z" }, + { url = "https://files.pythonhosted.org/packages/19/65/0cd9285ab010ee8214c83d67c6b49417c40d86ce46f1aa109457b5a9b8d7/mypy-1.20.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a65aa591af023864fd08a97da9974e919452cfe19cb146c8a5dc692626445dc", size = 14706163, upload-time = "2026-04-21T17:05:15.51Z" }, + { url = "https://files.pythonhosted.org/packages/94/97/48ff3b297cafcc94d185243a9190836fb1b01c1b0918fff64e941e973cc9/mypy-1.20.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:4fef51b01e638974a6e69885687e9bd40c8d1e09a6cd291cca0619625cf1f558", size = 14938677, upload-time = "2026-04-21T17:05:39.562Z" }, + { url = "https://files.pythonhosted.org/packages/fd/a1/1b4233d255bdd0b38a1f284feeb1c143ca508c19184964e22f8d837ec851/mypy-1.20.2-cp314-cp314-win_amd64.whl", hash = "sha256:913485a03f1bcf5d279409a9d2b9ed565c151f61c09f29991e5faa14033da4c8", size = 11089322, upload-time = "2026-04-21T17:06:44.29Z" }, + { url = "https://files.pythonhosted.org/packages/78/c2/ce7ee2ba36aeb954ba50f18fa25d9c1188578654b97d02a66a15b6f09531/mypy-1.20.2-cp314-cp314-win_arm64.whl", hash = "sha256:c3bae4f855d965b5453784300c12ffc63a548304ac7f99e55d4dc7c898673aa3", size = 10017775, upload-time = "2026-04-21T17:07:20.732Z" }, + { url = "https://files.pythonhosted.org/packages/4e/a1/9d93a7d0b5859af0ead82b4888b46df6c8797e1bc5e1e262a08518c6d48e/mypy-1.20.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2de3dcea53babc1c3237a19002bc3d228ce1833278f093b8d619e06e7cc79609", size = 15549002, upload-time = "2026-04-21T17:08:23.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/d2/09a6a10ee1bf0008f6c144d9676f2ca6a12512151b4e0ad0ff6c4fac5337/mypy-1.20.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:52b176444e2e5054dfcbcb8c75b0b719865c96247b37407184bbfca5c353f2c2", size = 14401942, upload-time = "2026-04-21T17:07:31.837Z" }, + { url = "https://files.pythonhosted.org/packages/57/da/9594b75c3c019e805250bed3583bdf4443ff9e6ef08f97e39ae308cb06f2/mypy-1.20.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:688c3312e5dadb573a2c69c82af3a298d43ecf9e6d264e0f95df960b5f6ac19c", size = 15041649, upload-time = "2026-04-21T17:09:34.653Z" }, + { url = "https://files.pythonhosted.org/packages/97/77/f75a65c278e6e8eba2071f7f5a90481891053ecc39878cc444634d892abe/mypy-1.20.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:29752dbbf8cc53f89f6ac096d363314333045c257c9c75cbd189ca2de0455744", size = 15864588, upload-time = "2026-04-21T17:11:44.936Z" }, + { url = "https://files.pythonhosted.org/packages/d7/46/1a4e1c66e96c1a3246ddf5403d122ac9b0a8d2b7e65730b9d6533ba7a6d3/mypy-1.20.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:803203d2b6ea644982c644895c2f78b28d0e208bba7b27d9b921e0ec5eb207c6", size = 16093956, upload-time = "2026-04-21T17:10:17.683Z" }, + { url = "https://files.pythonhosted.org/packages/5a/2c/78a8851264dec38cd736ca5b8bc9380674df0dd0be7792f538916157716c/mypy-1.20.2-cp314-cp314t-win_amd64.whl", hash = "sha256:9bcb8aa397ff0093c824182fd76a935a9ba7ad097fcbef80ae89bf6c1731d8ec", size = 12568661, upload-time = "2026-04-21T17:11:54.473Z" }, + { url = "https://files.pythonhosted.org/packages/83/01/cd7318aa03493322ce275a0e14f4f52b8896335e4e79d4fb8153a7ad2b77/mypy-1.20.2-cp314-cp314t-win_arm64.whl", hash = "sha256:e061b58443f1736f8a37c48978d7ab581636d6ab03e3d4f99e3fa90463bb9382", size = 10389240, upload-time = "2026-04-21T17:09:42.719Z" }, + { url = "https://files.pythonhosted.org/packages/28/9a/f23c163e25b11074188251b0b5a0342625fc1cdb6af604757174fa9acc9b/mypy-1.20.2-py3-none-any.whl", hash = "sha256:a94c5a76ab46c5e6257c7972b6c8cff0574201ca7dc05647e33e795d78680563", size = 2637314, upload-time = "2026-04-21T17:05:54.5Z" }, +] + +[[package]] +name = "mypy-extensions" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a2/6e/371856a3fb9d31ca8dac321cda606860fa4548858c0cc45d9d1d4ca2628b/mypy_extensions-1.1.0.tar.gz", hash = "sha256:52e68efc3284861e772bbcd66823fde5ae21fd2fdb51c62a211403730b916558", size = 6343, upload-time = "2025-04-22T14:54:24.164Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/79/7b/2c79738432f5c924bef5071f933bcc9efd0473bac3b4aa584a6f7c1c8df8/mypy_extensions-1.1.0-py3-none-any.whl", hash = "sha256:1be4cccdb0f2482337c4743e60421de3a356cd97508abadd57d47403e94f5505", size = 4963, upload-time = "2025-04-22T14:54:22.983Z" }, +] + +[[package]] +name = "nodeenv" +version = "1.10.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/bf/d1bda4f6168e0b2e9e5958945e01910052158313224ada5ce1fb2e1113b8/nodeenv-1.10.0.tar.gz", hash = "sha256:996c191ad80897d076bdfba80a41994c2b47c68e224c542b48feba42ba00f8bb", size = 55611, upload-time = "2025-12-20T14:08:54.006Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/b2/d0896bdcdc8d28a7fc5717c305f1a861c26e18c05047949fb371034d98bd/nodeenv-1.10.0-py2.py3-none-any.whl", hash = "sha256:5bb13e3eed2923615535339b3c620e76779af4cb4c6a90deccc9e36b274d3827", size = 23438, upload-time = "2025-12-20T14:08:52.782Z" }, +] + [[package]] name = "opensearch-protobufs" version = "0.19.0" @@ -871,6 +1038,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/7a/c2/920ef838e2f0028c8262f16101ec09ebd5969864e5a64c4c05fad0617c56/packaging-26.1-py3-none-any.whl", hash = "sha256:5d9c0669c6285e491e0ced2eee587eaf67b670d94a19e94e3984a481aba6802f", size = 95831, upload-time = "2026-04-14T21:12:47.56Z" }, ] +[[package]] +name = "pathspec" +version = "1.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2e/17/9c3094b822982b9f1ea666d8580ce59000f61f87c1663556fb72031ad9ec/pathspec-1.1.0.tar.gz", hash = "sha256:f5d7c555da02fd8dde3e4a2354b6aba817a89112fa8f333f7917a2a4834dd080", size = 133918, upload-time = "2026-04-23T01:46:22.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/c9/8eed0486f074e9f1ca7f8ce5ad663e65f12fdab344028d658fa1b03d35e0/pathspec-1.1.0-py3-none-any.whl", hash = "sha256:574b128f7456bd899045ccd142dd446af7e6cfd0072d63ad73fbc55fbb4aaa42", size = 56264, upload-time = "2026-04-23T01:46:20.606Z" }, +] + +[[package]] +name = "platformdirs" +version = "4.9.6" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9f/4a/0883b8e3802965322523f0b200ecf33d31f10991d0401162f4b23c698b42/platformdirs-4.9.6.tar.gz", hash = "sha256:3bfa75b0ad0db84096ae777218481852c0ebc6c727b3168c1b9e0118e458cf0a", size = 29400, upload-time = "2026-04-09T00:04:10.812Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/75/a6/a0a304dc33b49145b21f4808d763822111e67d1c3a32b524a1baf947b6e1/platformdirs-4.9.6-py3-none-any.whl", hash = "sha256:e61adb1d5e5cb3441b4b7710bea7e4c12250ca49439228cc1021c00dcfac0917", size = 21348, upload-time = "2026-04-09T00:04:09.463Z" }, +] + [[package]] name = "pluggy" version = "1.6.0" @@ -880,6 +1065,22 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "pre-commit" +version = "4.6.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cfgv" }, + { name = "identify" }, + { name = "nodeenv" }, + { name = "pyyaml" }, + { name = "virtualenv" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/8e/22/2de9408ac81acbb8a7d05d4cc064a152ccf33b3d480ebe0cd292153db239/pre_commit-4.6.0.tar.gz", hash = "sha256:718d2208cef53fdc38206e40524a6d4d9576d103eb16f0fec11c875e7716e9d9", size = 198525, upload-time = "2026-04-21T20:31:41.613Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/80/6e/4b28b62ecb6aae56769c34a8ff1d661473ec1e9519e2d5f8b2c150086b26/pre_commit-4.6.0-py2.py3-none-any.whl", hash = "sha256:e2cf246f7299edcabcf15f9b0571fdce06058527f0a06535068a86d38089f29b", size = 226472, upload-time = "2026-04-21T20:31:40.092Z" }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -1169,6 +1370,19 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, ] +[[package]] +name = "python-discovery" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "platformdirs" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/de/ef/3bae0e537cfe91e8431efcba4434463d2c5a65f5a89edd47c6cf2f03c55f/python_discovery-1.2.2.tar.gz", hash = "sha256:876e9c57139eb757cb5878cbdd9ae5379e5d96266c99ef731119e04fffe533bb", size = 58872, upload-time = "2026-04-07T17:28:49.249Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d8/db/795879cc3ddfe338599bddea6388cc5100b088db0a4caf6e6c1af1c27e04/python_discovery-1.2.2-py3-none-any.whl", hash = "sha256:e1ae95d9af875e78f15e19aed0c6137ab1bb49c200f21f5061786490c9585c7a", size = 31894, upload-time = "2026-04-07T17:28:48.09Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1178,6 +1392,52 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/0b/d7/1959b9648791274998a9c3526f6d0ec8fd2233e4d4acce81bbae76b44b2a/python_dotenv-1.2.2-py3-none-any.whl", hash = "sha256:1d8214789a24de455a8b8bd8ae6fe3c6b69a5e3d64aa8a8e5d68e694bbcb285a", size = 22101, upload-time = "2026-03-01T16:00:25.09Z" }, ] +[[package]] +name = "pyyaml" +version = "6.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" }, + { url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" }, + { url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" }, + { url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" }, + { url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" }, + { url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" }, + { url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" }, + { url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" }, + { url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" }, + { url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" }, + { url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" }, + { url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" }, + { url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" }, + { url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" }, + { url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" }, + { url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" }, + { url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" }, + { url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" }, + { url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" }, + { url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" }, + { url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" }, + { url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" }, + { url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" }, + { url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" }, + { url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" }, + { url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" }, + { url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" }, + { url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" }, + { url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" }, + { url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" }, + { url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" }, + { url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" }, + { url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" }, + { url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" }, + { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, +] + [[package]] name = "requests" version = "2.33.1" @@ -1205,6 +1465,31 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/57/41/bd1b81fd1e5a59c3afdf50c678a028498dd7c4197637f27406be0d1b55d2/requests_aws4auth-1.3.1-py3-none-any.whl", hash = "sha256:2969b5379ae6e60ee666638caf6cb94a32d67033f6bfcf0d50c95cd5474f2419", size = 24584, upload-time = "2024-07-21T21:29:14.216Z" }, ] +[[package]] +name = "ruff" +version = "0.15.11" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e4/8d/192f3d7103816158dfd5ea50d098ef2aec19194e6cbccd4b3485bdb2eb2d/ruff-0.15.11.tar.gz", hash = "sha256:f092b21708bf0e7437ce9ada249dfe688ff9a0954fc94abab05dcea7dcd29c33", size = 4637264, upload-time = "2026-04-16T18:46:26.58Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/02/1e/6aca3427f751295ab011828e15e9bf452200ac74484f1db4be0197b8170b/ruff-0.15.11-py3-none-linux_armv6l.whl", hash = "sha256:e927cfff503135c558eb581a0c9792264aae9507904eb27809cdcff2f2c847b7", size = 10607943, upload-time = "2026-04-16T18:46:05.967Z" }, + { url = "https://files.pythonhosted.org/packages/e7/26/1341c262e74f36d4e84f3d6f4df0ac68cd53331a66bfc5080daa17c84c0b/ruff-0.15.11-py3-none-macosx_10_12_x86_64.whl", hash = "sha256:7a1b5b2938d8f890b76084d4fa843604d787a912541eae85fd7e233398bbb73e", size = 10988592, upload-time = "2026-04-16T18:46:00.742Z" }, + { url = "https://files.pythonhosted.org/packages/03/71/850b1d6ffa9564fbb6740429bad53df1094082fe515c8c1e74b6d8d05f18/ruff-0.15.11-py3-none-macosx_11_0_arm64.whl", hash = "sha256:d4176f3d194afbdaee6e41b9ccb1a2c287dba8700047df474abfbe773825d1cb", size = 10338501, upload-time = "2026-04-16T18:46:03.723Z" }, + { url = "https://files.pythonhosted.org/packages/f2/11/cc1284d3e298c45a817a6aadb6c3e1d70b45c9b36d8d9cce3387b495a03a/ruff-0.15.11-py3-none-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3b17c886fb88203ced3afe7f14e8d5ae96e9d2f4ccc0ee66aa19f2c2675a27e4", size = 10670693, upload-time = "2026-04-16T18:46:41.941Z" }, + { url = "https://files.pythonhosted.org/packages/ce/9e/f8288b034ab72b371513c13f9a41d9ba3effac54e24bfb467b007daee2ca/ruff-0.15.11-py3-none-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:49fafa220220afe7758a487b048de4c8f9f767f37dfefad46b9dd06759d003eb", size = 10416177, upload-time = "2026-04-16T18:46:21.717Z" }, + { url = "https://files.pythonhosted.org/packages/85/71/504d79abfd3d92532ba6bbe3d1c19fada03e494332a59e37c7c2dabae427/ruff-0.15.11-py3-none-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f2ab8427e74a00d93b8bda1307b1e60970d40f304af38bccb218e056c220120d", size = 11221886, upload-time = "2026-04-16T18:46:15.086Z" }, + { url = "https://files.pythonhosted.org/packages/43/5a/947e6ab7a5ad603d65b474be15a4cbc6d29832db5d762cd142e4e3a74164/ruff-0.15.11-py3-none-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:195072c0c8e1fc8f940652073df082e37a5d9cb43b4ab1e4d0566ab8977a13b7", size = 12075183, upload-time = "2026-04-16T18:46:07.944Z" }, + { url = "https://files.pythonhosted.org/packages/9f/a1/0b7bb6268775fdd3a0818aee8efd8f5b4e231d24dd4d528ced2534023182/ruff-0.15.11-py3-none-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a3a0996d486af3920dec930a2e7daed4847dfc12649b537a9335585ada163e9e", size = 11516575, upload-time = "2026-04-16T18:46:31.687Z" }, + { url = "https://files.pythonhosted.org/packages/30/c3/bb5168fc4d233cc06e95f482770d0f3c87945a0cd9f614b90ea8dc2f2833/ruff-0.15.11-py3-none-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:1bef2cb556d509259f1fe440bb9cd33c756222cf0a7afe90d15edf0866702431", size = 11306537, upload-time = "2026-04-16T18:46:36.988Z" }, + { url = "https://files.pythonhosted.org/packages/e4/92/4cfae6441f3967317946f3b788136eecf093729b94d6561f963ed810c82e/ruff-0.15.11-py3-none-manylinux_2_31_riscv64.whl", hash = "sha256:030d921a836d7d4a12cf6e8d984a88b66094ccb0e0f17ddd55067c331191bf19", size = 11296813, upload-time = "2026-04-16T18:46:24.182Z" }, + { url = "https://files.pythonhosted.org/packages/43/26/972784c5dde8313acde8ac71ba8ac65475b85db4a2352a76c9934361f9bc/ruff-0.15.11-py3-none-musllinux_1_2_aarch64.whl", hash = "sha256:0e783b599b4577788dbbb66b9addcef87e9a8832f4ce0c19e34bf55543a2f890", size = 10633136, upload-time = "2026-04-16T18:46:39.802Z" }, + { url = "https://files.pythonhosted.org/packages/5b/53/3985a4f185020c2f367f2e08a103032e12564829742a1b417980ce1514a0/ruff-0.15.11-py3-none-musllinux_1_2_armv7l.whl", hash = "sha256:ae90592246625ba4a34349d68ec28d4400d75182b71baa196ddb9f82db025ef5", size = 10424701, upload-time = "2026-04-16T18:46:10.381Z" }, + { url = "https://files.pythonhosted.org/packages/d3/57/bf0dfb32241b56c83bb663a826133da4bf17f682ba8c096973065f6e6a68/ruff-0.15.11-py3-none-musllinux_1_2_i686.whl", hash = "sha256:1f111d62e3c983ed20e0ca2e800f8d77433a5b1161947df99a5c2a3fb60514f0", size = 10873887, upload-time = "2026-04-16T18:46:29.157Z" }, + { url = "https://files.pythonhosted.org/packages/02/05/e48076b2a57dc33ee8c7a957296f97c744ca891a8ffb4ffb1aaa3b3f517d/ruff-0.15.11-py3-none-musllinux_1_2_x86_64.whl", hash = "sha256:06f483d6646f59eaffba9ae30956370d3a886625f511a3108994000480621d1c", size = 11404316, upload-time = "2026-04-16T18:46:19.462Z" }, + { url = "https://files.pythonhosted.org/packages/88/27/0195d15fe7a897cbcba0904792c4b7c9fdd958456c3a17d2ea6093716a9a/ruff-0.15.11-py3-none-win32.whl", hash = "sha256:476a2aa56b7da0b73a3ee80b6b2f0e19cce544245479adde7baa65466664d5f3", size = 10655535, upload-time = "2026-04-16T18:46:12.47Z" }, + { url = "https://files.pythonhosted.org/packages/3a/5e/c927b325bd4c1d3620211a4b96f47864633199feed60fa936025ab27e090/ruff-0.15.11-py3-none-win_amd64.whl", hash = "sha256:8b6756d88d7e234fb0c98c91511aae3cd519d5e3ed271cae31b20f39cb2a12a3", size = 11779692, upload-time = "2026-04-16T18:46:17.268Z" }, + { url = "https://files.pythonhosted.org/packages/63/b6/aeadee5443e49baa2facd51131159fd6301cc4ccfc1541e4df7b021c37dd/ruff-0.15.11-py3-none-win_arm64.whl", hash = "sha256:063fed18cc1bbe0ee7393957284a6fe8b588c6a406a285af3ee3f46da2391ee4", size = 11032614, upload-time = "2026-04-16T18:46:34.487Z" }, +] + [[package]] name = "s3fs" version = "2026.3.0" @@ -1283,6 +1568,21 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/31/a3/5b1562db76a5a488274b2332a97199b32d0442aca0ed193697fd47786316/uvicorn-0.46.0-py3-none-any.whl", hash = "sha256:bbebbcbed972d162afca128605223022bedd345b7bc7855ce66deb31487a9048", size = 70926, upload-time = "2026-04-23T07:15:58.355Z" }, ] +[[package]] +name = "virtualenv" +version = "21.2.4" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "distlib" }, + { name = "filelock" }, + { name = "platformdirs" }, + { name = "python-discovery" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0c/98/3a7e644e19cb26133488caff231be390579860bbbb3da35913c49a1d0a46/virtualenv-21.2.4.tar.gz", hash = "sha256:b294ef68192638004d72524ce7ef303e9d0cf5a44c95ce2e54a7500a6381cada", size = 5850742, upload-time = "2026-04-14T22:15:31.438Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/27/8d/edd0bd910ff803c308ee9a6b7778621af0d10252219ad9f19ef4d4982a61/virtualenv-21.2.4-py3-none-any.whl", hash = "sha256:29d21e941795206138d0f22f4e45ff7050e5da6c6472299fb7103318763861ac", size = 5831232, upload-time = "2026-04-14T22:15:29.342Z" }, +] + [[package]] name = "werkzeug" version = "3.1.8"