Compare commits

..

No commits in common. "feat/ci-workflows" and "main" have entirely different histories.

35 changed files with 62 additions and 319 deletions

View file

@ -1,41 +0,0 @@
[changelog]
header = """
# Changelog\n
"""
body = """
{% if version %}\
## [{{ version | trim_start_matches(pat="v") }}] — {{ timestamp | date(format="%Y-%m-%d") }}
{% else %}\
## [Unreleased]
{% endif %}\
{% for group, commits in commits | group_by(attribute="group") %}
### {{ group | striptags | trim | upper_first }}
{% for commit in commits %}
- {% if commit.scope %}**{{ commit.scope }}:** {% endif %}{{ commit.message | upper_first }}\
{% endfor %}
{% endfor %}\n
"""
trim = true
footer = ""
[git]
conventional_commits = true
filter_unconventional = true
split_commits = false
commit_preprocessors = [
{ pattern = '\(#([0-9]+)\)', replace = "([#${1}](https://git.opensourcesolarpunk.com/Circuit-Forge/snipe/issues/${1}))" },
]
commit_parsers = [
{ message = "^feat", group = "Features" },
{ message = "^fix", group = "Bug Fixes" },
{ message = "^perf", group = "Performance" },
{ message = "^refactor", group = "Refactoring" },
{ message = "^docs", group = "Documentation" },
{ message = "^test", group = "Testing" },
{ message = "^revert", group = "Reverts" },
{ message = "^chore", skip = true },
]
filter_commits = true
tag_pattern = "v[0-9]*"
topo_order = false
sort_commits = "oldest"

View file

@ -1,62 +0,0 @@
name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
api:
name: API — lint + test
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: "3.11"
- name: Install circuitforge-core
env:
FORGEJO_TOKEN: ${{ secrets.FORGEJO_TOKEN }}
run: |
git clone https://x-token:${FORGEJO_TOKEN}@git.opensourcesolarpunk.com/Circuit-Forge/circuitforge-core.git /tmp/circuitforge-core
pip install -e /tmp/circuitforge-core
- name: Install snipe + dev deps
run: pip install -e ".[dev]"
- name: Lint (ruff)
run: ruff check app/ api/ tests/
- name: Run tests
run: pytest tests/ -v
web:
name: Web — typecheck + test + build
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: "20"
cache: "npm"
cache-dependency-path: web/package-lock.json
- name: Install deps
working-directory: web
run: npm ci
- name: Type check
working-directory: web
run: npx vue-tsc --noEmit
- name: Unit tests
working-directory: web
run: npm test
- name: Build
working-directory: web
run: npm run build

View file

@ -1,37 +0,0 @@
name: Mirror
on:
push:
branches: [main]
tags: ["v*"]
jobs:
mirror-github:
name: Mirror to GitHub
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push to GitHub
env:
GH_MIRROR_TOKEN: ${{ secrets.GH_MIRROR_TOKEN }}
run: |
git remote add github https://x-access-token:${GH_MIRROR_TOKEN}@github.com/CircuitForgeLLC/snipe.git
git push github --mirror
mirror-codeberg:
name: Mirror to Codeberg
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Push to Codeberg
env:
CODEBERG_MIRROR_TOKEN: ${{ secrets.CODEBERG_MIRROR_TOKEN }}
run: |
git remote add codeberg https://CircuitForge:${CODEBERG_MIRROR_TOKEN}@codeberg.org/CircuitForge/snipe.git
git push codeberg --mirror

View file

