Compare commits
No commits in common. "feat/ci-workflows" and "main" have entirely different histories.
feat/ci-wo
...
main
35 changed files with 62 additions and 319 deletions
41
.cliff.toml
41
.cliff.toml
|
|
@ -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"
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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
|
|
||||||
|
|
@ -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}}"
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
31
api/main.py
31
api/main.py
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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 = {
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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(
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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] = {
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,7 @@
|
||||||
"""Five metadata trust signals, each scored 0–20."""
|
"""Five metadata trust signals, each scored 0–20."""
|
||||||
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"}
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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__)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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"]
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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():
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue