Merge branch 'main' of github.com:adish-rmr/cosmoguard_backend

test
This commit is contained in:
Adish 2026-03-12 22:25:05 +01:00
commit a19e515daa
10 changed files with 491 additions and 48 deletions

View file

@ -19,17 +19,21 @@ dependencies = [
"psycopg2-binary>=2.9.11", "psycopg2-binary>=2.9.11",
"pubchemprops>=0.1.1", "pubchemprops>=0.1.1",
"pubchempy>=1.0.5", "pubchempy>=1.0.5",
"pydantic>=2.11.10", "pydantic[email]>=2.11.10",
"pymongo>=4.15.2", "pymongo>=4.15.2",
"pytest>=8.4.2", "pytest>=8.4.2",
"pytest-cov>=7.0.0", "pytest-cov>=7.0.0",
"pytest-mock>=3.15.1", "pytest-mock>=3.15.1",
"python-dotenv>=1.2.1", "python-dotenv>=1.2.1",
"httpx>=0.27.0",
"PyJWT>=2.9.0",
"requests>=2.32.5", "requests>=2.32.5",
"streamlit>=1.50.0", "streamlit>=1.50.0",
"extra-streamlit-components>=0.1.71",
"openpyxl>=3.1.0", "openpyxl>=3.1.0",
"uvicorn>=0.35.0", "uvicorn>=0.35.0",
"weasyprint>=66.0", "weasyprint>=66.0",
"cryptography>=46.0.5",
] ]
[project.scripts] [project.scripts]

View file

@ -0,0 +1,102 @@
import os
import httpx
from fastapi import APIRouter, HTTPException, status
from pydantic import BaseModel, EmailStr
from pif_compiler.functions.common_log import get_logger
logger = get_logger()
router = APIRouter()
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
SUPABASE_SECRET_KEY = os.getenv("SUPABASE_SECRET_KEY", "")
_SUPABASE_HEADERS = {
"apikey": SUPABASE_SECRET_KEY,
"Content-Type": "application/json",
}
class LoginRequest(BaseModel):
email: EmailStr
password: str
class RefreshRequest(BaseModel):
refresh_token: str
def _supabase_auth_url(path: str) -> str:
return f"{SUPABASE_URL}/auth/v1/{path}"
@router.post("/auth/login", tags=["Auth"])
async def login(request: LoginRequest):
"""Autentica con email e password. Ritorna access_token e refresh_token."""
async with httpx.AsyncClient() as client:
resp = await client.post(
_supabase_auth_url("token?grant_type=password"),
headers=_SUPABASE_HEADERS,
json={"email": request.email, "password": request.password},
timeout=10,
)
if resp.status_code == 400:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Credenziali non valide"
)
if resp.status_code != 200:
logger.error(f"login: Supabase HTTP {resp.status_code}{resp.text}")
raise HTTPException(
status_code=status.HTTP_502_BAD_GATEWAY,
detail="Errore nel servizio di autenticazione"
)
data = resp.json()
logger.info(f"login: accesso di {request.email}")
return {
"access_token": data["access_token"],
"refresh_token": data["refresh_token"],
"expires_in": data["expires_in"],
"token_type": "bearer",
}
@router.post("/auth/refresh", tags=["Auth"])
async def refresh(request: RefreshRequest):
"""Rinnova l'access_token con il refresh_token."""
async with httpx.AsyncClient() as client:
resp = await client.post(
_supabase_auth_url("token?grant_type=refresh_token"),
headers=_SUPABASE_HEADERS,
json={"refresh_token": request.refresh_token},
timeout=10,
)
if resp.status_code != 200:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Refresh token non valido o scaduto — effettua nuovamente il login"
)
data = resp.json()
return {
"access_token": data["access_token"],
"refresh_token": data["refresh_token"],
"expires_in": data["expires_in"],
"token_type": "bearer",
}
@router.post("/auth/logout", tags=["Auth"])
async def logout(request: RefreshRequest):
"""Invalida la sessione su Supabase tramite il refresh_token."""
async with httpx.AsyncClient() as client:
await client.post(
_supabase_auth_url("logout"),
headers=_SUPABASE_HEADERS,
json={"refresh_token": request.refresh_token},
timeout=10,
)
return {"success": True}

View file

@ -3,7 +3,7 @@ from pydantic import BaseModel, Field
from typing import List, Optional, Dict, Any from typing import List, Optional, Dict, Any
from pif_compiler.classes.models import Ingredient, ToxIndicator from pif_compiler.classes.models import Ingredient, ToxIndicator
from pif_compiler.functions.db_utils import get_all_ingredienti, get_all_clienti, upsert_cliente from pif_compiler.functions.db_utils import get_all_ingredienti, get_all_clienti, upsert_cliente, delete_cliente
from pif_compiler.functions.common_log import get_logger from pif_compiler.functions.common_log import get_logger
logger = get_logger() logger = get_logger()
@ -206,6 +206,29 @@ async def list_clients():
) )
@router.delete("/ingredients/clients/{nome_cliente}", tags=["Clients"])
async def remove_client(nome_cliente: str):
"""Elimina un cliente per nome. Fallisce se ha ordini collegati."""
result = delete_cliente(nome_cliente)
if result is None:
raise HTTPException(
status_code=status.HTTP_409_CONFLICT,
detail=f"Il cliente '{nome_cliente}' ha ordini collegati e non può essere eliminato"
)
if result is False:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Errore interno durante l'eliminazione"
)
if not result:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail=f"Cliente '{nome_cliente}' non trovato"
)
logger.info(f"Cliente '{nome_cliente}' eliminato")
return {"success": True, "nome_cliente": nome_cliente}
@router.post("/ingredients/clients", response_model=ClientCreateResponse, tags=["Clients"]) @router.post("/ingredients/clients", response_model=ClientCreateResponse, tags=["Clients"])
async def create_client(request: ClientCreateRequest): async def create_client(request: ClientCreateRequest):
"""Crea o recupera un cliente. Ritorna id_cliente.""" """Crea o recupera un cliente. Ritorna id_cliente."""

