feat: initial scaffold of osint_db (DuckDB source-of-truth service)
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
data/
|
||||||
|
.venv/
|
||||||
|
__pycache__/
|
||||||
|
*.pyc
|
||||||
|
.pytest_cache/
|
||||||
|
*.duckdb
|
||||||
|
*.duckdb.*
|
||||||
|
*.duckdb-wal
|
||||||
|
server.log
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
---
|
||||||
|
name: osint_db
|
||||||
|
lang: py
|
||||||
|
domain: osint
|
||||||
|
version: 0.1.0
|
||||||
|
description: "Service FastAPI local (solo 127.0.0.1, puerto 8771) dueño único de la base DuckDB data/osint.duckdb: fuente de verdad estructurada del ecosistema OSINT. Indexa el vault de Obsidian osint (notes + persons/organizations/domains/cases/places con note_path), importa las maestras DAV de Xandikos (contacts, events), computa derivadas sin referencias a notas y renderiza tableros Markdown con bloques sentinel idempotentes dentro del vault."
|
||||||
|
tags: [service, osint, duckdb, obsidian, dav, vault]
|
||||||
|
uses_functions:
|
||||||
|
- list_obsidian_notes_py_obsidian
|
||||||
|
- read_obsidian_note_py_obsidian
|
||||||
|
- update_obsidian_note_py_obsidian
|
||||||
|
- create_obsidian_note_py_obsidian
|
||||||
|
- slugify_obsidian_name_py_obsidian
|
||||||
|
- dav_get_collection_py_infra
|
||||||
|
- dav_list_calendars_py_infra
|
||||||
|
- dav_collection_ctag_py_infra
|
||||||
|
- pass_get_secret_py_infra
|
||||||
|
- duckdb_query_readonly_py_infra
|
||||||
|
- render_markdown_table_py_core
|
||||||
|
- upsert_sentinel_block_py_core
|
||||||
|
uses_types: []
|
||||||
|
framework: "fastapi"
|
||||||
|
entry_point: "server/main.py"
|
||||||
|
dir_path: "projects/osint/apps/osint_db"
|
||||||
|
repo_url: "https://gitea-dgg044oo04woo4ggcsws4gk0.organic-machine.com/dataforge/osint_db"
|
||||||
|
service:
|
||||||
|
port: 8771
|
||||||
|
health_endpoint: /api/health
|
||||||
|
health_timeout_s: 3
|
||||||
|
systemd_unit: null
|
||||||
|
systemd_scope: null
|
||||||
|
restart_policy: none
|
||||||
|
runtime: manual
|
||||||
|
pc_targets:
|
||||||
|
- lucas-linux
|
||||||
|
is_local_only: true
|
||||||
|
e2e_checks:
|
||||||
|
- id: tests
|
||||||
|
cmd: ".venv/bin/python -m pytest tests -q"
|
||||||
|
timeout_s: 120
|
||||||
|
- id: vault_missing
|
||||||
|
cmd: ".venv/bin/python server/main.py --vault /no/existe --port 0"
|
||||||
|
expect_exit: 2
|
||||||
|
timeout_s: 30
|
||||||
|
---
|
||||||
|
|
||||||
|
## Qué es
|
||||||
|
|
||||||
|
Service del project `osint` que posee en exclusiva la base DuckDB
|
||||||
|
`data/osint.duckdb`, la fuente de verdad estructurada del ecosistema OSINT
|
||||||
|
(vault de Obsidian `~/Obsidian/osint` + servidor Xandikos CardDAV/CalDAV). Un
|
||||||
|
plugin de Obsidian se construye en paralelo contra el contrato de esta API, por
|
||||||
|
eso los endpoints de datos responden SIEMPRE HTTP 200 con `status: ok|error`
|
||||||
|
en el body (el plugin parsea el body, no el código HTTP).
|
||||||
|
|
||||||
|
## Arquitectura de datos (3 categorías)
|
||||||
|
|
||||||
|
1. **Maestras con referencia a notas** (schema `main`): `notes` (índice
|
||||||
|
completo del vault) + `persons`, `organizations`, `domains`, `cases`,
|
||||||
|
`places` (fichas de nivel-1 de cada carpeta de entidades, excluyendo las
|
||||||
|
notas con prefijo `_`). Cada una lleva `note_path`: el path relativo de la
|
||||||
|
nota dentro del vault.
|
||||||
|
2. **Maestras DAV** (schema `main`): `contacts` y `events` importados de
|
||||||
|
Xandikos — fuente de verdad del lado agenda/calendario. `contacts.note_path`
|
||||||
|
se enlaza contra `persons` matcheando por UID `osint-<slug>`, por el
|
||||||
|
`dav_uid` extraído del campo `fuente` de la ficha, por teléfono normalizado
|
||||||
|
o por email.
|
||||||
|
3. **Derivadas** (schema `derived`): SOLO datos computados. **Regla dura:
|
||||||
|
ninguna tabla de `derived` lleva columna que referencie notas** (`note_path`
|
||||||
|
prohibido ahí; hay un test que lo verifica vía `information_schema`). Se
|
||||||
|
reconstruyen completas (DROP + CREATE) en cada ingest:
|
||||||
|
`derived.person_stats` (agregados por contexto/país/tag),
|
||||||
|
`derived.event_monthly` (eventos por calendario y mes) y
|
||||||
|
`derived.contact_link_quality` (contactos enlazados vs no, solo números).
|
||||||
|
|
||||||
|
Single-writer: SOLO este service escribe la DuckDB. La conexión de escritura se
|
||||||
|
abre bajo demanda (migraciones, ingest, render) serializada con un lock de
|
||||||
|
proceso y se cierra al terminar; las lecturas de `/api/query` abren su propia
|
||||||
|
conexión `read_only` vía `duckdb_query_readonly`.
|
||||||
|
|
||||||
|
## Migraciones
|
||||||
|
|
||||||
|
`migrations/NNN_*.sql` numeradas, aditivas e idempotentes, aplicadas en orden
|
||||||
|
al arrancar. La tabla `_migrations` registra las aplicadas (regla
|
||||||
|
`db_migrations` adaptada a DuckDB).
|
||||||
|
|
||||||
|
## Arrancar
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd projects/osint/apps/osint_db
|
||||||
|
uv sync # primera vez: crea .venv
|
||||||
|
.venv/bin/python server/main.py # defaults: vault ~/Obsidian/osint, puerto 8771
|
||||||
|
.venv/bin/python server/main.py --vault ~/Obsidian/osint --db data/osint.duckdb --port 8771
|
||||||
|
```
|
||||||
|
|
||||||
|
Health check: `curl http://127.0.0.1:8771/api/health`.
|
||||||
|
|
||||||
|
## Endpoints
|
||||||
|
|
||||||
|
| Método | Ruta | Qué hace |
|
||||||
|
|---|---|---|
|
||||||
|
| GET | `/api/health` | `{"status":"ok","db_path":"...","tables":N}` |
|
||||||
|
| GET | `/api/tables` | inventario: schema, name, kind master/derived, row_count, columnas |
|
||||||
|
| POST | `/api/query` | `{sql, params, max_rows}` → respuesta exacta de `duckdb_query_readonly` (solo lectura) |
|
||||||
|
| GET | `/api/queries` | catálogo de queries con nombre (`server/named_queries.py`) |
|
||||||
|
| POST | `/api/query/named` | `{name, max_rows}` → misma shape que `/api/query` |
|
||||||
|
| POST | `/api/ingest/vault` | escanea el vault completo y reconstruye notes + entidades + derivadas |
|
||||||
|
| POST | `/api/ingest/dav` | baja Xandikos (CardDAV + cada calendario CalDAV), reconstruye contacts/events, enlaza y reconstruye derivadas |
|
||||||
|
| POST | `/api/render/note` | `{note_path, block_id, sql\|query, title?}` → tabla Markdown upsertada como bloque sentinel `osintdb` en la nota (la crea si no existe) |
|
||||||
|
|
||||||
|
Queries con nombre incluidas: `personas_por_contexto`, `personas_recientes`,
|
||||||
|
`eventos_proximos`, `contactos_sin_nota`, `stats_personas`,
|
||||||
|
`calidad_enlace_contactos`, `eventos_por_mes`.
|
||||||
|
|
||||||
|
## Configuración
|
||||||
|
|
||||||
|
`server/config.py`: vault (`~/Obsidian/osint`), db (`data/osint.duckdb`
|
||||||
|
relativa a la app), DAV base/colecciones de Xandikos (los mismos valores que
|
||||||
|
`projects/osint/tools/sync_dav_to_osint.py`) y puerto 8771. Overrides por CLI:
|
||||||
|
`--vault`, `--db`, `--port`. La credencial DAV se resuelve SIEMPRE con
|
||||||
|
`pass_get_secret("dav/xandikos-enmanuel")`, nunca hardcodeada.
|
||||||
|
|
||||||
|
## Seguridad
|
||||||
|
|
||||||
|
- El vault y la base contienen datos personales sensibles: el server escucha
|
||||||
|
solo en `127.0.0.1` y `runtime: manual` (no se despliega a VPS,
|
||||||
|
`is_local_only: true`).
|
||||||
|
- `/api/query` es estrictamente read-only (conexión `read_only` de DuckDB).
|
||||||
|
- `/api/render/note` valida que el path destino no escapa del vault (realpath
|
||||||
|
bajo el realpath del vault).
|
||||||
|
- Vault inexistente al arrancar → error claro en stderr + exit 2.
|
||||||
|
|
||||||
|
## Tests
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd projects/osint/apps/osint_db
|
||||||
|
.venv/bin/python -m pytest tests -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Vault temporal + DuckDB temporal, sin red: migraciones idempotentes, ingest del
|
||||||
|
vault con fixture (conteos, exclusión de sub-notas y `_`), extracción de
|
||||||
|
`dav_uid` desde `fuente`, `/api/query` ok/error/solo-lectura (siempre HTTP
|
||||||
|
200), catálogo y queries con nombre, inventario de tablas, regla dura derivadas
|
||||||
|
sin `note_path`, enlace contacto→ficha por teléfono y render de nota con bloque
|
||||||
|
sentinel idempotente + validación de inputs y path traversal.
|
||||||
|
|
||||||
|
## Gotchas
|
||||||
|
|
||||||
|
- DuckDB bloquea el archivo a un escritor exclusivo: mientras un ingest está
|
||||||
|
escribiendo, una lectura `read_only` concurrente puede devolver
|
||||||
|
`{"status":"error"}` por conflicto de lock. El cliente reintenta.
|
||||||
|
- `/api/ingest/dav` requiere `pass` desbloqueado (gpg-agent); si gpg está
|
||||||
|
bloqueado devuelve `{"status":"error"}` con el detalle, sin crash.
|
||||||
|
- Las notas con prefijo `_` (`_indice.md`, `_plantilla.md`) y las sub-notas de
|
||||||
|
documento (`personas/<slug>/<doc>.md`) entran en `notes` pero NO cuentan como
|
||||||
|
entidades.
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
-- Migración inicial de la base osint.duckdb.
|
||||||
|
--
|
||||||
|
-- Tres categorías de datos:
|
||||||
|
-- 1. Tablas maestras con referencia a notas (schema main): índice del vault
|
||||||
|
-- (notes) + entidades estructuradas (persons, organizations, domains,
|
||||||
|
-- cases, places). Cada una lleva note_path: el path relativo de la nota
|
||||||
|
-- dentro del vault de Obsidian.
|
||||||
|
-- 2. Tablas maestras DAV (schema main): contacts y events importados del
|
||||||
|
-- servidor Xandikos (CardDAV/CalDAV) — fuente de verdad.
|
||||||
|
-- 3. Tablas derivadas (schema derived): SOLO datos computados. Regla dura:
|
||||||
|
-- ninguna tabla de derived lleva columna que referencie notas. Se
|
||||||
|
-- reconstruyen completas en cada ingest, por eso esta migración solo crea
|
||||||
|
-- el schema, no las tablas.
|
||||||
|
|
||||||
|
-- Índice completo del vault: una fila por nota .md.
|
||||||
|
CREATE TABLE IF NOT EXISTS notes (
|
||||||
|
note_path TEXT PRIMARY KEY,
|
||||||
|
slug TEXT,
|
||||||
|
tipo TEXT,
|
||||||
|
title TEXT,
|
||||||
|
mtime TIMESTAMP,
|
||||||
|
frontmatter JSON
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Fichas de persona (personas/<slug>.md), esquema canónico de CONVENTIONS.md 3b.
|
||||||
|
CREATE TABLE IF NOT EXISTS persons (
|
||||||
|
slug TEXT PRIMARY KEY,
|
||||||
|
note_path TEXT,
|
||||||
|
nombre TEXT,
|
||||||
|
aliases JSON,
|
||||||
|
sexo TEXT,
|
||||||
|
fecha_nacimiento TEXT,
|
||||||
|
dni TEXT,
|
||||||
|
telefono TEXT,
|
||||||
|
email TEXT,
|
||||||
|
direccion TEXT,
|
||||||
|
pais TEXT,
|
||||||
|
contexto TEXT,
|
||||||
|
fuente TEXT,
|
||||||
|
dav_uid TEXT,
|
||||||
|
tags JSON,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Resto de entidades del vault: organizaciones, dominios, casos y lugares.
|
||||||
|
CREATE TABLE IF NOT EXISTS organizations (
|
||||||
|
slug TEXT PRIMARY KEY,
|
||||||
|
note_path TEXT,
|
||||||
|
nombre TEXT,
|
||||||
|
tags JSON,
|
||||||
|
frontmatter JSON,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS domains (
|
||||||
|
slug TEXT PRIMARY KEY,
|
||||||
|
note_path TEXT,
|
||||||
|
nombre TEXT,
|
||||||
|
tags JSON,
|
||||||
|
frontmatter JSON,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS cases (
|
||||||
|
slug TEXT PRIMARY KEY,
|
||||||
|
note_path TEXT,
|
||||||
|
nombre TEXT,
|
||||||
|
tags JSON,
|
||||||
|
frontmatter JSON,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
CREATE TABLE IF NOT EXISTS places (
|
||||||
|
slug TEXT PRIMARY KEY,
|
||||||
|
note_path TEXT,
|
||||||
|
nombre TEXT,
|
||||||
|
tags JSON,
|
||||||
|
frontmatter JSON,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Maestras DAV: contactos del addressbook CardDAV. note_path enlaza el
|
||||||
|
-- contacto con su ficha del vault cuando el matching lo consigue.
|
||||||
|
CREATE TABLE IF NOT EXISTS contacts (
|
||||||
|
uid TEXT PRIMARY KEY,
|
||||||
|
collection TEXT,
|
||||||
|
etag TEXT,
|
||||||
|
fn TEXT,
|
||||||
|
tels JSON,
|
||||||
|
emails JSON,
|
||||||
|
raw TEXT,
|
||||||
|
note_path TEXT,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Maestras DAV: eventos de las colecciones CalDAV.
|
||||||
|
CREATE TABLE IF NOT EXISTS events (
|
||||||
|
uid TEXT PRIMARY KEY,
|
||||||
|
calendar TEXT,
|
||||||
|
etag TEXT,
|
||||||
|
dtstart TEXT,
|
||||||
|
dtend TEXT,
|
||||||
|
all_day BOOLEAN,
|
||||||
|
summary TEXT,
|
||||||
|
location TEXT,
|
||||||
|
rrule TEXT,
|
||||||
|
raw TEXT,
|
||||||
|
updated_at TIMESTAMP
|
||||||
|
);
|
||||||
|
|
||||||
|
-- Schema para las tablas derivadas (solo datos computados, sin note_path).
|
||||||
|
CREATE SCHEMA IF NOT EXISTS derived;
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
[project]
|
||||||
|
name = "osint-db"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Service FastAPI local dueño único de la base DuckDB osint.duckdb: fuente de verdad estructurada del ecosistema OSINT (vault Obsidian + Xandikos CardDAV/CalDAV)."
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
dependencies = [
|
||||||
|
"fastapi>=0.110",
|
||||||
|
"uvicorn>=0.29",
|
||||||
|
"pyyaml>=6.0",
|
||||||
|
"duckdb>=1.0",
|
||||||
|
]
|
||||||
|
|
||||||
|
[dependency-groups]
|
||||||
|
dev = [
|
||||||
|
"pytest>=8.0",
|
||||||
|
"httpx>=0.27",
|
||||||
|
]
|
||||||
|
|
||||||
|
[tool.pytest.ini_options]
|
||||||
|
testpaths = ["tests"]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
"""Paquete server de la app osint_db (service FastAPI + capa DuckDB)."""
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
"""Configuración del service osint_db.
|
||||||
|
|
||||||
|
Valores por defecto del PC de enmanuel; todo es sobreescribible por CLI
|
||||||
|
(--vault, --db, --port) o construyendo un Config a mano en los tests. La
|
||||||
|
configuración DAV (base_url, colecciones, secreto en pass) es la misma que usan
|
||||||
|
los tools del proyecto (projects/osint/tools/sync_dav_to_osint.py).
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
from dataclasses import dataclass, field
|
||||||
|
|
||||||
|
# Directorio raíz de la app (padre de server/).
|
||||||
|
APP_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
|
||||||
|
|
||||||
|
DEFAULT_VAULT = os.path.expanduser("~/Obsidian/osint")
|
||||||
|
DEFAULT_DB = os.path.join(APP_DIR, "data", "osint.duckdb")
|
||||||
|
DEFAULT_PORT = 8771
|
||||||
|
|
||||||
|
# Configuración Xandikos (espejo de projects/osint/tools/sync_dav_to_osint.py).
|
||||||
|
DAV_BASE = "https://dav-eedeb681c4ab89ab8e444ac9.organic-machine.com"
|
||||||
|
DAV_USER = "enmanuel"
|
||||||
|
DAV_CONTACTS_COLLECTION = "/enmanuel/contacts/addressbook/"
|
||||||
|
DAV_CALENDAR_HOME = "/enmanuel/calendars/"
|
||||||
|
PASS_SECRET = "dav/xandikos-enmanuel"
|
||||||
|
|
||||||
|
# Carpetas del vault que mapean a tablas maestras de entidades.
|
||||||
|
# (carpeta del vault, tipo de frontmatter, tabla destino)
|
||||||
|
ENTITY_FOLDERS = (
|
||||||
|
("personas", "persona", "persons"),
|
||||||
|
("organizaciones", "organizacion", "organizations"),
|
||||||
|
("dominios", "dominio", "domains"),
|
||||||
|
("casos", "caso", "cases"),
|
||||||
|
("lugares", "lugar", "places"),
|
||||||
|
)
|
||||||
|
|
||||||
|
# Marker de los bloques sentinel que este service gestiona en notas del vault.
|
||||||
|
SENTINEL_MARKER = "osintdb"
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Config:
|
||||||
|
"""Configuración efectiva del service (inyectable en tests)."""
|
||||||
|
|
||||||
|
vault_dir: str = DEFAULT_VAULT
|
||||||
|
db_path: str = DEFAULT_DB
|
||||||
|
port: int = DEFAULT_PORT
|
||||||
|
host: str = "127.0.0.1"
|
||||||
|
dav_base: str = DAV_BASE
|
||||||
|
dav_user: str = DAV_USER
|
||||||
|
dav_contacts_collection: str = DAV_CONTACTS_COLLECTION
|
||||||
|
dav_calendar_home: str = DAV_CALENDAR_HOME
|
||||||
|
pass_secret: str = PASS_SECRET
|
||||||
|
entity_folders: tuple = field(default=ENTITY_FOLDERS)
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
"""Parseo ligero de vCard y de iCalendar para el ingest DAV.
|
||||||
|
|
||||||
|
Lógica propia de la app (glue específico del dominio): el registry baja las
|
||||||
|
colecciones en bruto con dav_get_collection y aquí se extraen los campos
|
||||||
|
mínimos que las tablas maestras contacts y events necesitan. Mismo enfoque de
|
||||||
|
regex con unfold que projects/osint/tools/sync_dav_to_osint.py.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import re
|
||||||
|
|
||||||
|
|
||||||
|
def _unfold(text: str) -> str:
|
||||||
|
"""Deshace el folding de líneas (continuación con espacio o tab)."""
|
||||||
|
return re.sub(r"\r?\n[ \t]", "", text)
|
||||||
|
|
||||||
|
|
||||||
|
def _values(text: str, prop: str) -> list:
|
||||||
|
"""Devuelve todos los valores de una propiedad (TEL, UID, DTSTART, ...).
|
||||||
|
|
||||||
|
Acepta PROP;PARAMS:valor y PROP:valor (con prefijo itemN. opcional) y
|
||||||
|
decodifica los escapes simples (\\n, \\,, \\;, \\\\).
|
||||||
|
"""
|
||||||
|
vals = []
|
||||||
|
for line in text.splitlines():
|
||||||
|
m = re.match(rf"^(?:item\d+\.)?{prop}(?:;[^:]*)?:(.*)$", line, re.IGNORECASE)
|
||||||
|
if m:
|
||||||
|
v = m.group(1).strip()
|
||||||
|
v = (
|
||||||
|
v.replace("\\n", "\n")
|
||||||
|
.replace("\\,", ",")
|
||||||
|
.replace("\\;", ";")
|
||||||
|
.replace("\\\\", "\\")
|
||||||
|
).strip()
|
||||||
|
if v:
|
||||||
|
vals.append(v)
|
||||||
|
return vals
|
||||||
|
|
||||||
|
|
||||||
|
def _prop_line(text: str, prop: str) -> str:
|
||||||
|
"""Devuelve la primera línea completa de una propiedad (con sus params)."""
|
||||||
|
for line in text.splitlines():
|
||||||
|
if re.match(rf"^(?:item\d+\.)?{prop}(?:;|:)", line, re.IGNORECASE):
|
||||||
|
return line
|
||||||
|
return ""
|
||||||
|
|
||||||
|
|
||||||
|
def parse_vcard(vcard_text: str) -> dict:
|
||||||
|
"""Extrae los campos mínimos de un vCard para la tabla contacts.
|
||||||
|
|
||||||
|
Devuelve {uid, fn, tels, emails}. tels y emails son listas deduplicadas
|
||||||
|
preservando el orden.
|
||||||
|
"""
|
||||||
|
txt = _unfold(vcard_text)
|
||||||
|
return {
|
||||||
|
"uid": (_values(txt, "UID") or [""])[0],
|
||||||
|
"fn": (_values(txt, "FN") or [""])[0],
|
||||||
|
"tels": _dedup(_values(txt, "TEL")),
|
||||||
|
"emails": _dedup(_values(txt, "EMAIL")),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def parse_ical_events(ical_text: str) -> list:
|
||||||
|
"""Extrae los VEVENT de un VCALENDAR para la tabla events.
|
||||||
|
|
||||||
|
Devuelve una lista de dicts {uid, dtstart, dtend, all_day, summary,
|
||||||
|
location, rrule, raw}. dtstart/dtend se normalizan a ISO básico
|
||||||
|
(YYYY-MM-DD o YYYY-MM-DDTHH:MM:SS, conservando la Z de UTC si la trae).
|
||||||
|
all_day es True cuando DTSTART es VALUE=DATE (sin componente de hora).
|
||||||
|
"""
|
||||||
|
events = []
|
||||||
|
txt = _unfold(ical_text)
|
||||||
|
for block in re.findall(
|
||||||
|
r"BEGIN:VEVENT(.*?)END:VEVENT", txt, re.DOTALL | re.IGNORECASE
|
||||||
|
):
|
||||||
|
dtstart_line = _prop_line(block, "DTSTART")
|
||||||
|
dtstart_raw = (_values(block, "DTSTART") or [""])[0]
|
||||||
|
all_day = bool(
|
||||||
|
re.search(r";VALUE=DATE(?:;|:)", dtstart_line, re.IGNORECASE)
|
||||||
|
) or bool(re.fullmatch(r"\d{8}", dtstart_raw))
|
||||||
|
events.append(
|
||||||
|
{
|
||||||
|
"uid": (_values(block, "UID") or [""])[0],
|
||||||
|
"dtstart": _norm_dt(dtstart_raw),
|
||||||
|
"dtend": _norm_dt((_values(block, "DTEND") or [""])[0]),
|
||||||
|
"all_day": all_day,
|
||||||
|
"summary": (_values(block, "SUMMARY") or [""])[0],
|
||||||
|
"location": (_values(block, "LOCATION") or [None])[0],
|
||||||
|
"rrule": (_values(block, "RRULE") or [None])[0],
|
||||||
|
"raw": "BEGIN:VEVENT" + block + "END:VEVENT",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return events
|
||||||
|
|
||||||
|
|
||||||
|
def _norm_dt(value: str) -> str:
|
||||||
|
"""Normaliza un DATE/DATE-TIME de iCalendar a ISO legible.
|
||||||
|
|
||||||
|
20260115 -> 2026-01-15; 20260115T093000Z -> 2026-01-15T09:30:00Z. Valores
|
||||||
|
ya ISO o vacíos se devuelven tal cual.
|
||||||
|
"""
|
||||||
|
if not value:
|
||||||
|
return ""
|
||||||
|
m = re.fullmatch(r"(\d{4})(\d{2})(\d{2})", value)
|
||||||
|
if m:
|
||||||
|
return f"{m.group(1)}-{m.group(2)}-{m.group(3)}"
|
||||||
|
m = re.fullmatch(r"(\d{4})(\d{2})(\d{2})T(\d{2})(\d{2})(\d{2})(Z?)", value)
|
||||||
|
if m:
|
||||||
|
return (
|
||||||
|
f"{m.group(1)}-{m.group(2)}-{m.group(3)}"
|
||||||
|
f"T{m.group(4)}:{m.group(5)}:{m.group(6)}{m.group(7)}"
|
||||||
|
)
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _dedup(items: list) -> list:
|
||||||
|
"""Deduplica una lista de strings preservando el orden (case-insensitive)."""
|
||||||
|
seen, out = set(), []
|
||||||
|
for it in items:
|
||||||
|
key = str(it).strip().lower()
|
||||||
|
if key and key not in seen:
|
||||||
|
seen.add(key)
|
||||||
|
out.append(str(it).strip())
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def norm_phone(p) -> str:
|
||||||
|
"""Normaliza un teléfono a sus últimos 9 dígitos (número nacional ES)."""
|
||||||
|
if not p:
|
||||||
|
return ""
|
||||||
|
d = re.sub(r"\D", "", str(p))
|
||||||
|
return d[-9:] if len(d) >= 9 else d
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
"""Capa de acceso a la base DuckDB: single-writer + migraciones.
|
||||||
|
|
||||||
|
Single-writer: SOLO este service escribe osint.duckdb. DuckDB bloquea el
|
||||||
|
archivo a un escritor exclusivo, así que la conexión de escritura se abre bajo
|
||||||
|
demanda (migraciones, ingest, rebuild de derivadas), serializada con un lock de
|
||||||
|
proceso, y se cierra inmediatamente al terminar. Así, fuera de una escritura en
|
||||||
|
curso, las lecturas de /api/query pueden abrir su propia conexión read_only via
|
||||||
|
duckdb_query_readonly sin conflicto de lock.
|
||||||
|
|
||||||
|
Migraciones (regla db_migrations adaptada a DuckDB): archivos numerados
|
||||||
|
migrations/NNN_*.sql, aditivos e idempotentes, aplicados en orden al arrancar.
|
||||||
|
La tabla _migrations registra cuáles ya se aplicaron para no repetirlas.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import os
|
||||||
|
import threading
|
||||||
|
from contextlib import contextmanager
|
||||||
|
|
||||||
|
import duckdb
|
||||||
|
|
||||||
|
# Lock de proceso: serializa todas las aperturas de la conexión de escritura.
|
||||||
|
_WRITE_LOCK = threading.Lock()
|
||||||
|
|
||||||
|
MIGRATIONS_DIR = os.path.join(
|
||||||
|
os.path.dirname(os.path.dirname(os.path.abspath(__file__))), "migrations"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@contextmanager
|
||||||
|
def write_conn(db_path: str):
|
||||||
|
"""Abre la conexión de escritura del service de forma exclusiva.
|
||||||
|
|
||||||
|
Context manager: adquiere el lock de proceso, abre DuckDB en lectura y
|
||||||
|
escritura, cede la conexión y la cierra siempre al salir. Crea el
|
||||||
|
directorio padre del archivo si no existe.
|
||||||
|
"""
|
||||||
|
parent = os.path.dirname(os.path.abspath(db_path))
|
||||||
|
if parent:
|
||||||
|
os.makedirs(parent, exist_ok=True)
|
||||||
|
with _WRITE_LOCK:
|
||||||
|
conn = duckdb.connect(db_path)
|
||||||
|
try:
|
||||||
|
yield conn
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
|
||||||
|
|
||||||
|
def apply_migrations(db_path: str) -> list:
|
||||||
|
"""Aplica las migraciones pendientes en orden y devuelve las aplicadas.
|
||||||
|
|
||||||
|
Lee migrations/NNN_*.sql ordenados por nombre, salta las que ya están
|
||||||
|
registradas en _migrations y ejecuta el resto dentro de la conexión de
|
||||||
|
escritura. Idempotente: una segunda llamada no aplica nada.
|
||||||
|
"""
|
||||||
|
applied: list = []
|
||||||
|
files = sorted(glob.glob(os.path.join(MIGRATIONS_DIR, "*.sql")))
|
||||||
|
with write_conn(db_path) as conn:
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE IF NOT EXISTS _migrations ("
|
||||||
|
"name TEXT PRIMARY KEY, applied_at TIMESTAMP DEFAULT now())"
|
||||||
|
)
|
||||||
|
done = {row[0] for row in conn.execute("SELECT name FROM _migrations").fetchall()}
|
||||||
|
for path in files:
|
||||||
|
name = os.path.basename(path)
|
||||||
|
if name in done:
|
||||||
|
continue
|
||||||
|
with open(path, "r", encoding="utf-8") as fh:
|
||||||
|
sql = fh.read()
|
||||||
|
conn.execute("BEGIN")
|
||||||
|
try:
|
||||||
|
conn.execute(sql)
|
||||||
|
conn.execute("INSERT INTO _migrations (name) VALUES (?)", [name])
|
||||||
|
conn.execute("COMMIT")
|
||||||
|
except Exception:
|
||||||
|
conn.execute("ROLLBACK")
|
||||||
|
raise
|
||||||
|
applied.append(name)
|
||||||
|
return applied
|
||||||
|
|
||||||
|
|
||||||
|
def list_tables(db_path: str) -> list:
|
||||||
|
"""Inventario de tablas para /api/tables.
|
||||||
|
|
||||||
|
Devuelve una lista de dicts {schema, name, kind, row_count, columns} para
|
||||||
|
las tablas de los schemas main y derived (excluye _migrations). kind es
|
||||||
|
'master' para main y 'derived' para derived.
|
||||||
|
"""
|
||||||
|
out: list = []
|
||||||
|
conn = duckdb.connect(db_path, read_only=True)
|
||||||
|
try:
|
||||||
|
tables = conn.execute(
|
||||||
|
"SELECT table_schema, table_name FROM information_schema.tables "
|
||||||
|
"WHERE table_schema IN ('main', 'derived') AND table_name != '_migrations' "
|
||||||
|
"ORDER BY table_schema, table_name"
|
||||||
|
).fetchall()
|
||||||
|
for schema, name in tables:
|
||||||
|
cols = conn.execute(
|
||||||
|
"SELECT column_name, data_type FROM information_schema.columns "
|
||||||
|
"WHERE table_schema = ? AND table_name = ? ORDER BY ordinal_position",
|
||||||
|
[schema, name],
|
||||||
|
).fetchall()
|
||||||
|
row_count = conn.execute(
|
||||||
|
f'SELECT COUNT(*) FROM "{schema}"."{name}"'
|
||||||
|
).fetchone()[0]
|
||||||
|
out.append(
|
||||||
|
{
|
||||||
|
"schema": schema,
|
||||||
|
"name": name,
|
||||||
|
"kind": "derived" if schema == "derived" else "master",
|
||||||
|
"row_count": row_count,
|
||||||
|
"columns": [{"name": c, "type": t} for c, t in cols],
|
||||||
|
}
|
||||||
|
)
|
||||||
|
finally:
|
||||||
|
conn.close()
|
||||||
|
return out
|
||||||
@@ -0,0 +1,83 @@
|
|||||||
|
"""Reconstrucción de las tablas derivadas (schema derived).
|
||||||
|
|
||||||
|
Regla dura: las derivadas contienen SOLO datos computados — ninguna lleva
|
||||||
|
columna que referencie notas (note_path prohibido aquí). Se reconstruyen
|
||||||
|
completas (DROP + CREATE) en cada ingest, así que su contenido siempre refleja
|
||||||
|
el último estado de las maestras.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
|
||||||
|
# Nombres de las derivadas que este módulo gestiona (para reportar en ingest).
|
||||||
|
DERIVED_TABLES = ("person_stats", "event_monthly", "contact_link_quality")
|
||||||
|
|
||||||
|
|
||||||
|
def rebuild_derived(conn) -> list:
|
||||||
|
"""Reconstruye todas las tablas derivadas sobre la conexión de escritura.
|
||||||
|
|
||||||
|
Devuelve la lista de nombres cualificados de las tablas reconstruidas.
|
||||||
|
"""
|
||||||
|
_rebuild_person_stats(conn)
|
||||||
|
_rebuild_event_monthly(conn)
|
||||||
|
_rebuild_contact_link_quality(conn)
|
||||||
|
return [f"derived.{name}" for name in DERIVED_TABLES]
|
||||||
|
|
||||||
|
|
||||||
|
def _rebuild_person_stats(conn) -> None:
|
||||||
|
"""Agregados de persons por contexto, pais y tag (sin note_path).
|
||||||
|
|
||||||
|
Una fila por (dimension, valor) con el conteo de personas. Los tags se
|
||||||
|
expanden en Python desde el JSON de la columna tags para no depender de
|
||||||
|
funciones JSON del motor.
|
||||||
|
"""
|
||||||
|
rows = conn.execute("SELECT contexto, pais, tags FROM persons").fetchall()
|
||||||
|
counts: dict = {}
|
||||||
|
for contexto, pais, tags_json in rows:
|
||||||
|
counts[("contexto", contexto or "(sin)")] = (
|
||||||
|
counts.get(("contexto", contexto or "(sin)"), 0) + 1
|
||||||
|
)
|
||||||
|
counts[("pais", pais or "(sin)")] = counts.get(("pais", pais or "(sin)"), 0) + 1
|
||||||
|
try:
|
||||||
|
tags = json.loads(tags_json) if tags_json else []
|
||||||
|
except (TypeError, ValueError):
|
||||||
|
tags = []
|
||||||
|
if not isinstance(tags, list):
|
||||||
|
tags = [tags]
|
||||||
|
for tag in tags:
|
||||||
|
key = ("tag", str(tag))
|
||||||
|
counts[key] = counts.get(key, 0) + 1
|
||||||
|
|
||||||
|
conn.execute("DROP TABLE IF EXISTS derived.person_stats")
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE derived.person_stats (dimension TEXT, valor TEXT, n BIGINT)"
|
||||||
|
)
|
||||||
|
payload = [[dim, val, n] for (dim, val), n in sorted(counts.items())]
|
||||||
|
if payload:
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO derived.person_stats VALUES (?, ?, ?)", payload
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rebuild_event_monthly(conn) -> None:
|
||||||
|
"""Conteo de eventos por calendario y mes (sin note_path)."""
|
||||||
|
conn.execute("DROP TABLE IF EXISTS derived.event_monthly")
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE derived.event_monthly AS "
|
||||||
|
"SELECT calendar, substr(dtstart, 1, 7) AS month, COUNT(*) AS n "
|
||||||
|
"FROM events WHERE dtstart IS NOT NULL "
|
||||||
|
"GROUP BY calendar, substr(dtstart, 1, 7) ORDER BY calendar, month"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _rebuild_contact_link_quality(conn) -> None:
|
||||||
|
"""Calidad del enlace contacts -> persons: solo números, sin paths."""
|
||||||
|
conn.execute("DROP TABLE IF EXISTS derived.contact_link_quality")
|
||||||
|
conn.execute(
|
||||||
|
"CREATE TABLE derived.contact_link_quality AS "
|
||||||
|
"SELECT COUNT(*) AS total, "
|
||||||
|
"COUNT(*) FILTER (WHERE note_path IS NOT NULL) AS linked, "
|
||||||
|
"COUNT(*) FILTER (WHERE note_path IS NULL) AS unlinked "
|
||||||
|
"FROM contacts"
|
||||||
|
)
|
||||||
@@ -0,0 +1,364 @@
|
|||||||
|
"""Ingests del service osint_db: vault Obsidian y servidor DAV (Xandikos).
|
||||||
|
|
||||||
|
Las tablas maestras se reconstruyen por reemplazo completo (DELETE + INSERT en
|
||||||
|
una transacción): el vault y Xandikos son las fuentes de verdad, así que cada
|
||||||
|
ingest deja la base exactamente como el origen. Tras cada ingest se re-enlazan
|
||||||
|
los contactos con sus fichas y se reconstruyen las tablas derivadas.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
from . import davparse
|
||||||
|
from .config import Config
|
||||||
|
from .db import write_conn
|
||||||
|
from .derived import rebuild_derived
|
||||||
|
from .registry_bridge import (
|
||||||
|
dav_get_collection,
|
||||||
|
dav_list_calendars,
|
||||||
|
list_obsidian_notes,
|
||||||
|
pass_get_secret,
|
||||||
|
read_obsidian_note,
|
||||||
|
)
|
||||||
|
|
||||||
|
def _norm(value):
|
||||||
|
"""Normaliza 'null'/''/None del frontmatter a None real."""
|
||||||
|
if value is None:
|
||||||
|
return None
|
||||||
|
if isinstance(value, str) and value.strip().lower() in ("null", "none", ""):
|
||||||
|
return None
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _as_str(value):
|
||||||
|
"""Convierte un valor de frontmatter a str (o None), sin perder números."""
|
||||||
|
v = _norm(value)
|
||||||
|
return None if v is None else str(v)
|
||||||
|
|
||||||
|
|
||||||
|
def _as_list(value) -> list:
|
||||||
|
"""Convierte un valor de frontmatter a lista (los escalares se envuelven)."""
|
||||||
|
v = _norm(value)
|
||||||
|
if v is None:
|
||||||
|
return []
|
||||||
|
return v if isinstance(v, list) else [v]
|
||||||
|
|
||||||
|
|
||||||
|
def _json(value) -> str:
|
||||||
|
"""Serializa un valor a JSON compacto (sin escapar acentos)."""
|
||||||
|
return json.dumps(value, ensure_ascii=False, default=str)
|
||||||
|
|
||||||
|
|
||||||
|
def _dav_uid_from_fuente(fuente) -> str | None:
|
||||||
|
"""Extrae el UID de Xandikos cuando fuente es 'Xandikos UID <uid>'."""
|
||||||
|
if not fuente:
|
||||||
|
return None
|
||||||
|
m = re.search(r"Xandikos UID\s+(\S+)", str(fuente))
|
||||||
|
return m.group(1) if m else None
|
||||||
|
|
||||||
|
|
||||||
|
def ingest_vault(cfg: Config) -> dict:
|
||||||
|
"""Escanea el vault completo y reconstruye notes + tablas de entidades.
|
||||||
|
|
||||||
|
Devuelve {status:'ok', notes:N, persons:N, organizations:N, domains:N,
|
||||||
|
cases:N, places:N, skipped_unreadable:N, derived_rebuilt:[...]}.
|
||||||
|
"""
|
||||||
|
if not os.path.isdir(cfg.vault_dir):
|
||||||
|
return {"status": "error", "error": f"vault no encontrado: {cfg.vault_dir}"}
|
||||||
|
|
||||||
|
note_rows: list = []
|
||||||
|
entity_rows: dict = {table: [] for _, _, table in cfg.entity_folders}
|
||||||
|
folder_to_table = {folder: table for folder, _, table in cfg.entity_folders}
|
||||||
|
skipped = 0
|
||||||
|
|
||||||
|
for abs_path in list_obsidian_notes(cfg.vault_dir):
|
||||||
|
rel_path = os.path.relpath(abs_path, cfg.vault_dir)
|
||||||
|
base = os.path.splitext(os.path.basename(abs_path))[0]
|
||||||
|
try:
|
||||||
|
note = read_obsidian_note(abs_path)
|
||||||
|
except Exception: # noqa: BLE001 — una nota corrupta no aborta el ingest
|
||||||
|
skipped += 1
|
||||||
|
continue
|
||||||
|
fm = note.get("frontmatter") or {}
|
||||||
|
mtime = datetime.fromtimestamp(os.path.getmtime(abs_path), tz=timezone.utc)
|
||||||
|
slug = _as_str(fm.get("slug")) or base
|
||||||
|
note_rows.append(
|
||||||
|
[
|
||||||
|
rel_path,
|
||||||
|
slug,
|
||||||
|
_as_str(fm.get("tipo")),
|
||||||
|
_as_str(fm.get("nombre")) or base,
|
||||||
|
mtime,
|
||||||
|
_json(fm),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
# Entidad estructurada: ficha de nivel-1 dentro de una carpeta de
|
||||||
|
# entidades (personas/<slug>.md, no personas/<slug>/<doc>.md) y que
|
||||||
|
# no sea una nota de soporte (prefijo _).
|
||||||
|
top_folder = rel_path.split(os.sep)[0]
|
||||||
|
is_level1 = os.path.basename(os.path.dirname(abs_path)) == top_folder
|
||||||
|
if top_folder in folder_to_table and is_level1 and not base.startswith("_"):
|
||||||
|
table = folder_to_table[top_folder]
|
||||||
|
if table == "persons":
|
||||||
|
entity_rows[table].append(
|
||||||
|
[
|
||||||
|
slug,
|
||||||
|
rel_path,
|
||||||
|
_as_str(fm.get("nombre")) or base,
|
||||||
|
_json(_as_list(fm.get("aliases"))),
|
||||||
|
_as_str(fm.get("sexo")),
|
||||||
|
_as_str(fm.get("fecha_nacimiento")),
|
||||||
|
_as_str(fm.get("dni")),
|
||||||
|
_as_str(fm.get("telefono")),
|
||||||
|
_as_str(fm.get("email")),
|
||||||
|
_as_str(fm.get("direccion")),
|
||||||
|
_as_str(fm.get("pais")),
|
||||||
|
_as_str(fm.get("contexto")),
|
||||||
|
_as_str(fm.get("fuente")),
|
||||||
|
_dav_uid_from_fuente(fm.get("fuente")),
|
||||||
|
_json(_as_list(fm.get("tags"))),
|
||||||
|
mtime,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
entity_rows[table].append(
|
||||||
|
[
|
||||||
|
slug,
|
||||||
|
rel_path,
|
||||||
|
_as_str(fm.get("nombre")) or base,
|
||||||
|
_json(_as_list(fm.get("tags"))),
|
||||||
|
_json(fm),
|
||||||
|
mtime,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
derived_rebuilt: list = []
|
||||||
|
with write_conn(cfg.db_path) as conn:
|
||||||
|
conn.execute("BEGIN")
|
||||||
|
try:
|
||||||
|
conn.execute("DELETE FROM notes")
|
||||||
|
if note_rows:
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO notes VALUES (?, ?, ?, ?, ?, ?)", note_rows
|
||||||
|
)
|
||||||
|
conn.execute("DELETE FROM persons")
|
||||||
|
if entity_rows["persons"]:
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO persons VALUES "
|
||||||
|
"(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
_dedup_by_slug(entity_rows["persons"]),
|
||||||
|
)
|
||||||
|
for table in ("organizations", "domains", "cases", "places"):
|
||||||
|
conn.execute(f"DELETE FROM {table}")
|
||||||
|
if entity_rows[table]:
|
||||||
|
conn.executemany(
|
||||||
|
f"INSERT INTO {table} VALUES (?, ?, ?, ?, ?, ?)",
|
||||||
|
_dedup_by_slug(entity_rows[table]),
|
||||||
|
)
|
||||||
|
_link_contacts(conn)
|
||||||
|
conn.execute("COMMIT")
|
||||||
|
except Exception:
|
||||||
|
conn.execute("ROLLBACK")
|
||||||
|
raise
|
||||||
|
derived_rebuilt = rebuild_derived(conn)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"notes": len(note_rows),
|
||||||
|
"persons": len(_dedup_by_slug(entity_rows["persons"])),
|
||||||
|
"organizations": len(_dedup_by_slug(entity_rows["organizations"])),
|
||||||
|
"domains": len(_dedup_by_slug(entity_rows["domains"])),
|
||||||
|
"cases": len(_dedup_by_slug(entity_rows["cases"])),
|
||||||
|
"places": len(_dedup_by_slug(entity_rows["places"])),
|
||||||
|
"skipped_unreadable": skipped,
|
||||||
|
"derived_rebuilt": derived_rebuilt,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _dedup_by_slug(rows: list) -> list:
|
||||||
|
"""Quita filas con slug repetido (gana la primera) para respetar la PK."""
|
||||||
|
seen, out = set(), []
|
||||||
|
for row in rows:
|
||||||
|
if row[0] in seen:
|
||||||
|
continue
|
||||||
|
seen.add(row[0])
|
||||||
|
out.append(row)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def ingest_dav(cfg: Config) -> dict:
|
||||||
|
"""Baja las colecciones de Xandikos y reconstruye contacts + events.
|
||||||
|
|
||||||
|
Devuelve {status:'ok', contacts:N, events:N, calendars:[...],
|
||||||
|
contacts_linked:N, derived_rebuilt:[...]} o {status:'error', error}.
|
||||||
|
"""
|
||||||
|
secret = pass_get_secret(cfg.pass_secret)
|
||||||
|
if secret.get("status") != "ok":
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": f"pass no devolvió el secreto {cfg.pass_secret!r}: "
|
||||||
|
f"{secret.get('error')}",
|
||||||
|
}
|
||||||
|
pwd = secret["value"] # sensible: nunca logear
|
||||||
|
|
||||||
|
coll = dav_get_collection(
|
||||||
|
cfg.dav_base, cfg.dav_user, pwd, cfg.dav_contacts_collection, "vcard"
|
||||||
|
)
|
||||||
|
if coll.get("status") != "ok":
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": f"CardDAV: {coll.get('error')} (http {coll.get('http_status')})",
|
||||||
|
}
|
||||||
|
|
||||||
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
contact_rows: list = []
|
||||||
|
seen_uids: set = set()
|
||||||
|
for res in coll.get("resources", []):
|
||||||
|
parsed = davparse.parse_vcard(res.get("data", ""))
|
||||||
|
uid = parsed["uid"] or os.path.splitext(os.path.basename(res["href"]))[0]
|
||||||
|
if uid in seen_uids:
|
||||||
|
continue
|
||||||
|
seen_uids.add(uid)
|
||||||
|
contact_rows.append(
|
||||||
|
[
|
||||||
|
uid,
|
||||||
|
cfg.dav_contacts_collection,
|
||||||
|
res.get("etag"),
|
||||||
|
parsed["fn"] or None,
|
||||||
|
_json(parsed["tels"]),
|
||||||
|
_json(parsed["emails"]),
|
||||||
|
res.get("data", ""),
|
||||||
|
None, # note_path se rellena en el enlace posterior
|
||||||
|
now,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
cals = dav_list_calendars(cfg.dav_base, cfg.dav_user, pwd, cfg.dav_calendar_home)
|
||||||
|
if cals.get("status") != "ok":
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": f"CalDAV: {cals.get('error')} (http {cals.get('http_status')})",
|
||||||
|
}
|
||||||
|
|
||||||
|
event_rows: list = []
|
||||||
|
seen_event_uids: set = set()
|
||||||
|
calendar_names: list = []
|
||||||
|
for cal in cals.get("calendars", []):
|
||||||
|
cal_name = cal.get("name") or cal.get("href", "").strip("/").rsplit("/", 1)[-1]
|
||||||
|
calendar_names.append(cal_name)
|
||||||
|
cal_coll = dav_get_collection(
|
||||||
|
cfg.dav_base, cfg.dav_user, pwd, cal["href"], "ical"
|
||||||
|
)
|
||||||
|
if cal_coll.get("status") != "ok":
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": f"CalDAV {cal_name}: {cal_coll.get('error')} "
|
||||||
|
f"(http {cal_coll.get('http_status')})",
|
||||||
|
}
|
||||||
|
for res in cal_coll.get("resources", []):
|
||||||
|
for ev in davparse.parse_ical_events(res.get("data", "")):
|
||||||
|
uid = ev["uid"] or os.path.splitext(os.path.basename(res["href"]))[0]
|
||||||
|
if uid in seen_event_uids:
|
||||||
|
continue
|
||||||
|
seen_event_uids.add(uid)
|
||||||
|
event_rows.append(
|
||||||
|
[
|
||||||
|
uid,
|
||||||
|
cal_name,
|
||||||
|
res.get("etag"),
|
||||||
|
ev["dtstart"] or None,
|
||||||
|
ev["dtend"] or None,
|
||||||
|
ev["all_day"],
|
||||||
|
ev["summary"] or None,
|
||||||
|
ev["location"],
|
||||||
|
ev["rrule"],
|
||||||
|
ev["raw"],
|
||||||
|
now,
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
with write_conn(cfg.db_path) as conn:
|
||||||
|
conn.execute("BEGIN")
|
||||||
|
try:
|
||||||
|
conn.execute("DELETE FROM contacts")
|
||||||
|
if contact_rows:
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
contact_rows,
|
||||||
|
)
|
||||||
|
conn.execute("DELETE FROM events")
|
||||||
|
if event_rows:
|
||||||
|
conn.executemany(
|
||||||
|
"INSERT INTO events VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
event_rows,
|
||||||
|
)
|
||||||
|
linked = _link_contacts(conn)
|
||||||
|
conn.execute("COMMIT")
|
||||||
|
except Exception:
|
||||||
|
conn.execute("ROLLBACK")
|
||||||
|
raise
|
||||||
|
derived_rebuilt = rebuild_derived(conn)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"contacts": len(contact_rows),
|
||||||
|
"events": len(event_rows),
|
||||||
|
"calendars": calendar_names,
|
||||||
|
"contacts_linked": linked,
|
||||||
|
"derived_rebuilt": derived_rebuilt,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _link_contacts(conn) -> int:
|
||||||
|
"""Enlaza contacts.note_path contra las fichas de persons.
|
||||||
|
|
||||||
|
Orden de matching por fiabilidad: UID estilo osint-<slug> (creado por el
|
||||||
|
push del vault), dav_uid registrado en la ficha, teléfono normalizado y
|
||||||
|
por último email. Devuelve el número de contactos enlazados.
|
||||||
|
"""
|
||||||
|
persons = conn.execute(
|
||||||
|
"SELECT slug, note_path, telefono, email, dav_uid FROM persons"
|
||||||
|
).fetchall()
|
||||||
|
by_slug, by_dav_uid, by_phone, by_email = {}, {}, {}, {}
|
||||||
|
for slug, note_path, telefono, email, dav_uid in persons:
|
||||||
|
by_slug[slug] = note_path
|
||||||
|
if dav_uid:
|
||||||
|
by_dav_uid.setdefault(dav_uid, note_path)
|
||||||
|
if telefono:
|
||||||
|
key = davparse.norm_phone(telefono)
|
||||||
|
if key:
|
||||||
|
by_phone.setdefault(key, note_path)
|
||||||
|
if email:
|
||||||
|
by_email.setdefault(str(email).strip().lower(), note_path)
|
||||||
|
|
||||||
|
contacts = conn.execute("SELECT uid, tels, emails FROM contacts").fetchall()
|
||||||
|
linked = 0
|
||||||
|
for uid, tels_json, emails_json in contacts:
|
||||||
|
note_path = None
|
||||||
|
if uid.startswith("osint-") and uid[len("osint-"):] in by_slug:
|
||||||
|
note_path = by_slug[uid[len("osint-"):]]
|
||||||
|
if note_path is None and uid in by_dav_uid:
|
||||||
|
note_path = by_dav_uid[uid]
|
||||||
|
if note_path is None:
|
||||||
|
for tel in json.loads(tels_json or "[]"):
|
||||||
|
hit = by_phone.get(davparse.norm_phone(tel))
|
||||||
|
if hit:
|
||||||
|
note_path = hit
|
||||||
|
break
|
||||||
|
if note_path is None:
|
||||||
|
for em in json.loads(emails_json or "[]"):
|
||||||
|
hit = by_email.get(str(em).strip().lower())
|
||||||
|
if hit:
|
||||||
|
note_path = hit
|
||||||
|
break
|
||||||
|
if note_path is not None:
|
||||||
|
conn.execute(
|
||||||
|
"UPDATE contacts SET note_path = ? WHERE uid = ?", [note_path, uid]
|
||||||
|
)
|
||||||
|
linked += 1
|
||||||
|
return linked
|
||||||
+271
@@ -0,0 +1,271 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Service FastAPI osint_db: dueño único de la base DuckDB data/osint.duckdb.
|
||||||
|
|
||||||
|
La base es la fuente de verdad estructurada del ecosistema OSINT: índice del
|
||||||
|
vault de Obsidian (notes + entidades con note_path), maestras DAV importadas de
|
||||||
|
Xandikos (contacts, events) y derivadas computadas (schema derived, sin
|
||||||
|
referencias a notas). Solo este proceso escribe la base; las lecturas de
|
||||||
|
/api/query usan una conexión read_only separada (duckdb_query_readonly).
|
||||||
|
|
||||||
|
Registry-first: el service NO reimplementa parseo de Markdown, protocolo DAV,
|
||||||
|
acceso a pass, ejecución read-only de DuckDB, render de tablas Markdown ni
|
||||||
|
bloques sentinel — importa esas funciones del registry (server/registry_bridge.py)
|
||||||
|
y solo aporta la lógica propia del dominio (mapeo vault→tablas, matching
|
||||||
|
contacto→ficha, derivadas y la API).
|
||||||
|
|
||||||
|
Seguridad: el vault contiene datos personales sensibles, así que el server
|
||||||
|
escucha SOLO en 127.0.0.1 y /api/query es estrictamente de solo lectura.
|
||||||
|
|
||||||
|
Uso:
|
||||||
|
.venv/bin/python server/main.py --vault ~/Obsidian/osint --port 8771
|
||||||
|
|
||||||
|
Contrato (el plugin de Obsidian parsea el body, no el código HTTP: los
|
||||||
|
endpoints de datos responden SIEMPRE 200 con status ok|error en el body):
|
||||||
|
GET /api/health estado + db_path + número de tablas
|
||||||
|
GET /api/tables inventario de tablas (schema, kind, filas, columnas)
|
||||||
|
POST /api/query SELECT arbitrario read-only {sql, params, max_rows}
|
||||||
|
GET /api/queries catálogo de queries con nombre
|
||||||
|
POST /api/query/named ejecuta una query del catálogo {name, max_rows}
|
||||||
|
POST /api/ingest/vault re-escanea el vault y reconstruye maestras+derivadas
|
||||||
|
POST /api/ingest/dav baja Xandikos y reconstruye contacts/events+derivadas
|
||||||
|
POST /api/render/note ejecuta query y la upserta como bloque sentinel en una nota
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
# Permite ejecutar tanto `python server/main.py` como importar `server.main`.
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from fastapi import FastAPI # noqa: E402
|
||||||
|
from pydantic import BaseModel, Field # noqa: E402
|
||||||
|
|
||||||
|
from server.config import SENTINEL_MARKER, Config # noqa: E402
|
||||||
|
from server.db import apply_migrations, list_tables # noqa: E402
|
||||||
|
from server.ingest import ingest_dav, ingest_vault # noqa: E402
|
||||||
|
from server.named_queries import NAMED_QUERIES # noqa: E402
|
||||||
|
from server.registry_bridge import ( # noqa: E402
|
||||||
|
create_obsidian_note,
|
||||||
|
duckdb_query_readonly,
|
||||||
|
read_obsidian_note,
|
||||||
|
render_markdown_table,
|
||||||
|
update_obsidian_note,
|
||||||
|
upsert_sentinel_block,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Tope de filas que un render vuelca en una nota (las notas no son un export).
|
||||||
|
RENDER_MAX_ROWS = 200
|
||||||
|
|
||||||
|
|
||||||
|
class QueryBody(BaseModel):
|
||||||
|
"""Body de POST /api/query."""
|
||||||
|
|
||||||
|
sql: str
|
||||||
|
params: list = Field(default_factory=list)
|
||||||
|
max_rows: int = 500
|
||||||
|
|
||||||
|
|
||||||
|
class NamedQueryBody(BaseModel):
|
||||||
|
"""Body de POST /api/query/named."""
|
||||||
|
|
||||||
|
name: str
|
||||||
|
max_rows: int = 500
|
||||||
|
|
||||||
|
|
||||||
|
class RenderNoteBody(BaseModel):
|
||||||
|
"""Body de POST /api/render/note. Exactamente uno de sql|query."""
|
||||||
|
|
||||||
|
note_path: str
|
||||||
|
block_id: str
|
||||||
|
sql: str | None = None
|
||||||
|
query: str | None = None
|
||||||
|
title: str | None = None
|
||||||
|
|
||||||
|
|
||||||
|
def create_app(cfg: Config) -> FastAPI:
|
||||||
|
"""Construye la app FastAPI con la configuración dada (inyectable en tests)."""
|
||||||
|
app = FastAPI(title="osint_db", docs_url=None, redoc_url=None)
|
||||||
|
|
||||||
|
def run_readonly(sql: str, params: list, max_rows: int) -> dict:
|
||||||
|
"""Ejecuta un SELECT con la conexión read_only del registry, acotado."""
|
||||||
|
max_rows = max(1, min(int(max_rows), 10000))
|
||||||
|
return duckdb_query_readonly(cfg.db_path, sql, params or [], max_rows)
|
||||||
|
|
||||||
|
@app.get("/api/health")
|
||||||
|
def health() -> dict:
|
||||||
|
try:
|
||||||
|
tables = list_tables(cfg.db_path)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
return {"status": "error", "db_path": cfg.db_path, "error": str(e)}
|
||||||
|
return {"status": "ok", "db_path": cfg.db_path, "tables": len(tables)}
|
||||||
|
|
||||||
|
@app.get("/api/tables")
|
||||||
|
def tables() -> dict:
|
||||||
|
try:
|
||||||
|
return {"status": "ok", "tables": list_tables(cfg.db_path)}
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
@app.post("/api/query")
|
||||||
|
def query(body: QueryBody) -> dict:
|
||||||
|
return run_readonly(body.sql, body.params, body.max_rows)
|
||||||
|
|
||||||
|
@app.get("/api/queries")
|
||||||
|
def queries() -> dict:
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"queries": [
|
||||||
|
{"name": name, "description": q["description"], "sql": q["sql"]}
|
||||||
|
for name, q in NAMED_QUERIES.items()
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
@app.post("/api/query/named")
|
||||||
|
def query_named(body: NamedQueryBody) -> dict:
|
||||||
|
entry = NAMED_QUERIES.get(body.name)
|
||||||
|
if entry is None:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": f"query con nombre desconocida: {body.name!r} "
|
||||||
|
f"(disponibles: {', '.join(sorted(NAMED_QUERIES))})",
|
||||||
|
}
|
||||||
|
return run_readonly(entry["sql"], [], body.max_rows)
|
||||||
|
|
||||||
|
@app.post("/api/ingest/vault")
|
||||||
|
def api_ingest_vault() -> dict:
|
||||||
|
try:
|
||||||
|
return ingest_vault(cfg)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
@app.post("/api/ingest/dav")
|
||||||
|
def api_ingest_dav() -> dict:
|
||||||
|
try:
|
||||||
|
return ingest_dav(cfg)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
@app.post("/api/render/note")
|
||||||
|
def render_note(body: RenderNoteBody) -> dict:
|
||||||
|
# Resolver el SQL: o viene literal, o por nombre del catálogo.
|
||||||
|
if bool(body.sql) == bool(body.query):
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": "indica exactamente uno de los campos 'sql' o 'query' (named)",
|
||||||
|
}
|
||||||
|
if body.query:
|
||||||
|
entry = NAMED_QUERIES.get(body.query)
|
||||||
|
if entry is None:
|
||||||
|
return {
|
||||||
|
"status": "error",
|
||||||
|
"error": f"query con nombre desconocida: {body.query!r}",
|
||||||
|
}
|
||||||
|
sql = entry["sql"]
|
||||||
|
else:
|
||||||
|
sql = body.sql
|
||||||
|
|
||||||
|
result = run_readonly(sql, [], RENDER_MAX_ROWS)
|
||||||
|
if result.get("status") != "ok":
|
||||||
|
return result
|
||||||
|
|
||||||
|
table_md = render_markdown_table(
|
||||||
|
result["rows"], columns=result["columns"], max_rows=RENDER_MAX_ROWS
|
||||||
|
)
|
||||||
|
content = table_md if table_md else "_(sin filas)_"
|
||||||
|
if body.title:
|
||||||
|
content = f"### {body.title}\n\n{content}"
|
||||||
|
|
||||||
|
# La nota se referencia por path relativo al vault; el path no puede
|
||||||
|
# escapar del vault (mismo criterio anti-traversal que osint_web).
|
||||||
|
rel = body.note_path if body.note_path.endswith(".md") else body.note_path + ".md"
|
||||||
|
abs_path = os.path.abspath(os.path.join(cfg.vault_dir, rel))
|
||||||
|
vault_real = os.path.realpath(cfg.vault_dir)
|
||||||
|
if not os.path.realpath(abs_path).startswith(vault_real + os.sep):
|
||||||
|
return {"status": "error", "error": f"note_path fuera del vault: {body.note_path!r}"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
if os.path.exists(abs_path):
|
||||||
|
note = read_obsidian_note(abs_path)
|
||||||
|
new_body = upsert_sentinel_block(
|
||||||
|
note.get("body", "") or "",
|
||||||
|
body.block_id,
|
||||||
|
content,
|
||||||
|
marker=SENTINEL_MARKER,
|
||||||
|
)
|
||||||
|
update_obsidian_note(abs_path, body=new_body)
|
||||||
|
else:
|
||||||
|
new_body = upsert_sentinel_block(
|
||||||
|
"", body.block_id, content, marker=SENTINEL_MARKER
|
||||||
|
)
|
||||||
|
create_obsidian_note(
|
||||||
|
cfg.vault_dir,
|
||||||
|
rel,
|
||||||
|
body=new_body,
|
||||||
|
frontmatter={"tipo": "tablero", "tags": ["osintdb"]},
|
||||||
|
)
|
||||||
|
except Exception as e: # noqa: BLE001
|
||||||
|
return {"status": "error", "error": str(e)}
|
||||||
|
|
||||||
|
return {
|
||||||
|
"status": "ok",
|
||||||
|
"note_path": rel,
|
||||||
|
"rows_rendered": result["row_count"],
|
||||||
|
}
|
||||||
|
|
||||||
|
return app
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> int:
|
||||||
|
"""Punto de entrada CLI: valida el vault, migra la base y arranca uvicorn."""
|
||||||
|
parser = argparse.ArgumentParser(
|
||||||
|
description="Service osint_db: DuckDB fuente de verdad del ecosistema OSINT."
|
||||||
|
)
|
||||||
|
parser.add_argument("--vault", default=None, help="directorio del vault Obsidian")
|
||||||
|
parser.add_argument("--db", default=None, help="ruta del archivo osint.duckdb")
|
||||||
|
parser.add_argument("--port", type=int, default=None, help="puerto de escucha")
|
||||||
|
parser.add_argument(
|
||||||
|
"--host",
|
||||||
|
default="127.0.0.1",
|
||||||
|
help="host de escucha (datos sensibles: déjalo en 127.0.0.1)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
cfg = Config()
|
||||||
|
if args.vault:
|
||||||
|
cfg.vault_dir = os.path.expanduser(args.vault)
|
||||||
|
if args.db:
|
||||||
|
cfg.db_path = os.path.expanduser(args.db)
|
||||||
|
if args.port is not None:
|
||||||
|
cfg.port = args.port
|
||||||
|
cfg.host = args.host
|
||||||
|
|
||||||
|
if not os.path.isdir(cfg.vault_dir):
|
||||||
|
print(
|
||||||
|
f"ERROR: el vault no existe o no es un directorio: {cfg.vault_dir}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 2
|
||||||
|
|
||||||
|
if cfg.host != "127.0.0.1":
|
||||||
|
print(
|
||||||
|
"AVISO: la base contiene datos personales sensibles; escuchar fuera "
|
||||||
|
"de 127.0.0.1 no está soportado.",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
|
||||||
|
applied = apply_migrations(cfg.db_path)
|
||||||
|
if applied:
|
||||||
|
print(f"migraciones aplicadas: {', '.join(applied)}")
|
||||||
|
|
||||||
|
import uvicorn
|
||||||
|
|
||||||
|
app = create_app(cfg)
|
||||||
|
uvicorn.run(app, host=cfg.host, port=cfg.port, log_level="warning")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
sys.exit(main())
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
"""Catálogo de queries con nombre del service osint_db.
|
||||||
|
|
||||||
|
Cada entrada mapea un nombre estable a {sql, description}. El plugin de
|
||||||
|
Obsidian (y /api/render/note) las invoca por nombre via POST /api/query/named,
|
||||||
|
así el SQL vive en un solo sitio y el cliente no necesita conocer el schema.
|
||||||
|
Todas son SELECT puros: se ejecutan por duckdb_query_readonly.
|
||||||
|
"""
|
||||||
|
|
||||||
|
NAMED_QUERIES = {
|
||||||
|
"personas_por_contexto": {
|
||||||
|
"description": "Personas agrupadas por contexto (círculo de origen), de mayor a menor.",
|
||||||
|
"sql": (
|
||||||
|
"SELECT coalesce(contexto, '(sin)') AS contexto, COUNT(*) AS personas "
|
||||||
|
"FROM persons GROUP BY 1 ORDER BY personas DESC, contexto"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"personas_recientes": {
|
||||||
|
"description": "Últimas 50 fichas de persona tocadas en el vault (por mtime de la nota).",
|
||||||
|
"sql": (
|
||||||
|
"SELECT slug, nombre, contexto, note_path, updated_at "
|
||||||
|
"FROM persons ORDER BY updated_at DESC LIMIT 50"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"eventos_proximos": {
|
||||||
|
"description": "Próximos 50 eventos del calendario a partir de hoy, en orden cronológico.",
|
||||||
|
"sql": (
|
||||||
|
"SELECT uid, calendar, dtstart, dtend, all_day, summary, location "
|
||||||
|
"FROM events WHERE dtstart >= strftime(now(), '%Y-%m-%d') "
|
||||||
|
"ORDER BY dtstart LIMIT 50"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"contactos_sin_nota": {
|
||||||
|
"description": "Contactos CardDAV que no enlazan con ninguna ficha del vault (note_path IS NULL).",
|
||||||
|
"sql": (
|
||||||
|
"SELECT uid, fn, tels, emails FROM contacts "
|
||||||
|
"WHERE note_path IS NULL ORDER BY fn"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"stats_personas": {
|
||||||
|
"description": "Agregados de personas por dimensión (contexto, país, tag) desde derived.person_stats.",
|
||||||
|
"sql": (
|
||||||
|
"SELECT dimension, valor, n FROM derived.person_stats "
|
||||||
|
"ORDER BY dimension, n DESC, valor"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
"calidad_enlace_contactos": {
|
||||||
|
"description": "Resumen de contactos enlazados a persona vs sin enlazar (derived.contact_link_quality).",
|
||||||
|
"sql": "SELECT total, linked, unlinked FROM derived.contact_link_quality",
|
||||||
|
},
|
||||||
|
"eventos_por_mes": {
|
||||||
|
"description": "Conteo de eventos por calendario y mes (derived.event_monthly).",
|
||||||
|
"sql": (
|
||||||
|
"SELECT calendar, month, n FROM derived.event_monthly "
|
||||||
|
"ORDER BY month DESC, calendar"
|
||||||
|
),
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
"""Puente con el fn_registry: localiza python/functions y expone las funciones.
|
||||||
|
|
||||||
|
Registry-first: esta app NO reimplementa el parseo de notas Obsidian, el
|
||||||
|
protocolo DAV, el acceso a pass ni la ejecución read-only de DuckDB. Importa
|
||||||
|
las funciones del registry y las reexporta para el resto de módulos del
|
||||||
|
service. Toda función importada aquí está declarada en uses_functions del
|
||||||
|
app.md.
|
||||||
|
|
||||||
|
La localización de python/functions evita paths hardcodeados de usuario:
|
||||||
|
prueba las variables de entorno FN_REGISTRY_FUNCTIONS y FN_REGISTRY_ROOT,
|
||||||
|
después sube por los directorios padre de este archivo hasta encontrar una
|
||||||
|
raíz que contenga python/functions/obsidian (la app vive en
|
||||||
|
<root>/projects/osint/apps/osint_db/server/), y por último cae al layout
|
||||||
|
estándar del PC.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
|
||||||
|
|
||||||
|
def _registry_functions_dir() -> str:
|
||||||
|
"""Devuelve el directorio python/functions del fn_registry."""
|
||||||
|
env_functions = os.environ.get("FN_REGISTRY_FUNCTIONS")
|
||||||
|
if env_functions and os.path.isdir(os.path.join(env_functions, "obsidian")):
|
||||||
|
return env_functions
|
||||||
|
|
||||||
|
candidates: list[str] = []
|
||||||
|
env_root = os.environ.get("FN_REGISTRY_ROOT")
|
||||||
|
if env_root:
|
||||||
|
candidates.append(env_root)
|
||||||
|
current = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
while True:
|
||||||
|
candidates.append(current)
|
||||||
|
parent = os.path.dirname(current)
|
||||||
|
if parent == current:
|
||||||
|
break
|
||||||
|
current = parent
|
||||||
|
candidates.append(os.path.expanduser("~/fn_registry"))
|
||||||
|
for root in candidates:
|
||||||
|
functions_dir = os.path.join(root, "python", "functions")
|
||||||
|
if os.path.isdir(os.path.join(functions_dir, "obsidian")):
|
||||||
|
return functions_dir
|
||||||
|
raise RuntimeError(
|
||||||
|
"no se encontró python/functions/obsidian subiendo desde "
|
||||||
|
f"{os.path.abspath(__file__)}; define FN_REGISTRY_ROOT con la raíz "
|
||||||
|
"del fn_registry"
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
_FUNCTIONS_DIR = _registry_functions_dir()
|
||||||
|
if _FUNCTIONS_DIR not in sys.path:
|
||||||
|
sys.path.insert(0, _FUNCTIONS_DIR)
|
||||||
|
|
||||||
|
# Grupo obsidian: CRUD de notas del vault + slugs. El __init__ del paquete
|
||||||
|
# obsidian es ligero (sin dependencias pesadas), así que se importa directo.
|
||||||
|
from obsidian import ( # noqa: E402
|
||||||
|
create_obsidian_note,
|
||||||
|
list_obsidian_notes,
|
||||||
|
read_obsidian_note,
|
||||||
|
slugify_obsidian_name,
|
||||||
|
update_obsidian_note,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_registry_fn(package: str, module_name: str, attr: str):
|
||||||
|
"""Carga una función del registry por path, sin ejecutar el __init__ del paquete.
|
||||||
|
|
||||||
|
Los __init__ de los paquetes infra y core importan TODAS sus funciones, y
|
||||||
|
algunas arrastran dependencias pesadas (Pillow, etc.) que este service no
|
||||||
|
necesita. Cargamos el archivo concreto con importlib (mismo patrón que
|
||||||
|
osint_web). Sigue siendo registry-first: se usa la función del registry sin
|
||||||
|
reimplementarla, solo se importa de forma quirúrgica.
|
||||||
|
"""
|
||||||
|
import importlib.util
|
||||||
|
|
||||||
|
file_path = os.path.join(_FUNCTIONS_DIR, package, module_name + ".py")
|
||||||
|
spec = importlib.util.spec_from_file_location(
|
||||||
|
f"{package}_{module_name}", file_path
|
||||||
|
)
|
||||||
|
if spec is None or spec.loader is None: # pragma: no cover - defensivo
|
||||||
|
raise ImportError(f"no se pudo cargar {file_path}")
|
||||||
|
module = importlib.util.module_from_spec(spec)
|
||||||
|
spec.loader.exec_module(module)
|
||||||
|
return getattr(module, attr)
|
||||||
|
|
||||||
|
|
||||||
|
# Grupo dav: lectura bulk de colecciones Xandikos (CardDAV/CalDAV).
|
||||||
|
dav_get_collection = _load_registry_fn("infra", "dav_get_collection", "dav_get_collection")
|
||||||
|
dav_list_calendars = _load_registry_fn("infra", "dav_list_calendars", "dav_list_calendars")
|
||||||
|
dav_collection_ctag = _load_registry_fn("infra", "dav_collection_ctag", "dav_collection_ctag")
|
||||||
|
|
||||||
|
# Secretos via pass (credencial Xandikos, nunca hardcodeada).
|
||||||
|
pass_get_secret = _load_registry_fn("infra", "pass_get_secret", "pass_get_secret")
|
||||||
|
|
||||||
|
# Lectura read-only de DuckDB (la conexión de /api/query, separada del writer).
|
||||||
|
duckdb_query_readonly = _load_registry_fn(
|
||||||
|
"infra", "duckdb_query_readonly", "duckdb_query_readonly"
|
||||||
|
)
|
||||||
|
|
||||||
|
# Render de tablas Markdown + bloques sentinel idempotentes para las notas.
|
||||||
|
render_markdown_table = _load_registry_fn(
|
||||||
|
"core", "render_markdown_table", "render_markdown_table"
|
||||||
|
)
|
||||||
|
upsert_sentinel_block = _load_registry_fn(
|
||||||
|
"core", "upsert_sentinel_block", "upsert_sentinel_block"
|
||||||
|
)
|
||||||
|
|
||||||
|
__all__ = [
|
||||||
|
"create_obsidian_note",
|
||||||
|
"list_obsidian_notes",
|
||||||
|
"read_obsidian_note",
|
||||||
|
"slugify_obsidian_name",
|
||||||
|
"update_obsidian_note",
|
||||||
|
"dav_collection_ctag",
|
||||||
|
"dav_get_collection",
|
||||||
|
"dav_list_calendars",
|
||||||
|
"pass_get_secret",
|
||||||
|
"duckdb_query_readonly",
|
||||||
|
"render_markdown_table",
|
||||||
|
"upsert_sentinel_block",
|
||||||
|
]
|
||||||
@@ -0,0 +1,356 @@
|
|||||||
|
"""Tests del service osint_db: migraciones, ingest del vault, API y render.
|
||||||
|
|
||||||
|
Todo corre contra un vault temporal y una base DuckDB temporal, SIN red: el
|
||||||
|
ingest DAV no se ejercita aquí (requiere Xandikos + pass). El enlace
|
||||||
|
contacto→ficha sí se prueba insertando un contacto a mano y relanzando el
|
||||||
|
ingest del vault, que re-enlaza.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import os
|
||||||
|
import sys
|
||||||
|
from datetime import datetime, timezone
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
from fastapi.testclient import TestClient
|
||||||
|
|
||||||
|
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
|
||||||
|
|
||||||
|
from server.config import Config # noqa: E402
|
||||||
|
from server.db import apply_migrations, write_conn # noqa: E402
|
||||||
|
from server.main import create_app # noqa: E402
|
||||||
|
|
||||||
|
PERSONA_MD = """---
|
||||||
|
tipo: persona
|
||||||
|
nombre: "Ana García Pérez"
|
||||||
|
slug: ana-garcia-perez
|
||||||
|
aliases: ["Anita"]
|
||||||
|
sexo: mujer
|
||||||
|
fecha_nacimiento: 1990-04-12
|
||||||
|
dni: 12345678Z
|
||||||
|
telefono: "+34 600 111 222"
|
||||||
|
email: ana@example.com
|
||||||
|
direccion: null
|
||||||
|
pais: españa
|
||||||
|
relaciones: []
|
||||||
|
contexto: familia
|
||||||
|
fuente: "test fixture"
|
||||||
|
tags: [persona, osint]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
Ficha de prueba.
|
||||||
|
"""
|
||||||
|
|
||||||
|
PERSONA2_MD = """---
|
||||||
|
tipo: persona
|
||||||
|
nombre: "Luis Pérez"
|
||||||
|
slug: luis-perez
|
||||||
|
aliases: []
|
||||||
|
sexo: hombre
|
||||||
|
fecha_nacimiento: null
|
||||||
|
dni: null
|
||||||
|
telefono: null
|
||||||
|
email: null
|
||||||
|
direccion: null
|
||||||
|
pais: null
|
||||||
|
relaciones: []
|
||||||
|
contexto: movil
|
||||||
|
fuente: "Xandikos UID abc-123"
|
||||||
|
tags: [persona, osint, movil]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
"""
|
||||||
|
|
||||||
|
ORG_MD = """---
|
||||||
|
tipo: organizacion
|
||||||
|
nombre: "Acme S.L."
|
||||||
|
slug: acme-sl
|
||||||
|
tags: [organizacion, osint]
|
||||||
|
---
|
||||||
|
|
||||||
|
## Notas
|
||||||
|
"""
|
||||||
|
|
||||||
|
DOC_MD = """---
|
||||||
|
tipo: documento
|
||||||
|
doc_tipo: dni
|
||||||
|
---
|
||||||
|
|
||||||
|
Sub-nota de documento (NO debe contar como ficha de persona).
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def cfg(tmp_path):
|
||||||
|
"""Vault temporal con fichas de fixture + base DuckDB temporal migrada."""
|
||||||
|
vault = tmp_path / "vault"
|
||||||
|
(vault / "personas" / "ana-garcia-perez").mkdir(parents=True)
|
||||||
|
(vault / "organizaciones").mkdir()
|
||||||
|
(vault / "personas" / "ana-garcia-perez.md").write_text(
|
||||||
|
PERSONA_MD, encoding="utf-8"
|
||||||
|
)
|
||||||
|
(vault / "personas" / "luis-perez.md").write_text(PERSONA2_MD, encoding="utf-8")
|
||||||
|
(vault / "personas" / "_plantilla.md").write_text(
|
||||||
|
"---\ntipo: plantilla\n---\n", encoding="utf-8"
|
||||||
|
)
|
||||||
|
(vault / "personas" / "ana-garcia-perez" / "dni.md").write_text(
|
||||||
|
DOC_MD, encoding="utf-8"
|
||||||
|
)
|
||||||
|
(vault / "organizaciones" / "acme-sl.md").write_text(ORG_MD, encoding="utf-8")
|
||||||
|
|
||||||
|
config = Config(
|
||||||
|
vault_dir=str(vault),
|
||||||
|
db_path=str(tmp_path / "data" / "osint.duckdb"),
|
||||||
|
port=0,
|
||||||
|
)
|
||||||
|
apply_migrations(config.db_path)
|
||||||
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture()
|
||||||
|
def client(cfg):
|
||||||
|
return TestClient(create_app(cfg))
|
||||||
|
|
||||||
|
|
||||||
|
def test_migrations_son_idempotentes(cfg):
|
||||||
|
"""La segunda pasada de migraciones no aplica nada (tabla _migrations)."""
|
||||||
|
assert apply_migrations(cfg.db_path) == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_health(client, cfg):
|
||||||
|
r = client.get("/api/health").json()
|
||||||
|
assert r["status"] == "ok"
|
||||||
|
assert r["db_path"] == cfg.db_path
|
||||||
|
assert r["tables"] >= 8
|
||||||
|
|
||||||
|
|
||||||
|
def test_ingest_vault_cuenta_entidades(client):
|
||||||
|
r = client.post("/api/ingest/vault").json()
|
||||||
|
assert r["status"] == "ok"
|
||||||
|
# 5 notas: 2 personas + plantilla + sub-nota documento + organización.
|
||||||
|
assert r["notes"] == 5
|
||||||
|
# Solo las fichas de nivel-1 sin prefijo _ cuentan como persona.
|
||||||
|
assert r["persons"] == 2
|
||||||
|
assert r["organizations"] == 1
|
||||||
|
assert r["domains"] == 0
|
||||||
|
assert sorted(r["derived_rebuilt"]) == [
|
||||||
|
"derived.contact_link_quality",
|
||||||
|
"derived.event_monthly",
|
||||||
|
"derived.person_stats",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_ingest_vault_extrae_dav_uid_de_fuente(client):
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
r = client.post(
|
||||||
|
"/api/query",
|
||||||
|
json={"sql": "SELECT dav_uid FROM persons WHERE slug = 'luis-perez'"},
|
||||||
|
).json()
|
||||||
|
assert r["status"] == "ok"
|
||||||
|
assert r["rows"][0]["dav_uid"] == "abc-123"
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_query_ok_y_error_siempre_http_200(client):
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
ok = client.post(
|
||||||
|
"/api/query",
|
||||||
|
json={"sql": "SELECT slug, nombre FROM persons ORDER BY slug", "max_rows": 10},
|
||||||
|
)
|
||||||
|
assert ok.status_code == 200
|
||||||
|
body = ok.json()
|
||||||
|
assert body["status"] == "ok"
|
||||||
|
assert body["columns"] == ["slug", "nombre"]
|
||||||
|
assert body["row_count"] == 2
|
||||||
|
assert body["truncated"] is False
|
||||||
|
assert body["rows"][0]["slug"] == "ana-garcia-perez"
|
||||||
|
|
||||||
|
err = client.post("/api/query", json={"sql": "SELECT * FROM tabla_que_no_existe"})
|
||||||
|
assert err.status_code == 200
|
||||||
|
assert err.json()["status"] == "error"
|
||||||
|
assert err.json()["error"]
|
||||||
|
|
||||||
|
|
||||||
|
def test_api_query_es_solo_lectura(client):
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
r = client.post(
|
||||||
|
"/api/query", json={"sql": "DELETE FROM persons"}
|
||||||
|
).json()
|
||||||
|
assert r["status"] == "error"
|
||||||
|
|
||||||
|
|
||||||
|
def test_catalogo_de_queries_con_nombre(client):
|
||||||
|
r = client.get("/api/queries").json()
|
||||||
|
assert r["status"] == "ok"
|
||||||
|
names = {q["name"] for q in r["queries"]}
|
||||||
|
assert {
|
||||||
|
"personas_por_contexto",
|
||||||
|
"personas_recientes",
|
||||||
|
"eventos_proximos",
|
||||||
|
"contactos_sin_nota",
|
||||||
|
"stats_personas",
|
||||||
|
} <= names
|
||||||
|
assert all(q["sql"] and q["description"] for q in r["queries"])
|
||||||
|
|
||||||
|
|
||||||
|
def test_query_named_ok_y_desconocida(client):
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
r = client.post(
|
||||||
|
"/api/query/named", json={"name": "personas_por_contexto"}
|
||||||
|
).json()
|
||||||
|
assert r["status"] == "ok"
|
||||||
|
contextos = {row["contexto"]: row["personas"] for row in r["rows"]}
|
||||||
|
assert contextos == {"familia": 1, "movil": 1}
|
||||||
|
|
||||||
|
bad = client.post("/api/query/named", json={"name": "no_existe"}).json()
|
||||||
|
assert bad["status"] == "error"
|
||||||
|
|
||||||
|
|
||||||
|
def test_tables_inventario(client):
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
r = client.get("/api/tables").json()
|
||||||
|
assert r["status"] == "ok"
|
||||||
|
by_name = {(t["schema"], t["name"]): t for t in r["tables"]}
|
||||||
|
persons = by_name[("main", "persons")]
|
||||||
|
assert persons["kind"] == "master"
|
||||||
|
assert persons["row_count"] == 2
|
||||||
|
assert {"name": "note_path", "type": "VARCHAR"} in persons["columns"]
|
||||||
|
stats = by_name[("derived", "person_stats")]
|
||||||
|
assert stats["kind"] == "derived"
|
||||||
|
assert ("main", "_migrations") not in by_name
|
||||||
|
|
||||||
|
|
||||||
|
def test_derivadas_sin_note_path(client):
|
||||||
|
"""Regla dura: ninguna tabla del schema derived referencia notas."""
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
r = client.post(
|
||||||
|
"/api/query",
|
||||||
|
json={
|
||||||
|
"sql": (
|
||||||
|
"SELECT table_name, column_name FROM information_schema.columns "
|
||||||
|
"WHERE table_schema = 'derived' AND column_name LIKE '%note%'"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
).json()
|
||||||
|
assert r["status"] == "ok"
|
||||||
|
assert r["rows"] == []
|
||||||
|
# Y las tres derivadas existen de verdad.
|
||||||
|
t = client.post(
|
||||||
|
"/api/query",
|
||||||
|
json={
|
||||||
|
"sql": (
|
||||||
|
"SELECT table_name FROM information_schema.tables "
|
||||||
|
"WHERE table_schema = 'derived' ORDER BY table_name"
|
||||||
|
)
|
||||||
|
},
|
||||||
|
).json()
|
||||||
|
assert [row["table_name"] for row in t["rows"]] == [
|
||||||
|
"contact_link_quality",
|
||||||
|
"event_monthly",
|
||||||
|
"person_stats",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def test_link_contacts_por_telefono(client, cfg):
|
||||||
|
"""Un contacto con teléfono que casa con una ficha queda enlazado al re-ingestar."""
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
now = datetime.now(tz=timezone.utc)
|
||||||
|
with write_conn(cfg.db_path) as conn:
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
[
|
||||||
|
"uid-movil-1",
|
||||||
|
"/enmanuel/contacts/addressbook/",
|
||||||
|
"etag1",
|
||||||
|
"Ana G.",
|
||||||
|
'["600111222"]',
|
||||||
|
"[]",
|
||||||
|
"BEGIN:VCARD...",
|
||||||
|
None,
|
||||||
|
now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
conn.execute(
|
||||||
|
"INSERT INTO contacts VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)",
|
||||||
|
[
|
||||||
|
"uid-movil-2",
|
||||||
|
"/enmanuel/contacts/addressbook/",
|
||||||
|
"etag2",
|
||||||
|
"Desconocido",
|
||||||
|
'["699999999"]',
|
||||||
|
"[]",
|
||||||
|
"BEGIN:VCARD...",
|
||||||
|
None,
|
||||||
|
now,
|
||||||
|
],
|
||||||
|
)
|
||||||
|
# El ingest del vault re-enlaza contacts y reconstruye derivadas.
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
r = client.post(
|
||||||
|
"/api/query",
|
||||||
|
json={"sql": "SELECT uid, note_path FROM contacts ORDER BY uid"},
|
||||||
|
).json()
|
||||||
|
rows = {row["uid"]: row["note_path"] for row in r["rows"]}
|
||||||
|
assert rows["uid-movil-1"] == os.path.join("personas", "ana-garcia-perez.md")
|
||||||
|
assert rows["uid-movil-2"] is None
|
||||||
|
|
||||||
|
q = client.post("/api/query/named", json={"name": "contactos_sin_nota"}).json()
|
||||||
|
assert [row["uid"] for row in q["rows"]] == ["uid-movil-2"]
|
||||||
|
|
||||||
|
quality = client.post(
|
||||||
|
"/api/query/named", json={"name": "calidad_enlace_contactos"}
|
||||||
|
).json()
|
||||||
|
assert quality["rows"] == [{"total": 2, "linked": 1, "unlinked": 1}]
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_note_crea_bloque_sentinel_y_es_idempotente(client, cfg):
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
body = {
|
||||||
|
"note_path": "tableros/personas.md",
|
||||||
|
"block_id": "personas",
|
||||||
|
"query": "personas_por_contexto",
|
||||||
|
"title": "Personas por contexto",
|
||||||
|
}
|
||||||
|
r = client.post("/api/render/note", json=body).json()
|
||||||
|
assert r["status"] == "ok"
|
||||||
|
assert r["note_path"] == "tableros/personas.md"
|
||||||
|
assert r["rows_rendered"] == 2
|
||||||
|
|
||||||
|
note_file = os.path.join(cfg.vault_dir, "tableros", "personas.md")
|
||||||
|
content = open(note_file, encoding="utf-8").read()
|
||||||
|
assert "<!-- osintdb:begin id=personas -->" in content
|
||||||
|
assert "<!-- osintdb:end id=personas -->" in content
|
||||||
|
assert "### Personas por contexto" in content
|
||||||
|
assert "| contexto | personas |" in content
|
||||||
|
assert "| familia | 1 |" in content
|
||||||
|
|
||||||
|
# Idempotente: un segundo render no duplica el bloque ni la tabla.
|
||||||
|
r2 = client.post("/api/render/note", json=body).json()
|
||||||
|
assert r2["status"] == "ok"
|
||||||
|
content2 = open(note_file, encoding="utf-8").read()
|
||||||
|
assert content2.count("<!-- osintdb:begin id=personas -->") == 1
|
||||||
|
assert content2.count("| familia | 1 |") == 1
|
||||||
|
|
||||||
|
|
||||||
|
def test_render_note_valida_inputs(client):
|
||||||
|
client.post("/api/ingest/vault")
|
||||||
|
# Ni sql ni query.
|
||||||
|
r = client.post(
|
||||||
|
"/api/render/note", json={"note_path": "t.md", "block_id": "x"}
|
||||||
|
).json()
|
||||||
|
assert r["status"] == "error"
|
||||||
|
# Query con nombre desconocida.
|
||||||
|
r = client.post(
|
||||||
|
"/api/render/note",
|
||||||
|
json={"note_path": "t.md", "block_id": "x", "query": "nope"},
|
||||||
|
).json()
|
||||||
|
assert r["status"] == "error"
|
||||||
|
# Path traversal fuera del vault.
|
||||||
|
r = client.post(
|
||||||
|
"/api/render/note",
|
||||||
|
json={"note_path": "../fuera.md", "block_id": "x", "query": "stats_personas"},
|
||||||
|
).json()
|
||||||
|
assert r["status"] == "error"
|
||||||
|
assert "fuera del vault" in r["error"]
|
||||||
@@ -0,0 +1,563 @@
|
|||||||
|
version = 1
|
||||||
|
revision = 3
|
||||||
|
requires-python = ">=3.10"
|
||||||
|
|
||||||
|
[[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]]
|
||||||
|
name = "annotated-types"
|
||||||
|
version = "0.7.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/ee/67/531ea369ba64dcff5ec9c3402f9f51bf748cec26dde048a2f973a4eea7f5/annotated_types-0.7.0.tar.gz", hash = "sha256:aff07c09a53a08bc8cfccb9c85b05f1aa9a2a6f23728d790723543408344ce89", size = 16081, upload-time = "2024-05-20T21:33:25.928Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/78/b6/6307fbef88d9b5ee7421e68d78a9f162e0da4900bc5f5793f6d3d0e34fb8/annotated_types-0.7.0-py3-none-any.whl", hash = "sha256:1f02e8b43a8fbbc3f3e0d4f0f4bfc8131bcb4eebe8849b8e5c773f3a1c582a53", size = 13643, upload-time = "2024-05-20T21:33:24.1Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "anyio"
|
||||||
|
version = "4.13.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "idna" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/19/14/2c5dd9f512b66549ae92767a9c7b330ae88e1932ca57876909410251fe13/anyio-4.13.0.tar.gz", hash = "sha256:334b70e641fd2221c1505b3890c69882fe4a2df910cba14d97019b90b24439dc", size = 231622, upload-time = "2026-03-24T12:59:09.671Z" }
|
||||||
|
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 = "certifi"
|
||||||
|
version = "2026.5.20"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f3/ce/ee2ecad540810a79593028e88299baeae54d346cc7a0d94b6199988b89b1/certifi-2026.5.20.tar.gz", hash = "sha256:69dea482ab64caa7b9f6aba1c6bf48bb6a5448d1c0f1b17ab42ad8c763a5344d", size = 135422, upload-time = "2026-05-20T11:46:50.073Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/8c/57e832b7af6d7c5abe66eb3fbe3a3a32f4d11ea23a1aa7131371035be991/certifi-2026.5.20-py3-none-any.whl", hash = "sha256:3c52e209ba0a4ad7aebe60436a4ab349c39e1e602e8c134221e546902ad25897", size = 134134, upload-time = "2026-05-20T11:46:48.578Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "click"
|
||||||
|
version = "8.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9b/98/518d8e5081007684232226f475082b30087d0f585e8457db087298259f49/click-8.4.1.tar.gz", hash = "sha256:918b5633eddf6b41c32d4f454bf0de810065c74e3f7dbf8ee5452f8be88d3e96", size = 353007, upload-time = "2026-05-22T04:08:37.769Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c7/0d/67e5b4109ea4a837e80daa87c2c696711955e40449a97e8926672534def2/click-8.4.1-py3-none-any.whl", hash = "sha256:482be17c6991b8c19c5429a1e995d9b0efdbb63172824c41f99965dc0ade8ec2", size = 116639, upload-time = "2026-05-22T04:08:35.26Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "colorama"
|
||||||
|
version = "0.4.6"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d8/53/6f443c9a4a8358a93a6792e2acffb9d9d5cb0a5cfd8802644b7b1c9a02e4/colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44", size = 27697, upload-time = "2022-10-25T02:36:22.414Z" }
|
||||||
|
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 = "duckdb"
|
||||||
|
version = "1.5.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/69/00/d579dcb2a536b6ea3a2563cdad6844f77d81a9b2d4b22a858097f2468acf/duckdb-1.5.3.tar.gz", hash = "sha256:df39428eb130faa35ae96fd35245bdeae6ecf43936250b116b5fead568eb9f16", size = 18026640, upload-time = "2026-05-20T11:55:31.901Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c3/4a/06a81e9e3c01634760073b6a8911fccee2e33b23569f479971f910228972/duckdb-1.5.3-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:6ef8faf121d7b3ad95aab1c3ce31169a28be49da75abfa6099a1bec2e9a70189", size = 32575938, upload-time = "2026-05-20T11:53:56.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a0/ed/03ccf15147db7468f65891b5daa8489aa755d8cfcf75b24e7a4e4720e28c/duckdb-1.5.3-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:dd00f70231951a619908471b7b6397232ff3be8ccd1f49a47f1a2ccac59eaba1", size = 17276167, upload-time = "2026-05-20T11:53:59.558Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/bd/ffc9e7a52731eec047dd49d45f029580dc8f3a6f8e8802a9e1c4138b0f8a/duckdb-1.5.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:50379b85f3a0a169478d54880ef8bf971ecaa85772d05eeaa617d720c7704741", size = 15425037, upload-time = "2026-05-20T11:54:01.894Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/5d/44209bcd4ad47feb84831edd3f269397e03167efbb86aee2d102b2f71252/duckdb-1.5.3-cp310-cp310-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:d37650ec3ec8a951400ea12dc77edaea88e0baeda34801792776f95f2f922f4f", size = 19306975, upload-time = "2026-05-20T11:54:04.5Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/76/5c/bdb735011131c429cdb6d9e12e12fc8f1c8622158890d76a25819a58efc2/duckdb-1.5.3-cp310-cp310-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:a3fb3bad9bc1a3e101d66d33269142ce075dc3d75202ba74ba97d7e44c50b9cd", size = 21416979, upload-time = "2026-05-20T11:54:07.429Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/c2/929666ff6d71b15b128c6f800d893fb5589c7b6430ff9ecdcd4e6d175bea/duckdb-1.5.3-cp310-cp310-win_amd64.whl", hash = "sha256:fdc65233f0fcf9022e4c6a8ba2ba751a79deb291501073d660afb1aa9874051f", size = 13101915, upload-time = "2026-05-20T11:54:10.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/fc/a8a89c6c73f31c2b58c6abbc2f543e0b736042dd5ef7cc1784c24ec31428/duckdb-1.5.3-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:341a2672e2551ba51c95c1898f0ade983e76675e79038ccb16342c3d6cfb82d7", size = 32583465, upload-time = "2026-05-20T11:54:13.132Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/f1/3423a2f523dd034e505d4a5dd8e210ae577212e152598dc13b6a5e736e1b/duckdb-1.5.3-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:c9e8fa408705081160ede7ead238d16e73a36b8561b700f2bf2d650ae48e7b92", size = 17278520, upload-time = "2026-05-20T11:54:16.368Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e1/1a/7bf5ba1b7ea520557e6b2dbee1c85abab016bdac0c1779d9d0ef76c87300/duckdb-1.5.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:70a18f932cf6d87bd0e554613657a515c1443a1724aacfc7ec5137dd28698b03", size = 15424794, upload-time = "2026-05-20T11:54:19.891Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/16/ce4b1e386e45fab0268edbf1b85bace20e9437589e9edb2bd5f9a226fa44/duckdb-1.5.3-cp311-cp311-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:e80eb4d0fb59869cb2c7d7ef494c07fb92014fe8e77d96c170cd1ebc1488a708", size = 19306666, upload-time = "2026-05-20T11:54:22.77Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/1f/651f8453f26931e8061b7e27b3090f868868185814ecb9216d0bd71ec8ef/duckdb-1.5.3-cp311-cp311-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:3248b49cd835ea322574bc6aac0ae7a83be85547f49d4f5f5777cb380ee6627f", size = 21418306, upload-time = "2026-05-20T11:54:25.616Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bc/64/e1ffebf010b1631a6fef8d1508f46d4eab3e97c18729af986bb796fa8452/duckdb-1.5.3-cp311-cp311-win_amd64.whl", hash = "sha256:f4eff89c12c3a362efa012262e57b7b4ab904a7f79bad9178fe365510077abe8", size = 13101423, upload-time = "2026-05-20T11:54:28.107Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/42/b1d4e34f9658cc0e13d7aae581ab82643f50a548d5aee8767f0c587cc3a4/duckdb-1.5.3-cp311-cp311-win_arm64.whl", hash = "sha256:75d13308c9da3ee431d1e72b8ab720aa74a1b3e9159d4124cb62435924496334", size = 13951740, upload-time = "2026-05-20T11:54:30.886Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/c4/2e34929b16c8d544ef664fad8f7f3a2a9db05746aae1e7c8c4ee3a8b23e4/duckdb-1.5.3-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:ff11a457258148337ef9a392148a8cdbd1069b6c27c21958816c7b67fe6c542d", size = 32626494, upload-time = "2026-05-20T11:54:33.738Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3a/53/3af681793d03771365ae3e2215331151c196a3ac8193f613344840694671/duckdb-1.5.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:5fd25f533cb1b6b2c84cc767a9a9bab7769bb1aa44571a2a0bfc91ac3e4a38ac", size = 17301121, upload-time = "2026-05-20T11:54:36.928Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/e2/c80af1eac2ab5d35fc2c372ef0a84668842e549fbbf7799277b3fccf3e39/duckdb-1.5.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:10960400ed60cdf0fe05bab2086fa8eb733889cb0ceca18d07ff9a00c0e0be7b", size = 15449283, upload-time = "2026-05-20T11:54:39.777Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2d/9a/c63af233c9f761bf5178a5210437e1bc6bcb30fa8a9073de6398cfb12c03/duckdb-1.5.3-cp312-cp312-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c5f18e7561403054433706c187589e86629a7af09a7efc23a06a8b308e6acc68", size = 19332762, upload-time = "2026-05-20T11:54:42.51Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/cc/2d77af4fff86012f334ef82e6d54a995a86c8745e58074f1218ed7d25171/duckdb-1.5.3-cp312-cp312-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9fb7516255a8764545e30f7efacea408cc847764a3027b3b0b3e7d1a7bebbc5c", size = 21453290, upload-time = "2026-05-20T11:54:45.272Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/5e/9bc4817a98feb4dab83e56f2245cd3a30d00ee646d4dec7926464e2b3f28/duckdb-1.5.3-cp312-cp312-win_amd64.whl", hash = "sha256:8001eccbc28be244dfd04d708526f34ddd6460b47a8aeb5d0e39d6f7f9e3fe15", size = 13118308, upload-time = "2026-05-20T11:54:48.058Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/35/e3f32e4e53e2450ddb1db8312a17d1ce455d60cc4941b6ad2cfc908794b0/duckdb-1.5.3-cp312-cp312-win_arm64.whl", hash = "sha256:6d2835e39bb6af73891f73c0f8d4324f98afe00d0b00c6d34b2a582c2256cbb0", size = 13927187, upload-time = "2026-05-20T11:54:50.584Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cc/9c/a528eb09d8be51954c485864bd06753e616939a080cbc3dd4417e8c94a57/duckdb-1.5.3-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:e75a6122c12579a99848517f6f00a4e342aebda3590c30fe9b5cc5f39d5e6afc", size = 32626254, upload-time = "2026-05-20T11:54:53.65Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/3c/1534c0a6db347c05eb7d0f6ecfb7aefbe74cbff398e4892a8fd1903a20e8/duckdb-1.5.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:fd3963c1cb9d9567777f4a898a9dbe388a2fe9724681801b1e7d6d93eecf1b76", size = 17300917, upload-time = "2026-05-20T11:54:56.628Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/fa/beafb91e6e152d2161c4a9cbc472334c87607eb61ad7104b5a7fa8d8d7b1/duckdb-1.5.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:3d5db8c0b55e072cf437948ebb5d7e23d7b9d03d905fa5f9145583e65aa447f7", size = 15449411, upload-time = "2026-05-20T11:54:59.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/0a/49b6fe04e2fcd63729eb607dadd44818dde77342a4f5ce086c6c92f1dd4d/duckdb-1.5.3-cp313-cp313-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:0ce80aed7a538422129a57eaca9141e3afb51f8bf562b1908b1576c9725b5b22", size = 19333120, upload-time = "2026-05-20T11:55:01.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/4c/0907c3f76adb9dd90e67610b31e0304a35814e65c4c41a354a262c09b885/duckdb-1.5.3-cp313-cp313-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:787df63824f07bf18022dbc3b8ca4b2bfab0ebe616464f55c6e8cd0f59ea762e", size = 21453266, upload-time = "2026-05-20T11:55:04.5Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/9c/d2f23a7803ddbbd9413f7572ecf66a15120ed5ced7ce5c73e698c1406b76/duckdb-1.5.3-cp313-cp313-win_amd64.whl", hash = "sha256:bb5bb5dcdd09d62ee60f0ddbbef918e71cce304ffe28428b1131949d39ffaabf", size = 13118640, upload-time = "2026-05-20T11:55:07.389Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/27/d5/7ba2316415bcdab6edd765bbbe35c2ca8a3800f2fe695cd70e3cdb997f09/duckdb-1.5.3-cp313-cp313-win_arm64.whl", hash = "sha256:2fa17ecdd5d3db122836cb71bb93601c2106a3be883c17dffddc02fbf3fa7888", size = 13926409, upload-time = "2026-05-20T11:55:10.166Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a5/c2/d4b6f8a5e4d3bc25773be6da76a99d9661ebbf3552c007c460d2dd59dbf8/duckdb-1.5.3-cp314-cp314-macosx_10_15_universal2.whl", hash = "sha256:4bfa9a4dadf71e83e2c4eaca2f9421c82a54defecc1b0b4c0be95e2389dec4fe", size = 32636685, upload-time = "2026-05-20T11:55:13.158Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/58/e835c8298979d29db7a62cb5acc29e9b57aeaca7cdde2fcd3ac980f5cb18/duckdb-1.5.3-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:aea7baf67ad7e1829ac76f67d7dcbd7fb1f57c3eb179d55ac30952df4709ae30", size = 17308134, upload-time = "2026-05-20T11:55:16.194Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c9/46/617b51363f5613418c8b224b3cce16b58e6dde80904566bec232579c1d4e/duckdb-1.5.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:0b0b4f088a65d77e1217ce5d7eff889e63fedc44281200d899ff47c84d8ff836", size = 15449891, upload-time = "2026-05-20T11:55:18.687Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b3/72/354146656e8d9ba3853d3a5ee80a481b8c5f70edfc3d5ae80a8c4479c967/duckdb-1.5.3-cp314-cp314-manylinux_2_26_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:fe8d0c1f6a120aa03fa6e0d03897c71a1842e6cf7afd31d181348391f7108fe1", size = 19338499, upload-time = "2026-05-20T11:55:21.34Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/8f/65fc623b51448f2bfba1a9ec6ab3debb4664c0876c0113a5e782600b53ac/duckdb-1.5.3-cp314-cp314-manylinux_2_26_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:d0405eae18ec6e8210a471c97dbfe87a7e4d605274b7fe572a1f276e92158f13", size = 21455828, upload-time = "2026-05-20T11:55:23.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2b/db/d0274cbe9f5fe219f77c0bdf900ac77103569e83c102a4225ce04cbc607d/duckdb-1.5.3-cp314-cp314-win_amd64.whl", hash = "sha256:33ae08b3e818d7613d8936744b67718c2062c2f530376895bfd89efb51b81538", size = 13640011, upload-time = "2026-05-20T11:55:26.276Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/5d/8f1899b8bef291caf953992fcd6c24df9f29387a35645e58c2504a5ca473/duckdb-1.5.3-cp314-cp314-win_arm64.whl", hash = "sha256:746433e49bbc667b4df283153415fbe37e9083e0eff6c3cd6e54de7536869cd4", size = 14411554, upload-time = "2026-05-20T11:55:29.037Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "exceptiongroup"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fastapi"
|
||||||
|
version = "0.136.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-doc" },
|
||||||
|
{ name = "pydantic" },
|
||||||
|
{ name = "starlette" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/81/2d/ff8d91d7b564d464629a0fd50a4489c97fcb836ac230bf3a7269232a9b1f/fastapi-0.136.3.tar.gz", hash = "sha256:e487fae93ad408e6f47641ee4dfe389864fd7bec92e547ea8498fc13f43e83ab", size = 396410, upload-time = "2026-05-23T18:53:15.192Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e0/82/45359b62a067409bd929ae8a56b8ed13e5a8c8a61194b3c236920999ab83/fastapi-0.136.3-py3-none-any.whl", hash = "sha256:3d2a69bdf04b7e9f3afa292c3bc7a98816bbfafa10bc9b45f3f3700d2f761620", size = 117481, upload-time = "2026-05-23T18:53:16.924Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "h11"
|
||||||
|
version = "0.16.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/01/ee/02a2c011bdab74c6fb3c75474d40b3052059d95df7e73351460c8588d963/h11-0.16.0.tar.gz", hash = "sha256:4e35b956cf45792e4caa5885e69fba00bdbc6ffafbfa020300e549b208ee5ff1", size = 101250, upload-time = "2025-04-24T03:35:25.427Z" }
|
||||||
|
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 = "httpcore"
|
||||||
|
version = "1.0.9"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "h11" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/06/94/82699a10bca87a5556c9c59b5963f2d039dbd239f25bc2a63907a05a14cb/httpcore-1.0.9.tar.gz", hash = "sha256:6e34463af53fd2ab5d807f399a9b45ea31c3dfa2276f15a2c3f00afff6e176e8", size = 85484, upload-time = "2025-04-24T22:06:22.219Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784, upload-time = "2025-04-24T22:06:20.566Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "httpx"
|
||||||
|
version = "0.28.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "certifi" },
|
||||||
|
{ name = "httpcore" },
|
||||||
|
{ name = "idna" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/b1/df/48c586a5fe32a0f01324ee087459e112ebb7224f646c0b5023f5e79e9956/httpx-0.28.1.tar.gz", hash = "sha256:75e98c5f16b0f35b567856f597f06ff2270a374470a5c2392242528e3e3e42fc", size = 141406, upload-time = "2024-12-06T15:37:23.222Z" }
|
||||||
|
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 = "idna"
|
||||||
|
version = "3.18"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/cd/63/9496c57188a2ee585e0f1db071d75089a11e98aa86eb99d9d7618fc1edce/idna-3.18.tar.gz", hash = "sha256:ffb385a7e039654cef1ab9ef32c6fafe283c0c0467bba1d9029738ce4a14a848", size = 196711, upload-time = "2026-06-02T14:34:07.794Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1e/5e/d4e9f1a599fb8e573b7b87160658329fbf28d19eac2718f51fc3def3aa5a/idna-3.18-py3-none-any.whl", hash = "sha256:7f952cbe720b688055e3f87de14f5c3e5fdaa8bc3928985c4077ca689de849a2", size = 65455, upload-time = "2026-06-02T14:34:06.319Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "iniconfig"
|
||||||
|
version = "2.3.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" }
|
||||||
|
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 = "osint-db"
|
||||||
|
version = "0.1.0"
|
||||||
|
source = { virtual = "." }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "duckdb" },
|
||||||
|
{ name = "fastapi" },
|
||||||
|
{ name = "pyyaml" },
|
||||||
|
{ name = "uvicorn" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.dev-dependencies]
|
||||||
|
dev = [
|
||||||
|
{ name = "httpx" },
|
||||||
|
{ name = "pytest" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata]
|
||||||
|
requires-dist = [
|
||||||
|
{ name = "duckdb", specifier = ">=1.0" },
|
||||||
|
{ name = "fastapi", specifier = ">=0.110" },
|
||||||
|
{ name = "pyyaml", specifier = ">=6.0" },
|
||||||
|
{ name = "uvicorn", specifier = ">=0.29" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[package.metadata.requires-dev]
|
||||||
|
dev = [
|
||||||
|
{ name = "httpx", specifier = ">=0.27" },
|
||||||
|
{ name = "pytest", specifier = ">=8.0" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "packaging"
|
||||||
|
version = "26.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/d7/f1/e7a6dd94a8d4a5626c03e4e99c87f241ba9e350cd9e6d75123f992427270/packaging-26.2.tar.gz", hash = "sha256:ff452ff5a3e828ce110190feff1178bb1f2ea2281fa2075aadb987c2fb221661", size = 228134, upload-time = "2026-04-24T20:15:23.917Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/b2/87e62e8c3e2f4b32e5fe99e0b86d576da1312593b39f47d8ceef365e95ed/packaging-26.2-py3-none-any.whl", hash = "sha256:5fc45236b9446107ff2415ce77c807cee2862cb6fac22b8a73826d0693b0980e", size = 100195, upload-time = "2026-04-24T20:15:22.081Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pluggy"
|
||||||
|
version = "1.6.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" }
|
||||||
|
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 = "pydantic"
|
||||||
|
version = "2.13.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "annotated-types" },
|
||||||
|
{ name = "pydantic-core" },
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
{ name = "typing-inspection" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/18/a5/b60d21ac674192f8ab0ba4e9fd860690f9b4a6e51ca5df118733b487d8d6/pydantic-2.13.4.tar.gz", hash = "sha256:c40756b57adaa8b1efeeced5c196f3f3b7c435f90e84ea7f443901bec8099ef6", size = 844775, upload-time = "2026-05-06T13:43:05.343Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fd/7b/122376b1fd3c62c1ed9dc80c931ace4844b3c55407b6fb2d199377c9736f/pydantic-2.13.4-py3-none-any.whl", hash = "sha256:45a282cde31d808236fd7ea9d919b128653c8b38b393d1c4ab335c62924d9aba", size = 472262, upload-time = "2026-05-06T13:43:02.641Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pydantic-core"
|
||||||
|
version = "2.46.4"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/9d/56/921726b776ace8d8f5db44c4ef961006580d91dc52b803c489fafd1aa249/pydantic_core-2.46.4.tar.gz", hash = "sha256:62f875393d7f270851f20523dd2e29f082bcc82292d66db2b64ea71f64b6e1c1", size = 471464, upload-time = "2026-05-06T13:37:06.98Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e7/08/f1ba952f1c8ae5581c70fa9c6da89f247b83e3dd8c09c035d5d7931fc23d/pydantic_core-2.46.4-cp310-cp310-macosx_10_12_x86_64.whl", hash = "sha256:a396dcc17e5a0b164dbe026896245a4fa9ff402edca1dff0be3d53a517f74de4", size = 2113146, upload-time = "2026-05-06T13:37:36.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/56/c6/65f646c7ff09bd257f660434adb45c4dfcbbcebcc030562fecf6f5bf887d/pydantic_core-2.46.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:da4b951fe36dc7c3a1ccb4e3cd1747c3542b8c9ceede8fc86cae054e764485f5", size = 1949769, upload-time = "2026-05-06T13:37:46.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/ba/bfb1d928fd5b49e1258935ff104ae356e9fd89384a55bf9f847e9193ad40/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bb63e0198ca18aad131c089b9204c23079c3afa95487e561f4c522d519e55aba", size = 1974958, upload-time = "2026-05-06T13:37:28.611Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/74/76223bfb117b64af743c9b6670d1364516f5c0604f96b48f3272f6af6cc6/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:f47286a97f0bc9b8859519809077b91b2cefe4ae47fcbf5e466a009c1c5d742b", size = 2042118, upload-time = "2026-05-06T13:36:55.216Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/cb/7b/848732968bc8f48f3187542f08358b9d842db564147b256669426ebb1652/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:905a0ed8ea6f2d61c1738835f99b699348d7857379083e5fc497fa0c967a407c", size = 2222876, upload-time = "2026-05-06T13:38:25.455Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b5/2f/e90b63ee2e14bd8d3db8f705a6d75d64e6ee1b7c2c8833747ce706e1e0ce/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:ea793e075b70290d89d8142074262885d3f7da19634845135751bd6344f73b50", size = 2286703, upload-time = "2026-05-06T13:37:53.304Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ba/1e/acc4d70f88a0a277e4a1fa77ebb985ceabaf900430f875bf9338e11c9420/pydantic_core-2.46.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:395aebd9183f9d112f569aeb5b2214d1a10a33bec8456447f7fbdfa51d38d4cd", size = 2092042, upload-time = "2026-05-06T13:38:46.981Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a9/da/0a422b57bf8504102bf3c4ccea9c41bab5a5cee6a54650acf8faf67f5a24/pydantic_core-2.46.4-cp310-cp310-manylinux_2_31_riscv64.whl", hash = "sha256:b078afbc25f3a1436c7a1d2cd3e322497ee99615ba97c563566fdf46aff1ee01", size = 2117231, upload-time = "2026-05-06T13:39:23.146Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/2a/2ac13c3af305843e23c5078c53d135656b3f05a2fd78cb7bbbb12e97b473/pydantic_core-2.46.4-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:f747929cf940cddb5b3668a390056ddd5ba2e5010615ea2dcf4f9c4f3ab8791d", size = 2168388, upload-time = "2026-05-06T13:40:08.06Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/72/04/2beacf7e1607e93eefe4aed1b4709f079b905fb77530179d4f7c71745f22/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:daa27d92c36f24388fe3ad306b174781c747627f134452e4f128ea00ce1fe8c4", size = 2184769, upload-time = "2026-05-06T13:38:13.901Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9e/29/d2b9fd9f539133548eaf622c06a4ce176cb46ac59f32d0359c4abc0de047/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_armv7l.whl", hash = "sha256:19e51f073cd3df251856a8a4189fbdf1de4012c3ebacfb1884f94f1eb406079f", size = 2319312, upload-time = "2026-05-06T13:39:08.24Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/af/0f7a5b85fec6075bea96e3ef9187de38fccced0de92c1e7feda8d5cc7bb9/pydantic_core-2.46.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:c1747f85cee84c26985853c6f3d9bd3e75da5212912443fa111c113b9c246f39", size = 2361817, upload-time = "2026-05-06T13:38:43.2Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/25/a4/73363fec545fd3ec025490bdda2743c56d0dd5b6266b1a53bbe9e4265375/pydantic_core-2.46.4-cp310-cp310-win32.whl", hash = "sha256:2f84c03c8607173d16b5a854ec68a2f9079ae03237a54fb506d13af47e1d018d", size = 1987085, upload-time = "2026-05-06T13:39:25.497Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/01/aa/62f082da2c91fac1c234bc9ee0066257ce83f0604abd72e4c9d5991f2d84/pydantic_core-2.46.4-cp310-cp310-win_amd64.whl", hash = "sha256:8358a950c8909158e3df31538a7e4edc2d7265a7c54b47f0864d9e5bae9dcebf", size = 2074311, upload-time = "2026-05-06T13:39:59.922Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/fa/6d7708d2cfc1a832acb6aeb0cd16e801902df8a0f583bb3b4b527fde022e/pydantic_core-2.46.4-cp311-cp311-macosx_10_12_x86_64.whl", hash = "sha256:0e96592440881c74a213e5ad528e2b24d3d4f940de2766bed9010ab1d9e51594", size = 2111872, upload-time = "2026-05-06T13:40:27.596Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/6f/aa064a3e74b5745afbdf250594f38e7ead05e2d651bcb35994b9417a0d4d/pydantic_core-2.46.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:e0d65b8c354be7fb5f720c3caa8bc940bc2d20ce749c8e06135f07f8ed95dd7c", size = 1948255, upload-time = "2026-05-06T13:39:12.574Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/3a/41114a9f7569b84b4d84e7a018c57c56347dac30c0d4a872946ec4e36c46/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7bfb192b3f4b9e8a89b6277b6ce787564f62cfd272055f6e685726b111dc7826", size = 1972827, upload-time = "2026-05-06T13:38:19.841Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/25/1ab42e8048fe551934d9884e8d64daa7e990ad386f310a15981aeb6a5b08/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:9037063db01f09b09e237c282b6792bd4da634b5402c4e7f0c61effed7701a04", size = 2041051, upload-time = "2026-05-06T13:38:10.447Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/c2/1a934597ddf08da410385b3b7aae91956a5a76c635effef456074fad7e88/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fc010ab034c8c7452522748bf937df58020d256ccae0874463d1f4d01758af8e", size = 2221314, upload-time = "2026-05-06T13:40:13.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/6d/9e8ad178c9c4df27ad3c8f25d1fe2a7ab0d2ba0559fad4aee5d3d1f16771/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:8c5dac79fa1614d1e06ca695109c6105923bd9c7d1d6c918d4e637b7e6b32fd3", size = 2285146, upload-time = "2026-05-06T13:38:59.224Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/80/50/540cd3aeefc041beb111125c4bff779831a2111fc6b15a9138cda277d32c/pydantic_core-2.46.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f9fa868638bf362d3d138ea55829cefb3d5f4b0d7f142234382a15e2485dbec4", size = 2089685, upload-time = "2026-05-06T13:38:17.762Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/a4/b440ad35f05f6a38f89fa0f149accb3f0e02be94ca5e15f3c449a61b4bc9/pydantic_core-2.46.4-cp311-cp311-manylinux_2_31_riscv64.whl", hash = "sha256:17299feefe090f2caa5b8e37222bb5f663e4935a8bfa6931d4102e5df1a9f398", size = 2115420, upload-time = "2026-05-06T13:37:58.195Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/61/de4f55db8dfd57bfdfa9a12ec90fe1b57c4f41062f7ca86f08586b3e0ac0/pydantic_core-2.46.4-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:4c63ebc82684aa89d9a3bcbd13d515b3be44250dc68dd3bd81526c1cb31286c3", size = 2165122, upload-time = "2026-05-06T13:37:01.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f7/52/7c529d7bdb2d1068bd52f51fe32572c8301f9a4febf1948f10639f1436f5/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:aaa2a54443eff1950ba5ddc6b6ccda0d9c84a364276a62f969bdf2a390650848", size = 2182573, upload-time = "2026-05-06T13:38:45.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/37/b3/7c40325848ba78247f2812dcf9c7274e38cd801820ca6dd9fe63bcfb0eb4/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_armv7l.whl", hash = "sha256:18e5ceec2ab67e6d5f1a9085e5a24c9c4e2ac4545730bfe668680bca05e555f3", size = 2317139, upload-time = "2026-05-06T13:37:15.539Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d9/37/f913f81a657c865b75da6c0dbed79876073c2a43b5bd9edbe8da785e4d49/pydantic_core-2.46.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:a0f62d0a58f4e7da165457e995725421e0064f2255d8eccebc49f41bbc23b109", size = 2360433, upload-time = "2026-05-06T13:37:30.099Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/67/6acaa1be2567f9256b056d8477158cac7240813956ce86e49deae8e173b4/pydantic_core-2.46.4-cp311-cp311-win32.whl", hash = "sha256:041bde0a48fd37cf71cab1c9d56d3e8625a3793fef1f7dd232b3ff37e978ecda", size = 1985513, upload-time = "2026-05-06T13:38:15.669Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/aa/e6/c505f83dfeda9a2e5c995cfd872949e4d05e12f7feb3dca72f633daefa94/pydantic_core-2.46.4-cp311-cp311-win_amd64.whl", hash = "sha256:6f2eeda33a839975441c86a4119e1383c50b47faf0cbb5176985565c6bb02c33", size = 2071114, upload-time = "2026-05-06T13:40:35.416Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0f/da/7a263a96d965d9d0df5e8de8a475f33495451117035b09acb110288c381f/pydantic_core-2.46.4-cp311-cp311-win_arm64.whl", hash = "sha256:14f4c5d6db102bd796a627bbb3a17b4cf4574b9ae861d8b7c9a9661c6dd3362d", size = 2044298, upload-time = "2026-05-06T13:38:29.754Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/8c/af022f0af448d7747c5154288d46b5f2bc5f17366eaa0e23e9aa04d59f3b/pydantic_core-2.46.4-cp312-cp312-macosx_10_12_x86_64.whl", hash = "sha256:3245406455a5d98187ec35530fd772b1d799b26667980872c8d4614991e2c4a2", size = 2106158, upload-time = "2026-05-06T13:38:57.215Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/19/95/6195171e385007300f0f5574592e467c568becce2d937a0b6804f218bc49/pydantic_core-2.46.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:962ccbab7b642487b1d8b7df90ef677e03134cf1fd8880bf698649b22a69371f", size = 1951724, upload-time = "2026-05-06T13:37:02.697Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8e/bc/f47d1ff9cbb1620e1b5b697eef06010035735f07820180e74178226b27b3/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8233f2947cf85404441fd7e0085f53b10c93e0ee78611099b5c7237e36aacbf7", size = 1975742, upload-time = "2026-05-06T13:37:09.448Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/11/9b9a5b0306345664a2da6410877af6e8082481b5884b3ddd78d47c6013ce/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:3a233125ac121aa3ffba9a2b59edfc4a985a76092dc8279586ab4b71390875e7", size = 2052418, upload-time = "2026-05-06T13:37:38.234Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/b7/a65fec226f5d78fc39f4a13c4cc0c768c22b113438f60c14adc9d2865038/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:5b712b53160b79a5850310b912a5ef8e57e56947c8ad690c227f5c9d7e561712", size = 2232274, upload-time = "2026-05-06T13:38:27.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/f0/92039db98b907ef49269a8271f67db9cb78ae2fc68062ef7e4e77adb5f61/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9401557acd873c3a7f3eb9383edef8ac4968f9510e340f4808d427e75667e7b4", size = 2309940, upload-time = "2026-05-06T13:38:05.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5f/97/2aab507d3d00ca626e8e57c1eac6a79e4e5fbcc63eb99733ff55d1717f65/pydantic_core-2.46.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:926c9541b14b12b1681dca8a0b75feb510b06c6341b70a8e500c2fdcff837cce", size = 2094516, upload-time = "2026-05-06T13:39:10.577Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/37/a8aca44d40d737dde2bc05b3c6c07dff0de07ce6f82e9f3167aeaf4d5dea/pydantic_core-2.46.4-cp312-cp312-manylinux_2_31_riscv64.whl", hash = "sha256:56cb4851bcaf3d117eddcef4fe66afd750a50274b0da8e22be256d10e5611987", size = 2136854, upload-time = "2026-05-06T13:40:22.59Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/99/fcef1b79238c06a8cbec70819ac722ba76e02bc8ada9b0fd66eba40da01b/pydantic_core-2.46.4-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:c68fcd102d71ea85c5b2dfac3f4f8476eff42a9e078fd5faefff6d145063536b", size = 2180306, upload-time = "2026-05-06T13:40:10.666Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ae/6c/fc44000918855b42779d007ae63b0532794739027b2f417321cddbc44f6a/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:b2f69dec1725e79a012d920df1707de5caf7ed5e08f3be4435e25803efc47458", size = 2190044, upload-time = "2026-05-06T13:40:43.231Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/65/d9cadc9f1920d7a127ad2edba16c1db7916e59719285cd6c94600b0080ba/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_armv7l.whl", hash = "sha256:8d0820e8192167f80d88d64038e609c31452eeca865b4e1d9950a27a4609b00b", size = 2329133, upload-time = "2026-05-06T13:39:57.365Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d0/cf/c873d91679f3a30bcf5e7ac280ce5573483e72295307685120d0d5ad3416/pydantic_core-2.46.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:fbdb89b3e1c94a30cc5edfce477c6e6a5dc4d8f84665b455c27582f211a1c72c", size = 2374464, upload-time = "2026-05-06T13:38:06.976Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/47/bd/6f2fc8188f31bf10590f1e98e7b306336161fac930a8c514cd7bd828c7dc/pydantic_core-2.46.4-cp312-cp312-win32.whl", hash = "sha256:9aa768456404a8bf48a4406685ac2bec8e72b62c69313734fa3b73cf33b3a894", size = 1974823, upload-time = "2026-05-06T13:40:47.985Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/8c/985c1d41ea1107c2534abd9870e4ed5c8e7669b5c308297835c001e7a1c4/pydantic_core-2.46.4-cp312-cp312-win_amd64.whl", hash = "sha256:e9c26f834c65f5752f3f06cb08cb86a913ceb7274d0db6e267808a708b46bc89", size = 2072919, upload-time = "2026-05-06T13:39:21.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c4/ba/f463d006e0c47373ca7ec5e1a261c59dc01ef4d62b2657af925fb0deee3a/pydantic_core-2.46.4-cp312-cp312-win_arm64.whl", hash = "sha256:4fc73cb559bdb54b1134a706a2802a4cddd27a0633f5abb7e53056268751ac6a", size = 2027604, upload-time = "2026-05-06T13:39:03.753Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/51/a2/5d30b469c5267a17b39dec53208222f76a8d351dfac4af661888c5aee77d/pydantic_core-2.46.4-cp313-cp313-macosx_10_12_x86_64.whl", hash = "sha256:5d5902252db0d3cedf8d4a1bc68f70eeb430f7e4c7104c8c476753519b423008", size = 2106306, upload-time = "2026-05-06T13:37:48.029Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/81/4fa520eaffa8bd7d1525e644cd6d39e7d60b1592bc5b516693c7340b50f1/pydantic_core-2.46.4-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:c94f0688e7b8d0a67abf40e57a7eaaecd17cc9586706a31b76c031f63df052b4", size = 1951906, upload-time = "2026-05-06T13:37:17.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/03/d5/fd02da45b659668b05923b17ba3a0100a0a3d5541e3bd8fcc4ecb711309e/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f027324c56cd5406ca49c124b0db10e56c69064fec039acc571c29020cc87c76", size = 1976802, upload-time = "2026-05-06T13:37:35.113Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/21/f2/95727e1368be3d3ed485eaab7adbd7dda408f33f7a36e8b48e0144002b91/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:e739fee756ba1010f8bcccb534252e85a35fe45ae92c295a06059ce58b74ccd3", size = 2052446, upload-time = "2026-05-06T13:37:12.313Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9c/86/5d99feea3f77c7234b8718075b23db11532773c1a0dbd9b9490215dc2eeb/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9d56801be94b86a9da183e5f3766e6310752b99ff647e38b09a9500d88e46e76", size = 2232757, upload-time = "2026-05-06T13:39:01.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d2/3a/508ac615935ef7588cf6d9e9b91309fdc2da751af865e02a9098de88258c/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:2412e734dcb48da14d4e4006b82b46b74f2518b8a26ee7e58c6844a6cd6d03c4", size = 2309275, upload-time = "2026-05-06T13:37:41.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/f8/41db9de19d7987d6b04715a02b3b40aea467000275d9d758ffaa31af7d50/pydantic_core-2.46.4-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9551187363ffc0de2a00b2e47c25aeaeb1020b69b668762966df15fc5659dd5a", size = 2094467, upload-time = "2026-05-06T13:39:18.847Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2c/e2/f35033184cb11d0052daf4416e8e10a502ea2ac006fc4f459aee872727d1/pydantic_core-2.46.4-cp313-cp313-manylinux_2_31_riscv64.whl", hash = "sha256:0186750b482eefa11d7f435892b09c5c606193ef3375bcf94aa00ae6bfb66262", size = 2134417, upload-time = "2026-05-06T13:40:17.944Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/7b/6ceeb1cc90e193862f444ebe373d8fdf613f0a82572dde03fb10734c6c71/pydantic_core-2.46.4-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:5855698a4856556d86e8e6cd8434bc3ac0314ee8e12089ae0e143f64c6256e4e", size = 2179782, upload-time = "2026-05-06T13:40:32.618Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/f2/c8d7773ede6af08036423a00ae0ceffce266c3c52a096c435d68c896083f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:cbaf13819775b7f769bf4a1f066cb6df7a28d4480081a589828ef190226881cd", size = 2188782, upload-time = "2026-05-06T13:36:51.018Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/59/31/0c864784e31f09f05cdd87606f08923b9c9e7f6e51dd27f20f62f975ce9f/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_armv7l.whl", hash = "sha256:633147d34cf4550417f12e2b1a0383973bdf5cdfde212cb09e9a581cf10820be", size = 2328334, upload-time = "2026-05-06T13:40:37.764Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c2/eb/4f6c8a41efa30baa755590f4141abf3a8c370fab610915733e74134a7270/pydantic_core-2.46.4-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:82cf5301172168103724d49a1444d3378cb20cdee30b116a1bd6031236298a5d", size = 2372986, upload-time = "2026-05-06T13:39:34.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5b/24/b375a480d53113860c299764bfe9f349a3dc9108b3adc0d7f0d786492ebf/pydantic_core-2.46.4-cp313-cp313-win32.whl", hash = "sha256:9fa8ae11da9e2b3126c6426f147e0fba88d96d65921799bb30c6abd1cb2c97fb", size = 1973693, upload-time = "2026-05-06T13:37:55.072Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7e/e8/cff247591966f2d22ec8c003cd7587e27b7ba7b81ab2fb888e3ab75dc285/pydantic_core-2.46.4-cp313-cp313-win_amd64.whl", hash = "sha256:6b3ace8194b0e5204818c92802dcdca7fc6d88aabbb799d7c795540d9cd6d292", size = 2071819, upload-time = "2026-05-06T13:38:49.139Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c6/1a/f4aee670d5670e9e148e0c82c7db98d780be566c6e6a97ee8035528ca0b3/pydantic_core-2.46.4-cp313-cp313-win_arm64.whl", hash = "sha256:184c081504d17f1c1066e430e117142b2c77d9448a97f7b65c6ac9fd9aee238d", size = 2027411, upload-time = "2026-05-06T13:40:45.796Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8d/74/228a26ddad29c6672b805d9fd78e8d251cd04004fa7eed0e622096cd0250/pydantic_core-2.46.4-cp314-cp314-macosx_10_12_x86_64.whl", hash = "sha256:428e04521a40150c85216fc8b85e8d39fece235a9cf5e383761238c7fa9b96fb", size = 2102079, upload-time = "2026-05-06T13:38:41.019Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ad/1f/8970b150a4b4365623ae00fc88603491f763c627311ae8031e3111356d6e/pydantic_core-2.46.4-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:23ace664830ee0bfe014a0c7bc248b1f7f25ed7ad103852c317624a1083af462", size = 1952179, upload-time = "2026-05-06T13:36:59.812Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/95/30/5211a831ae054928054b2f79731661087a2bc5c01e825c672b3a4a8f1b3e/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ce5c1d2a8b27468f433ca974829c44060b8097eedc39933e3c206a90ee49c4a9", size = 1978926, upload-time = "2026-05-06T13:37:39.933Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/57/e9/689668733b1eb67adeef047db3c2e8788fcf65a7fd9c9e2b46b7744fe245/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:7283d57845ecf5a163403eb0702dfc220cc4fbdd18919cb5ccea4f95ee1cdab4", size = 2046785, upload-time = "2026-05-06T13:38:01.995Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/d9/6715260422ff50a2109878fd24d948a6c3446bb2664f34ee78cd972b3acd/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:8daafc69c93ee8a0204506a3b6b30f586ef54028f52aeeeb5c4cfc5184fd5914", size = 2228733, upload-time = "2026-05-06T13:40:50.371Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/18/ae/fdb2f64316afca925640f8e70bb1a564b0ec2721c1389e25b8eb4bf9a299/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cd2213145bcc2ba85884d0ac63d222fece9209678f77b9b4d76f054c561adb28", size = 2307534, upload-time = "2026-05-06T13:37:21.531Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/1d/8eff589b45bb8190a9d12c49cfad0f176a5cbd1534908a6b5125e2886239/pydantic_core-2.46.4-cp314-cp314-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7a5f930472650a82629163023e630d160863fce524c616f4e5186e5de9d9a49b", size = 2099732, upload-time = "2026-05-06T13:39:31.942Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/06/d5/ee5a3366637fee41dee51a1fc91562dcf12ddbc68fda34e6b253da2324bb/pydantic_core-2.46.4-cp314-cp314-manylinux_2_31_riscv64.whl", hash = "sha256:c1b3f518abeca3aa13c712fd202306e145abf59a18b094a6bafb2d2bbf59192c", size = 2129627, upload-time = "2026-05-06T13:37:25.033Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/94/33/2414be571d2c6a6c4d08be21f9292b6d3fdb08949a97b6dfe985017821db/pydantic_core-2.46.4-cp314-cp314-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:1a7dd0b3ee80d90150e3495a3a13ac34dbcbfd4f012996a6a1d8900e91b5c0fb", size = 2179141, upload-time = "2026-05-06T13:37:14.046Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/79/7daa95be995be0eecc4cf75064cb33f9bbbfe3fe0158caf2f0d4a996a5c7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_aarch64.whl", hash = "sha256:3fb702cd90b0446a3a1c5e470bfa0dd23c0233b676a9099ddcc964fa6ca13898", size = 2184325, upload-time = "2026-05-06T13:36:53.615Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9f/cb/d0a382f5c0de8a222dc61c65348e0ce831b1f68e0a018450d31c2cace3a5/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_armv7l.whl", hash = "sha256:b8458003118a712e66286df6a707db01c52c0f52f7db8e4a38f0da1d3b94fc4e", size = 2323990, upload-time = "2026-05-06T13:40:29.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/db/d9ba624cc4a5aced1598e88c04fdbd8310c8a69b9d38b9a3d39ce3a61ed7/pydantic_core-2.46.4-cp314-cp314-musllinux_1_1_x86_64.whl", hash = "sha256:372429a130e469c9cd698925ce5fc50940b7a1336b0d82038e63d5bbc4edc519", size = 2369978, upload-time = "2026-05-06T13:37:23.027Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/20/d15df15ba918c423461905802bfd2981c3af0bfa0e40d05e13edbfa48bc3/pydantic_core-2.46.4-cp314-cp314-win32.whl", hash = "sha256:85bb3611ff1802f3ee7fdd7dbff26b56f343fb432d57a4728fdd49b6ef35e2f4", size = 1966354, upload-time = "2026-05-06T13:38:03.499Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fc/b6/6b8de4c0a7d7ab3004c439c80c5c1e0a3e8d78bbae19379b01960383d9e5/pydantic_core-2.46.4-cp314-cp314-win_amd64.whl", hash = "sha256:811ff8e9c313ab425368bcbb36e5c4ebd7108c2bbf4e4089cfbb0b01eff63fac", size = 2072238, upload-time = "2026-05-06T13:39:40.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/32/36/51eb763beec1f4cf59b1db243a7dcc39cbb41230f050a09b9d69faaf0a48/pydantic_core-2.46.4-cp314-cp314-win_arm64.whl", hash = "sha256:bfec22eab3c8cc2ceec0248aec886624116dc079afa027ecc8ad4a7e62010f8a", size = 2018251, upload-time = "2026-05-06T13:37:26.72Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/91/855af51d625b23aa987116a19e231d2aaef9c4a415273ddc189b79a45fee/pydantic_core-2.46.4-cp314-cp314t-macosx_10_12_x86_64.whl", hash = "sha256:af8244b2bef6aaad6d92cda81372de7f8c8d36c9f0c3ea36e827c60e7d9467a0", size = 2099593, upload-time = "2026-05-06T13:39:47.682Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/1b/8784a54c65edb5f49f0a14d6977cf1b209bba85a4c77445b255c2de58ab3/pydantic_core-2.46.4-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:5a4330cdbc57162e4b3aa303f588ba752257694c9c9be3e7ebb11b4aca659b5d", size = 1935226, upload-time = "2026-05-06T13:40:40.428Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e8/e7/1955d28d1afc56dd4b3ad7cc0cf39df1b9852964cf16e5d13912756d6d6b/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:29c61fc04a3d840155ff08e475a04809278972fe6aef51e2720554e96367e34b", size = 1974605, upload-time = "2026-05-06T13:37:32.029Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/93/e2/3fedbf0ba7a22850e6e9fd78117f1c0f10f950182344d8a6c535d468fdd8/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_armv7l.manylinux2014_armv7l.whl", hash = "sha256:c50f2528cf200c5eed56faf3f4e22fcd5f38c157a8b78576e6ba3168ec35f000", size = 2030777, upload-time = "2026-05-06T13:38:55.239Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f8/61/46be275fcaaba0b4f5b9669dd852267ce1ff616592dccf7a7845588df091/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:0cbe8b01f948de4286c74cdd6c667aceb38f5c1e26f0693b3983d9d74887c65e", size = 2236641, upload-time = "2026-05-06T13:37:08.096Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/60/db/12e93e46a8bac9988be3c016860f83293daea8c716c029c9ace279036f2f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:617d7e2ca7dcb8c5cf6bcb8c59b8832c94b36196bbf1cbd1bfb56ed341905edd", size = 2286404, upload-time = "2026-05-06T13:40:20.221Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e2/4a/4d8b19008f38d31c53b8219cfedc2e3d5de5fe99d90076b7e767de29274f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:7027560ee92211647d0d34e3f7cd6f50da56399d26a9c8ad0da286d3869a53f3", size = 2109219, upload-time = "2026-05-06T13:38:12.153Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/70/3cbc40978fefb7bb09c6708d40d4ad1a5d70fd7213c3d17f971de868ec1f/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_31_riscv64.whl", hash = "sha256:f99626688942fb746e545232e7726926f3be91b5975f8b55327665fafda991c7", size = 2110594, upload-time = "2026-05-06T13:40:02.971Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/20/b8d36736216e29491125531685b2f9e61aa5b4b2599893f8268551da3338/pydantic_core-2.46.4-cp314-cp314t-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:fc3e9034a63de20e15e8ade85358bc6efc614008cab72898b4b4952bea0509ff", size = 2159542, upload-time = "2026-05-06T13:39:27.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1d/a2/367df868eb584dacf6bf82a389272406d7178e301c4ac82545ab98bc2dd9/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_aarch64.whl", hash = "sha256:97e7cf2be5c77b7d1a9713a05605d49460d02c6078d38d8bef3cbe323c548424", size = 2168146, upload-time = "2026-05-06T13:38:31.93Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/b8/4460f77f7e201893f649a29ab355dddd3beee8a97bcb1a320db414f9a06e/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_armv7l.whl", hash = "sha256:3bf92c5d0e00fefaab325a4d27828fe6b6e2a21848686b5b60d2d9eeb09d76c6", size = 2306309, upload-time = "2026-05-06T13:37:44.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/c4/be2639293acd87dc8ddbcec41a73cee9b2ebf996fe6d892a1a74e88ad3f7/pydantic_core-2.46.4-cp314-cp314t-musllinux_1_1_x86_64.whl", hash = "sha256:3ecbc122d18468d06ca279dc26a8c2e2d5acb10943bb35e36ae92096dc3b5565", size = 2369736, upload-time = "2026-05-06T13:37:05.645Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/30/a6/9f9f380dbb301f67023bf8f707aaa75daadf84f7152d95c410fd7e81d994/pydantic_core-2.46.4-cp314-cp314t-win32.whl", hash = "sha256:e846ae7835bf0703ae43f534ab79a867146dadd59dc9ca5c8b53d5c8f7c9ef02", size = 1955575, upload-time = "2026-05-06T13:38:51.116Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/40/1f/f1eb9eb350e795d1af8586289746f5c5677d16043040d63710e22abc43c9/pydantic_core-2.46.4-cp314-cp314t-win_amd64.whl", hash = "sha256:2108ba5c1c1eca18030634489dc544844144ee36357f2f9f780b93e7ddbb44b5", size = 2051624, upload-time = "2026-05-06T13:38:21.672Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f6/d2/42dd53d0a85c27606f316d3aa5d2869c4e8470a5ed6dec30e4a1abe19192/pydantic_core-2.46.4-cp314-cp314t-win_arm64.whl", hash = "sha256:4fcbe087dbc2068af7eda3aa87634eba216dbda64d1ae73c8684b621d33f6596", size = 2017325, upload-time = "2026-05-06T13:40:52.723Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ee/a4/73995fd4ebbb46ba0ee51e6fa049b8f02c40daebb762208feda8a6b7894d/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_10_12_x86_64.whl", hash = "sha256:14d4edf427bdcf950a8a02d7cb44a08614388dd6e1bdcbf4f67504fa7887da9c", size = 2111589, upload-time = "2026-05-06T13:37:10.817Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fb/7f/f37d3a5e8bfcc2e403f5c57a730f2d815693fb42119e8ea48b3789335af1/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-macosx_11_0_arm64.whl", hash = "sha256:0ce40cd7b21210e99342afafbd4d0f76d784eb5b1d60f3bdc566be4983c6c73b", size = 1944552, upload-time = "2026-05-06T13:36:56.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/15/3c/d7eb777b3ff43e8433a4efb39a17aa8fd98a4ee8561a24a67ef5db07b2d6/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:90884113d8b48f760e9587002789ddd741e76ab9f89518cd1e43b1f1a52ec44b", size = 1982984, upload-time = "2026-05-06T13:39:06.207Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/63/87/70b9f40170a81afd55ca26c9b2acb25c20d64bcfbf888fafecb3ba077d4c/pydantic_core-2.46.4-graalpy311-graalpy242_311_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:66ce7632c22d837c95301830e111ad0128a32b8207533b60896a96c4915192ea", size = 2138417, upload-time = "2026-05-06T13:39:45.476Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/1d/8987ad40f65ae1432753072f214fb5c74fe47ffbd0698bb9cbbb585664f8/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_10_12_x86_64.whl", hash = "sha256:1d8ba486450b14f3b1d63bc521d410ec7565e52f887b9fb671791886436a42f7", size = 2095527, upload-time = "2026-05-06T13:39:52.283Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/64/d3/84c282a7eee1d3ac4c0377546ef5a1ea436ce26840d9ac3b7ed54a377507/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-macosx_11_0_arm64.whl", hash = "sha256:3009f12e4e90b7f88b4f9adb1b0c4a3d58fe7820f3238c190047209d148026df", size = 1936024, upload-time = "2026-05-06T13:40:15.671Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ca/eac61596cdeb4d7e174d3dc0bd8a6238f14f75f97a24e7b7db4c7e7340a0/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ad785e92e6dc634c21555edc8bd6b64957ab844541bcb96a1366c202951ae526", size = 1990696, upload-time = "2026-05-06T13:38:34.717Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/fa/c3/7c8b240552251faf6b3a957db200fcfbbcec36763c050428b601e0c9b83b/pydantic_core-2.46.4-graalpy312-graalpy250_312_native-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:00c603d540afdd6b80eb39f078f33ebd46211f02f33e34a32d9f053bba711de0", size = 2147590, upload-time = "2026-05-06T13:39:29.883Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/11/cb/428de0385b6c8d44b716feba566abfacfbd23ee3c4439faa789a1456242f/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_10_12_x86_64.whl", hash = "sha256:0c563b08bca408dc7f65f700633d8442fffb2421fc47b8101377e9fd65051ff0", size = 2112782, upload-time = "2026-05-06T13:37:04.016Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0b/b5/6a17bdadd0fc1f170adfd05a20d37c832f52b117b4d9131da1f41bb097ce/pydantic_core-2.46.4-pp311-pypy311_pp73-macosx_11_0_arm64.whl", hash = "sha256:db06ffe51636ffe9ca531fe9023dd64bdd794be8754cb5df57c5498ae5b518a7", size = 1952146, upload-time = "2026-05-06T13:39:43.092Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/dc/03734d80e362cd43ef65428e9de77c730ce7f2f11c60d2b1e1b39f0fbf99/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:133878133d271ade3d41d1bfb2a45ec38dbdbda40bc065921c6b04e4630127e2", size = 2134492, upload-time = "2026-05-06T13:36:58.124Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/df/5e5ffc085ed07cc22d298134d3d911c63e91f6a0eb91fe646750a3209910/pydantic_core-2.46.4-pp311-pypy311_pp73-manylinux_2_5_i686.manylinux1_i686.whl", hash = "sha256:9bc519fbf2b7578398853d815009ae5e4d4603d12f4e3f91da8c06852d3da3e9", size = 2156604, upload-time = "2026-05-06T13:37:49.88Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/81/44/6e112a4253e56f5705467cbab7ab5e91ee7398ba3d56d358635958893d3e/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_aarch64.whl", hash = "sha256:c7a7bd4e39e8e4c12c39cd480356842b6a8a06e41b23a55a5e3e191718838ddf", size = 2183828, upload-time = "2026-05-06T13:37:43.053Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ac/ad/5565071e937d8e752842ac241463944c9eb14c87e2d269f2658a5bd05e98/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_armv7l.whl", hash = "sha256:d396ec2b979760aaf3218e76c24e65bd0aca24983298653b3a9d7a45f9e47b30", size = 2310000, upload-time = "2026-05-06T13:37:56.694Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4f/c3/66883a5cec183e7fba4d024b4cbbe61851a63750ef606b0afecc46d1f2bf/pydantic_core-2.46.4-pp311-pypy311_pp73-musllinux_1_1_x86_64.whl", hash = "sha256:86e1a4418c6cd97d60c95c71164158eaf7324fae7b0923264016baa993eba6fc", size = 2361286, upload-time = "2026-05-06T13:40:05.667Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4b/2d/69abac8f838090bbecd5df894befb2c2619e7996a98ddb949db9f3b93225/pydantic_core-2.46.4-pp311-pypy311_pp73-win_amd64.whl", hash = "sha256:d51026d73fcfd93610abc7b27789c26b313920fcfb20e27462d74a7f8b06e983", size = 2193071, upload-time = "2026-05-06T13:38:08.682Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pygments"
|
||||||
|
version = "2.20.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c3/b2/bc9c9196916376152d655522fdcebac55e66de6603a76a02bca1b6414f6c/pygments-2.20.0.tar.gz", hash = "sha256:6757cd03768053ff99f3039c1a36d6c0aa0b263438fcab17520b30a303a82b5f", size = 4955991, upload-time = "2026-03-29T13:29:33.898Z" }
|
||||||
|
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 = "pytest"
|
||||||
|
version = "9.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "colorama", marker = "sys_platform == 'win32'" },
|
||||||
|
{ name = "exceptiongroup", marker = "python_full_version < '3.11'" },
|
||||||
|
{ name = "iniconfig" },
|
||||||
|
{ name = "packaging" },
|
||||||
|
{ name = "pluggy" },
|
||||||
|
{ name = "pygments" },
|
||||||
|
{ name = "tomli", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/7d/0d/549bd94f1a0a402dc8cf64563a117c0f3765662e2e668477624baeec44d5/pytest-9.0.3.tar.gz", hash = "sha256:b86ada508af81d19edeb213c681b1d48246c1a91d304c6c81a427674c17eb91c", size = 1572165, upload-time = "2026-04-07T17:16:18.027Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d4/24/a372aaf5c9b7208e7112038812994107bc65a84cd00e0354a88c2c77a617/pytest-9.0.3-py3-none-any.whl", hash = "sha256:2c5efc453d45394fdd706ade797c0a81091eccd1d6e4bccfcd476e2b8e0ab5d9", size = 375249, upload-time = "2026-04-07T17:16:16.13Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "pyyaml"
|
||||||
|
version = "6.0.3"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/05/8e/961c0007c59b8dd7729d542c61a4d537767a59645b82a0b521206e1e25c2/pyyaml-6.0.3.tar.gz", hash = "sha256:d76623373421df22fb4cf8817020cbb7ef15c725b9d5e45f17e189bfc384190f", size = 130960, upload-time = "2025-09-25T21:33:16.546Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/a0/39350dd17dd6d6c6507025c0e53aef67a9293a6d37d3511f23ea510d5800/pyyaml-6.0.3-cp310-cp310-macosx_10_13_x86_64.whl", hash = "sha256:214ed4befebe12df36bcc8bc2b64b396ca31be9304b8f59e25c11cf94a4c033b", size = 184227, upload-time = "2025-09-25T21:31:46.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/14/52d505b5c59ce73244f59c7a50ecf47093ce4765f116cdb98286a71eeca2/pyyaml-6.0.3-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:02ea2dfa234451bbb8772601d7b8e426c2bfa197136796224e50e35a78777956", size = 174019, upload-time = "2025-09-25T21:31:47.706Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/43/f7/0e6a5ae5599c838c696adb4e6330a59f463265bfa1e116cfd1fbb0abaaae/pyyaml-6.0.3-cp310-cp310-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:b30236e45cf30d2b8e7b3e85881719e98507abed1011bf463a8fa23e9c3e98a8", size = 740646, upload-time = "2025-09-25T21:31:49.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2f/3a/61b9db1d28f00f8fd0ae760459a5c4bf1b941baf714e207b6eb0657d2578/pyyaml-6.0.3-cp310-cp310-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:66291b10affd76d76f54fad28e22e51719ef9ba22b29e1d7d03d6777a9174198", size = 840793, upload-time = "2025-09-25T21:31:50.735Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/1e/7acc4f0e74c4b3d9531e24739e0ab832a5edf40e64fbae1a9c01941cabd7/pyyaml-6.0.3-cp310-cp310-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:9c7708761fccb9397fe64bbc0395abcae8c4bf7b0eac081e12b809bf47700d0b", size = 770293, upload-time = "2025-09-25T21:31:51.828Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/ef/abd085f06853af0cd59fa5f913d61a8eab65d7639ff2a658d18a25d6a89d/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:418cf3f2111bc80e0933b2cd8cd04f286338bb88bdc7bc8e6dd775ebde60b5e0", size = 732872, upload-time = "2025-09-25T21:31:53.282Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1f/15/2bc9c8faf6450a8b3c9fc5448ed869c599c0a74ba2669772b1f3a0040180/pyyaml-6.0.3-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:5e0b74767e5f8c593e8c9b5912019159ed0533c70051e9cce3e8b6aa699fcd69", size = 758828, upload-time = "2025-09-25T21:31:54.807Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/a3/00/531e92e88c00f4333ce359e50c19b8d1de9fe8d581b1534e35ccfbc5f393/pyyaml-6.0.3-cp310-cp310-win32.whl", hash = "sha256:28c8d926f98f432f88adc23edf2e6d4921ac26fb084b028c733d01868d19007e", size = 142415, upload-time = "2025-09-25T21:31:55.885Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/2a/fa/926c003379b19fca39dd4634818b00dec6c62d87faf628d1394e137354d4/pyyaml-6.0.3-cp310-cp310-win_amd64.whl", hash = "sha256:bdb2c67c6c1390b63c6ff89f210c8fd09d9a1217a465701eac7316313c915e4c", size = 158561, upload-time = "2025-09-25T21:31:57.406Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/16/a95b6757765b7b031c9374925bb718d55e0a9ba8a1b6a12d25962ea44347/pyyaml-6.0.3-cp311-cp311-macosx_10_13_x86_64.whl", hash = "sha256:44edc647873928551a01e7a563d7452ccdebee747728c1080d881d68af7b997e", size = 185826, upload-time = "2025-09-25T21:31:58.655Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/19/13de8e4377ed53079ee996e1ab0a9c33ec2faf808a4647b7b4c0d46dd239/pyyaml-6.0.3-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:652cb6edd41e718550aad172851962662ff2681490a8a711af6a4d288dd96824", size = 175577, upload-time = "2025-09-25T21:32:00.088Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/0c/62/d2eb46264d4b157dae1275b573017abec435397aa59cbcdab6fc978a8af4/pyyaml-6.0.3-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:10892704fc220243f5305762e276552a0395f7beb4dbf9b14ec8fd43b57f126c", size = 775556, upload-time = "2025-09-25T21:32:01.31Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/cb/16c3f2cf3266edd25aaa00d6c4350381c8b012ed6f5276675b9eba8d9ff4/pyyaml-6.0.3-cp311-cp311-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:850774a7879607d3a6f50d36d04f00ee69e7fc816450e5f7e58d7f17f1ae5c00", size = 882114, upload-time = "2025-09-25T21:32:03.376Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/71/60/917329f640924b18ff085ab889a11c763e0b573da888e8404ff486657602/pyyaml-6.0.3-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:b8bb0864c5a28024fac8a632c443c87c5aa6f215c0b126c449ae1a150412f31d", size = 806638, upload-time = "2025-09-25T21:32:04.553Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/6f/529b0f316a9fd167281a6c3826b5583e6192dba792dd55e3203d3f8e655a/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:1d37d57ad971609cf3c53ba6a7e365e40660e3be0e5175fa9f2365a379d6095a", size = 767463, upload-time = "2025-09-25T21:32:06.152Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f2/6a/b627b4e0c1dd03718543519ffb2f1deea4a1e6d42fbab8021936a4d22589/pyyaml-6.0.3-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:37503bfbfc9d2c40b344d06b2199cf0e96e97957ab1c1b546fd4f87e53e5d3e4", size = 794986, upload-time = "2025-09-25T21:32:07.367Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/91/47a6e1c42d9ee337c4839208f30d9f09caa9f720ec7582917b264defc875/pyyaml-6.0.3-cp311-cp311-win32.whl", hash = "sha256:8098f252adfa6c80ab48096053f512f2321f0b998f98150cea9bd23d83e1467b", size = 142543, upload-time = "2025-09-25T21:32:08.95Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/e3/ea007450a105ae919a72393cb06f122f288ef60bba2dc64b26e2646fa315/pyyaml-6.0.3-cp311-cp311-win_amd64.whl", hash = "sha256:9f3bfb4965eb874431221a3ff3fdcddc7e74e3b07799e0e84ca4a0f867d449bf", size = 158763, upload-time = "2025-09-25T21:32:09.96Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/33/422b98d2195232ca1826284a76852ad5a86fe23e31b009c9886b2d0fb8b2/pyyaml-6.0.3-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:7f047e29dcae44602496db43be01ad42fc6f1cc0d8cd6c83d342306c32270196", size = 182063, upload-time = "2025-09-25T21:32:11.445Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/89/a0/6cf41a19a1f2f3feab0e9c0b74134aa2ce6849093d5517a0c550fe37a648/pyyaml-6.0.3-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:fc09d0aa354569bc501d4e787133afc08552722d3ab34836a80547331bb5d4a0", size = 173973, upload-time = "2025-09-25T21:32:12.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ed/23/7a778b6bd0b9a8039df8b1b1d80e2e2ad78aa04171592c8a5c43a56a6af4/pyyaml-6.0.3-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:9149cad251584d5fb4981be1ecde53a1ca46c891a79788c0df828d2f166bda28", size = 775116, upload-time = "2025-09-25T21:32:13.652Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/65/30/d7353c338e12baef4ecc1b09e877c1970bd3382789c159b4f89d6a70dc09/pyyaml-6.0.3-cp312-cp312-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:5fdec68f91a0c6739b380c83b951e2c72ac0197ace422360e6d5a959d8d97b2c", size = 844011, upload-time = "2025-09-25T21:32:15.21Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/9d/b3589d3877982d4f2329302ef98a8026e7f4443c765c46cfecc8858c6b4b/pyyaml-6.0.3-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:ba1cc08a7ccde2d2ec775841541641e4548226580ab850948cbfda66a1befcdc", size = 807870, upload-time = "2025-09-25T21:32:16.431Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/c0/b3be26a015601b822b97d9149ff8cb5ead58c66f981e04fedf4e762f4bd4/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:8dc52c23056b9ddd46818a57b78404882310fb473d63f17b07d5c40421e47f8e", size = 761089, upload-time = "2025-09-25T21:32:17.56Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/be/8e/98435a21d1d4b46590d5459a22d88128103f8da4c2d4cb8f14f2a96504e1/pyyaml-6.0.3-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:41715c910c881bc081f1e8872880d3c650acf13dfa8214bad49ed4cede7c34ea", size = 790181, upload-time = "2025-09-25T21:32:18.834Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/93/7baea19427dcfbe1e5a372d81473250b379f04b1bd3c4c5ff825e2327202/pyyaml-6.0.3-cp312-cp312-win32.whl", hash = "sha256:96b533f0e99f6579b3d4d4995707cf36df9100d67e0c8303a0c55b27b5f99bc5", size = 137658, upload-time = "2025-09-25T21:32:20.209Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/86/bf/899e81e4cce32febab4fb42bb97dcdf66bc135272882d1987881a4b519e9/pyyaml-6.0.3-cp312-cp312-win_amd64.whl", hash = "sha256:5fcd34e47f6e0b794d17de1b4ff496c00986e1c83f7ab2fb8fcfe9616ff7477b", size = 154003, upload-time = "2025-09-25T21:32:21.167Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/08/67bd04656199bbb51dbed1439b7f27601dfb576fb864099c7ef0c3e55531/pyyaml-6.0.3-cp312-cp312-win_arm64.whl", hash = "sha256:64386e5e707d03a7e172c0701abfb7e10f0fb753ee1d773128192742712a98fd", size = 140344, upload-time = "2025-09-25T21:32:22.617Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d1/11/0fd08f8192109f7169db964b5707a2f1e8b745d4e239b784a5a1dd80d1db/pyyaml-6.0.3-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:8da9669d359f02c0b91ccc01cac4a67f16afec0dac22c2ad09f46bee0697eba8", size = 181669, upload-time = "2025-09-25T21:32:23.673Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/16/95309993f1d3748cd644e02e38b75d50cbc0d9561d21f390a76242ce073f/pyyaml-6.0.3-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:2283a07e2c21a2aa78d9c4442724ec1eb15f5e42a723b99cb3d822d48f5f7ad1", size = 173252, upload-time = "2025-09-25T21:32:25.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/50/31/b20f376d3f810b9b2371e72ef5adb33879b25edb7a6d072cb7ca0c486398/pyyaml-6.0.3-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ee2922902c45ae8ccada2c5b501ab86c36525b883eff4255313a253a3160861c", size = 767081, upload-time = "2025-09-25T21:32:26.575Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/49/1e/a55ca81e949270d5d4432fbbd19dfea5321eda7c41a849d443dc92fd1ff7/pyyaml-6.0.3-cp313-cp313-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a33284e20b78bd4a18c8c2282d549d10bc8408a2a7ff57653c0cf0b9be0afce5", size = 841159, upload-time = "2025-09-25T21:32:27.727Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/74/27/e5b8f34d02d9995b80abcef563ea1f8b56d20134d8f4e5e81733b1feceb2/pyyaml-6.0.3-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:0f29edc409a6392443abf94b9cf89ce99889a1dd5376d94316ae5145dfedd5d6", size = 801626, upload-time = "2025-09-25T21:32:28.878Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f9/11/ba845c23988798f40e52ba45f34849aa8a1f2d4af4b798588010792ebad6/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f7057c9a337546edc7973c0d3ba84ddcdf0daa14533c2065749c9075001090e6", size = 753613, upload-time = "2025-09-25T21:32:30.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3d/e0/7966e1a7bfc0a45bf0a7fb6b98ea03fc9b8d84fa7f2229e9659680b69ee3/pyyaml-6.0.3-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:eda16858a3cab07b80edaf74336ece1f986ba330fdb8ee0d6c0d68fe82bc96be", size = 794115, upload-time = "2025-09-25T21:32:31.353Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/de/94/980b50a6531b3019e45ddeada0626d45fa85cbe22300844a7983285bed3b/pyyaml-6.0.3-cp313-cp313-win32.whl", hash = "sha256:d0eae10f8159e8fdad514efdc92d74fd8d682c933a6dd088030f3834bc8e6b26", size = 137427, upload-time = "2025-09-25T21:32:32.58Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/97/c9/39d5b874e8b28845e4ec2202b5da735d0199dbe5b8fb85f91398814a9a46/pyyaml-6.0.3-cp313-cp313-win_amd64.whl", hash = "sha256:79005a0d97d5ddabfeeea4cf676af11e647e41d81c9a7722a193022accdb6b7c", size = 154090, upload-time = "2025-09-25T21:32:33.659Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/73/e8/2bdf3ca2090f68bb3d75b44da7bbc71843b19c9f2b9cb9b0f4ab7a5a4329/pyyaml-6.0.3-cp313-cp313-win_arm64.whl", hash = "sha256:5498cd1645aa724a7c71c8f378eb29ebe23da2fc0d7a08071d89469bf1d2defb", size = 140246, upload-time = "2025-09-25T21:32:34.663Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/9d/8c/f4bd7f6465179953d3ac9bc44ac1a8a3e6122cf8ada906b4f96c60172d43/pyyaml-6.0.3-cp314-cp314-macosx_10_13_x86_64.whl", hash = "sha256:8d1fab6bb153a416f9aeb4b8763bc0f22a5586065f86f7664fc23339fc1c1fac", size = 181814, upload-time = "2025-09-25T21:32:35.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/bd/9c/4d95bb87eb2063d20db7b60faa3840c1b18025517ae857371c4dd55a6b3a/pyyaml-6.0.3-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:34d5fcd24b8445fadc33f9cf348c1047101756fd760b4dacb5c3e99755703310", size = 173809, upload-time = "2025-09-25T21:32:36.789Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/92/b5/47e807c2623074914e29dabd16cbbdd4bf5e9b2db9f8090fa64411fc5382/pyyaml-6.0.3-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:501a031947e3a9025ed4405a168e6ef5ae3126c59f90ce0cd6f2bfc477be31b7", size = 766454, upload-time = "2025-09-25T21:32:37.966Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/9e/e5e9b168be58564121efb3de6859c452fccde0ab093d8438905899a3a483/pyyaml-6.0.3-cp314-cp314-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:b3bc83488de33889877a0f2543ade9f70c67d66d9ebb4ac959502e12de895788", size = 836355, upload-time = "2025-09-25T21:32:39.178Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/f9/16491d7ed2a919954993e48aa941b200f38040928474c9e85ea9e64222c3/pyyaml-6.0.3-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:c458b6d084f9b935061bc36216e8a69a7e293a2f1e68bf956dcd9e6cbcd143f5", size = 794175, upload-time = "2025-09-25T21:32:40.865Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dd/3f/5989debef34dc6397317802b527dbbafb2b4760878a53d4166579111411e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7c6610def4f163542a622a73fb39f534f8c101d690126992300bf3207eab9764", size = 755228, upload-time = "2025-09-25T21:32:42.084Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d7/ce/af88a49043cd2e265be63d083fc75b27b6ed062f5f9fd6cdc223ad62f03e/pyyaml-6.0.3-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:5190d403f121660ce8d1d2c1bb2ef1bd05b5f68533fc5c2ea899bd15f4399b35", size = 789194, upload-time = "2025-09-25T21:32:43.362Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/23/20/bb6982b26a40bb43951265ba29d4c246ef0ff59c9fdcdf0ed04e0687de4d/pyyaml-6.0.3-cp314-cp314-win_amd64.whl", hash = "sha256:4a2e8cebe2ff6ab7d1050ecd59c25d4c8bd7e6f400f5f82b96557ac0abafd0ac", size = 156429, upload-time = "2025-09-25T21:32:57.844Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/f4/a4541072bb9422c8a883ab55255f918fa378ecf083f5b85e87fc2b4eda1b/pyyaml-6.0.3-cp314-cp314-win_arm64.whl", hash = "sha256:93dda82c9c22deb0a405ea4dc5f2d0cda384168e466364dec6255b293923b2f3", size = 143912, upload-time = "2025-09-25T21:32:59.247Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7c/f9/07dd09ae774e4616edf6cda684ee78f97777bdd15847253637a6f052a62f/pyyaml-6.0.3-cp314-cp314t-macosx_10_13_x86_64.whl", hash = "sha256:02893d100e99e03eda1c8fd5c441d8c60103fd175728e23e431db1b589cf5ab3", size = 189108, upload-time = "2025-09-25T21:32:44.377Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/4e/78/8d08c9fb7ce09ad8c38ad533c1191cf27f7ae1effe5bb9400a46d9437fcf/pyyaml-6.0.3-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:c1ff362665ae507275af2853520967820d9124984e0f7466736aea23d8611fba", size = 183641, upload-time = "2025-09-25T21:32:45.407Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/5b/3babb19104a46945cf816d047db2788bcaf8c94527a805610b0289a01c6b/pyyaml-6.0.3-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:6adc77889b628398debc7b65c073bcb99c4a0237b248cacaf3fe8a557563ef6c", size = 831901, upload-time = "2025-09-25T21:32:48.83Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8b/cc/dff0684d8dc44da4d22a13f35f073d558c268780ce3c6ba1b87055bb0b87/pyyaml-6.0.3-cp314-cp314t-manylinux2014_s390x.manylinux_2_17_s390x.manylinux_2_28_s390x.whl", hash = "sha256:a80cb027f6b349846a3bf6d73b5e95e782175e52f22108cfa17876aaeff93702", size = 861132, upload-time = "2025-09-25T21:32:50.149Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b1/5e/f77dc6b9036943e285ba76b49e118d9ea929885becb0a29ba8a7c75e29fe/pyyaml-6.0.3-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:00c4bdeba853cc34e7dd471f16b4114f4162dc03e6b7afcc2128711f0eca823c", size = 839261, upload-time = "2025-09-25T21:32:51.808Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/88/a9db1376aa2a228197c58b37302f284b5617f56a5d959fd1763fb1675ce6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:66e1674c3ef6f541c35191caae2d429b967b99e02040f5ba928632d9a7f0f065", size = 805272, upload-time = "2025-09-25T21:32:52.941Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/da/92/1446574745d74df0c92e6aa4a7b0b3130706a4142b2d1a5869f2eaa423c6/pyyaml-6.0.3-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:16249ee61e95f858e83976573de0f5b2893b3677ba71c9dd36b9cf8be9ac6d65", size = 829923, upload-time = "2025-09-25T21:32:54.537Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f0/7a/1c7270340330e575b92f397352af856a8c06f230aa3e76f86b39d01b416a/pyyaml-6.0.3-cp314-cp314t-win_amd64.whl", hash = "sha256:4ad1906908f2f5ae4e5a8ddfce73c320c2a1429ec52eafd27138b7f1cbe341c9", size = 174062, upload-time = "2025-09-25T21:32:55.767Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f1/12/de94a39c2ef588c7e6455cfbe7343d3b2dc9d6b6b2f40c4c6565744c873d/pyyaml-6.0.3-cp314-cp314t-win_arm64.whl", hash = "sha256:ebc55a14a21cb14062aa4162f906cd962b28e2e9ea38f9b4391244cd8de4ae0b", size = 149341, upload-time = "2025-09-25T21:32:56.828Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "starlette"
|
||||||
|
version = "1.3.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "anyio" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.13'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/eb/e3/7c1dc7381d9f8ab7d854328ebfa884e62cb3f3d8549ddfd37c7814f42afa/starlette-1.3.1.tar.gz", hash = "sha256:05d0213193f2fbaae60e2ecb593b4add4262ad4e46536b54abe36f11a71724e0", size = 2703240, upload-time = "2026-06-12T09:23:11.602Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ec/bb/2799cc2ede3ed41131f8975621e7213dfc7ef4acbbaadfa440f32500c370/starlette-1.3.1-py3-none-any.whl", hash = "sha256:c7372aae11c3c3f26a42df7bd626cec2f47d03483d261d369516a615a53714c6", size = 73632, upload-time = "2026-06-12T09:23:10.017Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "tomli"
|
||||||
|
version = "2.4.1"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" },
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "typing-extensions"
|
||||||
|
version = "4.15.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/72/94/1a15dd82efb362ac84269196e94cf00f187f7ed21c242792a923cdb1c61f/typing_extensions-4.15.0.tar.gz", hash = "sha256:0cea48d173cc12fa28ecabc3b837ea3cf6f38c6d1136f85cbaaf598984861466", size = 109391, upload-time = "2025-08-25T13:49:26.313Z" }
|
||||||
|
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 = "typing-inspection"
|
||||||
|
version = "0.4.2"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "typing-extensions" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/55/e3/70399cb7dd41c10ac53367ae42139cf4b1ca5f36bb3dc6c9d33acdb43655/typing_inspection-0.4.2.tar.gz", hash = "sha256:ba561c48a67c5958007083d386c3295464928b01faa735ab8547c5692e87f464", size = 75949, upload-time = "2025-10-01T02:14:41.687Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/dc/9b/47798a6c91d8bdb567fe2698fe81e0c6b7cb7ef4d13da4114b41d239f65d/typing_inspection-0.4.2-py3-none-any.whl", hash = "sha256:4ed1cacbdc298c220f1bd249ed5287caa16f34d44ef4e9c3d0cbad5b521545e7", size = 14611, upload-time = "2025-10-01T02:14:40.154Z" },
|
||||||
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "uvicorn"
|
||||||
|
version = "0.49.0"
|
||||||
|
source = { registry = "https://pypi.org/simple" }
|
||||||
|
dependencies = [
|
||||||
|
{ name = "click" },
|
||||||
|
{ name = "h11" },
|
||||||
|
{ name = "typing-extensions", marker = "python_full_version < '3.11'" },
|
||||||
|
]
|
||||||
|
sdist = { url = "https://files.pythonhosted.org/packages/c4/1f/fa18009dea8469069cca78a4e877a008ab78f08b064bfc9ab891579077ff/uvicorn-0.49.0.tar.gz", hash = "sha256:ebf4271aa580d9de97f93192d4595176df6e91f9aae919ca73e4fc07df1e66a3", size = 91284, upload-time = "2026-06-03T22:01:30.448Z" }
|
||||||
|
wheels = [
|
||||||
|
{ url = "https://files.pythonhosted.org/packages/88/fa/e1388bbcf24ef3274f45c0c1c7b501fd14971037c1b6ee23610553307497/uvicorn-0.49.0-py3-none-any.whl", hash = "sha256:ba3d14c3ee7e41c6c654c46c9eb489d33213cdd30aa1696eab1374337c13f68f", size = 71376, upload-time = "2026-06-03T22:01:29.037Z" },
|
||||||
|
]
|
||||||
Reference in New Issue
Block a user