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.
Files changed (118) hide show
  1. foilstack-0.2.0/.env.example +112 -0
  2. foilstack-0.2.0/.git-blame-ignore-revs +10 -0
  3. foilstack-0.2.0/.github/workflows/ci.yml +68 -0
  4. foilstack-0.2.0/.github/workflows/release.yml +79 -0
  5. foilstack-0.2.0/.gitignore +240 -0
  6. foilstack-0.2.0/.gitleaks.toml +59 -0
  7. foilstack-0.2.0/.pre-commit-config.yaml +81 -0
  8. foilstack-0.2.0/AGENTS.md +234 -0
  9. foilstack-0.2.0/Dockerfile.embedder +32 -0
  10. foilstack-0.2.0/Dockerfile.web +37 -0
  11. foilstack-0.2.0/LICENSE +661 -0
  12. foilstack-0.2.0/PKG-INFO +241 -0
  13. foilstack-0.2.0/README.md +193 -0
  14. foilstack-0.2.0/alembic.ini +49 -0
  15. foilstack-0.2.0/docker/entrypoint.sh +16 -0
  16. foilstack-0.2.0/docker-compose.gpu.yml +31 -0
  17. foilstack-0.2.0/docker-compose.yml +334 -0
  18. foilstack-0.2.0/docs/accounts.md +57 -0
  19. foilstack-0.2.0/docs/backups.md +58 -0
  20. foilstack-0.2.0/docs/encoder.md +110 -0
  21. foilstack-0.2.0/docs/plugins.md +36 -0
  22. foilstack-0.2.0/docs/prices.md +46 -0
  23. foilstack-0.2.0/pyproject.toml +150 -0
  24. foilstack-0.2.0/scripts/bump_version.py +124 -0
  25. foilstack-0.2.0/scripts/check-tests.sh +35 -0
  26. foilstack-0.2.0/scripts/check_categories.py +69 -0
  27. foilstack-0.2.0/scripts/demo.py +334 -0
  28. foilstack-0.2.0/scripts/preview.py +436 -0
  29. foilstack-0.2.0/scripts/restore.sh +53 -0
  30. foilstack-0.2.0/scripts/shots.py +77 -0
  31. foilstack-0.2.0/src/foilstack/__init__.py +1 -0
  32. foilstack-0.2.0/src/foilstack/cli.py +634 -0
  33. foilstack-0.2.0/src/foilstack/config.py +149 -0
  34. foilstack-0.2.0/src/foilstack/db.py +471 -0
  35. foilstack-0.2.0/src/foilstack/embedder/__init__.py +0 -0
  36. foilstack-0.2.0/src/foilstack/embedder/app.py +122 -0
  37. foilstack-0.2.0/src/foilstack/embedding.py +36 -0
  38. foilstack-0.2.0/src/foilstack/images.py +72 -0
  39. foilstack-0.2.0/src/foilstack/importing.py +385 -0
  40. foilstack-0.2.0/src/foilstack/inventory.py +439 -0
  41. foilstack-0.2.0/src/foilstack/migrations/README +11 -0
  42. foilstack-0.2.0/src/foilstack/migrations/env.py +93 -0
  43. foilstack-0.2.0/src/foilstack/migrations/script.py.mako +24 -0
  44. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2105-04a130322022_pgvector_extension.py +33 -0
  45. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2105-327c4a89b5f1_baseline_schema.py +195 -0
  46. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2106-ece6f4eba34c_card_embedding_hnsw_index.py +54 -0
  47. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2139-a6e3135d319e_scan_best_score.py +27 -0
  48. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2213-fd1c94a2fc8f_inventory_finish_sold_state_and_notes.py +65 -0
  49. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2229-ab360b649e52_drop_inventory_quantity.py +41 -0
  50. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2326-36ee08bea9f1_card_prices_price_history_and_sync_state.py +71 -0
  51. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_22_2349-d46268462eb9_declared_printing_on_inventory.py +27 -0
  52. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_23_0918-6842dd86917f_scan_size_in_bytes.py +37 -0
  53. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_23_1400-b91c4e2f7a10_card_name_trigram_index.py +37 -0
  54. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_23_1520-c73f8a1d4b62_scan_chosen_card.py +35 -0
  55. foilstack-0.2.0/src/foilstack/migrations/versions/2026_08_24_0210-d18b3f9c5e41_remember_missing_card_images.py +27 -0
  56. foilstack-0.2.0/src/foilstack/plugins/__init__.py +45 -0
  57. foilstack-0.2.0/src/foilstack/plugins/base.py +104 -0
  58. foilstack-0.2.0/src/foilstack/plugins/exports/ebay.toml +31 -0
  59. foilstack-0.2.0/src/foilstack/plugins/exports/tcgplayer.toml +44 -0
  60. foilstack-0.2.0/src/foilstack/plugins/exports.py +95 -0
  61. foilstack-0.2.0/src/foilstack/plugins/sources/__init__.py +0 -0
  62. foilstack-0.2.0/src/foilstack/plugins/sources/tcgcsv.py +265 -0
  63. foilstack-0.2.0/src/foilstack/prices.py +162 -0
  64. foilstack-0.2.0/src/foilstack/search.py +144 -0
  65. foilstack-0.2.0/src/foilstack/web/app.py +235 -0
  66. foilstack-0.2.0/src/foilstack/web/auth.py +209 -0
  67. foilstack-0.2.0/src/foilstack/web/chrome.py +178 -0
  68. foilstack-0.2.0/src/foilstack/web/deps.py +72 -0
  69. foilstack-0.2.0/src/foilstack/web/joblog.py +51 -0
  70. foilstack-0.2.0/src/foilstack/web/proof.py +54 -0
  71. foilstack-0.2.0/src/foilstack/web/ratelimit.py +103 -0
  72. foilstack-0.2.0/src/foilstack/web/routes/__init__.py +18 -0
  73. foilstack-0.2.0/src/foilstack/web/routes/accounts.py +227 -0
  74. foilstack-0.2.0/src/foilstack/web/routes/inventory.py +384 -0
  75. foilstack-0.2.0/src/foilstack/web/routes/listings.py +216 -0
  76. foilstack-0.2.0/src/foilstack/web/routes/media.py +181 -0
  77. foilstack-0.2.0/src/foilstack/web/routes/scans.py +527 -0
  78. foilstack-0.2.0/src/foilstack/web/static/app.css +1165 -0
  79. foilstack-0.2.0/src/foilstack/web/static/brand/apple-touch-icon.png +0 -0
  80. foilstack-0.2.0/src/foilstack/web/static/brand/favicon-32.png +0 -0
  81. foilstack-0.2.0/src/foilstack/web/static/brand/favicon.svg +15 -0
  82. foilstack-0.2.0/src/foilstack/web/static/brand/mark.svg +16 -0
  83. foilstack-0.2.0/src/foilstack/web/static/demo/foilstack.gif +0 -0
  84. foilstack-0.2.0/src/foilstack/web/static/demo/foilstack.webp +0 -0
  85. foilstack-0.2.0/src/foilstack/web/static/fonts/OFL.txt +93 -0
  86. foilstack-0.2.0/src/foilstack/web/static/fonts/README.md +15 -0
  87. foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-cyrillic-ext.woff2 +0 -0
  88. foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-cyrillic.woff2 +0 -0
  89. foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-greek.woff2 +0 -0
  90. foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-latin-ext.woff2 +0 -0
  91. foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-latin.woff2 +0 -0
  92. foilstack-0.2.0/src/foilstack/web/static/fonts/jetbrains-mono-vietnamese.woff2 +0 -0
  93. foilstack-0.2.0/src/foilstack/web/static/zoom.js +93 -0
  94. foilstack-0.2.0/src/foilstack/web/templates/_card_panel.html +141 -0
  95. foilstack-0.2.0/src/foilstack/web/templates/_match_options.html +17 -0
  96. foilstack-0.2.0/src/foilstack/web/templates/_match_panel.html +28 -0
  97. foilstack-0.2.0/src/foilstack/web/templates/analytics.html +126 -0
  98. foilstack-0.2.0/src/foilstack/web/templates/base.html +91 -0
  99. foilstack-0.2.0/src/foilstack/web/templates/card.html +295 -0
  100. foilstack-0.2.0/src/foilstack/web/templates/import.html +484 -0
  101. foilstack-0.2.0/src/foilstack/web/templates/inventory.html +177 -0
  102. foilstack-0.2.0/src/foilstack/web/templates/landing.html +208 -0
  103. foilstack-0.2.0/src/foilstack/web/templates/listings.html +124 -0
  104. foilstack-0.2.0/src/foilstack/web/templates/login.html +78 -0
  105. foilstack-0.2.0/src/foilstack/web/templates/plugins.html +90 -0
  106. foilstack-0.2.0/tests/conftest.py +48 -0
  107. foilstack-0.2.0/tests/test_auth.py +57 -0
  108. foilstack-0.2.0/tests/test_bump_version.py +145 -0
  109. foilstack-0.2.0/tests/test_cli.py +145 -0
  110. foilstack-0.2.0/tests/test_exports.py +57 -0
  111. foilstack-0.2.0/tests/test_hardening.py +179 -0
  112. foilstack-0.2.0/tests/test_images.py +82 -0
  113. foilstack-0.2.0/tests/test_importing.py +207 -0
  114. foilstack-0.2.0/tests/test_inventory.py +243 -0
  115. foilstack-0.2.0/tests/test_isolation.py +1355 -0
  116. foilstack-0.2.0/tests/test_plugins.py +21 -0
  117. foilstack-0.2.0/tests/test_search.py +100 -0
  118. 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