@ -1,67 +0,0 @@
name: Release
on:
push:
tags: ["v*"]
jobs:
release:
name: Build images + create release
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Generate changelog
uses: orhun/git-cliff-action@v3
id: cliff
with:
config: .cliff.toml
args: --current --strip header
env:
OUTPUT: CHANGES.md
- uses: docker/setup-buildx-action@v3
# BSL product: push to Forgejo registry only.
# GHCR push is deferred pending license-gate-at-startup implementation (Circuit-Forge/snipe#3).
- uses: docker/login-action@v3
with:
registry: git.opensourcesolarpunk.com
username: ${{ gitea.actor }}
password: ${{ secrets.FORGEJO_RELEASE_TOKEN }}
- name: Build and push API image
uses: docker/build-push-action@v5
with:
context: .
file: docker/api/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
git.opensourcesolarpunk.com/circuit-forge/snipe-api:${{ gitea.ref_name }}
git.opensourcesolarpunk.com/circuit-forge/snipe-api:latest
- name: Build and push web image
uses: docker/build-push-action@v5
with:
context: .
file: docker/web/Dockerfile
push: true
platforms: linux/amd64,linux/arm64
tags: |
git.opensourcesolarpunk.com/circuit-forge/snipe-web:${{ gitea.ref_name }}
git.opensourcesolarpunk.com/circuit-forge/snipe-web:latest
- name: Create Forgejo release
env:
FORGEJO_RELEASE_TOKEN: ${{ secrets.FORGEJO_RELEASE_TOKEN }}
TAG: ${{ gitea.ref_name }}
run: |
body=$(cat CHANGES.md | python3 -c "import sys, json; print(json.dumps(sys.stdin.read()))")
curl -sf -X POST \
"https://git.opensourcesolarpunk.com/api/v1/repos/Circuit-Forge/snipe/releases" \
-H "Authorization: token ${FORGEJO_RELEASE_TOKEN}" \
-H "Content-Type: application/json" \
-d "{\"tag_name\": \"${TAG}\", \"name\": \"${TAG}\", \"body\": ${body}}"

View file

