Skip to content

Commit 0d9381a

Browse files
committed
chore: initial goupix dex import
Made-with: Cursor
1 parent 15212ae commit 0d9381a

152 files changed

Lines changed: 38822 additions & 0 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.gitignore

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
.vscode
2+
.idea
3+
api/.venv
4+
api/__pycache__
5+
api/*.pyc
6+
api/.env
7+
web/.env

api/.env.example

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# Database
2+
# Local dev (Python on host): point to localhost or Docker-published port.
3+
DATABASE_URL=mysql+pymysql://goupix:goupix@127.0.0.1:3306/goupixdex
4+
# Docker Compose: the compose file sets DATABASE_URL to host "mariadb" for the api service
5+
# (overrides this line via process environment). Keep 127.0.0.1 for running uvicorn on the host.
6+
7+
# JWT
8+
JWT_SECRET=change-me-to-a-long-random-string
9+
10+
# Optional: seed default user (python seeders/user_seeder.py)
11+
SEED_USER_EMAIL=you@example.com
12+
SEED_USER_PASSWORD=your-secure-password
13+
14+
# Dev: ensure margin row for users missing settings (default 20%)
15+
# SEED_MARGIN_PERCENT=20
16+
17+
# Dev: fake articles — uses SEED_USER_EMAIL if SEED_ARTICLE_USER_EMAIL is empty
18+
# SEED_ARTICLE_USER_EMAIL=you@example.com
19+
20+
# Run all seeders in order: python seeders/run_all.py
21+
22+
# APIs
23+
POKE_WALLET_API_KEY=
24+
GROQ_API_KEY=
25+
26+
# Vinted: copied to users.vinted_email / users.vinted_password (Fernet-encrypted with JWT_SECRET) when you run user_seeder
27+
# (both keys required to sync). Used by cli_vinted_listings.py and publish fallback if DB fields are empty.
28+
VINTED_EMAIL_OR_USERNAME=
29+
VINTED_PASSWORD=
30+
31+
# When true, article creation skips real browser automation (recommended in dev)
32+
VINTED_PUBLISH_STUB=true
33+
34+
# CORS (comma-separated or *)
35+
CORS_ORIGINS=*

api/.gitignore

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
.idea
2+
.env
3+
node_modules
4+
ebayBuilder.ts
5+
.venv
6+
__pycache__/
7+
*.pyc
8+
uploads/

api/Dockerfile

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
FROM python:3.12-slim-bookworm
2+
3+
WORKDIR /app
4+
5+
COPY requirements.txt .
6+
RUN pip install --no-cache-dir -r requirements.txt
7+
8+
COPY . .
9+
10+
EXPOSE 8000
11+
12+
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"]

api/README.md

Lines changed: 143 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,143 @@
1+
# GoupixDex — API (Python)
2+
3+
Backend for **GoupixDex**: Pokémon TCG scanning, pricing (PokéWallet), inventory articles, JWT auth, and optional Vinted automation (nodriver).
4+
5+
## Stack
6+
7+
- **FastAPI** + **Uvicorn** — REST API, OpenAPI/Swagger at `/docs`
8+
- **SQLAlchemy 2** ORM — models under `models/` (not Pydantic)
9+
- **MariaDB / MySQL** — via **PyMySQL** (`mysql+pymysql://…`)
10+
- **JWT** (python-jose) + **bcrypt**
11+
- **Pydantic** — request/response validation only (`schemas/`)
12+
- **Custom SQL migrations** in `migrations/` (no Alembic)
13+
- **Docker Compose** — API (hot reload), MariaDB, phpMyAdmin
14+
15+
## Layout
16+
17+
```text
18+
api/
19+
main.py # FastAPI app
20+
config.py # AppSettings + legacy Vinted CLI defaults
21+
cli_vinted_listings.py # Standalone nodriver CLI (was main.py)
22+
app_types/ # TypedDict (PokéWallet, Groq, Vinted payloads)
23+
core/
24+
database.py # Engine, SessionLocal, get_db
25+
security.py # JWT + password hashing
26+
deps.py # Current user (Bearer)
27+
models/ # SQLAlchemy ORM (User, Article, Image, MarginSettings)
28+
schemas/ # Pydantic API bodies only
29+
routes/ # auth, users, articles, settings, scan
30+
services/
31+
ocr_service.py # Groq vision wrapper
32+
pricing_service.py # PokéWallet + EUR/USD average
33+
scan_service.py # Title/description templates
34+
article_service.py
35+
auth_service.py
36+
vinted_publish_service.py # Calls existing VintedService
37+
… (pokewallet, groq_vision_client, vinted_service, …)
38+
migrations/
39+
seeders/
40+
uploads/ # Local image storage (created at runtime)
41+
```
42+
43+
## Requirements
44+
45+
- Python **3.10+**
46+
- MariaDB or MySQL (local or Docker)
47+
- Optional: `POKE_WALLET_API_KEY`, `GROQ_API_KEY`, Chrome for real Vinted runs
48+
49+
## Install
50+
51+
```bash
52+
cd api
53+
python -m venv .venv
54+
.venv\Scripts\activate
55+
pip install -r requirements.txt
56+
```
57+
58+
Copy `.env.example` to `.env` and set at least `DATABASE_URL` and `JWT_SECRET`.
59+
60+
### Database
61+
62+
```bash
63+
python migrations/run_migrations.py
64+
```
65+
66+
### Seeders (optional, dev)
67+
68+
1. **User** — set `SEED_USER_EMAIL` and `SEED_USER_PASSWORD` in `.env`, then:
69+
70+
```bash
71+
python seeders/user_seeder.py
72+
```
73+
74+
2. **Margin (20% default)** — creates a `settings` row for any user that does not have one yet:
75+
76+
```bash
77+
python seeders/margin_seeder.py
78+
```
79+
80+
Override default percent with `SEED_MARGIN_PERCENT` (e.g. `25`).
81+
82+
3. **Fake articles** — requires an existing user. Uses `SEED_ARTICLE_USER_EMAIL` or falls back to `SEED_USER_EMAIL`. Deletes previous `[DEV] …` titles for that user, then inserts four sample articles (one marked sold):
83+
84+
```bash
85+
python seeders/article_seeder.py
86+
```
87+
88+
4. **Everything in order** (same env vars as above):
89+
90+
```bash
91+
python seeders/run_all.py
92+
```
93+
94+
## Run API
95+
96+
```bash
97+
uvicorn main:app --reload --host 0.0.0.0 --port 8000
98+
```
99+
100+
- Swagger UI: http://127.0.0.1:8000/docs
101+
- Health: `GET /health`
102+
- Uploaded files are served under `/uploads/...`
103+
104+
### Main endpoints
105+
106+
| Method | Path | Notes |
107+
|--------|------|--------|
108+
| POST | `/auth/login` | JWT Bearer |
109+
| POST | `/users` | Register |
110+
| GET/PUT/DELETE | `/users`, `/users/{id}`, `/users/me` | Auth required for most |
111+
| POST | `/scan-card` | Multipart image + optional `margin_percent`; OCR + pricing preview (no DB) |
112+
| CRUD | `/articles` | Create uses multipart (fields + `images` files); then Vinted attempt |
113+
| PATCH | `/articles/{id}/sold` | Mark sold + `sell_price` |
114+
| GET/PUT | `/settings` | Margin % (default 20) |
115+
116+
**Vinted:** set `VINTED_PUBLISH_STUB=true` in dev to skip browser automation. On `POST /articles`, publication uses the authenticated user’s `vinted_email` and **encrypted** `vinted_password` (Fernet, key derived from `JWT_SECRET`). Legacy rows still holding a bcrypt hash cannot be decrypted—re-save the Vinted password via user settings or the seeder, or set `VINTED_EMAIL_OR_USERNAME` / `VINTED_PASSWORD` in the environment as fallback.
117+
118+
### Legacy Vinted CLI (batch `items.json`)
119+
120+
```bash
121+
python cli_vinted_listings.py
122+
```
123+
124+
Uses `config.py` listing defaults and `items.json` / `images/` at the project root.
125+
126+
## Docker
127+
128+
From `api/`:
129+
130+
```bash
131+
docker compose up --build
132+
```
133+
134+
- API: port **8000** (migrations run on startup)
135+
- MariaDB: **3306**
136+
- phpMyAdmin: **8080**
137+
138+
Create a user after startup: `docker compose exec api python seeders/user_seeder.py` (with env vars set), or use `POST /users` and `POST /auth/login`.
139+
140+
## Limitations
141+
142+
- Vinted UI and anti-bot policies may break automation; use stub mode when developing.
143+
- Average price mixes Cardmarket (EUR) and TCGPlayer (USD) using `USD_TO_EUR` in `config` (`AppSettings.usd_to_eur`).

api/app_types/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
"""TypedDict and legacy typing helpers (not SQLAlchemy ORM)."""

api/app_types/groq_vision.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
"""Types for Groq vision OCR-style card extraction."""
2+
3+
from typing import Literal, NotRequired, TypedDict
4+
5+
GroqVisionImageMimeType = Literal["image/jpeg", "image/png", "image/webp"]
6+
7+
8+
class GroqVisionExtractOptions(TypedDict, total=False):
9+
"""Optional tuning for Groq vision extraction."""
10+
11+
temperature: float
12+
model: str
13+
include_raw_assistant_json: bool
14+
resolve_english_name_from_poke_wallet: bool
15+
16+
17+
class GroqVisionCardCollectorResult(TypedDict, total=False):
18+
"""
19+
Pokémon TCG collector info inferred from a card photo (set code, number, name).
20+
``None`` fields mean unreadable or absent per model (not guessed), except fraction parts
21+
derived from ``card_number`` when possible.
22+
"""
23+
24+
set_code: str | None
25+
card_number: str | None
26+
card_number_numerator: str | None
27+
card_number_denominator: str | None
28+
pokemon_name: str | None
29+
pokemon_name_english: str | None
30+
pokemon_name_french: str | None
31+
card_variant_label: str | None
32+
set_name_english: str | None
33+
rarity_english: str | None
34+
raw_assistant_json: NotRequired[str]

api/app_types/payload.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
"""Item listing payload shape (matches ``items.json`` entries)."""
2+
3+
from typing import NotRequired, TypedDict
4+
5+
from app_types.vinted import VintedConditions
6+
7+
8+
class ItemPayload(TypedDict):
9+
"""
10+
Describes one Vinted listing loaded from ``items.json``.
11+
12+
Attributes:
13+
title: Listing title shown on Vinted.
14+
description: Optional long description (see VintedService for how it is applied).
15+
price: Price as a number (currency follows your Vinted account).
16+
condition: One of the French Vinted condition labels.
17+
images: Filenames under the project ``images/`` folder.
18+
"""
19+
20+
title: str
21+
description: NotRequired[str]
22+
price: float | int
23+
condition: VintedConditions
24+
images: list[str]

api/app_types/pokewallet.py

Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
"""PokéWallet API response shapes (https://api.pokewallet.io)."""
2+
3+
from typing import NotRequired, TypedDict
4+
5+
6+
class PokeWalletSearchMetadata(TypedDict):
7+
"""Metadata returned alongside search results, summarising source coverage."""
8+
9+
total_count: int
10+
tcg: int
11+
cardmarket: int
12+
tcg_only: int
13+
cardmarket_only: int
14+
both_sources: int
15+
16+
17+
class PokeWalletPagination(TypedDict):
18+
"""Pagination envelope for the search endpoint."""
19+
20+
page: int
21+
limit: int
22+
total: int
23+
total_pages: int
24+
25+
26+
class PokeWalletSearchOptions(TypedDict, total=False):
27+
"""Optional query flags for ``GET /search`` (page default 1, limit default 20, max 100)."""
28+
29+
page: int
30+
limit: int
31+
pokemonName: str
32+
33+
34+
class PokeWalletGetCardOptions(TypedDict, total=False):
35+
"""Optional query flags for ``GET /cards/:id`` (disambiguation)."""
36+
37+
setCode: str
38+
39+
40+
class PokeWalletCardInfo(TypedDict, total=False):
41+
"""Core card attributes as returned by the API (fields may be omitted on partial matches)."""
42+
43+
name: str
44+
clean_name: str
45+
set_name: str
46+
set_code: str
47+
set_id: str
48+
card_number: str
49+
rarity: str
50+
card_type: str
51+
hp: str
52+
stage: str
53+
card_text: str | None
54+
attacks: list[str]
55+
weakness: str
56+
resistance: str | None
57+
retreat_cost: str
58+
59+
60+
class PokeWalletTcgPlayerPriceRow(TypedDict):
61+
"""A single TCGPlayer price row for a product sub-type (e.g. Holofoil)."""
62+
63+
low_price: float
64+
mid_price: float
65+
high_price: float
66+
updated_at: str
67+
market_price: float
68+
direct_low_price: float | None
69+
sub_type_name: str
70+
71+
72+
class PokeWalletTcgPlayerBlock(TypedDict):
73+
"""TCGPlayer listing block; may be absent when the card is not listed there."""
74+
75+
prices: list[PokeWalletTcgPlayerPriceRow]
76+
url: str
77+
78+
79+
class PokeWalletCardmarketPriceRow(TypedDict, total=False):
80+
"""
81+
Cardmarket price snapshot for a variant (normal / holo, etc.).
82+
Some rows omit fields (API returns partial objects for certain products).
83+
"""
84+
85+
avg: float | None
86+
low: float | None
87+
avg1: float | None
88+
avg7: float | None
89+
avg30: float | None
90+
trend: float | None
91+
updated_at: str
92+
variant_type: str
93+
94+
95+
class PokeWalletCardmarketBlock(TypedDict):
96+
"""Cardmarket listing block; absent when the card is TCG-only."""
97+
98+
product_name: str
99+
prices: list[PokeWalletCardmarketPriceRow]
100+
product_url: str
101+
102+
103+
class PokeWalletCard(TypedDict):
104+
"""
105+
One card hit from search, or the full payload from GET /cards/:id.
106+
``id`` may be ``pk_…`` (TCG) or a hex hash (CardMarket-only).
107+
"""
108+
109+
id: str
110+
card_info: PokeWalletCardInfo
111+
tcgplayer: PokeWalletTcgPlayerBlock | None
112+
cardmarket: PokeWalletCardmarketBlock | None
113+
114+
115+
class PokeWalletSearchResponse(TypedDict):
116+
"""Response body from GET /search."""
117+
118+
query: str
119+
results: list[PokeWalletCard]
120+
pagination: PokeWalletPagination
121+
metadata: PokeWalletSearchMetadata

0 commit comments

Comments
 (0)