View file

@ -1,13 +1,15 @@
from fastapi import APIRouter, HTTPException, status from fastapi import APIRouter, HTTPException, status
from fastapi.responses import FileResponse from fastapi.responses import FileResponse
from pydantic import BaseModel, Field, HttpUrl from pydantic import BaseModel, Field, HttpUrl
from typing import Optional, Dict, Any from typing import Optional, Dict, Any, Literal
from datetime import datetime as dt
import os import os
from pif_compiler.functions.common_func import generate_pdf from pif_compiler.functions.common_func import generate_pdf
from pif_compiler.services.srv_pubchem import pubchem_dap from pif_compiler.services.srv_pubchem import pubchem_dap
from pif_compiler.services.srv_cir import search_ingredient from pif_compiler.services.srv_cir import search_ingredient
from pif_compiler.functions.common_log import get_logger from pif_compiler.functions.common_log import get_logger
from pif_compiler.functions.db_utils import db_connect
logger = get_logger() logger = get_logger()
@ -269,6 +271,42 @@ async def cir_search_endpoint(request: CirSearchRequest):
) )
class SegnalazioneRequest(BaseModel):
cas: Optional[str] = None
page: str
description: str
error: Optional[str] = None
priority: Literal["bassa", "media", "alta"] = "media"
@router.post("/common/segnalazione", tags=["Common"])
async def create_segnalazione(request: SegnalazioneRequest):
"""Salva una segnalazione/ticket nella collection MongoDB 'segnalazioni'."""
collection = db_connect(collection_name="segnalazioni")
if collection is None:
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Connessione MongoDB fallita"
)
doc = {
**request.model_dump(),
"created_at": dt.now().isoformat(),
"stato": "aperta",
}
try:
result = collection.insert_one(doc)
logger.info(f"Segnalazione creata: id={result.inserted_id}, page={request.page}, priority={request.priority}")
return {"success": True, "id": str(result.inserted_id)}
except Exception as e:
logger.error(f"create_segnalazione: errore MongoDB — {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Errore nel salvataggio della segnalazione"
)
@router.get("/common/health", tags=["Common"]) @router.get("/common/health", tags=["Common"])
async def common_health_check(): async def common_health_check():
""" """

View file

@ -247,8 +247,33 @@ class Toxicity(BaseModel):
@model_validator(mode='after') @model_validator(mode='after')
def set_best_case(self) -> 'Toxicity': def set_best_case(self) -> 'Toxicity':
if self.indicators: if not self.indicators:
self.best_case = max(self.indicators, key=lambda x: x.priority_rank) return self
# 1. Pick highest priority rank (NOAEL=4 > LOAEL=3 > LD50=1)
max_rank = max(x.priority_rank for x in self.indicators)
top = [x for x in self.indicators if x.priority_rank == max_rank]
# 2. Tiebreak by route preference: dermal > oral > inhalation > other
def _route_order(ind: ToxIndicator) -> int:
r = ind.route.lower()
if "dermal" in r:
return 1
if "oral" in r:
return 2
if "inhalation" in r:
return 3
return 4
best_route = min(_route_order(x) for x in top)
top = [x for x in top if _route_order(x) == best_route]
# 3. For NOAEL/LOAEL, pick the lowest value (most conservative)
if top[0].indicator in ('NOAEL', 'LOAEL'):
self.best_case = min(top, key=lambda x: x.value)
else:
self.best_case = top[0]
self.factor = self.best_case.factor self.factor = self.best_case.factor
return self return self
@ -504,7 +529,7 @@ class Esposition(BaseModel):
sup_esposta: int = Field(ge=1, le=20000, description="Area di applicazione in cm2") sup_esposta: int = Field(ge=1, le=20000, description="Area di applicazione in cm2")
freq_applicazione: float = Field(default=1, description="Numero di applicazioni al giorno") freq_applicazione: float = Field(default=1, description="Numero di applicazioni al giorno")
qta_giornaliera: float = Field(..., description="Quantità di prodotto applicata (g/die)") qta_giornaliera: float = Field(..., description="Quantità di prodotto applicata (g/die)")
ritenzione: float = Field(default=1.0, ge=0, le=1.0, description="Fattore di ritenzione") ritenzione: float = Field(default=1.0, ge=0.01, le=1.0, description="Fattore di ritenzione")
note: Optional[str] = None note: Optional[str] = None

View file

@ -0,0 +1,70 @@
import os
import jwt
from jwt import PyJWKClient
from fastapi import Request, HTTPException, status
from pif_compiler.functions.common_log import get_logger
logger = get_logger()
SUPABASE_URL = os.getenv("SUPABASE_URL", "")
# Client JWKS inizializzato una volta sola — caching automatico delle chiavi pubbliche
_jwks_client: PyJWKClient | None = None
def _get_jwks_client() -> PyJWKClient:
global _jwks_client
if _jwks_client is None:
if not SUPABASE_URL:
logger.error("SUPABASE_URL non configurato")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Configurazione auth mancante sul server"
)
_jwks_client = PyJWKClient(f"{SUPABASE_URL}/auth/v1/.well-known/jwks.json")
return _jwks_client
def verify_jwt(token: str) -> dict:
"""Verifica il JWT Supabase tramite JWKS (RS256/ES256). Ritorna il payload decodificato."""
try:
client = _get_jwks_client()
signing_key = client.get_signing_key_from_jwt(token)
payload = jwt.decode(
token,
signing_key.key,
algorithms=["RS256", "ES256"],
audience="authenticated",
)
return payload
except jwt.ExpiredSignatureError:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Token scaduto"
)
except jwt.InvalidTokenError as e:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail=f"Token non valido: {e}"
)
except HTTPException:
raise
except Exception as e:
logger.error(f"verify_jwt: errore inatteso — {e}")
raise HTTPException(
status_code=status.HTTP_500_INTERNAL_SERVER_ERROR,
detail="Errore nella verifica del token"
)
def get_current_user(request: Request) -> dict:
"""FastAPI dependency: estrae e verifica il JWT dall'header Authorization."""
auth_header = request.headers.get("Authorization", "")
if not auth_header.startswith("Bearer "):
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authorization header mancante o malformato"
)
token = auth_header.removeprefix("Bearer ")
return verify_jwt(token)