@ -16,6 +16,8 @@ FastAPI usage:
""" """
from __future__ import annotations from __future__ import annotations
import hashlib
import hmac
import logging import logging
import os import os
import re import re
@ -75,6 +77,7 @@ def compute_features(tier: str) -> SessionFeatures:
"""Compute feature flags from tier. Evaluated server-side; sent to frontend.""" """Compute feature flags from tier. Evaluated server-side; sent to frontend."""
local = tier == "local" local = tier == "local"
paid_plus = local or tier in ("paid", "premium", "ultra") paid_plus = local or tier in ("paid", "premium", "ultra")
premium_plus = local or tier in ("premium", "ultra")
return SessionFeatures( return SessionFeatures(
saved_searches=True, # all tiers get saved searches saved_searches=True, # all tiers get saved searches

View file

@ -26,11 +26,11 @@ from pathlib import Path
from typing import Optional from typing import Optional
import requests import requests
from fastapi import APIRouter, Header, HTTPException, Request
from cryptography.exceptions import InvalidSignature from cryptography.exceptions import InvalidSignature
from cryptography.hazmat.primitives.asymmetric.ec import ECDSA from cryptography.hazmat.primitives.asymmetric.ec import ECDSA
from cryptography.hazmat.primitives.hashes import SHA1 from cryptography.hazmat.primitives.hashes import SHA1
from cryptography.hazmat.primitives.serialization import load_pem_public_key from cryptography.hazmat.primitives.serialization import load_pem_public_key
from fastapi import APIRouter, Header, HTTPException, Request
from app.db.store import Store from app.db.store import Store

View file

@ -1,11 +1,8 @@
"""Snipe FastAPI — search endpoint wired to ScrapedEbayAdapter + TrustScorer.""" """Snipe FastAPI — search endpoint wired to ScrapedEbayAdapter + TrustScorer."""
from __future__ import annotations from __future__ import annotations
import asyncio
import csv
import dataclasses import dataclasses
import hashlib import hashlib
import io
import json as _json import json as _json
import logging import logging
import os import os
@ -15,25 +12,28 @@ from concurrent.futures import ThreadPoolExecutor
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from pathlib import Path from pathlib import Path
from circuitforge_core.affiliates import wrap_url as _wrap_affiliate_url import asyncio
from circuitforge_core.api import make_feedback_router as _make_feedback_router import csv
from circuitforge_core.config import load_env import io
from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile
from fastapi.middleware.cors import CORSMiddleware from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile, File
from fastapi.responses import StreamingResponse from fastapi.responses import StreamingResponse
from pydantic import BaseModel from pydantic import BaseModel
from fastapi.middleware.cors import CORSMiddleware
from api.cloud_session import CloudUser, compute_features, get_session from circuitforge_core.config import load_env
from api.ebay_webhook import router as ebay_webhook_router from circuitforge_core.affiliates import wrap_url as _wrap_affiliate_url
from app.db.models import SavedSearch as SavedSearchModel from circuitforge_core.api import make_feedback_router as _make_feedback_router
from app.db.models import ScammerEntry
from app.db.store import Store from app.db.store import Store
from app.db.models import SavedSearch as SavedSearchModel, ScammerEntry
from app.platforms import SearchFilters from app.platforms import SearchFilters
from app.platforms.ebay.scraper import ScrapedEbayAdapter
from app.platforms.ebay.adapter import EbayAdapter from app.platforms.ebay.adapter import EbayAdapter
from app.platforms.ebay.auth import EbayTokenManager from app.platforms.ebay.auth import EbayTokenManager
from app.platforms.ebay.query_builder import expand_queries, parse_groups from app.platforms.ebay.query_builder import expand_queries, parse_groups
from app.platforms.ebay.scraper import ScrapedEbayAdapter
from app.trust import TrustScorer from app.trust import TrustScorer
from api.cloud_session import CloudUser, compute_features, get_session
from api.ebay_webhook import router as ebay_webhook_router
load_env(Path(".env")) load_env(Path(".env"))
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -50,8 +50,8 @@ async def _lifespan(app: FastAPI):
# Start vision/LLM background task scheduler. # Start vision/LLM background task scheduler.
# background_tasks queue lives in shared_db (cloud) or local_db (local) # background_tasks queue lives in shared_db (cloud) or local_db (local)
# so the scheduler has a single stable DB path across all cloud users. # so the scheduler has a single stable DB path across all cloud users.
from api.cloud_session import _LOCAL_SNIPE_DB, CLOUD_MODE, _shared_db_path
from app.tasks.scheduler import get_scheduler, reset_scheduler from app.tasks.scheduler import get_scheduler, reset_scheduler
from api.cloud_session import CLOUD_MODE, _LOCAL_SNIPE_DB, _shared_db_path
sched_db = _shared_db_path() if CLOUD_MODE else _LOCAL_SNIPE_DB sched_db = _shared_db_path() if CLOUD_MODE else _LOCAL_SNIPE_DB
get_scheduler(sched_db) get_scheduler(sched_db)
log.info("Snipe task scheduler started (db=%s)", sched_db) log.info("Snipe task scheduler started (db=%s)", sched_db)
@ -245,10 +245,9 @@ def _enqueue_vision_tasks(
trust_scores table in cloud mode. trust_scores table in cloud mode.
""" """
import json as _json import json as _json
from api.cloud_session import _LOCAL_SNIPE_DB, CLOUD_MODE, _shared_db_path
from app.tasks.runner import insert_task from app.tasks.runner import insert_task
from app.tasks.scheduler import get_scheduler from app.tasks.scheduler import get_scheduler
from api.cloud_session import CLOUD_MODE, _shared_db_path, _LOCAL_SNIPE_DB
sched_db = _shared_db_path() if CLOUD_MODE else _LOCAL_SNIPE_DB sched_db = _shared_db_path() if CLOUD_MODE else _LOCAL_SNIPE_DB
sched = get_scheduler(sched_db) sched = get_scheduler(sched_db)

View file

@ -1,6 +1,5 @@
"""Dataclasses for all Snipe domain objects.""" """Dataclasses for all Snipe domain objects."""
from __future__ import annotations from __future__ import annotations
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from typing import Optional

View file

@ -1,6 +1,5 @@
"""Thin SQLite read/write layer for all Snipe models.""" """Thin SQLite read/write layer for all Snipe models."""
from __future__ import annotations from __future__ import annotations
import json import json
from datetime import datetime, timezone from datetime import datetime, timezone
from pathlib import Path from pathlib import Path
@ -8,7 +7,7 @@ from typing import Optional
from circuitforge_core.db import get_connection, run_migrations from circuitforge_core.db import get_connection, run_migrations
from .models import Listing, MarketComp, SavedSearch, ScammerEntry, Seller, TrustScore from .models import Listing, Seller, TrustScore, MarketComp, SavedSearch, ScammerEntry
MIGRATIONS_DIR = Path(__file__).parent / "migrations" MIGRATIONS_DIR = Path(__file__).parent / "migrations"

View file

@ -1,10 +1,8 @@
"""PlatformAdapter abstract base and shared types.""" """PlatformAdapter abstract base and shared types."""
from __future__ import annotations from __future__ import annotations
from abc import ABC, abstractmethod from abc import ABC, abstractmethod
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from typing import Optional
from app.db.models import Listing, Seller from app.db.models import Listing, Seller

View file

@ -1,12 +1,10 @@
"""eBay Browse API adapter.""" """eBay Browse API adapter."""
from __future__ import annotations from __future__ import annotations
import hashlib import hashlib
import logging import logging
from dataclasses import replace from dataclasses import replace
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
import requests import requests
log = logging.getLogger(__name__) log = logging.getLogger(__name__)
@ -20,7 +18,7 @@ _SHOPPING_API_MAX_PER_SEARCH = 5 # sellers enriched per search call
_SHOPPING_API_INTER_REQUEST_DELAY = 0.5 # seconds between successive calls _SHOPPING_API_INTER_REQUEST_DELAY = 0.5 # seconds between successive calls
_SELLER_ENRICH_TTL_HOURS = 24 # skip re-enrichment within this window _SELLER_ENRICH_TTL_HOURS = 24 # skip re-enrichment within this window
from app.db.models import Listing, MarketComp, Seller from app.db.models import Listing, Seller, MarketComp
from app.db.store import Store from app.db.store import Store
from app.platforms import PlatformAdapter, SearchFilters from app.platforms import PlatformAdapter, SearchFilters
from app.platforms.ebay.auth import EbayTokenManager from app.platforms.ebay.auth import EbayTokenManager

View file

@ -1,10 +1,8 @@
"""eBay OAuth2 client credentials token manager.""" """eBay OAuth2 client credentials token manager."""
from __future__ import annotations from __future__ import annotations
import base64 import base64
import time import time
from typing import Optional from typing import Optional
import requests import requests
EBAY_OAUTH_URLS = { EBAY_OAUTH_URLS = {

View file

@ -1,10 +1,8 @@
"""Convert raw eBay API responses into Snipe domain objects.""" """Convert raw eBay API responses into Snipe domain objects."""
from __future__ import annotations from __future__ import annotations
import json import json
from datetime import datetime, timezone from datetime import datetime, timezone
from typing import Optional from typing import Optional
from app.db.models import Listing, Seller from app.db.models import Listing, Seller

View file

@ -16,7 +16,7 @@ import json
import logging import logging
import re import re
import time import time
from concurrent.futures import ThreadPoolExecutor from concurrent.futures import ThreadPoolExecutor, as_completed
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from typing import Optional from typing import Optional
@ -302,8 +302,7 @@ class ScrapedEbayAdapter(PlatformAdapter):
time.sleep(self._delay) time.sleep(self._delay)
import os import subprocess, os
import subprocess
display_num = next(_display_counter) display_num = next(_display_counter)
display = f":{display_num}" display = f":{display_num}"
xvfb = subprocess.Popen( xvfb = subprocess.Popen(
@ -314,10 +313,8 @@ class ScrapedEbayAdapter(PlatformAdapter):
env["DISPLAY"] = display env["DISPLAY"] = display
try: try:
from playwright.sync_api import ( from playwright.sync_api import sync_playwright # noqa: PLC0415 — lazy: only needed in Docker
sync_playwright, # noqa: PLC0415 — lazy: only needed in Docker from playwright_stealth import Stealth # noqa: PLC0415
)
from playwright_stealth import Stealth # noqa: PLC0415
with sync_playwright() as pw: with sync_playwright() as pw:
browser = pw.chromium.launch( browser = pw.chromium.launch(

View file

@ -19,6 +19,7 @@ import logging
from pathlib import Path from pathlib import Path
import requests import requests
from circuitforge_core.db import get_connection from circuitforge_core.db import get_connection
from circuitforge_core.llm import LLMRouter from circuitforge_core.llm import LLMRouter

View file

@ -5,10 +5,9 @@ from __future__ import annotations
from pathlib import Path from pathlib import Path
from circuitforge_core.tasks.scheduler import ( from circuitforge_core.tasks.scheduler import (
TaskScheduler, # re-export for tests TaskScheduler,
)
from circuitforge_core.tasks.scheduler import (
get_scheduler as _base_get_scheduler, get_scheduler as _base_get_scheduler,
reset_scheduler, # re-export for tests
) )
from app.tasks.runner import LLM_TASK_TYPES, VRAM_BUDGETS, run_task from app.tasks.runner import LLM_TASK_TYPES, VRAM_BUDGETS, run_task

View file

@ -14,8 +14,7 @@ Intentionally ungated (free for all):
- saved_searches retention feature; friction cost outweighs gate value - saved_searches retention feature; friction cost outweighs gate value
""" """
from __future__ import annotations from __future__ import annotations
from circuitforge_core.tiers import can_use as _core_can_use, TIERS # noqa: F401
from circuitforge_core.tiers import can_use as _core_can_use # noqa: F401
# Feature key → minimum tier required. # Feature key → minimum tier required.
FEATURES: dict[str, str] = { FEATURES: dict[str, str] = {

View file

@ -1,13 +1,10 @@
import hashlib
import math
from app.db.models import Listing, TrustScore
from app.db.models import Seller as Seller
from app.db.store import Store
from .aggregator import Aggregator
from .metadata import MetadataScorer from .metadata import MetadataScorer
from .photo import PhotoScorer from .photo import PhotoScorer
from .aggregator import Aggregator
from app.db.models import Seller, Listing, TrustScore
from app.db.store import Store
import hashlib
import math
class TrustScorer: class TrustScorer:

View file

@ -1,10 +1,8 @@
"""Composite score and red flag extraction.""" """Composite score and red flag extraction."""
from __future__ import annotations from __future__ import annotations
import json import json
from datetime import datetime from datetime import datetime, timezone
from typing import Optional from typing import Optional
from app.db.models import Seller, TrustScore from app.db.models import Seller, TrustScore
HARD_FILTER_AGE_DAYS = 7 HARD_FILTER_AGE_DAYS = 7

View file

@ -1,9 +1,7 @@
"""Five metadata trust signals, each scored 020.""" """Five metadata trust signals, each scored 020."""
from __future__ import annotations from __future__ import annotations
import json import json
from typing import Optional from typing import Optional
from app.db.models import Seller from app.db.models import Seller
ELECTRONICS_CATEGORIES = {"ELECTRONICS", "COMPUTERS_TABLETS", "VIDEO_GAMES", "CELL_PHONES"} ELECTRONICS_CATEGORIES = {"ELECTRONICS", "COMPUTERS_TABLETS", "VIDEO_GAMES", "CELL_PHONES"}

View file

@ -1,9 +1,7 @@
"""Perceptual hash deduplication within a result set (free tier, v0.1).""" """Perceptual hash deduplication within a result set (free tier, v0.1)."""
from __future__ import annotations from __future__ import annotations
import io
from typing import Optional from typing import Optional
import io
import requests import requests
try: try:

View file

@ -1,24 +1,19 @@
"""Main search + results page.""" """Main search + results page."""
from __future__ import annotations from __future__ import annotations
import logging import logging
import os import os
from pathlib import Path from pathlib import Path
import streamlit as st import streamlit as st
from circuitforge_core.config import load_env from circuitforge_core.config import load_env
from app.db.store import Store from app.db.store import Store
from app.platforms import PlatformAdapter, SearchFilters from app.platforms import PlatformAdapter, SearchFilters
from app.trust import TrustScorer from app.trust import TrustScorer
from app.ui.components.easter_eggs import ( from app.ui.components.filters import build_filter_options, render_filter_sidebar, FilterState
auction_hours_remaining,
check_snipe_mode,
inject_steal_css,
render_snipe_mode_banner,
)
from app.ui.components.filters import FilterState, build_filter_options, render_filter_sidebar
from app.ui.components.listing_row import render_listing_row from app.ui.components.listing_row import render_listing_row
from app.ui.components.easter_eggs import (
inject_steal_css, check_snipe_mode, render_snipe_mode_banner,
auction_hours_remaining,
)
log = logging.getLogger(__name__) log = logging.getLogger(__name__)

View file

@ -22,6 +22,7 @@ import streamlit as st
from app.db.models import Listing, TrustScore from app.db.models import Listing, TrustScore
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# 1. Konami → Snipe Mode # 1. Konami → Snipe Mode
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------

View file

@ -1,12 +1,9 @@
"""Build dynamic filter options from a result set and render the Streamlit sidebar.""" """Build dynamic filter options from a result set and render the Streamlit sidebar."""
from __future__ import annotations from __future__ import annotations
import json import json
from dataclasses import dataclass, field from dataclasses import dataclass, field
from typing import Optional from typing import Optional
import streamlit as st import streamlit as st
from app.db.models import Listing, TrustScore from app.db.models import Listing, TrustScore

View file

@ -1,17 +1,13 @@
"""Render a single listing row with trust score, badges, and error states.""" """Render a single listing row with trust score, badges, and error states."""
from __future__ import annotations from __future__ import annotations
import json import json
from typing import Optional from typing import Optional
import streamlit as st import streamlit as st
from app.db.models import Listing, Seller, TrustScore from app.db.models import Listing, TrustScore, Seller
from app.ui.components.easter_eggs import ( from app.ui.components.easter_eggs import (
auction_hours_remaining, is_steal, render_steal_banner, render_auction_notice, auction_hours_remaining,
is_steal,
render_auction_notice,
render_steal_banner,
) )

View file

@ -1,8 +1,6 @@
"""First-run wizard: collect eBay credentials and write .env.""" """First-run wizard: collect eBay credentials and write .env."""
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
import streamlit as st import streamlit as st
from circuitforge_core.wizard import BaseWizard from circuitforge_core.wizard import BaseWizard

View file

@ -25,23 +25,9 @@ dependencies = [
"PyJWT>=2.8", "PyJWT>=2.8",
] ]
[project.optional-dependencies]
dev = [
"pytest>=8.0",
"ruff>=0.4",
]
[tool.setuptools.packages.find] [tool.setuptools.packages.find]
where = ["."] where = ["."]
include = ["app*", "api*"] include = ["app*", "api*"]
[tool.pytest.ini_options] [tool.pytest.ini_options]
testpaths = ["tests"] testpaths = ["tests"]
[tool.ruff]
line-length = 100
target-version = "py311"
[tool.ruff.lint]
# E (pycodestyle) deferred — tighten incrementally once existing violations are resolved
select = ["F", "I"]

View file

@ -1,9 +1,8 @@
from datetime import datetime, timedelta, timezone
import pytest import pytest
from datetime import datetime, timedelta, timezone
from app.db.models import Listing, MarketComp, Seller from pathlib import Path
from app.db.store import Store from app.db.store import Store
from app.db.models import Listing, Seller, TrustScore, MarketComp
@pytest.fixture @pytest.fixture

View file

@ -1,9 +1,7 @@
import time import time
from unittest.mock import MagicMock, patch
import pytest
import requests import requests
from unittest.mock import patch, MagicMock
import pytest
from app.platforms.ebay.auth import EbayTokenManager from app.platforms.ebay.auth import EbayTokenManager

View file

@ -1,5 +1,4 @@
import pytest import pytest
from app.platforms.ebay.normaliser import normalise_listing, normalise_seller from app.platforms.ebay.normaliser import normalise_listing, normalise_seller

View file

@ -3,18 +3,16 @@
Uses a minimal HTML fixture mirroring eBay's current s-card markup. Uses a minimal HTML fixture mirroring eBay's current s-card markup.
No HTTP requests are made all tests operate on the pure parsing functions. No HTTP requests are made all tests operate on the pure parsing functions.
""" """
from datetime import timedelta
import pytest import pytest
from bs4 import BeautifulSoup from datetime import timedelta
from app.platforms.ebay.scraper import ( from app.platforms.ebay.scraper import (
_extract_seller_from_card,
_parse_price,
_parse_time_left,
scrape_listings, scrape_listings,
scrape_sellers, scrape_sellers,
_parse_price,
_parse_time_left,
_extract_seller_from_card,
) )
from bs4 import BeautifulSoup
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
# Minimal eBay search results HTML fixture (li.s-card schema) # Minimal eBay search results HTML fixture (li.s-card schema)

View file

@ -4,10 +4,12 @@ from __future__ import annotations
from collections.abc import Callable from collections.abc import Callable
from unittest.mock import MagicMock, patch from unittest.mock import MagicMock, patch
from circuitforge_core.api.feedback import make_feedback_router
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.testclient import TestClient from fastapi.testclient import TestClient
from circuitforge_core.api.feedback import make_feedback_router
# ── Test app factory ────────────────────────────────────────────────────────── # ── Test app factory ──────────────────────────────────────────────────────────
def _make_client(demo_mode_fn: Callable[[], bool] | None = None) -> TestClient: def _make_client(demo_mode_fn: Callable[[], bool] | None = None) -> TestClient:

View file

@ -4,7 +4,7 @@ from __future__ import annotations
import json import json
import sqlite3 import sqlite3
from pathlib import Path from pathlib import Path
from unittest.mock import patch from unittest.mock import MagicMock, patch
import pytest import pytest

View file

@ -1,4 +1,4 @@
from app.tiers import can_use from app.tiers import can_use, FEATURES, LOCAL_VISION_UNLOCKABLE
def test_metadata_scoring_is_free(): def test_metadata_scoring_is_free():

View file

@ -4,8 +4,10 @@ from __future__ import annotations
import json import json
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
import pytest
from app.db.models import Listing, TrustScore from app.db.models import Listing, TrustScore
from app.ui.components.easter_eggs import auction_hours_remaining, is_steal from app.ui.components.easter_eggs import is_steal, auction_hours_remaining
def _listing(**kwargs) -> Listing: def _listing(**kwargs) -> Listing: