From e255ae12631db58c3115170cfdf2fafc5386954f Mon Sep 17 00:00:00 2001 From: bsiggel Date: Mon, 30 Mar 2026 08:25:49 +0000 Subject: [PATCH] feat(graphiti): Implement Graphiti client and related steps for episode ingestion and querying - Added `graphiti_client.py` to manage the Graphiti client as a singleton. - Created `ingest_episode_event_step.py` to handle episode ingestion from HTTP webhook events. - Implemented `ingest_episode_step.py` for validating and enqueuing episode payloads via a POST request. - Developed `query_graph_step.py` for performing semantic searches in the Graphiti Knowledge-Graph. - Introduced an `__init__.py` file for the graphiti steps module. --- iii-config.yaml | 2 +- pyproject.toml | 1 + services/graphiti_client.py | 100 ++++++++++++ src/steps/graphiti/__init__.py | 0 .../graphiti/ingest_episode_event_step.py | 98 ++++++++++++ src/steps/graphiti/ingest_episode_step.py | 87 ++++++++++ src/steps/graphiti/query_graph_step.py | 149 ++++++++++++++++++ uv.lock | 131 +++++++++++++++ 8 files changed, 567 insertions(+), 1 deletion(-) create mode 100644 services/graphiti_client.py create mode 100644 src/steps/graphiti/__init__.py create mode 100644 src/steps/graphiti/ingest_episode_event_step.py create mode 100644 src/steps/graphiti/ingest_episode_step.py create mode 100644 src/steps/graphiti/query_graph_step.py diff --git a/iii-config.yaml b/iii-config.yaml index 88e05b9..7e8f7f7 100644 --- a/iii-config.yaml +++ b/iii-config.yaml @@ -21,7 +21,7 @@ modules: config: port: 3111 host: 0.0.0.0 - default_timeout: 30000 + default_timeout: 180000 concurrency_request_limit: 1024 cors: allowed_origins: diff --git a/pyproject.toml b/pyproject.toml index 6b47d90..cc59c7c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -22,5 +22,6 @@ dependencies = [ "langchain>=0.3.0", # LangChain framework "langchain-xai>=0.2.0", # xAI integration for LangChain "langchain-core>=0.3.0", # LangChain core + "graphiti-core>=0.28.0", # Graphiti Knowledge-Graph ] diff --git a/services/graphiti_client.py b/services/graphiti_client.py new file mode 100644 index 0000000..4c93b1f --- /dev/null +++ b/services/graphiti_client.py @@ -0,0 +1,100 @@ +"""Graphiti Knowledge-Graph – zentraler Client-Singleton. + +Wird von ingest_episode_event_step und query_graph_step genutzt. +Lazy-initialisiert beim ersten Aufruf. +""" +import asyncio +import os +from typing import Any, Optional + +from graphiti_core import Graphiti +from graphiti_core.llm_client import LLMConfig +from graphiti_core.llm_client.openai_generic_client import OpenAIGenericClient +from graphiti_core.embedder import OpenAIEmbedder, OpenAIEmbedderConfig + + +class GraphitiError(Exception): + """Fehler beim Zugriff auf den Graphiti-Client.""" + + +_graphiti_client: Graphiti | None = None +_graphiti_init_lock = asyncio.Lock() + + +async def get_graphiti(ctx: Optional[Any] = None) -> Graphiti: + """Gibt den gecachten Graphiti-Client zurück (Singleton). + + Args: + ctx: Optionaler Motia-Context für Logging während der Initialisierung. + + Benötigte Umgebungsvariablen: + NEO4J_URI – bolt://host:7687 + NEO4J_USER – neo4j + NEO4J_PASSWORD – Pflicht + XAI_API_KEY – xAI-Key für LLM (grok) + XAI_BASE_URL – optional, Standard: https://api.x.ai/v1 + XAI_MODEL – optional, Standard: grok-4-1-fast-reasoning + OPENAI_API_KEY – OpenAI-Key für Embeddings + GRAPHITI_EMBED_BASE_URL – optional, Standard: https://api.openai.com/v1 + GRAPHITI_EMBED_MODEL – optional, Standard: text-embedding-3-small + """ + global _graphiti_client + if _graphiti_client is None: + async with _graphiti_init_lock: + if _graphiti_client is None: + _log(ctx, "Initialisiere Graphiti-Client...") + try: + _graphiti_client = await _build_graphiti() + _log(ctx, "Graphiti-Client bereit.") + except KeyError as e: + raise GraphitiError(f"Konfigurationsfehler – Umgebungsvariable fehlt: {e}") from e + return _graphiti_client + + +def _log(ctx: Optional[Any], message: str, level: str = "info") -> None: + """Loggt via ctx.logger falls vorhanden, sonst print.""" + if ctx is not None and hasattr(ctx, "logger"): + getattr(ctx.logger, level)(f"[GraphitiClient] {message}") + else: + print(f"[GraphitiClient] {message}") + + +async def _build_graphiti() -> Graphiti: + neo4j_uri = os.environ["NEO4J_URI"] + neo4j_user = os.environ.get("NEO4J_USER", "neo4j") + neo4j_password = os.environ["NEO4J_PASSWORD"] + + xai_base_url = os.environ.get("XAI_BASE_URL", "https://api.x.ai/v1") + xai_api_key = os.environ["XAI_API_KEY"] + xai_model = os.environ.get("XAI_MODEL", "grok-4-1-fast-reasoning") + + embed_api_key = os.environ.get("OPENAI_API_KEY") or os.environ.get("GRAPHITI_EMBED_API_KEY", xai_api_key) + embed_base_url = os.environ.get("GRAPHITI_EMBED_BASE_URL", "https://api.openai.com/v1") + embed_model = os.environ.get("GRAPHITI_EMBED_MODEL", "text-embedding-3-small") + + llm_client = OpenAIGenericClient( + config=LLMConfig( + api_key=xai_api_key, + base_url=xai_base_url, + model=xai_model, + ) + ) + + embedder = OpenAIEmbedder( + config=OpenAIEmbedderConfig( + api_key=embed_api_key, + base_url=embed_base_url, + embedding_model=embed_model, + embedding_dim=1536, + ) + ) + + client = Graphiti( + uri=neo4j_uri, + user=neo4j_user, + password=neo4j_password, + llm_client=llm_client, + embedder=embedder, + ) + await client.build_indices_and_constraints() + return client diff --git a/src/steps/graphiti/__init__.py b/src/steps/graphiti/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/src/steps/graphiti/ingest_episode_event_step.py b/src/steps/graphiti/ingest_episode_event_step.py new file mode 100644 index 0000000..9f3e955 --- /dev/null +++ b/src/steps/graphiti/ingest_episode_event_step.py @@ -0,0 +1,98 @@ +"""Graphiti - Episode verarbeiten (Queue: graphiti.ingest_episode) + +Empfängt das Event vom HTTP-Webhook und schreibt die Episode asynchron +in den Graphiti Knowledge-Graph (Neo4j + xAI LLM + OpenAI Embeddings). +""" +import json +from datetime import datetime, timezone +from typing import Any + +from motia import FlowContext, queue + +from services.graphiti_client import get_graphiti, GraphitiError +from graphiti_core.nodes import EpisodeType + + +config = { + "name": "Graphiti Ingest Episode Worker", + "description": "Verarbeitet eine Episode und schreibt sie in den Knowledge-Graph.", + "flows": ["graphiti-knowledge-graph"], + "triggers": [ + queue("graphiti.ingest_episode") + ], + "enqueues": [], +} + + +async def handler(event_data: dict[str, Any], ctx: FlowContext[Any]) -> None: + """ + Schreibt die Episode in Graphiti. + + Erwartet im event_data: + rag_akten_id (str) – group_id im Graph + rag_dokument_id (str) – Episode-Name + chunk_ids (list[str]) + content (str) – Episoden-Text + source (str) – z. B. "Zeugenvernehmung" + valid_at (str | None) – ISO-String, None → jetzt + """ + ctx.logger.info("=" * 80) + ctx.logger.info("GRAPHITI INGEST WORKER: Starte Episode-Verarbeitung") + + rag_akten_id: str = event_data.get("rag_akten_id", "") + rag_dokument_id: str = event_data.get("rag_dokument_id", "") + chunk_ids: list[str] = event_data.get("chunk_ids") or [] + content: str = event_data.get("content", "") + source: str = event_data.get("source", "") + raw_valid_at: str | None = event_data.get("valid_at") + + ctx.logger.info(f"Akte: {rag_akten_id}") + ctx.logger.info(f"Dokument: {rag_dokument_id}") + ctx.logger.info(f"Source: {source}") + + # valid_at auflösen + if raw_valid_at: + try: + reference_time = datetime.fromisoformat(raw_valid_at) + if reference_time.tzinfo is None: + reference_time = reference_time.replace(tzinfo=timezone.utc) + except (ValueError, TypeError): + ctx.logger.info(f"valid_at '{raw_valid_at}' ungültig – verwende jetzt") + reference_time = datetime.now(timezone.utc) + else: + ctx.logger.info("Kein valid_at – verwende jetzt als reference_time") + reference_time = datetime.now(timezone.utc) + + ctx.logger.info(f"reference_time: {reference_time.isoformat()}") + + source_description = ( + f"{source} | " + f"rag_dokument_id:{rag_dokument_id} | " + f"chunk_ids:{json.dumps(chunk_ids, ensure_ascii=False)}" + ) + + try: + graphiti = await get_graphiti(ctx) + + result = await graphiti.add_episode( + name=rag_dokument_id, + episode_body=content, + source_description=source_description, + reference_time=reference_time, + source=EpisodeType.text, + group_id=rag_akten_id, + ) + + episode_id = result.episode.uuid + ctx.logger.info(f"Episode erfolgreich gespeichert: {episode_id}") + ctx.logger.info("=" * 80) + + except GraphitiError as e: + ctx.logger.error(f"GraphitiError: {e}") + ctx.logger.error("=" * 80) + raise # Motia retries + + except Exception as e: + ctx.logger.error(f"Unerwarteter Fehler: {type(e).__name__}: {e}") + ctx.logger.error("=" * 80) + raise diff --git a/src/steps/graphiti/ingest_episode_step.py b/src/steps/graphiti/ingest_episode_step.py new file mode 100644 index 0000000..794eba2 --- /dev/null +++ b/src/steps/graphiti/ingest_episode_step.py @@ -0,0 +1,87 @@ +"""Graphiti - Episode ingestieren (POST /ingest_episode) + +Dünner HTTP-Webhook: validiert den Payload und enqueued ihn sofort. +Die eigentliche Graphiti-Arbeit passiert in ingest_episode_event_step.py. +""" +from datetime import datetime, timezone +from typing import Any + +from motia import FlowContext, http, ApiRequest, ApiResponse + + +config = { + "name": "Graphiti Ingest Episode", + "description": "Nimmt Episode-Payload entgegen, validiert und enqueued ihn.", + "flows": ["graphiti-knowledge-graph"], + "triggers": [ + http("POST", "/ingest_episode") + ], + "enqueues": ["graphiti.ingest_episode"], +} + + +def _parse_valid_at(raw: Any) -> str | None: + """ISO-String validieren; gibt None zurück wenn ungültig/fehlend.""" + if not raw or not isinstance(raw, str): + return None + try: + dt = datetime.fromisoformat(raw) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt.isoformat() + except (ValueError, TypeError): + return None + + +async def handler(request: ApiRequest, ctx: FlowContext[Any]) -> ApiResponse: + """ + Validiert den Ingest-Payload und enqueued ein graphiti.ingest_episode-Event. + + Payload (JSON): + rag_akten_id (str, Pflicht) + rag_dokument_id (str, Pflicht) + chunk_ids (list[str], optional) + content (str, Pflicht) + source (str, Pflicht) + valid_at (ISO-String | null, optional) + + Returns: + 202 { status: "accepted", group_id, rag_dokument_id } + 400 bei fehlendem Pflichtfeld + """ + ctx.logger.info("=" * 80) + ctx.logger.info("GRAPHITI INGEST WEBHOOK: Payload empfangen") + + body: dict = request.body or {} + ctx.logger.info(f"Payload-Keys: {list(body.keys())}") + + # Pflichtfelder + missing = [f for f in ("rag_akten_id", "rag_dokument_id", "content", "source") if not body.get(f)] + if missing: + ctx.logger.error(f"Fehlende Pflichtfelder: {missing}") + return ApiResponse(status=400, body={"error": f"Fehlende Pflichtfelder: {missing}"}) + + valid_at = _parse_valid_at(body.get("valid_at")) + + event_data = { + "rag_akten_id": body["rag_akten_id"], + "rag_dokument_id": body["rag_dokument_id"], + "chunk_ids": body.get("chunk_ids") or [], + "content": body["content"], + "source": body["source"], + "valid_at": valid_at, # None → Queue-Step löst CRM-Fallback aus + } + + await ctx.enqueue({"topic": "graphiti.ingest_episode", "data": event_data}) + + ctx.logger.info(f"Event 'graphiti.ingest_episode' enqueued für {body['rag_akten_id']}/{body['rag_dokument_id']}") + ctx.logger.info("=" * 80) + + return ApiResponse( + status=202, + body={ + "status": "accepted", + "group_id": body["rag_akten_id"], + "rag_dokument_id": body["rag_dokument_id"], + }, + ) diff --git a/src/steps/graphiti/query_graph_step.py b/src/steps/graphiti/query_graph_step.py new file mode 100644 index 0000000..fb3b410 --- /dev/null +++ b/src/steps/graphiti/query_graph_step.py @@ -0,0 +1,149 @@ +"""Graphiti - Graph abfragen (POST /query_graph) + +Führt eine semantische Suche im Graphiti Knowledge-Graph durch, +gefiltert nach group_id (= rag_akten_id). Optionaler time_point +schränkt das Ergebnis auf zum Zeitpunkt gültige Kanten ein. +""" +from datetime import datetime, timezone +from typing import Any + +from motia import FlowContext, http, ApiRequest, ApiResponse + +from graphiti_core.search.search_filters import DateFilter, ComparisonOperator, SearchFilters + +from services.graphiti_client import get_graphiti, GraphitiError + + +config = { + "name": "Graphiti Query Graph", + "description": "Führt eine semantische Suche im Graphiti Knowledge-Graph durch.", + "flows": ["graphiti-knowledge-graph"], + "triggers": [ + http("POST", "/query_graph") + ], + "enqueues": [], +} + + +def _parse_dt(raw: Any) -> datetime | None: + """ISO-String → timezone-aware datetime, oder None bei Fehler.""" + if not raw or not isinstance(raw, str): + return None + try: + dt = datetime.fromisoformat(raw) + if dt.tzinfo is None: + dt = dt.replace(tzinfo=timezone.utc) + return dt + except (ValueError, TypeError): + return None + + +def _serialize_edge(edge: Any) -> dict: + """Serialisiert eine EntityEdge in ein dict für die JSON-Antwort.""" + return { + "uuid": edge.uuid, + "name": edge.name, + "fact": edge.fact, + "valid_at": edge.valid_at.isoformat() if edge.valid_at else None, + "invalid_at": edge.invalid_at.isoformat() if edge.invalid_at else None, + "created_at": edge.created_at.isoformat() if edge.created_at else None, + "source_node_uuid": edge.source_node_uuid, + "target_node_uuid": edge.target_node_uuid, + } + + +# --------------------------------------------------------------------------- +# HTTP-Handler +# --------------------------------------------------------------------------- +async def handler(request: ApiRequest, ctx: FlowContext[Any]) -> ApiResponse: + """ + Sucht im Graphiti Knowledge-Graph nach relevanten Fakten. + + Payload (JSON): + rag_akten_id (str, Pflicht) – filtert auf group_id + query (str, Pflicht) – Suchanfrage + time_point (str, optional) – ISO-Datum; filtert auf zum + Zeitpunkt gültige Kanten + + Returns: + 200 { status, group_id, query, time_point, num_results, results[] } + 400 bei fehlendem Pflichtfeld + 500 bei internem Fehler + """ + ctx.logger.info("=" * 80) + ctx.logger.info("GRAPHITI QUERY: Starte Graph-Suche") + + body: dict = request.body or {} + ctx.logger.info(f"Payload-Keys: {list(body.keys())}") + + # --- Pflichtfelder prüfen --- + missing = [f for f in ("rag_akten_id", "query") if not body.get(f)] + if missing: + ctx.logger.error(f"Fehlende Pflichtfelder: {missing}") + return ApiResponse(status=400, body={"error": f"Fehlende Pflichtfelder: {missing}"}) + + rag_akten_id: str = body["rag_akten_id"] + query: str = body["query"] + raw_time_point: Any = body.get("time_point") + + time_point = _parse_dt(raw_time_point) + if raw_time_point and time_point is None: + ctx.logger.warning( + f"time_point konnte nicht geparst werden ({raw_time_point!r}), wird ignoriert" + ) + + ctx.logger.info(f"group_id (rag_akten_id): {rag_akten_id}") + ctx.logger.info(f"query: {query!r}") + ctx.logger.info(f"time_point: {time_point.isoformat() if time_point else 'keiner'}") + + try: + graphiti = await get_graphiti(ctx) + + # Zeitfilter aufbauen: Kanten die zum time_point noch gültig waren + # (valid_at <= time_point AND (invalid_at IS NULL OR invalid_at > time_point)) + search_filter: SearchFilters | None = None + if time_point is not None: + search_filter = SearchFilters( + valid_at=[[DateFilter( + date=time_point, + comparison_operator=ComparisonOperator.less_than_equal, + )]], + invalid_at=[[DateFilter( + comparison_operator=ComparisonOperator.is_null, + )], [DateFilter( + date=time_point, + comparison_operator=ComparisonOperator.greater_than, + )]], + ) + + edges = await graphiti.search( + query=query, + group_ids=[rag_akten_id], + num_results=20, + search_filter=search_filter, + ) + + ctx.logger.info(f"Gefundene Kanten: {len(edges)}") + ctx.logger.info("=" * 80) + + return ApiResponse( + status=200, + body={ + "status": "success", + "group_id": rag_akten_id, + "query": query, + "time_point": time_point.isoformat() if time_point else None, + "num_results": len(edges), + "results": [_serialize_edge(e) for e in edges], + }, + ) + + except GraphitiError as e: + ctx.logger.error(f"GraphitiError: {e}") + ctx.logger.error("=" * 80) + return ApiResponse(status=500, body={"error": str(e)}) + + except Exception as e: + ctx.logger.error(f"Fehler bei der Graph-Suche: {type(e).__name__}: {e}") + ctx.logger.error("=" * 80) + return ApiResponse(status=500, body={"error": "Interner Fehler", "details": str(e)}) diff --git a/uv.lock b/uv.lock index c310daf..e8e2d81 100644 --- a/uv.lock +++ b/uv.lock @@ -556,6 +556,24 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/ab/09169d5a4612a5f92490806649ac8d41e3ec9129c636754575b3553f4ea4/googleapis_common_protos-1.72.0-py3-none-any.whl", hash = "sha256:4299c5a82d5ae1a9702ada957347726b167f9f8d1fc352477702a1e851ff4038", size = 297515, upload-time = "2025-11-06T18:29:13.14Z" }, ] +[[package]] +name = "graphiti-core" +version = "0.28.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "neo4j" }, + { name = "numpy" }, + { name = "openai" }, + { name = "posthog" }, + { name = "pydantic" }, + { name = "python-dotenv" }, + { name = "tenacity" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/62/6d/81b78aec2030bff0030bbabb8b227a6fa3c5fb49fb11e8501e7d0f39f3fe/graphiti_core-0.28.2.tar.gz", hash = "sha256:9b2a72f117827e015a21b610eb2c3acbe05310b79736abef7372e81247578e9d", size = 6846195, upload-time = "2026-03-11T16:20:02.736Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/cd/e6203f1fee0a8e2a797f2d5f9a867513e0c63af4e19fdd1c5a7e14a47670/graphiti_core-0.28.2-py3-none-any.whl", hash = "sha256:4e1c19b7bc70a73a612a473144ed4b3fe615ac6d4c5d6b10f48e206a858bcb53", size = 314919, upload-time = "2026-03-11T16:20:01.037Z" }, +] + [[package]] name = "grpcio" version = "1.78.0" @@ -937,6 +955,7 @@ dependencies = [ { name = "backoff" }, { name = "google-api-python-client" }, { name = "google-auth" }, + { name = "graphiti-core" }, { name = "iii-sdk" }, { name = "langchain" }, { name = "langchain-core" }, @@ -957,6 +976,7 @@ requires-dist = [ { name = "backoff", specifier = ">=2.2.1" }, { name = "google-api-python-client", specifier = ">=2.100.0" }, { name = "google-auth", specifier = ">=2.23.0" }, + { name = "graphiti-core", specifier = ">=0.28.0" }, { name = "iii-sdk", specifier = "==0.2.0" }, { name = "langchain", specifier = ">=0.3.0" }, { name = "langchain-core", specifier = ">=0.3.0" }, @@ -1069,6 +1089,79 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/81/08/7036c080d7117f28a4af526d794aab6a84463126db031b007717c1a6676e/multidict-6.7.1-py3-none-any.whl", hash = "sha256:55d97cc6dae627efa6a6e548885712d4864b81110ac76fa4e534c03819fa4a56", size = 12319, upload-time = "2026-01-26T02:46:44.004Z" }, ] +[[package]] +name = "neo4j" +version = "6.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "pytz" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1b/01/d6ce65e4647f6cb2b9cca3b813978f7329b54b4e36660aaec1ddf0ccce7a/neo4j-6.1.0.tar.gz", hash = "sha256:b5dde8c0d8481e7b6ae3733569d990dd3e5befdc5d452f531ad1884ed3500b84", size = 239629, upload-time = "2026-01-12T11:27:34.777Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/70/5c/ee71e2dd955045425ef44283f40ba1da67673cf06404916ca2950ac0cd39/neo4j-6.1.0-py3-none-any.whl", hash = "sha256:3bd93941f3a3559af197031157220af9fd71f4f93a311db687bd69ffa417b67d", size = 325326, upload-time = "2026-01-12T11:27:33.196Z" }, +] + +[[package]] +name = "numpy" +version = "2.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d7/9f/b8cef5bffa569759033adda9481211426f12f53299629b410340795c2514/numpy-2.4.4.tar.gz", hash = "sha256:2d390634c5182175533585cc89f3608a4682ccb173cc9bb940b2881c8d6f8fa0", size = 20731587, upload-time = "2026-03-29T13:22:01.298Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/28/05/32396bec30fb2263770ee910142f49c1476d08e8ad41abf8403806b520ce/numpy-2.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15716cfef24d3a9762e3acdf87e27f58dc823d1348f765bbea6bef8c639bfa1b", size = 16689272, upload-time = "2026-03-29T13:18:49.223Z" }, + { url = "https://files.pythonhosted.org/packages/c5/f3/a983d28637bfcd763a9c7aafdb6d5c0ebf3d487d1e1459ffdb57e2f01117/numpy-2.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:23cbfd4c17357c81021f21540da84ee282b9c8fba38a03b7b9d09ba6b951421e", size = 14699573, upload-time = "2026-03-29T13:18:52.629Z" }, + { url = "https://files.pythonhosted.org/packages/9b/fd/e5ecca1e78c05106d98028114f5c00d3eddb41207686b2b7de3e477b0e22/numpy-2.4.4-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:8b3b60bb7cba2c8c81837661c488637eee696f59a877788a396d33150c35d842", size = 5204782, upload-time = "2026-03-29T13:18:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/de/2f/702a4594413c1a8632092beae8aba00f1d67947389369b3777aed783fdca/numpy-2.4.4-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:e4a010c27ff6f210ff4c6ef34394cd61470d01014439b192ec22552ee867f2a8", size = 6552038, upload-time = "2026-03-29T13:18:57.769Z" }, + { url = "https://files.pythonhosted.org/packages/7f/37/eed308a8f56cba4d1fdf467a4fc67ef4ff4bf1c888f5fc980481890104b1/numpy-2.4.4-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f9e75681b59ddaa5e659898085ae0eaea229d054f2ac0c7e563a62205a700121", size = 15670666, upload-time = "2026-03-29T13:19:00.341Z" }, + { url = "https://files.pythonhosted.org/packages/0a/0d/0e3ecece05b7a7e87ab9fb587855548da437a061326fff64a223b6dcb78a/numpy-2.4.4-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:81f4a14bee47aec54f883e0cad2d73986640c1590eb9bfaaba7ad17394481e6e", size = 16645480, upload-time = "2026-03-29T13:19:03.63Z" }, + { url = "https://files.pythonhosted.org/packages/34/49/f2312c154b82a286758ee2f1743336d50651f8b5195db18cdb63675ff649/numpy-2.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:62d6b0f03b694173f9fcb1fb317f7222fd0b0b103e784c6549f5e53a27718c44", size = 17020036, upload-time = "2026-03-29T13:19:07.428Z" }, + { url = "https://files.pythonhosted.org/packages/7b/e9/736d17bd77f1b0ec4f9901aaec129c00d59f5d84d5e79bba540ef12c2330/numpy-2.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fbc356aae7adf9e6336d336b9c8111d390a05df88f1805573ebb0807bd06fd1d", size = 18368643, upload-time = "2026-03-29T13:19:10.775Z" }, + { url = "https://files.pythonhosted.org/packages/63/f6/d417977c5f519b17c8a5c3bc9e8304b0908b0e21136fe43bf628a1343914/numpy-2.4.4-cp312-cp312-win32.whl", hash = "sha256:0d35aea54ad1d420c812bfa0385c71cd7cc5bcf7c65fed95fc2cd02fe8c79827", size = 5961117, upload-time = "2026-03-29T13:19:13.464Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5b/e1deebf88ff431b01b7406ca3583ab2bbb90972bbe1c568732e49c844f7e/numpy-2.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:b5f0362dc928a6ecd9db58868fca5e48485205e3855957bdedea308f8672ea4a", size = 12320584, upload-time = "2026-03-29T13:19:16.155Z" }, + { url = "https://files.pythonhosted.org/packages/58/89/e4e856ac82a68c3ed64486a544977d0e7bdd18b8da75b78a577ca31c4395/numpy-2.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:846300f379b5b12cc769334464656bc882e0735d27d9726568bc932fdc49d5ec", size = 10221450, upload-time = "2026-03-29T13:19:18.994Z" }, + { url = "https://files.pythonhosted.org/packages/14/1d/d0a583ce4fefcc3308806a749a536c201ed6b5ad6e1322e227ee4848979d/numpy-2.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:08f2e31ed5e6f04b118e49821397f12767934cfdd12a1ce86a058f91e004ee50", size = 16684933, upload-time = "2026-03-29T13:19:22.47Z" }, + { url = "https://files.pythonhosted.org/packages/c1/62/2b7a48fbb745d344742c0277f01286dead15f3f68e4f359fbfcf7b48f70f/numpy-2.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:e823b8b6edc81e747526f70f71a9c0a07ac4e7ad13020aa736bb7c9d67196115", size = 14694532, upload-time = "2026-03-29T13:19:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/e5/87/499737bfba066b4a3bebff24a8f1c5b2dee410b209bc6668c9be692580f0/numpy-2.4.4-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:4a19d9dba1a76618dd86b164d608566f393f8ec6ac7c44f0cc879011c45e65af", size = 5199661, upload-time = "2026-03-29T13:19:28.31Z" }, + { url = "https://files.pythonhosted.org/packages/cd/da/464d551604320d1491bc345efed99b4b7034143a85787aab78d5691d5a0e/numpy-2.4.4-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:d2a8490669bfe99a233298348acc2d824d496dee0e66e31b66a6022c2ad74a5c", size = 6547539, upload-time = "2026-03-29T13:19:30.97Z" }, + { url = "https://files.pythonhosted.org/packages/7d/90/8d23e3b0dafd024bf31bdec225b3bb5c2dbfa6912f8a53b8659f21216cbf/numpy-2.4.4-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:45dbed2ab436a9e826e302fcdcbe9133f9b0006e5af7168afb8963a6520da103", size = 15668806, upload-time = "2026-03-29T13:19:33.887Z" }, + { url = "https://files.pythonhosted.org/packages/d1/73/a9d864e42a01896bb5974475438f16086be9ba1f0d19d0bb7a07427c4a8b/numpy-2.4.4-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c901b15172510173f5cb310eae652908340f8dede90fff9e3bf6c0d8dfd92f83", size = 16632682, upload-time = "2026-03-29T13:19:37.336Z" }, + { url = "https://files.pythonhosted.org/packages/34/fb/14570d65c3bde4e202a031210475ae9cde9b7686a2e7dc97ee67d2833b35/numpy-2.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:99d838547ace2c4aace6c4f76e879ddfe02bb58a80c1549928477862b7a6d6ed", size = 17019810, upload-time = "2026-03-29T13:19:40.963Z" }, + { url = "https://files.pythonhosted.org/packages/8a/77/2ba9d87081fd41f6d640c83f26fb7351e536b7ce6dd9061b6af5904e8e46/numpy-2.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:0aec54fd785890ecca25a6003fd9a5aed47ad607bbac5cd64f836ad8666f4959", size = 18357394, upload-time = "2026-03-29T13:19:44.859Z" }, + { url = "https://files.pythonhosted.org/packages/a2/23/52666c9a41708b0853fa3b1a12c90da38c507a3074883823126d4e9d5b30/numpy-2.4.4-cp313-cp313-win32.whl", hash = "sha256:07077278157d02f65c43b1b26a3886bce886f95d20aabd11f87932750dfb14ed", size = 5959556, upload-time = "2026-03-29T13:19:47.661Z" }, + { url = "https://files.pythonhosted.org/packages/57/fb/48649b4971cde70d817cf97a2a2fdc0b4d8308569f1dd2f2611959d2e0cf/numpy-2.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:5c70f1cc1c4efbe316a572e2d8b9b9cc44e89b95f79ca3331553fbb63716e2bf", size = 12317311, upload-time = "2026-03-29T13:19:50.67Z" }, + { url = "https://files.pythonhosted.org/packages/ba/d8/11490cddd564eb4de97b4579ef6bfe6a736cc07e94c1598590ae25415e01/numpy-2.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:ef4059d6e5152fa1a39f888e344c73fdc926e1b2dd58c771d67b0acfbf2aa67d", size = 10222060, upload-time = "2026-03-29T13:19:54.229Z" }, + { url = "https://files.pythonhosted.org/packages/99/5d/dab4339177a905aad3e2221c915b35202f1ec30d750dd2e5e9d9a72b804b/numpy-2.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4bbc7f303d125971f60ec0aaad5e12c62d0d2c925f0ab1273debd0e4ba37aba5", size = 14822302, upload-time = "2026-03-29T13:19:57.585Z" }, + { url = "https://files.pythonhosted.org/packages/eb/e4/0564a65e7d3d97562ed6f9b0fd0fb0a6f559ee444092f105938b50043876/numpy-2.4.4-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:4d6d57903571f86180eb98f8f0c839fa9ebbfb031356d87f1361be91e433f5b7", size = 5327407, upload-time = "2026-03-29T13:20:00.601Z" }, + { url = "https://files.pythonhosted.org/packages/29/8d/35a3a6ce5ad371afa58b4700f1c820f8f279948cca32524e0a695b0ded83/numpy-2.4.4-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:4636de7fd195197b7535f231b5de9e4b36d2c440b6e566d2e4e4746e6af0ca93", size = 6647631, upload-time = "2026-03-29T13:20:02.855Z" }, + { url = "https://files.pythonhosted.org/packages/f4/da/477731acbd5a58a946c736edfdabb2ac5b34c3d08d1ba1a7b437fa0884df/numpy-2.4.4-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ad2e2ef14e0b04e544ea2fa0a36463f847f113d314aa02e5b402fdf910ef309e", size = 15727691, upload-time = "2026-03-29T13:20:06.004Z" }, + { url = "https://files.pythonhosted.org/packages/e6/db/338535d9b152beabeb511579598418ba0212ce77cf9718edd70262cc4370/numpy-2.4.4-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a285b3b96f951841799528cd1f4f01cd70e7e0204b4abebac9463eecfcf2a40", size = 16681241, upload-time = "2026-03-29T13:20:09.417Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a9/ad248e8f58beb7a0219b413c9c7d8151c5d285f7f946c3e26695bdbbe2df/numpy-2.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:f8474c4241bc18b750be2abea9d7a9ec84f46ef861dbacf86a4f6e043401f79e", size = 17085767, upload-time = "2026-03-29T13:20:13.126Z" }, + { url = "https://files.pythonhosted.org/packages/b5/1a/3b88ccd3694681356f70da841630e4725a7264d6a885c8d442a697e1146b/numpy-2.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:4e874c976154687c1f71715b034739b45c7711bec81db01914770373d125e392", size = 18403169, upload-time = "2026-03-29T13:20:17.096Z" }, + { url = "https://files.pythonhosted.org/packages/c2/c9/fcfd5d0639222c6eac7f304829b04892ef51c96a75d479214d77e3ce6e33/numpy-2.4.4-cp313-cp313t-win32.whl", hash = "sha256:9c585a1790d5436a5374bac930dad6ed244c046ed91b2b2a3634eb2971d21008", size = 6083477, upload-time = "2026-03-29T13:20:20.195Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e3/3938a61d1c538aaec8ed6fd6323f57b0c2d2d2219512434c5c878db76553/numpy-2.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:93e15038125dc1e5345d9b5b68aa7f996ec33b98118d18c6ca0d0b7d6198b7e8", size = 12457487, upload-time = "2026-03-29T13:20:22.946Z" }, + { url = "https://files.pythonhosted.org/packages/97/6a/7e345032cc60501721ef94e0e30b60f6b0bd601f9174ebd36389a2b86d40/numpy-2.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:0dfd3f9d3adbe2920b68b5cd3d51444e13a10792ec7154cd0a2f6e74d4ab3233", size = 10292002, upload-time = "2026-03-29T13:20:25.909Z" }, + { url = "https://files.pythonhosted.org/packages/6e/06/c54062f85f673dd5c04cbe2f14c3acb8c8b95e3384869bb8cc9bff8cb9df/numpy-2.4.4-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:f169b9a863d34f5d11b8698ead99febeaa17a13ca044961aa8e2662a6c7766a0", size = 16684353, upload-time = "2026-03-29T13:20:29.504Z" }, + { url = "https://files.pythonhosted.org/packages/4c/39/8a320264a84404c74cc7e79715de85d6130fa07a0898f67fb5cd5bd79908/numpy-2.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:2483e4584a1cb3092da4470b38866634bafb223cbcd551ee047633fd2584599a", size = 14704914, upload-time = "2026-03-29T13:20:33.547Z" }, + { url = "https://files.pythonhosted.org/packages/91/fb/287076b2614e1d1044235f50f03748f31fa287e3dbe6abeb35cdfa351eca/numpy-2.4.4-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:2d19e6e2095506d1736b7d80595e0f252d76b89f5e715c35e06e937679ea7d7a", size = 5210005, upload-time = "2026-03-29T13:20:36.45Z" }, + { url = "https://files.pythonhosted.org/packages/63/eb/fcc338595309910de6ecabfcef2419a9ce24399680bfb149421fa2df1280/numpy-2.4.4-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:6a246d5914aa1c820c9443ddcee9c02bec3e203b0c080349533fae17727dfd1b", size = 6544974, upload-time = "2026-03-29T13:20:39.014Z" }, + { url = "https://files.pythonhosted.org/packages/44/5d/e7e9044032a716cdfaa3fba27a8e874bf1c5f1912a1ddd4ed071bf8a14a6/numpy-2.4.4-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:989824e9faf85f96ec9c7761cd8d29c531ad857bfa1daa930cba85baaecf1a9a", size = 15684591, upload-time = "2026-03-29T13:20:42.146Z" }, + { url = "https://files.pythonhosted.org/packages/98/7c/21252050676612625449b4807d6b695b9ce8a7c9e1c197ee6216c8a65c7c/numpy-2.4.4-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:27a8d92cd10f1382a67d7cf4db7ce18341b66438bdd9f691d7b0e48d104c2a9d", size = 16637700, upload-time = "2026-03-29T13:20:46.204Z" }, + { url = "https://files.pythonhosted.org/packages/b1/29/56d2bbef9465db24ef25393383d761a1af4f446a1df9b8cded4fe3a5a5d7/numpy-2.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:e44319a2953c738205bf3354537979eaa3998ed673395b964c1176083dd46252", size = 17035781, upload-time = "2026-03-29T13:20:50.242Z" }, + { url = "https://files.pythonhosted.org/packages/e3/2b/a35a6d7589d21f44cea7d0a98de5ddcbb3d421b2622a5c96b1edf18707c3/numpy-2.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e892aff75639bbef0d2a2cfd55535510df26ff92f63c92cd84ef8d4ba5a5557f", size = 18362959, upload-time = "2026-03-29T13:20:54.019Z" }, + { url = "https://files.pythonhosted.org/packages/64/c9/d52ec581f2390e0f5f85cbfd80fb83d965fc15e9f0e1aec2195faa142cde/numpy-2.4.4-cp314-cp314-win32.whl", hash = "sha256:1378871da56ca8943c2ba674530924bb8ca40cd228358a3b5f302ad60cf875fc", size = 6008768, upload-time = "2026-03-29T13:20:56.912Z" }, + { url = "https://files.pythonhosted.org/packages/fa/22/4cc31a62a6c7b74a8730e31a4274c5dc80e005751e277a2ce38e675e4923/numpy-2.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:715d1c092715954784bc79e1174fc2a90093dc4dc84ea15eb14dad8abdcdeb74", size = 12449181, upload-time = "2026-03-29T13:20:59.548Z" }, + { url = "https://files.pythonhosted.org/packages/70/2e/14cda6f4d8e396c612d1bf97f22958e92148801d7e4f110cabebdc0eef4b/numpy-2.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:2c194dd721e54ecad9ad387c1d35e63dce5c4450c6dc7dd5611283dda239aabb", size = 10496035, upload-time = "2026-03-29T13:21:02.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e8/8fed8c8d848d7ecea092dc3469643f9d10bc3a134a815a3b033da1d2039b/numpy-2.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2aa0613a5177c264ff5921051a5719d20095ea586ca88cc802c5c218d1c67d3e", size = 14824958, upload-time = "2026-03-29T13:21:05.671Z" }, + { url = "https://files.pythonhosted.org/packages/05/1a/d8007a5138c179c2bf33ef44503e83d70434d2642877ee8fbb230e7c0548/numpy-2.4.4-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:42c16925aa5a02362f986765f9ebabf20de75cdefdca827d14315c568dcab113", size = 5330020, upload-time = "2026-03-29T13:21:08.635Z" }, + { url = "https://files.pythonhosted.org/packages/99/64/ffb99ac6ae93faf117bcbd5c7ba48a7f45364a33e8e458545d3633615dda/numpy-2.4.4-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:874f200b2a981c647340f841730fc3a2b54c9d940566a3c4149099591e2c4c3d", size = 6650758, upload-time = "2026-03-29T13:21:10.949Z" }, + { url = "https://files.pythonhosted.org/packages/6e/6e/795cc078b78a384052e73b2f6281ff7a700e9bf53bcce2ee579d4f6dd879/numpy-2.4.4-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c9b39d38a9bd2ae1becd7eac1303d031c5c110ad31f2b319c6e7d98b135c934d", size = 15729948, upload-time = "2026-03-29T13:21:14.047Z" }, + { url = "https://files.pythonhosted.org/packages/5f/86/2acbda8cc2af5f3d7bfc791192863b9e3e19674da7b5e533fded124d1299/numpy-2.4.4-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b268594bccac7d7cf5844c7732e3f20c50921d94e36d7ec9b79e9857694b1b2f", size = 16679325, upload-time = "2026-03-29T13:21:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/bc/59/cafd83018f4aa55e0ac6fa92aa066c0a1877b77a615ceff1711c260ffae8/numpy-2.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:ac6b31e35612a26483e20750126d30d0941f949426974cace8e6b5c58a3657b0", size = 17084883, upload-time = "2026-03-29T13:21:21.106Z" }, + { url = "https://files.pythonhosted.org/packages/f0/85/a42548db84e65ece46ab2caea3d3f78b416a47af387fcbb47ec28e660dc2/numpy-2.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:8e3ed142f2728df44263aaf5fb1f5b0b99f4070c553a0d7f033be65338329150", size = 18403474, upload-time = "2026-03-29T13:21:24.828Z" }, + { url = "https://files.pythonhosted.org/packages/ed/ad/483d9e262f4b831000062e5d8a45e342166ec8aaa1195264982bca267e62/numpy-2.4.4-cp314-cp314t-win32.whl", hash = "sha256:dddbbd259598d7240b18c9d87c56a9d2fb3b02fe266f49a7c101532e78c1d871", size = 6155500, upload-time = "2026-03-29T13:21:28.205Z" }, + { url = "https://files.pythonhosted.org/packages/c7/03/2fc4e14c7bd4ff2964b74ba90ecb8552540b6315f201df70f137faa5c589/numpy-2.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:a7164afb23be6e37ad90b2f10426149fd75aee07ca55653d2aa41e66c4ef697e", size = 12637755, upload-time = "2026-03-29T13:21:31.107Z" }, + { url = "https://files.pythonhosted.org/packages/58/78/548fb8e07b1a341746bfbecb32f2c268470f45fa028aacdbd10d9bc73aab/numpy-2.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:ba203255017337d39f89bdd58417f03c4426f12beed0440cfd933cb15f8669c7", size = 10566643, upload-time = "2026-03-29T13:21:34.339Z" }, +] + [[package]] name = "openai" version = "2.26.0" @@ -1302,6 +1395,23 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/b7/b9/c538f279a4e237a006a2c98387d081e9eb060d203d8ed34467cc0f0b9b53/packaging-26.0-py3-none-any.whl", hash = "sha256:b36f1fef9334a5588b4166f8bcd26a14e521f2b55e6b9de3aaa80d3ff7a37529", size = 74366, upload-time = "2026-01-21T20:50:37.788Z" }, ] +[[package]] +name = "posthog" +version = "7.9.12" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "backoff" }, + { name = "distro" }, + { name = "python-dateutil" }, + { name = "requests" }, + { name = "six" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/1c/a7/2865487853061fbd62383492237b546d2d8f7c1846272350d2b9e14138cd/posthog-7.9.12.tar.gz", hash = "sha256:ebabf2eb2e1c1fbf22b0759df4644623fa43cc6c9dcbe9fd429b7937d14251ec", size = 176828, upload-time = "2026-03-12T09:01:15.184Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/a9/7a803aed5a5649cf78ea7b31e90d0080181ba21f739243e1741a1e607f1f/posthog-7.9.12-py3-none-any.whl", hash = "sha256:7175bd1698a566bfea98a016c64e3456399f8046aeeca8f1d04ae5bf6c5a38d0", size = 202469, upload-time = "2026-03-12T09:01:13.38Z" }, +] + [[package]] name = "propcache" version = "0.4.1" @@ -1538,6 +1648,18 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, ] +[[package]] +name = "python-dateutil" +version = "2.9.0.post0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "six" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/c0/0c8b6ad9f17a802ee498c46e004a0eb49bc148f2fd230864601a86dcf6db/python-dateutil-2.9.0.post0.tar.gz", hash = "sha256:37dd54208da7e1cd875388217d5e00ebd4179249f90fb72437e91a35459a0ad3", size = 342432, upload-time = "2024-03-01T18:36:20.211Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ec/57/56b9bcc3c9c6a792fcbaf139543cee77261f3651ca9da0c93f5c1221264b/python_dateutil-2.9.0.post0-py2.py3-none-any.whl", hash = "sha256:a8b2bc7bffae282281c8140a97d3aa9c14da0b136dfe83f850eea9a5f7470427", size = 229892, upload-time = "2024-03-01T18:36:18.57Z" }, +] + [[package]] name = "python-dotenv" version = "1.2.2" @@ -1751,6 +1873,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/64/8d/0133e4eb4beed9e425d9a98ed6e081a55d195481b7632472be1af08d2f6b/rsa-4.9.1-py3-none-any.whl", hash = "sha256:68635866661c6836b8d39430f97a996acbd61bfa49406748ea243539fe239762", size = 34696, upload-time = "2025-04-16T09:51:17.142Z" }, ] +[[package]] +name = "six" +version = "1.17.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/94/e7/b2c673351809dca68a0e064b6af791aa332cf192da575fd474ed7d6f16a2/six-1.17.0.tar.gz", hash = "sha256:ff70335d468e7eb6ec65b95b99d3a2836546063f63acc5171de367e834932a81", size = 34031, upload-time = "2024-12-04T17:35:28.174Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/ce/149a00dd41f10bc29e5921b496af8b574d8413afcd5e30dfa0ed46c2cc5e/six-1.17.0-py2.py3-none-any.whl", hash = "sha256:4721f391ed90541fddacab5acf947aa0d3dc7d27b2e1e8eda2be8970586c3274", size = 11050, upload-time = "2024-12-04T17:35:26.475Z" }, +] + [[package]] name = "sniffio" version = "1.3.1"