feat: wire Forgejo Actions CI/CD workflows (#22)
- ci.yml: API lint (ruff F+I) + pytest, web vue-tsc + vitest + build - mirror.yml: push to GitHub (CircuitForgeLLC) + Codeberg (CircuitForge) on main/tags - release.yml: Docker build → Forgejo registry + release via API; GHCR deferred pending BSL policy (cf-agents#3) - .cliff.toml: git-cliff changelog config for semver releases - pyproject.toml: add [dev] extras (pytest, ruff), ruff config - Fix 45 ruff violations across codebase (import sorting, unused vars, unused imports)
This commit is contained in:
parent
303b4bfb6f
commit
eb05be0612
35 changed files with 318 additions and 61 deletions
41
.cliff.toml
Normal file
41
.cliff.toml
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
[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"
|
||||
62
.forgejo/workflows/ci.yml
Normal file
62
.forgejo/workflows/ci.yml
Normal file
|
|
@ -0,0 +1,62 @@
|
|||
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
|
||||
37
.forgejo/workflows/mirror.yml
Normal file
37
.forgejo/workflows/mirror.yml
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
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:
|
||||
GITHUB_MIRROR_TOKEN: ${{ secrets.GITHUB_MIRROR_TOKEN }}
|
||||
run: |
|
||||
git remote add github https://x-access-token:${GITHUB_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
|
||||
67
.forgejo/workflows/release.yml
Normal file
67
.forgejo/workflows/release.yml
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
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,8 +16,6 @@ FastAPI usage:
|
|||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import hmac
|
||||
import logging
|
||||
import os
|
||||
import re
|
||||
|
|
@ -77,7 +75,6 @@ def compute_features(tier: str) -> SessionFeatures:
|
|||
"""Compute feature flags from tier. Evaluated server-side; sent to frontend."""
|
||||
local = tier == "local"
|
||||
paid_plus = local or tier in ("paid", "premium", "ultra")
|
||||
premium_plus = local or tier in ("premium", "ultra")
|
||||
|
||||
return SessionFeatures(
|
||||
saved_searches=True, # all tiers get saved searches
|
||||
|
|
|
|||
|
|
@ -26,11 +26,11 @@ from pathlib import Path
|
|||
from typing import Optional
|
||||
|
||||
import requests
|
||||
from fastapi import APIRouter, Header, HTTPException, Request
|
||||
from cryptography.exceptions import InvalidSignature
|
||||
from cryptography.hazmat.primitives.asymmetric.ec import ECDSA
|
||||
from cryptography.hazmat.primitives.hashes import SHA1
|
||||
from cryptography.hazmat.primitives.serialization import load_pem_public_key
|
||||
from fastapi import APIRouter, Header, HTTPException, Request
|
||||
|
||||
from app.db.store import Store
|
||||
|
||||
|
|
|
|||
33
api/main.py
33
api/main.py
|
|
@ -1,8 +1,11 @@
|
|||
"""Snipe FastAPI — search endpoint wired to ScrapedEbayAdapter + TrustScorer."""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import dataclasses
|
||||
import hashlib
|
||||
import io
|
||||
import json as _json
|
||||
import logging
|
||||
import os
|
||||
|
|
@ -12,28 +15,25 @@ from concurrent.futures import ThreadPoolExecutor
|
|||
from contextlib import asynccontextmanager
|
||||
from pathlib import Path
|
||||
|
||||
import asyncio
|
||||
import csv
|
||||
import io
|
||||
|
||||
from fastapi import Depends, FastAPI, HTTPException, Request, UploadFile, File
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
|
||||
from circuitforge_core.config import load_env
|
||||
from circuitforge_core.affiliates import wrap_url as _wrap_affiliate_url
|
||||
from circuitforge_core.api import make_feedback_router as _make_feedback_router
|
||||
from circuitforge_core.config import load_env
|
||||
from fastapi import Depends, FastAPI, File, HTTPException, Request, UploadFile
|
||||
from fastapi.middleware.cors import CORSMiddleware
|
||||
from fastapi.responses import StreamingResponse
|
||||
from pydantic import BaseModel
|
||||
|
||||
from api.cloud_session import CloudUser, compute_features, get_session
|
||||
from api.ebay_webhook import router as ebay_webhook_router
|
||||
from app.db.models import SavedSearch as SavedSearchModel
|
||||
from app.db.models import ScammerEntry
|
||||
from app.db.store import Store
|
||||
from app.db.models import SavedSearch as SavedSearchModel, ScammerEntry
|
||||
from app.platforms import SearchFilters
|
||||
from app.platforms.ebay.scraper import ScrapedEbayAdapter
|
||||
from app.platforms.ebay.adapter import EbayAdapter
|
||||
from app.platforms.ebay.auth import EbayTokenManager
|
||||
from app.platforms.ebay.query_builder import expand_queries, parse_groups
|
||||
from app.platforms.ebay.scraper import ScrapedEbayAdapter
|
||||
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"))
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -50,8 +50,8 @@ async def _lifespan(app: FastAPI):
|
|||
# Start vision/LLM background task scheduler.
|
||||
# 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.
|
||||
from api.cloud_session import _LOCAL_SNIPE_DB, CLOUD_MODE, _shared_db_path
|
||||
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
|
||||
get_scheduler(sched_db)
|
||||
log.info("Snipe task scheduler started (db=%s)", sched_db)
|
||||
|
|
@ -245,9 +245,10 @@ def _enqueue_vision_tasks(
|
|||
trust_scores table in cloud mode.
|
||||
"""
|
||||
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.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 = get_scheduler(sched_db)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Dataclasses for all Snipe domain objects."""
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
"""Thin SQLite read/write layer for all Snipe models."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from pathlib import Path
|
||||
|
|
@ -7,7 +8,7 @@ from typing import Optional
|
|||
|
||||
from circuitforge_core.db import get_connection, run_migrations
|
||||
|
||||
from .models import Listing, Seller, TrustScore, MarketComp, SavedSearch, ScammerEntry
|
||||
from .models import Listing, MarketComp, SavedSearch, ScammerEntry, Seller, TrustScore
|
||||
|
||||
MIGRATIONS_DIR = Path(__file__).parent / "migrations"
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"""PlatformAdapter abstract base and shared types."""
|
||||
from __future__ import annotations
|
||||
|
||||
from abc import ABC, abstractmethod
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
from app.db.models import Listing, Seller
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
"""eBay Browse API adapter."""
|
||||
from __future__ import annotations
|
||||
|
||||
import hashlib
|
||||
import logging
|
||||
from dataclasses import replace
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
|
@ -18,7 +20,7 @@ _SHOPPING_API_MAX_PER_SEARCH = 5 # sellers enriched per search call
|
|||
_SHOPPING_API_INTER_REQUEST_DELAY = 0.5 # seconds between successive calls
|
||||
_SELLER_ENRICH_TTL_HOURS = 24 # skip re-enrichment within this window
|
||||
|
||||
from app.db.models import Listing, Seller, MarketComp
|
||||
from app.db.models import Listing, MarketComp, Seller
|
||||
from app.db.store import Store
|
||||
from app.platforms import PlatformAdapter, SearchFilters
|
||||
from app.platforms.ebay.auth import EbayTokenManager
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"""eBay OAuth2 client credentials token manager."""
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import time
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
EBAY_OAUTH_URLS = {
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"""Convert raw eBay API responses into Snipe domain objects."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from typing import Optional
|
||||
|
||||
from app.db.models import Listing, Seller
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ import json
|
|||
import logging
|
||||
import re
|
||||
import time
|
||||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||||
from concurrent.futures import ThreadPoolExecutor
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from typing import Optional
|
||||
|
||||
|
|
@ -302,7 +302,8 @@ class ScrapedEbayAdapter(PlatformAdapter):
|
|||
|
||||
time.sleep(self._delay)
|
||||
|
||||
import subprocess, os
|
||||
import os
|
||||
import subprocess
|
||||
display_num = next(_display_counter)
|
||||
display = f":{display_num}"
|
||||
xvfb = subprocess.Popen(
|
||||
|
|
@ -313,8 +314,10 @@ class ScrapedEbayAdapter(PlatformAdapter):
|
|||
env["DISPLAY"] = display
|
||||
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright # noqa: PLC0415 — lazy: only needed in Docker
|
||||
from playwright_stealth import Stealth # noqa: PLC0415
|
||||
from playwright.sync_api import (
|
||||
sync_playwright, # noqa: PLC0415 — lazy: only needed in Docker
|
||||
)
|
||||
from playwright_stealth import Stealth # noqa: PLC0415
|
||||
|
||||
with sync_playwright() as pw:
|
||||
browser = pw.chromium.launch(
|
||||
|
|
|
|||
|
|
@ -19,7 +19,6 @@ import logging
|
|||
from pathlib import Path
|
||||
|
||||
import requests
|
||||
|
||||
from circuitforge_core.db import get_connection
|
||||
from circuitforge_core.llm import LLMRouter
|
||||
|
||||
|
|
|
|||
|
|
@ -5,9 +5,10 @@ from __future__ import annotations
|
|||
from pathlib import Path
|
||||
|
||||
from circuitforge_core.tasks.scheduler import (
|
||||
TaskScheduler,
|
||||
TaskScheduler, # re-export for tests
|
||||
)
|
||||
from circuitforge_core.tasks.scheduler import (
|
||||
get_scheduler as _base_get_scheduler,
|
||||
reset_scheduler, # re-export for tests
|
||||
)
|
||||
|
||||
from app.tasks.runner import LLM_TASK_TYPES, VRAM_BUDGETS, run_task
|
||||
|
|
|
|||
|
|
@ -14,7 +14,8 @@ Intentionally ungated (free for all):
|
|||
- saved_searches — retention feature; friction cost outweighs gate value
|
||||
"""
|
||||
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.
|
||||
FEATURES: dict[str, str] = {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,14 @@
|
|||
from .metadata import MetadataScorer
|
||||
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
|
||||
|
||||
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 .photo import PhotoScorer
|
||||
|
||||
|
||||
class TrustScorer:
|
||||
"""Orchestrates metadata + photo scoring for a batch of listings."""
|
||||
|
|
|
|||
|
|
@ -1,8 +1,10 @@
|
|||
"""Composite score and red flag extraction."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from datetime import datetime, timezone
|
||||
from datetime import datetime
|
||||
from typing import Optional
|
||||
|
||||
from app.db.models import Seller, TrustScore
|
||||
|
||||
HARD_FILTER_AGE_DAYS = 7
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"""Five metadata trust signals, each scored 0–20."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
from app.db.models import Seller
|
||||
|
||||
ELECTRONICS_CATEGORIES = {"ELECTRONICS", "COMPUTERS_TABLETS", "VIDEO_GAMES", "CELL_PHONES"}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
"""Perceptual hash deduplication within a result set (free tier, v0.1)."""
|
||||
from __future__ import annotations
|
||||
from typing import Optional
|
||||
|
||||
import io
|
||||
from typing import Optional
|
||||
|
||||
import requests
|
||||
|
||||
try:
|
||||
|
|
|
|||
|
|
@ -1,19 +1,24 @@
|
|||
"""Main search + results page."""
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import os
|
||||
from pathlib import Path
|
||||
|
||||
import streamlit as st
|
||||
from circuitforge_core.config import load_env
|
||||
|
||||
from app.db.store import Store
|
||||
from app.platforms import PlatformAdapter, SearchFilters
|
||||
from app.trust import TrustScorer
|
||||
from app.ui.components.filters import build_filter_options, render_filter_sidebar, FilterState
|
||||
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,
|
||||
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
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
|
|
|||
|
|
@ -22,7 +22,6 @@ import streamlit as st
|
|||
|
||||
from app.db.models import Listing, TrustScore
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# 1. Konami → Snipe Mode
|
||||
# ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -1,9 +1,12 @@
|
|||
"""Build dynamic filter options from a result set and render the Streamlit sidebar."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from dataclasses import dataclass, field
|
||||
from typing import Optional
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from app.db.models import Listing, TrustScore
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,13 +1,17 @@
|
|||
"""Render a single listing row with trust score, badges, and error states."""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from typing import Optional
|
||||
|
||||
import streamlit as st
|
||||
|
||||
from app.db.models import Listing, TrustScore, Seller
|
||||
from app.db.models import Listing, Seller, TrustScore
|
||||
from app.ui.components.easter_eggs import (
|
||||
is_steal, render_steal_banner, render_auction_notice, auction_hours_remaining,
|
||||
auction_hours_remaining,
|
||||
is_steal,
|
||||
render_auction_notice,
|
||||
render_steal_banner,
|
||||
)
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
"""First-run wizard: collect eBay credentials and write .env."""
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import streamlit as st
|
||||
from circuitforge_core.wizard import BaseWizard
|
||||
|
||||
|
|
|
|||
|
|
@ -25,9 +25,23 @@ dependencies = [
|
|||
"PyJWT>=2.8",
|
||||
]
|
||||
|
||||
[project.optional-dependencies]
|
||||
dev = [
|
||||
"pytest>=8.0",
|
||||
"ruff>=0.4",
|
||||
]
|
||||
|
||||
[tool.setuptools.packages.find]
|
||||
where = ["."]
|
||||
include = ["app*", "api*"]
|
||||
|
||||
[tool.pytest.ini_options]
|
||||
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,8 +1,9 @@
|
|||
import pytest
|
||||
from datetime import datetime, timedelta, timezone
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from app.db.models import Listing, MarketComp, Seller
|
||||
from app.db.store import Store
|
||||
from app.db.models import Listing, Seller, TrustScore, MarketComp
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
|
|||
|
|
@ -1,7 +1,9 @@
|
|||
import time
|
||||
import requests
|
||||
from unittest.mock import patch, MagicMock
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
from app.platforms.ebay.auth import EbayTokenManager
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
import pytest
|
||||
|
||||
from app.platforms.ebay.normaliser import normalise_listing, normalise_seller
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -3,16 +3,18 @@
|
|||
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.
|
||||
"""
|
||||
import pytest
|
||||
from datetime import timedelta
|
||||
|
||||
import pytest
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
from app.platforms.ebay.scraper import (
|
||||
scrape_listings,
|
||||
scrape_sellers,
|
||||
_extract_seller_from_card,
|
||||
_parse_price,
|
||||
_parse_time_left,
|
||||
_extract_seller_from_card,
|
||||
scrape_listings,
|
||||
scrape_sellers,
|
||||
)
|
||||
from bs4 import BeautifulSoup
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Minimal eBay search results HTML fixture (li.s-card schema)
|
||||
|
|
|
|||
|
|
@ -4,12 +4,10 @@ from __future__ import annotations
|
|||
from collections.abc import Callable
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
from circuitforge_core.api.feedback import make_feedback_router
|
||||
from fastapi import FastAPI
|
||||
from fastapi.testclient import TestClient
|
||||
|
||||
from circuitforge_core.api.feedback import make_feedback_router
|
||||
|
||||
|
||||
# ── Test app factory ──────────────────────────────────────────────────────────
|
||||
|
||||
def _make_client(demo_mode_fn: Callable[[], bool] | None = None) -> TestClient:
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@ from __future__ import annotations
|
|||
import json
|
||||
import sqlite3
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
from app.tiers import can_use, FEATURES, LOCAL_VISION_UNLOCKABLE
|
||||
from app.tiers import can_use
|
||||
|
||||
|
||||
def test_metadata_scoring_is_free():
|
||||
|
|
|
|||
|
|
@ -4,10 +4,8 @@ from __future__ import annotations
|
|||
import json
|
||||
from datetime import datetime, timedelta, timezone
|
||||
|
||||
import pytest
|
||||
|
||||
from app.db.models import Listing, TrustScore
|
||||
from app.ui.components.easter_eggs import is_steal, auction_hours_remaining
|
||||
from app.ui.components.easter_eggs import auction_hours_remaining, is_steal
|
||||
|
||||
|
||||
def _listing(**kwargs) -> Listing:
|
||||
|
|
|
|||
Loading…
Reference in a new issue