View file

@ -130,22 +130,21 @@ async def generate_project_source_pdfs(project, output_dir: str = "pdfs") -> lis
log.warning(f"PDF tox non generato per {pdf_name}") log.warning(f"PDF tox non generato per {pdf_name}")
# --- COSING PDF --- # --- COSING PDF ---
# Un solo PDF per ingrediente (il primo CosingInfo con reference valida).
if ing.cosing_info: if ing.cosing_info:
seen_refs = set()
for cosing in ing.cosing_info:
if not cosing.reference or cosing.reference in seen_refs:
continue
seen_refs.add(cosing.reference)
pdf_name = f"{pi.cas}_cosing" pdf_name = f"{pi.cas}_cosing"
pdf_path = os.path.join(output_dir, f"{pdf_name}.pdf") pdf_path = os.path.join(output_dir, f"{pdf_name}.pdf")
if os.path.exists(pdf_path): if os.path.exists(pdf_path):
generated.append(pdf_path) generated.append(pdf_path)
continue else:
reference = next(
log.info(f"Download COSING PDF: {pdf_name} (reference={cosing.reference})") (c.reference for c in ing.cosing_info if c.reference),
content = cosing_download(cosing.reference) None
)
if reference:
log.info(f"Download COSING PDF: {pdf_name} (reference={reference})")
content = cosing_download(reference)
if isinstance(content, bytes): if isinstance(content, bytes):
with open(pdf_path, 'wb') as f: with open(pdf_path, 'wb') as f:
f.write(content) f.write(content)
@ -190,10 +189,14 @@ def create_sources_zip(pdf_paths: list, zip_path: str) -> str:
if zip_dir: if zip_dir:
os.makedirs(zip_dir, exist_ok=True) os.makedirs(zip_dir, exist_ok=True)
seen_names: set[str] = set()
with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf: with zipfile.ZipFile(zip_path, 'w', zipfile.ZIP_DEFLATED) as zf:
for path in pdf_paths: for path in pdf_paths:
if os.path.exists(path): name = os.path.basename(path)
zf.write(path, os.path.basename(path)) if not os.path.exists(path) or name in seen_names:
continue
seen_names.add(name)
zf.write(path, name)
log.info(f"ZIP creato: {zip_path} ({len(pdf_paths)} file)") log.info(f"ZIP creato: {zip_path} ({len(pdf_paths)} file)")
return zip_path return zip_path

View file

@ -170,6 +170,28 @@ def get_all_clienti():
logger.error(f"Errore recupero clienti: {e}") logger.error(f"Errore recupero clienti: {e}")
return [] return []
def delete_cliente(nome_cliente: str) -> bool:
"""Elimina un cliente per nome. Ritorna None se ha ordini collegati."""
try:
conn = postgres_connect()
with conn.cursor() as cur:
cur.execute(
"SELECT COUNT(*) FROM ordini o JOIN clienti c ON o.id_cliente = c.id_cliente WHERE c.nome_cliente = %s",
(nome_cliente,)
)
count = cur.fetchone()[0]
if count > 0:
conn.close()
return None # segnale: cliente ha ordini, non eliminabile
cur.execute("DELETE FROM clienti WHERE nome_cliente = %s RETURNING id_cliente", (nome_cliente,))
deleted = cur.fetchone()
conn.commit()
conn.close()
return deleted is not None
except Exception as e:
logger.error(f"Errore eliminazione cliente '{nome_cliente}': {e}")
return False
def log_ricerche(cas, target, esito): def log_ricerche(cas, target, esito):
try: try:
conn = postgres_connect() conn = postgres_connect()

View file

