foilstack 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- foilstack-0.2.0/.env.example +112 -0
- foilstack-0.2.0/.git-blame-ignore-revs +10 -0
- foilstack-0.2.0/.github/workflows/ci.yml +68 -0
- foilstack-0.2.0/.github/workflows/release.yml +79 -0
- foilstack-0.2.0/.gitignore +240 -0
- foilstack-0.2.0/.gitleaks.toml +59 -0
- foilstack-0.2.0/.pre-commit-config.yaml +81 -0
- foilstack-0.2.0/AGENTS.md +234 -0
- foilstack-0.2.0/Dockerfile.embedder +32 -0
- foilstack-0.2.0/Dockerfile.web +37 -0
- foilstack-0.2.0/LICENSE +661 -0
- foilstack-0.2.0/PKG-INFO +241 -0
- foilstack-0.2.0/README.md +193 -0
- foilstack-0.2.0/alembic.ini +49 -0
- foilstack-0.2.0/docker/entrypoint.sh +16 -0
- foilstack-0.2.0/docker-compose.gpu.yml +31 -0
- foilstack-0.2.0/docker-compose.yml +334 -0
- foilstack-0.2.0/docs/accounts.md +57 -0
- foilstack-0.2.0/docs/backups.md +58 -0
- foilstack-0.2.0/docs/encoder.md +110 -0
- foilstack-0.2.0/docs/plugins.md +36 -0
- foilstack-0.2.0/docs/prices.md +46 -0
- foilstack-0.2.0/pyproject.toml +150 -0
- foilstack-0.2.0/scripts/bump_version.py +124 -0
- foilstack-0.2.0/scripts/check-tests.sh +35 -0
- foilstack-0.2.0/scripts/check_categories.py +69 -0
- foilstack-0.2.0/scripts/demo.py +334 -0
- foilstack-0.2.0/scripts/preview.py +436 -0
- foilstack-0.2.0/scripts/restore.sh +53 -0
- foilstack-0.2.0/scripts/shots.py +77 -0
- foilstack-0.2.0/src/foilstack/__init__.py +1 -0
- foilstack-0.2.0/src/foilstack/cli.py +634 -0
- foilstack-0.2.0/src/foilstack/config.py +149 -0
- foilstack-0.2.0/src/foilstack/db.py +471 -0
- foilstack-0.2.0/src/foilstack/embedder/__init__.py +0 -0
- foilstack-0.2.0/src/foilstack/embedder/app.py +122 -0
- foilstack-0.2.0/src/foilstack/embedding.py +36 -0
- foilstack-0.2.0/src/foilstack/images.py +72 -0
- foilstack-0.2.0/src/foilstack/importing.py +385 -0
- foilstack-0.2.0/src/foilstack/inventory.py +439 -0
- foilstack-0.2.0/src/foilstack/migrations/README +11 -0
- foilstack-0.2.0/src/foilstack/migrations/env.py +93 -0
- foilstack-0.2.0/src/foilstack/migrations/script.py.mako +24 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2105-04a130322022_pgvector_extension.py +33 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2105-327c4a89b5f1_baseline_schema.py +195 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2106-ece6f4eba34c_card_embedding_hnsw_index.py +54 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2139-a6e3135d319e_scan_best_score.py +27 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2213-fd1c94a2fc8f_inventory_finish_sold_state_and_notes.py +65 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2229-ab360b649e52_drop_inventory_quantity.py +41 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2326-36ee08bea9f1_card_prices_price_history_and_sync_state.py +71 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2349-d46268462eb9_declared_printing_on_inventory.py +27 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_23_0918-6842dd86917f_scan_size_in_bytes.py +37 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_23_1400-b91c4e2f7a10_card_name_trigram_index.py +37 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_23_1520-c73f8a1d4b62_scan_chosen_card.py +35 -0
- foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_24_0210-d18b3f9c5e41_remember_missing_card_images.py +27 -0
- foilstack-0.2.0/src/foilstack/plugins/__init__.py +45 -0
- foilstack-0.2.0/src/foilstack/plugins/base.py +104 -0
- foilstack-0.2.0/src/foilstack/plugins/exports/ebay.toml +31 -0
- foilstack-0.2.0/src/foilstack/plugins/exports/tcgplayer.toml +44 -0
- foilstack-0.2.0/src/foilstack/plugins/exports.py +95 -0
- foilstack-0.2.0/src/foilstack/plugins/sources/__init__.py +0 -0
- foilstack-0.2.0/src/foilstack/plugins/sources/tcgcsv.py +265 -0
- foilstack-0.2.0/src/foilstack/prices.py +162 -0
- foilstack-0.2.0/src/foilstack/search.py +144 -0
- foilstack-0.2.0/src/foilstack/web/app.py +235 -0
- foilstack-0.2.0/src/foilstack/web/auth.py +209 -0
- foilstack-0.2.0/src/foilstack/web/chrome.py +178 -0
- foilstack-0.2.0/src/foilstack/web/deps.py +72 -0
- foilstack-0.2.0/src/foilstack/web/joblog.py +51 -0
- foilstack-0.2.0/src/foilstack/web/proof.py +54 -0
- foilstack-0.2.0/src/foilstack/web/ratelimit.py +103 -0
- foilstack-0.2.0/src/foilstack/web/routes/__init__.py +18 -0
- foilstack-0.2.0/src/foilstack/web/routes/accounts.py +227 -0
- foilstack-0.2.0/src/foilstack/web/routes/inventory.py +384 -0
- foilstack-0.2.0/src/foilstack/web/routes/listings.py +216 -0
- foilstack-0.2.0/src/foilstack/web/routes/media.py +181 -0
- foilstack-0.2.0/src/foilstack/web/routes/scans.py +527 -0
- foilstack-0.2.0/src/foilstack/web/static/app.css +1165 -0
- foilstack-0.2.0/src/foilstack/web/static/brand/apple-touch-icon.png +0 -0
- foilstack-0.2.0/src/foilstack/web/static/brand/favicon-32.png +0 -0
- foilstack-0.2.0/src/foilstack/web/static/brand/favicon.svg +15 -0
- foilstack-0.2.0/src/foilstack/web/static/brand/mark.svg +16 -0
- foilstack-0.2.0/src/foilstack/web/static/demo/foilstack.gif +0 -0
- foilstack-0.2.0/src/foilstack/web/static/demo/foilstack.webp +0 -0
- foilstack-0.2.0/src/foilstack/web/static/fonts/OFL.txt +93 -0
- foilstack-0.2.0/src/foilstack/web/static/fonts/README.md +15 -0
- foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
- foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
- foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-greek.woff2 +0 -0
- foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
- foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-latin.woff2 +0 -0
- foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-vietnamese.woff2 +0 -0
- foilstack-0.2.0/src/foilstack/web/static/zoom.js +93 -0
- foilstack-0.2.0/src/foilstack/web/templates/_card_panel.html +141 -0
- foilstack-0.2.0/src/foilstack/web/templates/_match_options.html +17 -0
- foilstack-0.2.0/src/foilstack/web/templates/_match_panel.html +28 -0
- foilstack-0.2.0/src/foilstack/web/templates/analytics.html +126 -0
- foilstack-0.2.0/src/foilstack/web/templates/base.html +91 -0
- foilstack-0.2.0/src/foilstack/web/templates/card.html +295 -0
- foilstack-0.2.0/src/foilstack/web/templates/import.html +484 -0
- foilstack-0.2.0/src/foilstack/web/templates/inventory.html +177 -0
- foilstack-0.2.0/src/foilstack/web/templates/landing.html +208 -0
- foilstack-0.2.0/src/foilstack/web/templates/listings.html +124 -0
- foilstack-0.2.0/src/foilstack/web/templates/login.html +78 -0
- foilstack-0.2.0/src/foilstack/web/templates/plugins.html +90 -0
- foilstack-0.2.0/tests/conftest.py +48 -0
- foilstack-0.2.0/tests/test_auth.py +57 -0
- foilstack-0.2.0/tests/test_bump_version.py +145 -0
- foilstack-0.2.0/tests/test_cli.py +145 -0
- foilstack-0.2.0/tests/test_exports.py +57 -0
- foilstack-0.2.0/tests/test_hardening.py +179 -0
- foilstack-0.2.0/tests/test_images.py +82 -0
- foilstack-0.2.0/tests/test_importing.py +207 -0
- foilstack-0.2.0/tests/test_inventory.py +243 -0
- foilstack-0.2.0/tests/test_isolation.py +1355 -0
- foilstack-0.2.0/tests/test_plugins.py +21 -0
- foilstack-0.2.0/tests/test_search.py +100 -0
- foilstack-0.2.0/uv.lock +2587 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
# Copy to .env and adjust. Never commit .env.
|
|
2
|
+
# Every value here is a placeholder: this file must never hold a real secret.
|
|
3
|
+
|
|
4
|
+
# Where extracted scans and cached reference images live. The database is
|
|
5
|
+
# separate — it lives in the `postgres-data` volume, and the `backup` service
|
|
6
|
+
# is what protects it.
|
|
7
|
+
FOILSTACK_DATA_DIR=./data
|
|
8
|
+
|
|
9
|
+
# --- database -------------------------------------------------------------
|
|
10
|
+
# Used by the postgres container and, composed into DATABASE_URL, by the app.
|
|
11
|
+
POSTGRES_USER=foilstack
|
|
12
|
+
POSTGRES_PASSWORD=change-me
|
|
13
|
+
POSTGRES_DB=foilstack
|
|
14
|
+
# Host port for psql and pg_dump. Bound to loopback only.
|
|
15
|
+
POSTGRES_PORT=5434
|
|
16
|
+
|
|
17
|
+
# --- accounts -------------------------------------------------------------
|
|
18
|
+
# Off means no login screen and no passwords: one implicit owner holds
|
|
19
|
+
# everything, which is what you want running this for yourself. Turn it on for
|
|
20
|
+
# a deployment other people can reach.
|
|
21
|
+
FOILSTACK_MULTI_USER=false
|
|
22
|
+
# Signs session cookies. Anyone holding this can mint a session for any
|
|
23
|
+
# account, so generate your own before turning multi-user on:
|
|
24
|
+
# python -c 'import secrets; print(secrets.token_urlsafe(48))'
|
|
25
|
+
# The app refuses to start multi-user while this is the development default.
|
|
26
|
+
FOILSTACK_SECRET_KEY=dev-insecure-change-me
|
|
27
|
+
|
|
28
|
+
FOILSTACK_SUPPORT_URL=https://buymeacoffee.com/foilstack
|
|
29
|
+
|
|
30
|
+
# --- matching -------------------------------------------------------------
|
|
31
|
+
# The image encoder. Must be the same model that built your catalogue vectors —
|
|
32
|
+
# vectors from two different encoders are not comparable, and the failure is
|
|
33
|
+
# silent nonsense rather than an error.
|
|
34
|
+
EMBEDDER_URL=http://embedder:8100
|
|
35
|
+
EMBED_MODEL=facebook/dinov3-vitl16-pretrain-lvd1689m
|
|
36
|
+
|
|
37
|
+
# Where Hugging Face weights are cached on the host.
|
|
38
|
+
HF_CACHE_DIR=~/.cache/huggingface
|
|
39
|
+
|
|
40
|
+
# Matches at or above this cosine similarity are auto-accepted without review.
|
|
41
|
+
# Lower it and you will list cards you never looked at.
|
|
42
|
+
FOILSTACK_AUTO_ACCEPT=0.94
|
|
43
|
+
|
|
44
|
+
# How far ahead of the runner-up the top match must be. A high score alone is
|
|
45
|
+
# not evidence: reprints share artwork, so the second-place card is usually the
|
|
46
|
+
# same card in a different printing at a very different price.
|
|
47
|
+
FOILSTACK_AUTO_ACCEPT_MARGIN=0.04
|
|
48
|
+
|
|
49
|
+
# Largest archive the importer will accept, in megabytes.
|
|
50
|
+
FOILSTACK_MAX_ARCHIVE_MB=512
|
|
51
|
+
|
|
52
|
+
# --- who may sign up -------------------------------------------------------
|
|
53
|
+
# Multi-user only. Turning registration off leaves existing accounts working
|
|
54
|
+
# and stops new ones — the lever for the day a public deployment attracts the
|
|
55
|
+
# wrong attention, without taking the site down on the people already using it.
|
|
56
|
+
FOILSTACK_ALLOW_REGISTRATION=true
|
|
57
|
+
# The setting in between "anyone" and "nobody": registration stays open but
|
|
58
|
+
# asks for a code you handed out. Empty means no code is required.
|
|
59
|
+
FOILSTACK_INVITE_CODE=
|
|
60
|
+
|
|
61
|
+
# Failed sign-ins allowed per account and per address before the form starts
|
|
62
|
+
# refusing, and the window they are counted over. Generous for someone who has
|
|
63
|
+
# forgotten which password they used; ruinous for a script working a list.
|
|
64
|
+
FOILSTACK_LOGIN_ATTEMPTS=10
|
|
65
|
+
FOILSTACK_LOGIN_WINDOW_S=900
|
|
66
|
+
|
|
67
|
+
# The disk one account's scans may occupy. 0 means no ceiling, which is right
|
|
68
|
+
# for a self-hosted install where the only account is the person who owns the
|
|
69
|
+
# disk. Set a number wherever strangers can register, because otherwise the
|
|
70
|
+
# amount of disk an account may take is decided by that account.
|
|
71
|
+
FOILSTACK_MAX_ACCOUNT_MB=0
|
|
72
|
+
|
|
73
|
+
# --- price sync -----------------------------------------------------------
|
|
74
|
+
# Which catalogues the `prices` service keeps fresh. `all` means every game you
|
|
75
|
+
# have ingested, which is almost always what you want: naming games explicitly
|
|
76
|
+
# means a game you ingest later keeps its ingest-day prices for good, and
|
|
77
|
+
# nothing tells you it has stopped being updated.
|
|
78
|
+
FOILSTACK_SYNC_GAMES=all
|
|
79
|
+
# How often it *checks* upstream's build timestamp. It pulls at most once per
|
|
80
|
+
# rebuild, and TCGCSV rebuilds once a day — see https://tcgcsv.com/docs.
|
|
81
|
+
FOILSTACK_SYNC_CHECK_EVERY=21600
|
|
82
|
+
|
|
83
|
+
# --- backups --------------------------------------------------------------
|
|
84
|
+
# The `backup` service dumps into this host directory. A bind mount, not a
|
|
85
|
+
# named volume: a backup `docker compose down -v` can destroy is not a backup.
|
|
86
|
+
FOILSTACK_BACKUP_DIR=./backups
|
|
87
|
+
# Written as this uid:gid so the dumps are yours, not root's. `id -u`, `id -g`.
|
|
88
|
+
FOILSTACK_BACKUP_UID=1000
|
|
89
|
+
FOILSTACK_BACKUP_GID=1000
|
|
90
|
+
# Dump when the newest one is older than this many seconds (default daily),
|
|
91
|
+
# checking this often. How many to keep is a count, so disk cost is bounded.
|
|
92
|
+
FOILSTACK_BACKUP_INTERVAL=86400
|
|
93
|
+
FOILSTACK_BACKUP_CHECK_EVERY=3600
|
|
94
|
+
FOILSTACK_BACKUP_KEEP=14
|
|
95
|
+
# A floor that catches a truncated dump, not a size target.
|
|
96
|
+
FOILSTACK_BACKUP_MIN_BYTES=4096
|
|
97
|
+
# Timestamps in filenames are local to this zone.
|
|
98
|
+
TZ=UTC
|
|
99
|
+
|
|
100
|
+
# --- copying the backups off this machine ----------------------------------
|
|
101
|
+
# Everything above protects against failures inside the box. None of it
|
|
102
|
+
# survives the disk dying, because the dumps and the scan mirror sit on the
|
|
103
|
+
# same disk as the database. This is the part that fixes that, and it is off
|
|
104
|
+
# unless you ask for it:
|
|
105
|
+
#
|
|
106
|
+
# rclone config
|
|
107
|
+
# docker compose --profile offsite up -d
|
|
108
|
+
#
|
|
109
|
+
# Any rclone remote works; object storage is the cheap answer at this size.
|
|
110
|
+
FOILSTACK_OFFSITE_REMOTE=
|
|
111
|
+
FOILSTACK_OFFSITE_EVERY=86400
|
|
112
|
+
FOILSTACK_RCLONE_CONFIG=~/.config/rclone
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Commits that changed formatting and nothing else. Blame skips them:
|
|
2
|
+
#
|
|
3
|
+
# git config blame.ignoreRevsFile .git-blame-ignore-revs
|
|
4
|
+
#
|
|
5
|
+
# GitHub reads this file automatically. Add a revision here only when the
|
|
6
|
+
# commit is genuinely mechanical — a real change hidden in one becomes
|
|
7
|
+
# invisible to everyone looking for it later.
|
|
8
|
+
|
|
9
|
+
# Reformat with ruff
|
|
10
|
+
6c12bdd5065fe49199c9e8316b24580bf4c828a7
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
name: ci
|
|
2
|
+
on: [push, pull_request]
|
|
3
|
+
|
|
4
|
+
# Secret scanning is deliberately not here. CI runs after a push, and this
|
|
5
|
+
# repository is public — a credential this found would already be scraped, and
|
|
6
|
+
# the only remedy left would be rotation. The scan that can actually stop one
|
|
7
|
+
# is the pre-commit hook, which runs before the commit exists. See
|
|
8
|
+
# .pre-commit-config.yaml.
|
|
9
|
+
|
|
10
|
+
jobs:
|
|
11
|
+
# Unlike the secret scan, these are worth running here: a contributor may not
|
|
12
|
+
# have pre-commit installed, and a lint failure found after the push costs
|
|
13
|
+
# nothing but a second commit.
|
|
14
|
+
lint:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
steps:
|
|
17
|
+
- uses: actions/checkout@v4
|
|
18
|
+
- uses: astral-sh/setup-uv@v5
|
|
19
|
+
- run: uv sync --extra dev
|
|
20
|
+
- run: uv run ruff check src tests scripts
|
|
21
|
+
- run: uv run ruff format --check src tests scripts
|
|
22
|
+
- run: uv run mypy
|
|
23
|
+
|
|
24
|
+
test:
|
|
25
|
+
runs-on: ubuntu-latest
|
|
26
|
+
|
|
27
|
+
# Postgres is not optional here. `tests/test_isolation.py` builds its own
|
|
28
|
+
# database and drives the real application against it; without a server it
|
|
29
|
+
# *skips*, and skipping is exactly what it must not do — those are the
|
|
30
|
+
# tests that prove one account cannot read, edit or delete another's cards.
|
|
31
|
+
# Run without this, the suite reported 65 passed / 30 skipped and called it
|
|
32
|
+
# green.
|
|
33
|
+
services:
|
|
34
|
+
postgres:
|
|
35
|
+
image: pgvector/pgvector:pg17
|
|
36
|
+
env:
|
|
37
|
+
POSTGRES_USER: foilstack
|
|
38
|
+
# Named so it reads as disposable and so the repository's own
|
|
39
|
+
# gitleaks rule does not flag it — a literal password assignment is
|
|
40
|
+
# exactly what that rule is for, and it should keep firing on real
|
|
41
|
+
# ones rather than being allowlisted away for this file.
|
|
42
|
+
POSTGRES_PASSWORD: ci-placeholder
|
|
43
|
+
POSTGRES_DB: foilstack
|
|
44
|
+
ports: ["5434:5432"]
|
|
45
|
+
options: >-
|
|
46
|
+
--health-cmd "pg_isready -U foilstack -d foilstack"
|
|
47
|
+
--health-interval 10s --health-timeout 5s --health-retries 10
|
|
48
|
+
|
|
49
|
+
env:
|
|
50
|
+
FOILSTACK_TEST_DATABASE_URL: postgresql+psycopg://foilstack:ci-placeholder@localhost:5434/foilstack
|
|
51
|
+
|
|
52
|
+
steps:
|
|
53
|
+
- uses: actions/checkout@v4
|
|
54
|
+
- uses: astral-sh/setup-uv@v5
|
|
55
|
+
- run: uv sync --extra dev
|
|
56
|
+
|
|
57
|
+
# One run, then two questions of it: did anything fail, and did anything
|
|
58
|
+
# *skip*. A skip is a test that did not run, and the ones that skip here
|
|
59
|
+
# are the account-isolation tests — a green tick that means "we did not
|
|
60
|
+
# check" is worse than a red one.
|
|
61
|
+
- name: tests
|
|
62
|
+
run: |
|
|
63
|
+
set -o pipefail
|
|
64
|
+
uv run pytest -q --no-header -rs 2>&1 | tee /tmp/pytest.out
|
|
65
|
+
if grep -qE '[0-9]+ skipped' /tmp/pytest.out; then
|
|
66
|
+
echo "::error::tests were skipped — see the SKIPPED lines above"
|
|
67
|
+
exit 1
|
|
68
|
+
fi
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
name: release
|
|
2
|
+
|
|
3
|
+
# Publishing is deliberately manual and tag-driven. A release on every push to
|
|
4
|
+
# main would mean any merge burns a version number on PyPI, where versions
|
|
5
|
+
# cannot be reused or deleted — only yanked.
|
|
6
|
+
on:
|
|
7
|
+
push:
|
|
8
|
+
tags: ["v*"]
|
|
9
|
+
workflow_dispatch:
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
build:
|
|
13
|
+
runs-on: ubuntu-latest
|
|
14
|
+
steps:
|
|
15
|
+
- uses: actions/checkout@v4
|
|
16
|
+
- uses: astral-sh/setup-uv@v5
|
|
17
|
+
- run: uv build
|
|
18
|
+
|
|
19
|
+
# The wheel carries the templates, the static files and the migrations,
|
|
20
|
+
# none of which are Python and any of which can silently stop being
|
|
21
|
+
# included. Checked here rather than discovered by whoever pip-installs
|
|
22
|
+
# the release: a package that imports fine and 500s on its first page is
|
|
23
|
+
# worse than one that fails to build.
|
|
24
|
+
- name: the wheel has to contain more than Python
|
|
25
|
+
run: |
|
|
26
|
+
python - <<'PY'
|
|
27
|
+
import zipfile, glob, sys
|
|
28
|
+
whl = glob.glob("dist/*.whl")[0]
|
|
29
|
+
names = zipfile.ZipFile(whl).namelist()
|
|
30
|
+
required = [
|
|
31
|
+
"foilstack/web/templates/base.html",
|
|
32
|
+
"foilstack/web/static/app.css",
|
|
33
|
+
"foilstack/migrations/env.py",
|
|
34
|
+
]
|
|
35
|
+
missing = [r for r in required if r not in names]
|
|
36
|
+
versions = [n for n in names if "/migrations/versions/" in n and n.endswith(".py")]
|
|
37
|
+
if missing:
|
|
38
|
+
sys.exit(f"missing from {whl}: {missing}")
|
|
39
|
+
if not versions:
|
|
40
|
+
sys.exit(f"no migration revisions in {whl}")
|
|
41
|
+
print(f"{whl}: {len(names)} files, {len(versions)} migrations")
|
|
42
|
+
PY
|
|
43
|
+
|
|
44
|
+
# The tag is a claim about what is inside the wheel, and nothing else
|
|
45
|
+
# checks it. Hatchling reads the version from
|
|
46
|
+
# `src/foilstack/__init__.py`, so tagging v0.2.0 against a source that
|
|
47
|
+
# still says 0.1.21 publishes 0.1.21 — under a tag that says otherwise,
|
|
48
|
+
# to an index where that number can never be corrected.
|
|
49
|
+
- name: the tag has to match the version in the wheel
|
|
50
|
+
if: github.ref_type == 'tag'
|
|
51
|
+
run: |
|
|
52
|
+
python - <<'PY'
|
|
53
|
+
import glob, os, sys
|
|
54
|
+
built = os.path.basename(glob.glob("dist/*.whl")[0]).split("-")[1]
|
|
55
|
+
tagged = os.environ["GITHUB_REF_NAME"].removeprefix("v")
|
|
56
|
+
if built != tagged:
|
|
57
|
+
sys.exit(f"tag says {tagged}, wheel says {built} — refusing to publish")
|
|
58
|
+
print(f"tag and wheel agree on {built}")
|
|
59
|
+
PY
|
|
60
|
+
|
|
61
|
+
- uses: actions/upload-artifact@v4
|
|
62
|
+
with:
|
|
63
|
+
name: dist
|
|
64
|
+
path: dist/
|
|
65
|
+
|
|
66
|
+
publish:
|
|
67
|
+
needs: build
|
|
68
|
+
runs-on: ubuntu-latest
|
|
69
|
+
environment: pypi
|
|
70
|
+
# Trusted publishing: PyPI verifies this workflow's OIDC identity, so there
|
|
71
|
+
# is no API token to store, rotate or leak.
|
|
72
|
+
permissions:
|
|
73
|
+
id-token: write
|
|
74
|
+
steps:
|
|
75
|
+
- uses: actions/download-artifact@v4
|
|
76
|
+
with:
|
|
77
|
+
name: dist
|
|
78
|
+
path: dist/
|
|
79
|
+
- uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
# Byte-compiled / optimized / DLL files
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[codz]
|
|
4
|
+
*$py.class
|
|
5
|
+
|
|
6
|
+
# C extensions
|
|
7
|
+
*.so
|
|
8
|
+
|
|
9
|
+
# Distribution / packaging
|
|
10
|
+
.Python
|
|
11
|
+
build/
|
|
12
|
+
develop-eggs/
|
|
13
|
+
dist/
|
|
14
|
+
downloads/
|
|
15
|
+
eggs/
|
|
16
|
+
.eggs/
|
|
17
|
+
lib/
|
|
18
|
+
lib64/
|
|
19
|
+
parts/
|
|
20
|
+
sdist/
|
|
21
|
+
var/
|
|
22
|
+
wheels/
|
|
23
|
+
share/python-wheels/
|
|
24
|
+
*.egg-info/
|
|
25
|
+
.installed.cfg
|
|
26
|
+
*.egg
|
|
27
|
+
MANIFEST
|
|
28
|
+
|
|
29
|
+
# PyInstaller
|
|
30
|
+
# Usually these files are written by a python script from a template
|
|
31
|
+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
|
32
|
+
*.manifest
|
|
33
|
+
*.spec
|
|
34
|
+
|
|
35
|
+
# Installer logs
|
|
36
|
+
pip-log.txt
|
|
37
|
+
pip-delete-this-directory.txt
|
|
38
|
+
|
|
39
|
+
# Unit test / coverage reports
|
|
40
|
+
htmlcov/
|
|
41
|
+
.tox/
|
|
42
|
+
.nox/
|
|
43
|
+
.coverage
|
|
44
|
+
.coverage.*
|
|
45
|
+
.cache
|
|
46
|
+
nosetests.xml
|
|
47
|
+
coverage.xml
|
|
48
|
+
*.cover
|
|
49
|
+
*.py.cover
|
|
50
|
+
.hypothesis/
|
|
51
|
+
.pytest_cache/
|
|
52
|
+
cover/
|
|
53
|
+
|
|
54
|
+
# Translations
|
|
55
|
+
*.mo
|
|
56
|
+
*.pot
|
|
57
|
+
|
|
58
|
+
# Django stuff:
|
|
59
|
+
*.log
|
|
60
|
+
local_settings.py
|
|
61
|
+
db.sqlite3
|
|
62
|
+
db.sqlite3-journal
|
|
63
|
+
|
|
64
|
+
# Flask stuff:
|
|
65
|
+
instance/
|
|
66
|
+
.webassets-cache
|
|
67
|
+
|
|
68
|
+
# Scrapy stuff:
|
|
69
|
+
.scrapy
|
|
70
|
+
|
|
71
|
+
# Sphinx documentation
|
|
72
|
+
docs/_build/
|
|
73
|
+
|
|
74
|
+
# PyBuilder
|
|
75
|
+
.pybuilder/
|
|
76
|
+
target/
|
|
77
|
+
|
|
78
|
+
# Jupyter Notebook
|
|
79
|
+
.ipynb_checkpoints
|
|
80
|
+
|
|
81
|
+
# IPython
|
|
82
|
+
profile_default/
|
|
83
|
+
ipython_config.py
|
|
84
|
+
|
|
85
|
+
# pyenv
|
|
86
|
+
# For a library or package, you might want to ignore these files since the code is
|
|
87
|
+
# intended to run in multiple environments; otherwise, check them in:
|
|
88
|
+
# .python-version
|
|
89
|
+
|
|
90
|
+
# pipenv
|
|
91
|
+
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
|
92
|
+
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
|
93
|
+
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
|
94
|
+
# install all needed dependencies.
|
|
95
|
+
# Pipfile.lock
|
|
96
|
+
|
|
97
|
+
# UV
|
|
98
|
+
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
|
99
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
100
|
+
# commonly ignored for libraries.
|
|
101
|
+
# uv.lock
|
|
102
|
+
|
|
103
|
+
# poetry
|
|
104
|
+
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
|
105
|
+
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
|
106
|
+
# commonly ignored for libraries.
|
|
107
|
+
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
|
108
|
+
# poetry.lock
|
|
109
|
+
# poetry.toml
|
|
110
|
+
|
|
111
|
+
# pdm
|
|
112
|
+
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
|
113
|
+
# pdm recommends including project-wide configuration in pdm.toml, but excluding .pdm-python.
|
|
114
|
+
# https://pdm-project.org/en/latest/usage/project/#working-with-version-control
|
|
115
|
+
# pdm.lock
|
|
116
|
+
# pdm.toml
|
|
117
|
+
.pdm-python
|
|
118
|
+
.pdm-build/
|
|
119
|
+
|
|
120
|
+
# pixi
|
|
121
|
+
# Similar to Pipfile.lock, it is generally recommended to include pixi.lock in version control.
|
|
122
|
+
# pixi.lock
|
|
123
|
+
# Pixi creates a virtual environment in the .pixi directory, just like venv module creates one
|
|
124
|
+
# in the .venv directory. It is recommended not to include this directory in version control.
|
|
125
|
+
.pixi
|
|
126
|
+
|
|
127
|
+
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
|
128
|
+
__pypackages__/
|
|
129
|
+
|
|
130
|
+
# Celery stuff
|
|
131
|
+
celerybeat-schedule
|
|
132
|
+
celerybeat.pid
|
|
133
|
+
|
|
134
|
+
# Redis
|
|
135
|
+
*.rdb
|
|
136
|
+
*.aof
|
|
137
|
+
*.pid
|
|
138
|
+
|
|
139
|
+
# RabbitMQ
|
|
140
|
+
mnesia/
|
|
141
|
+
rabbitmq/
|
|
142
|
+
rabbitmq-data/
|
|
143
|
+
|
|
144
|
+
# ActiveMQ
|
|
145
|
+
activemq-data/
|
|
146
|
+
|
|
147
|
+
# SageMath parsed files
|
|
148
|
+
*.sage.py
|
|
149
|
+
|
|
150
|
+
# Environments
|
|
151
|
+
.env
|
|
152
|
+
.envrc
|
|
153
|
+
.venv
|
|
154
|
+
env/
|
|
155
|
+
venv/
|
|
156
|
+
ENV/
|
|
157
|
+
env.bak/
|
|
158
|
+
venv.bak/
|
|
159
|
+
|
|
160
|
+
# Spyder project settings
|
|
161
|
+
.spyderproject
|
|
162
|
+
.spyproject
|
|
163
|
+
|
|
164
|
+
# Rope project settings
|
|
165
|
+
.ropeproject
|
|
166
|
+
|
|
167
|
+
# mkdocs documentation
|
|
168
|
+
/site
|
|
169
|
+
|
|
170
|
+
# mypy
|
|
171
|
+
.mypy_cache/
|
|
172
|
+
.dmypy.json
|
|
173
|
+
dmypy.json
|
|
174
|
+
|
|
175
|
+
# Pyre type checker
|
|
176
|
+
.pyre/
|
|
177
|
+
|
|
178
|
+
# pytype static type analyzer
|
|
179
|
+
.pytype/
|
|
180
|
+
|
|
181
|
+
# Cython debug symbols
|
|
182
|
+
cython_debug/
|
|
183
|
+
|
|
184
|
+
# PyCharm
|
|
185
|
+
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
|
186
|
+
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
|
187
|
+
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
|
188
|
+
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
|
189
|
+
# .idea/
|
|
190
|
+
|
|
191
|
+
# Abstra
|
|
192
|
+
# Abstra is an AI-powered process automation framework.
|
|
193
|
+
# Ignore directories containing user credentials, local state, and settings.
|
|
194
|
+
# Learn more at https://abstra.io/docs
|
|
195
|
+
.abstra/
|
|
196
|
+
|
|
197
|
+
# Visual Studio Code
|
|
198
|
+
# Visual Studio Code specific template is maintained in a separate VisualStudioCode.gitignore
|
|
199
|
+
# that can be found at https://github.com/github/gitignore/blob/main/Global/VisualStudioCode.gitignore
|
|
200
|
+
# and can be added to the global gitignore or merged into this file. However, if you prefer,
|
|
201
|
+
# you could uncomment the following to ignore the entire vscode folder
|
|
202
|
+
# .vscode/
|
|
203
|
+
# Temporary file for partial code execution
|
|
204
|
+
tempCodeRunnerFile.py
|
|
205
|
+
|
|
206
|
+
# Ruff stuff:
|
|
207
|
+
.ruff_cache/
|
|
208
|
+
|
|
209
|
+
# PyPI configuration file
|
|
210
|
+
.pypirc
|
|
211
|
+
|
|
212
|
+
# Marimo
|
|
213
|
+
marimo/_static/
|
|
214
|
+
marimo/_lsp/
|
|
215
|
+
__marimo__/
|
|
216
|
+
|
|
217
|
+
# Streamlit
|
|
218
|
+
.streamlit/secrets.toml
|
|
219
|
+
|
|
220
|
+
# Your catalogue, scans and inventory. Local data, never committed — and the
|
|
221
|
+
# vector index alone is hundreds of megabytes.
|
|
222
|
+
/data/
|
|
223
|
+
|
|
224
|
+
# Deployment-specific compose settings (container names, tunnel, GPU services).
|
|
225
|
+
# Local to whichever host this runs on; never committed.
|
|
226
|
+
docker-compose.override.yml
|
|
227
|
+
|
|
228
|
+
# Database dumps written by the `backup` service. Never in git.
|
|
229
|
+
backups/
|
|
230
|
+
|
|
231
|
+
# Screenshots generated by scripts/preview.py. Regenerate them, do not commit
|
|
232
|
+
# them — they are a build product of a throwaway database and go stale silently.
|
|
233
|
+
# The one screenshot the README uses lives in docs/ and is committed on purpose.
|
|
234
|
+
shots/
|
|
235
|
+
|
|
236
|
+
# Demo build artifacts. The GIF and the WebP under the app's static directory
|
|
237
|
+
# are committed — the landing page serves both, WebP first with the GIF as the
|
|
238
|
+
# fallback, and the README shows the GIF. Playwright's webm is not: it is for
|
|
239
|
+
# posting to places that take real video, and regenerating it is one command.
|
|
240
|
+
src/foilstack/web/static/demo/video/
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Secret scanning config, used by the gitleaks pre-commit hook.
|
|
2
|
+
#
|
|
3
|
+
# Deliberately not run in CI: CI happens after the push, and this repo is
|
|
4
|
+
# public, so a credential it found would already be scraped. Rotation would be
|
|
5
|
+
# the only remedy left. The scan that can prevent a leak is the one that runs
|
|
6
|
+
# before the commit exists.
|
|
7
|
+
#
|
|
8
|
+
# This file earns its place. Measured against gitleaks v8.30 with its default
|
|
9
|
+
# ruleset and nothing else, a file containing POSTGRES_PASSWORD=<literal>,
|
|
10
|
+
# HF_TOKEN=hf_<literal> and a Postgres URL with an inline password produced
|
|
11
|
+
# "no leaks found". The defaults are tuned for cloud provider tokens; the
|
|
12
|
+
# secrets this project actually holds are none of those.
|
|
13
|
+
title = "foilstack"
|
|
14
|
+
|
|
15
|
+
[extend]
|
|
16
|
+
useDefault = true
|
|
17
|
+
|
|
18
|
+
[[rules]]
|
|
19
|
+
id = "foilstack-env-assignment"
|
|
20
|
+
description = "A secret-shaped environment variable assigned a literal value"
|
|
21
|
+
# No lookahead: gitleaks uses Go's RE2, which has none. Placeholders are
|
|
22
|
+
# excluded by the allowlist below rather than by the pattern.
|
|
23
|
+
regex = '''(?i)\b(CLOUDFLARE_TUNNEL_TOKEN|ADMIN_PASSWORD|POSTGRES_PASSWORD|HF_TOKEN|POSTHOG_API_KEY|TURNSTILE_SECRET_KEY|OPENAI_API_KEY|ANTHROPIC_API_KEY)\s*[:=]\s*['"]?[A-Za-z0-9_\-\.]{8,}'''
|
|
24
|
+
tags = ["key", "env"]
|
|
25
|
+
|
|
26
|
+
[rules.allowlist]
|
|
27
|
+
regexTarget = "line"
|
|
28
|
+
regexes = [
|
|
29
|
+
'''\$\{''', # ${VAR} indirection
|
|
30
|
+
'''(?i)your[-_]''',
|
|
31
|
+
'''(?i)changeme''',
|
|
32
|
+
'''(?i)placeholder''',
|
|
33
|
+
'''(?i)example''',
|
|
34
|
+
'''(?i)xxx+''',
|
|
35
|
+
]
|
|
36
|
+
|
|
37
|
+
[[rules]]
|
|
38
|
+
id = "foilstack-connection-uri"
|
|
39
|
+
description = "A connection string with the password written into it"
|
|
40
|
+
# The shape of DATABASE_URL, which is the most sensitive value this project
|
|
41
|
+
# holds and which neither the default ruleset nor the rule above catches: the
|
|
42
|
+
# password sits between a colon and an at-sign rather than after an equals.
|
|
43
|
+
regex = '''(?i)\b(postgres|postgresql|redis|amqp|mongodb|mysql)(\+[a-z]+)?://[^\s:/@]+:[^\s:/@]{6,}@'''
|
|
44
|
+
tags = ["key", "uri"]
|
|
45
|
+
|
|
46
|
+
[rules.allowlist]
|
|
47
|
+
regexTarget = "line"
|
|
48
|
+
regexes = [
|
|
49
|
+
'''\$\{''', # ${VAR} indirection, as in docker-compose
|
|
50
|
+
'''(?i)your[-_]''',
|
|
51
|
+
'''(?i)change[-_]?me''',
|
|
52
|
+
'''(?i)placeholder''',
|
|
53
|
+
'''(?i)example''',
|
|
54
|
+
'''(?i)localhost''',
|
|
55
|
+
'''(?i)\bfoilstack:foilstack\b''', # the documented local default
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
[allowlist]
|
|
59
|
+
paths = ['''\.env\.example$''', '''\.gitleaks\.toml$''']
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/gitleaks/gitleaks
|
|
3
|
+
rev: v8.30.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: gitleaks
|
|
6
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
7
|
+
rev: v6.0.0
|
|
8
|
+
hooks:
|
|
9
|
+
- id: check-added-large-files
|
|
10
|
+
args: ["--maxkb=2048"]
|
|
11
|
+
# Overlaps with gitleaks and costs a line. A stray id_rsa or .pem is the
|
|
12
|
+
# one secret that arrives as a whole file rather than a value on a line,
|
|
13
|
+
# and a check aimed squarely at it is cheap insurance.
|
|
14
|
+
- id: detect-private-key
|
|
15
|
+
# Licences are shipped verbatim. The whitespace hooks are right about
|
|
16
|
+
# every other file and wrong about these: the OFL asks that the licence
|
|
17
|
+
# travel with the fonts, and quietly reformatting somebody else's licence
|
|
18
|
+
# text — even a trailing space — is not our edit to make.
|
|
19
|
+
- id: end-of-file-fixer
|
|
20
|
+
exclude: ^(LICENSE|.*/OFL\.txt)$
|
|
21
|
+
- id: trailing-whitespace
|
|
22
|
+
exclude: ^(LICENSE|.*/OFL\.txt)$
|
|
23
|
+
|
|
24
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
25
|
+
rev: v0.16.4
|
|
26
|
+
hooks:
|
|
27
|
+
# Lint first so a fix that changes layout is then formatted, rather than
|
|
28
|
+
# the two disagreeing across a commit.
|
|
29
|
+
- id: ruff-check
|
|
30
|
+
args: ["--fix"]
|
|
31
|
+
- id: ruff-format
|
|
32
|
+
|
|
33
|
+
- repo: https://github.com/pre-commit/mirrors-mypy
|
|
34
|
+
rev: v2.3.1
|
|
35
|
+
hooks:
|
|
36
|
+
- id: mypy
|
|
37
|
+
# Runs against src/ only — the settings live in pyproject.toml so a
|
|
38
|
+
# local `uv run mypy` and the hook cannot disagree.
|
|
39
|
+
files: ^src/foilstack/
|
|
40
|
+
additional_dependencies:
|
|
41
|
+
- sqlalchemy[mypy]>=2.0
|
|
42
|
+
- pydantic>=2.7
|
|
43
|
+
- types-requests
|
|
44
|
+
|
|
45
|
+
# Bumps the patch version when application code is committed, so the footer
|
|
46
|
+
# and the User-Agent always name a build that is actually running. It stages
|
|
47
|
+
# its own edit; a hook that leaves the change unstaged makes every commit
|
|
48
|
+
# fail once and need repeating.
|
|
49
|
+
#
|
|
50
|
+
# `pass_filenames: false` because it decides for itself from the index which
|
|
51
|
+
# paths are staged, and `always_run: false` with a `files:` filter so a
|
|
52
|
+
# README or CI commit does not move the version — a number that moves for
|
|
53
|
+
# everything means nothing.
|
|
54
|
+
- repo: local
|
|
55
|
+
hooks:
|
|
56
|
+
- id: bump-version
|
|
57
|
+
name: bump the patch version
|
|
58
|
+
entry: python scripts/bump_version.py
|
|
59
|
+
# `language: python` rather than `system`: pre-commit provisions its own
|
|
60
|
+
# interpreter, so this works on a machine where `python` is not on PATH
|
|
61
|
+
# — which is this one, and is most Linux boxes. The script imports only
|
|
62
|
+
# the standard library, so the environment needs nothing installed.
|
|
63
|
+
language: python
|
|
64
|
+
files: ^src/foilstack/
|
|
65
|
+
pass_filenames: false
|
|
66
|
+
|
|
67
|
+
# The suite, on push rather than on commit. See scripts/check-tests.sh for
|
|
68
|
+
# why: a plain pytest hook is green on a machine with no Postgres, because
|
|
69
|
+
# the account-isolation tests skip rather than fail — and those are the ones
|
|
70
|
+
# worth gating on. Install it with:
|
|
71
|
+
#
|
|
72
|
+
# pre-commit install --hook-type pre-push
|
|
73
|
+
- repo: local
|
|
74
|
+
hooks:
|
|
75
|
+
- id: tests
|
|
76
|
+
name: tests (and nothing skipped)
|
|
77
|
+
entry: scripts/check-tests.sh
|
|
78
|
+
language: script
|
|
79
|
+
stages: [pre-push]
|
|
80
|
+
pass_filenames: false
|
|
81
|
+
always_run: true
|