diff --git a/.env.example b/.env.example index 2878285..e386401 100644 --- a/.env.example +++ b/.env.example @@ -127,6 +127,7 @@ CF_APP_NAME=snipe # ── In-app feedback (beta) ──────────────────────────────────────────────────── # When set, a feedback FAB appears in the UI and routes submissions to Forgejo. # Leave unset to silently hide the button (demo/offline deployments). -# FORGEJO_API_TOKEN= +# FORGEJO_API_TOKEN= # dev/admin token +# FORGEJO_BOT_TOKEN= # cf-bugbot token — used for in-app feedback; falls back to FORGEJO_API_TOKEN # FORGEJO_REPO=Circuit-Forge/snipe # FORGEJO_API_URL=https://git.opensourcesolarpunk.com/api/v1 diff --git a/app/db/pg_shared.py b/app/db/pg_shared.py index 7ab9ac9..027238d 100644 --- a/app/db/pg_shared.py +++ b/app/db/pg_shared.py @@ -5,7 +5,6 @@ from datetime import datetime, timezone from pathlib import Path from typing import Optional -import psycopg2 from psycopg2.pool import ThreadedConnectionPool from app.db.models import MarketComp, ScammerEntry, Seller @@ -262,9 +261,10 @@ class SnipeSharedStore: listing_store must be provided (it's always the per-user SQLite Store). Returns count of sellers updated. """ - from app.platforms.ebay.scraper import _classify_category_label # lazy to avoid circular import json + from app.platforms.ebay.scraper import _classify_category_label # lazy to avoid circular + if not seller_ids or listing_store is None: return 0 diff --git a/app/platforms/mercari/adapter.py b/app/platforms/mercari/adapter.py index 366948f..6f8dbbc 100644 --- a/app/platforms/mercari/adapter.py +++ b/app/platforms/mercari/adapter.py @@ -23,7 +23,7 @@ import logging import time from typing import Optional -from app.db.models import Listing, MarketComp, Seller +from app.db.models import Listing, Seller from app.db.store import Store from app.platforms import PlatformAdapter, SearchFilters from app.platforms.mercari.scraper import ( diff --git a/app/tasks/scheduler.py b/app/tasks/scheduler.py index 962666d..65dd1b1 100644 --- a/app/tasks/scheduler.py +++ b/app/tasks/scheduler.py @@ -6,10 +6,9 @@ from pathlib import Path from circuitforge_core.tasks.scheduler import ( TaskScheduler, # re-export for tests -) + ) from circuitforge_core.tasks.scheduler import ( get_scheduler as _base_get_scheduler, - reset_scheduler, # re-export for lifespan teardown ) from app.tasks.runner import LLM_TASK_TYPES, VRAM_BUDGETS, run_task diff --git a/mkdocs.yml b/mkdocs.yml index 468ca52..5e21b3f 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -16,7 +16,7 @@ theme: name: Switch to dark mode - scheme: slate primary: deep orange - accent: orange + accent: amber toggle: icon: material/brightness-4 name: Switch to light mode @@ -62,5 +62,8 @@ nav: - Tier System: reference/tier-system.md - Architecture: reference/architecture.md +extra_css: + - stylesheets/theme.css + extra_javascript: - plausible.js diff --git a/scripts/debug_fetch_fresh.py b/scripts/debug_fetch_fresh.py index a5382aa..aa99130 100644 --- a/scripts/debug_fetch_fresh.py +++ b/scripts/debug_fetch_fresh.py @@ -3,10 +3,14 @@ Run inside the container: docker exec -it snipe-api-1 python /app/snipe/scripts/debug_fetch_fresh.py """ -import sys, time, threading +import sys +import threading +import time + sys.path.insert(0, '/app/snipe') from bs4 import BeautifulSoup + from app.platforms.ebay.browser_pool import BrowserPool, _close_slot URL = "https://www.mercari.com/search/?keyword=rtx+4090&sortBy=SORT_SCORE&priceMax=800" @@ -34,6 +38,7 @@ print(f"Pool size after warmup: {pool2._q.qsize()}", flush=True) # Grab a slot and close it (simulating the thread-error path) import queue + try: slot = pool2._q.get(timeout=3.0) print(f"Got slot on display :{slot.display_num}", flush=True) diff --git a/scripts/probe_mercari.py b/scripts/probe_mercari.py index cf4a332..46372a8 100644 --- a/scripts/probe_mercari.py +++ b/scripts/probe_mercari.py @@ -8,7 +8,6 @@ from __future__ import annotations import itertools import os import subprocess -import sys import time _display_counter = itertools.count(200) diff --git a/tests/conftest.py b/tests/conftest.py index f390aa0..c8842dd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -1,4 +1,5 @@ import os + import pytest diff --git a/tests/db/test_pg_shared.py b/tests/db/test_pg_shared.py index d7555c8..8a84154 100644 --- a/tests/db/test_pg_shared.py +++ b/tests/db/test_pg_shared.py @@ -1,5 +1,6 @@ """Tests for SnipeSharedStore — requires live Postgres via SNIPE_SHARED_DB_URL.""" import pytest + from app.db.models import MarketComp, Seller from app.db.pg_shared import SnipeSharedDB, SnipeSharedStore from app.db.protocol import SharedTableProtocol diff --git a/tests/db/test_protocol.py b/tests/db/test_protocol.py index c369b84..84b20a9 100644 --- a/tests/db/test_protocol.py +++ b/tests/db/test_protocol.py @@ -17,11 +17,12 @@ def test_store_clone_returns_new_instance(tmp_path): def test_ebay_adapter_accepts_protocol(): - from app.platforms.ebay.adapter import EbayAdapter - import tempfile import pathlib + import tempfile from unittest.mock import MagicMock + from app.platforms.ebay.adapter import EbayAdapter + with tempfile.TemporaryDirectory() as tmp: s = Store(pathlib.Path(tmp) / "t.db") adapter = EbayAdapter(token_manager=MagicMock(), shared_store=s) @@ -29,9 +30,10 @@ def test_ebay_adapter_accepts_protocol(): def test_scraped_adapter_no_db_path_ref(): - from app.platforms.ebay.scraper import ScrapedEbayAdapter - import tempfile import pathlib + import tempfile + + from app.platforms.ebay.scraper import ScrapedEbayAdapter with tempfile.TemporaryDirectory() as tmp: s = Store(pathlib.Path(tmp) / "t.db") diff --git a/tests/test_api_search_build.py b/tests/test_api_search_build.py index e829857..7df70f9 100644 --- a/tests/test_api_search_build.py +++ b/tests/test_api_search_build.py @@ -2,7 +2,6 @@ from __future__ import annotations import json -from pathlib import Path from unittest.mock import MagicMock, patch import pytest diff --git a/tests/test_async_search.py b/tests/test_async_search.py index f64d0dc..dbfaa5e 100644 --- a/tests/test_async_search.py +++ b/tests/test_async_search.py @@ -9,13 +9,11 @@ Verifies: from __future__ import annotations import os -from pathlib import Path from unittest.mock import MagicMock, patch import pytest from fastapi.testclient import TestClient - # ── Fixtures ────────────────────────────────────────────────────────────────── @pytest.fixture @@ -109,8 +107,8 @@ def test_async_search_empty_query(client): assert data["status"] == "queued" assert "session_id" in data + from api.main import _update_queues - import queue as _queue sid = data["session_id"] assert sid in _update_queues q = _update_queues[sid] diff --git a/tests/test_ebay_categories.py b/tests/test_ebay_categories.py index b04a5ee..28282e1 100644 --- a/tests/test_ebay_categories.py +++ b/tests/test_ebay_categories.py @@ -1,7 +1,6 @@ """Unit tests for EbayCategoryCache.""" from __future__ import annotations -import sqlite3 from datetime import datetime, timedelta, timezone from pathlib import Path from unittest.mock import MagicMock, patch diff --git a/tests/test_preferences_currency.py b/tests/test_preferences_currency.py index ddb207f..79513ec 100644 --- a/tests/test_preferences_currency.py +++ b/tests/test_preferences_currency.py @@ -1,7 +1,6 @@ """Tests for PATCH /api/preferences display.currency validation.""" from __future__ import annotations -import os from pathlib import Path from unittest.mock import patch @@ -21,8 +20,9 @@ def client(tmp_path): """ db_path = tmp_path / "snipe.db" # Ensure the DB is initialised so the Store can create its tables. - import api.cloud_session as _cs from circuitforge_core.db import get_connection, run_migrations + + import api.cloud_session as _cs conn = get_connection(db_path) run_migrations(conn, Path("app/db/migrations")) conn.close() diff --git a/tests/test_query_translator.py b/tests/test_query_translator.py index 3841931..fbec627 100644 --- a/tests/test_query_translator.py +++ b/tests/test_query_translator.py @@ -7,8 +7,11 @@ from unittest.mock import MagicMock, patch import pytest -from app.llm.query_translator import QueryTranslator, QueryTranslatorError, SearchParamsResponse, _parse_response - +from app.llm.query_translator import ( + QueryTranslator, + QueryTranslatorError, + _parse_response, +) # ── _parse_response ─────────────────────────────────────────────────────────── @@ -75,9 +78,10 @@ def test_parse_response_missing_required_field(): # ── Fixtures ────────────────────────────────────────────────────────────────── -from app.platforms.ebay.categories import EbayCategoryCache from circuitforge_core.db import get_connection, run_migrations +from app.platforms.ebay.categories import EbayCategoryCache + @pytest.fixture def db_with_categories(tmp_path): diff --git a/tests/test_search_cache.py b/tests/test_search_cache.py index 07ad48c..d0f74f1 100644 --- a/tests/test_search_cache.py +++ b/tests/test_search_cache.py @@ -12,14 +12,11 @@ Covers: from __future__ import annotations import os -import queue as _queue import time -from pathlib import Path from unittest.mock import MagicMock, patch import pytest - # ── Helpers ─────────────────────────────────────────────────────────────────── def _clear_cache(): @@ -41,8 +38,9 @@ def isolated_cache(): def client(tmp_path): """TestClient backed by a fresh tmp DB.""" os.environ["SNIPE_DB"] = str(tmp_path / "snipe.db") - from api.main import app from fastapi.testclient import TestClient + + from api.main import app return TestClient(app, raise_server_exceptions=False) @@ -238,6 +236,7 @@ def test_evict_is_rate_limited(): def test_async_cache_hit_skips_scraper(client, tmp_path): """On a warm cache hit the scraper adapter must not be called.""" import threading + import api.main as _main from api.main import _cache_key @@ -292,6 +291,7 @@ def test_async_cache_hit_skips_scraper(client, tmp_path): def test_async_cache_miss_stores_result(client, tmp_path): """After a cache miss the result must be stored in _search_result_cache.""" import threading + import api.main as _main from api.main import _cache_key @@ -353,6 +353,7 @@ def test_async_cache_miss_stores_result(client, tmp_path): def test_async_refresh_bypasses_cache_read(client, tmp_path): """refresh=True must bypass cache read and invoke the scraper.""" import threading + import api.main as _main from api.main import _cache_key diff --git a/tests/test_tasks/test_monitor.py b/tests/test_tasks/test_monitor.py index af3608b..bc1fc3f 100644 --- a/tests/test_tasks/test_monitor.py +++ b/tests/test_tasks/test_monitor.py @@ -4,13 +4,12 @@ from __future__ import annotations import sqlite3 from datetime import datetime, timedelta, timezone from pathlib import Path -from unittest.mock import MagicMock, patch +from unittest.mock import patch import pytest from app.tasks.monitor import _AUCTION_ALERT_WINDOW_HOURS, should_alert - # --------------------------------------------------------------------------- # should_alert — pure function, no I/O # --------------------------------------------------------------------------- diff --git a/tests/test_tasks/test_runner.py b/tests/test_tasks/test_runner.py index 2d50ecc..312113c 100644 --- a/tests/test_tasks/test_runner.py +++ b/tests/test_tasks/test_runner.py @@ -4,7 +4,7 @@ from __future__ import annotations import json import sqlite3 from pathlib import Path -from unittest.mock import MagicMock, patch, call +from unittest.mock import MagicMock, patch import pytest