@ -1,14 +1,20 @@
from fastapi import FastAPI, Request, status from fastapi import Depends, FastAPI, Request, status
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.middleware.trustedhost import TrustedHostMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError from fastapi.exceptions import RequestValidationError
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
import os
import time import time
from dotenv import load_dotenv
from pif_compiler.functions.common_log import get_logger from pif_compiler.functions.common_log import get_logger
load_dotenv()
# Import dei tuoi router # Import dei tuoi router
from pif_compiler.api.routes import api_echa, api_cosing, common, api_ingredients, api_esposition, api_orders from pif_compiler.api.routes import api_echa, api_cosing, common, api_ingredients, api_esposition, api_orders, api_auth
from pif_compiler.functions.auth import get_current_user
# Configurazione logging # Configurazione logging
logger = get_logger() logger = get_logger()
@ -31,28 +37,40 @@ async def lifespan(app: FastAPI):
# - Cleanup risorse # - Cleanup risorse
ENVIRONMENT = os.getenv("ENVIRONMENT", "production")
ALLOWED_ORIGINS = [
"https://lmb.cosmoguard.it",
"http://localhost:8501", # Streamlit locale
]
TRUSTED_HOSTS = [
"lmb.cosmoguard.it",
"localhost",
"127.0.0.1",
]
# Inizializza FastAPI # Inizializza FastAPI
app = FastAPI( app = FastAPI(
title="Comsoguard API", title="Comsoguard API",
description="Central API for Comsoguard services", description="Central API for Comsoguard services",
version="0.0.1", version="0.0.1",
docs_url="/docs", docs_url="/docs" if ENVIRONMENT == "development" else None,
redoc_url="/redoc", redoc_url="/redoc" if ENVIRONMENT == "development" else None,
openapi_url="/openapi.json", openapi_url="/openapi.json" if ENVIRONMENT == "development" else None,
lifespan=lifespan lifespan=lifespan
) )
# ==================== MIDDLEWARE ==================== # ==================== MIDDLEWARE ====================
# 1. CORS - Configura in base alle tue esigenze # 1. Trusted Host — rifiuta richieste con Host header non autorizzato
app.add_middleware(TrustedHostMiddleware, allowed_hosts=TRUSTED_HOSTS)
# 2. CORS — solo origine autorizzata
app.add_middleware( app.add_middleware(
CORSMiddleware, CORSMiddleware,
allow_origins=[ allow_origins=ALLOWED_ORIGINS,
"*"
],
allow_credentials=True, allow_credentials=True,
allow_methods=["*"], allow_methods=["GET", "POST", "DELETE"],
allow_headers=["*"], allow_headers=["*"],
) )
@ -115,42 +133,52 @@ async def general_exception_handler(request: Request, exc: Exception):
# ==================== ROUTERS ==================== # ==================== ROUTERS ====================
# Include i tuoi router qui _auth = [Depends(get_current_user)]
# Auth — pubblico (login, refresh, logout non richiedono token)
app.include_router(api_auth.router, prefix="/api/v1")
# Tutti gli altri router richiedono JWT valido
app.include_router( app.include_router(
api_echa.router, api_echa.router,
prefix="/api/v1", prefix="/api/v1",
tags=["ECHA"] tags=["ECHA"],
dependencies=_auth,
) )
app.include_router( app.include_router(
api_cosing.router, api_cosing.router,
prefix="/api/v1", prefix="/api/v1",
tags=["COSING"] tags=["COSING"],
dependencies=_auth,
) )
app.include_router( app.include_router(
common.router, common.router,
prefix="/api/v1", prefix="/api/v1",
tags=["Common"] tags=["Common"],
dependencies=_auth,
) )
app.include_router( app.include_router(
api_ingredients.router, api_ingredients.router,
prefix="/api/v1", prefix="/api/v1",
tags=["Ingredients"] tags=["Ingredients"],
dependencies=_auth,
) )
app.include_router( app.include_router(
api_esposition.router, api_esposition.router,
prefix="/api/v1", prefix="/api/v1",
tags=["Esposition"] tags=["Esposition"],
dependencies=_auth,
) )
app.include_router( app.include_router(
api_orders.router, api_orders.router,
prefix="/api/v1", prefix="/api/v1",
tags=["Orders"] tags=["Orders"],
dependencies=_auth,
) )
# ==================== ROOT ENDPOINTS ==================== # ==================== ROOT ENDPOINTS ====================

132
uv.lock
View file

