Z User
v3.1.0: Part B architecture migration — eliminate Hugging Face inference credit dependency
6c516b6 Download app/config.py from sitcod3/modul-x: direct link, hf CLI and curl.
- Browser
- Download file 8.45 kB
-
https://huggingface.co/spaces/sitcod3/modul-x/resolve/main/app/config.py
- Command line
-
hf download hf://spaces/sitcod3/modul-x/app/config.py
-
curl -L -o config.py https://huggingface.co/spaces/sitcod3/modul-x/resolve/main/app/config.py
8.45 kB
| from urllib.parse import urlsplit | |
| from pydantic import model_validator | |
| from pydantic_settings import BaseSettings, SettingsConfigDict | |
| from typing import List | |
| def _is_https_origin(value: str) -> bool: | |
| try: | |
| parsed = urlsplit(value) | |
| _ = parsed.port | |
| except ValueError: | |
| return False | |
| return ( | |
| parsed.scheme == "https" | |
| and bool(parsed.hostname) | |
| and parsed.username is None | |
| and parsed.password is None | |
| and parsed.path in {"", "/"} | |
| and not parsed.query | |
| and not parsed.fragment | |
| ) | |
| class Settings(BaseSettings): | |
| model_config = SettingsConfigDict( | |
| env_file=".env", | |
| env_file_encoding="utf-8", | |
| case_sensitive=False, | |
| extra="ignore", | |
| ) | |
| # App | |
| app_environment: str = "development" | |
| app_name: str = "BRIDGE Modul - X" | |
| app_version: str = "3.1.0" | |
| debug: bool = False | |
| secret_key: str = "change-me-in-production" | |
| # Server | |
| host: str = "0.0.0.0" | |
| port: int = 8000 | |
| workers: int = 4 | |
| # Database | |
| database_url: str = "sqlite+aiosqlite:///./emailval.db" | |
| # Redis | |
| redis_url: str = "redis://localhost:6379/0" | |
| redis_cache_ttl: int = 86400 # 24 hours | |
| # CORS | |
| cors_origins: List[str] = [ | |
| "http://localhost:5173", | |
| "http://localhost:3000", | |
| "http://127.0.0.1:5173", | |
| ] | |
| # Validation | |
| smtp_timeout: int = 10 | |
| dns_timeout: int = 5 | |
| # Public resolvers used instead of the host's own resolver config. A stale | |
| # gateway list (dead hotspot IPs) makes every lookup burn the full timeout | |
| # and fail — see dns_check.build_resolver. | |
| dns_nameservers: list[str] = ["1.1.1.1", "8.8.8.8"] | |
| # Bulk validation only routes through Celery on hosts that actually run a | |
| # worker. The HF Space runs uvicorn alone (Dockerfile CMD), so a reachable | |
| # Redis must not by itself select the Celery branch — jobs would sit in | |
| # the queue forever. Opt in on the VPS deployment. | |
| celery_bulk_enabled: bool = False | |
| max_bulk_emails: int = 10000 | |
| rate_limit_per_minute: int = 60 | |
| bulk_sync_chunk_size: int = 50 # Process this many emails at a time in sync mode | |
| # Proxy | |
| use_proxies: bool = False | |
| proxy_list: List[str] = [] | |
| # Mailbox verification provider (Layer 5 catch-all + Layer 6 SMTP). | |
| # The engine is FULLY AUTONOMOUS: mailbox verdicts come from its own SMTP | |
| # probing engine — never from a third-party verification API. "local" | |
| # forces the built-in engine; "remote" forces the remote SMTP worker; | |
| # "auto" prefers the remote worker when SMTP_WORKER_URL is set and falls | |
| # back to the built-in engine otherwise. | |
| mailbox_provider: str = "auto" # auto | local | remote | |
| # Public HELO/MX identity used in the SMTP dialogue. A real, resolvable | |
| # hostname with matching PTR improves acceptance by strict receivers. | |
| smtp_helo_domain: str = "validator.local" | |
| smtp_mail_from: str = "probe@validator.local" | |
| # Remote SMTP probe worker (spec §3.3): orchestrator and SMTP worker are | |
| # separate deployments. Point this at a port-25-capable host running this | |
| # same image (its POST /api/v1/smtp/probe endpoints) and every mailbox | |
| # verdict flows through it. Shared secret via SMTP_WORKER_TOKEN. | |
| smtp_worker_url: str = "" | |
| smtp_worker_token: str = "" | |
| smtp_worker_timeout: int = 15 | |
| # Circuit breaker: after this many consecutive direct-SMTP connection | |
| # failures (blocked egress), fail fast for `smtp_cooldown_seconds` instead | |
| # of burning the full timeout on every address. | |
| smtp_fail_threshold: int = 3 | |
| smtp_cooldown_seconds: int = 300 | |
| # Catch-all is a domain property — cache the verdict for 72h so a bulk list | |
| # does not re-probe the same domain for every address. | |
| catch_all_ttl_seconds: int = 72 * 3600 | |
| # DNS/MX answers change slowly; a definitive mailbox verdict is stable for | |
| # about a day. Transient failures are never cached regardless of TTL. | |
| dns_cache_ttl_seconds: int = 24 * 3600 | |
| # Per-MX-host SMTP session cap and global concurrency for bulk runs. | |
| max_connections_per_host: int = 2 | |
| bulk_concurrency: int = 20 | |
| # --- DNS-over-HTTPS worker (spec §3.2) --- | |
| # MX resolution over free public DoH resolvers (Google + Cloudflare) with a | |
| # dual-resolver consistency check. Legacy dnspython stays as fallback. | |
| doh_enabled: bool = True | |
| doh_timeout: int = 5 | |
| # --- AI Risk Scorer (spec §3.4 — LOCAL inference, zero external credits) --- | |
| # Part B architecture migration: the Hugging Face Inference API path was | |
| # REMOVED. The model id below names a public HF Hub checkpoint whose | |
| # weights are pulled once (free, unmetered) and then run in-process via a | |
| # transformers pipeline — no token, no inference credits, no remote calls. | |
| # A DistilBERT-class NLI checkpoint is the spec-default (§5.3): same | |
| # feature set, ~4x lower CPU latency than bart-large-mnli on 2 vCPU. | |
| # Empty disables inference entirely (the rule-based heuristic then scores | |
| # every ambiguous address at zero marginal cost). Configured, never | |
| # hard-coded. | |
| ai_scorer_model: str = "typeform/distilbert-base-uncased-mnli" | |
| ai_scorer_timeout: int = 12 | |
| # Zero-shot mode: candidate labels ARE the risk taxonomy — works with any | |
| # local NLI checkpoint (e.g. typeform/distilbert-base-uncased-mnli or | |
| # facebook/bart-large-mnli if CPU budget allows). | |
| ai_scorer_zero_shot: bool = True | |
| # Master switch: False forces heuristic-only scoring (no model load at all). | |
| ai_scorer_local: bool = True | |
| # §2.2 cache aggressively: a repeated PII-free feature string never | |
| # re-invokes the pipeline inside this TTL window. 0 disables the cache. | |
| ai_score_cache_ttl_seconds: int = 3600 | |
| ai_circuit_fail_threshold: int = 3 | |
| ai_circuit_cooldown_seconds: int = 300 | |
| # --- DistilBERT email-risk signal (advisory, unknown domains only) --- | |
| # OFF by default: with the flag off the engine's behaviour is identical to | |
| # the pre-ML build. The model is a public HF repo trained on the engine's | |
| # own blocklists (see app/ml/). Promotion from advisory to blocking requires | |
| # measured DISPOSABLE precision >= 0.98 (app/ml/train.py reports it). | |
| # Hub downloads are free and unmetered; no token is required or used. | |
| ml_disposable_enabled: bool = False | |
| ml_model_repo: str = "" | |
| ml_batch_size: int = 64 | |
| ml_score_penalty: int = 5 | |
| # Run-quality monitor (report item 10): alert when >= this share of a bulk | |
| # run comes back risky/unknown/error — the signature of a systemic failure | |
| # (dead provider, blocked egress, engine regression). 0 disables it. | |
| risky_rate_alert_threshold: float = 0.5 | |
| # Celery | |
| celery_broker_url: str = "redis://localhost:6379/1" | |
| celery_result_backend: str = "redis://localhost:6379/2" | |
| # Supabase (Postgres + PostgREST) | |
| supabase_url: str = "" | |
| supabase_anon_key: str = "" | |
| supabase_service_role_key: str = "" | |
| enable_db_persistence: bool = True | |
| # Central billing gate: "true" enforces Supabase JWT + active license | |
| # (modul-x directly, via combo-validate-send, or via combo-full-bridge) | |
| # on every paid API route. Anything else = open demo mode. | |
| license_enforcement: str = "" | |
| def validate_production_settings(self) -> "Settings": | |
| if self.app_environment.strip().casefold() != "production": | |
| return self | |
| problems = [] | |
| if self.debug: | |
| problems.append("DEBUG must be false") | |
| if self.license_enforcement.strip().casefold() != "true": | |
| problems.append("LICENSE_ENFORCEMENT must be true") | |
| if not self.supabase_configured: | |
| problems.append("SUPABASE_URL and SUPABASE_SERVICE_ROLE_KEY are required") | |
| elif not _is_https_origin(self.supabase_url): | |
| problems.append("SUPABASE_URL must be an HTTPS URL") | |
| if not self.cors_origins or not all( | |
| _is_https_origin(origin) for origin in self.cors_origins | |
| ): | |
| problems.append("CORS_ORIGINS must contain only HTTPS origins") | |
| if problems: | |
| raise ValueError("Unsafe production configuration: " + "; ".join(problems)) | |
| return self | |
| def supabase_configured(self) -> bool: | |
| return bool(self.supabase_url and self.supabase_service_role_key) | |
| settings = Settings() | |