From 3878f91296223a900db687b933f5a3f5d6194454 Mon Sep 17 00:00:00 2001 From: Guillaume Duveau Date: Wed, 3 Dec 2025 15:19:21 +0100 Subject: [PATCH 1/4] better DB --- app/routers/domain.py | 116 ++++++++++++-------------------- app/services/cache.py | 109 ++++++++++++++++++++++++++++-- app/services/whois.py | 68 +++++++++++++++++++ scripts/migrate_whois_cache.py | 95 ++++++++++++++++++++++++++ tests/test_cache_persistence.py | 27 ++++++++ tests/test_whois_parsing.py | 39 +---------- 6 files changed, 337 insertions(+), 117 deletions(-) create mode 100644 scripts/migrate_whois_cache.py create mode 100644 tests/test_cache_persistence.py diff --git a/app/routers/domain.py b/app/routers/domain.py index 64ba470..84148ad 100644 --- a/app/routers/domain.py +++ b/app/routers/domain.py @@ -2,7 +2,7 @@ from pydantic import BaseModel from typing import Optional from app.services.cache import WhoisCache -from app.services.whois import WhoisService +from app.services.whois import WhoisService, parse_whois from app.services.rate_limiter import RateLimiter import logging @@ -46,80 +46,33 @@ async def get_whois( tld = parts[-1] # 2. Cache - def parse_whois(raw: str, tld: str): - """Extract statut, creation_date, registrar, pendingDelete, redemptionPeriod for all TLDs. - - This is heuristic: we search common WHOIS labels case-insensitively. - Returns a dict with keys 'statut', 'creation_date', 'registrar', 'pendingDelete', 'redemptionPeriod'. - """ - if not raw: - return { - "statut": None, - "creation_date": None, - "registrar": None, - "pendingDelete": False, - "redemptionPeriod": False, - } - - raw_lines = [l.strip() for l in raw.splitlines() if l.strip()] - lower = raw.lower() - - statut = None - creation_date = None - registrar = None - pendingDelete = False - redemptionPeriod = False - - import re - - # Common patterns (now generalized for all TLDs) - for line in raw_lines: - l = line.lower() - # Registrar: (ignore Registrar WHOIS Server and Registrar URL) - if registrar is None and l.startswith("registrar:") and not ("whois server" in l or "url" in l): - parts = line.split(":", 1) - if len(parts) == 2: - registrar = parts[1].strip() - continue - # Creation date - if creation_date is None and ("creation date" in l or "created on" in l or "created:" in l or "creation:" in l or "registered on" in l): - parts = line.split(":", 1) - if len(parts) == 2: - creation_date = parts[1].strip() - continue - # Status lines (can have multiple) - if "status:" in l or l.startswith("domain status"): - if statut is None: - parts = line.split(":", 1) - if len(parts) == 2: - statut = parts[1].strip() - # Check for pendingDelete and redemptionPeriod in any status line - if "pendingdelete" in l: - pendingDelete = True - if "redemptionperiod" in l: - redemptionPeriod = True - continue - - # Fallback regex for Registrar lines like 'Registrar Name' without colon - if registrar is None: - m = re.search(r"registrar\s+([\w\-\. ]{3,})", raw, re.IGNORECASE) - if m: - registrar = m.group(1).strip() - - return { - "statut": statut, - "creation_date": creation_date, - "registrar": registrar, - "pendingDelete": pendingDelete, - "redemptionPeriod": redemptionPeriod, - } + # parser is provided by app.services.whois.parse_whois if force != 1: cached_data = cache.get(domain) if cached_data: - # enrich from raw before removing it - parsed = parse_whois(cached_data.get("raw"), tld) - # ne pas exposer le champ raw dans la réponse JSON + # Prefer parsed fields persisted in DB. Only fallback to parsing raw if fields are missing. + parsed = { + "statut": cached_data.get("statut"), + "creation_date": cached_data.get("creation_date"), + "registrar": cached_data.get("registrar"), + "pendingDelete": cached_data.get("pendingDelete"), + "redemptionPeriod": cached_data.get("redemptionPeriod"), + } + # If any key is missing/None, parse raw as fallback + if not any(v is not None for v in parsed.values()): + parsed = parse_whois(cached_data.get("raw"), tld) + else: + # ensure booleans normalized (could be stored as 0/1) + try: + parsed["pendingDelete"] = bool(int(parsed["pendingDelete"])) if parsed["pendingDelete"] is not None else False + except Exception: + parsed["pendingDelete"] = bool(parsed.get("pendingDelete")) + try: + parsed["redemptionPeriod"] = bool(int(parsed["redemptionPeriod"])) if parsed["redemptionPeriod"] is not None else False + except Exception: + parsed["redemptionPeriod"] = bool(parsed.get("redemptionPeriod")) + # do not expose raw in responses cached_data.pop("raw", None) # inject parsed fields so response_model includes them cached_data.update(parsed) @@ -158,8 +111,25 @@ def parse_whois(raw: str, tld: str): cached_data = cache.get(domain) if not cached_data: raise HTTPException(status_code=500, detail="Failed to retrieve data from cache after save") - # enrich from raw before removing it (comme pour le cache hit) - parsed = parse_whois(cached_data.get("raw"), tld) + # Prefer parsed fields persisted in DB. Only fallback to parsing raw if fields are missing. + parsed = { + "statut": cached_data.get("statut"), + "creation_date": cached_data.get("creation_date"), + "registrar": cached_data.get("registrar"), + "pendingDelete": cached_data.get("pendingDelete"), + "redemptionPeriod": cached_data.get("redemptionPeriod"), + } + if not any(v is not None for v in parsed.values()): + parsed = parse_whois(cached_data.get("raw"), tld) + else: + try: + parsed["pendingDelete"] = bool(int(parsed["pendingDelete"])) if parsed["pendingDelete"] is not None else False + except Exception: + parsed["pendingDelete"] = bool(parsed.get("pendingDelete")) + try: + parsed["redemptionPeriod"] = bool(int(parsed["redemptionPeriod"])) if parsed["redemptionPeriod"] is not None else False + except Exception: + parsed["redemptionPeriod"] = bool(parsed.get("redemptionPeriod")) cached_data.pop("raw", None) cached_data.update(parsed) # ensure coherence: if pendingDelete or redemptionPeriod, available must be False diff --git a/app/services/cache.py b/app/services/cache.py index 87e2b90..ed6e4cd 100644 --- a/app/services/cache.py +++ b/app/services/cache.py @@ -7,20 +7,32 @@ logger = logging.getLogger(__name__) class WhoisCache: - def __init__(self): - self.db_path = "data/whois_cache.db" + def __init__(self, db_path: str = None): + # Allow overriding DB path for tests or alternate deployments + self.db_path = db_path or "data/whois_cache.db" os.makedirs(os.path.dirname(self.db_path), exist_ok=True) self._init_db() + # Run lightweight migrations/backfill if needed (safe to call on every start) + try: + self._migrate_if_needed() + except Exception: + logger.exception("Migration failed during cache init. Continuing without migration.") def _init_db(self): with sqlite3.connect(self.db_path) as conn: + # Create table with parsed fields. For older DBs, migration script will add missing columns. conn.execute(""" CREATE TABLE IF NOT EXISTS whois_cache ( domain TEXT PRIMARY KEY, tld TEXT, available BOOLEAN, checked_at TEXT, - raw TEXT + raw TEXT, + statut TEXT, + creation_date TEXT, + registrar TEXT, + pendingDelete BOOLEAN, + redemptionPeriod BOOLEAN ) """) @@ -39,15 +51,100 @@ def get(self, domain: str) -> Optional[Dict[str, Any]]: logger.error(f"Cache error on get({domain}): {e}") return None return None + def _ensure_bool(self, val): + # SQLite stores booleans as 0/1 or NULL. Normalize to Python bool where appropriate. + if val is None: + return False + try: + return bool(int(val)) + except Exception: + return bool(val) def set(self, domain: str, tld: str, available: bool, raw: str): checked_at = datetime.now(timezone.utc).isoformat().replace("+00:00", "Z") + # parse raw to extract fields to persist + try: + from app.services.whois import parse_whois + parsed = parse_whois(raw, tld) + except Exception: + parsed = {"statut": None, "creation_date": None, "registrar": None, "pendingDelete": False, "redemptionPeriod": False} + try: with sqlite3.connect(self.db_path) as conn: conn.execute(""" - INSERT OR REPLACE INTO whois_cache (domain, tld, available, checked_at, raw) - VALUES (?, ?, ?, ?, ?) - """, (domain, tld, available, checked_at, raw)) + INSERT OR REPLACE INTO whois_cache + (domain, tld, available, checked_at, raw, statut, creation_date, registrar, pendingDelete, redemptionPeriod) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + """, + ( + domain, + tld, + int(bool(available)), + checked_at, + raw, + parsed.get("statut"), + parsed.get("creation_date"), + parsed.get("registrar"), + int(bool(parsed.get("pendingDelete"))), + int(bool(parsed.get("redemptionPeriod"))) + )) logger.debug(f"Cache SET for domain: {domain} (checked_at: {checked_at})") except sqlite3.Error as e: logger.error(f"Cache error on set({domain}): {e}") + def _migrate_if_needed(self): + """Detect missing expected columns, add them, and backfill parsed fields from raw.""" + EXPECTED = { + "statut": "TEXT", + "creation_date": "TEXT", + "registrar": "TEXT", + "pendingDelete": "BOOLEAN", + "redemptionPeriod": "BOOLEAN", + } + + try: + with sqlite3.connect(self.db_path) as conn: + cur = conn.execute("PRAGMA table_info('whois_cache')") + existing = {row[1] for row in cur.fetchall()} # column names + to_add = [(n, t) for n, t in EXPECTED.items() if n not in existing] + if to_add: + logger.info(f"Cache migration: adding columns: {[n for n, _ in to_add]}") + for name, coltype in to_add: + try: + conn.execute(f"ALTER TABLE whois_cache ADD COLUMN {name} {coltype}") + except sqlite3.Error: + logger.exception(f"Failed to add column {name}; continuing") + conn.commit() + + # Backfill parsed fields for rows where raw is present and parsed columns are NULL/empty + sel = "SELECT domain, raw, tld FROM whois_cache WHERE raw IS NOT NULL AND (statut IS NULL OR creation_date IS NULL OR registrar IS NULL OR pendingDelete IS NULL OR redemptionPeriod IS NULL)" + rows = conn.execute(sel).fetchall() + if rows: + logger.info(f"Cache migration: backfilling parsed fields for {len(rows)} rows") + # Import parser locally to avoid circular issues + try: + from app.services.whois import parse_whois + except Exception: + logger.exception("Could not import parse_whois for migration; skipping backfill") + return + + upd = "UPDATE whois_cache SET statut = ?, creation_date = ?, registrar = ?, pendingDelete = ?, redemptionPeriod = ? WHERE domain = ?" + updated = 0 + for domain, raw, tld in rows: + try: + parsed = parse_whois(raw, tld) + conn.execute(upd, ( + parsed.get("statut"), + parsed.get("creation_date"), + parsed.get("registrar"), + int(bool(parsed.get("pendingDelete"))), + int(bool(parsed.get("redemptionPeriod"))), + domain, + )) + updated += 1 + except Exception: + logger.exception(f"Failed to backfill domain {domain}; skipping") + if updated: + conn.commit() + logger.info(f"Cache migration: backfilled {updated} rows") + except sqlite3.Error: + logger.exception("SQLite error during cache migration") diff --git a/app/services/whois.py b/app/services/whois.py index 0a01c51..280a0cd 100644 --- a/app/services/whois.py +++ b/app/services/whois.py @@ -53,3 +53,71 @@ def is_available(self, raw_output: str, tld: str) -> bool: return True return False + + +def parse_whois(raw: str, tld: str): + """Extract statut, creation_date, registrar, pendingDelete, redemptionPeriod for all TLDs. + + Heuristic parser reused across the app and migration scripts. + """ + if not raw: + return { + "statut": None, + "creation_date": None, + "registrar": None, + "pendingDelete": False, + "redemptionPeriod": False, + } + + raw_lines = [l.strip() for l in raw.splitlines() if l.strip()] + lower = raw.lower() + + statut = None + creation_date = None + registrar = None + pendingDelete = False + redemptionPeriod = False + + import re + + # Common patterns (generalized for many TLDs) + for line in raw_lines: + l = line.lower() + # Registrar: (ignore Registrar WHOIS Server and Registrar URL) + if registrar is None and l.startswith("registrar:") and not ("whois server" in l or "url" in l): + parts = line.split(":", 1) + if len(parts) == 2: + registrar = parts[1].strip() + continue + # Creation date + if creation_date is None and ("creation date" in l or "created on" in l or "created:" in l or "creation:" in l or "registered on" in l): + parts = line.split(":", 1) + if len(parts) == 2: + creation_date = parts[1].strip() + continue + # Status lines (can have multiple) + if "status:" in l or l.startswith("domain status"): + if statut is None: + parts = line.split(":", 1) + if len(parts) == 2: + statut = parts[1].strip() + # Check for pendingDelete and redemptionPeriod in any status line + if "pendingdelete" in l: + pendingDelete = True + if "redemptionperiod" in l: + redemptionPeriod = True + continue + + # Fallback regex for Registrar lines like 'Registrar Name' without colon + if registrar is None: + m = re.search(r"registrar\s+([\w\-\. ]{3,})", raw, re.IGNORECASE) + if m: + registrar = m.group(1).strip() + + return { + "statut": statut, + "creation_date": creation_date, + "registrar": registrar, + "pendingDelete": pendingDelete, + "redemptionPeriod": redemptionPeriod, + } diff --git a/scripts/migrate_whois_cache.py b/scripts/migrate_whois_cache.py new file mode 100644 index 0000000..5599dc8 --- /dev/null +++ b/scripts/migrate_whois_cache.py @@ -0,0 +1,95 @@ +#!/usr/bin/env python3 +""" +Migration script for whois_cache SQLite DB. +- Adds missing columns (statut, creation_date, registrar, pendingDelete, redemptionPeriod) +- Backfills parsed fields from existing raw values using app.services.whois.parse_whois + +Usage: + ./scripts/migrate_whois_cache.py + +It will operate on data/whois_cache.db relative to the repository root. +""" +import sqlite3 +import os +import sys +from pathlib import Path + +REPO_ROOT = Path(__file__).resolve().parents[1] +DB_PATH = REPO_ROOT / "data" / "whois_cache.db" + +EXPECTED_COLUMNS = { + "statut": "TEXT", + "creation_date": "TEXT", + "registrar": "TEXT", + "pendingDelete": "BOOLEAN", + "redemptionPeriod": "BOOLEAN", +} + + +def get_existing_columns(conn): + cur = conn.execute("PRAGMA table_info('whois_cache')") + return {row[1]: row for row in cur.fetchall()} # name -> row + + +def add_column(conn, name, coltype): + print(f"Adding column {name} {coltype}") + conn.execute(f"ALTER TABLE whois_cache ADD COLUMN {name} {coltype}") + + +def main(): + if not DB_PATH.exists(): + print(f"DB not found at {DB_PATH}, nothing to migrate.\nYou can run the app and it will create a fresh DB with the new schema.") + return + + import importlib + sys.path.insert(0, str(REPO_ROOT)) + try: + whois_mod = importlib.import_module("app.services.whois") + except Exception as e: + print(f"Failed to import app.services.whois: {e}") + raise + + parse_whois = getattr(whois_mod, "parse_whois", None) + if parse_whois is None: + print("parse_whois not found in app.services.whois; aborting") + return + + conn = sqlite3.connect(str(DB_PATH)) + try: + existing = get_existing_columns(conn) + to_add = [ (n,t) for n,t in EXPECTED_COLUMNS.items() if n not in existing ] + if not to_add: + print("No columns to add.") + else: + for name, coltype in to_add: + add_column(conn, name, coltype) + conn.commit() + print(f"Added {len(to_add)} columns.") + + # Backfill parsed values for rows that have raw + cur = conn.execute("SELECT domain, raw, tld FROM whois_cache WHERE raw IS NOT NULL") + rows = cur.fetchall() + print(f"Found {len(rows)} rows with raw to backfill.") + updated = 0 + for domain, raw, tld in rows: + parsed = parse_whois(raw, tld) + conn.execute( + "UPDATE whois_cache SET statut = ?, creation_date = ?, registrar = ?, pendingDelete = ?, redemptionPeriod = ? WHERE domain = ?", + ( + parsed.get("statut"), + parsed.get("creation_date"), + parsed.get("registrar"), + int(bool(parsed.get("pendingDelete"))), + int(bool(parsed.get("redemptionPeriod"))), + domain, + ) + ) + updated += 1 + conn.commit() + print(f"Backfilled parsed fields for {updated} rows.") + finally: + conn.close() + + +if __name__ == "__main__": + main() diff --git a/tests/test_cache_persistence.py b/tests/test_cache_persistence.py new file mode 100644 index 0000000..68c7faa --- /dev/null +++ b/tests/test_cache_persistence.py @@ -0,0 +1,27 @@ +import os +import pytest +from app.services.cache import WhoisCache + + +def test_cache_persistence(tmp_path): + # Use a temporary DB for isolation + db_file = tmp_path / "whois_cache.db" + cache = WhoisCache(db_path=str(db_file)) + + # sample data from tests/data + domain = "cadeaux.com" + tld = "com" + path = os.path.join(os.path.dirname(__file__), "data", "whois-cadeaux.com") + with open(path, encoding="utf-8") as f: + raw = f.read() + + # Ensure set() stores parsed fields + cache.set(domain, tld, available=False, raw=raw) + entry = cache.get(domain) + assert entry is not None + # parsed fields should be present and match expectations + assert entry.get("registrar") == "OVH sas" + assert entry.get("creation_date") == "2002-05-13T18:12:06Z" + assert entry.get("pendingDelete") in (0, 1, False, True) + # Normalize to boolean check + assert bool(int(entry.get("pendingDelete"))) is False diff --git a/tests/test_whois_parsing.py b/tests/test_whois_parsing.py index 0efca6e..0c5f717 100644 --- a/tests/test_whois_parsing.py +++ b/tests/test_whois_parsing.py @@ -1,43 +1,6 @@ import os import pytest - -def parse_whois(raw: str, tld: str): - if not raw: - return {"statut": None, "creation_date": None, "registrar": None, "pendingDelete": False, "redemptionPeriod": False} - raw_lines = [l.strip() for l in raw.splitlines() if l.strip()] - statut = None - creation_date = None - registrar = None - pendingDelete = False - redemptionPeriod = False - import re - for line in raw_lines: - l = line.lower() - if registrar is None and l.startswith("registrar:") and not ("whois server" in l or "url" in l): - parts = line.split(":", 1) - if len(parts) == 2: - registrar = parts[1].strip() - continue - if creation_date is None and ("creation date" in l or "created on" in l or "created:" in l or "creation:" in l or "registered on" in l): - parts = line.split(":", 1) - if len(parts) == 2: - creation_date = parts[1].strip() - continue - if "status:" in l or l.startswith("domain status"): - if statut is None: - parts = line.split(":", 1) - if len(parts) == 2: - statut = parts[1].strip() - if "pendingdelete" in l: - pendingDelete = True - if "redemptionperiod" in l: - redemptionPeriod = True - continue - if registrar is None: - m = re.search(r"registrar\s+([\w\-\. ]{3,})", raw, re.IGNORECASE) - if m: - registrar = m.group(1).strip() - return {"statut": statut, "creation_date": creation_date, "registrar": registrar, "pendingDelete": pendingDelete, "redemptionPeriod": redemptionPeriod} +from app.services.whois import parse_whois def test_parse_whois_cadeaux_com(): path = os.path.join(os.path.dirname(__file__), "data", "whois-cadeaux.com") From fac4c8db05c8d6aac94c3a681b00f1e98a4c9fa2 Mon Sep 17 00:00:00 2001 From: Guillaume Duveau Date: Wed, 3 Dec 2025 15:37:45 +0100 Subject: [PATCH 2/4] fix tests --- .github/workflows/test.yml | 44 +++++++------- gptkit.egg-info/PKG-INFO | 86 ++++++++++++++++++++++++++++ gptkit.egg-info/SOURCES.txt | 15 +++++ gptkit.egg-info/dependency_links.txt | 1 + gptkit.egg-info/requires.txt | 6 ++ gptkit.egg-info/top_level.txt | 1 + pyproject.toml | 34 +++++++++++ 7 files changed, 163 insertions(+), 24 deletions(-) create mode 100644 gptkit.egg-info/PKG-INFO create mode 100644 gptkit.egg-info/SOURCES.txt create mode 100644 gptkit.egg-info/dependency_links.txt create mode 100644 gptkit.egg-info/requires.txt create mode 100644 gptkit.egg-info/top_level.txt create mode 100644 pyproject.toml diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 799299a..d0676d0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,35 +1,31 @@ -name: Test +name: Tests on: + push: + branches: [ main, dev ] pull_request: - branches: [ "main" ] + branches: [ main, dev ] jobs: test: - name: Run tests runs-on: ubuntu-latest - steps: - - name: Checkout repository - uses: actions/checkout@v4 + strategy: + matrix: + python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] - - name: Set up Python - uses: actions/setup-python@v4 - with: - python-version: '3.11' + steps: + - uses: actions/checkout@v4 - - name: Restore pip cache - uses: actions/cache@v4 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('**/requirements.txt') }} - restore-keys: | - ${{ runner.os }}-pip- + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} - - name: Install dependencies - run: | - python -m pip install --upgrade pip - if [ -f requirements-dev.txt ]; then pip install -r requirements-dev.txt; fi + - name: Install dependencies + run: | + python -m pip install --upgrade pip + pip install -e ".[dev]" - - name: Run pytest - run: | - pytest -q + - name: Run tests + run: | + pytest -v diff --git a/gptkit.egg-info/PKG-INFO b/gptkit.egg-info/PKG-INFO new file mode 100644 index 0000000..c9f0cb1 --- /dev/null +++ b/gptkit.egg-info/PKG-INFO @@ -0,0 +1,86 @@ +Metadata-Version: 2.4 +Name: gptkit +Version: 1.0.0 +Summary: Backend for Custom GPT Actions +Requires-Python: >=3.8 +Description-Content-Type: text/markdown +Requires-Dist: fastapi +Requires-Dist: uvicorn[standard] +Requires-Dist: pydantic +Provides-Extra: dev +Requires-Dist: pytest; extra == "dev" + +# GPTKit + +GPTKit is a unified backend designed to provide tools via HTTP Actions for Custom GPTs. + +## Tools + +### WHOIS (`/domain/whois`) + +Allows checking domain name availability and retrieving WHOIS information. + +- **Endpoint**: `GET /domain/whois` +- **Parameters**: + - `domain` (required): The domain name to check (e.g., `google.com`). + - `force` (optional): `1` to force a fresh WHOIS lookup (ignores cache). +- **Features**: + - Persistent cache (SQLite). + - Rate limiting (global and per domain). + - Automatic availability parsing for major TLDs. + +## Deployment + +### Docker Compose + +Here is an example `docker-compose.yml` configuration to deploy GPTKit. + +> **Note**: The image is available on GHCR. Make sure to replace `your-username` with your GitHub username. + +```yaml +services: + gptkit: + image: ghcr.io/your-username/gptkit:latest + restart: unless-stopped + ports: + - "8000:8000" + volumes: + # Data persistence (WHOIS cache stored in /app/data/whois_cache.db) + - gptkit_data:/app/data + +volumes: + gptkit_data: +``` + +## Development + +1. **Installation**: + ```bash + pip install -r requirements.txt + ``` + +2. **Run**: + ```bash + uvicorn app.main:app --reload + ``` +3. **Tests**: + +- Quick API smoke test (curl): + ```bash + curl "http://localhost:8000/domain/whois?domain=example.com" + ``` + +- Run the unit test suite with pytest (from the project root): + ```bash + # activate your virtualenv if you have one, e.g.: + source venv/bin/activate + + # install test/dev dependencies if needed + pip install -r requirements.txt + + # run all tests + pytest -q + + # run a single test file + pytest tests/test_whois_parsing.py -q + ``` diff --git a/gptkit.egg-info/SOURCES.txt b/gptkit.egg-info/SOURCES.txt new file mode 100644 index 0000000..5f4e9a1 --- /dev/null +++ b/gptkit.egg-info/SOURCES.txt @@ -0,0 +1,15 @@ +README.md +pyproject.toml +./app/__init__.py +./app/main.py +./app/routers/domain.py +./app/services/cache.py +./app/services/rate_limiter.py +./app/services/whois.py +gptkit.egg-info/PKG-INFO +gptkit.egg-info/SOURCES.txt +gptkit.egg-info/dependency_links.txt +gptkit.egg-info/requires.txt +gptkit.egg-info/top_level.txt +tests/test_cache_persistence.py +tests/test_whois_parsing.py \ No newline at end of file diff --git a/gptkit.egg-info/dependency_links.txt b/gptkit.egg-info/dependency_links.txt new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/gptkit.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/gptkit.egg-info/requires.txt b/gptkit.egg-info/requires.txt new file mode 100644 index 0000000..ed0f5c8 --- /dev/null +++ b/gptkit.egg-info/requires.txt @@ -0,0 +1,6 @@ +fastapi +uvicorn[standard] +pydantic + +[dev] +pytest diff --git a/gptkit.egg-info/top_level.txt b/gptkit.egg-info/top_level.txt new file mode 100644 index 0000000..b80f0bd --- /dev/null +++ b/gptkit.egg-info/top_level.txt @@ -0,0 +1 @@ +app diff --git a/pyproject.toml b/pyproject.toml new file mode 100644 index 0000000..c0528e3 --- /dev/null +++ b/pyproject.toml @@ -0,0 +1,34 @@ +[build-system] +requires = ["setuptools>=61.0", "wheel"] +build-backend = "setuptools.build_meta" + +[project] +name = "gptkit" +version = "1.0.0" +description = "Backend for Custom GPT Actions" +readme = "README.md" +requires-python = ">=3.8" +dependencies = [ + "fastapi", + "uvicorn[standard]", + "pydantic", +] + +[project.optional-dependencies] +dev = [ + "pytest", +] + +[tool.setuptools] +package-dir = {"" = "."} + +[tool.setuptools.packages.find] +where = ["."] +include = ["app*"] + +[tool.pytest.ini_options] +testpaths = ["tests"] +python_files = ["test_*.py"] +python_classes = ["Test*"] +python_functions = ["test_*"] + From 9c9432bd6bf727566fc6b900f190fbd36fd9d005 Mon Sep 17 00:00:00 2001 From: Guillaume Duveau Date: Wed, 3 Dec 2025 15:55:11 +0100 Subject: [PATCH 3/4] better python --- .github/workflows/test.yml | 2 +- .python-version | 1 + gptkit.egg-info/SOURCES.txt | 6 ++++++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 .python-version diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index d0676d0..c681f24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -11,7 +11,7 @@ jobs: runs-on: ubuntu-latest strategy: matrix: - python-version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13"] + python-version: ["3.11"] steps: - uses: actions/checkout@v4 diff --git a/.python-version b/.python-version new file mode 100644 index 0000000..641602f --- /dev/null +++ b/.python-version @@ -0,0 +1 @@ +3.11.14 diff --git a/gptkit.egg-info/SOURCES.txt b/gptkit.egg-info/SOURCES.txt index 5f4e9a1..f8757dc 100644 --- a/gptkit.egg-info/SOURCES.txt +++ b/gptkit.egg-info/SOURCES.txt @@ -6,6 +6,12 @@ pyproject.toml ./app/services/cache.py ./app/services/rate_limiter.py ./app/services/whois.py +app/__init__.py +app/main.py +app/routers/domain.py +app/services/cache.py +app/services/rate_limiter.py +app/services/whois.py gptkit.egg-info/PKG-INFO gptkit.egg-info/SOURCES.txt gptkit.egg-info/dependency_links.txt From 0ef25ce3f0683acfb7e06c558365e8e23d0aa8fa Mon Sep 17 00:00:00 2001 From: Guillaume Duveau Date: Wed, 3 Dec 2025 16:01:37 +0100 Subject: [PATCH 4/4] fix triggers --- .github/workflows/test.yml | 4 +--- gptkit.egg-info/PKG-INFO | 2 +- pyproject.toml | 2 +- 3 files changed, 3 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index c681f24..1b53cda 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -1,10 +1,8 @@ name: Tests on: - push: - branches: [ main, dev ] pull_request: - branches: [ main, dev ] + branches: [ main ] jobs: test: diff --git a/gptkit.egg-info/PKG-INFO b/gptkit.egg-info/PKG-INFO index c9f0cb1..2052a13 100644 --- a/gptkit.egg-info/PKG-INFO +++ b/gptkit.egg-info/PKG-INFO @@ -2,7 +2,7 @@ Metadata-Version: 2.4 Name: gptkit Version: 1.0.0 Summary: Backend for Custom GPT Actions -Requires-Python: >=3.8 +Requires-Python: >=3.11 Description-Content-Type: text/markdown Requires-Dist: fastapi Requires-Dist: uvicorn[standard] diff --git a/pyproject.toml b/pyproject.toml index c0528e3..10499fd 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,7 +7,7 @@ name = "gptkit" version = "1.0.0" description = "Backend for Custom GPT Actions" readme = "README.md" -requires-python = ">=3.8" +requires-python = ">=3.11" dependencies = [ "fastapi", "uvicorn[standard]",