@ -348,6 +348,59 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" }, { url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952, upload-time = "2025-09-21T20:03:53.918Z" },
] ]
[[package]]
name = "cryptography"
version = "46.0.5"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "cffi", marker = "platform_python_implementation != 'PyPy'" },
]
sdist = { url = "https://files.pythonhosted.org/packages/60/04/ee2a9e8542e4fa2773b81771ff8349ff19cdd56b7258a0cc442639052edb/cryptography-46.0.5.tar.gz", hash = "sha256:abace499247268e3757271b2f1e244b36b06f8515cf27c4d49468fc9eb16e93d", size = 750064 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/f7/81/b0bb27f2ba931a65409c6b8a8b358a7f03c0e46eceacddff55f7c84b1f3b/cryptography-46.0.5-cp311-abi3-macosx_10_9_universal2.whl", hash = "sha256:351695ada9ea9618b3500b490ad54c739860883df6c1f555e088eaf25b1bbaad", size = 7176289 },
{ url = "https://files.pythonhosted.org/packages/ff/9e/6b4397a3e3d15123de3b1806ef342522393d50736c13b20ec4c9ea6693a6/cryptography-46.0.5-cp311-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:c18ff11e86df2e28854939acde2d003f7984f721eba450b56a200ad90eeb0e6b", size = 4275637 },
{ url = "https://files.pythonhosted.org/packages/63/e7/471ab61099a3920b0c77852ea3f0ea611c9702f651600397ac567848b897/cryptography-46.0.5-cp311-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:4d7e3d356b8cd4ea5aff04f129d5f66ebdc7b6f8eae802b93739ed520c47c79b", size = 4424742 },
{ url = "https://files.pythonhosted.org/packages/37/53/a18500f270342d66bf7e4d9f091114e31e5ee9e7375a5aba2e85a91e0044/cryptography-46.0.5-cp311-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:50bfb6925eff619c9c023b967d5b77a54e04256c4281b0e21336a130cd7fc263", size = 4277528 },
{ url = "https://files.pythonhosted.org/packages/22/29/c2e812ebc38c57b40e7c583895e73c8c5adb4d1e4a0cc4c5a4fdab2b1acc/cryptography-46.0.5-cp311-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:803812e111e75d1aa73690d2facc295eaefd4439be1023fefc4995eaea2af90d", size = 4947993 },
{ url = "https://files.pythonhosted.org/packages/6b/e7/237155ae19a9023de7e30ec64e5d99a9431a567407ac21170a046d22a5a3/cryptography-46.0.5-cp311-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ee190460e2fbe447175cda91b88b84ae8322a104fc27766ad09428754a618ed", size = 4456855 },
{ url = "https://files.pythonhosted.org/packages/2d/87/fc628a7ad85b81206738abbd213b07702bcbdada1dd43f72236ef3cffbb5/cryptography-46.0.5-cp311-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:f145bba11b878005c496e93e257c1e88f154d278d2638e6450d17e0f31e558d2", size = 3984635 },
{ url = "https://files.pythonhosted.org/packages/84/29/65b55622bde135aedf4565dc509d99b560ee4095e56989e815f8fd2aa910/cryptography-46.0.5-cp311-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:e9251e3be159d1020c4030bd2e5f84d6a43fe54b6c19c12f51cde9542a2817b2", size = 4277038 },
{ url = "https://files.pythonhosted.org/packages/bc/36/45e76c68d7311432741faf1fbf7fac8a196a0a735ca21f504c75d37e2558/cryptography-46.0.5-cp311-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:47fb8a66058b80e509c47118ef8a75d14c455e81ac369050f20ba0d23e77fee0", size = 4912181 },
{ url = "https://files.pythonhosted.org/packages/6d/1a/c1ba8fead184d6e3d5afcf03d569acac5ad063f3ac9fb7258af158f7e378/cryptography-46.0.5-cp311-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:4c3341037c136030cb46e4b1e17b7418ea4cbd9dd207e4a6f3b2b24e0d4ac731", size = 4456482 },
{ url = "https://files.pythonhosted.org/packages/f9/e5/3fb22e37f66827ced3b902cf895e6a6bc1d095b5b26be26bd13c441fdf19/cryptography-46.0.5-cp311-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:890bcb4abd5a2d3f852196437129eb3667d62630333aacc13dfd470fad3aaa82", size = 4405497 },
{ url = "https://files.pythonhosted.org/packages/1a/df/9d58bb32b1121a8a2f27383fabae4d63080c7ca60b9b5c88be742be04ee7/cryptography-46.0.5-cp311-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:80a8d7bfdf38f87ca30a5391c0c9ce4ed2926918e017c29ddf643d0ed2778ea1", size = 4667819 },
{ url = "https://files.pythonhosted.org/packages/ea/ed/325d2a490c5e94038cdb0117da9397ece1f11201f425c4e9c57fe5b9f08b/cryptography-46.0.5-cp311-abi3-win32.whl", hash = "sha256:60ee7e19e95104d4c03871d7d7dfb3d22ef8a9b9c6778c94e1c8fcc8365afd48", size = 3028230 },
{ url = "https://files.pythonhosted.org/packages/e9/5a/ac0f49e48063ab4255d9e3b79f5def51697fce1a95ea1370f03dc9db76f6/cryptography-46.0.5-cp311-abi3-win_amd64.whl", hash = "sha256:38946c54b16c885c72c4f59846be9743d699eee2b69b6988e0a00a01f46a61a4", size = 3480909 },
{ url = "https://files.pythonhosted.org/packages/00/13/3d278bfa7a15a96b9dc22db5a12ad1e48a9eb3d40e1827ef66a5df75d0d0/cryptography-46.0.5-cp314-cp314t-macosx_10_9_universal2.whl", hash = "sha256:94a76daa32eb78d61339aff7952ea819b1734b46f73646a07decb40e5b3448e2", size = 7119287 },
{ url = "https://files.pythonhosted.org/packages/67/c8/581a6702e14f0898a0848105cbefd20c058099e2c2d22ef4e476dfec75d7/cryptography-46.0.5-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:5be7bf2fb40769e05739dd0046e7b26f9d4670badc7b032d6ce4db64dddc0678", size = 4265728 },
{ url = "https://files.pythonhosted.org/packages/dd/4a/ba1a65ce8fc65435e5a849558379896c957870dd64fecea97b1ad5f46a37/cryptography-46.0.5-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:fe346b143ff9685e40192a4960938545c699054ba11d4f9029f94751e3f71d87", size = 4408287 },
{ url = "https://files.pythonhosted.org/packages/f8/67/8ffdbf7b65ed1ac224d1c2df3943553766914a8ca718747ee3871da6107e/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_aarch64.whl", hash = "sha256:c69fd885df7d089548a42d5ec05be26050ebcd2283d89b3d30676eb32ff87dee", size = 4270291 },
{ url = "https://files.pythonhosted.org/packages/f8/e5/f52377ee93bc2f2bba55a41a886fd208c15276ffbd2569f2ddc89d50e2c5/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_ppc64le.whl", hash = "sha256:8293f3dea7fc929ef7240796ba231413afa7b68ce38fd21da2995549f5961981", size = 4927539 },
{ url = "https://files.pythonhosted.org/packages/3b/02/cfe39181b02419bbbbcf3abdd16c1c5c8541f03ca8bda240debc467d5a12/cryptography-46.0.5-cp314-cp314t-manylinux_2_28_x86_64.whl", hash = "sha256:1abfdb89b41c3be0365328a410baa9df3ff8a9110fb75e7b52e66803ddabc9a9", size = 4442199 },
{ url = "https://files.pythonhosted.org/packages/c0/96/2fcaeb4873e536cf71421a388a6c11b5bc846e986b2b069c79363dc1648e/cryptography-46.0.5-cp314-cp314t-manylinux_2_31_armv7l.whl", hash = "sha256:d66e421495fdb797610a08f43b05269e0a5ea7f5e652a89bfd5a7d3c1dee3648", size = 3960131 },
{ url = "https://files.pythonhosted.org/packages/d8/d2/b27631f401ddd644e94c5cf33c9a4069f72011821cf3dc7309546b0642a0/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_aarch64.whl", hash = "sha256:4e817a8920bfbcff8940ecfd60f23d01836408242b30f1a708d93198393a80b4", size = 4270072 },
{ url = "https://files.pythonhosted.org/packages/f4/a7/60d32b0370dae0b4ebe55ffa10e8599a2a59935b5ece1b9f06edb73abdeb/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_ppc64le.whl", hash = "sha256:68f68d13f2e1cb95163fa3b4db4bf9a159a418f5f6e7242564fc75fcae667fd0", size = 4892170 },
{ url = "https://files.pythonhosted.org/packages/d2/b9/cf73ddf8ef1164330eb0b199a589103c363afa0cf794218c24d524a58eab/cryptography-46.0.5-cp314-cp314t-manylinux_2_34_x86_64.whl", hash = "sha256:a3d1fae9863299076f05cb8a778c467578262fae09f9dc0ee9b12eb4268ce663", size = 4441741 },
{ url = "https://files.pythonhosted.org/packages/5f/eb/eee00b28c84c726fe8fa0158c65afe312d9c3b78d9d01daf700f1f6e37ff/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:c4143987a42a2397f2fc3b4d7e3a7d313fbe684f67ff443999e803dd75a76826", size = 4396728 },
{ url = "https://files.pythonhosted.org/packages/65/f4/6bc1a9ed5aef7145045114b75b77c2a8261b4d38717bd8dea111a63c3442/cryptography-46.0.5-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:7d731d4b107030987fd61a7f8ab512b25b53cef8f233a97379ede116f30eb67d", size = 4652001 },
{ url = "https://files.pythonhosted.org/packages/86/ef/5d00ef966ddd71ac2e6951d278884a84a40ffbd88948ef0e294b214ae9e4/cryptography-46.0.5-cp314-cp314t-win32.whl", hash = "sha256:c3bcce8521d785d510b2aad26ae2c966092b7daa8f45dd8f44734a104dc0bc1a", size = 3003637 },
{ url = "https://files.pythonhosted.org/packages/b7/57/f3f4160123da6d098db78350fdfd9705057aad21de7388eacb2401dceab9/cryptography-46.0.5-cp314-cp314t-win_amd64.whl", hash = "sha256:4d8ae8659ab18c65ced284993c2265910f6c9e650189d4e3f68445ef82a810e4", size = 3469487 },
{ url = "https://files.pythonhosted.org/packages/e2/fa/a66aa722105ad6a458bebd64086ca2b72cdd361fed31763d20390f6f1389/cryptography-46.0.5-cp38-abi3-macosx_10_9_universal2.whl", hash = "sha256:4108d4c09fbbf2789d0c926eb4152ae1760d5a2d97612b92d508d96c861e4d31", size = 7170514 },
{ url = "https://files.pythonhosted.org/packages/0f/04/c85bdeab78c8bc77b701bf0d9bdcf514c044e18a46dcff330df5448631b0/cryptography-46.0.5-cp38-abi3-manylinux2014_aarch64.manylinux_2_17_aarch64.whl", hash = "sha256:7d1f30a86d2757199cb2d56e48cce14deddf1f9c95f1ef1b64ee91ea43fe2e18", size = 4275349 },
{ url = "https://files.pythonhosted.org/packages/5c/32/9b87132a2f91ee7f5223b091dc963055503e9b442c98fc0b8a5ca765fab0/cryptography-46.0.5-cp38-abi3-manylinux2014_x86_64.manylinux_2_17_x86_64.whl", hash = "sha256:039917b0dc418bb9f6edce8a906572d69e74bd330b0b3fea4f79dab7f8ddd235", size = 4420667 },
{ url = "https://files.pythonhosted.org/packages/a1/a6/a7cb7010bec4b7c5692ca6f024150371b295ee1c108bdc1c400e4c44562b/cryptography-46.0.5-cp38-abi3-manylinux_2_28_aarch64.whl", hash = "sha256:ba2a27ff02f48193fc4daeadf8ad2590516fa3d0adeeb34336b96f7fa64c1e3a", size = 4276980 },
{ url = "https://files.pythonhosted.org/packages/8e/7c/c4f45e0eeff9b91e3f12dbd0e165fcf2a38847288fcfd889deea99fb7b6d/cryptography-46.0.5-cp38-abi3-manylinux_2_28_ppc64le.whl", hash = "sha256:61aa400dce22cb001a98014f647dc21cda08f7915ceb95df0c9eaf84b4b6af76", size = 4939143 },
{ url = "https://files.pythonhosted.org/packages/37/19/e1b8f964a834eddb44fa1b9a9976f4e414cbb7aa62809b6760c8803d22d1/cryptography-46.0.5-cp38-abi3-manylinux_2_28_x86_64.whl", hash = "sha256:3ce58ba46e1bc2aac4f7d9290223cead56743fa6ab94a5d53292ffaac6a91614", size = 4453674 },
{ url = "https://files.pythonhosted.org/packages/db/ed/db15d3956f65264ca204625597c410d420e26530c4e2943e05a0d2f24d51/cryptography-46.0.5-cp38-abi3-manylinux_2_31_armv7l.whl", hash = "sha256:420d0e909050490d04359e7fdb5ed7e667ca5c3c402b809ae2563d7e66a92229", size = 3978801 },
{ url = "https://files.pythonhosted.org/packages/41/e2/df40a31d82df0a70a0daf69791f91dbb70e47644c58581d654879b382d11/cryptography-46.0.5-cp38-abi3-manylinux_2_34_aarch64.whl", hash = "sha256:582f5fcd2afa31622f317f80426a027f30dc792e9c80ffee87b993200ea115f1", size = 4276755 },
{ url = "https://files.pythonhosted.org/packages/33/45/726809d1176959f4a896b86907b98ff4391a8aa29c0aaaf9450a8a10630e/cryptography-46.0.5-cp38-abi3-manylinux_2_34_ppc64le.whl", hash = "sha256:bfd56bb4b37ed4f330b82402f6f435845a5f5648edf1ad497da51a8452d5d62d", size = 4901539 },
{ url = "https://files.pythonhosted.org/packages/99/0f/a3076874e9c88ecb2ecc31382f6e7c21b428ede6f55aafa1aa272613e3cd/cryptography-46.0.5-cp38-abi3-manylinux_2_34_x86_64.whl", hash = "sha256:a3d507bb6a513ca96ba84443226af944b0f7f47dcc9a399d110cd6146481d24c", size = 4452794 },
{ url = "https://files.pythonhosted.org/packages/02/ef/ffeb542d3683d24194a38f66ca17c0a4b8bf10631feef44a7ef64e631b1a/cryptography-46.0.5-cp38-abi3-musllinux_1_2_aarch64.whl", hash = "sha256:9f16fbdf4da055efb21c22d81b89f155f02ba420558db21288b3d0035bafd5f4", size = 4404160 },
{ url = "https://files.pythonhosted.org/packages/96/93/682d2b43c1d5f1406ed048f377c0fc9fc8f7b0447a478d5c65ab3d3a66eb/cryptography-46.0.5-cp38-abi3-musllinux_1_2_x86_64.whl", hash = "sha256:ced80795227d70549a411a4ab66e8ce307899fad2220ce5ab2f296e687eacde9", size = 4667123 },
{ url = "https://files.pythonhosted.org/packages/45/2d/9c5f2926cb5300a8eefc3f4f0b3f3df39db7f7ce40c8365444c49363cbda/cryptography-46.0.5-cp38-abi3-win32.whl", hash = "sha256:02f547fce831f5096c9a567fd41bc12ca8f11df260959ecc7c3202555cc47a72", size = 3010220 },
{ url = "https://files.pythonhosted.org/packages/48/ef/0c2f4a8e31018a986949d34a01115dd057bf536905dca38897bacd21fac3/cryptography-46.0.5-cp38-abi3-win_amd64.whl", hash = "sha256:556e106ee01aa13484ce9b0239bca667be5004efb0aabbed28d353df86445595", size = 3467050 },
]
[[package]] [[package]]
name = "cssselect2" name = "cssselect2"
version = "0.8.0" version = "0.8.0"
@ -410,6 +463,19 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/30/79/4f544d73fcc0513b71296cb3ebb28a227d22e80dec27204977039b9fa875/duckdb-1.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:280fd663dacdd12bb3c3bf41f3e5b2e5b95e00b88120afabb8b8befa5f335c6f", size = 12336460, upload-time = "2025-10-07T10:37:12.154Z" }, { url = "https://files.pythonhosted.org/packages/30/79/4f544d73fcc0513b71296cb3ebb28a227d22e80dec27204977039b9fa875/duckdb-1.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:280fd663dacdd12bb3c3bf41f3e5b2e5b95e00b88120afabb8b8befa5f335c6f", size = 12336460, upload-time = "2025-10-07T10:37:12.154Z" },
] ]
[[package]]
name = "email-validator"
version = "2.3.0"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "dnspython" },
{ name = "idna" },
]
sdist = { url = "https://files.pythonhosted.org/packages/f5/22/900cb125c76b7aaa450ce02fd727f452243f2e91a61af068b40adba60ea9/email_validator-2.3.0.tar.gz", hash = "sha256:9fc05c37f2f6cf439ff414f8fc46d917929974a82244c20eb10231ba60c54426", size = 51238 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/de/15/545e2b6cf2e3be84bc1ed85613edd75b8aea69807a71c26f4ca6a9258e82/email_validator-2.3.0-py3-none-any.whl", hash = "sha256:80f13f623413e6b197ae73bb10bf4eb0908faf509ad8362c5edeb0be7fd450b4", size = 35604 },
]
[[package]] [[package]]
name = "et-xmlfile" name = "et-xmlfile"
version = "2.0.0" version = "2.0.0"
@ -419,6 +485,18 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 }, { url = "https://files.pythonhosted.org/packages/c1/8b/5fe2cc11fee489817272089c4203e679c63b570a5aaeb18d852ae3cbba6a/et_xmlfile-2.0.0-py3-none-any.whl", hash = "sha256:7a91720bc756843502c3b7504c77b8fe44217c85c537d85037f0f536151b2caa", size = 18059 },
] ]
[[package]]
name = "extra-streamlit-components"
version = "0.1.81"
source = { registry = "https://pypi.org/simple" }
dependencies = [
{ name = "streamlit" },
]
sdist = { url = "https://files.pythonhosted.org/packages/24/49/9b47a3450034d74259f9d4887d85be4e6a771bc21da467b253323d78c4d9/extra_streamlit_components-0.1.81.tar.gz", hash = "sha256:eb9beb7bacfe8b3d238f1888a21c78ac6cfa569341be484bca08c3ea0b15f20d", size = 2250141 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/fd/8d/d2f1eeb52c50c990d14fd91bea35157890bb791c46b3f2bebaa5eef4bdf6/extra_streamlit_components-0.1.81-py3-none-any.whl", hash = "sha256:11a4651dbd03cac04edfbb8711757b1d10e3cdf280b8fa3a43f970d05e684619", size = 2278499 },
]
[[package]] [[package]]
name = "fastapi" name = "fastapi"
version = "0.121.2" version = "0.121.2"
@ -554,6 +632,34 @@ 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" }, { 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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/7e/f5/f66802a942d491edb555dd61e3a9961140fd64c90bce1eafd741609d334d/httpcore-1.0.9-py3-none-any.whl", hash = "sha256:2d400746a40668fc9dec9810239072b40b4484b640a8c38fd654a024c7a1bf55", size = 78784 },
]
[[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 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/2a/39/e50c7c3a983047577ee07d2a9e53faf5a69493943ec3f6a384bdc792deb2/httpx-0.28.1-py3-none-any.whl", hash = "sha256:d909fcccc110f8c7faf814ca82a9a4d816bc5a6dbfea25d6591d6985b8ba59ad", size = 73517 },
]
[[package]] [[package]]
name = "idna" name = "idna"
version = "3.10" version = "3.10"
@ -981,9 +1087,12 @@ version = "0.1.0"
source = { editable = "." } source = { editable = "." }
dependencies = [ dependencies = [
{ name = "beautifulsoup4" }, { name = "beautifulsoup4" },
{ name = "cryptography" },
{ name = "dotenv" }, { name = "dotenv" },
{ name = "duckdb" }, { name = "duckdb" },
{ name = "extra-streamlit-components" },
{ name = "fastapi" }, { name = "fastapi" },
{ name = "httpx" },
{ name = "marimo" }, { name = "marimo" },
{ name = "markdown-to-json" }, { name = "markdown-to-json" },
{ name = "markdownify" }, { name = "markdownify" },
@ -992,7 +1101,8 @@ dependencies = [
{ name = "psycopg2-binary" }, { name = "psycopg2-binary" },
{ name = "pubchemprops" }, { name = "pubchemprops" },
{ name = "pubchempy" }, { name = "pubchempy" },
{ name = "pydantic" }, { name = "pydantic", extra = ["email"] },
{ name = "pyjwt" },
{ name = "pymongo" }, { name = "pymongo" },
{ name = "pytest" }, { name = "pytest" },
{ name = "pytest-cov" }, { name = "pytest-cov" },
@ -1007,9 +1117,12 @@ dependencies = [
[package.metadata] [package.metadata]
requires-dist = [ requires-dist = [
{ name = "beautifulsoup4", specifier = ">=4.14.2" }, { name = "beautifulsoup4", specifier = ">=4.14.2" },
{ name = "cryptography", specifier = ">=46.0.5" },
{ name = "dotenv", specifier = ">=0.9.9" }, { name = "dotenv", specifier = ">=0.9.9" },
{ name = "duckdb", specifier = ">=1.4.1" }, { name = "duckdb", specifier = ">=1.4.1" },
{ name = "extra-streamlit-components", specifier = ">=0.1.71" },
{ name = "fastapi", specifier = ">=0.121.2" }, { name = "fastapi", specifier = ">=0.121.2" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "marimo", specifier = ">=0.16.5" }, { name = "marimo", specifier = ">=0.16.5" },
{ name = "markdown-to-json", specifier = ">=2.1.2" }, { name = "markdown-to-json", specifier = ">=2.1.2" },
{ name = "markdownify", specifier = ">=1.2.0" }, { name = "markdownify", specifier = ">=1.2.0" },
@ -1018,7 +1131,8 @@ requires-dist = [
{ name = "psycopg2-binary", specifier = ">=2.9.11" }, { name = "psycopg2-binary", specifier = ">=2.9.11" },
{ name = "pubchemprops", specifier = ">=0.1.1" }, { name = "pubchemprops", specifier = ">=0.1.1" },
{ name = "pubchempy", specifier = ">=1.0.5" }, { name = "pubchempy", specifier = ">=1.0.5" },
{ name = "pydantic", specifier = ">=2.11.10" }, { name = "pydantic", extras = ["email"], specifier = ">=2.11.10" },
{ name = "pyjwt", specifier = ">=2.9.0" },
{ name = "pymongo", specifier = ">=4.15.2" }, { name = "pymongo", specifier = ">=4.15.2" },
{ name = "pytest", specifier = ">=8.4.2" }, { name = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-cov", specifier = ">=7.0.0" }, { name = "pytest-cov", specifier = ">=7.0.0" },
@ -1263,6 +1377,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/1f/73c53fcbfb0b5a78f91176df41945ca466e71e9d9d836e5c522abda39ee7/pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a", size = 444823, upload-time = "2025-10-04T10:40:39.055Z" }, { url = "https://files.pythonhosted.org/packages/bd/1f/73c53fcbfb0b5a78f91176df41945ca466e71e9d9d836e5c522abda39ee7/pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a", size = 444823, upload-time = "2025-10-04T10:40:39.055Z" },
] ]
[package.optional-dependencies]
email = [
{ name = "email-validator" },
]
[[package]] [[package]]
name = "pydantic-core" name = "pydantic-core"
version = "2.33.2" version = "2.33.2"
@ -1348,6 +1467,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" }, { url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217, upload-time = "2025-06-21T13:39:07.939Z" },
] ]
[[package]]
name = "pyjwt"
version = "2.11.0"
source = { registry = "https://pypi.org/simple" }
sdist = { url = "https://files.pythonhosted.org/packages/5c/5a/b46fa56bf322901eee5b0454a34343cdbdae202cd421775a8ee4e42fd519/pyjwt-2.11.0.tar.gz", hash = "sha256:35f95c1f0fbe5d5ba6e43f00271c275f7a1a4db1dab27bf708073b75318ea623", size = 98019 }
wheels = [
{ url = "https://files.pythonhosted.org/packages/6f/01/c26ce75ba460d5cd503da9e13b21a33804d38c2165dec7b716d06b13010c/pyjwt-2.11.0-py3-none-any.whl", hash = "sha256:94a6bde30eb5c8e04fee991062b534071fd1439ef58d2adc9ccb823e7bcd0469", size = 28224 },
]
[[package]] [[package]]
name = "pymdown-extensions" name = "pymdown-extensions"
version = "10.16.1" version = "10.16.1"