diff --git a/pyproject.toml b/pyproject.toml index 1ba3666..0800608 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -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] diff --git a/src/pif_compiler/api/routes/api_auth.py b/src/pif_compiler/api/routes/api_auth.py new file mode 100644 index 0000000..9b607da --- /dev/null +++ b/src/pif_compiler/api/routes/api_auth.py @@ -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} diff --git a/src/pif_compiler/api/routes/api_ingredients.py b/src/pif_compiler/api/routes/api_ingredients.py index 75d91c3..8d1cf2e 100644 --- a/src/pif_compiler/api/routes/api_ingredients.py +++ b/src/pif_compiler/api/routes/api_ingredients.py @@ -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.""" diff --git a/src/pif_compiler/api/routes/common.py b/src/pif_compiler/api/routes/common.py index a7d8b1a..c35d1ab 100644 --- a/src/pif_compiler/api/routes/common.py +++ b/src/pif_compiler/api/routes/common.py @@ -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(): """ diff --git a/src/pif_compiler/classes/__init__.py b/src/pif_compiler/classes/__init__.py index 5bab273..00d1fd8 100644 --- a/src/pif_compiler/classes/__init__.py +++ b/src/pif_compiler/classes/__init__.py @@ -11,7 +11,6 @@ from pif_compiler.classes.models import ( ToxIndicator, Toxicity, Ingredient, - RetentionFactors, Esposition, ) diff --git a/src/pif_compiler/classes/models.py b/src/pif_compiler/classes/models.py index 1d8976f..b67949f 100644 --- a/src/pif_compiler/classes/models.py +++ b/src/pif_compiler/classes/models.py @@ -247,9 +247,34 @@ 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) - self.factor = self.best_case.factor + 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 @classmethod @@ -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 diff --git a/src/pif_compiler/functions/auth.py b/src/pif_compiler/functions/auth.py new file mode 100644 index 0000000..6c117c3 --- /dev/null +++ b/src/pif_compiler/functions/auth.py @@ -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) diff --git a/src/pif_compiler/functions/db_utils.py b/src/pif_compiler/functions/db_utils.py index 2f7265a..fb00eba 100644 --- a/src/pif_compiler/functions/db_utils.py +++ b/src/pif_compiler/functions/db_utils.py @@ -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() diff --git a/src/pif_compiler/main.py b/src/pif_compiler/main.py index 18949f0..f0a6fc0 100644 --- a/src/pif_compiler/main.py +++ b/src/pif_compiler/main.py @@ -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 ==================== diff --git a/uv.lock b/uv.lock index d6c32b1..6293565 100644 --- a/uv.lock +++ b/uv.lock @@ -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"