diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml
index 81537c29..99f18909 100644
--- a/.github/workflows/ci.yml
+++ b/.github/workflows/ci.yml
@@ -16,7 +16,7 @@ jobs:
if: github.event.pull_request.draft == false
strategy:
matrix:
- python-version: ["3.10", "3.11"]
+ python-version: ["3.10", "3.11", "3.12"]
runs-on: ubuntu-latest
steps:
- name: Check out the repository
diff --git a/.github/workflows/publish.yml b/.github/workflows/publish.yml
index 26bf158a..7e94aa22 100644
--- a/.github/workflows/publish.yml
+++ b/.github/workflows/publish.yml
@@ -19,7 +19,7 @@ jobs:
- name: Set up Python
uses: actions/setup-python@v5
with:
- python-version: "3.11"
+ python-version: "3.12"
- name: Bump version
run: |
VERSION=$(git describe --tags --abbrev=0)
diff --git a/cookbook/notereader_clinical_coding_fhir.py b/cookbook/notereader_clinical_coding_fhir.py
index 24f1f343..dd677e25 100644
--- a/cookbook/notereader_clinical_coding_fhir.py
+++ b/cookbook/notereader_clinical_coding_fhir.py
@@ -132,7 +132,7 @@ def run_server():
# Create sandbox client for testing
client = SandboxClient(
- url="http://localhost:8000/notereader/fhir/",
+ url="http://localhost:8000/notereader/?wsdl",
workflow="sign-note-inpatient",
protocol="soap",
)
diff --git a/healthchain/fhir/readers.py b/healthchain/fhir/readers.py
index d55fe7be..00c2ad4f 100644
--- a/healthchain/fhir/readers.py
+++ b/healthchain/fhir/readers.py
@@ -6,6 +6,7 @@
import logging
import importlib
+import re
from typing import Optional, Dict, Any, List
from fhir.resources.resource import Resource
@@ -14,6 +15,42 @@
logger = logging.getLogger(__name__)
+def _fix_timezone_naive_datetimes(data: Any) -> Any:
+ """
+ Recursively fix timezone-naive datetime strings by appending UTC timezone.
+
+ Pydantic v2 requires timezone-aware datetimes. This function walks through
+ nested dicts/lists and adds 'Z' (UTC) to datetime strings that match the
+ pattern YYYY-MM-DDTHH:MM:SS but lack timezone info.
+
+ Args:
+ data: Dict, list, or primitive value to process
+
+ Returns:
+ Processed data with timezone-aware datetime strings
+
+ Example:
+ >>> data = {"start": "2021-04-19T00:00:00", "name": "Test"}
+ >>> _fix_timezone_naive_datetimes(data)
+ {"start": "2021-04-19T00:00:00Z", "name": "Test"}
+ """
+ # Pattern: YYYY-MM-DDTHH:MM:SS optionally followed by microseconds
+ # Must NOT already have timezone (Z or +/-HH:MM)
+ datetime_pattern = re.compile(r"^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d+)?$")
+
+ if isinstance(data, dict):
+ return {
+ key: _fix_timezone_naive_datetimes(value) for key, value in data.items()
+ }
+ elif isinstance(data, list):
+ return [_fix_timezone_naive_datetimes(item) for item in data]
+ elif isinstance(data, str) and datetime_pattern.match(data):
+ # Add UTC timezone
+ return f"{data}Z"
+ else:
+ return data
+
+
def create_resource_from_dict(
resource_dict: Dict, resource_type: str
) -> Optional[Resource]:
@@ -99,8 +136,10 @@ def convert_prefetch_to_fhir_objects(
resource_type = resource_data.get("resourceType")
if resource_type:
try:
+ # Fix timezone-naive datetimes before validation
+ fixed_data = _fix_timezone_naive_datetimes(resource_data)
resource_class = get_fhir_model_class(resource_type)
- result[key] = resource_class(**resource_data)
+ result[key] = resource_class(**fixed_data)
except Exception as e:
logger.warning(
f"Failed to convert {resource_type} to FHIR object: {e}"
diff --git a/healthchain/gateway/api/app.py b/healthchain/gateway/api/app.py
index 0c67430b..01fcef11 100644
--- a/healthchain/gateway/api/app.py
+++ b/healthchain/gateway/api/app.py
@@ -11,7 +11,6 @@
from datetime import datetime
from fastapi import FastAPI, APIRouter, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
-from fastapi.middleware.wsgi import WSGIMiddleware
from fastapi.exceptions import RequestValidationError
from fastapi.responses import JSONResponse
from termcolor import colored
@@ -233,10 +232,10 @@ def _add_component_routes(
# Case 2: WSGI services (like NoteReaderService) - only for services
if (
component_type == "service"
- and hasattr(component, "create_wsgi_app")
- and callable(component.create_wsgi_app)
+ and hasattr(component, "create_fastapi_router")
+ and callable(component.create_fastapi_router)
):
- self._register_wsgi_service(
+ self._register_mounted_service(
component, component_name, endpoints_registry, path
)
return
@@ -249,7 +248,7 @@ def _add_component_routes(
else:
logger.warning(
f"Service {component_name} does not implement APIRouter or WSGI patterns. "
- f"Services must either inherit from APIRouter or implement create_wsgi_app()."
+ f"Services must either inherit from APIRouter or implement create_fastapi_router()."
)
def _register_api_router(
@@ -277,18 +276,16 @@ def _register_api_router(
else:
logger.debug(f"Registered {component_name} as router (routes unknown)")
- def _register_wsgi_service(
+ def _register_mounted_service( # Renamed from _register_wsgi_service
self,
service: BaseProtocolHandler,
service_name: str,
endpoints_registry: Dict[str, set],
path: Optional[str] = None,
) -> None:
- """Register a WSGI service."""
- # Create WSGI app
- wsgi_app = service.create_wsgi_app()
+ """Register a service with a custom router."""
+ router_or_app = service.create_fastapi_router()
- # Determine mount path with fallback chain
mount_path = (
path
or getattr(service.config, "default_mount_path", None)
@@ -296,10 +293,25 @@ def _register_wsgi_service(
or f"/{service_name.lower().replace('service', '').replace('gateway', '')}"
)
- # Mount the WSGI app
- self.mount(mount_path, WSGIMiddleware(wsgi_app))
- endpoints_registry[service_name].add(f"WSGI:{mount_path}")
- logger.debug(f"Registered WSGI service {service_name} at {mount_path}")
+ logger.info(f"🔧 Registering {service_name} at: {mount_path}")
+ logger.info(f" Router type: {type(router_or_app)}")
+
+ # Use include_router for APIRouter instances
+ if isinstance(router_or_app, APIRouter):
+ if hasattr(router_or_app, "routes"):
+ logger.info(f" Routes in router: {len(router_or_app.routes)}")
+ for route in router_or_app.routes:
+ if hasattr(route, "methods") and hasattr(route, "path"):
+ logger.info(f" - {route.methods} {route.path}")
+
+ self.include_router(router_or_app, prefix=mount_path)
+ endpoints_registry[service_name].add(f"INCLUDED:{mount_path}")
+ logger.info(f"✅ Included router {service_name} at {mount_path}")
+ else:
+ # For FastAPI apps, use mount
+ self.mount(mount_path, router_or_app)
+ endpoints_registry[service_name].add(f"MOUNTED:{mount_path}")
+ logger.info(f"✅ Mounted app {service_name} at {mount_path}")
def register_gateway(
self,
diff --git a/healthchain/gateway/soap/__init__.py b/healthchain/gateway/soap/__init__.py
index 8972b003..24b4b701 100644
--- a/healthchain/gateway/soap/__init__.py
+++ b/healthchain/gateway/soap/__init__.py
@@ -1,10 +1,9 @@
-from .notereader import NoteReaderService
-from .utils.epiccds import CDSServices
+from .notereader import NoteReaderService, NoteReaderConfig
from .utils.model import ClientFault, ServerFault
__all__ = [
"NoteReaderService",
- "CDSServices",
+ "NoteReaderConfig",
"ClientFault",
"ServerFault",
]
diff --git a/healthchain/gateway/soap/fastapi_server.py b/healthchain/gateway/soap/fastapi_server.py
new file mode 100644
index 00000000..8be84760
--- /dev/null
+++ b/healthchain/gateway/soap/fastapi_server.py
@@ -0,0 +1,379 @@
+# fastapi_server.py
+from fastapi import APIRouter, Request, Response
+import lxml.etree as ET
+from typing import Callable, Optional, Any, Dict
+from healthchain.models.requests.cdarequest import CdaRequest
+from healthchain.models.responses.cdaresponse import CdaResponse
+import base64
+import logging
+from pathlib import Path
+
+logger = logging.getLogger(__name__)
+
+# SOAP namespace for envelope
+SOAP_NS = "http://schemas.xmlsoap.org/soap/envelope/"
+NSMAP = {"soap": SOAP_NS}
+
+# WSDL target namespace (from your WSDL)
+WSDL_NS = "urn:epic-com:Common.2013.Services"
+
+
+def build_soap_envelope(body_xml: ET._Element) -> bytes:
+ """
+ Wrap the provided body element in a SOAP Envelope/Body and return bytes.
+ The body_xml element should already be namespaced (if desired).
+ """
+ # Define namespace map with tns prefix for the WSDL namespace
+ nsmap = {"soap": SOAP_NS, "tns": WSDL_NS}
+ envelope = ET.Element(ET.QName(SOAP_NS, "Envelope"), nsmap=nsmap)
+ body = ET.SubElement(envelope, ET.QName(SOAP_NS, "Body"))
+ body.append(body_xml)
+ return ET.tostring(envelope, xml_declaration=True, encoding="utf-8")
+
+
+def build_soap_fault(faultcode: str, faultstring: str) -> bytes:
+ """
+ Construct a SOAP Fault element and return the full SOAP envelope bytes.
+ """
+ # Fault must be in the Body, not namespaced by the WSDL
+ fault = ET.Element("Fault")
+ code_el = ET.SubElement(fault, "faultcode")
+ code_el.text = faultcode
+ string_el = ET.SubElement(fault, "faultstring")
+ string_el.text = faultstring
+ return build_soap_envelope(fault)
+
+
+def safe_text_of(el: ET._Element) -> Optional[str]:
+ """
+ Return the text content of an element.
+
+ If the element has child elements (like Document containing XML),
+ serialize the children as a string. Otherwise return the text content.
+ """
+ if el is None:
+ return None
+
+ # Check if element has child elements
+ if len(el) > 0:
+ # Element has children - serialize them as XML
+ # This handles cases like ...
+ children_xml = []
+ for child in el:
+ children_xml.append(ET.tostring(child, encoding="unicode"))
+ if children_xml:
+ return "".join(children_xml)
+
+ # No children - just get text content
+ text = el.text if el.text is not None else None
+ # Also check if text is just whitespace
+ if text and text.strip():
+ return text.strip()
+
+ return None
+
+
+def coerce_document_value(raw_val: Any) -> Optional[str]:
+ """
+ Accept a few possible document representations and return a string XML payload.
+ - If raw_val is bytes -> decode as utf-8
+ - If raw_val is a list -> use first element
+ - If raw_val looks like base64 -> decode
+ - If already a string -> return as-is
+ """
+ if raw_val is None:
+ return None
+
+ # if list, take first element
+ if isinstance(raw_val, (list, tuple)) and len(raw_val) > 0:
+ return coerce_document_value(raw_val[0])
+
+ if isinstance(raw_val, bytes):
+ try:
+ return raw_val.decode("utf-8")
+ except Exception:
+ # try base64 decode
+ try:
+ decoded = base64.b64decode(raw_val)
+ return decoded.decode("utf-8")
+ except Exception:
+ return raw_val.decode("latin1", errors="ignore")
+
+ if isinstance(raw_val, str):
+ text = raw_val.strip()
+
+ # First check if it looks like XML
+ if text.startswith("<"):
+ return text
+
+ # Heuristic: if looks like base64 (A-Z,a-z,0-9,+,/ and length multiple of 4)
+ # and is longer than 20 chars
+ bchars = set(
+ "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/="
+ )
+ if len(text) > 20 and all(c in bchars for c in text) and len(text) % 4 == 0:
+ try:
+ decoded = base64.b64decode(text)
+ # If decode yields bytes that look like XML/utf-8, return that
+ try:
+ decoded_str = decoded.decode("utf-8")
+ if decoded_str.strip().startswith("<"):
+ return decoded_str
+ # Otherwise return original
+ return text
+ except Exception:
+ # fallback to returning original string
+ return text
+ except Exception:
+ return text
+ return text
+
+ # fallback
+ return str(raw_val)
+
+
+def create_fastapi_soap_router(
+ service_name: str,
+ namespace: str,
+ handler: Callable[[CdaRequest], CdaResponse],
+ wsdl_path: Optional[str] = None,
+) -> APIRouter:
+ """
+ Create an APIRouter that exposes a SOAP endpoint for the ProcessDocument operation.
+
+ - Expects SOAP document-style (per WSDL).
+ - Handles Operation: ProcessDocument
+ - Maps incoming element names (SessionID, WorkType, OrganizationID, Document)
+ to CdaRequest fields (session_id, work_type, organization_id, document).
+ - Returns ProcessDocumentResponse (Document, Error) wrapped in SOAP envelope.
+ - Returns SOAP Faults for client/server errors.
+ - Optionally serves WSDL at ?wsdl endpoint
+
+ Args:
+ service_name: Name of the SOAP service
+ namespace: Target namespace for SOAP messages
+ handler: Handler function for ProcessDocument operation
+ wsdl_path: Optional path to WSDL file to serve
+ """
+
+ router = APIRouter()
+
+ # Add WSDL endpoint if path provided
+ if wsdl_path:
+
+ @router.get("/")
+ async def get_wsdl(request: Request):
+ """Serve WSDL when ?wsdl query parameter is present"""
+ if "wsdl" in request.query_params:
+ try:
+ wsdl_file = Path(wsdl_path)
+ if not wsdl_file.exists():
+ logger.error(f"WSDL file not found: {wsdl_path}")
+ return Response(content="WSDL file not found", status_code=404)
+
+ wsdl_content = wsdl_file.read_text(encoding="utf-8")
+
+ # Replace placeholder location with actual server URL
+ base_url = str(request.base_url).rstrip("/")
+ path = request.url.path.rstrip("/")
+ actual_location = f"{base_url}{path}"
+
+ # Replace the location in WSDL
+ wsdl_content = wsdl_content.replace(
+ "http://localhost:8000/notereader", actual_location
+ )
+ wsdl_content = wsdl_content.replace(
+ "http://127.0.0.1:8000/notereader/", actual_location
+ )
+
+ return Response(
+ content=wsdl_content, media_type="text/xml; charset=utf-8"
+ )
+ except Exception:
+ logger.exception("Error serving WSDL")
+ return Response(
+ content="An internal error has occurred while serving WSDL.",
+ status_code=500,
+ )
+
+ @router.post("/", summary=f"{service_name} SOAP entrypoint")
+ async def soap_entrypoint(request: Request):
+ raw = await request.body()
+ try:
+ parser = ET.XMLParser(resolve_entities=False)
+ xml = ET.fromstring(raw, parser=parser)
+ except ET.XMLSyntaxError as e:
+ logger.exception("Invalid XML received")
+ return Response(
+ content=build_soap_fault("Client", f"Invalid XML: {str(e)}"),
+ media_type="text/xml; charset=utf-8",
+ status_code=400,
+ )
+
+ # Find Body
+ body = xml.find("soap:Body", namespaces=NSMAP)
+ if body is None or len(body) == 0:
+ return Response(
+ content=build_soap_fault("Client", "Missing SOAP Body"),
+ media_type="text/xml; charset=utf-8",
+ status_code=400,
+ )
+
+ # The operation element (document style) should be the first child of Body
+ operation_el = body[0]
+ # operation local name (strip namespace if present)
+ operation_name = operation_el.tag.split("}")[-1]
+
+ if operation_name != "ProcessDocument":
+ return Response(
+ content=build_soap_fault(
+ "Client", f"Unknown operation: {operation_name}"
+ ),
+ media_type="text/xml; charset=utf-8",
+ status_code=400,
+ )
+
+ # Extract fields (namespace-agnostic)
+ soap_params: Dict[str, Optional[str]] = {}
+ for child in operation_el:
+ local = child.tag.split("}")[-1]
+ soap_params[local] = safe_text_of(child)
+
+ logger.info(f"Received SOAP request with params: {list(soap_params.keys())}")
+
+ # Map WSDL element names to CdaRequest field names
+ # WSDL: SessionID, WorkType, OrganizationID, Document
+ mapped = {
+ "session_id": soap_params.get("SessionID"),
+ "work_type": soap_params.get("WorkType"),
+ "organization_id": soap_params.get("OrganizationID"),
+ # Document may be large; attempt to coerce/ decode various forms
+ "document": coerce_document_value(soap_params.get("Document")),
+ }
+
+ # Validate minimal required fields
+ missing = []
+ if not mapped["session_id"]:
+ missing.append("SessionID")
+ if not mapped["work_type"]:
+ missing.append("WorkType")
+ if not mapped["organization_id"]:
+ missing.append("OrganizationID")
+ if not mapped["document"]:
+ missing.append("Document")
+
+ if missing:
+ return Response(
+ content=build_soap_fault(
+ "Client", f"Missing required parameter(s): {', '.join(missing)}"
+ ),
+ media_type="text/xml; charset=utf-8",
+ status_code=400,
+ )
+
+ # Build CdaRequest pydantic model
+ try:
+ request_obj = CdaRequest(
+ session_id=mapped["session_id"],
+ work_type=mapped["work_type"],
+ organization_id=mapped["organization_id"],
+ document=mapped["document"],
+ )
+ except Exception as e:
+ logger.exception("Failed to construct CdaRequest")
+ return Response(
+ content=build_soap_fault(
+ "Client", f"Invalid request parameters: {str(e)}"
+ ),
+ media_type="text/xml; charset=utf-8",
+ status_code=400,
+ )
+
+ # Call the provided handler (user-provided ProcessDocument function)
+ try:
+ resp_obj = handler(request_obj)
+ logger.info(
+ f"Handler returned response: document_length={len(resp_obj.document) if resp_obj.document else 0}, error={resp_obj.error}"
+ )
+
+ # IMPORTANT: Response Document must be base64-encoded per protocol
+ # Check if handler returned plain text or already-encoded base64
+ if resp_obj.document and isinstance(resp_obj.document, str):
+ # Try to decode as base64 to test if it's already encoded
+ is_already_base64 = False
+ try:
+ # If this succeeds, it's valid base64
+ base64.b64decode(resp_obj.document, validate=True)
+ is_already_base64 = True
+ logger.info("Document is already base64-encoded")
+ except Exception:
+ # Not valid base64, need to encode it
+ pass
+
+ if not is_already_base64:
+ # Encode the plain text to base64
+ resp_obj.document = base64.b64encode(
+ resp_obj.document.encode("utf-8")
+ ).decode("ascii")
+ logger.info("Encoded plain text document to base64")
+
+ except Exception as e:
+ logger.exception("Handler threw exception")
+ # Server fault
+ return Response(
+ content=build_soap_fault(
+ "Server", f"Server error processing request: {str(e)}"
+ ),
+ media_type="text/xml; charset=utf-8",
+ status_code=500,
+ )
+
+ # Convert response object to SOAP response element
+ # IMPORTANT: Match Spyne WSDL structure WITH ProcessDocumentResult wrapper!
+ #
+ #
+ # base64string
+ # string
+ #
+ #
+
+ # Create response with explicit namespace map including tns prefix
+ nsmap_response = {"tns": namespace}
+ resp_el = ET.Element(
+ ET.QName(namespace, "ProcessDocumentResponse"), nsmap=nsmap_response
+ )
+
+ # Add the ProcessDocumentResult wrapper (required by Spyne WSDL)
+ result_wrapper = ET.SubElement(
+ resp_el, ET.QName(namespace, "ProcessDocumentResult")
+ )
+
+ # Document element (optional) - as base64-encoded string
+ doc_el = ET.SubElement(result_wrapper, ET.QName(namespace, "Document"))
+ if resp_obj.document is not None:
+ if isinstance(resp_obj.document, str):
+ doc_el.text = resp_obj.document
+ elif isinstance(resp_obj.document, bytes):
+ # If bytes, decode to ASCII string (base64 is ASCII-safe)
+ doc_el.text = resp_obj.document.decode("ascii")
+ else:
+ doc_el.text = str(resp_obj.document)
+
+ # Error element (optional)
+ err_el = ET.SubElement(result_wrapper, ET.QName(namespace, "Error"))
+ if resp_obj.error is not None:
+ err_el.text = str(resp_obj.error)
+
+ envelope_bytes = build_soap_envelope(resp_el)
+
+ logger.info(
+ f"Sending SOAP response with document length: {len(resp_obj.document) if resp_obj.document else 0}"
+ )
+
+ return Response(
+ content=envelope_bytes,
+ media_type="text/xml; charset=utf-8",
+ status_code=200,
+ )
+
+ return router
diff --git a/healthchain/gateway/soap/notereader.py b/healthchain/gateway/soap/notereader.py
index 9bbd11ad..4d582ce6 100644
--- a/healthchain/gateway/soap/notereader.py
+++ b/healthchain/gateway/soap/notereader.py
@@ -9,18 +9,15 @@
from typing import Any, Callable, Dict, Optional, TypeVar, Union
-from pydantic import BaseModel
-from spyne import Application
-from spyne.protocol.soap import Soap11
-from spyne.server.wsgi import WsgiApplication
+from pydantic import BaseModel, model_validator
+from pathlib import Path
from healthchain.gateway.base import BaseProtocolHandler
from healthchain.gateway.events.dispatcher import EventDispatcher
from healthchain.gateway.soap.events import create_notereader_event
-from healthchain.gateway.soap.utils.epiccds import CDSServices
-from healthchain.gateway.soap.utils.model import ClientFault, ServerFault
from healthchain.models.requests.cdarequest import CdaRequest
from healthchain.models.responses.cdaresponse import CdaResponse
+from healthchain.gateway.soap.fastapi_server import create_fastapi_soap_router
logger = logging.getLogger(__name__)
@@ -36,6 +33,28 @@ class NoteReaderConfig(BaseModel):
namespace: str = "urn:epic-com:Common.2013.Services"
system_type: str = "EHR_CDA"
default_mount_path: str = "/notereader"
+ wsdl_path: Optional[str] = None
+
+ @model_validator(mode="after")
+ def resolve_wsdl_path(self):
+ """Auto-resolve WSDL path relative to healthchain package if not explicitly set"""
+ if self.wsdl_path is None:
+ try:
+ import healthchain
+
+ healthchain_root = Path(healthchain.__file__).parent
+ wsdl_file = healthchain_root / "templates" / "epic_service.wsdl"
+
+ if wsdl_file.exists():
+ self.wsdl_path = str(wsdl_file)
+ print(f"✅ Auto-detected WSDL at: {self.wsdl_path}")
+ else:
+ print(f"⚠️ WSDL not found at: {wsdl_file}")
+ print(" SOAP service will work but ?wsdl endpoint unavailable")
+ except Exception as e:
+ print(f"⚠️ Could not auto-detect WSDL path: {e}")
+
+ return self
class NoteReaderService(BaseProtocolHandler[CdaRequest, CdaResponse]):
@@ -60,8 +79,11 @@ def process_document(request: CdaRequest) -> CdaResponse:
error=None
)
- # Register the service with the API
- app.register_service(service)
+ # Get the FastAPI router
+ router = service.create_fastapi_router()
+
+ # Mount in FastAPI app
+ app.include_router(router, prefix="/notereader")
```
"""
@@ -101,6 +123,13 @@ def method(self, method_name: str) -> Callable:
Returns:
Decorator function that registers the handler
+
+ Example:
+ ```python
+ @service.method("ProcessDocument")
+ def process_document(request: CdaRequest) -> CdaResponse:
+ return CdaResponse(document="processed", error=None)
+ ```
"""
def decorator(handler):
@@ -113,6 +142,9 @@ def handle(self, operation: str, **params) -> Union[CdaResponse, Dict]:
"""
Process a SOAP request using registered handlers.
+ This method provides backward compatibility for existing code
+ that calls handle() directly instead of going through the SOAP router.
+
Args:
operation: The SOAP method name e.g. ProcessDocument
**params: Either a CdaRequest object or raw parameters
@@ -125,12 +157,12 @@ def handle(self, operation: str, **params) -> Union[CdaResponse, Dict]:
logger.warning(f"No handler registered for operation: {operation}")
return CdaResponse(document="", error=f"No handler for {operation}")
- # Extract or build the request object
+ # Extract the request object
request = self._extract_request(operation, params)
if not request:
return CdaResponse(document="", error="Invalid request parameters")
- # Execute the handler with the request
+ # Execute the handler
return self._execute_handler(operation, request)
def _extract_request(self, operation: str, params: Dict) -> Optional[CdaRequest]:
@@ -145,7 +177,7 @@ def _extract_request(self, operation: str, params: Dict) -> Optional[CdaRequest]
CdaRequest object or None if request couldn't be constructed
"""
try:
- # Case 1: Direct CdaRequest passed as a parameter
+ # Case 1: Direct CdaRequest passed as 'request' parameter
if "request" in params and isinstance(params["request"], CdaRequest):
return params["request"]
@@ -184,92 +216,122 @@ def _execute_handler(self, operation: str, request: CdaRequest) -> CdaResponse:
result = handler(request)
# Process the result
- return self._process_result(result)
+ response = self._process_result(result)
+
+ # Emit event if enabled
+ if self.use_events:
+ self._emit_document_event(operation, request, response)
+
+ return response
except Exception as e:
logger.error(f"Error in {operation} handler: {str(e)}", exc_info=True)
- return CdaResponse(document="", error=str(e))
+ error_response = CdaResponse(document="", error=str(e))
- def _process_result(self, result: Any) -> CdaResponse:
- """
- Convert handler result to a CdaResponse.
+ # Emit event for error case too
+ if self.use_events:
+ self._emit_document_event(operation, request, error_response)
- Args:
- result: The result returned by the handler
+ return error_response
- Returns:
- CdaResponse object
+ def create_fastapi_router(self):
"""
- # If the result is already a CdaResponse, return it
- if isinstance(result, CdaResponse):
- return result
- try:
- # Try to convert to CdaResponse if possible
- if isinstance(result, dict):
- return CdaResponse(**result)
- logger.warning(f"Unexpected result type from handler: {type(result)}")
- return CdaResponse(document=str(result), error=None)
- except Exception as e:
- logger.error(f"Error processing result to CdaResponse: {str(e)}")
- return CdaResponse(document="", error="Invalid response format")
-
- def create_wsgi_app(self) -> WsgiApplication:
- """
- Creates a WSGI application for the SOAP service.
+ Creates a FastAPI router for the SOAP service.
- This method sets up the WSGI application with proper SOAP protocol
- configuration and handler registration.
+ This method sets up the FastAPI router with proper SOAP protocol
+ configuration and handler registration. The router includes event
+ emission if an event dispatcher is configured.
Returns:
- A configured WsgiApplication ready to mount in FastAPI
+ APIRouter: A configured FastAPI router ready to mount
Raises:
ValueError: If no ProcessDocument handler is registered
- """
- # TODO: Maybe you want to be more explicit that you only need to register a handler for ProcessDocument
- # Can you register multiple services in the same app? Who knows?? Let's find out!!
+ Example:
+ ```python
+ service = NoteReaderService()
+
+ @service.method("ProcessDocument")
+ def handler(req):
+ return CdaResponse(document="ok", error=None)
+
+ router = service.create_fastapi_router()
+ app.include_router(router, prefix="/soap")
+ ```
+ """
if "ProcessDocument" not in self._handlers:
raise ValueError(
"No ProcessDocument handler registered. "
- "You must register a handler before creating the WSGI app. "
"Use @service.method('ProcessDocument') to register a handler."
)
- # Create adapter for SOAP service integration
- def service_adapter(cda_request: CdaRequest) -> CdaResponse:
- # This calls the handle method to process the request
+ # Get the base handler
+ base_handler = self._handlers["ProcessDocument"]
+
+ # Create a wrapper that handles events and error processing
+ def handler_with_events(request: CdaRequest) -> CdaResponse:
+ """Wrapper that adds event emission to the handler"""
try:
- # This will be executed synchronously in the SOAP context
- handler = self._handlers["ProcessDocument"]
- result = handler(cda_request)
- processed_result = self._process_result(result)
+ # Call the user's handler
+ result = base_handler(request)
+
+ # Process result to ensure it's a CdaResponse
+ response = self._process_result(result)
+
+ # Emit event if enabled (even if dispatcher is None, let emit_event handle it)
+ if self.use_events:
+ self._emit_document_event("ProcessDocument", request, response)
+
+ return response
- # Emit event if we have an event dispatcher
- if self.events.dispatcher and self.use_events:
+ except Exception as e:
+ logger.error(
+ f"Error in ProcessDocument handler: {str(e)}", exc_info=True
+ )
+ error_response = CdaResponse(document="", error=str(e))
+
+ # Emit event for error case too
+ if self.use_events:
self._emit_document_event(
- "ProcessDocument", cda_request, processed_result
+ "ProcessDocument", request, error_response
)
- return processed_result
- except Exception as e:
- logger.error(f"Error in SOAP service adapter: {str(e)}")
- return CdaResponse(document="", error=str(e))
-
- # Assign the service adapter function to CDSServices._service
- CDSServices._service = service_adapter
-
- # Configure the Spyne application
- application = Application(
- [CDSServices],
- name=self.config.service_name,
- tns=self.config.namespace,
- in_protocol=Soap11(validator="lxml"),
- out_protocol=Soap11(),
- classes=[ServerFault, ClientFault],
+ return error_response
+
+ # Create and return the FastAPI router
+ return create_fastapi_soap_router(
+ service_name=self.config.service_name,
+ namespace=self.config.namespace,
+ handler=handler_with_events,
+ wsdl_path=self.config.wsdl_path, # Pass WSDL path
)
- # Create WSGI app
- return WsgiApplication(application)
+
+ def _process_result(self, result: Any) -> CdaResponse:
+ """
+ Convert handler result to a CdaResponse.
+
+ Args:
+ result: The result returned by the handler
+
+ Returns:
+ CdaResponse object
+ """
+ # If the result is already a CdaResponse, return it
+ if isinstance(result, CdaResponse):
+ return result
+
+ try:
+ # Try to convert to CdaResponse if possible
+ if isinstance(result, dict):
+ return CdaResponse(**result)
+
+ logger.warning(f"Unexpected result type from handler: {type(result)}")
+ return CdaResponse(document=str(result), error=None)
+
+ except Exception as e:
+ logger.error(f"Error processing result to CdaResponse: {str(e)}")
+ return CdaResponse(document="", error="Invalid response format")
def _emit_document_event(
self, operation: str, request: CdaRequest, response: CdaResponse
diff --git a/healthchain/gateway/soap/utils/epiccds.py b/healthchain/gateway/soap/utils/epiccds.py
deleted file mode 100644
index 9b64ef88..00000000
--- a/healthchain/gateway/soap/utils/epiccds.py
+++ /dev/null
@@ -1,88 +0,0 @@
-import logging
-
-from spyne import rpc, ServiceBase, Unicode, ByteArray
-from healthchain.models.requests.cdarequest import CdaRequest
-
-from healthchain.gateway.soap.utils.model import Response, ClientFault, ServerFault
-
-
-log = logging.getLogger(__name__)
-
-
-# I'm not happy about this name either but that's what Epic wants
-class CDSServices(ServiceBase):
- """
- Represents a CDSServices object that provides methods for processing documents.
- """
-
- _service = None
-
- # TODO The _in_arg_names are order sensitive here so need to find a way to make this
- # configurable and easier to catch errors
- @rpc(
- Unicode,
- Unicode,
- Unicode,
- ByteArray,
- _in_arg_names={
- "sessionId": "SessionID",
- "workType": "WorkType",
- "organizationId": "OrganizationID",
- "document": "Document",
- },
- _wsdl_part_name="parameters",
- _returns=Response,
- _faults=[ClientFault, ServerFault],
- )
- def ProcessDocument(ctx, sessionId, workType, organizationId, document):
- """
- Processes a document using the specified session ID, work type, organization ID, and document.
-
- Args:
- ctx (object): The context object.
- sessionId (str): The session ID.
- workType (str): The work type.
- organizationId (str): The organization ID.
- document (bytes): The document to be processed.
-
- Returns:
- Response: The response object containing the processed document and any errors.
-
- Raises:
- ClientFault: If any of the required parameters are missing.
- ServerFault: If there is a server processing error.
- ServerFault: If an unexpected error occurs.
- """
- try:
- if not sessionId:
- raise ClientFault("Missing required parameter: sessionId")
- if not workType:
- raise ClientFault("Missing required parameter: workType")
- if not organizationId:
- raise ClientFault("Missing required parameter: organizationId")
- if not document:
- raise ClientFault("Missing required parameter: document")
-
- request_document_xml = document[0].decode("UTF-8")
-
- cda_request = CdaRequest(document=request_document_xml)
- cda_response = ctx.descriptor.service_class._service(cda_request)
-
- if cda_response.error:
- raise ServerFault(f"Server processing error: {cda_response.error}")
-
- response = Response(
- Document=cda_response.document.encode("UTF-8"), Error=cda_response.error
- )
-
- return response
-
- except ClientFault as e:
- # Re-raise client faults
- raise e
- except ServerFault as e:
- # Re-raise server faults
- raise e
- except Exception as e:
- # Catch all other exceptions and raise as server faults
- raise ServerFault(f"An unexpected error occurred: {str(e)}")
diff --git a/healthchain/gateway/soap/utils/model/__init__.py b/healthchain/gateway/soap/utils/model/__init__.py
index 1da9fdf9..c2f53efb 100644
--- a/healthchain/gateway/soap/utils/model/__init__.py
+++ b/healthchain/gateway/soap/utils/model/__init__.py
@@ -1,5 +1,3 @@
-from .epicserverfault import ServerFault
-from .epicclientfault import ClientFault
-from .epicresponse import Response
+from .fault_model import ClientFault, ServerFault, Response
__all__ = ["ServerFault", "ClientFault", "Response"]
diff --git a/healthchain/gateway/soap/utils/model/epicclientfault.py b/healthchain/gateway/soap/utils/model/epicclientfault.py
deleted file mode 100644
index 45023a82..00000000
--- a/healthchain/gateway/soap/utils/model/epicclientfault.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from spyne import Unicode, Fault
-
-
-class ClientFault(Fault):
- __namespace__ = "urn:epicsystems.com:Interconnect.2004-05.Faults"
- Type = Unicode
diff --git a/healthchain/gateway/soap/utils/model/epicresponse.py b/healthchain/gateway/soap/utils/model/epicresponse.py
deleted file mode 100644
index 3748e3f4..00000000
--- a/healthchain/gateway/soap/utils/model/epicresponse.py
+++ /dev/null
@@ -1,8 +0,0 @@
-from spyne import ComplexModel
-from spyne import Unicode, ByteArray
-
-
-class Response(ComplexModel):
- __namespace__ = "urn:epic-com:Common.2013.Services"
- Document = ByteArray
- Error = Unicode
diff --git a/healthchain/gateway/soap/utils/model/epicserverfault.py b/healthchain/gateway/soap/utils/model/epicserverfault.py
deleted file mode 100644
index 83929cbe..00000000
--- a/healthchain/gateway/soap/utils/model/epicserverfault.py
+++ /dev/null
@@ -1,6 +0,0 @@
-from spyne import Unicode, Fault
-
-
-class ServerFault(Fault):
- __namespace__ = "urn:epicsystems.com:Interconnect.2004-05.Faults"
- Type = Unicode
diff --git a/healthchain/gateway/soap/utils/model/fault_model.py b/healthchain/gateway/soap/utils/model/fault_model.py
new file mode 100644
index 00000000..604fbe1b
--- /dev/null
+++ b/healthchain/gateway/soap/utils/model/fault_model.py
@@ -0,0 +1,76 @@
+from dataclasses import dataclass
+from typing import Optional
+
+SOAP_FAULT_NS = "http://schemas.xmlsoap.org/soap/envelope/"
+CUSTOM_FAULT_NS = "urn:epicsystems.com:Interconnect.2004-05.Faults"
+RESPONSE_NS = "urn:epic-com:Common.2013.Services"
+
+
+@dataclass
+class ServerFault(Exception):
+ message: str
+ type: Optional[str] = None
+ code: str = "Server"
+
+ def to_xml(self) -> str:
+ """
+ Create a SOAP Fault XML block representing this error.
+ """
+ return f"""
+
+ soap:{self.code}
+ {self.message}
+
+
+ {self.type or ""}
+
+
+
+""".strip()
+
+
+@dataclass
+class ClientFault(Exception):
+ message: str
+ code: str = "Client"
+
+ def to_xml(self) -> str:
+ return f"""
+
+ soap:{self.code}
+ {self.message}
+
+""".strip()
+
+
+@dataclass
+class Response:
+ """
+ Replacement for Spyne ComplexModel Response.
+
+ Fields:
+ Document: bytes
+ Error: Optional[str]
+ """
+
+ Document: bytes
+ Error: Optional[str] = None
+
+ def to_xml(self) -> str:
+ """
+ Create the SOAP XML body for a successful response.
+ Mirrors Spyne's output shape.
+ """
+
+ # Escape XML content (very important)
+ from xml.sax.saxutils import escape
+
+ doc_value = escape(self.Document.decode("utf-8")) if self.Document else ""
+ error_value = escape(self.Error) if self.Error else ""
+
+ return f"""
+
+ {doc_value}
+ {error_value}
+
+""".strip()
diff --git a/healthchain/gateway/soap/utils/wsgi.py b/healthchain/gateway/soap/utils/wsgi.py
index 0cf45dc1..e69de29b 100644
--- a/healthchain/gateway/soap/utils/wsgi.py
+++ b/healthchain/gateway/soap/utils/wsgi.py
@@ -1,43 +0,0 @@
-from spyne import Application
-from spyne.protocol.soap import Soap11
-from spyne.server.wsgi import WsgiApplication
-
-from typing import Callable
-
-from healthchain.gateway.soap.utils.epiccds import CDSServices
-from healthchain.gateway.soap.utils.model import ClientFault, ServerFault
-
-
-def start_wsgi(
- service: Callable,
- app_name: str = "ICDSServices",
- tns: str = "urn:epic-com:Common.2013.Services",
-):
- """
- Starts the WSGI application for the SOAP service.
-
- Args:
- service (Callable): The service function to be used.
- app_name (str, optional): The name of the application. Defaults to "ICDSServices".
- tns (str, optional): The target namespace for the SOAP service. Defaults to "urn:epic-com:Common.2013.Services".
-
- Returns:
- WsgiApplication: The WSGI application for the SOAP service.
-
- # TODO: Add support for custom document interfaces
- """
- CDSServices._service = service
-
- application = Application(
- [CDSServices],
- name=app_name,
- tns=tns,
- in_protocol=Soap11(validator="lxml"),
- out_protocol=Soap11(),
- classes=[ServerFault, ClientFault],
- # documents_container=CustomInterfaceDocuments,
- )
-
- wsgi_app = WsgiApplication(application)
-
- return wsgi_app
diff --git a/healthchain/sandbox/sandboxclient.py b/healthchain/sandbox/sandboxclient.py
index c3f59576..6919e8b3 100644
--- a/healthchain/sandbox/sandboxclient.py
+++ b/healthchain/sandbox/sandboxclient.py
@@ -46,7 +46,7 @@ class SandboxClient:
Load CDA file from path:
>>> client = SandboxClient(
- ... url="http://localhost:8000/notereader/fhir/",
+ ... url="http://localhost:8000/notereader/?wsdl",
... protocol="soap"
... )
>>> client.load_from_path("./data/clinical_note.xml")
diff --git a/healthchain/templates/epic_service.wsdl b/healthchain/templates/epic_service.wsdl
new file mode 100644
index 00000000..0c3cdd0c
--- /dev/null
+++ b/healthchain/templates/epic_service.wsdl
@@ -0,0 +1,84 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Processes a document using the specified session ID, work type, organization ID, and document. Args: ctx (object): The context object. sessionId (str): The session ID. workType (str): The work type. organizationId (str): The organization ID. document (bytes): The document to be processed. Returns: Response: The response object containing the processed document and any errors. Raises: ClientFault: If any of the required parameters are missing. ServerFault: If there is a server processing error. ServerFault: If an unexpected error occurs.
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pyproject.toml b/pyproject.toml
index 77f30c4b..9f43551e 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ authors = [
{ name = "Jennifer Jiang-Kells", email = "jenniferjiangkells@gmail.com" },
{ name = "Adam Kells", email = "adamjkells93@gmail.com" },
]
-requires-python = ">=3.10,<3.12"
+requires-python = ">=3.10,<3.13"
readme = "README.md"
license = { text = "Apache-2.0" }
keywords = [
diff --git a/scripts/healthchainapi_e2e_demo.py b/scripts/healthchainapi_e2e_demo.py
index 27483168..0fe18ced 100644
--- a/scripts/healthchainapi_e2e_demo.py
+++ b/scripts/healthchainapi_e2e_demo.py
@@ -414,7 +414,7 @@ def create_sandboxes():
# NoteReader Sandbox
notereader_client = SandboxClient(
- url=base_url + "/notereader/fhir/",
+ url=base_url + "/notereader/?wsdl",
workflow=CONFIG["workflows"]["notereader"],
protocol="soap",
)
diff --git a/tests/gateway/test_fast_api_soap_server.py b/tests/gateway/test_fast_api_soap_server.py
new file mode 100644
index 00000000..3b54dc76
--- /dev/null
+++ b/tests/gateway/test_fast_api_soap_server.py
@@ -0,0 +1,402 @@
+"""
+Tests for FastAPI-based SOAP server implementation.
+
+These tests replace the old Spyne-based tests and verify the new
+FastAPI SOAP router functionality.
+"""
+
+import base64
+import pytest
+import lxml.etree as ET
+from fastapi import FastAPI
+from fastapi.testclient import TestClient
+
+from healthchain.gateway.soap.notereader import NoteReaderService, NoteReaderConfig
+from healthchain.models.requests.cdarequest import CdaRequest
+from healthchain.models.responses.cdaresponse import CdaResponse
+
+
+# Namespace mappings for parsing SOAP responses
+SOAP_NAMESPACES = {
+ "soap": "http://schemas.xmlsoap.org/soap/envelope/",
+ "tns": "urn:epic-com:Common.2013.Services",
+}
+
+
+def parse_soap_response(response_content: bytes) -> ET._Element:
+ """Parse SOAP response XML and return the root element"""
+ return ET.fromstring(response_content)
+
+
+def get_document_from_response(response_content: bytes) -> str:
+ """Extract and decode the Document element from SOAP response"""
+ xml = parse_soap_response(response_content)
+ doc_element = xml.find(".//tns:Document", SOAP_NAMESPACES)
+ if doc_element is not None and doc_element.text:
+ # Document is base64 encoded per WSDL
+ return base64.b64decode(doc_element.text).decode("utf-8")
+ return ""
+
+
+def get_error_from_response(response_content: bytes) -> str:
+ """Extract the Error element text from SOAP response"""
+ xml = parse_soap_response(response_content)
+ error_element = xml.find(".//tns:Error", SOAP_NAMESPACES)
+ if error_element is not None:
+ return error_element.text or ""
+ return ""
+
+
+@pytest.fixture
+def notereader_service():
+ """Create a NoteReaderService instance for testing"""
+ return NoteReaderService(use_events=False)
+
+
+@pytest.fixture
+def app_with_soap(notereader_service):
+ """Create a FastAPI app with SOAP service mounted"""
+ app = FastAPI()
+
+ # Register a basic handler
+ @notereader_service.method("ProcessDocument")
+ def process_document(request: CdaRequest) -> CdaResponse:
+ return CdaResponse(document=f"Processed: {request.document}", error=None)
+
+ # Mount the router
+ router = notereader_service.create_fastapi_router()
+ app.include_router(router, prefix="/soap")
+
+ return app
+
+
+@pytest.fixture
+def client(app_with_soap):
+ """Create a test client"""
+ return TestClient(app_with_soap)
+
+
+def build_soap_request(
+ session_id=None, work_type=None, organization_id=None, document=None
+):
+ """Helper to build SOAP request XML - only includes provided parameters"""
+ parts = [
+ '',
+ '',
+ " ",
+ ' ',
+ ]
+
+ if session_id is not None:
+ parts.append(f" {session_id}")
+ if work_type is not None:
+ parts.append(f" {work_type}")
+ if organization_id is not None:
+ parts.append(f" {organization_id}")
+ if document is not None:
+ parts.append(f" {document}")
+
+ parts.extend([" ", " ", ""])
+
+ return "\n".join(parts)
+
+
+class TestSOAPValidation:
+ """Test SOAP request validation"""
+
+ def test_debug_request(self, client):
+ """Debug test to see what's being sent and received"""
+ soap_request = build_soap_request(
+ session_id="12345",
+ work_type="TestWork",
+ organization_id="OrgID",
+ document="test",
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ assert response.status_code == 200
+
+ def test_missing_session_id(self, client):
+ """Test that missing SessionID returns SOAP fault"""
+ soap_request = build_soap_request(
+ work_type="WorkType", organization_id="OrgID", document="test"
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ assert response.status_code == 400
+ assert b"faultcode" in response.content
+ assert b"SessionID" in response.content
+
+ def test_missing_work_type(self, client):
+ """Test that missing WorkType returns SOAP fault"""
+ soap_request = build_soap_request(
+ session_id="12345", organization_id="OrgID", document="test"
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ assert response.status_code == 400
+ assert b"faultcode" in response.content
+ assert b"WorkType" in response.content
+
+ def test_missing_organization_id(self, client):
+ """Test that missing OrganizationID returns SOAP fault"""
+ soap_request = build_soap_request(
+ session_id="12345", work_type="WorkType", document="test"
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ assert response.status_code == 400
+ assert b"faultcode" in response.content
+ assert b"OrganizationID" in response.content
+
+ def test_missing_document(self, client):
+ """Test that missing Document returns SOAP fault"""
+ soap_request = build_soap_request(
+ session_id="12345", work_type="WorkType", organization_id="OrgID"
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ assert response.status_code == 400
+ assert b"faultcode" in response.content
+ assert b"Document" in response.content
+
+ def test_invalid_xml(self, client):
+ """Test that invalid XML returns SOAP fault"""
+ invalid_xml = "This is not XML"
+
+ response = client.post("/soap/", content=invalid_xml)
+
+ assert response.status_code == 400
+ assert b"faultcode" in response.content
+ assert b"Invalid XML" in response.content
+
+ def test_missing_soap_body(self, client):
+ """Test that missing SOAP body returns fault"""
+ soap_request = """
+
+"""
+
+ response = client.post("/soap/", content=soap_request)
+
+ assert response.status_code == 400
+ assert b"Missing SOAP Body" in response.content
+
+
+class TestProcessDocument:
+ """Test ProcessDocument operation"""
+
+ def test_successful_request(self, client):
+ """Test successful document processing"""
+ import base64
+ import lxml.etree as ET
+
+ # Encode the document as base64 (as required by WSDL)
+ plain_document = "test data"
+ encoded_document = base64.b64encode(plain_document.encode("utf-8")).decode(
+ "ascii"
+ )
+
+ soap_request = build_soap_request(
+ session_id="12345",
+ work_type="TestWork",
+ organization_id="OrgID",
+ document=encoded_document, # Send base64-encoded
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ assert response.status_code == 200
+ assert b"ProcessDocumentResponse" in response.content
+
+ # Parse response to check structure
+ xml = ET.fromstring(response.content)
+
+ # Find the Document element (it's base64 encoded per WSDL)
+ namespaces = {
+ "soap": "http://schemas.xmlsoap.org/soap/envelope/",
+ "tns": "urn:epic-com:Common.2013.Services",
+ }
+
+ doc_element = xml.find(".//tns:Document", namespaces)
+ assert doc_element is not None, "Document element should be present"
+
+ # Decode base64 content
+ if doc_element.text:
+ decoded = base64.b64decode(doc_element.text).decode("utf-8")
+ assert (
+ "Processed:" in decoded
+ ), f"Expected 'Processed:' in decoded document, got: {decoded}"
+ assert plain_document in decoded, "Expected original document in response"
+
+ def test_handler_returns_error(self):
+ """Test when handler returns a CdaResponse with error"""
+ app = FastAPI()
+ service = NoteReaderService(use_events=False)
+
+ @service.method("ProcessDocument")
+ def error_handler(request: CdaRequest) -> CdaResponse:
+ return CdaResponse(document="", error="Processing failed")
+
+ router = service.create_fastapi_router()
+ app.include_router(router, prefix="/soap")
+ client = TestClient(app)
+
+ soap_request = build_soap_request(
+ session_id="12345",
+ work_type="TestWork",
+ organization_id="OrgID",
+ document="test",
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ # Should return 200 with error in response
+ assert response.status_code == 200
+ assert b"" in response.content or b"" in response.content
+ assert b"Processing failed" in response.content
+
+ def test_handler_raises_exception(self):
+ """Test when handler raises an exception"""
+ app = FastAPI()
+ service = NoteReaderService(use_events=False)
+
+ @service.method("ProcessDocument")
+ def failing_handler(request: CdaRequest) -> CdaResponse:
+ raise ValueError("Something went wrong")
+
+ router = service.create_fastapi_router()
+ app.include_router(router, prefix="/soap")
+ client = TestClient(app)
+
+ soap_request = build_soap_request(
+ session_id="12345",
+ work_type="TestWork",
+ organization_id="OrgID",
+ document="test",
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ # Should return 200 with error in response (error handled gracefully)
+ assert response.status_code == 200
+ assert b"" in response.content or b"" in response.content
+ assert b"Something went wrong" in response.content
+
+ def test_document_with_special_characters(self, client):
+ """Test document with XML special characters"""
+ # Note: In real SOAP, this would be CDATA or encoded
+ soap_request = build_soap_request(
+ session_id="12345",
+ work_type="TestWork",
+ organization_id="OrgID",
+ document="<xml>test</xml>",
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ assert response.status_code == 200
+
+
+class TestServiceConfiguration:
+ """Test service configuration and setup"""
+
+ def test_no_handler_registered(self):
+ """Test that creating router without handler raises error"""
+ service = NoteReaderService(use_events=False)
+
+ with pytest.raises(ValueError) as exc_info:
+ service.create_fastapi_router()
+
+ assert "No ProcessDocument handler registered" in str(exc_info.value)
+
+ def test_custom_config(self):
+ """Test service with custom configuration"""
+ config = NoteReaderConfig(
+ service_name="CustomService",
+ namespace="urn:custom:namespace",
+ system_type="CUSTOM_SYSTEM",
+ )
+ service = NoteReaderService(config=config, use_events=False)
+
+ metadata = service.get_metadata()
+
+ assert metadata["soap_service"] == "CustomService"
+ assert metadata["namespace"] == "urn:custom:namespace"
+ assert metadata["system_type"] == "CUSTOM_SYSTEM"
+
+ def test_handler_decorator(self):
+ """Test that method decorator properly registers handler"""
+ service = NoteReaderService(use_events=False)
+
+ @service.method("ProcessDocument")
+ def my_handler(request: CdaRequest) -> CdaResponse:
+ return CdaResponse(document="test", error=None)
+
+ # Should be able to create router now
+ router = service.create_fastapi_router()
+ assert router is not None
+
+ def test_no_events_when_disabled(self):
+ """Test that events are not emitted when disabled"""
+ service = NoteReaderService(use_events=False)
+
+ # Mock the emit_event method to track calls
+ events_emitted = []
+
+ def mock_emit_event(*args, **kwargs):
+ events_emitted.append({"args": args, "kwargs": kwargs})
+
+ service.events.emit_event = mock_emit_event
+
+ @service.method("ProcessDocument")
+ def handler(request: CdaRequest) -> CdaResponse:
+ return CdaResponse(document="processed", error=None)
+
+ app = FastAPI()
+ router = service.create_fastapi_router()
+ app.include_router(router, prefix="/soap")
+ client = TestClient(app)
+
+ soap_request = build_soap_request(
+ session_id="12345",
+ work_type="TestWork",
+ organization_id="OrgID",
+ document="test",
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ assert response.status_code == 200
+
+ # When use_events=False, emit_event should not be called
+ assert (
+ len(events_emitted) == 0
+ ), f"Expected 0 events when disabled, got {len(events_emitted)}"
+
+
+class TestDocumentCoercion:
+ """Test document value coercion"""
+
+ def test_base64_encoded_document(self, client):
+ """Test handling of base64-encoded documents"""
+ import base64
+
+ original_doc = "test"
+ encoded_doc = base64.b64encode(original_doc.encode()).decode()
+
+ soap_request = build_soap_request(
+ session_id="12345",
+ work_type="TestWork",
+ organization_id="OrgID",
+ document=encoded_doc,
+ )
+
+ response = client.post("/soap/", content=soap_request)
+
+ # Should handle base64 decoding
+ assert response.status_code == 200
diff --git a/tests/gateway/test_notereader.py b/tests/gateway/test_notereader.py
index 60caccce..4a06fc20 100644
--- a/tests/gateway/test_notereader.py
+++ b/tests/gateway/test_notereader.py
@@ -1,5 +1,5 @@
import pytest
-from unittest.mock import patch, MagicMock
+from unittest.mock import MagicMock
from healthchain.gateway.soap.notereader import (
NoteReaderService,
@@ -7,6 +7,7 @@
)
from healthchain.models.requests import CdaRequest
from healthchain.models.responses.cdaresponse import CdaResponse
+from fastapi import APIRouter
@pytest.mark.parametrize(
@@ -110,13 +111,8 @@ def test_notereader_gateway_process_result():
assert result.document == "test_dict"
-@patch("healthchain.gateway.soap.notereader.Application")
-@patch("healthchain.gateway.soap.notereader.WsgiApplication")
-def test_notereader_gateway_create_wsgi_app(mock_wsgi, mock_application):
- """Test WSGI app creation for SOAP service"""
- mock_wsgi_instance = MagicMock()
- mock_wsgi.return_value = mock_wsgi_instance
-
+def test_notereader_gateway_create_fastapi_router():
+ """Test that NoteReaderService creates a valid FastAPI router"""
gateway = NoteReaderService()
# Register required ProcessDocument handler
@@ -124,13 +120,24 @@ def test_notereader_gateway_create_wsgi_app(mock_wsgi, mock_application):
def process_document(request):
return CdaResponse(document="processed", error=None)
- # Create WSGI app
- wsgi_app = gateway.create_wsgi_app()
+ # Create FastAPI router
+ router = gateway.create_fastapi_router()
+
+ # Verify it returns an APIRouter
+ assert isinstance(router, APIRouter)
+
+ # Verify the router has routes registered
+ routes = [route for route in router.routes]
+ assert len(routes) > 0, "Router should have routes"
- # Verify WSGI app was created
- assert wsgi_app is mock_wsgi_instance
- mock_wsgi.assert_called_once()
- mock_application.assert_called_once()
+ # Verify POST route exists (for SOAP requests)
+ post_routes = [r for r in routes if hasattr(r, "methods") and "POST" in r.methods]
+ assert len(post_routes) > 0, "Router should have POST route for SOAP"
+
+ # Verify GET route exists if WSDL is configured (for ?wsdl endpoint)
+ get_routes = [r for r in routes if hasattr(r, "methods") and "GET" in r.methods]
+ if gateway.config.wsdl_path:
+ assert len(get_routes) > 0, "Router should have GET route for WSDL"
# Verify we can get the default mount path from config
config = gateway.config
@@ -138,13 +145,67 @@ def process_document(request):
assert config.default_mount_path == "/notereader"
-def test_notereader_gateway_create_wsgi_app_no_handler():
+def test_notereader_gateway_create_fastapi_router_without_handler():
+ """Test that creating router without handler raises ValueError"""
+ gateway = NoteReaderService()
+
+ # Should raise error if no ProcessDocument handler registered
+ with pytest.raises(ValueError, match="No ProcessDocument handler registered"):
+ gateway.create_fastapi_router()
+
+
+@pytest.mark.asyncio
+async def test_notereader_gateway_soap_request_processing():
+ """Test that the router can process a SOAP request"""
+ from fastapi.testclient import TestClient
+ from fastapi import FastAPI
+
+ gateway = NoteReaderService()
+
+ @gateway.method("ProcessDocument")
+ def process_document(request):
+ # Return a CdaResponse with base64-encoded document
+ import base64
+
+ doc = base64.b64encode(b"processed").decode("ascii")
+ return CdaResponse(document=doc, error=None)
+
+ router = gateway.create_fastapi_router()
+ app = FastAPI()
+ app.include_router(router, prefix="/notereader")
+
+ client = TestClient(app)
+
+ # Create a minimal SOAP request
+ soap_request = """
+
+
+
+ test-session
+ 1
+ TEST
+ PHhtbD50ZXN0PC94bWw+
+
+
+ """
+
+ response = client.post(
+ "/notereader/", content=soap_request, headers={"Content-Type": "text/xml"}
+ )
+
+ assert response.status_code == 200
+ assert b"ProcessDocumentResponse" in response.content
+ assert b"ProcessDocumentResult" in response.content
+
+
+def test_notereader_gateway_create_fastapi_router_no_handler():
"""Test WSGI app creation fails without ProcessDocument handler"""
gateway = NoteReaderService()
# No handler registered - should raise ValueError
with pytest.raises(ValueError):
- gateway.create_wsgi_app()
+ gateway.create_fastapi_router()
def test_notereader_gateway_get_metadata():
diff --git a/tests/gateway/test_soap_server.py b/tests/gateway/test_soap_server.py
deleted file mode 100644
index 569caff4..00000000
--- a/tests/gateway/test_soap_server.py
+++ /dev/null
@@ -1,75 +0,0 @@
-import pytest
-
-from unittest.mock import MagicMock
-from healthchain.gateway.soap.utils.epiccds import CDSServices
-from healthchain.gateway.soap.utils.model import ClientFault, ServerFault
-
-
-@pytest.fixture
-def soap_cdsservices():
- return CDSServices()
-
-
-def test_ProcessDocument_missing_parameters(soap_cdsservices):
- mock_ctx = MagicMock()
- with pytest.raises(ClientFault) as exc_info:
- soap_cdsservices.ProcessDocument(
- mock_ctx, None, "WorkType", "OrganizationID", [b"..."]
- )
- assert "Missing required parameter: sessionId" in str(exc_info.value)
-
- with pytest.raises(ClientFault) as exc_info:
- soap_cdsservices.ProcessDocument(
- mock_ctx, "123456", None, "OrganizationID", [b"..."]
- )
- assert "Missing required parameter: workType" in str(exc_info.value)
-
- with pytest.raises(ClientFault) as exc_info:
- soap_cdsservices.ProcessDocument(
- mock_ctx, "123456", "WorkType", None, [b"..."]
- )
- assert "Missing required parameter: organizationId" in str(exc_info.value)
-
- with pytest.raises(ClientFault) as exc_info:
- soap_cdsservices.ProcessDocument(
- mock_ctx, "123456", "WorkType", "OrganizationID", None
- )
- assert "Missing required parameter: document" in str(exc_info.value)
-
-
-def test_ProcessDocument_successful_request(soap_cdsservices):
- mock_ctx = MagicMock()
- mock_ctx.descriptor.service_class._service.return_value = MagicMock(
- document="Document", error=None
- )
-
- sessionId = "123456"
- workType = "WorkType"
- organizationId = "OrganizationID"
- document = [b"..."]
-
- response = soap_cdsservices.ProcessDocument(
- mock_ctx, sessionId, workType, organizationId, document
- )
-
- assert response is not None
- assert response.Document is not None
- assert response.Error is None
-
-
-def test_ProcessDocument_server_processing_error(soap_cdsservices):
- mock_ctx = MagicMock()
- mock_ctx.descriptor.service_class._service.return_value = MagicMock(
- document="Document", error="Error"
- )
-
- sessionId = "123456"
- workType = "WorkType"
- organizationId = "OrganizationID"
- document = [b"..."]
-
- # Simulate a server processing error
- with pytest.raises(ServerFault):
- soap_cdsservices.ProcessDocument(
- mock_ctx, sessionId, workType, organizationId, document
- )
diff --git a/tests/sandbox/test_clindoc_sandbox.py b/tests/sandbox/test_clindoc_sandbox.py
index e1ae80d0..60f96586 100644
--- a/tests/sandbox/test_clindoc_sandbox.py
+++ b/tests/sandbox/test_clindoc_sandbox.py
@@ -24,7 +24,7 @@ def process_document(cda_request: CdaRequest) -> CdaResponse:
# Create SandboxClient for SOAP/CDA
client = SandboxClient(
- url="http://localhost:8000/notereader/fhir/",
+ url="http://localhost:8000/notereader/?wsdl",
workflow="sign-note-inpatient",
protocol="soap",
)
@@ -73,7 +73,7 @@ def test_notereader_sandbox_workflow_execution():
"""Test executing a NoteReader workflow with SandboxClient"""
# Create SandboxClient
client = SandboxClient(
- url="http://localhost:8000/notereader/fhir/",
+ url="http://localhost:8000/notereader/?wsdl",
workflow="sign-note-inpatient",
protocol="soap",
)
diff --git a/tests/sandbox/test_sandbox_client.py b/tests/sandbox/test_sandbox_client.py
index d7a71614..75ae184d 100644
--- a/tests/sandbox/test_sandbox_client.py
+++ b/tests/sandbox/test_sandbox_client.py
@@ -20,7 +20,7 @@ def test_load_from_path_single_xml_file(tmp_path):
cda_file.write_text("Test CDA")
client = SandboxClient(
- url="http://localhost:8000/notereader/fhir/",
+ url="http://localhost:8000/notereader/?wsdl",
workflow="sign-note-inpatient",
protocol="soap",
)
@@ -39,7 +39,7 @@ def test_load_from_path_directory_with_pattern(tmp_path):
(tmp_path / "other.txt").write_text("Not XML")
client = SandboxClient(
- url="http://localhost:8000/notereader/fhir/",
+ url="http://localhost:8000/notereader/?wsdl",
workflow="sign-note-inpatient",
protocol="soap",
)
@@ -56,7 +56,7 @@ def test_load_from_path_directory_all_files(tmp_path):
(tmp_path / "note2.xml").write_text("Note 2")
client = SandboxClient(
- url="http://localhost:8000/notereader/fhir/",
+ url="http://localhost:8000/notereader/?wsdl",
workflow="sign-note-inpatient",
protocol="soap",
)
@@ -69,7 +69,7 @@ def test_load_from_path_directory_all_files(tmp_path):
def test_load_from_path_error_handling(tmp_path):
"""load_from_path raises FileNotFoundError for nonexistent path."""
client = SandboxClient(
- url="http://localhost:8000/notereader/fhir/",
+ url="http://localhost:8000/notereader/?wsdl",
workflow="sign-note-inpatient",
protocol="soap",
)
diff --git a/uv.lock b/uv.lock
index b411f896..6660b970 100644
--- a/uv.lock
+++ b/uv.lock
@@ -1,8 +1,9 @@
version = 1
revision = 3
-requires-python = ">=3.10, <3.12"
+requires-python = ">=3.10, <3.13"
resolution-markers = [
- "python_full_version >= '3.11'",
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
"python_full_version < '3.11'",
]
@@ -17,17 +18,16 @@ wheels = [
[[package]]
name = "anyio"
-version = "4.11.0"
+version = "4.12.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
{ name = "idna" },
- { name = "sniffio" },
{ name = "typing-extensions" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/c6/78/7d432127c41b50bccba979505f272c16cbcadcc33645d5fa3a738110ae75/anyio-4.11.0.tar.gz", hash = "sha256:82a8d0b81e318cc5ce71a5f1f8b5c4e63619620b63141ef8c995fa0db95a57c4", size = 219094, upload-time = "2025-09-23T09:19:12.58Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/16/ce/8a777047513153587e5434fd752e89334ac33e379aa3497db860eeb60377/anyio-4.12.0.tar.gz", hash = "sha256:73c693b567b0c55130c104d0b43a9baf3aa6a31fc6110116509f27bf75e21ec0", size = 228266, upload-time = "2025-11-28T23:37:38.911Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/15/b3/9b1a8074496371342ec1e796a96f99c82c945a339cd81a8e73de28b4cf9e/anyio-4.11.0-py3-none-any.whl", hash = "sha256:0287e96f4d26d4149305414d4e3bc32f0dcd0862365a4bddea19d7a1ec38c4fc", size = 109097, upload-time = "2025-09-23T09:19:10.601Z" },
+ { url = "https://files.pythonhosted.org/packages/7f/9c/36c5c37947ebfb8c7f22e0eb6e4d188ee2d53aa3880f3f2744fb894f0cb1/anyio-4.12.0-py3-none-any.whl", hash = "sha256:dad2376a628f98eeca4881fc56cd06affd18f659b17a747d3ff0307ced94b1bb", size = 113362, upload-time = "2025-11-28T23:36:57.897Z" },
]
[[package]]
@@ -65,6 +65,7 @@ sdist = { url = "https://files.pythonhosted.org/packages/86/e3/bb3a439d5cb255c47
wheels = [
{ url = "https://files.pythonhosted.org/packages/3b/ee/c216d52f58ea75b5e1841022bbae24438b19834a29b163cb32aa3a2a7c6e/backrefs-6.1-py310-none-any.whl", hash = "sha256:2a2ccb96302337ce61ee4717ceacfbf26ba4efb1d55af86564b8bbaeda39cac1", size = 381059, upload-time = "2025-11-15T14:51:59.758Z" },
{ url = "https://files.pythonhosted.org/packages/e6/9a/8da246d988ded941da96c7ed945d63e94a445637eaad985a0ed88787cb89/backrefs-6.1-py311-none-any.whl", hash = "sha256:e82bba3875ee4430f4de4b6db19429a27275d95a5f3773c57e9e18abc23fd2b7", size = 392854, upload-time = "2025-11-15T14:52:01.194Z" },
+ { url = "https://files.pythonhosted.org/packages/37/c9/fd117a6f9300c62bbc33bc337fd2b3c6bfe28b6e9701de336b52d7a797ad/backrefs-6.1-py312-none-any.whl", hash = "sha256:c64698c8d2269343d88947c0735cb4b78745bd3ba590e10313fbf3f78c34da5a", size = 398770, upload-time = "2025-11-15T14:52:02.584Z" },
{ url = "https://files.pythonhosted.org/packages/02/e3/a4fa1946722c4c7b063cc25043a12d9ce9b4323777f89643be74cef2993c/backrefs-6.1-py39-none-any.whl", hash = "sha256:a9e99b8a4867852cad177a6430e31b0f6e495d65f8c6c134b68c14c3c95bf4b0", size = 381058, upload-time = "2025-11-15T14:52:06.698Z" },
]
@@ -91,6 +92,13 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/6e/a6/7733820aa62da32526287a63cd85c103b2b323b186c8ee43b7772ff7017c/blis-1.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:c4ae70629cf302035d268858a10ca4eb6242a01b2dc8d64422f8e6dcb8a8ee74", size = 3041954, upload-time = "2025-11-17T12:27:37.479Z" },
{ url = "https://files.pythonhosted.org/packages/87/53/e39d67fd3296b649772780ca6aab081412838ecb54e0b0c6432d01626a50/blis-1.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:45866a9027d43b93e8b59980a23c5d7358b6536fc04606286e39fdcfce1101c2", size = 14251222, upload-time = "2025-11-17T12:27:39.705Z" },
{ url = "https://files.pythonhosted.org/packages/ea/44/b749f8777b020b420bceaaf60f66432fc30cc904ca5b69640ec9cbef11ed/blis-1.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:27f82b8633030f8d095d2b412dffa7eb6dbc8ee43813139909a20012e54422ea", size = 6171233, upload-time = "2025-11-17T12:27:41.921Z" },
+ { url = "https://files.pythonhosted.org/packages/16/d1/429cf0cf693d4c7dc2efed969bd474e315aab636e4a95f66c4ed7264912d/blis-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2a1c74e100665f8e918ebdbae2794576adf1f691680b5cdb8b29578432f623ef", size = 6929663, upload-time = "2025-11-17T12:27:44.482Z" },
+ { url = "https://files.pythonhosted.org/packages/11/69/363c8df8d98b3cc97be19aad6aabb2c9c53f372490d79316bdee92d476e7/blis-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3f6c595185176ce021316263e1a1d636a3425b6c48366c1fd712d08d0b71849a", size = 1230939, upload-time = "2025-11-17T12:27:46.19Z" },
+ { url = "https://files.pythonhosted.org/packages/96/2a/fbf65d906d823d839076c5150a6f8eb5ecbc5f9135e0b6510609bda1e6b7/blis-1.3.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d734b19fba0be7944f272dfa7b443b37c61f9476d9ab054a9ac53555ceadd2e0", size = 2818835, upload-time = "2025-11-17T12:27:48.167Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/ad/58deaa3ad856dd3cc96493e40ffd2ed043d18d4d304f85a65cde1ccbf644/blis-1.3.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1ef6d6e2b599a3a2788eb6d9b443533961265aa4ec49d574ed4bb846e548dcdb", size = 11366550, upload-time = "2025-11-17T12:27:49.958Z" },
+ { url = "https://files.pythonhosted.org/packages/78/82/816a7adfe1f7acc8151f01ec86ef64467a3c833932d8f19f8e06613b8a4e/blis-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8c888438ae99c500422d50698e3028b65caa8ebb44e24204d87fda2df64058f7", size = 3023686, upload-time = "2025-11-17T12:27:52.062Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/e2/0e93b865f648b5519360846669a35f28ee8f4e1d93d054f6850d8afbabde/blis-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:8177879fd3590b5eecdd377f9deafb5dc8af6d684f065bd01553302fb3fcf9a7", size = 14250939, upload-time = "2025-11-17T12:27:53.847Z" },
+ { url = "https://files.pythonhosted.org/packages/20/07/fb43edc2ff0a6a367e4a94fc39eb3b85aa1e55e24cc857af2db145ce9f0d/blis-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:f20f7ad69aaffd1ce14fe77de557b6df9b61e0c9e582f75a843715d836b5c8af", size = 6192759, upload-time = "2025-11-17T12:27:56.176Z" },
]
[[package]]
@@ -145,6 +153,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/2b/c0/015b25184413d7ab0a410775fdb4a50fca20f5589b5dab1dbbfa3baad8ce/cffi-2.0.0-cp311-cp311-win32.whl", hash = "sha256:c649e3a33450ec82378822b3dad03cc228b8f5963c0c12fc3b1e0ab940f768a5", size = 172076, upload-time = "2025-09-08T23:22:40.95Z" },
{ url = "https://files.pythonhosted.org/packages/ae/8f/dc5531155e7070361eb1b7e4c1a9d896d0cb21c49f807a6c03fd63fc877e/cffi-2.0.0-cp311-cp311-win_amd64.whl", hash = "sha256:66f011380d0e49ed280c789fbd08ff0d40968ee7b665575489afa95c98196ab5", size = 182820, upload-time = "2025-09-08T23:22:42.463Z" },
{ url = "https://files.pythonhosted.org/packages/95/5c/1b493356429f9aecfd56bc171285a4c4ac8697f76e9bbbbb105e537853a1/cffi-2.0.0-cp311-cp311-win_arm64.whl", hash = "sha256:c6638687455baf640e37344fe26d37c404db8b80d037c3d29f58fe8d1c3b194d", size = 177635, upload-time = "2025-09-08T23:22:43.623Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/47/4f61023ea636104d4f16ab488e268b93008c3d0bb76893b1b31db1f96802/cffi-2.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d02d6655b0e54f54c4ef0b94eb6be0607b70853c45ce98bd278dc7de718be5d", size = 185271, upload-time = "2025-09-08T23:22:44.795Z" },
+ { url = "https://files.pythonhosted.org/packages/df/a2/781b623f57358e360d62cdd7a8c681f074a71d445418a776eef0aadb4ab4/cffi-2.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8eca2a813c1cb7ad4fb74d368c2ffbbb4789d377ee5bb8df98373c2cc0dee76c", size = 181048, upload-time = "2025-09-08T23:22:45.938Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/df/a4f0fbd47331ceeba3d37c2e51e9dfc9722498becbeec2bd8bc856c9538a/cffi-2.0.0-cp312-cp312-manylinux1_i686.manylinux2014_i686.manylinux_2_17_i686.manylinux_2_5_i686.whl", hash = "sha256:21d1152871b019407d8ac3985f6775c079416c282e431a4da6afe7aefd2bccbe", size = 212529, upload-time = "2025-09-08T23:22:47.349Z" },
+ { url = "https://files.pythonhosted.org/packages/d5/72/12b5f8d3865bf0f87cf1404d8c374e7487dcf097a1c91c436e72e6badd83/cffi-2.0.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b21e08af67b8a103c71a250401c78d5e0893beff75e28c53c98f4de42f774062", size = 220097, upload-time = "2025-09-08T23:22:48.677Z" },
+ { url = "https://files.pythonhosted.org/packages/c2/95/7a135d52a50dfa7c882ab0ac17e8dc11cec9d55d2c18dda414c051c5e69e/cffi-2.0.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.whl", hash = "sha256:1e3a615586f05fc4065a8b22b8152f0c1b00cdbc60596d187c2a74f9e3036e4e", size = 207983, upload-time = "2025-09-08T23:22:50.06Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/c8/15cb9ada8895957ea171c62dc78ff3e99159ee7adb13c0123c001a2546c1/cffi-2.0.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.whl", hash = "sha256:81afed14892743bbe14dacb9e36d9e0e504cd204e0b165062c488942b9718037", size = 206519, upload-time = "2025-09-08T23:22:51.364Z" },
+ { url = "https://files.pythonhosted.org/packages/78/2d/7fa73dfa841b5ac06c7b8855cfc18622132e365f5b81d02230333ff26e9e/cffi-2.0.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3e17ed538242334bf70832644a32a7aae3d83b57567f9fd60a26257e992b79ba", size = 219572, upload-time = "2025-09-08T23:22:52.902Z" },
+ { url = "https://files.pythonhosted.org/packages/07/e0/267e57e387b4ca276b90f0434ff88b2c2241ad72b16d31836adddfd6031b/cffi-2.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3925dd22fa2b7699ed2617149842d2e6adde22b262fcbfada50e3d195e4b3a94", size = 222963, upload-time = "2025-09-08T23:22:54.518Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/75/1f2747525e06f53efbd878f4d03bac5b859cbc11c633d0fb81432d98a795/cffi-2.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:2c8f814d84194c9ea681642fd164267891702542f028a15fc97d4674b6206187", size = 221361, upload-time = "2025-09-08T23:22:55.867Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/2b/2b6435f76bfeb6bbf055596976da087377ede68df465419d192acf00c437/cffi-2.0.0-cp312-cp312-win32.whl", hash = "sha256:da902562c3e9c550df360bfa53c035b2f241fed6d9aef119048073680ace4a18", size = 172932, upload-time = "2025-09-08T23:22:57.188Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/ed/13bd4418627013bec4ed6e54283b1959cf6db888048c7cf4b4c3b5b36002/cffi-2.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:da68248800ad6320861f129cd9c1bf96ca849a2771a59e0344e88681905916f5", size = 183557, upload-time = "2025-09-08T23:22:58.351Z" },
+ { url = "https://files.pythonhosted.org/packages/95/31/9f7f93ad2f8eff1dbc1c3656d7ca5bfd8fb52c9d786b4dcf19b2d02217fa/cffi-2.0.0-cp312-cp312-win_arm64.whl", hash = "sha256:4671d9dd5ec934cb9a73e7ee9676f9362aba54f7f34910956b84d727b0d73fb6", size = 177762, upload-time = "2025-09-08T23:22:59.668Z" },
]
[[package]]
@@ -194,6 +214,22 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/1a/86/584869fe4ddb6ffa3bd9f491b87a01568797fb9bd8933f557dba9771beaf/charset_normalizer-3.4.4-cp311-cp311-win32.whl", hash = "sha256:eecbc200c7fd5ddb9a7f16c7decb07b566c29fa2161a16cf67b8d068bd21690a", size = 99456, upload-time = "2025-10-14T04:40:49.376Z" },
{ url = "https://files.pythonhosted.org/packages/65/f6/62fdd5feb60530f50f7e38b4f6a1d5203f4d16ff4f9f0952962c044e919a/charset_normalizer-3.4.4-cp311-cp311-win_amd64.whl", hash = "sha256:5ae497466c7901d54b639cf42d5b8c1b6a4fead55215500d2f486d34db48d016", size = 106978, upload-time = "2025-10-14T04:40:50.844Z" },
{ url = "https://files.pythonhosted.org/packages/7a/9d/0710916e6c82948b3be62d9d398cb4fcf4e97b56d6a6aeccd66c4b2f2bd5/charset_normalizer-3.4.4-cp311-cp311-win_arm64.whl", hash = "sha256:65e2befcd84bc6f37095f5961e68a6f077bf44946771354a28ad434c2cce0ae1", size = 99969, upload-time = "2025-10-14T04:40:52.272Z" },
+ { url = "https://files.pythonhosted.org/packages/f3/85/1637cd4af66fa687396e757dec650f28025f2a2f5a5531a3208dc0ec43f2/charset_normalizer-3.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0a98e6759f854bd25a58a73fa88833fba3b7c491169f86ce1180c948ab3fd394", size = 208425, upload-time = "2025-10-14T04:40:53.353Z" },
+ { url = "https://files.pythonhosted.org/packages/9d/6a/04130023fef2a0d9c62d0bae2649b69f7b7d8d24ea5536feef50551029df/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b5b290ccc2a263e8d185130284f8501e3e36c5e02750fc6b6bdeb2e9e96f1e25", size = 148162, upload-time = "2025-10-14T04:40:54.558Z" },
+ { url = "https://files.pythonhosted.org/packages/78/29/62328d79aa60da22c9e0b9a66539feae06ca0f5a4171ac4f7dc285b83688/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:74bb723680f9f7a6234dcf67aea57e708ec1fbdf5699fb91dfd6f511b0a320ef", size = 144558, upload-time = "2025-10-14T04:40:55.677Z" },
+ { url = "https://files.pythonhosted.org/packages/86/bb/b32194a4bf15b88403537c2e120b817c61cd4ecffa9b6876e941c3ee38fe/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1e34719c6ed0b92f418c7c780480b26b5d9c50349e9a9af7d76bf757530350d", size = 161497, upload-time = "2025-10-14T04:40:57.217Z" },
+ { url = "https://files.pythonhosted.org/packages/19/89/a54c82b253d5b9b111dc74aca196ba5ccfcca8242d0fb64146d4d3183ff1/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:2437418e20515acec67d86e12bf70056a33abdacb5cb1655042f6538d6b085a8", size = 159240, upload-time = "2025-10-14T04:40:58.358Z" },
+ { url = "https://files.pythonhosted.org/packages/c0/10/d20b513afe03acc89ec33948320a5544d31f21b05368436d580dec4e234d/charset_normalizer-3.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:11d694519d7f29d6cd09f6ac70028dba10f92f6cdd059096db198c283794ac86", size = 153471, upload-time = "2025-10-14T04:40:59.468Z" },
+ { url = "https://files.pythonhosted.org/packages/61/fa/fbf177b55bdd727010f9c0a3c49eefa1d10f960e5f09d1d887bf93c2e698/charset_normalizer-3.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:ac1c4a689edcc530fc9d9aa11f5774b9e2f33f9a0c6a57864e90908f5208d30a", size = 150864, upload-time = "2025-10-14T04:41:00.623Z" },
+ { url = "https://files.pythonhosted.org/packages/05/12/9fbc6a4d39c0198adeebbde20b619790e9236557ca59fc40e0e3cebe6f40/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:21d142cc6c0ec30d2efee5068ca36c128a30b0f2c53c1c07bd78cb6bc1d3be5f", size = 150647, upload-time = "2025-10-14T04:41:01.754Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/1f/6a9a593d52e3e8c5d2b167daf8c6b968808efb57ef4c210acb907c365bc4/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:5dbe56a36425d26d6cfb40ce79c314a2e4dd6211d51d6d2191c00bed34f354cc", size = 145110, upload-time = "2025-10-14T04:41:03.231Z" },
+ { url = "https://files.pythonhosted.org/packages/30/42/9a52c609e72471b0fc54386dc63c3781a387bb4fe61c20231a4ebcd58bdd/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:5bfbb1b9acf3334612667b61bd3002196fe2a1eb4dd74d247e0f2a4d50ec9bbf", size = 162839, upload-time = "2025-10-14T04:41:04.715Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5b/c0682bbf9f11597073052628ddd38344a3d673fda35a36773f7d19344b23/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:d055ec1e26e441f6187acf818b73564e6e6282709e9bcb5b63f5b23068356a15", size = 150667, upload-time = "2025-10-14T04:41:05.827Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/24/a41afeab6f990cf2daf6cb8c67419b63b48cf518e4f56022230840c9bfb2/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:af2d8c67d8e573d6de5bc30cdb27e9b95e49115cd9baad5ddbd1a6207aaa82a9", size = 160535, upload-time = "2025-10-14T04:41:06.938Z" },
+ { url = "https://files.pythonhosted.org/packages/2a/e5/6a4ce77ed243c4a50a1fecca6aaaab419628c818a49434be428fe24c9957/charset_normalizer-3.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:780236ac706e66881f3b7f2f32dfe90507a09e67d1d454c762cf642e6e1586e0", size = 154816, upload-time = "2025-10-14T04:41:08.101Z" },
+ { url = "https://files.pythonhosted.org/packages/a8/ef/89297262b8092b312d29cdb2517cb1237e51db8ecef2e9af5edbe7b683b1/charset_normalizer-3.4.4-cp312-cp312-win32.whl", hash = "sha256:5833d2c39d8896e4e19b689ffc198f08ea58116bee26dea51e362ecc7cd3ed26", size = 99694, upload-time = "2025-10-14T04:41:09.23Z" },
+ { url = "https://files.pythonhosted.org/packages/3d/2d/1e5ed9dd3b3803994c155cd9aacb60c82c331bad84daf75bcb9c91b3295e/charset_normalizer-3.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:a79cfe37875f822425b89a82333404539ae63dbdddf97f84dcbc3d339aae9525", size = 107131, upload-time = "2025-10-14T04:41:10.467Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/d9/0ed4c7098a861482a7b6a95603edce4c0d9db2311af23da1fb2b75ec26fc/charset_normalizer-3.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:376bec83a63b8021bb5c8ea75e21c4ccb86e7e45ca4eb81146091b56599b80c3", size = 100390, upload-time = "2025-10-14T04:41:11.915Z" },
{ url = "https://files.pythonhosted.org/packages/0a/4c/925909008ed5a988ccbb72dcc897407e5d6d3bd72410d69e051fc0c14647/charset_normalizer-3.4.4-py3-none-any.whl", hash = "sha256:7a32c560861a02ff789ad905a2fe94e3f840803362c84fecf1851cb4cf3dc37f", size = 53402, upload-time = "2025-10-14T04:42:31.76Z" },
]
@@ -324,6 +360,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d4/2b/0e4664cafc581de2896d75000651fd2ce7094d33263f466185c28ffc96e4/cymem-2.0.13-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c90a6ecba994a15b17a3f45d7ec74d34081df2f73bd1b090e2adc0317e4e01b6", size = 248287, upload-time = "2025-11-14T14:57:29.055Z" },
{ url = "https://files.pythonhosted.org/packages/21/0f/f94c6950edbfc2aafb81194fc40b6cacc8e994e9359d3cb4328c5705b9b5/cymem-2.0.13-cp311-cp311-win_amd64.whl", hash = "sha256:ce821e6ba59148ed17c4567113b8683a6a0be9c9ac86f14e969919121efb61a5", size = 40116, upload-time = "2025-11-14T14:57:30.592Z" },
{ url = "https://files.pythonhosted.org/packages/00/df/2455eff6ac0381ff165db6883b311f7016e222e3dd62185517f8e8187ed0/cymem-2.0.13-cp311-cp311-win_arm64.whl", hash = "sha256:0dca715e708e545fd1d97693542378a00394b20a37779c1ae2c8bdbb43acef79", size = 36349, upload-time = "2025-11-14T14:57:31.573Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/52/478a2911ab5028cb710b4900d64aceba6f4f882fcb13fd8d40a456a1b6dc/cymem-2.0.13-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:e8afbc5162a0fe14b6463e1c4e45248a1b2fe2cbcecc8a5b9e511117080da0eb", size = 43745, upload-time = "2025-11-14T14:57:32.52Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/71/f0f8adee945524774b16af326bd314a14a478ed369a728a22834e6785a18/cymem-2.0.13-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:c9251d889348fe79a75e9b3e4d1b5fa651fca8a64500820685d73a3acc21b6a8", size = 42927, upload-time = "2025-11-14T14:57:33.827Z" },
+ { url = "https://files.pythonhosted.org/packages/62/6d/159780fe162ff715d62b809246e5fc20901cef87ca28b67d255a8d741861/cymem-2.0.13-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:742fc19764467a49ed22e56a4d2134c262d73a6c635409584ae3bf9afa092c33", size = 258346, upload-time = "2025-11-14T14:57:34.917Z" },
+ { url = "https://files.pythonhosted.org/packages/eb/12/678d16f7aa1996f947bf17b8cfb917ea9c9674ef5e2bd3690c04123d5680/cymem-2.0.13-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f190a92fe46197ee64d32560eb121c2809bb843341733227f51538ce77b3410d", size = 260843, upload-time = "2025-11-14T14:57:36.503Z" },
+ { url = "https://files.pythonhosted.org/packages/31/5d/0dd8c167c08cd85e70d274b7235cfe1e31b3cebc99221178eaf4bbb95c6f/cymem-2.0.13-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d670329ee8dbbbf241b7c08069fe3f1d3a1a3e2d69c7d05ea008a7010d826298", size = 254607, upload-time = "2025-11-14T14:57:38.036Z" },
+ { url = "https://files.pythonhosted.org/packages/b7/c9/d6514a412a1160aa65db539836b3d47f9b59f6675f294ec34ae32f867c82/cymem-2.0.13-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a84ba3178d9128b9ffb52ce81ebab456e9fe959125b51109f5b73ebdfc6b60d6", size = 262421, upload-time = "2025-11-14T14:57:39.265Z" },
+ { url = "https://files.pythonhosted.org/packages/dd/fe/3ee37d02ca4040f2fb22d34eb415198f955862b5dd47eee01df4c8f5454c/cymem-2.0.13-cp312-cp312-win_amd64.whl", hash = "sha256:2ff1c41fd59b789579fdace78aa587c5fc091991fa59458c382b116fc36e30dc", size = 40176, upload-time = "2025-11-14T14:57:40.706Z" },
+ { url = "https://files.pythonhosted.org/packages/94/fb/1b681635bfd5f2274d0caa8f934b58435db6c091b97f5593738065ddb786/cymem-2.0.13-cp312-cp312-win_arm64.whl", hash = "sha256:6bbd701338df7bf408648191dff52472a9b334f71bcd31a21a41d83821050f67", size = 35959, upload-time = "2025-11-14T14:57:41.682Z" },
]
[[package]]
@@ -340,6 +384,10 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/8f/6d/204f407df45600e2245b4a39860ed4ba32552330a0b3f5f160ae4cc30072/debugpy-1.8.17-cp311-cp311-manylinux_2_34_x86_64.whl", hash = "sha256:c6bdf134457ae0cac6fb68205776be635d31174eeac9541e1d0c062165c6461f", size = 3170322, upload-time = "2025-09-17T16:33:30.837Z" },
{ url = "https://files.pythonhosted.org/packages/f2/13/1b8f87d39cf83c6b713de2620c31205299e6065622e7dd37aff4808dd410/debugpy-1.8.17-cp311-cp311-win32.whl", hash = "sha256:e79a195f9e059edfe5d8bf6f3749b2599452d3e9380484cd261f6b7cd2c7c4da", size = 5155078, upload-time = "2025-09-17T16:33:33.331Z" },
{ url = "https://files.pythonhosted.org/packages/c2/c5/c012c60a2922cc91caa9675d0ddfbb14ba59e1e36228355f41cab6483469/debugpy-1.8.17-cp311-cp311-win_amd64.whl", hash = "sha256:b532282ad4eca958b1b2d7dbcb2b7218e02cb934165859b918e3b6ba7772d3f4", size = 5179011, upload-time = "2025-09-17T16:33:35.711Z" },
+ { url = "https://files.pythonhosted.org/packages/08/2b/9d8e65beb2751876c82e1aceb32f328c43ec872711fa80257c7674f45650/debugpy-1.8.17-cp312-cp312-macosx_15_0_universal2.whl", hash = "sha256:f14467edef672195c6f6b8e27ce5005313cb5d03c9239059bc7182b60c176e2d", size = 2549522, upload-time = "2025-09-17T16:33:38.466Z" },
+ { url = "https://files.pythonhosted.org/packages/b4/78/eb0d77f02971c05fca0eb7465b18058ba84bd957062f5eec82f941ac792a/debugpy-1.8.17-cp312-cp312-manylinux_2_34_x86_64.whl", hash = "sha256:24693179ef9dfa20dca8605905a42b392be56d410c333af82f1c5dff807a64cc", size = 4309417, upload-time = "2025-09-17T16:33:41.299Z" },
+ { url = "https://files.pythonhosted.org/packages/37/42/c40f1d8cc1fed1e75ea54298a382395b8b937d923fcf41ab0797a554f555/debugpy-1.8.17-cp312-cp312-win32.whl", hash = "sha256:6a4e9dacf2cbb60d2514ff7b04b4534b0139facbf2abdffe0639ddb6088e59cf", size = 5277130, upload-time = "2025-09-17T16:33:43.554Z" },
+ { url = "https://files.pythonhosted.org/packages/72/22/84263b205baad32b81b36eac076de0cdbe09fe2d0637f5b32243dc7c925b/debugpy-1.8.17-cp312-cp312-win_amd64.whl", hash = "sha256:e8f8f61c518952fb15f74a302e068b48d9c4691768ade433e4adeea961993464", size = 5319053, upload-time = "2025-09-17T16:33:53.033Z" },
{ url = "https://files.pythonhosted.org/packages/b0/d0/89247ec250369fc76db477720a26b2fce7ba079ff1380e4ab4529d2fe233/debugpy-1.8.17-py2.py3-none-any.whl", hash = "sha256:60c7dca6571efe660ccb7a9508d73ca14b8796c4ed484c2002abba714226cfef", size = 5283210, upload-time = "2025-09-17T16:34:25.835Z" },
]
@@ -651,7 +699,7 @@ dependencies = [
{ name = "comm" },
{ name = "debugpy" },
{ name = "ipython", version = "8.37.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" },
- { name = "ipython", version = "9.7.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
+ { name = "ipython", version = "9.8.0", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" },
{ name = "jupyter-client" },
{ name = "jupyter-core" },
{ name = "matplotlib-inline" },
@@ -694,10 +742,11 @@ wheels = [
[[package]]
name = "ipython"
-version = "9.7.0"
+version = "9.8.0"
source = { registry = "https://pypi.org/simple" }
resolution-markers = [
- "python_full_version >= '3.11'",
+ "python_full_version >= '3.12'",
+ "python_full_version == '3.11.*'",
]
dependencies = [
{ name = "colorama", marker = "python_full_version >= '3.11' and sys_platform == 'win32'" },
@@ -710,11 +759,11 @@ dependencies = [
{ name = "pygments", marker = "python_full_version >= '3.11'" },
{ name = "stack-data", marker = "python_full_version >= '3.11'" },
{ name = "traitlets", marker = "python_full_version >= '3.11'" },
- { name = "typing-extensions", marker = "python_full_version >= '3.11'" },
+ { name = "typing-extensions", marker = "python_full_version == '3.11.*'" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/29/e6/48c74d54039241a456add616464ea28c6ebf782e4110d419411b83dae06f/ipython-9.7.0.tar.gz", hash = "sha256:5f6de88c905a566c6a9d6c400a8fed54a638e1f7543d17aae2551133216b1e4e", size = 4422115, upload-time = "2025-11-05T12:18:54.646Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/12/51/a703c030f4928646d390b4971af4938a1b10c9dfce694f0d99a0bb073cb2/ipython-9.8.0.tar.gz", hash = "sha256:8e4ce129a627eb9dd221c41b1d2cdaed4ef7c9da8c17c63f6f578fe231141f83", size = 4424940, upload-time = "2025-12-03T10:18:24.353Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/05/aa/62893d6a591d337aa59dcc4c6f6c842f1fe20cd72c8c5c1f980255243252/ipython-9.7.0-py3-none-any.whl", hash = "sha256:bce8ac85eb9521adc94e1845b4c03d88365fd6ac2f4908ec4ed1eb1b0a065f9f", size = 618911, upload-time = "2025-11-05T12:18:52.484Z" },
+ { url = "https://files.pythonhosted.org/packages/f1/df/8ee1c5dd1e3308b5d5b2f2dfea323bb2f3827da8d654abb6642051199049/ipython-9.8.0-py3-none-any.whl", hash = "sha256:ebe6d1d58d7d988fbf23ff8ff6d8e1622cfdb194daf4b7b73b792c4ec3b85385", size = 621374, upload-time = "2025-12-03T10:18:22.335Z" },
]
[[package]]
@@ -755,7 +804,7 @@ wheels = [
[[package]]
name = "jupyter-client"
-version = "8.6.3"
+version = "8.7.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "jupyter-core" },
@@ -764,9 +813,9 @@ dependencies = [
{ name = "tornado" },
{ name = "traitlets" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/71/22/bf9f12fdaeae18019a468b68952a60fe6dbab5d67cd2a103cac7659b41ca/jupyter_client-8.6.3.tar.gz", hash = "sha256:35b3a0947c4a6e9d589eb97d7d4cd5e90f910ee73101611f01283732bd6d9419", size = 342019, upload-time = "2024-09-17T10:44:17.613Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/a6/27/d10de45e8ad4ce872372c4a3a37b7b35b6b064f6f023a5c14ffcced4d59d/jupyter_client-8.7.0.tar.gz", hash = "sha256:3357212d9cbe01209e59190f67a3a7e1f387a4f4e88d1e0433ad84d7b262531d", size = 344691, upload-time = "2025-12-09T18:37:01.953Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/11/85/b0394e0b6fcccd2c1eeefc230978a6f8cb0c5df1e4cd3e7625735a0d7d1e/jupyter_client-8.6.3-py3-none-any.whl", hash = "sha256:e8a19cc986cc45905ac3362915f410f3af85424b4c0905e94fa5f2cb08e8f23f", size = 106105, upload-time = "2024-09-17T10:44:15.218Z" },
+ { url = "https://files.pythonhosted.org/packages/bb/f5/fddaec430367be9d62a7ed125530e133bfd4a1c0350fe221149ee0f2b526/jupyter_client-8.7.0-py3-none-any.whl", hash = "sha256:3671a94fd25e62f5f2f554f5e95389c2294d89822378a5f2dd24353e1494a9e0", size = 106215, upload-time = "2025-12-09T18:37:00.024Z" },
]
[[package]]
@@ -834,6 +883,23 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/38/a2/0c49ec6941428b1bd4f280650d7b11a0f91ace9db7de32eb7aa23bcb39ff/lxml-5.4.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:e094ec83694b59d263802ed03a8384594fcce477ce484b0cbcd0008a211ca751", size = 4988195, upload-time = "2025-04-23T01:45:56.685Z" },
{ url = "https://files.pythonhosted.org/packages/7a/75/87a3963a08eafc46a86c1131c6e28a4de103ba30b5ae903114177352a3d7/lxml-5.4.0-cp311-cp311-win32.whl", hash = "sha256:4329422de653cdb2b72afa39b0aa04252fca9071550044904b2e7036d9d97fe4", size = 3474243, upload-time = "2025-04-23T01:45:58.863Z" },
{ url = "https://files.pythonhosted.org/packages/fa/f9/1f0964c4f6c2be861c50db380c554fb8befbea98c6404744ce243a3c87ef/lxml-5.4.0-cp311-cp311-win_amd64.whl", hash = "sha256:fd3be6481ef54b8cfd0e1e953323b7aa9d9789b94842d0e5b142ef4bb7999539", size = 3815197, upload-time = "2025-04-23T01:46:01.096Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/4c/d101ace719ca6a4ec043eb516fcfcb1b396a9fccc4fcd9ef593df34ba0d5/lxml-5.4.0-cp312-cp312-macosx_10_9_universal2.whl", hash = "sha256:b5aff6f3e818e6bdbbb38e5967520f174b18f539c2b9de867b1e7fde6f8d95a4", size = 8127392, upload-time = "2025-04-23T01:46:04.09Z" },
+ { url = "https://files.pythonhosted.org/packages/11/84/beddae0cec4dd9ddf46abf156f0af451c13019a0fa25d7445b655ba5ccb7/lxml-5.4.0-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:942a5d73f739ad7c452bf739a62a0f83e2578afd6b8e5406308731f4ce78b16d", size = 4415103, upload-time = "2025-04-23T01:46:07.227Z" },
+ { url = "https://files.pythonhosted.org/packages/d0/25/d0d93a4e763f0462cccd2b8a665bf1e4343dd788c76dcfefa289d46a38a9/lxml-5.4.0-cp312-cp312-manylinux_2_12_i686.manylinux2010_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:460508a4b07364d6abf53acaa0a90b6d370fafde5693ef37602566613a9b0779", size = 5024224, upload-time = "2025-04-23T01:46:10.237Z" },
+ { url = "https://files.pythonhosted.org/packages/31/ce/1df18fb8f7946e7f3388af378b1f34fcf253b94b9feedb2cec5969da8012/lxml-5.4.0-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:529024ab3a505fed78fe3cc5ddc079464e709f6c892733e3f5842007cec8ac6e", size = 4769913, upload-time = "2025-04-23T01:46:12.757Z" },
+ { url = "https://files.pythonhosted.org/packages/4e/62/f4a6c60ae7c40d43657f552f3045df05118636be1165b906d3423790447f/lxml-5.4.0-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:7ca56ebc2c474e8f3d5761debfd9283b8b18c76c4fc0967b74aeafba1f5647f9", size = 5290441, upload-time = "2025-04-23T01:46:16.037Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/aa/04f00009e1e3a77838c7fc948f161b5d2d5de1136b2b81c712a263829ea4/lxml-5.4.0-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a81e1196f0a5b4167a8dafe3a66aa67c4addac1b22dc47947abd5d5c7a3f24b5", size = 4820165, upload-time = "2025-04-23T01:46:19.137Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/1f/e0b2f61fa2404bf0f1fdf1898377e5bd1b74cc9b2cf2c6ba8509b8f27990/lxml-5.4.0-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00b8686694423ddae324cf614e1b9659c2edb754de617703c3d29ff568448df5", size = 4932580, upload-time = "2025-04-23T01:46:21.963Z" },
+ { url = "https://files.pythonhosted.org/packages/24/a2/8263f351b4ffe0ed3e32ea7b7830f845c795349034f912f490180d88a877/lxml-5.4.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c5681160758d3f6ac5b4fea370495c48aac0989d6a0f01bb9a72ad8ef5ab75c4", size = 4759493, upload-time = "2025-04-23T01:46:24.316Z" },
+ { url = "https://files.pythonhosted.org/packages/05/00/41db052f279995c0e35c79d0f0fc9f8122d5b5e9630139c592a0b58c71b4/lxml-5.4.0-cp312-cp312-manylinux_2_28_ppc64le.whl", hash = "sha256:2dc191e60425ad70e75a68c9fd90ab284df64d9cd410ba8d2b641c0c45bc006e", size = 5324679, upload-time = "2025-04-23T01:46:27.097Z" },
+ { url = "https://files.pythonhosted.org/packages/1d/be/ee99e6314cdef4587617d3b3b745f9356d9b7dd12a9663c5f3b5734b64ba/lxml-5.4.0-cp312-cp312-manylinux_2_28_s390x.whl", hash = "sha256:67f779374c6b9753ae0a0195a892a1c234ce8416e4448fe1e9f34746482070a7", size = 4890691, upload-time = "2025-04-23T01:46:30.009Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/36/239820114bf1d71f38f12208b9c58dec033cbcf80101cde006b9bde5cffd/lxml-5.4.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:79d5bfa9c1b455336f52343130b2067164040604e41f6dc4d8313867ed540079", size = 4955075, upload-time = "2025-04-23T01:46:32.33Z" },
+ { url = "https://files.pythonhosted.org/packages/d4/e1/1b795cc0b174efc9e13dbd078a9ff79a58728a033142bc6d70a1ee8fc34d/lxml-5.4.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3d3c30ba1c9b48c68489dc1829a6eede9873f52edca1dda900066542528d6b20", size = 4838680, upload-time = "2025-04-23T01:46:34.852Z" },
+ { url = "https://files.pythonhosted.org/packages/72/48/3c198455ca108cec5ae3662ae8acd7fd99476812fd712bb17f1b39a0b589/lxml-5.4.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1af80c6316ae68aded77e91cd9d80648f7dd40406cef73df841aa3c36f6907c8", size = 5391253, upload-time = "2025-04-23T01:46:37.608Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/10/5bf51858971c51ec96cfc13e800a9951f3fd501686f4c18d7d84fe2d6352/lxml-5.4.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:4d885698f5019abe0de3d352caf9466d5de2baded00a06ef3f1216c1a58ae78f", size = 5261651, upload-time = "2025-04-23T01:46:40.183Z" },
+ { url = "https://files.pythonhosted.org/packages/2b/11/06710dd809205377da380546f91d2ac94bad9ff735a72b64ec029f706c85/lxml-5.4.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:aea53d51859b6c64e7c51d522c03cc2c48b9b5d6172126854cc7f01aa11f52bc", size = 5024315, upload-time = "2025-04-23T01:46:43.333Z" },
+ { url = "https://files.pythonhosted.org/packages/f5/b0/15b6217834b5e3a59ebf7f53125e08e318030e8cc0d7310355e6edac98ef/lxml-5.4.0-cp312-cp312-win32.whl", hash = "sha256:d90b729fd2732df28130c064aac9bb8aff14ba20baa4aee7bd0795ff1187545f", size = 3486149, upload-time = "2025-04-23T01:46:45.684Z" },
+ { url = "https://files.pythonhosted.org/packages/91/1e/05ddcb57ad2f3069101611bd5f5084157d90861a2ef460bf42f45cced944/lxml-5.4.0-cp312-cp312-win_amd64.whl", hash = "sha256:1dc4ca99e89c335a7ed47d38964abcb36c5910790f9bd106f2a8fa2ee0b909d2", size = 3817095, upload-time = "2025-04-23T01:46:48.521Z" },
{ url = "https://files.pythonhosted.org/packages/c6/b0/e4d1cbb8c078bc4ae44de9c6a79fec4e2b4151b1b4d50af71d799e76b177/lxml-5.4.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:1b717b00a71b901b4667226bba282dd462c42ccf618ade12f9ba3674e1fabc55", size = 3892319, upload-time = "2025-04-23T01:49:22.069Z" },
{ url = "https://files.pythonhosted.org/packages/5b/aa/e2bdefba40d815059bcb60b371a36fbfcce970a935370e1b367ba1cc8f74/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:27a9ded0f0b52098ff89dd4c418325b987feed2ea5cc86e8860b0f844285d740", size = 4211614, upload-time = "2025-04-23T01:49:24.599Z" },
{ url = "https://files.pythonhosted.org/packages/3c/5f/91ff89d1e092e7cfdd8453a939436ac116db0a665e7f4be0cd8e65c7dc5a/lxml-5.4.0-pp310-pypy310_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b7ce10634113651d6f383aa712a194179dcd496bd8c41e191cec2099fa09de5", size = 4306273, upload-time = "2025-04-23T01:49:27.355Z" },
@@ -879,6 +945,17 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/0f/62/d9c46a7f5c9adbeeeda52f5b8d802e1094e9717705a645efc71b0913a0a8/markupsafe-3.0.3-cp311-cp311-win32.whl", hash = "sha256:0db14f5dafddbb6d9208827849fad01f1a2609380add406671a26386cdf15a19", size = 14572, upload-time = "2025-09-27T18:36:28.045Z" },
{ url = "https://files.pythonhosted.org/packages/83/8a/4414c03d3f891739326e1783338e48fb49781cc915b2e0ee052aa490d586/markupsafe-3.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:de8a88e63464af587c950061a5e6a67d3632e36df62b986892331d4620a35c01", size = 15077, upload-time = "2025-09-27T18:36:29.025Z" },
{ url = "https://files.pythonhosted.org/packages/35/73/893072b42e6862f319b5207adc9ae06070f095b358655f077f69a35601f0/markupsafe-3.0.3-cp311-cp311-win_arm64.whl", hash = "sha256:3b562dd9e9ea93f13d53989d23a7e775fdfd1066c33494ff43f5418bc8c58a5c", size = 13876, upload-time = "2025-09-27T18:36:29.954Z" },
+ { url = "https://files.pythonhosted.org/packages/5a/72/147da192e38635ada20e0a2e1a51cf8823d2119ce8883f7053879c2199b5/markupsafe-3.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d53197da72cc091b024dd97249dfc7794d6a56530370992a5e1a08983ad9230e", size = 11615, upload-time = "2025-09-27T18:36:30.854Z" },
+ { url = "https://files.pythonhosted.org/packages/9a/81/7e4e08678a1f98521201c3079f77db69fb552acd56067661f8c2f534a718/markupsafe-3.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1872df69a4de6aead3491198eaf13810b565bdbeec3ae2dc8780f14458ec73ce", size = 12020, upload-time = "2025-09-27T18:36:31.971Z" },
+ { url = "https://files.pythonhosted.org/packages/1e/2c/799f4742efc39633a1b54a92eec4082e4f815314869865d876824c257c1e/markupsafe-3.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:3a7e8ae81ae39e62a41ec302f972ba6ae23a5c5396c8e60113e9066ef893da0d", size = 24332, upload-time = "2025-09-27T18:36:32.813Z" },
+ { url = "https://files.pythonhosted.org/packages/3c/2e/8d0c2ab90a8c1d9a24f0399058ab8519a3279d1bd4289511d74e909f060e/markupsafe-3.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d6dd0be5b5b189d31db7cda48b91d7e0a9795f31430b7f271219ab30f1d3ac9d", size = 22947, upload-time = "2025-09-27T18:36:33.86Z" },
+ { url = "https://files.pythonhosted.org/packages/2c/54/887f3092a85238093a0b2154bd629c89444f395618842e8b0c41783898ea/markupsafe-3.0.3-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:94c6f0bb423f739146aec64595853541634bde58b2135f27f61c1ffd1cd4d16a", size = 21962, upload-time = "2025-09-27T18:36:35.099Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/2f/336b8c7b6f4a4d95e91119dc8521402461b74a485558d8f238a68312f11c/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:be8813b57049a7dc738189df53d69395eba14fb99345e0a5994914a3864c8a4b", size = 23760, upload-time = "2025-09-27T18:36:36.001Z" },
+ { url = "https://files.pythonhosted.org/packages/32/43/67935f2b7e4982ffb50a4d169b724d74b62a3964bc1a9a527f5ac4f1ee2b/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:83891d0e9fb81a825d9a6d61e3f07550ca70a076484292a70fde82c4b807286f", size = 21529, upload-time = "2025-09-27T18:36:36.906Z" },
+ { url = "https://files.pythonhosted.org/packages/89/e0/4486f11e51bbba8b0c041098859e869e304d1c261e59244baa3d295d47b7/markupsafe-3.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:77f0643abe7495da77fb436f50f8dab76dbc6e5fd25d39589a0f1fe6548bfa2b", size = 23015, upload-time = "2025-09-27T18:36:37.868Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/e1/78ee7a023dac597a5825441ebd17170785a9dab23de95d2c7508ade94e0e/markupsafe-3.0.3-cp312-cp312-win32.whl", hash = "sha256:d88b440e37a16e651bda4c7c2b930eb586fd15ca7406cb39e211fcff3bf3017d", size = 14540, upload-time = "2025-09-27T18:36:38.761Z" },
+ { url = "https://files.pythonhosted.org/packages/aa/5b/bec5aa9bbbb2c946ca2733ef9c4ca91c91b6a24580193e891b5f7dbe8e1e/markupsafe-3.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:26a5784ded40c9e318cfc2bdb30fe164bdb8665ded9cd64d500a34fb42067b1c", size = 15105, upload-time = "2025-09-27T18:36:39.701Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/f1/216fc1bbfd74011693a4fd837e7026152e89c4bcf3e77b6692fba9923123/markupsafe-3.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:35add3b638a5d900e807944a078b51922212fb3dedb01633a8defc4b01a3c85f", size = 13906, upload-time = "2025-09-27T18:36:40.689Z" },
]
[[package]]
@@ -1040,6 +1117,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/53/73/32f2aaa22c1e4afae337106baf0c938abf36a6cc879cfee83a00461bbbf7/murmurhash-1.0.15-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3c69b4d3bcd6233782a78907fe10b9b7a796bdc5d28060cf097d067bec280a5d", size = 127214, upload-time = "2025-11-14T09:50:09.265Z" },
{ url = "https://files.pythonhosted.org/packages/82/ed/812103a7f353eba2d83655b08205e13a38c93b4db0692f94756e1eb44516/murmurhash-1.0.15-cp311-cp311-win_amd64.whl", hash = "sha256:e43a69496342ce530bdd670264cb7c8f45490b296e4764c837ce577e3c7ebd53", size = 25241, upload-time = "2025-11-14T09:50:10.373Z" },
{ url = "https://files.pythonhosted.org/packages/eb/5f/2c511bdd28f7c24da37a00116ffd0432b65669d098f0d0260c66ac0ffdc2/murmurhash-1.0.15-cp311-cp311-win_arm64.whl", hash = "sha256:f3e99a6ee36ef5372df5f138e3d9c801420776d3641a34a49e5c2555f44edba7", size = 23216, upload-time = "2025-11-14T09:50:11.651Z" },
+ { url = "https://files.pythonhosted.org/packages/b6/46/be8522d3456fdccf1b8b049c6d82e7a3c1114c4fc2cfe14b04cba4b3e701/murmurhash-1.0.15-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d37e3ae44746bca80b1a917c2ea625cf216913564ed43f69d2888e5df97db0cb", size = 27884, upload-time = "2025-11-14T09:50:13.133Z" },
+ { url = "https://files.pythonhosted.org/packages/ed/cc/630449bf4f6178d7daf948ce46ad00b25d279065fc30abd8d706be3d87e0/murmurhash-1.0.15-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:0861cb11039409eaf46878456b7d985ef17b6b484103a6fc367b2ecec846891d", size = 27855, upload-time = "2025-11-14T09:50:14.859Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/30/ea8f601a9bf44db99468696efd59eb9cff1157cd55cb586d67116697583f/murmurhash-1.0.15-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:5a301decfaccfec70fe55cb01dde2a012c3014a874542eaa7cc73477bb749616", size = 134088, upload-time = "2025-11-14T09:50:15.958Z" },
+ { url = "https://files.pythonhosted.org/packages/c9/de/c40ce8c0877d406691e735b8d6e9c815f36a82b499d358313db5dbe219d7/murmurhash-1.0.15-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:32c6fde7bd7e9407003370a07b5f4addacabe1556ad3dc2cac246b7a2bba3400", size = 133978, upload-time = "2025-11-14T09:50:17.572Z" },
+ { url = "https://files.pythonhosted.org/packages/47/84/bd49963ecd84ebab2fe66595e2d1ed41d5e8b5153af5dc930f0bd827007c/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5d8b43a7011540dc3c7ce66f2134df9732e2bc3bbb4a35f6458bc755e48bde26", size = 132956, upload-time = "2025-11-14T09:50:18.742Z" },
+ { url = "https://files.pythonhosted.org/packages/4f/7c/2530769c545074417c862583f05f4245644599f1e9ff619b3dfe2969aafc/murmurhash-1.0.15-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:43bf4541892ecd95963fcd307bf1c575fc0fee1682f41c93007adee71ca2bb40", size = 134184, upload-time = "2025-11-14T09:50:19.941Z" },
+ { url = "https://files.pythonhosted.org/packages/84/a4/b249b042f5afe34d14ada2dc4afc777e883c15863296756179652e081c44/murmurhash-1.0.15-cp312-cp312-win_amd64.whl", hash = "sha256:f4ac15a2089dc42e6eb0966622d42d2521590a12c92480aafecf34c085302cca", size = 25647, upload-time = "2025-11-14T09:50:21.049Z" },
+ { url = "https://files.pythonhosted.org/packages/13/bf/028179259aebc18fd4ba5cae2601d1d47517427a537ab44336446431a215/murmurhash-1.0.15-cp312-cp312-win_arm64.whl", hash = "sha256:4a70ca4ae19e600d9be3da64d00710e79dde388a4d162f22078d64844d0ebdda", size = 23338, upload-time = "2025-11-14T09:50:22.359Z" },
]
[[package]]
@@ -1082,6 +1167,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/df/a0/4e0f14d847cfc2a633a1c8621d00724f3206cfeddeb66d35698c4e2cf3d2/numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a", size = 18093567, upload-time = "2024-02-05T23:54:11.696Z" },
{ url = "https://files.pythonhosted.org/packages/d2/b7/a734c733286e10a7f1a8ad1ae8c90f2d33bf604a96548e0a4a3a6739b468/numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20", size = 5968812, upload-time = "2024-02-05T23:54:26.453Z" },
{ url = "https://files.pythonhosted.org/packages/3f/6b/5610004206cf7f8e7ad91c5a85a8c71b2f2f8051a0c0c4d5916b76d6cbb2/numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2", size = 15811913, upload-time = "2024-02-05T23:54:53.933Z" },
+ { url = "https://files.pythonhosted.org/packages/95/12/8f2020a8e8b8383ac0177dc9570aad031a3beb12e38847f7129bacd96228/numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218", size = 20335901, upload-time = "2024-02-05T23:55:32.801Z" },
+ { url = "https://files.pythonhosted.org/packages/75/5b/ca6c8bd14007e5ca171c7c03102d17b4f4e0ceb53957e8c44343a9546dcc/numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b", size = 13685868, upload-time = "2024-02-05T23:55:56.28Z" },
+ { url = "https://files.pythonhosted.org/packages/79/f8/97f10e6755e2a7d027ca783f63044d5b1bc1ae7acb12afe6a9b4286eac17/numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b", size = 13925109, upload-time = "2024-02-05T23:56:20.368Z" },
+ { url = "https://files.pythonhosted.org/packages/0f/50/de23fde84e45f5c4fda2488c759b69990fd4512387a8632860f3ac9cd225/numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed", size = 17950613, upload-time = "2024-02-05T23:56:56.054Z" },
+ { url = "https://files.pythonhosted.org/packages/4c/0c/9c603826b6465e82591e05ca230dfc13376da512b25ccd0894709b054ed0/numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a", size = 13572172, upload-time = "2024-02-05T23:57:21.56Z" },
+ { url = "https://files.pythonhosted.org/packages/76/8c/2ba3902e1a0fc1c74962ea9bb33a534bb05984ad7ff9515bf8d07527cadd/numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0", size = 17786643, upload-time = "2024-02-05T23:57:56.585Z" },
+ { url = "https://files.pythonhosted.org/packages/28/4a/46d9e65106879492374999e76eb85f87b15328e06bd1550668f79f7b18c6/numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110", size = 5677803, upload-time = "2024-02-05T23:58:08.963Z" },
+ { url = "https://files.pythonhosted.org/packages/16/2e/86f24451c2d530c88daf997cb8d6ac622c1d40d19f5a031ed68a4b73a374/numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818", size = 15517754, upload-time = "2024-02-05T23:58:36.364Z" },
]
[[package]]
@@ -1128,6 +1221,13 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/00/a5ac8c7a0e67fd1a6059e40aa08fa1c52cc00709077d2300e210c3ce0322/pandas-2.3.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37b5848ba49824e5c30bedb9c830ab9b7751fd049bc7914533e01c65f79791", size = 13240453, upload-time = "2025-09-29T23:19:09.247Z" },
{ url = "https://files.pythonhosted.org/packages/27/4d/5c23a5bc7bd209231618dd9e606ce076272c9bc4f12023a70e03a86b4067/pandas-2.3.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:db4301b2d1f926ae677a751eb2bd0e8c5f5319c9cb3f88b0becbbb0b07b34151", size = 13890361, upload-time = "2025-09-29T23:19:25.342Z" },
{ url = "https://files.pythonhosted.org/packages/8e/59/712db1d7040520de7a4965df15b774348980e6df45c129b8c64d0dbe74ef/pandas-2.3.3-cp311-cp311-win_amd64.whl", hash = "sha256:f086f6fe114e19d92014a1966f43a3e62285109afe874f067f5abbdcbb10e59c", size = 11348702, upload-time = "2025-09-29T23:19:38.296Z" },
+ { url = "https://files.pythonhosted.org/packages/9c/fb/231d89e8637c808b997d172b18e9d4a4bc7bf31296196c260526055d1ea0/pandas-2.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6d21f6d74eb1725c2efaa71a2bfc661a0689579b58e9c0ca58a739ff0b002b53", size = 11597846, upload-time = "2025-09-29T23:19:48.856Z" },
+ { url = "https://files.pythonhosted.org/packages/5c/bd/bf8064d9cfa214294356c2d6702b716d3cf3bb24be59287a6a21e24cae6b/pandas-2.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3fd2f887589c7aa868e02632612ba39acb0b8948faf5cc58f0850e165bd46f35", size = 10729618, upload-time = "2025-09-29T23:39:08.659Z" },
+ { url = "https://files.pythonhosted.org/packages/57/56/cf2dbe1a3f5271370669475ead12ce77c61726ffd19a35546e31aa8edf4e/pandas-2.3.3-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecaf1e12bdc03c86ad4a7ea848d66c685cb6851d807a26aa245ca3d2017a1908", size = 11737212, upload-time = "2025-09-29T23:19:59.765Z" },
+ { url = "https://files.pythonhosted.org/packages/e5/63/cd7d615331b328e287d8233ba9fdf191a9c2d11b6af0c7a59cfcec23de68/pandas-2.3.3-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b3d11d2fda7eb164ef27ffc14b4fcab16a80e1ce67e9f57e19ec0afaf715ba89", size = 12362693, upload-time = "2025-09-29T23:20:14.098Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/de/8b1895b107277d52f2b42d3a6806e69cfef0d5cf1d0ba343470b9d8e0a04/pandas-2.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a68e15f780eddf2b07d242e17a04aa187a7ee12b40b930bfdd78070556550e98", size = 12771002, upload-time = "2025-09-29T23:20:26.76Z" },
+ { url = "https://files.pythonhosted.org/packages/87/21/84072af3187a677c5893b170ba2c8fbe450a6ff911234916da889b698220/pandas-2.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:371a4ab48e950033bcf52b6527eccb564f52dc826c02afd9a1bc0ab731bba084", size = 13450971, upload-time = "2025-09-29T23:20:41.344Z" },
+ { url = "https://files.pythonhosted.org/packages/86/41/585a168330ff063014880a80d744219dbf1dd7a1c706e75ab3425a987384/pandas-2.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:a16dcec078a01eeef8ee61bf64074b4e524a2a3f4b3be9326420cabe59c4778b", size = 10992722, upload-time = "2025-09-29T23:20:54.139Z" },
]
[[package]]
@@ -1162,11 +1262,11 @@ wheels = [
[[package]]
name = "platformdirs"
-version = "4.5.0"
+version = "4.5.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/61/33/9611380c2bdb1225fdef633e2a9610622310fed35ab11dac9620972ee088/platformdirs-4.5.0.tar.gz", hash = "sha256:70ddccdd7c99fc5942e9fc25636a8b34d04c24b335100223152c2803e4063312", size = 21632, upload-time = "2025-10-08T17:44:48.791Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/cf/86/0248f086a84f01b37aaec0fa567b397df1a119f73c16f6c7a9aac73ea309/platformdirs-4.5.1.tar.gz", hash = "sha256:61d5cdcc6065745cdd94f0f878977f8de9437be93de97c1c12f853c9c0cdcbda", size = 21715, upload-time = "2025-12-05T13:52:58.638Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/73/cb/ac7874b3e5d58441674fb70742e6c374b28b0c7cb988d37d991cde47166c/platformdirs-4.5.0-py3-none-any.whl", hash = "sha256:e578a81bb873cbb89a41fcc904c7ef523cc18284b7e3b3ccf06aca1403b7ebd3", size = 18651, upload-time = "2025-10-08T17:44:47.223Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/28/3bfe2fa5a7b9c46fe7e13c97bda14c895fb10fa2ebf1d0abb90e0cea7ee1/platformdirs-4.5.1-py3-none-any.whl", hash = "sha256:d03afa3963c806a9bed9d5125c8f4cb2fdaf74a55ab60e5d59b3fde758104d31", size = 18731, upload-time = "2025-12-05T13:52:56.823Z" },
]
[[package]]
@@ -1220,6 +1320,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/51/46/025f60fd3d51bf60606a0f8f0cd39c40068b9b5e4d249bca1682e4ff09c3/preshed-3.0.12-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:57159bcedca0cb4c99390f8a6e730f8659fdb663a5a3efcd9c4531e0f54b150e", size = 865504, upload-time = "2025-11-17T12:59:29.648Z" },
{ url = "https://files.pythonhosted.org/packages/88/b5/2e6ee5ab19b03e7983fc5e1850c812fb71dc178dd140d6aca3b45306bdf7/preshed-3.0.12-cp311-cp311-win_amd64.whl", hash = "sha256:8fe9cf1745e203e5aa58b8700436f78da1dcf0f0e2efb0054b467effd9d7d19d", size = 117736, upload-time = "2025-11-17T12:59:30.974Z" },
{ url = "https://files.pythonhosted.org/packages/1e/17/8a0a8f4b01e71b5fb7c5cd4c9fec04d7b852d42f1f9e096b01e7d2b16b17/preshed-3.0.12-cp311-cp311-win_arm64.whl", hash = "sha256:12d880f8786cb6deac34e99b8b07146fb92d22fbca0023208e03325f5944606b", size = 105127, upload-time = "2025-11-17T12:59:32.171Z" },
+ { url = "https://files.pythonhosted.org/packages/4b/f7/ff3aca937eeaee19c52c45ddf92979546e52ed0686e58be4bc09c47e7d88/preshed-3.0.12-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2779861f5d69480493519ed123a622a13012d1182126779036b99d9d989bf7e9", size = 129958, upload-time = "2025-11-17T12:59:33.391Z" },
+ { url = "https://files.pythonhosted.org/packages/80/24/fd654a9c0f5f3ed1a9b1d8a392f063ae9ca29ad0b462f0732ae0147f7cee/preshed-3.0.12-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ffe1fd7d92f51ed34383e20d8b734780c814ca869cfdb7e07f2d31651f90cdf4", size = 124550, upload-time = "2025-11-17T12:59:34.688Z" },
+ { url = "https://files.pythonhosted.org/packages/71/49/8271c7f680696f4b0880f44357d2a903d649cb9f6e60a1efc97a203104df/preshed-3.0.12-cp312-cp312-manylinux1_x86_64.manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:91893404858502cc4e856d338fef3d2a4a552135f79a1041c24eb919817c19db", size = 874987, upload-time = "2025-11-17T12:59:36.062Z" },
+ { url = "https://files.pythonhosted.org/packages/a3/a5/ca200187ca1632f1e2c458b72f1bd100fa8b55deecd5d72e1e4ebf09e98c/preshed-3.0.12-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:9e06e8f2ba52f183eb9817a616cdebe84a211bb859a2ffbc23f3295d0b189638", size = 866499, upload-time = "2025-11-17T12:59:37.586Z" },
+ { url = "https://files.pythonhosted.org/packages/87/a1/943b61f850c44899910c21996cb542d0ef5931744c6d492fdfdd8457e693/preshed-3.0.12-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:bbe8b8a2d4f9af14e8a39ecca524b9de6defc91d8abcc95eb28f42da1c23272c", size = 878064, upload-time = "2025-11-17T12:59:39.651Z" },
+ { url = "https://files.pythonhosted.org/packages/3e/75/d7fff7f1fa3763619aa85d6ba70493a5d9c6e6ea7958a6e8c9d3e6e88bbe/preshed-3.0.12-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5d0aaac9c5862f5471fddd0c931dc64d3af2efc5fe3eb48b50765adb571243b9", size = 900540, upload-time = "2025-11-17T12:59:41.384Z" },
+ { url = "https://files.pythonhosted.org/packages/e4/12/a2285b78bd097a1e53fb90a1743bc8ce0d35e5b65b6853f3b3c47da398ca/preshed-3.0.12-cp312-cp312-win_amd64.whl", hash = "sha256:0eb8d411afcb1e3b12a0602fb6a0e33140342a732a795251a0ce452aba401dc0", size = 118298, upload-time = "2025-11-17T12:59:42.65Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/34/4e8443fe99206a2fcfc63659969a8f8c8ab184836533594a519f3899b1ad/preshed-3.0.12-cp312-cp312-win_arm64.whl", hash = "sha256:dcd3d12903c9f720a39a5c5f1339f7f46e3ab71279fb7a39776768fb840b6077", size = 104746, upload-time = "2025-11-17T12:59:43.934Z" },
]
[[package]]
@@ -1325,6 +1433,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/72/9d/a241db83f973049a1092a079272ffe2e3e82e98561ef6214ab53fe53b1c7/pydantic_core-2.27.2-cp311-cp311-win32.whl", hash = "sha256:c70c26d2c99f78b125a3459f8afe1aed4d9687c24fd677c6a4436bc042e50d6c", size = 1812361, upload-time = "2024-12-18T11:28:23.53Z" },
{ url = "https://files.pythonhosted.org/packages/e8/ef/013f07248041b74abd48a385e2110aa3a9bbfef0fbd97d4e6d07d2f5b89a/pydantic_core-2.27.2-cp311-cp311-win_amd64.whl", hash = "sha256:08e125dbdc505fa69ca7d9c499639ab6407cfa909214d500897d02afb816e7cc", size = 1982484, upload-time = "2024-12-18T11:28:25.391Z" },
{ url = "https://files.pythonhosted.org/packages/10/1c/16b3a3e3398fd29dca77cea0a1d998d6bde3902fa2706985191e2313cc76/pydantic_core-2.27.2-cp311-cp311-win_arm64.whl", hash = "sha256:26f0d68d4b235a2bae0c3fc585c585b4ecc51382db0e3ba402a22cbc440915e4", size = 1867102, upload-time = "2024-12-18T11:28:28.593Z" },
+ { url = "https://files.pythonhosted.org/packages/d6/74/51c8a5482ca447871c93e142d9d4a92ead74de6c8dc5e66733e22c9bba89/pydantic_core-2.27.2-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:9e0c8cfefa0ef83b4da9588448b6d8d2a2bf1a53c3f1ae5fca39eb3061e2f0b0", size = 1893127, upload-time = "2024-12-18T11:28:30.346Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/f3/c97e80721735868313c58b89d2de85fa80fe8dfeeed84dc51598b92a135e/pydantic_core-2.27.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:83097677b8e3bd7eaa6775720ec8e0405f1575015a463285a92bfdfe254529ef", size = 1811340, upload-time = "2024-12-18T11:28:32.521Z" },
+ { url = "https://files.pythonhosted.org/packages/9e/91/840ec1375e686dbae1bd80a9e46c26a1e0083e1186abc610efa3d9a36180/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:172fce187655fece0c90d90a678424b013f8fbb0ca8b036ac266749c09438cb7", size = 1822900, upload-time = "2024-12-18T11:28:34.507Z" },
+ { url = "https://files.pythonhosted.org/packages/f6/31/4240bc96025035500c18adc149aa6ffdf1a0062a4b525c932065ceb4d868/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:519f29f5213271eeeeb3093f662ba2fd512b91c5f188f3bb7b27bc5973816934", size = 1869177, upload-time = "2024-12-18T11:28:36.488Z" },
+ { url = "https://files.pythonhosted.org/packages/fa/20/02fbaadb7808be578317015c462655c317a77a7c8f0ef274bc016a784c54/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:05e3a55d124407fffba0dd6b0c0cd056d10e983ceb4e5dbd10dda135c31071d6", size = 2038046, upload-time = "2024-12-18T11:28:39.409Z" },
+ { url = "https://files.pythonhosted.org/packages/06/86/7f306b904e6c9eccf0668248b3f272090e49c275bc488a7b88b0823444a4/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9c3ed807c7b91de05e63930188f19e921d1fe90de6b4f5cd43ee7fcc3525cb8c", size = 2685386, upload-time = "2024-12-18T11:28:41.221Z" },
+ { url = "https://files.pythonhosted.org/packages/8d/f0/49129b27c43396581a635d8710dae54a791b17dfc50c70164866bbf865e3/pydantic_core-2.27.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6fb4aadc0b9a0c063206846d603b92030eb6f03069151a625667f982887153e2", size = 1997060, upload-time = "2024-12-18T11:28:44.709Z" },
+ { url = "https://files.pythonhosted.org/packages/0d/0f/943b4af7cd416c477fd40b187036c4f89b416a33d3cc0ab7b82708a667aa/pydantic_core-2.27.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:28ccb213807e037460326424ceb8b5245acb88f32f3d2777427476e1b32c48c4", size = 2004870, upload-time = "2024-12-18T11:28:46.839Z" },
+ { url = "https://files.pythonhosted.org/packages/35/40/aea70b5b1a63911c53a4c8117c0a828d6790483f858041f47bab0b779f44/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:de3cd1899e2c279b140adde9357c4495ed9d47131b4a4eaff9052f23398076b3", size = 1999822, upload-time = "2024-12-18T11:28:48.896Z" },
+ { url = "https://files.pythonhosted.org/packages/f2/b3/807b94fd337d58effc5498fd1a7a4d9d59af4133e83e32ae39a96fddec9d/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:220f892729375e2d736b97d0e51466252ad84c51857d4d15f5e9692f9ef12be4", size = 2130364, upload-time = "2024-12-18T11:28:50.755Z" },
+ { url = "https://files.pythonhosted.org/packages/fc/df/791c827cd4ee6efd59248dca9369fb35e80a9484462c33c6649a8d02b565/pydantic_core-2.27.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:a0fcd29cd6b4e74fe8ddd2c90330fd8edf2e30cb52acda47f06dd615ae72da57", size = 2158303, upload-time = "2024-12-18T11:28:54.122Z" },
+ { url = "https://files.pythonhosted.org/packages/9b/67/4e197c300976af185b7cef4c02203e175fb127e414125916bf1128b639a9/pydantic_core-2.27.2-cp312-cp312-win32.whl", hash = "sha256:1e2cb691ed9834cd6a8be61228471d0a503731abfb42f82458ff27be7b2186fc", size = 1834064, upload-time = "2024-12-18T11:28:56.074Z" },
+ { url = "https://files.pythonhosted.org/packages/1f/ea/cd7209a889163b8dcca139fe32b9687dd05249161a3edda62860430457a5/pydantic_core-2.27.2-cp312-cp312-win_amd64.whl", hash = "sha256:cc3f1a99a4f4f9dd1de4fe0312c114e740b5ddead65bb4102884b384c15d8bc9", size = 1989046, upload-time = "2024-12-18T11:28:58.107Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/49/c54baab2f4658c26ac633d798dab66b4c3a9bbf47cff5284e9c182f4137a/pydantic_core-2.27.2-cp312-cp312-win_arm64.whl", hash = "sha256:3911ac9284cd8a1792d3cb26a2da18f3ca26c6908cc434a18f730dc0db7bfa3b", size = 1885092, upload-time = "2024-12-18T11:29:01.335Z" },
{ url = "https://files.pythonhosted.org/packages/46/72/af70981a341500419e67d5cb45abe552a7c74b66326ac8877588488da1ac/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_10_12_x86_64.whl", hash = "sha256:2bf14caea37e91198329b828eae1618c068dfb8ef17bb33287a7ad4b61ac314e", size = 1891159, upload-time = "2024-12-18T11:30:54.382Z" },
{ url = "https://files.pythonhosted.org/packages/ad/3d/c5913cccdef93e0a6a95c2d057d2c2cba347815c845cda79ddd3c0f5e17d/pydantic_core-2.27.2-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:b0cb791f5b45307caae8810c2023a184c74605ec3bcbb67d13846c28ff731ff8", size = 1768331, upload-time = "2024-12-18T11:30:58.178Z" },
{ url = "https://files.pythonhosted.org/packages/f6/f0/a3ae8fbee269e4934f14e2e0e00928f9346c5943174f2811193113e58252/pydantic_core-2.27.2-pp310-pypy310_pp73-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:688d3fd9fcb71f41c4c015c023d12a79d1c4c0732ec9eb35d96e3388a120dcf3", size = 1822467, upload-time = "2024-12-18T11:31:00.6Z" },
@@ -1347,15 +1469,15 @@ wheels = [
[[package]]
name = "pymdown-extensions"
-version = "10.17.2"
+version = "10.18"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "markdown" },
{ name = "pyyaml" },
]
-sdist = { url = "https://files.pythonhosted.org/packages/25/6d/af5378dbdb379fddd9a277f8b9888c027db480cde70028669ebd009d642a/pymdown_extensions-10.17.2.tar.gz", hash = "sha256:26bb3d7688e651606260c90fb46409fbda70bf9fdc3623c7868643a1aeee4713", size = 847344, upload-time = "2025-11-26T15:43:57.004Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/d4/95/e4fa281e3f13b3d9c4aaebb21ef44879840325fa418276dd921209a5e9f9/pymdown_extensions-10.18.tar.gz", hash = "sha256:20252abe6367354b24191431617a072ee6be9f68c5afcc74ea5573508a61f9e5", size = 847697, upload-time = "2025-12-07T17:22:12.857Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/93/78/b93cb80bd673bdc9f6ede63d8eb5b4646366953df15667eb3603be57a2b1/pymdown_extensions-10.17.2-py3-none-any.whl", hash = "sha256:bffae79a2e8b9e44aef0d813583a8fea63457b7a23643a43988055b7b79b4992", size = 266556, upload-time = "2025-11-26T15:43:55.162Z" },
+ { url = "https://files.pythonhosted.org/packages/46/a4/aa2bada4a2fd648f40f19affa55d2c01dc7ff5ea9cffd3dfdeb6114951db/pymdown_extensions-10.18-py3-none-any.whl", hash = "sha256:090bca72be43f7d3186374e23c782899dbef9dc153ef24c59dcd3c346f9ffcae", size = 266703, upload-time = "2025-12-07T17:22:11.22Z" },
]
[[package]]
@@ -1447,6 +1569,16 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
+ { 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" },
]
[[package]]
@@ -1547,6 +1679,20 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/66/47/dc2577c1f95f188c1e13e2e69d8825a5ac582ac709942f8a03af42ed6e93/regex-2025.11.3-cp311-cp311-win32.whl", hash = "sha256:3e816cc9aac1cd3cc9a4ec4d860f06d40f994b5c7b4d03b93345f44e08cc68bf", size = 265812, upload-time = "2025-11-03T21:31:29.72Z" },
{ url = "https://files.pythonhosted.org/packages/50/1e/15f08b2f82a9bbb510621ec9042547b54d11e83cb620643ebb54e4eb7d71/regex-2025.11.3-cp311-cp311-win_amd64.whl", hash = "sha256:087511f5c8b7dfbe3a03f5d5ad0c2a33861b1fc387f21f6f60825a44865a385a", size = 277737, upload-time = "2025-11-03T21:31:31.422Z" },
{ url = "https://files.pythonhosted.org/packages/f4/fc/6500eb39f5f76c5e47a398df82e6b535a5e345f839581012a418b16f9cc3/regex-2025.11.3-cp311-cp311-win_arm64.whl", hash = "sha256:1ff0d190c7f68ae7769cd0313fe45820ba07ffebfddfaa89cc1eb70827ba0ddc", size = 270290, upload-time = "2025-11-03T21:31:33.041Z" },
+ { url = "https://files.pythonhosted.org/packages/e8/74/18f04cb53e58e3fb107439699bd8375cf5a835eec81084e0bddbd122e4c2/regex-2025.11.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bc8ab71e2e31b16e40868a40a69007bc305e1109bd4658eb6cad007e0bf67c41", size = 489312, upload-time = "2025-11-03T21:31:34.343Z" },
+ { url = "https://files.pythonhosted.org/packages/78/3f/37fcdd0d2b1e78909108a876580485ea37c91e1acf66d3bb8e736348f441/regex-2025.11.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:22b29dda7e1f7062a52359fca6e58e548e28c6686f205e780b02ad8ef710de36", size = 291256, upload-time = "2025-11-03T21:31:35.675Z" },
+ { url = "https://files.pythonhosted.org/packages/bf/26/0a575f58eb23b7ebd67a45fccbc02ac030b737b896b7e7a909ffe43ffd6a/regex-2025.11.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:3a91e4a29938bc1a082cc28fdea44be420bf2bebe2665343029723892eb073e1", size = 288921, upload-time = "2025-11-03T21:31:37.07Z" },
+ { url = "https://files.pythonhosted.org/packages/ea/98/6a8dff667d1af907150432cf5abc05a17ccd32c72a3615410d5365ac167a/regex-2025.11.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:08b884f4226602ad40c5d55f52bf91a9df30f513864e0054bad40c0e9cf1afb7", size = 798568, upload-time = "2025-11-03T21:31:38.784Z" },
+ { url = "https://files.pythonhosted.org/packages/64/15/92c1db4fa4e12733dd5a526c2dd2b6edcbfe13257e135fc0f6c57f34c173/regex-2025.11.3-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3e0b11b2b2433d1c39c7c7a30e3f3d0aeeea44c2a8d0bae28f6b95f639927a69", size = 864165, upload-time = "2025-11-03T21:31:40.559Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/e7/3ad7da8cdee1ce66c7cd37ab5ab05c463a86ffeb52b1a25fe7bd9293b36c/regex-2025.11.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:87eb52a81ef58c7ba4d45c3ca74e12aa4b4e77816f72ca25258a85b3ea96cb48", size = 912182, upload-time = "2025-11-03T21:31:42.002Z" },
+ { url = "https://files.pythonhosted.org/packages/84/bd/9ce9f629fcb714ffc2c3faf62b6766ecb7a585e1e885eb699bcf130a5209/regex-2025.11.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a12ab1f5c29b4e93db518f5e3872116b7e9b1646c9f9f426f777b50d44a09e8c", size = 803501, upload-time = "2025-11-03T21:31:43.815Z" },
+ { url = "https://files.pythonhosted.org/packages/7c/0f/8dc2e4349d8e877283e6edd6c12bdcebc20f03744e86f197ab6e4492bf08/regex-2025.11.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7521684c8c7c4f6e88e35ec89680ee1aa8358d3f09d27dfbdf62c446f5d4c695", size = 787842, upload-time = "2025-11-03T21:31:45.353Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/73/cff02702960bc185164d5619c0c62a2f598a6abff6695d391b096237d4ab/regex-2025.11.3-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7fe6e5440584e94cc4b3f5f4d98a25e29ca12dccf8873679a635638349831b98", size = 858519, upload-time = "2025-11-03T21:31:46.814Z" },
+ { url = "https://files.pythonhosted.org/packages/61/83/0e8d1ae71e15bc1dc36231c90b46ee35f9d52fab2e226b0e039e7ea9c10a/regex-2025.11.3-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:8e026094aa12b43f4fd74576714e987803a315c76edb6b098b9809db5de58f74", size = 850611, upload-time = "2025-11-03T21:31:48.289Z" },
+ { url = "https://files.pythonhosted.org/packages/c8/f5/70a5cdd781dcfaa12556f2955bf170cd603cb1c96a1827479f8faea2df97/regex-2025.11.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:435bbad13e57eb5606a68443af62bed3556de2f46deb9f7d4237bc2f1c9fb3a0", size = 789759, upload-time = "2025-11-03T21:31:49.759Z" },
+ { url = "https://files.pythonhosted.org/packages/59/9b/7c29be7903c318488983e7d97abcf8ebd3830e4c956c4c540005fcfb0462/regex-2025.11.3-cp312-cp312-win32.whl", hash = "sha256:3839967cf4dc4b985e1570fd8d91078f0c519f30491c60f9ac42a8db039be204", size = 266194, upload-time = "2025-11-03T21:31:51.53Z" },
+ { url = "https://files.pythonhosted.org/packages/1a/67/3b92df89f179d7c367be654ab5626ae311cb28f7d5c237b6bb976cd5fbbb/regex-2025.11.3-cp312-cp312-win_amd64.whl", hash = "sha256:e721d1b46e25c481dc5ded6f4b3f66c897c58d2e8cfdf77bbced84339108b0b9", size = 277069, upload-time = "2025-11-03T21:31:53.151Z" },
+ { url = "https://files.pythonhosted.org/packages/d7/55/85ba4c066fe5094d35b249c3ce8df0ba623cfd35afb22d6764f23a52a1c5/regex-2025.11.3-cp312-cp312-win_arm64.whl", hash = "sha256:64350685ff08b1d3a6fff33f45a9ca183dc1d58bbfe4981604e70ec9801bbc26", size = 270330, upload-time = "2025-11-03T21:31:54.514Z" },
]
[[package]]
@@ -1668,6 +1814,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/d8/da/692b51e9e5be2766d2d1fb9a7c8122cfd99c337570e621f09c40ce94ad17/spacy-3.8.11-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:3f3cb91d7d42fafd92b8d5bf9f696571170d2f0747f85724a2c5b997753e33c9", size = 33117270, upload-time = "2025-11-17T20:38:53.596Z" },
{ url = "https://files.pythonhosted.org/packages/9b/13/a542ac9b61d071f3328fda1fd8087b523fb7a4f2c340010bc70b1f762485/spacy-3.8.11-cp311-cp311-win_amd64.whl", hash = "sha256:745c190923584935272188c604e0cc170f4179aace1025814a25d92ee90cf3de", size = 15348350, upload-time = "2025-11-17T20:38:56.833Z" },
{ url = "https://files.pythonhosted.org/packages/23/53/975c16514322f6385d6caa5929771613d69f5458fb24f03e189ba533f279/spacy-3.8.11-cp311-cp311-win_arm64.whl", hash = "sha256:27535d81d9dee0483b66660cadd93d14c1668f55e4faf4386aca4a11a41a8b97", size = 14701913, upload-time = "2025-11-17T20:38:59.507Z" },
+ { url = "https://files.pythonhosted.org/packages/51/fb/01eadf4ba70606b3054702dc41fc2ccf7d70fb14514b3cd57f0ff78ebea8/spacy-3.8.11-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:aa1ee8362074c30098feaaf2dd888c829a1a79c4311eec1b117a0a61f16fa6dd", size = 6073726, upload-time = "2025-11-17T20:39:01.679Z" },
+ { url = "https://files.pythonhosted.org/packages/3a/f8/07b03a2997fc2621aaeafae00af50f55522304a7da6926b07027bb6d0709/spacy-3.8.11-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:75a036d04c2cf11d6cb566c0a689860cc5a7a75b439e8fea1b3a6b673dabf25d", size = 5724702, upload-time = "2025-11-17T20:39:03.486Z" },
+ { url = "https://files.pythonhosted.org/packages/13/0c/c4fa0f379dbe3258c305d2e2df3760604a9fcd71b34f8f65c23e43f4cf55/spacy-3.8.11-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7cb599d2747d4a59a5f90e8a453c149b13db382a8297925cf126333141dbc4f7", size = 32727774, upload-time = "2025-11-17T20:39:05.894Z" },
+ { url = "https://files.pythonhosted.org/packages/ce/8e/6a4ba82bed480211ebdf5341b0f89e7271b454307525ac91b5e447825914/spacy-3.8.11-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:94632e302ad2fb79dc285bf1e9e4d4a178904d5c67049e0e02b7fb4a77af85c4", size = 33215053, upload-time = "2025-11-17T20:39:08.588Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/bc/44d863d248e9d7358c76a0aa8b3f196b8698df520650ed8de162e18fbffb/spacy-3.8.11-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aeca6cf34009d48cda9fb1bbfb532469e3d643817241a73e367b34ab99a5806f", size = 32074195, upload-time = "2025-11-17T20:39:11.601Z" },
+ { url = "https://files.pythonhosted.org/packages/6f/7d/0b115f3f16e1dd2d3f99b0f89497867fc11c41aed94f4b7a4367b4b54136/spacy-3.8.11-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:368a79b8df925b15d89dccb5e502039446fb2ce93cf3020e092d5b962c3349b9", size = 32996143, upload-time = "2025-11-17T20:39:14.705Z" },
+ { url = "https://files.pythonhosted.org/packages/7d/48/7e9581b476df76aaf9ee182888d15322e77c38b0bbbd5e80160ba0bddd4c/spacy-3.8.11-cp312-cp312-win_amd64.whl", hash = "sha256:88d65941a87f58d75afca1785bd64d01183a92f7269dcbcf28bd9d6f6a77d1a7", size = 14217511, upload-time = "2025-11-17T20:39:17.316Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/1f/307a16f32f90aa5ee7ad8d29ff8620a57132b80a4c8c536963d46d192e1a/spacy-3.8.11-cp312-cp312-win_arm64.whl", hash = "sha256:97b865d6d3658e2ab103a67d6c8a2d678e193e84a07f40d9938565b669ceee39", size = 13614446, upload-time = "2025-11-17T20:39:19.748Z" },
]
[[package]]
@@ -1720,6 +1874,13 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/91/7c/9a2c9d8141daf7b7a6f092c2be403421a0ab280e7c03cc62c223f37fdf47/srsly-2.5.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:9d5be1d8b79a4c4180073461425cb49c8924a184ab49d976c9c81a7bf87731d9", size = 1103935, upload-time = "2025-11-17T14:09:58.576Z" },
{ url = "https://files.pythonhosted.org/packages/f1/ad/8ae727430368fedbb1a7fa41b62d7a86237558bc962c5c5a9aa8bfa82548/srsly-2.5.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:c8e42d6bcddda2e6fc1a8438cc050c4a36d0e457a63bcc7117d23c5175dfedec", size = 1117985, upload-time = "2025-11-17T14:10:00.348Z" },
{ url = "https://files.pythonhosted.org/packages/60/69/d6afaef1a8d5192fd802752115c7c3cc104493a7d604b406112b8bc2b610/srsly-2.5.2-cp311-cp311-win_amd64.whl", hash = "sha256:e7362981e687eead00248525c3ef3b8ddd95904c93362c481988d91b26b6aeef", size = 654148, upload-time = "2025-11-17T14:10:01.772Z" },
+ { url = "https://files.pythonhosted.org/packages/8f/1c/21f658d98d602a559491b7886c7ca30245c2cd8987ff1b7709437c0f74b1/srsly-2.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:6f92b4f883e6be4ca77f15980b45d394d310f24903e25e1b2c46df783c7edcce", size = 656161, upload-time = "2025-11-17T14:10:03.181Z" },
+ { url = "https://files.pythonhosted.org/packages/2f/a2/bc6fd484ed703857043ae9abd6c9aea9152f9480a6961186ee6c1e0c49e8/srsly-2.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ac4790a54b00203f1af5495b6b8ac214131139427f30fcf05cf971dde81930eb", size = 653237, upload-time = "2025-11-17T14:10:04.636Z" },
+ { url = "https://files.pythonhosted.org/packages/ab/ea/e3895da29a15c8d325e050ad68a0d1238eece1d2648305796adf98dcba66/srsly-2.5.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ce5c6b016050857a7dd365c9dcdd00d96e7ac26317cfcb175db387e403de05bf", size = 1174418, upload-time = "2025-11-17T14:10:05.945Z" },
+ { url = "https://files.pythonhosted.org/packages/a6/a5/21996231f53ee97191d0746c3a672ba33a4d86a19ffad85a1c0096c91c5f/srsly-2.5.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:539c6d0016e91277b5e9be31ebed03f03c32580d49c960e4a92c9003baecf69e", size = 1183089, upload-time = "2025-11-17T14:10:07.335Z" },
+ { url = "https://files.pythonhosted.org/packages/7b/df/eb17aa8e4a828e8df7aa7dc471295529d9126e6b710f1833ebe0d8568a8e/srsly-2.5.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:9f24b2c4f4c29da04083f09158543eb3f8893ba0ac39818693b3b259ee8044f0", size = 1122594, upload-time = "2025-11-17T14:10:08.899Z" },
+ { url = "https://files.pythonhosted.org/packages/80/74/1654a80e6c8ec3ee32370ea08a78d3651e0ba1c4d6e6be31c9efdb9a2d10/srsly-2.5.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d34675047460a3f6999e43478f40d9b43917ea1e93a75c41d05bf7648f3e872d", size = 1139594, upload-time = "2025-11-17T14:10:10.286Z" },
+ { url = "https://files.pythonhosted.org/packages/73/aa/8393344ca7f0e81965febba07afc5cad68335ed0426408d480b861ab915b/srsly-2.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:81fd133ba3c66c07f0e3a889d2b4c852984d71ea833a665238a9d47d8e051ba5", size = 654750, upload-time = "2025-11-17T14:10:11.637Z" },
]
[[package]]
@@ -1792,6 +1953,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e5/de/da163a1533faaef5b17dd11dfb9ffd9fd5627dbef56e1160da6edbe1b224/thinc-8.3.10-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:9de5dd73ce7135dcf41d68625d35cd9f5cf8e5f55a3932001a188b45057c3379", size = 5262834, upload-time = "2025-11-17T17:20:57.459Z" },
{ url = "https://files.pythonhosted.org/packages/4c/4e/449d29e33f7ddda6ba1b9e06de3ea5155c2dc33c21f438f8faafebde4e13/thinc-8.3.10-cp311-cp311-win_amd64.whl", hash = "sha256:b6d64e390a1996d489872b9d99a584142542aba59ebdc60f941f473732582f6f", size = 1791864, upload-time = "2025-11-17T17:20:59.817Z" },
{ url = "https://files.pythonhosted.org/packages/4a/b3/68038d88d45d83a501c3f19bd654d275b7ac730c807f52bbb46f35f591bc/thinc-8.3.10-cp311-cp311-win_arm64.whl", hash = "sha256:3991b6ad72e611dfbfb58235de5b67bcc9f61426127cc023607f97e8c5f43e0e", size = 1717563, upload-time = "2025-11-17T17:21:01.634Z" },
+ { url = "https://files.pythonhosted.org/packages/d3/34/ba3b386d92edf50784b60ee34318d47c7f49c198268746ef7851c5bbe8cf/thinc-8.3.10-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:51bc6ef735bdbcab75ab2916731b8f61f94c66add6f9db213d900d3c6a244f95", size = 794509, upload-time = "2025-11-17T17:21:03.21Z" },
+ { url = "https://files.pythonhosted.org/packages/07/f3/9f52d18115cd9d8d7b2590d226cb2752d2a5ffec61576b19462b48410184/thinc-8.3.10-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4f48b4d346915f98e9722c0c50ef911cc16c6790a2b7afebc6e1a2c96a6ce6c6", size = 741084, upload-time = "2025-11-17T17:21:04.568Z" },
+ { url = "https://files.pythonhosted.org/packages/ad/9c/129c2b740c4e3d3624b6fb3dec1577ef27cb804bc1647f9bc3e1801ea20c/thinc-8.3.10-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5003f4db2db22cc8d686db8db83509acc3c50f4c55ebdcb2bbfcc1095096f7d2", size = 3846337, upload-time = "2025-11-17T17:21:06.079Z" },
+ { url = "https://files.pythonhosted.org/packages/22/d2/738cf188dea8240c2be081c83ea47270fea585eba446171757d2cdb9b675/thinc-8.3.10-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:b12484c3ed0632331fada2c334680dd6bc35972d0717343432dfc701f04a9b4c", size = 3901216, upload-time = "2025-11-17T17:21:07.842Z" },
+ { url = "https://files.pythonhosted.org/packages/22/92/32f66eb9b1a29b797bf378a0874615d810d79eefca1d6c736c5ca3f8b918/thinc-8.3.10-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8677c446d3f9b97a465472c58683b785b25dfcf26c683e3f4e8f8c7c188e4362", size = 4827286, upload-time = "2025-11-17T17:21:09.62Z" },
+ { url = "https://files.pythonhosted.org/packages/c4/5f/7ceae1e1f2029efd67ed88e23cd6dc13a5ee647cdc2b35113101b2a62c10/thinc-8.3.10-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:759c385ac08dcf950238b60b96a28f9c04618861141766928dff4a51b1679b25", size = 5024421, upload-time = "2025-11-17T17:21:11.199Z" },
+ { url = "https://files.pythonhosted.org/packages/0b/66/30f9d8d41049b78bc614213d492792fbcfeb1b28642adf661c42110a7ebd/thinc-8.3.10-cp312-cp312-win_amd64.whl", hash = "sha256:bf3f188c3fa1fdcefd547d1f90a1245c29025d6d0e3f71d7fdf21dad210b990c", size = 1718631, upload-time = "2025-11-17T17:21:12.965Z" },
+ { url = "https://files.pythonhosted.org/packages/f8/44/32e2a5018a1165a304d25eb9b1c74e5310da19a533a35331e8d824dc6a88/thinc-8.3.10-cp312-cp312-win_arm64.whl", hash = "sha256:234b7e57a6ef4e0260d99f4e8fdc328ed12d0ba9bbd98fdaa567294a17700d1c", size = 1642224, upload-time = "2025-11-17T17:21:14.371Z" },
]
[[package]]
@@ -1808,6 +1977,14 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ba/28/72f8afd73f1d0e7829bfc093f4cb98ce0a40ffc0cc997009ee1ed94ba705/tomli-2.3.0-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:74bf8464ff93e413514fefd2be591c3b0b23231a77f901db1eb30d6f712fc42c", size = 245128, upload-time = "2025-10-08T22:01:05.84Z" },
{ url = "https://files.pythonhosted.org/packages/b6/eb/a7679c8ac85208706d27436e8d421dfa39d4c914dcf5fa8083a9305f58d9/tomli-2.3.0-cp311-cp311-win32.whl", hash = "sha256:00b5f5d95bbfc7d12f91ad8c593a1659b6387b43f054104cda404be6bda62456", size = 96445, upload-time = "2025-10-08T22:01:06.896Z" },
{ url = "https://files.pythonhosted.org/packages/0a/fe/3d3420c4cb1ad9cb462fb52967080575f15898da97e21cb6f1361d505383/tomli-2.3.0-cp311-cp311-win_amd64.whl", hash = "sha256:4dc4ce8483a5d429ab602f111a93a6ab1ed425eae3122032db7e9acf449451be", size = 107165, upload-time = "2025-10-08T22:01:08.107Z" },
+ { url = "https://files.pythonhosted.org/packages/ff/b7/40f36368fcabc518bb11c8f06379a0fd631985046c038aca08c6d6a43c6e/tomli-2.3.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d7d86942e56ded512a594786a5ba0a5e521d02529b3826e7761a05138341a2ac", size = 154891, upload-time = "2025-10-08T22:01:09.082Z" },
+ { url = "https://files.pythonhosted.org/packages/f9/3f/d9dd692199e3b3aab2e4e4dd948abd0f790d9ded8cd10cbaae276a898434/tomli-2.3.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:73ee0b47d4dad1c5e996e3cd33b8a76a50167ae5f96a2607cbe8cc773506ab22", size = 148796, upload-time = "2025-10-08T22:01:10.266Z" },
+ { url = "https://files.pythonhosted.org/packages/60/83/59bff4996c2cf9f9387a0f5a3394629c7efa5ef16142076a23a90f1955fa/tomli-2.3.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:792262b94d5d0a466afb5bc63c7daa9d75520110971ee269152083270998316f", size = 242121, upload-time = "2025-10-08T22:01:11.332Z" },
+ { url = "https://files.pythonhosted.org/packages/45/e5/7c5119ff39de8693d6baab6c0b6dcb556d192c165596e9fc231ea1052041/tomli-2.3.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4f195fe57ecceac95a66a75ac24d9d5fbc98ef0962e09b2eddec5d39375aae52", size = 250070, upload-time = "2025-10-08T22:01:12.498Z" },
+ { url = "https://files.pythonhosted.org/packages/45/12/ad5126d3a278f27e6701abde51d342aa78d06e27ce2bb596a01f7709a5a2/tomli-2.3.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e31d432427dcbf4d86958c184b9bfd1e96b5b71f8eb17e6d02531f434fd335b8", size = 245859, upload-time = "2025-10-08T22:01:13.551Z" },
+ { url = "https://files.pythonhosted.org/packages/fb/a1/4d6865da6a71c603cfe6ad0e6556c73c76548557a8d658f9e3b142df245f/tomli-2.3.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:7b0882799624980785240ab732537fcfc372601015c00f7fc367c55308c186f6", size = 250296, upload-time = "2025-10-08T22:01:14.614Z" },
+ { url = "https://files.pythonhosted.org/packages/a0/b7/a7a7042715d55c9ba6e8b196d65d2cb662578b4d8cd17d882d45322b0d78/tomli-2.3.0-cp312-cp312-win32.whl", hash = "sha256:ff72b71b5d10d22ecb084d345fc26f42b5143c5533db5e2eaba7d2d335358876", size = 97124, upload-time = "2025-10-08T22:01:15.629Z" },
+ { url = "https://files.pythonhosted.org/packages/06/1e/f22f100db15a68b520664eb3328fb0ae4e90530887928558112c8d1f4515/tomli-2.3.0-cp312-cp312-win_amd64.whl", hash = "sha256:1cb4ed918939151a03f33d4242ccd0aa5f11b3547d0cf30f7c74a408a5b99878", size = 107698, upload-time = "2025-10-08T22:01:16.51Z" },
{ url = "https://files.pythonhosted.org/packages/77/b8/0135fadc89e73be292b473cb820b4f5a08197779206b33191e801feeae40/tomli-2.3.0-py3-none-any.whl", hash = "sha256:e95b1af3c5b07d9e643909b5abbec77cd9f1217e6d0bca72b0234736b9fb1f1b", size = 14408, upload-time = "2025-10-08T22:01:46.04Z" },
]
@@ -1884,11 +2061,11 @@ wheels = [
[[package]]
name = "urllib3"
-version = "2.5.0"
+version = "2.6.1"
source = { registry = "https://pypi.org/simple" }
-sdist = { url = "https://files.pythonhosted.org/packages/15/22/9ee70a2574a4f4599c47dd506532914ce044817c7752a79b6a51286319bc/urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760", size = 393185, upload-time = "2025-06-18T14:07:41.644Z" }
+sdist = { url = "https://files.pythonhosted.org/packages/5e/1d/0f3a93cca1ac5e8287842ed4eebbd0f7a991315089b1a0b01c7788aa7b63/urllib3-2.6.1.tar.gz", hash = "sha256:5379eb6e1aba4088bae84f8242960017ec8d8e3decf30480b3a1abdaa9671a3f", size = 432678, upload-time = "2025-12-08T15:25:26.773Z" }
wheels = [
- { url = "https://files.pythonhosted.org/packages/a7/c2/fe1e52489ae3122415c51f387e221dd0773709bad6c6cdaa599e8a2c5185/urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc", size = 129795, upload-time = "2025-06-18T14:07:40.39Z" },
+ { url = "https://files.pythonhosted.org/packages/bc/56/190ceb8cb10511b730b564fb1e0293fa468363dbad26145c34928a60cb0c/urllib3-2.6.1-py3-none-any.whl", hash = "sha256:e67d06fe947c36a7ca39f4994b08d73922d40e6cca949907be05efa6fd75110b", size = 131138, upload-time = "2025-12-08T15:25:25.51Z" },
]
[[package]]
@@ -1944,6 +2121,9 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/e0/24/d9be5cd6642a6aa68352ded4b4b10fb0d7889cb7f45814fb92cecd35f101/watchdog-6.0.0-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:6eb11feb5a0d452ee41f824e271ca311a09e250441c262ca2fd7ebcf2461a06c", size = 96393, upload-time = "2024-11-01T14:06:31.756Z" },
{ url = "https://files.pythonhosted.org/packages/63/7a/6013b0d8dbc56adca7fdd4f0beed381c59f6752341b12fa0886fa7afc78b/watchdog-6.0.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:ef810fbf7b781a5a593894e4f439773830bdecb885e6880d957d5b9382a960d2", size = 88392, upload-time = "2024-11-01T14:06:32.99Z" },
{ url = "https://files.pythonhosted.org/packages/d1/40/b75381494851556de56281e053700e46bff5b37bf4c7267e858640af5a7f/watchdog-6.0.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:afd0fe1b2270917c5e23c2a65ce50c2a4abb63daafb0d419fde368e272a76b7c", size = 89019, upload-time = "2024-11-01T14:06:34.963Z" },
+ { url = "https://files.pythonhosted.org/packages/39/ea/3930d07dafc9e286ed356a679aa02d777c06e9bfd1164fa7c19c288a5483/watchdog-6.0.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:bdd4e6f14b8b18c334febb9c4425a878a2ac20efd1e0b231978e7b150f92a948", size = 96471, upload-time = "2024-11-01T14:06:37.745Z" },
+ { url = "https://files.pythonhosted.org/packages/12/87/48361531f70b1f87928b045df868a9fd4e253d9ae087fa4cf3f7113be363/watchdog-6.0.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c7c15dda13c4eb00d6fb6fc508b3c0ed88b9d5d374056b239c4ad1611125c860", size = 88449, upload-time = "2024-11-01T14:06:39.748Z" },
+ { url = "https://files.pythonhosted.org/packages/5b/7e/8f322f5e600812e6f9a31b75d242631068ca8f4ef0582dd3ae6e72daecc8/watchdog-6.0.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:6f10cb2d5902447c7d0da897e2c6768bca89174d0c6e1e30abec5421af97a5b0", size = 89054, upload-time = "2024-11-01T14:06:41.009Z" },
{ url = "https://files.pythonhosted.org/packages/30/ad/d17b5d42e28a8b91f8ed01cb949da092827afb9995d4559fd448d0472763/watchdog-6.0.0-pp310-pypy310_pp73-macosx_10_15_x86_64.whl", hash = "sha256:c7ac31a19f4545dd92fc25d200694098f42c9a8e391bc00bdd362c5736dbf881", size = 87902, upload-time = "2024-11-01T14:06:53.119Z" },
{ url = "https://files.pythonhosted.org/packages/5c/ca/c3649991d140ff6ab67bfc85ab42b165ead119c9e12211e08089d763ece5/watchdog-6.0.0-pp310-pypy310_pp73-macosx_11_0_arm64.whl", hash = "sha256:9513f27a1a582d9808cf21a07dae516f0fab1cf2d7683a742c498b93eedabb11", size = 88380, upload-time = "2024-11-01T14:06:55.19Z" },
{ url = "https://files.pythonhosted.org/packages/a9/c7/ca4bf3e518cb57a686b2feb4f55a1892fd9a3dd13f470fca14e00f80ea36/watchdog-6.0.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:7607498efa04a3542ae3e05e64da8202e58159aa1fa4acddf7678d34a35d4f13", size = 79079, upload-time = "2024-11-01T14:06:59.472Z" },
@@ -2017,6 +2197,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/45/00/9499a3d14e636d1f7089339f96c4409bbc7544d0889f12264efa25502ae8/wrapt-2.0.1-cp311-cp311-win32.whl", hash = "sha256:fc007fdf480c77301ab1afdbb6ab22a5deee8885f3b1ed7afcb7e5e84a0e27be", size = 58028, upload-time = "2025-11-07T00:43:47.369Z" },
{ url = "https://files.pythonhosted.org/packages/70/5d/8f3d7eea52f22638748f74b102e38fdf88cb57d08ddeb7827c476a20b01b/wrapt-2.0.1-cp311-cp311-win_amd64.whl", hash = "sha256:47434236c396d04875180171ee1f3815ca1eada05e24a1ee99546320d54d1d1b", size = 60385, upload-time = "2025-11-07T00:43:44.34Z" },
{ url = "https://files.pythonhosted.org/packages/14/e2/32195e57a8209003587bbbad44d5922f13e0ced2a493bb46ca882c5b123d/wrapt-2.0.1-cp311-cp311-win_arm64.whl", hash = "sha256:837e31620e06b16030b1d126ed78e9383815cbac914693f54926d816d35d8edf", size = 58893, upload-time = "2025-11-07T00:43:46.161Z" },
+ { url = "https://files.pythonhosted.org/packages/cb/73/8cb252858dc8254baa0ce58ce382858e3a1cf616acebc497cb13374c95c6/wrapt-2.0.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1fdbb34da15450f2b1d735a0e969c24bdb8d8924892380126e2a293d9902078c", size = 78129, upload-time = "2025-11-07T00:43:48.852Z" },
+ { url = "https://files.pythonhosted.org/packages/19/42/44a0db2108526ee6e17a5ab72478061158f34b08b793df251d9fbb9a7eb4/wrapt-2.0.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3d32794fe940b7000f0519904e247f902f0149edbe6316c710a8562fb6738841", size = 61205, upload-time = "2025-11-07T00:43:50.402Z" },
+ { url = "https://files.pythonhosted.org/packages/4d/8a/5b4b1e44b791c22046e90d9b175f9a7581a8cc7a0debbb930f81e6ae8e25/wrapt-2.0.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:386fb54d9cd903ee0012c09291336469eb7b244f7183d40dc3e86a16a4bace62", size = 61692, upload-time = "2025-11-07T00:43:51.678Z" },
+ { url = "https://files.pythonhosted.org/packages/11/53/3e794346c39f462bcf1f58ac0487ff9bdad02f9b6d5ee2dc84c72e0243b2/wrapt-2.0.1-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:7b219cb2182f230676308cdcacd428fa837987b89e4b7c5c9025088b8a6c9faf", size = 121492, upload-time = "2025-11-07T00:43:55.017Z" },
+ { url = "https://files.pythonhosted.org/packages/c6/7e/10b7b0e8841e684c8ca76b462a9091c45d62e8f2de9c4b1390b690eadf16/wrapt-2.0.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:641e94e789b5f6b4822bb8d8ebbdfc10f4e4eae7756d648b717d980f657a9eb9", size = 123064, upload-time = "2025-11-07T00:43:56.323Z" },
+ { url = "https://files.pythonhosted.org/packages/0e/d1/3c1e4321fc2f5ee7fd866b2d822aa89b84495f28676fd976c47327c5b6aa/wrapt-2.0.1-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe21b118b9f58859b5ebaa4b130dee18669df4bd111daad082b7beb8799ad16b", size = 117403, upload-time = "2025-11-07T00:43:53.258Z" },
+ { url = "https://files.pythonhosted.org/packages/a4/b0/d2f0a413cf201c8c2466de08414a15420a25aa83f53e647b7255cc2fab5d/wrapt-2.0.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:17fb85fa4abc26a5184d93b3efd2dcc14deb4b09edcdb3535a536ad34f0b4dba", size = 121500, upload-time = "2025-11-07T00:43:57.468Z" },
+ { url = "https://files.pythonhosted.org/packages/bd/45/bddb11d28ca39970a41ed48a26d210505120f925918592283369219f83cc/wrapt-2.0.1-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:b89ef9223d665ab255ae42cc282d27d69704d94be0deffc8b9d919179a609684", size = 116299, upload-time = "2025-11-07T00:43:58.877Z" },
+ { url = "https://files.pythonhosted.org/packages/81/af/34ba6dd570ef7a534e7eec0c25e2615c355602c52aba59413411c025a0cb/wrapt-2.0.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:a453257f19c31b31ba593c30d997d6e5be39e3b5ad9148c2af5a7314061c63eb", size = 120622, upload-time = "2025-11-07T00:43:59.962Z" },
+ { url = "https://files.pythonhosted.org/packages/e2/3e/693a13b4146646fb03254636f8bafd20c621955d27d65b15de07ab886187/wrapt-2.0.1-cp312-cp312-win32.whl", hash = "sha256:3e271346f01e9c8b1130a6a3b0e11908049fe5be2d365a5f402778049147e7e9", size = 58246, upload-time = "2025-11-07T00:44:03.169Z" },
+ { url = "https://files.pythonhosted.org/packages/a7/36/715ec5076f925a6be95f37917b66ebbeaa1372d1862c2ccd7a751574b068/wrapt-2.0.1-cp312-cp312-win_amd64.whl", hash = "sha256:2da620b31a90cdefa9cd0c2b661882329e2e19d1d7b9b920189956b76c564d75", size = 60492, upload-time = "2025-11-07T00:44:01.027Z" },
+ { url = "https://files.pythonhosted.org/packages/ef/3e/62451cd7d80f65cc125f2b426b25fbb6c514bf6f7011a0c3904fc8c8df90/wrapt-2.0.1-cp312-cp312-win_arm64.whl", hash = "sha256:aea9c7224c302bc8bfc892b908537f56c430802560e827b75ecbde81b604598b", size = 58987, upload-time = "2025-11-07T00:44:02.095Z" },
{ url = "https://files.pythonhosted.org/packages/15/d1/b51471c11592ff9c012bd3e2f7334a6ff2f42a7aed2caffcf0bdddc9cb89/wrapt-2.0.1-py3-none-any.whl", hash = "sha256:4d2ce1bf1a48c5277d7969259232b57645aae5686dba1eaeade39442277afbca", size = 44046, upload-time = "2025-11-07T00:45:32.116Z" },
]