auth added + modfix

This commit is contained in:
adish-rmr 2026-03-09 21:26:33 +01:00
parent dc9d8b6d10
commit 9cf2072bb4
10 changed files with 466 additions and 27 deletions

View file

@ -19,17 +19,21 @@ dependencies = [
"psycopg2>=2.9.11",
"pubchemprops>=0.1.1",
"pubchempy>=1.0.5",
"pydantic>=2.11.10",
"pydantic[email]>=2.11.10",
"pymongo>=4.15.2",
"pytest>=8.4.2",
"pytest-cov>=7.0.0",
"pytest-mock>=3.15.1",
"python-dotenv>=1.2.1",
"httpx>=0.27.0",
"PyJWT>=2.9.0",
"requests>=2.32.5",
"streamlit>=1.50.0",
"extra-streamlit-components>=0.1.71",
"openpyxl>=3.1.0",
"uvicorn>=0.35.0",
"weasyprint>=66.0",
"cryptography>=46.0.5",
]
[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 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
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"])
async def create_client(request: ClientCreateRequest):
"""Crea o recupera un cliente. Ritorna id_cliente."""

View file

@ -1,13 +1,15 @@
from fastapi import APIRouter, HTTPException, status
from fastapi.responses import FileResponse
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
from pif_compiler.functions.common_func import generate_pdf
from pif_compiler.services.srv_pubchem import pubchem_dap
from pif_compiler.services.srv_cir import search_ingredient
from pif_compiler.functions.common_log import get_logger
from pif_compiler.functions.db_utils import db_connect
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"])
async def common_health_check():
"""

View file

@ -11,7 +11,6 @@ from pif_compiler.classes.models import (
ToxIndicator,
Toxicity,
Ingredient,
RetentionFactors,
Esposition,
)

View file

@ -247,8 +247,33 @@ class Toxicity(BaseModel):
@model_validator(mode='after')
def set_best_case(self) -> 'Toxicity':
if self.indicators:
self.best_case = max(self.indicators, key=lambda x: x.priority_rank)
if not self.indicators:
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
return self
@ -502,7 +527,7 @@ class Esposition(BaseModel):
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")
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

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

@ -170,6 +170,28 @@ def get_all_clienti():
logger.error(f"Errore recupero clienti: {e}")
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):
try:
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.trustedhost import TrustedHostMiddleware
from fastapi.responses import JSONResponse
from fastapi.exceptions import RequestValidationError
from contextlib import asynccontextmanager
import os
import time
from dotenv import load_dotenv
from pif_compiler.functions.common_log import get_logger
load_dotenv()
# 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
logger = get_logger()
@ -31,28 +37,40 @@ async def lifespan(app: FastAPI):
# - 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
app = FastAPI(
title="Comsoguard API",
description="Central API for Comsoguard services",
version="0.0.1",
docs_url="/docs",
redoc_url="/redoc",
openapi_url="/openapi.json",
docs_url="/docs" if ENVIRONMENT == "development" else None,
redoc_url="/redoc" if ENVIRONMENT == "development" else None,
openapi_url="/openapi.json" if ENVIRONMENT == "development" else None,
lifespan=lifespan
)
# ==================== 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(
CORSMiddleware,
allow_origins=[
"*"
],
allow_origins=ALLOWED_ORIGINS,
allow_credentials=True,
allow_methods=["*"],
allow_methods=["GET", "POST", "DELETE"],
allow_headers=["*"],
)
@ -115,42 +133,52 @@ async def general_exception_handler(request: Request, exc: Exception):
# ==================== 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(
api_echa.router,
prefix="/api/v1",
tags=["ECHA"]
tags=["ECHA"],
dependencies=_auth,
)
app.include_router(
api_cosing.router,
prefix="/api/v1",
tags=["COSING"]
tags=["COSING"],
dependencies=_auth,
)
app.include_router(
common.router,
prefix="/api/v1",
tags=["Common"]
tags=["Common"],
dependencies=_auth,
)
app.include_router(
api_ingredients.router,
prefix="/api/v1",
tags=["Ingredients"]
tags=["Ingredients"],
dependencies=_auth,
)
app.include_router(
api_esposition.router,
prefix="/api/v1",
tags=["Esposition"]
tags=["Esposition"],
dependencies=_auth,
)
app.include_router(
api_orders.router,
prefix="/api/v1",
tags=["Orders"]
tags=["Orders"],
dependencies=_auth,
)
# ==================== ROOT ENDPOINTS ====================

132
uv.lock
View file

@ -347,6 +347,59 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/ec/16/114df1c291c22cac3b0c127a73e0af5c12ed7bbb6558d310429a0ae24023/coverage-7.10.7-py3-none-any.whl", hash = "sha256:f7941f6f2fe6dd6807a1208737b8a0cbcf1cc6d7b07d24998ad2d63590868260", size = 209952 },
]
[[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]]
name = "cssselect2"
version = "0.8.0"
@ -409,6 +462,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 },
]
[[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]]
name = "et-xmlfile"
version = "2.0.0"
@ -418,6 +484,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 },
]
[[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]]
name = "fastapi"
version = "0.121.2"
@ -553,6 +631,34 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/04/4b/29cac41a4d98d144bf5f6d33995617b185d14b22401f75ca86f384e87ff1/h11-0.16.0-py3-none-any.whl", hash = "sha256:63cf8bbe7522de3bf65932fda1d9c2772064ffb3dae62d55932da54b31cb6c86", size = 37515 },
]
[[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]]
name = "idna"
version = "3.10"
@ -980,9 +1086,12 @@ version = "0.1.0"
source = { editable = "." }
dependencies = [
{ name = "beautifulsoup4" },
{ name = "cryptography" },
{ name = "dotenv" },
{ name = "duckdb" },
{ name = "extra-streamlit-components" },
{ name = "fastapi" },
{ name = "httpx" },
{ name = "marimo" },
{ name = "markdown-to-json" },
{ name = "markdownify" },
@ -991,7 +1100,8 @@ dependencies = [
{ name = "psycopg2" },
{ name = "pubchemprops" },
{ name = "pubchempy" },
{ name = "pydantic" },
{ name = "pydantic", extra = ["email"] },
{ name = "pyjwt" },
{ name = "pymongo" },
{ name = "pytest" },
{ name = "pytest-cov" },
@ -1006,9 +1116,12 @@ dependencies = [
[package.metadata]
requires-dist = [
{ name = "beautifulsoup4", specifier = ">=4.14.2" },
{ name = "cryptography", specifier = ">=46.0.5" },
{ name = "dotenv", specifier = ">=0.9.9" },
{ name = "duckdb", specifier = ">=1.4.1" },
{ name = "extra-streamlit-components", specifier = ">=0.1.71" },
{ name = "fastapi", specifier = ">=0.121.2" },
{ name = "httpx", specifier = ">=0.27.0" },
{ name = "marimo", specifier = ">=0.16.5" },
{ name = "markdown-to-json", specifier = ">=2.1.2" },
{ name = "markdownify", specifier = ">=1.2.0" },
@ -1017,7 +1130,8 @@ requires-dist = [
{ name = "psycopg2", specifier = ">=2.9.11" },
{ name = "pubchemprops", specifier = ">=0.1.1" },
{ 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 = "pytest", specifier = ">=8.4.2" },
{ name = "pytest-cov", specifier = ">=7.0.0" },
@ -1232,6 +1346,11 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/bd/1f/73c53fcbfb0b5a78f91176df41945ca466e71e9d9d836e5c522abda39ee7/pydantic-2.11.10-py3-none-any.whl", hash = "sha256:802a655709d49bd004c31e865ef37da30b540786a46bfce02333e0e24b5fe29a", size = 444823 },
]
[package.optional-dependencies]
email = [
{ name = "email-validator" },
]
[[package]]
name = "pydantic-core"
version = "2.33.2"
@ -1317,6 +1436,15 @@ wheels = [
{ url = "https://files.pythonhosted.org/packages/c7/21/705964c7812476f378728bdf590ca4b771ec72385c533964653c68e86bdc/pygments-2.19.2-py3-none-any.whl", hash = "sha256:86540386c03d588bb81d44bc3928634ff26449851e99741617ecb9037ee5ec0b", size = 1225217 },
]
[[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]]
name = "pymdown-extensions"
version = "10.16.1"