diff --git a/CHANGELOG.md b/CHANGELOG.md index df17e5ee..abcc15c0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,109 @@ Para contexto detallado del trabajo diario ver `docs/diary/`. Para decisiones ar - ADR `0003-orphan-tu-as-separate-function-entry.md` — cuando una funcion del registry necesita partir su `.cpp` en varios TUs por testabilidad o separacion ImGui-vs-puro, cada TU adicional se registra como entrada propia con su `.md` en lugar de extender `file_path` para listar varios archivos. El parent declara la nueva entrada en `uses_functions`. Razon: el indexer asume `1 .cpp = 1 .md`; un `file_path` multi-archivo rompe la convencion y deja apps nuevas sin saber que TUs enlazar. +### Added — sesion NER+RE para graph_explorer (tarde, 980 → 990 funciones) + +**18 funciones nuevas** sobre el ecosistema NER+RE, en dos rondas de `fn-constructor`: + +Ronda 1 — extraccion de relaciones (mREBEL/REBEL/MarianMT): +- `python/functions/datascience/parse_rebel_output.py` (pure) — parser wire `` REBEL/mREBEL. +- `python/functions/datascience/align_relations_to_entities.py` (pure) — string-match aligner. +- `python/functions/datascience/mrebel_load_model.py` (impure, **CC BY-NC-SA 4.0 — NO comercial**). +- `python/functions/datascience/mrebel_base_load_model.py` (impure, misma licencia). +- `python/functions/datascience/rebel_load_model.py` (impure, **Apache 2.0**, EN-only). +- `python/functions/datascience/marianmt_es_en_load_model.py` (impure) — Helsinki-NLP/opus-mt-es-en. +- `python/functions/datascience/translate_es_to_en.py` (impure) — wrapper traduccion frase a frase. +- `python/functions/datascience/extract_relations_mrebel.py` (impure) — pipeline mREBEL frase-a-frase + alineamiento. +- 21 tests pytest verdes. + +Ronda 2 — pipeline GLiNER2 + OpenIE schema-less + composicion (tarde): +- `python/functions/core/clean_pdf_text.py` (pure) — limpia artefactos PyPDF2. +- `python/functions/core/chunk_with_overlap.py` (pure) — sliding window con avance forzado. +- `python/functions/core/merge_entity_aliases.py` (pure) — coreferencia normalize+substring. +- `python/functions/core/filter_relations_by_entity_types.py` (pure) — post-filter typed. +- `python/functions/core/aggregate_extraction_results.py` (pure) — dedupe + Counter sobre N chunks. +- `python/functions/datascience/gliner2_load_model.py` (impure, **Apache 2.0**) — `fastino/gliner2-large-v1`. +- `python/functions/datascience/extract_graph_gliner2.py` (impure) — wrapper schema + threshold + include_confidence. +- `python/functions/datascience/spacy_es_load_model.py` (impure) — `es_core_news_md` cacheado. +- `python/functions/datascience/extract_triples_spacy_es.py` (impure) — OpenIE schema-less ES por reglas de dependencia (verbo del texto = predicado). +- `python/functions/pipelines/extract_graph_from_text.py` (impure pipeline) — composicion E2E: chunk → extract_graph_gliner2 (×N) → aggregate → filter typed → merge aliases → grafo final. +- 39 tests pytest verdes. + +### Added — analysis `gliner_glirel_tuning` + +`projects/osint_graph/analysis/gliner_glirel_tuning/` — investigacion empirica de modelos NER/RE. **9 notebooks** ejecutados: + +| # | Notebook | Hallazgo clave | +|---|---|---| +| 01 | `01_gliner_glirel_tuning.ipynb` | Calibracion de thresholds GLiNER+GLiREL | +| 02 | `02_e2e_spanish_graph.ipynb` | E2E texto ES — descubrimiento del fail de GLiREL en castellano | +| 03 | `03_mrebel_vs_glirel.ipynb` | mREBEL gana a GLiREL pero CC BY-NC-SA | +| 04 | `04_gliner2_winner.ipynb` ⭐ | **GLiNER2 (Apache 2.0, NER+RE joint, 340M)** elegido como motor principal | +| 05 | `05_long_text_and_pdf.ipynb` | Pipeline PDF E2E sobre `politica_proteccion_datos.pdf` (BBVA, 89.882 chars) | +| 06 | `06_improvements.ipynb` | Threshold 0.3 (vs default 0.5) → +187% relaciones; coref reduce 18% aislados | +| 07 | `07_nuextract_vs_gliner2.ipynb` | NuExtract GPU 2.6× mas lento, calidad similar — descartado por defecto | +| 08 | `08_improving_gliner2.ipynb` | snake_case verbal labels + post-filter typed = mejor combo | +| 09 | `09_spacy_es_openie.ipynb` | spaCy ES dep-rules: schema-less, predicado = verbo del texto | + +### Added — vault `osint_nlp_models` + +`projects/osint_graph/vaults/osint_nlp_models` (symlink a `~/vaults/osint_nlp_models/`): +- `models/` — fichas de gliner, glirel, mrebel, gliner2, candidates a probar. +- `decisions/` — 3 ADRs cortos del 2026-05-04 (mrebel-over-glirel mañana, gliner2-over-mrebel tarde, license-constraint). +- `benchmarks/corpus_v1.md` + `results_log.csv` (15 filas de experimentos). +- `test_documents/politica_proteccion_datos.pdf` (PDF de BBVA copiado para reproducibilidad). + +### Added — playground HTML + +`projects/osint_graph/analysis/gliner_glirel_tuning/playground/`: +- `server.py` — FastAPI con GLiNER2 cacheado, endpoints `GET /` (HTML) y `POST /extract` (texto → grafo). +- `index.html` — UI: textarea, KPIs (nodos/aristas/tiempo), grafo Sigma.js, JSON exportable. +- `static/sigma.min.js` + `graphology.umd.min.js` (servidos localmente para evitar bloqueo CDN por extensiones tipo MetaMask/SES). + +Stack aplicado por el server: +1. snake_case verbal labels (`works_at`, `ceo_of`, `headquartered_in`, `agreement_with`...) +2. threshold 0.3 (configurable) +3. chunking automatico > 1500 chars +4. post-filter typed (`(person, organization)` validos por relacion) +5. coreferencia normalize+substring +6. layout server-side via `networkx.spring_layout` +7. render Sigma.js (sin fisica → sin loops de ResizeObserver) + +### Added — issues + +- `dev/issues/0050-jupyter-exec-collab-client-failure.md` — bug `jupyter_exec` con cliente colaborativo + workaround documentado. +- `projects/osint_graph/apps/graph_explorer/issues/0041-split-confidence-thresholds.md` — split `confidence_threshold` en `entity_threshold` + `relation_threshold`. +- `projects/osint_graph/apps/graph_explorer/issues/0042-gliner2-unified-extractor.md` ⭐ — sustituir GLiREL por GLiNER2 en `extract_graph_hybrid`. Reemplaza 0042-mrebel. +- `projects/osint_graph/apps/graph_explorer/issues/0042-mrebel-relation-extractor.md.superseded` — version mREBEL del 0042 archivada al ganar GLiNER2. + +### Changed + +- `cpp/CMakeLists.txt` — `_GE_DIR` y `_DASH_DIR` sobreescribibles via `-D<...>=` para builds en worktrees (commit `e72d6364`). Habilita `parallel-fix-issues` sobre apps C++. +- `python/functions/datascience/glirel_load_model.py` — workaround compat `huggingface_hub` 1.x: classmethod monkey-patch idempotente para inyectar `proxies`/`resume_download` que el HF nuevo dejo de pasar (commit `3b3378cf`). +- Sub-repo `dataforge/graph_explorer` master local: merges `--no-ff` de `issue/0035e-polish-and-tests` (commit `f614a51`) + `issue/0013-paste-extract-panel` (commit `2a49c2b`). 125/125 tests pytest verdes. **Sin push aun** — pendiente confirmacion + validacion Windows. + +### Fixed (bugs encontrados + raiz + fix) + +| Bug | Raiz | Fix | +|---|---|---| +| `chunk_with_overlap` bucle infinito | Frase mas larga que `max_chars`, no avanzaba `i`, OOM-killed por overlap acumulado | Avance forzado: meter al menos UNA frase aunque exceda `max_chars` | +| NuExtract degenera en texto largo | Sin `repetition_penalty`, decoder entra en bucle de tokens repetidos hasta agotar 2048 max_new_tokens | `repetition_penalty=1.15` + chunking obligatorio (179/179 chunks parsed OK tras fix) | +| NuExtract `AutoProcessor.from_pretrained` rota en transformers 5.x | Sub-processor de video tira `TypeError: argument of type 'NoneType' is not iterable` (Qwen2-VL) | Bypass: `AutoTokenizer` + `AutoModelForImageTextToText` directamente | +| Vis-network ResizeObserver loop spam (en SES/MetaMask) | Vis-network usa physics simulation → ResizeObserver dispara warnings amplificados por SES | Migrar a Sigma.js + layout server-side via `networkx.spring_layout` (sin fisica frontend) | +| `jupyter_exec append` HTTP 405 | `jupyter_nbmodel_client` espera collab WebSocket Y.js, no soportado al 100% por jupyter-collaboration nuevo | Documentado en issue 0050; workaround actual: build_notebook scripts con `nbformat` + `nbconvert --execute` | +| Kernel startup shadows pip packages | `00_fn_registry.py` añade cada subdir de `python/functions/` a sys.path top-level → `bigquery/datasets.py` shadows HF `datasets` package needed by transformers | Workaround per-notebook: `sys.path = [p for p in sys.path if not p.startswith(_pf+'/')]` + añadir solo el padre. Issue futuro pendiente. | + +### Decisions — vault ADRs + +| Decision | Razon | +|---|---| +| **GLiNER2 (Apache 2.0)** sustituye a GLiREL en `extract_graph_hybrid` | 6/8 relaciones correctas vs 0/1 de GLiREL en es_corporate_short, 1.18s vs 22s de mREBEL, NER+RE en una pasada | +| mREBEL queda como fallback (no comercial) | 4/5 correctas pero CC BY-NC-SA 4.0 + 25× mas lento | +| spaCy ES dep-rules para OpenIE schema-less | Predicado = verbo del texto (`querer`, `abrazar`), 5ms/frase, sin alucinaciones | +| Threshold `0.3` (vs default `0.5`) sweet spot | +187% relaciones manteniendo precision; 0.2 mete +22% entidades dudosas | +| Coreferencia normalize+substring + post-filter typed = **gratis y decisivos** | Coref −18% aislados; post-filter elimina `Madrid president_of Persona` | +| Translate ES→EN + triplet-extract EN **NO** vale la pena | Pierdes verbos del texto (`querer` → `loves`), +500ms-1s, +300MB MarianMT, riesgo nombres propios | + ## 2026-04-28 ### Added diff --git a/dev/issues/0050-jupyter-exec-collab-client-failure.md b/dev/issues/0050-jupyter-exec-collab-client-failure.md new file mode 100644 index 00000000..f6f8216b --- /dev/null +++ b/dev/issues/0050-jupyter-exec-collab-client-failure.md @@ -0,0 +1,166 @@ +# 0050 — `jupyter_exec` falla por cliente colaborativo (workaround documentado) + +## APP Metadata + +| Campo | Valor | +|-------|-------| +| **ID** | 0050 | +| **Estado** | pendiente | +| **Prioridad** | media | +| **Tipo** | bug — `python/functions/notebook/jupyter_exec.py` | + +## Dependencias + +Ninguna. Independiente del resto. + +--- + +## Sintoma + +Al ejecutar `jupyter_exec.py append ` contra un Jupyter Lab +arrancado con el launcher estandar de los analyses (`run-jupyter-lab.sh`, +flag `--collaborative`), la operacion falla con: + +``` +{"error": "HTTP Error 405: Method Not Allowed"} +``` + +`jupyter_write.py append-code` y `append-markdown` SI funcionan (no usan el +canal colaborativo). El bug solo afecta a `jupyter_exec`, que necesita +ejecutar la celda en el kernel y para eso usa `jupyter_nbmodel_client` +con websocket Y.js. + +Reproducido en `2026-05-04` durante la construccion del analysis +`projects/osint_graph/analysis/gliner_glirel_tuning/`. El resto de +funciones del modulo `notebook/` quedan intactas: + +```bash +$JX append # ❌ HTTP 405 +$JW append-code # ✅ OK (sin ejecucion) +$JW append-markdown # ✅ OK +$JX cell # 🔁 No probado, pero usa el mismo cliente +$JX kernel # 🔁 No probado +``` + +--- + +## Diagnostico (parcial) + +`jupyter_nbmodel_client` espera que el server tenga la extension +`jupyter_collaboration` activa y montada en `/api/collaboration/...`. El +launcher arranca jupyter con el flag CLI `--collaborative`, que en +versiones recientes (`jupyter_server >= 2.x`, `jupyter-collaboration >= 4.x`) +**ya no es suficiente** — la extension se carga via entry-point y se +controla con flags distintos (`--YDocExtension.disable_rtc` o equivalente), +o requiere un fichero de config explicito. + +Salida de `jupyter_discover.py` confirma el sintoma indirectamente: + +```json +{ "url": "http://localhost:8888", "collaborative": false, ... } +``` + +aunque `--collaborative` esta en el launch command. Es decir: el server +arranca, expone la API REST, pero la capa colaborativa NO esta activa. + +--- + +## Workaround usado en `gliner_glirel_tuning` + +Cambio de tactica: en lugar de construir el notebook con `jupyter_exec +append` celda a celda, **se ejecutan los experimentos en un script +externo** y se empotran las celdas (codigo + outputs ya generados) con +`nbformat` directo a fichero. El notebook resultante es persistente y +no necesita el canal colaborativo. + +```python +# build_notebook.py +import nbformat as nbf +nb = nbf.v4.new_notebook() +for src, stdout in cells: + cell = nbf.v4.new_code_cell(src) + cell.outputs = [nbf.v4.new_output("stream", name="stdout", text=stdout)] + nb.cells.append(cell) +nbf.write(nb, "notebooks/01_foo.ipynb") +``` + +Si se quieren outputs reales (DataFrames como HTML, figuras matplotlib), +ejecutar despues con `nbconvert`: + +```bash +IPYTHONDIR=$(pwd)/.ipython ./.venv/bin/jupyter nbconvert \ + --to notebook --execute notebooks/01_foo.ipynb \ + --output 01_foo.ipynb --ExecutePreprocessor.timeout=300 +``` + +Esto bypassa completamente el canal colaborativo y produce un `.ipynb` +funcional, abrible en Jupyter Lab para ver / iterar / re-ejecutar. + +Ver `projects/osint_graph/analysis/gliner_glirel_tuning/build_notebook.py` +y `build_notebook_e2e.py` para ejemplos vivos. + +--- + +## Causas raiz a investigar + +1. **Verificar la version de `jupyter-collaboration`** en el venv del + analysis. Si es >=4.x, el flag `--collaborative` ya no aplica y el + launcher (`write_jupyter_launcher_bash_io`) tiene que actualizarse. +2. **El cliente** `jupyter_nbmodel_client` puede tener su propia + ventana de versiones soportadas — comprobar pinning en + `python/.venv` y en los venvs de analyses. +3. **El endpoint** `/api/collaboration/document` debe responder a un + `GET` con HTTP 200 cuando la extension esta activa. Si responde + `405`, el cliente intenta una operacion (POST/PUT) sobre un endpoint + que solo acepta GET, sintoma de mismatch. + +--- + +## Tareas + +1. Reproducir el `HTTP 405` con un notebook nuevo y un kernel nuevo + en un analysis recien creado. +2. Capturar la URL exacta y el metodo HTTP que dispara el 405 + (anadir logging a `jupyter_exec.py` linea ~192/229 donde llama a + `get_jupyter_notebook_websocket_url`). +3. Verificar version de `jupyter-collaboration` en el venv y comparar + con la matriz de compatibilidad de `jupyter_nbmodel_client`. +4. Una de dos: + - **(a)** Corregir el flag/config en `write_jupyter_launcher_bash_io` + para activar correctamente la colaboracion en versiones nuevas. + - **(b)** Si la API colaborativa cambio mucho, **migrar + `jupyter_exec.py` a usar el `JupyterClient` clasico** (REST + WebSocket + directo al kernel sin Y.js) que es estable a traves de versiones. + `jupyter_kernel.py` ya hace algo asi y funciona. +5. Anadir un test e2e basico en `tests/` que arranca jupyter, lanza + `jupyter_exec append`, verifica que la celda se ejecuto y captura + stdout. Sin esto el bug puede regresar. + +--- + +## Out of scope + +- Reescribir el sistema completo de notebook collaboration. +- Migrar a un MCP. La regla `notebook_collaboration.md` es explicita: + estas funciones reemplazan al MCP jupyter. + +--- + +## Riesgos + +- Si la causa es la matriz de versiones, la opcion (a) puede generar + fricion futura cada vez que `jupyter-collaboration` haga un breaking + change. La opcion (b) es mas robusta a largo plazo aunque pierde la + capacidad de ver cambios en tiempo real desde el navegador. + +## Notas operativas + +Mientras este bug exista, el patron recomendado para construir notebooks +desde un agente Claude en un analysis es: + +1. `build_notebook.py` con `nbformat` para estructura + outputs estaticos. +2. `nbconvert --execute` para outputs reales (HTML, plots). +3. Si necesitas tiempo real con el browser, abre el notebook ya generado + en Jupyter Lab y reejecuta a mano. + +El propio analysis `gliner_glirel_tuning` es referencia. diff --git a/dev/issues/0051-extraction-pipeline-followups.md b/dev/issues/0051-extraction-pipeline-followups.md new file mode 100644 index 00000000..4c923695 --- /dev/null +++ b/dev/issues/0051-extraction-pipeline-followups.md @@ -0,0 +1,313 @@ +# 0051 — Funciones pendientes del pipeline de extraccion (NER+RE+OpenIE) + +## APP Metadata + +| Campo | Valor | +|-------|-------| +| **ID** | 0051 | +| **Estado** | pendiente | +| **Prioridad** | media | +| **Tipo** | feature — `python/functions/{datascience,pipelines,core}/` | + +## Dependencias + +- Las 18 funciones NER/RE creadas el 2026-05-04 (gliner2_load_model, extract_graph_gliner2, extract_triples_spacy_es, etc.) — base ya construida. +- `extract_pdf_text_py_core` — ya existente, se reusa. + +**Desbloquea:** integracion completa del pipeline GLiNER2 + spaCy ES + chunking + coref + post-filter en `graph_explorer` panel `paste_extract` (issues 0041 y 0042 del sub-repo). + +--- + +## Contexto + +En la sesion del 2026-05-04 se construyeron 18 funciones NER+RE (ver CHANGELOG.md y `vaults/osint_nlp_models/`). Quedan **5 huecos** que no se construyeron en esa ronda y que deberian existir para cerrar el ciclo: + +1. NuExtract loader + extractor — descartado por velocidad pero util como engine "Rich extraction" opcional cuando hay GPU. +2. `extract_graph_from_pdf` pipeline — composicion `extract_pdf_text + clean_pdf_text + chunk_with_overlap + extract_graph_gliner2 + ...`. +3. spaCy ES V2 reglas — soportar pasiva refleja, copulares, coref simple de pronombres. +4. Fix del kernel startup que sombrea paquetes pip (`bigquery/datasets.py` rompe `import datasets` de HF). +5. `extract_relations_rebel` (paralela a `extract_relations_mrebel`) para texto en ingles con licencia Apache. + +Cada hueco se desglosa abajo con plantilla suficiente para que un proximo `fn-constructor` lo pueda construir sin abrir la conversacion original. + +--- + +## A. NuExtract 2.0 loader + extractor + +### Justificacion + +NuExtract 2.0-2B (`numind/NuExtract-2.0-2B`, **MIT license**) emite **JSON estructurado** rellenando un template. Util cuando el usuario quiere ficha rica por entidad (ej. para cada empresa: `{name, ceo, headquarters, subsidiaries, founded_in}`). Mas lento que GLiNER2 (310s vs 139s sobre el PDF de BBVA) pero mejor recall de atributos por entidad. + +Ver `notebooks/07_nuextract_vs_gliner2.ipynb` y `vaults/osint_nlp_models/models/` (no hay md de nuextract todavia, anadirlo). + +### Funciones a crear + +**A1. `nuextract_load_model_py_datascience` (impure)** + +```python +"""LICENSE: MIT (NuExtract-2.0-2B). Comercial OK. + +Version 4B es CC BY-NC-Qwen-Research (no comercial). +Version 8B es MIT. +""" +from typing import Any +_MODEL_CACHE: dict = {} + +def nuextract_load_model( + model_name: str = "numind/NuExtract-2.0-2B", + device: str = "auto", +) -> tuple[Any, Any]: + """Loads (and caches) NuExtract tokenizer + model. + + Returns (tokenizer, model). + Note: AutoProcessor is broken in transformers 5.x for Qwen2-VL — use AutoTokenizer + AutoModelForImageTextToText directly (no AutoProcessor). + + For GPU: bfloat16, attn_implementation='sdpa'. + For CPU: float32, attn_implementation='eager' (much slower, 10-30s/extraction). + """ + import torch + from transformers import AutoTokenizer, AutoModelForImageTextToText + use_gpu = device == "cuda" or (device == "auto" and torch.cuda.is_available()) + resolved = "cuda" if use_gpu else "cpu" + dtype = torch.bfloat16 if use_gpu else torch.float32 + attn = "sdpa" if use_gpu else "eager" + key = (model_name, resolved) + if key in _MODEL_CACHE: return _MODEL_CACHE[key] + tok = AutoTokenizer.from_pretrained(model_name, trust_remote_code=True, padding_side="left") + mdl = AutoModelForImageTextToText.from_pretrained( + model_name, trust_remote_code=True, + torch_dtype=dtype, attn_implementation=attn, + ) + if use_gpu: mdl = mdl.to(resolved) + mdl.eval() + _MODEL_CACHE[key] = (tok, mdl) + return tok, mdl +``` + +**A2. `extract_structured_nuextract_py_datascience` (impure)** + +```python +def extract_structured_nuextract( + text: str, + template: str, # JSON schema as string + tokenizer, + model, + max_new_tokens: int = 1024, + repetition_penalty: float = 1.15, # CRITICO — sin esto degenera en bucles + num_beams: int = 1, +) -> dict: + """Extract structured info from text using NuExtract 2.0 with a JSON template. + + Returns: + {"raw_text": str (el JSON crudo del modelo), + "parsed": dict | None (parseado con find('{') + truncate progresivo), + "elapsed_s": float, + "n_input_tokens": int, + "n_output_tokens": int} + + IMPORTANTE: NuExtract degenera en texto largo si repetition_penalty < ~1.1. + Usar repetition_penalty=1.15 (default) y trocear texto largo con chunk_with_overlap. + """ +``` + +Parser de output esta en `run_nuextract_full.py` linea ~120 (find('{') + truncate progresivo). + +### Tests + +- A1: cache hit/miss. +- A2: con stub de modelo, validar parser de JSON. Con corpus real solo si GPU disponible (skip otherwise). + +--- + +## B. `extract_graph_from_pdf_py_pipelines` + +### Justificacion + +Composicion natural: `extract_pdf_text` ya existe en `python/functions/core/`. Combinarlo con todo lo nuevo: + +``` +extract_pdf_text (existente) + → clean_pdf_text (NUEVO 2026-05-04) + → chunk_with_overlap (NUEVO 2026-05-04) + → extract_graph_gliner2 (×N, NUEVO 2026-05-04) + → aggregate_extraction_results + → filter_relations_by_entity_types + → merge_entity_aliases + → grafo final +``` + +### Firma + +```python +def extract_graph_from_pdf( + pdf_path: str, + entity_labels: list[str], + relation_labels, + allowed: dict, + model, # GLiNER2 model + threshold: float = 0.3, + max_chars_per_chunk: int = 1500, + overlap_sentences: int = 2, +) -> dict: + """End-to-end pipeline: PDF -> graph. + + Internally: + 1. extract_pdf_text (existing) + 2. clean_pdf_text + 3. chunk_with_overlap if len(text) > max_chars_per_chunk + 4. extract_graph_gliner2 per chunk + 5. aggregate_extraction_results + 6. filter_relations_by_entity_types + 7. merge_entity_aliases + + Returns: same shape as extract_graph_from_text. + """ +``` + +Esto es essencialmente `extract_graph_from_text(extract_pdf_text(path), ...)` con la limpieza intermedia. + +### Tests + +- Test smoke con PDF de fixture pequeño (1-2 paginas). +- Test que fallback a chunking solo dispara cuando `len(text) > max_chars`. + +### Donde poner el PDF de fixture + +`python/functions/pipelines/tests/fixtures/sample.pdf` — un PDF corto de uso libre. O reusar `vaults/osint_nlp_models/test_documents/politica_proteccion_datos.pdf` con un path absoluto en el test (skip si no existe). + +--- + +## C. spaCy ES V2 — reglas mejoradas + +### Justificacion + +Notebook 09 mostro que las reglas V1 (`extract_triples_spacy_es_py_datascience`) fallan en: + +1. **Pasiva refleja**: `Se firmaron acuerdos entre Iberdrola y Endesa.` → vacio. Debe emitir `(Iberdrola, firmar[pass], Endesa)` o similar. +2. **Copulares**: `Pablo Isla es expresidente de Inditex.` → vacio. Debe emitir `(Pablo Isla, ser, expresidente de Inditex)`. +3. **Coreferencia pronombres**: `Sara llamo a su madre Lucia.` → tripleta con span `'su madre Lucia'`. Debe resolver `su` al sujeto previo (Sara). +4. **Lematizacion**: `movilizara` → `movilizarar` (lemma incorrecta del modelo `es_core_news_md`). Considerar `es_core_news_lg` o post-process. + +### Funciones a crear + +**C1. `extract_triples_spacy_es_v2_py_datascience` (impure)** + +Mismo patron que V1 pero con reglas adicionales: + +```python +def extract_triples_spacy_es_v2(text: str, nlp: Any, resolve_pronouns: bool = True) -> dict: + """Improved Spanish OpenIE via spaCy dependency parsing. + + V2 changes vs V1: + - Pasiva refleja: detect 'se' + verb conjugated -> treat agent as subject if available + - Copulares: 'X es Y', 'X esta Y' -> emit (X, ser/estar, Y) + - Coref simple: track previous subject, resolve 'su X' to that subject + - Lemma override: hardcoded fixes for common errors (movilizarar -> movilizar) + + Returns: same shape as extract_triples_spacy_es V1. + """ +``` + +### Tests + +- Test pasiva refleja: `'Se firmaron acuerdos entre Iberdrola y Endesa'` -> tripleta con `firmar[pass]`. +- Test copular: `'Pablo es presidente'` -> `(Pablo, ser, presidente)`. +- Test coref: `'Sara llamo a su madre Lucia'` -> sujeto canonico Sara (no `'su madre Lucia'`). +- Test lemma override: `movilizara` -> lemma `movilizar`. + +--- + +## D. Fix kernel startup shadow de paquetes pip + +### Sintoma + +`.ipython/profile_default/startup/00_fn_registry.py` añade cada subdir de `python/functions/` al sys.path top-level. Como hay un `bigquery/datasets.py` en el registry, **shadows** el paquete `datasets` de HuggingFace que `transformers` necesita. Resultado: en cada notebook hay que aplicar un workaround: + +```python +_pf = '/home/lucas/fn_registry/python/functions' +sys.path = [p for p in sys.path if not p.startswith(_pf + '/')] +if _pf not in sys.path: sys.path.insert(0, _pf) +``` + +### Fix propuesto + +Modificar el template `write_jupyter_registry_kernel` (la funcion del registry que genera ese startup file en cada analysis nuevo) para: + +```python +# Solo el directorio padre 'python/functions/' (no los subdirs) +sys.path.insert(0, str(_python_functions)) + +# El usuario importa con paquete: +# from datascience.gliner_load_model import gliner_load_model +# from core.extract_pdf_text import extract_pdf_text +# (no `from gliner_load_model import ...` directo) +``` + +Esto requiere actualizar: +1. La funcion del registry que genera el startup file. +2. Re-generar el startup file en analyses existentes (script de migracion). +3. Documentar en `.claude/CLAUDE.md` que los imports en notebooks de analysis siguen el patron `from import `. + +### Tests + +- Test que el startup nuevo permite `import datasets` (huggingface) sin shadow. +- Test que sigue funcionando `from datascience.gliner_load_model import gliner_load_model`. + +--- + +## E. `extract_relations_rebel_py_datascience` + +### Justificacion + +`extract_relations_mrebel` ya existe (creado en ronda 1 del 2026-05-04). Para texto en **ingles** y casos donde se necesita licencia comercial sin caveat, REBEL (`Babelscape/rebel-large`, **Apache 2.0**) es la alternativa. + +### Firma + +```python +def extract_relations_rebel( + text: str, + entities: list, # list[EntityCandidate] + tokenizer, + model, + sentence_split_re: str = r"(?<=[\.])\s+", + min_sentence_chars: int = 20, + num_beams: int = 4, + max_length: int = 256, +) -> list: + """Extract relations from English text using REBEL, sentence by sentence. + + Same wire format as mREBEL — reuses `parse_rebel_output` and + `align_relations_to_entities` from the registry. + + LICENSE: Apache 2.0 (commercial OK). + """ +``` + +Practicamente identica a `extract_relations_mrebel` pero sin el `tgt_lang='tp_XX'` (REBEL es monolingue). + +--- + +## Priorizacion sugerida + +| # | Item | Impacto | Coste | Cuando | +|---|---|---|---|---| +| B | `extract_graph_from_pdf` pipeline | ⭐⭐⭐ — la composicion mas usada | Bajo (compone existentes) | Inmediato | +| C | spaCy ES V2 reglas | ⭐⭐ — desbloquea mas casos ES | Medio (reglas + tests) | Cuando V1 limita | +| D | Fix kernel startup | ⭐⭐ — limpia el flow notebooks | Medio (refactor + migracion) | Cuando se cree un analysis nuevo | +| A | NuExtract loader/extractor | ⭐ — engine alternativo opcional | Medio (GPU testing) | Cuando se quiera "Rich mode" | +| E | REBEL EN extractor | ⭐ — solo si llega caso EN comercial | Bajo (copy de mREBEL) | Cuando aparezca el caso | + +--- + +## Definicion de hecho (todos los items) + +- Funciones implementadas + frontmatter + tests pytest verdes. +- `./fn index` suma exactamente las funciones declaradas. +- `./fn check params` no marca ninguna nueva sin params_schema. +- Documentadas en `vaults/osint_nlp_models/models/` o seccion correspondiente del vault. +- Notas operativas en `app.md` del consumidor (graph_explorer) si toca uses_functions. + +## Out of scope explicito + +- LLM-as-validator para mejorar relaciones (Claude Haiku post-NuExtract). El usuario indico explicitamente que no quiere LLMs pesados en el flow. +- GLiDRE / ReLiK / AlignRE — solo si surge necesidad concreta. Listados en `vaults/osint_nlp_models/models/candidates.md`. diff --git a/dev/issues/0052-footprint-aurgi-extraction.md b/dev/issues/0052-footprint-aurgi-extraction.md new file mode 100644 index 00000000..587d382b --- /dev/null +++ b/dev/issues/0052-footprint-aurgi-extraction.md @@ -0,0 +1,46 @@ +--- +title: "Extracción masiva de footprint_aurgi → registry" +status: in_progress +created: 2026-05-04 +--- + +# 0052 — Extracción de funciones de `sources/footprint_aurgi/` + +Extracción de 45 funciones + 4 tipos del proyecto interno `footprint_aurgi` (código propio Aurgi, sin LICENSE — `source_license: internal-aurgi`). + +## Capacidades cubiertas + +1. Geocodificación y routing (Valhalla) +2. Generación de isócronas (sync + async batch) +3. Stack Docker geo (PostGIS + Martin + Valhalla) +4. Spatial primitivas (haversine, point-in-polygon, bbox, sindex) +5. Visualización en mapa (basemap OSM, KDE, alpha-shape hulls) +6. PDFs reporting (compresión ghostscript, table pages) +7. Estadística para distribuciones reales (skew/kurt, trimmed/geo means) +8. Fuzzy joining adaptativo +9. Normalización España (CP→provincia) +10. Data prep (CSV→Parquet via duckdb) + +## Batches + +| # | Dominio | Funciones | Owner | +|---|---|---|---| +| 1 | geo (puras) + tipos | haversine, point_in_polygon, bbox, extent, distance_bucket + LonLat, BBox, IsochroneRequest, Centro | agent-A | +| 2 | core (string ES) | slugify_ascii, normalize_for_join, cp_provincia_es, infer_provincia_from_cp | agent-B | +| 3 | datascience (stats) | trimmed_mean, geometric_mean, detect_distribution_type, best_central_tendency, summary_stats, kde_density_levels, alpha_shape_concave_hull | agent-C | +| 4 | datascience (fuzzy) | fuzzy_merge_adaptive, words_to_dataset, remove_words_from_column | agent-D | +| 5 | geo (Valhalla client) | valhalla_route, valhalla_matrix_1_to_n, valhalla_isochrone, valhalla_isochrones_async | agent-E | +| 6 | geo (I/O + viz) | load_geojson_polygons, load_boundary_gdf, add_basemap_osm, add_basemap_with_timeout, plot_kde_2d, plot_heatmap_log | agent-F | +| 7 | infra (PDF + data) | compress_pdf_ghostscript, render_table_page_pdfpages, add_header_logo, safe_read_csv_fallback, csv_to_parquet_duckdb, osm2pgsql_ingest | agent-G | +| 8 | infra (docker stack) | docker-compose footprint geo (PostGIS + Martin + Valhalla) — levantar y verificar | agent-H | +| 9 | pipelines | setup_geo_stack_docker, compute_centers_reachability, generate_isochrones_by_zone, count_points_per_zone | agent-I (wave 2) | + +## Fuente + +- Path: `sources/footprint_aurgi/` +- Sub-proyectos: aurgi_mapas, better_maps, frontend_mapas, fuzzy_joins, ponderacion_isochronas, zonas_mapas_aurgi +- Atribución uniforme: `source_repo: "internal:footprint_aurgi"`, `source_license: "internal-aurgi"` + +## Resultado esperado + +Reporte final por función: ✅ tests pasan / ❌ tests fallan / ⚠️ stub (requiere infra externa). diff --git a/dev/issues/README.md b/dev/issues/README.md index 8dbacab1..2b265f5f 100644 --- a/dev/issues/README.md +++ b/dev/issues/README.md @@ -66,3 +66,5 @@ | [0049i](completed/0049i-graph-layouts-static.md) | graph_layouts (radial/hierarchical/fixed) + viewport multi-select | completado | media | feature | parte de 0049 | | [0049j](completed/0049j-graph-labels.md) | graph_labels: render etiquetas con LabelPolicy | completado | media | feature | parte de 0049 | | [0049k](completed/0049k-graph-explorer-app.md) | App graph_explorer (proyecto osint_graph) — integracion final | completado | alta | feature | parte de 0049 | +| [0050](0050-jupyter-exec-collab-client-failure.md) | `jupyter_exec` falla por cliente colaborativo (workaround documentado) | pendiente | media | bug | — | +| [0051](0051-extraction-pipeline-followups.md) | Funciones pendientes del pipeline NER+RE (NuExtract, extract_graph_from_pdf, spaCy ES V2, kernel startup fix, REBEL EN) | pendiente | media | feature | — | diff --git a/docs/diary/2026-05-04.md b/docs/diary/2026-05-04.md index 7c51c8b6..dc5713b1 100644 --- a/docs/diary/2026-05-04.md +++ b/docs/diary/2026-05-04.md @@ -23,3 +23,24 @@ Sesion para verificar que las 5 apps C++ del registry usan programacion funciona - Pendiente: commit + push de los cambios (binary `fn` rebuilt no commiteado, .md y `registry/indexer.go` modificados). Refs: ADR [0003](../adr/0003-orphan-tu-as-separate-function-entry.md), funciones `graph_labels_select_cpp_viz`, `graph_viewport_selection_cpp_viz`, `graph_types_cpp_viz`, apps `chart_demo_cpp_viz`, `shaders_lab_cpp_gfx`. Cambio en indexer: `registry/indexer.go` lineas ~87-122. + +## 22:50 — Pipeline NER+RE para graph_explorer: 18 funciones nuevas + analysis + playground + vault + +- Hecho: investigacion empirica completa de modelos NER/RE en `projects/osint_graph/analysis/gliner_glirel_tuning/`. **9 notebooks** ejecutados (01-09), cada uno con su `build_notebook_*.py` y, los pesados, su `run_*.py` que vuelca a JSON. Los notebooks viven listos para abrir desde Jupyter Lab (`localhost:8888`). +- Hecho: dos rondas de `fn-constructor` el mismo dia. Ronda 1 (mañana): 8 funciones para mREBEL/REBEL/MarianMT (`parse_rebel_output`, `align_relations_to_entities`, `mrebel_load_model`, `mrebel_base_load_model`, `rebel_load_model`, `marianmt_es_en_load_model`, `translate_es_to_en`, `extract_relations_mrebel`). Ronda 2 (tarde): 10 funciones para GLiNER2 + OpenIE ES + composicion (`clean_pdf_text`, `chunk_with_overlap`, `merge_entity_aliases`, `filter_relations_by_entity_types`, `aggregate_extraction_results`, `gliner2_load_model`, `extract_graph_gliner2`, `spacy_es_load_model`, `extract_triples_spacy_es`, `extract_graph_from_text` pipeline). **Total registry: 980 → 990 funciones, 60 tests pytest verdes.** +- Hecho: 3 ADRs cortos en `vaults/osint_nlp_models/decisions/` registrando la cadena de decisiones: mañana `mrebel-over-glirel.md`, tarde `gliner2-over-mrebel.md` (decision final), `license-constraint.md`. Vault con 5 fichas `.md` por modelo (gliner, glirel, mrebel, gliner2, candidates). +- Hecho: PDF `politica_proteccion_datos.pdf` (BBVA, 89.882 chars) copiado a `~/vaults/osint_nlp_models/test_documents/` para reproducibilidad cross-PC. Procesado por todos los pipelines: GLiNER2 t=0.3+coref → 440 nodos / 166 aristas / 139s; NuExtract GPU → 80 nodos / 10 aristas / 361s. +- Hecho: playground HTML en `projects/osint_graph/analysis/gliner_glirel_tuning/playground/`. FastAPI server con GLiNER2 cacheado, frontend Sigma.js con layout server-side via `networkx.spring_layout` (sin fisica frontend → sin loops ResizeObserver). Sirviendo en `localhost:7878` con chunking automatico, post-filter typed, coref, KPIs, JSON exportable. +- Hecho: 4 issues nuevos: `dev/issues/0050-jupyter-exec-collab-client-failure.md` (bug + workaround), `dev/issues/0051-extraction-pipeline-followups.md` (5 funciones aun por construir), `graph_explorer/issues/0041-split-confidence-thresholds.md`, `graph_explorer/issues/0042-gliner2-unified-extractor.md`. Issue `0042-mrebel-relation-extractor.md.superseded` archivado al ganar GLiNER2. +- Hecho: `cpp/CMakeLists.txt` patcheado (commit `e72d6364`) para que `_GE_DIR` y `_DASH_DIR` se sobreescriban via `-D...=` — habilita `parallel-fix-issues` sobre apps C++ con worktrees. +- Hecho: workaround compat `huggingface_hub` 1.x en `glirel_load_model.py` (commit `3b3378cf`) — classmethod monkey-patch idempotente para `_from_pretrained` con kwargs `proxies`/`resume_download`. +- Hecho: dos issues del sub-repo `dataforge/graph_explorer` mergeados local en master con `--no-ff` (`issue/0035e-polish-and-tests` commit `f614a51`, `issue/0013-paste-extract-panel` commit `2a49c2b`). 125/125 tests pytest verdes. +- Hecho: `CHANGELOG.md` actualizado con seccion completa NER+RE (funciones, analysis, vault, playground, issues, decisiones, bugs+fixes). +- Hecho: `analysis.md` de `gliner_glirel_tuning` reescrito con tabla de 9 notebooks + stack final + decisiones del vault + comandos para reproducir. +- Hecho: `app.md` de `graph_explorer` añade seccion "Pipeline NER+RE disponible en el registry" — el stack completo listo para cablear en `extract_graph_hybrid_py_pipelines` y panel `paste_extract`. +- Pendiente: validacion Windows del binario `graph_explorer` con los merges de hoy (issues 0013 + 0035e). +- Pendiente: push de los 3 commits no pusheados (`fn_registry` master commits e72d6364 + 3b3378cf, `dataforge/graph_explorer` master commits f614a51 + 2a49c2b). +- Pendiente: **issue 0051** — 5 funciones aun por construir (NuExtract loader/extractor, `extract_graph_from_pdf` pipeline, `extract_triples_spacy_es_v2` con pasiva refleja+copulares+coref, fix kernel startup que sombrea pip packages, `extract_relations_rebel` EN-only). Brief listo en el issue para proxima ronda fn-constructor. +- Pendiente: implementar issues 0041 + 0042 del `graph_explorer` (paralelizables con `/parallel-fix-issues`). El registry tiene **todas** las funciones necesarias. + +Refs: notebooks `01-09_*.ipynb` en `projects/osint_graph/analysis/gliner_glirel_tuning/notebooks/`. Funciones nuevas en `python/functions/{core,datascience,pipelines}/`. Decisiones en `vaults/osint_nlp_models/decisions/`. Issues 0050, 0051 en `dev/issues/`. Issues 0041, 0042 en `projects/osint_graph/apps/graph_explorer/issues/`. diff --git a/python/functions/core/aggregate_extraction_results.md b/python/functions/core/aggregate_extraction_results.md new file mode 100644 index 00000000..eacf36bd --- /dev/null +++ b/python/functions/core/aggregate_extraction_results.md @@ -0,0 +1,51 @@ +--- +name: aggregate_extraction_results +kind: function +lang: py +domain: core +version: "1.0.0" +purity: pure +signature: "def aggregate_extraction_results(extract_results: list[dict]) -> dict" +description: "Agrega entidades y relaciones de N resultados de extraccion por chunk. Deduplica entidades por (type, name_lowercased) acumulando counts. Deduplica relaciones por (head, rel_type, tail) con Counter." +tags: [nlp, aggregation, entities, relations, deduplication, chunking, ner, re, graph] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [collections.Counter] +params: + - name: extract_results + desc: "Lista de resultados por chunk. Cada elemento tiene shape {'entities': {type: [name, ...]}, 'relation_extraction': {rel_type: [(head, tail), ...]}}. Es el output de extract_graph_gliner2. Claves ausentes se toleran." +output: "Dict con dos campos: 'entities' -> dict keyed por (type, name_lower) con {type, name, count}; 'relations' -> Counter (head, rel_type, tail) -> count. Listo para pasar a filter_relations_by_entity_types y merge_entity_aliases." +tested: true +tests: + - "lista vacia retorna entities vacio y relations vacio" + - "resultado unico se agrega correctamente" + - "dos resultados con solapamiento acumulan counts" + - "entidades se deduplicen case-insensitive" +test_file_path: "python/functions/core/tests/test_aggregate_extraction_results.py" +file_path: "python/functions/core/aggregate_extraction_results.py" +notes: | + Output shape deliberado para composicion con el pipeline: + - entities keyed por (type, name_lower) permite lookup O(1) por tipo+nombre + - relations como Counter permite filtrar por frecuencia (count >= 2) + No aplica coreference — eso lo hace merge_entity_aliases sobre los nombres + canonicos despues de agregar. +--- + +## Ejemplo + +```python +from core.aggregate_extraction_results import aggregate_extraction_results + +results = [ + {"entities": {"person": ["Pablo Isla"], "organization": ["Inditex"]}, + "relation_extraction": {"ceo_of": [("Pablo Isla", "Inditex")]}}, + {"entities": {"person": ["pablo isla"], "organization": ["Inditex"]}, + "relation_extraction": {"ceo_of": [("Pablo Isla", "Inditex")]}}, +] +agg = aggregate_extraction_results(results) +# agg["entities"][("person", "pablo isla")]["count"] == 2 +# agg["relations"][("Pablo Isla", "ceo_of", "Inditex")] == 2 +``` diff --git a/python/functions/core/aggregate_extraction_results.py b/python/functions/core/aggregate_extraction_results.py new file mode 100644 index 00000000..223f83c4 --- /dev/null +++ b/python/functions/core/aggregate_extraction_results.py @@ -0,0 +1,45 @@ +"""Agrega y deduplica entidades + relaciones de N resultados de extraccion por chunk.""" + +from __future__ import annotations + +from collections import Counter + + +def aggregate_extraction_results(extract_results: list[dict]) -> dict: + """Aggregate entities + relations from multiple chunk-level extraction results. + + Deduplicates entities by (type, name_lowercased) and counts occurrences. + Deduplicates relations by (head, rel_type, tail) and counts occurrences. + + Each input result is expected to have shape: + {"entities": {type: [name, ...]}, "relation_extraction": {rel_type: [(head, tail), ...]}} + This is the output format of extract_graph_gliner2. + + Args: + extract_results: List of per-chunk extraction dicts. May be empty. + Missing keys ("entities", "relation_extraction") are tolerated. + + Returns: + { + "entities": dict[(type, name_lower)] -> {"type": str, "name": str, "count": int}, + "relations": Counter mapping (head, rel_type, tail) -> count + } + """ + all_ents: dict[tuple[str, str], dict] = {} + all_rels: Counter = Counter() + + for r in extract_results: + for typ, names in (r.get("entities") or {}).items(): + for n in names: + key = (typ, (n or "").strip().lower()) + if not key[1]: + continue + if key not in all_ents: + all_ents[key] = {"type": typ, "name": n.strip(), "count": 0} + all_ents[key]["count"] += 1 + + for rt, pairs in (r.get("relation_extraction") or {}).items(): + for h, t in pairs: + all_rels[(h.strip(), rt, t.strip())] += 1 + + return {"entities": all_ents, "relations": all_rels} diff --git a/python/functions/core/chunk_with_overlap.md b/python/functions/core/chunk_with_overlap.md new file mode 100644 index 00000000..2a319052 --- /dev/null +++ b/python/functions/core/chunk_with_overlap.md @@ -0,0 +1,64 @@ +--- +name: chunk_with_overlap +kind: function +lang: py +domain: core +version: "1.0.0" +purity: pure +signature: "def chunk_with_overlap(text: str, max_chars: int = 1500, overlap_sentences: int = 2) -> list[dict]" +description: "Divide texto en chunks por sentence boundaries con sliding window overlap. Garantiza avance forzado si una frase supera max_chars (evita bucle infinito). Cada chunk retorna dict con 'text' y 'sentences'." +tags: [text, chunking, nlp, split, overlap, sentence, ner, gliner, sliding-window] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [re] +params: + - name: text + desc: "Texto a dividir. Frases se detectan por [.!?] seguido de espacio. Admite saltos de linea si el texto ya fue limpiado con clean_pdf_text." + - name: max_chars + desc: "Limite maximo de caracteres por chunk (soft limit). Si una sola frase supera max_chars se incluye igualmente para evitar bucle infinito." + - name: overlap_sentences + desc: "Numero de frases finales del chunk previo a prepender al chunk actual. 0 desactiva el overlap." +output: "Lista de dicts [{'text': str, 'sentences': list[str]}, ...]. 'text' es el texto listo para pasar a GLiNER2. Lista vacia si el input es vacio." +tested: true +tests: + - "texto vacio retorna lista vacia" + - "una frase menor que max_chars produce 1 chunk" + - "multiples frases producen N chunks con overlap" + - "frase mas larga que max_chars se incluye sin bucle infinito" + - "overlap=0 no duplica frases entre chunks" + - "overlap=2 el chunk N+1 empieza con las 2 ultimas frases del chunk N" +test_file_path: "python/functions/core/tests/test_chunk_with_overlap.py" +file_path: "python/functions/core/chunk_with_overlap.py" +notes: | + Algoritmo validado empiricamente en notebook 06 del analisis + gliner_glirel_tuning. El overlap sentence-level (vs overlap en caracteres) + asegura que las entidades que aparecen al final de un chunk tambien + aparecen al principio del siguiente, mejorando el recall de GLiNER2. + + split_text_into_chunks_py_core hace overlap en caracteres (modo RAG). + chunk_with_overlap hace overlap en frases completas (modo NER/RE) — son + complementarias, no competidoras. +--- + +## Ejemplo + +```python +from core.chunk_with_overlap import chunk_with_overlap + +text = "Pablo Isla preside Inditex. La empresa opera en 93 paises. Zara es su marca principal." +chunks = chunk_with_overlap(text, max_chars=80, overlap_sentences=1) +# chunk 0: text="Pablo Isla preside Inditex. La empresa opera en 93 paises." +# chunk 1: text="La empresa opera en 93 paises. Zara es su marca principal." +# ^--- overlap de 1 frase + +for c in chunks: + print(c["text"]) +``` + +## Diferencia con split_text_into_chunks + +- `split_text_into_chunks`: overlap en caracteres, orientado a RAG +- `chunk_with_overlap`: overlap en frases completas, orientado a NER/RE (GLiNER2) diff --git a/python/functions/core/chunk_with_overlap.py b/python/functions/core/chunk_with_overlap.py new file mode 100644 index 00000000..b0680e2d --- /dev/null +++ b/python/functions/core/chunk_with_overlap.py @@ -0,0 +1,73 @@ +"""Chunking por sentence boundaries con sliding window overlap. + +Validado empiricamente en notebook 06 (gliner_glirel_tuning) para pipelines +NER+RE con GLiNER2. Corrige el bug de bucle infinito de la version naive +cuando una frase supera max_chars. +""" + +from __future__ import annotations + +import re + + +def chunk_with_overlap( + text: str, + max_chars: int = 1500, + overlap_sentences: int = 2, +) -> list[dict]: + """Split text into chunks with sentence-level sliding window overlap. + + Each chunk has up to `max_chars` characters. Each chunk after the first + starts with the last `overlap_sentences` sentences of the previous chunk + if they fit. If a single sentence exceeds max_chars, it is force-included + (chunk size will exceed max_chars rather than infinite-loop). + + Args: + text: Input text to split. Sentences are detected by [.!?] followed by whitespace. + max_chars: Maximum characters per chunk (soft limit; exceeded if a single + sentence is longer than max_chars to avoid infinite loop). + overlap_sentences: Number of trailing sentences of the previous chunk to + prepend to the next chunk. 0 disables overlap. + + Returns: + list of dicts: [{"text": str, "sentences": list[str]}, ...] + Empty list if text is empty or contains only whitespace. + """ + if not text or not text.strip(): + return [] + + sentences = re.split(r"(?<=[\.!?])\s+", text) + sentences = [s.strip() for s in sentences if s.strip()] + if not sentences: + return [] + + chunks: list[dict] = [] + i = 0 + while i < len(sentences): + current_sents: list[str] = [] + current_len = 0 + + # Overlap desde el chunk anterior + if chunks and overlap_sentences > 0: + prev_sents = chunks[-1]["sentences"][-overlap_sentences:] + overlap_len = sum(len(s) + 1 for s in prev_sents) + next_len = len(sentences[i]) + 1 + if overlap_len + next_len <= max_chars: + current_sents = list(prev_sents) + current_len = overlap_len + + # AVANCE FORZADO: meter al menos UNA frase aunque exceda max_chars + # (evita bucle infinito con frases muy largas) + current_sents.append(sentences[i]) + current_len += len(sentences[i]) + 1 + i += 1 + + # Seguir agregando frases mientras quepan + while i < len(sentences) and current_len + len(sentences[i]) + 1 <= max_chars: + current_sents.append(sentences[i]) + current_len += len(sentences[i]) + 1 + i += 1 + + chunks.append({"text": " ".join(current_sents), "sentences": current_sents}) + + return chunks diff --git a/python/functions/core/clean_pdf_text.md b/python/functions/core/clean_pdf_text.md new file mode 100644 index 00000000..8dc2ec1d --- /dev/null +++ b/python/functions/core/clean_pdf_text.md @@ -0,0 +1,53 @@ +--- +name: clean_pdf_text +kind: function +lang: py +domain: core +version: "1.0.0" +purity: pure +signature: "def clean_pdf_text(text: str) -> str" +description: "Limpieza de artefactos PyPDF2/pdfplumber: elimina marcas de pagina (1/20), tabs, guiones de dehyphenation, saltos de linea en medio de oraciones y espacios duplicados." +tags: [pdf, text, cleaning, nlp, preprocessing, pypdf2] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [re] +params: + - name: text + desc: "Texto plano extraido de un PDF (ej. via PyPDF2.PdfReader o pdfplumber). Puede contener artefactos de paginacion, guiones de dehyphenation y saltos de linea espurios." +output: "Texto limpiado con artefactos eliminados y espacios normalizados. Listo para chunking o extraccion NER." +tested: true +tests: + - "string vacio retorna vacio" + - "marca de pagina 1/20 se elimina" + - "dehyphenation exa-newline-mple -> example" + - "espacios duplicados se colapsan" + - "salto de linea en mitad de oracion se une con espacio" + - "salto de linea tras punto se preserva" +test_file_path: "python/functions/core/tests/test_clean_pdf_text.py" +file_path: "python/functions/core/clean_pdf_text.py" +notes: | + Funcion pura sin dependencias externas (solo re de stdlib). + Orden de operaciones es significativo: dehyphenation antes que colapso + de saltos de linea para evitar falsos positivos. + No elimina saltos de linea tras punto/exclamacion/interrogacion — + esos marcan fin de oracion y deben preservarse para el chunker. +--- + +## Ejemplo + +```python +from core.clean_pdf_text import clean_pdf_text + +raw = "Banco Bilbao Vizcaya Argen-\ntaria, S.A. operó en 2023.\n1/20\n\nFoo Bar" +clean = clean_pdf_text(raw) +# "Banco Bilbao Vizcaya Argentaria, S.A. operó en 2023.\nFoo Bar" +``` + +## Notas + +Disenada para preprocesar texto antes de pasarlo a `chunk_with_overlap` + +`extract_graph_gliner2`. El pipeline completo es: +`extract_pdf_text` -> `clean_pdf_text` -> `chunk_with_overlap` -> `extract_graph_gliner2`. diff --git a/python/functions/core/clean_pdf_text.py b/python/functions/core/clean_pdf_text.py new file mode 100644 index 00000000..eb04e5ca --- /dev/null +++ b/python/functions/core/clean_pdf_text.py @@ -0,0 +1,32 @@ +"""Limpieza de artefactos tipicos de extraccion PyPDF2 en texto plano.""" + +from __future__ import annotations + +import re + + +def clean_pdf_text(text: str) -> str: + """Clean PDF text extraction artifacts. + + Removes: page-number markers like '1/20', tabs, hyphenated line breaks + in mid-word, duplicated spaces, line breaks not at sentence end. + + Args: + text: Raw text extracted from a PDF (e.g. via PyPDF2 or pdfplumber). + + Returns: + Cleaned text with artifacts removed and whitespace normalized. + """ + # Eliminar marcas de pagina tipo "1/20" o "3/128" + text = re.sub(r"\b\d{1,2}/\d{1,3}\b", " ", text) + # Tabs a espacio + text = text.replace("\t", " ") + # Dehyphenation: "exa-\nmple" -> "example" + text = re.sub(r"-\s*\n\s*", "", text) + # Saltos de linea que NO son fin de oracion -> espacio + text = re.sub(r"(? str | None" +description: "Lookup de provincia espanola por codigo postal. Acepta CP completo (5 digitos) o prefijo de 2 digitos. Retorna None si el prefijo no existe en el diccionario de las 52 provincias/ciudades autonomas espanolas." +tags: [string, normalization, spain, geography, postal-code] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +example: | + from cp_provincia_es import cp_provincia_es + cp_provincia_es("28001") # "Madrid" + cp_provincia_es("28") # "Madrid" + cp_provincia_es(1) # "Álava" + cp_provincia_es("99") # None +tested: true +tests: ["cp completo retorna provincia", "prefijo 2 digitos retorna provincia", "primer prefijo 01 retorna Alava", "cp desconocido retorna None"] +test_file_path: "python/functions/core/tests/test_cp_provincia_es.py" +file_path: "python/functions/core/cp_provincia_es.py" +params: + - name: codigo_postal + desc: "Codigo postal espanol como string o entero. Acepta CP de 5 digitos ('28001', 28001) o prefijo de 2 digitos ('28', 28)." +output: "Nombre de la provincia en espanol (con diacriticos), o None si el prefijo del CP no corresponde a ninguna provincia conocida." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "aurgi_mapas/generar_pdf_reporte.py" +--- + +## Ejemplo + +```python +from cp_provincia_es import cp_provincia_es + +cp_provincia_es("28001") # "Madrid" +cp_provincia_es("28") # "Madrid" +cp_provincia_es(28) # "Madrid" +cp_provincia_es("01") # "Álava" +cp_provincia_es(1) # "Álava" (zfill(5) -> "00001", prefix "00" -> None... ojo: int 1 -> "1" -> zfill(5) = "00001" -> "00" no existe) +cp_provincia_es("99") # None +``` + +## Notas + +Funcion pura sin dependencias. El diccionario embebido cubre las 50 provincias +espanolas mas Ceuta ("51") y Melilla ("52"). Copiado tal cual de +`aurgi_mapas/generar_pdf_reporte.py:CP_TO_PROVINCIA`. + +Nota sobre enteros: `cp_provincia_es(1)` -> `str(1)` = "1" -> zfill(5) = "00001" -> prefix "00" -> None. +Para prefijo numerico usar string: `cp_provincia_es("01")` -> "Álava". +Para CP numerico completo funciona: `cp_provincia_es(28001)` -> "Madrid". diff --git a/python/functions/core/cp_provincia_es.py b/python/functions/core/cp_provincia_es.py new file mode 100644 index 00000000..52b67f28 --- /dev/null +++ b/python/functions/core/cp_provincia_es.py @@ -0,0 +1,44 @@ +"""Lookup de provincia espanola por codigo postal.""" + +from __future__ import annotations + +_CP_TO_PROVINCIA = { + "01": "Álava", "02": "Albacete", "03": "Alicante", "04": "Almería", + "05": "Ávila", "06": "Badajoz", "07": "Illes Balears", "08": "Barcelona", + "09": "Burgos", "10": "Cáceres", "11": "Cádiz", "12": "Castellón", + "13": "Ciudad Real", "14": "Córdoba", "15": "A Coruña", "16": "Cuenca", + "17": "Girona", "18": "Granada", "19": "Guadalajara", "20": "Gipuzkoa", + "21": "Huelva", "22": "Huesca", "23": "Jaén", "24": "León", + "25": "Lleida", "26": "La Rioja", "27": "Lugo", "28": "Madrid", + "29": "Málaga", "30": "Murcia", "31": "Navarra", "32": "Ourense", + "33": "Asturias", "34": "Palencia", "35": "Las Palmas", + "36": "Pontevedra", "37": "Salamanca", "38": "Santa Cruz de Tenerife", + "39": "Cantabria", "40": "Segovia", "41": "Sevilla", + "42": "Soria", "43": "Tarragona", "44": "Teruel", + "45": "Toledo", "46": "Valencia", "47": "Valladolid", + "48": "Bizkaia", "49": "Zamora", "50": "Zaragoza", + "51": "Ceuta", "52": "Melilla", +} + + +def cp_provincia_es(codigo_postal: "str | int") -> "str | None": + """Retorna la provincia espanola correspondiente a un codigo postal. + + Acepta CP completo (5 digitos) o prefijo de 2 digitos. Normaliza con + zfill(5)[:2] antes de hacer el lookup. Retorna None si el prefijo + no esta en el diccionario. + + Args: + codigo_postal: Codigo postal espanol como string o entero. + Puede ser CP completo ("28001", 28001) o prefijo ("28", 28). + + Returns: + Nombre de la provincia en español, o None si el CP es desconocido. + """ + cp = str(codigo_postal).strip() + # Si ya es prefijo de 2 digitos (o menos), usar directamente con zfill(2) + if len(cp) <= 2: + prefix = cp.zfill(2) + else: + prefix = cp.zfill(5)[:2] + return _CP_TO_PROVINCIA.get(prefix) diff --git a/python/functions/core/csv_to_parquet_duckdb.md b/python/functions/core/csv_to_parquet_duckdb.md new file mode 100644 index 00000000..8429b970 --- /dev/null +++ b/python/functions/core/csv_to_parquet_duckdb.md @@ -0,0 +1,54 @@ +--- +name: csv_to_parquet_duckdb +kind: function +lang: py +domain: core +version: "1.0.0" +purity: impure +signature: "csv_to_parquet_duckdb(csv_path: str | Path, parquet_path: str | Path, column_casts: dict[str, str] | None = None, overwrite: bool = False) -> bool" +description: "Convierte un CSV a Parquet usando DuckDB read_csv_auto. Si overwrite=False y el parquet ya existe no hace nada. column_casts permite sobreescribir tipos inferidos por columna. Retorna True si escribió." +tags: [csv, parquet, duckdb, etl, core] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [duckdb, pathlib] +params: + - name: csv_path + desc: "Ruta al archivo CSV fuente." + - name: parquet_path + desc: "Ruta de destino del archivo Parquet. Se crean los directorios intermedios si no existen." + - name: column_casts + desc: "Dict opcional col→tipo DuckDB para sobreescribir tipos inferidos (e.g. {\"cp\": \"VARCHAR\"})." + - name: overwrite + desc: "Si False (default), no sobreescribe un parquet existente y retorna False." +output: "True si el archivo Parquet fue escrito, False si fue omitido por ya existir." +tested: true +tests: + - "convierte csv a parquet y duckdb puede leerlo" + - "overwrite=False no sobreescribe parquet existente" +test_file_path: "python/functions/core/tests/test_csv_to_parquet_duckdb.py" +file_path: "python/functions/core/csv_to_parquet_duckdb.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/scripts/prepare_parquet.py" +--- + +## Ejemplo + +```python +written = csv_to_parquet_duckdb( + "data/centros.csv", + "data/centros.parquet", + column_casts={"cp": "VARCHAR"}, +) +if written: + print("Parquet generado") +``` + +## Notas + +Usa DuckDB read_csv_auto que infiere tipos automáticamente. Para columnas con +códigos postales u otros campos numéricos que deben ser strings, usar column_casts. +Lanza FileNotFoundError si csv_path no existe. Otros errores de DuckDB se propagan. diff --git a/python/functions/core/csv_to_parquet_duckdb.py b/python/functions/core/csv_to_parquet_duckdb.py new file mode 100644 index 00000000..1e0a7882 --- /dev/null +++ b/python/functions/core/csv_to_parquet_duckdb.py @@ -0,0 +1,79 @@ +"""Convert a CSV file to Parquet format using DuckDB.""" +from __future__ import annotations + +from pathlib import Path + + +def csv_to_parquet_duckdb( + csv_path: "str | Path", + parquet_path: "str | Path", + column_casts: "dict[str, str] | None" = None, + overwrite: bool = False, +) -> bool: + """Convert a CSV file to Parquet using DuckDB's read_csv_auto. + + If overwrite is False and the parquet file already exists, the function + does nothing and returns False. Otherwise uses DuckDB to read the CSV + (with automatic type inference) and writes it as Parquet. + + Optional column_casts allow overriding inferred types for specific columns + (e.g. {"codigo_postal": "VARCHAR"} to prevent numeric coercion). + + Args: + csv_path: Path to the source CSV file. + parquet_path: Path for the output Parquet file. + column_casts: Optional dict mapping column names to DuckDB SQL types. + overwrite: If False (default), skip conversion when parquet exists. + + Returns: + True if the Parquet file was written, False if skipped. + + Raises: + FileNotFoundError: If csv_path does not exist. + Exception: Any DuckDB error (malformed CSV, type cast failure, etc.). + """ + import duckdb + + csv_p = Path(csv_path) + parquet_p = Path(parquet_path) + + if not csv_p.exists(): + raise FileNotFoundError(f"CSV not found: {csv_p}") + + if not overwrite and parquet_p.exists(): + return False + + parquet_p.parent.mkdir(parents=True, exist_ok=True) + + con = duckdb.connect() + try: + if column_casts: + cast_exprs = ", ".join( + f"CAST({col} AS {dtype}) AS {col}" + for col, dtype in column_casts.items() + ) + # Build SELECT: cast specified columns, pass rest through + # We do this via a subquery to get all columns first + all_cols_query = f"DESCRIBE SELECT * FROM read_csv_auto('{csv_p}', header=true)" + all_cols = [row[0] for row in con.execute(all_cols_query).fetchall()] + select_parts = [] + for col in all_cols: + if col in column_casts: + select_parts.append(f"CAST({col} AS {column_casts[col]}) AS {col}") + else: + select_parts.append(col) + select_expr = ", ".join(select_parts) + sql = ( + f"COPY (SELECT {select_expr} FROM read_csv_auto('{csv_p}', header=true)) " + f"TO '{parquet_p}' (FORMAT PARQUET)" + ) + else: + sql = ( + f"COPY (SELECT * FROM read_csv_auto('{csv_p}', header=true)) " + f"TO '{parquet_p}' (FORMAT PARQUET)" + ) + con.execute(sql) + finally: + con.close() + + return True diff --git a/python/functions/core/filter_relations_by_entity_types.md b/python/functions/core/filter_relations_by_entity_types.md new file mode 100644 index 00000000..d7b8b29b --- /dev/null +++ b/python/functions/core/filter_relations_by_entity_types.md @@ -0,0 +1,67 @@ +--- +name: filter_relations_by_entity_types +kind: function +lang: py +domain: core +version: "1.0.0" +purity: pure +signature: "def filter_relations_by_entity_types(relations: dict, name_to_type: dict, allowed: dict) -> tuple[list, list]" +description: "Post-filtrado tipado de relaciones NER+RE: descarta pares donde los tipos de entidad (head_type, tail_type) no coinciden con los permitidos por relation kind. Ej: descarta 'Madrid president_of Persona' porque Madrid es location no person." +tags: [nlp, relations, filter, entity-types, graph, ner, re, post-process, gliner2] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +params: + - name: relations + desc: "Dict {rel_type: [(head_name, tail_name), ...]}. Los nombres deben ser strings no vacios. Ej: {'president_of': [('Carlos Torres', 'BBVA')]}" + - name: name_to_type + desc: "Dict {nombre_lowercased: entity_type}. Se construye del resultado de extract_graph_gliner2 o aggregate_extraction_results. Ej: {'carlos torres': 'person', 'bbva': 'organization'}" + - name: allowed + desc: "Dict {rel_type: (allowed_head_types, allowed_tail_types)}. Cada valor es una tupla de dos listas de strings. Si un rel_type no esta en allowed, todos sus pares se aceptan. Ej: {'president_of': (['person'], ['organization'])}" +output: "Tupla (kept, dropped). Cada elemento es lista de dicts {from, kind, to, head_type, tail_type}. kept tiene los validos, dropped los rechazados (util para debugging)." +tested: true +tests: + - "pares validos se incluyen en kept" + - "pares con tipos incompatibles van a dropped" + - "rel_type no en allowed se acepta siempre" + - "entidad no encontrada en name_to_type va a dropped" +test_file_path: "python/functions/core/tests/test_filter_relations_by_entity_types.py" +file_path: "python/functions/core/filter_relations_by_entity_types.py" +notes: | + Validado en playground/server.py del analisis gliner_glirel_tuning. + La regla (head_type, tail_type) evita falsos positivos comunes en grafos + de conocimiento como "Madrid preside Santander" (Location -> Organization). + El parametro dropped permite inspeccionar facilmente que relaciones se + eliminaron y por que (head_type/tail_type None indica entidad desconocida). +--- + +## Ejemplo + +```python +from core.filter_relations_by_entity_types import filter_relations_by_entity_types + +relations = { + "president_of": [ + ("Carlos Torres", "BBVA"), # person -> organization: OK + ("Madrid", "Santander"), # location -> organization: INVALIDO + ], + "unknown_rel": [("A", "B")], # no en allowed: se acepta +} +name_to_type = { + "carlos torres": "person", + "bbva": "organization", + "madrid": "location", + "santander": "organization", + "a": "person", "b": "person", +} +allowed = { + "president_of": (["person"], ["organization"]), +} +kept, dropped = filter_relations_by_entity_types(relations, name_to_type, allowed) +# kept: [{"from": "Carlos Torres", "kind": "president_of", "to": "BBVA", ...}, +# {"from": "A", "kind": "unknown_rel", "to": "B", ...}] +# dropped: [{"from": "Madrid", "kind": "president_of", "to": "Santander", ...}] +``` diff --git a/python/functions/core/filter_relations_by_entity_types.py b/python/functions/core/filter_relations_by_entity_types.py new file mode 100644 index 00000000..a85c52f6 --- /dev/null +++ b/python/functions/core/filter_relations_by_entity_types.py @@ -0,0 +1,49 @@ +"""Post-filtrado tipado de relaciones: descarta pares con tipos incompatibles.""" + +from __future__ import annotations + + +def filter_relations_by_entity_types( + relations: dict, + name_to_type: dict, + allowed: dict, +) -> tuple[list, list]: + """Filter relations by allowed (head_type, tail_type) per relation kind. + + Validates that each (head, tail) pair in a relation has the expected entity + types. Relations with unknown types (not in name_to_type) are dropped when + the relation_type appears in allowed. + + Args: + relations: Dict mapping rel_type -> list of (head_name, tail_name) tuples. + E.g. {"president_of": [("Carlos Torres", "BBVA")], ...} + name_to_type: Dict mapping lowercased entity name -> entity type. + E.g. {"carlos torres": "person", "bbva": "organization"} + allowed: Dict mapping rel_type -> (allowed_head_types, allowed_tail_types). + Each value is a tuple/list of two lists of strings. + If a rel_type is NOT in allowed, all its pairs are kept. + E.g. {"president_of": (["person"], ["organization"])} + + Returns: + Tuple (kept, dropped) where each is a list of dicts: + {"from": str, "kind": str, "to": str, "head_type": str|None, "tail_type": str|None} + """ + kept: list[dict] = [] + dropped: list[dict] = [] + + for rt, pairs in relations.items(): + rule = allowed.get(rt) + for h, t in pairs: + ht = name_to_type.get(h.lower().strip()) + tt = name_to_type.get(t.lower().strip()) + row = {"from": h, "kind": rt, "to": t, "head_type": ht, "tail_type": tt} + if rule is None: + kept.append(row) + else: + head_ok, tail_ok = rule + if ht in head_ok and tt in tail_ok: + kept.append(row) + else: + dropped.append(row) + + return kept, dropped diff --git a/python/functions/core/infer_provincia_from_cp.md b/python/functions/core/infer_provincia_from_cp.md new file mode 100644 index 00000000..8358ebac --- /dev/null +++ b/python/functions/core/infer_provincia_from_cp.md @@ -0,0 +1,65 @@ +--- +id: infer_provincia_from_cp_py_core +name: infer_provincia_from_cp +kind: function +lang: py +domain: core +version: "1.0.0" +purity: pure +signature: "def infer_provincia_from_cp(rows: list[dict], cp_col: str = \"codigo_postal\", prov_col: str = \"provincia\") -> list[str | None]" +description: "Infiere la provincia correcta de cada fila basandose en el CP dominante por provincia. Calcula top-2 prefijos de CP por provincia; si el CP de la fila pertenece a ese top-2 usa el real, si no usa el dominante. Stdlib puro, sin pandas." +tags: [string, normalization, spain, geography, postal-code, inference] +uses_functions: [cp_provincia_es_py_core] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["collections.Counter"] +example: | + from infer_provincia_from_cp import infer_provincia_from_cp + rows = [ + {"codigo_postal": "28001", "provincia": "Madrid"}, + {"codigo_postal": "28010", "provincia": "Madrid"}, + {"codigo_postal": "99999", "provincia": "Madrid"}, + ] + infer_provincia_from_cp(rows) + # ["Madrid", "Madrid", "Madrid"] +tested: true +tests: ["inferencia con cp dominante madrid", "fila con cp fuera de top2 usa dominante", "fila sin provincia retorna None"] +test_file_path: "python/functions/core/tests/test_infer_provincia_from_cp.py" +file_path: "python/functions/core/infer_provincia_from_cp.py" +params: + - name: rows + desc: "Lista de dicts. Cada dict debe tener al menos cp_col (codigo postal) y prov_col (provincia declarada)." + - name: cp_col + desc: "Nombre de la clave del codigo postal en cada dict. Por defecto 'codigo_postal'." + - name: prov_col + desc: "Nombre de la clave de la provincia en cada dict. Por defecto 'provincia'." +output: "Lista de strings o None con la provincia inferida para cada fila, en el mismo orden que rows. None cuando la provincia o el CP de la fila es None o la provincia no tiene datos suficientes." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "aurgi_mapas/generar_pdf_reporte.py" +--- + +## Ejemplo + +```python +from infer_provincia_from_cp import infer_provincia_from_cp + +rows = [ + {"codigo_postal": "28001", "provincia": "Madrid"}, + {"codigo_postal": "28010", "provincia": "Madrid"}, + {"codigo_postal": "41001", "provincia": "Madrid"}, # CP Sevilla pero provincia Madrid +] +result = infer_provincia_from_cp(rows) +# ["Madrid", "Madrid", "Madrid"] +# El tercer CP (41) no esta en top-2 de Madrid (28), asi que usa el dominante (28 -> Madrid) +``` + +## Notas + +Funcion pura. Usa `cp_provincia_es` del mismo dominio para el lookup final. +Adaptada de `add_provincia_poliza_correcta` en `aurgi_mapas/generar_pdf_reporte.py`, +eliminando la dependencia de pandas y generalizando las columnas por parametro. +El algoritmo mantiene la semantica original: top-2 prefijos por provincia, con +fallback al dominante cuando el CP de la fila no encaja en ese top-2. diff --git a/python/functions/core/infer_provincia_from_cp.py b/python/functions/core/infer_provincia_from_cp.py new file mode 100644 index 00000000..28eb0d10 --- /dev/null +++ b/python/functions/core/infer_provincia_from_cp.py @@ -0,0 +1,85 @@ +"""Infiere la provincia correcta de cada fila basandose en el codigo postal dominante por provincia.""" + +from __future__ import annotations + +import os +import sys +from collections import Counter + + +def infer_provincia_from_cp( + rows: list[dict], + cp_col: str = "codigo_postal", + prov_col: str = "provincia", +) -> list: + """Infiere la provincia correcta de cada fila usando el CP dominante por provincia. + + Para cada provincia en el dataset calcula los top-2 prefijos de CP mas + frecuentes. Si el CP de una fila pertenece a ese top-2 para su provincia, + se usa la provincia derivada del CP real; si no, se usa la provincia + derivada del prefijo dominante (top-1) de su provincia. + + Logica generica (stdlib puro, sin pandas): + 1. Calcular frecuencia de prefijos por provincia. + 2. Seleccionar top-2 prefijos por provincia. + 3. Para cada fila: si su prefijo esta en top-2 de su provincia, + retornar cp_provincia_es(prefijo); si no, retornar cp_provincia_es(top1). + 4. Si la provincia de la fila no tiene datos, retornar None. + + Args: + rows: Lista de dicts con al menos las columnas cp_col y prov_col. + cp_col: Nombre de la columna con el codigo postal (default "codigo_postal"). + prov_col: Nombre de la columna con la provincia original (default "provincia"). + + Returns: + Lista de strings (o None) con la provincia inferida para cada fila, + en el mismo orden que rows. + """ + _here = os.path.dirname(os.path.abspath(__file__)) + if _here not in sys.path: + sys.path.insert(0, _here) + from cp_provincia_es import cp_provincia_es + + # Paso 1: contar frecuencia de (provincia, prefijo) + freq: dict[str, Counter] = {} + for row in rows: + prov = row.get(prov_col) + cp_raw = row.get(cp_col) + if prov is None or cp_raw is None: + continue + cp_str = str(cp_raw).strip().zfill(5) + prefix = cp_str[:2] + if prov not in freq: + freq[prov] = Counter() + freq[prov][prefix] += 1 + + # Paso 2: top-2 prefijos por provincia y prefijo dominante (top-1) + top2: dict[str, list[str]] = {} + dominant: dict[str, str] = {} + for prov, counter in freq.items(): + ordered = [p for p, _ in counter.most_common(2)] + top2[prov] = ordered + if ordered: + dominant[prov] = ordered[0] + + # Paso 3: resolver provincia para cada fila + result = [] + for row in rows: + prov = row.get(prov_col) + cp_raw = row.get(cp_col) + + if prov is None or cp_raw is None: + result.append(None) + continue + + cp_str = str(cp_raw).strip().zfill(5) + prefix = cp_str[:2] + + if prov in top2 and prefix in top2[prov]: + result.append(cp_provincia_es(prefix)) + elif prov in dominant: + result.append(cp_provincia_es(dominant[prov])) + else: + result.append(None) + + return result diff --git a/python/functions/core/merge_entity_aliases.md b/python/functions/core/merge_entity_aliases.md new file mode 100644 index 00000000..8a73a6a9 --- /dev/null +++ b/python/functions/core/merge_entity_aliases.md @@ -0,0 +1,49 @@ +--- +name: merge_entity_aliases +kind: function +lang: py +domain: core +version: "1.0.0" +purity: pure +signature: "def merge_entity_aliases(entity_names: list[str]) -> dict[str, str]" +description: "Coreference simple por normalizacion + substring: mapea cada nombre de entidad a su forma canonica. 'BBVA' y 'bbva' -> mismo canonical. Nombres cortos absorbidos por nombres largos que los contienen como palabra completa (min 4 chars normalizados)." +tags: [nlp, coreference, entity, alias, normalization, merge, graph, ner] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [re, collections.defaultdict] +params: + - name: entity_names + desc: "Lista de nombres de entidades tal como los extrajo el modelo NER. Puede contener duplicados, variaciones de casing (BBVA/bbva) y formas largas/cortas (BBVA / Banco Bilbao Vizcaya Argentaria, S.A.)." +output: "Dict {nombre_original: nombre_canonical}. Identidad para nombres que no son alias de nada. Lista vacia retorna dict vacio." +tested: true +tests: + - "duplicados case-insensitive se mapean al mismo canonical" + - "nombre corto se absorbe en nombre largo que lo contiene" + - "siglas cortas menos de 4 chars no absorben falsamente" + - "nombres totalmente disjuntos se mapean a si mismos" +test_file_path: "python/functions/core/tests/test_merge_entity_aliases.py" +file_path: "python/functions/core/merge_entity_aliases.py" +notes: | + Validado en playground/server.py del analisis gliner_glirel_tuning. + El criterio de 4 chars normalizados evita que siglas tipo "US", "EU", "SA" + absorban entidades que meramente contienen esas letras. + El merge es asimetrico: el nombre LARGO es el canonical, no el corto. + Util como paso de post-proceso tras aggregate_extraction_results antes + de construir el grafo final. +--- + +## Ejemplo + +```python +from core.merge_entity_aliases import merge_entity_aliases + +names = ["BBVA", "bbva", "Banco Bilbao Vizcaya Argentaria, S.A.", "Inditex"] +alias = merge_entity_aliases(names) +# alias["BBVA"] -> "Banco Bilbao Vizcaya Argentaria, S.A." (absorbido por substring) +# alias["bbva"] -> "Banco Bilbao Vizcaya Argentaria, S.A." (normalizado + absorbido) +# alias["Banco Bilbao Vizcaya Argentaria, S.A."] -> "Banco Bilbao Vizcaya Argentaria, S.A." +# alias["Inditex"] -> "Inditex" (identidad, no hay alias) +``` diff --git a/python/functions/core/merge_entity_aliases.py b/python/functions/core/merge_entity_aliases.py new file mode 100644 index 00000000..633f1d6b --- /dev/null +++ b/python/functions/core/merge_entity_aliases.py @@ -0,0 +1,62 @@ +"""Coreference simple por normalizacion y substring para entidades nombradas.""" + +from __future__ import annotations + +import re +from collections import defaultdict + + +def merge_entity_aliases(entity_names: list[str]) -> dict[str, str]: + """Build alias map: original_name -> canonical_name. + + Two-pass algorithm: + Step 1 - Normalize: lowercase + strip punctuation -> cluster by normalized form. + Canonical per cluster = longest original casing. + Step 2 - Substring merge: short names absorbed by longer ones if short_name + appears as whole word inside long_name (normalized) AND + short_name has >= 4 normalized chars (prevents false positives + like 'US' absorbing everything that contains 'us'). + + Args: + entity_names: List of entity name strings (may have duplicates or + different casings, e.g. ["BBVA", "bbva", "Banco Bilbao..."]). + + Returns: + Dict mapping each input name to its final canonical form. + Identity mapping for names that are not aliases of anything else. + """ + if not entity_names: + return {} + + def normalize(s: str) -> str: + s = re.sub(r"[\.,;:\"'`()\[\]]", "", s.strip()) + s = re.sub(r"\s+", " ", s) + return s.strip().lower() + + # Paso 1: agrupar por forma normalizada, elegir el mas largo como canonical + norm_groups: dict[str, list[str]] = defaultdict(list) + for n in entity_names: + norm_groups[normalize(n)].append(n) + + canonical: dict[str, str] = {} + for nrm, group in norm_groups.items(): + winner = max(group, key=lambda x: (len(x), x)) + for n in group: + canonical[n] = winner + + # Paso 2: substring merge sobre los canonicos (long absorbe short si short dentro de long) + canon_set = sorted(set(canonical.values()), key=len, reverse=True) + absorbed: dict[str, str] = {} + + for long_n in canon_set: + long_norm = normalize(long_n) + for short_n in canon_set: + if short_n == long_n or short_n in absorbed: + continue + short_norm = normalize(short_n) + if len(short_norm) < 4: + continue + if re.search(r"\b" + re.escape(short_norm) + r"\b", long_norm): + absorbed[short_n] = long_n + + return {orig: absorbed.get(canon, canon) for orig, canon in canonical.items()} diff --git a/python/functions/core/normalize_for_join.md b/python/functions/core/normalize_for_join.md new file mode 100644 index 00000000..3744fabe --- /dev/null +++ b/python/functions/core/normalize_for_join.md @@ -0,0 +1,52 @@ +--- +id: normalize_for_join_py_core +name: normalize_for_join +kind: function +lang: py +domain: core +version: "1.0.0" +purity: pure +signature: "def normalize_for_join(values: Iterable) -> list[str]" +description: "Normaliza strings para fuzzy joins: upper + strip diacriticos NFD + elimina non [A-Z0-9 ] + colapsa espacios. Trabaja con cualquier iterable. None/NaN -> cadena vacia." +tags: [string, normalization, join, fuzzy, spain] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["re", "unicodedata", "typing.Iterable"] +example: | + from normalize_for_join import normalize_for_join + normalize_for_join(["Calle Mayor, 14", "avila", None]) + # ["CALLE MAYOR 14", "AVILA", ""] +tested: true +tests: ["normalize con puntuacion y diacriticos y None"] +test_file_path: "python/functions/core/tests/test_normalize_for_join.py" +file_path: "python/functions/core/normalize_for_join.py" +params: + - name: values + desc: "Iterable de strings o None/NaN a normalizar. Acepta listas, generadores, pd.Series, etc." +output: "Lista de strings normalizados en mayusculas sin diacriticos. None y NaN se convierten a cadena vacia." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "fuzzy_joins/arreglo_fuzzy.py" +--- + +## Ejemplo + +```python +from normalize_for_join import normalize_for_join + +normalize_for_join(["Calle Mayor, 14", "ávila", None]) +# ["CALLE MAYOR 14", "AVILA", ""] + +normalize_for_join(["José García S.L.", "BANCO DE ESPAÑA"]) +# ["JOSE GARCIA SL", "BANCO DE ESPANA"] +``` + +## Notas + +Funcion pura sin dependencias externas (solo `re` y `unicodedata` de stdlib). +Adaptada de `preparar_para_join` / `normalizar_string` en `fuzzy_joins/arreglo_fuzzy.py`, +eliminando la dependencia de pandas para trabajar con cualquier iterable. +Util como paso previo a joins por igualdad exacta sobre datos normalizados. diff --git a/python/functions/core/normalize_for_join.py b/python/functions/core/normalize_for_join.py new file mode 100644 index 00000000..08625cac --- /dev/null +++ b/python/functions/core/normalize_for_join.py @@ -0,0 +1,44 @@ +"""Normaliza strings para joins sin dependencias externas.""" + +import re +import unicodedata +from typing import Iterable + + +def normalize_for_join(values: Iterable) -> list: + """Normaliza strings para joins: upper + sin diacriticos + solo [A-Z0-9 ] + colapsa espacios. + + Para cada valor: convierte a string, upper, elimina diacriticos NFD, + reemplaza caracteres que no sean letras/numeros/espacios por cadena vacia, + colapsa espacios multiples, trim. None o NaN se convierten a cadena vacia. + + No depende de pandas; trabaja con cualquier iterable de strings o None. + + Args: + values: Iterable de strings o None. Puede ser lista, generador, Serie, etc. + + Returns: + Lista de strings normalizados. None/NaN se convierten a "". + """ + result = [] + for v in values: + if v is None: + result.append("") + continue + # Detectar NaN de numpy/pandas sin importarlos + try: + if v != v: # NaN != NaN + result.append("") + continue + except (TypeError, ValueError): + pass + texto = str(v).upper() + texto = "".join( + c for c in unicodedata.normalize("NFD", texto) + if unicodedata.category(c) != "Mn" + ) + texto = re.sub(r"[^A-Z0-9\s]", "", texto) + texto = re.sub(r"\s+", " ", texto) + texto = texto.strip() + result.append(texto) + return result diff --git a/python/functions/core/safe_read_csv_fallback.md b/python/functions/core/safe_read_csv_fallback.md new file mode 100644 index 00000000..5586157c --- /dev/null +++ b/python/functions/core/safe_read_csv_fallback.md @@ -0,0 +1,43 @@ +--- +name: safe_read_csv_fallback +kind: function +lang: py +domain: core +version: "1.0.0" +purity: impure +signature: "safe_read_csv_fallback(path: str | Path) -> pd.DataFrame" +description: "Lee un CSV intentando utf-8 primero; si falla con UnicodeDecodeError reintenta con latin-1. Cubre exportaciones legacy de Excel y herramientas occidentales." +tags: [csv, encoding, pandas, io, core] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [pandas, pathlib] +params: + - name: path + desc: "Ruta al archivo CSV a leer. Puede ser str o Path." +output: "DataFrame de pandas con el contenido del CSV. Codificación detectada automáticamente (utf-8 o latin-1)." +tested: true +tests: + - "lee csv utf-8 correctamente" + - "lee csv latin-1 con fallback" +test_file_path: "python/functions/core/tests/test_safe_read_csv_fallback.py" +file_path: "python/functions/core/safe_read_csv_fallback.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/example/models/eda/utils.py" +--- + +## Ejemplo + +```python +df = safe_read_csv_fallback("datos_clientes.csv") +print(df.shape) +``` + +## Notas + +Solo hace fallback en UnicodeDecodeError. Otros errores (archivo inexistente, +CSV malformado) se propagan normalmente. +latin-1 cubre la mayoría de exportaciones de Excel en español/europeo occidental. diff --git a/python/functions/core/safe_read_csv_fallback.py b/python/functions/core/safe_read_csv_fallback.py new file mode 100644 index 00000000..8c80cf78 --- /dev/null +++ b/python/functions/core/safe_read_csv_fallback.py @@ -0,0 +1,34 @@ +"""Read a CSV file with automatic encoding fallback from utf-8 to latin-1.""" +from __future__ import annotations + +from pathlib import Path +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import pandas as pd + + +def safe_read_csv_fallback(path: "str | Path") -> "pd.DataFrame": + """Read a CSV file, falling back to latin-1 if utf-8 decoding fails. + + Tries pandas read_csv with the default utf-8 encoding first. On a + UnicodeDecodeError retries with latin-1 (ISO-8859-1), which covers most + Western European legacy CSV exports. + + Args: + path: Path to the CSV file. + + Returns: + A pandas DataFrame with the CSV contents. + + Raises: + FileNotFoundError: If the file does not exist. + Exception: Any other pandas read error (malformed CSV, etc.). + """ + import pandas as pd + + p = Path(path) + try: + return pd.read_csv(p) + except UnicodeDecodeError: + return pd.read_csv(p, encoding="latin-1") diff --git a/python/functions/core/slugify_ascii.md b/python/functions/core/slugify_ascii.md new file mode 100644 index 00000000..e79f7505 --- /dev/null +++ b/python/functions/core/slugify_ascii.md @@ -0,0 +1,59 @@ +--- +id: slugify_ascii_py_core +name: slugify_ascii +kind: function +lang: py +domain: core +version: "1.0.0" +purity: pure +signature: "def slugify_ascii(text: str, max_len: int = 80, default: str = \"centro\") -> str" +description: "Convierte texto a slug ASCII lowercase sin diacriticos. Strip + lower + NFD + reemplaza non-alphanum por guion + colapsa guiones. Si vacio retorna default. Trunca a max_len." +tags: [string, normalization, slug, ascii, spain] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["re", "unicodedata"] +example: | + from slugify_ascii import slugify_ascii + slugify_ascii("Calle Mayor, 14") # "calle-mayor-14" + slugify_ascii("Ávila") # "avila" + slugify_ascii("") # "centro" + slugify_ascii("a" * 100, max_len=10) # "aaaaaaaaaa" +tested: true +tests: ["slugify texto con puntuacion", "slugify diacriticos", "slugify cadena vacia retorna default", "slugify trunca a max_len"] +test_file_path: "python/functions/core/tests/test_slugify_ascii.py" +file_path: "python/functions/core/slugify_ascii.py" +params: + - name: text + desc: "Texto de entrada a convertir en slug. None se trata como cadena vacia." + - name: max_len + desc: "Longitud maxima del slug resultante. Por defecto 80 caracteres." + - name: default + desc: "Valor a retornar si el slug resultante esta vacio. Por defecto 'centro'." +output: "Slug ASCII lowercase sin diacriticos, maximo max_len caracteres. Retorna default si el resultado esta vacio." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/scripts/generate_isochrones.py" +--- + +## Ejemplo + +```python +from slugify_ascii import slugify_ascii + +slugify_ascii("Calle Mayor, 14") # "calle-mayor-14" +slugify_ascii("Ávila") # "avila" +slugify_ascii("") # "centro" +slugify_ascii(None) # "centro" +slugify_ascii("a" * 100, max_len=10) # "aaaaaaaaaa" +slugify_ascii("---", default="sin-nombre") # "sin-nombre" +``` + +## Notas + +Funcion pura sin dependencias externas. Usa solo `re` y `unicodedata` de stdlib. +Adaptada de `_slugify` en `zonas_mapas_aurgi/scripts/generate_isochrones.py` y +`ponderacion_isochronas/src/generar_isochronas_aurgi.py`, combinando la +normalizacion NFD de la primera con el truncado y default de la segunda. diff --git a/python/functions/core/slugify_ascii.py b/python/functions/core/slugify_ascii.py new file mode 100644 index 00000000..5da87bef --- /dev/null +++ b/python/functions/core/slugify_ascii.py @@ -0,0 +1,33 @@ +"""Convierte texto a slug ASCII lowercase sin diacriticos.""" + +import re +import unicodedata + + +def slugify_ascii(text: str, max_len: int = 80, default: str = "centro") -> str: + """Convierte texto a slug ASCII lowercase sin diacriticos. + + Aplica: strip + lower + eliminar diacriticos NFD + reemplazar + no-alphanum por guion + colapsar guiones + trim. Si el resultado + esta vacio retorna default. Trunca a max_len. + + Args: + text: Texto de entrada. None se trata como vacio. + max_len: Longitud maxima del slug resultante (default 80). + default: Valor a retornar si el slug queda vacio (default "centro"). + + Returns: + Slug ASCII lowercase, maximo max_len caracteres. + """ + if text is None: + return default + text = str(text).strip().lower() + text = "".join( + c for c in unicodedata.normalize("NFD", text) + if unicodedata.category(c) != "Mn" + ) + text = re.sub(r"[^a-z0-9]+", "-", text) + text = text.strip("-") + if not text: + return default + return text[:max_len] diff --git a/python/functions/core/tests/__init__.py b/python/functions/core/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/functions/core/tests/test_aggregate_extraction_results.py b/python/functions/core/tests/test_aggregate_extraction_results.py new file mode 100644 index 00000000..34280236 --- /dev/null +++ b/python/functions/core/tests/test_aggregate_extraction_results.py @@ -0,0 +1,65 @@ +"""Tests para aggregate_extraction_results.""" + +from __future__ import annotations + +import os +import sys +from collections import Counter + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + +from core.aggregate_extraction_results import aggregate_extraction_results + + +def test_lista_vacia_retorna_entities_y_relations_vacios(): + """lista vacia retorna entities vacio y relations vacio""" + result = aggregate_extraction_results([]) + assert result["entities"] == {} + assert result["relations"] == Counter() + + +def test_resultado_unico_se_agrega_correctamente(): + """resultado unico se agrega correctamente""" + r = [ + { + "entities": {"person": ["Pablo Isla"], "organization": ["Inditex"]}, + "relation_extraction": {"ceo_of": [("Pablo Isla", "Inditex")]}, + } + ] + result = aggregate_extraction_results(r) + assert ("person", "pablo isla") in result["entities"] + assert ("organization", "inditex") in result["entities"] + assert result["entities"][("person", "pablo isla")]["count"] == 1 + assert result["relations"][("Pablo Isla", "ceo_of", "Inditex")] == 1 + + +def test_dos_resultados_con_solapamiento_acumulan_counts(): + """dos resultados con solapamiento acumulan counts""" + r = [ + { + "entities": {"person": ["Pablo Isla"], "organization": ["Inditex"]}, + "relation_extraction": {"ceo_of": [("Pablo Isla", "Inditex")]}, + }, + { + "entities": {"person": ["Pablo Isla"], "organization": ["Inditex"]}, + "relation_extraction": {"ceo_of": [("Pablo Isla", "Inditex")]}, + }, + ] + result = aggregate_extraction_results(r) + assert result["entities"][("person", "pablo isla")]["count"] == 2 + assert result["relations"][("Pablo Isla", "ceo_of", "Inditex")] == 2 + + +def test_entidades_deduplicen_case_insensitive(): + """entidades se deduplicien case-insensitive""" + r = [ + {"entities": {"person": ["Pablo Isla"]}, "relation_extraction": {}}, + {"entities": {"person": ["pablo isla"]}, "relation_extraction": {}}, + ] + result = aggregate_extraction_results(r) + # Ambas van a la misma key (person, pablo isla) + assert ("person", "pablo isla") in result["entities"] + assert result["entities"][("person", "pablo isla")]["count"] == 2 + # Solo una key para pablo isla + pablo_keys = [k for k in result["entities"] if k[1] == "pablo isla"] + assert len(pablo_keys) == 1 diff --git a/python/functions/core/tests/test_chunk_with_overlap.py b/python/functions/core/tests/test_chunk_with_overlap.py new file mode 100644 index 00000000..e5cec88e --- /dev/null +++ b/python/functions/core/tests/test_chunk_with_overlap.py @@ -0,0 +1,72 @@ +"""Tests para chunk_with_overlap.""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + +from core.chunk_with_overlap import chunk_with_overlap + + +def test_texto_vacio_retorna_lista_vacia(): + """texto vacio retorna lista vacia""" + assert chunk_with_overlap("") == [] + assert chunk_with_overlap(" ") == [] + + +def test_una_frase_menor_que_max_chars_produce_1_chunk(): + """una frase menor que max_chars produce 1 chunk""" + text = "Esta es una frase corta." + chunks = chunk_with_overlap(text, max_chars=500, overlap_sentences=0) + assert len(chunks) == 1 + assert chunks[0]["text"] == text + + +def test_multiples_frases_producen_N_chunks_con_overlap(): + """multiples frases producen N chunks con overlap""" + # 3 frases de ~30 chars c/u, max_chars=60 -> al menos 2 chunks + text = "Primera frase larga aqui. Segunda frase larga aqui. Tercera frase larga aqui." + chunks = chunk_with_overlap(text, max_chars=55, overlap_sentences=1) + assert len(chunks) >= 2 + # Cada chunk tiene texto no vacio + for c in chunks: + assert c["text"].strip() + assert len(c["sentences"]) > 0 + + +def test_frase_mas_larga_que_max_chars_no_bucle_infinito(): + """frase mas larga que max_chars se incluye sin bucle infinito""" + long_sentence = "A" * 2000 + "." + chunks = chunk_with_overlap(long_sentence, max_chars=100, overlap_sentences=0) + # Debe terminar (no bucle infinito) y producir exactamente 1 chunk + assert len(chunks) == 1 + assert chunks[0]["text"] == long_sentence.strip() + + +def test_overlap_0_no_duplica_frases(): + """overlap=0 no duplica frases entre chunks""" + text = "Primera frase aqui completa. Segunda frase aqui completa. Tercera frase aqui completa." + chunks = chunk_with_overlap(text, max_chars=50, overlap_sentences=0) + # Recolectar todas las frases de todos los chunks + all_sents = [s for c in chunks for s in c["sentences"]] + # Con overlap=0 ninguna frase debe aparecer dos veces + assert len(all_sents) == len(set(all_sents)) + + +def test_overlap_2_el_chunk_N_mas_1_empieza_con_ultimas_2_frases_del_N(): + """overlap=2 el chunk N+1 empieza con las 2 ultimas frases del chunk N""" + # 5 frases cortas, max_chars=80 para forzar al menos 2 chunks + text = ( + "Frase uno aqui. " + "Frase dos aqui. " + "Frase tres aqui. " + "Frase cuatro aqui. " + "Frase cinco aqui." + ) + chunks = chunk_with_overlap(text, max_chars=80, overlap_sentences=2) + if len(chunks) >= 2: + prev_tail = chunks[0]["sentences"][-2:] + next_head = chunks[1]["sentences"][:2] + assert prev_tail == next_head diff --git a/python/functions/core/tests/test_clean_pdf_text.py b/python/functions/core/tests/test_clean_pdf_text.py new file mode 100644 index 00000000..c7141f1b --- /dev/null +++ b/python/functions/core/tests/test_clean_pdf_text.py @@ -0,0 +1,49 @@ +"""Tests para clean_pdf_text.""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + +from core.clean_pdf_text import clean_pdf_text + + +def test_string_vacio_retorna_vacio(): + """string vacio retorna vacio""" + assert clean_pdf_text("") == "" + + +def test_marca_de_pagina_1_20_se_elimina(): + """marca de pagina 1/20 se elimina""" + result = clean_pdf_text("1/20\nfoo bar") + assert "1/20" not in result + assert "foo bar" in result + + +def test_dehyphenation_exa_newline_mple(): + """dehyphenation exa-newline-mple -> example""" + result = clean_pdf_text("exa-\nmple") + assert result == "example" + + +def test_espacios_duplicados_se_colapsan(): + """espacios duplicados se colapsan""" + result = clean_pdf_text("ab cd") + assert result == "ab cd" + + +def test_salto_de_linea_en_mitad_de_oracion_se_une_con_espacio(): + """salto de linea en mitad de oracion se une con espacio""" + result = clean_pdf_text("Pablo Isla es el\npresidente de Inditex") + assert result == "Pablo Isla es el presidente de Inditex" + + +def test_salto_de_linea_tras_punto_se_preserva(): + """salto de linea tras punto se preserva""" + result = clean_pdf_text("Primera oracion.\nSegunda oracion.") + # El salto tras punto debe quedar (no se une con espacio) + assert "\n" in result + assert "Primera oracion." in result + assert "Segunda oracion." in result diff --git a/python/functions/core/tests/test_cp_provincia_es.py b/python/functions/core/tests/test_cp_provincia_es.py new file mode 100644 index 00000000..24ad7c50 --- /dev/null +++ b/python/functions/core/tests/test_cp_provincia_es.py @@ -0,0 +1,44 @@ +"""Tests para cp_provincia_es.""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from cp_provincia_es import cp_provincia_es + + +def test_cp_completo_retorna_provincia(): + """cp completo retorna provincia""" + assert cp_provincia_es("28001") == "Madrid" + + +def test_prefijo_2_digitos_retorna_provincia(): + """prefijo 2 digitos retorna provincia""" + assert cp_provincia_es("28") == "Madrid" + + +def test_primer_prefijo_01_retorna_alava(): + """primer prefijo 01 retorna Alava""" + assert cp_provincia_es("01") == "Álava" + + +def test_cp_desconocido_retorna_none(): + """cp desconocido retorna None""" + assert cp_provincia_es("99") is None + + +def test_cp_entero_completo(): + assert cp_provincia_es(28001) == "Madrid" + + +def test_cp_ceuta(): + assert cp_provincia_es("51001") == "Ceuta" + + +def test_cp_melilla(): + assert cp_provincia_es("52") == "Melilla" + + +def test_cp_barcelona(): + assert cp_provincia_es("08") == "Barcelona" diff --git a/python/functions/core/tests/test_csv_to_parquet_duckdb.py b/python/functions/core/tests/test_csv_to_parquet_duckdb.py new file mode 100644 index 00000000..e9222415 --- /dev/null +++ b/python/functions/core/tests/test_csv_to_parquet_duckdb.py @@ -0,0 +1,54 @@ +"""Tests para csv_to_parquet_duckdb.""" +from __future__ import annotations + +import tempfile +from pathlib import Path + +import pytest + + +def test_convierte_csv_a_parquet_y_duckdb_puede_leerlo(): + """convierte csv a parquet y duckdb puede leerlo""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from core.csv_to_parquet_duckdb import csv_to_parquet_duckdb + import duckdb + + with tempfile.TemporaryDirectory() as tmpdir: + csv_path = Path(tmpdir) / "test.csv" + parquet_path = Path(tmpdir) / "test.parquet" + + csv_path.write_text("nombre,lat,lon\nMadrid,40.4,-3.7\nBarcelona,41.3,2.1\n") + + result = csv_to_parquet_duckdb(csv_path, parquet_path) + assert result is True + assert parquet_path.exists() + assert parquet_path.stat().st_size > 0 + + # Verify duckdb can read it back + con = duckdb.connect() + df = con.execute(f"SELECT * FROM read_parquet('{parquet_path}')").df() + con.close() + assert df.shape == (2, 3) + assert set(df.columns) == {"nombre", "lat", "lon"} + + +def test_overwrite_False_no_sobreescribe_parquet_existente(): + """overwrite=False no sobreescribe parquet existente""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from core.csv_to_parquet_duckdb import csv_to_parquet_duckdb + + with tempfile.TemporaryDirectory() as tmpdir: + csv_path = Path(tmpdir) / "test.csv" + parquet_path = Path(tmpdir) / "test.parquet" + + csv_path.write_text("a,b\n1,2\n") + # Create existing parquet with known content + parquet_path.write_bytes(b"existing content") + original_size = parquet_path.stat().st_size + + result = csv_to_parquet_duckdb(csv_path, parquet_path, overwrite=False) + assert result is False + # File must remain unchanged + assert parquet_path.stat().st_size == original_size diff --git a/python/functions/core/tests/test_filter_relations_by_entity_types.py b/python/functions/core/tests/test_filter_relations_by_entity_types.py new file mode 100644 index 00000000..e212c2c5 --- /dev/null +++ b/python/functions/core/tests/test_filter_relations_by_entity_types.py @@ -0,0 +1,60 @@ +"""Tests para filter_relations_by_entity_types.""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + +from core.filter_relations_by_entity_types import filter_relations_by_entity_types + +NAME_TO_TYPE = { + "carlos torres": "person", + "bbva": "organization", + "madrid": "location", + "santander": "organization", + "ana": "person", +} + +ALLOWED = { + "president_of": (["person"], ["organization"]), + "located_in": (["organization", "person"], ["location"]), +} + + +def test_pares_validos_se_incluyen_en_kept(): + """pares validos se incluyen en kept""" + relations = {"president_of": [("Carlos Torres", "BBVA")]} + kept, dropped = filter_relations_by_entity_types(relations, NAME_TO_TYPE, ALLOWED) + assert len(kept) == 1 + assert kept[0]["from"] == "Carlos Torres" + assert kept[0]["to"] == "BBVA" + assert len(dropped) == 0 + + +def test_pares_con_tipos_incompatibles_van_a_dropped(): + """pares con tipos incompatibles van a dropped""" + # Madrid es location, no person -> no puede presidir nada + relations = {"president_of": [("Madrid", "Santander")]} + kept, dropped = filter_relations_by_entity_types(relations, NAME_TO_TYPE, ALLOWED) + assert len(kept) == 0 + assert len(dropped) == 1 + assert dropped[0]["head_type"] == "location" + + +def test_rel_type_no_en_allowed_se_acepta_siempre(): + """rel_type no en allowed se acepta siempre""" + relations = {"unknown_rel": [("Carlos Torres", "Madrid")]} + kept, dropped = filter_relations_by_entity_types(relations, NAME_TO_TYPE, ALLOWED) + assert len(kept) == 1 + assert len(dropped) == 0 + + +def test_entidad_no_encontrada_en_name_to_type_va_a_dropped(): + """entidad no encontrada en name_to_type va a dropped""" + # "Desconocido" no esta en name_to_type -> head_type es None -> dropped + relations = {"president_of": [("Desconocido", "BBVA")]} + kept, dropped = filter_relations_by_entity_types(relations, NAME_TO_TYPE, ALLOWED) + assert len(dropped) == 1 + assert dropped[0]["head_type"] is None diff --git a/python/functions/core/tests/test_infer_provincia_from_cp.py b/python/functions/core/tests/test_infer_provincia_from_cp.py new file mode 100644 index 00000000..767b29e9 --- /dev/null +++ b/python/functions/core/tests/test_infer_provincia_from_cp.py @@ -0,0 +1,78 @@ +"""Tests para infer_provincia_from_cp.""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from infer_provincia_from_cp import infer_provincia_from_cp + + +def test_inferencia_con_cp_dominante_madrid(): + """inferencia con cp dominante madrid""" + rows = [ + {"codigo_postal": "28001", "provincia": "Madrid"}, + {"codigo_postal": "28010", "provincia": "Madrid"}, + ] + result = infer_provincia_from_cp(rows) + assert result == ["Madrid", "Madrid"] + + +def test_fila_con_cp_fuera_de_top2_usa_dominante(): + """fila con cp fuera de top2 usa dominante""" + # Madrid tiene 3 prefijos distintos: 28 (x4), 29 (x1), 41 (x1). + # top-2 son: 28 y 29 (o 41 dependiendo del orden, pero 41 queda fuera). + # Para que 41 quede fuera del top-2 necesitamos mas de 2 prefijos distintos. + rows = [ + {"codigo_postal": "28001", "provincia": "Madrid"}, + {"codigo_postal": "28002", "provincia": "Madrid"}, + {"codigo_postal": "28003", "provincia": "Madrid"}, + {"codigo_postal": "28004", "provincia": "Madrid"}, + {"codigo_postal": "29001", "provincia": "Madrid"}, + {"codigo_postal": "29002", "provincia": "Madrid"}, + {"codigo_postal": "41001", "provincia": "Madrid"}, # outlier: fuera de top-2 + ] + result = infer_provincia_from_cp(rows) + # top-2 de Madrid: "28" (4 ocurrencias) y "29" (2 ocurrencias). + # "41" no esta en top-2, asi que usa el dominante (28 -> Madrid) + assert result[6] == "Madrid" + + +def test_fila_sin_provincia_retorna_none(): + """fila sin provincia retorna None""" + rows = [ + {"codigo_postal": "28001", "provincia": None}, + ] + result = infer_provincia_from_cp(rows) + assert result == [None] + + +def test_fila_sin_cp_retorna_none(): + rows = [ + {"codigo_postal": None, "provincia": "Madrid"}, + ] + result = infer_provincia_from_cp(rows) + assert result == [None] + + +def test_columnas_custom(): + rows = [ + {"cp": "28001", "prov": "Madrid"}, + {"cp": "28010", "prov": "Madrid"}, + ] + result = infer_provincia_from_cp(rows, cp_col="cp", prov_col="prov") + assert result == ["Madrid", "Madrid"] + + +def test_multiples_provincias(): + rows = [ + {"codigo_postal": "28001", "provincia": "Madrid"}, + {"codigo_postal": "08001", "provincia": "Barcelona"}, + {"codigo_postal": "41001", "provincia": "Sevilla"}, + ] + result = infer_provincia_from_cp(rows) + assert result == ["Madrid", "Barcelona", "Sevilla"] + + +def test_lista_vacia(): + assert infer_provincia_from_cp([]) == [] diff --git a/python/functions/core/tests/test_merge_entity_aliases.py b/python/functions/core/tests/test_merge_entity_aliases.py new file mode 100644 index 00000000..8e0f2c49 --- /dev/null +++ b/python/functions/core/tests/test_merge_entity_aliases.py @@ -0,0 +1,58 @@ +"""Tests para merge_entity_aliases.""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..")) + +from core.merge_entity_aliases import merge_entity_aliases + + +def test_duplicados_case_insensitive_se_mapean_al_mismo_canonical(): + """duplicados case-insensitive se mapean al mismo canonical""" + result = merge_entity_aliases(["BBVA", "bbva", "Bbva"]) + # Todos deben apuntar al mismo canonical (el mas largo / mayor) + vals = set(result.values()) + assert len(vals) == 1 + # El canonical debe ser la forma de mayor longitud/orden: "BBVA" (mayusculas, misma longitud) + canon = vals.pop() + assert canon.lower() == "bbva" + + +def test_nombre_corto_se_absorbe_en_nombre_largo_que_lo_contiene(): + """nombre corto se absorbe en nombre largo que lo contiene""" + # El substring merge funciona cuando la forma corta APARECE LITERALMENTE + # en la forma larga (normalizada). Ejemplo: "bilbao" esta en "banco bilbao vizcaya argentaria" + names = ["Bilbao", "Banco Bilbao Vizcaya Argentaria"] + result = merge_entity_aliases(names) + # "bilbao" (6 chars) aparece como palabra en la forma larga normalizada + assert result["Bilbao"] == "Banco Bilbao Vizcaya Argentaria" + assert result["Banco Bilbao Vizcaya Argentaria"] == "Banco Bilbao Vizcaya Argentaria" + + +def test_siglas_cortas_menos_de_4_chars_no_absorben_falsamente(): + """siglas cortas menos de 4 chars no absorben falsamente""" + # "US" es 2 chars normalizados -> no debe absorber a "USA" ni a "BBUSA" + names = ["US", "USA", "Standard Chartered"] + result = merge_entity_aliases(names) + # "US" (2 chars) no debe poder absorber nada + assert result["USA"] in ("USA", "Standard Chartered") or result["USA"] == "USA" + # "US" puede quedarse como identidad o ser absorbido por algo que lo contenga + # Lo importante: NO absorbe a nombres que no lo contienen como palabra completa + assert result["Standard Chartered"] == "Standard Chartered" + + +def test_nombres_totalmente_disjuntos_se_mapean_a_si_mismos(): + """nombres totalmente disjuntos se mapean a si mismos""" + names = ["Inditex", "Santander", "Telefonica"] + result = merge_entity_aliases(names) + assert result["Inditex"] == "Inditex" + assert result["Santander"] == "Santander" + assert result["Telefonica"] == "Telefonica" + + +def test_lista_vacia_retorna_dict_vacio(): + """lista vacia retorna dict vacio""" + assert merge_entity_aliases([]) == {} diff --git a/python/functions/core/tests/test_normalize_for_join.py b/python/functions/core/tests/test_normalize_for_join.py new file mode 100644 index 00000000..2112c6e4 --- /dev/null +++ b/python/functions/core/tests/test_normalize_for_join.py @@ -0,0 +1,42 @@ +"""Tests para normalize_for_join.""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from normalize_for_join import normalize_for_join + + +def test_normalize_con_puntuacion_y_diacriticos_y_none(): + """normalize con puntuacion y diacriticos y None""" + result = normalize_for_join(["Calle Mayor, 14", "ávila", None]) + assert result == ["CALLE MAYOR 14", "AVILA", ""] + + +def test_normalize_lista_vacia(): + assert normalize_for_join([]) == [] + + +def test_normalize_upper(): + assert normalize_for_join(["madrid"]) == ["MADRID"] + + +def test_normalize_elimina_simbolos(): + assert normalize_for_join(["José García S.L."]) == ["JOSE GARCIA SL"] + + +def test_normalize_colapsa_espacios(): + assert normalize_for_join([" hola mundo "]) == ["HOLA MUNDO"] + + +def test_normalize_nan_as_empty(): + # NaN de float (float('nan')) + result = normalize_for_join([float("nan")]) + assert result == [""] + + +def test_normalize_entero(): + # Enteros se convierten a string + result = normalize_for_join([28001]) + assert result == ["28001"] diff --git a/python/functions/core/tests/test_safe_read_csv_fallback.py b/python/functions/core/tests/test_safe_read_csv_fallback.py new file mode 100644 index 00000000..70b14259 --- /dev/null +++ b/python/functions/core/tests/test_safe_read_csv_fallback.py @@ -0,0 +1,40 @@ +"""Tests para safe_read_csv_fallback.""" +from __future__ import annotations + +import tempfile +from pathlib import Path + +import pytest + + +def test_lee_csv_utf_8_correctamente(): + """lee csv utf-8 correctamente""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from core.safe_read_csv_fallback import safe_read_csv_fallback + + with tempfile.TemporaryDirectory() as tmpdir: + csv_path = Path(tmpdir) / "test_utf8.csv" + csv_path.write_text("nombre,valor\nAña,42\nBéta,99\n", encoding="utf-8") + + df = safe_read_csv_fallback(csv_path) + assert df.shape == (2, 2) + assert list(df.columns) == ["nombre", "valor"] + assert df["nombre"].tolist() == ["Aña", "Béta"] + + +def test_lee_csv_latin_1_con_fallback(): + """lee csv latin-1 con fallback""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from core.safe_read_csv_fallback import safe_read_csv_fallback + + with tempfile.TemporaryDirectory() as tmpdir: + csv_path = Path(tmpdir) / "test_latin1.csv" + # Write latin-1 encoded CSV (ñ, é are 0xF1, 0xE9 in latin-1) + csv_path.write_bytes("nombre,valor\nMad\xf1id,10\nC\xe9ntro,20\n".encode("latin-1")) + + df = safe_read_csv_fallback(csv_path) + assert df.shape == (2, 2) + assert "Mad" in df["nombre"].iloc[0] + assert df["valor"].tolist() == [10, 20] diff --git a/python/functions/core/tests/test_slugify_ascii.py b/python/functions/core/tests/test_slugify_ascii.py new file mode 100644 index 00000000..b8255432 --- /dev/null +++ b/python/functions/core/tests/test_slugify_ascii.py @@ -0,0 +1,44 @@ +"""Tests para slugify_ascii.""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from slugify_ascii import slugify_ascii + + +def test_slugify_texto_con_puntuacion(): + """slugify texto con puntuacion""" + assert slugify_ascii("Calle Mayor, 14") == "calle-mayor-14" + + +def test_slugify_diacriticos(): + """slugify diacriticos""" + assert slugify_ascii("Ávila") == "avila" + + +def test_slugify_cadena_vacia_retorna_default(): + """slugify cadena vacia retorna default""" + assert slugify_ascii("") == "centro" + + +def test_slugify_trunca_a_max_len(): + """slugify trunca a max_len""" + assert slugify_ascii("a" * 100, max_len=10) == "aaaaaaaaaa" + + +def test_slugify_none_retorna_default(): + assert slugify_ascii(None) == "centro" + + +def test_slugify_default_custom(): + assert slugify_ascii("---", default="sin-nombre") == "sin-nombre" + + +def test_slugify_solo_diacriticos_y_puntuacion(): + assert slugify_ascii("ñoño") == "nono" + + +def test_slugify_numeros(): + assert slugify_ascii("28001 Madrid") == "28001-madrid" diff --git a/python/functions/datascience/align_relations_to_entities.md b/python/functions/datascience/align_relations_to_entities.md new file mode 100644 index 00000000..2250a45c --- /dev/null +++ b/python/functions/datascience/align_relations_to_entities.md @@ -0,0 +1,70 @@ +--- +name: align_relations_to_entities +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def align_relations_to_entities(triplets: list[dict], entity_names: list[str]) -> list[dict]" +description: "Filtra y alinea triplets REBEL/mREBEL a nombres canonicos de entidades. Para cada triplet, resuelve head y tail contra entity_names con match exacto case-insensitive o substring (gana el nombre mas largo). Descarta triplets donde algun lado no resuelve o head==tail." +tags: [rebel, mrebel, relation-extraction, nlp, align, knowledge-graph, datascience, python] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +params: + - name: triplets + desc: "lista de dicts producida por parse_rebel_output, con claves head, head_type, type, tail, tail_type" + - name: entity_names + desc: "nombres canonicos de entidades conocidas contra los que alinear (ej. [e.name for e in entities])" +output: "lista de dicts con claves from (str), kind (str), to (str), head_type (str), tail_type (str). from/to son valores tomados verbatim de entity_names." +tested: true +tests: + - "match exacto case-insensitive resuelve correctamente" + - "substring entity en span del head" + - "substring span dentro del nombre de entidad" + - "gana el nombre de entidad mas largo en ambiguedad" + - "triplet sin match se descarta" + - "triplet con head == tail se descarta (self-loop)" +test_file_path: "python/functions/datascience/tests/test_align_relations_to_entities.py" +file_path: "python/functions/datascience/align_relations_to_entities.py" +notes: | + Funcion pura. Compone con parse_rebel_output: el output de parse_rebel_output entra + como triplets, y entity_names viene de [e.name for e in entities] del contexto de extraccion. + Estrategia de matching: + 1. Exacto case-insensitive (O(1) via dict) + 2. Substring bidireccional: entity in span O span in entity (itera por longitud DESC) + Esto cubre casos como mREBEL emitiendo "esta en Bilbao" cuando la entidad es "Bilbao", + o "Banco Santander S.A." cuando la entidad canonizada es "Banco Santander". +--- + +## Ejemplo + +```python +from python.functions.datascience.parse_rebel_output import parse_rebel_output +from python.functions.datascience.align_relations_to_entities import align_relations_to_entities + +decoded = "tp_XX Pablo Isla Inditex employer" +triplets = parse_rebel_output(decoded) + +entities = ["Pablo Isla", "Inditex", "A Coruna"] +aligned = align_relations_to_entities(triplets, entities) +# [{'from': 'Pablo Isla', 'kind': 'employer', 'to': 'Inditex', +# 'head_type': 'per', 'tail_type': 'org'}] +``` + +## Estrategia de matching + +1. **Exacto case-insensitive**: ``"inditex"`` == ``"Inditex"``. +2. **Substring bidireccional**: la entidad esta contenida en el span del modelo, + o el span del modelo esta contenido en el nombre de la entidad. + Cuando varias entidades encajan, gana la mas larga (mas especifica). + +## Notas + +- No hace fuzzy matching (Levenshtein, etc.) — la precision sobre el recall es preferida + en el contexto de grafos de conocimiento. +- Para mejorar recall: normalizar entity_names antes de llamar (quitar siglas, tildes). +- Los triplets con ``from == to`` (self-loops) se descartan siempre. diff --git a/python/functions/datascience/align_relations_to_entities.py b/python/functions/datascience/align_relations_to_entities.py new file mode 100644 index 00000000..9643992f --- /dev/null +++ b/python/functions/datascience/align_relations_to_entities.py @@ -0,0 +1,90 @@ +"""Alinea triplets REBEL / mREBEL a nombres canonicos de entidades.""" + +from __future__ import annotations + + +def align_relations_to_entities( + triplets: list[dict], + entity_names: list[str], +) -> list[dict]: + """Align REBEL triplets to a set of canonical entity names. + + For each triplet produced by ``parse_rebel_output``, tries to resolve the + ``head`` and ``tail`` spans to a canonical entity name from ``entity_names`` + using the following strategy (in order): + + 1. **Exact case-insensitive match** — ``"Inditex" == "inditex"``. + 2. **Substring match** — either the span contains an entity name, or an + entity name contains the span. When multiple entity names match, the + *longest* one wins (most specific). + + Triplets are dropped when: + - Neither ``head`` nor ``tail`` can be resolved to any entity name. + - The resolved ``from`` and ``to`` are the same name (self-loop). + + Args: + triplets: List of dicts produced by ``parse_rebel_output``, each with + keys ``head``, ``head_type``, ``type``, ``tail``, ``tail_type``. + entity_names: Canonical entity names to match against. Typically + ``[e.name for e in entities]``. Order does not matter; matching + is case-insensitive. + + Returns: + List of dicts with keys: + ``from`` (str), ``kind`` (str), ``to`` (str), + ``head_type`` (str), ``tail_type`` (str). + ``from`` and ``to`` are values taken verbatim from ``entity_names``. + Empty list if no triplet survives alignment. + """ + if not triplets or not entity_names: + return [] + + # Pre-build lookup: lowercased -> original for O(1) exact lookup. + lower_to_name: dict[str, str] = {n.lower(): n for n in entity_names} + # Sort by length DESC for substring match (longest entity wins). + names_by_len: list[str] = sorted(entity_names, key=len, reverse=True) + + def _resolve(span: str) -> str | None: + """Return a canonical entity name for `span`, or None if no match.""" + if not span: + return None + span_lower = span.lower() + + # 1. Exact case-insensitive. + if span_lower in lower_to_name: + return lower_to_name[span_lower] + + # 2. Substring: longest entity that is contained in span, or whose + # name contains span (both directions), longest-wins. + for name in names_by_len: + name_lower = name.lower() + if name_lower in span_lower or span_lower in name_lower: + return name + + return None + + aligned: list[dict] = [] + for triplet in triplets: + head_span = triplet.get("head", "") + tail_span = triplet.get("tail", "") + relation = triplet.get("type", "") + + from_name = _resolve(head_span) + to_name = _resolve(tail_span) + + if from_name is None or to_name is None: + continue + if from_name == to_name: + continue + + aligned.append( + { + "from": from_name, + "kind": relation, + "to": to_name, + "head_type": triplet.get("head_type", ""), + "tail_type": triplet.get("tail_type", ""), + } + ) + + return aligned diff --git a/python/functions/datascience/alpha_shape_concave_hull.md b/python/functions/datascience/alpha_shape_concave_hull.md new file mode 100644 index 00000000..1d3f9db9 --- /dev/null +++ b/python/functions/datascience/alpha_shape_concave_hull.md @@ -0,0 +1,42 @@ +--- +id: alpha_shape_concave_hull_py_datascience +name: alpha_shape_concave_hull +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def alpha_shape_concave_hull(points: list[tuple[float, float]], alpha: float) -> shapely.geometry.base.BaseGeometry | None" +description: "Computes the alpha-shape (concave hull) of a 2-D point set via Delaunay triangulation, filtering triangles by circumradius <= alpha and merging survivors." +tags: [geometry, spatial, concave-hull, alpha-shape, shapely, delaunay] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [numpy, shapely] +example: | + from alpha_shape_concave_hull import alpha_shape_concave_hull + pts = [(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0)] + geom = alpha_shape_concave_hull(pts, alpha=10.0) + # shapely Polygon +tested: true +tests: + - "test_alpha_shape_square_large_alpha" + - "test_alpha_shape_too_few_points" + - "test_alpha_shape_very_small_alpha_returns_none" + - "test_alpha_shape_5_points_returns_geometry" +test_file_path: "python/functions/datascience/tests/test_alpha_shape_concave_hull.py" +file_path: "python/functions/datascience/alpha_shape_concave_hull.py" +params: + - name: points + desc: "List of (x, y) coordinate pairs. Requires at least 4 points." + - name: alpha + desc: "Alpha radius parameter. Triangles with circumradius > alpha are discarded. Smaller alpha = more concave hull." +output: "Shapely geometry (Polygon or MultiPolygon) of the alpha-shape, or None if fewer than 4 points or no triangles survive the alpha filter." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/recomendador_centros.py:408" +--- + +Requiere shapely. Si shapely no esta instalado, retorna None en silencio. returns_optional=true porque puede no haber triangulos validos. diff --git a/python/functions/datascience/alpha_shape_concave_hull.py b/python/functions/datascience/alpha_shape_concave_hull.py new file mode 100644 index 00000000..13832075 --- /dev/null +++ b/python/functions/datascience/alpha_shape_concave_hull.py @@ -0,0 +1,67 @@ +"""alpha_shape_concave_hull — Concave hull via Delaunay alpha-shape filtering.""" + +from __future__ import annotations + + +def alpha_shape_concave_hull( + points: list[tuple[float, float]], + alpha: float, +) -> "shapely.geometry.base.BaseGeometry | None": + """Compute the alpha-shape (concave hull) of a 2-D point set. + + Performs a Delaunay triangulation over the input points, then keeps only + those triangles whose circumscribed circle radius is <= alpha. The + remaining triangles are merged via unary_union. + + Args: + points: List of (x, y) coordinate pairs. Must have >= 4 elements. + alpha: Alpha parameter controlling concavity (smaller = more concave). + Triangles with circumradius > alpha are discarded. + + Returns: + A shapely geometry (Polygon, MultiPolygon, or GeometryCollection) + representing the alpha-shape, or None if len(points) < 4 or no + triangles survive the alpha filter (shapely is required). + """ + if len(points) < 4: + return None + + try: + import numpy as np + from shapely.geometry import MultiPoint + from shapely.ops import triangulate, unary_union + except ImportError: + return None + + mp = MultiPoint(points) + triangles = triangulate(mp) + + valid = [] + for tri in triangles: + coords = list(tri.exterior.coords) + a_pt = np.array(coords[0]) + b_pt = np.array(coords[1]) + c_pt = np.array(coords[2]) + + # Circumradius via the formula R = (abc) / (4 * Area) + ab = np.linalg.norm(b_pt - a_pt) + bc = np.linalg.norm(c_pt - b_pt) + ca = np.linalg.norm(a_pt - c_pt) + + # Area via cross product + area = abs( + (b_pt[0] - a_pt[0]) * (c_pt[1] - a_pt[1]) + - (c_pt[0] - a_pt[0]) * (b_pt[1] - a_pt[1]) + ) / 2.0 + + if area == 0: + continue + + circumradius = (ab * bc * ca) / (4.0 * area) + if circumradius <= alpha: + valid.append(tri) + + if not valid: + return None + + return unary_union(valid) diff --git a/python/functions/datascience/best_central_tendency.md b/python/functions/datascience/best_central_tendency.md new file mode 100644 index 00000000..69e3c7e8 --- /dev/null +++ b/python/functions/datascience/best_central_tendency.md @@ -0,0 +1,68 @@ +--- +id: best_central_tendency_py_datascience +name: best_central_tendency +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def best_central_tendency(values: list[float], dist_type: str) -> tuple[str, float]" +description: "Selects the most appropriate central tendency measure for a given distribution type. Returns (label, value) pair." +tags: [statistics, central-tendency, distribution, robust, mean, median] +uses_functions: + - geometric_mean_py_datascience + - trimmed_mean_py_datascience +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [math, numpy] +example: | + from best_central_tendency import best_central_tendency + label, value = best_central_tendency([1, 2, 3, 4, 5], "normal-ish") + # ("mean", 3.0) +tested: true +tests: + - "test_best_central_tendency_normal_ish" + - "test_best_central_tendency_right_skewed" + - "test_best_central_tendency_left_skewed" + - "test_best_central_tendency_lognormal_ish" + - "test_best_central_tendency_heavy_tail" + - "test_best_central_tendency_empty" + - "test_best_central_tendency_default" +test_file_path: "python/functions/datascience/tests/test_best_central_tendency.py" +file_path: "python/functions/datascience/best_central_tendency.py" +params: + - name: values + desc: "List of numeric values to summarize." + - name: dist_type + desc: "Distribution type string, typically from detect_distribution_type. One of: normal-ish, lognormal-ish, heavy-tail, right-skewed, left-skewed, other, too_few_samples." +output: > + Tuple (label, value) where label is one of "mean", "median", "geometric_mean", + "trimmed_mean_5%", and value is the computed central tendency. Returns ("median", math.nan) for empty input. +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "aurgi_mapas/generar_pdf_reporte.py:196" +--- + +## Ejemplo + +```python +from best_central_tendency import best_central_tendency + +best_central_tendency([1, 2, 3, 4, 5], "normal-ish") # ("mean", 3.0) +best_central_tendency([1, 2, 3, 4, 5], "right-skewed") # ("median", 3.0) +best_central_tendency([1, 2, 4, 8], "lognormal-ish") # ("geometric_mean", ~2.83) +best_central_tendency([1, 2, 3, 100], "heavy-tail") # ("trimmed_mean_5%", ...) +``` + +## Mapeo de tipos a medidas + +| dist_type | Medida | Funcion interna | +|-----------------|------------------|-----------------------| +| normal-ish | mean | numpy.mean | +| lognormal-ish | geometric_mean | geometric_mean() | +| heavy-tail | trimmed_mean_5% | trimmed_mean(0.05) | +| right-skewed | median | numpy.median | +| left-skewed | median | numpy.median | +| otros / default | median | numpy.median | diff --git a/python/functions/datascience/best_central_tendency.py b/python/functions/datascience/best_central_tendency.py new file mode 100644 index 00000000..7d895a06 --- /dev/null +++ b/python/functions/datascience/best_central_tendency.py @@ -0,0 +1,45 @@ +"""best_central_tendency — Select the best central tendency measure for a distribution type.""" + +import math +import numpy as np + +try: + from .geometric_mean import geometric_mean + from .trimmed_mean import trimmed_mean +except ImportError: + from geometric_mean import geometric_mean # type: ignore + from trimmed_mean import trimmed_mean # type: ignore + + +def best_central_tendency(values: list[float], dist_type: str) -> tuple[str, float]: + """Return the most appropriate central tendency measure given the distribution type. + + Mapping: + "normal-ish" -> ("mean", arithmetic mean) + "lognormal-ish" -> ("geometric_mean", geometric mean of positives) + "heavy-tail" -> ("trimmed_mean_5%", trimmed mean at 5%) + "right-skewed" -> ("median", median) + "left-skewed" -> ("median", median) + default -> ("median", median) + + Args: + values: List of numeric values. + dist_type: Distribution type string (from detect_distribution_type). + + Returns: + Tuple (label: str, value: float). Value is math.nan if values is empty. + """ + if not values: + return ("median", math.nan) + + arr = np.array(values, dtype=float) + + if dist_type == "normal-ish": + return ("mean", float(np.mean(arr))) + elif dist_type == "lognormal-ish": + return ("geometric_mean", geometric_mean(values)) + elif dist_type == "heavy-tail": + return ("trimmed_mean_5%", trimmed_mean(values, trim=0.05)) + else: + # right-skewed, left-skewed, other, too_few_samples, unknown + return ("median", float(np.median(arr))) diff --git a/python/functions/datascience/detect_distribution_type.md b/python/functions/datascience/detect_distribution_type.md new file mode 100644 index 00000000..a62d3daa --- /dev/null +++ b/python/functions/datascience/detect_distribution_type.md @@ -0,0 +1,67 @@ +--- +id: detect_distribution_type_py_datascience +name: detect_distribution_type +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def detect_distribution_type(values: list[float]) -> dict" +description: "Classifies the shape of a numeric distribution using skewness, excess kurtosis, tail ratio and log-skewness. Returns a type label and raw stats." +tags: [statistics, distribution, classification, skewness, kurtosis] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [math, numpy] +example: | + from detect_distribution_type import detect_distribution_type + import numpy as np + result = detect_distribution_type(np.random.normal(0, 1, 200).tolist()) + # {"type": "normal-ish", "stats": {"n": 200, "skew": ..., ...}} +tested: true +tests: + - "test_detect_too_few_samples" + - "test_detect_normal_ish" + - "test_detect_right_skewed" + - "test_detect_stats_keys" + - "test_detect_exactly_30" +test_file_path: "python/functions/datascience/tests/test_detect_distribution_type.py" +file_path: "python/functions/datascience/detect_distribution_type.py" +params: + - name: values + desc: "List of numeric values to classify. Minimum 30 for meaningful classification." +output: > + Dict with "type" (str) and "stats" (dict). Type is one of: normal-ish, + lognormal-ish, heavy-tail, right-skewed, left-skewed, other, too_few_samples. + Stats contains: n, skew, kurtosis, tail_ratio, log_skew. +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "aurgi_mapas/generar_pdf_reporte.py:133" +--- + +## Ejemplo + +```python +from detect_distribution_type import detect_distribution_type +import numpy as np + +detect_distribution_type(np.random.normal(0, 1, 200).tolist()) +# {"type": "normal-ish", "stats": {"n": 200, "skew": 0.03, ...}} + +detect_distribution_type([1]*5) +# {"type": "too_few_samples", "stats": {"n": 5}} +``` + +## Logica de clasificacion + +- n < 30 → too_few_samples +- excess kurtosis > 3 → heavy-tail +- |skew| <= 0.5 AND |kurt| <= 1 → normal-ish +- skew > 0.5 AND log_skew cerca de 0 AND tail_ratio > 2 → lognormal-ish +- skew > 0.5 → right-skewed +- skew < -0.5 → left-skewed +- default → other + +tail_ratio = p99/p50; log_skew calculado solo si hay >= 30 positivos. diff --git a/python/functions/datascience/detect_distribution_type.py b/python/functions/datascience/detect_distribution_type.py new file mode 100644 index 00000000..f9adf170 --- /dev/null +++ b/python/functions/datascience/detect_distribution_type.py @@ -0,0 +1,89 @@ +"""detect_distribution_type — Classify the distribution shape of a sample.""" + +import math +import numpy as np + + +def detect_distribution_type(values: list[float]) -> dict: + """Classify the distribution shape of a numeric sample. + + Uses skewness, excess kurtosis, tail ratio (p99/p50), and log-skewness + to assign one of: normal-ish, lognormal-ish, heavy-tail, right-skewed, + left-skewed, other, or too_few_samples (n < 30). + + Args: + values: List of numeric values. + + Returns: + Dict with keys: + "type" (str): distribution label. + "stats" (dict): {"n", "skew", "kurtosis", "tail_ratio", "log_skew"}. + """ + n = len(values) + if n < 30: + return {"type": "too_few_samples", "stats": {"n": n}} + + arr = np.array(values, dtype=float) + + mean = float(np.mean(arr)) + std = float(np.std(arr, ddof=1)) + + # Skewness + if std == 0: + skew = 0.0 + else: + skew = float(np.mean(((arr - mean) / std) ** 3)) + + # Excess kurtosis + if std == 0: + kurt = 0.0 + else: + kurt = float(np.mean(((arr - mean) / std) ** 4)) - 3.0 + + # Tail ratio: p99 / p50 (only meaningful when median != 0) + p50 = float(np.percentile(arr, 50)) + p99 = float(np.percentile(arr, 99)) + tail_ratio = (p99 / p50) if p50 != 0 else math.nan + + # Log-skewness on positive values + positives = arr[arr > 0] + if len(positives) >= 30: + log_arr = np.log(positives) + log_mean = float(np.mean(log_arr)) + log_std = float(np.std(log_arr, ddof=1)) + if log_std == 0: + log_skew = 0.0 + else: + log_skew = float(np.mean(((log_arr - log_mean) / log_std) ** 3)) + else: + log_skew = math.nan + + stats = { + "n": n, + "skew": skew, + "kurtosis": kurt, + "tail_ratio": tail_ratio, + "log_skew": log_skew, + } + + # Classification logic + if kurt > 3.0: + dist_type = "heavy-tail" + elif abs(skew) <= 0.5 and abs(kurt) <= 1.0: + dist_type = "normal-ish" + elif ( + skew > 0.5 + and not math.isnan(log_skew) + and abs(log_skew) <= 0.5 + and not math.isnan(tail_ratio) + and tail_ratio > 2.0 + ): + dist_type = "lognormal-ish" + elif skew > 0.5: + dist_type = "right-skewed" + elif skew < -0.5: + dist_type = "left-skewed" + else: + dist_type = "other" + + return {"type": dist_type, "stats": stats} diff --git a/python/functions/datascience/extract_graph_gliner2.md b/python/functions/datascience/extract_graph_gliner2.md new file mode 100644 index 00000000..e3112119 --- /dev/null +++ b/python/functions/datascience/extract_graph_gliner2.md @@ -0,0 +1,65 @@ +--- +name: extract_graph_gliner2 +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def extract_graph_gliner2(text: str, entity_labels: list[str], relation_labels: list | dict, model: Any, threshold: float = 0.3, include_confidence: bool = False) -> dict" +description: "Extrae entidades + relaciones en una sola pasada con GLiNER2. Wrapper de alto nivel: construye schema, ejecuta extraccion, normaliza a dict plano. No aplica post-filtrado ni coreference." +tags: [gliner2, ner, relation-extraction, nlp, extraction, graph, zero-shot, datascience, python, apache2] +uses_functions: + - gliner2_load_model_py_datascience +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [time, typing.Any] +params: + - name: text + desc: "Texto a analizar. Recomendado hasta 1500 chars (pre-chunkeado con chunk_with_overlap). Textos mas largos degradan el recall de GLiNER2." + - name: entity_labels + desc: "Lista de strings con los tipos de entidad en minusculas snake_case. E.g. ['person', 'organization', 'location']. Labels en snake_case mejoran el recall segun notebook 08." + - name: relation_labels + desc: "Lista de strings o dict {label: description} con los tipos de relacion. E.g. ['works_at', 'ceo_of'] o {'works_at': 'person works at an organization'}." + - name: model + desc: "Instancia GLiNER2 cargada con gliner2_load_model. Inyectada por el caller (no se carga aqui)." + - name: threshold + desc: "Umbral de confianza entre 0 y 1. 0.3 validado empiricamente en notebook 04 (gliner_glirel_tuning). Valores mas bajos = mas recall, mas ruido." + - name: include_confidence + desc: "Si True, GLiNER2 devuelve scores internos por entidad y relacion. False por defecto para output mas limpio." +output: "Dict con tres campos: 'entities' -> {type: [name, ...]}, 'relation_extraction' -> {rel_type: [(head, tail), ...]}, 'elapsed_s' -> float. Compatible con aggregate_extraction_results." +tested: true +tests: + - "output tiene claves entities relation_extraction elapsed_s" + - "stub model retorna shape correcto" +test_file_path: "python/functions/datascience/tests/test_extract_graph_gliner2.py" +file_path: "python/functions/datascience/extract_graph_gliner2.py" +notes: | + LICENSE: GLiNER2 (fastino/gliner2-large-v1) es Apache 2.0 — uso comercial OK. + + impure: invoca inferencia del modelo (side effect computacional + tiempo variable). + El model se inyecta externamente para permitir cache y reutilizacion entre llamadas. + Para textos largos usar chunk_with_overlap antes y llamar esta funcion por chunk, + luego agregar con aggregate_extraction_results. +--- + +## Ejemplo + +```python +from datascience.gliner2_load_model import gliner2_load_model +from datascience.extract_graph_gliner2 import extract_graph_gliner2 + +model = gliner2_load_model(device="auto") + +result = extract_graph_gliner2( + text="Carlos Torres es presidente de BBVA, con sede en Bilbao.", + entity_labels=["person", "organization", "location"], + relation_labels=["president_of", "headquartered_in"], + model=model, + threshold=0.3, +) +# result["entities"] -> {"person": ["Carlos Torres"], ...} +# result["relation_extraction"]-> {"president_of": [("Carlos Torres", "BBVA")]} +# result["elapsed_s"] -> 0.234 +``` diff --git a/python/functions/datascience/extract_graph_gliner2.py b/python/functions/datascience/extract_graph_gliner2.py new file mode 100644 index 00000000..c6f203f0 --- /dev/null +++ b/python/functions/datascience/extract_graph_gliner2.py @@ -0,0 +1,60 @@ +"""Extraccion de entidades + relaciones en una pasada con GLiNER2.""" + +from __future__ import annotations + +import time +from typing import Any + + +def extract_graph_gliner2( + text: str, + entity_labels: list[str], + relation_labels: list | dict, + model: Any, + threshold: float = 0.3, + include_confidence: bool = False, +) -> dict: + """Extract entities + relations using GLiNER2 with one schema pass. + + Wrapper de alto nivel sobre la API de GLiNER2. Construye el schema, + ejecuta la extraccion y normaliza el resultado a un dict plano. + NO aplica post-filtrado ni coreference — eso lo hace el caller con + filter_relations_by_entity_types y merge_entity_aliases. + + Args: + text: Texto a analizar. Recomendado: <= 1500 chars (pre-chunked). + entity_labels: Lista de strings con los tipos de entidad. + E.g. ["person", "organization", "location"] + relation_labels: Lista de strings o dict {label: description} con + los tipos de relacion. + E.g. ["works_at", "ceo_of"] o + {"works_at": "person works at organization"} + model: Instancia GLiNER2 cargada con gliner2_load_model. + threshold: Umbral de confianza (0-1). 0.3 es el valor validado + empiricamente en los notebooks del analisis. + include_confidence: Si True, el modelo devuelve scores por entidad + y relacion (formato interno de GLiNER2). + + Returns: + { + "entities": {type: [name, ...]}, + "relation_extraction": {rel_type: [(head, tail), ...]}, + "elapsed_s": float + } + """ + schema = model.create_schema().entities(entity_labels).relations(relation_labels) + + t0 = time.time() + r = model.extract( + text, + schema=schema, + threshold=threshold, + include_confidence=include_confidence, + ) + elapsed = round(time.time() - t0, 3) + + return { + "entities": r.get("entities", {}), + "relation_extraction": r.get("relation_extraction", {}), + "elapsed_s": elapsed, + } diff --git a/python/functions/datascience/extract_relations_mrebel.md b/python/functions/datascience/extract_relations_mrebel.md new file mode 100644 index 00000000..839751f8 --- /dev/null +++ b/python/functions/datascience/extract_relations_mrebel.md @@ -0,0 +1,114 @@ +--- +name: extract_relations_mrebel +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def extract_relations_mrebel(text: str, entities: list[EntityCandidate], tokenizer: Any, model: Any, src_lang: str = 'es_XX', sentence_split_re: str = r'(?<=[.!?])\\s+', min_sentence_chars: int = 20, num_beams: int = 4, max_length: int = 256) -> list[RelationCandidate]" +description: "Extrae relaciones entre entidades usando mREBEL (seq2seq multilingue). Divide el texto por oraciones, genera triplets con mREBEL, parsea con parse_rebel_output y alinea a entidades conocidas con align_relations_to_entities. Drop-in con extract_relations_glirel para benchmarks." +tags: [mrebel, relation-extraction, nlp, extract, knowledge-graph, seq2seq, multilingual, datascience, python] +uses_functions: + - mrebel_load_model_py_datascience + - parse_rebel_output_py_datascience + - align_relations_to_entities_py_datascience +uses_types: + - entity_candidate_py_datascience + - relation_candidate_py_datascience +returns: + - relation_candidate_py_datascience +returns_optional: false +error_type: "error_go_core" +imports: [re] +params: + - name: text + desc: "texto fuente en el idioma de src_lang (mismo chunk usado para extraer las entidades)" + - name: entities + desc: "entidades ya extraidas de este texto (de extract_entities_gliner o similar). Solo se conservan relaciones entre entidades de esta lista." + - name: tokenizer + desc: "tokenizer mREBEL cargado con mrebel_load_model — inyectado por el caller para evitar re-carga en batch" + - name: model + desc: "modelo mREBEL cargado con mrebel_load_model — inyectado por el caller" + - name: src_lang + desc: "informativo — el idioma con que se cargo el tokenizer (ej. 'es_XX'). No se usa en runtime." + - name: sentence_split_re + desc: "patron regex para dividir el texto en oraciones. Defecto: split despues de [.!?] seguido de espacio." + - name: min_sentence_chars + desc: "longitud minima de caracteres para procesar una oracion. Fragmentos mas cortos se saltan (defecto 20)." + - name: num_beams + desc: "ancho del beam search para model.generate (defecto 4)" + - name: max_length + desc: "longitud maxima en tokens para tokenizacion y generacion (defecto 256)" +output: "lista de RelationCandidate con confidence=1.0 (mREBEL no produce score continuo). from_name/to_name siempre coinciden con entidades del input." +tested: true +tests: + - "flujo completo con stub produce RelationCandidate correctos" + - "menos de 2 entidades retorna vacio" + - "texto vacio retorna vacio" + - "triplets no alineables se descartan" +test_file_path: "python/functions/datascience/tests/test_extract_relations_mrebel.py" +file_path: "python/functions/datascience/extract_relations_mrebel.py" +notes: | + impure: model.generate es I/O computacional con estado externo (pesos del modelo). + + mREBEL no produce un confidence score continuo — devuelve los triplets que el modelo + decodifico como output mas probable. confidence=1.0 es un marcador "el modelo lo emitio", + no una probabilidad calibrada. Para filtrar por calidad, usar el numero de beams + como proxy o combinar con un clasificador posterior. + + Drop-in con extract_relations_glirel para benchmarks: + - Misma interfaz de entrada (text, entities, model) + - Misma salida (list[RelationCandidate]) + - Diferencia: mREBEL no necesita relation_types (genera relaciones libre), + glirel necesita relation_types (zero-shot discriminativo). + + LICENCIA del modelo: Babelscape/mrebel-large es CC BY-NC-SA 4.0 (no comercial). + Ver mrebel_load_model para mas detalles. +--- + +## Ejemplo + +```python +from python.functions.datascience.mrebel_load_model import mrebel_load_model +from python.functions.datascience.extract_relations_mrebel import extract_relations_mrebel +from python.types.datascience.entity_candidate import EntityCandidate + +tokenizer, model = mrebel_load_model(src_lang="es_XX") + +text = "Pablo Isla es el presidente de Inditex. La empresa tiene sede en Arteixo, A Coruna." +entities = [ + EntityCandidate(name="Pablo Isla", type_label="PER", confidence=0.95), + EntityCandidate(name="Inditex", type_label="ORG", confidence=0.92), + EntityCandidate(name="Arteixo", type_label="LOC", confidence=0.88), + EntityCandidate(name="A Coruna", type_label="LOC", confidence=0.85), +] + +relations = extract_relations_mrebel( + text=text, + entities=entities, + tokenizer=tokenizer, + model=model, +) +# [RelationCandidate(from_name='Pablo Isla', to_name='Inditex', +# relation_type='employer', confidence=1.0, ...), ...] +``` + +## Comparacion con extract_relations_glirel + +| | mREBEL | GLiREL | +|---|---|---| +| Tipo | Seq2seq generativo | Discriminativo zero-shot | +| relation_types | No (genera libre) | Si (obligatorio) | +| Confidence | 1.0 fijo (no calibrado) | 0.0-1.0 (calibrado) | +| Idiomas | 30+ multilingue | Principalmente EN | +| Licencia modelo | CC BY-NC-SA (no comercial) | Apache 2.0 | +| Velocidad | Mas lento (seq2seq) | Mas rapido (clasificador) | + +## Notas de diseno + +- `parse_rebel_output` y `align_relations_to_entities` son funciones puras + compuestas por esta funcion impura — testeable independientemente. +- Errores de tokenizacion/generacion por frase se capturan y saltan (la frase + se ignora, el resto del texto se procesa). +- `source_chunk_index` rastrea el indice de oracion de origen, no de chunk + de texto — util para debugging. diff --git a/python/functions/datascience/extract_relations_mrebel.py b/python/functions/datascience/extract_relations_mrebel.py new file mode 100644 index 00000000..de2e6c5f --- /dev/null +++ b/python/functions/datascience/extract_relations_mrebel.py @@ -0,0 +1,136 @@ +"""Extrae relaciones entre entidades usando mREBEL (seq2seq multilingue).""" + +from __future__ import annotations + +import os +import re +import sys +from typing import Any + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..")) + +from python.types.datascience.entity_candidate import EntityCandidate +from python.types.datascience.relation_candidate import RelationCandidate +from python.functions.datascience.parse_rebel_output import parse_rebel_output +from python.functions.datascience.align_relations_to_entities import align_relations_to_entities + + +def extract_relations_mrebel( + text: str, + entities: list[EntityCandidate], + tokenizer: Any, + model: Any, + src_lang: str = "es_XX", + sentence_split_re: str = r"(?<=[.!?])\s+", + min_sentence_chars: int = 20, + num_beams: int = 4, + max_length: int = 256, +) -> list[RelationCandidate]: + """Extract relations from text using mREBEL, sentence by sentence. + + Orchestrates the full pipeline: + + 1. Split ``text`` into sentences using ``sentence_split_re``. + 2. Filter out sentences shorter than ``min_sentence_chars``. + 3. For each sentence: tokenize → generate → decode (with special tokens) + → ``parse_rebel_output`` → accumulate raw triplets. + 4. Collect all entity names from ``entities``, sorted DESC by length + (so longer names win in substring matching). + 5. Call ``align_relations_to_entities`` to resolve head/tail spans to + canonical entity names and drop unresolved / self-loop triplets. + 6. Wrap each aligned triplet in a ``RelationCandidate``. + + mREBEL does not produce a continuous confidence score — ``confidence`` + is set to ``1.0`` as a marker meaning "the model emitted this triplet". + + Args: + text: Source text (same language as ``src_lang``). + entities: Entities already extracted from this text (e.g. via + ``extract_entities_gliner``). Used to filter triplets to + known entities only. + tokenizer: mREBEL tokenizer loaded with ``mrebel_load_model``. + model: mREBEL model loaded with ``mrebel_load_model``. + src_lang: Informational — the language the tokenizer was loaded with. + Not used at inference time (mBART lang tokens are set at load time). + sentence_split_re: Regex pattern for sentence splitting. Default splits + on whitespace that follows ``.``, ``!`` or ``?``. + min_sentence_chars: Minimum character length for a sentence to be + processed. Shorter fragments are skipped. + num_beams: Beam search width for ``model.generate``. Default 4. + max_length: Max token length for both tokenization and generation. + + Returns: + List of ``RelationCandidate`` where ``from_name`` and ``to_name`` + always correspond to names in ``entities``. Empty list if no aligned + triplets are found or ``entities`` has fewer than 2 items. + """ + if len(entities) < 2: + return [] + if not text or not text.strip(): + return [] + + split_re = re.compile(sentence_split_re) + sentences = split_re.split(text.strip()) + sentences = [s.strip() for s in sentences if s.strip() and len(s.strip()) >= min_sentence_chars] + if not sentences: + return [] + + # Step 1-3: gather raw triplets from all sentences. + raw_triplets: list[dict] = [] + for idx, sentence in enumerate(sentences): + try: + inputs = tokenizer( + sentence, + return_tensors="pt", + max_length=max_length, + truncation=True, + ) + generated = model.generate( + **inputs, + num_beams=num_beams, + length_penalty=1.0, + max_length=max_length, + ) + decoded = tokenizer.decode(generated[0], skip_special_tokens=False) + except Exception: + # Skip sentences that fail (e.g. tokenizer errors on special chars). + continue + + sentence_triplets = parse_rebel_output(decoded) + # Tag each triplet with the sentence index for source_chunk_index. + for t in sentence_triplets: + t["_sentence_idx"] = idx + raw_triplets.extend(sentence_triplets) + + if not raw_triplets: + return [] + + # Step 4-5: align to entity names (sorted DESC by length for substring match). + entity_names = sorted([e.name for e in entities if e.name], key=len, reverse=True) + aligned = align_relations_to_entities(raw_triplets, entity_names) + + # Step 6: wrap in RelationCandidate. + candidates: list[RelationCandidate] = [] + for item in aligned: + # Recover sentence_idx from raw triplet — find matching raw by head/tail/type. + sentence_idx = -1 + for raw in raw_triplets: + if ( + raw.get("head", "").strip() and + raw.get("type", "").strip() == item["kind"] + ): + sentence_idx = raw.get("_sentence_idx", -1) + break + + candidates.append( + RelationCandidate( + from_name=item["from"], + to_name=item["to"], + relation_type=item["kind"], + description="", + confidence=1.0, + source_chunk_index=sentence_idx, + ) + ) + + return candidates diff --git a/python/functions/datascience/extract_triples_spacy_es.md b/python/functions/datascience/extract_triples_spacy_es.md new file mode 100644 index 00000000..8b1d29df --- /dev/null +++ b/python/functions/datascience/extract_triples_spacy_es.md @@ -0,0 +1,65 @@ +--- +name: extract_triples_spacy_es +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def extract_triples_spacy_es(text: str, nlp: Any) -> dict" +description: "Extraccion OpenIE schema-less en castellano via reglas de dependencia spaCy. Detecta patrones sujeto-verbo-objeto con el lemma del verbo como relacion (sin vocabulario fijo). Tambien extrae entidades NER (PER, ORG, LOC, MISC)." +tags: [spacy, openie, nlp, spanish, triples, dependency, ner, schema-less, datascience, python, mit] +uses_functions: + - spacy_es_load_model_py_datascience +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [time, typing.Any] +params: + - name: text + desc: "Texto en castellano a analizar. Funciona mejor con oraciones completas. Admite multiples oraciones en el mismo texto." + - name: nlp + desc: "Instancia spaCy Language cargada con spacy_es_load_model. Debe incluir dependencias + POS + NER (es_core_news_md o lg)." +output: "Dict con 'text' (input), 'triples' (lista de {subject, relation, object, verb_form, object_dep, prep}), 'entities' (lista de {text, label}) y 'elapsed_s'. La relacion es el lemma del verbo, opcionalmente sufijado con preposicion (_en, _con) o modo pasivo ([pass])." +tested: true +tests: + - "oracion simple produce tripleta con sujeto verbo objeto" + - "carlos torres preside bbva produce tripleta president" + - "amancio ortega fundo inditex en 1985 produce tripletas con fundar_en" + - "texto sin verbos produce tripletas vacias" + - "entities NER detecta PER ORG LOC" +test_file_path: "python/functions/datascience/tests/test_extract_triples_spacy_es.py" +file_path: "python/functions/datascience/extract_triples_spacy_es.py" +notes: | + LICENSE: spaCy es MIT. Modelo es_core_news_md es CC BY-SA 4.0. + Uso comercial permitido con atribucion. + + Validado en notebook 09 del analisis gliner_glirel_tuning. + Complementa a extract_graph_gliner2: GLiNER2 usa vocabulario fijo de relaciones + pero mayor precision; spaCy OpenIE usa lemmas verbales (sin vocabulario fijo) + pero requiere post-filtrado manual. + + impure: invoca inferencia del modelo (side effect computacional). + El nlp se inyecta externamente para permitir cache y reutilizacion. + + Relaciones compuestas: 'fundar_en' (fundar + preposicion 'en'), + 'ser_nombrado[pass]' (pasiva), 'trabajar_con' (trabajar + 'con'). +--- + +## Ejemplo + +```python +from datascience.spacy_es_load_model import spacy_es_load_model +from datascience.extract_triples_spacy_es import extract_triples_spacy_es + +nlp = spacy_es_load_model() + +result = extract_triples_spacy_es( + "Amancio Ortega fundo Inditex en 1985 en La Coruna.", + nlp=nlp, +) +# result["triples"]: +# [{"subject": "Amancio Ortega", "relation": "fundar", "object": "Inditex", ...}, +# {"subject": "Amancio Ortega", "relation": "fundar_en", "object": "1985", ...}, +# {"subject": "Amancio Ortega", "relation": "fundar_en", "object": "La Coruna", ...}] +``` diff --git a/python/functions/datascience/extract_triples_spacy_es.py b/python/functions/datascience/extract_triples_spacy_es.py new file mode 100644 index 00000000..a5e09b1f --- /dev/null +++ b/python/functions/datascience/extract_triples_spacy_es.py @@ -0,0 +1,124 @@ +"""Extraccion de tripletas OpenIE schema-less en castellano via reglas de dependencia. + +Validado en notebook 09 del analisis gliner_glirel_tuning. +LICENSE: spaCy MIT + es_core_news_md CC BY-SA 4.0. +""" + +from __future__ import annotations + +import time +from typing import Any + +# Determinantes y pronombres que no son entidades significativas +STOP_TOKENS = { + "el", "la", "los", "las", "un", "una", "unos", "unas", + "esto", "eso", "aquello", "esta", "este", "estos", "estas", + "que", "quien", "cual", "cuales", +} + + +def _clean_span(span_tokens) -> str: # type: ignore[type-arg] + """Extrae texto de un span de tokens, eliminando preposiciones iniciales.""" + toks = list(span_tokens) + while toks and toks[0].pos_ == "ADP": + toks = toks[1:] + return " ".join(t.text for t in toks).strip() + + +def _is_meaningful(text: str) -> bool: + """Comprueba que un span no es vacio ni una stopword.""" + if not text or not text.strip(): + return False + if text.lower() in STOP_TOKENS: + return False + return True + + +def extract_triples_spacy_es(text: str, nlp: Any) -> dict: + """Extract OpenIE-style (subject, relation, object) triples from Spanish text. + + Uses spaCy dependency rules to find subject-verb-object patterns. + Schema-LESS: the relation is the verb's lemma (no fixed vocabulary). + Also extracts spaCy NER entities (PER, ORG, LOC, MISC). + + Args: + text: Spanish text to analyze. Works best with complete sentences. + nlp: spaCy Language instance loaded with spacy_es_load_model. + + Returns: + { + "text": str, + "triples": [ + {"subject": str, "relation": str, "object": str, + "verb_form": str, "object_dep": str, "prep": str|None}, + ... + ], + "entities": [{"text": str, "label": str}, ...], + "elapsed_s": float + } + """ + t0 = time.time() + doc = nlp(text) + triples: list[dict] = [] + + for tok in doc: + if tok.pos_ not in ("VERB", "AUX"): + continue + + verb_lemma = tok.lemma_ + verb_form = tok.text + + subjs = [ + c for c in tok.children + if c.dep_ in ("nsubj", "nsubj:pass", "csubj") + ] + if not subjs: + continue + + objects: list[tuple] = [] + for c in tok.children: + if c.dep_ in ("obj", "dobj", "iobj", "attr", "xcomp", "ccomp"): + objects.append((c, c.dep_, None)) + elif c.dep_ in ("obl", "obl:agent", "nmod"): + prep = None + for cc in c.children: + if cc.dep_ == "case" and cc.pos_ == "ADP": + prep = cc.text.lower() + break + objects.append((c, c.dep_, prep)) + + for s in subjs: + s_text = _clean_span(s.subtree) + if not _is_meaningful(s_text): + continue + for o, dep, prep in objects: + o_text = _clean_span(o.subtree) + if not _is_meaningful(o_text): + continue + + # Construir etiqueta de relacion + rel = verb_lemma + # Pasiva: marcar con [pass] + if any(c.dep_ == "nsubj:pass" for c in tok.children): + rel = f"{verb_lemma}[pass]" + # Oblicuo con preposicion (excl. agente y "a" directa) + elif prep and dep != "obl:agent" and prep != "a": + rel = f"{verb_lemma}_{prep}" + + triples.append({ + "subject": s_text, + "relation": rel, + "object": o_text, + "verb_form": verb_form, + "object_dep": dep, + "prep": prep, + }) + + ents = [{"text": e.text, "label": e.label_} for e in doc.ents] + + return { + "text": text, + "triples": triples, + "entities": ents, + "elapsed_s": round(time.time() - t0, 3), + } diff --git a/python/functions/datascience/fuzzy_merge_adaptive.md b/python/functions/datascience/fuzzy_merge_adaptive.md new file mode 100644 index 00000000..1f0d353a --- /dev/null +++ b/python/functions/datascience/fuzzy_merge_adaptive.md @@ -0,0 +1,58 @@ +--- +name: fuzzy_merge_adaptive +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def fuzzy_merge_adaptive(left: list[dict], right: list[dict], left_key: str, right_key: str, thresholds: list[int] | None = None, how: str = 'left') -> list[dict]" +description: "Fuzzy join adaptativo entre dos listas de dicts usando rapidfuzz.token_sort_ratio. Prueba thresholds de mayor a menor y asigna el mayor cumplido. Soporta how='left' (todos los de left) e how='inner' (solo con match). Campos colisionantes reciben sufijos _left/_right." +tags: [fuzzy, matching, join, merge, rapidfuzz, string-similarity, datascience] +params: + - name: left + desc: Lista de dicts (lado izquierdo del join). + - name: right + desc: Lista de dicts (lado derecho del join). + - name: left_key + desc: Clave en los dicts de left usada para matching de strings. + - name: right_key + desc: Clave en los dicts de right usada para matching de strings. + - name: thresholds + desc: Lista de thresholds enteros a probar en orden descendente. Default [90,80,70,60,50]. + - name: how + desc: "'left' incluye todos los items de left; 'inner' solo los que tienen match." +output: "Lista de dicts mergeados con campos de left + right (sufijos _left/_right si colisionan) + fuzzy_match (str|None), match_score (int), threshold_used (int|None)." +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["rapidfuzz"] +tested: true +tests: + - "left join con typo" + - "inner join excluye sin match" + - "left join sin match devuelve none" + - "threshold adaptativo" + - "colision de claves usa sufijos" +test_file_path: "python/functions/datascience/tests/test_fuzzy_merge_adaptive.py" +file_path: "python/functions/datascience/fuzzy_merge_adaptive.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "fuzzy_joins/fuzzy_en_batches.py" +--- + +## Ejemplo + +```python +from fuzzy_merge_adaptive import fuzzy_merge_adaptive + +left = [{"name": "Madrid"}, {"name": "Barclona"}] +right = [{"name": "Madrid", "cp": "28"}, {"name": "Barcelona", "cp": "08"}] +result = fuzzy_merge_adaptive(left, right, left_key="name", right_key="name") +# result[1]["fuzzy_match"] == "Barcelona", result[1]["match_score"] >= 80 +``` + +## Notas + +Migrado de thefuzz a rapidfuzz (API compatible, mayor velocidad). Sin pandas: el merge se implementa manualmente via dict lookup por right_key. Los thresholds se prueban de mayor a menor; el primero cumplido se asigna a threshold_used. diff --git a/python/functions/datascience/fuzzy_merge_adaptive.py b/python/functions/datascience/fuzzy_merge_adaptive.py new file mode 100644 index 00000000..5e1418b8 --- /dev/null +++ b/python/functions/datascience/fuzzy_merge_adaptive.py @@ -0,0 +1,108 @@ +"""Fuzzy merge adaptativo con multiples thresholds usando rapidfuzz.""" + +from __future__ import annotations + +from typing import Iterable + + +def fuzzy_merge_adaptive( + left: list[dict], + right: list[dict], + left_key: str, + right_key: str, + thresholds: list[int] | None = None, + how: str = "left", +) -> list[dict]: + """Realiza un fuzzy join adaptativo entre dos listas de dicts. + + Para cada item en left busca en right el mejor match usando + rapidfuzz.fuzz.token_sort_ratio. Prueba thresholds de mayor a menor + y asigna threshold_used al mayor threshold cumplido. Si no cumple + ninguno, match es None. + + Args: + left: Lista de dicts (lado izquierdo del join). + right: Lista de dicts (lado derecho del join). + left_key: Clave en los dicts de left usada para matching. + right_key: Clave en los dicts de right usada para matching. + thresholds: Thresholds a probar en orden descendente. + Default [90, 80, 70, 60, 50]. + how: Tipo de join. 'left' incluye todos los items de left + (con None en campos de right si no hay match). + 'inner' incluye solo items con match. + + Returns: + Lista de dicts mergeados con campos de left + campos de right + (sufijos _left/_right si colisionan) + fuzzy_match, match_score, + threshold_used. + """ + from rapidfuzz import fuzz, process + + if thresholds is None: + thresholds = [90, 80, 70, 60, 50] + + right_values = [ + str(r[right_key]) for r in right if r.get(right_key) is not None + ] + + def find_best_match(value: str | None) -> tuple[str | None, int, int | None]: + if value is None: + return None, 0, None + result = process.extractOne(str(value), right_values, scorer=fuzz.token_sort_ratio) + if not result: + return None, 0, None + match_str, score = result[0], result[1] + for t in thresholds: + if score >= t: + return match_str, score, t + return None, 0, None + + # Detectar colisiones de claves + left_keys = set(left[0].keys()) if left else set() + right_keys = set(right[0].keys()) if right else set() + collision_keys = left_keys & right_keys + + # Construir indice de right por right_key + right_index: dict[str, dict] = {} + for r in right: + val = r.get(right_key) + if val is not None: + right_index[str(val)] = r + + result_rows = [] + for item in left: + value = item.get(left_key) + fuzzy_match, score, threshold_used = find_best_match(value) + + if fuzzy_match is None and how == "inner": + continue + + row: dict = {} + # Campos de left + for k, v in item.items(): + if k in collision_keys: + row[f"{k}_left"] = v + else: + row[k] = v + + # Campos de right + matched_right = right_index.get(fuzzy_match) if fuzzy_match else None + if matched_right is not None: + for k, v in matched_right.items(): + if k in collision_keys: + row[f"{k}_right"] = v + else: + row[k] = v + else: + for k in right_keys: + if k in collision_keys: + row[f"{k}_right"] = None + else: + row[k] = None + + row["fuzzy_match"] = fuzzy_match + row["match_score"] = score + row["threshold_used"] = threshold_used + result_rows.append(row) + + return result_rows diff --git a/python/functions/datascience/geometric_mean.md b/python/functions/datascience/geometric_mean.md new file mode 100644 index 00000000..6fada697 --- /dev/null +++ b/python/functions/datascience/geometric_mean.md @@ -0,0 +1,52 @@ +--- +id: geometric_mean_py_datascience +name: geometric_mean +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def geometric_mean(values: list[float]) -> float" +description: "Geometric mean of positive elements via exp(mean(log(x))). Non-positive values are filtered out. Returns math.nan if no positives." +tags: [statistics, mean, geometric, distribution, lognormal] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [math, numpy] +example: | + from geometric_mean import geometric_mean + result = geometric_mean([1, 2, 4, 8]) # ~2.828 (2^1.5) +tested: true +tests: + - "test_geometric_mean_powers_of_two" + - "test_geometric_mean_filters_non_positive" + - "test_geometric_mean_empty_returns_nan" + - "test_geometric_mean_all_negative_returns_nan" + - "test_geometric_mean_single_positive" +test_file_path: "python/functions/datascience/tests/test_geometric_mean.py" +file_path: "python/functions/datascience/geometric_mean.py" +params: + - name: values + desc: "List of numeric values. Non-positive elements are silently ignored." +output: "Geometric mean as float, computed over positive elements only. Returns math.nan if there are no positive values." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "aurgi_mapas/generar_pdf_reporte.py:126" +--- + +## Ejemplo + +```python +from geometric_mean import geometric_mean + +geometric_mean([1, 2, 4, 8]) # 2.828... (= 2^1.5) +geometric_mean([1, -2, 3]) # exp((log(1)+log(3))/2) — ignores -2 +geometric_mean([]) # math.nan +geometric_mean([-1, -2]) # math.nan — no positives +``` + +## Notas + +Apropiado para distribuciones lognormales o datos multiplicativos (precios, ratios, crecimientos). Equivalente a la raiz n-esima del producto pero numericamente mas estable via log-space. diff --git a/python/functions/datascience/geometric_mean.py b/python/functions/datascience/geometric_mean.py new file mode 100644 index 00000000..13e5e5f4 --- /dev/null +++ b/python/functions/datascience/geometric_mean.py @@ -0,0 +1,23 @@ +"""geometric_mean — Geometric mean of positive values.""" + +import math +import numpy as np + + +def geometric_mean(values: list[float]) -> float: + """Return the geometric mean of the positive elements in values. + + Filters out non-positive numbers before computing exp(mean(log(x))). + Returns math.nan if there are no positive values. + + Args: + values: List of numeric values (non-positive elements are ignored). + + Returns: + Geometric mean as float, or math.nan if no positive values exist. + """ + positives = [v for v in values if v > 0] + if not positives: + return math.nan + arr = np.array(positives, dtype=float) + return float(np.exp(np.mean(np.log(arr)))) diff --git a/python/functions/datascience/gliner2_load_model.md b/python/functions/datascience/gliner2_load_model.md new file mode 100644 index 00000000..03c492e5 --- /dev/null +++ b/python/functions/datascience/gliner2_load_model.md @@ -0,0 +1,67 @@ +--- +name: gliner2_load_model +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def gliner2_load_model(model_name: str = 'fastino/gliner2-large-v1', device: str = 'auto') -> Any" +description: "Carga (y cachea por (model_name, device)) un modelo GLiNER2 (NER+RE joint). GLiNER2 extrae entidades y relaciones en una sola pasada con schema unificado. ~2x mas rapido que GLiNER + GLiREL separados. LICENSE: Apache 2.0." +tags: [gliner2, ner, relation-extraction, nlp, model, huggingface, zero-shot, joint, datascience, python, apache2] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [gliner2] +params: + - name: model_name + desc: "ID del modelo en HuggingFace Hub. Default: fastino/gliner2-large-v1. Alternativas: fastino/gliner2-base-v1 (mas ligero)." + - name: device + desc: "'auto' usa CUDA si disponible, sino CPU. Valores: 'cpu', 'cuda', 'cuda:0', 'cuda:1'. auto es el default recomendado." +output: "Instancia GLiNER2 cacheada por (model_name, device). Tiene metodos .create_schema().entities(...).relations(...) y .extract(text, schema=schema, threshold=0.3)." +tested: true +tests: + - "cache devuelve la misma instancia con los mismos parametros" + - "device=auto resuelve a cpu si torch no esta instalado" + - "ImportError si gliner2 no esta instalado" +test_file_path: "python/functions/datascience/tests/test_gliner2_load_model.py" +file_path: "python/functions/datascience/gliner2_load_model.py" +notes: | + LICENSE: fastino/gliner2-large-v1 es Apache 2.0 — uso comercial OK. + Diferencia con gliner_load_model: GLiNER hace solo NER, GLiNER2 hace NER+RE + en una sola pasada (joint schema). Para pipelines de grafo usar GLiNER2 + cuando se necesiten ambas tareas simultaneamente. + + impure: descarga red/disco la primera vez, mantiene estado en _MODEL_CACHE. + Tamanio: fastino/gliner2-large-v1 ~500 MB. Primera carga 15-30s en CPU. + Inferencia CPU: 10-50 KB texto/s con schema tipico (3 entity + 8 relation labels). +--- + +## Ejemplo + +```python +from datascience.gliner2_load_model import gliner2_load_model + +model = gliner2_load_model(device="auto") + +schema = (model.create_schema() + .entities(["person", "organization", "location"]) + .relations(["works_at", "ceo_of", "located_in"])) + +result = model.extract( + "Pablo Isla es el CEO de Inditex, empresa con sede en Arteixo.", + schema=schema, + threshold=0.3, +) +# result["entities"] -> {"person": ["Pablo Isla"], "organization": ["Inditex"], ...} +# result["relation_extraction"] -> {"ceo_of": [("Pablo Isla", "Inditex")], ...} +``` + +## Instalacion + +```bash +cd python && uv pip install gliner2 +# o con el extra NLP completo: +cd python && uv pip install -e '.[nlp]' +``` diff --git a/python/functions/datascience/gliner2_load_model.py b/python/functions/datascience/gliner2_load_model.py new file mode 100644 index 00000000..14bd967b --- /dev/null +++ b/python/functions/datascience/gliner2_load_model.py @@ -0,0 +1,62 @@ +"""Carga (y cachea) un modelo GLiNER2 (NER+RE joint en una sola pasada). + +LICENSE: Apache 2.0 — uso comercial permitido. +Modelo por defecto: fastino/gliner2-large-v1 +""" + +from __future__ import annotations + +from typing import Any + +# Cache global: (model_name, device) -> instancia GLiNER2 +_MODEL_CACHE: dict[tuple[str, str], Any] = {} + + +def _resolve_device(device: str) -> str: + """Resuelve 'auto' a 'cuda' o 'cpu' segun disponibilidad de torch.""" + if device != "auto": + return device + try: + import torch + except ImportError: + return "cpu" + return "cuda" if torch.cuda.is_available() else "cpu" + + +def gliner2_load_model( + model_name: str = "fastino/gliner2-large-v1", + device: str = "auto", +) -> Any: + """Load (and cache) a GLiNER2 model. + + GLiNER2 extracts entities AND relations in a single forward pass using + a joint schema (entities + relation_labels). This is ~2x faster than + running GLiNER + GLiREL separately for co-occurring entities. + + Returns model instance with .extract() and .create_schema() methods. + + LICENSE: Apache 2.0 — commercial use OK. + + Args: + model_name: HuggingFace Hub model ID. Default: fastino/gliner2-large-v1. + device: 'auto' uses CUDA if available, else CPU. 'cpu', 'cuda', 'cuda:N'. + + Returns: + GLiNER2 instance cached by (model_name, device). + """ + resolved = _resolve_device(device) + key = (model_name, resolved) + if key in _MODEL_CACHE: + return _MODEL_CACHE[key] + + from gliner2 import GLiNER2 # type: ignore[import] + + m = GLiNER2.from_pretrained(model_name) + if hasattr(m, "to") and resolved != "cpu": + try: + m.to(resolved) + except Exception: + pass # Fallback to CPU silently + + _MODEL_CACHE[key] = m + return m diff --git a/python/functions/datascience/kde_density_levels.md b/python/functions/datascience/kde_density_levels.md new file mode 100644 index 00000000..d1503de7 --- /dev/null +++ b/python/functions/datascience/kde_density_levels.md @@ -0,0 +1,52 @@ +--- +id: kde_density_levels_py_datascience +name: kde_density_levels +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def kde_density_levels(xs: list[float], ys: list[float], bw_adjust: float = 0.6, abs_quantile: float = 0.1, dense_quantile: float = 0.85, bins: int = 80) -> dict | None" +description: "Estimates 2-D density via KDE (scipy) or histogram fallback (numpy) and returns per-point density values plus absolute and dense quantile thresholds." +tags: [statistics, kde, density, spatial, geospatial, scipy, numpy] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [numpy, scipy] +example: | + from kde_density_levels import kde_density_levels + import numpy as np + rng = np.random.default_rng(42) + result = kde_density_levels(rng.normal(0,1,50).tolist(), rng.normal(0,1,50).tolist()) + # {"method": "kde", "densities": array(...), "abs_level": ..., "dense_level": ...} +tested: true +tests: + - "test_kde_density_levels_returns_dict_for_50_points" + - "test_kde_density_levels_none_for_few_points" + - "test_kde_density_levels_none_for_4_points" + - "test_kde_density_levels_levels_ordered" + - "test_kde_density_levels_mismatched_lengths" +test_file_path: "python/functions/datascience/tests/test_kde_density_levels.py" +file_path: "python/functions/datascience/kde_density_levels.py" +params: + - name: xs + desc: "X-coordinates of the 2-D point cloud." + - name: ys + desc: "Y-coordinates of the 2-D point cloud. Must have same length as xs." + - name: bw_adjust + desc: "Bandwidth adjustment factor for gaussian_kde. Default 0.6." + - name: abs_quantile + desc: "Quantile of density values used as the absolute (sparse) threshold. Default 0.1." + - name: dense_quantile + desc: "Quantile of density values used as the dense cluster threshold. Default 0.85." + - name: bins + desc: "Number of bins per axis for the histogram fallback. Default 80." +output: "Dict with method (str), densities (np.ndarray of per-point density), abs_level (float), dense_level (float). Returns None if len(xs) < 5 or lengths differ." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/recomendador_centros.py:305" +--- + +Funcion pura que no escribe nada en disco. returns_optional=true porque devuelve None cuando hay menos de 5 puntos. diff --git a/python/functions/datascience/kde_density_levels.py b/python/functions/datascience/kde_density_levels.py new file mode 100644 index 00000000..73d366d5 --- /dev/null +++ b/python/functions/datascience/kde_density_levels.py @@ -0,0 +1,65 @@ +"""kde_density_levels — Compute density levels via KDE or histogram fallback.""" + +import math +import numpy as np + + +def kde_density_levels( + xs: list[float], + ys: list[float], + bw_adjust: float = 0.6, + abs_quantile: float = 0.1, + dense_quantile: float = 0.85, + bins: int = 80, +) -> dict | None: + """Estimate 2-D density and compute absolute and dense threshold levels. + + Uses scipy.stats.gaussian_kde when available; falls back to + numpy.histogram2d if scipy is not installed. + + Args: + xs: X-coordinates of points. + ys: Y-coordinates of points. + bw_adjust: Bandwidth adjustment factor for KDE (ignored for histogram fallback). + abs_quantile: Quantile of density values used as the absolute threshold. + dense_quantile: Quantile of density values used as the dense threshold. + bins: Number of bins per axis for the histogram fallback. + + Returns: + Dict with keys: + "method" (str): "kde" or "hist". + "densities" (np.ndarray): 1-D array of per-point density estimates. + "abs_level" (float): density at abs_quantile. + "dense_level" (float): density at dense_quantile. + Returns None if len(xs) < 5 or xs and ys have different lengths. + """ + if len(xs) < 5 or len(xs) != len(ys): + return None + + xs_arr = np.array(xs, dtype=float) + ys_arr = np.array(ys, dtype=float) + points = np.vstack([xs_arr, ys_arr]) + + try: + from scipy.stats import gaussian_kde # type: ignore + + kde = gaussian_kde(points, bw_method=bw_adjust) + densities = kde(points) + method = "kde" + except ImportError: + # Histogram fallback + h, xedges, yedges = np.histogram2d(xs_arr, ys_arr, bins=bins) + xi = np.clip(np.searchsorted(xedges, xs_arr) - 1, 0, bins - 1) + yi = np.clip(np.searchsorted(yedges, ys_arr) - 1, 0, bins - 1) + densities = h[xi, yi].astype(float) + method = "hist" + + abs_level = float(np.quantile(densities, abs_quantile)) + dense_level = float(np.quantile(densities, dense_quantile)) + + return { + "method": method, + "densities": densities, + "abs_level": abs_level, + "dense_level": dense_level, + } diff --git a/python/functions/datascience/marianmt_es_en_load_model.md b/python/functions/datascience/marianmt_es_en_load_model.md new file mode 100644 index 00000000..ab967dbc --- /dev/null +++ b/python/functions/datascience/marianmt_es_en_load_model.md @@ -0,0 +1,61 @@ +--- +name: marianmt_es_en_load_model +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def marianmt_es_en_load_model(model_name: str = 'Helsinki-NLP/opus-mt-es-en') -> tuple[Any, Any]" +description: "Carga (y cachea) el tokenizer y modelo MarianMT para traduccion ES->EN (Helsinki-NLP, ~300 MB). Licencia Apache 2.0. Cache por model_name." +tags: [marianmt, translation, es-en, nlp, model, huggingface, apache2, datascience, python] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [transformers] +params: + - name: model_name + desc: "ID del modelo en HuggingFace Hub (defecto: Helsinki-NLP/opus-mt-es-en, ~300 MB)" +output: "tupla (tokenizer, model) listos para inferencia, cacheados por model_name." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/datascience/marianmt_es_en_load_model.py" +notes: | + LICENCIA: Apache 2.0 — uso comercial permitido. + + Util como paso previo a REBEL (monolingue EN): traducir ES -> EN con MarianMT + y luego pasar a rebel_load_model para extraccion de relaciones en ingles. + + impure: descarga red/disco la primera vez, mantiene estado en _MODEL_CACHE. + Usa MarianTokenizer y MarianMTModel en vez de Auto* porque los modelos Marian + tienen tokenizer especializado con vocabulario SPM. +--- + +## Ejemplo + +```python +from python.functions.datascience.marianmt_es_en_load_model import marianmt_es_en_load_model +from python.functions.datascience.translate_es_to_en import translate_es_to_en + +tokenizer, model = marianmt_es_en_load_model() +translated = translate_es_to_en("Pablo Isla es presidente de Inditex.", tokenizer, model) +# "Pablo Isla is president of Inditex." +``` + +## Tamanio y latencia + +- `Helsinki-NLP/opus-mt-es-en`: ~300 MB en disco. +- Primera carga: 5-15 s en CPU. +- Inferencia CPU: 0.5-2 s por frase. +- GPU: mucho mas rapido. + +## Uso como preprocesador para REBEL + +``` +texto ES -> marianmt_es_en -> texto EN -> rebel_load_model -> triplets +``` + +Esta pipeline permite usar REBEL (Apache 2.0, solo EN) con textos en espanol. +Alternativa directa: usar mrebel_load_model (CC BY-NC-SA, multilingue). diff --git a/python/functions/datascience/marianmt_es_en_load_model.py b/python/functions/datascience/marianmt_es_en_load_model.py new file mode 100644 index 00000000..6756308f --- /dev/null +++ b/python/functions/datascience/marianmt_es_en_load_model.py @@ -0,0 +1,54 @@ +"""Carga (y cachea) el modelo MarianMT para traduccion ES -> EN.""" + +from __future__ import annotations + +from typing import Any + +# Cache global: model_name -> (tokenizer, model) +_MODEL_CACHE: dict[str, tuple[Any, Any]] = {} + + +def marianmt_es_en_load_model( + model_name: str = "Helsinki-NLP/opus-mt-es-en", +) -> tuple[Any, Any]: + """Loads (and caches) a MarianMT model for Spanish-to-English translation. + + MarianMT is a lightweight seq2seq translation model (~300 MB) from + Helsinki-NLP, trained on the OPUS parallel corpus. + + LICENSE: Apache 2.0 — commercial use permitted. + + The first call downloads the model from HuggingFace Hub (~300 MB). + Subsequent calls with the same ``model_name`` return the cached instance. + + Args: + model_name: HuggingFace Hub model ID. Default is the ES->EN model. + Other available models follow the pattern + ``Helsinki-NLP/opus-mt-{src}-{tgt}``. + + Returns: + Tuple ``(tokenizer, model)`` both ready for inference with + ``model.generate(...)`` and ``tokenizer.decode(...)``. + + Raises: + ImportError: if ``transformers`` is not installed. + OSError: if the model cannot be downloaded or loaded from disk. + """ + cached = _MODEL_CACHE.get(model_name) + if cached is not None: + return cached + + try: + from transformers import MarianMTModel, MarianTokenizer + except ImportError as exc: + raise ImportError( + "transformers no esta instalado. Instalalo con " + "`uv pip install transformers` o `uv pip install -e '.[nlp]'`." + ) from exc + + tokenizer = MarianTokenizer.from_pretrained(model_name) + model = MarianMTModel.from_pretrained(model_name) + model.eval() + + _MODEL_CACHE[model_name] = (tokenizer, model) + return tokenizer, model diff --git a/python/functions/datascience/mrebel_base_load_model.md b/python/functions/datascience/mrebel_base_load_model.md new file mode 100644 index 00000000..e9339db6 --- /dev/null +++ b/python/functions/datascience/mrebel_base_load_model.md @@ -0,0 +1,56 @@ +--- +name: mrebel_base_load_model +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def mrebel_base_load_model(model_name: str = 'Babelscape/mrebel-base', src_lang: str = 'es_XX', tgt_lang: str = 'tp_XX') -> tuple[Any, Any]" +description: "Variante rapida de mrebel_load_model con checkpoint base (250M params, ~900 MB). Delega completamente en mrebel_load_model. Misma licencia CC BY-NC-SA 4.0 — solo uso no comercial." +tags: [mrebel, relation-extraction, nlp, model, huggingface, multilingual, seq2seq, datascience, python] +uses_functions: [mrebel_load_model_py_datascience] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +params: + - name: model_name + desc: "ID del modelo en HuggingFace Hub (defecto: Babelscape/mrebel-base, 250M params)" + - name: src_lang + desc: "codigo de idioma fuente para el tokenizer mBART: 'es_XX' (ES), 'en_XX' (EN), etc." + - name: tgt_lang + desc: "token de idioma destino del decoder — siempre 'tp_XX'" +output: "tupla (tokenizer, model) listos para inferencia, cacheados por (model_name, src_lang) en la cache compartida de mrebel_load_model." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/datascience/mrebel_base_load_model.py" +notes: | + LICENCIA: Babelscape/mrebel-base esta bajo CC BY-NC-SA 4.0 (Creative Commons + Non-Commercial Share-Alike). Solo uso no comercial. NO usar en productos comerciales. + + Esta funcion es un thin wrapper — NO duplica logica de carga/cache. Toda la + logica vive en mrebel_load_model. Util para benchmarks donde se quiere comparar + base vs large con la misma interfaz. + + La cache es compartida con mrebel_load_model (mismo dict _MODEL_CACHE del modulo). +--- + +## Ejemplo + +```python +from python.functions.datascience.mrebel_base_load_model import mrebel_base_load_model + +# 250M params vs 600M — misma interfaz +tokenizer, model = mrebel_base_load_model(src_lang="es_XX") +``` + +## Comparacion base vs large + +| Variant | Params | Size | Latencia CPU/frase | Recall tipico | +|---------|--------|------|-------------------|---------------| +| mrebel-large | 600M | ~2.4 GB | 15-30 s | alto | +| mrebel-base | 250M | ~900 MB | 5-10 s | medio | + +Para benchmarks de velocidad en graph_explorer, usar base. Para produccion final, evaluar large. diff --git a/python/functions/datascience/mrebel_base_load_model.py b/python/functions/datascience/mrebel_base_load_model.py new file mode 100644 index 00000000..f8fd825f --- /dev/null +++ b/python/functions/datascience/mrebel_base_load_model.py @@ -0,0 +1,41 @@ +"""Carga (y cachea) el modelo mREBEL-base (variante rapida, 250M params).""" + +from __future__ import annotations + +from typing import Any + +from python.functions.datascience.mrebel_load_model import mrebel_load_model + + +def mrebel_base_load_model( + model_name: str = "Babelscape/mrebel-base", + src_lang: str = "es_XX", + tgt_lang: str = "tp_XX", +) -> tuple[Any, Any]: + """Loads (and caches) the mREBEL-base tokenizer and model. + + Thin wrapper over ``mrebel_load_model`` with the base checkpoint as + default (250M params, ~900 MB). Faster than the large variant at the + cost of some recall on complex sentences. + + LICENSE NOTICE: Babelscape/mrebel-base is licensed under CC BY-NC-SA 4.0 + (Creative Commons Non-Commercial Share-Alike). Do NOT use in commercial + products without replacing this model. + + Args: + model_name: HuggingFace Hub model ID. Defaults to the base checkpoint. + src_lang: Source language code for the mBART tokenizer. + tgt_lang: Target language token for the decoder (always ``"tp_XX"``). + + Returns: + Tuple ``(tokenizer, model)`` ready for inference. + + Raises: + ImportError: if ``transformers`` is not installed. + OSError: if the model cannot be downloaded or loaded from disk. + """ + return mrebel_load_model( + model_name=model_name, + src_lang=src_lang, + tgt_lang=tgt_lang, + ) diff --git a/python/functions/datascience/mrebel_load_model.md b/python/functions/datascience/mrebel_load_model.md new file mode 100644 index 00000000..f3a9ce21 --- /dev/null +++ b/python/functions/datascience/mrebel_load_model.md @@ -0,0 +1,76 @@ +--- +name: mrebel_load_model +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def mrebel_load_model(model_name: str = 'Babelscape/mrebel-large', src_lang: str = 'es_XX', tgt_lang: str = 'tp_XX') -> tuple[Any, Any]" +description: "Carga (y cachea) el tokenizer y modelo mREBEL (mBART-based, ~600M params, ~2.4 GB). Multilingue 30+ idiomas. Cache por (model_name, src_lang). Primera llamada descarga de HuggingFace. LICENCIA CC BY-NC-SA 4.0 — solo uso no comercial." +tags: [mrebel, relation-extraction, nlp, model, huggingface, multilingual, seq2seq, datascience, python] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [transformers] +params: + - name: model_name + desc: "ID del modelo en HuggingFace Hub (defecto: Babelscape/mrebel-large, 600M params)" + - name: src_lang + desc: "codigo de idioma fuente para el tokenizer mBART: 'es_XX' (ES), 'en_XX' (EN), 'fr_XX' (FR), etc." + - name: tgt_lang + desc: "token de idioma destino del decoder — siempre 'tp_XX' para el formato triplet de mREBEL" +output: "tupla (tokenizer, model) listos para inferencia. Cacheados por (model_name, src_lang)." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/datascience/mrebel_load_model.py" +notes: | + LICENCIA: Babelscape/mrebel-large esta bajo CC BY-NC-SA 4.0 (Creative Commons + Non-Commercial Share-Alike). Solo uso no comercial. NO usar en productos + comerciales sin sustituir por un modelo con licencia comercial. + + impure: descarga red/disco la primera vez, mantiene estado en _MODEL_CACHE. + No necesita el patch HF kwargs de glirel — AutoModelForSeq2SeqLM es path estandar. + Cache es por (model_name, src_lang): dos idiomas distintos crean dos instancias + porque el tokenizer tiene src_lang hardcodeado. +--- + +## Ejemplo + +```python +from python.functions.datascience.mrebel_load_model import mrebel_load_model +from python.functions.datascience.parse_rebel_output import parse_rebel_output + +tokenizer, model = mrebel_load_model(src_lang="es_XX") + +text = "Pablo Isla es el presidente de Inditex." +inputs = tokenizer(text, return_tensors="pt", max_length=512, truncation=True) +generated = model.generate(**inputs, num_beams=4, length_penalty=1.0, max_length=256) +decoded = tokenizer.decode(generated[0], skip_special_tokens=False) +triplets = parse_rebel_output(decoded) +``` + +## Tamanio y latencia + +- `Babelscape/mrebel-large`: ~2.4 GB en disco (modelo + tokenizer). +- Primera carga: 30-90 s en CPU, depende de red y disco. +- Inferencia CPU: 5-15 s por frase (mBART es mas lento que REBEL/BART). +- Inferencia GPU (CUDA T4): 0.5-2 s por frase. + +## Idiomas soportados + +mREBEL soporta los idiomas de mBART-50. Ejemplos: +- `es_XX` — Espanol +- `en_XX` — Ingles +- `fr_XX` — Frances +- `de_DE` — Aleman +- `pt_XX` — Portugues +- `it_IT` — Italiano + +## Notas + +- Para ingles y usos comerciales, usar `rebel_load_model` (Apache 2.0). +- Para benchmarks rapidos, usar `mrebel_base_load_model` (250M params, misma licencia). +- `model.eval()` se llama al cargar para desactivar dropout en inferencia. diff --git a/python/functions/datascience/mrebel_load_model.py b/python/functions/datascience/mrebel_load_model.py new file mode 100644 index 00000000..d68899b6 --- /dev/null +++ b/python/functions/datascience/mrebel_load_model.py @@ -0,0 +1,69 @@ +"""Carga (y cachea) el modelo mREBEL para extraccion de relaciones multilingue.""" + +from __future__ import annotations + +from typing import Any + +# Cache global: (model_name, src_lang) -> (tokenizer, model) +_MODEL_CACHE: dict[tuple[str, str], tuple[Any, Any]] = {} + + +def mrebel_load_model( + model_name: str = "Babelscape/mrebel-large", + src_lang: str = "es_XX", + tgt_lang: str = "tp_XX", +) -> tuple[Any, Any]: + """Loads (and caches) the mREBEL tokenizer and model. + + mREBEL is a multilingual seq2seq model (mBART-based, ~600M params, ~2.4 GB) + for relation extraction. It supports 30+ languages via language codes + (``src_lang``). + + LICENSE NOTICE: Babelscape/mrebel-large is licensed under CC BY-NC-SA 4.0 + (Creative Commons Non-Commercial Share-Alike). Do NOT use in commercial + products without replacing this model with a commercially-licensed + alternative (e.g. Babelscape/rebel-large which is Apache 2.0 but + English-only). + + The first call downloads the model from HuggingFace Hub (~2.4 GB). + Subsequent calls with the same ``(model_name, src_lang)`` return the + cached instance without re-loading. + + Args: + model_name: HuggingFace Hub model ID. Default is the large variant. + src_lang: Source language code for the mBART tokenizer, e.g. + ``"es_XX"`` (Spanish), ``"en_XX"`` (English), ``"fr_XX"`` (French). + tgt_lang: Target language token for the decoder (always ``"tp_XX"`` + for the triplet format — only change if using a custom checkpoint). + + Returns: + Tuple ``(tokenizer, model)`` both ready for inference with + ``model.generate(...)`` and ``tokenizer.decode(...)``. + + Raises: + ImportError: if ``transformers`` is not installed. + OSError: if the model cannot be downloaded or loaded from disk. + """ + cache_key = (model_name, src_lang) + cached = _MODEL_CACHE.get(cache_key) + if cached is not None: + return cached + + try: + from transformers import AutoModelForSeq2SeqLM, AutoTokenizer + except ImportError as exc: + raise ImportError( + "transformers no esta instalado. Instalalo con " + "`uv pip install transformers` o `uv pip install -e '.[nlp]'`." + ) from exc + + tokenizer = AutoTokenizer.from_pretrained( + model_name, + src_lang=src_lang, + tgt_lang=tgt_lang, + ) + model = AutoModelForSeq2SeqLM.from_pretrained(model_name) + model.eval() + + _MODEL_CACHE[cache_key] = (tokenizer, model) + return tokenizer, model diff --git a/python/functions/datascience/parse_rebel_output.md b/python/functions/datascience/parse_rebel_output.md new file mode 100644 index 00000000..ca29a719 --- /dev/null +++ b/python/functions/datascience/parse_rebel_output.md @@ -0,0 +1,65 @@ +--- +name: parse_rebel_output +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def parse_rebel_output(decoded_text: str) -> list[dict]" +description: "Parser puro del wire format de REBEL / mREBEL. Convierte la cadena decoded por el tokenizer (con skip_special_tokens=False) a una lista de triplets tipados {head, head_type, type, tail, tail_type}. Nunca lanza excepcion." +tags: [rebel, mrebel, relation-extraction, nlp, parser, knowledge-graph, datascience, python] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +params: + - name: decoded_text + desc: "cadena raw producida por tokenizer.decode(..., skip_special_tokens=False) — incluye tokens especiales como , , , , tp_XX, etc." +output: "lista de dicts con claves head (str), head_type (str), type (str), tail (str), tail_type (str). Lista vacia si no hay triplets completos o el input es vacio." +tested: true +tests: + - "string vacio retorna lista vacia" + - "un triplet completo retorna un dict con campos correctos" + - "dos triplets retorna dos dicts" + - "triplet incompleto sin cierre no rompe" + - "tokens angulares desconocidos no lanzan excepcion" +test_file_path: "python/functions/datascience/tests/test_parse_rebel_output.py" +file_path: "python/functions/datascience/parse_rebel_output.py" +notes: | + Funcion pura. Adapta el parser oficial del README de Babelscape/rebel al estilo del registry. + Compatible con mREBEL (prefijo tp_XX, lang token __es__, __en__) y REBEL (sin prefijo de idioma). + El formato wire incluye para separar triplets y tokens para cerrar spans + de head/tail. El estado de la maquina es: t=leyendo head, s=leyendo tail, o=leyendo relacion. +--- + +## Ejemplo + +```python +from python.functions.datascience.parse_rebel_output import parse_rebel_output + +decoded = "tp_XX Pablo Isla Inditex employer" +triplets = parse_rebel_output(decoded) +# [{'head': 'Pablo Isla', 'head_type': 'per', 'type': 'employer', +# 'tail': 'Inditex', 'tail_type': 'org'}] +``` + +## Formato wire REBEL / mREBEL + +``` +tp_XX HEAD_TOKENS TAIL_TOKENS RELATION_TOKENS ... +``` + +- `` — marca el inicio de un nuevo triplet (y cierra el anterior). +- `` — cierra el span del head y abre el span del tail. +- `` — cierra el span del tail y abre el span de la relacion. +- El ultimo triplet se cierra con `` (ya eliminado antes del split). + +## Notas + +- No valida ni filtra los `head_type`/`tail_type` — los devuelve tal cual emite el modelo. +- Compatible con cualquier variante seq2seq que use el mismo wire format (Babelscape/rebel, + Babelscape/mrebel-large, Babelscape/mrebel-base). +- Para usar el output en el grafo, pasar por `align_relations_to_entities` que resuelve + head/tail a nombres canonicos del conjunto de entidades conocido. diff --git a/python/functions/datascience/parse_rebel_output.py b/python/functions/datascience/parse_rebel_output.py new file mode 100644 index 00000000..12f3b255 --- /dev/null +++ b/python/functions/datascience/parse_rebel_output.py @@ -0,0 +1,105 @@ +"""Parser puro del wire format de REBEL / mREBEL.""" + +from __future__ import annotations + + +def parse_rebel_output(decoded_text: str) -> list[dict]: + """Parse REBEL / mREBEL decoded output into typed triplets. + + The input is the string produced by the HuggingFace tokenizer with + ``skip_special_tokens=False``, e.g.:: + + tp_XX Pablo Isla Inditex employer ... + + Args: + decoded_text: Raw decoded string from the seq2seq model, including + special tokens like ````, ````, ````, + ````, ````, etc. + + Returns: + List of dicts with keys: + ``head`` (str), ``head_type`` (str), + ``type`` (str), ``tail`` (str), ``tail_type`` (str). + Returns an empty list on empty input or if no complete triplet is + found. Never raises. + """ + if not decoded_text or not decoded_text.strip(): + return [] + + triplets: list[dict] = [] + + # Strip language / padding tokens common to mREBEL. + text = ( + decoded_text + .replace("", "") + .replace("", "") + .replace("", "") + .replace("tp_XX", "") + .replace("__en__", "") + .strip() + ) + + current = "x" # x=init, t=head span, s=tail span, o=relation span + subject = "" + relation = "" + object_ = "" + object_type = "" + subject_type = "" + + for token in text.split(): + if token in ("", ""): + current = "t" + if relation: + triplets.append( + { + "head": subject.strip(), + "head_type": subject_type, + "type": relation.strip(), + "tail": object_.strip(), + "tail_type": object_type, + } + ) + relation = "" + subject = "" + elif token.startswith("<") and token.endswith(">"): + if current in ("t", "o"): + # Closing the head span — now reading tail. + current = "s" + if relation: + triplets.append( + { + "head": subject.strip(), + "head_type": subject_type, + "type": relation.strip(), + "tail": object_.strip(), + "tail_type": object_type, + } + ) + object_ = "" + subject_type = token[1:-1] + else: + # Closing the tail span — now reading relation. + current = "o" + object_type = token[1:-1] + relation = "" + else: + if current == "t": + subject += " " + token + elif current == "s": + object_ += " " + token + elif current == "o": + relation += " " + token + + # Flush the last triplet if all fields are present. + if subject and relation and object_ and object_type and subject_type: + triplets.append( + { + "head": subject.strip(), + "head_type": subject_type, + "type": relation.strip(), + "tail": object_.strip(), + "tail_type": object_type, + } + ) + + return triplets diff --git a/python/functions/datascience/plot_heatmap_log.md b/python/functions/datascience/plot_heatmap_log.md new file mode 100644 index 00000000..aff522d5 --- /dev/null +++ b/python/functions/datascience/plot_heatmap_log.md @@ -0,0 +1,64 @@ +--- +name: plot_heatmap_log +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def plot_heatmap_log(ax: Axes, xs: list[float] | np.ndarray, ys: list[float] | np.ndarray, extent: tuple[float, float, float, float], bins: int = 200, cmap: str = 'hot', alpha: float = 0.6) -> None" +description: "Dibuja un heatmap 2D con escala log1p sobre un Axes de matplotlib. Usa np.histogram2d con el extent dado y ax.imshow para renderizar." +tags: [visualization, heatmap, histogram, matplotlib, datascience, log] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["numpy", "matplotlib"] +params: + - name: ax + desc: "matplotlib Axes sobre el que se dibuja el heatmap." + - name: xs + desc: "Coordenadas X de los puntos." + - name: ys + desc: "Coordenadas Y de los puntos." + - name: extent + desc: "Bounding box como (minx, maxx, miny, maxy) que define el rango del histograma." + - name: bins + desc: "Número de bins del histograma en cada eje. Default 200." + - name: cmap + desc: "Nombre del colormap de matplotlib. Default 'hot'." + - name: alpha + desc: "Opacidad del overlay (0-1). Default 0.6." +output: "None. Modifica el Axes in-place añadiendo el heatmap como imagen con ax.imshow." +tested: true +tests: + - "100 puntos no lanza excepción" + - "ax tiene al menos una imagen tras la llamada" +test_file_path: "python/functions/datascience/tests/test_plot_heatmap_log.py" +file_path: "python/functions/datascience/plot_heatmap_log.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/examples/generar_reporte_madrid.py:62" +--- + +## Ejemplo + +```python +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +from datascience.plot_heatmap_log import plot_heatmap_log + +rng = np.random.default_rng(42) +xs = rng.uniform(-4.0, -3.5, 500) +ys = rng.uniform(40.3, 40.6, 500) + +fig, ax = plt.subplots() +plot_heatmap_log(ax, xs, ys, extent=(-4.0, -3.5, 40.3, 40.6), bins=100) +fig.savefig("heatmap.png") +``` + +## Notas + +Aplica `np.log1p` a las cuentas del histograma para comprimir el rango dinámico y hacer visibles tanto zonas densas como dispersas. El histograma se transpone (`counts.T`) antes de pasar a imshow para alinear correctamente los ejes x/y. `aspect="auto"` permite que la imagen se estire al aspecto del Axes. diff --git a/python/functions/datascience/plot_heatmap_log.py b/python/functions/datascience/plot_heatmap_log.py new file mode 100644 index 00000000..fcc6d880 --- /dev/null +++ b/python/functions/datascience/plot_heatmap_log.py @@ -0,0 +1,53 @@ +"""Plot a log-scale 2D histogram heatmap on a matplotlib Axes.""" + +from __future__ import annotations + + +def plot_heatmap_log( + ax: "Axes", + xs: "list[float] | np.ndarray", + ys: "list[float] | np.ndarray", + extent: "tuple[float, float, float, float]", + bins: int = 200, + cmap: str = "hot", + alpha: float = 0.6, +) -> None: + """Plot a log-scale 2D density heatmap using histogram binning. + + Computes a 2D histogram over the given points within ``extent``, applies + log1p to compress the dynamic range, and renders the result as an image + overlay on the Axes. + + Args: + ax: matplotlib Axes to draw on. + xs: X coordinates (longitude or projected x). + ys: Y coordinates (latitude or projected y). + extent: Bounding box as (minx, maxx, miny, maxy). + bins: Number of histogram bins along each axis. Default 200. + cmap: Matplotlib colormap name. Default "hot". + alpha: Opacity of the heatmap overlay (0–1). Default 0.6. + """ + import numpy as np # type: ignore + + xs_arr = np.asarray(xs, dtype=float) + ys_arr = np.asarray(ys, dtype=float) + + minx, maxx, miny, maxy = extent + + counts, _xedges, _yedges = np.histogram2d( + xs_arr, + ys_arr, + bins=bins, + range=[[minx, maxx], [miny, maxy]], + ) + + log_counts = np.log1p(counts.T) + + ax.imshow( + log_counts, + extent=[minx, maxx, miny, maxy], + origin="lower", + cmap=cmap, + alpha=alpha, + aspect="auto", + ) diff --git a/python/functions/datascience/plot_kde_2d.md b/python/functions/datascience/plot_kde_2d.md new file mode 100644 index 00000000..aa6a25fc --- /dev/null +++ b/python/functions/datascience/plot_kde_2d.md @@ -0,0 +1,66 @@ +--- +name: plot_kde_2d +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def plot_kde_2d(ax: Axes, xs: list[float] | np.ndarray, ys: list[float] | np.ndarray, cmap: str = 'magma', alpha: float = 0.35, thresh: float = 0.02, levels: int = 30, bw_adjust: float = 0.6) -> None" +description: "Dibuja un KDE 2D como contornos rellenos sobre un Axes de matplotlib usando seaborn.kdeplot. Si los arrays están vacíos retorna sin pintar." +tags: [visualization, kde, density, seaborn, matplotlib, datascience] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["numpy", "seaborn", "matplotlib"] +params: + - name: ax + desc: "matplotlib Axes sobre el que se dibuja la densidad." + - name: xs + desc: "Coordenadas X de los puntos (longitud o x proyectada)." + - name: ys + desc: "Coordenadas Y de los puntos (latitud o y proyectada)." + - name: cmap + desc: "Nombre del colormap de matplotlib para el relleno de densidad. Default 'magma'." + - name: alpha + desc: "Opacidad del overlay de densidad (0-1). Default 0.35." + - name: thresh + desc: "Umbral de densidad por debajo del cual no se dibujan contornos (0-1). Default 0.02." + - name: levels + desc: "Número de niveles de contorno. Default 30." + - name: bw_adjust + desc: "Factor de ajuste del ancho de banda del kernel. Valores < 1 producen estimaciones más detalladas. Default 0.6." +output: "None. Modifica el Axes in-place añadiendo los contornos de densidad." +tested: true +tests: + - "50 puntos aleatorios no lanza excepción" + - "arrays vacíos retorna sin error" +test_file_path: "python/functions/datascience/tests/test_plot_kde_2d.py" +file_path: "python/functions/datascience/plot_kde_2d.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/recomendador_centros.py:275" +--- + +## Ejemplo + +```python +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np +from datascience.plot_kde_2d import plot_kde_2d + +rng = np.random.default_rng(42) +xs = rng.normal(0, 1, 200) +ys = rng.normal(0, 1, 200) + +fig, ax = plt.subplots() +plot_kde_2d(ax, xs, ys, cmap="viridis", alpha=0.5) +fig.savefig("kde.png") +``` + +## Notas + +Requiere seaborn y numpy. El parámetro `fill=True` se pasa a seaborn.kdeplot para renderizar contornos rellenos (disponible desde seaborn 0.11). Arrays vacíos se detectan con `np.asarray(xs).size == 0` antes de llamar a seaborn para evitar errores internos. diff --git a/python/functions/datascience/plot_kde_2d.py b/python/functions/datascience/plot_kde_2d.py new file mode 100644 index 00000000..be466fae --- /dev/null +++ b/python/functions/datascience/plot_kde_2d.py @@ -0,0 +1,53 @@ +"""Plot a 2D KDE density overlay on a matplotlib Axes using seaborn.""" + +from __future__ import annotations + + +def plot_kde_2d( + ax: "Axes", + xs: "list[float] | np.ndarray", + ys: "list[float] | np.ndarray", + cmap: str = "magma", + alpha: float = 0.35, + thresh: float = 0.02, + levels: int = 30, + bw_adjust: float = 0.6, +) -> None: + """Plot a 2D kernel density estimate as a filled contour overlay. + + Uses seaborn.kdeplot to render a smooth density surface over the given + scatter of (x, y) points. If either array is empty the function returns + immediately without painting anything. + + Args: + ax: matplotlib Axes to draw on. + xs: X coordinates (longitude or projected x). + ys: Y coordinates (latitude or projected y). + cmap: Matplotlib colormap name for the density fill. Default "magma". + alpha: Opacity of the density overlay (0–1). Default 0.35. + thresh: Density threshold below which contours are not drawn (0–1). + Default 0.02 removes very sparse outlier contours. + levels: Number of contour levels. Default 30. + bw_adjust: Bandwidth adjustment factor for the kernel. Values < 1 + produce tighter, more detailed estimates. Default 0.6. + """ + import numpy as np # type: ignore + import seaborn as sns # type: ignore + + xs_arr = np.asarray(xs) + ys_arr = np.asarray(ys) + + if xs_arr.size == 0 or ys_arr.size == 0: + return + + sns.kdeplot( + x=xs_arr, + y=ys_arr, + ax=ax, + cmap=cmap, + fill=True, + alpha=alpha, + thresh=thresh, + levels=levels, + bw_adjust=bw_adjust, + ) diff --git a/python/functions/datascience/rebel_load_model.md b/python/functions/datascience/rebel_load_model.md new file mode 100644 index 00000000..b5791294 --- /dev/null +++ b/python/functions/datascience/rebel_load_model.md @@ -0,0 +1,65 @@ +--- +name: rebel_load_model +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def rebel_load_model(model_name: str = 'Babelscape/rebel-large') -> tuple[Any, Any]" +description: "Carga (y cachea) el tokenizer y modelo REBEL (BART-based, ~1.5 GB). Solo ingles. Licencia Apache 2.0 — uso comercial permitido. Cache por model_name." +tags: [rebel, relation-extraction, nlp, model, huggingface, english, seq2seq, apache2, datascience, python] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [transformers] +params: + - name: model_name + desc: "ID del modelo en HuggingFace Hub (defecto: Babelscape/rebel-large, BART ~1.5 GB, solo EN)" +output: "tupla (tokenizer, model) listos para inferencia, cacheados por model_name." +tested: false +tests: [] +test_file_path: "" +file_path: "python/functions/datascience/rebel_load_model.py" +notes: | + LICENCIA: Apache 2.0 — uso comercial permitido (a diferencia de mREBEL que es CC BY-NC-SA). + Solo funciona bien con texto en INGLES. Para espanol usar mrebel_load_model. + + REBEL usa el mismo wire format que mREBEL, por lo que parse_rebel_output es compatible. + Diferencia vs mREBEL: no emite el prefijo tp_XX de idioma en el output (parse_rebel_output + lo maneja porque ya hace .replace('tp_XX', '')). + + impure: descarga red/disco la primera vez, mantiene estado en _MODEL_CACHE. + Cache separada de mrebel_load_model (modulo distinto). +--- + +## Ejemplo + +```python +from python.functions.datascience.rebel_load_model import rebel_load_model +from python.functions.datascience.parse_rebel_output import parse_rebel_output + +tokenizer, model = rebel_load_model() + +text = "Pablo Isla is the CEO of Inditex, based in Arteixo." +inputs = tokenizer(text, return_tensors="pt", max_length=512, truncation=True) +generated = model.generate(**inputs, num_beams=4, length_penalty=1.0, max_length=256) +decoded = tokenizer.decode(generated[0], skip_special_tokens=False) +triplets = parse_rebel_output(decoded) +``` + +## Comparacion REBEL vs mREBEL + +| | REBEL | mREBEL | +|---|---|---| +| Licencia | Apache 2.0 (comercial OK) | CC BY-NC-SA 4.0 (no comercial) | +| Idiomas | Solo ingles | 30+ (es_XX, en_XX, fr_XX...) | +| Tamanio | ~1.5 GB | ~2.4 GB (large) / ~900 MB (base) | +| Base | BART | mBART-50 | + +## Tamanio y latencia + +- `Babelscape/rebel-large`: ~1.5 GB en disco. +- Primera carga: 20-60 s en CPU. +- Inferencia CPU: 3-10 s por frase (mas rapido que mREBEL por ser BART vs mBART). diff --git a/python/functions/datascience/rebel_load_model.py b/python/functions/datascience/rebel_load_model.py new file mode 100644 index 00000000..d7adfe51 --- /dev/null +++ b/python/functions/datascience/rebel_load_model.py @@ -0,0 +1,52 @@ +"""Carga (y cachea) el modelo REBEL para extraccion de relaciones en ingles.""" + +from __future__ import annotations + +from typing import Any + +# Cache global: model_name -> (tokenizer, model) +_MODEL_CACHE: dict[str, tuple[Any, Any]] = {} + + +def rebel_load_model( + model_name: str = "Babelscape/rebel-large", +) -> tuple[Any, Any]: + """Loads (and caches) the REBEL tokenizer and model. English only. + + REBEL is a BART-based seq2seq model (~1.5 GB) for relation extraction, + trained on English Wikipedia (KELM). It extracts triplets (head, relation, + tail) from English text. + + LICENSE: Apache 2.0 — commercial use permitted. + + The first call downloads the model from HuggingFace Hub (~1.5 GB). + Subsequent calls with the same ``model_name`` return the cached instance. + + Args: + model_name: HuggingFace Hub model ID. Default is the large variant. + + Returns: + Tuple ``(tokenizer, model)`` both ready for inference. + + Raises: + ImportError: if ``transformers`` is not installed. + OSError: if the model cannot be downloaded or loaded from disk. + """ + cached = _MODEL_CACHE.get(model_name) + if cached is not None: + return cached + + try: + from transformers import AutoModelForSeq2SeqLM, AutoTokenizer + except ImportError as exc: + raise ImportError( + "transformers no esta instalado. Instalalo con " + "`uv pip install transformers` o `uv pip install -e '.[nlp]'`." + ) from exc + + tokenizer = AutoTokenizer.from_pretrained(model_name) + model = AutoModelForSeq2SeqLM.from_pretrained(model_name) + model.eval() + + _MODEL_CACHE[model_name] = (tokenizer, model) + return tokenizer, model diff --git a/python/functions/datascience/remove_words_from_column.md b/python/functions/datascience/remove_words_from_column.md new file mode 100644 index 00000000..bc0cd79d --- /dev/null +++ b/python/functions/datascience/remove_words_from_column.md @@ -0,0 +1,52 @@ +--- +name: remove_words_from_column +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def remove_words_from_column(values: Iterable[str | None], words: list[str]) -> list[str]" +description: "Elimina palabras especificas de un iterable de strings usando regex de palabra completa (\\b). Case-insensitive. Colapsa espacios multiples y hace strip. None se convierte en cadena vacia. Sin pandas." +tags: [text, cleaning, regex, words, nlp, datascience] +params: + - name: values + desc: Iterable de strings o None a limpiar. + - name: words + desc: Lista de palabras a eliminar. Matching case-insensitive por palabra completa (no parcial). +output: "Lista de strings con las palabras eliminadas y espacios normalizados. Misma longitud que el input." +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: true +tests: + - "elimina palabras case insensitive" + - "none devuelve string vacio" + - "colapsa espacios multiples" + - "palabras vacias no modifica" + - "palabra completa no parcial" + - "lista vacia" +test_file_path: "python/functions/datascience/tests/test_remove_words_from_column.py" +file_path: "python/functions/datascience/remove_words_from_column.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "fuzzy_joins/arreglo_fuzzy.py" +--- + +## Ejemplo + +```python +from remove_words_from_column import remove_words_from_column + +result = remove_words_from_column( + ["Calle Mayor 14", "Avenida del Sol"], + words=["calle", "avenida", "del"] +) +# ["Mayor 14", "Sol"] +``` + +## Notas + +El patron regex se compila una sola vez para todo el iterable (eficiente). Usa \\b para no eliminar palabras parciales ("calle" no toca "calleja"). None en el input produce "" en el output. diff --git a/python/functions/datascience/remove_words_from_column.py b/python/functions/datascience/remove_words_from_column.py new file mode 100644 index 00000000..54824557 --- /dev/null +++ b/python/functions/datascience/remove_words_from_column.py @@ -0,0 +1,42 @@ +"""Elimina palabras especificas de una lista de strings.""" + +from __future__ import annotations + +import re +from typing import Iterable + + +def remove_words_from_column( + values: Iterable[str | None], + words: list[str], +) -> list[str]: + """Elimina palabras de una lista de strings usando regex de palabra completa. + + Para cada string aplica un patron regex \\b(w1|w2|...)\\b case-insensitive, + reemplaza por cadena vacia, colapsa espacios multiples y hace strip. + None se convierte en cadena vacia. + + Args: + values: Iterable de strings (o None) a limpiar. + words: Lista de palabras a eliminar (case-insensitive). + + Returns: + Lista de strings con las palabras eliminadas y espacios normalizados. + """ + if not words: + return [v if v is not None else "" for v in values] + + pattern = re.compile( + r"\b(" + "|".join(re.escape(w) for w in words) + r")\b", + flags=re.IGNORECASE, + ) + + result = [] + for value in values: + if value is None: + result.append("") + continue + cleaned = pattern.sub("", str(value)) + cleaned = re.sub(r"\s+", " ", cleaned).strip() + result.append(cleaned) + return result diff --git a/python/functions/datascience/spacy_es_load_model.md b/python/functions/datascience/spacy_es_load_model.md new file mode 100644 index 00000000..7dc95239 --- /dev/null +++ b/python/functions/datascience/spacy_es_load_model.md @@ -0,0 +1,61 @@ +--- +name: spacy_es_load_model +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def spacy_es_load_model(model_name: str = 'es_core_news_md') -> Any" +description: "Carga (y cachea) un modelo spaCy en castellano. Provee POS, dependencias y NER (PER, ORG, LOC, MISC). Usado por extract_triples_spacy_es para OpenIE schema-less. LICENSE: spaCy MIT + es_core_news_md CC BY-SA 4.0." +tags: [spacy, nlp, spanish, ner, dependency-parsing, openie, model, datascience, python, mit, cc-by-sa] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [spacy] +params: + - name: model_name + desc: "Nombre del modelo spaCy instalado. Default: es_core_news_md (equilibrio precision/tamanio). Alternativas: es_core_news_sm (menor, menos preciso), es_core_news_lg (mayor, mas preciso)." +output: "Instancia spaCy Language cacheada por model_name. Provee nlp(text) -> Doc con tokens, POS, deps y ents." +tested: true +tests: + - "cache devuelve la misma instancia" + - "OSError si el modelo no esta instalado" +test_file_path: "python/functions/datascience/tests/test_spacy_es_load_model.py" +file_path: "python/functions/datascience/spacy_es_load_model.py" +notes: | + LICENSE: spaCy es MIT. El modelo es_core_news_md usa pesos entrenados sobre + el corpus CoNLL-2002 (CC BY-SA 4.0). Uso comercial permitido con atribucion. + + Instalar el modelo antes de usar: + python -m spacy download es_core_news_md + + impure: carga modelo desde disco la primera vez, mantiene estado en _MODEL_CACHE. + Tamanio: es_core_news_md ~43 MB. Primera carga ~1-3s en CPU. +--- + +## Ejemplo + +```python +from datascience.spacy_es_load_model import spacy_es_load_model + +nlp = spacy_es_load_model() + +doc = nlp("Carlos Torres preside BBVA en Bilbao.") +for ent in doc.ents: + print(ent.text, ent.label_) +# Carlos Torres PER +# BBVA ORG +# Bilbao LOC +``` + +## Instalacion + +```bash +# En el venv del registry: +python/.venv/bin/python3 -m spacy download es_core_news_md + +# O via uv: +cd python && uv run python -m spacy download es_core_news_md +``` diff --git a/python/functions/datascience/spacy_es_load_model.py b/python/functions/datascience/spacy_es_load_model.py new file mode 100644 index 00000000..9dae7c1b --- /dev/null +++ b/python/functions/datascience/spacy_es_load_model.py @@ -0,0 +1,40 @@ +"""Carga (y cachea) un modelo spaCy en castellano para NER y OpenIE. + +LICENSE: spaCy = MIT. Modelo es_core_news_md = CC BY-SA 4.0 (datos CoNLL-2002). +""" + +from __future__ import annotations + +from typing import Any + +# Cache global: model_name -> instancia spaCy nlp +_MODEL_CACHE: dict[str, Any] = {} + + +def spacy_es_load_model(model_name: str = "es_core_news_md") -> Any: + """Load (and cache) a spaCy Spanish language model. + + The model provides dependency parsing, POS tagging and NER (PER, ORG, LOC, MISC). + Used by extract_triples_spacy_es for schema-less OpenIE in Spanish. + + LICENSE: spaCy = MIT. es_core_news_md = CC BY-SA 4.0 (CoNLL-2002 corpus). + + Args: + model_name: Name of the spaCy model. Default: es_core_news_md. + Alternatives: es_core_news_sm (smaller), es_core_news_lg (larger). + + Returns: + spaCy Language instance cached by model_name. + + Raises: + OSError: If the model is not installed. Install with: + python -m spacy download es_core_news_md + """ + if model_name in _MODEL_CACHE: + return _MODEL_CACHE[model_name] + + import spacy # type: ignore[import] + + nlp = spacy.load(model_name) + _MODEL_CACHE[model_name] = nlp + return nlp diff --git a/python/functions/datascience/summary_stats.md b/python/functions/datascience/summary_stats.md new file mode 100644 index 00000000..1e8b6fbd --- /dev/null +++ b/python/functions/datascience/summary_stats.md @@ -0,0 +1,38 @@ +--- +id: summary_stats_py_datascience +name: summary_stats +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def summary_stats(values: list[float]) -> dict" +description: "Returns basic descriptive statistics (n, mean, median, p25, p75) for a list of floats. Empty input returns n=0 and nan for all numeric fields." +tags: [statistics, descriptive, eda, summary, percentile] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [math, numpy] +example: | + from summary_stats import summary_stats + result = summary_stats([1, 2, 3, 4, 5]) +tested: true +tests: + - "test_summary_stats_basic" + - "test_summary_stats_empty" + - "test_summary_stats_single" + - "test_summary_stats_keys" +test_file_path: "python/functions/datascience/tests/test_summary_stats.py" +file_path: "python/functions/datascience/summary_stats.py" +params: + - name: values + desc: "List of numeric values to summarize." +output: "Dict with n (int), mean, median, p25, p75 (floats). All floats are math.nan when values is empty." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/example/models/eda/utils.py:60" +--- + +Funcion pura minimal para EDA rapido. No incluye std, min, max ni otros percentiles — mantener la interfaz pequena. diff --git a/python/functions/datascience/summary_stats.py b/python/functions/datascience/summary_stats.py new file mode 100644 index 00000000..4206ad18 --- /dev/null +++ b/python/functions/datascience/summary_stats.py @@ -0,0 +1,36 @@ +"""summary_stats — Compute descriptive statistics for a numeric list.""" + +import math +import numpy as np + + +def summary_stats(values: list[float]) -> dict: + """Return basic descriptive statistics for a list of floats. + + Args: + values: List of numeric values. + + Returns: + Dict with keys: + "n" (int): number of elements. + "mean" (float): arithmetic mean, or math.nan if empty. + "median" (float): median, or math.nan if empty. + "p25" (float): 25th percentile, or math.nan if empty. + "p75" (float): 75th percentile, or math.nan if empty. + """ + if not values: + return { + "n": 0, + "mean": math.nan, + "median": math.nan, + "p25": math.nan, + "p75": math.nan, + } + arr = np.array(values, dtype=float) + return { + "n": int(len(arr)), + "mean": float(np.mean(arr)), + "median": float(np.median(arr)), + "p25": float(np.percentile(arr, 25)), + "p75": float(np.percentile(arr, 75)), + } diff --git a/python/functions/datascience/tests/test_align_relations_to_entities.py b/python/functions/datascience/tests/test_align_relations_to_entities.py new file mode 100644 index 00000000..777050b2 --- /dev/null +++ b/python/functions/datascience/tests/test_align_relations_to_entities.py @@ -0,0 +1,103 @@ +"""Tests para align_relations_to_entities.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.datascience.align_relations_to_entities import align_relations_to_entities + + +def _t(head, head_type, relation, tail, tail_type): + return { + "head": head, + "head_type": head_type, + "type": relation, + "tail": tail, + "tail_type": tail_type, + } + + +def test_match_exacto_case_insensitive_resuelve_correctamente(): + triplets = [_t("pablo isla", "per", "employer", "inditex", "org")] + entities = ["Pablo Isla", "Inditex"] + result = align_relations_to_entities(triplets, entities) + assert len(result) == 1 + assert result[0]["from"] == "Pablo Isla" + assert result[0]["to"] == "Inditex" + assert result[0]["kind"] == "employer" + + +def test_substring_entity_en_span_del_head(): + # mREBEL emite "esta en Bilbao" pero la entidad es "Bilbao" + triplets = [_t("esta en Bilbao", "loc", "located in", "Espana", "loc")] + entities = ["Bilbao", "Espana"] + result = align_relations_to_entities(triplets, entities) + assert len(result) == 1 + assert result[0]["from"] == "Bilbao" + assert result[0]["to"] == "Espana" + + +def test_substring_span_dentro_del_nombre_de_entidad(): + # El span "Santander" esta contenido en el entity name "Banco Santander" + triplets = [_t("Santander", "org", "owns", "Openbank", "org")] + entities = ["Banco Santander", "Openbank"] + result = align_relations_to_entities(triplets, entities) + assert len(result) == 1 + assert result[0]["from"] == "Banco Santander" + assert result[0]["to"] == "Openbank" + + +def test_gana_nombre_de_entidad_mas_largo_en_ambiguedad(): + # Dos entidades: "Madrid" y "Comunidad de Madrid". El span "Madrid" deberia + # preferir "Comunidad de Madrid" si ese es el mas largo y contiene "madrid". + # En la logica actual: substring bidireccional, gana el primero de names_by_len + # (que ordena DESC por len). "Comunidad de Madrid" es mas largo y su lower + # contiene "madrid", asi que gana. + triplets = [_t("Madrid", "loc", "capital of", "Espana", "loc")] + entities = ["Madrid", "Comunidad de Madrid", "Espana"] + result = align_relations_to_entities(triplets, entities) + assert len(result) == 1 + # El exacto case-insensitive resuelve "Madrid" -> "Madrid" directamente + # (antes que la busqueda substring). Verificamos que no rompe y que + # from/to son valores de entities. + assert result[0]["from"] in entities + assert result[0]["to"] in entities + + +def test_triplet_sin_match_se_descarta(): + triplets = [_t("Unknown Entity", "per", "works for", "Another Unknown", "org")] + entities = ["Pablo Isla", "Inditex"] + result = align_relations_to_entities(triplets, entities) + assert result == [] + + +def test_triplet_con_head_igual_tail_se_descarta_self_loop(): + triplets = [_t("Inditex", "org", "owns", "Inditex", "org")] + entities = ["Inditex", "Zara"] + result = align_relations_to_entities(triplets, entities) + assert result == [] + + +def test_lista_triplets_vacia_retorna_vacia(): + result = align_relations_to_entities([], ["Pablo Isla", "Inditex"]) + assert result == [] + + +def test_lista_entity_names_vacia_retorna_vacia(): + triplets = [_t("Pablo Isla", "per", "employer", "Inditex", "org")] + result = align_relations_to_entities(triplets, []) + assert result == [] + + +def test_multiples_triplets_con_mezcla_de_matches_y_descartes(): + triplets = [ + _t("Pablo Isla", "per", "employer", "Inditex", "org"), # match + _t("Ghost Entity", "per", "employer", "Inditex", "org"), # head sin match + _t("Pablo Isla", "per", "employer", "Pablo Isla", "per"), # self-loop + ] + entities = ["Pablo Isla", "Inditex"] + result = align_relations_to_entities(triplets, entities) + assert len(result) == 1 + assert result[0]["from"] == "Pablo Isla" + assert result[0]["to"] == "Inditex" diff --git a/python/functions/datascience/tests/test_alpha_shape_concave_hull.py b/python/functions/datascience/tests/test_alpha_shape_concave_hull.py new file mode 100644 index 00000000..decb7f0d --- /dev/null +++ b/python/functions/datascience/tests/test_alpha_shape_concave_hull.py @@ -0,0 +1,38 @@ +"""Tests para alpha_shape_concave_hull.""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from alpha_shape_concave_hull import alpha_shape_concave_hull + + +def test_alpha_shape_square_large_alpha(): + """4 corner points with large alpha should return a geometry.""" + pts = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] + result = alpha_shape_concave_hull(pts, alpha=10.0) + assert result is not None + + +def test_alpha_shape_too_few_points(): + result = alpha_shape_concave_hull([(0, 0), (1, 0), (0, 1)], alpha=10.0) + assert result is None + + +def test_alpha_shape_very_small_alpha_returns_none(): + """Alpha so small that no triangle circumradius fits.""" + pts = [(0.0, 0.0), (100.0, 0.0), (100.0, 100.0), (0.0, 100.0)] + result = alpha_shape_concave_hull(pts, alpha=0.0001) + assert result is None + + +def test_alpha_shape_5_points_returns_geometry(): + pts = [ + (0.0, 0.0), + (2.0, 0.0), + (2.0, 2.0), + (0.0, 2.0), + (1.0, 1.0), + ] + result = alpha_shape_concave_hull(pts, alpha=5.0) + assert result is not None diff --git a/python/functions/datascience/tests/test_best_central_tendency.py b/python/functions/datascience/tests/test_best_central_tendency.py new file mode 100644 index 00000000..446fe99b --- /dev/null +++ b/python/functions/datascience/tests/test_best_central_tendency.py @@ -0,0 +1,47 @@ +"""Tests para best_central_tendency.""" + +import math +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from best_central_tendency import best_central_tendency + + +def test_best_central_tendency_normal_ish(): + label, value = best_central_tendency([1, 2, 3, 4, 5], "normal-ish") + assert label == "mean" + assert abs(value - 3.0) < 1e-9 + + +def test_best_central_tendency_right_skewed(): + label, value = best_central_tendency([1, 2, 3, 4, 5], "right-skewed") + assert label == "median" + assert abs(value - 3.0) < 1e-9 + + +def test_best_central_tendency_left_skewed(): + label, value = best_central_tendency([1, 2, 3, 4, 5], "left-skewed") + assert label == "median" + + +def test_best_central_tendency_lognormal_ish(): + label, value = best_central_tendency([1, 2, 4, 8], "lognormal-ish") + assert label == "geometric_mean" + assert abs(value - 2 ** 1.5) < 1e-6 + + +def test_best_central_tendency_heavy_tail(): + label, value = best_central_tendency([1, 2, 3, 4, 5, 100], "heavy-tail") + assert label == "trimmed_mean_5%" + assert not math.isnan(value) + + +def test_best_central_tendency_empty(): + label, value = best_central_tendency([], "normal-ish") + assert math.isnan(value) + + +def test_best_central_tendency_default(): + label, value = best_central_tendency([1, 2, 3, 4, 5], "other") + assert label == "median" diff --git a/python/functions/datascience/tests/test_detect_distribution_type.py b/python/functions/datascience/tests/test_detect_distribution_type.py new file mode 100644 index 00000000..912bf933 --- /dev/null +++ b/python/functions/datascience/tests/test_detect_distribution_type.py @@ -0,0 +1,45 @@ +"""Tests para detect_distribution_type.""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from detect_distribution_type import detect_distribution_type + +import numpy as np + + +def test_detect_too_few_samples(): + result = detect_distribution_type([1] * 5) + assert result["type"] == "too_few_samples" + + +def test_detect_normal_ish(): + rng = np.random.default_rng(42) + values = rng.normal(0, 1, 200).tolist() + result = detect_distribution_type(values) + assert result["type"] == "normal-ish", f"Got {result['type']}" + + +def test_detect_right_skewed(): + rng = np.random.default_rng(0) + # Exponential distribution is heavily right-skewed + values = rng.exponential(scale=1.0, size=200).tolist() + result = detect_distribution_type(values) + assert result["type"] in ("right-skewed", "lognormal-ish", "heavy-tail"), f"Got {result['type']}" + + +def test_detect_stats_keys(): + rng = np.random.default_rng(7) + values = rng.normal(5, 2, 100).tolist() + result = detect_distribution_type(values) + assert "stats" in result + assert "n" in result["stats"] + assert result["stats"]["n"] == 100 + + +def test_detect_exactly_30(): + rng = np.random.default_rng(1) + values = rng.normal(0, 1, 30).tolist() + result = detect_distribution_type(values) + assert result["type"] != "too_few_samples" diff --git a/python/functions/datascience/tests/test_extract_graph_gliner2.py b/python/functions/datascience/tests/test_extract_graph_gliner2.py new file mode 100644 index 00000000..79360ddc --- /dev/null +++ b/python/functions/datascience/tests/test_extract_graph_gliner2.py @@ -0,0 +1,67 @@ +"""Tests para extract_graph_gliner2. + +Usa un stub GLiNER2 para validar el contrato sin descargar el modelo real. +""" + +from __future__ import annotations + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.datascience.extract_graph_gliner2 import extract_graph_gliner2 + + +class _Schema: + def entities(self, labels): + self._entities = labels + return self + + def relations(self, labels): + self._relations = labels + return self + + +class _StubModel: + """Stub que devuelve entidades y relaciones conocidas.""" + + _extract_result = { + "entities": {"person": ["Pablo Isla"], "organization": ["Inditex"]}, + "relation_extraction": {"ceo_of": [("Pablo Isla", "Inditex")]}, + } + + def create_schema(self): + return _Schema() + + def extract(self, text, schema=None, threshold=0.3, include_confidence=False): + return self._extract_result + + +def test_output_tiene_claves_entities_relation_extraction_elapsed_s(): + """output tiene claves entities relation_extraction elapsed_s""" + result = extract_graph_gliner2( + text="Pablo Isla es CEO de Inditex.", + entity_labels=["person", "organization"], + relation_labels=["ceo_of"], + model=_StubModel(), + ) + assert "entities" in result + assert "relation_extraction" in result + assert "elapsed_s" in result + assert isinstance(result["elapsed_s"], float) + + +def test_stub_model_retorna_shape_correcto(): + """stub model retorna shape correcto""" + result = extract_graph_gliner2( + text="Texto cualquiera.", + entity_labels=["person"], + relation_labels=["works_at"], + model=_StubModel(), + threshold=0.3, + ) + assert result["entities"] == {"person": ["Pablo Isla"], "organization": ["Inditex"]} + assert "ceo_of" in result["relation_extraction"] diff --git a/python/functions/datascience/tests/test_extract_relations_mrebel.py b/python/functions/datascience/tests/test_extract_relations_mrebel.py new file mode 100644 index 00000000..4e9294d7 --- /dev/null +++ b/python/functions/datascience/tests/test_extract_relations_mrebel.py @@ -0,0 +1,112 @@ +"""Tests para extract_relations_mrebel con stubs de modelo.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.datascience.extract_relations_mrebel import extract_relations_mrebel +from python.types.datascience.entity_candidate import EntityCandidate +from python.types.datascience.relation_candidate import RelationCandidate + + +# --------------------------------------------------------------------------- +# Stubs +# --------------------------------------------------------------------------- + +class _TokenizerStub: + """Tokenizer stub que devuelve inputs triviales y decodifica el wire format canonico.""" + + def __init__(self, decoded_output: str = ""): + self._decoded = decoded_output + + def __call__(self, text, return_tensors=None, max_length=512, truncation=True): + return {"input_ids": [[1, 2, 3]]} + + def decode(self, token_ids, skip_special_tokens=True): + return self._decoded + + +class _ModelStub: + """Modelo stub que devuelve tokens triviales.""" + + def generate(self, input_ids=None, num_beams=4, length_penalty=1.0, max_length=256, **kwargs): + return [[10, 11, 12]] + + +# --------------------------------------------------------------------------- +# Tests +# --------------------------------------------------------------------------- + +def test_flujo_completo_con_stub_produce_relation_candidates_correctos(): + # Wire format canonico con un triplet valido + decoded = " Pablo Isla Inditex employer" + tok = _TokenizerStub(decoded_output=decoded) + model = _ModelStub() + + entities = [ + EntityCandidate(name="Pablo Isla", type_label="PER", confidence=0.95), + EntityCandidate(name="Inditex", type_label="ORG", confidence=0.92), + ] + text = "Pablo Isla es el presidente de Inditex." + + result = extract_relations_mrebel(text, entities, tok, model) + + assert len(result) == 1 + rc = result[0] + assert isinstance(rc, RelationCandidate) + assert rc.from_name == "Pablo Isla" + assert rc.to_name == "Inditex" + assert rc.relation_type == "employer" + assert rc.confidence == 1.0 + + +def test_menos_de_2_entidades_retorna_vacio(): + tok = _TokenizerStub() + model = _ModelStub() + entities = [EntityCandidate(name="Pablo Isla", type_label="PER")] + result = extract_relations_mrebel("Texto cualquiera.", entities, tok, model) + assert result == [] + + +def test_texto_vacio_retorna_vacio(): + tok = _TokenizerStub() + model = _ModelStub() + entities = [ + EntityCandidate(name="A", type_label="PER"), + EntityCandidate(name="B", type_label="ORG"), + ] + assert extract_relations_mrebel("", entities, tok, model) == [] + + +def test_triplets_no_alineables_se_descartan(): + # El stub emite entidades que no estan en la lista + decoded = " Ghost Entity Unknown Org some relation" + tok = _TokenizerStub(decoded_output=decoded) + model = _ModelStub() + + entities = [ + EntityCandidate(name="Pablo Isla", type_label="PER"), + EntityCandidate(name="Inditex", type_label="ORG"), + ] + result = extract_relations_mrebel("Texto largo suficiente.", entities, tok, model) + assert result == [] + + +def test_multiples_frases_generan_multiples_candidates(): + # El stub siempre emite el mismo triplet valido — una por frase + decoded = " Pablo Isla Inditex employer" + tok = _TokenizerStub(decoded_output=decoded) + model = _ModelStub() + + entities = [ + EntityCandidate(name="Pablo Isla", type_label="PER"), + EntityCandidate(name="Inditex", type_label="ORG"), + ] + # Dos frases separadas por ". " + text = "Pablo Isla es el presidente de Inditex. Inditex tiene sedes en todo el mundo." + + result = extract_relations_mrebel(text, entities, tok, model) + # Puede haber 1 o 2 dependiendo de la dedup — lo importante es que no es vacio + assert len(result) >= 1 + assert all(isinstance(rc, RelationCandidate) for rc in result) diff --git a/python/functions/datascience/tests/test_extract_triples_spacy_es.py b/python/functions/datascience/tests/test_extract_triples_spacy_es.py new file mode 100644 index 00000000..26337e04 --- /dev/null +++ b/python/functions/datascience/tests/test_extract_triples_spacy_es.py @@ -0,0 +1,81 @@ +"""Tests para extract_triples_spacy_es. + +Requiere spaCy y es_core_news_md instalados. Si no estan, los tests se omiten. +""" + +from __future__ import annotations + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.datascience.extract_triples_spacy_es import extract_triples_spacy_es + +spacy = pytest.importorskip("spacy", reason="spacy not installed — skip") + + +def _load_nlp(): + try: + return spacy.load("es_core_news_md") + except OSError: + return None + + +_NLP = _load_nlp() +pytestmark = pytest.mark.skipif( + _NLP is None, + reason="es_core_news_md not installed — run: python -m spacy download es_core_news_md", +) + + +def test_oracion_simple_produce_tripleta_con_sujeto_verbo_objeto(): + """oracion simple produce tripleta con sujeto verbo objeto""" + result = extract_triples_spacy_es("Enmanuel quiere a Ashlly.", _NLP) + assert len(result["triples"]) >= 1 + # Al menos una tripleta con sujeto que contenga Enmanuel + subjs = [t["subject"] for t in result["triples"]] + assert any("Enmanuel" in s or "enmanuel" in s.lower() for s in subjs) + + +def test_carlos_torres_preside_bbva(): + """carlos torres preside bbva produce tripleta president""" + result = extract_triples_spacy_es("Carlos Torres preside BBVA.", _NLP) + triples = result["triples"] + assert len(triples) >= 1 + rels = [t["relation"] for t in triples] + assert any("presidir" in r or "presidir" in r.lower() for r in rels) + + +def test_amancio_ortega_fundo_inditex_en_1985(): + """amancio ortega fundo inditex en 1985 produce tripletas con fundar_en""" + result = extract_triples_spacy_es( + "Amancio Ortega fundo Inditex en 1985.", _NLP + ) + triples = result["triples"] + assert len(triples) >= 1 + # El verbo y sus objetos deben producir al menos 2 tripletas (Inditex + 1985 como oblicuo) + subjs = {t["subject"] for t in triples} + assert any("Amancio" in s or "Ortega" in s for s in subjs) + # Debe haber al menos la tripleta directa con Inditex + objects = {t["object"] for t in triples} + assert any("Inditex" in o or "1985" in o for o in objects) + + +def test_texto_sin_verbos_produce_tripletas_vacias(): + """texto sin verbos produce tripletas vacias""" + result = extract_triples_spacy_es("BBVA Santander Inditex.", _NLP) + assert result["triples"] == [] + + +def test_entities_ner_detecta_categorias(): + """entities NER detecta PER ORG LOC""" + result = extract_triples_spacy_es( + "Carlos Torres es presidente de BBVA en Bilbao.", _NLP + ) + ents = result["entities"] + labels = {e["label"] for e in ents} + # Debe detectar al menos uno de PER, ORG o LOC + assert labels & {"PER", "ORG", "LOC"} diff --git a/python/functions/datascience/tests/test_fuzzy_merge_adaptive.py b/python/functions/datascience/tests/test_fuzzy_merge_adaptive.py new file mode 100644 index 00000000..e115c7c7 --- /dev/null +++ b/python/functions/datascience/tests/test_fuzzy_merge_adaptive.py @@ -0,0 +1,67 @@ +"""Tests para fuzzy_merge_adaptive.""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from fuzzy_merge_adaptive import fuzzy_merge_adaptive + + +def test_left_join_con_typo(): + left = [{"name": "Madrid"}, {"name": "Barclona"}] + right = [{"name": "Madrid", "cp": "28"}, {"name": "Barcelona", "cp": "08"}] + result = fuzzy_merge_adaptive(left, right, left_key="name", right_key="name") + assert len(result) == 2 + scores = [r["match_score"] for r in result] + assert all(s >= 80 for s in scores), f"Scores bajos: {scores}" + assert result[0]["cp"] == "28" + assert result[1]["cp"] == "08" + + +def test_inner_join_excluye_sin_match(): + left = [{"name": "Madrid"}, {"name": "ZZZinexistente"}] + right = [{"name": "Madrid", "cp": "28"}] + result = fuzzy_merge_adaptive( + left, right, left_key="name", right_key="name", + thresholds=[90, 80, 70], how="inner" + ) + assert len(result) == 1 + assert result[0]["fuzzy_match"] == "Madrid" + + +def test_left_join_sin_match_devuelve_none(): + left = [{"name": "ZZZinexistente"}] + right = [{"name": "Madrid", "cp": "28"}] + result = fuzzy_merge_adaptive( + left, right, left_key="name", right_key="name", + thresholds=[95], how="left" + ) + assert len(result) == 1 + assert result[0]["fuzzy_match"] is None + assert result[0]["match_score"] == 0 + assert result[0]["threshold_used"] is None + + +def test_threshold_adaptativo(): + left = [{"name": "Bcn"}] + right = [{"name": "Barcelona", "cp": "08"}] + result = fuzzy_merge_adaptive( + left, right, left_key="name", right_key="name", + thresholds=[90, 80, 70, 60, 50] + ) + assert len(result) == 1 + # Puede matchear o no segun score, pero threshold_used <= 90 + if result[0]["threshold_used"] is not None: + assert result[0]["threshold_used"] <= 90 + + +def test_colision_de_claves_usa_sufijos(): + left = [{"name": "Madrid", "info": "left_info"}] + right = [{"name": "Madrid", "info": "right_info"}] + result = fuzzy_merge_adaptive(left, right, left_key="name", right_key="name") + assert len(result) == 1 + assert "info_left" in result[0] + assert "info_right" in result[0] + assert result[0]["info_left"] == "left_info" + assert result[0]["info_right"] == "right_info" diff --git a/python/functions/datascience/tests/test_geometric_mean.py b/python/functions/datascience/tests/test_geometric_mean.py new file mode 100644 index 00000000..75eea457 --- /dev/null +++ b/python/functions/datascience/tests/test_geometric_mean.py @@ -0,0 +1,35 @@ +"""Tests para geometric_mean.""" + +import math +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from geometric_mean import geometric_mean + + +def test_geometric_mean_powers_of_two(): + result = geometric_mean([1, 2, 4, 8]) + expected = 2 ** 1.5 # ~2.828 + assert abs(result - expected) < 1e-6, f"Expected ~{expected}, got {result}" + + +def test_geometric_mean_filters_non_positive(): + result = geometric_mean([1, -2, 3]) + expected = math.exp((math.log(1) + math.log(3)) / 2) + assert abs(result - expected) < 1e-6 + + +def test_geometric_mean_empty_returns_nan(): + result = geometric_mean([]) + assert math.isnan(result) + + +def test_geometric_mean_all_negative_returns_nan(): + result = geometric_mean([-1, -2, -3]) + assert math.isnan(result) + + +def test_geometric_mean_single_positive(): + result = geometric_mean([9.0]) + assert abs(result - 9.0) < 1e-9 diff --git a/python/functions/datascience/tests/test_gliner2_load_model.py b/python/functions/datascience/tests/test_gliner2_load_model.py new file mode 100644 index 00000000..845b3f8e --- /dev/null +++ b/python/functions/datascience/tests/test_gliner2_load_model.py @@ -0,0 +1,84 @@ +"""Tests para gliner2_load_model. + +El modelo real (gliner2) es opcional. Los tests usan un stub para validar +el cache sin descargar el modelo. Tests que requieran el modelo real se +marcan con pytest.importorskip('gliner2'). +""" + +from __future__ import annotations + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.datascience.gliner2_load_model import ( + _MODEL_CACHE, + _resolve_device, + gliner2_load_model, +) + + +class _StubGLiNER2: + """Stub duck-typed para validar el cache sin descargar el modelo real.""" + + @classmethod + def from_pretrained(cls, model_name: str) -> "_StubGLiNER2": + return cls() + + def create_schema(self): + return self + + def entities(self, labels): + return self + + def relations(self, labels): + return self + + def extract(self, text, **kwargs): + return {"entities": {}, "relation_extraction": {}} + + +def test_cache_devuelve_la_misma_instancia(monkeypatch): + """cache devuelve la misma instancia con los mismos parametros""" + _MODEL_CACHE.clear() + monkeypatch.setattr( + "python.functions.datascience.gliner2_load_model.GLiNER2", + _StubGLiNER2, + raising=False, + ) + # Patch el import dentro de la funcion + import python.functions.datascience.gliner2_load_model as mod + original = None + try: + from gliner2 import GLiNER2 as _real # type: ignore[import] + original = _real + except ImportError: + pass + + _MODEL_CACHE.clear() + # Insertar stub directamente en el cache para simular primera carga + key = ("fastino/gliner2-large-v1", "cpu") + stub = _StubGLiNER2() + _MODEL_CACHE[key] = stub + + # Segunda llamada debe devolver el mismo objeto + result = gliner2_load_model(model_name="fastino/gliner2-large-v1", device="cpu") + assert result is stub + _MODEL_CACHE.clear() + + +def test_device_auto_resuelve_a_cpu_si_torch_no_esta(monkeypatch): + """device=auto resuelve a cpu si torch no esta instalado""" + import sys + # Simular que torch no esta disponible + monkeypatch.setitem(sys.modules, "torch", None) + resolved = _resolve_device("auto") + assert resolved == "cpu" + + +def test_import_error_si_gliner2_no_esta_instalado(): + """ImportError si gliner2 no esta instalado""" + pytest.importorskip("gliner2", reason="gliner2 not installed — skip real model test") diff --git a/python/functions/datascience/tests/test_kde_density_levels.py b/python/functions/datascience/tests/test_kde_density_levels.py new file mode 100644 index 00000000..54a36ea8 --- /dev/null +++ b/python/functions/datascience/tests/test_kde_density_levels.py @@ -0,0 +1,46 @@ +"""Tests para kde_density_levels.""" + +import sys +import os +import numpy as np + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from kde_density_levels import kde_density_levels + + +def test_kde_density_levels_returns_dict_for_50_points(): + rng = np.random.default_rng(42) + xs = rng.normal(0, 1, 50).tolist() + ys = rng.normal(0, 1, 50).tolist() + result = kde_density_levels(xs, ys) + assert result is not None + assert "method" in result + assert result["method"] in ("kde", "hist") + assert "densities" in result + assert len(result["densities"]) == 50 + assert "abs_level" in result + assert "dense_level" in result + + +def test_kde_density_levels_none_for_few_points(): + result = kde_density_levels([1.0, 2.0, 3.0], [1.0, 2.0, 3.0]) + assert result is None + + +def test_kde_density_levels_none_for_4_points(): + result = kde_density_levels([1, 2, 3, 4], [1, 2, 3, 4]) + assert result is None + + +def test_kde_density_levels_levels_ordered(): + rng = np.random.default_rng(0) + xs = rng.uniform(0, 10, 100).tolist() + ys = rng.uniform(0, 10, 100).tolist() + result = kde_density_levels(xs, ys, abs_quantile=0.1, dense_quantile=0.85) + assert result is not None + assert result["abs_level"] <= result["dense_level"] + + +def test_kde_density_levels_mismatched_lengths(): + result = kde_density_levels([1, 2, 3, 4, 5], [1, 2, 3]) + assert result is None diff --git a/python/functions/datascience/tests/test_parse_rebel_output.py b/python/functions/datascience/tests/test_parse_rebel_output.py new file mode 100644 index 00000000..ef9e59f4 --- /dev/null +++ b/python/functions/datascience/tests/test_parse_rebel_output.py @@ -0,0 +1,75 @@ +"""Tests para parse_rebel_output.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.datascience.parse_rebel_output import parse_rebel_output + + +def test_string_vacio_retorna_lista_vacia(): + assert parse_rebel_output("") == [] + + +def test_string_solo_espacios_retorna_lista_vacia(): + assert parse_rebel_output(" ") == [] + + +def test_un_triplet_completo_retorna_un_dict_con_campos_correctos(): + decoded = "tp_XX Pablo Isla Inditex employer" + result = parse_rebel_output(decoded) + assert len(result) == 1 + t = result[0] + assert t["head"] == "Pablo Isla" + assert t["head_type"] == "per" + assert t["tail"] == "Inditex" + assert t["tail_type"] == "org" + assert t["type"] == "employer" + + +def test_dos_triplets_retorna_dos_dicts(): + decoded = ( + "tp_XX Pablo Isla Inditex employer " + " Arteixo A Coruna located in the administrative territorial entity" + ) + result = parse_rebel_output(decoded) + assert len(result) == 2 + assert result[0]["head"] == "Pablo Isla" + assert result[0]["tail"] == "Inditex" + assert result[1]["head"] == "Arteixo" + assert result[1]["tail"] == "A Coruna" + assert "located" in result[1]["type"] + + +def test_triplet_incompleto_sin_cierre_no_rompe(): + # Solo head span, sin tail ni relacion + decoded = "tp_XX Pablo Isla" + result = parse_rebel_output(decoded) + # No hay cierre, puede retornar lista vacia o incompleta pero no rompe + assert isinstance(result, list) + + +def test_tokens_angulares_desconocidos_no_lanzan_excepcion(): + # Un tipo desconocido como no debe romper el parser + decoded = " Entity One Entity Two some relation" + result = parse_rebel_output(decoded) + assert isinstance(result, list) + + +def test_sin_prefijo_tp_xx_funciona(): + # REBEL monolingue no emite tp_XX + decoded = " Barack Obama United States president of" + result = parse_rebel_output(decoded) + assert len(result) == 1 + assert result[0]["head"] == "Barack Obama" + assert result[0]["tail"] == "United States" + assert result[0]["type"] == "president of" + + +def test_strip_tags_s_pad(): + decoded = "tp_XX Ana BBVA works at" + result = parse_rebel_output(decoded) + assert len(result) == 1 + assert result[0]["head"] == "Ana" + assert result[0]["tail"] == "BBVA" diff --git a/python/functions/datascience/tests/test_plot_heatmap_log.py b/python/functions/datascience/tests/test_plot_heatmap_log.py new file mode 100644 index 00000000..4fd73070 --- /dev/null +++ b/python/functions/datascience/tests/test_plot_heatmap_log.py @@ -0,0 +1,38 @@ +"""Tests para plot_heatmap_log.""" + +import sys +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from datascience.plot_heatmap_log import plot_heatmap_log + + +def test_100_puntos_no_lanza_excepcion(): + import matplotlib.pyplot as plt + import numpy as np + + rng = np.random.default_rng(0) + xs = rng.uniform(-4.0, -3.5, 100) + ys = rng.uniform(40.3, 40.6, 100) + + fig, ax = plt.subplots() + plot_heatmap_log(ax, xs, ys, extent=(-4.0, -3.5, 40.3, 40.6), bins=50) + plt.close(fig) + + +def test_ax_tiene_imagen_tras_la_llamada(): + import matplotlib.pyplot as plt + import numpy as np + + rng = np.random.default_rng(1) + xs = rng.uniform(-4.0, -3.5, 100) + ys = rng.uniform(40.3, 40.6, 100) + + fig, ax = plt.subplots() + plot_heatmap_log(ax, xs, ys, extent=(-4.0, -3.5, 40.3, 40.6), bins=50) + assert len(ax.images) > 0, "ax should have at least one image after heatmap" + plt.close(fig) diff --git a/python/functions/datascience/tests/test_plot_kde_2d.py b/python/functions/datascience/tests/test_plot_kde_2d.py new file mode 100644 index 00000000..153116af --- /dev/null +++ b/python/functions/datascience/tests/test_plot_kde_2d.py @@ -0,0 +1,32 @@ +"""Tests para plot_kde_2d.""" + +import sys +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from datascience.plot_kde_2d import plot_kde_2d + + +def test_50_puntos_aleatorios_no_lanza_excepcion(): + import matplotlib.pyplot as plt + import numpy as np + + rng = np.random.default_rng(42) + xs = rng.normal(0, 1, 50) + ys = rng.normal(0, 1, 50) + + fig, ax = plt.subplots() + plot_kde_2d(ax, xs, ys) + plt.close(fig) + + +def test_arrays_vacios_retorna_sin_error(): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + plot_kde_2d(ax, [], []) + plt.close(fig) diff --git a/python/functions/datascience/tests/test_remove_words_from_column.py b/python/functions/datascience/tests/test_remove_words_from_column.py new file mode 100644 index 00000000..e724e1af --- /dev/null +++ b/python/functions/datascience/tests/test_remove_words_from_column.py @@ -0,0 +1,42 @@ +"""Tests para remove_words_from_column.""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from remove_words_from_column import remove_words_from_column + + +def test_elimina_palabras_case_insensitive(): + values = ["Calle Mayor 14", "Avenida del Sol"] + result = remove_words_from_column(values, words=["calle", "avenida", "del"]) + assert result == ["Mayor 14", "Sol"] + + +def test_none_devuelve_string_vacio(): + result = remove_words_from_column([None, "hola mundo"], words=["hola"]) + assert result[0] == "" + assert result[1] == "mundo" + + +def test_colapsa_espacios_multiples(): + result = remove_words_from_column(["uno dos tres"], words=["dos"]) + assert result[0] == "uno tres" + + +def test_palabras_vacias_no_modifica(): + values = ["hola mundo", "foo bar"] + result = remove_words_from_column(values, words=[]) + assert result == ["hola mundo", "foo bar"] + + +def test_palabra_completa_no_parcial(): + # "calle" no debe eliminar "calleja" + result = remove_words_from_column(["calleja mayor"], words=["calle"]) + assert result[0] == "calleja mayor" + + +def test_lista_vacia(): + result = remove_words_from_column([], words=["foo"]) + assert result == [] diff --git a/python/functions/datascience/tests/test_spacy_es_load_model.py b/python/functions/datascience/tests/test_spacy_es_load_model.py new file mode 100644 index 00000000..7cb938be --- /dev/null +++ b/python/functions/datascience/tests/test_spacy_es_load_model.py @@ -0,0 +1,46 @@ +"""Tests para spacy_es_load_model.""" + +from __future__ import annotations + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.datascience.spacy_es_load_model import ( + _MODEL_CACHE, + spacy_es_load_model, +) + +spacy = pytest.importorskip("spacy", reason="spacy not installed — skip") + + +def _has_model(model_name: str) -> bool: + try: + spacy.load(model_name) + return True + except OSError: + return False + + +@pytest.mark.skipif( + not _has_model("es_core_news_md"), + reason="es_core_news_md not installed", +) +def test_cache_devuelve_la_misma_instancia(): + """cache devuelve la misma instancia""" + _MODEL_CACHE.clear() + m1 = spacy_es_load_model("es_core_news_md") + m2 = spacy_es_load_model("es_core_news_md") + assert m1 is m2 + _MODEL_CACHE.clear() + + +def test_oserror_si_el_modelo_no_esta_instalado(): + """OSError si el modelo no esta instalado""" + _MODEL_CACHE.clear() + with pytest.raises(OSError): + spacy_es_load_model("es_nonexistent_model_xyz") + _MODEL_CACHE.clear() diff --git a/python/functions/datascience/tests/test_summary_stats.py b/python/functions/datascience/tests/test_summary_stats.py new file mode 100644 index 00000000..52913403 --- /dev/null +++ b/python/functions/datascience/tests/test_summary_stats.py @@ -0,0 +1,38 @@ +"""Tests para summary_stats.""" + +import math +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from summary_stats import summary_stats + + +def test_summary_stats_basic(): + result = summary_stats([1, 2, 3, 4, 5]) + assert result["n"] == 5 + assert abs(result["mean"] - 3.0) < 1e-9 + assert abs(result["median"] - 3.0) < 1e-9 + assert abs(result["p25"] - 2.0) < 0.01 + assert abs(result["p75"] - 4.0) < 0.01 + + +def test_summary_stats_empty(): + result = summary_stats([]) + assert result["n"] == 0 + assert math.isnan(result["mean"]) + assert math.isnan(result["median"]) + assert math.isnan(result["p25"]) + assert math.isnan(result["p75"]) + + +def test_summary_stats_single(): + result = summary_stats([7.0]) + assert result["n"] == 1 + assert abs(result["mean"] - 7.0) < 1e-9 + assert abs(result["median"] - 7.0) < 1e-9 + + +def test_summary_stats_keys(): + result = summary_stats([1, 2, 3]) + assert set(result.keys()) == {"n", "mean", "median", "p25", "p75"} diff --git a/python/functions/datascience/tests/test_translate_es_to_en.py b/python/functions/datascience/tests/test_translate_es_to_en.py new file mode 100644 index 00000000..dd70685f --- /dev/null +++ b/python/functions/datascience/tests/test_translate_es_to_en.py @@ -0,0 +1,62 @@ +"""Tests para translate_es_to_en — smoke tests con modelo stub.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.datascience.translate_es_to_en import translate_es_to_en + + +class _StubTokenizer: + """Tokenizer stub que devuelve inputs triviales.""" + + def __call__(self, text, return_tensors=None, max_length=512, truncation=True): + # Devuelve un dict con una clave 'input_ids' que el modelo stub acepta. + return {"input_ids": [[1, 2, 3]], "_text": text} + + def decode(self, token_ids, skip_special_tokens=True): + # Devuelve siempre "translated" para testing. + return "translated" + + +class _StubModel: + """Modelo stub que devuelve tokens triviales.""" + + def generate(self, input_ids=None, num_beams=4, max_length=512, **kwargs): + return [[10, 11, 12]] + + +def test_texto_vacio_retorna_string_vacio(): + tok = _StubTokenizer() + model = _StubModel() + assert translate_es_to_en("", tok, model) == "" + + +def test_solo_espacios_retorna_string_vacio(): + tok = _StubTokenizer() + model = _StubModel() + assert translate_es_to_en(" ", tok, model) == "" + + +def test_una_frase_en_espanol_produce_output_no_vacio(): + tok = _StubTokenizer() + model = _StubModel() + result = translate_es_to_en("Pablo Isla es presidente de Inditex.", tok, model) + assert isinstance(result, str) + assert len(result) > 0 + + +def test_multiples_frases_se_unen_con_espacio(): + tok = _StubTokenizer() + model = _StubModel() + # El stub siempre devuelve "translated" por frase + result = translate_es_to_en( + "Primera frase. Segunda frase. Tercera frase.", + tok, + model, + ) + # Con el stub, cada frase produce "translated", unidas con espacio + parts = result.split(" ") + assert all(p == "translated" for p in parts) + assert len(parts) >= 1 diff --git a/python/functions/datascience/tests/test_trimmed_mean.py b/python/functions/datascience/tests/test_trimmed_mean.py new file mode 100644 index 00000000..b7fb7a1a --- /dev/null +++ b/python/functions/datascience/tests/test_trimmed_mean.py @@ -0,0 +1,33 @@ +"""Tests para trimmed_mean.""" + +import math +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) +from trimmed_mean import trimmed_mean + + +def test_trimmed_mean_basic(): + result = trimmed_mean([1, 2, 3, 4, 5, 100], 0.1) + assert abs(result - 3.5) < 0.5, f"Expected ~3.5, got {result}" + + +def test_trimmed_mean_empty_returns_nan(): + result = trimmed_mean([], 0.05) + assert math.isnan(result) + + +def test_trimmed_mean_no_trim(): + result = trimmed_mean([1.0, 2.0, 3.0, 4.0, 5.0], 0.0) + assert abs(result - 3.0) < 1e-9 + + +def test_trimmed_mean_single_element(): + result = trimmed_mean([42.0], 0.05) + assert abs(result - 42.0) < 1e-9 + + +def test_trimmed_mean_uniform(): + result = trimmed_mean([5.0, 5.0, 5.0, 5.0, 5.0], 0.1) + assert abs(result - 5.0) < 1e-9 diff --git a/python/functions/datascience/tests/test_words_to_dataset.py b/python/functions/datascience/tests/test_words_to_dataset.py new file mode 100644 index 00000000..be26dffc --- /dev/null +++ b/python/functions/datascience/tests/test_words_to_dataset.py @@ -0,0 +1,49 @@ +"""Tests para words_to_dataset.""" + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +from words_to_dataset import words_to_dataset + + +def test_cuenta_palabras_repetidas(): + texts = ["calle mayor", "calle del sol", "avenida principal"] + result = words_to_dataset(texts) + palabras = {r["palabra"]: r["ocurrencias"] for r in result} + assert palabras["CALLE"] == 2 + + +def test_eliminar_stopwords_filtra_del(): + texts = ["calle mayor", "calle del sol", "avenida principal"] + result = words_to_dataset(texts, eliminar_stopwords=True) + palabras = {r["palabra"] for r in result} + assert "DEL" not in palabras + + +def test_min_ocurrencias_filtra(): + texts = ["calle mayor", "calle del sol", "avenida principal"] + result = words_to_dataset(texts, min_ocurrencias=2) + palabras = {r["palabra"]: r["ocurrencias"] for r in result} + assert "CALLE" in palabras + assert "MAYOR" not in palabras + + +def test_none_ignorados(): + texts = ["hola mundo", None, "hola"] + result = words_to_dataset(texts) + palabras = {r["palabra"]: r["ocurrencias"] for r in result} + assert palabras["HOLA"] == 2 + + +def test_lista_vacia(): + result = words_to_dataset([]) + assert result == [] + + +def test_orden_descendente(): + texts = ["a a a", "b b", "c"] + result = words_to_dataset(texts) + counts = [r["ocurrencias"] for r in result] + assert counts == sorted(counts, reverse=True) diff --git a/python/functions/datascience/translate_es_to_en.md b/python/functions/datascience/translate_es_to_en.md new file mode 100644 index 00000000..13729b22 --- /dev/null +++ b/python/functions/datascience/translate_es_to_en.md @@ -0,0 +1,85 @@ +--- +name: translate_es_to_en +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: impure +signature: "def translate_es_to_en(text: str, tokenizer: Any, model: Any, max_length: int = 512, num_beams: int = 4) -> str" +description: "Traduce texto espanol a ingles frase a frase usando MarianMT. Divide por boundaries de oracion, traduce cada una independientemente y une con espacio. Preserva nombres propios mejor que pasar el parrafo entero." +tags: [marianmt, translation, es-en, nlp, datascience, python] +uses_functions: [marianmt_es_en_load_model_py_datascience] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [re] +params: + - name: text + desc: "texto en espanol a traducir — puede ser una frase o un parrafo multi-oracion" + - name: tokenizer + desc: "tokenizer MarianMT cargado con marianmt_es_en_load_model" + - name: model + desc: "modelo MarianMT cargado con marianmt_es_en_load_model" + - name: max_length + desc: "longitud maxima en tokens por oracion para tokenizacion y generacion (defecto 512)" + - name: num_beams + desc: "numero de beams para beam search — mas alto = mejor calidad, mas lento (defecto 4)" +output: "texto traducido al ingles. Frases unidas con espacio simple. String vacio si el input es vacio." +tested: true +tests: + - "texto vacio retorna string vacio" + - "una frase en espanol produce output no vacio" +test_file_path: "python/functions/datascience/tests/test_translate_es_to_en.py" +file_path: "python/functions/datascience/translate_es_to_en.py" +notes: | + impure: invoca model.generate que depende del estado del modelo (pesos, device). + + El split por oracion usa regex lookahead-behind sobre [.!?] seguidos de espacio. + Esto preserva nombres propios con puntos (S.A., U.S.A.) mejor que NLTK sent_tokenize + porque no usa reglas de abreviacion — simplemente divide donde hay espacio despues + de puntuacion terminal. + + Util como preprocesador para rebel_load_model (English-only, Apache 2.0): + ES text -> translate_es_to_en -> EN text -> REBEL -> triplets + Alternativa directa: mrebel_load_model (multilingue, CC BY-NC-SA). +--- + +## Ejemplo + +```python +from python.functions.datascience.marianmt_es_en_load_model import marianmt_es_en_load_model +from python.functions.datascience.translate_es_to_en import translate_es_to_en + +tokenizer, model = marianmt_es_en_load_model() + +text = "Pablo Isla es presidente de Inditex. La empresa tiene sede en Arteixo." +translated = translate_es_to_en(text, tokenizer, model) +# "Pablo Isla is president of Inditex. The company is headquartered in Arteixo." +``` + +## Por que frase a frase + +Pasar el parrafo entero a MarianMT puede degradar la traduccion de nombres propios +porque el modelo redistribuye la atencion sobre el contexto completo. Dividir por oraciones: + +1. Contexto mas corto → menos confusion en nombres propios. +2. Truncation menos probable (512 tokens alcanza para oraciones normales). +3. Pipeline mas predecible para debugging (se puede inspeccionar cada frase). + +## Patron pipeline ES -> EN -> REBEL + +```python +# Paso 1: cargar modelos +mt_tok, mt_model = marianmt_es_en_load_model() +rebel_tok, rebel_model = rebel_load_model() + +# Paso 2: traducir +en_text = translate_es_to_en(es_text, mt_tok, mt_model) + +# Paso 3: extraer relaciones +inputs = rebel_tok(en_text, return_tensors="pt", max_length=512, truncation=True) +generated = rebel_model.generate(**inputs, num_beams=4, max_length=256) +decoded = rebel_tok.decode(generated[0], skip_special_tokens=False) +triplets = parse_rebel_output(decoded) +``` diff --git a/python/functions/datascience/translate_es_to_en.py b/python/functions/datascience/translate_es_to_en.py new file mode 100644 index 00000000..abcb3ff6 --- /dev/null +++ b/python/functions/datascience/translate_es_to_en.py @@ -0,0 +1,68 @@ +"""Traduce texto espanol a ingles usando MarianMT, frase a frase.""" + +from __future__ import annotations + +import re +from typing import Any + +# Patron de split por oraciones: punto, exclamacion, interrogacion seguidos de espacio. +_SENTENCE_RE = re.compile(r"(?<=[.!?])\s+") + + +def translate_es_to_en( + text: str, + tokenizer: Any, + model: Any, + max_length: int = 512, + num_beams: int = 4, +) -> str: + """Translate Spanish text to English, sentence by sentence. + + Splits the input on sentence boundaries (after ``.``, ``!``, ``?``), + translates each sentence independently, and rejoins with a single space. + Processing sentence by sentence preserves proper nouns (names, companies, + locations) better than passing the full paragraph in a single call, because + the translation model can focus on shorter context windows. + + Args: + text: Spanish text to translate. Can be a single sentence or a + multi-sentence paragraph. + tokenizer: MarianMT tokenizer loaded with ``marianmt_es_en_load_model``. + model: MarianMT model loaded with ``marianmt_es_en_load_model``. + max_length: Maximum token length for each sentence during tokenization + and generation. Sentences longer than this are truncated. + num_beams: Number of beams for beam search. Higher = better quality, + slower. Default 4 is a good tradeoff. + + Returns: + Translated English text. Sentences joined with a single space. + Returns an empty string if ``text`` is empty or whitespace-only. + + Raises: + RuntimeError: if model.generate fails (propagated from transformers). + """ + if not text or not text.strip(): + return "" + + sentences = _SENTENCE_RE.split(text.strip()) + sentences = [s.strip() for s in sentences if s.strip()] + if not sentences: + return "" + + translated_parts: list[str] = [] + for sentence in sentences: + inputs = tokenizer( + sentence, + return_tensors="pt", + max_length=max_length, + truncation=True, + ) + generated = model.generate( + **inputs, + num_beams=num_beams, + max_length=max_length, + ) + decoded = tokenizer.decode(generated[0], skip_special_tokens=True) + translated_parts.append(decoded.strip()) + + return " ".join(translated_parts) diff --git a/python/functions/datascience/trimmed_mean.md b/python/functions/datascience/trimmed_mean.md new file mode 100644 index 00000000..89ddd0ad --- /dev/null +++ b/python/functions/datascience/trimmed_mean.md @@ -0,0 +1,53 @@ +--- +id: trimmed_mean_py_datascience +name: trimmed_mean +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def trimmed_mean(values: list[float], trim: float = 0.05) -> float" +description: "Arithmetic mean after cutting the bottom and top trim percentiles. Returns math.nan for empty input." +tags: [statistics, mean, robust, trimming, outliers] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [math, numpy] +example: | + from trimmed_mean import trimmed_mean + result = trimmed_mean([1, 2, 3, 4, 5, 100], 0.1) # ~3.5 +tested: true +tests: + - "test_trimmed_mean_basic" + - "test_trimmed_mean_empty_returns_nan" + - "test_trimmed_mean_no_trim" + - "test_trimmed_mean_single_element" + - "test_trimmed_mean_uniform" +test_file_path: "python/functions/datascience/tests/test_trimmed_mean.py" +file_path: "python/functions/datascience/trimmed_mean.py" +params: + - name: values + desc: "List of numeric values to average." + - name: trim + desc: "Fraction to cut from each tail before averaging (0 <= trim < 0.5). Default 0.05." +output: "Trimmed arithmetic mean as float. Returns math.nan if values is empty or all values are trimmed away." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "aurgi_mapas/generar_pdf_reporte.py:117" +--- + +## Ejemplo + +```python +from trimmed_mean import trimmed_mean + +trimmed_mean([1, 2, 3, 4, 5, 100], 0.1) # ~3.5 (100 is trimmed) +trimmed_mean([], 0.05) # math.nan +trimmed_mean([5.0, 5.0, 5.0], 0.0) # 5.0 +``` + +## Notas + +Usa numpy.percentile para calcular los umbrales lo y hi, luego filtra valores dentro del rango [lo, hi]. Util para calcular promedios robustos cuando hay valores extremos en la distribucion. diff --git a/python/functions/datascience/trimmed_mean.py b/python/functions/datascience/trimmed_mean.py new file mode 100644 index 00000000..9577cda2 --- /dev/null +++ b/python/functions/datascience/trimmed_mean.py @@ -0,0 +1,28 @@ +"""trimmed_mean — Arithmetic mean after trimming extreme percentiles.""" + +import math +import numpy as np + + +def trimmed_mean(values: list[float], trim: float = 0.05) -> float: + """Return the trimmed arithmetic mean of values. + + Cuts the bottom `trim` and top `trim` percentiles before averaging. + Returns math.nan for an empty list or when trimming removes all elements. + + Args: + values: List of numeric values. + trim: Fraction to cut from each tail (0 <= trim < 0.5). + + Returns: + Trimmed mean as float, or math.nan if the list is empty. + """ + if not values: + return math.nan + arr = np.array(values, dtype=float) + lo = np.percentile(arr, trim * 100) + hi = np.percentile(arr, (1 - trim) * 100) + trimmed = arr[(arr >= lo) & (arr <= hi)] + if len(trimmed) == 0: + return math.nan + return float(np.mean(trimmed)) diff --git a/python/functions/datascience/words_to_dataset.md b/python/functions/datascience/words_to_dataset.md new file mode 100644 index 00000000..87b6e104 --- /dev/null +++ b/python/functions/datascience/words_to_dataset.md @@ -0,0 +1,55 @@ +--- +name: words_to_dataset +kind: function +lang: py +domain: datascience +version: "1.0.0" +purity: pure +signature: "def words_to_dataset(texts: Iterable[str | None], min_ocurrencias: int = 1, eliminar_stopwords: bool = False) -> list[dict]" +description: "Extrae palabras y sus ocurrencias de un iterable de textos. Tokeniza con \\b\\w+\\b, convierte a mayusculas, cuenta con Counter, filtra por minimo de ocurrencias y opcionalmente elimina stopwords en espanol. Sin pandas." +tags: [nlp, text, words, frequency, counter, stopwords, spanish, datascience] +params: + - name: texts + desc: Iterable de strings o None. Los None se ignoran silenciosamente. + - name: min_ocurrencias + desc: Numero minimo de ocurrencias para incluir una palabra. Default 1. + - name: eliminar_stopwords + desc: Si True, filtra un conjunto embebido de stopwords comunes en espanol. +output: "Lista de dicts {'palabra': str, 'ocurrencias': int} ordenada por ocurrencias descendente." +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +tested: true +tests: + - "cuenta palabras repetidas" + - "eliminar stopwords filtra del" + - "min ocurrencias filtra" + - "none ignorados" + - "lista vacia" + - "orden descendente" +test_file_path: "python/functions/datascience/tests/test_words_to_dataset.py" +file_path: "python/functions/datascience/words_to_dataset.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "fuzzy_joins/arreglo_fuzzy.py" +--- + +## Ejemplo + +```python +from words_to_dataset import words_to_dataset + +texts = ["calle mayor", "calle del sol", "avenida principal"] +result = words_to_dataset(texts) +# [{"palabra": "CALLE", "ocurrencias": 2}, {"palabra": "MAYOR", "ocurrencias": 1}, ...] + +result_clean = words_to_dataset(texts, eliminar_stopwords=True) +# "DEL" no aparece +``` + +## Notas + +Stopwords embebidas (frozenset de ~40 palabras ES). Funcion pura: solo stdlib (re, collections.Counter). Tokens en mayusculas para unificar "Calle" y "CALLE". diff --git a/python/functions/datascience/words_to_dataset.py b/python/functions/datascience/words_to_dataset.py new file mode 100644 index 00000000..8628aeae --- /dev/null +++ b/python/functions/datascience/words_to_dataset.py @@ -0,0 +1,54 @@ +"""Extrae palabras y sus ocurrencias de textos en bruto.""" + +from __future__ import annotations + +import re +from collections import Counter +from typing import Iterable + + +_STOPWORDS_ES: frozenset[str] = frozenset({ + "DE", "LA", "EL", "EN", "Y", "A", "LOS", "DEL", "SE", "LAS", + "UN", "POR", "CON", "NO", "UNA", "SU", "PARA", "ES", "AL", "LO", + "COMO", "MAS", "O", "PERO", "SUS", "LE", "YA", "ESTE", + "SI", "PORQUE", "ESTA", "ENTRE", "CUANDO", "MUY", "SIN", "SOBRE", + "TAMBIEN", "ME", "HASTA", "HAY", "DONDE", "QUIEN", "DESDE", "TODO", + "NOS", "DURANTE", "TODOS", "UNO", "LES", "NI", "CONTRA", "OTROS", +}) + + +def words_to_dataset( + texts: Iterable[str | None], + min_ocurrencias: int = 1, + eliminar_stopwords: bool = False, +) -> list[dict]: + """Extrae palabras y ocurrencias de una coleccion de textos. + + Sin dependencias externas. Tokeniza cada texto con regex \\b\\w+\\b, + convierte a mayusculas, cuenta ocurrencias y filtra por minimo. + + Args: + texts: Iterable de strings (o None). Los None se ignoran. + min_ocurrencias: Numero minimo de ocurrencias para incluir una + palabra. Default 1. + eliminar_stopwords: Si True, filtra palabras comunes en espanol. + + Returns: + Lista de dicts {"palabra": str, "ocurrencias": int} ordenada + por ocurrencias descendente. + """ + all_words: list[str] = [] + for text in texts: + if text is None: + continue + words = re.findall(r"\b\w+\b", str(text).upper()) + if eliminar_stopwords: + words = [w for w in words if w not in _STOPWORDS_ES] + all_words.extend(words) + + counter = Counter(all_words) + return [ + {"palabra": word, "ocurrencias": count} + for word, count in counter.most_common() + if count >= min_ocurrencias + ] diff --git a/python/functions/geo/__init__.py b/python/functions/geo/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/functions/geo/add_basemap_osm.md b/python/functions/geo/add_basemap_osm.md new file mode 100644 index 00000000..ed2e34b1 --- /dev/null +++ b/python/functions/geo/add_basemap_osm.md @@ -0,0 +1,52 @@ +--- +name: add_basemap_osm +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: impure +signature: "def add_basemap_osm(ax: Axes, zoom: int = 9, cache_dir: str | Path | None = None) -> None" +description: "Añade un basemap OpenStreetMap Mapnik a un Axes de matplotlib usando contextily. Captura silenciosamente errores de red — nunca lanza." +tags: [geo, visualization, basemap, osm, contextily, matplotlib] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["contextily", "matplotlib"] +params: + - name: ax + desc: "matplotlib Axes con extensión proyectada. El CRS debe ser EPSG:3857 para coincidir con los tiles OSM." + - name: zoom + desc: "Nivel de zoom de los tiles (1-19). A mayor zoom, mayor resolución pero más requests." + - name: cache_dir + desc: "Directorio opcional para cachear tiles descargados. None usa el cache por defecto de contextily." +output: "None. Modifica el Axes in-place añadiendo el basemap como imagen de fondo." +tested: true +tests: + - "no lanza excepción con Axes válido" +test_file_path: "python/functions/geo/tests/test_add_basemap_osm.py" +file_path: "python/functions/geo/add_basemap_osm.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/recomendador_centros.py:220" +--- + +## Ejemplo + +```python +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from geo.add_basemap_osm import add_basemap_osm + +fig, ax = plt.subplots() +ax.set_xlim(-430000, -350000) +ax.set_ylim(4500000, 4600000) +add_basemap_osm(ax, zoom=10) +fig.savefig("mapa.png") +``` + +## Notas + +Requiere contextily. Si contextily no está instalado, retorna sin hacer nada. Errores de red (timeout, sin conexión, tile no disponible) se capturan con `except Exception: pass` para no interrumpir el pipeline de generación de reportes. diff --git a/python/functions/geo/add_basemap_osm.py b/python/functions/geo/add_basemap_osm.py new file mode 100644 index 00000000..2c2551ff --- /dev/null +++ b/python/functions/geo/add_basemap_osm.py @@ -0,0 +1,38 @@ +"""Add an OpenStreetMap basemap to a matplotlib Axes.""" + +from __future__ import annotations + +from pathlib import Path + + +def add_basemap_osm( + ax: "Axes", + zoom: int = 9, + cache_dir: "str | Path | None" = None, +) -> None: + """Add an OpenStreetMap Mapnik basemap to a matplotlib Axes. + + Uses contextily to fetch and render map tiles. Network errors and tile + fetch failures are silently captured — the map will render without a + basemap rather than raising. + + Args: + ax: matplotlib Axes with a projected extent (CRS must match tile CRS, + typically EPSG:3857). The caller is responsible for ensuring the + Axes limits are set before calling this function. + zoom: Tile zoom level (1–19). Higher values fetch more tiles and + produce a sharper basemap at the cost of more network requests. + cache_dir: Optional directory for caching downloaded tiles. If None, + contextily uses its default cache location. + """ + try: + import contextily as ctx # type: ignore + except ImportError: + return + + try: + if cache_dir is not None: + ctx.set_cache_dir(str(cache_dir)) + ctx.add_basemap(ax, source=ctx.providers.OpenStreetMap.Mapnik, zoom=zoom) + except Exception: # noqa: BLE001 + pass diff --git a/python/functions/geo/add_basemap_with_timeout.md b/python/functions/geo/add_basemap_with_timeout.md new file mode 100644 index 00000000..14e5ab28 --- /dev/null +++ b/python/functions/geo/add_basemap_with_timeout.md @@ -0,0 +1,56 @@ +--- +name: add_basemap_with_timeout +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: impure +signature: "def add_basemap_with_timeout(ax: Axes, zoom: int = 9, cache_dir: str | Path | None = None, timeout_s: float = 15.0) -> bool" +description: "Igual que add_basemap_osm pero con timeout SIGALRM. Retorna True si cargó el basemap, False si timeout o error. Solo Unix — en Windows retorna False inmediatamente." +tags: [geo, visualization, basemap, osm, contextily, matplotlib, timeout] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["contextily", "matplotlib", "signal"] +params: + - name: ax + desc: "matplotlib Axes con extensión proyectada en EPSG:3857." + - name: zoom + desc: "Nivel de zoom de los tiles (1-19)." + - name: cache_dir + desc: "Directorio opcional para cachear tiles." + - name: timeout_s + desc: "Segundos máximos para la descarga de tiles. Default 15.0." +output: "True si el basemap se añadió correctamente; False en timeout, error de red, o Windows." +tested: true +tests: + - "timeout muy corto retorna False sin colgar" +test_file_path: "python/functions/geo/tests/test_add_basemap_with_timeout.py" +file_path: "python/functions/geo/add_basemap_with_timeout.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/reporte_clientes_aurgi.py:69" +--- + +## Ejemplo + +```python +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +from geo.add_basemap_with_timeout import add_basemap_with_timeout + +fig, ax = plt.subplots() +ax.set_xlim(-430000, -350000) +ax.set_ylim(4500000, 4600000) +ok = add_basemap_with_timeout(ax, zoom=10, timeout_s=20.0) +if not ok: + print("Basemap no disponible (sin red o timeout)") +fig.savefig("mapa.png") +``` + +## Notas + +Limitación: usa SIGALRM que solo está disponible en Unix/Linux/macOS. En Windows retorna False inmediatamente sin intentar la descarga. El timeout usa `signal.setitimer(ITIMER_REAL, ...)` que tiene resolución de microsegundos. El handler original de SIGALRM se restaura en el bloque finally. diff --git a/python/functions/geo/add_basemap_with_timeout.py b/python/functions/geo/add_basemap_with_timeout.py new file mode 100644 index 00000000..85429c4d --- /dev/null +++ b/python/functions/geo/add_basemap_with_timeout.py @@ -0,0 +1,57 @@ +"""Add an OSM basemap with a SIGALRM-based timeout (Unix only).""" + +from __future__ import annotations + +import signal +from pathlib import Path + + +def add_basemap_with_timeout( + ax: "Axes", + zoom: int = 9, + cache_dir: "str | Path | None" = None, + timeout_s: float = 15.0, +) -> bool: + """Add an OpenStreetMap basemap with a hard timeout (Unix only). + + Uses SIGALRM to enforce a wall-clock timeout on the tile fetch. If the + download does not complete within ``timeout_s`` seconds the function + returns False without modifying the Axes further. + + **Unix only** — SIGALRM is not available on Windows. On Windows this + function always returns False immediately. + + Args: + ax: matplotlib Axes with projected extent (EPSG:3857). + zoom: Tile zoom level (1–19). + cache_dir: Optional tile cache directory. + timeout_s: Maximum seconds to wait for tiles. Default 15.0. + + Returns: + True if the basemap was added successfully; False on timeout or error. + """ + if not hasattr(signal, "SIGALRM"): + # Windows — SIGALRM not available + return False + + try: + import contextily as ctx # type: ignore + except ImportError: + return False + + def _handler(signum: int, frame: object) -> None: # noqa: ARG001 + raise TimeoutError("basemap fetch timed out") + + old_handler = signal.signal(signal.SIGALRM, _handler) + signal.setitimer(signal.ITIMER_REAL, timeout_s) + + try: + if cache_dir is not None: + ctx.set_cache_dir(str(cache_dir)) + ctx.add_basemap(ax, source=ctx.providers.OpenStreetMap.Mapnik, zoom=zoom) + return True + except Exception: # noqa: BLE001 + return False + finally: + signal.setitimer(signal.ITIMER_REAL, 0) + signal.signal(signal.SIGALRM, old_handler) diff --git a/python/functions/geo/distance_bucket.md b/python/functions/geo/distance_bucket.md new file mode 100644 index 00000000..7ffb6648 --- /dev/null +++ b/python/functions/geo/distance_bucket.md @@ -0,0 +1,47 @@ +--- +id: distance_bucket_py_geo +name: distance_bucket +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: pure +signature: "distance_bucket(distance_km: float) -> str" +description: "Clasifica una distancia en km en uno de los buckets: 0-5, 5-10, 10-20, 20-40, 40-80, 80-160, 160+." +tags: [geo, distance, bucket, classification] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +example: | + from geo.distance_bucket import distance_bucket + distance_bucket(3.0) # "0-5" + distance_bucket(7.0) # "5-10" + distance_bucket(200.0) # "160+" +tested: true +tests: ["bucket_0_5", "bucket_5_10", "bucket_borde_exacto", "bucket_160_mas"] +test_file_path: "python/functions/geo/tests/test_distance_bucket.py" +file_path: "python/functions/geo/distance_bucket.py" +params: + - {name: distance_km, desc: "distancia en kilometros a clasificar (valor >= 0)"} +output: "cadena con el rango al que pertenece la distancia, p.ej. '0-5' o '160+'" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/backend/app.py:678" +--- + +## Ejemplo + +```python +from geo.distance_bucket import distance_bucket + +distance_bucket(3.0) # "0-5" +distance_bucket(50.0) # "40-80" +distance_bucket(200.0) # "160+" +``` + +## Notas + +Los bordes son inclusivos por la izquierda: distance_km <= edge retorna el bucket. diff --git a/python/functions/geo/distance_bucket.py b/python/functions/geo/distance_bucket.py new file mode 100644 index 00000000..1b225d94 --- /dev/null +++ b/python/functions/geo/distance_bucket.py @@ -0,0 +1,19 @@ +"""Clasifica una distancia en km en un bucket de rango predefinido.""" + + +def distance_bucket(distance_km: float) -> str: + """Asigna la distancia a un bucket de rango: 0-5, 5-10, 10-20, 20-40, 40-80, 80-160, 160+. + + Args: + distance_km: distancia en kilometros (valor >= 0). + + Returns: + Cadena con el rango al que pertenece la distancia, p.ej. "0-5" o "160+". + """ + edges = [5, 10, 20, 40, 80, 160] + start = 0 + for edge in edges: + if distance_km <= edge: + return f"{start}-{edge}" + start = edge + return "160+" diff --git a/python/functions/geo/extent_with_padding.md b/python/functions/geo/extent_with_padding.md new file mode 100644 index 00000000..9b24120c --- /dev/null +++ b/python/functions/geo/extent_with_padding.md @@ -0,0 +1,46 @@ +--- +id: extent_with_padding_py_geo +name: extent_with_padding +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: pure +signature: "extent_with_padding(bounds: tuple[float, float, float, float], pad_ratio: float = 0.05) -> tuple[float, float, float, float]" +description: "Expande un bounding box (minx,miny,maxx,maxy) anadiendo un margen proporcional. La salida es (minx-padx, maxx+padx, miny-pady, maxy+pady)." +tags: [geo, bbox, extent, padding, matplotlib] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +example: | + from geo.extent_with_padding import extent_with_padding + extent = extent_with_padding((0.0, 0.0, 10.0, 10.0), 0.1) + # (-1.0, 11.0, -1.0, 11.0) +tested: true +tests: ["bbox_cuadrado_con_10_pct", "pad_ratio_cero_no_cambia"] +test_file_path: "python/functions/geo/tests/test_extent_with_padding.py" +file_path: "python/functions/geo/extent_with_padding.py" +params: + - {name: bounds, desc: "bounding box de entrada como tupla (minx, miny, maxx, maxy)"} + - {name: pad_ratio, desc: "fraccion del ancho/alto a anadir como margen por cada lado; por defecto 0.05 (5%)"} +output: "tupla (minx-padx, maxx+padx, miny-pady, maxy+pady) con el extent expandido" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/reporte_clientes_aurgi.py:90" +--- + +## Ejemplo + +```python +from geo.extent_with_padding import extent_with_padding + +extent_with_padding((0.0, 0.0, 10.0, 10.0), 0.1) +# (-1.0, 11.0, -1.0, 11.0) +``` + +## Notas + +El orden de salida (minx, maxx, miny, maxy) es el que espera matplotlib ax.set_xlim/set_ylim. diff --git a/python/functions/geo/extent_with_padding.py b/python/functions/geo/extent_with_padding.py new file mode 100644 index 00000000..3e42839b --- /dev/null +++ b/python/functions/geo/extent_with_padding.py @@ -0,0 +1,22 @@ +"""Expande un bounding box con un margen proporcional.""" + + +def extent_with_padding( + bounds: tuple[float, float, float, float], + pad_ratio: float = 0.05, +) -> tuple[float, float, float, float]: + """Retorna un extent expandido con padding proporcional al tamano del bbox. + + El orden de salida es (minx, maxx, miny, maxy), conveniente para ejes de matplotlib. + + Args: + bounds: tupla (minx, miny, maxx, maxy) del bounding box original. + pad_ratio: fraccion del ancho/alto a anadir como margen por cada lado (por defecto 0.05). + + Returns: + Tupla (minx - padx, maxx + padx, miny - pady, maxy + pady). + """ + minx, miny, maxx, maxy = bounds + pad_x = (maxx - minx) * pad_ratio + pad_y = (maxy - miny) * pad_ratio + return (minx - pad_x, maxx + pad_x, miny - pad_y, maxy + pad_y) diff --git a/python/functions/geo/haversine_km.md b/python/functions/geo/haversine_km.md new file mode 100644 index 00000000..22839257 --- /dev/null +++ b/python/functions/geo/haversine_km.md @@ -0,0 +1,47 @@ +--- +id: haversine_km_py_geo +name: haversine_km +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: pure +signature: "haversine_km(lon1: float, lat1: float, lon2: float, lat2: float) -> float" +description: "Calcula la distancia en kilometros entre dos puntos lon/lat usando la formula de Haversine con R=6371.0." +tags: [geo, distance, haversine, coordinates] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: ["math"] +example: | + from geo.haversine_km import haversine_km + d = haversine_km(-3.7038, 40.4168, 2.1686, 41.3874) # Madrid -> Barcelona ~504 km +tested: true +tests: ["madrid_barcelona_aproximado", "misma_coordenada_es_cero"] +test_file_path: "python/functions/geo/tests/test_haversine_km.py" +file_path: "python/functions/geo/haversine_km.py" +params: + - {name: lon1, desc: "longitud del primer punto en grados decimales"} + - {name: lat1, desc: "latitud del primer punto en grados decimales"} + - {name: lon2, desc: "longitud del segundo punto en grados decimales"} + - {name: lat2, desc: "latitud del segundo punto en grados decimales"} +output: "distancia en kilometros entre los dos puntos" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/backend/app.py:668" +--- + +## Ejemplo + +```python +from geo.haversine_km import haversine_km + +d = haversine_km(-3.7038, 40.4168, 2.1686, 41.3874) +# d ≈ 504.0 km +``` + +## Notas + +Funcion pura. Usa R=6371.0 km (radio medio de la Tierra). No maneja NaN ni Inf. diff --git a/python/functions/geo/haversine_km.py b/python/functions/geo/haversine_km.py new file mode 100644 index 00000000..847d7578 --- /dev/null +++ b/python/functions/geo/haversine_km.py @@ -0,0 +1,24 @@ +"""Distancia en km entre dos puntos lon/lat usando la formula de Haversine.""" + +import math + + +def haversine_km(lon1: float, lat1: float, lon2: float, lat2: float) -> float: + """Calcula la distancia en kilometros entre dos puntos dados en grados decimales. + + Args: + lon1: longitud del primer punto en grados. + lat1: latitud del primer punto en grados. + lon2: longitud del segundo punto en grados. + lat2: latitud del segundo punto en grados. + + Returns: + Distancia en kilometros entre los dos puntos. + """ + r = 6371.0 + phi1 = math.radians(lat1) + phi2 = math.radians(lat2) + dphi = math.radians(lat2 - lat1) + dlambda = math.radians(lon2 - lon1) + a = math.sin(dphi / 2) ** 2 + math.cos(phi1) * math.cos(phi2) * math.sin(dlambda / 2) ** 2 + return 2 * r * math.atan2(math.sqrt(a), math.sqrt(1 - a)) diff --git a/python/functions/geo/load_boundary_gdf.md b/python/functions/geo/load_boundary_gdf.md new file mode 100644 index 00000000..7b72cef8 --- /dev/null +++ b/python/functions/geo/load_boundary_gdf.md @@ -0,0 +1,46 @@ +--- +name: load_boundary_gdf +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: impure +signature: "def load_boundary_gdf(path: str | Path, crs: str = 'EPSG:4326') -> GeoDataFrame" +description: "Lee un GeoJSON con geopandas y normaliza el CRS. Si el archivo no tiene CRS lo asigna; si ya tiene CRS lo reproyecta al solicitado." +tags: [geo, geojson, geopandas, crs, boundary, io] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["geopandas", "pathlib"] +params: + - name: path + desc: "Ruta al archivo GeoJSON." + - name: crs + desc: "CRS destino en formato EPSG. Default EPSG:4326 (WGS84)." +output: "GeoDataFrame con el CRS solicitado. Cada fila es una feature del GeoJSON." +tested: true +tests: + - "retorna GeoDataFrame con CRS EPSG:4326" + - "archivo inexistente lanza FileNotFoundError" +test_file_path: "python/functions/geo/tests/test_load_boundary_gdf.py" +file_path: "python/functions/geo/load_boundary_gdf.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/recomendador_centros.py:199" +--- + +## Ejemplo + +```python +from geo.load_boundary_gdf import load_boundary_gdf + +gdf = load_boundary_gdf("boundary.geojson", crs="EPSG:4326") +print(gdf.crs) # EPSG:4326 +print(gdf.shape) # (n_features, n_columns) +``` + +## Notas + +Requiere geopandas. Si el archivo ya tiene CRS se llama `to_crs`; si no tiene CRS se llama `set_crs` para evitar advertencias de geopandas. Lanza FileNotFoundError antes de llamar a geopandas si el archivo no existe. diff --git a/python/functions/geo/load_boundary_gdf.py b/python/functions/geo/load_boundary_gdf.py new file mode 100644 index 00000000..e8df996b --- /dev/null +++ b/python/functions/geo/load_boundary_gdf.py @@ -0,0 +1,40 @@ +"""Load a GeoJSON boundary as a GeoDataFrame with normalized CRS.""" + +from __future__ import annotations + +from pathlib import Path + + +def load_boundary_gdf( + path: "str | Path", + crs: str = "EPSG:4326", +) -> "GeoDataFrame": + """Load a GeoJSON file as a GeoDataFrame with a normalized CRS. + + Args: + path: Path to the GeoJSON file. + crs: Target CRS string (e.g. "EPSG:4326"). If the file has no CRS + set, it is assigned this CRS. If the file already has a CRS, + it is reprojected to this CRS. + + Returns: + GeoDataFrame with the requested CRS set. + + Raises: + FileNotFoundError: If the file does not exist. + fiona.errors.DriverError: If the file is not a valid GeoJSON. + """ + import geopandas as gpd # type: ignore + + p = Path(path) + if not p.exists(): + raise FileNotFoundError(f"GeoJSON file not found: {p}") + + gdf = gpd.read_file(str(p)) + + if gdf.crs is None: + gdf = gdf.set_crs(crs) + else: + gdf = gdf.to_crs(crs) + + return gdf diff --git a/python/functions/geo/load_geojson_polygons.md b/python/functions/geo/load_geojson_polygons.md new file mode 100644 index 00000000..c77f1a69 --- /dev/null +++ b/python/functions/geo/load_geojson_polygons.md @@ -0,0 +1,44 @@ +--- +name: load_geojson_polygons +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: impure +signature: "def load_geojson_polygons(path: str | Path) -> list[list[list[tuple[float, float]]]]" +description: "Lee un GeoJSON y devuelve los polígonos como listas de anillos de tuplas (x, y). Polygon produce 1 polígono; MultiPolygon produce N polígonos." +tags: [geo, geojson, polygon, io, parse] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: ["json", "pathlib"] +params: + - name: path + desc: "Ruta al archivo GeoJSON. Puede ser str o Path." +output: "Lista de polígonos. Cada polígono es lista de anillos; cada anillo es lista de tuplas (x, y) float." +tested: true +tests: + - "polygon simple produce 1 polígono con 1 anillo" + - "archivo inexistente lanza FileNotFoundError" +test_file_path: "python/functions/geo/tests/test_load_geojson_polygons.py" +file_path: "python/functions/geo/load_geojson_polygons.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/backend/app.py:688" +--- + +## Ejemplo + +```python +from geo.load_geojson_polygons import load_geojson_polygons + +polygons = load_geojson_polygons("boundary.geojson") +# polygons[0] → lista de anillos del primer polígono +# polygons[0][0][0] → (lon, lat) del primer punto del exterior +``` + +## Notas + +Soporta Polygon y MultiPolygon. Geometrías nulas se omiten. Los puntos de cada anillo se convierten a tuplas (x, y) descartando la coordenada Z si existe. Lanza FileNotFoundError si el archivo no existe. diff --git a/python/functions/geo/load_geojson_polygons.py b/python/functions/geo/load_geojson_polygons.py new file mode 100644 index 00000000..539706a5 --- /dev/null +++ b/python/functions/geo/load_geojson_polygons.py @@ -0,0 +1,57 @@ +"""Load polygon rings from a GeoJSON file.""" + +from __future__ import annotations + +import json +from pathlib import Path + + +def load_geojson_polygons( + path: "str | Path", +) -> "list[list[list[tuple[float, float]]]]": + """Load polygons from a GeoJSON file. + + Reads every feature geometry. Polygon features produce one polygon; + MultiPolygon features produce N polygons. Each polygon is a list of rings + where each ring is a list of (x, y) float tuples. + + Args: + path: Path to the GeoJSON file. + + Returns: + List of polygons. Each polygon is a list of rings (exterior + holes). + Each ring is a list of (x, y) tuples. + + Raises: + FileNotFoundError: If the file does not exist. + ValueError: If the file is not valid GeoJSON or contains unsupported + geometry types. + """ + p = Path(path) + if not p.exists(): + raise FileNotFoundError(f"GeoJSON file not found: {p}") + + with p.open(encoding="utf-8") as f: + data = json.load(f) + + features = data.get("features", []) + result: list[list[list[tuple[float, float]]]] = [] + + for feature in features: + geom = feature.get("geometry") + if geom is None: + continue + gtype = geom.get("type") + coords = geom.get("coordinates", []) + + if gtype == "Polygon": + # coords = [exterior_ring, *hole_rings] + rings = [[(x, y) for x, y, *_ in ring] for ring in coords] + result.append(rings) + elif gtype == "MultiPolygon": + # coords = [polygon, ...] each polygon = [ring, ...] + for polygon_coords in coords: + rings = [[(x, y) for x, y, *_ in ring] for ring in polygon_coords] + result.append(rings) + + return result diff --git a/python/functions/geo/point_in_polygon.md b/python/functions/geo/point_in_polygon.md new file mode 100644 index 00000000..99547c89 --- /dev/null +++ b/python/functions/geo/point_in_polygon.md @@ -0,0 +1,51 @@ +--- +id: point_in_polygon_py_geo +name: point_in_polygon +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: pure +signature: "point_in_polygon(lon: float, lat: float, polygon: list[list[tuple[float, float]]]) -> bool" +description: "Determina si el punto esta dentro del poligono GeoJSON (exterior + holes). True si esta en el anillo exterior y NO en ningun hole." +tags: [geo, polygon, point-in-polygon, holes] +uses_functions: [point_in_ring_py_geo] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +example: | + from geo.point_in_polygon import point_in_polygon + outer = [(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)] + hole = [(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0)] + point_in_polygon(0.5, 0.5, [outer, hole]) # True (exterior, fuera del hole) + point_in_polygon(2.0, 2.0, [outer, hole]) # False (dentro del hole) +tested: true +tests: ["punto_en_exterior", "punto_en_hole", "punto_fuera", "poligono_vacio"] +test_file_path: "python/functions/geo/tests/test_point_in_polygon.py" +file_path: "python/functions/geo/point_in_polygon.py" +params: + - {name: lon, desc: "longitud del punto a comprobar en grados decimales"} + - {name: lat, desc: "latitud del punto a comprobar en grados decimales"} + - {name: polygon, desc: "lista de anillos; el primero es el exterior, el resto son agujeros (holes)"} +output: "True si el punto esta dentro del poligono y fuera de todos los holes" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/backend/app.py:733" +--- + +## Ejemplo + +```python +from geo.point_in_polygon import point_in_polygon + +outer = [(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)] +hole = [(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0)] +point_in_polygon(0.5, 0.5, [outer, hole]) # True +point_in_polygon(2.0, 2.0, [outer, hole]) # False +``` + +## Notas + +Compone point_in_ring. Soporta el formato de coordenadas GeoJSON (lista de anillos). diff --git a/python/functions/geo/point_in_polygon.py b/python/functions/geo/point_in_polygon.py new file mode 100644 index 00000000..6b55baa5 --- /dev/null +++ b/python/functions/geo/point_in_polygon.py @@ -0,0 +1,28 @@ +"""Comprobacion de punto dentro de poligono con soporte de agujeros (holes).""" + +from python.functions.geo.point_in_ring import point_in_ring + + +def point_in_polygon(lon: float, lat: float, polygon: list[list[tuple[float, float]]]) -> bool: + """Determina si el punto (lon, lat) esta dentro del poligono. + + polygon[0] es el anillo exterior. polygon[1:] son agujeros (holes). + Retorna True si el punto esta en el exterior y NO en ningun agujero. + + Args: + lon: longitud del punto en grados. + lat: latitud del punto en grados. + polygon: lista de anillos; el primero es el exterior, el resto son agujeros. + + Returns: + True si el punto esta dentro del poligono (y fuera de todos los holes). + """ + if not polygon: + return False + outer = polygon[0] + if not point_in_ring(lon, lat, outer): + return False + for hole in polygon[1:]: + if point_in_ring(lon, lat, hole): + return False + return True diff --git a/python/functions/geo/point_in_polygons_bbox.md b/python/functions/geo/point_in_polygons_bbox.md new file mode 100644 index 00000000..1d320584 --- /dev/null +++ b/python/functions/geo/point_in_polygons_bbox.md @@ -0,0 +1,53 @@ +--- +id: point_in_polygons_bbox_py_geo +name: point_in_polygons_bbox +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: pure +signature: "point_in_polygons_bbox(lon: float, lat: float, polygons: list[list[list[tuple[float, float]]]], bboxes: list[tuple[float, float, float, float]]) -> bool" +description: "Comprueba si el punto esta en CUALQUIER poligono de la lista usando prefiltraje por bounding box para mayor rendimiento." +tags: [geo, polygon, point-in-polygon, bbox, batch] +uses_functions: [point_in_polygon_py_geo] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +example: | + from geo.point_in_polygons_bbox import point_in_polygons_bbox + from geo.polygon_bbox import polygon_bbox + p1 = [[(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0)]] + p2 = [[(5.0,5.0),(6.0,5.0),(6.0,6.0),(5.0,6.0)]] + bboxes = [polygon_bbox(p1), polygon_bbox(p2)] + point_in_polygons_bbox(0.5, 0.5, [p1, p2], bboxes) # True +tested: true +tests: ["punto_en_primer_poligono", "punto_en_segundo_poligono", "punto_fuera_de_todos"] +test_file_path: "python/functions/geo/tests/test_point_in_polygons_bbox.py" +file_path: "python/functions/geo/point_in_polygons_bbox.py" +params: + - {name: lon, desc: "longitud del punto a comprobar en grados decimales"} + - {name: lat, desc: "latitud del punto a comprobar en grados decimales"} + - {name: polygons, desc: "lista de poligonos, cada uno como lista de anillos (exterior + holes)"} + - {name: bboxes, desc: "lista de bboxes precalculados (minx, miny, maxx, maxy) para cada poligono, en el mismo orden"} +output: "True si el punto esta dentro de al menos uno de los poligonos" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/backend/app.py:745" +--- + +## Ejemplo + +```python +from geo.point_in_polygons_bbox import point_in_polygons_bbox +from geo.polygon_bbox import polygon_bbox + +p1 = [[(0.0,0.0),(1.0,0.0),(1.0,1.0),(0.0,1.0)]] +bboxes = [polygon_bbox(p1)] +point_in_polygons_bbox(0.5, 0.5, [p1], bboxes) # True +``` + +## Notas + +Los bboxes deben precalcularse con polygon_bbox y pasarse en el mismo orden que polygons. diff --git a/python/functions/geo/point_in_polygons_bbox.py b/python/functions/geo/point_in_polygons_bbox.py new file mode 100644 index 00000000..daa197ed --- /dev/null +++ b/python/functions/geo/point_in_polygons_bbox.py @@ -0,0 +1,31 @@ +"""Comprobacion de punto contra una lista de poligonos con prefiltraje por bbox.""" + +from python.functions.geo.point_in_polygon import point_in_polygon + + +def point_in_polygons_bbox( + lon: float, + lat: float, + polygons: list[list[list[tuple[float, float]]]], + bboxes: list[tuple[float, float, float, float]], +) -> bool: + """Determina si el punto (lon, lat) esta dentro de CUALQUIER poligono de la lista. + + Aplica un prefiltraje por bounding box antes de ejecutar el ray casting completo, + lo que mejora el rendimiento cuando hay muchos poligonos. + + Args: + lon: longitud del punto en grados. + lat: latitud del punto en grados. + polygons: lista de poligonos, cada uno como lista de anillos (exterior + holes). + bboxes: lista de bboxes precalculados (minx, miny, maxx, maxy) para cada poligono. + + Returns: + True si el punto esta dentro de al menos uno de los poligonos. + """ + for polygon, (minx, miny, maxx, maxy) in zip(polygons, bboxes): + if lon < minx or lon > maxx or lat < miny or lat > maxy: + continue + if point_in_polygon(lon, lat, polygon): + return True + return False diff --git a/python/functions/geo/point_in_ring.md b/python/functions/geo/point_in_ring.md new file mode 100644 index 00000000..a0027f6b --- /dev/null +++ b/python/functions/geo/point_in_ring.md @@ -0,0 +1,48 @@ +--- +id: point_in_ring_py_geo +name: point_in_ring +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: pure +signature: "point_in_ring(lon: float, lat: float, ring: list[tuple[float, float]]) -> bool" +description: "Determina si el punto (lon, lat) esta dentro de un anillo poligonal usando ray casting. Retorna False si len(ring) < 3." +tags: [geo, polygon, ray-casting, point-in-polygon] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +example: | + from geo.point_in_ring import point_in_ring + ring = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] + inside = point_in_ring(0.5, 0.5, ring) # True +tested: true +tests: ["punto_dentro_cuadrado", "punto_fuera_cuadrado", "ring_menor_3_vertices"] +test_file_path: "python/functions/geo/tests/test_point_in_ring.py" +file_path: "python/functions/geo/point_in_ring.py" +params: + - {name: lon, desc: "longitud del punto a comprobar en grados decimales"} + - {name: lat, desc: "latitud del punto a comprobar en grados decimales"} + - {name: ring, desc: "lista de vertices (lon, lat) que forman el anillo cerrado"} +output: "True si el punto esta dentro del anillo, False en caso contrario" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/backend/app.py:715" +--- + +## Ejemplo + +```python +from geo.point_in_ring import point_in_ring + +ring = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] +point_in_ring(0.5, 0.5, ring) # True +point_in_ring(2.0, 2.0, ring) # False +``` + +## Notas + +Algoritmo de ray casting clasico. El epsilon 1e-15 en el denominador evita division por cero en aristas horizontales. diff --git a/python/functions/geo/point_in_ring.py b/python/functions/geo/point_in_ring.py new file mode 100644 index 00000000..7194b67e --- /dev/null +++ b/python/functions/geo/point_in_ring.py @@ -0,0 +1,31 @@ +"""Ray casting para determinar si un punto esta dentro de un anillo (ring) poligonal.""" + + +def point_in_ring(lon: float, lat: float, ring: list[tuple[float, float]]) -> bool: + """Determina si el punto (lon, lat) esta dentro del anillo cerrado ring. + + Usa el algoritmo de ray casting. Retorna False si len(ring) < 3. + + Args: + lon: longitud del punto en grados. + lat: latitud del punto en grados. + ring: lista de vertices (lon, lat) que forman el anillo. + + Returns: + True si el punto esta dentro del anillo, False en caso contrario. + """ + inside = False + n = len(ring) + if n < 3: + return False + j = n - 1 + for i in range(n): + xi, yi = ring[i] + xj, yj = ring[j] + intersects = ((yi > lat) != (yj > lat)) and ( + lon < (xj - xi) * (lat - yi) / (yj - yi + 1e-15) + xi + ) + if intersects: + inside = not inside + j = i + return inside diff --git a/python/functions/geo/polygon_bbox.md b/python/functions/geo/polygon_bbox.md new file mode 100644 index 00000000..3f6edfc6 --- /dev/null +++ b/python/functions/geo/polygon_bbox.md @@ -0,0 +1,45 @@ +--- +id: polygon_bbox_py_geo +name: polygon_bbox +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: pure +signature: "polygon_bbox(polygon: list[list[tuple[float, float]]]) -> tuple[float, float, float, float]" +description: "Calcula el bounding box (minx, miny, maxx, maxy) que envuelve todos los anillos de un poligono." +tags: [geo, polygon, bbox, bounds] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "" +imports: [] +example: | + from geo.polygon_bbox import polygon_bbox + ring = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] + bbox = polygon_bbox([ring]) # (0.0, 0.0, 1.0, 1.0) +tested: true +tests: ["cuadrado_unitario", "poligono_con_hole"] +test_file_path: "python/functions/geo/tests/test_polygon_bbox.py" +file_path: "python/functions/geo/polygon_bbox.py" +params: + - {name: polygon, desc: "lista de anillos [(lon, lat)]; puede incluir anillo exterior y agujeros"} +output: "tupla (minx, miny, maxx, maxy) con las coordenadas extremas del poligono" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/backend/app.py:709" +--- + +## Ejemplo + +```python +from geo.polygon_bbox import polygon_bbox + +ring = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] +polygon_bbox([ring]) # (0.0, 0.0, 1.0, 1.0) +``` + +## Notas + +Recorre todos los anillos (exterior + holes) para calcular el bbox global. diff --git a/python/functions/geo/polygon_bbox.py b/python/functions/geo/polygon_bbox.py new file mode 100644 index 00000000..4299e932 --- /dev/null +++ b/python/functions/geo/polygon_bbox.py @@ -0,0 +1,15 @@ +"""Calculo del bounding box (minx, miny, maxx, maxy) de un poligono.""" + + +def polygon_bbox(polygon: list[list[tuple[float, float]]]) -> tuple[float, float, float, float]: + """Calcula el bounding box que envuelve todos los anillos del poligono. + + Args: + polygon: lista de anillos [(lon, lat), ...]; puede tener varios anillos (exterior + holes). + + Returns: + Tupla (minx, miny, maxx, maxy) con las coordenadas extremas del poligono. + """ + xs = [pt[0] for ring in polygon for pt in ring] + ys = [pt[1] for ring in polygon for pt in ring] + return min(xs), min(ys), max(xs), max(ys) diff --git a/python/functions/geo/tests/__init__.py b/python/functions/geo/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/functions/geo/tests/test_add_basemap_osm.py b/python/functions/geo/tests/test_add_basemap_osm.py new file mode 100644 index 00000000..194365d4 --- /dev/null +++ b/python/functions/geo/tests/test_add_basemap_osm.py @@ -0,0 +1,22 @@ +"""Tests para add_basemap_osm.""" + +import sys +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from geo.add_basemap_osm import add_basemap_osm + + +def test_no_lanza_excepcion_con_Axes_valido(): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + ax.set_xlim(-430000, -350000) + ax.set_ylim(4500000, 4600000) + # Must not raise regardless of network availability + add_basemap_osm(ax, zoom=5) + plt.close(fig) diff --git a/python/functions/geo/tests/test_add_basemap_with_timeout.py b/python/functions/geo/tests/test_add_basemap_with_timeout.py new file mode 100644 index 00000000..a8c0896f --- /dev/null +++ b/python/functions/geo/tests/test_add_basemap_with_timeout.py @@ -0,0 +1,23 @@ +"""Tests para add_basemap_with_timeout.""" + +import sys +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from geo.add_basemap_with_timeout import add_basemap_with_timeout + + +def test_timeout_muy_corto_retorna_False_sin_colgar(): + import matplotlib.pyplot as plt + + fig, ax = plt.subplots() + ax.set_xlim(-430000, -350000) + ax.set_ylim(4500000, 4600000) + # 0.001 s timeout — should fail/timeout fast and return False + result = add_basemap_with_timeout(ax, zoom=9, timeout_s=0.001) + plt.close(fig) + assert result is False, f"expected False with 0.001s timeout, got {result}" diff --git a/python/functions/geo/tests/test_distance_bucket.py b/python/functions/geo/tests/test_distance_bucket.py new file mode 100644 index 00000000..8825f6d2 --- /dev/null +++ b/python/functions/geo/tests/test_distance_bucket.py @@ -0,0 +1,25 @@ +"""Tests para distance_bucket.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.geo.distance_bucket import distance_bucket + + +def test_bucket_0_5(): + assert distance_bucket(3.0) == "0-5" + + +def test_bucket_5_10(): + assert distance_bucket(7.0) == "5-10" + + +def test_bucket_borde_exacto(): + # 10 <= 10 → "5-10" + assert distance_bucket(10.0) == "5-10" + + +def test_bucket_160_mas(): + assert distance_bucket(200.0) == "160+" diff --git a/python/functions/geo/tests/test_extent_with_padding.py b/python/functions/geo/tests/test_extent_with_padding.py new file mode 100644 index 00000000..96d7b16c --- /dev/null +++ b/python/functions/geo/tests/test_extent_with_padding.py @@ -0,0 +1,19 @@ +"""Tests para extent_with_padding.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.geo.extent_with_padding import extent_with_padding + + +def test_bbox_cuadrado_con_10_pct(): + result = extent_with_padding((0.0, 0.0, 10.0, 10.0), 0.1) + assert result == (-1.0, 11.0, -1.0, 11.0) + + +def test_pad_ratio_cero_no_cambia(): + bounds = (2.0, 3.0, 8.0, 9.0) + result = extent_with_padding(bounds, 0.0) + assert result == (2.0, 8.0, 3.0, 9.0) diff --git a/python/functions/geo/tests/test_haversine_km.py b/python/functions/geo/tests/test_haversine_km.py new file mode 100644 index 00000000..a9b961de --- /dev/null +++ b/python/functions/geo/tests/test_haversine_km.py @@ -0,0 +1,18 @@ +"""Tests para haversine_km.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.geo.haversine_km import haversine_km + + +def test_madrid_barcelona_aproximado(): + d = haversine_km(-3.7038, 40.4168, 2.1686, 41.3874) + assert abs(d - 504.0) < 2.0, f"Esperado ~504 km, got {d:.1f}" + + +def test_misma_coordenada_es_cero(): + d = haversine_km(0.0, 0.0, 0.0, 0.0) + assert d == 0.0, f"Misma coordenada debe ser 0, got {d}" diff --git a/python/functions/geo/tests/test_load_boundary_gdf.py b/python/functions/geo/tests/test_load_boundary_gdf.py new file mode 100644 index 00000000..343a587f --- /dev/null +++ b/python/functions/geo/tests/test_load_boundary_gdf.py @@ -0,0 +1,61 @@ +"""Tests para load_boundary_gdf.""" + +import json +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from geo.load_boundary_gdf import load_boundary_gdf + + +def _write_geojson(data: dict) -> Path: + f = tempfile.NamedTemporaryFile( + mode="w", suffix=".geojson", delete=False, encoding="utf-8" + ) + json.dump(data, f) + f.close() + return Path(f.name) + + +def test_retorna_GeoDataFrame_con_CRS_EPSG4326(): + geojson = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-3.7, 40.4], + [-3.6, 40.4], + [-3.6, 40.5], + [-3.7, 40.5], + [-3.7, 40.4], + ] + ], + }, + "properties": {"name": "test"}, + } + ], + } + path = _write_geojson(geojson) + try: + gdf = load_boundary_gdf(path, crs="EPSG:4326") + import geopandas as gpd # type: ignore + + assert isinstance(gdf, gpd.GeoDataFrame), "result should be a GeoDataFrame" + assert gdf.crs is not None, "CRS should be set" + assert gdf.crs.to_epsg() == 4326, f"expected EPSG:4326, got {gdf.crs}" + assert len(gdf) == 1, f"expected 1 feature, got {len(gdf)}" + finally: + path.unlink(missing_ok=True) + + +def test_archivo_inexistente_lanza_FileNotFoundError(): + import pytest + + with pytest.raises(FileNotFoundError): + load_boundary_gdf("/tmp/this_file_does_not_exist_xyz.geojson") diff --git a/python/functions/geo/tests/test_load_geojson_polygons.py b/python/functions/geo/tests/test_load_geojson_polygons.py new file mode 100644 index 00000000..21c94bc5 --- /dev/null +++ b/python/functions/geo/tests/test_load_geojson_polygons.py @@ -0,0 +1,59 @@ +"""Tests para load_geojson_polygons.""" + +import json +import sys +import tempfile +from pathlib import Path + +sys.path.insert(0, str(Path(__file__).parent.parent.parent)) + +from geo.load_geojson_polygons import load_geojson_polygons + + +def _write_geojson(data: dict) -> Path: + f = tempfile.NamedTemporaryFile( + mode="w", suffix=".geojson", delete=False, encoding="utf-8" + ) + json.dump(data, f) + f.close() + return Path(f.name) + + +def test_polygon_simple_produce_1_poligono_con_1_anillo(): + geojson = { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [-3.7, 40.4], + [-3.6, 40.4], + [-3.6, 40.5], + [-3.7, 40.5], + [-3.7, 40.4], + ] + ], + }, + "properties": {}, + } + ], + } + path = _write_geojson(geojson) + try: + result = load_geojson_polygons(path) + assert len(result) == 1, f"expected 1 polygon, got {len(result)}" + assert len(result[0]) == 1, "expected 1 ring" + assert len(result[0][0]) >= 4, "ring should have >= 4 points" + assert isinstance(result[0][0][0], tuple), "points should be tuples" + finally: + path.unlink(missing_ok=True) + + +def test_archivo_inexistente_lanza_FileNotFoundError(): + import pytest + + with pytest.raises(FileNotFoundError): + load_geojson_polygons("/tmp/this_file_does_not_exist_xyz.geojson") diff --git a/python/functions/geo/tests/test_point_in_polygon.py b/python/functions/geo/tests/test_point_in_polygon.py new file mode 100644 index 00000000..ad4da000 --- /dev/null +++ b/python/functions/geo/tests/test_point_in_polygon.py @@ -0,0 +1,29 @@ +"""Tests para point_in_polygon.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.geo.point_in_polygon import point_in_polygon + +OUTER = [(0.0, 0.0), (4.0, 0.0), (4.0, 4.0), (0.0, 4.0)] +HOLE = [(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0)] + + +def test_punto_en_exterior(): + # Punto en el anillo exterior, fuera del hole + assert point_in_polygon(0.5, 0.5, [OUTER, HOLE]) is True + + +def test_punto_en_hole(): + # Punto dentro del hole → False + assert point_in_polygon(2.0, 2.0, [OUTER, HOLE]) is False + + +def test_punto_fuera(): + assert point_in_polygon(10.0, 10.0, [OUTER, HOLE]) is False + + +def test_poligono_vacio(): + assert point_in_polygon(0.5, 0.5, []) is False diff --git a/python/functions/geo/tests/test_point_in_polygons_bbox.py b/python/functions/geo/tests/test_point_in_polygons_bbox.py new file mode 100644 index 00000000..5f27b417 --- /dev/null +++ b/python/functions/geo/tests/test_point_in_polygons_bbox.py @@ -0,0 +1,25 @@ +"""Tests para point_in_polygons_bbox.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.geo.point_in_polygons_bbox import point_in_polygons_bbox +from python.functions.geo.polygon_bbox import polygon_bbox + +P1 = [[(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]] +P2 = [[(5.0, 5.0), (6.0, 5.0), (6.0, 6.0), (5.0, 6.0)]] +BBOXES = [polygon_bbox(P1), polygon_bbox(P2)] + + +def test_punto_en_primer_poligono(): + assert point_in_polygons_bbox(0.5, 0.5, [P1, P2], BBOXES) is True + + +def test_punto_en_segundo_poligono(): + assert point_in_polygons_bbox(5.5, 5.5, [P1, P2], BBOXES) is True + + +def test_punto_fuera_de_todos(): + assert point_in_polygons_bbox(10.0, 10.0, [P1, P2], BBOXES) is False diff --git a/python/functions/geo/tests/test_point_in_ring.py b/python/functions/geo/tests/test_point_in_ring.py new file mode 100644 index 00000000..a23618fa --- /dev/null +++ b/python/functions/geo/tests/test_point_in_ring.py @@ -0,0 +1,22 @@ +"""Tests para point_in_ring.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.geo.point_in_ring import point_in_ring + +SQUARE = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] + + +def test_punto_dentro_cuadrado(): + assert point_in_ring(0.5, 0.5, SQUARE) is True + + +def test_punto_fuera_cuadrado(): + assert point_in_ring(2.0, 2.0, SQUARE) is False + + +def test_ring_menor_3_vertices(): + assert point_in_ring(0.0, 0.0, [(0.0, 0.0), (1.0, 1.0)]) is False diff --git a/python/functions/geo/tests/test_polygon_bbox.py b/python/functions/geo/tests/test_polygon_bbox.py new file mode 100644 index 00000000..47315fc9 --- /dev/null +++ b/python/functions/geo/tests/test_polygon_bbox.py @@ -0,0 +1,19 @@ +"""Tests para polygon_bbox.""" + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.geo.polygon_bbox import polygon_bbox + + +def test_cuadrado_unitario(): + ring = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)] + assert polygon_bbox([ring]) == (0.0, 0.0, 1.0, 1.0) + + +def test_poligono_con_hole(): + outer = [(0.0, 0.0), (5.0, 0.0), (5.0, 5.0), (0.0, 5.0)] + hole = [(1.0, 1.0), (3.0, 1.0), (3.0, 3.0), (1.0, 3.0)] + assert polygon_bbox([outer, hole]) == (0.0, 0.0, 5.0, 5.0) diff --git a/python/functions/geo/tests/test_valhalla_isochrone.py b/python/functions/geo/tests/test_valhalla_isochrone.py new file mode 100644 index 00000000..50398211 --- /dev/null +++ b/python/functions/geo/tests/test_valhalla_isochrone.py @@ -0,0 +1,36 @@ +"""Tests para valhalla_isochrone.""" + +from __future__ import annotations + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import httpx +import pytest + +from valhalla_isochrone import valhalla_isochrone + + +def _valhalla_alive(url: str = "http://localhost:8002") -> bool: + try: + r = httpx.get(f"{url}/status", timeout=2.0) + return r.status_code < 500 + except Exception: + return False + + +VALHALLA_OK = _valhalla_alive() +skip_if_no_valhalla = pytest.mark.skipif( + not VALHALLA_OK, reason="Valhalla no activo en :8002" +) + + +@skip_if_no_valhalla +def test_isócrona_10_min_madrid_contiene_features(): + """isócrona 10 min Madrid contiene features""" + gj = valhalla_isochrone(lat=40.4168, lon=-3.7038, minutes=10) + assert gj is not None, "Esperaba GeoJSON, obtuvo None" + assert "features" in gj, "GeoJSON no contiene 'features'" + assert len(gj["features"]) > 0, "features está vacío" diff --git a/python/functions/geo/tests/test_valhalla_isochrones_async.py b/python/functions/geo/tests/test_valhalla_isochrones_async.py new file mode 100644 index 00000000..eaf31b8b --- /dev/null +++ b/python/functions/geo/tests/test_valhalla_isochrones_async.py @@ -0,0 +1,43 @@ +"""Tests para valhalla_isochrones_async.""" + +from __future__ import annotations + +import asyncio +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import httpx +import pytest + +from valhalla_isochrones_async import valhalla_isochrones_async + + +def _valhalla_alive(url: str = "http://localhost:8002") -> bool: + try: + r = httpx.get(f"{url}/status", timeout=2.0) + return r.status_code < 500 + except Exception: + return False + + +VALHALLA_OK = _valhalla_alive() +skip_if_no_valhalla = pytest.mark.skipif( + not VALHALLA_OK, reason="Valhalla no activo en :8002" +) + + +@skip_if_no_valhalla +def test_3_puntos_madrid_retornan_lista_de_3(): + """3 puntos Madrid retornan lista de 3""" + pts = [ + {"lat": 40.4168, "lon": -3.7038, "minutes": 10, "id": "sol"}, + {"lat": 40.4530, "lon": -3.6883, "minutes": 10, "id": "retiro"}, + {"lat": 40.4005, "lon": -3.7057, "minutes": 10, "id": "atocha"}, + ] + results = asyncio.run(valhalla_isochrones_async(pts)) + assert len(results) == 3, f"Esperaba 3 resultados, obtuvo {len(results)}" + for i, gj in enumerate(results): + assert gj is not None, f"Resultado {i} es None" + assert "features" in gj, f"Resultado {i} no contiene 'features'" diff --git a/python/functions/geo/tests/test_valhalla_matrix_1_to_n.py b/python/functions/geo/tests/test_valhalla_matrix_1_to_n.py new file mode 100644 index 00000000..c7a9df48 --- /dev/null +++ b/python/functions/geo/tests/test_valhalla_matrix_1_to_n.py @@ -0,0 +1,46 @@ +"""Tests para valhalla_matrix_1_to_n.""" + +from __future__ import annotations + +import math +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import httpx +import pytest + +from valhalla_matrix_1_to_n import valhalla_matrix_1_to_n + + +def _valhalla_alive(url: str = "http://localhost:8002") -> bool: + try: + r = httpx.get(f"{url}/status", timeout=2.0) + return r.status_code < 500 + except Exception: + return False + + +VALHALLA_OK = _valhalla_alive() +skip_if_no_valhalla = pytest.mark.skipif( + not VALHALLA_OK, reason="Valhalla no activo en :8002" +) + + +@skip_if_no_valhalla +def test_matrix_1_origen_2_destinos_retorna_2_dicts_con_meters_mayor_0(): + """matrix 1 origen 2 destinos retorna 2 dicts con meters > 0""" + origins = [(40.4168, -3.7038)] # Madrid + destinations = [ + (41.3874, 2.1686), # Barcelona + (37.3886, -5.9823), # Sevilla + ] + pairs = [(0, 0), (0, 1)] + + results = valhalla_matrix_1_to_n(origins, destinations, pairs) + assert len(results) == 2, f"Esperaba 2 resultados, obtuvo {len(results)}" + for i, r in enumerate(results): + assert r["error"] == 0, f"Par {i} tiene error={r['error']}" + assert r["meters"] > 0, f"Par {i} tiene meters={r['meters']}" + assert not math.isnan(r["seconds"]), f"Par {i} tiene seconds=NaN" diff --git a/python/functions/geo/tests/test_valhalla_route.py b/python/functions/geo/tests/test_valhalla_route.py new file mode 100644 index 00000000..131dd729 --- /dev/null +++ b/python/functions/geo/tests/test_valhalla_route.py @@ -0,0 +1,41 @@ +"""Tests para valhalla_route.""" + +from __future__ import annotations + +import sys +import os + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..")) + +import httpx +import pytest + +from valhalla_route import valhalla_route + + +def _valhalla_alive(url: str = "http://localhost:8002") -> bool: + try: + r = httpx.get(f"{url}/status", timeout=2.0) + return r.status_code < 500 + except Exception: + return False + + +VALHALLA_OK = _valhalla_alive() +skip_if_no_valhalla = pytest.mark.skipif( + not VALHALLA_OK, reason="Valhalla no activo en :8002" +) + + +@skip_if_no_valhalla +def test_ruta_madrid_barcelona_supera_500_km(): + """ruta Madrid-Barcelona supera 500 km""" + result = valhalla_route( + locations=[ + {"lat": 40.4168, "lon": -3.7038}, + {"lat": 41.3874, "lon": 2.1686}, + ] + ) + assert result is not None, "Esperaba respuesta, obtuvo None" + summary = result["trip"]["summary"] + assert summary["length"] > 500, f"Distancia {summary['length']} km < 500 km" diff --git a/python/functions/geo/valhalla_isochrone.md b/python/functions/geo/valhalla_isochrone.md new file mode 100644 index 00000000..de25be2c --- /dev/null +++ b/python/functions/geo/valhalla_isochrone.md @@ -0,0 +1,56 @@ +--- +name: valhalla_isochrone +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: impure +signature: "def valhalla_isochrone(lat: float, lon: float, minutes: int, base_url: str = 'http://localhost:8002', costing: str = 'auto', denoise: float = 0.6, generalize_m: int = 50, polygons: bool = True, timeout_s: float = 120.0) -> dict | None" +description: "Calcula la isócrona (área alcanzable en N minutos) de un punto usando Valhalla. Retorna GeoJSON dict con el polígono o None si error." +tags: [valhalla, isochrone, geo, http, geojson] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: true +error_type: "error_go_core" +imports: [httpx] +params: + - name: lat + desc: "Latitud del punto de origen en grados decimales (WGS84)." + - name: lon + desc: "Longitud del punto de origen en grados decimales (WGS84)." + - name: minutes + desc: "Tiempo de viaje en minutos para el contorno de la isócrona." + - name: base_url + desc: "URL base del servidor Valhalla. Por defecto http://localhost:8002." + - name: costing + desc: "Modelo de coste: 'auto', 'bicycle', 'pedestrian', etc." + - name: denoise + desc: "Factor de suavizado del contorno (0-1). Valores menores dan contornos más fragmentados." + - name: generalize_m + desc: "Tolerancia de generalización de la geometría en metros." + - name: polygons + desc: "Si True retorna polígono cerrado; si False retorna línea del contorno." + - name: timeout_s + desc: "Timeout en segundos para la request HTTP." +output: "GeoJSON dict con campo 'features' conteniendo el polígono o línea de la isócrona, o None si el servidor no responde o retorna error." +tested: true +tests: ["isócrona 10 min Madrid contiene features"] +test_file_path: "python/functions/geo/tests/test_valhalla_isochrone.py" +file_path: "python/functions/geo/valhalla_isochrone.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/recomendador_centros.py" +--- + +## Ejemplo + +```python +gj = valhalla_isochrone(lat=40.4168, lon=-3.7038, minutes=15) +if gj: + print(f"{len(gj['features'])} features en la isócrona de 15 min") +``` + +## Notas + +Extraida de `_fetch_isochrone_polygon` en `recomendador_centros.py`. Parametrizada para ser reutilizable (el original usaba constantes globales VALHALLA_URL, CONCURRENCY, TIMEOUT_S). Retorna None ante cualquier excepcion. diff --git a/python/functions/geo/valhalla_isochrone.py b/python/functions/geo/valhalla_isochrone.py new file mode 100644 index 00000000..1162114d --- /dev/null +++ b/python/functions/geo/valhalla_isochrone.py @@ -0,0 +1,50 @@ +"""Isócrona de un punto via Valhalla routing engine.""" + +from __future__ import annotations + +import httpx + + +def valhalla_isochrone( + lat: float, + lon: float, + minutes: int, + base_url: str = "http://localhost:8002", + costing: str = "auto", + denoise: float = 0.6, + generalize_m: int = 50, + polygons: bool = True, + timeout_s: float = 120.0, +) -> dict | None: + """Calcula la isócrona de un punto usando Valhalla. + + Args: + lat: Latitud del punto de origen. + lon: Longitud del punto de origen. + minutes: Tiempo de viaje en minutos para el contorno. + base_url: URL base del servidor Valhalla. + costing: Modelo de coste ('auto', 'bicycle', 'pedestrian', etc.). + denoise: Factor de suavizado del contorno (0-1). Por defecto 0.6. + generalize_m: Tolerancia de generalización en metros. Por defecto 50. + polygons: Si True retorna polígono; si False retorna línea. + timeout_s: Timeout en segundos. Por defecto 120. + + Returns: + GeoJSON dict con la isócrona o None si error. + """ + url = f"{base_url.rstrip('/')}/isochrone" + payload = { + "locations": [{"lat": lat, "lon": lon}], + "costing": costing, + "contours": [{"time": minutes}], + "polygons": polygons, + "denoise": denoise, + "generalize": generalize_m, + "format": "geojson", + } + try: + r = httpx.post(url, json=payload, timeout=httpx.Timeout(timeout_s)) + r.raise_for_status() + return r.json() + except Exception: + return None diff --git a/python/functions/geo/valhalla_isochrones_async.md b/python/functions/geo/valhalla_isochrones_async.md new file mode 100644 index 00000000..b08d8922 --- /dev/null +++ b/python/functions/geo/valhalla_isochrones_async.md @@ -0,0 +1,59 @@ +--- +name: valhalla_isochrones_async +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: impure +signature: "async def valhalla_isochrones_async(requests: list[dict], base_url: str = 'http://localhost:8002', costing: str = 'auto', concurrency: int = 6, timeout_s: float = 120.0, denoise: float = 0.6, generalize_m: int = 50) -> list[dict | None]" +description: "Calcula isócronas para múltiples puntos en paralelo usando httpx.AsyncClient y asyncio.Semaphore. Order-preserving: la lista retornada es paralela a la de entrada." +tags: [valhalla, isochrone, geo, http, async, asyncio, geojson, batch] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: true +error_type: "error_go_core" +imports: [httpx, asyncio] +params: + - name: requests + desc: "Lista de dicts con 'lat' (float), 'lon' (float), 'minutes' (int) y opcionalmente 'id' (str). Un elemento por punto." + - name: base_url + desc: "URL base del servidor Valhalla. Por defecto http://localhost:8002." + - name: costing + desc: "Modelo de coste: 'auto', 'bicycle', 'pedestrian', etc." + - name: concurrency + desc: "Número máximo de requests HTTP simultáneas (Semaphore). Por defecto 6." + - name: timeout_s + desc: "Timeout en segundos por request individual." + - name: denoise + desc: "Factor de suavizado del contorno (0-1)." + - name: generalize_m + desc: "Tolerancia de generalización de la geometría en metros." +output: "Lista paralela a 'requests' con GeoJSON dict (campo 'features') o None por cada punto. Preserva el orden de entrada. Nunca lanza excepcion — fallos individuales se mapean a None." +tested: true +tests: ["3 puntos Madrid retornan lista de 3"] +test_file_path: "python/functions/geo/tests/test_valhalla_isochrones_async.py" +file_path: "python/functions/geo/valhalla_isochrones_async.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/generar_isochronas_aurgi.py" +--- + +## Ejemplo + +```python +import asyncio + +pts = [ + {"lat": 40.4168, "lon": -3.7038, "minutes": 10, "id": "madrid_centro"}, + {"lat": 40.4530, "lon": -3.6883, "minutes": 10, "id": "retiro"}, +] +results = asyncio.run(valhalla_isochrones_async(pts)) +for req, gj in zip(pts, results): + status = "ok" if gj else "error" + print(f"{req['id']}: {status}") +``` + +## Notas + +Adaptada de `_run_isochrones` y `_fetch_isochrone` en `generar_isochronas_aurgi.py`. La versión original acoplaba pandas DataFrames y escritura a disco — esta versión es pandas-free y retorna datos en memoria. Usa asyncio.gather para preservar el orden de resultados. diff --git a/python/functions/geo/valhalla_isochrones_async.py b/python/functions/geo/valhalla_isochrones_async.py new file mode 100644 index 00000000..9b9d04b8 --- /dev/null +++ b/python/functions/geo/valhalla_isochrones_async.py @@ -0,0 +1,62 @@ +"""Isócronas de múltiples puntos en paralelo via Valhalla (async).""" + +from __future__ import annotations + +import asyncio + +import httpx + + +async def valhalla_isochrones_async( + requests: list[dict], + base_url: str = "http://localhost:8002", + costing: str = "auto", + concurrency: int = 6, + timeout_s: float = 120.0, + denoise: float = 0.6, + generalize_m: int = 50, +) -> list[dict | None]: + """Calcula isócronas para múltiples puntos en paralelo usando Valhalla. + + Args: + requests: Lista de dicts con 'lat' (float), 'lon' (float), 'minutes' (int) + y opcionalmente 'id' (str). Cada elemento genera una isócrona. + base_url: URL base del servidor Valhalla. + costing: Modelo de coste ('auto', 'bicycle', 'pedestrian', etc.). + concurrency: Número máximo de requests simultáneas. + timeout_s: Timeout en segundos por request. + denoise: Factor de suavizado del contorno (0-1). + generalize_m: Tolerancia de generalización en metros. + + Returns: + Lista paralela a 'requests' con GeoJSON dict o None por cada punto. + Preserva el orden de entrada. + """ + url = f"{base_url.rstrip('/')}/isochrone" + sem = asyncio.Semaphore(concurrency) + timeout = httpx.Timeout(timeout_s) + + async def _fetch_one( + client: httpx.AsyncClient, + req: dict, + ) -> dict | None: + payload = { + "locations": [{"lat": float(req["lat"]), "lon": float(req["lon"])}], + "costing": costing, + "contours": [{"time": int(req["minutes"])}], + "polygons": True, + "denoise": denoise, + "generalize": generalize_m, + "format": "geojson", + } + try: + async with sem: + r = await client.post(url, json=payload) + r.raise_for_status() + return r.json() + except Exception: + return None + + async with httpx.AsyncClient(timeout=timeout) as client: + tasks = [_fetch_one(client, req) for req in requests] + return list(await asyncio.gather(*tasks)) diff --git a/python/functions/geo/valhalla_matrix_1_to_n.md b/python/functions/geo/valhalla_matrix_1_to_n.md new file mode 100644 index 00000000..675299a1 --- /dev/null +++ b/python/functions/geo/valhalla_matrix_1_to_n.md @@ -0,0 +1,65 @@ +--- +name: valhalla_matrix_1_to_n +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: impure +signature: "def valhalla_matrix_1_to_n(origins: list[tuple[float, float]], destinations: list[tuple[float, float]], pairs: list[tuple[int, int]], base_url: str = 'http://localhost:8002', costing: str = 'auto', max_targets_per_request: int = 400, max_distance_m: float = 30000.0, timeout_s: float = 120.0, concurrency: int = 12, search_radius_m: float = 0.0) -> list[dict]" +description: "Calcula distancias (metros) y tiempos (segundos) para pares origen-destino usando Valhalla sources_to_targets. Agrupa pares por origen para minimizar requests. Paraleliza con ThreadPoolExecutor. Pandas-free." +tags: [valhalla, matrix, geo, http, routing, distance, batch, threading] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [json, math, collections, concurrent.futures, threading, urllib.request] +params: + - name: origins + desc: "Lista de (lat, lon) en grados decimales WGS84. Son los posibles puntos de origen." + - name: destinations + desc: "Lista de (lat, lon) en grados decimales WGS84. Son los posibles destinos." + - name: pairs + desc: "Lista de (origin_idx, dest_idx) indicando qué pares calcular. Los índices referencian las listas origins y destinations." + - name: base_url + desc: "URL base del servidor Valhalla." + - name: costing + desc: "Modelo de coste: 'auto', 'bicycle', 'pedestrian', etc." + - name: max_targets_per_request + desc: "Máximo de destinos por request POST a Valhalla. Chunks más grandes son más eficientes pero pueden saturar el servidor." + - name: max_distance_m + desc: "Distancia en metros usada como fallback cuando un par falla (error=1)." + - name: timeout_s + desc: "Timeout en segundos por request HTTP." + - name: concurrency + desc: "Número de threads paralelos para las requests." + - name: search_radius_m + desc: "Radio en metros para snapping de coordenadas a la red viaria (0 = default de Valhalla)." +output: "Lista paralela a 'pairs' con dicts {\"meters\": float, \"seconds\": float, \"error\": int}. error=0 si OK, error=1 si Valhalla falló. En error: meters=max_distance_m, seconds=NaN." +tested: true +tests: ["matrix 1 origen 2 destinos retorna 2 dicts con meters > 0"] +test_file_path: "python/functions/geo/tests/test_valhalla_matrix_1_to_n.py" +file_path: "python/functions/geo/valhalla_matrix_1_to_n.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/add_distancias_valhalla.py" +--- + +## Ejemplo + +```python +origins = [(40.4168, -3.7038)] # Madrid +destinations = [ + (41.3874, 2.1686), # Barcelona + (37.3886, -5.9823), # Sevilla +] +pairs = [(0, 0), (0, 1)] # Madrid->Barcelona, Madrid->Sevilla + +results = valhalla_matrix_1_to_n(origins, destinations, pairs) +for (oi, di), r in zip(pairs, results): + print(f"pair ({oi},{di}): {r['meters']/1000:.1f} km, {r['seconds']/3600:.2f} h") +``` + +## Notas + +Adaptada de `add_valhalla_meters_seconds_1_to_n` en `add_distancias_valhalla.py`. El original acoplaba pandas DataFrames, columnas por nombre, resume/checkpoint y verbose logging — esta versión es pandas-free con firma genérica basada en índices. Usa urllib.request (stdlib) para evitar dependencia de httpx en threads (httpx.Client no es thread-safe sin precauciones). Valhalla retorna distancia en km — se convierte a metros multiplicando por 1000. diff --git a/python/functions/geo/valhalla_matrix_1_to_n.py b/python/functions/geo/valhalla_matrix_1_to_n.py new file mode 100644 index 00000000..31fc3702 --- /dev/null +++ b/python/functions/geo/valhalla_matrix_1_to_n.py @@ -0,0 +1,110 @@ +"""Matriz de distancias y tiempos 1-a-N via Valhalla sources_to_targets.""" + +from __future__ import annotations + +import math +from collections import defaultdict +from concurrent.futures import ThreadPoolExecutor, as_completed +from threading import Lock +from typing import Any +from urllib import request as urllib_request +import json + + +def valhalla_matrix_1_to_n( + origins: list[tuple[float, float]], + destinations: list[tuple[float, float]], + pairs: list[tuple[int, int]], + base_url: str = "http://localhost:8002", + costing: str = "auto", + max_targets_per_request: int = 400, + max_distance_m: float = 30_000.0, + timeout_s: float = 120.0, + concurrency: int = 12, + search_radius_m: float = 0.0, +) -> list[dict]: + """Calcula distancias y tiempos para pares origen-destino usando Valhalla. + + Args: + origins: Lista de (lat, lon) de los posibles orígenes. + destinations: Lista de (lat, lon) de los posibles destinos. + pairs: Lista de (origin_idx, dest_idx) con los índices a calcular. + base_url: URL base del servidor Valhalla. + costing: Modelo de coste ('auto', 'bicycle', 'pedestrian', etc.). + max_targets_per_request: Máximo de destinos por request a Valhalla. + max_distance_m: Distancia de fallback en metros cuando Valhalla falla. + timeout_s: Timeout en segundos por request. + concurrency: Número de threads paralelos. + search_radius_m: Radio de búsqueda de nodo en metros (0 = default). + + Returns: + Lista paralela a 'pairs' con dicts {"meters": float, "seconds": float, "error": int}. + error=1 si Valhalla falló para ese par; meters=max_distance_m, seconds=NaN en error. + """ + url = f"{base_url.rstrip('/')}/sources_to_targets" + n_pairs = len(pairs) + meters: list[float] = [max_distance_m] * n_pairs + seconds: list[float] = [math.nan] * n_pairs + errors: list[int] = [1] * n_pairs + + def _loc(lat: float, lon: float) -> dict[str, Any]: + loc: dict[str, Any] = {"lat": float(lat), "lon": float(lon)} + if search_radius_m and search_radius_m > 0: + loc["search_radius"] = float(search_radius_m) + return loc + + def _post_json(payload: dict) -> dict: + data = json.dumps(payload).encode() + req = urllib_request.Request( + url, + data=data, + headers={"Content-Type": "application/json"}, + method="POST", + ) + with urllib_request.urlopen(req, timeout=timeout_s) as resp: + return json.loads(resp.read().decode("utf-8")) + + # Group pair indices by origin index + groups: dict[int, list[int]] = defaultdict(list) + for pair_pos, (oi, _di) in enumerate(pairs): + groups[oi].append(pair_pos) + + # Build tasks: (origin_idx, [pair_positions_chunk]) + tasks: list[tuple[int, list[int]]] = [] + for oi, pair_positions in groups.items(): + for i in range(0, len(pair_positions), max_targets_per_request): + tasks.append((oi, pair_positions[i : i + max_targets_per_request])) + + lock = Lock() + + def _run_chunk(oi: int, chunk_positions: list[int]) -> None: + source = [_loc(*origins[oi])] + targets = [_loc(*destinations[pairs[pos][1]]) for pos in chunk_positions] + payload = {"sources": source, "targets": targets, "costing": costing} + try: + data = _post_json(payload) + matrix = data["sources_to_targets"][0] + with lock: + for j, pos in enumerate(chunk_positions): + cell = matrix[j] + d = cell.get("distance") + t = cell.get("time") + meters[pos] = d * 1000 if d is not None else max_distance_m + seconds[pos] = float(t) if t is not None else math.nan + errors[pos] = 0 + except Exception: + with lock: + for pos in chunk_positions: + meters[pos] = max_distance_m + seconds[pos] = math.nan + errors[pos] = 1 + + with ThreadPoolExecutor(max_workers=concurrency) as ex: + futures = [ex.submit(_run_chunk, oi, chunk) for oi, chunk in tasks] + for fut in as_completed(futures): + fut.result() # propagate thread exceptions silently — already handled inside + + return [ + {"meters": meters[i], "seconds": seconds[i], "error": errors[i]} + for i in range(n_pairs) + ] diff --git a/python/functions/geo/valhalla_route.md b/python/functions/geo/valhalla_route.md new file mode 100644 index 00000000..25ac7270 --- /dev/null +++ b/python/functions/geo/valhalla_route.md @@ -0,0 +1,54 @@ +--- +name: valhalla_route +kind: function +lang: py +domain: geo +version: "1.0.0" +purity: impure +signature: "def valhalla_route(locations: list[dict], base_url: str = 'http://localhost:8002', costing: str = 'auto', units: str = 'metric', timeout_s: float = 60.0) -> dict | None" +description: "Calcula una ruta punto a punto usando el motor de enrutamiento Valhalla. POST a /route, retorna la respuesta JSON con trip.summary (length, time) o None si error." +tags: [valhalla, routing, geo, http, isochrone] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: true +error_type: "error_go_core" +imports: [httpx] +params: + - name: locations + desc: "Lista de dicts con 'lat' y 'lon'. Minimo 2 puntos (origen y destino)." + - name: base_url + desc: "URL base del servidor Valhalla. Por defecto http://localhost:8002." + - name: costing + desc: "Modelo de coste de enrutamiento: 'auto', 'bicycle', 'pedestrian', etc." + - name: units + desc: "Unidades de distancia: 'metric' (km) o 'imperial' (millas)." + - name: timeout_s + desc: "Timeout en segundos para la request HTTP. Por defecto 60." +output: "Dict con la respuesta JSON de Valhalla (campo 'trip' con summary.length en km y summary.time en segundos), o None si el servidor no responde o retorna error." +tested: true +tests: ["ruta Madrid-Barcelona supera 500 km"] +test_file_path: "python/functions/geo/tests/test_valhalla_route.py" +file_path: "python/functions/geo/valhalla_route.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "better_maps/scripts/test_valhalla_route.py" +--- + +## Ejemplo + +```python +result = valhalla_route( + locations=[ + {"lat": 40.4168, "lon": -3.7038}, # Madrid + {"lat": 41.3874, "lon": 2.1686}, # Barcelona + ] +) +if result: + summary = result["trip"]["summary"] + print(f"{summary['length']} km, {summary['time'] / 3600:.2f} h") +``` + +## Notas + +Retorna None ante cualquier excepcion (timeout, HTTP error, JSON invalido). Si necesitas el error concreto, usa httpx directamente. Requiere Valhalla activo en base_url. diff --git a/python/functions/geo/valhalla_route.py b/python/functions/geo/valhalla_route.py new file mode 100644 index 00000000..895941a0 --- /dev/null +++ b/python/functions/geo/valhalla_route.py @@ -0,0 +1,38 @@ +"""Ruta punto a punto via Valhalla routing engine.""" + +from __future__ import annotations + +import httpx + + +def valhalla_route( + locations: list[dict], + base_url: str = "http://localhost:8002", + costing: str = "auto", + units: str = "metric", + timeout_s: float = 60.0, +) -> dict | None: + """Calcula una ruta entre una lista de ubicaciones usando Valhalla. + + Args: + locations: Lista de dicts con 'lat' y 'lon' (al menos 2 puntos). + base_url: URL base del servidor Valhalla. + costing: Modelo de coste ('auto', 'bicycle', 'pedestrian', etc.). + units: Unidades de distancia ('metric' o 'imperial'). + timeout_s: Timeout en segundos para la request HTTP. + + Returns: + Respuesta JSON parseada con 'trip' o None si error. + """ + url = f"{base_url.rstrip('/')}/route" + payload = { + "locations": locations, + "costing": costing, + "units": units, + } + try: + r = httpx.post(url, json=payload, timeout=httpx.Timeout(timeout_s)) + r.raise_for_status() + return r.json() + except Exception: + return None diff --git a/python/functions/infra/add_header_logo.md b/python/functions/infra/add_header_logo.md new file mode 100644 index 00000000..73dadde1 --- /dev/null +++ b/python/functions/infra/add_header_logo.md @@ -0,0 +1,58 @@ +--- +name: add_header_logo +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "add_header_logo(fig: Figure, image: np.ndarray, x: float = 0.88, y: float = 0.905, width: float = 0.08, height: float = 0.08) -> None" +description: "Añade un logo como axes inset en la esquina superior derecha de una figura matplotlib. Usa fig.add_axes + imshow + axis off. Útil para branding en páginas de informe PDF." +tags: [pdf, matplotlib, logo, report, infra] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [matplotlib] +params: + - name: fig + desc: "Figura matplotlib donde se inserta el logo." + - name: image + desc: "Array numpy H×W×C con los datos de imagen del logo (e.g. de imread o PIL)." + - name: x + desc: "Borde izquierdo del axes en coordenadas de figura (0-1). Default 0.88." + - name: y + desc: "Borde inferior del axes en coordenadas de figura (0-1). Default 0.905." + - name: width + desc: "Ancho del axes en coordenadas de figura (0-1). Default 0.08." + - name: height + desc: "Alto del axes en coordenadas de figura (0-1). Default 0.08." +output: "None. Modifica la figura in-place añadiendo un axes con el logo." +tested: true +tests: + - "figura nueva con imagen zeros no lanza excepcion" + - "axes de logo tiene axis off" +test_file_path: "python/functions/infra/tests/test_add_header_logo.py" +file_path: "python/functions/infra/add_header_logo.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/recomendador_centros.py" +--- + +## Ejemplo + +```python +import matplotlib +matplotlib.use("Agg") +import matplotlib.pyplot as plt +import numpy as np + +logo = np.zeros((50, 200, 3), dtype=np.uint8) # o matplotlib.image.imread("logo.png") +fig, ax = plt.subplots(figsize=(11.69, 8.27)) +add_header_logo(fig, logo) +``` + +## Notas + +La posición por defecto (x=0.88, y=0.905) coloca el logo en la esquina superior derecha +para figuras A4 landscape. Ajustar x, y, width, height para otros tamaños. diff --git a/python/functions/infra/add_header_logo.py b/python/functions/infra/add_header_logo.py new file mode 100644 index 00000000..c0e6d0cb --- /dev/null +++ b/python/functions/infra/add_header_logo.py @@ -0,0 +1,35 @@ +"""Add a logo image as an inset axes header to a matplotlib figure.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + import numpy as np + from matplotlib.figure import Figure + + +def add_header_logo( + fig: "Figure", + image: "np.ndarray", + x: float = 0.88, + y: float = 0.905, + width: float = 0.08, + height: float = 0.08, +) -> None: + """Add a logo image as an inset axes in the upper-right area of a figure. + + Creates a new axes at the given figure coordinates and renders the image + with axis lines and ticks hidden. Suitable for branding in report pages. + + Args: + fig: The matplotlib Figure to add the logo to. + image: Image array (H x W x C) as numpy ndarray, e.g. loaded with + matplotlib.image.imread or PIL. + x: Left edge of the logo axes in figure coordinates (0-1). + y: Bottom edge of the logo axes in figure coordinates (0-1). + width: Width of the logo axes in figure coordinates (0-1). + height: Height of the logo axes in figure coordinates (0-1). + """ + ax_logo = fig.add_axes([x, y, width, height]) + ax_logo.imshow(image) + ax_logo.axis("off") diff --git a/python/functions/infra/compress_pdf_ghostscript.md b/python/functions/infra/compress_pdf_ghostscript.md new file mode 100644 index 00000000..4d309312 --- /dev/null +++ b/python/functions/infra/compress_pdf_ghostscript.md @@ -0,0 +1,46 @@ +--- +name: compress_pdf_ghostscript +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "compress_pdf_ghostscript(pdf_path: str | Path, quality: str = 'screen') -> bool" +description: "Comprime un PDF en disco usando Ghostscript con downsampling 96/200 dpi. Reemplaza el archivo solo si el comprimido es menor. Retorna True si comprimió, False si gs no disponible o no hubo mejora." +tags: [pdf, ghostscript, compression, infra] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [shutil, subprocess, tempfile, pathlib] +params: + - name: pdf_path + desc: "Ruta al archivo PDF a comprimir. Se modifica en sitio si la compresión mejora el tamaño." + - name: quality + desc: "Perfil PDFSETTINGS de Ghostscript: screen (96 dpi), ebook, printer, prepress." +output: "True si el archivo fue reemplazado por la versión comprimida, False si gs no está disponible, el archivo no existe, falló o el resultado no era menor." +tested: true +tests: + - "crea pdf temporal y comprime - retorna bool sin excepcion" + - "retorna False cuando gs no esta disponible" +test_file_path: "python/functions/infra/tests/test_compress_pdf_ghostscript.py" +file_path: "python/functions/infra/compress_pdf_ghostscript.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/recomendador_centros.py" +--- + +## Ejemplo + +```python +compressed = compress_pdf_ghostscript("report.pdf", quality="ebook") +if compressed: + print("PDF comprimido correctamente") +``` + +## Notas + +Requiere `gs` (Ghostscript) en el PATH. Si no está disponible retorna False sin lanzar excepción. +El perfil `screen` produce la mayor compresión (96 dpi), útil para distribución web. +El reemplazo es atómico: el original no se toca si la compresión falla o no mejora el tamaño. diff --git a/python/functions/infra/compress_pdf_ghostscript.py b/python/functions/infra/compress_pdf_ghostscript.py new file mode 100644 index 00000000..4b9f90d3 --- /dev/null +++ b/python/functions/infra/compress_pdf_ghostscript.py @@ -0,0 +1,63 @@ +"""Compress a PDF file in-place using Ghostscript.""" +from __future__ import annotations + +import shutil +import subprocess +import tempfile +from pathlib import Path + + +def compress_pdf_ghostscript( + pdf_path: "str | Path", + quality: str = "screen", +) -> bool: + """Compress a PDF in-place using Ghostscript. + + Runs gs with downsampling (96 dpi color/gray, 200 dpi mono). Replaces the + original file only when the compressed output is strictly smaller. Returns + True if the file was replaced, False if gs is not available, the file does + not exist, compression failed, or the output was not smaller. + + Args: + pdf_path: Path to the PDF file to compress (modified in-place on success). + quality: Ghostscript PDFSETTINGS profile. One of "screen", "ebook", + "printer", "prepress". + + Returns: + True if the file was compressed and replaced, False otherwise. + """ + path = Path(pdf_path) + gs = shutil.which("gs") + if not gs or not path.exists(): + return False + + with tempfile.TemporaryDirectory() as tmpdir: + compressed = Path(tmpdir) / "compressed.pdf" + cmd = [ + gs, + "-sDEVICE=pdfwrite", + "-dCompatibilityLevel=1.4", + f"-dPDFSETTINGS=/{quality}", + "-dDownsampleColorImages=true", + "-dDownsampleGrayImages=true", + "-dDownsampleMonoImages=true", + "-dColorImageResolution=96", + "-dGrayImageResolution=96", + "-dMonoImageResolution=200", + "-dNOPAUSE", + "-dQUIET", + "-dBATCH", + f"-sOutputFile={compressed}", + str(path), + ] + try: + subprocess.run(cmd, check=True, capture_output=True) + except subprocess.CalledProcessError: + return False + + if compressed.exists() and compressed.stat().st_size < path.stat().st_size: + import shutil as _sh + _sh.copy2(str(compressed), str(path)) + return True + + return False diff --git a/python/functions/infra/osm2pgsql_ingest.md b/python/functions/infra/osm2pgsql_ingest.md new file mode 100644 index 00000000..0a67f98e --- /dev/null +++ b/python/functions/infra/osm2pgsql_ingest.md @@ -0,0 +1,68 @@ +--- +name: osm2pgsql_ingest +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "osm2pgsql_ingest(osm_pbf_path: str | Path, host: str = 'localhost', port: int = 5432, dbname: str = 'gis', user: str = 'geoserver', password: str = 'geoserver', style: str | None = None, ensure_hstore: bool = True) -> dict" +description: "Ingesta un archivo .osm.pbf en PostGIS usando osm2pgsql con --create --slim --hstore --multi-geometry. Verifica osm2pgsql en PATH, opcionalmente crea extensión hstore. Retorna dict {ok, rows_loaded, stderr}." +tags: [osm, postgis, gis, osm2pgsql, infra] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [os, shutil, subprocess, pathlib] +params: + - name: osm_pbf_path + desc: "Ruta al archivo .osm.pbf a ingestar." + - name: host + desc: "Host de PostGIS (default: localhost)." + - name: port + desc: "Puerto de PostGIS (default: 5432)." + - name: dbname + desc: "Nombre de la base de datos PostGIS (default: gis)." + - name: user + desc: "Usuario de la base de datos (default: geoserver)." + - name: password + desc: "Contraseña de la base de datos (default: geoserver)." + - name: style + desc: "Ruta opcional a archivo .style de osm2pgsql. Si None usa el estilo por defecto." + - name: ensure_hstore + desc: "Si True, ejecuta psql para crear la extensión hstore antes de la ingesta." +output: "dict con ok (bool), rows_loaded (int|None, siempre None porque osm2pgsql no reporta conteos), stderr (str con salida combinada stdout+stderr)." +tested: true +tests: + - "lanza FileNotFoundError con path inexistente" + - "lanza RuntimeError si osm2pgsql no esta en PATH" +test_file_path: "python/functions/infra/tests/test_osm2pgsql_ingest.py" +file_path: "python/functions/infra/osm2pgsql_ingest.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "better_maps/ingest_osm.py" +--- + +## Ejemplo + +```python +result = osm2pgsql_ingest( + "data/spain-latest.osm.pbf", + host="localhost", + dbname="gis", + user="geoserver", + password="secret", +) +if result["ok"]: + print("Ingesta completada") +else: + print(result["stderr"]) +``` + +## Notas + +Requiere `osm2pgsql` en el PATH. Lanza RuntimeError si no está disponible. +El campo `rows_loaded` siempre es None: osm2pgsql no reporta conteos de filas +en su salida estándar. Para obtener conteos, consultar directamente las tablas +planet_osm_* en PostGIS. +La contraseña se pasa via PGPASSWORD en el entorno del subproceso. diff --git a/python/functions/infra/osm2pgsql_ingest.py b/python/functions/infra/osm2pgsql_ingest.py new file mode 100644 index 00000000..90a3f13f --- /dev/null +++ b/python/functions/infra/osm2pgsql_ingest.py @@ -0,0 +1,97 @@ +"""Ingest an OSM PBF file into a PostGIS database using osm2pgsql.""" +from __future__ import annotations + +import os +import shutil +import subprocess +from pathlib import Path + + +def osm2pgsql_ingest( + osm_pbf_path: "str | Path", + host: str = "localhost", + port: int = 5432, + dbname: str = "gis", + user: str = "geoserver", + password: str = "geoserver", + style: "str | None" = None, + ensure_hstore: bool = True, +) -> dict: + """Ingest an OSM PBF file into PostGIS using osm2pgsql. + + Verifies osm2pgsql is in PATH (raises RuntimeError if not). If + ensure_hstore=True, runs psql to CREATE EXTENSION IF NOT EXISTS hstore. + Then runs osm2pgsql with --create --slim --hstore --multi-geometry. + + Args: + osm_pbf_path: Path to the .osm.pbf file to ingest. + host: PostGIS host (default: localhost). + port: PostGIS port (default: 5432). + dbname: Database name (default: gis). + user: Database user (default: geoserver). + password: Database password (default: geoserver). + style: Optional path to a .style file for osm2pgsql. If None, uses + osm2pgsql's built-in default style. + ensure_hstore: If True, create the hstore extension before ingesting. + + Returns: + dict with keys: + - ok (bool): True if ingestion succeeded. + - rows_loaded (int | None): Not directly available from osm2pgsql stdout; + always None (osm2pgsql does not report row counts). + - stderr (str): Combined stdout+stderr output from osm2pgsql. + + Raises: + RuntimeError: If osm2pgsql is not found in PATH. + FileNotFoundError: If osm_pbf_path does not exist. + """ + pbf = Path(osm_pbf_path) + if not pbf.exists(): + raise FileNotFoundError(f"OSM PBF file not found: {pbf}") + + if shutil.which("osm2pgsql") is None: + raise RuntimeError( + "osm2pgsql not found in PATH. Install it before calling this function." + ) + + env = os.environ.copy() + env["PGPASSWORD"] = password + + if ensure_hstore and shutil.which("psql") is not None: + psql_cmd = [ + "psql", + f"--host={host}", + f"--port={port}", + f"--dbname={dbname}", + f"--username={user}", + "--command", + "CREATE EXTENSION IF NOT EXISTS hstore;", + ] + subprocess.run(psql_cmd, env=env, capture_output=True, text=True) + + cmd = [ + "osm2pgsql", + f"--host={host}", + f"--port={port}", + f"--database={dbname}", + f"--user={user}", + "--create", + "--slim", + "--hstore", + "--multi-geometry", + ] + if style: + cmd += ["--style", str(style)] + cmd.append(str(pbf)) + + result = subprocess.run( + cmd, + env=env, + capture_output=True, + text=True, + ) + + combined = result.stdout + result.stderr + if result.returncode == 0: + return {"ok": True, "rows_loaded": None, "stderr": combined} + return {"ok": False, "rows_loaded": None, "stderr": combined} diff --git a/python/functions/infra/render_table_page_pdfpages.md b/python/functions/infra/render_table_page_pdfpages.md new file mode 100644 index 00000000..496d1090 --- /dev/null +++ b/python/functions/infra/render_table_page_pdfpages.md @@ -0,0 +1,61 @@ +--- +name: render_table_page_pdfpages +kind: function +lang: py +domain: infra +version: "1.0.0" +purity: impure +signature: "render_table_page_pdfpages(pdf: PdfPages, title: str, rows: list[list[str]], col_labels: list[str], max_rows: int = 28, figsize: tuple[float, float] = (11.69, 8.27), fontsize: int = 8, dpi: int = 300) -> None" +description: "Renderiza filas como páginas de tabla paginadas en un PdfPages abierto. Usa matplotlib.pyplot.table con paginación automática por max_rows. Una página A4 landscape por chunk." +tags: [pdf, matplotlib, table, report, infra] +uses_functions: [] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [matplotlib] +params: + - name: pdf + desc: "Objeto PdfPages abierto de matplotlib donde se escriben las páginas." + - name: title + desc: "Título mostrado encima de la tabla en cada página." + - name: rows + desc: "Lista de filas; cada fila es una lista de strings con los valores de celda." + - name: col_labels + desc: "Etiquetas de las columnas (cabecera de tabla)." + - name: max_rows + desc: "Número máximo de filas por página antes de crear una nueva (default 28)." + - name: figsize + desc: "Tamaño de figura en pulgadas. Default A4 landscape (11.69x8.27)." + - name: fontsize + desc: "Tamaño de fuente para las celdas de la tabla." + - name: dpi + desc: "Resolución al guardar cada página (default 300)." +output: "None. Escribe páginas directamente en el PdfPages proporcionado." +tested: true +tests: + - "50 filas con max_rows=28 genera 2 paginas en pdf no vacio" + - "0 filas genera 1 pagina vacia sin excepcion" +test_file_path: "python/functions/infra/tests/test_render_table_page_pdfpages.py" +file_path: "python/functions/infra/render_table_page_pdfpages.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "ponderacion_isochronas/src/recomendador_centros.py" +--- + +## Ejemplo + +```python +import matplotlib +matplotlib.use("Agg") +from matplotlib.backends.backend_pdf import PdfPages + +rows = [[str(i), f"valor_{i}"] for i in range(50)] +with PdfPages("tabla.pdf") as pdf: + render_table_page_pdfpages(pdf, "Informe de centros", rows, ["ID", "Valor"]) +``` + +## Notas + +Requiere `matplotlib`. Backend Agg recomendado en entornos sin pantalla. +Cada chunk de filas genera exactamente una página. Con rows vacío genera una página vacía. diff --git a/python/functions/infra/render_table_page_pdfpages.py b/python/functions/infra/render_table_page_pdfpages.py new file mode 100644 index 00000000..00b59ab5 --- /dev/null +++ b/python/functions/infra/render_table_page_pdfpages.py @@ -0,0 +1,57 @@ +"""Render paginated table pages into a matplotlib PdfPages object.""" +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from matplotlib.backends.backend_pdf import PdfPages + + +def render_table_page_pdfpages( + pdf: "PdfPages", + title: str, + rows: list[list[str]], + col_labels: list[str], + max_rows: int = 28, + figsize: tuple[float, float] = (11.69, 8.27), + fontsize: int = 8, + dpi: int = 300, +) -> None: + """Render rows as paginated table pages into an open PdfPages object. + + Partitions rows into chunks of max_rows and writes one A4-landscape page + per chunk using matplotlib's table widget. Each page carries the given title. + + Args: + pdf: An open matplotlib PdfPages context. + title: Page title shown above the table. + rows: List of rows, each row is a list of string cell values. + col_labels: Column header labels. + max_rows: Maximum rows per page before starting a new page. + figsize: Figure size in inches (default A4 landscape 11.69x8.27). + fontsize: Font size for table cells. + dpi: Resolution used when saving each page. + """ + import matplotlib + matplotlib.use("Agg") + import matplotlib.pyplot as plt + + # Always render at least one page; use a placeholder row when rows is empty + chunks: list[list[list[str]]] = [] + if not rows: + chunks = [[]] + else: + for start in range(0, len(rows), max_rows): + chunks.append(rows[start: start + max_rows]) + + for chunk in chunks: + fig, ax = plt.subplots(figsize=figsize) + ax.axis("off") + if chunk: + table = ax.table(cellText=chunk, colLabels=col_labels, loc="center") + table.auto_set_font_size(False) + table.set_fontsize(fontsize) + table.scale(1, 1.3) + ax.set_title(title, fontsize=14, pad=12) + pdf.savefig(fig, dpi=dpi) + plt.close(fig) diff --git a/python/functions/infra/tests/__init__.py b/python/functions/infra/tests/__init__.py new file mode 100644 index 00000000..e69de29b diff --git a/python/functions/infra/tests/test_add_header_logo.py b/python/functions/infra/tests/test_add_header_logo.py new file mode 100644 index 00000000..f5f2c822 --- /dev/null +++ b/python/functions/infra/tests/test_add_header_logo.py @@ -0,0 +1,45 @@ +"""Tests para add_header_logo.""" +from __future__ import annotations + +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") +import numpy as np +import pytest + + +def test_figura_nueva_con_imagen_zeros_no_lanza_excepcion(): + """figura nueva con imagen zeros no lanza excepcion""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from infra.add_header_logo import add_header_logo + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(11.69, 8.27)) + image = np.zeros((50, 200, 3), dtype=np.uint8) + + # Should not raise + add_header_logo(fig, image) + plt.close(fig) + + +def test_axes_de_logo_tiene_axis_off(): + """axes de logo tiene axis off""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from infra.add_header_logo import add_header_logo + import matplotlib.pyplot as plt + + fig, ax = plt.subplots(figsize=(11.69, 8.27)) + initial_axes_count = len(fig.axes) + image = np.zeros((10, 10, 3), dtype=np.uint8) + + add_header_logo(fig, image, x=0.88, y=0.905, width=0.08, height=0.08) + + # A new axes should have been added + assert len(fig.axes) == initial_axes_count + 1 + logo_ax = fig.axes[-1] + # axis("off") disables both x and y axis visibility + assert not logo_ax.axison + plt.close(fig) diff --git a/python/functions/infra/tests/test_compress_pdf_ghostscript.py b/python/functions/infra/tests/test_compress_pdf_ghostscript.py new file mode 100644 index 00000000..f1400238 --- /dev/null +++ b/python/functions/infra/tests/test_compress_pdf_ghostscript.py @@ -0,0 +1,62 @@ +"""Tests para compress_pdf_ghostscript.""" +from __future__ import annotations + +import shutil +import tempfile +from pathlib import Path + +import pytest + + +def _make_simple_pdf(path: Path) -> None: + """Create a minimal valid PDF using fpdf2.""" + try: + from fpdf import FPDF + pdf = FPDF() + pdf.add_page() + pdf.set_font("Helvetica", size=12) + pdf.cell(200, 10, text="Test PDF for ghostscript compression", ln=True) + pdf.output(str(path)) + except ImportError: + # Fallback: write a minimal PDF manually + content = ( + b"%PDF-1.4\n" + b"1 0 obj\n<< /Type /Catalog /Pages 2 0 R >>\nendobj\n" + b"2 0 obj\n<< /Type /Pages /Kids [3 0 R] /Count 1 >>\nendobj\n" + b"3 0 obj\n<< /Type /Page /Parent 2 0 R /MediaBox [0 0 612 792] >>\nendobj\n" + b"xref\n0 4\n0000000000 65535 f \n0000000009 00000 n \n" + b"0000000068 00000 n \n0000000125 00000 n \n" + b"trailer\n<< /Size 4 /Root 1 0 R >>\nstartxref\n210\n%%EOF\n" + ) + path.write_bytes(content) + + +def test_crea_pdf_temporal_y_comprime_retorna_bool_sin_excepcion(): + """crea pdf temporal y comprime - retorna bool sin excepcion""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from infra.compress_pdf_ghostscript import compress_pdf_ghostscript + + with tempfile.TemporaryDirectory() as tmpdir: + pdf_path = Path(tmpdir) / "test.pdf" + _make_simple_pdf(pdf_path) + assert pdf_path.exists() + result = compress_pdf_ghostscript(pdf_path) + assert isinstance(result, bool) + # File must still exist regardless of whether compression happened + assert pdf_path.exists() + + +def test_retorna_False_cuando_gs_no_esta_disponible(monkeypatch): + """retorna False cuando gs no esta disponible""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from infra.compress_pdf_ghostscript import compress_pdf_ghostscript + + monkeypatch.setattr("shutil.which", lambda x: None) + + with tempfile.TemporaryDirectory() as tmpdir: + pdf_path = Path(tmpdir) / "test.pdf" + _make_simple_pdf(pdf_path) + result = compress_pdf_ghostscript(pdf_path) + assert result is False diff --git a/python/functions/infra/tests/test_osm2pgsql_ingest.py b/python/functions/infra/tests/test_osm2pgsql_ingest.py new file mode 100644 index 00000000..cf4902cb --- /dev/null +++ b/python/functions/infra/tests/test_osm2pgsql_ingest.py @@ -0,0 +1,38 @@ +"""Tests para osm2pgsql_ingest.""" +from __future__ import annotations + +import shutil +import tempfile +from pathlib import Path +from unittest.mock import patch + +import pytest + + +def test_lanza_FileNotFoundError_con_path_inexistente(): + """lanza FileNotFoundError con path inexistente""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from infra.osm2pgsql_ingest import osm2pgsql_ingest + + with pytest.raises(FileNotFoundError): + osm2pgsql_ingest("/tmp/non_existent_file_that_does_not_exist.osm.pbf") + + +def test_lanza_RuntimeError_si_osm2pgsql_no_esta_en_PATH(): + """lanza RuntimeError si osm2pgsql no esta en PATH""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from infra.osm2pgsql_ingest import osm2pgsql_ingest + + with tempfile.TemporaryDirectory() as tmpdir: + pbf_path = Path(tmpdir) / "fake.osm.pbf" + # Create a dummy file so FileNotFoundError is not raised first + pbf_path.write_bytes(b"PBF") + + # Skip test if osm2pgsql is actually in PATH (CI environment may have it) + if shutil.which("osm2pgsql") is not None: + pytest.skip("osm2pgsql is available in PATH; skipping RuntimeError test") + + with pytest.raises(RuntimeError, match="osm2pgsql"): + osm2pgsql_ingest(pbf_path) diff --git a/python/functions/infra/tests/test_render_table_page_pdfpages.py b/python/functions/infra/tests/test_render_table_page_pdfpages.py new file mode 100644 index 00000000..fd6ab506 --- /dev/null +++ b/python/functions/infra/tests/test_render_table_page_pdfpages.py @@ -0,0 +1,53 @@ +"""Tests para render_table_page_pdfpages.""" +from __future__ import annotations + +import tempfile +from pathlib import Path + +import matplotlib +matplotlib.use("Agg") +import pytest + + +def test_50_filas_con_max_rows_28_genera_2_paginas_en_pdf_no_vacio(): + """50 filas con max_rows=28 genera 2 paginas en pdf no vacio""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from infra.render_table_page_pdfpages import render_table_page_pdfpages + from matplotlib.backends.backend_pdf import PdfPages + + rows = [[str(i), f"valor_{i}", f"extra_{i}"] for i in range(50)] + col_labels = ["ID", "Valor", "Extra"] + + with tempfile.TemporaryDirectory() as tmpdir: + pdf_path = Path(tmpdir) / "test_table.pdf" + with PdfPages(str(pdf_path)) as pdf: + render_table_page_pdfpages(pdf, "Test Tabla", rows, col_labels, max_rows=28) + + assert pdf_path.exists() + assert pdf_path.stat().st_size > 0 + + # Verify 2 pages were generated by reading PDF metadata + try: + from pypdf import PdfReader + reader = PdfReader(str(pdf_path)) + assert len(reader.pages) == 2 + except ImportError: + # If pypdf not available, just check file size + assert pdf_path.stat().st_size > 1000 + + +def test_0_filas_genera_1_pagina_vacia_sin_excepcion(): + """0 filas genera 1 pagina vacia sin excepcion""" + import sys + sys.path.insert(0, str(Path(__file__).resolve().parents[2])) + from infra.render_table_page_pdfpages import render_table_page_pdfpages + from matplotlib.backends.backend_pdf import PdfPages + + with tempfile.TemporaryDirectory() as tmpdir: + pdf_path = Path(tmpdir) / "empty_table.pdf" + with PdfPages(str(pdf_path)) as pdf: + render_table_page_pdfpages(pdf, "Vacío", [], ["Col1", "Col2"]) + + assert pdf_path.exists() + assert pdf_path.stat().st_size > 0 diff --git a/python/functions/pipelines/compute_centers_reachability_pipeline.md b/python/functions/pipelines/compute_centers_reachability_pipeline.md new file mode 100644 index 00000000..d83cffa5 --- /dev/null +++ b/python/functions/pipelines/compute_centers_reachability_pipeline.md @@ -0,0 +1,66 @@ +--- +id: compute_centers_reachability_pipeline_py_pipelines +name: compute_centers_reachability_pipeline +kind: pipeline +lang: py +domain: pipelines +version: "1.0.0" +purity: impure +signature: "async def compute_centers_reachability_pipeline(origins, centers, isochrone_minutes, base_url, concurrency) -> dict" +description: "Calcula la accesibilidad de centros de servicio: matriz tiempo/distancia clientes→centros e isócronas por centro usando Valhalla." +tags: [pipeline, geo, footprint, valhalla, isochrone, matrix, reachability] +uses_functions: ["valhalla_matrix_1_to_n_py_geo", "valhalla_isochrones_async_py_geo"] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +example: | + import asyncio + result = asyncio.run(compute_centers_reachability_pipeline( + origins=[(40.4168, -3.7038), (37.3891, -5.9845)], + centers=[(41.3851, 2.1734), (43.2627, -2.9253)], + isochrone_minutes=15, + )) + # result["matrix"] tiene 4 entries (2 orígenes × 2 centros) + # result["isochrones"] tiene 2 GeoJSON dicts +tested: true +tests: ["test_compute_centers_reachability_pipeline"] +test_file_path: "python/functions/pipelines/tests/test_compute_centers_reachability_pipeline.py" +file_path: "python/functions/pipelines/compute_centers_reachability_pipeline.py" +params: + - {name: origins, desc: "Lista de (lat, lon) de los clientes u orígenes. Coordenadas WGS84."} + - {name: centers, desc: "Lista de (lat, lon) de los centros de servicio. Coordenadas WGS84."} + - {name: isochrone_minutes, desc: "Minutos de tiempo de viaje para las isócronas de cada centro. Default 15."} + - {name: base_url, desc: "URL base del servidor Valhalla. Default http://localhost:8002."} + - {name: concurrency, desc: "Número máximo de requests async simultáneos para isócronas. Default 6."} +output: "Dict con 'matrix' (list[dict] con i,j,meters,seconds,error por par) e 'isochrones' (list[dict|None] con GeoJSON por centro)." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "internal:composed" +--- + +## Ejemplo + +```python +import asyncio + +result = asyncio.run(compute_centers_reachability_pipeline( + origins=[(40.4168, -3.7038), (37.3891, -5.9845)], # Madrid, Sevilla + centers=[(41.3851, 2.1734), (43.2627, -2.9253)], # Barcelona, Bilbao + isochrone_minutes=15, + base_url="http://localhost:8002", +)) + +for entry in result["matrix"]: + print(f"origen[{entry['i']}] → centro[{entry['j']}]: {entry['meters']:.0f}m / {entry['seconds']:.0f}s") + +print(f"Isócronas calculadas: {sum(1 for iso in result['isochrones'] if iso is not None)}") +``` + +## Notas + +Función async — requiere `asyncio.run(...)` o `await` dentro de un contexto async. +La matriz se calcula con threads (valhalla_matrix_1_to_n usa ThreadPoolExecutor). +Las isócronas se calculan con httpx async (valhalla_isochrones_async). +Orden de la matrix: [(0,0),(0,1),...,(0,N-1),(1,0),...,(M-1,N-1)] donde M=orígenes, N=centros. diff --git a/python/functions/pipelines/compute_centers_reachability_pipeline.py b/python/functions/pipelines/compute_centers_reachability_pipeline.py new file mode 100644 index 00000000..9a4b5943 --- /dev/null +++ b/python/functions/pipelines/compute_centers_reachability_pipeline.py @@ -0,0 +1,82 @@ +"""Pipeline: calcula matriz de tiempo/distancia y isócronas para centros de servicio.""" + +from __future__ import annotations + +import asyncio +import sys +import os + +_FUNCTIONS_DIR = os.path.join(os.path.dirname(__file__), "..") +if _FUNCTIONS_DIR not in sys.path: + sys.path.insert(0, _FUNCTIONS_DIR) + +from geo.valhalla_matrix_1_to_n import valhalla_matrix_1_to_n +from geo.valhalla_isochrones_async import valhalla_isochrones_async + + +async def compute_centers_reachability_pipeline( + origins: list[tuple[float, float]], + centers: list[tuple[float, float]], + isochrone_minutes: int = 15, + base_url: str = "http://localhost:8002", + concurrency: int = 6, +) -> dict: + """Calcula la accesibilidad de centros de servicio desde orígenes clientes. + + Compone valhalla_matrix_1_to_n para obtener tiempos/distancias de todos + los pares (origin, center) y valhalla_isochrones_async para generar una + isócrona por cada centro. + + Args: + origins: Lista de (lat, lon) de los clientes o puntos de origen. + centers: Lista de (lat, lon) de los centros de servicio. + isochrone_minutes: Minutos de isócrona a calcular para cada centro. + base_url: URL base del servidor Valhalla. + concurrency: Número máximo de requests async simultáneos para isócronas. + + Returns: + Dict con claves: + "matrix" (list[dict]): Un dict por par (origin_i, center_j) con + {i, j, meters, seconds, error}. Orden: todos los centros + para origin[0], luego origin[1], etc. + "isochrones" (list[dict|None]): Una isócrona GeoJSON por cada center, + en el mismo orden. None si Valhalla falló para ese centro. + """ + n_origins = len(origins) + n_centers = len(centers) + + # Build all pairs: (origin_idx, center_idx) + pairs = [(i, j) for i in range(n_origins) for j in range(n_centers)] + + # 1. Matrix: origins as sources, centers as destinations + raw_matrix = valhalla_matrix_1_to_n( + origins=origins, + destinations=centers, + pairs=pairs, + base_url=base_url, + concurrency=concurrency, + ) + + matrix = [ + { + "i": pairs[k][0], + "j": pairs[k][1], + "meters": raw_matrix[k]["meters"], + "seconds": raw_matrix[k]["seconds"], + "error": raw_matrix[k]["error"], + } + for k in range(len(pairs)) + ] + + # 2. Isochrones: one per center + iso_requests = [ + {"lat": lat, "lon": lon, "minutes": isochrone_minutes, "id": str(idx)} + for idx, (lat, lon) in enumerate(centers) + ] + isochrones = await valhalla_isochrones_async( + requests=iso_requests, + base_url=base_url, + concurrency=concurrency, + ) + + return {"matrix": matrix, "isochrones": list(isochrones)} diff --git a/python/functions/pipelines/count_points_per_zone_pipeline.md b/python/functions/pipelines/count_points_per_zone_pipeline.md new file mode 100644 index 00000000..79ee1125 --- /dev/null +++ b/python/functions/pipelines/count_points_per_zone_pipeline.md @@ -0,0 +1,58 @@ +--- +id: count_points_per_zone_pipeline_py_pipelines +name: count_points_per_zone_pipeline +kind: pipeline +lang: py +domain: pipelines +version: "1.0.0" +purity: impure +signature: "def count_points_per_zone_pipeline(points: list[tuple[float, float]], zones: list[dict]) -> dict" +description: "Cuenta cuántos puntos (lon, lat) caen dentro de cada zona geográfica definida por GeoJSON." +tags: [pipeline, geo, footprint, geojson, spatial, count, zone] +uses_functions: ["load_geojson_polygons_py_geo", "polygon_bbox_py_geo", "point_in_polygons_bbox_py_geo"] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +example: | + result = count_points_per_zone_pipeline( + points=[(-3.70, 40.41), (-3.65, 40.45)], + zones=[{"label": "Madrid centro", "geojson_path": "zones/madrid_centro.geojson"}], + ) + # result["counts"] == {"Madrid centro": 2}, result["unassigned"] == 0 +tested: true +tests: ["test_count_points_per_zone_pipeline"] +test_file_path: "python/functions/pipelines/tests/test_count_points_per_zone_pipeline.py" +file_path: "python/functions/pipelines/count_points_per_zone_pipeline.py" +params: + - {name: points, desc: "Lista de (lon, lat) de los puntos a clasificar por zona. Coordenadas WGS84."} + - {name: zones, desc: "Lista de dicts con label y geojson_path. Define las zonas geográficas a evaluar."} +output: "Dict con counts (label→int), total_points, total_assigned y unassigned." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "internal:composed" +--- + +## Ejemplo + +```python +result = count_points_per_zone_pipeline( + points=[(-3.70, 40.41), (-3.65, 40.45), (-4.50, 39.80)], + zones=[ + {"label": "Madrid centro", "geojson_path": "zones/madrid_centro.geojson"}, + {"label": "Área metropolitana", "geojson_path": "zones/madrid_metro.geojson"}, + ], +) + +print(result["counts"]) +# {"Madrid centro": 2, "Área metropolitana": 2} +print(f"Sin zona: {result['unassigned']}") # 1 (el punto en Toledo) +``` + +## Notas + +Los puntos se representan como (lon, lat) — orden GeoJSON estándar (longitud primero). +Un punto puede contarse en múltiples zonas si los polígonos se solapan: `total_assigned` puede superar `total_points`. +`unassigned` cuenta los puntos que no cayeron en ninguna de las zonas especificadas. +La carga de polígonos se hace una vez por zona; el prefiltraje por bbox reduce el coste de ray-casting. diff --git a/python/functions/pipelines/count_points_per_zone_pipeline.py b/python/functions/pipelines/count_points_per_zone_pipeline.py new file mode 100644 index 00000000..bee4cd09 --- /dev/null +++ b/python/functions/pipelines/count_points_per_zone_pipeline.py @@ -0,0 +1,66 @@ +"""Pipeline: cuenta cuántos puntos caen dentro de cada zona geográfica GeoJSON.""" + +from __future__ import annotations + +import sys +import os + +_FUNCTIONS_DIR = os.path.join(os.path.dirname(__file__), "..") +if _FUNCTIONS_DIR not in sys.path: + sys.path.insert(0, _FUNCTIONS_DIR) + +from geo.load_geojson_polygons import load_geojson_polygons +from geo.polygon_bbox import polygon_bbox +from geo.point_in_polygons_bbox import point_in_polygons_bbox + + +def count_points_per_zone_pipeline( + points: list[tuple[float, float]], + zones: list[dict], +) -> dict: + """Cuenta cuántos puntos caen dentro de cada zona geográfica. + + Para cada zona carga los polígonos GeoJSON, precalcula los bboxes + y aplica prefiltraje+ray-casting para contar puntos dentro. + Un mismo punto puede contarse en varias zonas si los polígonos se solapan. + + Args: + points: Lista de (lon, lat) de los puntos a clasificar. + zones: Lista de dicts con: + - "label" (str): nombre de la zona. + - "geojson_path" (str): ruta al GeoJSON de la zona. + + Returns: + Dict con claves: + "counts" (dict[str, int]): {label: n_puntos_en_zona}. + "total_points" (int): total de puntos de entrada. + "total_assigned" (int): suma de conteos (puede superar total_points si hay solapamiento). + "unassigned" (int): puntos que no caen en ninguna zona. + """ + counts: dict[str, int] = {} + # Track which points were assigned to at least one zone + assigned_flags = [False] * len(points) + + for zone in zones: + label = zone["label"] + polygons = load_geojson_polygons(zone["geojson_path"]) + bboxes = [polygon_bbox(p) for p in polygons] + + zone_count = 0 + for idx, (lon, lat) in enumerate(points): + if point_in_polygons_bbox(float(lon), float(lat), polygons, bboxes): + zone_count += 1 + assigned_flags[idx] = True + + counts[label] = zone_count + + total_points = len(points) + total_assigned = sum(counts.values()) + unassigned = sum(1 for f in assigned_flags if not f) + + return { + "counts": counts, + "total_points": total_points, + "total_assigned": total_assigned, + "unassigned": unassigned, + } diff --git a/python/functions/pipelines/extract_graph_from_text.md b/python/functions/pipelines/extract_graph_from_text.md new file mode 100644 index 00000000..8aadde8a --- /dev/null +++ b/python/functions/pipelines/extract_graph_from_text.md @@ -0,0 +1,89 @@ +--- +name: extract_graph_from_text +kind: pipeline +lang: py +domain: pipelines +version: "1.0.0" +purity: impure +signature: "def extract_graph_from_text(text: str, entity_labels: list[str], relation_labels: list | dict, allowed: dict, model: Any, threshold: float = 0.3, max_chars_per_chunk: int = 1500, overlap_sentences: int = 2) -> dict" +description: "Pipeline E2E: texto -> grafo de entidades y relaciones. Orquesta chunking, extraccion con GLiNER2 por chunk, agregacion, filtrado tipado y resolucion de alias. Refactorizacion del playground del analisis gliner_glirel_tuning." +tags: [pipeline, graph, ner, relation-extraction, gliner2, nlp, e2e, knowledge-graph, datascience, python] +uses_functions: + - chunk_with_overlap_py_core + - extract_graph_gliner2_py_datascience + - aggregate_extraction_results_py_core + - filter_relations_by_entity_types_py_core + - merge_entity_aliases_py_core +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [time, typing.Any] +params: + - name: text + desc: "Texto de entrada de cualquier longitud. Se auto-chunkea si supera max_chars_per_chunk. Recomendado: pre-limpiar con clean_pdf_text si viene de un PDF." + - name: entity_labels + desc: "Tipos de entidad para GLiNER2. E.g. ['person', 'organization', 'location']. Usar snake_case (mejor recall segun notebook 08)." + - name: relation_labels + desc: "Tipos de relacion. Lista de strings o dict {label: description}. E.g. ['works_at', 'ceo_of'] o {'ceo_of': 'person is CEO of organization'}." + - name: allowed + desc: "Reglas de filtrado tipado {rel_type: (head_types, tail_types)}. Pasar {} para desactivar el filtrado. E.g. {'ceo_of': (['person'], ['organization'])}." + - name: model + desc: "Instancia GLiNER2 cargada con gliner2_load_model. Inyectada por el caller para permitir cache entre llamadas." + - name: threshold + desc: "Umbral de confianza GLiNER2 (0-1). 0.3 validado empiricamente. Menor = mas recall, mas ruido." + - name: max_chars_per_chunk + desc: "Maximo de caracteres por chunk antes de dividir. 1500 es el valor optimo para GLiNER2-large." + - name: overlap_sentences + desc: "Frases de overlap entre chunks consecutivos. 2 evita perder entidades en los bordes de chunk." +output: "Dict con 'nodes' (lista de {id, type, count}), 'edges' (lista de {from, to, kind}) y 'stats' ({n_chunks, n_nodes, n_edges, n_dropped_typed, elapsed_s}). Listo para serializar a JSON y visualizar con Sigma/D3." +tested: true +tests: + - "texto corto produce nodos y aristas esperados con stub model" + - "stats tiene todos los campos requeridos" +test_file_path: "python/functions/pipelines/tests/test_extract_graph_from_text.py" +file_path: "python/functions/pipelines/extract_graph_from_text.py" +notes: | + Refactorizacion directa del playground/server.py del analisis + projects/osint_graph/analysis/gliner_glirel_tuning. + Todas las recetas validadas empiricamente en los notebooks 04, 06 y 08: + - threshold=0.3 (notebook 04) + - overlap_sentences=2 (notebook 06) + - filtrado tipado (notebook 08) + - coreference normalize+substring (playground/server.py) + + Para PDFs: usar extract_pdf_text + clean_pdf_text antes de llamar a este pipeline. + Para OpenIE sin vocabulario fijo: usar extract_triples_spacy_es como alternativa. +--- + +## Ejemplo + +```python +from datascience.gliner2_load_model import gliner2_load_model +from pipelines.extract_graph_from_text import extract_graph_from_text + +model = gliner2_load_model(device="auto") + +ENTITY_LABELS = ["person", "organization", "location"] +RELATION_LABELS = ["works_at", "ceo_of", "headquartered_in", "president_of"] +ALLOWED = { + "ceo_of": (["person"], ["organization"]), + "president_of": (["person"], ["organization"]), + "works_at": (["person"], ["organization"]), + "headquartered_in": (["organization"], ["location"]), +} + +text = """Carlos Torres Blanco es el presidente de BBVA. +BBVA tiene su sede corporativa en Bilbao, aunque opera en mas de 30 paises.""" + +graph = extract_graph_from_text( + text=text, + entity_labels=ENTITY_LABELS, + relation_labels=RELATION_LABELS, + allowed=ALLOWED, + model=model, +) +# graph["nodes"] -> [{"id": "Carlos Torres Blanco", "type": "person", "count": 1}, ...] +# graph["edges"] -> [{"from": "Carlos Torres Blanco", "to": "BBVA", "kind": "president_of"}] +# graph["stats"] -> {"n_chunks": 1, "n_nodes": 3, "n_edges": 2, ...} +``` diff --git a/python/functions/pipelines/extract_graph_from_text.py b/python/functions/pipelines/extract_graph_from_text.py new file mode 100644 index 00000000..1e7a73f9 --- /dev/null +++ b/python/functions/pipelines/extract_graph_from_text.py @@ -0,0 +1,147 @@ +"""Pipeline E2E: text -> entities + relations + graph nodes/edges. + +Compone las funciones del registry: + - chunk_with_overlap (si len(text) > max_chars_per_chunk) + - extract_graph_gliner2 (por chunk) + - aggregate_extraction_results + - filter_relations_by_entity_types + - merge_entity_aliases + +Es el flujo completo del playground (server.py del analisis gliner_glirel_tuning) +refactorizado como funcion componible. +""" + +from __future__ import annotations + +import os +import sys +import time +from typing import Any + +_ROOT = os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) +if _ROOT not in sys.path: + sys.path.insert(0, _ROOT) + +from python.functions.core.chunk_with_overlap import chunk_with_overlap +from python.functions.core.aggregate_extraction_results import aggregate_extraction_results +from python.functions.core.filter_relations_by_entity_types import filter_relations_by_entity_types +from python.functions.core.merge_entity_aliases import merge_entity_aliases +from python.functions.datascience.extract_graph_gliner2 import extract_graph_gliner2 + + +def extract_graph_from_text( + text: str, + entity_labels: list[str], + relation_labels: list | dict, + allowed: dict, + model: Any, + threshold: float = 0.3, + max_chars_per_chunk: int = 1500, + overlap_sentences: int = 2, +) -> dict: + """Full pipeline: text -> graph (nodes + edges). + + Orchestrates chunking, per-chunk extraction, aggregation, typed filtering + and alias resolution. Returns a graph ready for visualization. + + Args: + text: Input text (any length). Auto-chunked if > max_chars_per_chunk. + entity_labels: E.g. ["person", "organization", "location"]. + relation_labels: E.g. ["works_at", "ceo_of", "located_in"] or dict + with descriptions per label. + allowed: Typed filter rules {rel_type: (head_types, tail_types)}. + Pass {} to skip typed filtering. + model: GLiNER2 model instance from gliner2_load_model. + threshold: Confidence threshold (0.3 validated empirically). + max_chars_per_chunk: Max chars per chunk before splitting. + overlap_sentences: Sentence overlap between consecutive chunks. + + Returns: + { + "nodes": [{"id": str, "type": str, "count": int}, ...], + "edges": [{"from": str, "to": str, "kind": str}, ...], + "stats": { + "n_chunks": int, + "n_nodes": int, + "n_edges": int, + "n_dropped_typed": int, + "elapsed_s": float + } + } + """ + t0 = time.time() + + # 1. Chunking + if len(text) <= max_chars_per_chunk: + chunks = [text] + else: + chunks = [ + c["text"] + for c in chunk_with_overlap( + text, + max_chars=max_chars_per_chunk, + overlap_sentences=overlap_sentences, + ) + ] + + # 2. Extraccion por chunk + results = [ + extract_graph_gliner2( + chunk, + entity_labels=entity_labels, + relation_labels=relation_labels, + model=model, + threshold=threshold, + ) + for chunk in chunks + ] + + # 3. Agregacion + agg = aggregate_extraction_results(results) + + # 4. name_to_type para el filtrado tipado + name_to_type = {key[1]: data["type"] for key, data in agg["entities"].items()} + + # 5. Convertir Counter a dict {rel_type: [(h, t), ...]} + raw_relations: dict[str, list] = {} + for (h, rt, t), _count in agg["relations"].items(): + raw_relations.setdefault(rt, []).append((h, t)) + + # 6. Filtrado tipado + keep, drop = filter_relations_by_entity_types(raw_relations, name_to_type, allowed) + + # 7. Coreference / alias + original_names = [data["name"] for data in agg["entities"].values()] + alias = merge_entity_aliases(original_names) + + # 8. Construir nodos con alias aplicado + nodes_dict: dict[str, dict] = {} + for (typ, _key), data in agg["entities"].items(): + canon = alias.get(data["name"], data["name"]) + if canon not in nodes_dict: + nodes_dict[canon] = {"type": typ, "count": data["count"]} + else: + nodes_dict[canon]["count"] += data["count"] + + # 9. Construir aristas deduplicadas con alias aplicado + edges_set: set[tuple[str, str, str]] = set() + for e in keep: + h_canon = alias.get(e["from"], e["from"]) + t_canon = alias.get(e["to"], e["to"]) + if h_canon == t_canon: + continue + edges_set.add((h_canon, e["kind"], t_canon)) + + elapsed = round(time.time() - t0, 2) + + return { + "nodes": [{"id": n, "type": info["type"], "count": info["count"]} for n, info in nodes_dict.items()], + "edges": [{"from": h, "to": t, "kind": k} for h, k, t in edges_set], + "stats": { + "n_chunks": len(chunks), + "n_nodes": len(nodes_dict), + "n_edges": len(edges_set), + "n_dropped_typed": len(drop), + "elapsed_s": elapsed, + }, + } diff --git a/python/functions/pipelines/generate_isochrones_by_zone_pipeline.md b/python/functions/pipelines/generate_isochrones_by_zone_pipeline.md new file mode 100644 index 00000000..15a44ded --- /dev/null +++ b/python/functions/pipelines/generate_isochrones_by_zone_pipeline.md @@ -0,0 +1,72 @@ +--- +id: generate_isochrones_by_zone_pipeline_py_pipelines +name: generate_isochrones_by_zone_pipeline +kind: pipeline +lang: py +domain: pipelines +version: "1.0.0" +purity: impure +signature: "async def generate_isochrones_by_zone_pipeline(zones, points, centers, base_url, concurrency) -> dict" +description: "Genera isócronas Valhalla por zona geográfica usando el p75 de tiempos de viaje de los puntos en cada zona." +tags: [pipeline, geo, footprint, valhalla, isochrone, zone, p75, geojson] +uses_functions: ["load_geojson_polygons_py_geo", "polygon_bbox_py_geo", "point_in_polygons_bbox_py_geo", "summary_stats_py_datascience", "valhalla_isochrones_async_py_geo"] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +example: | + import asyncio + result = asyncio.run(generate_isochrones_by_zone_pipeline( + zones=[{"label": "M-30", "geojson_path": "zones/m30.geojson", "exclude_geojson_path": None}], + points=[{"lat": 40.41, "lon": -3.70, "seconds": 600.0}], + centers=[{"lat": 40.42, "lon": -3.69, "id": "centro_a"}], + )) + # result["zones"][0]["minutes"] == 10.0, isochrones lista con GeoJSON +tested: true +tests: ["test_generate_isochrones_by_zone_pipeline"] +test_file_path: "python/functions/pipelines/tests/test_generate_isochrones_by_zone_pipeline.py" +file_path: "python/functions/pipelines/generate_isochrones_by_zone_pipeline.py" +params: + - {name: zones, desc: "Lista de dicts con label, geojson_path y exclude_geojson_path (opcional). Define las zonas geográficas."} + - {name: points, desc: "Lista de dicts con lat, lon y seconds (tiempo de viaje real medido). Se calcula p75 por zona."} + - {name: centers, desc: "Lista de dicts con lat, lon e id. Se filtra por zona para generar isócronas."} + - {name: base_url, desc: "URL base del servidor Valhalla. Default http://localhost:8002."} + - {name: concurrency, desc: "Número máximo de requests async simultáneos para isócronas. Default 6."} +output: "Dict con 'zones': lista de {label, minutes, n_points, n_centers, isochrones} por zona." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "internal:composed" +--- + +## Ejemplo + +```python +import asyncio + +result = asyncio.run(generate_isochrones_by_zone_pipeline( + zones=[ + {"label": "M-30", "geojson_path": "zones/m30.geojson", "exclude_geojson_path": None}, + {"label": "M-40", "geojson_path": "zones/m40.geojson", "exclude_geojson_path": "zones/m30.geojson"}, + ], + points=[ + {"lat": 40.41, "lon": -3.70, "seconds": 600.0}, + {"lat": 40.38, "lon": -3.72, "seconds": 900.0}, + ], + centers=[ + {"lat": 40.42, "lon": -3.69, "id": "centro_a"}, + {"lat": 40.35, "lon": -3.75, "id": "centro_b"}, + ], +)) + +for z in result["zones"]: + print(f"{z['label']}: {z['n_points']} pts, p75={z['minutes']:.1f}min, {z['n_centers']} centros") +``` + +## Notas + +Función async — requiere `asyncio.run(...)` o `await`. +El p75 se calcula sobre `seconds` de los puntos en zona; luego se divide entre 60 para obtener minutos. +Si una zona no tiene puntos con seconds válidos, `minutes=None` y no se generan isócronas. +El polígono de exclusión permite hacer anillos (ej: M-40 excluyendo M-30). +Los centros se asignan a la primera zona que los contiene — no se deduplicam entre zonas. diff --git a/python/functions/pipelines/generate_isochrones_by_zone_pipeline.py b/python/functions/pipelines/generate_isochrones_by_zone_pipeline.py new file mode 100644 index 00000000..f3cc39e9 --- /dev/null +++ b/python/functions/pipelines/generate_isochrones_by_zone_pipeline.py @@ -0,0 +1,130 @@ +"""Pipeline: genera isócronas por zona geográfica usando p75 de tiempos de los puntos en cada zona.""" + +from __future__ import annotations + +import asyncio +import math +import sys +import os + +_FUNCTIONS_DIR = os.path.join(os.path.dirname(__file__), "..") +if _FUNCTIONS_DIR not in sys.path: + sys.path.insert(0, _FUNCTIONS_DIR) + +from geo.load_geojson_polygons import load_geojson_polygons +from geo.polygon_bbox import polygon_bbox +from geo.point_in_polygons_bbox import point_in_polygons_bbox +from datascience.summary_stats import summary_stats +from geo.valhalla_isochrones_async import valhalla_isochrones_async + + +async def generate_isochrones_by_zone_pipeline( + zones: list[dict], + points: list[dict], + centers: list[dict], + base_url: str = "http://localhost:8002", + concurrency: int = 6, +) -> dict: + """Genera isócronas por zona usando el p75 de tiempos de viaje de los puntos en cada zona. + + Para cada zona: + 1. Carga los polígonos GeoJSON de la zona (y de exclusión si se indica). + 2. Filtra los points que caen dentro de la zona (no en exclusión). + 3. Calcula el p75(seconds/60) → minutos de isócrona representativa. + 4. Filtra los centers que caen en la zona. + 5. Genera una isócrona Valhalla por cada center con esos minutos. + + Args: + zones: Lista de dicts con: + - "label" (str): nombre de la zona. + - "geojson_path" (str): ruta al GeoJSON de la zona. + - "exclude_geojson_path" (str | None): ruta GeoJSON de exclusión (opcional). + points: Lista de dicts con "lat" (float), "lon" (float) y "seconds" (float | None). + Los puntos sin seconds o con seconds=None se omiten del cálculo de p75. + centers: Lista de dicts con "lat" (float), "lon" (float) e "id" (str). + base_url: URL base del servidor Valhalla. + concurrency: Número máximo de requests async simultáneos para isócronas. + + Returns: + Dict con clave "zones": lista de dicts con: + - "label" (str): nombre de la zona. + - "minutes" (float | None): p75 en minutos usado para las isócronas. None si sin datos. + - "n_points" (int): puntos en la zona con seconds válidos. + - "n_centers" (int): centros en la zona. + - "isochrones" (list[dict | None]): GeoJSON por cada center de la zona. + """ + zone_results = [] + + for zone in zones: + label = zone["label"] + geojson_path = zone["geojson_path"] + exclude_path = zone.get("exclude_geojson_path") + + # 1. Cargar polígonos de la zona + polygons = load_geojson_polygons(geojson_path) + bboxes = [polygon_bbox(p) for p in polygons] + + # Cargar polígonos de exclusión si se indica + exclude_polygons: list = [] + exclude_bboxes: list = [] + if exclude_path: + exclude_polygons = load_geojson_polygons(exclude_path) + exclude_bboxes = [polygon_bbox(p) for p in exclude_polygons] + + # 2. Filtrar points en zona (y no en exclusión) + zone_seconds: list[float] = [] + for pt in points: + lon = float(pt["lon"]) + lat = float(pt["lat"]) + secs = pt.get("seconds") + if secs is None or (isinstance(secs, float) and math.isnan(secs)): + continue + if not point_in_polygons_bbox(lon, lat, polygons, bboxes): + continue + if exclude_polygons and point_in_polygons_bbox(lon, lat, exclude_polygons, exclude_bboxes): + continue + zone_seconds.append(float(secs)) + + # 3. Calcular p75 en minutos + minutes: float | None = None + if zone_seconds: + stats = summary_stats(zone_seconds) + p75_secs = stats.get("p75", math.nan) + if not math.isnan(p75_secs): + minutes = p75_secs / 60.0 + + # 4. Filtrar centers en zona + zone_centers = [ + c for c in centers + if point_in_polygons_bbox(float(c["lon"]), float(c["lat"]), polygons, bboxes) + ] + + # 5. Generar isócronas para cada center de la zona + isochrones: list = [] + if zone_centers and minutes is not None and minutes > 0: + iso_requests = [ + { + "lat": float(c["lat"]), + "lon": float(c["lon"]), + "minutes": max(1, round(minutes)), + "id": c.get("id", str(idx)), + } + for idx, c in enumerate(zone_centers) + ] + isochrones = await valhalla_isochrones_async( + requests=iso_requests, + base_url=base_url, + concurrency=concurrency, + ) + + zone_results.append( + { + "label": label, + "minutes": minutes, + "n_points": len(zone_seconds), + "n_centers": len(zone_centers), + "isochrones": list(isochrones), + } + ) + + return {"zones": zone_results} diff --git a/python/functions/pipelines/setup_geo_stack_docker_pipeline.md b/python/functions/pipelines/setup_geo_stack_docker_pipeline.md new file mode 100644 index 00000000..f1b81f90 --- /dev/null +++ b/python/functions/pipelines/setup_geo_stack_docker_pipeline.md @@ -0,0 +1,56 @@ +--- +id: setup_geo_stack_docker_pipeline_py_pipelines +name: setup_geo_stack_docker_pipeline +kind: pipeline +lang: py +domain: pipelines +version: "1.0.0" +purity: impure +signature: "def setup_geo_stack_docker_pipeline(compose_path: str, wait_seconds: int, verify: bool) -> dict" +description: "Levanta el geo stack Docker (Valhalla + PostGIS + Martin) via docker compose up -d y verifica que los tres servicios responden." +tags: [pipeline, geo, footprint, docker, valhalla, postgis, martin] +uses_functions: ["valhalla_route_py_geo"] +uses_types: [] +returns: [] +returns_optional: false +error_type: "error_go_core" +imports: [] +example: | + result = setup_geo_stack_docker_pipeline( + compose_path="apps/footprint_geo_stack/docker-compose.yml", + wait_seconds=60, + verify=True, + ) + # {"docker_up": True, "valhalla_ok": True, "postgis_ok": True, "martin_ok": True} +tested: true +tests: ["test_setup_geo_stack_docker_pipeline"] +test_file_path: "python/functions/pipelines/tests/test_setup_geo_stack_docker_pipeline.py" +file_path: "python/functions/pipelines/setup_geo_stack_docker_pipeline.py" +params: + - {name: compose_path, desc: "Ruta al docker-compose.yml del geo stack (Valhalla + PostGIS + Martin)."} + - {name: wait_seconds, desc: "Segundos a esperar tras docker compose up -d antes de verificar servicios. Default 60."} + - {name: verify, desc: "Si True, verifica los tres servicios via HTTP/docker exec. Default True."} +output: "Dict con docker_up, valhalla_ok, postgis_ok y martin_ok (bool cada uno)." +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "internal:composed" +--- + +## Ejemplo + +```python +result = setup_geo_stack_docker_pipeline( + compose_path="apps/footprint_geo_stack/docker-compose.yml", + wait_seconds=60, + verify=True, +) +print(result) +# {"docker_up": True, "valhalla_ok": True, "postgis_ok": True, "martin_ok": True} +``` + +## Notas + +Verifica Valhalla via GET /status, PostGIS via `docker exec footprint_postgis pg_isready -U postgres`, +y Martin via GET /health en http://localhost:3000/health. +Si `verify=False` solo retorna `docker_up` y el resto en False. +El nombre del contenedor PostGIS (`footprint_postgis`) debe coincidir con el definido en el compose. diff --git a/python/functions/pipelines/setup_geo_stack_docker_pipeline.py b/python/functions/pipelines/setup_geo_stack_docker_pipeline.py new file mode 100644 index 00000000..ee5b85d6 --- /dev/null +++ b/python/functions/pipelines/setup_geo_stack_docker_pipeline.py @@ -0,0 +1,98 @@ +"""Pipeline: levanta el geo stack Docker (Valhalla + PostGIS + Martin) y verifica servicios.""" + +from __future__ import annotations + +import json +import subprocess +import time +from urllib import request as urllib_request +from urllib.error import URLError + + +def setup_geo_stack_docker_pipeline( + compose_path: str = "apps/footprint_geo_stack/docker-compose.yml", + wait_seconds: int = 60, + verify: bool = True, +) -> dict: + """Levanta el geo stack via docker compose y verifica que los servicios responden. + + Ejecuta `docker compose up -d` sobre el compose_path dado, espera wait_seconds + y luego verifica (si verify=True) que Valhalla, PostGIS y Martin están operativos. + + Args: + compose_path: Ruta al docker-compose.yml del geo stack. + wait_seconds: Segundos a esperar tras `docker compose up -d` antes de verificar. + verify: Si True, verifica los tres servicios via HTTP/docker exec. + + Returns: + Dict con claves: + "docker_up" (bool): True si docker compose arrancó sin error. + "valhalla_ok" (bool): True si Valhalla responde a /status. + "postgis_ok" (bool): True si pg_isready retorna OK via docker exec. + "martin_ok" (bool): True si Martin responde a /health. + """ + result = { + "docker_up": False, + "valhalla_ok": False, + "postgis_ok": False, + "martin_ok": False, + } + + # Step 1: docker compose up -d + try: + proc = subprocess.run( + ["docker", "compose", "-f", compose_path, "up", "-d"], + capture_output=True, + text=True, + timeout=120, + ) + result["docker_up"] = proc.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + return result + + if not result["docker_up"]: + return result + + if not verify: + return result + + # Step 2: wait for services to be ready + if wait_seconds > 0: + time.sleep(wait_seconds) + + # Step 3: verify Valhalla via POST /route (lightweight status check via /status) + try: + req = urllib_request.Request( + "http://localhost:8002/status", + method="GET", + ) + with urllib_request.urlopen(req, timeout=10) as resp: + data = json.loads(resp.read().decode()) + result["valhalla_ok"] = isinstance(data, dict) + except (URLError, OSError, json.JSONDecodeError, Exception): + result["valhalla_ok"] = False + + # Step 4: verify PostGIS via pg_isready inside docker exec + try: + proc = subprocess.run( + ["docker", "exec", "footprint_postgis", "pg_isready", "-U", "postgres"], + capture_output=True, + text=True, + timeout=15, + ) + result["postgis_ok"] = proc.returncode == 0 + except (subprocess.TimeoutExpired, FileNotFoundError, OSError): + result["postgis_ok"] = False + + # Step 5: verify Martin via /health + try: + req = urllib_request.Request( + "http://localhost:3000/health", + method="GET", + ) + with urllib_request.urlopen(req, timeout=10) as resp: + result["martin_ok"] = resp.status == 200 + except (URLError, OSError, Exception): + result["martin_ok"] = False + + return result diff --git a/python/functions/pipelines/tests/test_compute_centers_reachability_pipeline.py b/python/functions/pipelines/tests/test_compute_centers_reachability_pipeline.py new file mode 100644 index 00000000..6d2e235a --- /dev/null +++ b/python/functions/pipelines/tests/test_compute_centers_reachability_pipeline.py @@ -0,0 +1,62 @@ +"""Tests para compute_centers_reachability_pipeline. + +Usa 2 orígenes y 2 centros reales en España con el stack Valhalla activo. +""" + +from __future__ import annotations + +import asyncio +import math +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.pipelines.compute_centers_reachability_pipeline import ( + compute_centers_reachability_pipeline, +) + + +def test_compute_centers_reachability_pipeline(): + """Matrix 2×2 y 2 isócronas con Valhalla en localhost:8002.""" + origins = [ + (40.4168, -3.7038), # Madrid + (37.3891, -5.9845), # Sevilla + ] + centers = [ + (41.3851, 2.1734), # Barcelona + (43.2627, -2.9253), # Bilbao + ] + + result = asyncio.run( + compute_centers_reachability_pipeline( + origins=origins, + centers=centers, + isochrone_minutes=15, + base_url="http://localhost:8002", + concurrency=4, + ) + ) + + assert isinstance(result, dict) + assert "matrix" in result + assert "isochrones" in result + + # Matrix: 2 orígenes × 2 centros = 4 entradas + matrix = result["matrix"] + assert len(matrix) == 4, f"Esperadas 4 entradas en matrix, got {len(matrix)}" + + for entry in matrix: + assert "i" in entry and "j" in entry + assert "meters" in entry and "seconds" in entry and "error" in entry + # Si Valhalla resolvió el par, meters > 0 + if entry["error"] == 0: + assert entry["meters"] > 0, "meters debe ser > 0 cuando no hay error" + + # Isochrones: 2 centros → 2 entradas + isochrones = result["isochrones"] + assert len(isochrones) == 2, f"Esperadas 2 isócronas, got {len(isochrones)}" + + # Al menos una isócrona debe ser un dict GeoJSON válido + valid_isos = [iso for iso in isochrones if isinstance(iso, dict)] + assert len(valid_isos) >= 1, "Al menos una isócrona debe ser un dict GeoJSON" diff --git a/python/functions/pipelines/tests/test_count_points_per_zone_pipeline.py b/python/functions/pipelines/tests/test_count_points_per_zone_pipeline.py new file mode 100644 index 00000000..e0a512ac --- /dev/null +++ b/python/functions/pipelines/tests/test_count_points_per_zone_pipeline.py @@ -0,0 +1,86 @@ +"""Tests para count_points_per_zone_pipeline. + +Usa un cuadrado sintético como zona y puntos aleatorios entre lat[40,41] lon[-4,-3]. +""" + +from __future__ import annotations + +import json +import os +import random +import sys +import tempfile + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.pipelines.count_points_per_zone_pipeline import ( + count_points_per_zone_pipeline, +) + + +def _make_square_geojson(min_lon: float, min_lat: float, max_lon: float, max_lat: float) -> dict: + """Crea un GeoJSON Polygon cuadrado con las coordenadas dadas.""" + return { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [min_lon, min_lat], + [max_lon, min_lat], + [max_lon, max_lat], + [min_lon, max_lat], + [min_lon, min_lat], + ] + ], + }, + "properties": {}, + } + ], + } + + +def test_count_points_per_zone_pipeline(): + """100 puntos aleatorios en [40,41]x[-4,-3]. Zona = cuadrado interior [40.2,40.8]x[-3.8,-3.2].""" + random.seed(42) + # Puntos (lon, lat) — orden GeoJSON + points = [ + (random.uniform(-4.0, -3.0), random.uniform(40.0, 41.0)) + for _ in range(100) + ] + + # Zona interior: cuadrado centrado que cubre ~36% del área total → espera ~36 puntos + zone_min_lon, zone_max_lon = -3.8, -3.2 + zone_min_lat, zone_max_lat = 40.2, 40.8 + + # Cuántos puntos deben caer (referencia para assert) + expected_inside = sum( + 1 for lon, lat in points + if zone_min_lon <= lon <= zone_max_lon and zone_min_lat <= lat <= zone_max_lat + ) + + with tempfile.TemporaryDirectory() as tmpdir: + zone_path = os.path.join(tmpdir, "zone_centro.geojson") + with open(zone_path, "w") as f: + json.dump( + _make_square_geojson(zone_min_lon, zone_min_lat, zone_max_lon, zone_max_lat), + f, + ) + + zones = [{"label": "Centro", "geojson_path": zone_path}] + + result = count_points_per_zone_pipeline(points=points, zones=zones) + + assert isinstance(result, dict) + assert set(result.keys()) == {"counts", "total_points", "total_assigned", "unassigned"} + + assert result["total_points"] == 100 + assert result["counts"]["Centro"] > 0, "Debe haber puntos en la zona" + assert result["counts"]["Centro"] == expected_inside, ( + f"Esperados {expected_inside} puntos, got {result['counts']['Centro']}" + ) + assert result["total_assigned"] == expected_inside + assert result["unassigned"] == 100 - expected_inside diff --git a/python/functions/pipelines/tests/test_extract_graph_from_text.py b/python/functions/pipelines/tests/test_extract_graph_from_text.py new file mode 100644 index 00000000..e8188657 --- /dev/null +++ b/python/functions/pipelines/tests/test_extract_graph_from_text.py @@ -0,0 +1,93 @@ +"""Tests para extract_graph_from_text pipeline. + +Usa stubs para GLiNER2 para validar el flujo completo sin descargar modelos. +""" + +from __future__ import annotations + +import os +import sys + +import pytest + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.pipelines.extract_graph_from_text import extract_graph_from_text + + +class _Schema: + def entities(self, labels): + return self + + def relations(self, labels): + return self + + +class _StubModel: + """Stub que retorna un grafo conocido para cualquier texto.""" + + def create_schema(self): + return _Schema() + + def extract(self, text, schema=None, threshold=0.3, include_confidence=False): + return { + "entities": { + "person": ["Carlos Torres"], + "organization": ["BBVA"], + "location": ["Bilbao"], + }, + "relation_extraction": { + "president_of": [("Carlos Torres", "BBVA")], + "headquartered_in": [("BBVA", "Bilbao")], + }, + } + + +ENTITY_LABELS = ["person", "organization", "location"] +RELATION_LABELS = ["president_of", "headquartered_in", "works_at"] +ALLOWED = { + "president_of": (["person"], ["organization"]), + "headquartered_in": (["organization"], ["location"]), +} + + +def test_texto_corto_produce_nodos_y_aristas_esperados(): + """texto corto produce nodos y aristas esperados con stub model""" + text = "Carlos Torres es presidente de BBVA con sede en Bilbao." + result = extract_graph_from_text( + text=text, + entity_labels=ENTITY_LABELS, + relation_labels=RELATION_LABELS, + allowed=ALLOWED, + model=_StubModel(), + threshold=0.3, + ) + + node_ids = {n["id"] for n in result["nodes"]} + assert "Carlos Torres" in node_ids + assert "BBVA" in node_ids + assert "Bilbao" in node_ids + + edge_kinds = {e["kind"] for e in result["edges"]} + assert "president_of" in edge_kinds + assert "headquartered_in" in edge_kinds + + +def test_stats_tiene_todos_los_campos_requeridos(): + """stats tiene todos los campos requeridos""" + text = "Texto de prueba para el pipeline." + result = extract_graph_from_text( + text=text, + entity_labels=ENTITY_LABELS, + relation_labels=RELATION_LABELS, + allowed=ALLOWED, + model=_StubModel(), + ) + stats = result["stats"] + assert "n_chunks" in stats + assert "n_nodes" in stats + assert "n_edges" in stats + assert "n_dropped_typed" in stats + assert "elapsed_s" in stats + assert stats["n_chunks"] >= 1 + assert stats["n_nodes"] >= 0 diff --git a/python/functions/pipelines/tests/test_generate_isochrones_by_zone_pipeline.py b/python/functions/pipelines/tests/test_generate_isochrones_by_zone_pipeline.py new file mode 100644 index 00000000..cca62943 --- /dev/null +++ b/python/functions/pipelines/tests/test_generate_isochrones_by_zone_pipeline.py @@ -0,0 +1,117 @@ +"""Tests para generate_isochrones_by_zone_pipeline. + +Crea archivos GeoJSON temporales con zonas sintéticas sobre Madrid +y verifica el resultado del pipeline con el stack Valhalla activo. +""" + +from __future__ import annotations + +import asyncio +import json +import os +import sys +import tempfile + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.pipelines.generate_isochrones_by_zone_pipeline import ( + generate_isochrones_by_zone_pipeline, +) + + +def _make_square_geojson(center_lon: float, center_lat: float, half: float) -> dict: + """Crea un GeoJSON Polygon cuadrado alrededor de (center_lon, center_lat).""" + lo, hi_lon = center_lon - half, center_lon + half + la, hi_lat = center_lat - half, center_lat + half + return { + "type": "FeatureCollection", + "features": [ + { + "type": "Feature", + "geometry": { + "type": "Polygon", + "coordinates": [ + [ + [lo, la], + [hi_lon, la], + [hi_lon, hi_lat], + [lo, hi_lat], + [lo, la], + ] + ], + }, + "properties": {}, + } + ], + } + + +def test_generate_isochrones_by_zone_pipeline(): + """Dos zonas cuadradas sintéticas, 50 puntos con seconds=600, 3 centros en Madrid.""" + with tempfile.TemporaryDirectory() as tmpdir: + # Zona norte: cuadrado alrededor de Chamartín (Madrid norte) + zone_norte_path = os.path.join(tmpdir, "zone_norte.geojson") + with open(zone_norte_path, "w") as f: + json.dump(_make_square_geojson(-3.685, 40.47, 0.05), f) + + # Zona sur: cuadrado alrededor de Vallecas (Madrid sur) + zone_sur_path = os.path.join(tmpdir, "zone_sur.geojson") + with open(zone_sur_path, "w") as f: + json.dump(_make_square_geojson(-3.666, 40.38, 0.05), f) + + zones = [ + {"label": "Norte", "geojson_path": zone_norte_path, "exclude_geojson_path": None}, + {"label": "Sur", "geojson_path": zone_sur_path, "exclude_geojson_path": None}, + ] + + # 50 puntos: 25 en zona norte, 25 en zona sur, todos con seconds=600 (10 min) + points_norte = [ + {"lat": 40.47 + i * 0.001, "lon": -3.685 + i * 0.001, "seconds": 600.0} + for i in range(-12, 13) + ] + points_sur = [ + {"lat": 40.38 + i * 0.001, "lon": -3.666 + i * 0.001, "seconds": 600.0} + for i in range(-12, 13) + ] + points = points_norte + points_sur + + # 3 centros: 2 en norte, 1 en sur + centers = [ + {"lat": 40.47, "lon": -3.685, "id": "centro_norte_a"}, + {"lat": 40.465, "lon": -3.680, "id": "centro_norte_b"}, + {"lat": 40.380, "lon": -3.666, "id": "centro_sur_a"}, + ] + + result = asyncio.run( + generate_isochrones_by_zone_pipeline( + zones=zones, + points=points, + centers=centers, + base_url="http://localhost:8002", + concurrency=4, + ) + ) + + assert isinstance(result, dict) + assert "zones" in result + zone_results = result["zones"] + + assert len(zone_results) == 2, f"Esperadas 2 zonas, got {len(zone_results)}" + + for z in zone_results: + assert "label" in z + assert "minutes" in z + assert "n_points" in z + assert "n_centers" in z + assert "isochrones" in z + assert isinstance(z["isochrones"], list) + + # p75 de 600s = 10 min → minutes ≈ 10 + for z in zone_results: + if z["n_points"] > 0: + assert z["minutes"] is not None + assert 9.5 <= z["minutes"] <= 10.5, f"p75 esperado ~10 min, got {z['minutes']}" + + # Al menos una zona debe tener isócronas (Valhalla activo) + total_isos = sum(len(z["isochrones"]) for z in zone_results) + assert total_isos >= 1, "Al menos una isócrona debe generarse" diff --git a/python/functions/pipelines/tests/test_setup_geo_stack_docker_pipeline.py b/python/functions/pipelines/tests/test_setup_geo_stack_docker_pipeline.py new file mode 100644 index 00000000..8e1a5979 --- /dev/null +++ b/python/functions/pipelines/tests/test_setup_geo_stack_docker_pipeline.py @@ -0,0 +1,38 @@ +"""Tests para setup_geo_stack_docker_pipeline. + +El geo stack ya está corriendo en localhost:8002 (Valhalla), por lo que +verify=True retorna flags reales del stack activo. +""" + +from __future__ import annotations + +import os +import sys + +sys.path.insert(0, os.path.join(os.path.dirname(__file__), "..", "..", "..", "..")) + +from python.functions.pipelines.setup_geo_stack_docker_pipeline import ( + setup_geo_stack_docker_pipeline, +) + + +def test_setup_geo_stack_docker_pipeline(): + """Verifica el geo stack activo en localhost (docker ya arrancado).""" + # Llamamos con verify=True pero sin relanzar docker compose + # (pasamos wait_seconds=0 para no esperar, el stack ya está up) + result = setup_geo_stack_docker_pipeline( + compose_path="apps/footprint_geo_stack/docker-compose.yml", + wait_seconds=0, + verify=True, + ) + + assert isinstance(result, dict) + assert set(result.keys()) == {"docker_up", "valhalla_ok", "postgis_ok", "martin_ok"} + + # docker_up puede ser False si el compose no existe en CI, pero verify sí corre + # Lo importante: los flags son bool + for key in ("docker_up", "valhalla_ok", "postgis_ok", "martin_ok"): + assert isinstance(result[key], bool), f"{key} debe ser bool" + + # Valhalla está activo en localhost:8002 + assert result["valhalla_ok"] is True, "Valhalla debe responder en localhost:8002" diff --git a/python/pyproject.toml b/python/pyproject.toml index ec166b3c..e400cfb5 100644 --- a/python/pyproject.toml +++ b/python/pyproject.toml @@ -5,17 +5,26 @@ description = "Funciones Python del fn-registry: Metabase API, ML, utilidades" readme = "README.md" requires-python = ">=3.12" dependencies = [ + "contextily>=1.7.0", "cryptography>=46.0.6", + "duckdb>=1.5.2", "fpdf2>=2.8.7", + "geopandas>=1.1.3", "google-cloud-bigquery>=3.25", "google-cloud-bigquery-datatransfer>=3.22.0", "google-cloud-bigquery-storage>=2.27", "google-cloud-storage>=3.10.1", "httpx", + "matplotlib>=3.10.9", "openpyxl>=3.1.5", "pypdf>=6.10.0", + "pyproj>=3.7.2", "python-docx>=1.2.0", "pyyaml>=6.0.3", + "rapidfuzz>=3.14.5", + "reportlab>=4.5.0", + "seaborn>=0.13.2", + "shapely>=2.1.2", "xlrd>=2.0.2", ] diff --git a/python/types/geo/bbox.md b/python/types/geo/bbox.md new file mode 100644 index 00000000..15db2267 --- /dev/null +++ b/python/types/geo/bbox.md @@ -0,0 +1,26 @@ +--- +id: BBox_py_geo +name: BBox +lang: py +domain: geo +version: "1.0.0" +algebraic: product +definition: | + @dataclass(frozen=True) + class BBox: + minx: float + miny: float + maxx: float + maxy: float +description: "Bounding box geografico inmutable con los cuatro extremos en grados decimales." +tags: [geo, type, bbox, bounds] +uses_types: [] +file_path: "python/types/geo/bbox.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +--- + +## Notas + +Tipo producto inmutable (frozen=True). Equivalente a (minx, miny, maxx, maxy) como tupla, +pero con campos nombrados para mayor legibilidad. Compatible con polygon_bbox_py_geo. diff --git a/python/types/geo/bbox.py b/python/types/geo/bbox.py new file mode 100644 index 00000000..1c891a96 --- /dev/null +++ b/python/types/geo/bbox.py @@ -0,0 +1,13 @@ +"""Tipo BBox: bounding box geografico inmutable.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class BBox: + """Bounding box geografico (minx, miny, maxx, maxy) en grados decimales.""" + + minx: float + miny: float + maxx: float + maxy: float diff --git a/python/types/geo/centro.md b/python/types/geo/centro.md new file mode 100644 index 00000000..70889023 --- /dev/null +++ b/python/types/geo/centro.md @@ -0,0 +1,30 @@ +--- +id: Centro_py_geo +name: Centro +lang: py +domain: geo +version: "1.0.0" +algebraic: product +definition: | + @dataclass + class Centro: + name: str + lat: float + lon: float + company: str + color: str = "" + description: str | None = None + nav_id: str | None = None +description: "Centro de servicio (taller, concesionario, etc.) con ubicacion geografica y metadatos de empresa." +tags: [geo, type, centro, aurgi, poi] +uses_types: [] +file_path: "python/types/geo/centro.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +source_file: "zonas_mapas_aurgi/backend/app.py:107" +--- + +## Notas + +No frozen (mutable) para permitir actualizacion de color post-construccion. nav_id es el identificador +interno de navegacion GPS. company identifica el grupo empresarial (AURGI, GLASSDRIVE, etc.). diff --git a/python/types/geo/centro.py b/python/types/geo/centro.py new file mode 100644 index 00000000..e5efb693 --- /dev/null +++ b/python/types/geo/centro.py @@ -0,0 +1,16 @@ +"""Tipo Centro: centro de servicio Aurgi con coordenadas y metadatos.""" + +from dataclasses import dataclass + + +@dataclass +class Centro: + """Centro de servicio (taller, concesionario, etc.) con ubicacion y empresa.""" + + name: str + lat: float + lon: float + company: str + color: str = "" + description: str | None = None + nav_id: str | None = None diff --git a/python/types/geo/isochrone_request.md b/python/types/geo/isochrone_request.md new file mode 100644 index 00000000..dd777725 --- /dev/null +++ b/python/types/geo/isochrone_request.md @@ -0,0 +1,29 @@ +--- +id: IsochroneRequest_py_geo +name: IsochroneRequest +lang: py +domain: geo +version: "1.0.0" +algebraic: product +definition: | + @dataclass(frozen=True) + class IsochroneRequest: + lat: float + lon: float + minutes: int + costing: str = "auto" + denoise: float = 0.6 + generalize_m: int = 50 + polygons: bool = True +description: "Parametros inmutables para solicitar una isocrona a la API de Valhalla (OTP/Valhalla)." +tags: [geo, type, isochrone, valhalla, routing] +uses_types: [] +file_path: "python/types/geo/isochrone_request.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +--- + +## Notas + +costing: "auto" (coche), "bicycle", "pedestrian". denoise: 0.0-1.0, filtra contornos pequeños. +generalize_m: tolerancia de simplificacion en metros. polygons: true retorna poligonos, false retorna lineas. diff --git a/python/types/geo/isochrone_request.py b/python/types/geo/isochrone_request.py new file mode 100644 index 00000000..05f9144d --- /dev/null +++ b/python/types/geo/isochrone_request.py @@ -0,0 +1,16 @@ +"""Tipo IsochroneRequest: parametros para una solicitud de isocronas.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class IsochroneRequest: + """Parametros para solicitar una isocrona a la API de Valhalla.""" + + lat: float + lon: float + minutes: int + costing: str = "auto" + denoise: float = 0.6 + generalize_m: int = 50 + polygons: bool = True diff --git a/python/types/geo/lon_lat.md b/python/types/geo/lon_lat.md new file mode 100644 index 00000000..3af15e9e --- /dev/null +++ b/python/types/geo/lon_lat.md @@ -0,0 +1,24 @@ +--- +id: LonLat_py_geo +name: LonLat +lang: py +domain: geo +version: "1.0.0" +algebraic: product +definition: | + @dataclass(frozen=True) + class LonLat: + lat: float + lon: float +description: "Coordenada geografica inmutable (longitud, latitud) en grados decimales." +tags: [geo, type, coordinate, lonlat] +uses_types: [] +file_path: "python/types/geo/lon_lat.py" +source_repo: "internal:footprint_aurgi" +source_license: "internal-aurgi" +--- + +## Notas + +Tipo producto inmutable (frozen=True). Los campos siguen la convencion lat/lon usada en APIs REST, +aunque el par lon/lat es el orden habitual en GeoJSON. diff --git a/python/types/geo/lon_lat.py b/python/types/geo/lon_lat.py new file mode 100644 index 00000000..af6a63b3 --- /dev/null +++ b/python/types/geo/lon_lat.py @@ -0,0 +1,11 @@ +"""Tipo LonLat: coordenada geografica inmutable.""" + +from dataclasses import dataclass + + +@dataclass(frozen=True) +class LonLat: + """Coordenada geografica (longitud, latitud) en grados decimales.""" + + lat: float + lon: float diff --git a/python/uv.lock b/python/uv.lock index 5d5c1341..c98717b6 100644 --- a/python/uv.lock +++ b/python/uv.lock @@ -2,9 +2,140 @@ version = 1 revision = 3 requires-python = ">=3.12" resolution-markers = [ - "python_full_version >= '3.14'", - "python_full_version == '3.13.*'", - "python_full_version < '3.13'", + "python_full_version >= '3.14' and sys_platform == 'win32'", + "python_full_version >= '3.14' and sys_platform == 'emscripten'", + "python_full_version >= '3.14' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'win32'", + "python_full_version == '3.13.*' and sys_platform == 'emscripten'", + "python_full_version == '3.13.*' and sys_platform != 'emscripten' and sys_platform != 'win32'", + "python_full_version < '3.13' and sys_platform == 'win32'", + "python_full_version < '3.13' and sys_platform == 'emscripten'", + "python_full_version < '3.13' and sys_platform != 'emscripten' and sys_platform != 'win32'", +] + +[[package]] +name = "affine" +version = "2.4.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/69/98/d2f0bb06385069e799fc7d2870d9e078cfa0fa396dc8a2b81227d0da08b9/affine-2.4.0.tar.gz", hash = "sha256:a24d818d6a836c131976d22f8c27b8d3ca32d0af64c1d8d29deb7bafa4da1eea", size = 17132, upload-time = "2023-01-19T23:44:30.696Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0b/f7/85273299ab57117850cc0a936c64151171fac4da49bc6fba0dad984a7c5f/affine-2.4.0-py3-none-any.whl", hash = "sha256:8a3df80e2b2378aef598a83c1392efd47967afec4242021a0b06b4c7cbc61a92", size = 15662, upload-time = "2023-01-19T23:44:28.833Z" }, +] + +[[package]] +name = "aiohappyeyeballs" +version = "2.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/26/30/f84a107a9c4331c14b2b586036f40965c128aa4fee4dda5d3d51cb14ad54/aiohappyeyeballs-2.6.1.tar.gz", hash = "sha256:c3f9d0113123803ccadfdf3f0faa505bc78e6a72d1cc4806cbd719826e943558", size = 22760, upload-time = "2025-03-12T01:42:48.764Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/0f/15/5bf3b99495fb160b63f95972b81750f18f7f4e02ad051373b669d17d44f2/aiohappyeyeballs-2.6.1-py3-none-any.whl", hash = "sha256:f349ba8f4b75cb25c99c5c2d84e997e485204d2902a9597802b0371f09331fb8", size = 15265, upload-time = "2025-03-12T01:42:47.083Z" }, +] + +[[package]] +name = "aiohttp" +version = "3.13.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "aiohappyeyeballs" }, + { name = "aiosignal" }, + { name = "attrs" }, + { name = "frozenlist" }, + { name = "multidict" }, + { name = "propcache" }, + { name = "yarl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/77/9a/152096d4808df8e4268befa55fba462f440f14beab85e8ad9bf990516918/aiohttp-3.13.5.tar.gz", hash = "sha256:9d98cc980ecc96be6eb4c1994ce35d28d8b1f5e5208a23b421187d1209dbb7d1", size = 7858271, upload-time = "2026-03-31T22:01:03.343Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/6f/353954c29e7dcce7cf00280a02c75f30e133c00793c7a2ed3776d7b2f426/aiohttp-3.13.5-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:023ecba036ddd840b0b19bf195bfae970083fd7024ce1ac22e9bba90464620e9", size = 748876, upload-time = "2026-03-31T21:57:36.319Z" }, + { url = "https://files.pythonhosted.org/packages/f5/1b/428a7c64687b3b2e9cd293186695affc0e1e54a445d0361743b231f11066/aiohttp-3.13.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:15c933ad7920b7d9a20de151efcd05a6e38302cbf0e10c9b2acb9a42210a2416", size = 499557, upload-time = "2026-03-31T21:57:38.236Z" }, + { url = "https://files.pythonhosted.org/packages/29/47/7be41556bfbb6917069d6a6634bb7dd5e163ba445b783a90d40f5ac7e3a7/aiohttp-3.13.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ab2899f9fa2f9f741896ebb6fa07c4c883bfa5c7f2ddd8cf2aafa86fa981b2d2", size = 500258, upload-time = "2026-03-31T21:57:39.923Z" }, + { url = "https://files.pythonhosted.org/packages/67/84/c9ecc5828cb0b3695856c07c0a6817a99d51e2473400f705275a2b3d9239/aiohttp-3.13.5-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a60eaa2d440cd4707696b52e40ed3e2b0f73f65be07fd0ef23b6b539c9c0b0b4", size = 1749199, upload-time = "2026-03-31T21:57:41.938Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d3/3c6d610e66b495657622edb6ae7c7fd31b2e9086b4ec50b47897ad6042a9/aiohttp-3.13.5-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:55b3bdd3292283295774ab585160c4004f4f2f203946997f49aac032c84649e9", size = 1721013, upload-time = "2026-03-31T21:57:43.904Z" }, + { url = "https://files.pythonhosted.org/packages/49/a0/24409c12217456df0bae7babe3b014e460b0b38a8e60753d6cb339f6556d/aiohttp-3.13.5-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c2b2355dc094e5f7d45a7bb262fe7207aa0460b37a0d87027dcf21b5d890e7d5", size = 1781501, upload-time = "2026-03-31T21:57:46.285Z" }, + { url = "https://files.pythonhosted.org/packages/98/9d/b65ec649adc5bccc008b0957a9a9c691070aeac4e41cea18559fef49958b/aiohttp-3.13.5-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b38765950832f7d728297689ad78f5f2cf79ff82487131c4d26fe6ceecdc5f8e", size = 1878981, upload-time = "2026-03-31T21:57:48.734Z" }, + { url = "https://files.pythonhosted.org/packages/57/d8/8d44036d7eb7b6a8ec4c5494ea0c8c8b94fbc0ed3991c1a7adf230df03bf/aiohttp-3.13.5-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b18f31b80d5a33661e08c89e202edabf1986e9b49c42b4504371daeaa11b47c1", size = 1767934, upload-time = "2026-03-31T21:57:51.171Z" }, + { url = "https://files.pythonhosted.org/packages/31/04/d3f8211f273356f158e3464e9e45484d3fb8c4ce5eb2f6fe9405c3273983/aiohttp-3.13.5-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:33add2463dde55c4f2d9635c6ab33ce154e5ecf322bd26d09af95c5f81cfa286", size = 1566671, upload-time = "2026-03-31T21:57:53.326Z" }, + { url = "https://files.pythonhosted.org/packages/41/db/073e4ebe00b78e2dfcacff734291651729a62953b48933d765dc513bf798/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:327cc432fdf1356fb4fbc6fe833ad4e9f6aacb71a8acaa5f1855e4b25910e4a9", size = 1705219, upload-time = "2026-03-31T21:57:55.385Z" }, + { url = "https://files.pythonhosted.org/packages/48/45/7dfba71a2f9fd97b15c95c06819de7eb38113d2cdb6319669195a7d64270/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:7c35b0bf0b48a70b4cb4fc5d7bed9b932532728e124874355de1a0af8ec4bc88", size = 1743049, upload-time = "2026-03-31T21:57:57.341Z" }, + { url = "https://files.pythonhosted.org/packages/18/71/901db0061e0f717d226386a7f471bb59b19566f2cae5f0d93874b017271f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:df23d57718f24badef8656c49743e11a89fd6f5358fa8a7b96e728fda2abf7d3", size = 1749557, upload-time = "2026-03-31T21:57:59.626Z" }, + { url = "https://files.pythonhosted.org/packages/08/d5/41eebd16066e59cd43728fe74bce953d7402f2b4ddfdfef2c0e9f17ca274/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:02e048037a6501a5ec1f6fc9736135aec6eb8a004ce48838cb951c515f32c80b", size = 1558931, upload-time = "2026-03-31T21:58:01.972Z" }, + { url = "https://files.pythonhosted.org/packages/30/e6/4a799798bf05740e66c3a1161079bda7a3dd8e22ca392481d7a7f9af82a6/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31cebae8b26f8a615d2b546fee45d5ffb76852ae6450e2a03f42c9102260d6fe", size = 1774125, upload-time = "2026-03-31T21:58:04.007Z" }, + { url = "https://files.pythonhosted.org/packages/84/63/7749337c90f92bc2cb18f9560d67aa6258c7060d1397d21529b8004fcf6f/aiohttp-3.13.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:888e78eb5ca55a615d285c3c09a7a91b42e9dd6fc699b166ebd5dee87c9ccf14", size = 1732427, upload-time = "2026-03-31T21:58:06.337Z" }, + { url = "https://files.pythonhosted.org/packages/98/de/cf2f44ff98d307e72fb97d5f5bbae3bfcb442f0ea9790c0bf5c5c2331404/aiohttp-3.13.5-cp312-cp312-win32.whl", hash = "sha256:8bd3ec6376e68a41f9f95f5ed170e2fcf22d4eb27a1f8cb361d0508f6e0557f3", size = 433534, upload-time = "2026-03-31T21:58:08.712Z" }, + { url = "https://files.pythonhosted.org/packages/aa/ca/eadf6f9c8fa5e31d40993e3db153fb5ed0b11008ad5d9de98a95045bed84/aiohttp-3.13.5-cp312-cp312-win_amd64.whl", hash = "sha256:110e448e02c729bcebb18c60b9214a87ba33bac4a9fa5e9a5f139938b56c6cb1", size = 460446, upload-time = "2026-03-31T21:58:10.945Z" }, + { url = "https://files.pythonhosted.org/packages/78/e9/d76bf503005709e390122d34e15256b88f7008e246c4bdbe915cd4f1adce/aiohttp-3.13.5-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:a5029cc80718bbd545123cd8fe5d15025eccaaaace5d0eeec6bd556ad6163d61", size = 742930, upload-time = "2026-03-31T21:58:13.155Z" }, + { url = "https://files.pythonhosted.org/packages/57/00/4b7b70223deaebd9bb85984d01a764b0d7bd6526fcdc73cca83bcbe7243e/aiohttp-3.13.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4bb6bf5811620003614076bdc807ef3b5e38244f9d25ca5fe888eaccea2a9832", size = 496927, upload-time = "2026-03-31T21:58:15.073Z" }, + { url = "https://files.pythonhosted.org/packages/9c/f5/0fb20fb49f8efdcdce6cd8127604ad2c503e754a8f139f5e02b01626523f/aiohttp-3.13.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:a84792f8631bf5a94e52d9cc881c0b824ab42717165a5579c760b830d9392ac9", size = 497141, upload-time = "2026-03-31T21:58:17.009Z" }, + { url = "https://files.pythonhosted.org/packages/3b/86/b7c870053e36a94e8951b803cb5b909bfbc9b90ca941527f5fcafbf6b0fa/aiohttp-3.13.5-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:57653eac22c6a4c13eb22ecf4d673d64a12f266e72785ab1c8b8e5940d0e8090", size = 1732476, upload-time = "2026-03-31T21:58:18.925Z" }, + { url = "https://files.pythonhosted.org/packages/b5/e5/4e161f84f98d80c03a238671b4136e6530453d65262867d989bbe78244d0/aiohttp-3.13.5-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e5e5f7debc7a57af53fdf5c5009f9391d9f4c12867049d509bf7bb164a6e295b", size = 1706507, upload-time = "2026-03-31T21:58:21.094Z" }, + { url = "https://files.pythonhosted.org/packages/d4/56/ea11a9f01518bd5a2a2fcee869d248c4b8a0cfa0bb13401574fa31adf4d4/aiohttp-3.13.5-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c719f65bebcdf6716f10e9eff80d27567f7892d8988c06de12bbbd39307c6e3a", size = 1773465, upload-time = "2026-03-31T21:58:23.159Z" }, + { url = "https://files.pythonhosted.org/packages/eb/40/333ca27fb74b0383f17c90570c748f7582501507307350a79d9f9f3c6eb1/aiohttp-3.13.5-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d97f93fdae594d886c5a866636397e2bcab146fd7a132fd6bb9ce182224452f8", size = 1873523, upload-time = "2026-03-31T21:58:25.59Z" }, + { url = "https://files.pythonhosted.org/packages/f0/d2/e2f77eef1acb7111405433c707dc735e63f67a56e176e72e9e7a2cd3f493/aiohttp-3.13.5-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3df334e39d4c2f899a914f1dba283c1aadc311790733f705182998c6f7cae665", size = 1754113, upload-time = "2026-03-31T21:58:27.624Z" }, + { url = "https://files.pythonhosted.org/packages/fb/56/3f653d7f53c89669301ec9e42c95233e2a0c0a6dd051269e6e678db4fdb0/aiohttp-3.13.5-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fe6970addfea9e5e081401bcbadf865d2b6da045472f58af08427e108d618540", size = 1562351, upload-time = "2026-03-31T21:58:29.918Z" }, + { url = "https://files.pythonhosted.org/packages/ec/a6/9b3e91eb8ae791cce4ee736da02211c85c6f835f1bdfac0594a8a3b7018c/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:7becdf835feff2f4f335d7477f121af787e3504b48b449ff737afb35869ba7bb", size = 1693205, upload-time = "2026-03-31T21:58:32.214Z" }, + { url = "https://files.pythonhosted.org/packages/98/fc/bfb437a99a2fcebd6b6eaec609571954de2ed424f01c352f4b5504371dd3/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:676e5651705ad5d8a70aeb8eb6936c436d8ebbd56e63436cb7dd9bb36d2a9a46", size = 1730618, upload-time = "2026-03-31T21:58:34.728Z" }, + { url = "https://files.pythonhosted.org/packages/e4/b6/c8534862126191a034f68153194c389addc285a0f1347d85096d349bbc15/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:9b16c653d38eb1a611cc898c41e76859ca27f119d25b53c12875fd0474ae31a8", size = 1745185, upload-time = "2026-03-31T21:58:36.909Z" }, + { url = "https://files.pythonhosted.org/packages/0b/93/4ca8ee2ef5236e2707e0fd5fecb10ce214aee1ff4ab307af9c558bda3b37/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:999802d5fa0389f58decd24b537c54aa63c01c3219ce17d1214cbda3c2b22d2d", size = 1557311, upload-time = "2026-03-31T21:58:39.38Z" }, + { url = "https://files.pythonhosted.org/packages/57/ae/76177b15f18c5f5d094f19901d284025db28eccc5ae374d1d254181d33f4/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ec707059ee75732b1ba130ed5f9580fe10ff75180c812bc267ded039db5128c6", size = 1773147, upload-time = "2026-03-31T21:58:41.476Z" }, + { url = "https://files.pythonhosted.org/packages/01/a4/62f05a0a98d88af59d93b7fcac564e5f18f513cb7471696ac286db970d6a/aiohttp-3.13.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:2d6d44a5b48132053c2f6cd5c8cb14bc67e99a63594e336b0f2af81e94d5530c", size = 1730356, upload-time = "2026-03-31T21:58:44.049Z" }, + { url = "https://files.pythonhosted.org/packages/e4/85/fc8601f59dfa8c9523808281f2da571f8b4699685f9809a228adcc90838d/aiohttp-3.13.5-cp313-cp313-win32.whl", hash = "sha256:329f292ed14d38a6c4c435e465f48bebb47479fd676a0411936cc371643225cc", size = 432637, upload-time = "2026-03-31T21:58:46.167Z" }, + { url = "https://files.pythonhosted.org/packages/c0/1b/ac685a8882896acf0f6b31d689e3792199cfe7aba37969fa91da63a7fa27/aiohttp-3.13.5-cp313-cp313-win_amd64.whl", hash = "sha256:69f571de7500e0557801c0b51f4780482c0ec5fe2ac851af5a92cfce1af1cb83", size = 458896, upload-time = "2026-03-31T21:58:48.119Z" }, + { url = "https://files.pythonhosted.org/packages/5d/ce/46572759afc859e867a5bc8ec3487315869013f59281ce61764f76d879de/aiohttp-3.13.5-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:eb4639f32fd4a9904ab8fb45bf3383ba71137f3d9d4ba25b3b3f3109977c5b8c", size = 745721, upload-time = "2026-03-31T21:58:50.229Z" }, + { url = "https://files.pythonhosted.org/packages/13/fe/8a2efd7626dbe6049b2ef8ace18ffda8a4dfcbe1bcff3ac30c0c7575c20b/aiohttp-3.13.5-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:7e5dc4311bd5ac493886c63cbf76ab579dbe4641268e7c74e48e774c74b6f2be", size = 497663, upload-time = "2026-03-31T21:58:52.232Z" }, + { url = "https://files.pythonhosted.org/packages/9b/91/cc8cc78a111826c54743d88651e1687008133c37e5ee615fee9b57990fac/aiohttp-3.13.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:756c3c304d394977519824449600adaf2be0ccee76d206ee339c5e76b70ded25", size = 499094, upload-time = "2026-03-31T21:58:54.566Z" }, + { url = "https://files.pythonhosted.org/packages/0a/33/a8362cb15cf16a3af7e86ed11962d5cd7d59b449202dc576cdc731310bde/aiohttp-3.13.5-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ecc26751323224cf8186efcf7fbcbc30f4e1d8c7970659daf25ad995e4032a56", size = 1726701, upload-time = "2026-03-31T21:58:56.864Z" }, + { url = "https://files.pythonhosted.org/packages/45/0c/c091ac5c3a17114bd76cbf85d674650969ddf93387876cf67f754204bd77/aiohttp-3.13.5-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:10a75acfcf794edf9d8db50e5a7ec5fc818b2a8d3f591ce93bc7b1210df016d2", size = 1683360, upload-time = "2026-03-31T21:58:59.072Z" }, + { url = "https://files.pythonhosted.org/packages/23/73/bcee1c2b79bc275e964d1446c55c54441a461938e70267c86afaae6fba27/aiohttp-3.13.5-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:0f7a18f258d124cd678c5fe072fe4432a4d5232b0657fca7c1847f599233c83a", size = 1773023, upload-time = "2026-03-31T21:59:01.776Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ef/720e639df03004fee2d869f771799d8c23046dec47d5b81e396c7cda583a/aiohttp-3.13.5-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:df6104c009713d3a89621096f3e3e88cc323fd269dbd7c20afe18535094320be", size = 1853795, upload-time = "2026-03-31T21:59:04.568Z" }, + { url = "https://files.pythonhosted.org/packages/bd/c9/989f4034fb46841208de7aeeac2c6d8300745ab4f28c42f629ba77c2d916/aiohttp-3.13.5-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:241a94f7de7c0c3b616627aaad530fe2cb620084a8b144d3be7b6ecfe95bae3b", size = 1730405, upload-time = "2026-03-31T21:59:07.221Z" }, + { url = "https://files.pythonhosted.org/packages/ce/75/ee1fd286ca7dc599d824b5651dad7b3be7ff8d9a7e7b3fe9820d9180f7db/aiohttp-3.13.5-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c974fb66180e58709b6fc402846f13791240d180b74de81d23913abe48e96d94", size = 1558082, upload-time = "2026-03-31T21:59:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/c3/20/1e9e6650dfc436340116b7aa89ff8cb2bbdf0abc11dfaceaad8f74273a10/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:6e27ea05d184afac78aabbac667450c75e54e35f62238d44463131bd3f96753d", size = 1692346, upload-time = "2026-03-31T21:59:12.068Z" }, + { url = "https://files.pythonhosted.org/packages/d8/40/8ebc6658d48ea630ac7903912fe0dd4e262f0e16825aa4c833c56c9f1f56/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:a79a6d399cef33a11b6f004c67bb07741d91f2be01b8d712d52c75711b1e07c7", size = 1698891, upload-time = "2026-03-31T21:59:14.552Z" }, + { url = "https://files.pythonhosted.org/packages/d8/78/ea0ae5ec8ba7a5c10bdd6e318f1ba5e76fcde17db8275188772afc7917a4/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:c632ce9c0b534fbe25b52c974515ed674937c5b99f549a92127c85f771a78772", size = 1742113, upload-time = "2026-03-31T21:59:17.068Z" }, + { url = "https://files.pythonhosted.org/packages/8a/66/9d308ed71e3f2491be1acb8769d96c6f0c47d92099f3bc9119cada27b357/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:fceedde51fbd67ee2bcc8c0b33d0126cc8b51ef3bbde2f86662bd6d5a6f10ec5", size = 1553088, upload-time = "2026-03-31T21:59:19.541Z" }, + { url = "https://files.pythonhosted.org/packages/da/a6/6cc25ed8dfc6e00c90f5c6d126a98e2cf28957ad06fa1036bd34b6f24a2c/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f92995dfec9420bb69ae629abf422e516923ba79ba4403bc750d94fb4a6c68c1", size = 1757976, upload-time = "2026-03-31T21:59:22.311Z" }, + { url = "https://files.pythonhosted.org/packages/c1/2b/cce5b0ffe0de99c83e5e36d8f828e4161e415660a9f3e58339d07cce3006/aiohttp-3.13.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:20ae0ff08b1f2c8788d6fb85afcb798654ae6ba0b747575f8562de738078457b", size = 1712444, upload-time = "2026-03-31T21:59:24.635Z" }, + { url = "https://files.pythonhosted.org/packages/6c/cf/9e1795b4160c58d29421eafd1a69c6ce351e2f7c8d3c6b7e4ca44aea1a5b/aiohttp-3.13.5-cp314-cp314-win32.whl", hash = "sha256:b20df693de16f42b2472a9c485e1c948ee55524786a0a34345511afdd22246f3", size = 438128, upload-time = "2026-03-31T21:59:27.291Z" }, + { url = "https://files.pythonhosted.org/packages/22/4d/eaedff67fc805aeba4ba746aec891b4b24cebb1a7d078084b6300f79d063/aiohttp-3.13.5-cp314-cp314-win_amd64.whl", hash = "sha256:f85c6f327bf0b8c29da7d93b1cabb6363fb5e4e160a32fa241ed2dce21b73162", size = 464029, upload-time = "2026-03-31T21:59:29.429Z" }, + { url = "https://files.pythonhosted.org/packages/79/11/c27d9332ee20d68dd164dc12a6ecdef2e2e35ecc97ed6cf0d2442844624b/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:1efb06900858bb618ff5cee184ae2de5828896c448403d51fb633f09e109be0a", size = 778758, upload-time = "2026-03-31T21:59:31.547Z" }, + { url = "https://files.pythonhosted.org/packages/04/fb/377aead2e0a3ba5f09b7624f702a964bdf4f08b5b6728a9799830c80041e/aiohttp-3.13.5-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:fee86b7c4bd29bdaf0d53d14739b08a106fdda809ca5fe032a15f52fae5fe254", size = 512883, upload-time = "2026-03-31T21:59:34.098Z" }, + { url = "https://files.pythonhosted.org/packages/bb/a6/aa109a33671f7a5d3bd78b46da9d852797c5e665bfda7d6b373f56bff2ec/aiohttp-3.13.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:20058e23909b9e65f9da62b396b77dfa95965cbe840f8def6e572538b1d32e36", size = 516668, upload-time = "2026-03-31T21:59:36.497Z" }, + { url = "https://files.pythonhosted.org/packages/79/b3/ca078f9f2fa9563c36fb8ef89053ea2bb146d6f792c5104574d49d8acb63/aiohttp-3.13.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:8cf20a8d6868cb15a73cab329ffc07291ba8c22b1b88176026106ae39aa6df0f", size = 1883461, upload-time = "2026-03-31T21:59:38.723Z" }, + { url = "https://files.pythonhosted.org/packages/b7/e3/a7ad633ca1ca497b852233a3cce6906a56c3225fb6d9217b5e5e60b7419d/aiohttp-3.13.5-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:330f5da04c987f1d5bdb8ae189137c77139f36bd1cb23779ca1a354a4b027800", size = 1747661, upload-time = "2026-03-31T21:59:41.187Z" }, + { url = "https://files.pythonhosted.org/packages/33/b9/cd6fe579bed34a906d3d783fe60f2fa297ef55b27bb4538438ee49d4dc41/aiohttp-3.13.5-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:6f1cbf0c7926d315c3c26c2da41fd2b5d2fe01ac0e157b78caefc51a782196cf", size = 1863800, upload-time = "2026-03-31T21:59:43.84Z" }, + { url = "https://files.pythonhosted.org/packages/c0/3f/2c1e2f5144cefa889c8afd5cf431994c32f3b29da9961698ff4e3811b79a/aiohttp-3.13.5-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:53fc049ed6390d05423ba33103ded7281fe897cf97878f369a527070bd95795b", size = 1958382, upload-time = "2026-03-31T21:59:46.187Z" }, + { url = "https://files.pythonhosted.org/packages/66/1d/f31ec3f1013723b3babe3609e7f119c2c2fb6ef33da90061a705ef3e1bc8/aiohttp-3.13.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:898703aa2667e3c5ca4c54ca36cd73f58b7a38ef87a5606414799ebce4d3fd3a", size = 1803724, upload-time = "2026-03-31T21:59:48.656Z" }, + { url = "https://files.pythonhosted.org/packages/0e/b4/57712dfc6f1542f067daa81eb61da282fab3e6f1966fca25db06c4fc62d5/aiohttp-3.13.5-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0494a01ca9584eea1e5fbd6d748e61ecff218c51b576ee1999c23db7066417d8", size = 1640027, upload-time = "2026-03-31T21:59:51.284Z" }, + { url = "https://files.pythonhosted.org/packages/25/3c/734c878fb43ec083d8e31bf029daae1beafeae582d1b35da234739e82ee7/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:6cf81fe010b8c17b09495cbd15c1d35afbc8fb405c0c9cf4738e5ae3af1d65be", size = 1806644, upload-time = "2026-03-31T21:59:53.753Z" }, + { url = "https://files.pythonhosted.org/packages/20/a5/f671e5cbec1c21d044ff3078223f949748f3a7f86b14e34a365d74a5d21f/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:c564dd5f09ddc9d8f2c2d0a301cd30a79a2cc1b46dd1a73bef8f0038863d016b", size = 1791630, upload-time = "2026-03-31T21:59:56.239Z" }, + { url = "https://files.pythonhosted.org/packages/0b/63/fb8d0ad63a0b8a99be97deac8c04dacf0785721c158bdf23d679a87aa99e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:2994be9f6e51046c4f864598fd9abeb4fba6e88f0b2152422c9666dcd4aea9c6", size = 1809403, upload-time = "2026-03-31T21:59:59.103Z" }, + { url = "https://files.pythonhosted.org/packages/59/0c/bfed7f30662fcf12206481c2aac57dedee43fe1c49275e85b3a1e1742294/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:157826e2fa245d2ef46c83ea8a5faf77ca19355d278d425c29fda0beb3318037", size = 1634924, upload-time = "2026-03-31T22:00:02.116Z" }, + { url = "https://files.pythonhosted.org/packages/17/d6/fd518d668a09fd5a3319ae5e984d4d80b9a4b3df4e21c52f02251ef5a32e/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:a8aca50daa9493e9e13c0f566201a9006f080e7c50e5e90d0b06f53146a54500", size = 1836119, upload-time = "2026-03-31T22:00:04.756Z" }, + { url = "https://files.pythonhosted.org/packages/78/b7/15fb7a9d52e112a25b621c67b69c167805cb1f2ab8f1708a5c490d1b52fe/aiohttp-3.13.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:3b13560160d07e047a93f23aaa30718606493036253d5430887514715b67c9d9", size = 1772072, upload-time = "2026-03-31T22:00:07.494Z" }, + { url = "https://files.pythonhosted.org/packages/7e/df/57ba7f0c4a553fc2bd8b6321df236870ec6fd64a2a473a8a13d4f733214e/aiohttp-3.13.5-cp314-cp314t-win32.whl", hash = "sha256:9a0f4474b6ea6818b41f82172d799e4b3d29e22c2c520ce4357856fced9af2f8", size = 471819, upload-time = "2026-03-31T22:00:10.277Z" }, + { url = "https://files.pythonhosted.org/packages/62/29/2f8418269e46454a26171bfdd6a055d74febf32234e474930f2f60a17145/aiohttp-3.13.5-cp314-cp314t-win_amd64.whl", hash = "sha256:18a2f6c1182c51baa1d28d68fea51513cb2a76612f038853c0ad3c145423d3d9", size = 505441, upload-time = "2026-03-31T22:00:12.791Z" }, +] + +[[package]] +name = "aiosignal" +version = "1.4.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "frozenlist" }, + { name = "typing-extensions", marker = "python_full_version < '3.13'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/61/62/06741b579156360248d1ec624842ad0edf697050bbaf7c3e46394e106ad1/aiosignal-1.4.0.tar.gz", hash = "sha256:f47eecd9468083c2029cc99945502cb7708b082c232f9aca65da147157b251c7", size = 25007, upload-time = "2025-07-03T22:54:43.528Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fb/76/641ae371508676492379f16e2fa48f4e2c11741bd63c48be4b12a6b09cba/aiosignal-1.4.0-py3-none-any.whl", hash = "sha256:053243f8b92b990551949e63930a839ff0cf0b0ebbe0597b0f3fb19e1a0fe82e", size = 7490, upload-time = "2025-07-03T22:54:42.156Z" }, +] + +[[package]] +name = "annotated-doc" +version = "0.0.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/57/ba/046ceea27344560984e26a590f90bc7f4a75b06701f653222458922b558c/annotated_doc-0.0.4.tar.gz", hash = "sha256:fbcda96e87e9c92ad167c2e53839e57503ecfda18804ea28102353485033faa4", size = 7288, upload-time = "2025-11-10T22:07:42.062Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/d3/26bf1008eb3d2daa8ef4cacc7f3bfdc11818d111f7e2d0201bc6e3b49d45/annotated_doc-0.0.4-py3-none-any.whl", hash = "sha256:571ac1dc6991c450b25a9c2d84a3705e2ae7a53467b5d111c24fa8baabbed320", size = 5303, upload-time = "2025-11-10T22:07:40.673Z" }, ] [[package]] @@ -20,6 +151,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/da/42/e921fccf5015463e32a3cf6ee7f980a6ed0f395ceeaa45060b61d86486c2/anyio-4.13.0-py3-none-any.whl", hash = "sha256:08b310f9e24a9594186fd75b4f73f4a4152069e3853f1ed8bfbf58369f4ad708", size = 114353, upload-time = "2026-03-24T12:59:08.246Z" }, ] +[[package]] +name = "attrs" +version = "26.1.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9a/8e/82a0fe20a541c03148528be8cac2408564a6c9a0cc7e9171802bc1d26985/attrs-26.1.0.tar.gz", hash = "sha256:d03ceb89cb322a8fd706d4fb91940737b6642aa36998fe130a9bc96c985eff32", size = 952055, upload-time = "2026-03-19T14:22:25.026Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/64/b4/17d4b0b2a2dc85a6df63d1157e028ed19f90d4cd97c36717afef2bc2f395/attrs-26.1.0-py3-none-any.whl", hash = "sha256:c647aa4a12dfbad9333ca4e71fe62ddc36f4e63b2d260a37a8b83d2f043ac309", size = 67548, upload-time = "2026-03-19T14:22:23.645Z" }, +] + [[package]] name = "certifi" version = "2026.2.25" @@ -159,6 +299,30 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/db/8f/61959034484a4a7c527811f4721e75d02d653a35afb0b6054474d8185d4c/charset_normalizer-3.4.7-py3-none-any.whl", hash = "sha256:3dce51d0f5e7951f8bb4900c257dad282f49190fdbebecd4ba99bcc41fef404d", size = 61958, upload-time = "2026-04-02T09:28:37.794Z" }, ] +[[package]] +name = "click" +version = "8.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/bb/63/f9e1ea081ce35720d8b92acde70daaedace594dc93b693c869e0d5910718/click-8.3.3.tar.gz", hash = "sha256:398329ad4837b2ff7cbe1dd166a4c0f8900c3ca3a218de04466f38f6497f18a2", size = 328061, upload-time = "2026-04-22T15:11:27.506Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ae/44/c1221527f6a71a01ec6fbad7fa78f1d50dfa02217385cf0fa3eec7087d59/click-8.3.3-py3-none-any.whl", hash = "sha256:a2bf429bb3033c89fa4936ffb35d5cb471e3719e1f3c8a7c3fff0b8314305613", size = 110502, upload-time = "2026-04-22T15:11:25.044Z" }, +] + +[[package]] +name = "cligj" +version = "0.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/ea/0d/837dbd5d8430fd0f01ed72c4cfb2f548180f4c68c635df84ce87956cff32/cligj-0.7.2.tar.gz", hash = "sha256:a4bc13d623356b373c2c27c53dbd9c68cae5d526270bfa71f6c6fa69669c6b27", size = 9803, upload-time = "2021-05-28T21:23:27.935Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/73/86/43fa9f15c5b9fb6e82620428827cd3c284aa933431405d1bcf5231ae3d3e/cligj-0.7.2-py3-none-any.whl", hash = "sha256:c1ca117dbce1fe20a5809dc96f01e1c2840f6dcc939b3ddbb1111bf330ba82df", size = 7069, upload-time = "2021-05-28T21:23:26.877Z" }, +] + [[package]] name = "colorama" version = "0.4.6" @@ -168,6 +332,91 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d1/d6/3965ed04c63042e047cb6a3e6ed1a63a35087b6a609aa3a15ed8ac56c221/colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6", size = 25335, upload-time = "2022-10-25T02:36:20.889Z" }, ] +[[package]] +name = "contextily" +version = "1.7.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "geopy" }, + { name = "joblib" }, + { name = "matplotlib" }, + { name = "mercantile" }, + { name = "pillow" }, + { name = "rasterio" }, + { name = "requests" }, + { name = "xyzservices" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/cc/f3/4c7fdb1ef92c8e3db1de9741595ae1815ddc8a7e2088539e6107c83e2268/contextily-1.7.0.tar.gz", hash = "sha256:6534faa5702b89b46d0d81b4c538754f2d8b3dd8cc298454b11ccedfa67e73ac", size = 22462157, upload-time = "2025-11-24T19:54:39.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/6a/bb/6c824d0da874eef900ecf0a71e0889f0d4624560b0160fd9e333a146ee4f/contextily-1.7.0-py3-none-any.whl", hash = "sha256:38393b8e7dc38580ef1f60211a9ba1c3eb142a439c260509c201987754fa9dba", size = 16849, upload-time = "2025-11-24T19:54:37.018Z" }, +] + +[[package]] +name = "contourpy" +version = "1.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/58/01/1253e6698a07380cd31a736d248a3f2a50a7c88779a1813da27503cadc2a/contourpy-1.3.3.tar.gz", hash = "sha256:083e12155b210502d0bca491432bb04d56dc3432f95a979b429f2848c3dbe880", size = 13466174, upload-time = "2025-07-26T12:03:12.549Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/be/45/adfee365d9ea3d853550b2e735f9d66366701c65db7855cd07621732ccfc/contourpy-1.3.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:b08a32ea2f8e42cf1d4be3169a98dd4be32bafe4f22b6c4cb4ba810fa9e5d2cb", size = 293419, upload-time = "2025-07-26T12:01:21.16Z" }, + { url = "https://files.pythonhosted.org/packages/53/3e/405b59cfa13021a56bba395a6b3aca8cec012b45bf177b0eaf7a202cde2c/contourpy-1.3.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:556dba8fb6f5d8742f2923fe9457dbdd51e1049c4a43fd3986a0b14a1d815fc6", size = 273979, upload-time = "2025-07-26T12:01:22.448Z" }, + { url = "https://files.pythonhosted.org/packages/d4/1c/a12359b9b2ca3a845e8f7f9ac08bdf776114eb931392fcad91743e2ea17b/contourpy-1.3.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92d9abc807cf7d0e047b95ca5d957cf4792fcd04e920ca70d48add15c1a90ea7", size = 332653, upload-time = "2025-07-26T12:01:24.155Z" }, + { url = "https://files.pythonhosted.org/packages/63/12/897aeebfb475b7748ea67b61e045accdfcf0d971f8a588b67108ed7f5512/contourpy-1.3.3-cp312-cp312-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b2e8faa0ed68cb29af51edd8e24798bb661eac3bd9f65420c1887b6ca89987c8", size = 379536, upload-time = "2025-07-26T12:01:25.91Z" }, + { url = "https://files.pythonhosted.org/packages/43/8a/a8c584b82deb248930ce069e71576fc09bd7174bbd35183b7943fb1064fd/contourpy-1.3.3-cp312-cp312-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:626d60935cf668e70a5ce6ff184fd713e9683fb458898e4249b63be9e28286ea", size = 384397, upload-time = "2025-07-26T12:01:27.152Z" }, + { url = "https://files.pythonhosted.org/packages/cc/8f/ec6289987824b29529d0dfda0d74a07cec60e54b9c92f3c9da4c0ac732de/contourpy-1.3.3-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4d00e655fcef08aba35ec9610536bfe90267d7ab5ba944f7032549c55a146da1", size = 362601, upload-time = "2025-07-26T12:01:28.808Z" }, + { url = "https://files.pythonhosted.org/packages/05/0a/a3fe3be3ee2dceb3e615ebb4df97ae6f3828aa915d3e10549ce016302bd1/contourpy-1.3.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:451e71b5a7d597379ef572de31eeb909a87246974d960049a9848c3bc6c41bf7", size = 1331288, upload-time = "2025-07-26T12:01:31.198Z" }, + { url = "https://files.pythonhosted.org/packages/33/1d/acad9bd4e97f13f3e2b18a3977fe1b4a37ecf3d38d815333980c6c72e963/contourpy-1.3.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:459c1f020cd59fcfe6650180678a9993932d80d44ccde1fa1868977438f0b411", size = 1403386, upload-time = "2025-07-26T12:01:33.947Z" }, + { url = "https://files.pythonhosted.org/packages/cf/8f/5847f44a7fddf859704217a99a23a4f6417b10e5ab1256a179264561540e/contourpy-1.3.3-cp312-cp312-win32.whl", hash = "sha256:023b44101dfe49d7d53932be418477dba359649246075c996866106da069af69", size = 185018, upload-time = "2025-07-26T12:01:35.64Z" }, + { url = "https://files.pythonhosted.org/packages/19/e8/6026ed58a64563186a9ee3f29f41261fd1828f527dd93d33b60feca63352/contourpy-1.3.3-cp312-cp312-win_amd64.whl", hash = "sha256:8153b8bfc11e1e4d75bcb0bff1db232f9e10b274e0929de9d608027e0d34ff8b", size = 226567, upload-time = "2025-07-26T12:01:36.804Z" }, + { url = "https://files.pythonhosted.org/packages/d1/e2/f05240d2c39a1ed228d8328a78b6f44cd695f7ef47beb3e684cf93604f86/contourpy-1.3.3-cp312-cp312-win_arm64.whl", hash = "sha256:07ce5ed73ecdc4a03ffe3e1b3e3c1166db35ae7584be76f65dbbe28a7791b0cc", size = 193655, upload-time = "2025-07-26T12:01:37.999Z" }, + { url = "https://files.pythonhosted.org/packages/68/35/0167aad910bbdb9599272bd96d01a9ec6852f36b9455cf2ca67bd4cc2d23/contourpy-1.3.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:177fb367556747a686509d6fef71d221a4b198a3905fe824430e5ea0fda54eb5", size = 293257, upload-time = "2025-07-26T12:01:39.367Z" }, + { url = "https://files.pythonhosted.org/packages/96/e4/7adcd9c8362745b2210728f209bfbcf7d91ba868a2c5f40d8b58f54c509b/contourpy-1.3.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d002b6f00d73d69333dac9d0b8d5e84d9724ff9ef044fd63c5986e62b7c9e1b1", size = 274034, upload-time = "2025-07-26T12:01:40.645Z" }, + { url = "https://files.pythonhosted.org/packages/73/23/90e31ceeed1de63058a02cb04b12f2de4b40e3bef5e082a7c18d9c8ae281/contourpy-1.3.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:348ac1f5d4f1d66d3322420f01d42e43122f43616e0f194fc1c9f5d830c5b286", size = 334672, upload-time = "2025-07-26T12:01:41.942Z" }, + { url = "https://files.pythonhosted.org/packages/ed/93/b43d8acbe67392e659e1d984700e79eb67e2acb2bd7f62012b583a7f1b55/contourpy-1.3.3-cp313-cp313-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:655456777ff65c2c548b7c454af9c6f33f16c8884f11083244b5819cc214f1b5", size = 381234, upload-time = "2025-07-26T12:01:43.499Z" }, + { url = "https://files.pythonhosted.org/packages/46/3b/bec82a3ea06f66711520f75a40c8fc0b113b2a75edb36aa633eb11c4f50f/contourpy-1.3.3-cp313-cp313-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:644a6853d15b2512d67881586bd03f462c7ab755db95f16f14d7e238f2852c67", size = 385169, upload-time = "2025-07-26T12:01:45.219Z" }, + { url = "https://files.pythonhosted.org/packages/4b/32/e0f13a1c5b0f8572d0ec6ae2f6c677b7991fafd95da523159c19eff0696a/contourpy-1.3.3-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4debd64f124ca62069f313a9cb86656ff087786016d76927ae2cf37846b006c9", size = 362859, upload-time = "2025-07-26T12:01:46.519Z" }, + { url = "https://files.pythonhosted.org/packages/33/71/e2a7945b7de4e58af42d708a219f3b2f4cff7386e6b6ab0a0fa0033c49a9/contourpy-1.3.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a15459b0f4615b00bbd1e91f1b9e19b7e63aea7483d03d804186f278c0af2659", size = 1332062, upload-time = "2025-07-26T12:01:48.964Z" }, + { url = "https://files.pythonhosted.org/packages/12/fc/4e87ac754220ccc0e807284f88e943d6d43b43843614f0a8afa469801db0/contourpy-1.3.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:ca0fdcd73925568ca027e0b17ab07aad764be4706d0a925b89227e447d9737b7", size = 1403932, upload-time = "2025-07-26T12:01:51.979Z" }, + { url = "https://files.pythonhosted.org/packages/a6/2e/adc197a37443f934594112222ac1aa7dc9a98faf9c3842884df9a9d8751d/contourpy-1.3.3-cp313-cp313-win32.whl", hash = "sha256:b20c7c9a3bf701366556e1b1984ed2d0cedf999903c51311417cf5f591d8c78d", size = 185024, upload-time = "2025-07-26T12:01:53.245Z" }, + { url = "https://files.pythonhosted.org/packages/18/0b/0098c214843213759692cc638fce7de5c289200a830e5035d1791d7a2338/contourpy-1.3.3-cp313-cp313-win_amd64.whl", hash = "sha256:1cadd8b8969f060ba45ed7c1b714fe69185812ab43bd6b86a9123fe8f99c3263", size = 226578, upload-time = "2025-07-26T12:01:54.422Z" }, + { url = "https://files.pythonhosted.org/packages/8a/9a/2f6024a0c5995243cd63afdeb3651c984f0d2bc727fd98066d40e141ad73/contourpy-1.3.3-cp313-cp313-win_arm64.whl", hash = "sha256:fd914713266421b7536de2bfa8181aa8c699432b6763a0ea64195ebe28bff6a9", size = 193524, upload-time = "2025-07-26T12:01:55.73Z" }, + { url = "https://files.pythonhosted.org/packages/c0/b3/f8a1a86bd3298513f500e5b1f5fd92b69896449f6cab6a146a5d52715479/contourpy-1.3.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:88df9880d507169449d434c293467418b9f6cbe82edd19284aa0409e7fdb933d", size = 306730, upload-time = "2025-07-26T12:01:57.051Z" }, + { url = "https://files.pythonhosted.org/packages/3f/11/4780db94ae62fc0c2053909b65dc3246bd7cecfc4f8a20d957ad43aa4ad8/contourpy-1.3.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:d06bb1f751ba5d417047db62bca3c8fde202b8c11fb50742ab3ab962c81e8216", size = 287897, upload-time = "2025-07-26T12:01:58.663Z" }, + { url = "https://files.pythonhosted.org/packages/ae/15/e59f5f3ffdd6f3d4daa3e47114c53daabcb18574a26c21f03dc9e4e42ff0/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e4e6b05a45525357e382909a4c1600444e2a45b4795163d3b22669285591c1ae", size = 326751, upload-time = "2025-07-26T12:02:00.343Z" }, + { url = "https://files.pythonhosted.org/packages/0f/81/03b45cfad088e4770b1dcf72ea78d3802d04200009fb364d18a493857210/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ab3074b48c4e2cf1a960e6bbeb7f04566bf36b1861d5c9d4d8ac04b82e38ba20", size = 375486, upload-time = "2025-07-26T12:02:02.128Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ba/49923366492ffbdd4486e970d421b289a670ae8cf539c1ea9a09822b371a/contourpy-1.3.3-cp313-cp313t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6c3d53c796f8647d6deb1abe867daeb66dcc8a97e8455efa729516b997b8ed99", size = 388106, upload-time = "2025-07-26T12:02:03.615Z" }, + { url = "https://files.pythonhosted.org/packages/9f/52/5b00ea89525f8f143651f9f03a0df371d3cbd2fccd21ca9b768c7a6500c2/contourpy-1.3.3-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50ed930df7289ff2a8d7afeb9603f8289e5704755c7e5c3bbd929c90c817164b", size = 352548, upload-time = "2025-07-26T12:02:05.165Z" }, + { url = "https://files.pythonhosted.org/packages/32/1d/a209ec1a3a3452d490f6b14dd92e72280c99ae3d1e73da74f8277d4ee08f/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:4feffb6537d64b84877da813a5c30f1422ea5739566abf0bd18065ac040e120a", size = 1322297, upload-time = "2025-07-26T12:02:07.379Z" }, + { url = "https://files.pythonhosted.org/packages/bc/9e/46f0e8ebdd884ca0e8877e46a3f4e633f6c9c8c4f3f6e72be3fe075994aa/contourpy-1.3.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2b7e9480ffe2b0cd2e787e4df64270e3a0440d9db8dc823312e2c940c167df7e", size = 1391023, upload-time = "2025-07-26T12:02:10.171Z" }, + { url = "https://files.pythonhosted.org/packages/b9/70/f308384a3ae9cd2209e0849f33c913f658d3326900d0ff5d378d6a1422d2/contourpy-1.3.3-cp313-cp313t-win32.whl", hash = "sha256:283edd842a01e3dcd435b1c5116798d661378d83d36d337b8dde1d16a5fc9ba3", size = 196157, upload-time = "2025-07-26T12:02:11.488Z" }, + { url = "https://files.pythonhosted.org/packages/b2/dd/880f890a6663b84d9e34a6f88cded89d78f0091e0045a284427cb6b18521/contourpy-1.3.3-cp313-cp313t-win_amd64.whl", hash = "sha256:87acf5963fc2b34825e5b6b048f40e3635dd547f590b04d2ab317c2619ef7ae8", size = 240570, upload-time = "2025-07-26T12:02:12.754Z" }, + { url = "https://files.pythonhosted.org/packages/80/99/2adc7d8ffead633234817ef8e9a87115c8a11927a94478f6bb3d3f4d4f7d/contourpy-1.3.3-cp313-cp313t-win_arm64.whl", hash = "sha256:3c30273eb2a55024ff31ba7d052dde990d7d8e5450f4bbb6e913558b3d6c2301", size = 199713, upload-time = "2025-07-26T12:02:14.4Z" }, + { url = "https://files.pythonhosted.org/packages/72/8b/4546f3ab60f78c514ffb7d01a0bd743f90de36f0019d1be84d0a708a580a/contourpy-1.3.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:fde6c716d51c04b1c25d0b90364d0be954624a0ee9d60e23e850e8d48353d07a", size = 292189, upload-time = "2025-07-26T12:02:16.095Z" }, + { url = "https://files.pythonhosted.org/packages/fd/e1/3542a9cb596cadd76fcef413f19c79216e002623158befe6daa03dbfa88c/contourpy-1.3.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:cbedb772ed74ff5be440fa8eee9bd49f64f6e3fc09436d9c7d8f1c287b121d77", size = 273251, upload-time = "2025-07-26T12:02:17.524Z" }, + { url = "https://files.pythonhosted.org/packages/b1/71/f93e1e9471d189f79d0ce2497007731c1e6bf9ef6d1d61b911430c3db4e5/contourpy-1.3.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:22e9b1bd7a9b1d652cd77388465dc358dafcd2e217d35552424aa4f996f524f5", size = 335810, upload-time = "2025-07-26T12:02:18.9Z" }, + { url = "https://files.pythonhosted.org/packages/91/f9/e35f4c1c93f9275d4e38681a80506b5510e9327350c51f8d4a5a724d178c/contourpy-1.3.3-cp314-cp314-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a22738912262aa3e254e4f3cb079a95a67132fc5a063890e224393596902f5a4", size = 382871, upload-time = "2025-07-26T12:02:20.418Z" }, + { url = "https://files.pythonhosted.org/packages/b5/71/47b512f936f66a0a900d81c396a7e60d73419868fba959c61efed7a8ab46/contourpy-1.3.3-cp314-cp314-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:afe5a512f31ee6bd7d0dda52ec9864c984ca3d66664444f2d72e0dc4eb832e36", size = 386264, upload-time = "2025-07-26T12:02:21.916Z" }, + { url = "https://files.pythonhosted.org/packages/04/5f/9ff93450ba96b09c7c2b3f81c94de31c89f92292f1380261bd7195bea4ea/contourpy-1.3.3-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f64836de09927cba6f79dcd00fdd7d5329f3fccc633468507079c829ca4db4e3", size = 363819, upload-time = "2025-07-26T12:02:23.759Z" }, + { url = "https://files.pythonhosted.org/packages/3e/a6/0b185d4cc480ee494945cde102cb0149ae830b5fa17bf855b95f2e70ad13/contourpy-1.3.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1fd43c3be4c8e5fd6e4f2baeae35ae18176cf2e5cced681cca908addf1cdd53b", size = 1333650, upload-time = "2025-07-26T12:02:26.181Z" }, + { url = "https://files.pythonhosted.org/packages/43/d7/afdc95580ca56f30fbcd3060250f66cedbde69b4547028863abd8aa3b47e/contourpy-1.3.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6afc576f7b33cf00996e5c1102dc2a8f7cc89e39c0b55df93a0b78c1bd992b36", size = 1404833, upload-time = "2025-07-26T12:02:28.782Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e2/366af18a6d386f41132a48f033cbd2102e9b0cf6345d35ff0826cd984566/contourpy-1.3.3-cp314-cp314-win32.whl", hash = "sha256:66c8a43a4f7b8df8b71ee1840e4211a3c8d93b214b213f590e18a1beca458f7d", size = 189692, upload-time = "2025-07-26T12:02:30.128Z" }, + { url = "https://files.pythonhosted.org/packages/7d/c2/57f54b03d0f22d4044b8afb9ca0e184f8b1afd57b4f735c2fa70883dc601/contourpy-1.3.3-cp314-cp314-win_amd64.whl", hash = "sha256:cf9022ef053f2694e31d630feaacb21ea24224be1c3ad0520b13d844274614fd", size = 232424, upload-time = "2025-07-26T12:02:31.395Z" }, + { url = "https://files.pythonhosted.org/packages/18/79/a9416650df9b525737ab521aa181ccc42d56016d2123ddcb7b58e926a42c/contourpy-1.3.3-cp314-cp314-win_arm64.whl", hash = "sha256:95b181891b4c71de4bb404c6621e7e2390745f887f2a026b2d99e92c17892339", size = 198300, upload-time = "2025-07-26T12:02:32.956Z" }, + { url = "https://files.pythonhosted.org/packages/1f/42/38c159a7d0f2b7b9c04c64ab317042bb6952b713ba875c1681529a2932fe/contourpy-1.3.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:33c82d0138c0a062380332c861387650c82e4cf1747aaa6938b9b6516762e772", size = 306769, upload-time = "2025-07-26T12:02:34.2Z" }, + { url = "https://files.pythonhosted.org/packages/c3/6c/26a8205f24bca10974e77460de68d3d7c63e282e23782f1239f226fcae6f/contourpy-1.3.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ea37e7b45949df430fe649e5de8351c423430046a2af20b1c1961cae3afcda77", size = 287892, upload-time = "2025-07-26T12:02:35.807Z" }, + { url = "https://files.pythonhosted.org/packages/66/06/8a475c8ab718ebfd7925661747dbb3c3ee9c82ac834ccb3570be49d129f4/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d304906ecc71672e9c89e87c4675dc5c2645e1f4269a5063b99b0bb29f232d13", size = 326748, upload-time = "2025-07-26T12:02:37.193Z" }, + { url = "https://files.pythonhosted.org/packages/b4/a3/c5ca9f010a44c223f098fccd8b158bb1cb287378a31ac141f04730dc49be/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ca658cd1a680a5c9ea96dc61cdbae1e85c8f25849843aa799dfd3cb370ad4fbe", size = 375554, upload-time = "2025-07-26T12:02:38.894Z" }, + { url = "https://files.pythonhosted.org/packages/80/5b/68bd33ae63fac658a4145088c1e894405e07584a316738710b636c6d0333/contourpy-1.3.3-cp314-cp314t-manylinux_2_26_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ab2fd90904c503739a75b7c8c5c01160130ba67944a7b77bbf36ef8054576e7f", size = 388118, upload-time = "2025-07-26T12:02:40.642Z" }, + { url = "https://files.pythonhosted.org/packages/40/52/4c285a6435940ae25d7410a6c36bda5145839bc3f0beb20c707cda18b9d2/contourpy-1.3.3-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b7301b89040075c30e5768810bc96a8e8d78085b47d8be6e4c3f5a0b4ed478a0", size = 352555, upload-time = "2025-07-26T12:02:42.25Z" }, + { url = "https://files.pythonhosted.org/packages/24/ee/3e81e1dd174f5c7fefe50e85d0892de05ca4e26ef1c9a59c2a57e43b865a/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:2a2a8b627d5cc6b7c41a4beff6c5ad5eb848c88255fda4a8745f7e901b32d8e4", size = 1322295, upload-time = "2025-07-26T12:02:44.668Z" }, + { url = "https://files.pythonhosted.org/packages/3c/b2/6d913d4d04e14379de429057cd169e5e00f6c2af3bb13e1710bcbdb5da12/contourpy-1.3.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:fd6ec6be509c787f1caf6b247f0b1ca598bef13f4ddeaa126b7658215529ba0f", size = 1391027, upload-time = "2025-07-26T12:02:47.09Z" }, + { url = "https://files.pythonhosted.org/packages/93/8a/68a4ec5c55a2971213d29a9374913f7e9f18581945a7a31d1a39b5d2dfe5/contourpy-1.3.3-cp314-cp314t-win32.whl", hash = "sha256:e74a9a0f5e3fff48fb5a7f2fd2b9b70a3fe014a67522f79b7cca4c0c7e43c9ae", size = 202428, upload-time = "2025-07-26T12:02:48.691Z" }, + { url = "https://files.pythonhosted.org/packages/fa/96/fd9f641ffedc4fa3ace923af73b9d07e869496c9cc7a459103e6e978992f/contourpy-1.3.3-cp314-cp314t-win_amd64.whl", hash = "sha256:13b68d6a62db8eafaebb8039218921399baf6e47bf85006fd8529f2a08ef33fc", size = 250331, upload-time = "2025-07-26T12:02:50.137Z" }, + { url = "https://files.pythonhosted.org/packages/ae/8c/469afb6465b853afff216f9528ffda78a915ff880ed58813ba4faf4ba0b6/contourpy-1.3.3-cp314-cp314t-win_arm64.whl", hash = "sha256:b7448cb5a725bb1e35ce88771b86fba35ef418952474492cf7c764059933ff8b", size = 203831, upload-time = "2025-07-26T12:02:51.449Z" }, +] + [[package]] name = "cryptography" version = "46.0.6" @@ -221,6 +470,109 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/1b/82/ca4893968aeb2709aacfb57a30dec6fa2ab25b10fa9f064b8882ce33f599/cryptography-46.0.6-cp38-abi3-win_amd64.whl", hash = "sha256:79e865c642cfc5c0b3eb12af83c35c5aeff4fa5c672dc28c43721c2c9fdd2f0f", size = 3471160, upload-time = "2026-03-25T23:34:37.191Z" }, ] +[[package]] +name = "cuda-bindings" +version = "13.2.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-pathfinder", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/52/c8/b2589d68acf7e3d63e2be330b84bc25712e97ed799affbca7edd7eae25d6/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e865447abfb83d6a98ad5130ed3c70b1fc295ae3eeee39fd07b4ddb0671b6788", size = 5722404, upload-time = "2026-03-11T00:12:44.041Z" }, + { url = "https://files.pythonhosted.org/packages/1f/92/f899f7bbb5617bb65ec52a6eac1e9a1447a86b916c4194f8a5001b8cde0c/cuda_bindings-13.2.0-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46d8776a55d6d5da9dd6e9858fba2efcda2abe6743871dee47dd06eb8cb6d955", size = 6320619, upload-time = "2026-03-11T00:12:45.939Z" }, + { url = "https://files.pythonhosted.org/packages/df/93/eef988860a3ca985f82c4f3174fc0cdd94e07331ba9a92e8e064c260337f/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6629ca2df6f795b784752409bcaedbd22a7a651b74b56a165ebc0c9dcbd504d0", size = 5614610, upload-time = "2026-03-11T00:12:50.337Z" }, + { url = "https://files.pythonhosted.org/packages/18/23/6db3aba46864aee357ab2415135b3fe3da7e9f1fa0221fa2a86a5968099c/cuda_bindings-13.2.0-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7dca0da053d3b4cc4869eff49c61c03f3c5dbaa0bcd712317a358d5b8f3f385d", size = 6149914, upload-time = "2026-03-11T00:12:52.374Z" }, + { url = "https://files.pythonhosted.org/packages/c0/87/87a014f045b77c6de5c8527b0757fe644417b184e5367db977236a141602/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a6464b30f46692d6c7f65d4a0e0450d81dd29de3afc1bb515653973d01c2cd6e", size = 5685673, upload-time = "2026-03-11T00:12:56.371Z" }, + { url = "https://files.pythonhosted.org/packages/ee/5e/c0fe77a73aaefd3fff25ffaccaac69c5a63eafdf8b9a4c476626ef0ac703/cuda_bindings-13.2.0-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4af9f3e1be603fa12d5ad6cfca7844c9d230befa9792b5abdf7dd79979c3626", size = 6191386, upload-time = "2026-03-11T00:12:58.965Z" }, + { url = "https://files.pythonhosted.org/packages/5f/58/ed2c3b39c8dd5f96aa7a4abef0d47a73932c7a988e30f5fa428f00ed0da1/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:df850a1ff8ce1b3385257b08e47b70e959932f5f432d0a4e46a355962b4e4771", size = 5507469, upload-time = "2026-03-11T00:13:04.063Z" }, + { url = "https://files.pythonhosted.org/packages/1f/01/0c941b112ceeb21439b05895eace78ca1aa2eaaf695c8521a068fd9b4c00/cuda_bindings-13.2.0-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e8a16384c6494e5485f39314b0b4afb04bee48d49edb16d5d8593fd35bbd231b", size = 6059693, upload-time = "2026-03-11T00:13:06.003Z" }, +] + +[[package]] +name = "cuda-pathfinder" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/11/d0/c177e29701cf1d3008d7d2b16b5fc626592ce13bd535f8795c5f57187e0e/cuda_pathfinder-1.5.4-py3-none-any.whl", hash = "sha256:9563d3175ce1828531acf4b94e1c1c7d67208c347ca002493e2654878b26f4b7", size = 51657, upload-time = "2026-04-27T22:42:07.712Z" }, +] + +[[package]] +name = "cuda-toolkit" +version = "13.0.2" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/57/b2/453099f5f3b698d7d0eab38916aac44c7f76229f451709e2eb9db6615dcd/cuda_toolkit-13.0.2-py2.py3-none-any.whl", hash = "sha256:b198824cf2f54003f50d64ada3a0f184b42ca0846c1c94192fa269ecd97a66eb", size = 2364, upload-time = "2025-12-19T23:24:07.328Z" }, +] + +[package.optional-dependencies] +cublas = [ + { name = "nvidia-cublas", marker = "sys_platform == 'linux'" }, +] +cudart = [ + { name = "nvidia-cuda-runtime", marker = "sys_platform == 'linux'" }, +] +cufft = [ + { name = "nvidia-cufft", marker = "sys_platform == 'linux'" }, +] +cufile = [ + { name = "nvidia-cufile", marker = "sys_platform == 'linux'" }, +] +cupti = [ + { name = "nvidia-cuda-cupti", marker = "sys_platform == 'linux'" }, +] +curand = [ + { name = "nvidia-curand", marker = "sys_platform == 'linux'" }, +] +cusolver = [ + { name = "nvidia-cusolver", marker = "sys_platform == 'linux'" }, +] +cusparse = [ + { name = "nvidia-cusparse", marker = "sys_platform == 'linux'" }, +] +nvjitlink = [ + { name = "nvidia-nvjitlink", marker = "sys_platform == 'linux'" }, +] +nvrtc = [ + { name = "nvidia-cuda-nvrtc", marker = "sys_platform == 'linux'" }, +] +nvtx = [ + { name = "nvidia-nvtx", marker = "sys_platform == 'linux'" }, +] + +[[package]] +name = "cycler" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/a9/95/a3dbbb5028f35eafb79008e7522a75244477d2838f38cbb722248dabc2a8/cycler-0.12.1.tar.gz", hash = "sha256:88bb128f02ba341da8ef447245a9e138fae777f6a23943da4540077d3601eb1c", size = 7615, upload-time = "2023-10-07T05:32:18.335Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e7/05/c19819d5e3d95294a6f5947fb9b9629efb316b96de511b418c53d245aae6/cycler-0.12.1-py3-none-any.whl", hash = "sha256:85cef7cff222d8644161529808465972e51340599459b8ac3ccbac5a854e0d30", size = 8321, upload-time = "2023-10-07T05:32:16.783Z" }, +] + +[[package]] +name = "datasets" +version = "4.8.5" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, + { name = "filelock" }, + { name = "fsspec", extra = ["http"] }, + { name = "httpx" }, + { name = "huggingface-hub" }, + { name = "multiprocess" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyarrow" }, + { name = "pyyaml" }, + { name = "requests" }, + { name = "tqdm" }, + { name = "xxhash" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/66/34/14cd8e76f907f7d4dca2334cfeec9f81d30fd15c25a015f99aaea694eaed/datasets-4.8.5.tar.gz", hash = "sha256:0f0c1c3d56ffff2c93b2f4c63c95bac94f3d7e8621aea2a2a576275233bba772", size = 605649, upload-time = "2026-04-27T15:43:57.384Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/65/99/00f3196036501b53032c4b1ab8337a0b978dee832ed276dae3815df4e8b5/datasets-4.8.5-py3-none-any.whl", hash = "sha256:5079900781719c0e063a8efdd2cd95a31ad0c63209178669cd23cf1b926149ff", size = 528973, upload-time = "2026-04-27T15:43:53.702Z" }, +] + [[package]] name = "defusedxml" version = "0.7.1" @@ -230,6 +582,44 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/07/6c/aa3f2f849e01cb6a001cd8554a88d4c77c5c1a31c95bdf1cf9301e6d9ef4/defusedxml-0.7.1-py2.py3-none-any.whl", hash = "sha256:a352e7e428770286cc899e2542b6cdaedb2b4953ff269a210103ec58f6198a61", size = 25604, upload-time = "2021-03-08T10:59:24.45Z" }, ] +[[package]] +name = "dill" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/81/e1/56027a71e31b02ddc53c7d65b01e68edf64dea2932122fe7746a516f75d5/dill-0.4.1.tar.gz", hash = "sha256:423092df4182177d4d8ba8290c8a5b640c66ab35ec7da59ccfa00f6fa3eea5fa", size = 187315, upload-time = "2026-01-19T02:36:56.85Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/77/dc8c558f7593132cf8fefec57c4f60c83b16941c574ac5f619abb3ae7933/dill-0.4.1-py3-none-any.whl", hash = "sha256:1e1ce33e978ae97fcfcff5638477032b801c46c7c65cf717f95fbc2248f79a9d", size = 120019, upload-time = "2026-01-19T02:36:55.663Z" }, +] + +[[package]] +name = "duckdb" +version = "1.5.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0c/66/744b4931b799a42f8cb9bc7a6f169e7b8e51195b62b246db407fd90bf15f/duckdb-1.5.2.tar.gz", hash = "sha256:638da0d5102b6cb6f7d47f83d0600708ac1d3cb46c5e9aaabc845f9ba4d69246", size = 18017166, upload-time = "2026-04-13T11:30:09.065Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/41/de/ebe66bbe78125fc610f4fd415447a65349d94245950f3b3dfb31d028af02/duckdb-1.5.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e6495b00cad16888384119842797c49316a96ae1cb132bb03856d980d95afee1", size = 30064950, upload-time = "2026-04-13T11:29:11.468Z" }, + { url = "https://files.pythonhosted.org/packages/2d/8a/3e25b5d03bcf1fb99d189912f8ce92b1db4f9c8778e1b1f55745973a855a/duckdb-1.5.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:d72b8856b1839d35648f38301b058f6232f4d36b463fe4dc8f4d3fdff2df1a2e", size = 15969113, upload-time = "2026-04-13T11:29:14.139Z" }, + { url = "https://files.pythonhosted.org/packages/19/bb/58001f0815002b1a93431bf907f77854085c7d049b83d521814a07b9db0b/duckdb-1.5.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:2a1de4f4d454b8c97aec546c82003fc834d3422ce4bc6a19902f3462ef293bed", size = 14224774, upload-time = "2026-04-13T11:29:16.758Z" }, + { url = "https://files.pythonhosted.org/packages/d3/2f/a7f0de9509d1cef35608aeb382919041cdd70f58c173865c3da6a0d87979/duckdb-1.5.2-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ce0b8141a10d37ecef729c45bc41d334854013f4389f1488bd6035c5579aaac1", size = 19313510, upload-time = "2026-04-13T11:29:19.574Z" }, + { url = "https://files.pythonhosted.org/packages/26/78/eb1e064ea8b9df3b87b167bfd7a407b2f615a4291e06cba756727adfa06c/duckdb-1.5.2-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c99ef73a277c8921bc0a1f16dee38d924484251d9cfd20951748c20fcd5ed855", size = 21429692, upload-time = "2026-04-13T11:29:22.575Z" }, + { url = "https://files.pythonhosted.org/packages/5b/12/05b0c47d14839925c5e35b79081d918ca82e3f236bb724a6f58409dd5291/duckdb-1.5.2-cp312-cp312-win_amd64.whl", hash = "sha256:8d599758b4e48bf12e18c9b960cf491d219f0c4972d19a45489c05cc5ab36f83", size = 13107594, upload-time = "2026-04-13T11:29:25.43Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2c/80558a82b236e044330e84a154b96aacddb343316b479f3d49be03ea11cb/duckdb-1.5.2-cp312-cp312-win_arm64.whl", hash = "sha256:fc85a5dbcbe6eccac1113c72370d1d3aacfdd49198d63950bdf7d8638a307f00", size = 13927537, upload-time = "2026-04-13T11:29:27.842Z" }, + { url = "https://files.pythonhosted.org/packages/98/f2/e3d742808f138d374be4bb516fade3d1f33749b813650810ab7885cdc363/duckdb-1.5.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:4420b3f47027a7849d0e1815532007f377fa95ee5810b47ea717d35525c12f79", size = 30064879, upload-time = "2026-04-13T11:29:30.763Z" }, + { url = "https://files.pythonhosted.org/packages/72/0d/f3dc1cf97e1267ca15e4307d456f96ce583961f0703fd75e62b2ad8d64fa/duckdb-1.5.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:bb42e6ed543902e14eae647850da24103a89f0bc2587dec5601b1c1f213bd2ed", size = 15969327, upload-time = "2026-04-13T11:29:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e0/d5418def53ae4e05a63075705ff44ed5af5a1a5932627eb2b600c5df1c93/duckdb-1.5.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:98c0535cd6d901f61a5ea3c2e26a1fd28482953d794deb183daf568e3aa5dda6", size = 14225107, upload-time = "2026-04-13T11:29:35.882Z" }, + { url = "https://files.pythonhosted.org/packages/16/a7/15aaa59dbecc35e9711980fcdbf525b32a52470b32d18ef678193a146213/duckdb-1.5.2-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:486c862bf7f163c0110b6d85b3e5c031d224a671cca468f12ebb1d3a348f6b39", size = 19313433, upload-time = "2026-04-13T11:29:38.367Z" }, + { url = "https://files.pythonhosted.org/packages/bd/21/d903cc63a5140c822b7b62b373a87dc557e60c29b321dfb435061c5e67cf/duckdb-1.5.2-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:70631c847ca918ee710ec874241b00cf9d2e5be90762cbb2a0389f17823c08f7", size = 21429837, upload-time = "2026-04-13T11:29:41.135Z" }, + { url = "https://files.pythonhosted.org/packages/e3/0a/b770d1f60c70597302130d6247f418549b7094251a02348fbaf1c7e147ae/duckdb-1.5.2-cp313-cp313-win_amd64.whl", hash = "sha256:52a21823f3fbb52f0f0e5425e20b07391ad882464b955879499b5ff0b45a376b", size = 13107699, upload-time = "2026-04-13T11:29:43.905Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/e200fe431d700962d1a908d2ce89f53ccee1cc8db260174ae663ba09686b/duckdb-1.5.2-cp313-cp313-win_arm64.whl", hash = "sha256:411ad438bd4140f189a10e7f515781335962c5d18bd07837dc6d202e3985253d", size = 13927646, upload-time = "2026-04-13T11:29:46.598Z" }, + { url = "https://files.pythonhosted.org/packages/83/a1/f6286c67726cc1ea60a6e3c0d9fbc66527dde24ae089a51bbe298b13ca78/duckdb-1.5.2-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:6b0fe75c148000f060aa1a27b293cacc0ea08cc1cad724fbf2143d56070a3785", size = 30078598, upload-time = "2026-04-13T11:29:49.828Z" }, + { url = "https://files.pythonhosted.org/packages/de/6a/59febb02f21a4a5c6b0b0099ef7c965fdd5e61e4904cf813809bb792e35f/duckdb-1.5.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:35579b8e3a064b5eaf15b0eafc558056a13f79a0a62e34cc4baf57119daecfec", size = 15975120, upload-time = "2026-04-13T11:29:52.631Z" }, + { url = "https://files.pythonhosted.org/packages/09/70/ce750854d37bb5a45cccbb2c3cb04df4af56aea8fc30a2499bb643b4a9c0/duckdb-1.5.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:ea58ff5b0880593a280cf5511734b17711b32ee1f58b47d726e8600848358160", size = 14227762, upload-time = "2026-04-13T11:29:55.564Z" }, + { url = "https://files.pythonhosted.org/packages/28/dc/ad45ac3c0b6c4687dc649e8f6cf01af1c8b0443932a39b2abb4ebcb3babd/duckdb-1.5.2-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef461bca07313412dc09961c4a4757a851f56b95ac01c58fac6007632b7b94f2", size = 19315668, upload-time = "2026-04-13T11:29:58.427Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b1/1464f468d2e5813f5808de95df9d3113a645a5bfa2ffcaecbc542ddae272/duckdb-1.5.2-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:be37680ddb380015cb37318e378c53511c45c4f0d8fac5599d22b7d092b9217a", size = 21434056, upload-time = "2026-04-13T11:30:01.238Z" }, + { url = "https://files.pythonhosted.org/packages/ce/32/6673607e024722473fa7aafdd29c0e3dd231dd528f6cd8b5797fbeeb229d/duckdb-1.5.2-cp314-cp314-win_amd64.whl", hash = "sha256:0b291786014df1133f8f18b9df4d004484613146e858d71a21791e0fcca16cf4", size = 13633667, upload-time = "2026-04-13T11:30:04.05Z" }, + { url = "https://files.pythonhosted.org/packages/7a/e3/9d34173ec068631faea3ea6e73050700729363e7e33306a9a3218e5cdc61/duckdb-1.5.2-cp314-cp314-win_arm64.whl", hash = "sha256:c9f3e0b71b8a50fccfb42794899285d9d318ce2503782b9dd54868e5ecd0ad31", size = 14402513, upload-time = "2026-04-13T11:30:06.609Z" }, +] + [[package]] name = "et-xmlfile" version = "2.0.0" @@ -239,25 +629,57 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059, upload-time = "2024-10-25T17:25:39.051Z" }, ] +[[package]] +name = "filelock" +version = "3.29.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b5/fe/997687a931ab51049acce6fa1f23e8f01216374ea81374ddee763c493db5/filelock-3.29.0.tar.gz", hash = "sha256:69974355e960702e789734cb4871f884ea6fe50bd8404051a3530bc07809cf90", size = 57571, upload-time = "2026-04-19T15:39:10.068Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/81/47/dd9a212ef6e343a6857485ffe25bba537304f1913bdbed446a23f7f592e1/filelock-3.29.0-py3-none-any.whl", hash = "sha256:96f5f6344709aa1572bbf631c640e4ebeeb519e08da902c39a001882f30ac258", size = 39812, upload-time = "2026-04-19T15:39:08.752Z" }, +] + +[[package]] +name = "flatbuffers" +version = "25.12.19" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e8/2d/d2a548598be01649e2d46231d151a6c56d10b964d94043a335ae56ea2d92/flatbuffers-25.12.19-py2.py3-none-any.whl", hash = "sha256:7634f50c427838bb021c2d66a3d1168e9d199b0607e6329399f04846d42e20b4", size = 26661, upload-time = "2025-12-19T23:16:13.622Z" }, +] + [[package]] name = "fn-registry-python" version = "0.1.0" source = { virtual = "." } dependencies = [ + { name = "contextily" }, { name = "cryptography" }, + { name = "duckdb" }, { name = "fpdf2" }, + { name = "geopandas" }, { name = "google-cloud-bigquery" }, { name = "google-cloud-bigquery-datatransfer" }, { name = "google-cloud-bigquery-storage" }, { name = "google-cloud-storage" }, { name = "httpx" }, + { name = "matplotlib" }, { name = "openpyxl" }, { name = "pypdf" }, + { name = "pyproj" }, { name = "python-docx" }, { name = "pyyaml" }, + { name = "rapidfuzz" }, + { name = "reportlab" }, + { name = "seaborn" }, + { name = "shapely" }, { name = "xlrd" }, ] +[package.optional-dependencies] +nlp = [ + { name = "gliner" }, + { name = "glirel" }, +] + [package.dev-dependencies] dev = [ { name = "pytest" }, @@ -265,19 +687,31 @@ dev = [ [package.metadata] requires-dist = [ + { name = "contextily", specifier = ">=1.7.0" }, { name = "cryptography", specifier = ">=46.0.6" }, + { name = "duckdb", specifier = ">=1.5.2" }, { name = "fpdf2", specifier = ">=2.8.7" }, + { name = "geopandas", specifier = ">=1.1.3" }, + { name = "gliner", marker = "extra == 'nlp'", specifier = ">=0.2.13" }, + { name = "glirel", marker = "extra == 'nlp'", specifier = ">=1.0.0" }, { name = "google-cloud-bigquery", specifier = ">=3.25" }, { name = "google-cloud-bigquery-datatransfer", specifier = ">=3.22.0" }, { name = "google-cloud-bigquery-storage", specifier = ">=2.27" }, { name = "google-cloud-storage", specifier = ">=3.10.1" }, { name = "httpx" }, + { name = "matplotlib", specifier = ">=3.10.9" }, { name = "openpyxl", specifier = ">=3.1.5" }, { name = "pypdf", specifier = ">=6.10.0" }, + { name = "pyproj", specifier = ">=3.7.2" }, { name = "python-docx", specifier = ">=1.2.0" }, { name = "pyyaml", specifier = ">=6.0.3" }, + { name = "rapidfuzz", specifier = ">=3.14.5" }, + { name = "reportlab", specifier = ">=4.5.0" }, + { name = "seaborn", specifier = ">=0.13.2" }, + { name = "shapely", specifier = ">=2.1.2" }, { name = "xlrd", specifier = ">=2.0.2" }, ] +provides-extras = ["nlp"] [package.metadata.requires-dev] dev = [{ name = "pytest", specifier = ">=9.0.2" }] @@ -337,6 +771,181 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/66/0a/cf50ecffa1e3747ed9380a3adfc829259f1f86b3fdbd9e505af789003141/fpdf2-2.8.7-py3-none-any.whl", hash = "sha256:d391fc508a3ce02fc43a577c830cda4fe6f37646f2d143d489839940932fbc19", size = 327056, upload-time = "2026-02-28T05:39:14.619Z" }, ] +[[package]] +name = "frozenlist" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2d/f5/c831fac6cc817d26fd54c7eaccd04ef7e0288806943f7cc5bbf69f3ac1f0/frozenlist-1.8.0.tar.gz", hash = "sha256:3ede829ed8d842f6cd48fc7081d7a41001a56f1f38603f9d49bf3020d59a31ad", size = 45875, upload-time = "2025-10-06T05:38:17.865Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/69/29/948b9aa87e75820a38650af445d2ef2b6b8a6fab1a23b6bb9e4ef0be2d59/frozenlist-1.8.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:78f7b9e5d6f2fdb88cdde9440dc147259b62b9d3b019924def9f6478be254ac1", size = 87782, upload-time = "2025-10-06T05:36:06.649Z" }, + { url = "https://files.pythonhosted.org/packages/64/80/4f6e318ee2a7c0750ed724fa33a4bdf1eacdc5a39a7a24e818a773cd91af/frozenlist-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:229bf37d2e4acdaf808fd3f06e854a4a7a3661e871b10dc1f8f1896a3b05f18b", size = 50594, upload-time = "2025-10-06T05:36:07.69Z" }, + { url = "https://files.pythonhosted.org/packages/2b/94/5c8a2b50a496b11dd519f4a24cb5496cf125681dd99e94c604ccdea9419a/frozenlist-1.8.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f833670942247a14eafbb675458b4e61c82e002a148f49e68257b79296e865c4", size = 50448, upload-time = "2025-10-06T05:36:08.78Z" }, + { url = "https://files.pythonhosted.org/packages/6a/bd/d91c5e39f490a49df14320f4e8c80161cfcce09f1e2cde1edd16a551abb3/frozenlist-1.8.0-cp312-cp312-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:494a5952b1c597ba44e0e78113a7266e656b9794eec897b19ead706bd7074383", size = 242411, upload-time = "2025-10-06T05:36:09.801Z" }, + { url = "https://files.pythonhosted.org/packages/8f/83/f61505a05109ef3293dfb1ff594d13d64a2324ac3482be2cedc2be818256/frozenlist-1.8.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96f423a119f4777a4a056b66ce11527366a8bb92f54e541ade21f2374433f6d4", size = 243014, upload-time = "2025-10-06T05:36:11.394Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cb/cb6c7b0f7d4023ddda30cf56b8b17494eb3a79e3fda666bf735f63118b35/frozenlist-1.8.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3462dd9475af2025c31cc61be6652dfa25cbfb56cbbf52f4ccfe029f38decaf8", size = 234909, upload-time = "2025-10-06T05:36:12.598Z" }, + { url = "https://files.pythonhosted.org/packages/31/c5/cd7a1f3b8b34af009fb17d4123c5a778b44ae2804e3ad6b86204255f9ec5/frozenlist-1.8.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c4c800524c9cd9bac5166cd6f55285957fcfc907db323e193f2afcd4d9abd69b", size = 250049, upload-time = "2025-10-06T05:36:14.065Z" }, + { url = "https://files.pythonhosted.org/packages/c0/01/2f95d3b416c584a1e7f0e1d6d31998c4a795f7544069ee2e0962a4b60740/frozenlist-1.8.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:d6a5df73acd3399d893dafc71663ad22534b5aa4f94e8a2fabfe856c3c1b6a52", size = 256485, upload-time = "2025-10-06T05:36:15.39Z" }, + { url = "https://files.pythonhosted.org/packages/ce/03/024bf7720b3abaebcff6d0793d73c154237b85bdf67b7ed55e5e9596dc9a/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:405e8fe955c2280ce66428b3ca55e12b3c4e9c336fb2103a4937e891c69a4a29", size = 237619, upload-time = "2025-10-06T05:36:16.558Z" }, + { url = "https://files.pythonhosted.org/packages/69/fa/f8abdfe7d76b731f5d8bd217827cf6764d4f1d9763407e42717b4bed50a0/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:908bd3f6439f2fef9e85031b59fd4f1297af54415fb60e4254a95f75b3cab3f3", size = 250320, upload-time = "2025-10-06T05:36:17.821Z" }, + { url = "https://files.pythonhosted.org/packages/f5/3c/b051329f718b463b22613e269ad72138cc256c540f78a6de89452803a47d/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:294e487f9ec720bd8ffcebc99d575f7eff3568a08a253d1ee1a0378754b74143", size = 246820, upload-time = "2025-10-06T05:36:19.046Z" }, + { url = "https://files.pythonhosted.org/packages/0f/ae/58282e8f98e444b3f4dd42448ff36fa38bef29e40d40f330b22e7108f565/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:74c51543498289c0c43656701be6b077f4b265868fa7f8a8859c197006efb608", size = 250518, upload-time = "2025-10-06T05:36:20.763Z" }, + { url = "https://files.pythonhosted.org/packages/8f/96/007e5944694d66123183845a106547a15944fbbb7154788cbf7272789536/frozenlist-1.8.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:776f352e8329135506a1d6bf16ac3f87bc25b28e765949282dcc627af36123aa", size = 239096, upload-time = "2025-10-06T05:36:22.129Z" }, + { url = "https://files.pythonhosted.org/packages/66/bb/852b9d6db2fa40be96f29c0d1205c306288f0684df8fd26ca1951d461a56/frozenlist-1.8.0-cp312-cp312-win32.whl", hash = "sha256:433403ae80709741ce34038da08511d4a77062aa924baf411ef73d1146e74faf", size = 39985, upload-time = "2025-10-06T05:36:23.661Z" }, + { url = "https://files.pythonhosted.org/packages/b8/af/38e51a553dd66eb064cdf193841f16f077585d4d28394c2fa6235cb41765/frozenlist-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:34187385b08f866104f0c0617404c8eb08165ab1272e884abc89c112e9c00746", size = 44591, upload-time = "2025-10-06T05:36:24.958Z" }, + { url = "https://files.pythonhosted.org/packages/a7/06/1dc65480ab147339fecc70797e9c2f69d9cea9cf38934ce08df070fdb9cb/frozenlist-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:fe3c58d2f5db5fbd18c2987cba06d51b0529f52bc3a6cdc33d3f4eab725104bd", size = 40102, upload-time = "2025-10-06T05:36:26.333Z" }, + { url = "https://files.pythonhosted.org/packages/2d/40/0832c31a37d60f60ed79e9dfb5a92e1e2af4f40a16a29abcc7992af9edff/frozenlist-1.8.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:8d92f1a84bb12d9e56f818b3a746f3efba93c1b63c8387a73dde655e1e42282a", size = 85717, upload-time = "2025-10-06T05:36:27.341Z" }, + { url = "https://files.pythonhosted.org/packages/30/ba/b0b3de23f40bc55a7057bd38434e25c34fa48e17f20ee273bbde5e0650f3/frozenlist-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:96153e77a591c8adc2ee805756c61f59fef4cf4073a9275ee86fe8cba41241f7", size = 49651, upload-time = "2025-10-06T05:36:28.855Z" }, + { url = "https://files.pythonhosted.org/packages/0c/ab/6e5080ee374f875296c4243c381bbdef97a9ac39c6e3ce1d5f7d42cb78d6/frozenlist-1.8.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f21f00a91358803399890ab167098c131ec2ddd5f8f5fd5fe9c9f2c6fcd91e40", size = 49417, upload-time = "2025-10-06T05:36:29.877Z" }, + { url = "https://files.pythonhosted.org/packages/d5/4e/e4691508f9477ce67da2015d8c00acd751e6287739123113a9fca6f1604e/frozenlist-1.8.0-cp313-cp313-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:fb30f9626572a76dfe4293c7194a09fb1fe93ba94c7d4f720dfae3b646b45027", size = 234391, upload-time = "2025-10-06T05:36:31.301Z" }, + { url = "https://files.pythonhosted.org/packages/40/76/c202df58e3acdf12969a7895fd6f3bc016c642e6726aa63bd3025e0fc71c/frozenlist-1.8.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eaa352d7047a31d87dafcacbabe89df0aa506abb5b1b85a2fb91bc3faa02d822", size = 233048, upload-time = "2025-10-06T05:36:32.531Z" }, + { url = "https://files.pythonhosted.org/packages/f9/c0/8746afb90f17b73ca5979c7a3958116e105ff796e718575175319b5bb4ce/frozenlist-1.8.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:03ae967b4e297f58f8c774c7eabcce57fe3c2434817d4385c50661845a058121", size = 226549, upload-time = "2025-10-06T05:36:33.706Z" }, + { url = "https://files.pythonhosted.org/packages/7e/eb/4c7eefc718ff72f9b6c4893291abaae5fbc0c82226a32dcd8ef4f7a5dbef/frozenlist-1.8.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f6292f1de555ffcc675941d65fffffb0a5bcd992905015f85d0592201793e0e5", size = 239833, upload-time = "2025-10-06T05:36:34.947Z" }, + { url = "https://files.pythonhosted.org/packages/c2/4e/e5c02187cf704224f8b21bee886f3d713ca379535f16893233b9d672ea71/frozenlist-1.8.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:29548f9b5b5e3460ce7378144c3010363d8035cea44bc0bf02d57f5a685e084e", size = 245363, upload-time = "2025-10-06T05:36:36.534Z" }, + { url = "https://files.pythonhosted.org/packages/1f/96/cb85ec608464472e82ad37a17f844889c36100eed57bea094518bf270692/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ec3cc8c5d4084591b4237c0a272cc4f50a5b03396a47d9caaf76f5d7b38a4f11", size = 229314, upload-time = "2025-10-06T05:36:38.582Z" }, + { url = "https://files.pythonhosted.org/packages/5d/6f/4ae69c550e4cee66b57887daeebe006fe985917c01d0fff9caab9883f6d0/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:517279f58009d0b1f2e7c1b130b377a349405da3f7621ed6bfae50b10adf20c1", size = 243365, upload-time = "2025-10-06T05:36:40.152Z" }, + { url = "https://files.pythonhosted.org/packages/7a/58/afd56de246cf11780a40a2c28dc7cbabbf06337cc8ddb1c780a2d97e88d8/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:db1e72ede2d0d7ccb213f218df6a078a9c09a7de257c2fe8fcef16d5925230b1", size = 237763, upload-time = "2025-10-06T05:36:41.355Z" }, + { url = "https://files.pythonhosted.org/packages/cb/36/cdfaf6ed42e2644740d4a10452d8e97fa1c062e2a8006e4b09f1b5fd7d63/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:b4dec9482a65c54a5044486847b8a66bf10c9cb4926d42927ec4e8fd5db7fed8", size = 240110, upload-time = "2025-10-06T05:36:42.716Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/9ea226fbefad669f11b52e864c55f0bd57d3c8d7eb07e9f2e9a0b39502e1/frozenlist-1.8.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:21900c48ae04d13d416f0e1e0c4d81f7931f73a9dfa0b7a8746fb2fe7dd970ed", size = 233717, upload-time = "2025-10-06T05:36:44.251Z" }, + { url = "https://files.pythonhosted.org/packages/1e/0b/1b5531611e83ba7d13ccc9988967ea1b51186af64c42b7a7af465dcc9568/frozenlist-1.8.0-cp313-cp313-win32.whl", hash = "sha256:8b7b94a067d1c504ee0b16def57ad5738701e4ba10cec90529f13fa03c833496", size = 39628, upload-time = "2025-10-06T05:36:45.423Z" }, + { url = "https://files.pythonhosted.org/packages/d8/cf/174c91dbc9cc49bc7b7aab74d8b734e974d1faa8f191c74af9b7e80848e6/frozenlist-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:878be833caa6a3821caf85eb39c5ba92d28e85df26d57afb06b35b2efd937231", size = 43882, upload-time = "2025-10-06T05:36:46.796Z" }, + { url = "https://files.pythonhosted.org/packages/c1/17/502cd212cbfa96eb1388614fe39a3fc9ab87dbbe042b66f97acb57474834/frozenlist-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:44389d135b3ff43ba8cc89ff7f51f5a0bb6b63d829c8300f79a2fe4fe61bcc62", size = 39676, upload-time = "2025-10-06T05:36:47.8Z" }, + { url = "https://files.pythonhosted.org/packages/d2/5c/3bbfaa920dfab09e76946a5d2833a7cbdf7b9b4a91c714666ac4855b88b4/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:e25ac20a2ef37e91c1b39938b591457666a0fa835c7783c3a8f33ea42870db94", size = 89235, upload-time = "2025-10-06T05:36:48.78Z" }, + { url = "https://files.pythonhosted.org/packages/d2/d6/f03961ef72166cec1687e84e8925838442b615bd0b8854b54923ce5b7b8a/frozenlist-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:07cdca25a91a4386d2e76ad992916a85038a9b97561bf7a3fd12d5d9ce31870c", size = 50742, upload-time = "2025-10-06T05:36:49.837Z" }, + { url = "https://files.pythonhosted.org/packages/1e/bb/a6d12b7ba4c3337667d0e421f7181c82dda448ce4e7ad7ecd249a16fa806/frozenlist-1.8.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:4e0c11f2cc6717e0a741f84a527c52616140741cd812a50422f83dc31749fb52", size = 51725, upload-time = "2025-10-06T05:36:50.851Z" }, + { url = "https://files.pythonhosted.org/packages/bc/71/d1fed0ffe2c2ccd70b43714c6cab0f4188f09f8a67a7914a6b46ee30f274/frozenlist-1.8.0-cp313-cp313t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:b3210649ee28062ea6099cfda39e147fa1bc039583c8ee4481cb7811e2448c51", size = 284533, upload-time = "2025-10-06T05:36:51.898Z" }, + { url = "https://files.pythonhosted.org/packages/c9/1f/fb1685a7b009d89f9bf78a42d94461bc06581f6e718c39344754a5d9bada/frozenlist-1.8.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:581ef5194c48035a7de2aefc72ac6539823bb71508189e5de01d60c9dcd5fa65", size = 292506, upload-time = "2025-10-06T05:36:53.101Z" }, + { url = "https://files.pythonhosted.org/packages/e6/3b/b991fe1612703f7e0d05c0cf734c1b77aaf7c7d321df4572e8d36e7048c8/frozenlist-1.8.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:3ef2d026f16a2b1866e1d86fc4e1291e1ed8a387b2c333809419a2f8b3a77b82", size = 274161, upload-time = "2025-10-06T05:36:54.309Z" }, + { url = "https://files.pythonhosted.org/packages/ca/ec/c5c618767bcdf66e88945ec0157d7f6c4a1322f1473392319b7a2501ded7/frozenlist-1.8.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5500ef82073f599ac84d888e3a8c1f77ac831183244bfd7f11eaa0289fb30714", size = 294676, upload-time = "2025-10-06T05:36:55.566Z" }, + { url = "https://files.pythonhosted.org/packages/7c/ce/3934758637d8f8a88d11f0585d6495ef54b2044ed6ec84492a91fa3b27aa/frozenlist-1.8.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:50066c3997d0091c411a66e710f4e11752251e6d2d73d70d8d5d4c76442a199d", size = 300638, upload-time = "2025-10-06T05:36:56.758Z" }, + { url = "https://files.pythonhosted.org/packages/fc/4f/a7e4d0d467298f42de4b41cbc7ddaf19d3cfeabaf9ff97c20c6c7ee409f9/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:5c1c8e78426e59b3f8005e9b19f6ff46e5845895adbde20ece9218319eca6506", size = 283067, upload-time = "2025-10-06T05:36:57.965Z" }, + { url = "https://files.pythonhosted.org/packages/dc/48/c7b163063d55a83772b268e6d1affb960771b0e203b632cfe09522d67ea5/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:eefdba20de0d938cec6a89bd4d70f346a03108a19b9df4248d3cf0d88f1b0f51", size = 292101, upload-time = "2025-10-06T05:36:59.237Z" }, + { url = "https://files.pythonhosted.org/packages/9f/d0/2366d3c4ecdc2fd391e0afa6e11500bfba0ea772764d631bbf82f0136c9d/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:cf253e0e1c3ceb4aaff6df637ce033ff6535fb8c70a764a8f46aafd3d6ab798e", size = 289901, upload-time = "2025-10-06T05:37:00.811Z" }, + { url = "https://files.pythonhosted.org/packages/b8/94/daff920e82c1b70e3618a2ac39fbc01ae3e2ff6124e80739ce5d71c9b920/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:032efa2674356903cd0261c4317a561a6850f3ac864a63fc1583147fb05a79b0", size = 289395, upload-time = "2025-10-06T05:37:02.115Z" }, + { url = "https://files.pythonhosted.org/packages/e3/20/bba307ab4235a09fdcd3cc5508dbabd17c4634a1af4b96e0f69bfe551ebd/frozenlist-1.8.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6da155091429aeba16851ecb10a9104a108bcd32f6c1642867eadaee401c1c41", size = 283659, upload-time = "2025-10-06T05:37:03.711Z" }, + { url = "https://files.pythonhosted.org/packages/fd/00/04ca1c3a7a124b6de4f8a9a17cc2fcad138b4608e7a3fc5877804b8715d7/frozenlist-1.8.0-cp313-cp313t-win32.whl", hash = "sha256:0f96534f8bfebc1a394209427d0f8a63d343c9779cda6fc25e8e121b5fd8555b", size = 43492, upload-time = "2025-10-06T05:37:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/59/5e/c69f733a86a94ab10f68e496dc6b7e8bc078ebb415281d5698313e3af3a1/frozenlist-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:5d63a068f978fc69421fb0e6eb91a9603187527c86b7cd3f534a5b77a592b888", size = 48034, upload-time = "2025-10-06T05:37:06.343Z" }, + { url = "https://files.pythonhosted.org/packages/16/6c/be9d79775d8abe79b05fa6d23da99ad6e7763a1d080fbae7290b286093fd/frozenlist-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf0a7e10b077bf5fb9380ad3ae8ce20ef919a6ad93b4552896419ac7e1d8e042", size = 41749, upload-time = "2025-10-06T05:37:07.431Z" }, + { url = "https://files.pythonhosted.org/packages/f1/c8/85da824b7e7b9b6e7f7705b2ecaf9591ba6f79c1177f324c2735e41d36a2/frozenlist-1.8.0-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:cee686f1f4cadeb2136007ddedd0aaf928ab95216e7691c63e50a8ec066336d0", size = 86127, upload-time = "2025-10-06T05:37:08.438Z" }, + { url = "https://files.pythonhosted.org/packages/8e/e8/a1185e236ec66c20afd72399522f142c3724c785789255202d27ae992818/frozenlist-1.8.0-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:119fb2a1bd47307e899c2fac7f28e85b9a543864df47aa7ec9d3c1b4545f096f", size = 49698, upload-time = "2025-10-06T05:37:09.48Z" }, + { url = "https://files.pythonhosted.org/packages/a1/93/72b1736d68f03fda5fdf0f2180fb6caaae3894f1b854d006ac61ecc727ee/frozenlist-1.8.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:4970ece02dbc8c3a92fcc5228e36a3e933a01a999f7094ff7c23fbd2beeaa67c", size = 49749, upload-time = "2025-10-06T05:37:10.569Z" }, + { url = "https://files.pythonhosted.org/packages/a7/b2/fabede9fafd976b991e9f1b9c8c873ed86f202889b864756f240ce6dd855/frozenlist-1.8.0-cp314-cp314-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:cba69cb73723c3f329622e34bdbf5ce1f80c21c290ff04256cff1cd3c2036ed2", size = 231298, upload-time = "2025-10-06T05:37:11.993Z" }, + { url = "https://files.pythonhosted.org/packages/3a/3b/d9b1e0b0eed36e70477ffb8360c49c85c8ca8ef9700a4e6711f39a6e8b45/frozenlist-1.8.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:778a11b15673f6f1df23d9586f83c4846c471a8af693a22e066508b77d201ec8", size = 232015, upload-time = "2025-10-06T05:37:13.194Z" }, + { url = "https://files.pythonhosted.org/packages/dc/94/be719d2766c1138148564a3960fc2c06eb688da592bdc25adcf856101be7/frozenlist-1.8.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0325024fe97f94c41c08872db482cf8ac4800d80e79222c6b0b7b162d5b13686", size = 225038, upload-time = "2025-10-06T05:37:14.577Z" }, + { url = "https://files.pythonhosted.org/packages/e4/09/6712b6c5465f083f52f50cf74167b92d4ea2f50e46a9eea0523d658454ae/frozenlist-1.8.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:97260ff46b207a82a7567b581ab4190bd4dfa09f4db8a8b49d1a958f6aa4940e", size = 240130, upload-time = "2025-10-06T05:37:15.781Z" }, + { url = "https://files.pythonhosted.org/packages/f8/d4/cd065cdcf21550b54f3ce6a22e143ac9e4836ca42a0de1022da8498eac89/frozenlist-1.8.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54b2077180eb7f83dd52c40b2750d0a9f175e06a42e3213ce047219de902717a", size = 242845, upload-time = "2025-10-06T05:37:17.037Z" }, + { url = "https://files.pythonhosted.org/packages/62/c3/f57a5c8c70cd1ead3d5d5f776f89d33110b1addae0ab010ad774d9a44fb9/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:2f05983daecab868a31e1da44462873306d3cbfd76d1f0b5b69c473d21dbb128", size = 229131, upload-time = "2025-10-06T05:37:18.221Z" }, + { url = "https://files.pythonhosted.org/packages/6c/52/232476fe9cb64f0742f3fde2b7d26c1dac18b6d62071c74d4ded55e0ef94/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:33f48f51a446114bc5d251fb2954ab0164d5be02ad3382abcbfe07e2531d650f", size = 240542, upload-time = "2025-10-06T05:37:19.771Z" }, + { url = "https://files.pythonhosted.org/packages/5f/85/07bf3f5d0fb5414aee5f47d33c6f5c77bfe49aac680bfece33d4fdf6a246/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:154e55ec0655291b5dd1b8731c637ecdb50975a2ae70c606d100750a540082f7", size = 237308, upload-time = "2025-10-06T05:37:20.969Z" }, + { url = "https://files.pythonhosted.org/packages/11/99/ae3a33d5befd41ac0ca2cc7fd3aa707c9c324de2e89db0e0f45db9a64c26/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:4314debad13beb564b708b4a496020e5306c7333fa9a3ab90374169a20ffab30", size = 238210, upload-time = "2025-10-06T05:37:22.252Z" }, + { url = "https://files.pythonhosted.org/packages/b2/60/b1d2da22f4970e7a155f0adde9b1435712ece01b3cd45ba63702aea33938/frozenlist-1.8.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:073f8bf8becba60aa931eb3bc420b217bb7d5b8f4750e6f8b3be7f3da85d38b7", size = 231972, upload-time = "2025-10-06T05:37:23.5Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ab/945b2f32de889993b9c9133216c068b7fcf257d8595a0ac420ac8677cab0/frozenlist-1.8.0-cp314-cp314-win32.whl", hash = "sha256:bac9c42ba2ac65ddc115d930c78d24ab8d4f465fd3fc473cdedfccadb9429806", size = 40536, upload-time = "2025-10-06T05:37:25.581Z" }, + { url = "https://files.pythonhosted.org/packages/59/ad/9caa9b9c836d9ad6f067157a531ac48b7d36499f5036d4141ce78c230b1b/frozenlist-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:3e0761f4d1a44f1d1a47996511752cf3dcec5bbdd9cc2b4fe595caf97754b7a0", size = 44330, upload-time = "2025-10-06T05:37:26.928Z" }, + { url = "https://files.pythonhosted.org/packages/82/13/e6950121764f2676f43534c555249f57030150260aee9dcf7d64efda11dd/frozenlist-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:d1eaff1d00c7751b7c6662e9c5ba6eb2c17a2306ba5e2a37f24ddf3cc953402b", size = 40627, upload-time = "2025-10-06T05:37:28.075Z" }, + { url = "https://files.pythonhosted.org/packages/c0/c7/43200656ecc4e02d3f8bc248df68256cd9572b3f0017f0a0c4e93440ae23/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:d3bb933317c52d7ea5004a1c442eef86f426886fba134ef8cf4226ea6ee1821d", size = 89238, upload-time = "2025-10-06T05:37:29.373Z" }, + { url = "https://files.pythonhosted.org/packages/d1/29/55c5f0689b9c0fb765055629f472c0de484dcaf0acee2f7707266ae3583c/frozenlist-1.8.0-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:8009897cdef112072f93a0efdce29cd819e717fd2f649ee3016efd3cd885a7ed", size = 50738, upload-time = "2025-10-06T05:37:30.792Z" }, + { url = "https://files.pythonhosted.org/packages/ba/7d/b7282a445956506fa11da8c2db7d276adcbf2b17d8bb8407a47685263f90/frozenlist-1.8.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:2c5dcbbc55383e5883246d11fd179782a9d07a986c40f49abe89ddf865913930", size = 51739, upload-time = "2025-10-06T05:37:32.127Z" }, + { url = "https://files.pythonhosted.org/packages/62/1c/3d8622e60d0b767a5510d1d3cf21065b9db874696a51ea6d7a43180a259c/frozenlist-1.8.0-cp314-cp314t-manylinux1_x86_64.manylinux_2_28_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:39ecbc32f1390387d2aa4f5a995e465e9e2f79ba3adcac92d68e3e0afae6657c", size = 284186, upload-time = "2025-10-06T05:37:33.21Z" }, + { url = "https://files.pythonhosted.org/packages/2d/14/aa36d5f85a89679a85a1d44cd7a6657e0b1c75f61e7cad987b203d2daca8/frozenlist-1.8.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:92db2bf818d5cc8d9c1f1fc56b897662e24ea5adb36ad1f1d82875bd64e03c24", size = 292196, upload-time = "2025-10-06T05:37:36.107Z" }, + { url = "https://files.pythonhosted.org/packages/05/23/6bde59eb55abd407d34f77d39a5126fb7b4f109a3f611d3929f14b700c66/frozenlist-1.8.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:2dc43a022e555de94c3b68a4ef0b11c4f747d12c024a520c7101709a2144fb37", size = 273830, upload-time = "2025-10-06T05:37:37.663Z" }, + { url = "https://files.pythonhosted.org/packages/d2/3f/22cff331bfad7a8afa616289000ba793347fcd7bc275f3b28ecea2a27909/frozenlist-1.8.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:cb89a7f2de3602cfed448095bab3f178399646ab7c61454315089787df07733a", size = 294289, upload-time = "2025-10-06T05:37:39.261Z" }, + { url = "https://files.pythonhosted.org/packages/a4/89/5b057c799de4838b6c69aa82b79705f2027615e01be996d2486a69ca99c4/frozenlist-1.8.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:33139dc858c580ea50e7e60a1b0ea003efa1fd42e6ec7fdbad78fff65fad2fd2", size = 300318, upload-time = "2025-10-06T05:37:43.213Z" }, + { url = "https://files.pythonhosted.org/packages/30/de/2c22ab3eb2a8af6d69dc799e48455813bab3690c760de58e1bf43b36da3e/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:168c0969a329b416119507ba30b9ea13688fafffac1b7822802537569a1cb0ef", size = 282814, upload-time = "2025-10-06T05:37:45.337Z" }, + { url = "https://files.pythonhosted.org/packages/59/f7/970141a6a8dbd7f556d94977858cfb36fa9b66e0892c6dd780d2219d8cd8/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:28bd570e8e189d7f7b001966435f9dac6718324b5be2990ac496cf1ea9ddb7fe", size = 291762, upload-time = "2025-10-06T05:37:46.657Z" }, + { url = "https://files.pythonhosted.org/packages/c1/15/ca1adae83a719f82df9116d66f5bb28bb95557b3951903d39135620ef157/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b2a095d45c5d46e5e79ba1e5b9cb787f541a8dee0433836cea4b96a2c439dcd8", size = 289470, upload-time = "2025-10-06T05:37:47.946Z" }, + { url = "https://files.pythonhosted.org/packages/ac/83/dca6dc53bf657d371fbc88ddeb21b79891e747189c5de990b9dfff2ccba1/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:eab8145831a0d56ec9c4139b6c3e594c7a83c2c8be25d5bcf2d86136a532287a", size = 289042, upload-time = "2025-10-06T05:37:49.499Z" }, + { url = "https://files.pythonhosted.org/packages/96/52/abddd34ca99be142f354398700536c5bd315880ed0a213812bc491cff5e4/frozenlist-1.8.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:974b28cf63cc99dfb2188d8d222bc6843656188164848c4f679e63dae4b0708e", size = 283148, upload-time = "2025-10-06T05:37:50.745Z" }, + { url = "https://files.pythonhosted.org/packages/af/d3/76bd4ed4317e7119c2b7f57c3f6934aba26d277acc6309f873341640e21f/frozenlist-1.8.0-cp314-cp314t-win32.whl", hash = "sha256:342c97bf697ac5480c0a7ec73cd700ecfa5a8a40ac923bd035484616efecc2df", size = 44676, upload-time = "2025-10-06T05:37:52.222Z" }, + { url = "https://files.pythonhosted.org/packages/89/76/c615883b7b521ead2944bb3480398cbb07e12b7b4e4d073d3752eb721558/frozenlist-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:06be8f67f39c8b1dc671f5d83aaefd3358ae5cdcf8314552c57e7ed3e6475bdd", size = 49451, upload-time = "2025-10-06T05:37:53.425Z" }, + { url = "https://files.pythonhosted.org/packages/e0/a3/5982da14e113d07b325230f95060e2169f5311b1017ea8af2a29b374c289/frozenlist-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:102e6314ca4da683dca92e3b1355490fed5f313b768500084fbe6371fddfdb79", size = 42507, upload-time = "2025-10-06T05:37:54.513Z" }, + { url = "https://files.pythonhosted.org/packages/9a/9a/e35b4a917281c0b8419d4207f4334c8e8c5dbf4f3f5f9ada73958d937dcc/frozenlist-1.8.0-py3-none-any.whl", hash = "sha256:0c18a16eab41e82c295618a77502e17b195883241c563b00f0aa5106fc4eaa0d", size = 13409, upload-time = "2025-10-06T05:38:16.721Z" }, +] + +[[package]] +name = "fsspec" +version = "2026.2.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/51/7c/f60c259dcbf4f0c47cc4ddb8f7720d2dcdc8888c8e5ad84c73ea4531cc5b/fsspec-2026.2.0.tar.gz", hash = "sha256:6544e34b16869f5aacd5b90bdf1a71acb37792ea3ddf6125ee69a22a53fb8bff", size = 313441, upload-time = "2026-02-05T21:50:53.743Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e6/ab/fb21f4c939bb440104cc2b396d3be1d9b7a9fd3c6c2a53d98c45b3d7c954/fsspec-2026.2.0-py3-none-any.whl", hash = "sha256:98de475b5cb3bd66bedd5c4679e87b4fdfe1a3bf4d707b151b3c07e58c9a2437", size = 202505, upload-time = "2026-02-05T21:50:51.819Z" }, +] + +[package.optional-dependencies] +http = [ + { name = "aiohttp" }, +] + +[[package]] +name = "geographiclib" +version = "2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/df/78/4892343230a9d29faa1364564e525307a37e54ad776ea62c12129dbba704/geographiclib-2.1.tar.gz", hash = "sha256:6a6545e6262d0ed3522e13c515713718797e37ed8c672c31ad7b249f372ef108", size = 37004, upload-time = "2025-08-21T21:34:26Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/31/b3/802576f2ea5dcb48501bb162e4c7b7b3ca5654a42b2c968ef98a797a4c79/geographiclib-2.1-py3-none-any.whl", hash = "sha256:e2a873b9b9e7fc38721ad73d5f4e6c9ed140d428a339970f505c07056997d40b", size = 40740, upload-time = "2025-08-21T21:34:24.955Z" }, +] + +[[package]] +name = "geopandas" +version = "1.1.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "packaging" }, + { name = "pandas" }, + { name = "pyogrio" }, + { name = "pyproj" }, + { name = "shapely" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/85/ba/8e6b2091878e99e86a36a814dcaeff652ed48bdb03d53e78e15aaa63a914/geopandas-1.1.3.tar.gz", hash = "sha256:91a31989b6f566012838d21d5f8033f37dce882079ccb7cfdc40d5ccce7f284f", size = 336718, upload-time = "2026-03-09T21:49:09.545Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3c/78/6a04792ace63a93e162f1305392d500ae8ddcb620e7eb88a22fd622b35bb/geopandas-1.1.3-py3-none-any.whl", hash = "sha256:90d62a64f95eaa3be2ccc115c5f3d6e24208bb11983b390fdc0621a3eccd0230", size = 342514, upload-time = "2026-03-09T21:49:07.973Z" }, +] + +[[package]] +name = "geopy" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "geographiclib" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/fd/ef6d53875ceab72c1fad22dbed5ec1ad04eb378c2251a6a8024bad890c3b/geopy-2.4.1.tar.gz", hash = "sha256:50283d8e7ad07d89be5cb027338c6365a32044df3ae2556ad3f52f4840b3d0d1", size = 117625, upload-time = "2023-11-23T21:49:32.734Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/15/cf2a69ade4b194aa524ac75112d5caac37414b20a3a03e6865dfe0bd1539/geopy-2.4.1-py3-none-any.whl", hash = "sha256:ae8b4bc5c1131820f4d75fce9d4aaaca0c85189b3aa5d64c3dcaf5e3b7b882a7", size = 125437, upload-time = "2023-11-23T21:49:30.421Z" }, +] + +[[package]] +name = "gliner" +version = "0.2.26" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "onnxruntime" }, + { name = "sentencepiece" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/18/e199cb97147c4a9260c75e4caf51e17be6ff969b0604a029c9c62810cbe0/gliner-0.2.26.tar.gz", hash = "sha256:6783be92b4b81caa878dcc4269ba37800207c37118d8ff9be028b93bddd6813d", size = 181224, upload-time = "2026-03-19T15:07:22.707Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7c/6e/d54d3d2867e29b68a22b144f570c8204209647fccc7879cec5218d6ed5fb/gliner-0.2.26-py3-none-any.whl", hash = "sha256:b9baa47641efb90b9d069add0528ed2464d137991ff097f42b0cab37a91ba991", size = 170429, upload-time = "2026-03-19T15:07:19.914Z" }, +] + +[[package]] +name = "glirel" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "datasets" }, + { name = "huggingface-hub" }, + { name = "seqeval" }, + { name = "torch" }, + { name = "tqdm" }, + { name = "transformers" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/aa/17/366a8bcdf3631af9f11b989c3387b952cf3fd3b786df95952e826d64374a/glirel-1.2.1.tar.gz", hash = "sha256:96e0636810c6f020707a515d4f08aa36b7950c7d06030b5fe149b86d716e00e4", size = 49623, upload-time = "2025-04-11T10:10:58.396Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2f/62/fce91d0f41f493d1fb69db3df828aa6c2a76390a4a38db5db6a40b3b2150/glirel-1.2.1-py3-none-any.whl", hash = "sha256:ecbe2f955d8f2a406931022a10753e889761de7a736ec4090187830e49689aaf", size = 54298, upload-time = "2025-04-11T10:10:57.277Z" }, +] + [[package]] name = "google-api-core" version = "2.30.2" @@ -563,6 +1172,38 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515, upload-time = "2025-04-24T03:35:24.344Z" }, ] +[[package]] +name = "hf-xet" +version = "1.4.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/53/92/ec9ad04d0b5728dca387a45af7bc98fbb0d73b2118759f5f6038b61a57e8/hf_xet-1.4.3.tar.gz", hash = "sha256:8ddedb73c8c08928c793df2f3401ec26f95be7f7e516a7bee2fbb546f6676113", size = 670477, upload-time = "2026-03-31T22:40:07.874Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/72/43/724d307b34e353da0abd476e02f72f735cdd2bc86082dee1b32ea0bfee1d/hf_xet-1.4.3-cp313-cp313t-macosx_10_12_x86_64.whl", hash = "sha256:7551659ba4f1e1074e9623996f28c3873682530aee0a846b7f2f066239228144", size = 3800935, upload-time = "2026-03-31T22:39:49.618Z" }, + { url = "https://files.pythonhosted.org/packages/2b/d2/8bee5996b699262edb87dbb54118d287c0e1b2fc78af7cdc41857ba5e3c4/hf_xet-1.4.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:bee693ada985e7045997f05f081d0e12c4c08bd7626dc397f8a7c487e6c04f7f", size = 3558942, upload-time = "2026-03-31T22:39:47.938Z" }, + { url = "https://files.pythonhosted.org/packages/c3/a1/e993d09cbe251196fb60812b09a58901c468127b7259d2bf0f68bf6088eb/hf_xet-1.4.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21644b404bb0100fe3857892f752c4d09642586fd988e61501c95bbf44b393a3", size = 4207657, upload-time = "2026-03-31T22:39:39.69Z" }, + { url = "https://files.pythonhosted.org/packages/64/44/9eb6d21e5c34c63e5e399803a6932fa983cabdf47c0ecbcfe7ea97684b8c/hf_xet-1.4.3-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:987f09cfe418237812896a6736b81b1af02a3a6dcb4b4944425c4c4fca7a7cf8", size = 3986765, upload-time = "2026-03-31T22:39:37.936Z" }, + { url = "https://files.pythonhosted.org/packages/ea/7b/8ad6f16fdb82f5f7284a34b5ec48645bd575bdcd2f6f0d1644775909c486/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:60cf7fc43a99da0a853345cf86d23738c03983ee5249613a6305d3e57a5dca74", size = 4188162, upload-time = "2026-03-31T22:39:58.382Z" }, + { url = "https://files.pythonhosted.org/packages/1b/c4/39d6e136cbeea9ca5a23aad4b33024319222adbdc059ebcda5fc7d9d5ff4/hf_xet-1.4.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:2815a49a7a59f3e2edf0cf113ae88e8cb2ca2a221bf353fb60c609584f4884d4", size = 4424525, upload-time = "2026-03-31T22:40:00.225Z" }, + { url = "https://files.pythonhosted.org/packages/46/f2/adc32dae6bdbc367853118b9878139ac869419a4ae7ba07185dc31251b76/hf_xet-1.4.3-cp313-cp313t-win_amd64.whl", hash = "sha256:42ee323265f1e6a81b0e11094564fb7f7e0ec75b5105ffd91ae63f403a11931b", size = 3671610, upload-time = "2026-03-31T22:40:10.42Z" }, + { url = "https://files.pythonhosted.org/packages/e2/19/25d897dcc3f81953e0c2cde9ec186c7a0fee413eb0c9a7a9130d87d94d3a/hf_xet-1.4.3-cp313-cp313t-win_arm64.whl", hash = "sha256:27c976ba60079fb8217f485b9c5c7fcd21c90b0367753805f87cb9f3cdc4418a", size = 3528529, upload-time = "2026-03-31T22:40:09.106Z" }, + { url = "https://files.pythonhosted.org/packages/ec/36/3e8f85ca9fe09b8de2b2e10c63b3b3353d7dda88a0b3d426dffbe7b8313b/hf_xet-1.4.3-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:5251d5ece3a81815bae9abab41cf7ddb7bcb8f56411bce0827f4a3071c92fdc6", size = 3801019, upload-time = "2026-03-31T22:39:56.651Z" }, + { url = "https://files.pythonhosted.org/packages/b5/9c/defb6cb1de28bccb7bd8d95f6e60f72a3d3fa4cb3d0329c26fb9a488bfe7/hf_xet-1.4.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1feb0f3abeacee143367c326a128a2e2b60868ec12a36c225afb1d6c5a05e6d2", size = 3558746, upload-time = "2026-03-31T22:39:54.766Z" }, + { url = "https://files.pythonhosted.org/packages/c1/bd/8d001191893178ff8e826e46ad5299446e62b93cd164e17b0ffea08832ec/hf_xet-1.4.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8b301fc150290ca90b4fccd079829b84bb4786747584ae08b94b4577d82fb791", size = 4207692, upload-time = "2026-03-31T22:39:46.246Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/6790b402803250e9936435613d3a78b9aaeee7973439f0918848dde58309/hf_xet-1.4.3-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:d972fbe95ddc0d3c0fc49b31a8a69f47db35c1e3699bf316421705741aab6653", size = 3986281, upload-time = "2026-03-31T22:39:44.648Z" }, + { url = "https://files.pythonhosted.org/packages/51/56/ea62552fe53db652a9099eda600b032d75554d0e86c12a73824bfedef88b/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c5b48db1ee344a805a1b9bd2cda9b6b65fe77ed3787bd6e87ad5521141d317cd", size = 4187414, upload-time = "2026-03-31T22:40:04.951Z" }, + { url = "https://files.pythonhosted.org/packages/7d/f5/bc1456d4638061bea997e6d2db60a1a613d7b200e0755965ec312dc1ef79/hf_xet-1.4.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:22bdc1f5fb8b15bf2831440b91d1c9bbceeb7e10c81a12e8d75889996a5c9da8", size = 4424368, upload-time = "2026-03-31T22:40:06.347Z" }, + { url = "https://files.pythonhosted.org/packages/e4/76/ab597bae87e1f06d18d3ecb8ed7f0d3c9a37037fc32ce76233d369273c64/hf_xet-1.4.3-cp314-cp314t-win_amd64.whl", hash = "sha256:0392c79b7cf48418cd61478c1a925246cf10639f4cd9d94368d8ca1e8df9ea07", size = 3672280, upload-time = "2026-03-31T22:40:16.401Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/2e462d34e23a09a74d73785dbed71cc5dbad82a72eee2ad60a72a554155d/hf_xet-1.4.3-cp314-cp314t-win_arm64.whl", hash = "sha256:681c92a07796325778a79d76c67011764ecc9042a8c3579332b61b63ae512075", size = 3528945, upload-time = "2026-03-31T22:40:14.995Z" }, + { url = "https://files.pythonhosted.org/packages/ac/9f/9c23e4a447b8f83120798f9279d0297a4d1360bdbf59ef49ebec78fe2545/hf_xet-1.4.3-cp37-abi3-macosx_10_12_x86_64.whl", hash = "sha256:d0da85329eaf196e03e90b84c2d0aca53bd4573d097a75f99609e80775f98025", size = 3805048, upload-time = "2026-03-31T22:39:53.105Z" }, + { url = "https://files.pythonhosted.org/packages/0b/f8/7aacb8e5f4a7899d39c787b5984e912e6c18b11be136ef13947d7a66d265/hf_xet-1.4.3-cp37-abi3-macosx_11_0_arm64.whl", hash = "sha256:e23717ce4186b265f69afa66e6f0069fe7efbf331546f5c313d00e123dc84583", size = 3562178, upload-time = "2026-03-31T22:39:51.295Z" }, + { url = "https://files.pythonhosted.org/packages/df/9a/a24b26dc8a65f0ecc0fe5be981a19e61e7ca963b85e062c083f3a9100529/hf_xet-1.4.3-cp37-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fc360b70c815bf340ed56c7b8c63aacf11762a4b099b2fe2c9bd6d6068668c08", size = 4212320, upload-time = "2026-03-31T22:39:42.922Z" }, + { url = "https://files.pythonhosted.org/packages/53/60/46d493db155d2ee2801b71fb1b0fd67696359047fdd8caee2c914cc50c79/hf_xet-1.4.3-cp37-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:39f2d2e9654cd9b4319885733993807aab6de9dfbd34c42f0b78338d6617421f", size = 3991546, upload-time = "2026-03-31T22:39:41.335Z" }, + { url = "https://files.pythonhosted.org/packages/bc/f5/067363e1c96c6b17256910830d1b54099d06287e10f4ec6ec4e7e08371fc/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:49ad8a8cead2b56051aa84d7fce3e1335efe68df3cf6c058f22a65513885baac", size = 4193200, upload-time = "2026-03-31T22:40:01.936Z" }, + { url = "https://files.pythonhosted.org/packages/42/4b/53951592882d9c23080c7644542fda34a3813104e9e11fa1a7d82d419cb8/hf_xet-1.4.3-cp37-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:7716d62015477a70ea272d2d68cd7cad140f61c52ee452e133e139abfe2c17ba", size = 4429392, upload-time = "2026-03-31T22:40:03.492Z" }, + { url = "https://files.pythonhosted.org/packages/8a/21/75a6c175b4e79662ad8e62f46a40ce341d8d6b206b06b4320d07d55b188c/hf_xet-1.4.3-cp37-abi3-win_amd64.whl", hash = "sha256:6b591fcad34e272a5b02607485e4f2a1334aebf1bc6d16ce8eb1eb8978ac2021", size = 3677359, upload-time = "2026-03-31T22:40:13.619Z" }, + { url = "https://files.pythonhosted.org/packages/8a/7c/44314ecd0e89f8b2b51c9d9e5e7a60a9c1c82024ac471d415860557d3cd8/hf_xet-1.4.3-cp37-abi3-win_arm64.whl", hash = "sha256:7c2c7e20bcfcc946dc67187c203463f5e932e395845d098cc2a93f5b67ca0b47", size = 3533664, upload-time = "2026-03-31T22:40:12.152Z" }, +] + [[package]] name = "httpcore" version = "1.0.9" @@ -591,6 +1232,26 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517, upload-time = "2024-12-06T15:37:21.509Z" }, ] +[[package]] +name = "huggingface-hub" +version = "1.13.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "filelock" }, + { name = "fsspec" }, + { name = "hf-xet", marker = "platform_machine == 'AMD64' or platform_machine == 'aarch64' or platform_machine == 'amd64' or platform_machine == 'arm64' or platform_machine == 'x86_64'" }, + { name = "httpx" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "tqdm" }, + { name = "typer" }, + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/89/ff/ec7ed2eb43bd7ce8bb2233d109cc235c3e807ffe5e469dc09db261fac05e/huggingface_hub-1.13.0.tar.gz", hash = "sha256:f6df2dac5abe82ce2fe05873d10d5ff47bc677d616a2f521f4ee26db9415d9d0", size = 781788, upload-time = "2026-04-30T11:57:33.858Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/93/db/4b1cdae9460ae1f3ca020cd767f013430ce23eb1d9c890ae3a0609b38d26/huggingface_hub-1.13.0-py3-none-any.whl", hash = "sha256:e942cb50d6a08dd5306688b1ac05bda157fd2fcc88b63dae405f7bd0d3234005", size = 660643, upload-time = "2026-04-30T11:57:31.802Z" }, +] + [[package]] name = "idna" version = "3.11" @@ -609,6 +1270,113 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, ] +[[package]] +name = "jinja2" +version = "3.1.6" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markupsafe" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/df/bf/f7da0350254c0ed7c72f3e33cef02e048281fec7ecec5f032d4aac52226b/jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d", size = 245115, upload-time = "2025-03-05T20:05:02.478Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/62/a1/3d680cbfd5f4b8f15abc1d571870c5fc3e594bb582bc3b64ea099db13e56/jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67", size = 134899, upload-time = "2025-03-05T20:05:00.369Z" }, +] + +[[package]] +name = "joblib" +version = "1.5.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/41/f2/d34e8b3a08a9cc79a50b2208a93dce981fe615b64d5a4d4abee421d898df/joblib-1.5.3.tar.gz", hash = "sha256:8561a3269e6801106863fd0d6d84bb737be9e7631e33aaed3fb9ce5953688da3", size = 331603, upload-time = "2025-12-15T08:41:46.427Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/7b/91/984aca2ec129e2757d1e4e3c81c3fcda9d0f85b74670a094cc443d9ee949/joblib-1.5.3-py3-none-any.whl", hash = "sha256:5fc3c5039fc5ca8c0276333a188bbd59d6b7ab37fe6632daa76bc7f9ec18e713", size = 309071, upload-time = "2025-12-15T08:41:44.973Z" }, +] + +[[package]] +name = "kiwisolver" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d0/67/9c61eccb13f0bdca9307614e782fec49ffdde0f7a2314935d489fa93cd9c/kiwisolver-1.5.0.tar.gz", hash = "sha256:d4193f3d9dc3f6f79aaed0e5637f45d98850ebf01f7ca20e69457f3e8946b66a", size = 103482, upload-time = "2026-03-09T13:15:53.382Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4d/b2/818b74ebea34dabe6d0c51cb1c572e046730e64844da6ed646d5298c40ce/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:4e9750bc21b886308024f8a54ccb9a2cc38ac9fa813bf4348434e3d54f337ff9", size = 123158, upload-time = "2026-03-09T13:13:23.127Z" }, + { url = "https://files.pythonhosted.org/packages/bf/d9/405320f8077e8e1c5c4bd6adc45e1e6edf6d727b6da7f2e2533cf58bff71/kiwisolver-1.5.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:72ec46b7eba5b395e0a7b63025490d3214c11013f4aacb4f5e8d6c3041829588", size = 66388, upload-time = "2026-03-09T13:13:24.765Z" }, + { url = "https://files.pythonhosted.org/packages/99/9f/795fedf35634f746151ca8839d05681ceb6287fbed6cc1c9bf235f7887c2/kiwisolver-1.5.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ed3a984b31da7481b103f68776f7128a89ef26ed40f4dc41a2223cda7fb24819", size = 64068, upload-time = "2026-03-09T13:13:25.878Z" }, + { url = "https://files.pythonhosted.org/packages/c4/13/680c54afe3e65767bed7ec1a15571e1a2f1257128733851ade24abcefbcc/kiwisolver-1.5.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:bb5136fb5352d3f422df33f0c879a1b0c204004324150cc3b5e3c4f310c9049f", size = 1477934, upload-time = "2026-03-09T13:13:27.166Z" }, + { url = "https://files.pythonhosted.org/packages/c8/2f/cebfcdb60fd6a9b0f6b47a9337198bcbad6fbe15e68189b7011fd914911f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b2af221f268f5af85e776a73d62b0845fc8baf8ef0abfae79d29c77d0e776aaf", size = 1278537, upload-time = "2026-03-09T13:13:28.707Z" }, + { url = "https://files.pythonhosted.org/packages/f2/0d/9b782923aada3fafb1d6b84e13121954515c669b18af0c26e7d21f579855/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:b0f172dc8ffaccb8522d7c5d899de00133f2f1ca7b0a49b7da98e901de87bf2d", size = 1296685, upload-time = "2026-03-09T13:13:30.528Z" }, + { url = "https://files.pythonhosted.org/packages/27/70/83241b6634b04fe44e892688d5208332bde130f38e610c0418f9ede47ded/kiwisolver-1.5.0-cp312-cp312-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6ab8ba9152203feec73758dad83af9a0bbe05001eb4639e547207c40cfb52083", size = 1346024, upload-time = "2026-03-09T13:13:32.818Z" }, + { url = "https://files.pythonhosted.org/packages/e4/db/30ed226fb271ae1a6431fc0fe0edffb2efe23cadb01e798caeb9f2ceae8f/kiwisolver-1.5.0-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:cdee07c4d7f6d72008d3f73b9bf027f4e11550224c7c50d8df1ae4a37c1402a6", size = 987241, upload-time = "2026-03-09T13:13:34.435Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bd/c314595208e4c9587652d50959ead9e461995389664e490f4dce7ff0f782/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:7c60d3c9b06fb23bd9c6139281ccbdc384297579ae037f08ae90c69f6845c0b1", size = 2227742, upload-time = "2026-03-09T13:13:36.4Z" }, + { url = "https://files.pythonhosted.org/packages/c1/43/0499cec932d935229b5543d073c2b87c9c22846aab48881e9d8d6e742a2d/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:e315e5ec90d88e140f57696ff85b484ff68bb311e36f2c414aa4286293e6dee0", size = 2323966, upload-time = "2026-03-09T13:13:38.204Z" }, + { url = "https://files.pythonhosted.org/packages/3d/6f/79b0d760907965acfd9d61826a3d41f8f093c538f55cd2633d3f0db269f6/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1465387ac63576c3e125e5337a6892b9e99e0627d52317f3ca79e6930d889d15", size = 1977417, upload-time = "2026-03-09T13:13:39.966Z" }, + { url = "https://files.pythonhosted.org/packages/ab/31/01d0537c41cb75a551a438c3c7a80d0c60d60b81f694dac83dd436aec0d0/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:530a3fd64c87cffa844d4b6b9768774763d9caa299e9b75d8eca6a4423b31314", size = 2491238, upload-time = "2026-03-09T13:13:41.698Z" }, + { url = "https://files.pythonhosted.org/packages/e4/34/8aefdd0be9cfd00a44509251ba864f5caf2991e36772e61c408007e7f417/kiwisolver-1.5.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:1d9daea4ea6b9be74fe2f01f7fbade8d6ffab263e781274cffca0dba9be9eec9", size = 2294947, upload-time = "2026-03-09T13:13:43.343Z" }, + { url = "https://files.pythonhosted.org/packages/ad/cf/0348374369ca588f8fe9c338fae49fa4e16eeb10ffb3d012f23a54578a9e/kiwisolver-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:f18c2d9782259a6dc132fdc7a63c168cbc74b35284b6d75c673958982a378384", size = 73569, upload-time = "2026-03-09T13:13:45.792Z" }, + { url = "https://files.pythonhosted.org/packages/28/26/192b26196e2316e2bd29deef67e37cdf9870d9af8e085e521afff0fed526/kiwisolver-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:f7c7553b13f69c1b29a5bde08ddc6d9d0c8bfb84f9ed01c30db25944aeb852a7", size = 64997, upload-time = "2026-03-09T13:13:46.878Z" }, + { url = "https://files.pythonhosted.org/packages/9d/69/024d6711d5ba575aa65d5538042e99964104e97fa153a9f10bc369182bc2/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:fd40bb9cd0891c4c3cb1ddf83f8bbfa15731a248fdc8162669405451e2724b09", size = 123166, upload-time = "2026-03-09T13:13:48.032Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/adbb40df306f587054a348831220812b9b1d787aff714cfbc8556e38fccd/kiwisolver-1.5.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c0e1403fd7c26d77c1f03e096dc58a5c726503fa0db0456678b8668f76f521e3", size = 66395, upload-time = "2026-03-09T13:13:49.365Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3a/d0a972b34e1c63e2409413104216cd1caa02c5a37cb668d1687d466c1c45/kiwisolver-1.5.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:dda366d548e89a90d88a86c692377d18d8bd64b39c1fb2b92cb31370e2896bbd", size = 64065, upload-time = "2026-03-09T13:13:50.562Z" }, + { url = "https://files.pythonhosted.org/packages/2b/0a/7b98e1e119878a27ba8618ca1e18b14f992ff1eda40f47bccccf4de44121/kiwisolver-1.5.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:332b4f0145c30b5f5ad9374881133e5aa64320428a57c2c2b61e9d891a51c2f3", size = 1477903, upload-time = "2026-03-09T13:13:52.084Z" }, + { url = "https://files.pythonhosted.org/packages/18/d8/55638d89ffd27799d5cc3d8aa28e12f4ce7a64d67b285114dbedc8ea4136/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0c50b89ffd3e1a911c69a1dd3de7173c0cd10b130f56222e57898683841e4f96", size = 1278751, upload-time = "2026-03-09T13:13:54.673Z" }, + { url = "https://files.pythonhosted.org/packages/b8/97/b4c8d0d18421ecceba20ad8701358453b88e32414e6f6950b5a4bad54e65/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4db576bb8c3ef9365f8b40fe0f671644de6736ae2c27a2c62d7d8a1b4329f099", size = 1296793, upload-time = "2026-03-09T13:13:56.287Z" }, + { url = "https://files.pythonhosted.org/packages/c4/10/f862f94b6389d8957448ec9df59450b81bec4abb318805375c401a1e6892/kiwisolver-1.5.0-cp313-cp313-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0b85aad90cea8ac6797a53b5d5f2e967334fa4d1149f031c4537569972596cb8", size = 1346041, upload-time = "2026-03-09T13:13:58.269Z" }, + { url = "https://files.pythonhosted.org/packages/a3/6a/f1650af35821eaf09de398ec0bc2aefc8f211f0cda50204c9f1673741ba9/kiwisolver-1.5.0-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:d36ca54cb4c6c4686f7cbb7b817f66f5911c12ddb519450bbe86707155028f87", size = 987292, upload-time = "2026-03-09T13:13:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/de/19/d7fb82984b9238115fe629c915007be608ebd23dc8629703d917dbfaffd4/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:38f4a703656f493b0ad185211ccfca7f0386120f022066b018eb5296d8613e23", size = 2227865, upload-time = "2026-03-09T13:14:01.401Z" }, + { url = "https://files.pythonhosted.org/packages/7f/b9/46b7f386589fd222dac9e9de9c956ce5bcefe2ee73b4e79891381dda8654/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:3ac2360e93cb41be81121755c6462cff3beaa9967188c866e5fce5cf13170859", size = 2324369, upload-time = "2026-03-09T13:14:02.972Z" }, + { url = "https://files.pythonhosted.org/packages/92/8b/95e237cf3d9c642960153c769ddcbe278f182c8affb20cecc1cc983e7cc5/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:c95cab08d1965db3d84a121f1c7ce7479bdd4072c9b3dafd8fecce48a2e6b902", size = 1977989, upload-time = "2026-03-09T13:14:04.503Z" }, + { url = "https://files.pythonhosted.org/packages/1b/95/980c9df53501892784997820136c01f62bc1865e31b82b9560f980c0e649/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:fc20894c3d21194d8041a28b65622d5b86db786da6e3cfe73f0c762951a61167", size = 2491645, upload-time = "2026-03-09T13:14:06.106Z" }, + { url = "https://files.pythonhosted.org/packages/cb/32/900647fd0840abebe1561792c6b31e6a7c0e278fc3973d30572a965ca14c/kiwisolver-1.5.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:7a32f72973f0f950c1920475d5c5ea3d971b81b6f0ec53b8d0a956cc965f22e0", size = 2295237, upload-time = "2026-03-09T13:14:08.891Z" }, + { url = "https://files.pythonhosted.org/packages/be/8a/be60e3bbcf513cc5a50f4a3e88e1dcecebb79c1ad607a7222877becaa101/kiwisolver-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:0bf3acf1419fa93064a4c2189ac0b58e3be7872bf6ee6177b0d4c63dc4cea276", size = 73573, upload-time = "2026-03-09T13:14:12.327Z" }, + { url = "https://files.pythonhosted.org/packages/4d/d2/64be2e429eb4fca7f7e1c52a91b12663aeaf25de3895e5cca0f47ef2a8d0/kiwisolver-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:fa8eb9ecdb7efb0b226acec134e0d709e87a909fa4971a54c0c4f6e88635484c", size = 64998, upload-time = "2026-03-09T13:14:13.469Z" }, + { url = "https://files.pythonhosted.org/packages/b0/69/ce68dd0c85755ae2de490bf015b62f2cea5f6b14ff00a463f9d0774449ff/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:db485b3847d182b908b483b2ed133c66d88d49cacf98fd278fadafe11b4478d1", size = 125700, upload-time = "2026-03-09T13:14:14.636Z" }, + { url = "https://files.pythonhosted.org/packages/74/aa/937aac021cf9d4349990d47eb319309a51355ed1dbdc9c077cdc9224cb11/kiwisolver-1.5.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:be12f931839a3bdfe28b584db0e640a65a8bcbc24560ae3fdb025a449b3d754e", size = 67537, upload-time = "2026-03-09T13:14:15.808Z" }, + { url = "https://files.pythonhosted.org/packages/ee/20/3a87fbece2c40ad0f6f0aefa93542559159c5f99831d596050e8afae7a9f/kiwisolver-1.5.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:16b85d37c2cbb3253226d26e64663f755d88a03439a9c47df6246b35defbdfb7", size = 65514, upload-time = "2026-03-09T13:14:18.035Z" }, + { url = "https://files.pythonhosted.org/packages/f0/7f/f943879cda9007c45e1f7dba216d705c3a18d6b35830e488b6c6a4e7cdf0/kiwisolver-1.5.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4432b835675f0ea7414aab3d37d119f7226d24869b7a829caeab49ebda407b0c", size = 1584848, upload-time = "2026-03-09T13:14:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/37/f8/4d4f85cc1870c127c88d950913370dd76138482161cd07eabbc450deff01/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b0feb50971481a2cc44d94e88bdb02cdd497618252ae226b8eb1201b957e368", size = 1391542, upload-time = "2026-03-09T13:14:21.54Z" }, + { url = "https://files.pythonhosted.org/packages/04/0b/65dd2916c84d252b244bd405303220f729e7c17c9d7d33dca6feeff9ffc4/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:56fa888f10d0f367155e76ce849fa1166fc9730d13bd2d65a2aa13b6f5424489", size = 1404447, upload-time = "2026-03-09T13:14:23.205Z" }, + { url = "https://files.pythonhosted.org/packages/39/5c/2606a373247babce9b1d056c03a04b65f3cf5290a8eac5d7bdead0a17e21/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:940dda65d5e764406b9fb92761cbf462e4e63f712ab60ed98f70552e496f3bf1", size = 1455918, upload-time = "2026-03-09T13:14:24.74Z" }, + { url = "https://files.pythonhosted.org/packages/d5/d1/c6078b5756670658e9192a2ef11e939c92918833d2745f85cd14a6004bdf/kiwisolver-1.5.0-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:89fc958c702ee9a745e4700378f5d23fddbc46ff89e8fdbf5395c24d5c1452a3", size = 1072856, upload-time = "2026-03-09T13:14:26.597Z" }, + { url = "https://files.pythonhosted.org/packages/cb/c8/7def6ddf16eb2b3741d8b172bdaa9af882b03c78e9b0772975408801fa63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9027d773c4ff81487181a925945743413f6069634d0b122d0b37684ccf4f1e18", size = 2333580, upload-time = "2026-03-09T13:14:28.237Z" }, + { url = "https://files.pythonhosted.org/packages/9e/87/2ac1fce0eb1e616fcd3c35caa23e665e9b1948bb984f4764790924594128/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:5b233ea3e165e43e35dba1d2b8ecc21cf070b45b65ae17dd2747d2713d942021", size = 2423018, upload-time = "2026-03-09T13:14:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/67/13/c6700ccc6cc218716bfcda4935e4b2997039869b4ad8a94f364c5a3b8e63/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:ce9bf03dad3b46408c08649c6fbd6ca28a9fce0eb32fdfffa6775a13103b5310", size = 2062804, upload-time = "2026-03-09T13:14:32.888Z" }, + { url = "https://files.pythonhosted.org/packages/1b/bd/877056304626943ff0f1f44c08f584300c199b887cb3176cd7e34f1515f1/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:fc4d3f1fb9ca0ae9f97b095963bc6326f1dbfd3779d6679a1e016b9baaa153d3", size = 2597482, upload-time = "2026-03-09T13:14:34.971Z" }, + { url = "https://files.pythonhosted.org/packages/75/19/c60626c47bf0f8ac5dcf72c6c98e266d714f2fbbfd50cf6dab5ede3aaa50/kiwisolver-1.5.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f443b4825c50a51ee68585522ab4a1d1257fac65896f282b4c6763337ac9f5d2", size = 2394328, upload-time = "2026-03-09T13:14:36.816Z" }, + { url = "https://files.pythonhosted.org/packages/47/84/6a6d5e5bb8273756c27b7d810d47f7ef2f1f9b9fd23c9ee9a3f8c75c9cef/kiwisolver-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:893ff3a711d1b515ba9da14ee090519bad4610ed1962fbe298a434e8c5f8db53", size = 68410, upload-time = "2026-03-09T13:14:38.695Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/060f45052f2a01ad5762c8fdecd6d7a752b43400dc29ff75cd47225a40fd/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8df31fe574b8b3993cc61764f40941111b25c2d9fea13d3ce24a49907cd2d615", size = 123231, upload-time = "2026-03-09T13:14:41.323Z" }, + { url = "https://files.pythonhosted.org/packages/c2/a7/78da680eadd06ff35edef6ef68a1ad273bad3e2a0936c9a885103230aece/kiwisolver-1.5.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1d49a49ac4cbfb7c1375301cd1ec90169dfeae55ff84710d782260ce77a75a02", size = 66489, upload-time = "2026-03-09T13:14:42.534Z" }, + { url = "https://files.pythonhosted.org/packages/49/b2/97980f3ad4fae37dd7fe31626e2bf75fbf8bdf5d303950ec1fab39a12da8/kiwisolver-1.5.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0cbe94b69b819209a62cb27bdfa5dc2a8977d8de2f89dfd97ba4f53ed3af754e", size = 64063, upload-time = "2026-03-09T13:14:44.759Z" }, + { url = "https://files.pythonhosted.org/packages/e7/f9/b06c934a6aa8bc91f566bd2a214fd04c30506c2d9e2b6b171953216a65b6/kiwisolver-1.5.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:80aa065ffd378ff784822a6d7c3212f2d5f5e9c3589614b5c228b311fd3063ac", size = 1475913, upload-time = "2026-03-09T13:14:46.247Z" }, + { url = "https://files.pythonhosted.org/packages/6b/f0/f768ae564a710135630672981231320bc403cf9152b5596ec5289de0f106/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e7f886f47ab881692f278ae901039a234e4025a68e6dfab514263a0b1c4ae05", size = 1282782, upload-time = "2026-03-09T13:14:48.458Z" }, + { url = "https://files.pythonhosted.org/packages/e2/9f/1de7aad00697325f05238a5f2eafbd487fb637cc27a558b5367a5f37fb7f/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:5060731cc3ed12ca3a8b57acd4aeca5bbc2f49216dd0bec1650a1acd89486bcd", size = 1300815, upload-time = "2026-03-09T13:14:50.721Z" }, + { url = "https://files.pythonhosted.org/packages/5a/c2/297f25141d2e468e0ce7f7a7b92e0cf8918143a0cbd3422c1ad627e85a06/kiwisolver-1.5.0-cp314-cp314-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7a4aa69609f40fce3cbc3f87b2061f042eee32f94b8f11db707b66a26461591a", size = 1347925, upload-time = "2026-03-09T13:14:52.304Z" }, + { url = "https://files.pythonhosted.org/packages/b9/d3/f4c73a02eb41520c47610207b21afa8cdd18fdbf64ffd94674ae21c4812d/kiwisolver-1.5.0-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:d168fda2dbff7b9b5f38e693182d792a938c31db4dac3a80a4888de603c99554", size = 991322, upload-time = "2026-03-09T13:14:54.637Z" }, + { url = "https://files.pythonhosted.org/packages/7b/46/d3f2efef7732fcda98d22bf4ad5d3d71d545167a852ca710a494f4c15343/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:413b820229730d358efd838ecbab79902fe97094565fdc80ddb6b0a18c18a581", size = 2232857, upload-time = "2026-03-09T13:14:56.471Z" }, + { url = "https://files.pythonhosted.org/packages/3f/ec/2d9756bf2b6d26ae4349b8d3662fb3993f16d80c1f971c179ce862b9dbae/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:5124d1ea754509b09e53738ec185584cc609aae4a3b510aaf4ed6aa047ef9303", size = 2329376, upload-time = "2026-03-09T13:14:58.072Z" }, + { url = "https://files.pythonhosted.org/packages/8f/9f/876a0a0f2260f1bde92e002b3019a5fabc35e0939c7d945e0fa66185eb20/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:e4415a8db000bf49a6dd1c478bf70062eaacff0f462b92b0ba68791a905861f9", size = 1982549, upload-time = "2026-03-09T13:14:59.668Z" }, + { url = "https://files.pythonhosted.org/packages/6c/4f/ba3624dfac23a64d54ac4179832860cb537c1b0af06024936e82ca4154a0/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:d618fd27420381a4f6044faa71f46d8bfd911bd077c555f7138ed88729bfbe79", size = 2494680, upload-time = "2026-03-09T13:15:01.364Z" }, + { url = "https://files.pythonhosted.org/packages/39/b7/97716b190ab98911b20d10bf92eca469121ec483b8ce0edd314f51bc85af/kiwisolver-1.5.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5092eb5b1172947f57d6ea7d89b2f29650414e4293c47707eb499ec07a0ac796", size = 2297905, upload-time = "2026-03-09T13:15:03.925Z" }, + { url = "https://files.pythonhosted.org/packages/a3/36/4e551e8aa55c9188bca9abb5096805edbf7431072b76e2298e34fd3a3008/kiwisolver-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:d76e2d8c75051d58177e762164d2e9ab92886534e3a12e795f103524f221dd8e", size = 75086, upload-time = "2026-03-09T13:15:07.775Z" }, + { url = "https://files.pythonhosted.org/packages/70/15/9b90f7df0e31a003c71649cf66ef61c3c1b862f48c81007fa2383c8bd8d7/kiwisolver-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:fa6248cd194edff41d7ea9425ced8ca3a6f838bfb295f6f1d6e6bb694a8518df", size = 66577, upload-time = "2026-03-09T13:15:09.139Z" }, + { url = "https://files.pythonhosted.org/packages/17/01/7dc8c5443ff42b38e72731643ed7cf1ed9bf01691ae5cdca98501999ed83/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:d1ffeb80b5676463d7a7d56acbe8e37a20ce725570e09549fe738e02ca6b7e1e", size = 125794, upload-time = "2026-03-09T13:15:10.525Z" }, + { url = "https://files.pythonhosted.org/packages/46/8a/b4ebe46ebaac6a303417fab10c2e165c557ddaff558f9699d302b256bc53/kiwisolver-1.5.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:bc4d8e252f532ab46a1de9349e2d27b91fce46736a9eedaa37beaca66f574ed4", size = 67646, upload-time = "2026-03-09T13:15:12.016Z" }, + { url = "https://files.pythonhosted.org/packages/60/35/10a844afc5f19d6f567359bf4789e26661755a2f36200d5d1ed8ad0126e5/kiwisolver-1.5.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6783e069732715ad0c3ce96dbf21dbc2235ab0593f2baf6338101f70371f4028", size = 65511, upload-time = "2026-03-09T13:15:13.311Z" }, + { url = "https://files.pythonhosted.org/packages/f8/8a/685b297052dd041dcebce8e8787b58923b6e78acc6115a0dc9189011c44b/kiwisolver-1.5.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:e7c4c09a490dc4d4a7f8cbee56c606a320f9dc28cf92a7157a39d1ce7676a657", size = 1584858, upload-time = "2026-03-09T13:15:15.103Z" }, + { url = "https://files.pythonhosted.org/packages/9e/80/04865e3d4638ac5bddec28908916df4a3075b8c6cc101786a96803188b96/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2a075bd7bd19c70cf67c8badfa36cf7c5d8de3c9ddb8420c51e10d9c50e94920", size = 1392539, upload-time = "2026-03-09T13:15:16.661Z" }, + { url = "https://files.pythonhosted.org/packages/ba/01/77a19cacc0893fa13fafa46d1bba06fb4dc2360b3292baf4b56d8e067b24/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:bdd3e53429ff02aa319ba59dfe4ceeec345bf46cf180ec2cf6fd5b942e7975e9", size = 1405310, upload-time = "2026-03-09T13:15:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/53/39/bcaf5d0cca50e604cfa9b4e3ae1d64b50ca1ae5b754122396084599ef903/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_24_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3cdcb35dc9d807259c981a85531048ede628eabcffb3239adf3d17463518992d", size = 1456244, upload-time = "2026-03-09T13:15:20.444Z" }, + { url = "https://files.pythonhosted.org/packages/d0/7a/72c187abc6975f6978c3e39b7cf67aeb8b3c0a8f9790aa7fd412855e9e1f/kiwisolver-1.5.0-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:70d593af6a6ca332d1df73d519fddb5148edb15cd90d5f0155e3746a6d4fcc65", size = 1073154, upload-time = "2026-03-09T13:15:22.039Z" }, + { url = "https://files.pythonhosted.org/packages/c7/ca/cf5b25783ebbd59143b4371ed0c8428a278abe68d6d0104b01865b1bbd0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:377815a8616074cabbf3f53354e1d040c35815a134e01d7614b7692e4bf8acfa", size = 2334377, upload-time = "2026-03-09T13:15:23.741Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e5/b1f492adc516796e88751282276745340e2a72dcd0d36cf7173e0daf3210/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:0255a027391d52944eae1dbb5d4cc5903f57092f3674e8e544cdd2622826b3f0", size = 2425288, upload-time = "2026-03-09T13:15:25.789Z" }, + { url = "https://files.pythonhosted.org/packages/e6/e5/9b21fbe91a61b8f409d74a26498706e97a48008bfcd1864373d32a6ba31c/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:012b1eb16e28718fa782b5e61dc6f2da1f0792ca73bd05d54de6cb9561665fc9", size = 2063158, upload-time = "2026-03-09T13:15:27.63Z" }, + { url = "https://files.pythonhosted.org/packages/b1/02/83f47986138310f95ea95531f851b2a62227c11cbc3e690ae1374fe49f0f/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:0e3aafb33aed7479377e5e9a82e9d4bf87063741fc99fc7ae48b0f16e32bdd6f", size = 2597260, upload-time = "2026-03-09T13:15:29.421Z" }, + { url = "https://files.pythonhosted.org/packages/07/18/43a5f24608d8c313dd189cf838c8e68d75b115567c6279de7796197cfb6a/kiwisolver-1.5.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e7a116ae737f0000343218c4edf5bd45893bfeaff0993c0b215d7124c9f77646", size = 2394403, upload-time = "2026-03-09T13:15:31.517Z" }, + { url = "https://files.pythonhosted.org/packages/3b/b5/98222136d839b8afabcaa943b09bd05888c2d36355b7e448550211d1fca4/kiwisolver-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:1dd9b0b119a350976a6d781e7278ec7aca0b201e1a9e2d23d9804afecb6ca681", size = 79687, upload-time = "2026-03-09T13:15:33.204Z" }, + { url = "https://files.pythonhosted.org/packages/99/a2/ca7dc962848040befed12732dff6acae7fb3c4f6fc4272b3f6c9a30b8713/kiwisolver-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:58f812017cd2985c21fbffb4864d59174d4903dd66fa23815e74bbc7a0e2dd57", size = 70032, upload-time = "2026-03-09T13:15:34.411Z" }, + { url = "https://files.pythonhosted.org/packages/1c/fa/2910df836372d8761bb6eff7d8bdcb1613b5c2e03f260efe7abe34d388a7/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_10_13_x86_64.whl", hash = "sha256:5ae8e62c147495b01a0f4765c878e9bfdf843412446a247e28df59936e99e797", size = 130262, upload-time = "2026-03-09T13:15:35.629Z" }, + { url = "https://files.pythonhosted.org/packages/0f/41/c5f71f9f00aabcc71fee8b7475e3f64747282580c2fe748961ba29b18385/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:f6764a4ccab3078db14a632420930f6186058750df066b8ea2a7106df91d3203", size = 138036, upload-time = "2026-03-09T13:15:36.894Z" }, + { url = "https://files.pythonhosted.org/packages/fa/06/7399a607f434119c6e1fdc8ec89a8d51ccccadf3341dee4ead6bd14caaf5/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c31c13da98624f957b0fb1b5bae5383b2333c2c3f6793d9825dd5ce79b525cb7", size = 194295, upload-time = "2026-03-09T13:15:38.22Z" }, + { url = "https://files.pythonhosted.org/packages/b5/91/53255615acd2a1eaca307ede3c90eb550bae9c94581f8c00081b6b1c8f44/kiwisolver-1.5.0-graalpy312-graalpy250_312_native-win_amd64.whl", hash = "sha256:1f1489f769582498610e015a8ef2d36f28f505ab3096d0e16b4858a9ec214f57", size = 75987, upload-time = "2026-03-09T13:15:39.65Z" }, +] + [[package]] name = "lxml" version = "6.0.2" @@ -689,6 +1457,532 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/92/aa/df863bcc39c5e0946263454aba394de8a9084dbaff8ad143846b0d844739/lxml-6.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:bb4c1847b303835d89d785a18801a883436cdfd5dc3d62947f9c49e24f0f5a2c", size = 3822205, upload-time = "2025-09-22T04:03:36.249Z" }, ] +[[package]] +name = "markdown-it-py" +version = "4.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mdurl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/5b/f5/4ec618ed16cc4f8fb3b701563655a69816155e79e24a17b651541804721d/markdown_it_py-4.0.0.tar.gz", hash = "sha256:cb0a2b4aa34f932c007117b194e945bd74e0ec24133ceb5bac59009cda1cb9f3", size = 73070, upload-time = "2025-08-11T12:57:52.854Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/94/54/e7d793b573f298e1c9013b8c4dade17d481164aa517d1d7148619c2cedbf/markdown_it_py-4.0.0-py3-none-any.whl", hash = "sha256:87327c59b172c5011896038353a81343b6754500a08cd7a4973bb48c6d578147", size = 87321, upload-time = "2025-08-11T12:57:51.923Z" }, +] + +[[package]] +name = "markupsafe" +version = "3.0.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7e/99/7690b6d4034fffd95959cbe0c02de8deb3098cc577c67bb6a24fe5d7caa7/markupsafe-3.0.3.tar.gz", hash = "sha256:722695808f4b6457b320fdc131280796bdceb04ab50fe1795cd540799ebe1698", size = 80313, upload-time = "2025-09-27T18:37:40.426Z" } +wheels = [ + { 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" }, + { url = "https://files.pythonhosted.org/packages/38/2f/907b9c7bbba283e68f20259574b13d005c121a0fa4c175f9bed27c4597ff/markupsafe-3.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:e1cf1972137e83c5d4c136c43ced9ac51d0e124706ee1c8aa8532c1287fa8795", size = 11622, upload-time = "2025-09-27T18:36:41.777Z" }, + { url = "https://files.pythonhosted.org/packages/9c/d9/5f7756922cdd676869eca1c4e3c0cd0df60ed30199ffd775e319089cb3ed/markupsafe-3.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:116bb52f642a37c115f517494ea5feb03889e04df47eeff5b130b1808ce7c219", size = 12029, upload-time = "2025-09-27T18:36:43.257Z" }, + { url = "https://files.pythonhosted.org/packages/00/07/575a68c754943058c78f30db02ee03a64b3c638586fba6a6dd56830b30a3/markupsafe-3.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:133a43e73a802c5562be9bbcd03d090aa5a1fe899db609c29e8c8d815c5f6de6", size = 24374, upload-time = "2025-09-27T18:36:44.508Z" }, + { url = "https://files.pythonhosted.org/packages/a9/21/9b05698b46f218fc0e118e1f8168395c65c8a2c750ae2bab54fc4bd4e0e8/markupsafe-3.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ccfcd093f13f0f0b7fdd0f198b90053bf7b2f02a3927a30e63f3ccc9df56b676", size = 22980, upload-time = "2025-09-27T18:36:45.385Z" }, + { url = "https://files.pythonhosted.org/packages/7f/71/544260864f893f18b6827315b988c146b559391e6e7e8f7252839b1b846a/markupsafe-3.0.3-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:509fa21c6deb7a7a273d629cf5ec029bc209d1a51178615ddf718f5918992ab9", size = 21990, upload-time = "2025-09-27T18:36:46.916Z" }, + { url = "https://files.pythonhosted.org/packages/c2/28/b50fc2f74d1ad761af2f5dcce7492648b983d00a65b8c0e0cb457c82ebbe/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a4afe79fb3de0b7097d81da19090f4df4f8d3a2b3adaa8764138aac2e44f3af1", size = 23784, upload-time = "2025-09-27T18:36:47.884Z" }, + { url = "https://files.pythonhosted.org/packages/ed/76/104b2aa106a208da8b17a2fb72e033a5a9d7073c68f7e508b94916ed47a9/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:795e7751525cae078558e679d646ae45574b47ed6e7771863fcc079a6171a0fc", size = 21588, upload-time = "2025-09-27T18:36:48.82Z" }, + { url = "https://files.pythonhosted.org/packages/b5/99/16a5eb2d140087ebd97180d95249b00a03aa87e29cc224056274f2e45fd6/markupsafe-3.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8485f406a96febb5140bfeca44a73e3ce5116b2501ac54fe953e488fb1d03b12", size = 23041, upload-time = "2025-09-27T18:36:49.797Z" }, + { url = "https://files.pythonhosted.org/packages/19/bc/e7140ed90c5d61d77cea142eed9f9c303f4c4806f60a1044c13e3f1471d0/markupsafe-3.0.3-cp313-cp313-win32.whl", hash = "sha256:bdd37121970bfd8be76c5fb069c7751683bdf373db1ed6c010162b2a130248ed", size = 14543, upload-time = "2025-09-27T18:36:51.584Z" }, + { url = "https://files.pythonhosted.org/packages/05/73/c4abe620b841b6b791f2edc248f556900667a5a1cf023a6646967ae98335/markupsafe-3.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:9a1abfdc021a164803f4d485104931fb8f8c1efd55bc6b748d2f5774e78b62c5", size = 15113, upload-time = "2025-09-27T18:36:52.537Z" }, + { url = "https://files.pythonhosted.org/packages/f0/3a/fa34a0f7cfef23cf9500d68cb7c32dd64ffd58a12b09225fb03dd37d5b80/markupsafe-3.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:7e68f88e5b8799aa49c85cd116c932a1ac15caaa3f5db09087854d218359e485", size = 13911, upload-time = "2025-09-27T18:36:53.513Z" }, + { url = "https://files.pythonhosted.org/packages/e4/d7/e05cd7efe43a88a17a37b3ae96e79a19e846f3f456fe79c57ca61356ef01/markupsafe-3.0.3-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:218551f6df4868a8d527e3062d0fb968682fe92054e89978594c28e642c43a73", size = 11658, upload-time = "2025-09-27T18:36:54.819Z" }, + { url = "https://files.pythonhosted.org/packages/99/9e/e412117548182ce2148bdeacdda3bb494260c0b0184360fe0d56389b523b/markupsafe-3.0.3-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:3524b778fe5cfb3452a09d31e7b5adefeea8c5be1d43c4f810ba09f2ceb29d37", size = 12066, upload-time = "2025-09-27T18:36:55.714Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/fa0ffcda717ef64a5108eaa7b4f5ed28d56122c9a6d70ab8b72f9f715c80/markupsafe-3.0.3-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4e885a3d1efa2eadc93c894a21770e4bc67899e3543680313b09f139e149ab19", size = 25639, upload-time = "2025-09-27T18:36:56.908Z" }, + { url = "https://files.pythonhosted.org/packages/96/ec/2102e881fe9d25fc16cb4b25d5f5cde50970967ffa5dddafdb771237062d/markupsafe-3.0.3-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8709b08f4a89aa7586de0aadc8da56180242ee0ada3999749b183aa23df95025", size = 23569, upload-time = "2025-09-27T18:36:57.913Z" }, + { url = "https://files.pythonhosted.org/packages/4b/30/6f2fce1f1f205fc9323255b216ca8a235b15860c34b6798f810f05828e32/markupsafe-3.0.3-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:b8512a91625c9b3da6f127803b166b629725e68af71f8184ae7e7d54686a56d6", size = 23284, upload-time = "2025-09-27T18:36:58.833Z" }, + { url = "https://files.pythonhosted.org/packages/58/47/4a0ccea4ab9f5dcb6f79c0236d954acb382202721e704223a8aafa38b5c8/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:9b79b7a16f7fedff2495d684f2b59b0457c3b493778c9eed31111be64d58279f", size = 24801, upload-time = "2025-09-27T18:36:59.739Z" }, + { url = "https://files.pythonhosted.org/packages/6a/70/3780e9b72180b6fecb83a4814d84c3bf4b4ae4bf0b19c27196104149734c/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:12c63dfb4a98206f045aa9563db46507995f7ef6d83b2f68eda65c307c6829eb", size = 22769, upload-time = "2025-09-27T18:37:00.719Z" }, + { url = "https://files.pythonhosted.org/packages/98/c5/c03c7f4125180fc215220c035beac6b9cb684bc7a067c84fc69414d315f5/markupsafe-3.0.3-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8f71bc33915be5186016f675cd83a1e08523649b0e33efdb898db577ef5bb009", size = 23642, upload-time = "2025-09-27T18:37:01.673Z" }, + { url = "https://files.pythonhosted.org/packages/80/d6/2d1b89f6ca4bff1036499b1e29a1d02d282259f3681540e16563f27ebc23/markupsafe-3.0.3-cp313-cp313t-win32.whl", hash = "sha256:69c0b73548bc525c8cb9a251cddf1931d1db4d2258e9599c28c07ef3580ef354", size = 14612, upload-time = "2025-09-27T18:37:02.639Z" }, + { url = "https://files.pythonhosted.org/packages/2b/98/e48a4bfba0a0ffcf9925fe2d69240bfaa19c6f7507b8cd09c70684a53c1e/markupsafe-3.0.3-cp313-cp313t-win_amd64.whl", hash = "sha256:1b4b79e8ebf6b55351f0d91fe80f893b4743f104bff22e90697db1590e47a218", size = 15200, upload-time = "2025-09-27T18:37:03.582Z" }, + { url = "https://files.pythonhosted.org/packages/0e/72/e3cc540f351f316e9ed0f092757459afbc595824ca724cbc5a5d4263713f/markupsafe-3.0.3-cp313-cp313t-win_arm64.whl", hash = "sha256:ad2cf8aa28b8c020ab2fc8287b0f823d0a7d8630784c31e9ee5edea20f406287", size = 13973, upload-time = "2025-09-27T18:37:04.929Z" }, + { url = "https://files.pythonhosted.org/packages/33/8a/8e42d4838cd89b7dde187011e97fe6c3af66d8c044997d2183fbd6d31352/markupsafe-3.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:eaa9599de571d72e2daf60164784109f19978b327a3910d3e9de8c97b5b70cfe", size = 11619, upload-time = "2025-09-27T18:37:06.342Z" }, + { url = "https://files.pythonhosted.org/packages/b5/64/7660f8a4a8e53c924d0fa05dc3a55c9cee10bbd82b11c5afb27d44b096ce/markupsafe-3.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c47a551199eb8eb2121d4f0f15ae0f923d31350ab9280078d1e5f12b249e0026", size = 12029, upload-time = "2025-09-27T18:37:07.213Z" }, + { url = "https://files.pythonhosted.org/packages/da/ef/e648bfd021127bef5fa12e1720ffed0c6cbb8310c8d9bea7266337ff06de/markupsafe-3.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f34c41761022dd093b4b6896d4810782ffbabe30f2d443ff5f083e0cbbb8c737", size = 24408, upload-time = "2025-09-27T18:37:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/41/3c/a36c2450754618e62008bf7435ccb0f88053e07592e6028a34776213d877/markupsafe-3.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:457a69a9577064c05a97c41f4e65148652db078a3a509039e64d3467b9e7ef97", size = 23005, upload-time = "2025-09-27T18:37:10.58Z" }, + { url = "https://files.pythonhosted.org/packages/bc/20/b7fdf89a8456b099837cd1dc21974632a02a999ec9bf7ca3e490aacd98e7/markupsafe-3.0.3-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:e8afc3f2ccfa24215f8cb28dcf43f0113ac3c37c2f0f0806d8c70e4228c5cf4d", size = 22048, upload-time = "2025-09-27T18:37:11.547Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a7/591f592afdc734f47db08a75793a55d7fbcc6902a723ae4cfbab61010cc5/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:ec15a59cf5af7be74194f7ab02d0f59a62bdcf1a537677ce67a2537c9b87fcda", size = 23821, upload-time = "2025-09-27T18:37:12.48Z" }, + { url = "https://files.pythonhosted.org/packages/7d/33/45b24e4f44195b26521bc6f1a82197118f74df348556594bd2262bda1038/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:0eb9ff8191e8498cca014656ae6b8d61f39da5f95b488805da4bb029cccbfbaf", size = 21606, upload-time = "2025-09-27T18:37:13.485Z" }, + { url = "https://files.pythonhosted.org/packages/ff/0e/53dfaca23a69fbfbbf17a4b64072090e70717344c52eaaaa9c5ddff1e5f0/markupsafe-3.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:2713baf880df847f2bece4230d4d094280f4e67b1e813eec43b4c0e144a34ffe", size = 23043, upload-time = "2025-09-27T18:37:14.408Z" }, + { url = "https://files.pythonhosted.org/packages/46/11/f333a06fc16236d5238bfe74daccbca41459dcd8d1fa952e8fbd5dccfb70/markupsafe-3.0.3-cp314-cp314-win32.whl", hash = "sha256:729586769a26dbceff69f7a7dbbf59ab6572b99d94576a5592625d5b411576b9", size = 14747, upload-time = "2025-09-27T18:37:15.36Z" }, + { url = "https://files.pythonhosted.org/packages/28/52/182836104b33b444e400b14f797212f720cbc9ed6ba34c800639d154e821/markupsafe-3.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:bdc919ead48f234740ad807933cdf545180bfbe9342c2bb451556db2ed958581", size = 15341, upload-time = "2025-09-27T18:37:16.496Z" }, + { url = "https://files.pythonhosted.org/packages/6f/18/acf23e91bd94fd7b3031558b1f013adfa21a8e407a3fdb32745538730382/markupsafe-3.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:5a7d5dc5140555cf21a6fefbdbf8723f06fcd2f63ef108f2854de715e4422cb4", size = 14073, upload-time = "2025-09-27T18:37:17.476Z" }, + { url = "https://files.pythonhosted.org/packages/3c/f0/57689aa4076e1b43b15fdfa646b04653969d50cf30c32a102762be2485da/markupsafe-3.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:1353ef0c1b138e1907ae78e2f6c63ff67501122006b0f9abad68fda5f4ffc6ab", size = 11661, upload-time = "2025-09-27T18:37:18.453Z" }, + { url = "https://files.pythonhosted.org/packages/89/c3/2e67a7ca217c6912985ec766c6393b636fb0c2344443ff9d91404dc4c79f/markupsafe-3.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:1085e7fbddd3be5f89cc898938f42c0b3c711fdcb37d75221de2666af647c175", size = 12069, upload-time = "2025-09-27T18:37:19.332Z" }, + { url = "https://files.pythonhosted.org/packages/f0/00/be561dce4e6ca66b15276e184ce4b8aec61fe83662cce2f7d72bd3249d28/markupsafe-3.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1b52b4fb9df4eb9ae465f8d0c228a00624de2334f216f178a995ccdcf82c4634", size = 25670, upload-time = "2025-09-27T18:37:20.245Z" }, + { url = "https://files.pythonhosted.org/packages/50/09/c419f6f5a92e5fadde27efd190eca90f05e1261b10dbd8cbcb39cd8ea1dc/markupsafe-3.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fed51ac40f757d41b7c48425901843666a6677e3e8eb0abcff09e4ba6e664f50", size = 23598, upload-time = "2025-09-27T18:37:21.177Z" }, + { url = "https://files.pythonhosted.org/packages/22/44/a0681611106e0b2921b3033fc19bc53323e0b50bc70cffdd19f7d679bb66/markupsafe-3.0.3-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f190daf01f13c72eac4efd5c430a8de82489d9cff23c364c3ea822545032993e", size = 23261, upload-time = "2025-09-27T18:37:22.167Z" }, + { url = "https://files.pythonhosted.org/packages/5f/57/1b0b3f100259dc9fffe780cfb60d4be71375510e435efec3d116b6436d43/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e56b7d45a839a697b5eb268c82a71bd8c7f6c94d6fd50c3d577fa39a9f1409f5", size = 24835, upload-time = "2025-09-27T18:37:23.296Z" }, + { url = "https://files.pythonhosted.org/packages/26/6a/4bf6d0c97c4920f1597cc14dd720705eca0bf7c787aebc6bb4d1bead5388/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:f3e98bb3798ead92273dc0e5fd0f31ade220f59a266ffd8a4f6065e0a3ce0523", size = 22733, upload-time = "2025-09-27T18:37:24.237Z" }, + { url = "https://files.pythonhosted.org/packages/14/c7/ca723101509b518797fedc2fdf79ba57f886b4aca8a7d31857ba3ee8281f/markupsafe-3.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5678211cb9333a6468fb8d8be0305520aa073f50d17f089b5b4b477ea6e67fdc", size = 23672, upload-time = "2025-09-27T18:37:25.271Z" }, + { url = "https://files.pythonhosted.org/packages/fb/df/5bd7a48c256faecd1d36edc13133e51397e41b73bb77e1a69deab746ebac/markupsafe-3.0.3-cp314-cp314t-win32.whl", hash = "sha256:915c04ba3851909ce68ccc2b8e2cd691618c4dc4c4232fb7982bca3f41fd8c3d", size = 14819, upload-time = "2025-09-27T18:37:26.285Z" }, + { url = "https://files.pythonhosted.org/packages/1a/8a/0402ba61a2f16038b48b39bccca271134be00c5c9f0f623208399333c448/markupsafe-3.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4faffd047e07c38848ce017e8725090413cd80cbc23d86e55c587bf979e579c9", size = 15426, upload-time = "2025-09-27T18:37:27.316Z" }, + { url = "https://files.pythonhosted.org/packages/70/bc/6f1c2f612465f5fa89b95bead1f44dcb607670fd42891d8fdcd5d039f4f4/markupsafe-3.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:32001d6a8fc98c8cb5c947787c5d08b0a50663d139f1305bac5885d98d9b40fa", size = 14146, upload-time = "2025-09-27T18:37:28.327Z" }, +] + +[[package]] +name = "matplotlib" +version = "3.10.9" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "contourpy" }, + { name = "cycler" }, + { name = "fonttools" }, + { name = "kiwisolver" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pillow" }, + { name = "pyparsing" }, + { name = "python-dateutil" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/63/1b/4be5be87d43d327a0cf4de1a56e86f7f84c89312452406cf122efe2839e6/matplotlib-3.10.9.tar.gz", hash = "sha256:fd66508e8c6877d98e586654b608a0456db8d7e8a546eb1e2600efd957302358", size = 34811233, upload-time = "2026-04-24T00:14:13.539Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/c6/5581e26c72233ebb2a2a6fed2d24fb7c66b4700120b813f51b0555acf0b6/matplotlib-3.10.9-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:f0c3c28d9fbcc1fe7a03be236d73430cf6409c41fb2383a7ac52fe932b072cb1", size = 8319908, upload-time = "2026-04-24T00:12:21.323Z" }, + { url = "https://files.pythonhosted.org/packages/b7/18/4880dd762e40cd360c1bf06e890c5a97b997e91cb324602b1a19950ad5ce/matplotlib-3.10.9-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:41cb28c2bd769aa3e98322c6ab09854cbcc52ab69d2759d681bba3e327b2b320", size = 8216016, upload-time = "2026-04-24T00:12:23.4Z" }, + { url = "https://files.pythonhosted.org/packages/32/91/d024616abdba99e83120e07a20658976f6a343646710760c4a51df126029/matplotlib-3.10.9-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:ae20801130378b82d647ff5047c07316295b68dc054ca6b3c13519d0ea624285", size = 8789336, upload-time = "2026-04-24T00:12:26.096Z" }, + { url = "https://files.pythonhosted.org/packages/5c/04/030a2f61ef2158f5e4c259487a92ac877732499fb33d871585d89e03c42d/matplotlib-3.10.9-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6c63ebcd8b4b169eb2f5c200552ae6b8be8999a005b6b507ed76fb8d7d674fe2", size = 9604602, upload-time = "2026-04-24T00:12:29.052Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c2/541e4d09d87bb6b5830fc28b4c887a9a8cf4e1c6cee698a8c05552ae2003/matplotlib-3.10.9-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:d75d11c949914165976c621b2324f9ef162af7ebf4b057ddf95dd1dba7e5edcf", size = 9670966, upload-time = "2026-04-24T00:12:32.131Z" }, + { url = "https://files.pythonhosted.org/packages/04/a1/4571fc46e7702de8d0c2dc54ad1b2f8e29328dea3ee90831181f7353d93c/matplotlib-3.10.9-cp312-cp312-win_amd64.whl", hash = "sha256:d091f9d758b34aaaaa6331d13574bf01891d903b3dec59bfff458ef7551de5d6", size = 8217462, upload-time = "2026-04-24T00:12:35.226Z" }, + { url = "https://files.pythonhosted.org/packages/4b/d0/2269edb12aa30c13c8bcc9382892e39943ce1d28aab4ec296e0381798e81/matplotlib-3.10.9-cp312-cp312-win_arm64.whl", hash = "sha256:10cc5ce06d10231c36f40e875f3c7e8050362a4ee8f0ee5d29a6b3277d57bb42", size = 8136688, upload-time = "2026-04-24T00:12:37.442Z" }, + { url = "https://files.pythonhosted.org/packages/aa/d3/8d4f6afbecb49fc04e060a57c0fce39ea51cc163a6bd87303ccd698e4fa6/matplotlib-3.10.9-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b580440f1ff81a0e34122051a3dfabb7e4b7f9e380629929bde0eff9af72165f", size = 8320331, upload-time = "2026-04-24T00:12:39.688Z" }, + { url = "https://files.pythonhosted.org/packages/63/d9/9e14bc7564bf92d5ffa801ae5fac819ce74b925dfb55e3ebde61a3bbad3e/matplotlib-3.10.9-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:b1b745c489cd1a77a0dc1120a05dc87af9798faebc913601feb8c73d89bf2d1e", size = 8216461, upload-time = "2026-04-24T00:12:42.494Z" }, + { url = "https://files.pythonhosted.org/packages/8a/17/4402d0d14ccf1dfc70932600b68097fbbf9c898a4871d2cbbe79c7801a32/matplotlib-3.10.9-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:8f3bcac1ca5ed000a6f4337d47ba67dfddf37ed6a46c15fd7f014997f7bf865f", size = 8790091, upload-time = "2026-04-24T00:12:44.789Z" }, + { url = "https://files.pythonhosted.org/packages/3e/0b/322aeec06dd9b91411f92028b37d447342770a24392aa4813e317064dad5/matplotlib-3.10.9-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a8d66a55def891c33147ba3ba9bfcabf0b526a43764c818acbb4525e5ed0838", size = 9605027, upload-time = "2026-04-24T00:12:47.583Z" }, + { url = "https://files.pythonhosted.org/packages/74/88/5f13482f55e7b00bcfc09838b093c2456e1379978d2a146844aae05350ad/matplotlib-3.10.9-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:d843374407c4017a6403b59c6c81606773d136f3259d5b6da3131bc814542cc2", size = 9671269, upload-time = "2026-04-24T00:12:50.878Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/0840fd2f93da988ec660b8ad1984abe9f25d2aed22a5e394ff1c68c88307/matplotlib-3.10.9-cp313-cp313-win_amd64.whl", hash = "sha256:f4399f64b3e94cd500195490972ae1ee81170df1636fa15364d157d5bdd7b921", size = 8217588, upload-time = "2026-04-24T00:12:53.784Z" }, + { url = "https://files.pythonhosted.org/packages/47/b9/d706d06dd605c49b9f83a2aed8c13e3e5db70697d7a80b7e3d7915de6b17/matplotlib-3.10.9-cp313-cp313-win_arm64.whl", hash = "sha256:ba7b3b8ef09eab7df0e86e9ae086faa433efbfbdb46afcb3aa16aabf779469a8", size = 8136913, upload-time = "2026-04-24T00:12:56.501Z" }, + { url = "https://files.pythonhosted.org/packages/9b/45/6e32d96978264c8ca8c4b1010adb955a1a49cfaf314e212bbc8908f04a61/matplotlib-3.10.9-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:09218df8a93712bd6ea133e83a153c755448cf7868316c531cffcc43f69d1cc9", size = 8368019, upload-time = "2026-04-24T00:12:58.896Z" }, + { url = "https://files.pythonhosted.org/packages/86/0a/c8e3d3bba245f0f7fc424937f8ff7ef77291a36af3edb97ccd78aa93d84f/matplotlib-3.10.9-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:82368699727bfb7b0182e1aa13082e3c08e092fa1a25d3e1fd92405bff96f6d4", size = 8264645, upload-time = "2026-04-24T00:13:01.406Z" }, + { url = "https://files.pythonhosted.org/packages/3d/aa/5bf5a14fe4fed73a4209a155606f8096ff797aad89c6c35179026571133e/matplotlib-3.10.9-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:3225f4e1edcb8c86c884ddf79ebe20ecd0a67d30188f279897554ccd8fded4dc", size = 8802194, upload-time = "2026-04-24T00:13:03.702Z" }, + { url = "https://files.pythonhosted.org/packages/dd/5e/b4be852d6bba6fd15893fadf91ff26ae49cb91aac789e95dde9d342e664f/matplotlib-3.10.9-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:de2445a0c6690d21b7eb6ce071cebad6d40a2e9bdf10d039074a96ba19797b99", size = 9622684, upload-time = "2026-04-24T00:13:06.647Z" }, + { url = "https://files.pythonhosted.org/packages/4c/3d/ed428c971139112ef730f62770654d609467346d09d4b62617e1afd68a5a/matplotlib-3.10.9-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:b2b9516251cb89ff618d757daec0e2ed1bf21248013844a853d87ef85ab3081d", size = 9680790, upload-time = "2026-04-24T00:13:10.009Z" }, + { url = "https://files.pythonhosted.org/packages/e7/09/052e884aaf2b985c63cb79f715f1d5b6a3eaa7de78f6a52b9dbc077d5b53/matplotlib-3.10.9-cp313-cp313t-win_amd64.whl", hash = "sha256:e9fae004b941b23ff2edcf1567a857ed77bafc8086ffa258190462328434faf8", size = 8287571, upload-time = "2026-04-24T00:13:13.087Z" }, + { url = "https://files.pythonhosted.org/packages/f4/38/ae27288e788c35a4250491422f3db7750366fc8c97d6f36fbdecfc1f5518/matplotlib-3.10.9-cp313-cp313t-win_arm64.whl", hash = "sha256:6b63d9c7c769b88ab81e10dc86e4e0607cf56817b9f9e6cf24b2a5f1693b8e38", size = 8188292, upload-time = "2026-04-24T00:13:15.546Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e6/3bd8afd04949f02eabc1c17115ea5255e19cacd4d06fc5abdde4eeb0052c/matplotlib-3.10.9-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:172db52c9e683f5d12eaf57f0f54834190e12581fe1cc2a19595a8f5acb4e77d", size = 8321276, upload-time = "2026-04-24T00:13:18.318Z" }, + { url = "https://files.pythonhosted.org/packages/41/86/86231232fff41c9f8e4a1a7d7a597d349a02527109c3af7d618366122139/matplotlib-3.10.9-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:97e35e8d39ccc85859095e01a53847432ba9a53ddf7986f7a54a11b73d0e143f", size = 8218218, upload-time = "2026-04-24T00:13:20.974Z" }, + { url = "https://files.pythonhosted.org/packages/85/8f/becc9722cafc64f5d2eb0b7c1bf5f585271c618a45dbd8fabeb021f898b6/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aba1615dabe83188e19d4f75a253c6a08423e04c1425e64039f800050a69de6b", size = 9608145, upload-time = "2026-04-24T00:13:23.228Z" }, + { url = "https://files.pythonhosted.org/packages/32/5d/f7e914f7d9325abff4057cee62c0fa70263683189f774473cbfb534cd13b/matplotlib-3.10.9-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34cf8167e023ad956c15f36302911d5406bd99a9862c1a8499ea6f7c0e015dc2", size = 9885085, upload-time = "2026-04-24T00:13:25.849Z" }, + { url = "https://files.pythonhosted.org/packages/a5/fd/fa69f2221534e80cc5772ac2b7d222011a2acafc2ec7216d5dd174c864ae/matplotlib-3.10.9-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59476c6d29d612b8e9bb6ce8c5b631be6ba8f9e3a2421f22a02b192c7dd28716", size = 9672358, upload-time = "2026-04-24T00:13:28.906Z" }, + { url = "https://files.pythonhosted.org/packages/ab/1a/5a4f747a8b271cbb024946d2dd3c913ab5032ba430626f8c3528ada96b4b/matplotlib-3.10.9-cp314-cp314-win_amd64.whl", hash = "sha256:336b9acc64d309063126edcdaca00db9373af3c476bb94388fe9c5a53ad13e6f", size = 8349970, upload-time = "2026-04-24T00:13:31.904Z" }, + { url = "https://files.pythonhosted.org/packages/64/dc/95d60ecaefe30680a154b52ea96ab4b0dab547f1fd6aa12f5fb655e89cae/matplotlib-3.10.9-cp314-cp314-win_arm64.whl", hash = "sha256:2dc9477819ffd78ad12a20df1d9d6a6bd4fec6aaa9072681465fddca052f1456", size = 8272785, upload-time = "2026-04-24T00:13:34.511Z" }, + { url = "https://files.pythonhosted.org/packages/70/a0/005d68bc8b8418300ce6591f18586910a8526806e2ab663933d9f20a41e9/matplotlib-3.10.9-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:da4e09638420548f31c354032a6250e473c68e5a4e96899b4844cf39ddea23fe", size = 8367999, upload-time = "2026-04-24T00:13:36.962Z" }, + { url = "https://files.pythonhosted.org/packages/22/05/1236cc9290be70b2498af20ca348add76e3fffe7f67b477db5133a84f3ea/matplotlib-3.10.9-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:345f6f68ecc8da0ca56fad2ea08fde1a115eda530079eca185d50a7bc3e146c6", size = 8264543, upload-time = "2026-04-24T00:13:39.851Z" }, + { url = "https://files.pythonhosted.org/packages/cd/c2/071f5a5ff6c5bd63aaaf2f45c811d9bf2ced94bde188d9e1a519e21d0cba/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4edcfbd8565339aa62f1cd4012f7180926fdbe71850f7b0d3c379c175cd6b66c", size = 9622800, upload-time = "2026-04-24T00:13:42.296Z" }, + { url = "https://files.pythonhosted.org/packages/95/57/da7d1f10a85624b9e7db68e069dd94e58dc41dbf9463c5921632ecbe3661/matplotlib-3.10.9-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6be157fe17fc37cb95ac1d7374cf717ce9259616edec911a78d9d26dae8522d4", size = 9888561, upload-time = "2026-04-24T00:13:45.026Z" }, + { url = "https://files.pythonhosted.org/packages/67/b2/ef8d6bb59b0edb6c16c968b70f548aa13b54348972def5aa6ac85df67145/matplotlib-3.10.9-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:4e42042d54db34fda4e95a7bd3e5789c2a995d2dad3eb8850232ee534092fbbf", size = 9680884, upload-time = "2026-04-24T00:13:48.066Z" }, + { url = "https://files.pythonhosted.org/packages/61/1c/d21bfeb9931881ebe96bcfcff27c7ae4b160ae0ec291a714c42641a56d75/matplotlib-3.10.9-cp314-cp314t-win_amd64.whl", hash = "sha256:c27df8b3848f32a83d1767566595e43cfaa4460380974da06f4279a7ec143c39", size = 8432333, upload-time = "2026-04-24T00:13:51.008Z" }, + { url = "https://files.pythonhosted.org/packages/78/23/92493c3e6e1b635ccfff146f7b99e674808787915420373ac399283764c2/matplotlib-3.10.9-cp314-cp314t-win_arm64.whl", hash = "sha256:a49f1eadc84ca85fd72fa4e89e70e61bf86452df6f971af04b12c60761a0772c", size = 8324785, upload-time = "2026-04-24T00:13:53.633Z" }, +] + +[[package]] +name = "mdurl" +version = "0.1.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/d6/54/cfe61301667036ec958cb99bd3efefba235e65cdeb9c84d24a8293ba1d90/mdurl-0.1.2.tar.gz", hash = "sha256:bb413d29f5eea38f31dd4754dd7377d4465116fb207585f97bf925588687c1ba", size = 8729, upload-time = "2022-08-14T12:40:10.846Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b3/38/89ba8ad64ae25be8de66a6d463314cf1eb366222074cfda9ee839c56a4b4/mdurl-0.1.2-py3-none-any.whl", hash = "sha256:84008a41e51615a49fc9966191ff91509e3c40b939176e643fd50a5c2196b8f8", size = 9979, upload-time = "2022-08-14T12:40:09.779Z" }, +] + +[[package]] +name = "mercantile" +version = "1.2.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "click" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/d2/c6/87409bcb26fb68c393fa8cf58ba09363aa7298cfb438a0109b5cb14bc98b/mercantile-1.2.1.tar.gz", hash = "sha256:fa3c6db15daffd58454ac198b31887519a19caccee3f9d63d17ae7ff61b3b56b", size = 26352, upload-time = "2021-04-21T14:42:41.096Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b2/d6/de0cc74f8d36976aeca0dd2e9cbf711882ff8e177495115fd82459afdc4d/mercantile-1.2.1-py3-none-any.whl", hash = "sha256:30f457a73ee88261aab787b7069d85961a5703bb09dc57a170190bc042cd023f", size = 14779, upload-time = "2021-04-21T14:42:39.841Z" }, +] + +[[package]] +name = "mpmath" +version = "1.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/e0/47/dd32fa426cc72114383ac549964eecb20ecfd886d1e5ccf5340b55b02f57/mpmath-1.3.0.tar.gz", hash = "sha256:7a28eb2a9774d00c7bc92411c19a89209d5da7c4c9a9e227be8330a23a25b91f", size = 508106, upload-time = "2023-03-07T16:47:11.061Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/43/e3/7d92a15f894aa0c9c4b49b8ee9ac9850d6e63b03c9c32c0367a13ae62209/mpmath-1.3.0-py3-none-any.whl", hash = "sha256:a0b2b9fe80bbcd81a6647ff13108738cfb482d481d826cc0e02f5b35e5c88d2c", size = 536198, upload-time = "2023-03-07T16:47:09.197Z" }, +] + +[[package]] +name = "multidict" +version = "6.7.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/1a/c2/c2d94cbe6ac1753f3fc980da97b3d930efe1da3af3c9f5125354436c073d/multidict-6.7.1.tar.gz", hash = "sha256:ec6652a1bee61c53a3e5776b6049172c53b6aaba34f18c9ad04f82712bac623d", size = 102010, upload-time = "2026-01-26T02:46:45.979Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/9c/f20e0e2cf80e4b2e4b1c365bf5fe104ee633c751a724246262db8f1a0b13/multidict-6.7.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:a90f75c956e32891a4eda3639ce6dd86e87105271f43d43442a3aedf3cddf172", size = 76893, upload-time = "2026-01-26T02:43:52.754Z" }, + { url = "https://files.pythonhosted.org/packages/fe/cf/18ef143a81610136d3da8193da9d80bfe1cb548a1e2d1c775f26b23d024a/multidict-6.7.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:3fccb473e87eaa1382689053e4a4618e7ba7b9b9b8d6adf2027ee474597128cd", size = 45456, upload-time = "2026-01-26T02:43:53.893Z" }, + { url = "https://files.pythonhosted.org/packages/a9/65/1caac9d4cd32e8433908683446eebc953e82d22b03d10d41a5f0fefe991b/multidict-6.7.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:b0fa96985700739c4c7853a43c0b3e169360d6855780021bfc6d0f1ce7c123e7", size = 43872, upload-time = "2026-01-26T02:43:55.041Z" }, + { url = "https://files.pythonhosted.org/packages/cf/3b/d6bd75dc4f3ff7c73766e04e705b00ed6dbbaccf670d9e05a12b006f5a21/multidict-6.7.1-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:cb2a55f408c3043e42b40cc8eecd575afa27b7e0b956dfb190de0f8499a57a53", size = 251018, upload-time = "2026-01-26T02:43:56.198Z" }, + { url = "https://files.pythonhosted.org/packages/fd/80/c959c5933adedb9ac15152e4067c702a808ea183a8b64cf8f31af8ad3155/multidict-6.7.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:eb0ce7b2a32d09892b3dd6cc44877a0d02a33241fafca5f25c8b6b62374f8b75", size = 258883, upload-time = "2026-01-26T02:43:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/86/85/7ed40adafea3d4f1c8b916e3b5cc3a8e07dfcdcb9cd72800f4ed3ca1b387/multidict-6.7.1-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c3a32d23520ee37bf327d1e1a656fec76a2edd5c038bf43eddfa0572ec49c60b", size = 242413, upload-time = "2026-01-26T02:43:58.755Z" }, + { url = "https://files.pythonhosted.org/packages/d2/57/b8565ff533e48595503c785f8361ff9a4fde4d67de25c207cd0ba3befd03/multidict-6.7.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:9c90fed18bffc0189ba814749fdcc102b536e83a9f738a9003e569acd540a733", size = 268404, upload-time = "2026-01-26T02:44:00.216Z" }, + { url = "https://files.pythonhosted.org/packages/e0/50/9810c5c29350f7258180dfdcb2e52783a0632862eb334c4896ac717cebcb/multidict-6.7.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:da62917e6076f512daccfbbde27f46fed1c98fee202f0559adec8ee0de67f71a", size = 269456, upload-time = "2026-01-26T02:44:02.202Z" }, + { url = "https://files.pythonhosted.org/packages/f3/8d/5e5be3ced1d12966fefb5c4ea3b2a5b480afcea36406559442c6e31d4a48/multidict-6.7.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:bfde23ef6ed9db7eaee6c37dcec08524cb43903c60b285b172b6c094711b3961", size = 256322, upload-time = "2026-01-26T02:44:03.56Z" }, + { url = "https://files.pythonhosted.org/packages/31/6e/d8a26d81ac166a5592782d208dd90dfdc0a7a218adaa52b45a672b46c122/multidict-6.7.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3758692429e4e32f1ba0df23219cd0b4fc0a52f476726fff9337d1a57676a582", size = 253955, upload-time = "2026-01-26T02:44:04.845Z" }, + { url = "https://files.pythonhosted.org/packages/59/4c/7c672c8aad41534ba619bcd4ade7a0dc87ed6b8b5c06149b85d3dd03f0cd/multidict-6.7.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:398c1478926eca669f2fd6a5856b6de9c0acf23a2cb59a14c0ba5844fa38077e", size = 251254, upload-time = "2026-01-26T02:44:06.133Z" }, + { url = "https://files.pythonhosted.org/packages/7b/bd/84c24de512cbafbdbc39439f74e967f19570ce7924e3007174a29c348916/multidict-6.7.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:c102791b1c4f3ab36ce4101154549105a53dc828f016356b3e3bcae2e3a039d3", size = 252059, upload-time = "2026-01-26T02:44:07.518Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ba/f5449385510825b73d01c2d4087bf6d2fccc20a2d42ac34df93191d3dd03/multidict-6.7.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:a088b62bd733e2ad12c50dad01b7d0166c30287c166e137433d3b410add807a6", size = 263588, upload-time = "2026-01-26T02:44:09.382Z" }, + { url = "https://files.pythonhosted.org/packages/d7/11/afc7c677f68f75c84a69fe37184f0f82fce13ce4b92f49f3db280b7e92b3/multidict-6.7.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:3d51ff4785d58d3f6c91bdbffcb5e1f7ddfda557727043aa20d20ec4f65e324a", size = 259642, upload-time = "2026-01-26T02:44:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/2b/17/ebb9644da78c4ab36403739e0e6e0e30ebb135b9caf3440825001a0bddcb/multidict-6.7.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:fc5907494fccf3e7d3f94f95c91d6336b092b5fc83811720fae5e2765890dfba", size = 251377, upload-time = "2026-01-26T02:44:12.042Z" }, + { url = "https://files.pythonhosted.org/packages/ca/a4/840f5b97339e27846c46307f2530a2805d9d537d8b8bd416af031cad7fa0/multidict-6.7.1-cp312-cp312-win32.whl", hash = "sha256:28ca5ce2fd9716631133d0e9a9b9a745ad7f60bac2bccafb56aa380fc0b6c511", size = 41887, upload-time = "2026-01-26T02:44:14.245Z" }, + { url = "https://files.pythonhosted.org/packages/80/31/0b2517913687895f5904325c2069d6a3b78f66cc641a86a2baf75a05dcbb/multidict-6.7.1-cp312-cp312-win_amd64.whl", hash = "sha256:fcee94dfbd638784645b066074b338bc9cc155d4b4bffa4adce1615c5a426c19", size = 46053, upload-time = "2026-01-26T02:44:15.371Z" }, + { url = "https://files.pythonhosted.org/packages/0c/5b/aba28e4ee4006ae4c7df8d327d31025d760ffa992ea23812a601d226e682/multidict-6.7.1-cp312-cp312-win_arm64.whl", hash = "sha256:ba0a9fb644d0c1a2194cf7ffb043bd852cea63a57f66fbd33959f7dae18517bf", size = 43307, upload-time = "2026-01-26T02:44:16.852Z" }, + { url = "https://files.pythonhosted.org/packages/f2/22/929c141d6c0dba87d3e1d38fbdf1ba8baba86b7776469f2bc2d3227a1e67/multidict-6.7.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:2b41f5fed0ed563624f1c17630cb9941cf2309d4df00e494b551b5f3e3d67a23", size = 76174, upload-time = "2026-01-26T02:44:18.509Z" }, + { url = "https://files.pythonhosted.org/packages/c7/75/bc704ae15fee974f8fccd871305e254754167dce5f9e42d88a2def741a1d/multidict-6.7.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:84e61e3af5463c19b67ced91f6c634effb89ef8bfc5ca0267f954451ed4bb6a2", size = 45116, upload-time = "2026-01-26T02:44:19.745Z" }, + { url = "https://files.pythonhosted.org/packages/79/76/55cd7186f498ed080a18440c9013011eb548f77ae1b297206d030eb1180a/multidict-6.7.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:935434b9853c7c112eee7ac891bc4cb86455aa631269ae35442cb316790c1445", size = 43524, upload-time = "2026-01-26T02:44:21.571Z" }, + { url = "https://files.pythonhosted.org/packages/e9/3c/414842ef8d5a1628d68edee29ba0e5bcf235dbfb3ccd3ea303a7fe8c72ff/multidict-6.7.1-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:432feb25a1cb67fe82a9680b4d65fb542e4635cb3166cd9c01560651ad60f177", size = 249368, upload-time = "2026-01-26T02:44:22.803Z" }, + { url = "https://files.pythonhosted.org/packages/f6/32/befed7f74c458b4a525e60519fe8d87eef72bb1e99924fa2b0f9d97a221e/multidict-6.7.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e82d14e3c948952a1a85503817e038cba5905a3352de76b9a465075d072fba23", size = 256952, upload-time = "2026-01-26T02:44:24.306Z" }, + { url = "https://files.pythonhosted.org/packages/03/d6/c878a44ba877f366630c860fdf74bfb203c33778f12b6ac274936853c451/multidict-6.7.1-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:4cfb48c6ea66c83bcaaf7e4dfa7ec1b6bbcf751b7db85a328902796dfde4c060", size = 240317, upload-time = "2026-01-26T02:44:25.772Z" }, + { url = "https://files.pythonhosted.org/packages/68/49/57421b4d7ad2e9e60e25922b08ceb37e077b90444bde6ead629095327a6f/multidict-6.7.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1d540e51b7e8e170174555edecddbd5538105443754539193e3e1061864d444d", size = 267132, upload-time = "2026-01-26T02:44:27.648Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fe/ec0edd52ddbcea2a2e89e174f0206444a61440b40f39704e64dc807a70bd/multidict-6.7.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:273d23f4b40f3dce4d6c8a821c741a86dec62cded82e1175ba3d99be128147ed", size = 268140, upload-time = "2026-01-26T02:44:29.588Z" }, + { url = "https://files.pythonhosted.org/packages/b0/73/6e1b01cbeb458807aa0831742232dbdd1fa92bfa33f52a3f176b4ff3dc11/multidict-6.7.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9d624335fd4fa1c08a53f8b4be7676ebde19cd092b3895c421045ca87895b429", size = 254277, upload-time = "2026-01-26T02:44:30.902Z" }, + { url = "https://files.pythonhosted.org/packages/6a/b2/5fb8c124d7561a4974c342bc8c778b471ebbeb3cc17df696f034a7e9afe7/multidict-6.7.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:12fad252f8b267cc75b66e8fc51b3079604e8d43a75428ffe193cd9e2195dfd6", size = 252291, upload-time = "2026-01-26T02:44:32.31Z" }, + { url = "https://files.pythonhosted.org/packages/5a/96/51d4e4e06bcce92577fcd488e22600bd38e4fd59c20cb49434d054903bd2/multidict-6.7.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:03ede2a6ffbe8ef936b92cb4529f27f42be7f56afcdab5ab739cd5f27fb1cbf9", size = 250156, upload-time = "2026-01-26T02:44:33.734Z" }, + { url = "https://files.pythonhosted.org/packages/db/6b/420e173eec5fba721a50e2a9f89eda89d9c98fded1124f8d5c675f7a0c0f/multidict-6.7.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:90efbcf47dbe33dcf643a1e400d67d59abeac5db07dc3f27d6bdeae497a2198c", size = 249742, upload-time = "2026-01-26T02:44:35.222Z" }, + { url = "https://files.pythonhosted.org/packages/44/a3/ec5b5bd98f306bc2aa297b8c6f11a46714a56b1e6ef5ebda50a4f5d7c5fb/multidict-6.7.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:5c4b9bfc148f5a91be9244d6264c53035c8a0dcd2f51f1c3c6e30e30ebaa1c84", size = 262221, upload-time = "2026-01-26T02:44:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/cd/f7/e8c0d0da0cd1e28d10e624604e1a36bcc3353aaebdfdc3a43c72bc683a12/multidict-6.7.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:401c5a650f3add2472d1d288c26deebc540f99e2fb83e9525007a74cd2116f1d", size = 258664, upload-time = "2026-01-26T02:44:38.008Z" }, + { url = "https://files.pythonhosted.org/packages/52/da/151a44e8016dd33feed44f730bd856a66257c1ee7aed4f44b649fb7edeb3/multidict-6.7.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:97891f3b1b3ffbded884e2916cacf3c6fc87b66bb0dde46f7357404750559f33", size = 249490, upload-time = "2026-01-26T02:44:39.386Z" }, + { url = "https://files.pythonhosted.org/packages/87/af/a3b86bf9630b732897f6fc3f4c4714b90aa4361983ccbdcd6c0339b21b0c/multidict-6.7.1-cp313-cp313-win32.whl", hash = "sha256:e1c5988359516095535c4301af38d8a8838534158f649c05dd1050222321bcb3", size = 41695, upload-time = "2026-01-26T02:44:41.318Z" }, + { url = "https://files.pythonhosted.org/packages/b2/35/e994121b0e90e46134673422dd564623f93304614f5d11886b1b3e06f503/multidict-6.7.1-cp313-cp313-win_amd64.whl", hash = "sha256:960c83bf01a95b12b08fd54324a4eb1d5b52c88932b5cba5d6e712bb3ed12eb5", size = 45884, upload-time = "2026-01-26T02:44:42.488Z" }, + { url = "https://files.pythonhosted.org/packages/ca/61/42d3e5dbf661242a69c97ea363f2d7b46c567da8eadef8890022be6e2ab0/multidict-6.7.1-cp313-cp313-win_arm64.whl", hash = "sha256:563fe25c678aaba333d5399408f5ec3c383ca5b663e7f774dd179a520b8144df", size = 43122, upload-time = "2026-01-26T02:44:43.664Z" }, + { url = "https://files.pythonhosted.org/packages/6d/b3/e6b21c6c4f314bb956016b0b3ef2162590a529b84cb831c257519e7fde44/multidict-6.7.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:c76c4bec1538375dad9d452d246ca5368ad6e1c9039dadcf007ae59c70619ea1", size = 83175, upload-time = "2026-01-26T02:44:44.894Z" }, + { url = "https://files.pythonhosted.org/packages/fb/76/23ecd2abfe0957b234f6c960f4ade497f55f2c16aeb684d4ecdbf1c95791/multidict-6.7.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:57b46b24b5d5ebcc978da4ec23a819a9402b4228b8a90d9c656422b4bdd8a963", size = 48460, upload-time = "2026-01-26T02:44:46.106Z" }, + { url = "https://files.pythonhosted.org/packages/c4/57/a0ed92b23f3a042c36bc4227b72b97eca803f5f1801c1ab77c8a212d455e/multidict-6.7.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:e954b24433c768ce78ab7929e84ccf3422e46deb45a4dc9f93438f8217fa2d34", size = 46930, upload-time = "2026-01-26T02:44:47.278Z" }, + { url = "https://files.pythonhosted.org/packages/b5/66/02ec7ace29162e447f6382c495dc95826bf931d3818799bbef11e8f7df1a/multidict-6.7.1-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3bd231490fa7217cc832528e1cd8752a96f0125ddd2b5749390f7c3ec8721b65", size = 242582, upload-time = "2026-01-26T02:44:48.604Z" }, + { url = "https://files.pythonhosted.org/packages/58/18/64f5a795e7677670e872673aca234162514696274597b3708b2c0d276cce/multidict-6.7.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:253282d70d67885a15c8a7716f3a73edf2d635793ceda8173b9ecc21f2fb8292", size = 250031, upload-time = "2026-01-26T02:44:50.544Z" }, + { url = "https://files.pythonhosted.org/packages/c8/ed/e192291dbbe51a8290c5686f482084d31bcd9d09af24f63358c3d42fd284/multidict-6.7.1-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:0b4c48648d7649c9335cf1927a8b87fa692de3dcb15faa676c6a6f1f1aabda43", size = 228596, upload-time = "2026-01-26T02:44:51.951Z" }, + { url = "https://files.pythonhosted.org/packages/1e/7e/3562a15a60cf747397e7f2180b0a11dc0c38d9175a650e75fa1b4d325e15/multidict-6.7.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:98bc624954ec4d2c7cb074b8eefc2b5d0ce7d482e410df446414355d158fe4ca", size = 257492, upload-time = "2026-01-26T02:44:53.902Z" }, + { url = "https://files.pythonhosted.org/packages/24/02/7d0f9eae92b5249bb50ac1595b295f10e263dd0078ebb55115c31e0eaccd/multidict-6.7.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:1b99af4d9eec0b49927b4402bcbb58dea89d3e0db8806a4086117019939ad3dd", size = 255899, upload-time = "2026-01-26T02:44:55.316Z" }, + { url = "https://files.pythonhosted.org/packages/00/e3/9b60ed9e23e64c73a5cde95269ef1330678e9c6e34dd4eb6b431b85b5a10/multidict-6.7.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:6aac4f16b472d5b7dc6f66a0d49dd57b0e0902090be16594dc9ebfd3d17c47e7", size = 247970, upload-time = "2026-01-26T02:44:56.783Z" }, + { url = "https://files.pythonhosted.org/packages/3e/06/538e58a63ed5cfb0bd4517e346b91da32fde409d839720f664e9a4ae4f9d/multidict-6.7.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:21f830fe223215dffd51f538e78c172ed7c7f60c9b96a2bf05c4848ad49921c3", size = 245060, upload-time = "2026-01-26T02:44:58.195Z" }, + { url = "https://files.pythonhosted.org/packages/b2/2f/d743a3045a97c895d401e9bd29aaa09b94f5cbdf1bd561609e5a6c431c70/multidict-6.7.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:f5dd81c45b05518b9aa4da4aa74e1c93d715efa234fd3e8a179df611cc85e5f4", size = 235888, upload-time = "2026-01-26T02:44:59.57Z" }, + { url = "https://files.pythonhosted.org/packages/38/83/5a325cac191ab28b63c52f14f1131f3b0a55ba3b9aa65a6d0bf2a9b921a0/multidict-6.7.1-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:eb304767bca2bb92fb9c5bd33cedc95baee5bb5f6c88e63706533a1c06ad08c8", size = 243554, upload-time = "2026-01-26T02:45:01.054Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/9d2327086bd15da2725ef6aae624208e2ef828ed99892b17f60c344e57ed/multidict-6.7.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c9035dde0f916702850ef66460bc4239d89d08df4d02023a5926e7446724212c", size = 252341, upload-time = "2026-01-26T02:45:02.484Z" }, + { url = "https://files.pythonhosted.org/packages/e8/2c/2a1aa0280cf579d0f6eed8ee5211c4f1730bd7e06c636ba2ee6aafda302e/multidict-6.7.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:af959b9beeb66c822380f222f0e0a1889331597e81f1ded7f374f3ecb0fd6c52", size = 246391, upload-time = "2026-01-26T02:45:03.862Z" }, + { url = "https://files.pythonhosted.org/packages/e5/03/7ca022ffc36c5a3f6e03b179a5ceb829be9da5783e6fe395f347c0794680/multidict-6.7.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:41f2952231456154ee479651491e94118229844dd7226541788be783be2b5108", size = 243422, upload-time = "2026-01-26T02:45:05.296Z" }, + { url = "https://files.pythonhosted.org/packages/dc/1d/b31650eab6c5778aceed46ba735bd97f7c7d2f54b319fa916c0f96e7805b/multidict-6.7.1-cp313-cp313t-win32.whl", hash = "sha256:df9f19c28adcb40b6aae30bbaa1478c389efd50c28d541d76760199fc1037c32", size = 47770, upload-time = "2026-01-26T02:45:06.754Z" }, + { url = "https://files.pythonhosted.org/packages/ac/5b/2d2d1d522e51285bd61b1e20df8f47ae1a9d80839db0b24ea783b3832832/multidict-6.7.1-cp313-cp313t-win_amd64.whl", hash = "sha256:d54ecf9f301853f2c5e802da559604b3e95bb7a3b01a9c295c6ee591b9882de8", size = 53109, upload-time = "2026-01-26T02:45:08.044Z" }, + { url = "https://files.pythonhosted.org/packages/3d/a3/cc409ba012c83ca024a308516703cf339bdc4b696195644a7215a5164a24/multidict-6.7.1-cp313-cp313t-win_arm64.whl", hash = "sha256:5a37ca18e360377cfda1d62f5f382ff41f2b8c4ccb329ed974cc2e1643440118", size = 45573, upload-time = "2026-01-26T02:45:09.349Z" }, + { url = "https://files.pythonhosted.org/packages/91/cc/db74228a8be41884a567e88a62fd589a913708fcf180d029898c17a9a371/multidict-6.7.1-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:8f333ec9c5eb1b7105e3b84b53141e66ca05a19a605368c55450b6ba208cb9ee", size = 75190, upload-time = "2026-01-26T02:45:10.651Z" }, + { url = "https://files.pythonhosted.org/packages/d5/22/492f2246bb5b534abd44804292e81eeaf835388901f0c574bac4eeec73c5/multidict-6.7.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:a407f13c188f804c759fc6a9f88286a565c242a76b27626594c133b82883b5c2", size = 44486, upload-time = "2026-01-26T02:45:11.938Z" }, + { url = "https://files.pythonhosted.org/packages/f1/4f/733c48f270565d78b4544f2baddc2fb2a245e5a8640254b12c36ac7ac68e/multidict-6.7.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0e161ddf326db5577c3a4cc2d8648f81456e8a20d40415541587a71620d7a7d1", size = 43219, upload-time = "2026-01-26T02:45:14.346Z" }, + { url = "https://files.pythonhosted.org/packages/24/bb/2c0c2287963f4259c85e8bcbba9182ced8d7fca65c780c38e99e61629d11/multidict-6.7.1-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:1e3a8bb24342a8201d178c3b4984c26ba81a577c80d4d525727427460a50c22d", size = 245132, upload-time = "2026-01-26T02:45:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/a7/f9/44d4b3064c65079d2467888794dea218d1601898ac50222ab8a9a8094460/multidict-6.7.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:97231140a50f5d447d3164f994b86a0bed7cd016e2682f8650d6a9158e14fd31", size = 252420, upload-time = "2026-01-26T02:45:17.293Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/78f7275e73fa17b24c9a51b0bd9d73ba64bb32d0ed51b02a746eb876abe7/multidict-6.7.1-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:6b10359683bd8806a200fd2909e7c8ca3a7b24ec1d8132e483d58e791d881048", size = 233510, upload-time = "2026-01-26T02:45:19.356Z" }, + { url = "https://files.pythonhosted.org/packages/4b/25/8167187f62ae3cbd52da7893f58cb036b47ea3fb67138787c76800158982/multidict-6.7.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:283ddac99f7ac25a4acadbf004cb5ae34480bbeb063520f70ce397b281859362", size = 264094, upload-time = "2026-01-26T02:45:20.834Z" }, + { url = "https://files.pythonhosted.org/packages/a1/e7/69a3a83b7b030cf283fb06ce074a05a02322359783424d7edf0f15fe5022/multidict-6.7.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:538cec1e18c067d0e6103aa9a74f9e832904c957adc260e61cd9d8cf0c3b3d37", size = 260786, upload-time = "2026-01-26T02:45:22.818Z" }, + { url = "https://files.pythonhosted.org/packages/fe/3b/8ec5074bcfc450fe84273713b4b0a0dd47c0249358f5d82eb8104ffe2520/multidict-6.7.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7eee46ccb30ff48a1e35bb818cc90846c6be2b68240e42a78599166722cea709", size = 248483, upload-time = "2026-01-26T02:45:24.368Z" }, + { url = "https://files.pythonhosted.org/packages/48/5a/d5a99e3acbca0e29c5d9cba8f92ceb15dce78bab963b308ae692981e3a5d/multidict-6.7.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:fa263a02f4f2dd2d11a7b1bb4362aa7cb1049f84a9235d31adf63f30143469a0", size = 248403, upload-time = "2026-01-26T02:45:25.982Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/e58cd31f6c7d5102f2a4bf89f96b9cf7e00b6c6f3d04ecc44417c00a5a3c/multidict-6.7.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:2e1425e2f99ec5bd36c15a01b690a1a2456209c5deed58f95469ffb46039ccbb", size = 240315, upload-time = "2026-01-26T02:45:27.487Z" }, + { url = "https://files.pythonhosted.org/packages/94/33/1cd210229559cb90b6786c30676bb0c58249ff42f942765f88793b41fdce/multidict-6.7.1-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:497394b3239fc6f0e13a78a3e1b61296e72bf1c5f94b4c4eb80b265c37a131cd", size = 245528, upload-time = "2026-01-26T02:45:28.991Z" }, + { url = "https://files.pythonhosted.org/packages/64/f2/6e1107d226278c876c783056b7db43d800bb64c6131cec9c8dfb6903698e/multidict-6.7.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:233b398c29d3f1b9676b4b6f75c518a06fcb2ea0b925119fb2c1bc35c05e1601", size = 258784, upload-time = "2026-01-26T02:45:30.503Z" }, + { url = "https://files.pythonhosted.org/packages/4d/c1/11f664f14d525e4a1b5327a82d4de61a1db604ab34c6603bb3c2cc63ad34/multidict-6.7.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:93b1818e4a6e0930454f0f2af7dfce69307ca03cdcfb3739bf4d91241967b6c1", size = 251980, upload-time = "2026-01-26T02:45:32.603Z" }, + { url = "https://files.pythonhosted.org/packages/e1/9f/75a9ac888121d0c5bbd4ecf4eead45668b1766f6baabfb3b7f66a410e231/multidict-6.7.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:f33dc2a3abe9249ea5d8360f969ec7f4142e7ac45ee7014d8f8d5acddf178b7b", size = 243602, upload-time = "2026-01-26T02:45:34.043Z" }, + { url = "https://files.pythonhosted.org/packages/9a/e7/50bf7b004cc8525d80dbbbedfdc7aed3e4c323810890be4413e589074032/multidict-6.7.1-cp314-cp314-win32.whl", hash = "sha256:3ab8b9d8b75aef9df299595d5388b14530839f6422333357af1339443cff777d", size = 40930, upload-time = "2026-01-26T02:45:36.278Z" }, + { url = "https://files.pythonhosted.org/packages/e0/bf/52f25716bbe93745595800f36fb17b73711f14da59ed0bb2eba141bc9f0f/multidict-6.7.1-cp314-cp314-win_amd64.whl", hash = "sha256:5e01429a929600e7dab7b166062d9bb54a5eed752384c7384c968c2afab8f50f", size = 45074, upload-time = "2026-01-26T02:45:37.546Z" }, + { url = "https://files.pythonhosted.org/packages/97/ab/22803b03285fa3a525f48217963da3a65ae40f6a1b6f6cf2768879e208f9/multidict-6.7.1-cp314-cp314-win_arm64.whl", hash = "sha256:4885cb0e817aef5d00a2e8451d4665c1808378dc27c2705f1bf4ef8505c0d2e5", size = 42471, upload-time = "2026-01-26T02:45:38.889Z" }, + { url = "https://files.pythonhosted.org/packages/e0/6d/f9293baa6146ba9507e360ea0292b6422b016907c393e2f63fc40ab7b7b5/multidict-6.7.1-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:0458c978acd8e6ea53c81eefaddbbee9c6c5e591f41b3f5e8e194780fe026581", size = 82401, upload-time = "2026-01-26T02:45:40.254Z" }, + { url = "https://files.pythonhosted.org/packages/7a/68/53b5494738d83558d87c3c71a486504d8373421c3e0dbb6d0db48ad42ee0/multidict-6.7.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:c0abd12629b0af3cf590982c0b413b1e7395cd4ec026f30986818ab95bfaa94a", size = 48143, upload-time = "2026-01-26T02:45:41.635Z" }, + { url = "https://files.pythonhosted.org/packages/37/e8/5284c53310dcdc99ce5d66563f6e5773531a9b9fe9ec7a615e9bc306b05f/multidict-6.7.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:14525a5f61d7d0c94b368a42cff4c9a4e7ba2d52e2672a7b23d84dc86fb02b0c", size = 46507, upload-time = "2026-01-26T02:45:42.99Z" }, + { url = "https://files.pythonhosted.org/packages/e4/fc/6800d0e5b3875568b4083ecf5f310dcf91d86d52573160834fb4bfcf5e4f/multidict-6.7.1-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:17307b22c217b4cf05033dabefe68255a534d637c6c9b0cc8382718f87be4262", size = 239358, upload-time = "2026-01-26T02:45:44.376Z" }, + { url = "https://files.pythonhosted.org/packages/41/75/4ad0973179361cdf3a113905e6e088173198349131be2b390f9fa4da5fc6/multidict-6.7.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:7a7e590ff876a3eaf1c02a4dfe0724b6e69a9e9de6d8f556816f29c496046e59", size = 246884, upload-time = "2026-01-26T02:45:47.167Z" }, + { url = "https://files.pythonhosted.org/packages/c3/9c/095bb28b5da139bd41fb9a5d5caff412584f377914bd8787c2aa98717130/multidict-6.7.1-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:5fa6a95dfee63893d80a34758cd0e0c118a30b8dcb46372bf75106c591b77889", size = 225878, upload-time = "2026-01-26T02:45:48.698Z" }, + { url = "https://files.pythonhosted.org/packages/07/d0/c0a72000243756e8f5a277b6b514fa005f2c73d481b7d9e47cd4568aa2e4/multidict-6.7.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a0543217a6a017692aa6ae5cc39adb75e587af0f3a82288b1492eb73dd6cc2a4", size = 253542, upload-time = "2026-01-26T02:45:50.164Z" }, + { url = "https://files.pythonhosted.org/packages/c0/6b/f69da15289e384ecf2a68837ec8b5ad8c33e973aa18b266f50fe55f24b8c/multidict-6.7.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f99fe611c312b3c1c0ace793f92464d8cd263cc3b26b5721950d977b006b6c4d", size = 252403, upload-time = "2026-01-26T02:45:51.779Z" }, + { url = "https://files.pythonhosted.org/packages/a2/76/b9669547afa5a1a25cd93eaca91c0da1c095b06b6d2d8ec25b713588d3a1/multidict-6.7.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9004d8386d133b7e6135679424c91b0b854d2d164af6ea3f289f8f2761064609", size = 244889, upload-time = "2026-01-26T02:45:53.27Z" }, + { url = "https://files.pythonhosted.org/packages/7e/a9/a50d2669e506dad33cfc45b5d574a205587b7b8a5f426f2fbb2e90882588/multidict-6.7.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:e628ef0e6859ffd8273c69412a2465c4be4a9517d07261b33334b5ec6f3c7489", size = 241982, upload-time = "2026-01-26T02:45:54.919Z" }, + { url = "https://files.pythonhosted.org/packages/c5/bb/1609558ad8b456b4827d3c5a5b775c93b87878fd3117ed3db3423dfbce1b/multidict-6.7.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:841189848ba629c3552035a6a7f5bf3b02eb304e9fea7492ca220a8eda6b0e5c", size = 232415, upload-time = "2026-01-26T02:45:56.981Z" }, + { url = "https://files.pythonhosted.org/packages/d8/59/6f61039d2aa9261871e03ab9dc058a550d240f25859b05b67fd70f80d4b3/multidict-6.7.1-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:ce1bbd7d780bb5a0da032e095c951f7014d6b0a205f8318308140f1a6aba159e", size = 240337, upload-time = "2026-01-26T02:45:58.698Z" }, + { url = "https://files.pythonhosted.org/packages/a1/29/fdc6a43c203890dc2ae9249971ecd0c41deaedfe00d25cb6564b2edd99eb/multidict-6.7.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:b26684587228afed0d50cf804cc71062cc9c1cdf55051c4c6345d372947b268c", size = 248788, upload-time = "2026-01-26T02:46:00.862Z" }, + { url = "https://files.pythonhosted.org/packages/a9/14/a153a06101323e4cf086ecee3faadba52ff71633d471f9685c42e3736163/multidict-6.7.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:9f9af11306994335398293f9958071019e3ab95e9a707dc1383a35613f6abcb9", size = 242842, upload-time = "2026-01-26T02:46:02.824Z" }, + { url = "https://files.pythonhosted.org/packages/41/5f/604ae839e64a4a6efc80db94465348d3b328ee955e37acb24badbcd24d83/multidict-6.7.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:b4938326284c4f1224178a560987b6cf8b4d38458b113d9b8c1db1a836e640a2", size = 240237, upload-time = "2026-01-26T02:46:05.898Z" }, + { url = "https://files.pythonhosted.org/packages/5f/60/c3a5187bf66f6fb546ff4ab8fb5a077cbdd832d7b1908d4365c7f74a1917/multidict-6.7.1-cp314-cp314t-win32.whl", hash = "sha256:98655c737850c064a65e006a3df7c997cd3b220be4ec8fe26215760b9697d4d7", size = 48008, upload-time = "2026-01-26T02:46:07.468Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f7/addf1087b860ac60e6f382240f64fb99f8bfb532bb06f7c542b83c29ca61/multidict-6.7.1-cp314-cp314t-win_amd64.whl", hash = "sha256:497bde6223c212ba11d462853cfa4f0ae6ef97465033e7dc9940cdb3ab5b48e5", size = 53542, upload-time = "2026-01-26T02:46:08.809Z" }, + { url = "https://files.pythonhosted.org/packages/4c/81/4629d0aa32302ef7b2ec65c75a728cc5ff4fa410c50096174c1632e70b3e/multidict-6.7.1-cp314-cp314t-win_arm64.whl", hash = "sha256:2bbd113e0d4af5db41d5ebfe9ccaff89de2120578164f86a5d17d5a576d1e5b2", size = 44719, upload-time = "2026-01-26T02:46:11.146Z" }, + { 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 = "multiprocess" +version = "0.70.19" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "dill" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a2/f2/e783ac7f2aeeed14e9e12801f22529cc7e6b7ab80928d6dcce4e9f00922d/multiprocess-0.70.19.tar.gz", hash = "sha256:952021e0e6c55a4a9fe4cd787895b86e239a40e76802a789d6305398d3975897", size = 2079989, upload-time = "2026-01-19T06:47:39.744Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e3/45/8004d1e6b9185c1a444d6b55ac5682acf9d98035e54386d967366035a03a/multiprocess-0.70.19-py310-none-any.whl", hash = "sha256:97404393419dcb2a8385910864eedf47a3cadf82c66345b44f036420eb0b5d87", size = 134948, upload-time = "2026-01-19T06:47:32.325Z" }, + { url = "https://files.pythonhosted.org/packages/86/c2/dec9722dc3474c164a0b6bcd9a7ed7da542c98af8cabce05374abab35edd/multiprocess-0.70.19-py311-none-any.whl", hash = "sha256:928851ae7973aea4ce0eaf330bbdafb2e01398a91518d5c8818802845564f45c", size = 144457, upload-time = "2026-01-19T06:47:33.711Z" }, + { url = "https://files.pythonhosted.org/packages/71/70/38998b950a97ea279e6bd657575d22d1a2047256caf707d9a10fbce4f065/multiprocess-0.70.19-py312-none-any.whl", hash = "sha256:3a56c0e85dd5025161bac5ce138dcac1e49174c7d8e74596537e729fd5c53c28", size = 150281, upload-time = "2026-01-19T06:47:35.037Z" }, + { url = "https://files.pythonhosted.org/packages/7f/74/d2c27e03cb84251dfe7249b8e82923643c6d48fa4883b9476b025e7dc7eb/multiprocess-0.70.19-py313-none-any.whl", hash = "sha256:8d5eb4ec5017ba2fab4e34a747c6d2c2b6fecfe9e7236e77988db91580ada952", size = 156414, upload-time = "2026-01-19T06:47:35.915Z" }, + { url = "https://files.pythonhosted.org/packages/a0/61/af9115673a5870fd885247e2f1b68c4f1197737da315b520a91c757a861a/multiprocess-0.70.19-py314-none-any.whl", hash = "sha256:e8cc7fbdff15c0613f0a1f1f8744bef961b0a164c0ca29bdff53e9d2d93c5e5f", size = 160318, upload-time = "2026-01-19T06:47:37.497Z" }, + { url = "https://files.pythonhosted.org/packages/7e/82/69e539c4c2027f1e1697e09aaa2449243085a0edf81ae2c6341e84d769b6/multiprocess-0.70.19-py39-none-any.whl", hash = "sha256:0d4b4397ed669d371c81dcd1ef33fd384a44d6c3de1bd0ca7ac06d837720d3c5", size = 133477, upload-time = "2026-01-19T06:47:38.619Z" }, +] + +[[package]] +name = "networkx" +version = "3.6.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/6a/51/63fe664f3908c97be9d2e4f1158eb633317598cfa6e1fc14af5383f17512/networkx-3.6.1.tar.gz", hash = "sha256:26b7c357accc0c8cde558ad486283728b65b6a95d85ee1cd66bafab4c8168509", size = 2517025, upload-time = "2025-12-08T17:02:39.908Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/9e/c9/b2622292ea83fbb4ec318f5b9ab867d0a28ab43c5717bb85b0a5f6b3b0a4/networkx-3.6.1-py3-none-any.whl", hash = "sha256:d47fbf302e7d9cbbb9e2555a0d267983d2aa476bac30e90dfbe5669bd57f3762", size = 2068504, upload-time = "2025-12-08T17:02:38.159Z" }, +] + +[[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 = "nvidia-cublas" +version = "13.1.0.3" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/a5/fce49e2ae977e0ccc084e5adafceb4f0ac0c8333cb6863501618a7277f67/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:c86fc7f7ae36d7528288c5d88098edcb7b02c633d262e7ddbb86b0ad91be5df2", size = 542851226, upload-time = "2025-10-09T08:59:04.818Z" }, + { url = "https://files.pythonhosted.org/packages/e7/44/423ac00af4dd95a5aeb27207e2c0d9b7118702149bf4704c3ddb55bb7429/nvidia_cublas-13.1.0.3-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:ee8722c1f0145ab246bccb9e452153b5e0515fd094c3678df50b2a0888b8b171", size = 423133236, upload-time = "2025-10-09T08:59:32.536Z" }, +] + +[[package]] +name = "nvidia-cuda-cupti" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/2a/2a/80353b103fc20ce05ef51e928daed4b6015db4aaa9162ed0997090fe2250/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_aarch64.whl", hash = "sha256:796bd679890ee55fb14a94629b698b6db54bcfd833d391d5e94017dd9d7d3151", size = 10310827, upload-time = "2025-09-04T08:26:42.012Z" }, + { url = "https://files.pythonhosted.org/packages/33/6d/737d164b4837a9bbd202f5ae3078975f0525a55730fe871d8ed4e3b952b0/nvidia_cuda_cupti-13.0.85-py3-none-manylinux_2_25_x86_64.whl", hash = "sha256:4eb01c08e859bf924d222250d2e8f8b8ff6d3db4721288cf35d14252a4d933c8", size = 10715597, upload-time = "2025-09-04T08:26:51.312Z" }, +] + +[[package]] +name = "nvidia-cuda-nvrtc" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c3/68/483a78f5e8f31b08fb1bb671559968c0ca3a065ac7acabfc7cee55214fd6/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:ad9b6d2ead2435f11cbb6868809d2adeeee302e9bb94bcf0539c7a40d80e8575", size = 90215200, upload-time = "2025-09-04T08:28:44.204Z" }, + { url = "https://files.pythonhosted.org/packages/b7/dc/6bb80850e0b7edd6588d560758f17e0550893a1feaf436807d64d2da040f/nvidia_cuda_nvrtc-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:d27f20a0ca67a4bb34268a5e951033496c5b74870b868bacd046b1b8e0c3267b", size = 43015449, upload-time = "2025-09-04T08:28:20.239Z" }, +] + +[[package]] +name = "nvidia-cuda-runtime" +version = "13.0.96" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/87/4f/17d7b9b8e285199c58ce28e31b5c5bbaa4d8271af06a89b6405258245de2/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:ef9bcbe90493a2b9d810e43d249adb3d02e98dd30200d86607d8d02687c43f55", size = 2261060, upload-time = "2025-10-09T08:55:15.78Z" }, + { url = "https://files.pythonhosted.org/packages/2e/24/d1558f3b68b1d26e706813b1d10aa1d785e4698c425af8db8edc3dced472/nvidia_cuda_runtime-13.0.96-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7f82250d7782aa23b6cfe765ecc7db554bd3c2870c43f3d1821f1d18aebf0548", size = 2243632, upload-time = "2025-10-09T08:55:36.117Z" }, +] + +[[package]] +name = "nvidia-cudnn-cu13" +version = "9.19.0.56" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f1/84/26025437c1e6b61a707442184fa0c03d083b661adf3a3eecfd6d21677740/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:6ed29ffaee1176c612daf442e4dd6cfeb6a0caa43ddcbeb59da94953030b1be4", size = 433781201, upload-time = "2026-02-03T20:40:53.805Z" }, + { url = "https://files.pythonhosted.org/packages/a3/22/0b4b932655d17a6da1b92fa92ab12844b053bb2ac2475e179ba6f043da1e/nvidia_cudnn_cu13-9.19.0.56-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:d20e1734305e9d68889a96e3f35094d733ff1f83932ebe462753973e53a572bf", size = 366066321, upload-time = "2026-02-03T20:44:52.837Z" }, +] + +[[package]] +name = "nvidia-cufft" +version = "12.0.0.61" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/8b/ae/f417a75c0259e85c1d2f83ca4e960289a5f814ed0cea74d18c353d3e989d/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:2708c852ef8cd89d1d2068bdbece0aa188813a0c934db3779b9b1faa8442e5f5", size = 214053554, upload-time = "2025-09-04T08:31:38.196Z" }, + { url = "https://files.pythonhosted.org/packages/a8/2f/7b57e29836ea8714f81e9898409196f47d772d5ddedddf1592eadb8ab743/nvidia_cufft-12.0.0.61-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:6c44f692dce8fd5ffd3e3df134b6cdb9c2f72d99cf40b62c32dde45eea9ddad3", size = 214085489, upload-time = "2025-09-04T08:31:56.044Z" }, +] + +[[package]] +name = "nvidia-cufile" +version = "1.15.1.6" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/70/4f193de89a48b71714e74602ee14d04e4019ad36a5a9f20c425776e72cd6/nvidia_cufile-1.15.1.6-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:08a3ecefae5a01c7f5117351c64f17c7c62efa5fffdbe24fc7d298da19cd0b44", size = 1223672, upload-time = "2025-09-04T08:32:22.779Z" }, + { url = "https://files.pythonhosted.org/packages/ab/73/cc4a14c9813a8a0d509417cf5f4bdaba76e924d58beb9864f5a7baceefbf/nvidia_cufile-1.15.1.6-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:bdc0deedc61f548bddf7733bdc216456c2fdb101d020e1ab4b88d232d5e2f6d1", size = 1136992, upload-time = "2025-09-04T08:32:14.119Z" }, +] + +[[package]] +name = "nvidia-curand" +version = "10.4.0.35" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1e/72/7c2ae24fb6b63a32e6ae5d241cc65263ea18d08802aaae087d9f013335a2/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:133df5a7509c3e292aaa2b477afd0194f06ce4ea24d714d616ff36439cee349a", size = 61962106, upload-time = "2025-08-04T10:21:41.128Z" }, + { url = "https://files.pythonhosted.org/packages/a5/9f/be0a41ca4a4917abf5cb9ae0daff1a6060cc5de950aec0396de9f3b52bc5/nvidia_curand-10.4.0.35-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:1aee33a5da6e1db083fe2b90082def8915f30f3248d5896bcec36a579d941bfc", size = 59544258, upload-time = "2025-08-04T10:22:03.992Z" }, +] + +[[package]] +name = "nvidia-cusolver" +version = "12.0.4.66" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-cublas", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-cusparse", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c8/c3/b30c9e935fc01e3da443ec0116ed1b2a009bb867f5324d3f2d7e533e776b/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_aarch64.whl", hash = "sha256:02c2457eaa9e39de20f880f4bd8820e6a1cfb9f9a34f820eb12a155aa5bc92d2", size = 223467760, upload-time = "2025-09-04T08:33:04.222Z" }, + { url = "https://files.pythonhosted.org/packages/5f/67/cba3777620cdacb99102da4042883709c41c709f4b6323c10781a9c3aa34/nvidia_cusolver-12.0.4.66-py3-none-manylinux_2_27_x86_64.whl", hash = "sha256:0a759da5dea5c0ea10fd307de75cdeb59e7ea4fcb8add0924859b944babf1112", size = 200941980, upload-time = "2025-09-04T08:33:22.767Z" }, +] + +[[package]] +name = "nvidia-cusparse" +version = "12.6.3.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "nvidia-nvjitlink", marker = "sys_platform != 'emscripten' and sys_platform != 'win32'" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/f8/94/5c26f33738ae35276672f12615a64bd008ed5be6d1ebcb23579285d960a9/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:80bcc4662f23f1054ee334a15c72b8940402975e0eab63178fc7e670aa59472c", size = 162155568, upload-time = "2025-09-04T08:33:42.864Z" }, + { url = "https://files.pythonhosted.org/packages/fa/18/623c77619c31d62efd55302939756966f3ecc8d724a14dab2b75f1508850/nvidia_cusparse-12.6.3.3-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:2b3c89c88d01ee0e477cb7f82ef60a11a4bcd57b6b87c33f789350b59759360b", size = 145942937, upload-time = "2025-09-04T08:33:58.029Z" }, +] + +[[package]] +name = "nvidia-cusparselt-cu13" +version = "0.8.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/46/10/8dcd1175260706a2fc92a16a52e306b71d4c1ea0b0cc4a9484183399818a/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_aarch64.whl", hash = "sha256:400c6ed1cf6780fc6efedd64ec9f1345871767e6a1a0a552a1ea0578117ea77c", size = 220791277, upload-time = "2025-08-13T19:22:40.982Z" }, + { url = "https://files.pythonhosted.org/packages/fd/53/43b0d71f4e702fa9733f8b4571fdca50a8813f1e450b656c239beff12315/nvidia_cusparselt_cu13-0.8.0-py3-none-manylinux2014_x86_64.whl", hash = "sha256:25e30a8a7323935d4ad0340b95a0b69926eee755767e8e0b1cf8dd85b197d3fd", size = 169884119, upload-time = "2025-08-13T19:23:41.967Z" }, +] + +[[package]] +name = "nvidia-nccl-cu13" +version = "2.28.9" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/39/55/1920646a2e43ffd4fc958536b276197ed740e9e0c54105b4bb3521591fc7/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_aarch64.whl", hash = "sha256:01c873ba1626b54caa12272ed228dc5b2781545e0ae8ba3f432a8ef1c6d78643", size = 196561677, upload-time = "2025-11-18T05:49:03.45Z" }, + { url = "https://files.pythonhosted.org/packages/b0/b4/878fefaad5b2bcc6fcf8d474a25e3e3774bc5133e4b58adff4d0bca238bc/nvidia_nccl_cu13-2.28.9-py3-none-manylinux_2_18_x86_64.whl", hash = "sha256:e4553a30f34195f3fa1da02a6da3d6337d28f2003943aa0a3d247bbc25fefc42", size = 196493177, upload-time = "2025-11-18T05:49:17.677Z" }, +] + +[[package]] +name = "nvidia-nvjitlink" +version = "13.0.88" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/56/7a/123e033aaff487c77107195fa5a2b8686795ca537935a24efae476c41f05/nvidia_nvjitlink-13.0.88-py3-none-manylinux2010_x86_64.manylinux_2_12_x86_64.whl", hash = "sha256:13a74f429e23b921c1109976abefacc69835f2f433ebd323d3946e11d804e47b", size = 40713933, upload-time = "2025-09-04T08:35:43.553Z" }, + { url = "https://files.pythonhosted.org/packages/ab/2c/93c5250e64df4f894f1cbb397c6fd71f79813f9fd79d7cd61de3f97b3c2d/nvidia_nvjitlink-13.0.88-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:e931536ccc7d467a98ba1d8b89ff7fa7f1fa3b13f2b0069118cd7f47bff07d0c", size = 38768748, upload-time = "2025-09-04T08:35:20.008Z" }, +] + +[[package]] +name = "nvidia-nvshmem-cu13" +version = "3.4.5" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/dc/0f/05cc9c720236dcd2db9c1ab97fff629e96821be2e63103569da0c9b72f19/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:6dc2a197f38e5d0376ad52cd1a2a3617d3cdc150fd5966f4aee9bcebb1d68fe9", size = 60215947, upload-time = "2025-09-06T00:32:20.022Z" }, + { url = "https://files.pythonhosted.org/packages/3c/35/a9bf80a609e74e3b000fef598933235c908fcefcef9026042b8e6dfde2a9/nvidia_nvshmem_cu13-3.4.5-py3-none-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:290f0a2ee94c9f3687a02502f3b9299a9f9fe826e6d0287ee18482e78d495b80", size = 60412546, upload-time = "2025-09-06T00:32:41.564Z" }, +] + +[[package]] +name = "nvidia-nvtx" +version = "13.0.85" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/c2/f3/d86c845465a2723ad7e1e5c36dcd75ddb82898b3f53be47ebd429fb2fa5d/nvidia_nvtx-13.0.85-py3-none-manylinux1_x86_64.manylinux_2_5_x86_64.whl", hash = "sha256:4936d1d6780fbe68db454f5e72a42ff64d1fd6397df9f363ae786930fd5c1cd4", size = 148047, upload-time = "2025-09-04T08:29:01.761Z" }, + { url = "https://files.pythonhosted.org/packages/a8/64/3708a90d1ebe202ffdeb7185f878a3c84d15c2b2c31858da2ce0583e2def/nvidia_nvtx-13.0.85-py3-none-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:cb7780edb6b14107373c835bf8b72e7a178bac7367e23da7acb108f973f157a6", size = 148878, upload-time = "2025-09-04T08:28:53.627Z" }, +] + +[[package]] +name = "onnxruntime" +version = "1.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "flatbuffers" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "protobuf" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/c0/52/8b2a10e8dedf5d486332bc2b3bca0b1ed8049c0b9e4a5cced95413aadfdd/onnxruntime-1.25.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:66e52f7a30d1f780a34aa84d68a0a04d382d9f5b141884ecbf45b7566b9fbde9", size = 17770987, upload-time = "2026-04-27T22:00:47.985Z" }, + { url = "https://files.pythonhosted.org/packages/3f/87/a424d2867477c42ef8c60172709281120797f7b0f1fd33cc36b24329c825/onnxruntime-1.25.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:a5f41779f044d1ff75593df5c10a4d311bc82563687796d5218e2685b8f9da25", size = 15871829, upload-time = "2026-04-27T21:59:39.088Z" }, + { url = "https://files.pythonhosted.org/packages/d4/55/7819e64c515f17c86005447ede8122b974ca851255a94125e2119376f0f8/onnxruntime-1.25.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:905409e9eb2ef87f8226e073f56e71faf731c3e480ebd34952cf953730e4a4ff", size = 18024586, upload-time = "2026-04-27T22:00:05.359Z" }, + { url = "https://files.pythonhosted.org/packages/89/36/b4f3eb5e95c66389aafd490950b5255e87c9333742cf90516eb50898e1dc/onnxruntime-1.25.1-cp312-cp312-win_amd64.whl", hash = "sha256:d4097b75b77486bb45835a8ed25b9a67976040ec6c258aeabae6aadfbdd1201c", size = 12905112, upload-time = "2026-04-27T22:00:36.478Z" }, + { url = "https://files.pythonhosted.org/packages/38/fa/e5c43397632a399f542663ed3e3e37763ee203ba845b10b266cd2ede8925/onnxruntime-1.25.1-cp312-cp312-win_arm64.whl", hash = "sha256:b6c7aa5cae606d5c90a392679fac074b60f80025a2e83e1e90fdf882bd2a97f0", size = 12634433, upload-time = "2026-04-27T22:00:25.918Z" }, + { url = "https://files.pythonhosted.org/packages/d2/ee/db3ac55ef770347a926ac0f1317df0ab42c8bc604350833b30c7356bf936/onnxruntime-1.25.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e9d9b3b1694196bc3c5bc66f760a237a5e27d7688aaa2e2c9c0f66abd0486699", size = 17770761, upload-time = "2026-04-27T21:59:54.853Z" }, + { url = "https://files.pythonhosted.org/packages/dc/9a/33225481a94a59906fce44e27ab12fc3bddd2aaecdc6160bd73341ca1aba/onnxruntime-1.25.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:311d29b943e46a55ca72ca1ea48d7815c993122bfc359f68215fddeb9583fff4", size = 15871542, upload-time = "2026-04-27T21:59:41.881Z" }, + { url = "https://files.pythonhosted.org/packages/8b/09/f20aac60f6fcf840543be54d4e9252cfeb7e8c2bb6d22477aaeb180e763e/onnxruntime-1.25.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:98016a038b31160db23208706139fa3b99cd60bc1c5ffdade77aafd6a37a92ad", size = 18036960, upload-time = "2026-04-27T22:00:10.739Z" }, + { url = "https://files.pythonhosted.org/packages/50/83/47964ac7e2f7e2f9e83c69ec466642c6835466252cc2ef0561eafeb56b66/onnxruntime-1.25.1-cp313-cp313-win_amd64.whl", hash = "sha256:08717d6eee2820807ba60b1b17032af207bd7aaca5b6c4abaee71f83feae877b", size = 12904886, upload-time = "2026-04-27T22:00:39.878Z" }, + { url = "https://files.pythonhosted.org/packages/d4/6c/a6c5aea47dc95fca7728f8a5af67c184ec9e7d4e7882125c7062e4bba8dd/onnxruntime-1.25.1-cp313-cp313-win_arm64.whl", hash = "sha256:84f8963d70e00167bae273ab7e80e9795bfc5eb94f6b23236a99c5c11af00844", size = 12634117, upload-time = "2026-04-27T22:00:29.15Z" }, + { url = "https://files.pythonhosted.org/packages/a8/8a/3b65e7911eec86c125e3d6f43d690a6f68671500543c0390ecd6eb59b771/onnxruntime-1.25.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:03e800b3a4b48d9f3a2d23aacc4fa95486a3b406b14e51d1a9b8b6981d9adf9c", size = 15882935, upload-time = "2026-04-27T21:59:44.912Z" }, + { url = "https://files.pythonhosted.org/packages/3c/bb/410a760694f8ae7bbfc5fa81ccbeb7da241e6d520ee02a333a439cf462a2/onnxruntime-1.25.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fd83ef5c10cfc051a1cb465db692d57b996a1bc75a2a97b161398e29cdbc47ff", size = 18021727, upload-time = "2026-04-27T22:00:13.846Z" }, + { url = "https://files.pythonhosted.org/packages/fb/aa/04530bd38e31e26970fa1212346d76cf81705dc16a8ee5e6f4fb24634c11/onnxruntime-1.25.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:395eb662c437fa2407f44266e4778b75bff261b17c2a6fef042421f9069f871d", size = 17773721, upload-time = "2026-04-27T21:59:59.24Z" }, + { url = "https://files.pythonhosted.org/packages/ef/7f/ec79ab5cece6a688c944a7fa214a8511d548b9d5142a15d1a3d730b705f1/onnxruntime-1.25.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9ae85395f41b291ae3e61780ec5092640181d369ef6c268aa8141c478b509e69", size = 15875954, upload-time = "2026-04-27T21:59:49.394Z" }, + { url = "https://files.pythonhosted.org/packages/67/fe/20428215d822099ea2c1e3cf35c295cf1a58f467bf18b6c607597a39c18a/onnxruntime-1.25.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:828e1b12710fbedb6dfab5e7bae6f11563617cddf3c2e7e8d84c64de566a4a3a", size = 18038703, upload-time = "2026-04-27T22:00:16.199Z" }, + { url = "https://files.pythonhosted.org/packages/5a/b1/b15db965e6a68bc47ca7eb584de4e6b3d2d2f484d46cc57f715b596f6528/onnxruntime-1.25.1-cp314-cp314-win_amd64.whl", hash = "sha256:2affc9d2fd9ab013b9c9637464e649a0cca870d57ae18bfef74180eee65c3369", size = 13218513, upload-time = "2026-04-27T22:00:42.506Z" }, + { url = "https://files.pythonhosted.org/packages/5a/f9/25cd2d1b29cdc8140eee4afbb6fb930b69125526632b1d579bc747975306/onnxruntime-1.25.1-cp314-cp314-win_arm64.whl", hash = "sha256:3387d75d1a815b4b2495b4e47a05ef1b3bcb64a817ddc68587e0bfcb9702bcf6", size = 12969835, upload-time = "2026-04-27T22:00:31.504Z" }, + { url = "https://files.pythonhosted.org/packages/8d/0e/6c507d1e65b2421fb44e241cbba577c7276792279485024fb1752b43f5c5/onnxruntime-1.25.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:06280b06604660595037f783c6d24bc70cbe5c6093975f194cd1482e77d450de", size = 15883298, upload-time = "2026-04-27T21:59:51.991Z" }, + { url = "https://files.pythonhosted.org/packages/df/4e/1c9df57496409dc86b320bd38f29ad7a34b7115e4f35b8fca44a827568a7/onnxruntime-1.25.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:7e79fd5ce7db10ebcc24e020e2ed0159476e69e2326b9b7828e5aadcf6184212", size = 18021249, upload-time = "2026-04-27T22:00:18.954Z" }, +] + [[package]] name = "openpyxl" version = "3.1.5" @@ -710,6 +2004,58 @@ 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 = "pandas" +version = "3.0.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "python-dateutil" }, + { name = "tzdata", marker = "sys_platform == 'emscripten' or sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/da/99/b342345300f13440fe9fe385c3c481e2d9a595ee3bab4d3219247ac94e9a/pandas-3.0.2.tar.gz", hash = "sha256:f4753e73e34c8d83221ba58f232433fca2748be8b18dbca02d242ed153945043", size = 4645855, upload-time = "2026-03-31T06:48:30.816Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f3/b0/c20bd4d6d3f736e6bd6b55794e9cd0a617b858eaad27c8f410ea05d953b7/pandas-3.0.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:232a70ebb568c0c4d2db4584f338c1577d81e3af63292208d615907b698a0f18", size = 10347921, upload-time = "2026-03-31T06:46:33.36Z" }, + { url = "https://files.pythonhosted.org/packages/35/d0/4831af68ce30cc2d03c697bea8450e3225a835ef497d0d70f31b8cdde965/pandas-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:970762605cff1ca0d3f71ed4f3a769ea8f85fc8e6348f6e110b8fea7e6eb5a14", size = 9888127, upload-time = "2026-03-31T06:46:36.253Z" }, + { url = "https://files.pythonhosted.org/packages/61/a9/16ea9346e1fc4a96e2896242d9bc674764fb9049b0044c0132502f7a771e/pandas-3.0.2-cp312-cp312-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:aff4e6f4d722e0652707d7bcb190c445fe58428500c6d16005b02401764b1b3d", size = 10399577, upload-time = "2026-03-31T06:46:39.224Z" }, + { url = "https://files.pythonhosted.org/packages/c4/a8/3a61a721472959ab0ce865ef05d10b0d6bfe27ce8801c99f33d4fa996e65/pandas-3.0.2-cp312-cp312-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ef8b27695c3d3dc78403c9a7d5e59a62d5464a7e1123b4e0042763f7104dc74f", size = 10880030, upload-time = "2026-03-31T06:46:42.412Z" }, + { url = "https://files.pythonhosted.org/packages/da/65/7225c0ea4d6ce9cb2160a7fb7f39804871049f016e74782e5dade4d14109/pandas-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:f8d68083e49e16b84734eb1a4dcae4259a75c90fb6e2251ab9a00b61120c06ab", size = 11409468, upload-time = "2026-03-31T06:46:45.2Z" }, + { url = "https://files.pythonhosted.org/packages/fa/5b/46e7c76032639f2132359b5cf4c785dd8cf9aea5ea64699eac752f02b9db/pandas-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:32cc41f310ebd4a296d93515fcac312216adfedb1894e879303987b8f1e2b97d", size = 11936381, upload-time = "2026-03-31T06:46:48.293Z" }, + { url = "https://files.pythonhosted.org/packages/7b/8b/721a9cff6fa6a91b162eb51019c6243b82b3226c71bb6c8ef4a9bd65cbc6/pandas-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:a4785e1d6547d8427c5208b748ae2efb64659a21bd82bf440d4262d02bfa02a4", size = 9744993, upload-time = "2026-03-31T06:46:51.488Z" }, + { url = "https://files.pythonhosted.org/packages/d5/18/7f0bd34ae27b28159aa80f2a6799f47fda34f7fb938a76e20c7b7fe3b200/pandas-3.0.2-cp312-cp312-win_arm64.whl", hash = "sha256:08504503f7101300107ecdc8df73658e4347586db5cfdadabc1592e9d7e7a0fd", size = 9056118, upload-time = "2026-03-31T06:46:54.548Z" }, + { url = "https://files.pythonhosted.org/packages/bf/ca/3e639a1ea6fcd0617ca4e8ca45f62a74de33a56ae6cd552735470b22c8d3/pandas-3.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b5918ba197c951dec132b0c5929a00c0bf05d5942f590d3c10a807f6e15a57d3", size = 10321105, upload-time = "2026-03-31T06:46:57.327Z" }, + { url = "https://files.pythonhosted.org/packages/0b/77/dbc82ff2fb0e63c6564356682bf201edff0ba16c98630d21a1fb312a8182/pandas-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:d606a041c89c0a474a4702d532ab7e73a14fe35c8d427b972a625c8e46373668", size = 9864088, upload-time = "2026-03-31T06:46:59.935Z" }, + { url = "https://files.pythonhosted.org/packages/5c/2b/341f1b04bbca2e17e13cd3f08c215b70ef2c60c5356ef1e8c6857449edc7/pandas-3.0.2-cp313-cp313-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:710246ba0616e86891b58ab95f2495143bb2bc83ab6b06747c74216f583a6ac9", size = 10369066, upload-time = "2026-03-31T06:47:02.792Z" }, + { url = "https://files.pythonhosted.org/packages/12/c5/cbb1ffefb20a93d3f0e1fdcda699fb84976210d411b008f97f48bf6ce27e/pandas-3.0.2-cp313-cp313-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5d3cfe227c725b1f3dff4278b43d8c784656a42a9325b63af6b1492a8232209e", size = 10876780, upload-time = "2026-03-31T06:47:06.205Z" }, + { url = "https://files.pythonhosted.org/packages/98/fe/2249ae5e0a69bd0ddf17353d0a5d26611d70970111f5b3600cdc8be883e7/pandas-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:c3b723df9087a9a9a840e263ebd9f88b64a12075d1bf2ea401a5a42f254f084d", size = 11375181, upload-time = "2026-03-31T06:47:09.383Z" }, + { url = "https://files.pythonhosted.org/packages/de/64/77a38b09e70b6464883b8d7584ab543e748e42c1b5d337a2ee088e0df741/pandas-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a3096110bf9eac0070b7208465f2740e2d8a670d5cb6530b5bb884eca495fd39", size = 11928899, upload-time = "2026-03-31T06:47:12.686Z" }, + { url = "https://files.pythonhosted.org/packages/5e/52/42855bf626868413f761addd574acc6195880ae247a5346477a4361c3acb/pandas-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:07a10f5c36512eead51bc578eb3354ad17578b22c013d89a796ab5eee90cd991", size = 9746574, upload-time = "2026-03-31T06:47:15.64Z" }, + { url = "https://files.pythonhosted.org/packages/88/39/21304ae06a25e8bf9fc820d69b29b2c495b2ae580d1e143146c309941760/pandas-3.0.2-cp313-cp313-win_arm64.whl", hash = "sha256:5fdbfa05931071aba28b408e59226186b01eb5e92bea2ab78b65863ca3228d84", size = 9047156, upload-time = "2026-03-31T06:47:18.595Z" }, + { url = "https://files.pythonhosted.org/packages/72/20/7defa8b27d4f330a903bb68eea33be07d839c5ea6bdda54174efcec0e1d2/pandas-3.0.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:dbc20dea3b9e27d0e66d74c42b2d0c1bed9c2ffe92adea33633e3bedeb5ac235", size = 10756238, upload-time = "2026-03-31T06:47:22.012Z" }, + { url = "https://files.pythonhosted.org/packages/e9/95/49433c14862c636afc0e9b2db83ff16b3ad92959364e52b2955e44c8e94c/pandas-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:b75c347eff42497452116ce05ef461822d97ce5b9ff8df6edacb8076092c855d", size = 10408520, upload-time = "2026-03-31T06:47:25.197Z" }, + { url = "https://files.pythonhosted.org/packages/3b/f8/462ad2b5881d6b8ec8e5f7ed2ea1893faa02290d13870a1600fe72ad8efc/pandas-3.0.2-cp313-cp313t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d1478075142e83a5571782ad007fb201ed074bdeac7ebcc8890c71442e96adf7", size = 10324154, upload-time = "2026-03-31T06:47:28.097Z" }, + { url = "https://files.pythonhosted.org/packages/0a/65/d1e69b649cbcddda23ad6e4c40ef935340f6f652a006e5cbc3555ac8adb3/pandas-3.0.2-cp313-cp313t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5880314e69e763d4c8b27937090de570f1fb8d027059a7ada3f7f8e98bdcb677", size = 10714449, upload-time = "2026-03-31T06:47:30.85Z" }, + { url = "https://files.pythonhosted.org/packages/47/a4/85b59bc65b8190ea3689882db6cdf32a5003c0ccd5a586c30fdcc3ffc4fc/pandas-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:b5329e26898896f06035241a626d7c335daa479b9bbc82be7c2742d048e41172", size = 11338475, upload-time = "2026-03-31T06:47:34.026Z" }, + { url = "https://files.pythonhosted.org/packages/1e/c4/bc6966c6e38e5d9478b935272d124d80a589511ed1612a5d21d36f664c68/pandas-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:81526c4afd31971f8b62671442a4b2b51e0aa9acc3819c9f0f12a28b6fcf85f1", size = 11786568, upload-time = "2026-03-31T06:47:36.941Z" }, + { url = "https://files.pythonhosted.org/packages/e8/74/09298ca9740beed1d3504e073d67e128aa07e5ca5ca2824b0c674c0b8676/pandas-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:7cadd7e9a44ec13b621aec60f9150e744cfc7a3dd32924a7e2f45edff31823b0", size = 10488652, upload-time = "2026-03-31T06:47:40.612Z" }, + { url = "https://files.pythonhosted.org/packages/bb/40/c6ea527147c73b24fc15c891c3fcffe9c019793119c5742b8784a062c7db/pandas-3.0.2-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:db0dbfd2a6cdf3770aa60464d50333d8f3d9165b2f2671bcc299b72de5a6677b", size = 10326084, upload-time = "2026-03-31T06:47:43.834Z" }, + { url = "https://files.pythonhosted.org/packages/95/25/bdb9326c3b5455f8d4d3549fce7abcf967259de146fe2cf7a82368141948/pandas-3.0.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0555c5882688a39317179ab4a0ed41d3ebc8812ab14c69364bbee8fb7a3f6288", size = 9914146, upload-time = "2026-03-31T06:47:46.67Z" }, + { url = "https://files.pythonhosted.org/packages/8d/77/3a227ff3337aa376c60d288e1d61c5d097131d0ac71f954d90a8f369e422/pandas-3.0.2-cp314-cp314-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01f31a546acd5574ef77fe199bc90b55527c225c20ccda6601cf6b0fd5ed597c", size = 10444081, upload-time = "2026-03-31T06:47:49.681Z" }, + { url = "https://files.pythonhosted.org/packages/15/88/3cdd54fa279341afa10acf8d2b503556b1375245dccc9315659f795dd2e9/pandas-3.0.2-cp314-cp314-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:deeca1b5a931fdf0c2212c8a659ade6d3b1edc21f0914ce71ef24456ca7a6535", size = 10897535, upload-time = "2026-03-31T06:47:53.033Z" }, + { url = "https://files.pythonhosted.org/packages/06/9d/98cc7a7624f7932e40f434299260e2917b090a579d75937cb8a57b9d2de3/pandas-3.0.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:0f48afd9bb13300ffb5a3316973324c787054ba6665cda0da3fbd67f451995db", size = 11446992, upload-time = "2026-03-31T06:47:56.193Z" }, + { url = "https://files.pythonhosted.org/packages/9a/cd/19ff605cc3760e80602e6826ddef2824d8e7050ed80f2e11c4b079741dc3/pandas-3.0.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:6c4d8458b97a35717b62469a4ea0e85abd5ed8687277f5ccfc67f8a5126f8c53", size = 11968257, upload-time = "2026-03-31T06:47:59.137Z" }, + { url = "https://files.pythonhosted.org/packages/db/60/aba6a38de456e7341285102bede27514795c1eaa353bc0e7638b6b785356/pandas-3.0.2-cp314-cp314-win_amd64.whl", hash = "sha256:b35d14bb5d8285d9494fe93815a9e9307c0876e10f1e8e89ac5b88f728ec8dcf", size = 9865893, upload-time = "2026-03-31T06:48:02.038Z" }, + { url = "https://files.pythonhosted.org/packages/08/71/e5ec979dd2e8a093dacb8864598c0ff59a0cee0bbcdc0bfec16a51684d4f/pandas-3.0.2-cp314-cp314-win_arm64.whl", hash = "sha256:63d141b56ef686f7f0d714cfb8de4e320475b86bf4b620aa0b7da89af8cbdbbb", size = 9188644, upload-time = "2026-03-31T06:48:05.045Z" }, + { url = "https://files.pythonhosted.org/packages/f1/6c/7b45d85db19cae1eb524f2418ceaa9d85965dcf7b764ed151386b7c540f0/pandas-3.0.2-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:140f0cffb1fa2524e874dde5b477d9defe10780d8e9e220d259b2c0874c89d9d", size = 10776246, upload-time = "2026-03-31T06:48:07.789Z" }, + { url = "https://files.pythonhosted.org/packages/a8/3e/7b00648b086c106e81766f25322b48aa8dfa95b55e621dbdf2fdd413a117/pandas-3.0.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:ae37e833ff4fed0ba352f6bdd8b73ba3ab3256a85e54edfd1ab51ae40cca0af8", size = 10424801, upload-time = "2026-03-31T06:48:10.897Z" }, + { url = "https://files.pythonhosted.org/packages/da/6e/558dd09a71b53b4008e7fc8a98ec6d447e9bfb63cdaeea10e5eb9b2dabe8/pandas-3.0.2-cp314-cp314t-manylinux_2_24_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4d888a5c678a419a5bb41a2a93818e8ed9fd3172246555c0b37b7cc27027effd", size = 10345643, upload-time = "2026-03-31T06:48:13.7Z" }, + { url = "https://files.pythonhosted.org/packages/be/e3/921c93b4d9a280409451dc8d07b062b503bbec0531d2627e73a756e99a82/pandas-3.0.2-cp314-cp314t-manylinux_2_24_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b444dc64c079e84df91baa8bf613d58405645461cabca929d9178f2cd392398d", size = 10743641, upload-time = "2026-03-31T06:48:16.659Z" }, + { url = "https://files.pythonhosted.org/packages/56/ca/fd17286f24fa3b4d067965d8d5d7e14fe557dd4f979a0b068ac0deaf8228/pandas-3.0.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:4544c7a54920de8eeacaa1466a6b7268ecfbc9bc64ab4dbb89c6bbe94d5e0660", size = 11361993, upload-time = "2026-03-31T06:48:19.475Z" }, + { url = "https://files.pythonhosted.org/packages/e4/a5/2f6ed612056819de445a433ca1f2821ac3dab7f150d569a59e9cc105de1d/pandas-3.0.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:734be7551687c00fbd760dc0522ed974f82ad230d4a10f54bf51b80d44a08702", size = 11815274, upload-time = "2026-03-31T06:48:22.695Z" }, + { url = "https://files.pythonhosted.org/packages/00/2f/b622683e99ec3ce00b0854bac9e80868592c5b051733f2cf3a868e5fea26/pandas-3.0.2-cp314-cp314t-win_amd64.whl", hash = "sha256:57a07209bebcbcf768d2d13c9b78b852f9a15978dac41b9e6421a81ad4cdd276", size = 10888530, upload-time = "2026-03-31T06:48:25.806Z" }, + { url = "https://files.pythonhosted.org/packages/cb/2b/f8434233fab2bd66a02ec014febe4e5adced20e2693e0e90a07d118ed30e/pandas-3.0.2-cp314-cp314t-win_arm64.whl", hash = "sha256:5371b72c2d4d415d08765f32d689217a43227484e81b2305b52076e328f6f482", size = 9455341, upload-time = "2026-03-31T06:48:28.418Z" }, +] + [[package]] name = "pillow" version = "12.2.0" @@ -788,6 +2134,90 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, ] +[[package]] +name = "propcache" +version = "0.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/9e/da/e9fc233cf63743258bff22b3dfa7ea5baef7b5bc324af47a0ad89b8ffc6f/propcache-0.4.1.tar.gz", hash = "sha256:f48107a8c637e80362555f37ecf49abe20370e557cc4ab374f04ec4423c97c3d", size = 46442, upload-time = "2025-10-08T19:49:02.291Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/0f/f17b1b2b221d5ca28b4b876e8bb046ac40466513960646bda8e1853cdfa2/propcache-0.4.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:e153e9cd40cc8945138822807139367f256f89c6810c2634a4f6902b52d3b4e2", size = 80061, upload-time = "2025-10-08T19:46:46.075Z" }, + { url = "https://files.pythonhosted.org/packages/76/47/8ccf75935f51448ba9a16a71b783eb7ef6b9ee60f5d14c7f8a8a79fbeed7/propcache-0.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:cd547953428f7abb73c5ad82cbb32109566204260d98e41e5dfdc682eb7f8403", size = 46037, upload-time = "2025-10-08T19:46:47.23Z" }, + { url = "https://files.pythonhosted.org/packages/0a/b6/5c9a0e42df4d00bfb4a3cbbe5cf9f54260300c88a0e9af1f47ca5ce17ac0/propcache-0.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:f048da1b4f243fc44f205dfd320933a951b8d89e0afd4c7cacc762a8b9165207", size = 47324, upload-time = "2025-10-08T19:46:48.384Z" }, + { url = "https://files.pythonhosted.org/packages/9e/d3/6c7ee328b39a81ee877c962469f1e795f9db87f925251efeb0545e0020d0/propcache-0.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ec17c65562a827bba85e3872ead335f95405ea1674860d96483a02f5c698fa72", size = 225505, upload-time = "2025-10-08T19:46:50.055Z" }, + { url = "https://files.pythonhosted.org/packages/01/5d/1c53f4563490b1d06a684742cc6076ef944bc6457df6051b7d1a877c057b/propcache-0.4.1-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:405aac25c6394ef275dee4c709be43745d36674b223ba4eb7144bf4d691b7367", size = 230242, upload-time = "2025-10-08T19:46:51.815Z" }, + { url = "https://files.pythonhosted.org/packages/20/e1/ce4620633b0e2422207c3cb774a0ee61cac13abc6217763a7b9e2e3f4a12/propcache-0.4.1-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:0013cb6f8dde4b2a2f66903b8ba740bdfe378c943c4377a200551ceb27f379e4", size = 238474, upload-time = "2025-10-08T19:46:53.208Z" }, + { url = "https://files.pythonhosted.org/packages/46/4b/3aae6835b8e5f44ea6a68348ad90f78134047b503765087be2f9912140ea/propcache-0.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15932ab57837c3368b024473a525e25d316d8353016e7cc0e5ba9eb343fbb1cf", size = 221575, upload-time = "2025-10-08T19:46:54.511Z" }, + { url = "https://files.pythonhosted.org/packages/6e/a5/8a5e8678bcc9d3a1a15b9a29165640d64762d424a16af543f00629c87338/propcache-0.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:031dce78b9dc099f4c29785d9cf5577a3faf9ebf74ecbd3c856a7b92768c3df3", size = 216736, upload-time = "2025-10-08T19:46:56.212Z" }, + { url = "https://files.pythonhosted.org/packages/f1/63/b7b215eddeac83ca1c6b934f89d09a625aa9ee4ba158338854c87210cc36/propcache-0.4.1-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:ab08df6c9a035bee56e31af99be621526bd237bea9f32def431c656b29e41778", size = 213019, upload-time = "2025-10-08T19:46:57.595Z" }, + { url = "https://files.pythonhosted.org/packages/57/74/f580099a58c8af587cac7ba19ee7cb418506342fbbe2d4a4401661cca886/propcache-0.4.1-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:4d7af63f9f93fe593afbf104c21b3b15868efb2c21d07d8732c0c4287e66b6a6", size = 220376, upload-time = "2025-10-08T19:46:59.067Z" }, + { url = "https://files.pythonhosted.org/packages/c4/ee/542f1313aff7eaf19c2bb758c5d0560d2683dac001a1c96d0774af799843/propcache-0.4.1-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:cfc27c945f422e8b5071b6e93169679e4eb5bf73bbcbf1ba3ae3a83d2f78ebd9", size = 226988, upload-time = "2025-10-08T19:47:00.544Z" }, + { url = "https://files.pythonhosted.org/packages/8f/18/9c6b015dd9c6930f6ce2229e1f02fb35298b847f2087ea2b436a5bfa7287/propcache-0.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:35c3277624a080cc6ec6f847cbbbb5b49affa3598c4535a0a4682a697aaa5c75", size = 215615, upload-time = "2025-10-08T19:47:01.968Z" }, + { url = "https://files.pythonhosted.org/packages/80/9e/e7b85720b98c45a45e1fca6a177024934dc9bc5f4d5dd04207f216fc33ed/propcache-0.4.1-cp312-cp312-win32.whl", hash = "sha256:671538c2262dadb5ba6395e26c1731e1d52534bfe9ae56d0b5573ce539266aa8", size = 38066, upload-time = "2025-10-08T19:47:03.503Z" }, + { url = "https://files.pythonhosted.org/packages/54/09/d19cff2a5aaac632ec8fc03737b223597b1e347416934c1b3a7df079784c/propcache-0.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:cb2d222e72399fcf5890d1d5cc1060857b9b236adff2792ff48ca2dfd46c81db", size = 41655, upload-time = "2025-10-08T19:47:04.973Z" }, + { url = "https://files.pythonhosted.org/packages/68/ab/6b5c191bb5de08036a8c697b265d4ca76148efb10fa162f14af14fb5f076/propcache-0.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:204483131fb222bdaaeeea9f9e6c6ed0cac32731f75dfc1d4a567fc1926477c1", size = 37789, upload-time = "2025-10-08T19:47:06.077Z" }, + { url = "https://files.pythonhosted.org/packages/bf/df/6d9c1b6ac12b003837dde8a10231a7344512186e87b36e855bef32241942/propcache-0.4.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:43eedf29202c08550aac1d14e0ee619b0430aaef78f85864c1a892294fbc28cf", size = 77750, upload-time = "2025-10-08T19:47:07.648Z" }, + { url = "https://files.pythonhosted.org/packages/8b/e8/677a0025e8a2acf07d3418a2e7ba529c9c33caf09d3c1f25513023c1db56/propcache-0.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:d62cdfcfd89ccb8de04e0eda998535c406bf5e060ffd56be6c586cbcc05b3311", size = 44780, upload-time = "2025-10-08T19:47:08.851Z" }, + { url = "https://files.pythonhosted.org/packages/89/a4/92380f7ca60f99ebae761936bc48a72a639e8a47b29050615eef757cb2a7/propcache-0.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:cae65ad55793da34db5f54e4029b89d3b9b9490d8abe1b4c7ab5d4b8ec7ebf74", size = 46308, upload-time = "2025-10-08T19:47:09.982Z" }, + { url = "https://files.pythonhosted.org/packages/2d/48/c5ac64dee5262044348d1d78a5f85dd1a57464a60d30daee946699963eb3/propcache-0.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:333ddb9031d2704a301ee3e506dc46b1fe5f294ec198ed6435ad5b6a085facfe", size = 208182, upload-time = "2025-10-08T19:47:11.319Z" }, + { url = "https://files.pythonhosted.org/packages/c6/0c/cd762dd011a9287389a6a3eb43aa30207bde253610cca06824aeabfe9653/propcache-0.4.1-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:fd0858c20f078a32cf55f7e81473d96dcf3b93fd2ccdb3d40fdf54b8573df3af", size = 211215, upload-time = "2025-10-08T19:47:13.146Z" }, + { url = "https://files.pythonhosted.org/packages/30/3e/49861e90233ba36890ae0ca4c660e95df565b2cd15d4a68556ab5865974e/propcache-0.4.1-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:678ae89ebc632c5c204c794f8dab2837c5f159aeb59e6ed0539500400577298c", size = 218112, upload-time = "2025-10-08T19:47:14.913Z" }, + { url = "https://files.pythonhosted.org/packages/f1/8b/544bc867e24e1bd48f3118cecd3b05c694e160a168478fa28770f22fd094/propcache-0.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d472aeb4fbf9865e0c6d622d7f4d54a4e101a89715d8904282bb5f9a2f476c3f", size = 204442, upload-time = "2025-10-08T19:47:16.277Z" }, + { url = "https://files.pythonhosted.org/packages/50/a6/4282772fd016a76d3e5c0df58380a5ea64900afd836cec2c2f662d1b9bb3/propcache-0.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:4d3df5fa7e36b3225954fba85589da77a0fe6a53e3976de39caf04a0db4c36f1", size = 199398, upload-time = "2025-10-08T19:47:17.962Z" }, + { url = "https://files.pythonhosted.org/packages/3e/ec/d8a7cd406ee1ddb705db2139f8a10a8a427100347bd698e7014351c7af09/propcache-0.4.1-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:ee17f18d2498f2673e432faaa71698032b0127ebf23ae5974eeaf806c279df24", size = 196920, upload-time = "2025-10-08T19:47:19.355Z" }, + { url = "https://files.pythonhosted.org/packages/f6/6c/f38ab64af3764f431e359f8baf9e0a21013e24329e8b85d2da32e8ed07ca/propcache-0.4.1-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:580e97762b950f993ae618e167e7be9256b8353c2dcd8b99ec100eb50f5286aa", size = 203748, upload-time = "2025-10-08T19:47:21.338Z" }, + { url = "https://files.pythonhosted.org/packages/d6/e3/fa846bd70f6534d647886621388f0a265254d30e3ce47e5c8e6e27dbf153/propcache-0.4.1-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:501d20b891688eb8e7aa903021f0b72d5a55db40ffaab27edefd1027caaafa61", size = 205877, upload-time = "2025-10-08T19:47:23.059Z" }, + { url = "https://files.pythonhosted.org/packages/e2/39/8163fc6f3133fea7b5f2827e8eba2029a0277ab2c5beee6c1db7b10fc23d/propcache-0.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:9a0bd56e5b100aef69bd8562b74b46254e7c8812918d3baa700c8a8009b0af66", size = 199437, upload-time = "2025-10-08T19:47:24.445Z" }, + { url = "https://files.pythonhosted.org/packages/93/89/caa9089970ca49c7c01662bd0eeedfe85494e863e8043565aeb6472ce8fe/propcache-0.4.1-cp313-cp313-win32.whl", hash = "sha256:bcc9aaa5d80322bc2fb24bb7accb4a30f81e90ab8d6ba187aec0744bc302ad81", size = 37586, upload-time = "2025-10-08T19:47:25.736Z" }, + { url = "https://files.pythonhosted.org/packages/f5/ab/f76ec3c3627c883215b5c8080debb4394ef5a7a29be811f786415fc1e6fd/propcache-0.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:381914df18634f5494334d201e98245c0596067504b9372d8cf93f4bb23e025e", size = 40790, upload-time = "2025-10-08T19:47:26.847Z" }, + { url = "https://files.pythonhosted.org/packages/59/1b/e71ae98235f8e2ba5004d8cb19765a74877abf189bc53fc0c80d799e56c3/propcache-0.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:8873eb4460fd55333ea49b7d189749ecf6e55bf85080f11b1c4530ed3034cba1", size = 37158, upload-time = "2025-10-08T19:47:27.961Z" }, + { url = "https://files.pythonhosted.org/packages/83/ce/a31bbdfc24ee0dcbba458c8175ed26089cf109a55bbe7b7640ed2470cfe9/propcache-0.4.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:92d1935ee1f8d7442da9c0c4fa7ac20d07e94064184811b685f5c4fada64553b", size = 81451, upload-time = "2025-10-08T19:47:29.445Z" }, + { url = "https://files.pythonhosted.org/packages/25/9c/442a45a470a68456e710d96cacd3573ef26a1d0a60067e6a7d5e655621ed/propcache-0.4.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:473c61b39e1460d386479b9b2f337da492042447c9b685f28be4f74d3529e566", size = 46374, upload-time = "2025-10-08T19:47:30.579Z" }, + { url = "https://files.pythonhosted.org/packages/f4/bf/b1d5e21dbc3b2e889ea4327044fb16312a736d97640fb8b6aa3f9c7b3b65/propcache-0.4.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:c0ef0aaafc66fbd87842a3fe3902fd889825646bc21149eafe47be6072725835", size = 48396, upload-time = "2025-10-08T19:47:31.79Z" }, + { url = "https://files.pythonhosted.org/packages/f4/04/5b4c54a103d480e978d3c8a76073502b18db0c4bc17ab91b3cb5092ad949/propcache-0.4.1-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f95393b4d66bfae908c3ca8d169d5f79cd65636ae15b5e7a4f6e67af675adb0e", size = 275950, upload-time = "2025-10-08T19:47:33.481Z" }, + { url = "https://files.pythonhosted.org/packages/b4/c1/86f846827fb969c4b78b0af79bba1d1ea2156492e1b83dea8b8a6ae27395/propcache-0.4.1-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:c07fda85708bc48578467e85099645167a955ba093be0a2dcba962195676e859", size = 273856, upload-time = "2025-10-08T19:47:34.906Z" }, + { url = "https://files.pythonhosted.org/packages/36/1d/fc272a63c8d3bbad6878c336c7a7dea15e8f2d23a544bda43205dfa83ada/propcache-0.4.1-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:af223b406d6d000830c6f65f1e6431783fc3f713ba3e6cc8c024d5ee96170a4b", size = 280420, upload-time = "2025-10-08T19:47:36.338Z" }, + { url = "https://files.pythonhosted.org/packages/07/0c/01f2219d39f7e53d52e5173bcb09c976609ba30209912a0680adfb8c593a/propcache-0.4.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a78372c932c90ee474559c5ddfffd718238e8673c340dc21fe45c5b8b54559a0", size = 263254, upload-time = "2025-10-08T19:47:37.692Z" }, + { url = "https://files.pythonhosted.org/packages/2d/18/cd28081658ce597898f0c4d174d4d0f3c5b6d4dc27ffafeef835c95eb359/propcache-0.4.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:564d9f0d4d9509e1a870c920a89b2fec951b44bf5ba7d537a9e7c1ccec2c18af", size = 261205, upload-time = "2025-10-08T19:47:39.659Z" }, + { url = "https://files.pythonhosted.org/packages/7a/71/1f9e22eb8b8316701c2a19fa1f388c8a3185082607da8e406a803c9b954e/propcache-0.4.1-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:17612831fda0138059cc5546f4d12a2aacfb9e47068c06af35c400ba58ba7393", size = 247873, upload-time = "2025-10-08T19:47:41.084Z" }, + { url = "https://files.pythonhosted.org/packages/4a/65/3d4b61f36af2b4eddba9def857959f1016a51066b4f1ce348e0cf7881f58/propcache-0.4.1-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:41a89040cb10bd345b3c1a873b2bf36413d48da1def52f268a055f7398514874", size = 262739, upload-time = "2025-10-08T19:47:42.51Z" }, + { url = "https://files.pythonhosted.org/packages/2a/42/26746ab087faa77c1c68079b228810436ccd9a5ce9ac85e2b7307195fd06/propcache-0.4.1-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:e35b88984e7fa64aacecea39236cee32dd9bd8c55f57ba8a75cf2399553f9bd7", size = 263514, upload-time = "2025-10-08T19:47:43.927Z" }, + { url = "https://files.pythonhosted.org/packages/94/13/630690fe201f5502d2403dd3cfd451ed8858fe3c738ee88d095ad2ff407b/propcache-0.4.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f8b465489f927b0df505cbe26ffbeed4d6d8a2bbc61ce90eb074ff129ef0ab1", size = 257781, upload-time = "2025-10-08T19:47:45.448Z" }, + { url = "https://files.pythonhosted.org/packages/92/f7/1d4ec5841505f423469efbfc381d64b7b467438cd5a4bbcbb063f3b73d27/propcache-0.4.1-cp313-cp313t-win32.whl", hash = "sha256:2ad890caa1d928c7c2965b48f3a3815c853180831d0e5503d35cf00c472f4717", size = 41396, upload-time = "2025-10-08T19:47:47.202Z" }, + { url = "https://files.pythonhosted.org/packages/48/f0/615c30622316496d2cbbc29f5985f7777d3ada70f23370608c1d3e081c1f/propcache-0.4.1-cp313-cp313t-win_amd64.whl", hash = "sha256:f7ee0e597f495cf415bcbd3da3caa3bd7e816b74d0d52b8145954c5e6fd3ff37", size = 44897, upload-time = "2025-10-08T19:47:48.336Z" }, + { url = "https://files.pythonhosted.org/packages/fd/ca/6002e46eccbe0e33dcd4069ef32f7f1c9e243736e07adca37ae8c4830ec3/propcache-0.4.1-cp313-cp313t-win_arm64.whl", hash = "sha256:929d7cbe1f01bb7baffb33dc14eb5691c95831450a26354cd210a8155170c93a", size = 39789, upload-time = "2025-10-08T19:47:49.876Z" }, + { url = "https://files.pythonhosted.org/packages/8e/5c/bca52d654a896f831b8256683457ceddd490ec18d9ec50e97dfd8fc726a8/propcache-0.4.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:3f7124c9d820ba5548d431afb4632301acf965db49e666aa21c305cbe8c6de12", size = 78152, upload-time = "2025-10-08T19:47:51.051Z" }, + { url = "https://files.pythonhosted.org/packages/65/9b/03b04e7d82a5f54fb16113d839f5ea1ede58a61e90edf515f6577c66fa8f/propcache-0.4.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c0d4b719b7da33599dfe3b22d3db1ef789210a0597bc650b7cee9c77c2be8c5c", size = 44869, upload-time = "2025-10-08T19:47:52.594Z" }, + { url = "https://files.pythonhosted.org/packages/b2/fa/89a8ef0468d5833a23fff277b143d0573897cf75bd56670a6d28126c7d68/propcache-0.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:9f302f4783709a78240ebc311b793f123328716a60911d667e0c036bc5dcbded", size = 46596, upload-time = "2025-10-08T19:47:54.073Z" }, + { url = "https://files.pythonhosted.org/packages/86/bd/47816020d337f4a746edc42fe8d53669965138f39ee117414c7d7a340cfe/propcache-0.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c80ee5802e3fb9ea37938e7eecc307fb984837091d5fd262bb37238b1ae97641", size = 206981, upload-time = "2025-10-08T19:47:55.715Z" }, + { url = "https://files.pythonhosted.org/packages/df/f6/c5fa1357cc9748510ee55f37173eb31bfde6d94e98ccd9e6f033f2fc06e1/propcache-0.4.1-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:ed5a841e8bb29a55fb8159ed526b26adc5bdd7e8bd7bf793ce647cb08656cdf4", size = 211490, upload-time = "2025-10-08T19:47:57.499Z" }, + { url = "https://files.pythonhosted.org/packages/80/1e/e5889652a7c4a3846683401a48f0f2e5083ce0ec1a8a5221d8058fbd1adf/propcache-0.4.1-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:55c72fd6ea2da4c318e74ffdf93c4fe4e926051133657459131a95c846d16d44", size = 215371, upload-time = "2025-10-08T19:47:59.317Z" }, + { url = "https://files.pythonhosted.org/packages/b2/f2/889ad4b2408f72fe1a4f6a19491177b30ea7bf1a0fd5f17050ca08cfc882/propcache-0.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8326e144341460402713f91df60ade3c999d601e7eb5ff8f6f7862d54de0610d", size = 201424, upload-time = "2025-10-08T19:48:00.67Z" }, + { url = "https://files.pythonhosted.org/packages/27/73/033d63069b57b0812c8bd19f311faebeceb6ba31b8f32b73432d12a0b826/propcache-0.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:060b16ae65bc098da7f6d25bf359f1f31f688384858204fe5d652979e0015e5b", size = 197566, upload-time = "2025-10-08T19:48:02.604Z" }, + { url = "https://files.pythonhosted.org/packages/dc/89/ce24f3dc182630b4e07aa6d15f0ff4b14ed4b9955fae95a0b54c58d66c05/propcache-0.4.1-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:89eb3fa9524f7bec9de6e83cf3faed9d79bffa560672c118a96a171a6f55831e", size = 193130, upload-time = "2025-10-08T19:48:04.499Z" }, + { url = "https://files.pythonhosted.org/packages/a9/24/ef0d5fd1a811fb5c609278d0209c9f10c35f20581fcc16f818da959fc5b4/propcache-0.4.1-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:dee69d7015dc235f526fe80a9c90d65eb0039103fe565776250881731f06349f", size = 202625, upload-time = "2025-10-08T19:48:06.213Z" }, + { url = "https://files.pythonhosted.org/packages/f5/02/98ec20ff5546f68d673df2f7a69e8c0d076b5abd05ca882dc7ee3a83653d/propcache-0.4.1-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:5558992a00dfd54ccbc64a32726a3357ec93825a418a401f5cc67df0ac5d9e49", size = 204209, upload-time = "2025-10-08T19:48:08.432Z" }, + { url = "https://files.pythonhosted.org/packages/a0/87/492694f76759b15f0467a2a93ab68d32859672b646aa8a04ce4864e7932d/propcache-0.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:c9b822a577f560fbd9554812526831712c1436d2c046cedee4c3796d3543b144", size = 197797, upload-time = "2025-10-08T19:48:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/ee/36/66367de3575db1d2d3f3d177432bd14ee577a39d3f5d1b3d5df8afe3b6e2/propcache-0.4.1-cp314-cp314-win32.whl", hash = "sha256:ab4c29b49d560fe48b696cdcb127dd36e0bc2472548f3bf56cc5cb3da2b2984f", size = 38140, upload-time = "2025-10-08T19:48:11.232Z" }, + { url = "https://files.pythonhosted.org/packages/0c/2a/a758b47de253636e1b8aef181c0b4f4f204bf0dd964914fb2af90a95b49b/propcache-0.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:5a103c3eb905fcea0ab98be99c3a9a5ab2de60228aa5aceedc614c0281cf6153", size = 41257, upload-time = "2025-10-08T19:48:12.707Z" }, + { url = "https://files.pythonhosted.org/packages/34/5e/63bd5896c3fec12edcbd6f12508d4890d23c265df28c74b175e1ef9f4f3b/propcache-0.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:74c1fb26515153e482e00177a1ad654721bf9207da8a494a0c05e797ad27b992", size = 38097, upload-time = "2025-10-08T19:48:13.923Z" }, + { url = "https://files.pythonhosted.org/packages/99/85/9ff785d787ccf9bbb3f3106f79884a130951436f58392000231b4c737c80/propcache-0.4.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:824e908bce90fb2743bd6b59db36eb4f45cd350a39637c9f73b1c1ea66f5b75f", size = 81455, upload-time = "2025-10-08T19:48:15.16Z" }, + { url = "https://files.pythonhosted.org/packages/90/85/2431c10c8e7ddb1445c1f7c4b54d886e8ad20e3c6307e7218f05922cad67/propcache-0.4.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:c2b5e7db5328427c57c8e8831abda175421b709672f6cfc3d630c3b7e2146393", size = 46372, upload-time = "2025-10-08T19:48:16.424Z" }, + { url = "https://files.pythonhosted.org/packages/01/20/b0972d902472da9bcb683fa595099911f4d2e86e5683bcc45de60dd05dc3/propcache-0.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:6f6ff873ed40292cd4969ef5310179afd5db59fdf055897e282485043fc80ad0", size = 48411, upload-time = "2025-10-08T19:48:17.577Z" }, + { url = "https://files.pythonhosted.org/packages/e2/e3/7dc89f4f21e8f99bad3d5ddb3a3389afcf9da4ac69e3deb2dcdc96e74169/propcache-0.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:49a2dc67c154db2c1463013594c458881a069fcf98940e61a0569016a583020a", size = 275712, upload-time = "2025-10-08T19:48:18.901Z" }, + { url = "https://files.pythonhosted.org/packages/20/67/89800c8352489b21a8047c773067644e3897f02ecbbd610f4d46b7f08612/propcache-0.4.1-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:005f08e6a0529984491e37d8dbc3dd86f84bd78a8ceb5fa9a021f4c48d4984be", size = 273557, upload-time = "2025-10-08T19:48:20.762Z" }, + { url = "https://files.pythonhosted.org/packages/e2/a1/b52b055c766a54ce6d9c16d9aca0cad8059acd9637cdf8aa0222f4a026ef/propcache-0.4.1-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5c3310452e0d31390da9035c348633b43d7e7feb2e37be252be6da45abd1abcc", size = 280015, upload-time = "2025-10-08T19:48:22.592Z" }, + { url = "https://files.pythonhosted.org/packages/48/c8/33cee30bd890672c63743049f3c9e4be087e6780906bfc3ec58528be59c1/propcache-0.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4c3c70630930447f9ef1caac7728c8ad1c56bc5015338b20fed0d08ea2480b3a", size = 262880, upload-time = "2025-10-08T19:48:23.947Z" }, + { url = "https://files.pythonhosted.org/packages/0c/b1/8f08a143b204b418285c88b83d00edbd61afbc2c6415ffafc8905da7038b/propcache-0.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8e57061305815dfc910a3634dcf584f08168a8836e6999983569f51a8544cd89", size = 260938, upload-time = "2025-10-08T19:48:25.656Z" }, + { url = "https://files.pythonhosted.org/packages/cf/12/96e4664c82ca2f31e1c8dff86afb867348979eb78d3cb8546a680287a1e9/propcache-0.4.1-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:521a463429ef54143092c11a77e04056dd00636f72e8c45b70aaa3140d639726", size = 247641, upload-time = "2025-10-08T19:48:27.207Z" }, + { url = "https://files.pythonhosted.org/packages/18/ed/e7a9cfca28133386ba52278136d42209d3125db08d0a6395f0cba0c0285c/propcache-0.4.1-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:120c964da3fdc75e3731aa392527136d4ad35868cc556fd09bb6d09172d9a367", size = 262510, upload-time = "2025-10-08T19:48:28.65Z" }, + { url = "https://files.pythonhosted.org/packages/f5/76/16d8bf65e8845dd62b4e2b57444ab81f07f40caa5652b8969b87ddcf2ef6/propcache-0.4.1-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:d8f353eb14ee3441ee844ade4277d560cdd68288838673273b978e3d6d2c8f36", size = 263161, upload-time = "2025-10-08T19:48:30.133Z" }, + { url = "https://files.pythonhosted.org/packages/e7/70/c99e9edb5d91d5ad8a49fa3c1e8285ba64f1476782fed10ab251ff413ba1/propcache-0.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:ab2943be7c652f09638800905ee1bab2c544e537edb57d527997a24c13dc1455", size = 257393, upload-time = "2025-10-08T19:48:31.567Z" }, + { url = "https://files.pythonhosted.org/packages/08/02/87b25304249a35c0915d236575bc3574a323f60b47939a2262b77632a3ee/propcache-0.4.1-cp314-cp314t-win32.whl", hash = "sha256:05674a162469f31358c30bcaa8883cb7829fa3110bf9c0991fe27d7896c42d85", size = 42546, upload-time = "2025-10-08T19:48:32.872Z" }, + { url = "https://files.pythonhosted.org/packages/cb/ef/3c6ecf8b317aa982f309835e8f96987466123c6e596646d4e6a1dfcd080f/propcache-0.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:990f6b3e2a27d683cb7602ed6c86f15ee6b43b1194736f9baaeb93d0016633b1", size = 46259, upload-time = "2025-10-08T19:48:34.226Z" }, + { url = "https://files.pythonhosted.org/packages/c4/2d/346e946d4951f37eca1e4f55be0f0174c52cd70720f84029b02f296f4a38/propcache-0.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:ecef2343af4cc68e05131e45024ba34f6095821988a9d0a02aa7c73fcc448aa9", size = 40428, upload-time = "2025-10-08T19:48:35.441Z" }, + { url = "https://files.pythonhosted.org/packages/5b/5a/bc7b4a4ef808fa59a816c17b20c4bef6884daebbdf627ff2a161da67da19/propcache-0.4.1-py3-none-any.whl", hash = "sha256:af2a6052aeb6cf17d3e46ee169099044fd8224cbaf75c76a2ef596e8163e2237", size = 13305, upload-time = "2025-10-08T19:49:00.792Z" }, +] + [[package]] name = "proto-plus" version = "1.27.2" @@ -815,6 +2245,49 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c4/72/02445137af02769918a93807b2b7890047c32bfb9f90371cbc12688819eb/protobuf-6.33.6-py3-none-any.whl", hash = "sha256:77179e006c476e69bf8e8ce866640091ec42e1beb80b213c3900006ecfba6901", size = 170656, upload-time = "2026-03-18T19:04:59.826Z" }, ] +[[package]] +name = "pyarrow" +version = "24.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/91/13/13e1069b351bdc3881266e11147ffccf687505dbb0ea74036237f5d454a5/pyarrow-24.0.0.tar.gz", hash = "sha256:85fe721a14dd823aca09127acbb06c3ca723efbd436c004f16bca601b04dcc83", size = 1180261, upload-time = "2026-04-21T10:51:25.837Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b4/a9/9686d9f07837f91f775e8932659192e02c74f9d8920524b480b85212cc68/pyarrow-24.0.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:6233c9ed9ab9d1db47de57d9753256d9dcffbf42db341576099f0fd9f6bf4810", size = 34981559, upload-time = "2026-04-21T10:47:22.17Z" }, + { url = "https://files.pythonhosted.org/packages/80/b6/0ddf0e9b6ead3474ab087ae598c76b031fc45532bf6a63f3a553440fb258/pyarrow-24.0.0-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:f7616236ec1bc2b15bfdec22a71ab38851c86f8f05ff64f379e1278cf20c634a", size = 36663654, upload-time = "2026-04-21T10:47:28.315Z" }, + { url = "https://files.pythonhosted.org/packages/7c/3b/926382efe8ce27ba729071d3566ade6dfb86bdf112f366000196b2f5780a/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:1617043b99bd33e5318ae18eb2919af09c71322ef1ca46566cdafc6e6712fb66", size = 45679394, upload-time = "2026-04-21T10:47:34.821Z" }, + { url = "https://files.pythonhosted.org/packages/b3/7a/829f7d9dfd37c207206081d6dad474d81dde29952401f07f2ba507814818/pyarrow-24.0.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:6165461f55ef6314f026de6638d661188e3455d3ec49834556a0ebbdbace18bb", size = 48863122, upload-time = "2026-04-21T10:47:42.056Z" }, + { url = "https://files.pythonhosted.org/packages/5f/e8/f88ce625fe8babaae64e8db2d417c7653adb3019b08aae85c5ed787dc816/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:3b13dedfe76a0ad2d1d859b0811b53827a4e9d93a0bcb05cf59333ab4980cc7e", size = 49376032, upload-time = "2026-04-21T10:47:48.967Z" }, + { url = "https://files.pythonhosted.org/packages/36/7a/82c363caa145fff88fb475da50d3bf52bb024f61917be5424c3392eaf878/pyarrow-24.0.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:25ea65d868eb04015cd18e6df2fbe98f07e5bda2abefabcb88fce39a947716f6", size = 51929490, upload-time = "2026-04-21T10:47:55.981Z" }, + { url = "https://files.pythonhosted.org/packages/66/1c/e3e72c8014ad2743ca64a701652c733cc5cbcee15c0463a32a8c55518d9e/pyarrow-24.0.0-cp312-cp312-win_amd64.whl", hash = "sha256:295f0a7f2e242dabd513737cf076007dc5b2d59237e3eca37b05c0c6446f3826", size = 27355660, upload-time = "2026-04-21T10:48:01.718Z" }, + { url = "https://files.pythonhosted.org/packages/6f/d3/a1abf004482026ddc17f4503db227787fa3cfe41ec5091ff20e4fea55e57/pyarrow-24.0.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:02b001b3ed4723caa44f6cd1af2d5c86aa2cf9971dacc2ffa55b21237713dfba", size = 34976759, upload-time = "2026-04-21T10:48:07.258Z" }, + { url = "https://files.pythonhosted.org/packages/4f/4a/34f0a36d28a2dd32225301b79daad44e243dc1a2bb77d43b60749be255c4/pyarrow-24.0.0-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:04920d6a71aabd08a0417709efce97d45ea8e6fb733d9ca9ecffb13c67839f68", size = 36658471, upload-time = "2026-04-21T10:48:13.347Z" }, + { url = "https://files.pythonhosted.org/packages/1f/78/543b94712ae8bb1a6023bcc1acf1a740fbff8286747c289cd9468fced2a5/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:a964266397740257f16f7bb2e4f08a0c81454004beab8ff59dd531b73610e9f2", size = 45675981, upload-time = "2026-04-21T10:48:20.201Z" }, + { url = "https://files.pythonhosted.org/packages/84/9f/8fb7c222b100d314137fa40ec050de56cd8c6d957d1cfff685ce72f15b17/pyarrow-24.0.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:6f066b179d68c413374294bc1735f68475457c933258df594443bb9d88ddc2a0", size = 48859172, upload-time = "2026-04-21T10:48:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a7/d3/1ea72538e6c8b3b475ed78d1049a2c518e655761ea50fe1171fc855fcab7/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1183baeb14c5f587b1ec52831e665718ce632caab84b7cd6b85fd44f96114495", size = 49385733, upload-time = "2026-04-21T10:48:34.7Z" }, + { url = "https://files.pythonhosted.org/packages/c3/be/c3d8b06a1ba35f2260f8e1f771abbee7d5e345c0937aab90675706b1690a/pyarrow-24.0.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:806f24b4085453c197a5078218d1ee08783ebbba271badd153d1ae22a3ee804f", size = 51934335, upload-time = "2026-04-21T10:48:42.099Z" }, + { url = "https://files.pythonhosted.org/packages/9c/62/89e07a1e7329d2cde3e3c6994ba0839a24977a2beda8be6005ea3d860b99/pyarrow-24.0.0-cp313-cp313-win_amd64.whl", hash = "sha256:e4505fc6583f7b05ab854934896bcac8253b04ac1171a77dfb73efef92076d91", size = 27271748, upload-time = "2026-04-21T10:49:42.532Z" }, + { url = "https://files.pythonhosted.org/packages/17/1a/cff3a59f80b5b1658549d46611b67163f65e0664431c076ad728bf9d5af4/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1a4e45017efbf115032e4475ee876d525e0e36c742214fbe405332480ecd6275", size = 35238554, upload-time = "2026-04-21T10:48:48.526Z" }, + { url = "https://files.pythonhosted.org/packages/a8/99/cce0f42a327bfef2c420fb6078a3eb834826e5d6697bf3009fe11d2ad051/pyarrow-24.0.0-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:7986f1fa71cee060ad00758bcc79d3a93bab8559bf978fab9e53472a2e25a17b", size = 36782301, upload-time = "2026-04-21T10:48:55.181Z" }, + { url = "https://files.pythonhosted.org/packages/2a/66/8e560d5ff6793ca29aca213c53eec0dd482dd46cb93b2819e5aab52e4252/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:d3e0b61e8efb24ed38898e5cdc5fffa9124be480008d401a1f8071500494ae42", size = 45721929, upload-time = "2026-04-21T10:49:03.676Z" }, + { url = "https://files.pythonhosted.org/packages/27/0c/a26e25505d030716e078d9f16eb74973cbf0b33b672884e9f9da1c83b871/pyarrow-24.0.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:55a3bc1e3df3b5567b7d27ef551b2283f0c68a5e86f1cd56abc569da4f31335b", size = 48825365, upload-time = "2026-04-21T10:49:11.714Z" }, + { url = "https://files.pythonhosted.org/packages/5f/eb/771f9ecb0c65e73fe9dccdd1717901b9594f08c4515d000c7c62df573811/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:641f795b361874ac9da5294f8f443dfdbee355cf2bd9e3b8d97aaac2306b9b37", size = 49451819, upload-time = "2026-04-21T10:49:21.474Z" }, + { url = "https://files.pythonhosted.org/packages/48/da/61ae89a88732f5a785646f3ec6125dbb640fa98a540eb2b9889caa561403/pyarrow-24.0.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8adc8e6ce5fccf5dc707046ae4914fd537def529709cc0d285d37a7f9cd442ca", size = 51909252, upload-time = "2026-04-21T10:49:31.164Z" }, + { url = "https://files.pythonhosted.org/packages/cb/1a/8dd5cafab7b66573fa91c03d06d213356ad4edd71813aa75e08ce2b3a844/pyarrow-24.0.0-cp313-cp313t-win_amd64.whl", hash = "sha256:9b18371ad2f44044b81a8d23bc2d8a9b6a6226dca775e8e16cfee640473d6c5d", size = 27388127, upload-time = "2026-04-21T10:49:37.334Z" }, + { url = "https://files.pythonhosted.org/packages/ad/80/d022a34ff05d2cbedd8ccf841fc1f532ecfa9eb5ed1711b56d0e0ea71fc9/pyarrow-24.0.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:1cc9057f0319e26333b357e17f3c2c022f1a83739b48a88b25bfd5fa2dc18838", size = 35007997, upload-time = "2026-04-21T10:49:48.796Z" }, + { url = "https://files.pythonhosted.org/packages/1a/ff/f01485fda6f4e5d441afb8dd5e7681e4db18826c1e271852f5d3957d6a80/pyarrow-24.0.0-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e6f1278ee4785b6db21229374a1c9e54ec7c549de5d1efc9630b6207de7e170b", size = 36678720, upload-time = "2026-04-21T10:49:55.858Z" }, + { url = "https://files.pythonhosted.org/packages/9e/c2/2d2d5fea814237923f71b36495211f20b43a1576f9a4d6da7e751a64ec6f/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:adbbedc55506cbdabb830890444fb856bfb0060c46c6f8026c6c2f2cf86ae795", size = 45741852, upload-time = "2026-04-21T10:50:04.624Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3a/28ba9c1c1ebdbb5f1b94dfebb46f207e52e6a554b7fe4132540fde29a3a0/pyarrow-24.0.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:ae8a1145af31d903fa9bb166824d7abe9b4681a000b0159c9fb99c11bc11ad26", size = 48889852, upload-time = "2026-04-21T10:50:12.293Z" }, + { url = "https://files.pythonhosted.org/packages/df/51/4a389acfd31dca009f8fb82d7f510bb4130f2b3a8e18cf00194d0687d8ac/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d7027eba1df3b2069e2e8d80f644fa0918b68c46432af3d088ddd390d063ecde", size = 49445207, upload-time = "2026-04-21T10:50:20.677Z" }, + { url = "https://files.pythonhosted.org/packages/19/4b/0bab2b23d2ae901b1b9a03c0efd4b2d070256f8ce3fc43f6e58c167b2081/pyarrow-24.0.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:e56a1ffe9bf7b727432b89104cc0849c21582949dd7bdcb34f17b2001a351a76", size = 51954117, upload-time = "2026-04-21T10:50:29.14Z" }, + { url = "https://files.pythonhosted.org/packages/29/88/f4e9145da0417b3d2c12035a8492b35ff4a3dbc653e614fcfb51d9dedb38/pyarrow-24.0.0-cp314-cp314-win_amd64.whl", hash = "sha256:38be1808cdd068605b787e6ca9119b27eb275a0234e50212c3492331680c3b1e", size = 28001155, upload-time = "2026-04-21T10:51:22.337Z" }, + { url = "https://files.pythonhosted.org/packages/79/4f/46a49a63f43526da895b1a45bbb51d5baf8e4d77159f8528fc3e5490007f/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:418e48ce50a45a6a6c73c454677203a9c75c966cb1e92ca3370959185f197a05", size = 35250387, upload-time = "2026-04-21T10:50:35.552Z" }, + { url = "https://files.pythonhosted.org/packages/a0/da/d5e0cd5ef00796922404806d5f00325cdadc3441ce2c13fe7115f2df9a64/pyarrow-24.0.0-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2f16197705a230a78270cdd4ea8a1d57e86b2fdcbc34a1f6aebc72e65c986f9a", size = 36797102, upload-time = "2026-04-21T10:50:42.417Z" }, + { url = "https://files.pythonhosted.org/packages/34/c7/5904145b0a593a05236c882933d439b5720f0a145381179063722fbfc123/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fb24ac194bfc5e86839d7dcd52092ee31e5fe6733fe11f5e3b06ef0812b20072", size = 45745118, upload-time = "2026-04-21T10:50:49.324Z" }, + { url = "https://files.pythonhosted.org/packages/13/d3/cca42fe166d1c6e4d5b80e530b7949104d10e17508a90ae202dac205ce2a/pyarrow-24.0.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:9700ebd9a51f5895ce75ff4ac4b3c47a7d4b42bc618be8e713e5d56bacf5f931", size = 48844765, upload-time = "2026-04-21T10:50:55.579Z" }, + { url = "https://files.pythonhosted.org/packages/b0/49/942c3b79878ba928324d1e17c274ed84581db8c0a749b24bcf4cbdf15bd3/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:d8ddd2768da81d3ee08cfea9b597f4abb4e8e1dc8ae7e204b608d23a0d3ab699", size = 49471890, upload-time = "2026-04-21T10:51:02.439Z" }, + { url = "https://files.pythonhosted.org/packages/76/97/ff71431000a75d84135a1ace5ca4ba11726a231a8007bbb320a4c54075d5/pyarrow-24.0.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:61a3d7eaa97a14768b542f3d284dc6400dd2470d9f080708b13cd46b6ae18136", size = 51932250, upload-time = "2026-04-21T10:51:10.576Z" }, + { url = "https://files.pythonhosted.org/packages/51/be/6f79d55816d5c22557cf27533543d5d70dfe692adfbee4b99f2760674f38/pyarrow-24.0.0-cp314-cp314t-win_amd64.whl", hash = "sha256:c91d00057f23b8d353039520dc3a6c09d8608164c692e9f59a175a42b2ae0c19", size = 28131282, upload-time = "2026-04-21T10:51:16.815Z" }, +] + [[package]] name = "pyasn1" version = "0.6.3" @@ -854,6 +2327,58 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f4/7e/a72dd26f3b0f4f2bf1dd8923c85f7ceb43172af56d63c7383eb62b332364/pygments-2.20.0-py3-none-any.whl", hash = "sha256:81a9e26dd42fd28a23a2d169d86d7ac03b46e2f8b59ed4698fb4785f946d0176", size = 1231151, upload-time = "2026-03-29T13:29:30.038Z" }, ] +[[package]] +name = "pyogrio" +version = "0.12.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, + { name = "numpy" }, + { name = "packaging" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/49/d4/12f86b1ed09721363da4c09622464b604c851a9223fc0c6b393fb2012208/pyogrio-0.12.1.tar.gz", hash = "sha256:e548ab705bb3e5383693717de1e6c76da97f3762ab92522cb310f93128a75ff1", size = 303289, upload-time = "2025-11-28T19:04:53.341Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ad/e0/656b6536549d41b5aec57e0deca1f269b4f17532f0636836f587e581603a/pyogrio-0.12.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:7a0d5ca39184030aec4cde30f4258f75b227a854530d2659babc8189d76e657d", size = 23661857, upload-time = "2025-11-28T19:03:27.744Z" }, + { url = "https://files.pythonhosted.org/packages/14/78/313259e40da728bdb60106ffdc7ea8224d164498cb838ecb79b634aab967/pyogrio-0.12.1-cp312-cp312-macosx_12_0_x86_64.whl", hash = "sha256:feaff42bbe8087ca0b30e33b09d1ce049ca55fe83ad83db1139ef37d1d04f30c", size = 25237106, upload-time = "2025-11-28T19:03:30.018Z" }, + { url = "https://files.pythonhosted.org/packages/8f/ca/5368571a8b00b941ccfbe6ea29a5566aaffd45d4eb1553b956f7755af43e/pyogrio-0.12.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:81096a5139532de5a8003ef02b41d5d2444cb382a9aecd1165b447eb549180d3", size = 31417048, upload-time = "2025-11-28T19:03:32.572Z" }, + { url = "https://files.pythonhosted.org/packages/ef/85/6eeb875f27bf498d657eb5dab9f58e4c48b36c9037122787abee9a1ba4ba/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:41b78863f782f7a113ed0d36a5dc74d59735bd3a82af53510899bb02a18b06bb", size = 30952115, upload-time = "2025-11-28T19:03:35.332Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/cf8bec9024625947e1a71441906f60a5fa6f9e4c441c4428037e73b1fcc8/pyogrio-0.12.1-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:8b65be8c4258b27cc8f919b21929cecdadda4c353e3637fa30850339ef4d15c5", size = 32537246, upload-time = "2025-11-28T19:03:37.969Z" }, + { url = "https://files.pythonhosted.org/packages/ab/10/7c9f5e428273574e69f217eba3a6c0c42936188ad4dcd9e2c41ebb711188/pyogrio-0.12.1-cp312-cp312-win_amd64.whl", hash = "sha256:1291b866c2c81d991bda15021b08b3621709b40ee3a85689229929e9465788bf", size = 22933980, upload-time = "2025-11-28T19:03:41.047Z" }, + { url = "https://files.pythonhosted.org/packages/be/56/f56e79f71b84aa9bea25fdde39fab3846841bd7926be96f623eb7253b7e1/pyogrio-0.12.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:ec0e47a5a704e575092b2fd5c83fa0472a1d421e590f94093eb837bb0a11125d", size = 23658483, upload-time = "2025-11-28T19:03:43.567Z" }, + { url = "https://files.pythonhosted.org/packages/66/ac/5559f8a35d58a16cbb2dd7602dd11936ff8796d8c9bf789f14da88764ec3/pyogrio-0.12.1-cp313-cp313-macosx_12_0_x86_64.whl", hash = "sha256:b4c888fc08f388be4dd99dfca5e84a5cdc5994deeec0230cc45144d3460e2b21", size = 25232737, upload-time = "2025-11-28T19:03:45.92Z" }, + { url = "https://files.pythonhosted.org/packages/59/58/925f1c129ddd7cbba8dea4e7609797cea7a76dbc863ac9afd318a679c4b9/pyogrio-0.12.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:73a88436f9962750d782853727897ac2722cac5900d920e39fab3e56d7a6a7f1", size = 31377986, upload-time = "2025-11-28T19:03:48.495Z" }, + { url = "https://files.pythonhosted.org/packages/18/5f/c87034e92847b1844d0e8492a6a8e3301147d32c5e57909397ce64dbedf5/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:b5d248a0d59fe9bbf9a35690b70004c67830ee0ebe7d4f7bb8ffd8659f684b3a", size = 30915791, upload-time = "2025-11-28T19:03:51.267Z" }, + { url = "https://files.pythonhosted.org/packages/46/35/b874f79d03e9f900012cf609f7fff97b77164f2e14ee5aac282f8a999c1b/pyogrio-0.12.1-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:0622bc1a186421547660271083079b38d42e6f868802936d8538c0b379f1ab6b", size = 32499754, upload-time = "2025-11-28T19:03:58.776Z" }, + { url = "https://files.pythonhosted.org/packages/c3/c4/705678c9c4200130290b3a104b45c0cc10aaa48fcef3b2585b34e34ab3e1/pyogrio-0.12.1-cp313-cp313-win_amd64.whl", hash = "sha256:207bd60c7ffbcea84584596e3637653aa7095e9ee20fa408f90c7f9460392613", size = 22933945, upload-time = "2025-11-28T19:04:01.551Z" }, + { url = "https://files.pythonhosted.org/packages/f9/e0/d92d4944001330bc87742d43f112d63d12fc89378b6187e62ff3fc1e8e85/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:1511b39a283fa27cda906cd187a791578942a87a40b6a06697d9b43bb8ac80b0", size = 23692697, upload-time = "2025-11-28T19:04:04.208Z" }, + { url = "https://files.pythonhosted.org/packages/e5/d7/40acbe06d1b1140e3bb27b79e9163776469c1dc785f1be7d9a7fc7b95c87/pyogrio-0.12.1-cp313-cp313t-macosx_12_0_x86_64.whl", hash = "sha256:e486cd6aa9ea8a15394a5f84e019d61ec18f257eeeb642348bd68c3d1e57280b", size = 25258083, upload-time = "2025-11-28T19:04:07.121Z" }, + { url = "https://files.pythonhosted.org/packages/87/a1/39fefd9cddd95986700524f43d3093b4350f6e4fc200623c3838424a5080/pyogrio-0.12.1-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:d3f1a19f63bfd1d3042e45f37ad1d6598123a5a604b6c4ba3f38b419273486cd", size = 31368995, upload-time = "2025-11-28T19:04:09.88Z" }, + { url = "https://files.pythonhosted.org/packages/18/d7/da88c566e67d741a03851eb8d01358949d52e0b0fc2cd953582dc6d89ff8/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:f3dcc59b3316b8a0f59346bcc638a4d69997864a4d21da839192f50c4c92369a", size = 31035589, upload-time = "2025-11-28T19:04:12.993Z" }, + { url = "https://files.pythonhosted.org/packages/11/ac/8f0199f0d31b8ddbc4b4ea1918df8070fdf3e0a63100b898633ec9396224/pyogrio-0.12.1-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:a0643e041dee3e8e038fce69f52a915ecb486e6d7b674c0f9919f3c9e9629689", size = 32487973, upload-time = "2025-11-28T19:04:16.103Z" }, + { url = "https://files.pythonhosted.org/packages/bd/64/8541a27e9635a335835d234dfaeb19d6c26097fd88224eda7791f83ca98d/pyogrio-0.12.1-cp313-cp313t-win_amd64.whl", hash = "sha256:5881017f29e110d3613819667657844d8e961b747f2d35cf92f273c27af6d068", size = 22987374, upload-time = "2025-11-28T19:04:18.91Z" }, + { url = "https://files.pythonhosted.org/packages/f4/6f/b4d5e285e08c0c60bcc23b50d73038ddc7335d8de79cc25678cd486a3db0/pyogrio-0.12.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:5a1b0453d1c9e7b03715dd57296c8f3790acb8b50d7e3b5844b3074a18f50709", size = 23660673, upload-time = "2025-11-28T19:04:21.662Z" }, + { url = "https://files.pythonhosted.org/packages/8d/75/4b29e71489c5551aa1a1c5ca8c5160a60203c94f2f68c87c0e3614d58965/pyogrio-0.12.1-cp314-cp314-macosx_12_0_x86_64.whl", hash = "sha256:e7ee560422239dd09ca7f8284cc8483a8919c30d25f3049bb0249bff4c38dec4", size = 25232194, upload-time = "2025-11-28T19:04:23.975Z" }, + { url = "https://files.pythonhosted.org/packages/89/6e/e9929d2261a07c36301983de2767bcde90d441ab5bf1d767ce56dd07f8b4/pyogrio-0.12.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:648c6f7f5f214d30e6cf493b4af1d59782907ac068af9119ca35f18153d6865a", size = 31336936, upload-time = "2025-11-28T19:04:26.594Z" }, + { url = "https://files.pythonhosted.org/packages/1d/9e/c59941d734ed936d4e5c89b4b99cb5541307cc42b3fd466ee78a1850c177/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:58042584f3fd4cabb0f55d26c1405053f656be8a5c266c38140316a1e981aca0", size = 30902210, upload-time = "2025-11-28T19:04:29.143Z" }, + { url = "https://files.pythonhosted.org/packages/d1/68/cc07320a63f9c2586e60bf11d148b00e12d0e707673bffe609bbdcb7e754/pyogrio-0.12.1-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:b438e38e4ccbaedaa5cb5824ff5de5539315d9b2fde6547c1e816576924ee8ca", size = 32461674, upload-time = "2025-11-28T19:04:31.792Z" }, + { url = "https://files.pythonhosted.org/packages/13/bc/e4522f429c45a3b6ad28185849dd76e5c8718b780883c4795e7ee41841ae/pyogrio-0.12.1-cp314-cp314-win_amd64.whl", hash = "sha256:f1d8d8a2fea3781dc2a05982c050259261ebc0f6c5e03732d6d79d582adf9363", size = 23550575, upload-time = "2025-11-28T19:04:34.556Z" }, + { url = "https://files.pythonhosted.org/packages/bd/ac/34f0664d0e391994a7b68529ae07a96432b2b4926dbac173ddc4ec94d310/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:9fe7286946f35a73e6370dc5855bc7a5e8e7babf9e4a8bad7a3279a1d94c7ea9", size = 23694285, upload-time = "2025-11-28T19:04:37.833Z" }, + { url = "https://files.pythonhosted.org/packages/8a/93/873255529faff1da09d0b27287e85ec805a318c60c0c74fd7df77f94e557/pyogrio-0.12.1-cp314-cp314t-macosx_12_0_x86_64.whl", hash = "sha256:2c50345b382f1be801d654ec22c70ee974d6057d4ba7afe984b55f2192bc94ee", size = 25259825, upload-time = "2025-11-28T19:04:40.125Z" }, + { url = "https://files.pythonhosted.org/packages/27/95/4d4c3644695d99c6fa0b0b42f0d6266ae9dfaf64478a3371eaac950bdd02/pyogrio-0.12.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:f0db95765ac0ca935c7fe579e29451294e3ab19c317b0c59c31fbe92a69155e0", size = 31371995, upload-time = "2025-11-28T19:04:42.736Z" }, + { url = "https://files.pythonhosted.org/packages/4c/6f/71f6bcca8754c8bf55a4b7153c61c91f8ac5ba992568e9fa3e54a0ee76fd/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:fc882779075982b93064b3bf3d8642514a6df00d9dd752493b104817072cfb01", size = 31035498, upload-time = "2025-11-28T19:04:45.79Z" }, + { url = "https://files.pythonhosted.org/packages/fd/47/75c1aa165a988347317afab9b938a01ad25dbca559b582ea34473703dc38/pyogrio-0.12.1-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:806f620e0c54b54dbdd65e9b6368d24f344cda84c9343364b40a57eb3e1c4dca", size = 32496390, upload-time = "2025-11-28T19:04:48.786Z" }, + { url = "https://files.pythonhosted.org/packages/31/93/4641dc5d952f6bdb71dabad2c50e3f8a5d58396cdea6ff8f8a08bfd4f4a6/pyogrio-0.12.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5399f66730978d8852ef5f44dbafa0f738e7f28f4f784349f36830b69a9d2134", size = 23620996, upload-time = "2025-11-28T19:04:51.132Z" }, +] + +[[package]] +name = "pyparsing" +version = "3.3.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f3/91/9c6ee907786a473bf81c5f53cf703ba0957b23ab84c264080fb5a450416f/pyparsing-3.3.2.tar.gz", hash = "sha256:c777f4d763f140633dcb6d8a3eda953bf7a214dc4eff598413c070bcdc117cbc", size = 6851574, upload-time = "2026-01-21T03:57:59.36Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/10/bd/c038d7cc38edc1aa5bf91ab8068b63d4308c66c4c8bb3cbba7dfbc049f9c/pyparsing-3.3.2-py3-none-any.whl", hash = "sha256:850ba148bd908d7e2411587e247a1e4f0327839c40e2e5e6d05a007ecc69911d", size = 122781, upload-time = "2026-01-21T03:57:55.912Z" }, +] + [[package]] name = "pypdf" version = "6.10.0" @@ -863,6 +2388,62 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/55/f2/7ebe366f633f30a6ad105f650f44f24f98cb1335c4157d21ae47138b3482/pypdf-6.10.0-py3-none-any.whl", hash = "sha256:90005e959e1596c6e6c84c8b0ad383285b3e17011751cedd17f2ce8fcdfc86de", size = 334459, upload-time = "2026-04-10T09:34:54.966Z" }, ] +[[package]] +name = "pyproj" +version = "3.7.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "certifi" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/04/90/67bd7260b4ea9b8b20b4f58afef6c223ecb3abf368eb4ec5bc2cdef81b49/pyproj-3.7.2.tar.gz", hash = "sha256:39a0cf1ecc7e282d1d30f36594ebd55c9fae1fda8a2622cee5d100430628f88c", size = 226279, upload-time = "2025-08-14T12:05:42.18Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8d/ab/9893ea9fb066be70ed9074ae543914a618c131ed8dff2da1e08b3a4df4db/pyproj-3.7.2-cp312-cp312-macosx_13_0_x86_64.whl", hash = "sha256:0a9bb26a6356fb5b033433a6d1b4542158fb71e3c51de49b4c318a1dff3aeaab", size = 6219832, upload-time = "2025-08-14T12:04:10.264Z" }, + { url = "https://files.pythonhosted.org/packages/53/78/4c64199146eed7184eb0e85bedec60a4aa8853b6ffe1ab1f3a8b962e70a0/pyproj-3.7.2-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:567caa03021178861fad27fabde87500ec6d2ee173dd32f3e2d9871e40eebd68", size = 4620650, upload-time = "2025-08-14T12:04:11.978Z" }, + { url = "https://files.pythonhosted.org/packages/b6/ac/14a78d17943898a93ef4f8c6a9d4169911c994e3161e54a7cedeba9d8dde/pyproj-3.7.2-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:c203101d1dc3c038a56cff0447acc515dd29d6e14811406ac539c21eed422b2a", size = 9667087, upload-time = "2025-08-14T12:04:13.964Z" }, + { url = "https://files.pythonhosted.org/packages/b8/be/212882c450bba74fc8d7d35cbd57e4af84792f0a56194819d98106b075af/pyproj-3.7.2-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:1edc34266c0c23ced85f95a1ee8b47c9035eae6aca5b6b340327250e8e281630", size = 9552797, upload-time = "2025-08-14T12:04:16.624Z" }, + { url = "https://files.pythonhosted.org/packages/ba/c0/c0f25c87b5d2a8686341c53c1792a222a480d6c9caf60311fec12c99ec26/pyproj-3.7.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:aa9f26c21bc0e2dc3d224cb1eb4020cf23e76af179a7c66fea49b828611e4260", size = 10837036, upload-time = "2025-08-14T12:04:18.733Z" }, + { url = "https://files.pythonhosted.org/packages/5d/37/5cbd6772addde2090c91113332623a86e8c7d583eccb2ad02ea634c4a89f/pyproj-3.7.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f9428b318530625cb389b9ddc9c51251e172808a4af79b82809376daaeabe5e9", size = 10775952, upload-time = "2025-08-14T12:04:20.709Z" }, + { url = "https://files.pythonhosted.org/packages/69/a1/dc250e3cf83eb4b3b9a2cf86fdb5e25288bd40037ae449695550f9e96b2f/pyproj-3.7.2-cp312-cp312-win32.whl", hash = "sha256:b3d99ed57d319da042f175f4554fc7038aa4bcecc4ac89e217e350346b742c9d", size = 5898872, upload-time = "2025-08-14T12:04:22.485Z" }, + { url = "https://files.pythonhosted.org/packages/4a/a6/6fe724b72b70f2b00152d77282e14964d60ab092ec225e67c196c9b463e5/pyproj-3.7.2-cp312-cp312-win_amd64.whl", hash = "sha256:11614a054cd86a2ed968a657d00987a86eeb91fdcbd9ad3310478685dc14a128", size = 6312176, upload-time = "2025-08-14T12:04:24.736Z" }, + { url = "https://files.pythonhosted.org/packages/5d/68/915cc32c02a91e76d02c8f55d5a138d6ef9e47a0d96d259df98f4842e558/pyproj-3.7.2-cp312-cp312-win_arm64.whl", hash = "sha256:509a146d1398bafe4f53273398c3bb0b4732535065fa995270e52a9d3676bca3", size = 6233452, upload-time = "2025-08-14T12:04:27.287Z" }, + { url = "https://files.pythonhosted.org/packages/be/14/faf1b90d267cea68d7e70662e7f88cefdb1bc890bd596c74b959e0517a72/pyproj-3.7.2-cp313-cp313-macosx_13_0_x86_64.whl", hash = "sha256:19466e529b1b15eeefdf8ff26b06fa745856c044f2f77bf0edbae94078c1dfa1", size = 6214580, upload-time = "2025-08-14T12:04:28.804Z" }, + { url = "https://files.pythonhosted.org/packages/35/48/da9a45b184d375f62667f62eba0ca68569b0bd980a0bb7ffcc1d50440520/pyproj-3.7.2-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:c79b9b84c4a626c5dc324c0d666be0bfcebd99f7538d66e8898c2444221b3da7", size = 4615388, upload-time = "2025-08-14T12:04:30.553Z" }, + { url = "https://files.pythonhosted.org/packages/5e/e7/d2b459a4a64bca328b712c1b544e109df88e5c800f7c143cfbc404d39bfb/pyproj-3.7.2-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:ceecf374cacca317bc09e165db38ac548ee3cad07c3609442bd70311c59c21aa", size = 9628455, upload-time = "2025-08-14T12:04:32.435Z" }, + { url = "https://files.pythonhosted.org/packages/f8/85/c2b1706e51942de19076eff082f8495e57d5151364e78b5bef4af4a1d94a/pyproj-3.7.2-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:5141a538ffdbe4bfd157421828bb2e07123a90a7a2d6f30fa1462abcfb5ce681", size = 9514269, upload-time = "2025-08-14T12:04:34.599Z" }, + { url = "https://files.pythonhosted.org/packages/34/38/07a9b89ae7467872f9a476883a5bad9e4f4d1219d31060f0f2b282276cbe/pyproj-3.7.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f000841e98ea99acbb7b8ca168d67773b0191de95187228a16110245c5d954d5", size = 10808437, upload-time = "2025-08-14T12:04:36.485Z" }, + { url = "https://files.pythonhosted.org/packages/12/56/fda1daeabbd39dec5b07f67233d09f31facb762587b498e6fc4572be9837/pyproj-3.7.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:8115faf2597f281a42ab608ceac346b4eb1383d3b45ab474fd37341c4bf82a67", size = 10745540, upload-time = "2025-08-14T12:04:38.568Z" }, + { url = "https://files.pythonhosted.org/packages/0d/90/c793182cbba65a39a11db2ac6b479fe76c59e6509ae75e5744c344a0da9d/pyproj-3.7.2-cp313-cp313-win32.whl", hash = "sha256:f18c0579dd6be00b970cb1a6719197fceecc407515bab37da0066f0184aafdf3", size = 5896506, upload-time = "2025-08-14T12:04:41.059Z" }, + { url = "https://files.pythonhosted.org/packages/be/0f/747974129cf0d800906f81cd25efd098c96509026e454d4b66868779ab04/pyproj-3.7.2-cp313-cp313-win_amd64.whl", hash = "sha256:bb41c29d5f60854b1075853fe80c58950b398d4ebb404eb532536ac8d2834ed7", size = 6310195, upload-time = "2025-08-14T12:04:42.974Z" }, + { url = "https://files.pythonhosted.org/packages/82/64/fc7598a53172c4931ec6edf5228280663063150625d3f6423b4c20f9daff/pyproj-3.7.2-cp313-cp313-win_arm64.whl", hash = "sha256:2b617d573be4118c11cd96b8891a0b7f65778fa7733ed8ecdb297a447d439100", size = 6230748, upload-time = "2025-08-14T12:04:44.491Z" }, + { url = "https://files.pythonhosted.org/packages/aa/f0/611dd5cddb0d277f94b7af12981f56e1441bf8d22695065d4f0df5218498/pyproj-3.7.2-cp313-cp313t-macosx_13_0_x86_64.whl", hash = "sha256:d27b48f0e81beeaa2b4d60c516c3a1cfbb0c7ff6ef71256d8e9c07792f735279", size = 6241729, upload-time = "2025-08-14T12:04:46.274Z" }, + { url = "https://files.pythonhosted.org/packages/15/93/40bd4a6c523ff9965e480870611aed7eda5aa2c6128c6537345a2b77b542/pyproj-3.7.2-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:55a3610d75023c7b1c6e583e48ef8f62918e85a2ae81300569d9f104d6684bb6", size = 4652497, upload-time = "2025-08-14T12:04:48.203Z" }, + { url = "https://files.pythonhosted.org/packages/1b/ae/7150ead53c117880b35e0d37960d3138fe640a235feb9605cb9386f50bb0/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:8d7349182fa622696787cc9e195508d2a41a64765da9b8a6bee846702b9e6220", size = 9942610, upload-time = "2025-08-14T12:04:49.652Z" }, + { url = "https://files.pythonhosted.org/packages/d8/17/7a4a7eafecf2b46ab64e5c08176c20ceb5844b503eaa551bf12ccac77322/pyproj-3.7.2-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:d230b186eb876ed4f29a7c5ee310144c3a0e44e89e55f65fb3607e13f6db337c", size = 9692390, upload-time = "2025-08-14T12:04:51.731Z" }, + { url = "https://files.pythonhosted.org/packages/c3/55/ae18f040f6410f0ea547a21ada7ef3e26e6c82befa125b303b02759c0e9d/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:237499c7862c578d0369e2b8ac56eec550e391a025ff70e2af8417139dabb41c", size = 11047596, upload-time = "2025-08-14T12:04:53.748Z" }, + { url = "https://files.pythonhosted.org/packages/e6/2e/d3fff4d2909473f26ae799f9dda04caa322c417a51ff3b25763f7d03b233/pyproj-3.7.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:8c225f5978abd506fd9a78eaaf794435e823c9156091cabaab5374efb29d7f69", size = 10896975, upload-time = "2025-08-14T12:04:55.875Z" }, + { url = "https://files.pythonhosted.org/packages/f2/bc/8fc7d3963d87057b7b51ebe68c1e7c51c23129eee5072ba6b86558544a46/pyproj-3.7.2-cp313-cp313t-win32.whl", hash = "sha256:2da731876d27639ff9d2d81c151f6ab90a1546455fabd93368e753047be344a2", size = 5953057, upload-time = "2025-08-14T12:04:58.466Z" }, + { url = "https://files.pythonhosted.org/packages/cc/27/ea9809966cc47d2d51e6d5ae631ea895f7c7c7b9b3c29718f900a8f7d197/pyproj-3.7.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f54d91ae18dd23b6c0ab48126d446820e725419da10617d86a1b69ada6d881d3", size = 6375414, upload-time = "2025-08-14T12:04:59.861Z" }, + { url = "https://files.pythonhosted.org/packages/5b/f8/1ef0129fba9a555c658e22af68989f35e7ba7b9136f25758809efec0cd6e/pyproj-3.7.2-cp313-cp313t-win_arm64.whl", hash = "sha256:fc52ba896cfc3214dc9f9ca3c0677a623e8fdd096b257c14a31e719d21ff3fdd", size = 6262501, upload-time = "2025-08-14T12:05:01.39Z" }, + { url = "https://files.pythonhosted.org/packages/42/17/c2b050d3f5b71b6edd0d96ae16c990fdc42a5f1366464a5c2772146de33a/pyproj-3.7.2-cp314-cp314-macosx_13_0_x86_64.whl", hash = "sha256:2aaa328605ace41db050d06bac1adc11f01b71fe95c18661497763116c3a0f02", size = 6214541, upload-time = "2025-08-14T12:05:03.166Z" }, + { url = "https://files.pythonhosted.org/packages/03/68/68ada9c8aea96ded09a66cfd9bf87aa6db8c2edebe93f5bf9b66b0143fbc/pyproj-3.7.2-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:35dccbce8201313c596a970fde90e33605248b66272595c061b511c8100ccc08", size = 4617456, upload-time = "2025-08-14T12:05:04.563Z" }, + { url = "https://files.pythonhosted.org/packages/81/e4/4c50ceca7d0e937977866b02cb64e6ccf4df979a5871e521f9e255df6073/pyproj-3.7.2-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:25b0b7cb0042444c29a164b993c45c1b8013d6c48baa61dc1160d834a277e83b", size = 9615590, upload-time = "2025-08-14T12:05:06.094Z" }, + { url = "https://files.pythonhosted.org/packages/05/1e/ada6fb15a1d75b5bd9b554355a69a798c55a7dcc93b8d41596265c1772e3/pyproj-3.7.2-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:85def3a6388e9ba51f964619aa002a9d2098e77c6454ff47773bb68871024281", size = 9474960, upload-time = "2025-08-14T12:05:07.973Z" }, + { url = "https://files.pythonhosted.org/packages/51/07/9d48ad0a8db36e16f842f2c8a694c1d9d7dcf9137264846bef77585a71f3/pyproj-3.7.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:b1bccefec3875ab81eabf49059e2b2ea77362c178b66fd3528c3e4df242f1516", size = 10799478, upload-time = "2025-08-14T12:05:14.102Z" }, + { url = "https://files.pythonhosted.org/packages/85/cf/2f812b529079f72f51ff2d6456b7fef06c01735e5cfd62d54ffb2b548028/pyproj-3.7.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:d5371ca114d6990b675247355a801925814eca53e6c4b2f1b5c0a956336ee36e", size = 10710030, upload-time = "2025-08-14T12:05:16.317Z" }, + { url = "https://files.pythonhosted.org/packages/99/9b/4626a19e1f03eba4c0e77b91a6cf0f73aa9cb5d51a22ee385c22812bcc2c/pyproj-3.7.2-cp314-cp314-win32.whl", hash = "sha256:77f066626030f41be543274f5ac79f2a511fe89860ecd0914f22131b40a0ec25", size = 5991181, upload-time = "2025-08-14T12:05:19.492Z" }, + { url = "https://files.pythonhosted.org/packages/04/b2/5a6610554306a83a563080c2cf2c57565563eadd280e15388efa00fb5b33/pyproj-3.7.2-cp314-cp314-win_amd64.whl", hash = "sha256:5a964da1696b8522806f4276ab04ccfff8f9eb95133a92a25900697609d40112", size = 6434721, upload-time = "2025-08-14T12:05:21.022Z" }, + { url = "https://files.pythonhosted.org/packages/ae/ce/6c910ea2e1c74ef673c5d48c482564b8a7824a44c4e35cca2e765b68cfcc/pyproj-3.7.2-cp314-cp314-win_arm64.whl", hash = "sha256:e258ab4dbd3cf627809067c0ba8f9884ea76c8e5999d039fb37a1619c6c3e1f6", size = 6363821, upload-time = "2025-08-14T12:05:22.627Z" }, + { url = "https://files.pythonhosted.org/packages/e4/e4/5532f6f7491812ba782a2177fe9de73fd8e2912b59f46a1d056b84b9b8f2/pyproj-3.7.2-cp314-cp314t-macosx_13_0_x86_64.whl", hash = "sha256:bbbac2f930c6d266f70ec75df35ef851d96fdb3701c674f42fd23a9314573b37", size = 6241773, upload-time = "2025-08-14T12:05:24.577Z" }, + { url = "https://files.pythonhosted.org/packages/20/1f/0938c3f2bbbef1789132d1726d9b0e662f10cfc22522743937f421ad664e/pyproj-3.7.2-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:b7544e0a3d6339dc9151e9c8f3ea62a936ab7cc446a806ec448bbe86aebb979b", size = 4652537, upload-time = "2025-08-14T12:05:26.391Z" }, + { url = "https://files.pythonhosted.org/packages/c7/a8/488b1ed47d25972f33874f91f09ca8f2227902f05f63a2b80dc73e7b1c97/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:f7f5133dca4c703e8acadf6f30bc567d39a42c6af321e7f81975c2518f3ed357", size = 9940864, upload-time = "2025-08-14T12:05:27.985Z" }, + { url = "https://files.pythonhosted.org/packages/c7/cc/7f4c895d0cb98e47b6a85a6d79eaca03eb266129eed2f845125c09cf31ff/pyproj-3.7.2-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:5aff3343038d7426aa5076f07feb88065f50e0502d1b0d7c22ddfdd2c75a3f81", size = 9688868, upload-time = "2025-08-14T12:05:30.425Z" }, + { url = "https://files.pythonhosted.org/packages/b2/b7/c7e306b8bb0f071d9825b753ee4920f066c40fbfcce9372c4f3cfb2fc4ed/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b0552178c61f2ac1c820d087e8ba6e62b29442debddbb09d51c4bf8acc84d888", size = 11045910, upload-time = "2025-08-14T12:05:32.507Z" }, + { url = "https://files.pythonhosted.org/packages/42/fb/538a4d2df695980e2dde5c04d965fbdd1fe8c20a3194dc4aaa3952a4d1be/pyproj-3.7.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:47d87db2d2c436c5fd0409b34d70bb6cdb875cca2ebe7a9d1c442367b0ab8d59", size = 10895724, upload-time = "2025-08-14T12:05:35.465Z" }, + { url = "https://files.pythonhosted.org/packages/e8/8b/a3f0618b03957de9db5489a04558a8826f43906628bb0b766033aa3b5548/pyproj-3.7.2-cp314-cp314t-win32.whl", hash = "sha256:c9b6f1d8ad3e80a0ee0903a778b6ece7dca1d1d40f6d114ae01bc8ddbad971aa", size = 6056848, upload-time = "2025-08-14T12:05:37.553Z" }, + { url = "https://files.pythonhosted.org/packages/bc/56/413240dd5149dd3291eda55aa55a659da4431244a2fd1319d0ae89407cfb/pyproj-3.7.2-cp314-cp314t-win_amd64.whl", hash = "sha256:1914e29e27933ba6f9822663ee0600f169014a2859f851c054c88cf5ea8a333c", size = 6517676, upload-time = "2025-08-14T12:05:39.126Z" }, + { url = "https://files.pythonhosted.org/packages/15/73/a7141a1a0559bf1a7aa42a11c879ceb19f02f5c6c371c6d57fd86cefd4d1/pyproj-3.7.2-cp314-cp314t-win_arm64.whl", hash = "sha256:d9d25bae416a24397e0d85739f84d323b55f6511e45a522dd7d7eae70d10c7e4", size = 6391844, upload-time = "2025-08-14T12:05:40.745Z" }, +] + [[package]] name = "pytest" version = "9.0.2" @@ -950,6 +2531,217 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" }, ] +[[package]] +name = "rapidfuzz" +version = "3.14.5" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/2c/21/ef6157213316e85790041254259907eb722e00b03480256c0545d98acd33/rapidfuzz-3.14.5.tar.gz", hash = "sha256:ba10ac57884ce82112f7ed910b67e7fb6072d8ef2c06e30dc63c0f604a112e0e", size = 57901753, upload-time = "2026-04-07T11:16:31.931Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/d3/e3/574435c6aafb80254c191ef40d7aca2cb2bb97a095ec9395e9fa59ac307a/rapidfuzz-3.14.5-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:0d3378f471ef440473a396ce2f8e97ee12f89a78b495540e0a5617bbfe895638", size = 1944601, upload-time = "2026-04-07T11:14:18.771Z" }, + { url = "https://files.pythonhosted.org/packages/d0/1f/fbad3102a255ecc112ce9a7e779bacab7fd14398217be8868dc9082ba363/rapidfuzz-3.14.5-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1e910eebca9fd0eba245c0555e764597e8a0cccb673a92da2dc2397050725f48", size = 1164293, upload-time = "2026-04-07T11:14:20.534Z" }, + { url = "https://files.pythonhosted.org/packages/88/37/a3eb7ff6121ed3a5f199a8c38cc86c8e481816f879cb0e0b738b078c9a7e/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:01550fe5f60fd176aa66b7611289d46dc4aa4b1b904874c7b6d1d54e581c5ec1", size = 1371999, upload-time = "2026-04-07T11:14:22.63Z" }, + { url = "https://files.pythonhosted.org/packages/79/72/97a9728c711c7c1b06e107d3f0623880fb4ef90e147ed13c551a1730e7cc/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:48bee0b91bebfaec41e1081e351000659ab7570cc4598d617aa04d5bf827f9e6", size = 3145715, upload-time = "2026-04-07T11:14:24.508Z" }, + { url = "https://files.pythonhosted.org/packages/ed/54/d5caabbea233ac90c286c87c260e49d7641467e87438a18d858e41c82e91/rapidfuzz-3.14.5-cp312-cp312-manylinux_2_39_riscv64.whl", hash = "sha256:7e580cb04ad849ae9b786fa21383c6b994b6e6c1444ad1cb9f22392759d72741", size = 1456304, upload-time = "2026-04-07T11:14:26.515Z" }, + { url = "https://files.pythonhosted.org/packages/fc/a7/2d1a81250ac8c01a0100c026018e76f0e7a097ff63e4c553e02a6938c6fb/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:09d6c9ba091854f07817055d795d604179c12a8f308ba4c7d56f3719dfea1646", size = 2389089, upload-time = "2026-04-07T11:14:28.635Z" }, + { url = "https://files.pythonhosted.org/packages/65/0d/c47c3872203ae88e6506997c0b576ad731f5261daa25d559be09c9756658/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:1e989f86113be66574113b9c7bdf4793f3f863d248e47d911b355e05ca6b6b10", size = 2493404, upload-time = "2026-04-07T11:14:30.577Z" }, + { url = "https://files.pythonhosted.org/packages/8f/2f/71e0a5a3130792146c8a200a2dd1e52aa16f7c1074012e17f2601eea9a90/rapidfuzz-3.14.5-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:0ebd1a18e2e47bc0b292a07e6ed9c3642f8aaa672d12253885f599b50807a4f9", size = 4251709, upload-time = "2026-04-07T11:14:32.451Z" }, + { url = "https://files.pythonhosted.org/packages/86/45/d39874901abacef325adb5b34ae416817c8486dfb4fb87c7a9b74ec5b072/rapidfuzz-3.14.5-cp312-cp312-win32.whl", hash = "sha256:9981d38a703b86f0e315a3cd229fd1906fe1d91c989ed121fb975b3c849f89f5", size = 1710069, upload-time = "2026-04-07T11:14:34.37Z" }, + { url = "https://files.pythonhosted.org/packages/85/0b/f65572c53de8a1c704bda707f63a447b67bdbe95d7cdc70d18885e191df5/rapidfuzz-3.14.5-cp312-cp312-win_amd64.whl", hash = "sha256:d8375e3da319593389727c3187ccaf3e0e84199accc530866b8e0f2b79af05e9", size = 1540630, upload-time = "2026-04-07T11:14:36.287Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c3/143be3a578f989758cae516f3270d5cbb49783a7bfdf57cc27a670e00456/rapidfuzz-3.14.5-cp312-cp312-win_arm64.whl", hash = "sha256:478b59bb018a6780d73f33e38d0b3ec5e968a6c1ed42876b993dd456b7aa20e8", size = 813137, upload-time = "2026-04-07T11:14:38.289Z" }, + { url = "https://files.pythonhosted.org/packages/11/66/252803f2010ba699618cdc048b6e1f7cc1f433c08b4a9a17579b92ab0142/rapidfuzz-3.14.5-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:ebd8fd343bf8492a1e60bcb6dc99f90f74f65d98d8241a6b3e1fed225b76ecd6", size = 1940205, upload-time = "2026-04-07T11:14:40.319Z" }, + { url = "https://files.pythonhosted.org/packages/ea/59/b2afd98e41af9cd54554a4c1c423d84cdd60e6b1c0a09496f033b55f60ec/rapidfuzz-3.14.5-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:6737b35d5af7479c5bf9710f7b17edd9d2c43128d974d25fb4ea653e42c64609", size = 1159639, upload-time = "2026-04-07T11:14:42.52Z" }, + { url = "https://files.pythonhosted.org/packages/a3/31/7aa7e62c4c516a7af322ed0c4f0774208b72d457d0cfec808bad0df12f4a/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b002c7994cc9f2bc9d9856f0fbaee6e8072c983873846c92f25cefba5b2a925f", size = 1367194, upload-time = "2026-04-07T11:14:44.25Z" }, + { url = "https://files.pythonhosted.org/packages/90/79/2fc252a63bc91d3c3b234d0a3a6ad4ebc460037a23cdcdaf9285f986e6c9/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:17a34330cd2a538c1ce5d400b61ba358c5b72c654b928ff87b362e88f8b864c7", size = 3151805, upload-time = "2026-04-07T11:14:46.21Z" }, + { url = "https://files.pythonhosted.org/packages/17/54/0c83508f2683ea70e2d05f8527eb07328acf7bb1e9d97a3bece5702378e7/rapidfuzz-3.14.5-cp313-cp313-manylinux_2_39_riscv64.whl", hash = "sha256:95d937e74c1a7a1287dfb03b62a827be08ede10a155cf1af73bbf47f2b73ee6e", size = 1455667, upload-time = "2026-04-07T11:14:47.991Z" }, + { url = "https://files.pythonhosted.org/packages/71/1b/070175e873177814d58850a01ebe80e20ae11e93eb4da894d563988660fa/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:46b92a9970dcc34f0096901c792644094cab49554ac3547f35e3aebbdf0a3610", size = 2388246, upload-time = "2026-04-07T11:14:50.098Z" }, + { url = "https://files.pythonhosted.org/packages/c9/dd/77caf7aaf9c2be050ad1f128d7c24ff0f59079aa62c5f62f9df41c0af45e/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e012177c8e8a8a0754ae0d6027d63042aa5ff036d9f40f07cb3466a6082e21b8", size = 2494333, upload-time = "2026-04-07T11:14:52.303Z" }, + { url = "https://files.pythonhosted.org/packages/2c/e2/dd7e1f2aa31a8fbbfc16b0610af1d770ffaf1287490f3c8c5b1c52da264f/rapidfuzz-3.14.5-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a2ae6f53f99c9a0eca7a0afc5b4e45fc73bc1dd4ac74c00509031d76df80ed98", size = 4258579, upload-time = "2026-04-07T11:14:54.538Z" }, + { url = "https://files.pythonhosted.org/packages/9c/0a/ac99e1ba347ba0e85e0bb60b74231d55fb93c0eff43f2920ccb413d0be08/rapidfuzz-3.14.5-cp313-cp313-win32.whl", hash = "sha256:4a60f0057231188e3bd30216f7b4e0f279b11fa4ec818bb6c1d9f014d1562fbc", size = 1709231, upload-time = "2026-04-07T11:14:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/cf/cb/0e251d731b3166378644238e8f0cf9e89858c024e19f75ca9f7e3ae83fd5/rapidfuzz-3.14.5-cp313-cp313-win_amd64.whl", hash = "sha256:11bfc2ed8fbe4ab86bd516fadefab126f90e6dcadffa761739fcb304707dfd35", size = 1538519, upload-time = "2026-04-07T11:14:58.635Z" }, + { url = "https://files.pythonhosted.org/packages/30/6f/4548132acc947db6d5346a248e44a8b3a22d608ef30e770fb578caaf2d00/rapidfuzz-3.14.5-cp313-cp313-win_arm64.whl", hash = "sha256:b486b5218808f6f4dc471b114b1054e63553db69705c97da0271f47bd706aedd", size = 812628, upload-time = "2026-04-07T11:15:00.552Z" }, + { url = "https://files.pythonhosted.org/packages/00/60/69b177577290c5eab892c6f75fe89c3aff3f9ae80298a78d9372b1cecb9a/rapidfuzz-3.14.5-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:39ef8658aaf67d51667e7bdaf7096f432333377d8302ac43c70b5df8a4cf89b8", size = 1970231, upload-time = "2026-04-07T11:15:02.603Z" }, + { url = "https://files.pythonhosted.org/packages/48/38/2fd790052659cc4e2907b63c25433f0987864b445c1aeec1a302ef5ad948/rapidfuzz-3.14.5-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:9ad37a0be705b544af6296da8edddc260d10a8ae5462530fc9991f66498bb1f9", size = 1194394, upload-time = "2026-04-07T11:15:04.572Z" }, + { url = "https://files.pythonhosted.org/packages/80/f4/28430ad8472fc3536e8ebd51a864a226e979cfe924c6e3f83d111373aa74/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d45e06f60729e07d9b20c205f7e5cff90b6ef2584e852eecf46e045aea69627d", size = 1377051, upload-time = "2026-04-07T11:15:06.728Z" }, + { url = "https://files.pythonhosted.org/packages/77/7e/9aeacabcfd1e77397968362e5b98fe14248b8307011136b17daf99752a8e/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:e52da10236aa6212de71b9e170bace65b64b129c0dea7fc243d6c9ce976f5074", size = 3160565, upload-time = "2026-04-07T11:15:08.667Z" }, + { url = "https://files.pythonhosted.org/packages/56/f4/db4dd7be0cd2f2022117ac5407d905f435d60e48baaea313a567ad27e865/rapidfuzz-3.14.5-cp313-cp313t-manylinux_2_39_riscv64.whl", hash = "sha256:440d30faaf682ca496170a7f0cc5453ec942e3e079f0fd802c9a7f938dfb50a3", size = 1442113, upload-time = "2026-04-07T11:15:11.138Z" }, + { url = "https://files.pythonhosted.org/packages/a4/99/0e9f6aa57f3e32a767216f797e56dc96b720fcecfb9d8ee907ecc82f8d66/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:56227a61fd3d17b0cd9793132431f3a3d07c8654be96794ba9f89fe0fc8b2d09", size = 2396618, upload-time = "2026-04-07T11:15:13.154Z" }, + { url = "https://files.pythonhosted.org/packages/60/94/44a78e39ffce17cbdd3e2b53b696acc751d5d153be0f499d052b07a4d904/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:2e83cd2e25bb4edd97b689d9979d9c3acccdaaf26ceac08212ceece202febcfa", size = 2478220, upload-time = "2026-04-07T11:15:15.193Z" }, + { url = "https://files.pythonhosted.org/packages/dd/df/454311469a09a507e9d784a35796742bec22e4cebe75551e2da4e0e290fd/rapidfuzz-3.14.5-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:af3b859726cd3374287e405e14b9634563c078c5531a4f62375508addebddad1", size = 4265027, upload-time = "2026-04-07T11:15:17.28Z" }, + { url = "https://files.pythonhosted.org/packages/fc/01/175465a9ab3e3b70ba669058372f009d1d49c1746e2dcd56b69df188d3a5/rapidfuzz-3.14.5-cp313-cp313t-win32.whl", hash = "sha256:8ce1d850b3c0178440efde9e884d98421b5e87ff925f364d6d79e23910d7593f", size = 1766814, upload-time = "2026-04-07T11:15:19.687Z" }, + { url = "https://files.pythonhosted.org/packages/1b/a0/a9b84a47af06ebed94a1439eb2f02adebfb8628bcd30af1fe3e02f5ef56c/rapidfuzz-3.14.5-cp313-cp313t-win_amd64.whl", hash = "sha256:c84af70bcf34e99aee894e46a0f1ac77f17d0ef828179c387407642e2466d28a", size = 1582448, upload-time = "2026-04-07T11:15:21.98Z" }, + { url = "https://files.pythonhosted.org/packages/1e/f1/5937800238b3f8248e70860d79f69ba8f73e764fff47e36bc9e2f26dbcc6/rapidfuzz-3.14.5-cp313-cp313t-win_arm64.whl", hash = "sha256:aac0ad28c686a5e72b81668b906c030ee28050b244544b8af68e12fb32543895", size = 832932, upload-time = "2026-04-07T11:15:24.358Z" }, + { url = "https://files.pythonhosted.org/packages/81/41/aa3ffb3355e62e1bf91f6599b3092e866bc88487a07c524004943c7676df/rapidfuzz-3.14.5-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:1a31cc6d7d03e7318a0974c038959c59e19c752b81115f2e9138b3331cd64d45", size = 1943327, upload-time = "2026-04-07T11:15:26.266Z" }, + { url = "https://files.pythonhosted.org/packages/2d/e1/c2141f1840a41e07ad2db6f724945f8f8ff3065463899a22939152dd6e09/rapidfuzz-3.14.5-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0298d357e2bc59d572da4db0bc631009b6f8f6c9bc8c11e99a12b833f16b6575", size = 1161755, upload-time = "2026-04-07T11:15:28.659Z" }, + { url = "https://files.pythonhosted.org/packages/ca/07/66e753eeaa353161d1d331b7dd517bb349b0bacfebe8496d7b26be26f81f/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:59b3dba758661a318995655435c6ab20a04ade79fa51e75bc8dc107cac8df280", size = 1376571, upload-time = "2026-04-07T11:15:31.225Z" }, + { url = "https://files.pythonhosted.org/packages/c8/85/9535df0b78ba51f478c9ce7eb6d1f85535cc31fe356773b48fd9d3e563ca/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:4900143d82071bdda533b00300c40b14b963ff826b3642cc463b6dd0f036585e", size = 3156468, upload-time = "2026-04-07T11:15:33.428Z" }, + { url = "https://files.pythonhosted.org/packages/81/ee/b667eb93bba6dc4e0de658edd778e1619dc4d6aab68fa5e5c7f075152735/rapidfuzz-3.14.5-cp314-cp314-manylinux_2_39_riscv64.whl", hash = "sha256:feedf219672eef83ea6be6f3bb093bba396a8560fc75be85ba225f082903df0a", size = 1458311, upload-time = "2026-04-07T11:15:35.557Z" }, + { url = "https://files.pythonhosted.org/packages/7d/ce/479074f5624364a48df3403c538797ef22d3ac49c19dc76c3f79fcdcc70c/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:419e4397a36e2665ec992d8d64c20ba4b2a42500c76ecadeca78a4f19cb9cc32", size = 2398228, upload-time = "2026-04-07T11:15:37.669Z" }, + { url = "https://files.pythonhosted.org/packages/0b/15/a8982f649150fffbdcd6f17565974501f6ab33b2795267bffbd4a7ba905b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:97131ab2be39043054ee28d99e09efe316e6d53449b7e962dfcf3c2de8b2b246", size = 2497226, upload-time = "2026-04-07T11:15:39.857Z" }, + { url = "https://files.pythonhosted.org/packages/19/52/5267c03ef6759831b7d4625a0c9c06e87baa2fae084b61ac9c388858317b/rapidfuzz-3.14.5-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:593c00dac4e30231c35bf3b4f1da8ec0998762e9e94425586a5d636fcd57f9d0", size = 4262283, upload-time = "2026-04-07T11:15:42.279Z" }, + { url = "https://files.pythonhosted.org/packages/71/c0/2579f343a97f5254c43bb5853baccc01488357dcb64a27bcb869b7888a4a/rapidfuzz-3.14.5-cp314-cp314-win32.whl", hash = "sha256:0084b687b02b4e569b46d8d6d4ad25659528e6081cd6d067ca453a69035f07e4", size = 1744614, upload-time = "2026-04-07T11:15:44.498Z" }, + { url = "https://files.pythonhosted.org/packages/17/eb/8edfed1e80119dc9c35b11df4bc701eea85622ad681fff0263b6961d3224/rapidfuzz-3.14.5-cp314-cp314-win_amd64.whl", hash = "sha256:5dfa89d78f22cd773054caff44827b846161a29f2dcf7e78b8f90d086621e502", size = 1588971, upload-time = "2026-04-07T11:15:46.86Z" }, + { url = "https://files.pythonhosted.org/packages/f6/04/5676df93c85cfa57a3045d8047318df9f3cd58c7b8a99340dd95f874795e/rapidfuzz-3.14.5-cp314-cp314-win_arm64.whl", hash = "sha256:67f3f9d2b444268ab53e47d31bab89954888d23c04c6789f2c727e51fe4b1d13", size = 834985, upload-time = "2026-04-07T11:15:49.411Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/4a8988cea658fe335048ddef8c876addff1b6daa3c9ca8ad65a5a2196e69/rapidfuzz-3.14.5-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:77eac0526899b3c3ad1454bb2b03cdb491d67358ec8ef0c9c48bd61b632b431d", size = 1972517, upload-time = "2026-04-07T11:15:51.819Z" }, + { url = "https://files.pythonhosted.org/packages/1c/a3/f5cfd9965a9d9a9e32249159797c47b5d6299ea6d1629f9126b25f1c10a3/rapidfuzz-3.14.5-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:b9c6bd754d11f6e78ac54e3d86b4b11dc1ba2f13e5fc958899574532897f5a99", size = 1196056, upload-time = "2026-04-07T11:15:54.292Z" }, + { url = "https://files.pythonhosted.org/packages/64/07/561c2e40cfd10e6630a7b0ac5a2a813aef50d944bcd1f3d260319d659d5b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:738c96944d076deeaff70e92b65696ab4f7ecb8081d7791c5403a3257dfaf8ff", size = 1374732, upload-time = "2026-04-07T11:15:56.584Z" }, + { url = "https://files.pythonhosted.org/packages/c2/39/123bb94fee40e2fb3b7c49b80827c7ef42d838e18def3fc2fef5a3cf817a/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f4c1bca487a17fe4226b4ffb2d30e799d2b274d692cffa76bd0746f56235fca3", size = 3166902, upload-time = "2026-04-07T11:15:58.768Z" }, + { url = "https://files.pythonhosted.org/packages/75/0a/45716fafc9fd2e028cf20b5ac5bc704887081cd312f84edb0e325599414b/rapidfuzz-3.14.5-cp314-cp314t-manylinux_2_39_riscv64.whl", hash = "sha256:af6a90a4ed2a48fa1a2d17e9d824e6c7c950bea5bad0b707c77fd55751e6bfef", size = 1452130, upload-time = "2026-04-07T11:16:01.453Z" }, + { url = "https://files.pythonhosted.org/packages/ca/49/4e96c413114398481c0a5b0086af32c364a18613c9a2ea578d17c4bea4ee/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:bf5018938208d4597b2e679a4f8cff9fd252f1df53583130ae56281a21801b64", size = 2396308, upload-time = "2026-04-07T11:16:03.588Z" }, + { url = "https://files.pythonhosted.org/packages/89/b7/49fea9fc6878d59bd259d01dd1972d9b86117992b1c66d9b16f0a65273c3/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:c0919d1f89ddf91129906705723118ea09754171e4116f5a5dbc667c7bc9b261", size = 2488210, upload-time = "2026-04-07T11:16:05.871Z" }, + { url = "https://files.pythonhosted.org/packages/0c/44/a1f732b93ffacbdad077b7c801149549b2938e1bece6addb5ad85ed74df8/rapidfuzz-3.14.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:93d8da883a35116d6813432177f35e570db5b0a5e30ecb0cbd7cb39c815735df", size = 4270621, upload-time = "2026-04-07T11:16:08.483Z" }, + { url = "https://files.pythonhosted.org/packages/bb/ce/ff942d19fce5385054650bb71a58495ddda299d94661ccc4e6e7fa44868b/rapidfuzz-3.14.5-cp314-cp314t-win32.whl", hash = "sha256:0f23e37019ec07712d58976b1ab2b889f8649a7f7c2f626a2f34ea9139e79279", size = 1803950, upload-time = "2026-04-07T11:16:10.873Z" }, + { url = "https://files.pythonhosted.org/packages/5c/0f/9aafc63f9661222b819b391c187eed29fc90ad5935f9690e5ecc2d2047a4/rapidfuzz-3.14.5-cp314-cp314t-win_amd64.whl", hash = "sha256:7d5ca9c7832e6879a707296d1463685f7c243a27846227044504741640caec66", size = 1632357, upload-time = "2026-04-07T11:16:13.1Z" }, + { url = "https://files.pythonhosted.org/packages/70/a6/51fc1b0e61e3326e1c68a61cfd0c6b3c34c843681c4b1eefbf0596f59162/rapidfuzz-3.14.5-cp314-cp314t-win_arm64.whl", hash = "sha256:3e91dcd2549b8f8d843f98ba03a17e01f3d8b72ce942adbbb6761bc58ffce813", size = 855409, upload-time = "2026-04-07T11:16:15.787Z" }, +] + +[[package]] +name = "rasterio" +version = "1.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "affine" }, + { name = "attrs" }, + { name = "certifi" }, + { name = "click" }, + { name = "cligj" }, + { name = "numpy" }, + { name = "pyparsing" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/f6/88/edb4b66b6cb2c13f123af5a3896bf70c0cbe73ab3cd4243cb4eb0212a0f6/rasterio-1.5.0.tar.gz", hash = "sha256:1e0ea56b02eea4989b36edf8e58a5a3ef40e1b7edcb04def2603accd5ab3ee7b", size = 452184, upload-time = "2026-01-05T16:06:47.169Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ac/de/ba1cd11d7d1182bfb26e758bf07016d04e5442f4f5fea35b0d7279b72399/rasterio-1.5.0-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:420656074897a460f5ef46f657b3061d2e004f9d99e613914b0671643e69d92c", size = 22787192, upload-time = "2026-01-05T16:05:19.779Z" }, + { url = "https://files.pythonhosted.org/packages/e6/42/efaeb6dc531dbcd02fec01c791a853bb5a139a5126ecec579ac0f735eeb9/rasterio-1.5.0-cp312-cp312-macosx_15_0_x86_64.whl", hash = "sha256:c5c3597a783857e760550e8f26365d928b0377ac5ffc3e12ba447ac65ca5406d", size = 24412221, upload-time = "2026-01-05T16:05:22.526Z" }, + { url = "https://files.pythonhosted.org/packages/a2/14/89645988424c40cbcb8334f94305ffe094dd28d85c643341d9690704c9f0/rasterio-1.5.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:e14d07a09833b6df6024ce7a57aee1e1977b3aec682e30b1e58ce773462f2382", size = 36128020, upload-time = "2026-01-05T16:05:25.556Z" }, + { url = "https://files.pythonhosted.org/packages/85/23/5a52319a98451ff910f42e5f7f4804bfb39f9327933a89daab685d1ce2dd/rasterio-1.5.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:26dbcffcf0d01fc121cbb92186bc1cb78e16efe62b17be45ad7494446b325cf8", size = 37634010, upload-time = "2026-01-05T16:05:28.673Z" }, + { url = "https://files.pythonhosted.org/packages/57/d6/fe8826f813c98b046d8d4c3bc83053c89c71f367f89257d211fe5dd0b0ba/rasterio-1.5.0-cp312-cp312-win_amd64.whl", hash = "sha256:ac8d04eee66ca8060763ead607800e5611d857dd005905d920365e24a16ba20a", size = 30142328, upload-time = "2026-01-05T16:05:31.357Z" }, + { url = "https://files.pythonhosted.org/packages/af/62/6397379271d5628ed65ef781bf2d3a8f56094a86e6d8479c6ca506a1b960/rasterio-1.5.0-cp312-cp312-win_arm64.whl", hash = "sha256:31f1edc45c781ebd087e60cc00a4fc37028dd3fe25cff4098e4139fc9d0565be", size = 28500710, upload-time = "2026-01-05T16:05:33.906Z" }, + { url = "https://files.pythonhosted.org/packages/42/87/42865a77cebf2e524d27b6afc71db48984799ecd1dbe6a213d4713f42f5f/rasterio-1.5.0-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:e7b25b0a19975ccd511e507e6de45b0a2d8fb6802abe49bb726cf48588e34833", size = 22776107, upload-time = "2026-01-05T16:05:36.967Z" }, + { url = "https://files.pythonhosted.org/packages/6a/53/e81683fbbfdf04e019e68b042d9cff8524b0571aa80e4f4d81c373c31a49/rasterio-1.5.0-cp313-cp313-macosx_15_0_x86_64.whl", hash = "sha256:1162c18eaece9f6d2aa1c2ff6b373b99651d93f113f24120a991eaebf28aa4f4", size = 24401477, upload-time = "2026-01-05T16:05:39.702Z" }, + { url = "https://files.pythonhosted.org/packages/bc/3c/6aa6e0690b18eea02a61739cb362a47c5df66138f0a02cc69e1181b964e5/rasterio-1.5.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:8eb87fd6f843eea109f3df9bef83f741b053b716b0465932276e2c0577dfb929", size = 36018214, upload-time = "2026-01-05T16:05:42.741Z" }, + { url = "https://files.pythonhosted.org/packages/48/4a/1af9aa9810fb30668568f2c4dd3eec2412c8e9762b69201d971c509b295e/rasterio-1.5.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:08a7580cbb9b3bd320bdf827e10c9b2424d0df066d8eef6f2feb37e154ce0c17", size = 37544972, upload-time = "2026-01-05T16:05:45.815Z" }, + { url = "https://files.pythonhosted.org/packages/01/62/bfe3408743c9837919ff232474a09ece9eaa88d4ee8c040711fa3dff6dad/rasterio-1.5.0-cp313-cp313-win_amd64.whl", hash = "sha256:d7d6729c0739b5ec48c33686668a30e27f5bdb361093f180ee7818ff19665547", size = 30140141, upload-time = "2026-01-05T16:05:48.751Z" }, + { url = "https://files.pythonhosted.org/packages/63/ca/e90e19a6d065a718cc3d468a12b9f015289ad17017656dea8c76f7318d1f/rasterio-1.5.0-cp313-cp313-win_arm64.whl", hash = "sha256:8af7c368c22f0a99d1259ccc5a5cd96c432c2bde6f132c1ac78508cd7445a745", size = 28498556, upload-time = "2026-01-05T16:05:51.334Z" }, + { url = "https://files.pythonhosted.org/packages/a0/ba/e37462d8c33bbbd6c152a0390ec6911a3d9614ded3d2bc6f6a48e147e833/rasterio-1.5.0-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:b4ccfcc8ed9400e4f14efdf2005533fcf72048748b727f85ff89b9291ecdf98a", size = 22920107, upload-time = "2026-01-05T16:05:53.773Z" }, + { url = "https://files.pythonhosted.org/packages/66/dc/7bfa9cf96ac39b451b2f94dfc584c223ec584c52c148df2e4bab60c3341b/rasterio-1.5.0-cp313-cp313t-macosx_15_0_x86_64.whl", hash = "sha256:2f57c36ca4d3c896f7024226bd71eeb5cd10c8183c2a94508534d78cc05ff9e7", size = 24508993, upload-time = "2026-01-05T16:05:57.062Z" }, + { url = "https://files.pythonhosted.org/packages/e5/55/7293743f3b69de4b726c67b8dc9da01fc194070b6becc51add4ca8a20a27/rasterio-1.5.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:cc1395475e4bb7032cd81dda4d5558061c4c7d5a50b1b5e146bdf9716d0b9353", size = 36565784, upload-time = "2026-01-05T16:06:00.019Z" }, + { url = "https://files.pythonhosted.org/packages/cf/ef/5354c47de16c6e289728c3a3d6961ffcf7a9ad6313aef7e8db5d6a40c46e/rasterio-1.5.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:592a485e2057b1aaeab4f843c9897628e60e3ff45e2509325c3e1479116599cb", size = 37686456, upload-time = "2026-01-05T16:06:02.772Z" }, + { url = "https://files.pythonhosted.org/packages/b7/fc/fe1f034b1acd1900d9fbd616826d001a3d5811f1d0c97c785f88f525853e/rasterio-1.5.0-cp313-cp313t-win_amd64.whl", hash = "sha256:0c739e70a72fb080f039ee1570c5d02b974dde32ded1a3216e1f13fe38ac4844", size = 30355842, upload-time = "2026-01-05T16:06:06.359Z" }, + { url = "https://files.pythonhosted.org/packages/e0/cb/4dee9697891c9c6474b240d00e27688e03ecd882d3c83cc97eb25c2266ff/rasterio-1.5.0-cp313-cp313t-win_arm64.whl", hash = "sha256:a3539a2f401a7b4b2e94ff2db334878c0e15a2d1c9fe90bb0879c52f89367ae5", size = 28589538, upload-time = "2026-01-05T16:06:09.662Z" }, + { url = "https://files.pythonhosted.org/packages/77/9f/f84dfa54110c1c82f9f4fd929465d12519569b6f5d015273aa0957013b2e/rasterio-1.5.0-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:597be8df418d5ba7b6a927b6b9febfcb42b192882448a8d5b2e2e75a1296631f", size = 22788832, upload-time = "2026-01-05T16:06:12.247Z" }, + { url = "https://files.pythonhosted.org/packages/20/f1/de55255c918b17afd7292f793a3500c4aea7e9530b2b3f5b3a57836c7d49/rasterio-1.5.0-cp314-cp314-macosx_15_0_x86_64.whl", hash = "sha256:dd292030d39d685c0b35eddef233e7f1cb8b43052578a3ec97a2da57799693be", size = 24405917, upload-time = "2026-01-05T16:06:14.603Z" }, + { url = "https://files.pythonhosted.org/packages/a9/57/054087a9d5011ad5dfa799277ba8814e41775e1967d37a59ab7b8e2f1876/rasterio-1.5.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:62c3f97a3c72643c74f2d0f310621a09c35c0c412229c327ae6bcc1ee4b9c3bc", size = 35987536, upload-time = "2026-01-05T16:06:17.707Z" }, + { url = "https://files.pythonhosted.org/packages/c9/72/5fbe5f67ae75d7e89ffb718c500d5fecbaa84f6ba354db306de689faf961/rasterio-1.5.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:19577f0f0c5f1158af47b57f73356961cbd1782a5f6ae6f3adf6f2650f4eb369", size = 37408048, upload-time = "2026-01-05T16:06:20.82Z" }, + { url = "https://files.pythonhosted.org/packages/c4/3e/0c4ef19980204bdcbc8f9e084056adebc97916ff4edcc718750ef34e5bf9/rasterio-1.5.0-cp314-cp314-win_amd64.whl", hash = "sha256:015c1ab6e5453312c5e29692752e7ad73568fe4d13567cbd448d7893128cbd2d", size = 30949590, upload-time = "2026-01-05T16:06:23.425Z" }, + { url = "https://files.pythonhosted.org/packages/c2/d8/2e6b81505408926c00e629d7d3d73fd0454213201bd9907450e0fe82f3dd/rasterio-1.5.0-cp314-cp314-win_arm64.whl", hash = "sha256:ff677c0a9d3ba667c067227ef2b76872488b37ff29b061bc3e576fad9baa3286", size = 29337287, upload-time = "2026-01-05T16:06:26.599Z" }, + { url = "https://files.pythonhosted.org/packages/19/49/7b6e6afb28d4e3f69f2229f990ed87dfdc21a3e15ca63b96b2fd9ba17d89/rasterio-1.5.0-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:508251b9c746d8d008771a30c2160ff321bfc3b41f6a1aa8e8ef1dd4a00d97ba", size = 22926149, upload-time = "2026-01-05T16:06:29.617Z" }, + { url = "https://files.pythonhosted.org/packages/24/30/19345d8bc7d2b96c1172594026b9009702e9ab9f0baf07079d3612aaadae/rasterio-1.5.0-cp314-cp314t-macosx_15_0_x86_64.whl", hash = "sha256:742841ed48bc70f6ef517b8fa3521f231780bf408fde0aa6d73770337a36374e", size = 24516040, upload-time = "2026-01-05T16:06:32.964Z" }, + { url = "https://files.pythonhosted.org/packages/9e/43/dc7a4518fa78904bc41952cbf346c3c2a88a20e61b479154058392914c0b/rasterio-1.5.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c9a9eee49ce9410c2f352b34c370bb3a96bb518b6a7f97b3a72ee4c835fd4b5c", size = 36589519, upload-time = "2026-01-05T16:06:35.922Z" }, + { url = "https://files.pythonhosted.org/packages/8f/f2/8f706083c6c163054d12c7ed6d5ac4e4ed02252b761288d74e6158871b34/rasterio-1.5.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:b9fd87a0b63ab5c6267dfb0bc96f54fdf49d000651b9ee85ed37798141cff046", size = 37714599, upload-time = "2026-01-05T16:06:38.818Z" }, + { url = "https://files.pythonhosted.org/packages/a6/d5/bbca726d5fea5864f7e4bcf3ee893095369e93ad51120495e8c40e2aa1a0/rasterio-1.5.0-cp314-cp314t-win_amd64.whl", hash = "sha256:f459db8953ba30ca04fcef2b5e1260eeeff0eae8158bd9c3d6adbe56289765cc", size = 31233931, upload-time = "2026-01-05T16:06:42.208Z" }, + { url = "https://files.pythonhosted.org/packages/6e/d1/8b017856e63ccaff3cbd0e82490dbb01363a42f3a462a41b1d8a391e1443/rasterio-1.5.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f4b9c2c3b5f10469eb9588f105086e68f0279e62cc9095c4edd245e3f9b88c8a", size = 29418321, upload-time = "2026-01-05T16:06:44.758Z" }, +] + +[[package]] +name = "regex" +version = "2026.4.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/cb/0e/3a246dbf05666918bd3664d9d787f84a9108f6f43cc953a077e4a7dfdb7e/regex-2026.4.4.tar.gz", hash = "sha256:e08270659717f6973523ce3afbafa53515c4dc5dcad637dc215b6fd50f689423", size = 416000, upload-time = "2026-04-03T20:56:28.155Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e5/28/b972a4d3df61e1d7bcf1b59fdb3cddef22f88b6be43f161bb41ebc0e4081/regex-2026.4.4-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:c07ab8794fa929e58d97a0e1796b8b76f70943fa39df225ac9964615cf1f9d52", size = 490434, upload-time = "2026-04-03T20:53:40.219Z" }, + { url = "https://files.pythonhosted.org/packages/84/20/30041446cf6dc3e0eab344fc62770e84c23b6b68a3b657821f9f80cb69b4/regex-2026.4.4-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:2c785939dc023a1ce4ec09599c032cc9933d258a998d16ca6f2b596c010940eb", size = 292061, upload-time = "2026-04-03T20:53:41.862Z" }, + { url = "https://files.pythonhosted.org/packages/62/c8/3baa06d75c98c46d4cc4262b71fd2edb9062b5665e868bca57859dadf93a/regex-2026.4.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:1b1ce5c81c9114f1ce2f9288a51a8fd3aeea33a0cc440c415bf02da323aa0a76", size = 289628, upload-time = "2026-04-03T20:53:43.701Z" }, + { url = "https://files.pythonhosted.org/packages/31/87/3accf55634caad8c0acab23f5135ef7d4a21c39f28c55c816ae012931408/regex-2026.4.4-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:760ef21c17d8e6a4fe8cf406a97cf2806a4df93416ccc82fc98d25b1c20425be", size = 796651, upload-time = "2026-04-03T20:53:45.379Z" }, + { url = "https://files.pythonhosted.org/packages/f6/0c/aaa2c83f34efedbf06f61cb1942c25f6cf1ee3b200f832c4d05f28306c2e/regex-2026.4.4-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:7088fcdcb604a4417c208e2169715800d28838fefd7455fbe40416231d1d47c1", size = 865916, upload-time = "2026-04-03T20:53:47.064Z" }, + { url = "https://files.pythonhosted.org/packages/d9/f6/8c6924c865124643e8f37823eca845dc27ac509b2ee58123685e71cd0279/regex-2026.4.4-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:07edca1ba687998968f7db5bc355288d0c6505caa7374f013d27356d93976d13", size = 912287, upload-time = "2026-04-03T20:53:49.422Z" }, + { url = "https://files.pythonhosted.org/packages/11/0e/a9f6f81013e0deaf559b25711623864970fe6a098314e374ccb1540a4152/regex-2026.4.4-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:993f657a7c1c6ec51b5e0ba97c9817d06b84ea5fa8d82e43b9405de0defdc2b9", size = 801126, upload-time = "2026-04-03T20:53:51.096Z" }, + { url = "https://files.pythonhosted.org/packages/71/61/3a0cc8af2dc0c8deb48e644dd2521f173f7e6513c6e195aad9aa8dd77ac5/regex-2026.4.4-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:2b69102a743e7569ebee67e634a69c4cb7e59d6fa2e1aa7d3bdbf3f61435f62d", size = 776788, upload-time = "2026-04-03T20:53:52.889Z" }, + { url = "https://files.pythonhosted.org/packages/64/0b/8bb9cbf21ef7dee58e49b0fdb066a7aded146c823202e16494a36777594f/regex-2026.4.4-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:6dac006c8b6dda72d86ea3d1333d45147de79a3a3f26f10c1cf9287ca4ca0ac3", size = 785184, upload-time = "2026-04-03T20:53:55.627Z" }, + { url = "https://files.pythonhosted.org/packages/99/c2/d3e80e8137b25ee06c92627de4e4d98b94830e02b3e6f81f3d2e3f504cf5/regex-2026.4.4-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:50a766ee2010d504554bfb5f578ed2e066898aa26411d57e6296230627cdefa0", size = 859913, upload-time = "2026-04-03T20:53:57.249Z" }, + { url = "https://files.pythonhosted.org/packages/bc/e6/9d5d876157d969c804622456ef250017ac7a8f83e0e14f903b9e6df5ce95/regex-2026.4.4-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:9e2f5217648f68e3028c823df58663587c1507a5ba8419f4fdfc8a461be76043", size = 765732, upload-time = "2026-04-03T20:53:59.428Z" }, + { url = "https://files.pythonhosted.org/packages/82/80/b568935b4421388561c8ed42aff77247285d3ae3bb2a6ca22af63bae805e/regex-2026.4.4-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:39d8de85a08e32632974151ba59c6e9140646dcc36c80423962b1c5c0a92e244", size = 852152, upload-time = "2026-04-03T20:54:01.505Z" }, + { url = "https://files.pythonhosted.org/packages/39/29/f0f81217e21cd998245da047405366385d5c6072048038a3d33b37a79dc0/regex-2026.4.4-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:55d9304e0e7178dfb1e106c33edf834097ddf4a890e2f676f6c5118f84390f73", size = 789076, upload-time = "2026-04-03T20:54:03.323Z" }, + { url = "https://files.pythonhosted.org/packages/49/1d/1d957a61976ab9d4e767dd4f9d04b66cc0c41c5e36cf40e2d43688b5ae6f/regex-2026.4.4-cp312-cp312-win32.whl", hash = "sha256:04bb679bc0bde8a7bfb71e991493d47314e7b98380b083df2447cda4b6edb60f", size = 266700, upload-time = "2026-04-03T20:54:05.639Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/bf575d396aeb58ea13b06ef2adf624f65b70fafef6950a80fc3da9cae3bc/regex-2026.4.4-cp312-cp312-win_amd64.whl", hash = "sha256:db0ac18435a40a2543dbb3d21e161a6c78e33e8159bd2e009343d224bb03bb1b", size = 277768, upload-time = "2026-04-03T20:54:07.312Z" }, + { url = "https://files.pythonhosted.org/packages/c9/27/049df16ec6a6828ccd72add3c7f54b4df029669bea8e9817df6fff58be90/regex-2026.4.4-cp312-cp312-win_arm64.whl", hash = "sha256:4ce255cc05c1947a12989c6db801c96461947adb7a59990f1360b5983fab4983", size = 270568, upload-time = "2026-04-03T20:54:09.484Z" }, + { url = "https://files.pythonhosted.org/packages/9d/83/c4373bc5f31f2cf4b66f9b7c31005bd87fe66f0dce17701f7db4ee79ee29/regex-2026.4.4-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:62f5519042c101762509b1d717b45a69c0139d60414b3c604b81328c01bd1943", size = 490273, upload-time = "2026-04-03T20:54:11.202Z" }, + { url = "https://files.pythonhosted.org/packages/46/f8/fe62afbcc3cf4ad4ac9adeaafd98aa747869ae12d3e8e2ac293d0593c435/regex-2026.4.4-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:3790ba9fb5dd76715a7afe34dbe603ba03f8820764b1dc929dd08106214ed031", size = 291954, upload-time = "2026-04-03T20:54:13.412Z" }, + { url = "https://files.pythonhosted.org/packages/5a/92/4712b9fe6a33d232eeb1c189484b80c6c4b8422b90e766e1195d6e758207/regex-2026.4.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8fae3c6e795d7678963f2170152b0d892cf6aee9ee8afc8c45e6be38d5107fe7", size = 289487, upload-time = "2026-04-03T20:54:15.824Z" }, + { url = "https://files.pythonhosted.org/packages/88/2c/f83b93f85e01168f1070f045a42d4c937b69fdb8dd7ae82d307253f7e36e/regex-2026.4.4-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:298c3ec2d53225b3bf91142eb9691025bab610e0c0c51592dde149db679b3d17", size = 796646, upload-time = "2026-04-03T20:54:18.229Z" }, + { url = "https://files.pythonhosted.org/packages/df/55/61a2e17bf0c4dc57e11caf8dd11771280d8aaa361785f9e3bc40d653f4a7/regex-2026.4.4-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:e9638791082eaf5b3ac112c587518ee78e083a11c4b28012d8fe2a0f536dfb17", size = 865904, upload-time = "2026-04-03T20:54:20.019Z" }, + { url = "https://files.pythonhosted.org/packages/45/32/1ac8ed1b5a346b5993a3d256abe0a0f03b0b73c8cc88d928537368ac65b6/regex-2026.4.4-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:ae3e764bd4c5ff55035dc82a8d49acceb42a5298edf6eb2fc4d328ee5dd7afae", size = 912304, upload-time = "2026-04-03T20:54:22.403Z" }, + { url = "https://files.pythonhosted.org/packages/26/47/2ee5c613ab546f0eddebf9905d23e07beb933416b1246c2d8791d01979b4/regex-2026.4.4-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ffa81f81b80047ba89a3c69ae6a0f78d06f4a42ce5126b0eb2a0a10ad44e0b2e", size = 801126, upload-time = "2026-04-03T20:54:24.308Z" }, + { url = "https://files.pythonhosted.org/packages/75/cd/41dacd129ca9fd20bd7d02f83e0fad83e034ac8a084ec369c90f55ef37e2/regex-2026.4.4-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:f56ebf9d70305307a707911b88469213630aba821e77de7d603f9d2f0730687d", size = 776772, upload-time = "2026-04-03T20:54:26.319Z" }, + { url = "https://files.pythonhosted.org/packages/89/6d/5af0b588174cb5f46041fa7dd64d3fd5cd2fe51f18766703d1edc387f324/regex-2026.4.4-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:773d1dfd652bbffb09336abf890bfd64785c7463716bf766d0eb3bc19c8b7f27", size = 785228, upload-time = "2026-04-03T20:54:28.387Z" }, + { url = "https://files.pythonhosted.org/packages/b7/3b/f5a72b7045bd59575fc33bf1345f156fcfd5a8484aea6ad84b12c5a82114/regex-2026.4.4-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:d51d20befd5275d092cdffba57ded05f3c436317ee56466c8928ac32d960edaf", size = 860032, upload-time = "2026-04-03T20:54:30.641Z" }, + { url = "https://files.pythonhosted.org/packages/39/a4/72a317003d6fcd7a573584a85f59f525dfe8f67e355ca74eb6b53d66a5e2/regex-2026.4.4-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:0a51cdb3c1e9161154f976cb2bef9894bc063ac82f31b733087ffb8e880137d0", size = 765714, upload-time = "2026-04-03T20:54:32.789Z" }, + { url = "https://files.pythonhosted.org/packages/25/1e/5672e16f34dbbcb2560cc7e6a2fbb26dfa8b270711e730101da4423d3973/regex-2026.4.4-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:ae5266a82596114e41fb5302140e9630204c1b5f325c770bec654b95dd54b0aa", size = 852078, upload-time = "2026-04-03T20:54:34.546Z" }, + { url = "https://files.pythonhosted.org/packages/f7/0d/c813f0af7c6cc7ed7b9558bac2e5120b60ad0fa48f813e4d4bd55446f214/regex-2026.4.4-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:c882cd92ec68585e9c1cf36c447ec846c0d94edd706fe59e0c198e65822fd23b", size = 789181, upload-time = "2026-04-03T20:54:36.642Z" }, + { url = "https://files.pythonhosted.org/packages/ea/6d/a344608d1adbd2a95090ddd906cec09a11be0e6517e878d02a5123e0917f/regex-2026.4.4-cp313-cp313-win32.whl", hash = "sha256:05568c4fbf3cb4fa9e28e3af198c40d3237cf6041608a9022285fe567ec3ad62", size = 266690, upload-time = "2026-04-03T20:54:38.343Z" }, + { url = "https://files.pythonhosted.org/packages/31/07/54049f89b46235ca6f45cd6c88668a7050e77d4a15555e47dd40fde75263/regex-2026.4.4-cp313-cp313-win_amd64.whl", hash = "sha256:3384df51ed52db0bea967e21458ab0a414f67cdddfd94401688274e55147bb81", size = 277733, upload-time = "2026-04-03T20:54:40.11Z" }, + { url = "https://files.pythonhosted.org/packages/0e/21/61366a8e20f4d43fb597708cac7f0e2baadb491ecc9549b4980b2be27d16/regex-2026.4.4-cp313-cp313-win_arm64.whl", hash = "sha256:acd38177bd2c8e69a411d6521760806042e244d0ef94e2dd03ecdaa8a3c99427", size = 270565, upload-time = "2026-04-03T20:54:41.883Z" }, + { url = "https://files.pythonhosted.org/packages/f1/1e/3a2b9672433bef02f5d39aa1143ca2c08f311c1d041c464a42be9ae648dc/regex-2026.4.4-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:f94a11a9d05afcfcfa640e096319720a19cc0c9f7768e1a61fceee6a3afc6c7c", size = 494126, upload-time = "2026-04-03T20:54:43.602Z" }, + { url = "https://files.pythonhosted.org/packages/4e/4b/c132a4f4fe18ad3340d89fcb56235132b69559136036b845be3c073142ed/regex-2026.4.4-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:36bcb9d6d1307ab629edc553775baada2aefa5c50ccc0215fbfd2afcfff43141", size = 293882, upload-time = "2026-04-03T20:54:45.41Z" }, + { url = "https://files.pythonhosted.org/packages/f4/5f/eaa38092ce7a023656280f2341dbbd4ad5f05d780a70abba7bb4f4bea54c/regex-2026.4.4-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:261c015b3e2ed0919157046d768774ecde57f03d8fa4ba78d29793447f70e717", size = 292334, upload-time = "2026-04-03T20:54:47.051Z" }, + { url = "https://files.pythonhosted.org/packages/5f/f6/dd38146af1392dac33db7074ab331cec23cced3759167735c42c5460a243/regex-2026.4.4-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c228cf65b4a54583763645dcd73819b3b381ca8b4bb1b349dee1c135f4112c07", size = 811691, upload-time = "2026-04-03T20:54:49.074Z" }, + { url = "https://files.pythonhosted.org/packages/7a/f0/dc54c2e69f5eeec50601054998ec3690d5344277e782bd717e49867c1d29/regex-2026.4.4-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:dd2630faeb6876fb0c287f664d93ddce4d50cd46c6e88e60378c05c9047e08ca", size = 871227, upload-time = "2026-04-03T20:54:51.035Z" }, + { url = "https://files.pythonhosted.org/packages/a1/af/cb16bd5dc61621e27df919a4449bbb7e5a1034c34d307e0a706e9cc0f3e3/regex-2026.4.4-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:6a50ab11b7779b849472337191f3a043e27e17f71555f98d0092fa6d73364520", size = 917435, upload-time = "2026-04-03T20:54:52.994Z" }, + { url = "https://files.pythonhosted.org/packages/5c/71/8b260897f22996b666edd9402861668f45a2ca259f665ac029e6104a2d7d/regex-2026.4.4-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0734f63afe785138549fbe822a8cfeaccd1bae814c5057cc0ed5b9f2de4fc883", size = 816358, upload-time = "2026-04-03T20:54:54.884Z" }, + { url = "https://files.pythonhosted.org/packages/1c/60/775f7f72a510ef238254906c2f3d737fc80b16ca85f07d20e318d2eea894/regex-2026.4.4-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c4ee50606cb1967db7e523224e05f32089101945f859928e65657a2cbb3d278b", size = 785549, upload-time = "2026-04-03T20:54:57.01Z" }, + { url = "https://files.pythonhosted.org/packages/58/42/34d289b3627c03cf381e44da534a0021664188fa49ba41513da0b4ec6776/regex-2026.4.4-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6c1818f37be3ca02dcb76d63f2c7aaba4b0dc171b579796c6fbe00148dfec6b1", size = 801364, upload-time = "2026-04-03T20:54:58.981Z" }, + { url = "https://files.pythonhosted.org/packages/fc/20/f6ecf319b382a8f1ab529e898b222c3f30600fcede7834733c26279e7465/regex-2026.4.4-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:f5bfc2741d150d0be3e4a0401a5c22b06e60acb9aa4daa46d9e79a6dcd0f135b", size = 866221, upload-time = "2026-04-03T20:55:00.88Z" }, + { url = "https://files.pythonhosted.org/packages/92/6a/9f16d3609d549bd96d7a0b2aee1625d7512ba6a03efc01652149ef88e74d/regex-2026.4.4-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:504ffa8a03609a087cad81277a629b6ce884b51a24bd388a7980ad61748618ff", size = 772530, upload-time = "2026-04-03T20:55:03.213Z" }, + { url = "https://files.pythonhosted.org/packages/fa/f6/aa9768bc96a4c361ac96419fbaf2dcdc33970bb813df3ba9b09d5d7b6d96/regex-2026.4.4-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:70aadc6ff12e4b444586e57fc30771f86253f9f0045b29016b9605b4be5f7dfb", size = 856989, upload-time = "2026-04-03T20:55:05.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/b4/c671db3556be2473ae3e4bb7a297c518d281452871501221251ea4ecba57/regex-2026.4.4-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:f4f83781191007b6ef43b03debc35435f10cad9b96e16d147efe84a1d48bdde4", size = 803241, upload-time = "2026-04-03T20:55:07.162Z" }, + { url = "https://files.pythonhosted.org/packages/2a/5c/83e3b1d89fa4f6e5a1bc97b4abd4a9a97b3c1ac7854164f694f5f0ba98a0/regex-2026.4.4-cp313-cp313t-win32.whl", hash = "sha256:e014a797de43d1847df957c0a2a8e861d1c17547ee08467d1db2c370b7568baa", size = 269921, upload-time = "2026-04-03T20:55:09.62Z" }, + { url = "https://files.pythonhosted.org/packages/28/07/077c387121f42cdb4d92b1301133c0d93b5709d096d1669ab847dda9fe2e/regex-2026.4.4-cp313-cp313t-win_amd64.whl", hash = "sha256:b15b88b0d52b179712632832c1d6e58e5774f93717849a41096880442da41ab0", size = 281240, upload-time = "2026-04-03T20:55:11.521Z" }, + { url = "https://files.pythonhosted.org/packages/9d/22/ead4a4abc7c59a4d882662aa292ca02c8b617f30b6e163bc1728879e9353/regex-2026.4.4-cp313-cp313t-win_arm64.whl", hash = "sha256:586b89cdadf7d67bf86ae3342a4dcd2b8d70a832d90c18a0ae955105caf34dbe", size = 272440, upload-time = "2026-04-03T20:55:13.365Z" }, + { url = "https://files.pythonhosted.org/packages/f0/f5/ed97c2dc47b5fbd4b73c0d7d75f9ebc8eca139f2bbef476bba35f28c0a77/regex-2026.4.4-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:2da82d643fa698e5e5210e54af90181603d5853cf469f5eedf9bfc8f59b4b8c7", size = 490343, upload-time = "2026-04-03T20:55:15.241Z" }, + { url = "https://files.pythonhosted.org/packages/80/e9/de4828a7385ec166d673a5790ad06ac48cdaa98bc0960108dd4b9cc1aef7/regex-2026.4.4-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:54a1189ad9d9357760557c91103d5e421f0a2dabe68a5cdf9103d0dcf4e00752", size = 291909, upload-time = "2026-04-03T20:55:17.558Z" }, + { url = "https://files.pythonhosted.org/packages/b4/d6/5cfbfc97f3201a4d24b596a77957e092030dcc4205894bc035cedcfce62f/regex-2026.4.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:76d67d5afb1fe402d10a6403bae668d000441e2ab115191a804287d53b772951", size = 289692, upload-time = "2026-04-03T20:55:20.561Z" }, + { url = "https://files.pythonhosted.org/packages/8e/ac/f2212d9fd56fe897e36d0110ba30ba2d247bd6410c5bd98499c7e5a1e1f2/regex-2026.4.4-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e7cd3e4ee8d80447a83bbc9ab0c8459781fa77087f856c3e740d7763be0df27f", size = 796979, upload-time = "2026-04-03T20:55:22.56Z" }, + { url = "https://files.pythonhosted.org/packages/c9/e3/a016c12675fbac988a60c7e1c16e67823ff0bc016beb27bd7a001dbdabc6/regex-2026.4.4-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2e19e18c568d2866d8b6a6dfad823db86193503f90823a8f66689315ba28fbe8", size = 866744, upload-time = "2026-04-03T20:55:24.646Z" }, + { url = "https://files.pythonhosted.org/packages/af/a4/0b90ca4cf17adc3cb43de80ec71018c37c88ad64987e8d0d481a95ca60b5/regex-2026.4.4-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7698a6f38730fd1385d390d1ed07bb13dce39aa616aca6a6d89bea178464b9a4", size = 911613, upload-time = "2026-04-03T20:55:27.033Z" }, + { url = "https://files.pythonhosted.org/packages/8e/3b/2b3dac0b82d41ab43aa87c6ecde63d71189d03fe8854b8ca455a315edac3/regex-2026.4.4-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:173a66f3651cdb761018078e2d9487f4cf971232c990035ec0eb1cdc6bf929a9", size = 800551, upload-time = "2026-04-03T20:55:29.532Z" }, + { url = "https://files.pythonhosted.org/packages/25/fe/5365eb7aa0e753c4b5957815c321519ecab033c279c60e1b1ae2367fa810/regex-2026.4.4-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:fa7922bbb2cc84fa062d37723f199d4c0cd200245ce269c05db82d904db66b83", size = 776911, upload-time = "2026-04-03T20:55:31.526Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b3/7fb0072156bba065e3b778a7bc7b0a6328212be5dd6a86fd207e0c4f2dab/regex-2026.4.4-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:59f67cd0a0acaf0e564c20bbd7f767286f23e91e2572c5703bf3e56ea7557edb", size = 785751, upload-time = "2026-04-03T20:55:33.797Z" }, + { url = "https://files.pythonhosted.org/packages/02/1a/9f83677eb699273e56e858f7bd95acdbee376d42f59e8bfca2fd80d79df3/regex-2026.4.4-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:475e50f3f73f73614f7cba5524d6de49dee269df00272a1b85e3d19f6d498465", size = 860484, upload-time = "2026-04-03T20:55:35.745Z" }, + { url = "https://files.pythonhosted.org/packages/3b/7a/93937507b61cfcff8b4c5857f1b452852b09f741daa9acae15c971d8554e/regex-2026.4.4-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a1c0c7d67b64d85ac2e1879923bad2f08a08f3004055f2f406ef73c850114bd4", size = 765939, upload-time = "2026-04-03T20:55:37.972Z" }, + { url = "https://files.pythonhosted.org/packages/86/ea/81a7f968a351c6552b1670ead861e2a385be730ee28402233020c67f9e0f/regex-2026.4.4-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:1371c2ccbb744d66ee63631cc9ca12aa233d5749972626b68fe1a649dd98e566", size = 851417, upload-time = "2026-04-03T20:55:39.92Z" }, + { url = "https://files.pythonhosted.org/packages/4c/7e/323c18ce4b5b8f44517a36342961a0306e931e499febbd876bb149d900f0/regex-2026.4.4-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:59968142787042db793348a3f5b918cf24ced1f23247328530e063f89c128a95", size = 789056, upload-time = "2026-04-03T20:55:42.303Z" }, + { url = "https://files.pythonhosted.org/packages/c0/af/e7510f9b11b1913b0cd44eddb784b2d650b2af6515bfce4cffcc5bfd1d38/regex-2026.4.4-cp314-cp314-win32.whl", hash = "sha256:59efe72d37fd5a91e373e5146f187f921f365f4abc1249a5ab446a60f30dd5f8", size = 272130, upload-time = "2026-04-03T20:55:44.995Z" }, + { url = "https://files.pythonhosted.org/packages/9a/51/57dae534c915e2d3a21490e88836fa2ae79dde3b66255ecc0c0a155d2c10/regex-2026.4.4-cp314-cp314-win_amd64.whl", hash = "sha256:e0aab3ff447845049d676827d2ff714aab4f73f340e155b7de7458cf53baa5a4", size = 280992, upload-time = "2026-04-03T20:55:47.316Z" }, + { url = "https://files.pythonhosted.org/packages/0a/5e/abaf9f4c3792e34edb1434f06717fae2b07888d85cb5cec29f9204931bf8/regex-2026.4.4-cp314-cp314-win_arm64.whl", hash = "sha256:a7a5bb6aa0cf62208bb4fa079b0c756734f8ad0e333b425732e8609bd51ee22f", size = 273563, upload-time = "2026-04-03T20:55:49.273Z" }, + { url = "https://files.pythonhosted.org/packages/ff/06/35da85f9f217b9538b99cbb170738993bcc3b23784322decb77619f11502/regex-2026.4.4-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:97850d0638391bdc7d35dc1c1039974dcb921eaafa8cc935ae4d7f272b1d60b3", size = 494191, upload-time = "2026-04-03T20:55:51.258Z" }, + { url = "https://files.pythonhosted.org/packages/54/5b/1bc35f479eef8285c4baf88d8c002023efdeebb7b44a8735b36195486ae7/regex-2026.4.4-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:ee7337f88f2a580679f7bbfe69dc86c043954f9f9c541012f49abc554a962f2e", size = 293877, upload-time = "2026-04-03T20:55:53.214Z" }, + { url = "https://files.pythonhosted.org/packages/39/5b/f53b9ad17480b3ddd14c90da04bfb55ac6894b129e5dea87bcaf7d00e336/regex-2026.4.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7429f4e6192c11d659900c0648ba8776243bf396ab95558b8c51a345afeddde6", size = 292410, upload-time = "2026-04-03T20:55:55.736Z" }, + { url = "https://files.pythonhosted.org/packages/bb/56/52377f59f60a7c51aa4161eecf0b6032c20b461805aca051250da435ffc9/regex-2026.4.4-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc4f10fbd5dd13dcf4265b4cc07d69ca70280742870c97ae10093e3d66000359", size = 811831, upload-time = "2026-04-03T20:55:57.802Z" }, + { url = "https://files.pythonhosted.org/packages/dd/63/8026310bf066f702a9c361f83a8c9658f3fe4edb349f9c1e5d5273b7c40c/regex-2026.4.4-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a152560af4f9742b96f3827090f866eeec5becd4765c8e0d3473d9d280e76a5a", size = 871199, upload-time = "2026-04-03T20:56:00.333Z" }, + { url = "https://files.pythonhosted.org/packages/20/9f/a514bbb00a466dbb506d43f187a04047f7be1505f10a9a15615ead5080ee/regex-2026.4.4-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:54170b3e95339f415d54651f97df3bff7434a663912f9358237941bbf9143f55", size = 917649, upload-time = "2026-04-03T20:56:02.445Z" }, + { url = "https://files.pythonhosted.org/packages/cb/6b/8399f68dd41a2030218839b9b18360d79b86d22b9fab5ef477c7f23ca67c/regex-2026.4.4-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:07f190d65f5a72dcb9cf7106bfc3d21e7a49dd2879eda2207b683f32165e4d99", size = 816388, upload-time = "2026-04-03T20:56:04.595Z" }, + { url = "https://files.pythonhosted.org/packages/1e/9c/103963f47c24339a483b05edd568594c2be486188f688c0170fd504b2948/regex-2026.4.4-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:9a2741ce5a29d3c84b0b94261ba630ab459a1b847a0d6beca7d62d188175c790", size = 785746, upload-time = "2026-04-03T20:56:07.13Z" }, + { url = "https://files.pythonhosted.org/packages/fa/ee/7f6054c0dec0cee3463c304405e4ff42e27cff05bf36fcb34be549ab17bd/regex-2026.4.4-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:b26c30df3a28fd9793113dac7385a4deb7294a06c0f760dd2b008bd49a9139bc", size = 801483, upload-time = "2026-04-03T20:56:09.365Z" }, + { url = "https://files.pythonhosted.org/packages/30/c2/51d3d941cf6070dc00c3338ecf138615fc3cce0421c3df6abe97a08af61a/regex-2026.4.4-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:421439d1bee44b19f4583ccf42670ca464ffb90e9fdc38d37f39d1ddd1e44f1f", size = 866331, upload-time = "2026-04-03T20:56:12.039Z" }, + { url = "https://files.pythonhosted.org/packages/16/e8/76d50dcc122ac33927d939f350eebcfe3dbcbda96913e03433fc36de5e63/regex-2026.4.4-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:b40379b53ecbc747fd9bdf4a0ea14eb8188ca1bd0f54f78893a39024b28f4863", size = 772673, upload-time = "2026-04-03T20:56:14.558Z" }, + { url = "https://files.pythonhosted.org/packages/a5/6e/5f6bf75e20ea6873d05ba4ec78378c375cbe08cdec571c83fbb01606e563/regex-2026.4.4-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:08c55c13d2eef54f73eeadc33146fb0baaa49e7335eb1aff6ae1324bf0ddbe4a", size = 857146, upload-time = "2026-04-03T20:56:16.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/33/3c76d9962949e487ebba353a18e89399f292287204ac8f2f4cfc3a51c233/regex-2026.4.4-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:9776b85f510062f5a75ef112afe5f494ef1635607bf1cc220c1391e9ac2f5e81", size = 803463, upload-time = "2026-04-03T20:56:18.923Z" }, + { url = "https://files.pythonhosted.org/packages/19/eb/ef32dcd2cb69b69bc0c3e55205bce94a7def48d495358946bc42186dcccc/regex-2026.4.4-cp314-cp314t-win32.whl", hash = "sha256:385edaebde5db5be103577afc8699fea73a0e36a734ba24870be7ffa61119d74", size = 275709, upload-time = "2026-04-03T20:56:20.996Z" }, + { url = "https://files.pythonhosted.org/packages/a0/86/c291bf740945acbf35ed7dbebf8e2eea2f3f78041f6bd7cdab80cb274dc0/regex-2026.4.4-cp314-cp314t-win_amd64.whl", hash = "sha256:5d354b18839328927832e2fa5f7c95b7a3ccc39e7a681529e1685898e6436d45", size = 285622, upload-time = "2026-04-03T20:56:23.641Z" }, + { url = "https://files.pythonhosted.org/packages/d5/e7/ec846d560ae6a597115153c02ca6138a7877a1748b2072d9521c10a93e58/regex-2026.4.4-cp314-cp314t-win_arm64.whl", hash = "sha256:af0384cb01a33600c49505c27c6c57ab0b27bf84a74e28524c92ca897ebdac9d", size = 275773, upload-time = "2026-04-03T20:56:26.07Z" }, +] + +[[package]] +name = "reportlab" +version = "4.5.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "charset-normalizer" }, + { name = "pillow" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/dd/23/b8a8b9a5e596ce3de71237c8d6c6a976c763e930878b16340aff3d67ed53/reportlab-4.5.0.tar.gz", hash = "sha256:e595932789ab7a107ba253e83f7815622708a9fd49920d0d6a909880eb66ac75", size = 3914127, upload-time = "2026-04-29T09:12:26.785Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/1c/13/bc43591a54dd38ac5c19e7e849f0311d879737a0d07e032e5be79849a5fb/reportlab-4.5.0-py3-none-any.whl", hash = "sha256:b8cc8996947d84e805368b47b2376070966f091d029351a0d8a1f238984c2c7f", size = 1957238, upload-time = "2026-04-29T09:12:22.904Z" }, +] + [[package]] name = "requests" version = "2.33.1" @@ -965,6 +2757,287 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/d7/8e/7540e8a2036f79a125c1d2ebadf69ed7901608859186c856fa0388ef4197/requests-2.33.1-py3-none-any.whl", hash = "sha256:4e6d1ef462f3626a1f0a0a9c42dd93c63bad33f9f1c1937509b8c5c8718ab56a", size = 64947, upload-time = "2026-03-30T16:09:13.83Z" }, ] +[[package]] +name = "rich" +version = "15.0.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "markdown-it-py" }, + { name = "pygments" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c0/8f/0722ca900cc807c13a6a0c696dacf35430f72e0ec571c4275d2371fca3e9/rich-15.0.0.tar.gz", hash = "sha256:edd07a4824c6b40189fb7ac9bc4c52536e9780fbbfbddf6f1e2502c31b068c36", size = 230680, upload-time = "2026-04-12T08:24:00.75Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/82/3b/64d4899d73f91ba49a8c18a8ff3f0ea8f1c1d75481760df8c68ef5235bf5/rich-15.0.0-py3-none-any.whl", hash = "sha256:33bd4ef74232fb73fe9279a257718407f169c09b78a87ad3d296f548e27de0bb", size = 310654, upload-time = "2026-04-12T08:24:02.83Z" }, +] + +[[package]] +name = "safetensors" +version = "0.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/29/9c/6e74567782559a63bd040a236edca26fd71bc7ba88de2ef35d75df3bca5e/safetensors-0.7.0.tar.gz", hash = "sha256:07663963b67e8bd9f0b8ad15bb9163606cd27cc5a1b96235a50d8369803b96b0", size = 200878, upload-time = "2025-11-19T15:18:43.199Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/fa/47/aef6c06649039accf914afef490268e1067ed82be62bcfa5b7e886ad15e8/safetensors-0.7.0-cp38-abi3-macosx_10_12_x86_64.whl", hash = "sha256:c82f4d474cf725255d9e6acf17252991c3c8aac038d6ef363a4bf8be2f6db517", size = 467781, upload-time = "2025-11-19T15:18:35.84Z" }, + { url = "https://files.pythonhosted.org/packages/e8/00/374c0c068e30cd31f1e1b46b4b5738168ec79e7689ca82ee93ddfea05109/safetensors-0.7.0-cp38-abi3-macosx_11_0_arm64.whl", hash = "sha256:94fd4858284736bb67a897a41608b5b0c2496c9bdb3bf2af1fa3409127f20d57", size = 447058, upload-time = "2025-11-19T15:18:34.416Z" }, + { url = "https://files.pythonhosted.org/packages/f1/06/578ffed52c2296f93d7fd2d844cabfa92be51a587c38c8afbb8ae449ca89/safetensors-0.7.0-cp38-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e07d91d0c92a31200f25351f4acb2bc6aff7f48094e13ebb1d0fb995b54b6542", size = 491748, upload-time = "2025-11-19T15:18:09.79Z" }, + { url = "https://files.pythonhosted.org/packages/ae/33/1debbbb70e4791dde185edb9413d1fe01619255abb64b300157d7f15dddd/safetensors-0.7.0-cp38-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:8469155f4cb518bafb4acf4865e8bb9d6804110d2d9bdcaa78564b9fd841e104", size = 503881, upload-time = "2025-11-19T15:18:16.145Z" }, + { url = "https://files.pythonhosted.org/packages/8e/1c/40c2ca924d60792c3be509833df711b553c60effbd91da6f5284a83f7122/safetensors-0.7.0-cp38-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:54bef08bf00a2bff599982f6b08e8770e09cc012d7bba00783fc7ea38f1fb37d", size = 623463, upload-time = "2025-11-19T15:18:21.11Z" }, + { url = "https://files.pythonhosted.org/packages/9b/3a/13784a9364bd43b0d61eef4bea2845039bc2030458b16594a1bd787ae26e/safetensors-0.7.0-cp38-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:42cb091236206bb2016d245c377ed383aa7f78691748f3bb6ee1bfa51ae2ce6a", size = 532855, upload-time = "2025-11-19T15:18:25.719Z" }, + { url = "https://files.pythonhosted.org/packages/a0/60/429e9b1cb3fc651937727befe258ea24122d9663e4d5709a48c9cbfceecb/safetensors-0.7.0-cp38-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:dac7252938f0696ddea46f5e855dd3138444e82236e3be475f54929f0c510d48", size = 507152, upload-time = "2025-11-19T15:18:33.023Z" }, + { url = "https://files.pythonhosted.org/packages/3c/a8/4b45e4e059270d17af60359713ffd83f97900d45a6afa73aaa0d737d48b6/safetensors-0.7.0-cp38-abi3-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1d060c70284127fa805085d8f10fbd0962792aed71879d00864acda69dbab981", size = 541856, upload-time = "2025-11-19T15:18:31.075Z" }, + { url = "https://files.pythonhosted.org/packages/06/87/d26d8407c44175d8ae164a95b5a62707fcc445f3c0c56108e37d98070a3d/safetensors-0.7.0-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:cdab83a366799fa730f90a4ebb563e494f28e9e92c4819e556152ad55e43591b", size = 674060, upload-time = "2025-11-19T15:18:37.211Z" }, + { url = "https://files.pythonhosted.org/packages/11/f5/57644a2ff08dc6325816ba7217e5095f17269dada2554b658442c66aed51/safetensors-0.7.0-cp38-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:672132907fcad9f2aedcb705b2d7b3b93354a2aec1b2f706c4db852abe338f85", size = 771715, upload-time = "2025-11-19T15:18:38.689Z" }, + { url = "https://files.pythonhosted.org/packages/86/31/17883e13a814bd278ae6e266b13282a01049b0c81341da7fd0e3e71a80a3/safetensors-0.7.0-cp38-abi3-musllinux_1_2_i686.whl", hash = "sha256:5d72abdb8a4d56d4020713724ba81dac065fedb7f3667151c4a637f1d3fb26c0", size = 714377, upload-time = "2025-11-19T15:18:40.162Z" }, + { url = "https://files.pythonhosted.org/packages/4a/d8/0c8a7dc9b41dcac53c4cbf9df2b9c83e0e0097203de8b37a712b345c0be5/safetensors-0.7.0-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:b0f6d66c1c538d5a94a73aa9ddca8ccc4227e6c9ff555322ea40bdd142391dd4", size = 677368, upload-time = "2025-11-19T15:18:41.627Z" }, + { url = "https://files.pythonhosted.org/packages/05/e5/cb4b713c8a93469e3c5be7c3f8d77d307e65fe89673e731f5c2bfd0a9237/safetensors-0.7.0-cp38-abi3-win32.whl", hash = "sha256:c74af94bf3ac15ac4d0f2a7c7b4663a15f8c2ab15ed0fc7531ca61d0835eccba", size = 326423, upload-time = "2025-11-19T15:18:45.74Z" }, + { url = "https://files.pythonhosted.org/packages/5d/e6/ec8471c8072382cb91233ba7267fd931219753bb43814cbc71757bfd4dab/safetensors-0.7.0-cp38-abi3-win_amd64.whl", hash = "sha256:d1239932053f56f3456f32eb9625590cc7582e905021f94636202a864d470755", size = 341380, upload-time = "2025-11-19T15:18:44.427Z" }, +] + +[[package]] +name = "scikit-learn" +version = "1.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "joblib" }, + { name = "numpy" }, + { name = "scipy" }, + { name = "threadpoolctl" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/0e/d4/40988bf3b8e34feec1d0e6a051446b1f66225f8529b9309becaeef62b6c4/scikit_learn-1.8.0.tar.gz", hash = "sha256:9bccbb3b40e3de10351f8f5068e105d0f4083b1a65fa07b6634fbc401a6287fd", size = 7335585, upload-time = "2025-12-10T07:08:53.618Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/90/74/e6a7cc4b820e95cc38cf36cd74d5aa2b42e8ffc2d21fe5a9a9c45c1c7630/scikit_learn-1.8.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fb63362b5a7ddab88e52b6dbb47dac3fd7dafeee740dc6c8d8a446ddedade8e", size = 8548242, upload-time = "2025-12-10T07:07:51.568Z" }, + { url = "https://files.pythonhosted.org/packages/49/d8/9be608c6024d021041c7f0b3928d4749a706f4e2c3832bbede4fb4f58c95/scikit_learn-1.8.0-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:5025ce924beccb28298246e589c691fe1b8c1c96507e6d27d12c5fadd85bfd76", size = 8079075, upload-time = "2025-12-10T07:07:53.697Z" }, + { url = "https://files.pythonhosted.org/packages/dd/47/f187b4636ff80cc63f21cd40b7b2d177134acaa10f6bb73746130ee8c2e5/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:4496bb2cf7a43ce1a2d7524a79e40bc5da45cf598dbf9545b7e8316ccba47bb4", size = 8660492, upload-time = "2025-12-10T07:07:55.574Z" }, + { url = "https://files.pythonhosted.org/packages/97/74/b7a304feb2b49df9fafa9382d4d09061a96ee9a9449a7cbea7988dda0828/scikit_learn-1.8.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a0bcfe4d0d14aec44921545fd2af2338c7471de9cb701f1da4c9d85906ab847a", size = 8931904, upload-time = "2025-12-10T07:07:57.666Z" }, + { url = "https://files.pythonhosted.org/packages/9f/c4/0ab22726a04ede56f689476b760f98f8f46607caecff993017ac1b64aa5d/scikit_learn-1.8.0-cp312-cp312-win_amd64.whl", hash = "sha256:35c007dedb2ffe38fe3ee7d201ebac4a2deccd2408e8621d53067733e3c74809", size = 8019359, upload-time = "2025-12-10T07:07:59.838Z" }, + { url = "https://files.pythonhosted.org/packages/24/90/344a67811cfd561d7335c1b96ca21455e7e472d281c3c279c4d3f2300236/scikit_learn-1.8.0-cp312-cp312-win_arm64.whl", hash = "sha256:8c497fff237d7b4e07e9ef1a640887fa4fb765647f86fbe00f969ff6280ce2bb", size = 7641898, upload-time = "2025-12-10T07:08:01.36Z" }, + { url = "https://files.pythonhosted.org/packages/03/aa/e22e0768512ce9255eba34775be2e85c2048da73da1193e841707f8f039c/scikit_learn-1.8.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:0d6ae97234d5d7079dc0040990a6f7aeb97cb7fa7e8945f1999a429b23569e0a", size = 8513770, upload-time = "2025-12-10T07:08:03.251Z" }, + { url = "https://files.pythonhosted.org/packages/58/37/31b83b2594105f61a381fc74ca19e8780ee923be2d496fcd8d2e1147bd99/scikit_learn-1.8.0-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:edec98c5e7c128328124a029bceb09eda2d526997780fef8d65e9a69eead963e", size = 8044458, upload-time = "2025-12-10T07:08:05.336Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5a/3f1caed8765f33eabb723596666da4ebbf43d11e96550fb18bdec42b467b/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:74b66d8689d52ed04c271e1329f0c61635bcaf5b926db9b12d58914cdc01fe57", size = 8610341, upload-time = "2025-12-10T07:08:07.732Z" }, + { url = "https://files.pythonhosted.org/packages/38/cf/06896db3f71c75902a8e9943b444a56e727418f6b4b4a90c98c934f51ed4/scikit_learn-1.8.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:8fdf95767f989b0cfedb85f7ed8ca215d4be728031f56ff5a519ee1e3276dc2e", size = 8900022, upload-time = "2025-12-10T07:08:09.862Z" }, + { url = "https://files.pythonhosted.org/packages/1c/f9/9b7563caf3ec8873e17a31401858efab6b39a882daf6c1bfa88879c0aa11/scikit_learn-1.8.0-cp313-cp313-win_amd64.whl", hash = "sha256:2de443b9373b3b615aec1bb57f9baa6bb3a9bd093f1269ba95c17d870422b271", size = 7989409, upload-time = "2025-12-10T07:08:12.028Z" }, + { url = "https://files.pythonhosted.org/packages/49/bd/1f4001503650e72c4f6009ac0c4413cb17d2d601cef6f71c0453da2732fc/scikit_learn-1.8.0-cp313-cp313-win_arm64.whl", hash = "sha256:eddde82a035681427cbedded4e6eff5e57fa59216c2e3e90b10b19ab1d0a65c3", size = 7619760, upload-time = "2025-12-10T07:08:13.688Z" }, + { url = "https://files.pythonhosted.org/packages/d2/7d/a630359fc9dcc95496588c8d8e3245cc8fd81980251079bc09c70d41d951/scikit_learn-1.8.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:7cc267b6108f0a1499a734167282c00c4ebf61328566b55ef262d48e9849c735", size = 8826045, upload-time = "2025-12-10T07:08:15.215Z" }, + { url = "https://files.pythonhosted.org/packages/cc/56/a0c86f6930cfcd1c7054a2bc417e26960bb88d32444fe7f71d5c2cfae891/scikit_learn-1.8.0-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:fe1c011a640a9f0791146011dfd3c7d9669785f9fed2b2a5f9e207536cf5c2fd", size = 8420324, upload-time = "2025-12-10T07:08:17.561Z" }, + { url = "https://files.pythonhosted.org/packages/46/1e/05962ea1cebc1cf3876667ecb14c283ef755bf409993c5946ade3b77e303/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:72358cce49465d140cc4e7792015bb1f0296a9742d5622c67e31399b75468b9e", size = 8680651, upload-time = "2025-12-10T07:08:19.952Z" }, + { url = "https://files.pythonhosted.org/packages/fe/56/a85473cd75f200c9759e3a5f0bcab2d116c92a8a02ee08ccd73b870f8bb4/scikit_learn-1.8.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:80832434a6cc114f5219211eec13dcbc16c2bac0e31ef64c6d346cde3cf054cb", size = 8925045, upload-time = "2025-12-10T07:08:22.11Z" }, + { url = "https://files.pythonhosted.org/packages/cc/b7/64d8cfa896c64435ae57f4917a548d7ac7a44762ff9802f75a79b77cb633/scikit_learn-1.8.0-cp313-cp313t-win_amd64.whl", hash = "sha256:ee787491dbfe082d9c3013f01f5991658b0f38aa8177e4cd4bf434c58f551702", size = 8507994, upload-time = "2025-12-10T07:08:23.943Z" }, + { url = "https://files.pythonhosted.org/packages/5e/37/e192ea709551799379958b4c4771ec507347027bb7c942662c7fbeba31cb/scikit_learn-1.8.0-cp313-cp313t-win_arm64.whl", hash = "sha256:bf97c10a3f5a7543f9b88cbf488d33d175e9146115a451ae34568597ba33dcde", size = 7869518, upload-time = "2025-12-10T07:08:25.71Z" }, + { url = "https://files.pythonhosted.org/packages/24/05/1af2c186174cc92dcab2233f327336058c077d38f6fe2aceb08e6ab4d509/scikit_learn-1.8.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:c22a2da7a198c28dd1a6e1136f19c830beab7fdca5b3e5c8bba8394f8a5c45b3", size = 8528667, upload-time = "2025-12-10T07:08:27.541Z" }, + { url = "https://files.pythonhosted.org/packages/a8/25/01c0af38fe969473fb292bba9dc2b8f9b451f3112ff242c647fee3d0dfe7/scikit_learn-1.8.0-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:6b595b07a03069a2b1740dc08c2299993850ea81cce4fe19b2421e0c970de6b7", size = 8066524, upload-time = "2025-12-10T07:08:29.822Z" }, + { url = "https://files.pythonhosted.org/packages/be/ce/a0623350aa0b68647333940ee46fe45086c6060ec604874e38e9ab7d8e6c/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:29ffc74089f3d5e87dfca4c2c8450f88bdc61b0fc6ed5d267f3988f19a1309f6", size = 8657133, upload-time = "2025-12-10T07:08:31.865Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/861b41341d6f1245e6ca80b1c1a8c4dfce43255b03df034429089ca2a2c5/scikit_learn-1.8.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:fb65db5d7531bccf3a4f6bec3462223bea71384e2cda41da0f10b7c292b9e7c4", size = 8923223, upload-time = "2025-12-10T07:08:34.166Z" }, + { url = "https://files.pythonhosted.org/packages/76/18/a8def8f91b18cd1ba6e05dbe02540168cb24d47e8dcf69e8d00b7da42a08/scikit_learn-1.8.0-cp314-cp314-win_amd64.whl", hash = "sha256:56079a99c20d230e873ea40753102102734c5953366972a71d5cb39a32bc40c6", size = 8096518, upload-time = "2025-12-10T07:08:36.339Z" }, + { url = "https://files.pythonhosted.org/packages/d1/77/482076a678458307f0deb44e29891d6022617b2a64c840c725495bee343f/scikit_learn-1.8.0-cp314-cp314-win_arm64.whl", hash = "sha256:3bad7565bc9cf37ce19a7c0d107742b320c1285df7aab1a6e2d28780df167242", size = 7754546, upload-time = "2025-12-10T07:08:38.128Z" }, + { url = "https://files.pythonhosted.org/packages/2d/d1/ef294ca754826daa043b2a104e59960abfab4cf653891037d19dd5b6f3cf/scikit_learn-1.8.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:4511be56637e46c25721e83d1a9cea9614e7badc7040c4d573d75fbe257d6fd7", size = 8848305, upload-time = "2025-12-10T07:08:41.013Z" }, + { url = "https://files.pythonhosted.org/packages/5b/e2/b1f8b05138ee813b8e1a4149f2f0d289547e60851fd1bb268886915adbda/scikit_learn-1.8.0-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:a69525355a641bf8ef136a7fa447672fb54fe8d60cab5538d9eb7c6438543fb9", size = 8432257, upload-time = "2025-12-10T07:08:42.873Z" }, + { url = "https://files.pythonhosted.org/packages/26/11/c32b2138a85dcb0c99f6afd13a70a951bfdff8a6ab42d8160522542fb647/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c2656924ec73e5939c76ac4c8b026fc203b83d8900362eb2599d8aee80e4880f", size = 8678673, upload-time = "2025-12-10T07:08:45.362Z" }, + { url = "https://files.pythonhosted.org/packages/c7/57/51f2384575bdec454f4fe4e7a919d696c9ebce914590abf3e52d47607ab8/scikit_learn-1.8.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:15fc3b5d19cc2be65404786857f2e13c70c83dd4782676dd6814e3b89dc8f5b9", size = 8922467, upload-time = "2025-12-10T07:08:47.408Z" }, + { url = "https://files.pythonhosted.org/packages/35/4d/748c9e2872637a57981a04adc038dacaa16ba8ca887b23e34953f0b3f742/scikit_learn-1.8.0-cp314-cp314t-win_amd64.whl", hash = "sha256:00d6f1d66fbcf4eba6e356e1420d33cc06c70a45bb1363cd6f6a8e4ebbbdece2", size = 8774395, upload-time = "2025-12-10T07:08:49.337Z" }, + { url = "https://files.pythonhosted.org/packages/60/22/d7b2ebe4704a5e50790ba089d5c2ae308ab6bb852719e6c3bd4f04c3a363/scikit_learn-1.8.0-cp314-cp314t-win_arm64.whl", hash = "sha256:f28dd15c6bb0b66ba09728cf09fd8736c304be29409bd8445a080c1280619e8c", size = 8002647, upload-time = "2025-12-10T07:08:51.601Z" }, +] + +[[package]] +name = "scipy" +version = "1.17.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/7a/97/5a3609c4f8d58b039179648e62dd220f89864f56f7357f5d4f45c29eb2cc/scipy-1.17.1.tar.gz", hash = "sha256:95d8e012d8cb8816c226aef832200b1d45109ed4464303e997c5b13122b297c0", size = 30573822, upload-time = "2026-02-23T00:26:24.851Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/35/48/b992b488d6f299dbe3f11a20b24d3dda3d46f1a635ede1c46b5b17a7b163/scipy-1.17.1-cp312-cp312-macosx_10_14_x86_64.whl", hash = "sha256:35c3a56d2ef83efc372eaec584314bd0ef2e2f0d2adb21c55e6ad5b344c0dcb8", size = 31610954, upload-time = "2026-02-23T00:17:49.855Z" }, + { url = "https://files.pythonhosted.org/packages/b2/02/cf107b01494c19dc100f1d0b7ac3cc08666e96ba2d64db7626066cee895e/scipy-1.17.1-cp312-cp312-macosx_12_0_arm64.whl", hash = "sha256:fcb310ddb270a06114bb64bbe53c94926b943f5b7f0842194d585c65eb4edd76", size = 28172662, upload-time = "2026-02-23T00:18:01.64Z" }, + { url = "https://files.pythonhosted.org/packages/cf/a9/599c28631bad314d219cf9ffd40e985b24d603fc8a2f4ccc5ae8419a535b/scipy-1.17.1-cp312-cp312-macosx_14_0_arm64.whl", hash = "sha256:cc90d2e9c7e5c7f1a482c9875007c095c3194b1cfedca3c2f3291cdc2bc7c086", size = 20344366, upload-time = "2026-02-23T00:18:12.015Z" }, + { url = "https://files.pythonhosted.org/packages/35/f5/906eda513271c8deb5af284e5ef0206d17a96239af79f9fa0aebfe0e36b4/scipy-1.17.1-cp312-cp312-macosx_14_0_x86_64.whl", hash = "sha256:c80be5ede8f3f8eded4eff73cc99a25c388ce98e555b17d31da05287015ffa5b", size = 22704017, upload-time = "2026-02-23T00:18:21.502Z" }, + { url = "https://files.pythonhosted.org/packages/da/34/16f10e3042d2f1d6b66e0428308ab52224b6a23049cb2f5c1756f713815f/scipy-1.17.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e19ebea31758fac5893a2ac360fedd00116cbb7628e650842a6691ba7ca28a21", size = 32927842, upload-time = "2026-02-23T00:18:35.367Z" }, + { url = "https://files.pythonhosted.org/packages/01/8e/1e35281b8ab6d5d72ebe9911edcdffa3f36b04ed9d51dec6dd140396e220/scipy-1.17.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:02ae3b274fde71c5e92ac4d54bc06c42d80e399fec704383dcd99b301df37458", size = 35235890, upload-time = "2026-02-23T00:18:49.188Z" }, + { url = "https://files.pythonhosted.org/packages/c5/5c/9d7f4c88bea6e0d5a4f1bc0506a53a00e9fcb198de372bfe4d3652cef482/scipy-1.17.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8a604bae87c6195d8b1045eddece0514d041604b14f2727bbc2b3020172045eb", size = 35003557, upload-time = "2026-02-23T00:18:54.74Z" }, + { url = "https://files.pythonhosted.org/packages/65/94/7698add8f276dbab7a9de9fb6b0e02fc13ee61d51c7c3f85ac28b65e1239/scipy-1.17.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:f590cd684941912d10becc07325a3eeb77886fe981415660d9265c4c418d0bea", size = 37625856, upload-time = "2026-02-23T00:19:00.307Z" }, + { url = "https://files.pythonhosted.org/packages/a2/84/dc08d77fbf3d87d3ee27f6a0c6dcce1de5829a64f2eae85a0ecc1f0daa73/scipy-1.17.1-cp312-cp312-win_amd64.whl", hash = "sha256:41b71f4a3a4cab9d366cd9065b288efc4d4f3c0b37a91a8e0947fb5bd7f31d87", size = 36549682, upload-time = "2026-02-23T00:19:07.67Z" }, + { url = "https://files.pythonhosted.org/packages/bc/98/fe9ae9ffb3b54b62559f52dedaebe204b408db8109a8c66fdd04869e6424/scipy-1.17.1-cp312-cp312-win_arm64.whl", hash = "sha256:f4115102802df98b2b0db3cce5cb9b92572633a1197c77b7553e5203f284a5b3", size = 24547340, upload-time = "2026-02-23T00:19:12.024Z" }, + { url = "https://files.pythonhosted.org/packages/76/27/07ee1b57b65e92645f219b37148a7e7928b82e2b5dbeccecb4dff7c64f0b/scipy-1.17.1-cp313-cp313-macosx_10_14_x86_64.whl", hash = "sha256:5e3c5c011904115f88a39308379c17f91546f77c1667cea98739fe0fccea804c", size = 31590199, upload-time = "2026-02-23T00:19:17.192Z" }, + { url = "https://files.pythonhosted.org/packages/ec/ae/db19f8ab842e9b724bf5dbb7db29302a91f1e55bc4d04b1025d6d605a2c5/scipy-1.17.1-cp313-cp313-macosx_12_0_arm64.whl", hash = "sha256:6fac755ca3d2c3edcb22f479fceaa241704111414831ddd3bc6056e18516892f", size = 28154001, upload-time = "2026-02-23T00:19:22.241Z" }, + { url = "https://files.pythonhosted.org/packages/5b/58/3ce96251560107b381cbd6e8413c483bbb1228a6b919fa8652b0d4090e7f/scipy-1.17.1-cp313-cp313-macosx_14_0_arm64.whl", hash = "sha256:7ff200bf9d24f2e4d5dc6ee8c3ac64d739d3a89e2326ba68aaf6c4a2b838fd7d", size = 20325719, upload-time = "2026-02-23T00:19:26.329Z" }, + { url = "https://files.pythonhosted.org/packages/b2/83/15087d945e0e4d48ce2377498abf5ad171ae013232ae31d06f336e64c999/scipy-1.17.1-cp313-cp313-macosx_14_0_x86_64.whl", hash = "sha256:4b400bdc6f79fa02a4d86640310dde87a21fba0c979efff5248908c6f15fad1b", size = 22683595, upload-time = "2026-02-23T00:19:30.304Z" }, + { url = "https://files.pythonhosted.org/packages/b4/e0/e58fbde4a1a594c8be8114eb4aac1a55bcd6587047efc18a61eb1f5c0d30/scipy-1.17.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2b64ca7d4aee0102a97f3ba22124052b4bd2152522355073580bf4845e2550b6", size = 32896429, upload-time = "2026-02-23T00:19:35.536Z" }, + { url = "https://files.pythonhosted.org/packages/f5/5f/f17563f28ff03c7b6799c50d01d5d856a1d55f2676f537ca8d28c7f627cd/scipy-1.17.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:581b2264fc0aa555f3f435a5944da7504ea3a065d7029ad60e7c3d1ae09c5464", size = 35203952, upload-time = "2026-02-23T00:19:42.259Z" }, + { url = "https://files.pythonhosted.org/packages/8d/a5/9afd17de24f657fdfe4df9a3f1ea049b39aef7c06000c13db1530d81ccca/scipy-1.17.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:beeda3d4ae615106d7094f7e7cef6218392e4465cc95d25f900bebabfded0950", size = 34979063, upload-time = "2026-02-23T00:19:47.547Z" }, + { url = "https://files.pythonhosted.org/packages/8b/13/88b1d2384b424bf7c924f2038c1c409f8d88bb2a8d49d097861dd64a57b2/scipy-1.17.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:6609bc224e9568f65064cfa72edc0f24ee6655b47575954ec6339534b2798369", size = 37598449, upload-time = "2026-02-23T00:19:53.238Z" }, + { url = "https://files.pythonhosted.org/packages/35/e5/d6d0e51fc888f692a35134336866341c08655d92614f492c6860dc45bb2c/scipy-1.17.1-cp313-cp313-win_amd64.whl", hash = "sha256:37425bc9175607b0268f493d79a292c39f9d001a357bebb6b88fdfaff13f6448", size = 36510943, upload-time = "2026-02-23T00:20:50.89Z" }, + { url = "https://files.pythonhosted.org/packages/2a/fd/3be73c564e2a01e690e19cc618811540ba5354c67c8680dce3281123fb79/scipy-1.17.1-cp313-cp313-win_arm64.whl", hash = "sha256:5cf36e801231b6a2059bf354720274b7558746f3b1a4efb43fcf557ccd484a87", size = 24545621, upload-time = "2026-02-23T00:20:55.871Z" }, + { url = "https://files.pythonhosted.org/packages/6f/6b/17787db8b8114933a66f9dcc479a8272e4b4da75fe03b0c282f7b0ade8cd/scipy-1.17.1-cp313-cp313t-macosx_10_14_x86_64.whl", hash = "sha256:d59c30000a16d8edc7e64152e30220bfbd724c9bbb08368c054e24c651314f0a", size = 31936708, upload-time = "2026-02-23T00:19:58.694Z" }, + { url = "https://files.pythonhosted.org/packages/38/2e/524405c2b6392765ab1e2b722a41d5da33dc5c7b7278184a8ad29b6cb206/scipy-1.17.1-cp313-cp313t-macosx_12_0_arm64.whl", hash = "sha256:010f4333c96c9bb1a4516269e33cb5917b08ef2166d5556ca2fd9f082a9e6ea0", size = 28570135, upload-time = "2026-02-23T00:20:03.934Z" }, + { url = "https://files.pythonhosted.org/packages/fd/c3/5bd7199f4ea8556c0c8e39f04ccb014ac37d1468e6cfa6a95c6b3562b76e/scipy-1.17.1-cp313-cp313t-macosx_14_0_arm64.whl", hash = "sha256:2ceb2d3e01c5f1d83c4189737a42d9cb2fc38a6eeed225e7515eef71ad301dce", size = 20741977, upload-time = "2026-02-23T00:20:07.935Z" }, + { url = "https://files.pythonhosted.org/packages/d9/b8/8ccd9b766ad14c78386599708eb745f6b44f08400a5fd0ade7cf89b6fc93/scipy-1.17.1-cp313-cp313t-macosx_14_0_x86_64.whl", hash = "sha256:844e165636711ef41f80b4103ed234181646b98a53c8f05da12ca5ca289134f6", size = 23029601, upload-time = "2026-02-23T00:20:12.161Z" }, + { url = "https://files.pythonhosted.org/packages/6d/a0/3cb6f4d2fb3e17428ad2880333cac878909ad1a89f678527b5328b93c1d4/scipy-1.17.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:158dd96d2207e21c966063e1635b1063cd7787b627b6f07305315dd73d9c679e", size = 33019667, upload-time = "2026-02-23T00:20:17.208Z" }, + { url = "https://files.pythonhosted.org/packages/f3/c3/2d834a5ac7bf3a0c806ad1508efc02dda3c8c61472a56132d7894c312dea/scipy-1.17.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74cbb80d93260fe2ffa334efa24cb8f2f0f622a9b9febf8b483c0b865bfb3475", size = 35264159, upload-time = "2026-02-23T00:20:23.087Z" }, + { url = "https://files.pythonhosted.org/packages/4d/77/d3ed4becfdbd217c52062fafe35a72388d1bd82c2d0ba5ca19d6fcc93e11/scipy-1.17.1-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:dbc12c9f3d185f5c737d801da555fb74b3dcfa1a50b66a1a93e09190f41fab50", size = 35102771, upload-time = "2026-02-23T00:20:28.636Z" }, + { url = "https://files.pythonhosted.org/packages/bd/12/d19da97efde68ca1ee5538bb261d5d2c062f0c055575128f11a2730e3ac1/scipy-1.17.1-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:94055a11dfebe37c656e70317e1996dc197e1a15bbcc351bcdd4610e128fe1ca", size = 37665910, upload-time = "2026-02-23T00:20:34.743Z" }, + { url = "https://files.pythonhosted.org/packages/06/1c/1172a88d507a4baaf72c5a09bb6c018fe2ae0ab622e5830b703a46cc9e44/scipy-1.17.1-cp313-cp313t-win_amd64.whl", hash = "sha256:e30bdeaa5deed6bc27b4cc490823cd0347d7dae09119b8803ae576ea0ce52e4c", size = 36562980, upload-time = "2026-02-23T00:20:40.575Z" }, + { url = "https://files.pythonhosted.org/packages/70/b0/eb757336e5a76dfa7911f63252e3b7d1de00935d7705cf772db5b45ec238/scipy-1.17.1-cp313-cp313t-win_arm64.whl", hash = "sha256:a720477885a9d2411f94a93d16f9d89bad0f28ca23c3f8daa521e2dcc3f44d49", size = 24856543, upload-time = "2026-02-23T00:20:45.313Z" }, + { url = "https://files.pythonhosted.org/packages/cf/83/333afb452af6f0fd70414dc04f898647ee1423979ce02efa75c3b0f2c28e/scipy-1.17.1-cp314-cp314-macosx_10_14_x86_64.whl", hash = "sha256:a48a72c77a310327f6a3a920092fa2b8fd03d7deaa60f093038f22d98e096717", size = 31584510, upload-time = "2026-02-23T00:21:01.015Z" }, + { url = "https://files.pythonhosted.org/packages/ed/a6/d05a85fd51daeb2e4ea71d102f15b34fedca8e931af02594193ae4fd25f7/scipy-1.17.1-cp314-cp314-macosx_12_0_arm64.whl", hash = "sha256:45abad819184f07240d8a696117a7aacd39787af9e0b719d00285549ed19a1e9", size = 28170131, upload-time = "2026-02-23T00:21:05.888Z" }, + { url = "https://files.pythonhosted.org/packages/db/7b/8624a203326675d7746a254083a187398090a179335b2e4a20e2ddc46e83/scipy-1.17.1-cp314-cp314-macosx_14_0_arm64.whl", hash = "sha256:3fd1fcdab3ea951b610dc4cef356d416d5802991e7e32b5254828d342f7b7e0b", size = 20342032, upload-time = "2026-02-23T00:21:09.904Z" }, + { url = "https://files.pythonhosted.org/packages/c9/35/2c342897c00775d688d8ff3987aced3426858fd89d5a0e26e020b660b301/scipy-1.17.1-cp314-cp314-macosx_14_0_x86_64.whl", hash = "sha256:7bdf2da170b67fdf10bca777614b1c7d96ae3ca5794fd9587dce41eb2966e866", size = 22678766, upload-time = "2026-02-23T00:21:14.313Z" }, + { url = "https://files.pythonhosted.org/packages/ef/f2/7cdb8eb308a1a6ae1e19f945913c82c23c0c442a462a46480ce487fdc0ac/scipy-1.17.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:adb2642e060a6549c343603a3851ba76ef0b74cc8c079a9a58121c7ec9fe2350", size = 32957007, upload-time = "2026-02-23T00:21:19.663Z" }, + { url = "https://files.pythonhosted.org/packages/0b/2e/7eea398450457ecb54e18e9d10110993fa65561c4f3add5e8eccd2b9cd41/scipy-1.17.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:eee2cfda04c00a857206a4330f0c5e3e56535494e30ca445eb19ec624ae75118", size = 35221333, upload-time = "2026-02-23T00:21:25.278Z" }, + { url = "https://files.pythonhosted.org/packages/d9/77/5b8509d03b77f093a0d52e606d3c4f79e8b06d1d38c441dacb1e26cacf46/scipy-1.17.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:d2650c1fb97e184d12d8ba010493ee7b322864f7d3d00d3f9bb97d9c21de4068", size = 35042066, upload-time = "2026-02-23T00:21:31.358Z" }, + { url = "https://files.pythonhosted.org/packages/f9/df/18f80fb99df40b4070328d5ae5c596f2f00fffb50167e31439e932f29e7d/scipy-1.17.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:08b900519463543aa604a06bec02461558a6e1cef8fdbb8098f77a48a83c8118", size = 37612763, upload-time = "2026-02-23T00:21:37.247Z" }, + { url = "https://files.pythonhosted.org/packages/4b/39/f0e8ea762a764a9dc52aa7dabcfad51a354819de1f0d4652b6a1122424d6/scipy-1.17.1-cp314-cp314-win_amd64.whl", hash = "sha256:3877ac408e14da24a6196de0ddcace62092bfc12a83823e92e49e40747e52c19", size = 37290984, upload-time = "2026-02-23T00:22:35.023Z" }, + { url = "https://files.pythonhosted.org/packages/7c/56/fe201e3b0f93d1a8bcf75d3379affd228a63d7e2d80ab45467a74b494947/scipy-1.17.1-cp314-cp314-win_arm64.whl", hash = "sha256:f8885db0bc2bffa59d5c1b72fad7a6a92d3e80e7257f967dd81abb553a90d293", size = 25192877, upload-time = "2026-02-23T00:22:39.798Z" }, + { url = "https://files.pythonhosted.org/packages/96/ad/f8c414e121f82e02d76f310f16db9899c4fcde36710329502a6b2a3c0392/scipy-1.17.1-cp314-cp314t-macosx_10_14_x86_64.whl", hash = "sha256:1cc682cea2ae55524432f3cdff9e9a3be743d52a7443d0cba9017c23c87ae2f6", size = 31949750, upload-time = "2026-02-23T00:21:42.289Z" }, + { url = "https://files.pythonhosted.org/packages/7c/b0/c741e8865d61b67c81e255f4f0a832846c064e426636cd7de84e74d209be/scipy-1.17.1-cp314-cp314t-macosx_12_0_arm64.whl", hash = "sha256:2040ad4d1795a0ae89bfc7e8429677f365d45aa9fd5e4587cf1ea737f927b4a1", size = 28585858, upload-time = "2026-02-23T00:21:47.706Z" }, + { url = "https://files.pythonhosted.org/packages/ed/1b/3985219c6177866628fa7c2595bfd23f193ceebbe472c98a08824b9466ff/scipy-1.17.1-cp314-cp314t-macosx_14_0_arm64.whl", hash = "sha256:131f5aaea57602008f9822e2115029b55d4b5f7c070287699fe45c661d051e39", size = 20757723, upload-time = "2026-02-23T00:21:52.039Z" }, + { url = "https://files.pythonhosted.org/packages/c0/19/2a04aa25050d656d6f7b9e7b685cc83d6957fb101665bfd9369ca6534563/scipy-1.17.1-cp314-cp314t-macosx_14_0_x86_64.whl", hash = "sha256:9cdc1a2fcfd5c52cfb3045feb399f7b3ce822abdde3a193a6b9a60b3cb5854ca", size = 23043098, upload-time = "2026-02-23T00:21:56.185Z" }, + { url = "https://files.pythonhosted.org/packages/86/f1/3383beb9b5d0dbddd030335bf8a8b32d4317185efe495374f134d8be6cce/scipy-1.17.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6e3dcd57ab780c741fde8dc68619de988b966db759a3c3152e8e9142c26295ad", size = 33030397, upload-time = "2026-02-23T00:22:01.404Z" }, + { url = "https://files.pythonhosted.org/packages/41/68/8f21e8a65a5a03f25a79165ec9d2b28c00e66dc80546cf5eb803aeeff35b/scipy-1.17.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a9956e4d4f4a301ebf6cde39850333a6b6110799d470dbbb1e25326ac447f52a", size = 35281163, upload-time = "2026-02-23T00:22:07.024Z" }, + { url = "https://files.pythonhosted.org/packages/84/8d/c8a5e19479554007a5632ed7529e665c315ae7492b4f946b0deb39870e39/scipy-1.17.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:a4328d245944d09fd639771de275701ccadf5f781ba0ff092ad141e017eccda4", size = 35116291, upload-time = "2026-02-23T00:22:12.585Z" }, + { url = "https://files.pythonhosted.org/packages/52/52/e57eceff0e342a1f50e274264ed47497b59e6a4e3118808ee58ddda7b74a/scipy-1.17.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:a77cbd07b940d326d39a1d1b37817e2ee4d79cb30e7338f3d0cddffae70fcaa2", size = 37682317, upload-time = "2026-02-23T00:22:18.513Z" }, + { url = "https://files.pythonhosted.org/packages/11/2f/b29eafe4a3fbc3d6de9662b36e028d5f039e72d345e05c250e121a230dd4/scipy-1.17.1-cp314-cp314t-win_amd64.whl", hash = "sha256:eb092099205ef62cd1782b006658db09e2fed75bffcae7cc0d44052d8aa0f484", size = 37345327, upload-time = "2026-02-23T00:22:24.442Z" }, + { url = "https://files.pythonhosted.org/packages/07/39/338d9219c4e87f3e708f18857ecd24d22a0c3094752393319553096b98af/scipy-1.17.1-cp314-cp314t-win_arm64.whl", hash = "sha256:200e1050faffacc162be6a486a984a0497866ec54149a01270adc8a59b7c7d21", size = 25489165, upload-time = "2026-02-23T00:22:29.563Z" }, +] + +[[package]] +name = "seaborn" +version = "0.13.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "matplotlib" }, + { name = "numpy" }, + { name = "pandas" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/86/59/a451d7420a77ab0b98f7affa3a1d78a313d2f7281a57afb1a34bae8ab412/seaborn-0.13.2.tar.gz", hash = "sha256:93e60a40988f4d65e9f4885df477e2fdaff6b73a9ded434c1ab356dd57eefff7", size = 1457696, upload-time = "2024-01-25T13:21:52.551Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/83/11/00d3c3dfc25ad54e731d91449895a79e4bf2384dc3ac01809010ba88f6d5/seaborn-0.13.2-py3-none-any.whl", hash = "sha256:636f8336facf092165e27924f223d3c62ca560b1f2bb5dff7ab7fad265361987", size = 294914, upload-time = "2024-01-25T13:21:49.598Z" }, +] + +[[package]] +name = "sentencepiece" +version = "0.2.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/15/15/2e7a025fc62d764b151ae6d0f2a92f8081755ebe8d4a64099accc6f77ba6/sentencepiece-0.2.1.tar.gz", hash = "sha256:8138cec27c2f2282f4a34d9a016e3374cd40e5c6e9cb335063db66a0a3b71fad", size = 3228515, upload-time = "2025-08-12T07:00:51.718Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/4a/be/32ce495aa1d0e0c323dcb1ba87096037358edee539cac5baf8755a6bd396/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:57cae326c8727de58c85977b175af132a7138d84c764635d7e71bbee7e774133", size = 1943152, upload-time = "2025-08-12T06:59:40.048Z" }, + { url = "https://files.pythonhosted.org/packages/88/7e/ff23008899a58678e98c6ff592bf4d368eee5a71af96d0df6b38a039dd4f/sentencepiece-0.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:56dd39a3c4d6493db3cdca7e8cc68c6b633f0d4195495cbadfcf5af8a22d05a6", size = 1325651, upload-time = "2025-08-12T06:59:41.536Z" }, + { url = "https://files.pythonhosted.org/packages/19/84/42eb3ce4796777a1b5d3699dfd4dca85113e68b637f194a6c8d786f16a04/sentencepiece-0.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:d9381351182ff9888cc80e41c632e7e274b106f450de33d67a9e8f6043da6f76", size = 1253645, upload-time = "2025-08-12T06:59:42.903Z" }, + { url = "https://files.pythonhosted.org/packages/89/fa/d3d5ebcba3cb9e6d3775a096251860c41a6bc53a1b9461151df83fe93255/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:99f955df238021bf11f0fc37cdb54fd5e5b5f7fd30ecc3d93fb48b6815437167", size = 1316273, upload-time = "2025-08-12T06:59:44.476Z" }, + { url = "https://files.pythonhosted.org/packages/04/88/14f2f4a2b922d8b39be45bf63d79e6cd3a9b2f248b2fcb98a69b12af12f5/sentencepiece-0.2.1-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0cdfecef430d985f1c2bcbfff3defd1d95dae876fbd0173376012d2d7d24044b", size = 1387881, upload-time = "2025-08-12T06:59:46.09Z" }, + { url = "https://files.pythonhosted.org/packages/fd/b8/903e5ccb77b4ef140605d5d71b4f9e0ad95d456d6184688073ed11712809/sentencepiece-0.2.1-cp312-cp312-win32.whl", hash = "sha256:a483fd29a34c3e34c39ac5556b0a90942bec253d260235729e50976f5dba1068", size = 999540, upload-time = "2025-08-12T06:59:48.023Z" }, + { url = "https://files.pythonhosted.org/packages/2d/81/92df5673c067148c2545b1bfe49adfd775bcc3a169a047f5a0e6575ddaca/sentencepiece-0.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:4cdc7c36234fda305e85c32949c5211faaf8dd886096c7cea289ddc12a2d02de", size = 1054671, upload-time = "2025-08-12T06:59:49.895Z" }, + { url = "https://files.pythonhosted.org/packages/fe/02/c5e3bc518655d714622bec87d83db9cdba1cd0619a4a04e2109751c4f47f/sentencepiece-0.2.1-cp312-cp312-win_arm64.whl", hash = "sha256:daeb5e9e9fcad012324807856113708614d534f596d5008638eb9b40112cd9e4", size = 1033923, upload-time = "2025-08-12T06:59:51.952Z" }, + { url = "https://files.pythonhosted.org/packages/ba/4a/85fbe1706d4d04a7e826b53f327c4b80f849cf1c7b7c5e31a20a97d8f28b/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:dcd8161eee7b41aae57ded06272905dbd680a0a04b91edd0f64790c796b2f706", size = 1943150, upload-time = "2025-08-12T06:59:53.588Z" }, + { url = "https://files.pythonhosted.org/packages/c2/83/4cfb393e287509fc2155480b9d184706ef8d9fa8cbf5505d02a5792bf220/sentencepiece-0.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:c6c8f42949f419ff8c7e9960dbadcfbc982d7b5efc2f6748210d3dd53a7de062", size = 1325651, upload-time = "2025-08-12T06:59:55.073Z" }, + { url = "https://files.pythonhosted.org/packages/8d/de/5a007fb53b1ab0aafc69d11a5a3dd72a289d5a3e78dcf2c3a3d9b14ffe93/sentencepiece-0.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:097f3394e99456e9e4efba1737c3749d7e23563dd1588ce71a3d007f25475fff", size = 1253641, upload-time = "2025-08-12T06:59:56.562Z" }, + { url = "https://files.pythonhosted.org/packages/2c/d2/f552be5928105588f4f4d66ee37dd4c61460d8097e62d0e2e0eec41bc61d/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d7b670879c370d350557edabadbad1f6561a9e6968126e6debca4029e5547820", size = 1316271, upload-time = "2025-08-12T06:59:58.109Z" }, + { url = "https://files.pythonhosted.org/packages/96/df/0cfe748ace5485be740fed9476dee7877f109da32ed0d280312c94ec259f/sentencepiece-0.2.1-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c7f0fd2f2693309e6628aeeb2e2faf6edd221134dfccac3308ca0de01f8dab47", size = 1387882, upload-time = "2025-08-12T07:00:00.701Z" }, + { url = "https://files.pythonhosted.org/packages/ac/dd/f7774d42a881ced8e1739f393ab1e82ece39fc9abd4779e28050c2e975b5/sentencepiece-0.2.1-cp313-cp313-win32.whl", hash = "sha256:92b3816aa2339355fda2c8c4e021a5de92180b00aaccaf5e2808972e77a4b22f", size = 999541, upload-time = "2025-08-12T07:00:02.709Z" }, + { url = "https://files.pythonhosted.org/packages/dd/e9/932b9eae6fd7019548321eee1ab8d5e3b3d1294df9d9a0c9ac517c7b636d/sentencepiece-0.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:10ed3dab2044c47f7a2e7b4969b0c430420cdd45735d78c8f853191fa0e3148b", size = 1054669, upload-time = "2025-08-12T07:00:04.915Z" }, + { url = "https://files.pythonhosted.org/packages/c9/3a/76488a00ea7d6931689cda28726a1447d66bf1a4837943489314593d5596/sentencepiece-0.2.1-cp313-cp313-win_arm64.whl", hash = "sha256:ac650534e2251083c5f75dde4ff28896ce7c8904133dc8fef42780f4d5588fcd", size = 1033922, upload-time = "2025-08-12T07:00:06.496Z" }, + { url = "https://files.pythonhosted.org/packages/4a/b6/08fe2ce819e02ccb0296f4843e3f195764ce9829cbda61b7513f29b95718/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:8dd4b477a7b069648d19363aad0cab9bad2f4e83b2d179be668efa672500dc94", size = 1946052, upload-time = "2025-08-12T07:00:08.136Z" }, + { url = "https://files.pythonhosted.org/packages/ab/d9/1ea0e740591ff4c6fc2b6eb1d7510d02f3fb885093f19b2f3abd1363b402/sentencepiece-0.2.1-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:0c0f672da370cc490e4c59d89e12289778310a0e71d176c541e4834759e1ae07", size = 1327408, upload-time = "2025-08-12T07:00:09.572Z" }, + { url = "https://files.pythonhosted.org/packages/99/7e/1fb26e8a21613f6200e1ab88824d5d203714162cf2883248b517deb500b7/sentencepiece-0.2.1-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:ad8493bea8432dae8d6830365352350f3b4144415a1d09c4c8cb8d30cf3b6c3c", size = 1254857, upload-time = "2025-08-12T07:00:11.021Z" }, + { url = "https://files.pythonhosted.org/packages/bc/85/c72fd1f3c7a6010544d6ae07f8ddb38b5e2a7e33bd4318f87266c0bbafbf/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b81a24733726e3678d2db63619acc5a8dccd074f7aa7a54ecd5ca33ca6d2d596", size = 1315722, upload-time = "2025-08-12T07:00:12.989Z" }, + { url = "https://files.pythonhosted.org/packages/4a/e8/661e5bd82a8aa641fd6c1020bd0e890ef73230a2b7215ddf9c8cd8e941c2/sentencepiece-0.2.1-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0a81799d0a68d618e89063fb423c3001a034c893069135ffe51fee439ae474d6", size = 1387452, upload-time = "2025-08-12T07:00:15.088Z" }, + { url = "https://files.pythonhosted.org/packages/99/5e/ae66c361023a470afcbc1fbb8da722c72ea678a2fcd9a18f1a12598c7501/sentencepiece-0.2.1-cp313-cp313t-win32.whl", hash = "sha256:89a3ea015517c42c0341d0d962f3e6aaf2cf10d71b1932d475c44ba48d00aa2b", size = 1002501, upload-time = "2025-08-12T07:00:16.966Z" }, + { url = "https://files.pythonhosted.org/packages/c1/03/d332828c4ff764e16c1b56c2c8f9a33488bbe796b53fb6b9c4205ddbf167/sentencepiece-0.2.1-cp313-cp313t-win_amd64.whl", hash = "sha256:33f068c9382dc2e7c228eedfd8163b52baa86bb92f50d0488bf2b7da7032e484", size = 1057555, upload-time = "2025-08-12T07:00:18.573Z" }, + { url = "https://files.pythonhosted.org/packages/88/14/5aee0bf0864df9bd82bd59e7711362908e4935e3f9cdc1f57246b5d5c9b9/sentencepiece-0.2.1-cp313-cp313t-win_arm64.whl", hash = "sha256:b3616ad246f360e52c85781e47682d31abfb6554c779e42b65333d4b5f44ecc0", size = 1036042, upload-time = "2025-08-12T07:00:20.209Z" }, + { url = "https://files.pythonhosted.org/packages/24/9c/89eb8b2052f720a612478baf11c8227dcf1dc28cd4ea4c0c19506b5af2a2/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_universal2.whl", hash = "sha256:5d0350b686c320068702116276cfb26c066dc7e65cfef173980b11bb4d606719", size = 1943147, upload-time = "2025-08-12T07:00:21.809Z" }, + { url = "https://files.pythonhosted.org/packages/82/0b/a1432bc87f97c2ace36386ca23e8bd3b91fb40581b5e6148d24b24186419/sentencepiece-0.2.1-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:c7f54a31cde6fa5cb030370566f68152a742f433f8d2be458463d06c208aef33", size = 1325624, upload-time = "2025-08-12T07:00:23.289Z" }, + { url = "https://files.pythonhosted.org/packages/ea/99/bbe054ebb5a5039457c590e0a4156ed073fb0fe9ce4f7523404dd5b37463/sentencepiece-0.2.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:c83b85ab2d6576607f31df77ff86f28182be4a8de6d175d2c33ca609925f5da1", size = 1253670, upload-time = "2025-08-12T07:00:24.69Z" }, + { url = "https://files.pythonhosted.org/packages/19/ad/d5c7075f701bd97971d7c2ac2904f227566f51ef0838dfbdfdccb58cd212/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1855f57db07b51fb51ed6c9c452f570624d2b169b36f0f79ef71a6e6c618cd8b", size = 1316247, upload-time = "2025-08-12T07:00:26.435Z" }, + { url = "https://files.pythonhosted.org/packages/fb/03/35fbe5f3d9a7435eebd0b473e09584bd3cc354ce118b960445b060d33781/sentencepiece-0.2.1-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01e6912125cb45d3792f530a4d38f8e21bf884d6b4d4ade1b2de5cf7a8d2a52b", size = 1387894, upload-time = "2025-08-12T07:00:28.339Z" }, + { url = "https://files.pythonhosted.org/packages/dc/aa/956ef729aafb6c8f9c443104c9636489093bb5c61d6b90fc27aa1a865574/sentencepiece-0.2.1-cp314-cp314-win32.whl", hash = "sha256:c415c9de1447e0a74ae3fdb2e52f967cb544113a3a5ce3a194df185cbc1f962f", size = 1096698, upload-time = "2025-08-12T07:00:29.764Z" }, + { url = "https://files.pythonhosted.org/packages/b8/cb/fe400d8836952cc535c81a0ce47dc6875160e5fedb71d2d9ff0e9894c2a6/sentencepiece-0.2.1-cp314-cp314-win_amd64.whl", hash = "sha256:881b2e44b14fc19feade3cbed314be37de639fc415375cefaa5bc81a4be137fd", size = 1155115, upload-time = "2025-08-12T07:00:32.865Z" }, + { url = "https://files.pythonhosted.org/packages/32/89/047921cf70f36c7b6b6390876b2399b3633ab73b8d0cb857e5a964238941/sentencepiece-0.2.1-cp314-cp314-win_arm64.whl", hash = "sha256:2005242a16d2dc3ac5fe18aa7667549134d37854823df4c4db244752453b78a8", size = 1133890, upload-time = "2025-08-12T07:00:34.763Z" }, + { url = "https://files.pythonhosted.org/packages/a1/11/5b414b9fae6255b5fb1e22e2ed3dc3a72d3a694e5703910e640ac78346bb/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_universal2.whl", hash = "sha256:a19adcec27c524cb7069a1c741060add95f942d1cbf7ad0d104dffa0a7d28a2b", size = 1946081, upload-time = "2025-08-12T07:00:36.97Z" }, + { url = "https://files.pythonhosted.org/packages/77/eb/7a5682bb25824db8545f8e5662e7f3e32d72a508fdce086029d89695106b/sentencepiece-0.2.1-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:e37e4b4c4a11662b5db521def4e44d4d30ae69a1743241412a93ae40fdcab4bb", size = 1327406, upload-time = "2025-08-12T07:00:38.669Z" }, + { url = "https://files.pythonhosted.org/packages/03/b0/811dae8fb9f2784e138785d481469788f2e0d0c109c5737372454415f55f/sentencepiece-0.2.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:477c81505db072b3ab627e7eab972ea1025331bd3a92bacbf798df2b75ea86ec", size = 1254846, upload-time = "2025-08-12T07:00:40.611Z" }, + { url = "https://files.pythonhosted.org/packages/ef/23/195b2e7ec85ebb6a547969f60b723c7aca5a75800ece6cc3f41da872d14e/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:010f025a544ef770bb395091d57cb94deb9652d8972e0d09f71d85d5a0816c8c", size = 1315721, upload-time = "2025-08-12T07:00:42.914Z" }, + { url = "https://files.pythonhosted.org/packages/7e/aa/553dbe4178b5f23eb28e59393dddd64186178b56b81d9b8d5c3ff1c28395/sentencepiece-0.2.1-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:733e59ff1794d26db706cd41fc2d7ca5f6c64a820709cb801dc0ea31780d64ab", size = 1387458, upload-time = "2025-08-12T07:00:44.56Z" }, + { url = "https://files.pythonhosted.org/packages/66/7c/08ff0012507297a4dd74a5420fdc0eb9e3e80f4e88cab1538d7f28db303d/sentencepiece-0.2.1-cp314-cp314t-win32.whl", hash = "sha256:d3233770f78e637dc8b1fda2cd7c3b99ec77e7505041934188a4e7fe751de3b0", size = 1099765, upload-time = "2025-08-12T07:00:46.058Z" }, + { url = "https://files.pythonhosted.org/packages/91/d5/2a69e1ce15881beb9ddfc7e3f998322f5cedcd5e4d244cb74dade9441663/sentencepiece-0.2.1-cp314-cp314t-win_amd64.whl", hash = "sha256:5e4366c97b68218fd30ea72d70c525e6e78a6c0a88650f57ac4c43c63b234a9d", size = 1157807, upload-time = "2025-08-12T07:00:47.673Z" }, + { url = "https://files.pythonhosted.org/packages/f3/16/54f611fcfc2d1c46cbe3ec4169780b2cfa7cf63708ef2b71611136db7513/sentencepiece-0.2.1-cp314-cp314t-win_arm64.whl", hash = "sha256:105e36e75cbac1292642045458e8da677b2342dcd33df503e640f0b457cb6751", size = 1136264, upload-time = "2025-08-12T07:00:49.485Z" }, +] + +[[package]] +name = "seqeval" +version = "1.2.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, + { name = "scikit-learn" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/9d/2d/233c79d5b4e5ab1dbf111242299153f3caddddbb691219f363ad55ce783d/seqeval-1.2.2.tar.gz", hash = "sha256:f28e97c3ab96d6fcd32b648f6438ff2e09cfba87f05939da9b3970713ec56e6f", size = 43605, upload-time = "2020-10-24T00:24:54.926Z" } + +[[package]] +name = "setuptools" +version = "81.0.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/0d/1c/73e719955c59b8e424d015ab450f51c0af856ae46ea2da83eba51cc88de1/setuptools-81.0.0.tar.gz", hash = "sha256:487b53915f52501f0a79ccfd0c02c165ffe06631443a886740b91af4b7a5845a", size = 1198299, upload-time = "2026-02-06T21:10:39.601Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e1/e3/c164c88b2e5ce7b24d667b9bd83589cf4f3520d97cad01534cd3c4f55fdb/setuptools-81.0.0-py3-none-any.whl", hash = "sha256:fdd925d5c5d9f62e4b74b30d6dd7828ce236fd6ed998a08d81de62ce5a6310d6", size = 1062021, upload-time = "2026-02-06T21:10:37.175Z" }, +] + +[[package]] +name = "shapely" +version = "2.1.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "numpy" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/4d/bc/0989043118a27cccb4e906a46b7565ce36ca7b57f5a18b78f4f1b0f72d9d/shapely-2.1.2.tar.gz", hash = "sha256:2ed4ecb28320a433db18a5bf029986aa8afcfd740745e78847e330d5d94922a9", size = 315489, upload-time = "2025-09-24T13:51:41.432Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/c0/f3b6453cf2dfa99adc0ba6675f9aaff9e526d2224cbd7ff9c1a879238693/shapely-2.1.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:fe2533caae6a91a543dec62e8360fe86ffcdc42a7c55f9dfd0128a977a896b94", size = 1833550, upload-time = "2025-09-24T13:50:30.019Z" }, + { url = "https://files.pythonhosted.org/packages/86/07/59dee0bc4b913b7ab59ab1086225baca5b8f19865e6101db9ebb7243e132/shapely-2.1.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ba4d1333cc0bc94381d6d4308d2e4e008e0bd128bdcff5573199742ee3634359", size = 1643556, upload-time = "2025-09-24T13:50:32.291Z" }, + { url = "https://files.pythonhosted.org/packages/26/29/a5397e75b435b9895cd53e165083faed5d12fd9626eadec15a83a2411f0f/shapely-2.1.2-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:0bd308103340030feef6c111d3eb98d50dc13feea33affc8a6f9fa549e9458a3", size = 2988308, upload-time = "2025-09-24T13:50:33.862Z" }, + { url = "https://files.pythonhosted.org/packages/b9/37/e781683abac55dde9771e086b790e554811a71ed0b2b8a1e789b7430dd44/shapely-2.1.2-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:1e7d4d7ad262a48bb44277ca12c7c78cb1b0f56b32c10734ec9a1d30c0b0c54b", size = 3099844, upload-time = "2025-09-24T13:50:35.459Z" }, + { url = "https://files.pythonhosted.org/packages/d8/f3/9876b64d4a5a321b9dc482c92bb6f061f2fa42131cba643c699f39317cb9/shapely-2.1.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:e9eddfe513096a71896441a7c37db72da0687b34752c4e193577a145c71736fc", size = 3988842, upload-time = "2025-09-24T13:50:37.478Z" }, + { url = "https://files.pythonhosted.org/packages/d1/a0/704c7292f7014c7e74ec84eddb7b109e1fbae74a16deae9c1504b1d15565/shapely-2.1.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:980c777c612514c0cf99bc8a9de6d286f5e186dcaf9091252fcd444e5638193d", size = 4152714, upload-time = "2025-09-24T13:50:39.9Z" }, + { url = "https://files.pythonhosted.org/packages/53/46/319c9dc788884ad0785242543cdffac0e6530e4d0deb6c4862bc4143dcf3/shapely-2.1.2-cp312-cp312-win32.whl", hash = "sha256:9111274b88e4d7b54a95218e243282709b330ef52b7b86bc6aaf4f805306f454", size = 1542745, upload-time = "2025-09-24T13:50:41.414Z" }, + { url = "https://files.pythonhosted.org/packages/ec/bf/cb6c1c505cb31e818e900b9312d514f381fbfa5c4363edfce0fcc4f8c1a4/shapely-2.1.2-cp312-cp312-win_amd64.whl", hash = "sha256:743044b4cfb34f9a67205cee9279feaf60ba7d02e69febc2afc609047cb49179", size = 1722861, upload-time = "2025-09-24T13:50:43.35Z" }, + { url = "https://files.pythonhosted.org/packages/c3/90/98ef257c23c46425dc4d1d31005ad7c8d649fe423a38b917db02c30f1f5a/shapely-2.1.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:b510dda1a3672d6879beb319bc7c5fd302c6c354584690973c838f46ec3e0fa8", size = 1832644, upload-time = "2025-09-24T13:50:44.886Z" }, + { url = "https://files.pythonhosted.org/packages/6d/ab/0bee5a830d209adcd3a01f2d4b70e587cdd9fd7380d5198c064091005af8/shapely-2.1.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:8cff473e81017594d20ec55d86b54bc635544897e13a7cfc12e36909c5309a2a", size = 1642887, upload-time = "2025-09-24T13:50:46.735Z" }, + { url = "https://files.pythonhosted.org/packages/2d/5e/7d7f54ba960c13302584c73704d8c4d15404a51024631adb60b126a4ae88/shapely-2.1.2-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:fe7b77dc63d707c09726b7908f575fc04ff1d1ad0f3fb92aec212396bc6cfe5e", size = 2970931, upload-time = "2025-09-24T13:50:48.374Z" }, + { url = "https://files.pythonhosted.org/packages/f2/a2/83fc37e2a58090e3d2ff79175a95493c664bcd0b653dd75cb9134645a4e5/shapely-2.1.2-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:7ed1a5bbfb386ee8332713bf7508bc24e32d24b74fc9a7b9f8529a55db9f4ee6", size = 3082855, upload-time = "2025-09-24T13:50:50.037Z" }, + { url = "https://files.pythonhosted.org/packages/44/2b/578faf235a5b09f16b5f02833c53822294d7f21b242f8e2d0cf03fb64321/shapely-2.1.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:a84e0582858d841d54355246ddfcbd1fce3179f185da7470f41ce39d001ee1af", size = 3979960, upload-time = "2025-09-24T13:50:51.74Z" }, + { url = "https://files.pythonhosted.org/packages/4d/04/167f096386120f692cc4ca02f75a17b961858997a95e67a3cb6a7bbd6b53/shapely-2.1.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:dc3487447a43d42adcdf52d7ac73804f2312cbfa5d433a7d2c506dcab0033dfd", size = 4142851, upload-time = "2025-09-24T13:50:53.49Z" }, + { url = "https://files.pythonhosted.org/packages/48/74/fb402c5a6235d1c65a97348b48cdedb75fb19eca2b1d66d04969fc1c6091/shapely-2.1.2-cp313-cp313-win32.whl", hash = "sha256:9c3a3c648aedc9f99c09263b39f2d8252f199cb3ac154fadc173283d7d111350", size = 1541890, upload-time = "2025-09-24T13:50:55.337Z" }, + { url = "https://files.pythonhosted.org/packages/41/47/3647fe7ad990af60ad98b889657a976042c9988c2807cf322a9d6685f462/shapely-2.1.2-cp313-cp313-win_amd64.whl", hash = "sha256:ca2591bff6645c216695bdf1614fca9c82ea1144d4a7591a466fef64f28f0715", size = 1722151, upload-time = "2025-09-24T13:50:57.153Z" }, + { url = "https://files.pythonhosted.org/packages/3c/49/63953754faa51ffe7d8189bfbe9ca34def29f8c0e34c67cbe2a2795f269d/shapely-2.1.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:2d93d23bdd2ed9dc157b46bc2f19b7da143ca8714464249bef6771c679d5ff40", size = 1834130, upload-time = "2025-09-24T13:50:58.49Z" }, + { url = "https://files.pythonhosted.org/packages/7f/ee/dce001c1984052970ff60eb4727164892fb2d08052c575042a47f5a9e88f/shapely-2.1.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:01d0d304b25634d60bd7cf291828119ab55a3bab87dc4af1e44b07fb225f188b", size = 1642802, upload-time = "2025-09-24T13:50:59.871Z" }, + { url = "https://files.pythonhosted.org/packages/da/e7/fc4e9a19929522877fa602f705706b96e78376afb7fad09cad5b9af1553c/shapely-2.1.2-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:8d8382dd120d64b03698b7298b89611a6ea6f55ada9d39942838b79c9bc89801", size = 3018460, upload-time = "2025-09-24T13:51:02.08Z" }, + { url = "https://files.pythonhosted.org/packages/a1/18/7519a25db21847b525696883ddc8e6a0ecaa36159ea88e0fef11466384d0/shapely-2.1.2-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:19efa3611eef966e776183e338b2d7ea43569ae99ab34f8d17c2c054d3205cc0", size = 3095223, upload-time = "2025-09-24T13:51:04.472Z" }, + { url = "https://files.pythonhosted.org/packages/48/de/b59a620b1f3a129c3fecc2737104a0a7e04e79335bd3b0a1f1609744cf17/shapely-2.1.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:346ec0c1a0fcd32f57f00e4134d1200e14bf3f5ae12af87ba83ca275c502498c", size = 4030760, upload-time = "2025-09-24T13:51:06.455Z" }, + { url = "https://files.pythonhosted.org/packages/96/b3/c6655ee7232b417562bae192ae0d3ceaadb1cc0ffc2088a2ddf415456cc2/shapely-2.1.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6305993a35989391bd3476ee538a5c9a845861462327efe00dd11a5c8c709a99", size = 4170078, upload-time = "2025-09-24T13:51:08.584Z" }, + { url = "https://files.pythonhosted.org/packages/a0/8e/605c76808d73503c9333af8f6cbe7e1354d2d238bda5f88eea36bfe0f42a/shapely-2.1.2-cp313-cp313t-win32.whl", hash = "sha256:c8876673449f3401f278c86eb33224c5764582f72b653a415d0e6672fde887bf", size = 1559178, upload-time = "2025-09-24T13:51:10.73Z" }, + { url = "https://files.pythonhosted.org/packages/36/f7/d317eb232352a1f1444d11002d477e54514a4a6045536d49d0c59783c0da/shapely-2.1.2-cp313-cp313t-win_amd64.whl", hash = "sha256:4a44bc62a10d84c11a7a3d7c1c4fe857f7477c3506e24c9062da0db0ae0c449c", size = 1739756, upload-time = "2025-09-24T13:51:12.105Z" }, + { url = "https://files.pythonhosted.org/packages/fc/c4/3ce4c2d9b6aabd27d26ec988f08cb877ba9e6e96086eff81bfea93e688c7/shapely-2.1.2-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:9a522f460d28e2bf4e12396240a5fc1518788b2fcd73535166d748399ef0c223", size = 1831290, upload-time = "2025-09-24T13:51:13.56Z" }, + { url = "https://files.pythonhosted.org/packages/17/b9/f6ab8918fc15429f79cb04afa9f9913546212d7fb5e5196132a2af46676b/shapely-2.1.2-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:1ff629e00818033b8d71139565527ced7d776c269a49bd78c9df84e8f852190c", size = 1641463, upload-time = "2025-09-24T13:51:14.972Z" }, + { url = "https://files.pythonhosted.org/packages/a5/57/91d59ae525ca641e7ac5551c04c9503aee6f29b92b392f31790fcb1a4358/shapely-2.1.2-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:f67b34271dedc3c653eba4e3d7111aa421d5be9b4c4c7d38d30907f796cb30df", size = 2970145, upload-time = "2025-09-24T13:51:16.961Z" }, + { url = "https://files.pythonhosted.org/packages/8a/cb/4948be52ee1da6927831ab59e10d4c29baa2a714f599f1f0d1bc747f5777/shapely-2.1.2-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:21952dc00df38a2c28375659b07a3979d22641aeb104751e769c3ee825aadecf", size = 3073806, upload-time = "2025-09-24T13:51:18.712Z" }, + { url = "https://files.pythonhosted.org/packages/03/83/f768a54af775eb41ef2e7bec8a0a0dbe7d2431c3e78c0a8bdba7ab17e446/shapely-2.1.2-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:1f2f33f486777456586948e333a56ae21f35ae273be99255a191f5c1fa302eb4", size = 3980803, upload-time = "2025-09-24T13:51:20.37Z" }, + { url = "https://files.pythonhosted.org/packages/9f/cb/559c7c195807c91c79d38a1f6901384a2878a76fbdf3f1048893a9b7534d/shapely-2.1.2-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:cf831a13e0d5a7eb519e96f58ec26e049b1fad411fc6fc23b162a7ce04d9cffc", size = 4133301, upload-time = "2025-09-24T13:51:21.887Z" }, + { url = "https://files.pythonhosted.org/packages/80/cd/60d5ae203241c53ef3abd2ef27c6800e21afd6c94e39db5315ea0cbafb4a/shapely-2.1.2-cp314-cp314-win32.whl", hash = "sha256:61edcd8d0d17dd99075d320a1dd39c0cb9616f7572f10ef91b4b5b00c4aeb566", size = 1583247, upload-time = "2025-09-24T13:51:23.401Z" }, + { url = "https://files.pythonhosted.org/packages/74/d4/135684f342e909330e50d31d441ace06bf83c7dc0777e11043f99167b123/shapely-2.1.2-cp314-cp314-win_amd64.whl", hash = "sha256:a444e7afccdb0999e203b976adb37ea633725333e5b119ad40b1ca291ecf311c", size = 1773019, upload-time = "2025-09-24T13:51:24.873Z" }, + { url = "https://files.pythonhosted.org/packages/a3/05/a44f3f9f695fa3ada22786dc9da33c933da1cbc4bfe876fe3a100bafe263/shapely-2.1.2-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:5ebe3f84c6112ad3d4632b1fd2290665aa75d4cef5f6c5d77c4c95b324527c6a", size = 1834137, upload-time = "2025-09-24T13:51:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/52/7e/4d57db45bf314573427b0a70dfca15d912d108e6023f623947fa69f39b72/shapely-2.1.2-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5860eb9f00a1d49ebb14e881f5caf6c2cf472c7fd38bd7f253bbd34f934eb076", size = 1642884, upload-time = "2025-09-24T13:51:28.029Z" }, + { url = "https://files.pythonhosted.org/packages/5a/27/4e29c0a55d6d14ad7422bf86995d7ff3f54af0eba59617eb95caf84b9680/shapely-2.1.2-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:b705c99c76695702656327b819c9660768ec33f5ce01fa32b2af62b56ba400a1", size = 3018320, upload-time = "2025-09-24T13:51:29.903Z" }, + { url = "https://files.pythonhosted.org/packages/9f/bb/992e6a3c463f4d29d4cd6ab8963b75b1b1040199edbd72beada4af46bde5/shapely-2.1.2-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:a1fd0ea855b2cf7c9cddaf25543e914dd75af9de08785f20ca3085f2c9ca60b0", size = 3094931, upload-time = "2025-09-24T13:51:32.699Z" }, + { url = "https://files.pythonhosted.org/packages/9c/16/82e65e21070e473f0ed6451224ed9fa0be85033d17e0c6e7213a12f59d12/shapely-2.1.2-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:df90e2db118c3671a0754f38e36802db75fe0920d211a27481daf50a711fdf26", size = 4030406, upload-time = "2025-09-24T13:51:34.189Z" }, + { url = "https://files.pythonhosted.org/packages/7c/75/c24ed871c576d7e2b64b04b1fe3d075157f6eb54e59670d3f5ffb36e25c7/shapely-2.1.2-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:361b6d45030b4ac64ddd0a26046906c8202eb60d0f9f53085f5179f1d23021a0", size = 4169511, upload-time = "2025-09-24T13:51:36.297Z" }, + { url = "https://files.pythonhosted.org/packages/b1/f7/b3d1d6d18ebf55236eec1c681ce5e665742aab3c0b7b232720a7d43df7b6/shapely-2.1.2-cp314-cp314t-win32.whl", hash = "sha256:b54df60f1fbdecc8ebc2c5b11870461a6417b3d617f555e5033f1505d36e5735", size = 1602607, upload-time = "2025-09-24T13:51:37.757Z" }, + { url = "https://files.pythonhosted.org/packages/9a/f6/f09272a71976dfc138129b8faf435d064a811ae2f708cb147dccdf7aacdb/shapely-2.1.2-cp314-cp314t-win_amd64.whl", hash = "sha256:0036ac886e0923417932c2e6369b6c52e38e0ff5d9120b90eef5cd9a5fc5cae9", size = 1796682, upload-time = "2025-09-24T13:51:39.233Z" }, +] + +[[package]] +name = "shellingham" +version = "1.5.4" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/58/15/8b3609fd3830ef7b27b655beb4b4e9c62313a4e8da8c676e142cc210d58e/shellingham-1.5.4.tar.gz", hash = "sha256:8dbca0739d487e5bd35ab3ca4b36e11c4078f3a234bfce294b0a0291363404de", size = 10310, upload-time = "2023-10-24T04:13:40.426Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/e0/f9/0595336914c5619e5f28a1fb793285925a8cd4b432c9da0a987836c7f822/shellingham-1.5.4-py2.py3-none-any.whl", hash = "sha256:7ecfff8f2fd72616f7481040475a65b2bf8af90a56c89140852d1120324e8686", size = 9755, upload-time = "2023-10-24T04:13:38.866Z" }, +] + [[package]] name = "six" version = "1.17.0" @@ -974,6 +3047,172 @@ 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 = "sympy" +version = "1.14.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "mpmath" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/83/d3/803453b36afefb7c2bb238361cd4ae6125a569b4db67cd9e79846ba2d68c/sympy-1.14.0.tar.gz", hash = "sha256:d3d3fe8df1e5a0b42f0e7bdf50541697dbe7d23746e894990c030e2b05e72517", size = 7793921, upload-time = "2025-04-27T18:05:01.611Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a2/09/77d55d46fd61b4a135c444fc97158ef34a095e5681d0a6c10b75bf356191/sympy-1.14.0-py3-none-any.whl", hash = "sha256:e091cc3e99d2141a0ba2847328f5479b05d94a6635cb96148ccb3f34671bd8f5", size = 6299353, upload-time = "2025-04-27T18:04:59.103Z" }, +] + +[[package]] +name = "threadpoolctl" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/b7/4d/08c89e34946fce2aec4fbb45c9016efd5f4d7f24af8e5d93296e935631d8/threadpoolctl-3.6.0.tar.gz", hash = "sha256:8ab8b4aa3491d812b623328249fab5302a68d2d71745c8a4c719a2fcaba9f44e", size = 21274, upload-time = "2025-03-13T13:49:23.031Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/32/d5/f9a850d79b0851d1d4ef6456097579a9005b31fea68726a4ae5f2d82ddd9/threadpoolctl-3.6.0-py3-none-any.whl", hash = "sha256:43a0b8fd5a2928500110039e43a5eed8480b918967083ea48dc3ab9f13c4a7fb", size = 18638, upload-time = "2025-03-13T13:49:21.846Z" }, +] + +[[package]] +name = "tokenizers" +version = "0.22.2" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/73/6f/f80cfef4a312e1fb34baf7d85c72d4411afde10978d4657f8cdd811d3ccc/tokenizers-0.22.2.tar.gz", hash = "sha256:473b83b915e547aa366d1eee11806deaf419e17be16310ac0a14077f1e28f917", size = 372115, upload-time = "2026-01-05T10:45:15.988Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/92/97/5dbfabf04c7e348e655e907ed27913e03db0923abb5dfdd120d7b25630e1/tokenizers-0.22.2-cp39-abi3-macosx_10_12_x86_64.whl", hash = "sha256:544dd704ae7238755d790de45ba8da072e9af3eea688f698b137915ae959281c", size = 3100275, upload-time = "2026-01-05T10:41:02.158Z" }, + { url = "https://files.pythonhosted.org/packages/2e/47/174dca0502ef88b28f1c9e06b73ce33500eedfac7a7692108aec220464e7/tokenizers-0.22.2-cp39-abi3-macosx_11_0_arm64.whl", hash = "sha256:1e418a55456beedca4621dbab65a318981467a2b188e982a23e117f115ce5001", size = 2981472, upload-time = "2026-01-05T10:41:00.276Z" }, + { url = "https://files.pythonhosted.org/packages/d6/84/7990e799f1309a8b87af6b948f31edaa12a3ed22d11b352eaf4f4b2e5753/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2249487018adec45d6e3554c71d46eb39fa8ea67156c640f7513eb26f318cec7", size = 3290736, upload-time = "2026-01-05T10:40:32.165Z" }, + { url = "https://files.pythonhosted.org/packages/78/59/09d0d9ba94dcd5f4f1368d4858d24546b4bdc0231c2354aa31d6199f0399/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:25b85325d0815e86e0bac263506dd114578953b7b53d7de09a6485e4a160a7dd", size = 3168835, upload-time = "2026-01-05T10:40:38.847Z" }, + { url = "https://files.pythonhosted.org/packages/47/50/b3ebb4243e7160bda8d34b731e54dd8ab8b133e50775872e7a434e524c28/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bfb88f22a209ff7b40a576d5324bf8286b519d7358663db21d6246fb17eea2d5", size = 3521673, upload-time = "2026-01-05T10:40:56.614Z" }, + { url = "https://files.pythonhosted.org/packages/e0/fa/89f4cb9e08df770b57adb96f8cbb7e22695a4cb6c2bd5f0c4f0ebcf33b66/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:1c774b1276f71e1ef716e5486f21e76333464f47bece56bbd554485982a9e03e", size = 3724818, upload-time = "2026-01-05T10:40:44.507Z" }, + { url = "https://files.pythonhosted.org/packages/64/04/ca2363f0bfbe3b3d36e95bf67e56a4c88c8e3362b658e616d1ac185d47f2/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:df6c4265b289083bf710dff49bc51ef252f9d5be33a45ee2bed151114a56207b", size = 3379195, upload-time = "2026-01-05T10:40:51.139Z" }, + { url = "https://files.pythonhosted.org/packages/2e/76/932be4b50ef6ccedf9d3c6639b056a967a86258c6d9200643f01269211ca/tokenizers-0.22.2-cp39-abi3-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:369cc9fc8cc10cb24143873a0d95438bb8ee257bb80c71989e3ee290e8d72c67", size = 3274982, upload-time = "2026-01-05T10:40:58.331Z" }, + { url = "https://files.pythonhosted.org/packages/1d/28/5f9f5a4cc211b69e89420980e483831bcc29dade307955cc9dc858a40f01/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:29c30b83d8dcd061078b05ae0cb94d3c710555fbb44861139f9f83dcca3dc3e4", size = 9478245, upload-time = "2026-01-05T10:41:04.053Z" }, + { url = "https://files.pythonhosted.org/packages/6c/fb/66e2da4704d6aadebf8cb39f1d6d1957df667ab24cff2326b77cda0dcb85/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_armv7l.whl", hash = "sha256:37ae80a28c1d3265bb1f22464c856bd23c02a05bb211e56d0c5301a435be6c1a", size = 9560069, upload-time = "2026-01-05T10:45:10.673Z" }, + { url = "https://files.pythonhosted.org/packages/16/04/fed398b05caa87ce9b1a1bb5166645e38196081b225059a6edaff6440fac/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_i686.whl", hash = "sha256:791135ee325f2336f498590eb2f11dc5c295232f288e75c99a36c5dbce63088a", size = 9899263, upload-time = "2026-01-05T10:45:12.559Z" }, + { url = "https://files.pythonhosted.org/packages/05/a1/d62dfe7376beaaf1394917e0f8e93ee5f67fea8fcf4107501db35996586b/tokenizers-0.22.2-cp39-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:38337540fbbddff8e999d59970f3c6f35a82de10053206a7562f1ea02d046fa5", size = 10033429, upload-time = "2026-01-05T10:45:14.333Z" }, + { url = "https://files.pythonhosted.org/packages/fd/18/a545c4ea42af3df6effd7d13d250ba77a0a86fb20393143bbb9a92e434d4/tokenizers-0.22.2-cp39-abi3-win32.whl", hash = "sha256:a6bf3f88c554a2b653af81f3204491c818ae2ac6fbc09e76ef4773351292bc92", size = 2502363, upload-time = "2026-01-05T10:45:20.593Z" }, + { url = "https://files.pythonhosted.org/packages/65/71/0670843133a43d43070abeb1949abfdef12a86d490bea9cd9e18e37c5ff7/tokenizers-0.22.2-cp39-abi3-win_amd64.whl", hash = "sha256:c9ea31edff2968b44a88f97d784c2f16dc0729b8b143ed004699ebca91f05c48", size = 2747786, upload-time = "2026-01-05T10:45:18.411Z" }, + { url = "https://files.pythonhosted.org/packages/72/f4/0de46cfa12cdcbcd464cc59fde36912af405696f687e53a091fb432f694c/tokenizers-0.22.2-cp39-abi3-win_arm64.whl", hash = "sha256:9ce725d22864a1e965217204946f830c37876eee3b2ba6fc6255e8e903d5fcbc", size = 2612133, upload-time = "2026-01-05T10:45:17.232Z" }, +] + +[[package]] +name = "torch" +version = "2.11.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "cuda-bindings", marker = "sys_platform == 'linux'" }, + { name = "cuda-toolkit", extra = ["cublas", "cudart", "cufft", "cufile", "cupti", "curand", "cusolver", "cusparse", "nvjitlink", "nvrtc", "nvtx"], marker = "sys_platform == 'linux'" }, + { name = "filelock" }, + { name = "fsspec" }, + { name = "jinja2" }, + { name = "networkx" }, + { name = "nvidia-cudnn-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-cusparselt-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nccl-cu13", marker = "sys_platform == 'linux'" }, + { name = "nvidia-nvshmem-cu13", marker = "sys_platform == 'linux'" }, + { name = "setuptools" }, + { name = "sympy" }, + { name = "triton", marker = "sys_platform == 'linux'" }, + { name = "typing-extensions" }, +] +wheels = [ + { url = "https://files.pythonhosted.org/packages/6f/8b/69e3008d78e5cee2b30183340cc425081b78afc5eff3d080daab0adda9aa/torch-2.11.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:4b5866312ee6e52ea625cd211dcb97d6a2cdc1131a5f15cc0d87eec948f6dd34", size = 80606338, upload-time = "2026-03-23T18:11:34.781Z" }, + { url = "https://files.pythonhosted.org/packages/13/16/42e5915ebe4868caa6bac83a8ed59db57f12e9a61b7d749d584776ed53d5/torch-2.11.0-cp312-cp312-manylinux_2_28_aarch64.whl", hash = "sha256:f99924682ef0aa6a4ab3b1b76f40dc6e273fca09f367d15a524266db100a723f", size = 419731115, upload-time = "2026-03-23T18:11:06.944Z" }, + { url = "https://files.pythonhosted.org/packages/1a/c9/82638ef24d7877510f83baf821f5619a61b45568ce21c0a87a91576510aa/torch-2.11.0-cp312-cp312-manylinux_2_28_x86_64.whl", hash = "sha256:0f68f4ac6d95d12e896c3b7a912b5871619542ec54d3649cf48cc1edd4dd2756", size = 530712279, upload-time = "2026-03-23T18:10:31.481Z" }, + { url = "https://files.pythonhosted.org/packages/1c/ff/6756f1c7ee302f6d202120e0f4f05b432b839908f9071157302cedfc5232/torch-2.11.0-cp312-cp312-win_amd64.whl", hash = "sha256:fbf39280699d1b869f55eac536deceaa1b60bd6788ba74f399cc67e60a5fab10", size = 114556047, upload-time = "2026-03-23T18:10:55.931Z" }, + { url = "https://files.pythonhosted.org/packages/87/89/5ea6722763acee56b045435fb84258db7375c48165ec8be7880ab2b281c5/torch-2.11.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:1e6debd97ccd3205bbb37eb806a9d8219e1139d15419982c09e23ef7d4369d18", size = 80606801, upload-time = "2026-03-23T18:10:18.649Z" }, + { url = "https://files.pythonhosted.org/packages/32/d1/8ed2173589cbfe744ed54e5a73efc107c0085ba5777ee93a5f4c1ab90553/torch-2.11.0-cp313-cp313-manylinux_2_28_aarch64.whl", hash = "sha256:63a68fa59de8f87acc7e85a5478bb2dddbb3392b7593ec3e78827c793c4b73fd", size = 419732382, upload-time = "2026-03-23T18:08:30.835Z" }, + { url = "https://files.pythonhosted.org/packages/3d/e1/b73f7c575a4b8f87a5928f50a1e35416b5e27295d8be9397d5293e7e8d4c/torch-2.11.0-cp313-cp313-manylinux_2_28_x86_64.whl", hash = "sha256:cc89b9b173d9adfab59fd227f0ab5e5516d9a52b658ae41d64e59d2e55a418db", size = 530711509, upload-time = "2026-03-23T18:08:47.213Z" }, + { url = "https://files.pythonhosted.org/packages/66/82/3e3fcdd388fbe54e29fd3f991f36846ff4ac90b0d0181e9c8f7236565f82/torch-2.11.0-cp313-cp313-win_amd64.whl", hash = "sha256:4dda3b3f52d121063a731ddb835f010dc137b920d7fec2778e52f60d8e4bf0cd", size = 114555842, upload-time = "2026-03-23T18:09:52.111Z" }, + { url = "https://files.pythonhosted.org/packages/db/38/8ac78069621b8c2b4979c2f96dc8409ef5e9c4189f6aac629189a78677ca/torch-2.11.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8b394322f49af4362d4f80e424bcaca7efcd049619af03a4cf4501520bdf0fb4", size = 80959574, upload-time = "2026-03-23T18:10:14.214Z" }, + { url = "https://files.pythonhosted.org/packages/6d/6c/56bfb37073e7136e6dd86bfc6af7339946dd684e0ecf2155ac0eee687ae1/torch-2.11.0-cp313-cp313t-manylinux_2_28_aarch64.whl", hash = "sha256:2658f34ce7e2dabf4ec73b45e2ca68aedad7a5be87ea756ad656eaf32bf1e1ea", size = 419732324, upload-time = "2026-03-23T18:09:36.604Z" }, + { url = "https://files.pythonhosted.org/packages/07/f4/1b666b6d61d3394cca306ea543ed03a64aad0a201b6cd159f1d41010aeb1/torch-2.11.0-cp313-cp313t-manylinux_2_28_x86_64.whl", hash = "sha256:98bb213c3084cfe176302949bdc360074b18a9da7ab59ef2edc9d9f742504778", size = 530596026, upload-time = "2026-03-23T18:09:20.842Z" }, + { url = "https://files.pythonhosted.org/packages/48/6b/30d1459fa7e4b67e9e3fe1685ca1d8bb4ce7c62ef436c3a615963c6c866c/torch-2.11.0-cp313-cp313t-win_amd64.whl", hash = "sha256:a97b94bbf62992949b4730c6cd2cc9aee7b335921ee8dc207d930f2ed09ae2db", size = 114793702, upload-time = "2026-03-23T18:09:47.304Z" }, + { url = "https://files.pythonhosted.org/packages/26/0d/8603382f61abd0db35841148ddc1ffd607bf3100b11c6e1dab6d2fc44e72/torch-2.11.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:01018087326984a33b64e04c8cb5c2795f9120e0d775ada1f6638840227b04d7", size = 80573442, upload-time = "2026-03-23T18:09:10.117Z" }, + { url = "https://files.pythonhosted.org/packages/c7/86/7cd7c66cb9cec6be330fff36db5bd0eef386d80c031b581ec81be1d4b26c/torch-2.11.0-cp314-cp314-manylinux_2_28_aarch64.whl", hash = "sha256:2bb3cc54bd0dea126b0060bb1ec9de0f9c7f7342d93d436646516b0330cd5be7", size = 419749385, upload-time = "2026-03-23T18:07:33.77Z" }, + { url = "https://files.pythonhosted.org/packages/47/e8/b98ca2d39b2e0e4730c0ee52537e488e7008025bc77ca89552ff91021f7c/torch-2.11.0-cp314-cp314-manylinux_2_28_x86_64.whl", hash = "sha256:4dc8b3809469b6c30b411bb8c4cad3828efd26236153d9beb6a3ec500f211a60", size = 530716756, upload-time = "2026-03-23T18:07:50.02Z" }, + { url = "https://files.pythonhosted.org/packages/78/88/d4a4cda8362f8a30d1ed428564878c3cafb0d87971fbd3947d4c84552095/torch-2.11.0-cp314-cp314-win_amd64.whl", hash = "sha256:2b4e811728bd0cc58fb2b0948fe939a1ee2bf1422f6025be2fca4c7bd9d79718", size = 114552300, upload-time = "2026-03-23T18:09:05.617Z" }, + { url = "https://files.pythonhosted.org/packages/bf/46/4419098ed6d801750f26567b478fc185c3432e11e2cad712bc6b4c2ab0d0/torch-2.11.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:8245477871c3700d4370352ffec94b103cfcb737229445cf9946cddb7b2ca7cd", size = 80959460, upload-time = "2026-03-23T18:09:00.818Z" }, + { url = "https://files.pythonhosted.org/packages/fd/66/54a56a4a6ceaffb567231994a9745821d3af922a854ed33b0b3a278e0a99/torch-2.11.0-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:ab9a8482f475f9ba20e12db84b0e55e2f58784bdca43a854a6ccd3fd4b9f75e6", size = 419735835, upload-time = "2026-03-23T18:07:18.974Z" }, + { url = "https://files.pythonhosted.org/packages/b1/e7/0b6665f533aa9e337662dc190425abc0af1fe3234088f4454c52393ded61/torch-2.11.0-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:563ed3d25542d7e7bbc5b235ccfacfeb97fb470c7fee257eae599adb8005c8a2", size = 530613405, upload-time = "2026-03-23T18:08:07.014Z" }, + { url = "https://files.pythonhosted.org/packages/cf/bf/c8d12a2c86dbfd7f40fb2f56fbf5a505ccf2d9ce131eb559dfc7c51e1a04/torch-2.11.0-cp314-cp314t-win_amd64.whl", hash = "sha256:b2a43985ff5ef6ddd923bbcf99943e5f58059805787c5c9a2622bf05ca2965b0", size = 114792991, upload-time = "2026-03-23T18:08:19.216Z" }, +] + +[[package]] +name = "tqdm" +version = "4.67.3" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/09/a9/6ba95a270c6f1fbcd8dac228323f2777d886cb206987444e4bce66338dd4/tqdm-4.67.3.tar.gz", hash = "sha256:7d825f03f89244ef73f1d4ce193cb1774a8179fd96f31d7e1dcde62092b960bb", size = 169598, upload-time = "2026-02-03T17:35:53.048Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/16/e1/3079a9ff9b8e11b846c6ac5c8b5bfb7ff225eee721825310c91b3b50304f/tqdm-4.67.3-py3-none-any.whl", hash = "sha256:ee1e4c0e59148062281c49d80b25b67771a127c85fc9676d3be5f243206826bf", size = 78374, upload-time = "2026-02-03T17:35:50.982Z" }, +] + +[[package]] +name = "transformers" +version = "5.1.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "huggingface-hub" }, + { name = "numpy" }, + { name = "packaging" }, + { name = "pyyaml" }, + { name = "regex" }, + { name = "safetensors" }, + { name = "tokenizers" }, + { name = "tqdm" }, + { name = "typer-slim" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/c9/1d/a7d91500a6c02ec76058bc9e65fcdec1bdb8882854dec8e4adf12d0aa8b0/transformers-5.1.0.tar.gz", hash = "sha256:c60d6180e5845ea1b4eed38d7d1b06fcc4cc341c6b7fa5c1dc767d7e25fe0139", size = 8531810, upload-time = "2026-02-05T15:41:42.932Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/b7/66/57042d4b0f1ede8046d7ae6409bf3640df996e9cbc3fe20467aa29badc54/transformers-5.1.0-py3-none-any.whl", hash = "sha256:de534b50c9b2ce6217fc56421075a1734241fb40704fdc90f50f6a08fc533d59", size = 10276537, upload-time = "2026-02-05T15:41:40.358Z" }, +] + +[[package]] +name = "triton" +version = "3.6.0" +source = { registry = "https://pypi.org/simple" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/17/5d/08201db32823bdf77a0e2b9039540080b2e5c23a20706ddba942924ebcd6/triton-3.6.0-cp312-cp312-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:374f52c11a711fd062b4bfbb201fd9ac0a5febd28a96fb41b4a0f51dde3157f4", size = 176128243, upload-time = "2026-01-20T16:16:07.857Z" }, + { url = "https://files.pythonhosted.org/packages/ab/a8/cdf8b3e4c98132f965f88c2313a4b493266832ad47fb52f23d14d4f86bb5/triton-3.6.0-cp312-cp312-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:74caf5e34b66d9f3a429af689c1c7128daba1d8208df60e81106b115c00d6fca", size = 188266850, upload-time = "2026-01-20T16:00:43.041Z" }, + { url = "https://files.pythonhosted.org/packages/3c/12/34d71b350e89a204c2c7777a9bba0dcf2f19a5bfdd70b57c4dbc5ffd7154/triton-3.6.0-cp313-cp313-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:448e02fe6dc898e9e5aa89cf0ee5c371e99df5aa5e8ad976a80b93334f3494fd", size = 176133521, upload-time = "2026-01-20T16:16:13.321Z" }, + { url = "https://files.pythonhosted.org/packages/f9/0b/37d991d8c130ce81a8728ae3c25b6e60935838e9be1b58791f5997b24a54/triton-3.6.0-cp313-cp313-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:10c7f76c6e72d2ef08df639e3d0d30729112f47a56b0c81672edc05ee5116ac9", size = 188289450, upload-time = "2026-01-20T16:00:49.136Z" }, + { url = "https://files.pythonhosted.org/packages/ce/4e/41b0c8033b503fd3cfcd12392cdd256945026a91ff02452bef40ec34bee7/triton-3.6.0-cp313-cp313t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1722e172d34e32abc3eb7711d0025bb69d7959ebea84e3b7f7a341cd7ed694d6", size = 176276087, upload-time = "2026-01-20T16:16:18.989Z" }, + { url = "https://files.pythonhosted.org/packages/35/f8/9c66bfc55361ec6d0e4040a0337fb5924ceb23de4648b8a81ae9d33b2b38/triton-3.6.0-cp313-cp313t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d002e07d7180fd65e622134fbd980c9a3d4211fb85224b56a0a0efbd422ab72f", size = 188400296, upload-time = "2026-01-20T16:00:56.042Z" }, + { url = "https://files.pythonhosted.org/packages/49/55/5ecf0dcaa0f2fbbd4420f7ef227ee3cb172e91e5fede9d0ecaddc43363b4/triton-3.6.0-cp314-cp314-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ef5523241e7d1abca00f1d240949eebdd7c673b005edbbce0aca95b8191f1d43", size = 176138577, upload-time = "2026-01-20T16:16:25.426Z" }, + { url = "https://files.pythonhosted.org/packages/df/3d/9e7eee57b37c80cec63322c0231bb6da3cfe535a91d7a4d64896fcb89357/triton-3.6.0-cp314-cp314-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a17a5d5985f0ac494ed8a8e54568f092f7057ef60e1b0fa09d3fd1512064e803", size = 188273063, upload-time = "2026-01-20T16:01:07.278Z" }, + { url = "https://files.pythonhosted.org/packages/48/db/56ee649cab5eaff4757541325aca81f52d02d4a7cd3506776cad2451e060/triton-3.6.0-cp314-cp314t-manylinux_2_27_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0b3a97e8ed304dfa9bd23bb41ca04cdf6b2e617d5e782a8653d616037a5d537d", size = 176274804, upload-time = "2026-01-20T16:16:31.528Z" }, + { url = "https://files.pythonhosted.org/packages/f6/56/6113c23ff46c00aae423333eb58b3e60bdfe9179d542781955a5e1514cb3/triton-3.6.0-cp314-cp314t-manylinux_2_27_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:46bd1c1af4b6704e554cad2eeb3b0a6513a980d470ccfa63189737340c7746a7", size = 188397994, upload-time = "2026-01-20T16:01:14.236Z" }, +] + +[[package]] +name = "typer" +version = "0.25.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "annotated-doc" }, + { name = "click" }, + { name = "rich" }, + { name = "shellingham" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/51/9aed62104cea109b820bbd6c14245af756112017d309da813ef107d42e7e/typer-0.25.1.tar.gz", hash = "sha256:9616eb8853a09ffeabab1698952f33c6f29ffdbceb4eaeecf571880e8d7664cc", size = 122276, upload-time = "2026-04-30T19:32:16.964Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/3f/f9/2b3ff4e56e5fa7debfaf9eb135d0da96f3e9a1d5b27222223c7296336e5f/typer-0.25.1-py3-none-any.whl", hash = "sha256:75caa44ed46a03fb2dab8808753ffacdbfea88495e74c85a28c5eefcf5f39c89", size = 58409, upload-time = "2026-04-30T19:32:18.271Z" }, +] + +[[package]] +name = "typer-slim" +version = "0.24.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typer" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/a7/a7/e6aecc4b4eb59598829a3b5076a93aff291b4fdaa2ded25efc4e1f4d219c/typer_slim-0.24.0.tar.gz", hash = "sha256:f0ed36127183f52ae6ced2ecb2521789995992c521a46083bfcdbb652d22ad34", size = 4776, upload-time = "2026-02-16T22:08:51.2Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a7/24/5480c20380dfd18cf33d14784096dca45a24eae6102e91d49a718d3b6855/typer_slim-0.24.0-py3-none-any.whl", hash = "sha256:d5d7ee1ee2834d5020c7c616ed5e0d0f29b9a4b1dd283bdebae198ec09778d0e", size = 3394, upload-time = "2026-02-16T22:08:49.92Z" }, +] + [[package]] name = "typing-extensions" version = "4.15.0" @@ -983,6 +3222,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/18/67/36e9267722cc04a6b9f15c7f3441c2363321a3ea07da7ae0c0707beb2a9c/typing_extensions-4.15.0-py3-none-any.whl", hash = "sha256:f0fa19c6845758ab08074a0cfa8b7aecb71c999ca73d62883bc25cc018c4e548", size = 44614, upload-time = "2025-08-25T13:49:24.86Z" }, ] +[[package]] +name = "tzdata" +version = "2026.2" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/ba/19/1b9b0e29f30c6d35cb345486df41110984ea67ae69dddbc0e8a100999493/tzdata-2026.2.tar.gz", hash = "sha256:9173fde7d80d9018e02a662e168e5a2d04f87c41ea174b139fbef642eda62d10", size = 198254, upload-time = "2026-04-24T15:22:08.651Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ce/e4/dccd7f47c4b64213ac01ef921a1337ee6e30e8c6466046018326977efd95/tzdata-2026.2-py2.py3-none-any.whl", hash = "sha256:bbe9af844f658da81a5f95019480da3a89415801f6cc966806612cc7169bffe7", size = 349321, upload-time = "2026-04-24T15:22:05.876Z" }, +] + [[package]] name = "urllib3" version = "2.6.3" @@ -1000,3 +3248,229 @@ sdist = { url = "https://files.pythonhosted.org/packages/07/5a/377161c2d3538d199 wheels = [ { url = "https://files.pythonhosted.org/packages/1a/62/c8d562e7766786ba6587d09c5a8ba9f718ed3fa8af7f4553e8f91c36f302/xlrd-2.0.2-py2.py3-none-any.whl", hash = "sha256:ea762c3d29f4cca48d82df517b6d89fbce4db3107f9d78713e48cd321d5c9aa9", size = 96555, upload-time = "2025-06-14T08:46:37.766Z" }, ] + +[[package]] +name = "xxhash" +version = "3.7.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/24/2f/e183a1b407002f5af81822bee18b61cdb94b8670208ef34734d8d2b8ebe9/xxhash-3.7.0.tar.gz", hash = "sha256:6cc4eefbb542a5d6ffd6d70ea9c502957c925e800f998c5630ecc809d6702bae", size = 82022, upload-time = "2026-04-25T11:10:32.553Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f2/8a/51a14cdef4728c6c2337db8a7d8704422cc65676d9199d77215464c880af/xxhash-3.7.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:082c87bfdd2b9f457606c7a4a53457f4c4b48b0cdc48de0277f4349d79bb3d7a", size = 33357, upload-time = "2026-04-25T11:06:20.44Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/0c2c933809421ffd9bf42b59315552c143c755db5d9a816b2f1ae273e884/xxhash-3.7.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:5e7ce913b61f35b0c1c839a49ac9c8e75dd8d860150688aed353b0ce1bf409d8", size = 30869, upload-time = "2026-04-25T11:06:21.989Z" }, + { url = "https://files.pythonhosted.org/packages/03/a8/89d5fdd6ee12d70ba99451de46dd0e8010167468dcd913ec855653f4dd50/xxhash-3.7.0-cp312-cp312-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:3beb1de3b1e9694fcdd853e570ee64c631c7062435d2f8c69c1adf809bc086f0", size = 194100, upload-time = "2026-04-25T11:06:23.586Z" }, + { url = "https://files.pythonhosted.org/packages/87/ee/2f9f2ed993e77206d1e66991290a1ebe22e843351ca3ebec8e49e01ba186/xxhash-3.7.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:f3e7b689c3bce16699efcf736066f5c6cc4472c3840fe4b22bd8279daf4abdac", size = 212977, upload-time = "2026-04-25T11:06:25.019Z" }, + { url = "https://files.pythonhosted.org/packages/de/60/5a91644615a9e9d4e42c2e9925f1908e3a24e4e691d9de7340d565bea024/xxhash-3.7.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:a6545e6b409e3d5cbafc850fb84c55a1ca26ed15a6b11e3bf07a0e0cd84517c8", size = 236373, upload-time = "2026-04-25T11:06:26.482Z" }, + { url = "https://files.pythonhosted.org/packages/22/c0/f3a9384eaaed9d14d4d062a5d953aa0da489bfe9747877aa994caa87cd0b/xxhash-3.7.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:31ab1461c77a11461d703c88eb949e132a1c6515933cf675d97ec680f4bd18de", size = 212229, upload-time = "2026-04-25T11:06:28.065Z" }, + { url = "https://files.pythonhosted.org/packages/2e/67/02f07a9fd79726804190f2172c4894c3ed9a4ebccaca05653c84beb58025/xxhash-3.7.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:7c4d596b7676f811172687ec567cbafb9e4dea2f9be1bbb4f622410cb7f40f40", size = 445462, upload-time = "2026-04-25T11:06:30.048Z" }, + { url = "https://files.pythonhosted.org/packages/40/37/558f5a90c0672fc9b4402dc25d87ac5b7406616e8969430c9ca4e52ee74d/xxhash-3.7.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:13805f0461cba0a857924e70ff91ae6d52d2598f79a884e788db80532614a4a1", size = 193932, upload-time = "2026-04-25T11:06:31.857Z" }, + { url = "https://files.pythonhosted.org/packages/d5/90/aaa09cd58661d32044dbbad7df55bbe22a623032b810e7ed3b8c569a2a6f/xxhash-3.7.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:1d398f372496152f1c6933a33566373f8d1b37b98b8c9d608fa6edc0976f23b2", size = 284807, upload-time = "2026-04-25T11:06:33.697Z" }, + { url = "https://files.pythonhosted.org/packages/d6/f3/53df3719ab127a02c174f0c1c74924fcd110866e89c966bc7909cfa8fa84/xxhash-3.7.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:d610aa62cdb7d4d497740741772a24a794903bf3e79eaa51d2e800082abe11e5", size = 210445, upload-time = "2026-04-25T11:06:35.488Z" }, + { url = "https://files.pythonhosted.org/packages/72/33/d219975c0e8b6fa2eb9ccd486fe47e21bf1847985b878dd2fbc3126e0d5c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:073c23900a9fbf3d26616c17c830db28af9803677cd5b33aea3224d824111514", size = 241273, upload-time = "2026-04-25T11:06:37.24Z" }, + { url = "https://files.pythonhosted.org/packages/3e/50/49b1afe610eb3964cedcb90a4d4c3d46a261ee8669cbd4f060652619ae3c/xxhash-3.7.0-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:418a463c3e6a590c0cdc890f8be19adb44a8c8acd175ca5b2a6de77e61d0b386", size = 197950, upload-time = "2026-04-25T11:06:39.148Z" }, + { url = "https://files.pythonhosted.org/packages/c6/75/5f42a1a4c78717d906a4b6a140c6dbf837ab1f547a54d23c4e2903310936/xxhash-3.7.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:03f8ff4474ee61c845758ce00711d7087a770d77efb36f7e74a6e867301000b8", size = 210709, upload-time = "2026-04-25T11:06:40.958Z" }, + { url = "https://files.pythonhosted.org/packages/8a/85/237e446c25abced71e9c53d269f2cef5bab8a82b3f88a12e00c5368e7368/xxhash-3.7.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:44fba4a5f1d179b7ddc7b3dc40f56f9209046421679b57025d4d8821b376fd8d", size = 275345, upload-time = "2026-04-25T11:06:42.525Z" }, + { url = "https://files.pythonhosted.org/packages/62/34/c2c26c0a6a9cc739bc2a5f0ae03ba8b87deb12b8bce35f7ac495e790dc6d/xxhash-3.7.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:31e3516a0f829d06ded4a2c0f3c7c5561993256bfa1c493975fb9dc7bfa828a1", size = 414056, upload-time = "2026-04-25T11:06:44.343Z" }, + { url = "https://files.pythonhosted.org/packages/a0/aa/5c58e9bc8071b8afd8dcf297ff362f723c4892168faba149f19904132bf4/xxhash-3.7.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b59ee2ac81de57771a09ecad09191e840a1d2fae1ef684208320591055768f83", size = 191485, upload-time = "2026-04-25T11:06:46.262Z" }, + { url = "https://files.pythonhosted.org/packages/d4/69/a929cf9d1e2e65a48b818cdce72cb6b69eab2e6877f21436d0a1942aff43/xxhash-3.7.0-cp312-cp312-win32.whl", hash = "sha256:74bbd92f8c7fcc397ba0a11bfdc106bc72ad7f11e3a60277753f87e7532b4d81", size = 30671, upload-time = "2026-04-25T11:06:48.039Z" }, + { url = "https://files.pythonhosted.org/packages/b9/1b/104b41a8947f4e1d4a66ce1e628eea752f37d1890bfd7453559ca7a3d950/xxhash-3.7.0-cp312-cp312-win_amd64.whl", hash = "sha256:7bd7bc82dd4f185f28f35193c2e968ef46131628e3cac62f639dadf321cba4d1", size = 31514, upload-time = "2026-04-25T11:06:49.279Z" }, + { url = "https://files.pythonhosted.org/packages/98/a0/1fd0ea1f1b886d9e7c73f0397571e22333a7d79e31da6d7127c2a4a71d75/xxhash-3.7.0-cp312-cp312-win_arm64.whl", hash = "sha256:7d7148180ec99ba36585b42c8c5de25e9b40191613bc4be68909b4d25a77a852", size = 27761, upload-time = "2026-04-25T11:06:50.448Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ca/d5174b4c36d10f64d4ca7050563138c5a599efb01a765858ddefc9c1202a/xxhash-3.7.0-cp313-cp313-android_21_arm64_v8a.whl", hash = "sha256:4b6d6b33f141158692bd4eafbb96edbc5aa0dabdb593a962db01a91983d4f8fa", size = 36813, upload-time = "2026-04-25T11:06:51.73Z" }, + { url = "https://files.pythonhosted.org/packages/41/d0/abc6c9d347ba1f1e1e1d98125d0881a0452c7f9a76a9dd03a7b5d2197f23/xxhash-3.7.0-cp313-cp313-android_21_x86_64.whl", hash = "sha256:845d347df254d6c619f616afa921331bada8614b8d373d58725c663ba97c3605", size = 35121, upload-time = "2026-04-25T11:06:53.048Z" }, + { url = "https://files.pythonhosted.org/packages/bf/11/4cc834eb3d79f2f2b3a6ef7324195208bcdfbdcf7534d2b17267aa5f3a8f/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphoneos.whl", hash = "sha256:fddbbb69a6fff4f421e7a0d1fa28f894b20112e9e3fab306af451e2dfd0e459b", size = 29624, upload-time = "2026-04-25T11:06:54.311Z" }, + { url = "https://files.pythonhosted.org/packages/23/83/e97d3e7b635fe73a1dfb1e91f805324dd6d930bb42041cbf18f183bc0b6d/xxhash-3.7.0-cp313-cp313-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:54876a4e45101cec2bf8f31a973cda073a23e2e108538dad224ba07f85f22487", size = 30638, upload-time = "2026-04-25T11:06:55.864Z" }, + { url = "https://files.pythonhosted.org/packages/f4/40/d84951d80c35db1f4c40a29a64a8520eea5d56e764c603906b4fe763580f/xxhash-3.7.0-cp313-cp313-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:0c72fe9c7e3d6dfd7f1e21e224a877917fa09c465694ba4e06464b9511b65544", size = 33323, upload-time = "2026-04-25T11:06:57.336Z" }, + { url = "https://files.pythonhosted.org/packages/89/cc/c7dc6558d97e9ab023f663d69ab28b340ed9bf4d2d94f2c259cf896bb354/xxhash-3.7.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:a6d73a830b17ef49bc04e00182bd839164c1b3c59c127cd7c54fcb10c7ed8ee8", size = 33362, upload-time = "2026-04-25T11:06:58.656Z" }, + { url = "https://files.pythonhosted.org/packages/2a/6e/46b84017b1301d54091430353d4ad5901654a3e0871649877a416f7f1644/xxhash-3.7.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:91c3b07cf3362086d8f126c6aecd8e5e9396ad8b2f2219ea7e49a8250c318acd", size = 30874, upload-time = "2026-04-25T11:06:59.834Z" }, + { url = "https://files.pythonhosted.org/packages/df/5e/8f9158e3ab906ad3fec51e09b5ea0093e769f12207bfa42a368ca204e7ab/xxhash-3.7.0-cp313-cp313-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:50e879ebbac351c81565ca108db766d7832f5b8b6a5b14b8c0151f7190028e3d", size = 194185, upload-time = "2026-04-25T11:07:01.658Z" }, + { url = "https://files.pythonhosted.org/packages/f3/29/a804ded9f5d3d3758292678d23e7528b08fda7b7e750688d08b052322475/xxhash-3.7.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:921c14e93817842dd0dd9f372890a0f0c72e534650b6ab13c5be5cd0db11d47e", size = 213033, upload-time = "2026-04-25T11:07:03.606Z" }, + { url = "https://files.pythonhosted.org/packages/8b/91/1ce5a7d2fdc975267320e2c78fc1cecfe7ab735ccbcf6993ec5dd541cb2c/xxhash-3.7.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e64a7c9d7dfca3e0fafcbc5e455519090706a3e36e95d655cec3e04e79f95aaa", size = 236140, upload-time = "2026-04-25T11:07:05.396Z" }, + { url = "https://files.pythonhosted.org/packages/34/04/fd595a4fd8617b05fa27bd9b684ecb4985bfed27917848eea85d54036d06/xxhash-3.7.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:2220af08163baf5fa36c2b8af079dc2cbe6e66ae061385267f9472362dfd53c6", size = 212291, upload-time = "2026-04-25T11:07:06.966Z" }, + { url = "https://files.pythonhosted.org/packages/03/fb/f1a379cbc372ae5b9f4ab36154c48a849ca6ebe3ac477067a57865bf3bc6/xxhash-3.7.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:f14bb8b22a4a91325813e3d553b8963c10cf8c756cff65ee50c194431296c655", size = 445532, upload-time = "2026-04-25T11:07:08.525Z" }, + { url = "https://files.pythonhosted.org/packages/65/59/172424b79f8cfd4b6d8a122b2193e6b8ad4b11f7159bb3b6f9b3191329bb/xxhash-3.7.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:496736f86a9bedaf64b0dc70e3539d0766df01c71ea22032698e88f3f04a1ce9", size = 193990, upload-time = "2026-04-25T11:07:10.315Z" }, + { url = "https://files.pythonhosted.org/packages/b9/19/aeac22161d953f139f07ba5586cb4a17c5b7b6dff985122803bb12933500/xxhash-3.7.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:0ff71596bd79816975b3de7130ab1ff4541410285a3c084584eeb1c8239996fd", size = 284876, upload-time = "2026-04-25T11:07:12.15Z" }, + { url = "https://files.pythonhosted.org/packages/77/d5/4fd0b59e7a02242953da05ff679fbb961b0a4368eac97a217e11dae110c1/xxhash-3.7.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1ad86695c19b1d46fe106925db3c7a37f16be37669dcf58dcc70a9dd6e324676", size = 210495, upload-time = "2026-04-25T11:07:13.952Z" }, + { url = "https://files.pythonhosted.org/packages/aa/fb/976a3165c728c7faf74aa1b5ab3cf6a85e6d731612894741840524c7d28c/xxhash-3.7.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:970f9f8c50961d639cbd0d988c96f80ddf66006de93641719282c4fe7a87c5e6", size = 241331, upload-time = "2026-04-25T11:07:15.557Z" }, + { url = "https://files.pythonhosted.org/packages/4a/2c/6763d5901d53ac9e6ba296e5717ae599025c9d268396e8faa8b4b0a8e0ac/xxhash-3.7.0-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:5886ad85e9e347911783760a1d16cb6b393e8f9e3b52c982568226cb56927bdc", size = 198037, upload-time = "2026-04-25T11:07:17.563Z" }, + { url = "https://files.pythonhosted.org/packages/61/2b/876e722d533833f5f9a83473e6ba993e48745701096944e77bbecf29b2c3/xxhash-3.7.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:6e934bbae1e0ec74e27d5f0d7f37ef547ce5ff9f0a7e63fb39e559fc99526734", size = 210744, upload-time = "2026-04-25T11:07:19.055Z" }, + { url = "https://files.pythonhosted.org/packages/21/e6/d7e7baef7ce24166b4668d3c48557bb35a23b92ecadcac7e7718d099ab69/xxhash-3.7.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:3b6b3d28228af044ebcded71c4a3dd86e1dbd7e2f4645bf40f7b5da65bb5fb5a", size = 275406, upload-time = "2026-04-25T11:07:20.908Z" }, + { url = "https://files.pythonhosted.org/packages/92/fe/198b3763b2e01ca908f2154969a2352ec99bda892b574a11a9a151c5ede4/xxhash-3.7.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:6be4d70d9ab76c9f324ead9c01af6ff52c324745ea0c3731682a0cf99720f1fe", size = 414125, upload-time = "2026-04-25T11:07:23.037Z" }, + { url = "https://files.pythonhosted.org/packages/3a/6d/019a11affd5a5499137cacca53808659964785439855b5aa40dfd3412916/xxhash-3.7.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:151d7520838d4465461a0b7f4ae488b3b00de16183dd3214c1a6b14bf89d7fb6", size = 191555, upload-time = "2026-04-25T11:07:24.991Z" }, + { url = "https://files.pythonhosted.org/packages/76/21/b96d58568df2d01533244c3e0e5cbdd0c8b2b25c4bec4d72f19259a292d7/xxhash-3.7.0-cp313-cp313-win32.whl", hash = "sha256:d798c1e291bffb8e37b5bbe0dda77fc767cd19e89cadaf66e6ed5d0ff88c9fe6", size = 30668, upload-time = "2026-04-25T11:07:26.665Z" }, + { url = "https://files.pythonhosted.org/packages/99/57/d849a8d3afa1f8f4bc6a831cd89f49f9706fbbad94d2975d6140a171988c/xxhash-3.7.0-cp313-cp313-win_amd64.whl", hash = "sha256:875811ba23c543b1a1c3143c926e43996eb27ebb8f52d3500744aa608c275aed", size = 31524, upload-time = "2026-04-25T11:07:27.92Z" }, + { url = "https://files.pythonhosted.org/packages/81/52/bacc753e92dee78b058af8dcef0a50815f5f860986c664a92d75f965b6a5/xxhash-3.7.0-cp313-cp313-win_arm64.whl", hash = "sha256:54a675cb300dda83d71daae2a599389d22db8021a0f8db0dd659e14626eb3ecc", size = 27768, upload-time = "2026-04-25T11:07:29.113Z" }, + { url = "https://files.pythonhosted.org/packages/1c/47/ddbd683b7fc7e592c1a8d9d65f73ce9ab513f082b3967eee2baf549b8fc6/xxhash-3.7.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a3b19a42111c4057c1547a4a1396a53961dca576a0f6b82bfa88a2d1561764b2", size = 33576, upload-time = "2026-04-25T11:07:30.469Z" }, + { url = "https://files.pythonhosted.org/packages/07/f2/36d3310161db7f72efb4562aadde0ed429f1d0531782dd6345b12d2da527/xxhash-3.7.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:8f4608a06e4d61b7a3425665a46d00e0579122e1a2fae97a0c52953a3aad9aa3", size = 31123, upload-time = "2026-04-25T11:07:31.989Z" }, + { url = "https://files.pythonhosted.org/packages/0d/3f/75937a5c69556ed213021e43cbedd84c8e0279d0d74e7d41a255d84ba4b1/xxhash-3.7.0-cp313-cp313t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:ad37c7792479e49cf96c1ab25517d7003fe0d93687a772ba19a097d235bbe41e", size = 196491, upload-time = "2026-04-25T11:07:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/22/29/f10d7ff8c7a733d4403a43b9de18c8fabc005f98cec054644f04418659ee/xxhash-3.7.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:dc026e3b89d98e30a8288c95cb696e77d150b3f0fb7a51f73dcd49ee6b5577fa", size = 215793, upload-time = "2026-04-25T11:07:34.919Z" }, + { url = "https://files.pythonhosted.org/packages/8b/fd/778f60aa295f58907938f030a8b514611f391405614a525cccd2ffc00eb5/xxhash-3.7.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:c9b31ab1f28b078a6a1ac1a54eb35e7d5390deddd56870d0be3a0a733d1c321c", size = 237993, upload-time = "2026-04-25T11:07:36.638Z" }, + { url = "https://files.pythonhosted.org/packages/70/f5/736db5de387b4a540e37a05b84b40dc58a1ce974bfd2b4e5754ce29b68c3/xxhash-3.7.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:3bb5fd680c038fd5229e44e9c493782f90df9bef632fd0499d442374688ff70b", size = 214887, upload-time = "2026-04-25T11:07:38.564Z" }, + { url = "https://files.pythonhosted.org/packages/4d/aa/09a095f22fdb9a27fbb716841fbff52119721f9ca4261952d07a912f7839/xxhash-3.7.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:030c0fd688fce3569fbb49a2feefd4110cbb0b650186fb4610759ecfac677548", size = 448407, upload-time = "2026-04-25T11:07:40.552Z" }, + { url = "https://files.pythonhosted.org/packages/74/8a/b745efeeca9e34a91c26fdc97ad8514c43d5a81ac78565cba80a1353870a/xxhash-3.7.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5b1bde10324f4c31812ae0d0502e92d916ae8917cad7209353f122b8b8f610c3", size = 196119, upload-time = "2026-04-25T11:07:42.101Z" }, + { url = "https://files.pythonhosted.org/packages/8a/5c/0cfceb024af90c191f665c7933b1f318ee234f4797858383bebd1881d52f/xxhash-3.7.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:503722d52a615f2604f5e7611de7d43878df010dc0053094ef91cb9a9ac3d987", size = 286751, upload-time = "2026-04-25T11:07:43.568Z" }, + { url = "https://files.pythonhosted.org/packages/0b/0a/0793e405dc3cf8f4ebe2c1acec1e4e4608cd9e7e50ea691dabbc2a95ccbb/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:c72500a3b6d6c30ebfc135035bcace9eb5884f2dc220804efcaaba43e9f611dd", size = 212961, upload-time = "2026-04-25T11:07:45.388Z" }, + { url = "https://files.pythonhosted.org/packages/0c/7e/721118ffc63bfff94aa565bcf2555a820f9f4bdb0f001e0d609bdfad70de/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:43475925a766d01ca8cd9a857fd87f3d50406983c8506a4c07c4df12adcc867f", size = 243703, upload-time = "2026-04-25T11:07:47.053Z" }, + { url = "https://files.pythonhosted.org/packages/6e/18/16f6267160488b8276fd3d449d425712512add292ba545c1b6946bfdb7dd/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:8d09dfd2ab135b985daf868b594315ebe11ad86cd9fea46e6c69f19b28f7d25a", size = 200894, upload-time = "2026-04-25T11:07:48.657Z" }, + { url = "https://files.pythonhosted.org/packages/2d/94/80ba841287fd97e3e9cac1d228788c8ef623746f570404961eec748ecb5c/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:c50269d0055ac1faecfd559886d2cbe4b730de236585aba0e873f9d9dadbe585", size = 213357, upload-time = "2026-04-25T11:07:50.257Z" }, + { url = "https://files.pythonhosted.org/packages/a1/7e/106d4067130c59f1e18a55ffadcd876d8c68534883a1e02685b29d3d8153/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:1910df4756a5ab58cfad8744fc2d0f23926e3efcc346ee76e87b974abab922f4", size = 277600, upload-time = "2026-04-25T11:07:51.745Z" }, + { url = "https://files.pythonhosted.org/packages/c5/86/a081dd30da71d720b2612a792bfd55e45fa9a07ac76a0507f60487473c25/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:d006faf3b491957efcb433489be3c149efe4787b7063d5cddb8ddaefdc60e0c1", size = 416980, upload-time = "2026-04-25T11:07:53.504Z" }, + { url = "https://files.pythonhosted.org/packages/35/29/1a95221a029a3c1293773869e1ab47b07cbbdd82444a42809e8c60156626/xxhash-3.7.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:abb65b4e947e958f7b3b0d71db3ce447d1bc5f37f5eab871ce7223bda8768a04", size = 193840, upload-time = "2026-04-25T11:07:55.103Z" }, + { url = "https://files.pythonhosted.org/packages/c5/e0/db909dd0823285de2286f67e10ee4d81e96ad35d7d8e964ecb07fccd8af9/xxhash-3.7.0-cp313-cp313t-win32.whl", hash = "sha256:178959906cb1716a1ce08e0d69c82886c70a15a6f2790fc084fdd146ca30cd49", size = 30966, upload-time = "2026-04-25T11:07:56.524Z" }, + { url = "https://files.pythonhosted.org/packages/7b/ff/d705b15b22f21ee106adce239cb65d35067a158c630b240270f09b17c2e6/xxhash-3.7.0-cp313-cp313t-win_amd64.whl", hash = "sha256:2524a1e20d4c231d13b50f7cf39e44265b055669a64a7a4b9a2a44faa03f19b6", size = 31784, upload-time = "2026-04-25T11:07:57.758Z" }, + { url = "https://files.pythonhosted.org/packages/a2/1f/b2cf83c3638fd0588e0b17f22e5a9400bdfb1a3e3755324ac0aee2250b88/xxhash-3.7.0-cp313-cp313t-win_arm64.whl", hash = "sha256:37d994d0ffe81ef087bb330d392caa809bb5853c77e22ea3f71db024a0543dba", size = 27932, upload-time = "2026-04-25T11:07:59.109Z" }, + { url = "https://files.pythonhosted.org/packages/0e/cc/431db584f6fbb9312e40a173af027644e5580d39df1f73603cbb9dca4d6b/xxhash-3.7.0-cp314-cp314-android_24_arm64_v8a.whl", hash = "sha256:8c5fcfd806c335bfa2adf1cd0b3110a44fc7b6995c3a648c27489bae85801465", size = 36644, upload-time = "2026-04-25T11:08:00.658Z" }, + { url = "https://files.pythonhosted.org/packages/bc/01/255ec513e0a705d1f9a61413e78dfce4e3235203f0ed525a24c2b4b56345/xxhash-3.7.0-cp314-cp314-android_24_x86_64.whl", hash = "sha256:506a0b488f190f0a06769575e30caf71615c898ed93ab18b0dbcb6dec5c3713c", size = 35003, upload-time = "2026-04-25T11:08:02.338Z" }, + { url = "https://files.pythonhosted.org/packages/68/70/c55fc33c93445b44d8fc5a17b41ed99e3cebe92bcf8396809e63fc9a1165/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphoneos.whl", hash = "sha256:ec68dbba21532c0173a9872298e65c89749f7c9d21538c3a78b5bb6105871568", size = 29655, upload-time = "2026-04-25T11:08:03.701Z" }, + { url = "https://files.pythonhosted.org/packages/c2/72/ff8de73df000d74467d12a59ce6d6e2b2a368b978d41ab7b1fba5ed442be/xxhash-3.7.0-cp314-cp314-ios_13_0_arm64_iphonesimulator.whl", hash = "sha256:fa77e7ec1450d415d20129961814787c9abd9a07f98872f070b1fe96c5084611", size = 30664, upload-time = "2026-04-25T11:08:05.011Z" }, + { url = "https://files.pythonhosted.org/packages/b6/91/08416d9bd9bc3bf39d831abe8a5631ac2db5141dfd6fe81c3fe59a1f9264/xxhash-3.7.0-cp314-cp314-ios_13_0_x86_64_iphonesimulator.whl", hash = "sha256:fe32736295ea38e43e7d9424053c8c47c9f64fecfc7c895fb3da9b30b131c9ee", size = 33317, upload-time = "2026-04-25T11:08:06.413Z" }, + { url = "https://files.pythonhosted.org/packages/0e/3b/86b1caa4dee10a99f4bf9521e623359341c5e50d05158fa10c275b2bd079/xxhash-3.7.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:ab9dd2c83c4bbd63e422181a76f13502d049d3ddcac9a1bdc29196263d692bb8", size = 33457, upload-time = "2026-04-25T11:08:08.099Z" }, + { url = "https://files.pythonhosted.org/packages/ed/38/98ea14ad1517e1461292a65906951458d520689782bfbae111050145bdba/xxhash-3.7.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:3afec3a336a2286601a437cb07562ab0227685e6fbb9ec17e8c18457ff348ecf", size = 30894, upload-time = "2026-04-25T11:08:09.429Z" }, + { url = "https://files.pythonhosted.org/packages/61/a2/074654d0b893606541199993c7db70067d9fc63b748e0d60020a52a1bd36/xxhash-3.7.0-cp314-cp314-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:565df64437a9390f84465dcca33e7377114c7ede8d05cd2cf20081f831ea788e", size = 194409, upload-time = "2026-04-25T11:08:10.91Z" }, + { url = "https://files.pythonhosted.org/packages/e2/26/6d2a1afc468189f77ca28c32e1c83e1b9da1178231e05641dbc1b350e332/xxhash-3.7.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:12eca820a5d558633d423bf8bb78ce72a55394823f64089247f788a7e0ae691e", size = 213135, upload-time = "2026-04-25T11:08:12.575Z" }, + { url = "https://files.pythonhosted.org/packages/8e/0e/d8aecf95e09c42547453137be74d2f7b8b14e08f5177fa2fab6144a19061/xxhash-3.7.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f262b8f7599516567e070abf607b9af649052b2c4bd6f9be02b0cb41b7024805", size = 236379, upload-time = "2026-04-25T11:08:14.206Z" }, + { url = "https://files.pythonhosted.org/packages/f2/74/8140e8210536b3dd0cc816c4faaeb5ba6e63e8125ab25af4bcddd6a037b3/xxhash-3.7.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f1598916cb197681e03e601901e4ab96a9a963de398c59d0964f8a6f44a2b361", size = 212447, upload-time = "2026-04-25T11:08:15.79Z" }, + { url = "https://files.pythonhosted.org/packages/a0/d2/462001d2903b4bee5a5689598a0a55e5e7cd1ac7f4247a5545cff10d3ebb/xxhash-3.7.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:322b2f0622230f526aeb1738149948a7ae357a9e2ceb1383c6fd1fdaecdafa16", size = 445660, upload-time = "2026-04-25T11:08:17.441Z" }, + { url = "https://files.pythonhosted.org/packages/23/09/2bd1ed7f8689b20e51727952cac8329d50c694dc32b2eba06ba5bc742b37/xxhash-3.7.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:24cc22070880cc57b830a65cde4e65fa884c6d9b28ae4803b5ee05911e7bafba", size = 194076, upload-time = "2026-04-25T11:08:19.134Z" }, + { url = "https://files.pythonhosted.org/packages/c9/6e/692302cd0a5f4ac4e6289f37fa888dc2e1e07750b68fe3e4bfe939b8cea3/xxhash-3.7.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:cb5a888a968b2434abf9ecda357b5d43f10d7b5a6da6fdbbe036208473aff0e2", size = 284990, upload-time = "2026-04-25T11:08:20.618Z" }, + { url = "https://files.pythonhosted.org/packages/05/d9/e54b159b3d9df7999d2a7c676ce7b323d1b5588a64f8f51ed8172567bd87/xxhash-3.7.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:a999771ff97bec27d18341be4f3a36b163bb1ac41ec17bef6d2dabd84acd33c7", size = 210590, upload-time = "2026-04-25T11:08:22.24Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/0e0df1a3a196ced4ca71de76d65ead25d8e87bbfb87b64306ea47a40c00d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:ed4a6efe2dee1655adb73e7ad40c6aa955a6892422b1e3b95de6a34de56e3cbb", size = 241442, upload-time = "2026-04-25T11:08:23.844Z" }, + { url = "https://files.pythonhosted.org/packages/9a/a9/d917a7a814e90b218f8a0d37967105eea91bf752c3303683c99a1f7bfc1f/xxhash-3.7.0-cp314-cp314-musllinux_1_2_i686.whl", hash = "sha256:9fd17f14ac0faa12126c2f9ca774a8cf342957265ec3c8669c144e5e6cdb478c", size = 198356, upload-time = "2026-04-25T11:08:25.99Z" }, + { url = "https://files.pythonhosted.org/packages/89/5e/f2ba1877c39469abbefc72991d6ebdcbd4c0880db01ae8cb1f553b0c537d/xxhash-3.7.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:05fd1254268c59b5cb2a029dfc204275e9fc52de2913f1e53aa8d01442c96b4d", size = 210898, upload-time = "2026-04-25T11:08:27.608Z" }, + { url = "https://files.pythonhosted.org/packages/90/c6/be56b58e73de531f39a10de1355bb77ceb663900dc4bf2d6d3002a9c3f9e/xxhash-3.7.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:a2eae53197c6276d5b317f75a1be226bbf440c20b58bf525f36b5d0e1f657ca6", size = 275519, upload-time = "2026-04-25T11:08:29.301Z" }, + { url = "https://files.pythonhosted.org/packages/92/e2/17ddc85d5765b9c709f192009ed8f5a1fc876f4eb35bba7c307b5b1169f9/xxhash-3.7.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:bfe6f92e3522dcbe8c4281efd74fa7542a336cb00b0e3272c4ec0edabeaeaf67", size = 414191, upload-time = "2026-04-25T11:08:31.16Z" }, + { url = "https://files.pythonhosted.org/packages/9c/42/85f5b79f4bf1ec7ba052491164adfd4f4e9519f5dc7246de4fbd64a1bd56/xxhash-3.7.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:7ab9a49c410d8c6c786ab99e79c529938d894c01433130353dd0fe999111077a", size = 191604, upload-time = "2026-04-25T11:08:32.862Z" }, + { url = "https://files.pythonhosted.org/packages/b8/d0/6127b623aa4cca18d8b7743592b048d689fd6c6e37ff26a22cddf6cd9d7f/xxhash-3.7.0-cp314-cp314-win32.whl", hash = "sha256:040ea63668f9185b92bc74942df09c7e65703deed71431333678fc6e739a9955", size = 31271, upload-time = "2026-04-25T11:08:34.651Z" }, + { url = "https://files.pythonhosted.org/packages/64/4f/44fc4788568004c43921701cbc127f48218a1eede2c9aea231115323564d/xxhash-3.7.0-cp314-cp314-win_amd64.whl", hash = "sha256:2a61e2a3fb23c892496d587b470dee7fa1b58b248a187719c65ea8e94ec13257", size = 32284, upload-time = "2026-04-25T11:08:35.987Z" }, + { url = "https://files.pythonhosted.org/packages/6d/77/18bb895eb60a49453d16e17d67990e5caff557c78eafc90ad4e2eabf4570/xxhash-3.7.0-cp314-cp314-win_arm64.whl", hash = "sha256:c7741c7524961d8c0cb4d4c21b28957ff731a3fd5b5cd8b856dc80a40e9e5acc", size = 28701, upload-time = "2026-04-25T11:08:37.767Z" }, + { url = "https://files.pythonhosted.org/packages/45/a0/46f72244570c550fbbb7db1ef554183dd5ebe9136385f30e032b781ae8f6/xxhash-3.7.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:fc84bf7aa7592f31ec63a3e7b11d624f468a3f19f5238cec7282a42e838ab1d7", size = 33646, upload-time = "2026-04-25T11:08:39.109Z" }, + { url = "https://files.pythonhosted.org/packages/4a/3a/453846a7eceea11e75def361eed01ec6a0205b9822c19927ed364ccae7cc/xxhash-3.7.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:9f1563fdc8abfc389748e6932c7e4e99c89a53e4ec37d4563c24fc06f5e5644b", size = 31125, upload-time = "2026-04-25T11:08:40.467Z" }, + { url = "https://files.pythonhosted.org/packages/bd/3e/49434aba738885d512f9e486db1bdd19db28dfa40372b56da26ef7a4e738/xxhash-3.7.0-cp314-cp314t-manylinux1_i686.manylinux_2_28_i686.manylinux_2_5_i686.whl", hash = "sha256:2d415f18becf6f153046ab6adc97da77e3643a0ee205dae61c4012604113a020", size = 196633, upload-time = "2026-04-25T11:08:41.943Z" }, + { url = "https://files.pythonhosted.org/packages/a4/e9/006cb6127baeb9f8abe6d15e62faa01349f09b34e2bfd65175b2422d026b/xxhash-3.7.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:bb16aa13ed175bc9be5c2491ba031b85a9b51c4ed90e0b3d4ebe63cf3fb54f8e", size = 215899, upload-time = "2026-04-25T11:08:43.645Z" }, + { url = "https://files.pythonhosted.org/packages/27/e4/cc57d72e66df0ae29b914335f1c6dcf61e8f3746ddf0ae3c471aa4f15e00/xxhash-3.7.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:f9fd595f1e5941b3d7863e4774e4b30caa6731fc34b9277da032295aa5656ee5", size = 238116, upload-time = "2026-04-25T11:08:45.698Z" }, + { url = "https://files.pythonhosted.org/packages/af/78/3531d4a3fd8a0038cc6be1f265a69c1b3587f557a10b677dd736de2202c1/xxhash-3.7.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:1295325c5a98d552333fa53dc2b026b0ef0ec9c8e73ca3a952990b4c7d65d459", size = 215012, upload-time = "2026-04-25T11:08:47.355Z" }, + { url = "https://files.pythonhosted.org/packages/b4/f6/259fb1eaaec921f59b17203b0daee69829761226d3b980d5191d7723dd83/xxhash-3.7.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:3573a651d146912da9daa9e29e5fbc45994420daaa9ef1e2fa5823e1dc485513", size = 448534, upload-time = "2026-04-25T11:08:49.149Z" }, + { url = "https://files.pythonhosted.org/packages/7b/16/a66d0eaf6a7e68532c07714361ddc904c663ec940f3b028c1ae4a21a7b9d/xxhash-3.7.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5ec1e080a3d02d94ea9335bfab0e3374b877e25411422c18f51a943fa4b46381", size = 196217, upload-time = "2026-04-25T11:08:50.805Z" }, + { url = "https://files.pythonhosted.org/packages/8d/ef/d2efc7fc51756dc52509109d1a25cefc859d74bc4b19a167b12dbd8c2786/xxhash-3.7.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:84415265192072d8638a3afc3c1bc5995e310570cd9acb54dc46d3939e364fe0", size = 286906, upload-time = "2026-04-25T11:08:52.418Z" }, + { url = "https://files.pythonhosted.org/packages/fc/67/25decd1d4a4018582ec4db2a868a2b7e40640f4adb20dfeb19ac923aa825/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8d4dea659b57443989ef32f4295104fd6912c73d0bf26d1d148bb88a9f159b02", size = 213057, upload-time = "2026-04-25T11:08:54.105Z" }, + { url = "https://files.pythonhosted.org/packages/0d/5d/17651eb29d06786cdc40c60ae3d27d645aa5d61d2eca6237a7ba0b94789b/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:05ece0fe4d9c9c2728912d1981ae1566cfc83a011571b24732cbf76e1fb70dca", size = 243886, upload-time = "2026-04-25T11:08:56.109Z" }, + { url = "https://files.pythonhosted.org/packages/8a/d4/174d9cf7502243d586e6a9ae842b1ae23026620995114f85f1380e588bc9/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_i686.whl", hash = "sha256:fd880353cf1ffaf321bc18dd663e111976dbd0d3bbd8a66d58d2b470dfa7f396", size = 201015, upload-time = "2026-04-25T11:08:57.777Z" }, + { url = "https://files.pythonhosted.org/packages/91/8c/2254e2d06c3ac5e6fe22eaf3da791b87ea823ae9f2c17b4af66755c5752d/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:4e15cc9e2817f6481160f930c62842b3ff419e20e13072bcbab12230943092bc", size = 213457, upload-time = "2026-04-25T11:08:59.826Z" }, + { url = "https://files.pythonhosted.org/packages/79/a2/e3daa762545921173e3360f3b4ff7fc63c2d27359f7230ec1a7a74e117f6/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:90b9d1a8bd37d768ffc92a1f651ec69afc532a96fa1ac2ea7abbed5d630b3237", size = 277738, upload-time = "2026-04-25T11:09:01.423Z" }, + { url = "https://files.pythonhosted.org/packages/e1/4c/e186da2c46b87f5204640e008d42730bf3c1ee9f0efb71ae1ebcdfeac681/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:157c49475b34ecea8809e51123d9769a534e139d1247942f7a4bc67710bb2533", size = 417127, upload-time = "2026-04-25T11:09:03.592Z" }, + { url = "https://files.pythonhosted.org/packages/17/28/3798e15007a3712d0da3d3fe70f8e11916569858b5cc371053bc26270832/xxhash-3.7.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:5a6ddec83325685e729ca119d1f5c518ec39294212ecd770e60693cdc5f7eb79", size = 193962, upload-time = "2026-04-25T11:09:06.228Z" }, + { url = "https://files.pythonhosted.org/packages/ad/95/a26baa93b5241fd7630998816a4ec47a5a0bad193b3f8fc8f3593e1a4a67/xxhash-3.7.0-cp314-cp314t-win32.whl", hash = "sha256:a04a6cab47e2166435aaf5b9e5ee41d1532cc8300efdef87f2a4d0acb7db19ed", size = 31643, upload-time = "2026-04-25T11:09:08.153Z" }, + { url = "https://files.pythonhosted.org/packages/44/36/5454f13c447e395f9b06a3e91274c59f503d31fad84e1836efe3bdb71f6a/xxhash-3.7.0-cp314-cp314t-win_amd64.whl", hash = "sha256:8653dd7c2eda020545bb2c71c7f7039b53fe7434d0fc1a0a9deb79ab3f1a4fc1", size = 32522, upload-time = "2026-04-25T11:09:09.534Z" }, + { url = "https://files.pythonhosted.org/packages/74/35/698e7e3ff38e22992ea24870a511d8762474fb6783627a2910ff22a185c2/xxhash-3.7.0-cp314-cp314t-win_arm64.whl", hash = "sha256:468f0fc114faaa4b36699f8e328bbc3bb11dc418ba94ac52c26dd736d4b6c637", size = 28807, upload-time = "2026-04-25T11:09:11.234Z" }, +] + +[[package]] +name = "xyzservices" +version = "2026.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f6/08/3cb9f67a8d48021aca2a02292cc26eecd71d949ae70ad66420a8730cc302/xyzservices-2026.3.0.tar.gz", hash = "sha256:d226866a5d8e9fef337034d8da37a8298f0a1d9d1489b4018e69579eb321fea4", size = 1135736, upload-time = "2026-03-30T14:42:25.596Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/a8/a9/d23012099dc88ec69a29c6407b41d89681cb674c2043cd5b467c7e299c08/xyzservices-2026.3.0-py3-none-any.whl", hash = "sha256:503183d4b322bfebc3c50cdd21192aa3e81e36c5efbf9133d54ae82143e0576b", size = 94101, upload-time = "2026-03-30T14:42:24.608Z" }, +] + +[[package]] +name = "yarl" +version = "1.23.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "idna" }, + { name = "multidict" }, + { name = "propcache" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/23/6e/beb1beec874a72f23815c1434518bfc4ed2175065173fb138c3705f658d4/yarl-1.23.0.tar.gz", hash = "sha256:53b1ea6ca88ebd4420379c330aea57e258408dd0df9af0992e5de2078dc9f5d5", size = 194676, upload-time = "2026-03-01T22:07:53.373Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/88/8a/94615bc31022f711add374097ad4144d569e95ff3c38d39215d07ac153a0/yarl-1.23.0-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:1932b6b8bba8d0160a9d1078aae5838a66039e8832d41d2992daa9a3a08f7860", size = 124737, upload-time = "2026-03-01T22:05:12.897Z" }, + { url = "https://files.pythonhosted.org/packages/e3/6f/c6554045d59d64052698add01226bc867b52fe4a12373415d7991fdca95d/yarl-1.23.0-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:411225bae281f114067578891bc75534cfb3d92a3b4dfef7a6ca78ba354e6069", size = 87029, upload-time = "2026-03-01T22:05:14.376Z" }, + { url = "https://files.pythonhosted.org/packages/19/2a/725ecc166d53438bc88f76822ed4b1e3b10756e790bafd7b523fe97c322d/yarl-1.23.0-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:13a563739ae600a631c36ce096615fe307f131344588b0bc0daec108cdb47b25", size = 86310, upload-time = "2026-03-01T22:05:15.71Z" }, + { url = "https://files.pythonhosted.org/packages/99/30/58260ed98e6ff7f90ba84442c1ddd758c9170d70327394a6227b310cd60f/yarl-1.23.0-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9cbf44c5cb4a7633d078788e1b56387e3d3cf2b8139a3be38040b22d6c3221c8", size = 97587, upload-time = "2026-03-01T22:05:17.384Z" }, + { url = "https://files.pythonhosted.org/packages/76/0a/8b08aac08b50682e65759f7f8dde98ae8168f72487e7357a5d684c581ef9/yarl-1.23.0-cp312-cp312-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:53ad387048f6f09a8969631e4de3f1bf70c50e93545d64af4f751b2498755072", size = 92528, upload-time = "2026-03-01T22:05:18.804Z" }, + { url = "https://files.pythonhosted.org/packages/52/07/0b7179101fe5f8385ec6c6bb5d0cb9f76bd9fb4a769591ab6fb5cdbfc69a/yarl-1.23.0-cp312-cp312-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:4a59ba56f340334766f3a4442e0efd0af895fae9e2b204741ef885c446b3a1a8", size = 105339, upload-time = "2026-03-01T22:05:20.235Z" }, + { url = "https://files.pythonhosted.org/packages/d3/8a/36d82869ab5ec829ca8574dfcb92b51286fcfb1e9c7a73659616362dc880/yarl-1.23.0-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:803a3c3ce4acc62eaf01eaca1208dcf0783025ef27572c3336502b9c232005e7", size = 105061, upload-time = "2026-03-01T22:05:22.268Z" }, + { url = "https://files.pythonhosted.org/packages/66/3e/868e5c3364b6cee19ff3e1a122194fa4ce51def02c61023970442162859e/yarl-1.23.0-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3d2bff8f37f8d0f96c7ec554d16945050d54462d6e95414babaa18bfafc7f51", size = 100132, upload-time = "2026-03-01T22:05:23.638Z" }, + { url = "https://files.pythonhosted.org/packages/cf/26/9c89acf82f08a52cb52d6d39454f8d18af15f9d386a23795389d1d423823/yarl-1.23.0-cp312-cp312-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c75eb09e8d55bceb4367e83496ff8ef2bc7ea6960efb38e978e8073ea59ecb67", size = 99289, upload-time = "2026-03-01T22:05:25.749Z" }, + { url = "https://files.pythonhosted.org/packages/6f/54/5b0db00d2cb056922356104468019c0a132e89c8d3ab67d8ede9f4483d2a/yarl-1.23.0-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:877b0738624280e34c55680d6054a307aa94f7d52fa0e3034a9cc6e790871da7", size = 96950, upload-time = "2026-03-01T22:05:27.318Z" }, + { url = "https://files.pythonhosted.org/packages/f6/40/10fa93811fd439341fad7e0718a86aca0de9548023bbb403668d6555acab/yarl-1.23.0-cp312-cp312-musllinux_1_2_armv7l.whl", hash = "sha256:b5405bb8f0e783a988172993cfc627e4d9d00432d6bbac65a923041edacf997d", size = 93960, upload-time = "2026-03-01T22:05:28.738Z" }, + { url = "https://files.pythonhosted.org/packages/bc/d2/8ae2e6cd77d0805f4526e30ec43b6f9a3dfc542d401ac4990d178e4bf0cf/yarl-1.23.0-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:1c3a3598a832590c5a3ce56ab5576361b5688c12cb1d39429cf5dba30b510760", size = 104703, upload-time = "2026-03-01T22:05:30.438Z" }, + { url = "https://files.pythonhosted.org/packages/2f/0c/b3ceacf82c3fe21183ce35fa2acf5320af003d52bc1fcf5915077681142e/yarl-1.23.0-cp312-cp312-musllinux_1_2_riscv64.whl", hash = "sha256:8419ebd326430d1cbb7efb5292330a2cf39114e82df5cc3d83c9a0d5ebeaf2f2", size = 98325, upload-time = "2026-03-01T22:05:31.835Z" }, + { url = "https://files.pythonhosted.org/packages/9d/e0/12900edd28bdab91a69bd2554b85ad7b151f64e8b521fe16f9ad2f56477a/yarl-1.23.0-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:be61f6fff406ca40e3b1d84716fde398fc08bc63dd96d15f3a14230a0973ed86", size = 105067, upload-time = "2026-03-01T22:05:33.358Z" }, + { url = "https://files.pythonhosted.org/packages/15/61/74bb1182cf79c9bbe4eb6b1f14a57a22d7a0be5e9cedf8e2d5c2086474c3/yarl-1.23.0-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:3ceb13c5c858d01321b5d9bb65e4cf37a92169ea470b70fec6f236b2c9dd7e34", size = 100285, upload-time = "2026-03-01T22:05:35.4Z" }, + { url = "https://files.pythonhosted.org/packages/69/7f/cd5ef733f2550de6241bd8bd8c3febc78158b9d75f197d9c7baa113436af/yarl-1.23.0-cp312-cp312-win32.whl", hash = "sha256:fffc45637bcd6538de8b85f51e3df3223e4ad89bccbfca0481c08c7fc8b7ed7d", size = 82359, upload-time = "2026-03-01T22:05:36.811Z" }, + { url = "https://files.pythonhosted.org/packages/f5/be/25216a49daeeb7af2bec0db22d5e7df08ed1d7c9f65d78b14f3b74fd72fc/yarl-1.23.0-cp312-cp312-win_amd64.whl", hash = "sha256:f69f57305656a4852f2a7203efc661d8c042e6cc67f7acd97d8667fb448a426e", size = 87674, upload-time = "2026-03-01T22:05:38.171Z" }, + { url = "https://files.pythonhosted.org/packages/d2/35/aeab955d6c425b227d5b7247eafb24f2653fedc32f95373a001af5dfeb9e/yarl-1.23.0-cp312-cp312-win_arm64.whl", hash = "sha256:6e87a6e8735b44816e7db0b2fbc9686932df473c826b0d9743148432e10bb9b9", size = 81879, upload-time = "2026-03-01T22:05:40.006Z" }, + { url = "https://files.pythonhosted.org/packages/9a/4b/a0a6e5d0ee8a2f3a373ddef8a4097d74ac901ac363eea1440464ccbe0898/yarl-1.23.0-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:16c6994ac35c3e74fb0ae93323bf8b9c2a9088d55946109489667c510a7d010e", size = 123796, upload-time = "2026-03-01T22:05:41.412Z" }, + { url = "https://files.pythonhosted.org/packages/67/b6/8925d68af039b835ae876db5838e82e76ec87b9782ecc97e192b809c4831/yarl-1.23.0-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:4a42e651629dafb64fd5b0286a3580613702b5809ad3f24934ea87595804f2c5", size = 86547, upload-time = "2026-03-01T22:05:42.841Z" }, + { url = "https://files.pythonhosted.org/packages/ae/50/06d511cc4b8e0360d3c94af051a768e84b755c5eb031b12adaaab6dec6e5/yarl-1.23.0-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:7c6b9461a2a8b47c65eef63bb1c76a4f1c119618ffa99ea79bc5bb1e46c5821b", size = 85854, upload-time = "2026-03-01T22:05:44.85Z" }, + { url = "https://files.pythonhosted.org/packages/c4/f4/4e30b250927ffdab4db70da08b9b8d2194d7c7b400167b8fbeca1e4701ca/yarl-1.23.0-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:2569b67d616eab450d262ca7cb9f9e19d2f718c70a8b88712859359d0ab17035", size = 98351, upload-time = "2026-03-01T22:05:46.836Z" }, + { url = "https://files.pythonhosted.org/packages/86/fc/4118c5671ea948208bdb1492d8b76bdf1453d3e73df051f939f563e7dcc5/yarl-1.23.0-cp313-cp313-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:e9d9a4d06d3481eab79803beb4d9bd6f6a8e781ec078ac70d7ef2dcc29d1bea5", size = 92711, upload-time = "2026-03-01T22:05:48.316Z" }, + { url = "https://files.pythonhosted.org/packages/56/11/1ed91d42bd9e73c13dc9e7eb0dd92298d75e7ac4dd7f046ad0c472e231cd/yarl-1.23.0-cp313-cp313-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:f514f6474e04179d3d33175ed3f3e31434d3130d42ec153540d5b157deefd735", size = 106014, upload-time = "2026-03-01T22:05:50.028Z" }, + { url = "https://files.pythonhosted.org/packages/ce/c9/74e44e056a23fbc33aca71779ef450ca648a5bc472bdad7a82339918f818/yarl-1.23.0-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fda207c815b253e34f7e1909840fd14299567b1c0eb4908f8c2ce01a41265401", size = 105557, upload-time = "2026-03-01T22:05:51.416Z" }, + { url = "https://files.pythonhosted.org/packages/66/fe/b1e10b08d287f518994f1e2ff9b6d26f0adeecd8dd7d533b01bab29a3eda/yarl-1.23.0-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:34b6cf500e61c90f305094911f9acc9c86da1a05a7a3f5be9f68817043f486e4", size = 101559, upload-time = "2026-03-01T22:05:52.872Z" }, + { url = "https://files.pythonhosted.org/packages/72/59/c5b8d94b14e3d3c2a9c20cb100119fd534ab5a14b93673ab4cc4a4141ea5/yarl-1.23.0-cp313-cp313-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:d7504f2b476d21653e4d143f44a175f7f751cd41233525312696c76aa3dbb23f", size = 100502, upload-time = "2026-03-01T22:05:54.954Z" }, + { url = "https://files.pythonhosted.org/packages/77/4f/96976cb54cbfc5c9fd73ed4c51804f92f209481d1fb190981c0f8a07a1d7/yarl-1.23.0-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:578110dd426f0d209d1509244e6d4a3f1a3e9077655d98c5f22583d63252a08a", size = 98027, upload-time = "2026-03-01T22:05:56.409Z" }, + { url = "https://files.pythonhosted.org/packages/63/6e/904c4f476471afdbad6b7e5b70362fb5810e35cd7466529a97322b6f5556/yarl-1.23.0-cp313-cp313-musllinux_1_2_armv7l.whl", hash = "sha256:609d3614d78d74ebe35f54953c5bbd2ac647a7ddb9c30a5d877580f5e86b22f2", size = 95369, upload-time = "2026-03-01T22:05:58.141Z" }, + { url = "https://files.pythonhosted.org/packages/9d/40/acfcdb3b5f9d68ef499e39e04d25e141fe90661f9d54114556cf83be8353/yarl-1.23.0-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:4966242ec68afc74c122f8459abd597afd7d8a60dc93d695c1334c5fd25f762f", size = 105565, upload-time = "2026-03-01T22:06:00.286Z" }, + { url = "https://files.pythonhosted.org/packages/5e/c6/31e28f3a6ba2869c43d124f37ea5260cac9c9281df803c354b31f4dd1f3c/yarl-1.23.0-cp313-cp313-musllinux_1_2_riscv64.whl", hash = "sha256:e0fd068364a6759bc794459f0a735ab151d11304346332489c7972bacbe9e72b", size = 99813, upload-time = "2026-03-01T22:06:01.712Z" }, + { url = "https://files.pythonhosted.org/packages/08/1f/6f65f59e72d54aa467119b63fc0b0b1762eff0232db1f4720cd89e2f4a17/yarl-1.23.0-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:39004f0ad156da43e86aa71f44e033de68a44e5a31fc53507b36dd253970054a", size = 105632, upload-time = "2026-03-01T22:06:03.188Z" }, + { url = "https://files.pythonhosted.org/packages/a3/c4/18b178a69935f9e7a338127d5b77d868fdc0f0e49becd286d51b3a18c61d/yarl-1.23.0-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:e5723c01a56c5028c807c701aa66722916d2747ad737a046853f6c46f4875543", size = 101895, upload-time = "2026-03-01T22:06:04.651Z" }, + { url = "https://files.pythonhosted.org/packages/8f/54/f5b870b5505663911dba950a8e4776a0dbd51c9c54c0ae88e823e4b874a0/yarl-1.23.0-cp313-cp313-win32.whl", hash = "sha256:1b6b572edd95b4fa8df75de10b04bc81acc87c1c7d16bcdd2035b09d30acc957", size = 82356, upload-time = "2026-03-01T22:06:06.04Z" }, + { url = "https://files.pythonhosted.org/packages/7a/84/266e8da36879c6edcd37b02b547e2d9ecdfea776be49598e75696e3316e1/yarl-1.23.0-cp313-cp313-win_amd64.whl", hash = "sha256:baaf55442359053c7d62f6f8413a62adba3205119bcb6f49594894d8be47e5e3", size = 87515, upload-time = "2026-03-01T22:06:08.107Z" }, + { url = "https://files.pythonhosted.org/packages/00/fd/7e1c66efad35e1649114fa13f17485f62881ad58edeeb7f49f8c5e748bf9/yarl-1.23.0-cp313-cp313-win_arm64.whl", hash = "sha256:fb4948814a2a98e3912505f09c9e7493b1506226afb1f881825368d6fb776ee3", size = 81785, upload-time = "2026-03-01T22:06:10.181Z" }, + { url = "https://files.pythonhosted.org/packages/9c/fc/119dd07004f17ea43bb91e3ece6587759edd7519d6b086d16bfbd3319982/yarl-1.23.0-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:aecfed0b41aa72b7881712c65cf764e39ce2ec352324f5e0837c7048d9e6daaa", size = 130719, upload-time = "2026-03-01T22:06:11.708Z" }, + { url = "https://files.pythonhosted.org/packages/e6/0d/9f2348502fbb3af409e8f47730282cd6bc80dec6630c1e06374d882d6eb2/yarl-1.23.0-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:a41bcf68efd19073376eb8cf948b8d9be0af26256403e512bb18f3966f1f9120", size = 89690, upload-time = "2026-03-01T22:06:13.429Z" }, + { url = "https://files.pythonhosted.org/packages/50/93/e88f3c80971b42cfc83f50a51b9d165a1dbf154b97005f2994a79f212a07/yarl-1.23.0-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cde9a2ecd91668bcb7f077c4966d8ceddb60af01b52e6e3e2680e4cf00ad1a59", size = 89851, upload-time = "2026-03-01T22:06:15.53Z" }, + { url = "https://files.pythonhosted.org/packages/1c/07/61c9dd8ba8f86473263b4036f70fb594c09e99c0d9737a799dfd8bc85651/yarl-1.23.0-cp313-cp313t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:5023346c4ee7992febc0068e7593de5fa2bf611848c08404b35ebbb76b1b0512", size = 95874, upload-time = "2026-03-01T22:06:17.553Z" }, + { url = "https://files.pythonhosted.org/packages/9e/e9/f9ff8ceefba599eac6abddcfb0b3bee9b9e636e96dbf54342a8577252379/yarl-1.23.0-cp313-cp313t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:d1009abedb49ae95b136a8904a3f71b342f849ffeced2d3747bf29caeda218c4", size = 88710, upload-time = "2026-03-01T22:06:19.004Z" }, + { url = "https://files.pythonhosted.org/packages/eb/78/0231bfcc5d4c8eec220bc2f9ef82cb4566192ea867a7c5b4148f44f6cbcd/yarl-1.23.0-cp313-cp313t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a8d00f29b42f534cc8aa3931cfe773b13b23e561e10d2b26f27a8d309b0e82a1", size = 101033, upload-time = "2026-03-01T22:06:21.203Z" }, + { url = "https://files.pythonhosted.org/packages/cd/9b/30ea5239a61786f18fd25797151a17fbb3be176977187a48d541b5447dd4/yarl-1.23.0-cp313-cp313t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:95451e6ce06c3e104556d73b559f5da6c34a069b6b62946d3ad66afcd51642ea", size = 100817, upload-time = "2026-03-01T22:06:22.738Z" }, + { url = "https://files.pythonhosted.org/packages/62/e2/a4980481071791bc83bce2b7a1a1f7adcabfa366007518b4b845e92eeee3/yarl-1.23.0-cp313-cp313t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:531ef597132086b6cf96faa7c6c1dcd0361dd5f1694e5cc30375907b9b7d3ea9", size = 97482, upload-time = "2026-03-01T22:06:24.21Z" }, + { url = "https://files.pythonhosted.org/packages/e5/1e/304a00cf5f6100414c4b5a01fc7ff9ee724b62158a08df2f8170dfc72a2d/yarl-1.23.0-cp313-cp313t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:88f9fb0116fbfcefcab70f85cf4b74a2b6ce5d199c41345296f49d974ddb4123", size = 95949, upload-time = "2026-03-01T22:06:25.697Z" }, + { url = "https://files.pythonhosted.org/packages/68/03/093f4055ed4cae649ac53bca3d180bd37102e9e11d048588e9ab0c0108d0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:e7b0460976dc75cb87ad9cc1f9899a4b97751e7d4e77ab840fc9b6d377b8fd24", size = 95839, upload-time = "2026-03-01T22:06:27.309Z" }, + { url = "https://files.pythonhosted.org/packages/b9/28/4c75ebb108f322aa8f917ae10a8ffa4f07cae10a8a627b64e578617df6a0/yarl-1.23.0-cp313-cp313t-musllinux_1_2_armv7l.whl", hash = "sha256:115136c4a426f9da976187d238e84139ff6b51a20839aa6e3720cd1026d768de", size = 90696, upload-time = "2026-03-01T22:06:29.048Z" }, + { url = "https://files.pythonhosted.org/packages/23/9c/42c2e2dd91c1a570402f51bdf066bfdb1241c2240ba001967bad778e77b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_ppc64le.whl", hash = "sha256:ead11956716a940c1abc816b7df3fa2b84d06eaed8832ca32f5c5e058c65506b", size = 100865, upload-time = "2026-03-01T22:06:30.525Z" }, + { url = "https://files.pythonhosted.org/packages/74/05/1bcd60a8a0a914d462c305137246b6f9d167628d73568505fce3f1cb2e65/yarl-1.23.0-cp313-cp313t-musllinux_1_2_riscv64.whl", hash = "sha256:fe8f8f5e70e6dbdfca9882cd9deaac058729bcf323cf7a58660901e55c9c94f6", size = 96234, upload-time = "2026-03-01T22:06:32.692Z" }, + { url = "https://files.pythonhosted.org/packages/90/b2/f52381aac396d6778ce516b7bc149c79e65bfc068b5de2857ab69eeea3b7/yarl-1.23.0-cp313-cp313t-musllinux_1_2_s390x.whl", hash = "sha256:a0e317df055958a0c1e79e5d2aa5a5eaa4a6d05a20d4b0c9c3f48918139c9fc6", size = 100295, upload-time = "2026-03-01T22:06:34.268Z" }, + { url = "https://files.pythonhosted.org/packages/e5/e8/638bae5bbf1113a659b2435d8895474598afe38b4a837103764f603aba56/yarl-1.23.0-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:6f0fd84de0c957b2d280143522c4f91a73aada1923caee763e24a2b3fda9f8a5", size = 97784, upload-time = "2026-03-01T22:06:35.864Z" }, + { url = "https://files.pythonhosted.org/packages/80/25/a3892b46182c586c202629fc2159aa13975d3741d52ebd7347fd501d48d5/yarl-1.23.0-cp313-cp313t-win32.whl", hash = "sha256:93a784271881035ab4406a172edb0faecb6e7d00f4b53dc2f55919d6c9688595", size = 88313, upload-time = "2026-03-01T22:06:37.39Z" }, + { url = "https://files.pythonhosted.org/packages/43/68/8c5b36aa5178900b37387937bc2c2fe0e9505537f713495472dcf6f6fccc/yarl-1.23.0-cp313-cp313t-win_amd64.whl", hash = "sha256:dd00607bffbf30250fe108065f07453ec124dbf223420f57f5e749b04295e090", size = 94932, upload-time = "2026-03-01T22:06:39.579Z" }, + { url = "https://files.pythonhosted.org/packages/c6/cc/d79ba8292f51f81f4dc533a8ccfb9fc6992cabf0998ed3245de7589dc07c/yarl-1.23.0-cp313-cp313t-win_arm64.whl", hash = "sha256:ac09d42f48f80c9ee1635b2fcaa819496a44502737660d3c0f2ade7526d29144", size = 84786, upload-time = "2026-03-01T22:06:41.988Z" }, + { url = "https://files.pythonhosted.org/packages/90/98/b85a038d65d1b92c3903ab89444f48d3cee490a883477b716d7a24b1a78c/yarl-1.23.0-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:21d1b7305a71a15b4794b5ff22e8eef96ff4a6d7f9657155e5aa419444b28912", size = 124455, upload-time = "2026-03-01T22:06:43.615Z" }, + { url = "https://files.pythonhosted.org/packages/39/54/bc2b45559f86543d163b6e294417a107bb87557609007c007ad889afec18/yarl-1.23.0-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:85610b4f27f69984932a7abbe52703688de3724d9f72bceb1cca667deff27474", size = 86752, upload-time = "2026-03-01T22:06:45.425Z" }, + { url = "https://files.pythonhosted.org/packages/24/f9/e8242b68362bffe6fb536c8db5076861466fc780f0f1b479fc4ffbebb128/yarl-1.23.0-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23f371bd662cf44a7630d4d113101eafc0cfa7518a2760d20760b26021454719", size = 86291, upload-time = "2026-03-01T22:06:46.974Z" }, + { url = "https://files.pythonhosted.org/packages/ea/d8/d1cb2378c81dd729e98c716582b1ccb08357e8488e4c24714658cc6630e8/yarl-1.23.0-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c4a80f77dc1acaaa61f0934176fccca7096d9b1ff08c8ba9cddf5ae034a24319", size = 99026, upload-time = "2026-03-01T22:06:48.459Z" }, + { url = "https://files.pythonhosted.org/packages/0a/ff/7196790538f31debe3341283b5b0707e7feb947620fc5e8236ef28d44f72/yarl-1.23.0-cp314-cp314-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:bd654fad46d8d9e823afbb4f87c79160b5a374ed1ff5bde24e542e6ba8f41434", size = 92355, upload-time = "2026-03-01T22:06:50.306Z" }, + { url = "https://files.pythonhosted.org/packages/c1/56/25d58c3eddde825890a5fe6aa1866228377354a3c39262235234ab5f616b/yarl-1.23.0-cp314-cp314-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:682bae25f0a0dd23a056739f23a134db9f52a63e2afd6bfb37ddc76292bbd723", size = 106417, upload-time = "2026-03-01T22:06:52.1Z" }, + { url = "https://files.pythonhosted.org/packages/51/8a/882c0e7bc8277eb895b31bce0138f51a1ba551fc2e1ec6753ffc1e7c1377/yarl-1.23.0-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a82836cab5f197a0514235aaf7ffccdc886ccdaa2324bc0aafdd4ae898103039", size = 106422, upload-time = "2026-03-01T22:06:54.424Z" }, + { url = "https://files.pythonhosted.org/packages/42/2b/fef67d616931055bf3d6764885990a3ac647d68734a2d6a9e1d13de437a2/yarl-1.23.0-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:1c57676bdedc94cd3bc37724cf6f8cd2779f02f6aba48de45feca073e714fe52", size = 101915, upload-time = "2026-03-01T22:06:55.895Z" }, + { url = "https://files.pythonhosted.org/packages/18/6a/530e16aebce27c5937920f3431c628a29a4b6b430fab3fd1c117b26ff3f6/yarl-1.23.0-cp314-cp314-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:c7f8dc16c498ff06497c015642333219871effba93e4a2e8604a06264aca5c5c", size = 100690, upload-time = "2026-03-01T22:06:58.21Z" }, + { url = "https://files.pythonhosted.org/packages/88/08/93749219179a45e27b036e03260fda05190b911de8e18225c294ac95bbc9/yarl-1.23.0-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:5ee586fb17ff8f90c91cf73c6108a434b02d69925f44f5f8e0d7f2f260607eae", size = 98750, upload-time = "2026-03-01T22:06:59.794Z" }, + { url = "https://files.pythonhosted.org/packages/d9/cf/ea424a004969f5d81a362110a6ac1496d79efdc6d50c2c4b2e3ea0fc2519/yarl-1.23.0-cp314-cp314-musllinux_1_2_armv7l.whl", hash = "sha256:17235362f580149742739cc3828b80e24029d08cbb9c4bda0242c7b5bc610a8e", size = 94685, upload-time = "2026-03-01T22:07:01.375Z" }, + { url = "https://files.pythonhosted.org/packages/e2/b7/14341481fe568e2b0408bcf1484c652accafe06a0ade9387b5d3fd9df446/yarl-1.23.0-cp314-cp314-musllinux_1_2_ppc64le.whl", hash = "sha256:0793e2bd0cf14234983bbb371591e6bea9e876ddf6896cdcc93450996b0b5c85", size = 106009, upload-time = "2026-03-01T22:07:03.151Z" }, + { url = "https://files.pythonhosted.org/packages/0a/e6/5c744a9b54f4e8007ad35bce96fbc9218338e84812d36f3390cea616881a/yarl-1.23.0-cp314-cp314-musllinux_1_2_riscv64.whl", hash = "sha256:3650dc2480f94f7116c364096bc84b1d602f44224ef7d5c7208425915c0475dd", size = 100033, upload-time = "2026-03-01T22:07:04.701Z" }, + { url = "https://files.pythonhosted.org/packages/0c/23/e3bfc188d0b400f025bc49d99793d02c9abe15752138dcc27e4eaf0c4a9e/yarl-1.23.0-cp314-cp314-musllinux_1_2_s390x.whl", hash = "sha256:f40e782d49630ad384db66d4d8b73ff4f1b8955dc12e26b09a3e3af064b3b9d6", size = 106483, upload-time = "2026-03-01T22:07:06.231Z" }, + { url = "https://files.pythonhosted.org/packages/72/42/f0505f949a90b3f8b7a363d6cbdf398f6e6c58946d85c6d3a3bc70595b26/yarl-1.23.0-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:94f8575fbdf81749008d980c17796097e645574a3b8c28ee313931068dad14fe", size = 102175, upload-time = "2026-03-01T22:07:08.4Z" }, + { url = "https://files.pythonhosted.org/packages/aa/65/b39290f1d892a9dd671d1c722014ca062a9c35d60885d57e5375db0404b5/yarl-1.23.0-cp314-cp314-win32.whl", hash = "sha256:c8aa34a5c864db1087d911a0b902d60d203ea3607d91f615acd3f3108ac32169", size = 83871, upload-time = "2026-03-01T22:07:09.968Z" }, + { url = "https://files.pythonhosted.org/packages/a9/5b/9b92f54c784c26e2a422e55a8d2607ab15b7ea3349e28359282f84f01d43/yarl-1.23.0-cp314-cp314-win_amd64.whl", hash = "sha256:63e92247f383c85ab00dd0091e8c3fa331a96e865459f5ee80353c70a4a42d70", size = 89093, upload-time = "2026-03-01T22:07:11.501Z" }, + { url = "https://files.pythonhosted.org/packages/e0/7d/8a84dc9381fd4412d5e7ff04926f9865f6372b4c2fd91e10092e65d29eb8/yarl-1.23.0-cp314-cp314-win_arm64.whl", hash = "sha256:70efd20be968c76ece7baa8dafe04c5be06abc57f754d6f36f3741f7aa7a208e", size = 83384, upload-time = "2026-03-01T22:07:13.069Z" }, + { url = "https://files.pythonhosted.org/packages/dd/8d/d2fad34b1c08aa161b74394183daa7d800141aaaee207317e82c790b418d/yarl-1.23.0-cp314-cp314t-macosx_10_15_universal2.whl", hash = "sha256:9a18d6f9359e45722c064c97464ec883eb0e0366d33eda61cb19a244bf222679", size = 131019, upload-time = "2026-03-01T22:07:14.903Z" }, + { url = "https://files.pythonhosted.org/packages/19/ff/33009a39d3ccf4b94d7d7880dfe17fb5816c5a4fe0096d9b56abceea9ac7/yarl-1.23.0-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:2803ed8b21ca47a43da80a6fd1ed3019d30061f7061daa35ac54f63933409412", size = 89894, upload-time = "2026-03-01T22:07:17.372Z" }, + { url = "https://files.pythonhosted.org/packages/0c/f1/dab7ac5e7306fb79c0190766a3c00b4cb8d09a1f390ded68c85a5934faf5/yarl-1.23.0-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:394906945aa8b19fc14a61cf69743a868bb8c465efe85eee687109cc540b98f4", size = 89979, upload-time = "2026-03-01T22:07:19.361Z" }, + { url = "https://files.pythonhosted.org/packages/aa/b1/08e95f3caee1fad6e65017b9f26c1d79877b502622d60e517de01e72f95d/yarl-1.23.0-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:71d006bee8397a4a89f469b8deb22469fe7508132d3c17fa6ed871e79832691c", size = 95943, upload-time = "2026-03-01T22:07:21.266Z" }, + { url = "https://files.pythonhosted.org/packages/c0/cc/6409f9018864a6aa186c61175b977131f373f1988e198e031236916e87e4/yarl-1.23.0-cp314-cp314t-manylinux2014_armv7l.manylinux_2_17_armv7l.manylinux_2_31_armv7l.whl", hash = "sha256:62694e275c93d54f7ccedcfef57d42761b2aad5234b6be1f3e3026cae4001cd4", size = 88786, upload-time = "2026-03-01T22:07:23.129Z" }, + { url = "https://files.pythonhosted.org/packages/76/40/cc22d1d7714b717fde2006fad2ced5efe5580606cb059ae42117542122f3/yarl-1.23.0-cp314-cp314t-manylinux2014_ppc64le.manylinux_2_17_ppc64le.manylinux_2_28_ppc64le.whl", hash = "sha256:a31de1613658308efdb21ada98cbc86a97c181aa050ba22a808120bb5be3ab94", size = 101307, upload-time = "2026-03-01T22:07:24.689Z" }, + { url = "https://files.pythonhosted.org/packages/8f/0d/476c38e85ddb4c6ec6b20b815bdd779aa386a013f3d8b85516feee55c8dc/yarl-1.23.0-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:fb1e8b8d66c278b21d13b0a7ca22c41dd757a7c209c6b12c313e445c31dd3b28", size = 100904, upload-time = "2026-03-01T22:07:26.287Z" }, + { url = "https://files.pythonhosted.org/packages/72/32/0abe4a76d59adf2081dcb0397168553ece4616ada1c54d1c49d8936c74f8/yarl-1.23.0-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:50f9d8d531dfb767c565f348f33dd5139a6c43f5cbdf3f67da40d54241df93f6", size = 97728, upload-time = "2026-03-01T22:07:27.906Z" }, + { url = "https://files.pythonhosted.org/packages/b7/35/7b30f4810fba112f60f5a43237545867504e15b1c7647a785fbaf588fac2/yarl-1.23.0-cp314-cp314t-manylinux_2_31_riscv64.manylinux_2_39_riscv64.whl", hash = "sha256:575aa4405a656e61a540f4a80eaa5260f2a38fff7bfdc4b5f611840d76e9e277", size = 95964, upload-time = "2026-03-01T22:07:30.198Z" }, + { url = "https://files.pythonhosted.org/packages/2d/86/ed7a73ab85ef00e8bb70b0cb5421d8a2a625b81a333941a469a6f4022828/yarl-1.23.0-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:041b1a4cefacf65840b4e295c6985f334ba83c30607441ae3cf206a0eed1a2e4", size = 95882, upload-time = "2026-03-01T22:07:32.132Z" }, + { url = "https://files.pythonhosted.org/packages/19/90/d56967f61a29d8498efb7afb651e0b2b422a1e9b47b0ab5f4e40a19b699b/yarl-1.23.0-cp314-cp314t-musllinux_1_2_armv7l.whl", hash = "sha256:d38c1e8231722c4ce40d7593f28d92b5fc72f3e9774fe73d7e800ec32299f63a", size = 90797, upload-time = "2026-03-01T22:07:34.404Z" }, + { url = "https://files.pythonhosted.org/packages/72/00/8b8f76909259f56647adb1011d7ed8b321bcf97e464515c65016a47ecdf0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_ppc64le.whl", hash = "sha256:d53834e23c015ee83a99377db6e5e37d8484f333edb03bd15b4bc312cc7254fb", size = 101023, upload-time = "2026-03-01T22:07:35.953Z" }, + { url = "https://files.pythonhosted.org/packages/ac/e2/cab11b126fb7d440281b7df8e9ddbe4851e70a4dde47a202b6642586b8d9/yarl-1.23.0-cp314-cp314t-musllinux_1_2_riscv64.whl", hash = "sha256:2e27c8841126e017dd2a054a95771569e6070b9ee1b133366d8b31beb5018a41", size = 96227, upload-time = "2026-03-01T22:07:37.594Z" }, + { url = "https://files.pythonhosted.org/packages/c2/9b/2c893e16bfc50e6b2edf76c1a9eb6cb0c744346197e74c65e99ad8d634d0/yarl-1.23.0-cp314-cp314t-musllinux_1_2_s390x.whl", hash = "sha256:76855800ac56f878847a09ce6dba727c93ca2d89c9e9d63002d26b916810b0a2", size = 100302, upload-time = "2026-03-01T22:07:39.334Z" }, + { url = "https://files.pythonhosted.org/packages/28/ec/5498c4e3a6d5f1003beb23405671c2eb9cdbf3067d1c80f15eeafe301010/yarl-1.23.0-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:e09fd068c2e169a7070d83d3bde728a4d48de0549f975290be3c108c02e499b4", size = 98202, upload-time = "2026-03-01T22:07:41.717Z" }, + { url = "https://files.pythonhosted.org/packages/fe/c3/cd737e2d45e70717907f83e146f6949f20cc23cd4bf7b2688727763aa458/yarl-1.23.0-cp314-cp314t-win32.whl", hash = "sha256:73309162a6a571d4cbd3b6a1dcc703c7311843ae0d1578df6f09be4e98df38d4", size = 90558, upload-time = "2026-03-01T22:07:43.433Z" }, + { url = "https://files.pythonhosted.org/packages/e1/19/3774d162f6732d1cfb0b47b4140a942a35ca82bb19b6db1f80e9e7bdc8f8/yarl-1.23.0-cp314-cp314t-win_amd64.whl", hash = "sha256:4503053d296bc6e4cbd1fad61cf3b6e33b939886c4f249ba7c78b602214fabe2", size = 97610, upload-time = "2026-03-01T22:07:45.773Z" }, + { url = "https://files.pythonhosted.org/packages/51/47/3fa2286c3cb162c71cdb34c4224d5745a1ceceb391b2bd9b19b668a8d724/yarl-1.23.0-cp314-cp314t-win_arm64.whl", hash = "sha256:44bb7bef4ea409384e3f8bc36c063d77ea1b8d4a5b2706956c0d6695f07dcc25", size = 86041, upload-time = "2026-03-01T22:07:49.026Z" }, + { url = "https://files.pythonhosted.org/packages/69/68/c8739671f5699c7dc470580a4f821ef37c32c4cb0b047ce223a7f115757f/yarl-1.23.0-py3-none-any.whl", hash = "sha256:a2df6afe50dea8ae15fa34c9f824a3ee958d785fd5d089063d960bae1daa0a3f", size = 48288, upload-time = "2026-03-01T22:07:51.388Z" }, +]