regstack 0.1.1__tar.gz → 0.2.1__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 (190) hide show
  1. {regstack-0.1.1 → regstack-0.2.1}/.github/workflows/publish.yml +4 -1
  2. regstack-0.2.1/.github/workflows/test.yml +147 -0
  3. regstack-0.2.1/CHANGELOG.md +42 -0
  4. {regstack-0.1.1 → regstack-0.2.1}/CLAUDE.md +24 -0
  5. regstack-0.2.1/PKG-INFO +272 -0
  6. regstack-0.2.1/README.md +208 -0
  7. regstack-0.2.1/docs/architecture.md +210 -0
  8. regstack-0.2.1/docs/changelog.md +179 -0
  9. {regstack-0.1.1 → regstack-0.2.1}/docs/cli.md +5 -4
  10. {regstack-0.1.1 → regstack-0.2.1}/docs/configuration.md +36 -5
  11. regstack-0.2.1/docs/embedding.md +200 -0
  12. regstack-0.2.1/docs/index.md +99 -0
  13. regstack-0.2.1/docs/quickstart.md +157 -0
  14. regstack-0.2.1/docs/security.md +226 -0
  15. regstack-0.2.1/examples/_common/app.py +101 -0
  16. regstack-0.2.1/examples/mongo/main.py +31 -0
  17. {regstack-0.1.1/examples/minimal → regstack-0.2.1/examples/mongo}/regstack.toml +1 -0
  18. regstack-0.2.1/examples/postgres/README.md +29 -0
  19. regstack-0.2.1/examples/postgres/main.py +30 -0
  20. regstack-0.2.1/examples/postgres/regstack.toml +29 -0
  21. regstack-0.2.1/examples/sqlite/README.md +33 -0
  22. regstack-0.2.1/examples/sqlite/main.py +34 -0
  23. regstack-0.2.1/examples/sqlite/regstack.toml +30 -0
  24. {regstack-0.1.1 → regstack-0.2.1}/pyproject.toml +16 -3
  25. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/app.py +31 -15
  26. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/auth/dependencies.py +2 -2
  27. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/auth/lockout.py +1 -1
  28. regstack-0.2.1/src/regstack/backends/__init__.py +21 -0
  29. regstack-0.2.1/src/regstack/backends/base.py +73 -0
  30. regstack-0.2.1/src/regstack/backends/factory.py +50 -0
  31. regstack-0.2.1/src/regstack/backends/mongo/__init__.py +5 -0
  32. regstack-0.2.1/src/regstack/backends/mongo/backend.py +57 -0
  33. regstack-0.2.1/src/regstack/backends/mongo/client.py +38 -0
  34. regstack-0.2.1/src/regstack/backends/mongo/repositories/__init__.py +27 -0
  35. {regstack-0.1.1/src/regstack/db → regstack-0.2.1/src/regstack/backends/mongo}/repositories/login_attempt_repo.py +6 -0
  36. {regstack-0.1.1/src/regstack/db → regstack-0.2.1/src/regstack/backends/mongo}/repositories/mfa_code_repo.py +16 -16
  37. {regstack-0.1.1/src/regstack/db → regstack-0.2.1/src/regstack/backends/mongo}/repositories/pending_repo.py +2 -2
  38. {regstack-0.1.1/src/regstack/db → regstack-0.2.1/src/regstack/backends/mongo}/repositories/user_repo.py +20 -6
  39. regstack-0.2.1/src/regstack/backends/protocols.py +181 -0
  40. regstack-0.2.1/src/regstack/backends/sql/__init__.py +3 -0
  41. regstack-0.2.1/src/regstack/backends/sql/backend.py +73 -0
  42. regstack-0.2.1/src/regstack/backends/sql/migrations/__init__.py +124 -0
  43. regstack-0.2.1/src/regstack/backends/sql/migrations/env.py +69 -0
  44. regstack-0.2.1/src/regstack/backends/sql/migrations/script.py.mako +28 -0
  45. regstack-0.2.1/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +108 -0
  46. regstack-0.2.1/src/regstack/backends/sql/repositories/blacklist_repo.py +36 -0
  47. regstack-0.2.1/src/regstack/backends/sql/repositories/login_attempt_repo.py +45 -0
  48. regstack-0.2.1/src/regstack/backends/sql/repositories/mfa_code_repo.py +115 -0
  49. regstack-0.2.1/src/regstack/backends/sql/repositories/pending_repo.py +69 -0
  50. regstack-0.2.1/src/regstack/backends/sql/repositories/user_repo.py +198 -0
  51. regstack-0.2.1/src/regstack/backends/sql/schema.py +139 -0
  52. regstack-0.2.1/src/regstack/backends/sql/types.py +37 -0
  53. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/cli/__main__.py +3 -1
  54. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/cli/_runtime.py +7 -9
  55. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/cli/doctor.py +57 -33
  56. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/cli/init.py +46 -8
  57. regstack-0.2.1/src/regstack/cli/migrate.py +64 -0
  58. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/config/schema.py +7 -1
  59. regstack-0.2.1/src/regstack/models/_objectid.py +55 -0
  60. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/account.py +1 -1
  61. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/admin.py +10 -4
  62. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/login.py +1 -1
  63. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/phone.py +1 -1
  64. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/register.py +1 -2
  65. regstack-0.2.1/src/regstack/version.py +1 -0
  66. regstack-0.2.1/tasks.py +347 -0
  67. regstack-0.2.1/tests/conftest.py +266 -0
  68. {regstack-0.1.1 → regstack-0.2.1}/tests/integration/test_indexes.py +14 -0
  69. regstack-0.2.1/tests/integration/test_sql_migrations.py +89 -0
  70. regstack-0.2.1/tests/unit/__init__.py +0 -0
  71. regstack-0.2.1/tests/unit/test_base_install_imports.py +69 -0
  72. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_cli.py +13 -12
  73. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_lockout.py +20 -4
  74. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_mfa_code_repo.py +17 -4
  75. {regstack-0.1.1 → regstack-0.2.1}/uv.lock +219 -5
  76. regstack-0.1.1/.github/workflows/test.yml +0 -71
  77. regstack-0.1.1/CHANGELOG.md +0 -19
  78. regstack-0.1.1/PKG-INFO +0 -209
  79. regstack-0.1.1/README.md +0 -153
  80. regstack-0.1.1/docs/architecture.md +0 -134
  81. regstack-0.1.1/docs/changelog.md +0 -82
  82. regstack-0.1.1/docs/embedding.md +0 -146
  83. regstack-0.1.1/docs/index.md +0 -55
  84. regstack-0.1.1/docs/quickstart.md +0 -115
  85. regstack-0.1.1/docs/security.md +0 -170
  86. regstack-0.1.1/examples/minimal/main.py +0 -109
  87. regstack-0.1.1/src/regstack/db/__init__.py +0 -17
  88. regstack-0.1.1/src/regstack/db/client.py +0 -26
  89. regstack-0.1.1/src/regstack/models/_objectid.py +0 -30
  90. regstack-0.1.1/src/regstack/version.py +0 -1
  91. regstack-0.1.1/tasks.py +0 -98
  92. regstack-0.1.1/tests/conftest.py +0 -144
  93. {regstack-0.1.1 → regstack-0.2.1}/.gitignore +0 -0
  94. {regstack-0.1.1 → regstack-0.2.1}/.python-version +0 -0
  95. {regstack-0.1.1 → regstack-0.2.1}/.readthedocs.yaml +0 -0
  96. {regstack-0.1.1 → regstack-0.2.1}/LICENSE +0 -0
  97. {regstack-0.1.1 → regstack-0.2.1}/NOTICE +0 -0
  98. {regstack-0.1.1 → regstack-0.2.1}/SECURITY.md +0 -0
  99. {regstack-0.1.1 → regstack-0.2.1}/docs/_static/.gitkeep +0 -0
  100. {regstack-0.1.1 → regstack-0.2.1}/docs/_templates/.gitkeep +0 -0
  101. {regstack-0.1.1 → regstack-0.2.1}/docs/api.md +0 -0
  102. {regstack-0.1.1 → regstack-0.2.1}/docs/conf.py +0 -0
  103. {regstack-0.1.1 → regstack-0.2.1}/docs/theming.md +0 -0
  104. {regstack-0.1.1/src/regstack/cli → regstack-0.2.1/examples/_common}/__init__.py +0 -0
  105. {regstack-0.1.1/examples/minimal → regstack-0.2.1/examples/mongo}/README.md +0 -0
  106. {regstack-0.1.1/examples/minimal → regstack-0.2.1/examples/mongo}/branding/theme.css +0 -0
  107. {regstack-0.1.1 → regstack-0.2.1}/regstack.toml.example +0 -0
  108. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/__init__.py +0 -0
  109. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/auth/__init__.py +0 -0
  110. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/auth/clock.py +0 -0
  111. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/auth/jwt.py +0 -0
  112. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/auth/mfa.py +0 -0
  113. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/auth/password.py +0 -0
  114. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/auth/tokens.py +0 -0
  115. {regstack-0.1.1/src/regstack/db → regstack-0.2.1/src/regstack/backends/mongo}/indexes.py +0 -0
  116. {regstack-0.1.1/src/regstack/db → regstack-0.2.1/src/regstack/backends/mongo}/repositories/blacklist_repo.py +0 -0
  117. {regstack-0.1.1/src/regstack/db → regstack-0.2.1/src/regstack/backends/sql}/repositories/__init__.py +0 -0
  118. {regstack-0.1.1/tests → regstack-0.2.1/src/regstack/cli}/__init__.py +0 -0
  119. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/cli/admin.py +0 -0
  120. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/config/__init__.py +0 -0
  121. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/config/loader.py +0 -0
  122. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/config/secrets.py +0 -0
  123. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/__init__.py +0 -0
  124. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/base.py +0 -0
  125. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/composer.py +0 -0
  126. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/console.py +0 -0
  127. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/factory.py +0 -0
  128. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/ses.py +0 -0
  129. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/smtp.py +0 -0
  130. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/email_change.html +0 -0
  131. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/email_change.subject.txt +0 -0
  132. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/email_change.txt +0 -0
  133. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/password_reset.html +0 -0
  134. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  135. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/password_reset.txt +0 -0
  136. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  137. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  138. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/verification.html +0 -0
  139. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/verification.subject.txt +0 -0
  140. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/email/templates/verification.txt +0 -0
  141. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/hooks/__init__.py +0 -0
  142. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/hooks/events.py +0 -0
  143. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/models/__init__.py +0 -0
  144. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/models/login_attempt.py +0 -0
  145. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/models/mfa_code.py +0 -0
  146. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/models/pending_registration.py +0 -0
  147. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/models/user.py +0 -0
  148. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/__init__.py +0 -0
  149. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/_schemas.py +0 -0
  150. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/logout.py +0 -0
  151. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/password.py +0 -0
  152. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/routers/verify.py +0 -0
  153. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/sms/__init__.py +0 -0
  154. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/sms/base.py +0 -0
  155. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/sms/factory.py +0 -0
  156. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/sms/null.py +0 -0
  157. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/sms/sns.py +0 -0
  158. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/sms/twilio.py +0 -0
  159. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/__init__.py +0 -0
  160. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/pages.py +0 -0
  161. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/static/css/core.css +0 -0
  162. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/static/css/theme.css +0 -0
  163. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/static/js/regstack.js +0 -0
  164. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  165. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/templates/auth/forgot.html +0 -0
  166. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/templates/auth/login.html +0 -0
  167. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/templates/auth/me.html +0 -0
  168. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  169. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/templates/auth/register.html +0 -0
  170. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/templates/auth/reset.html +0 -0
  171. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/templates/auth/verify.html +0 -0
  172. {regstack-0.1.1 → regstack-0.2.1}/src/regstack/ui/templates/base.html +0 -0
  173. {regstack-0.1.1/tests/integration → regstack-0.2.1/tests}/__init__.py +0 -0
  174. {regstack-0.1.1/tests/unit → regstack-0.2.1/tests/integration}/__init__.py +0 -0
  175. {regstack-0.1.1 → regstack-0.2.1}/tests/integration/test_account_management.py +0 -0
  176. {regstack-0.1.1 → regstack-0.2.1}/tests/integration/test_admin_router.py +0 -0
  177. {regstack-0.1.1 → regstack-0.2.1}/tests/integration/test_happy_path.py +0 -0
  178. {regstack-0.1.1 → regstack-0.2.1}/tests/integration/test_login_lockout.py +0 -0
  179. {regstack-0.1.1 → regstack-0.2.1}/tests/integration/test_mfa.py +0 -0
  180. {regstack-0.1.1 → regstack-0.2.1}/tests/integration/test_password_reset.py +0 -0
  181. {regstack-0.1.1 → regstack-0.2.1}/tests/integration/test_ui_router.py +0 -0
  182. {regstack-0.1.1 → regstack-0.2.1}/tests/integration/test_verification.py +0 -0
  183. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_config_loader.py +0 -0
  184. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_jwt.py +0 -0
  185. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_mail_composer.py +0 -0
  186. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_password.py +0 -0
  187. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_ses_backend.py +0 -0
  188. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_sms.py +0 -0
  189. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_smtp_backend.py +0 -0
  190. {regstack-0.1.1 → regstack-0.2.1}/tests/unit/test_ui_env.py +0 -0
@@ -30,7 +30,10 @@ jobs:
30
30
  run: |
31
31
  tag="${GITHUB_REF##*/}" # e.g. v0.1.0
32
32
  tag_version="${tag#v}" # e.g. 0.1.0
33
- pkg_version=$(uv run python -c 'from regstack.version import __version__; print(__version__)')
33
+ # Read version from pyproject.toml directly importing the package
34
+ # would pull in optional-extra dependencies (e.g. pymongo) that
35
+ # aren't installed in this build job.
36
+ pkg_version=$(python -c 'import tomllib; print(tomllib.load(open("pyproject.toml","rb"))["project"]["version"])')
34
37
  if [ "$tag_version" != "$pkg_version" ]; then
35
38
  echo "Tag $tag does not match package version $pkg_version" >&2
36
39
  exit 1
@@ -0,0 +1,147 @@
1
+ name: test
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ workflow_dispatch:
8
+
9
+ concurrency:
10
+ group: test-${{ github.ref }}
11
+ cancel-in-progress: true
12
+
13
+ jobs:
14
+ pytest:
15
+ runs-on: ubuntu-latest
16
+ strategy:
17
+ fail-fast: false
18
+ matrix:
19
+ python-version: ["3.11", "3.12"]
20
+ services:
21
+ mongodb:
22
+ image: mongo:7
23
+ ports:
24
+ - 27017:27017
25
+ options: >-
26
+ --health-cmd "mongosh --quiet --eval 'db.runCommand({ping:1}).ok' --port 27017"
27
+ --health-interval 5s
28
+ --health-timeout 5s
29
+ --health-retries 12
30
+ --health-start-period 10s
31
+ postgres:
32
+ image: postgres:16
33
+ ports:
34
+ - 5432:5432
35
+ env:
36
+ POSTGRES_USER: regstack
37
+ POSTGRES_PASSWORD: regstack
38
+ POSTGRES_DB: regstack
39
+ options: >-
40
+ --health-cmd "pg_isready -U regstack"
41
+ --health-interval 5s
42
+ --health-timeout 5s
43
+ --health-retries 12
44
+ --health-start-period 10s
45
+ env:
46
+ # Drives the parametrized backend fixture in tests/conftest.py.
47
+ # Connect as superuser so the per-test fixture can CREATE/DROP DATABASE.
48
+ REGSTACK_TEST_POSTGRES_URL: postgresql+asyncpg://regstack:regstack@localhost:5432
49
+ steps:
50
+ - uses: actions/checkout@v4
51
+
52
+ - uses: astral-sh/setup-uv@v3
53
+ with:
54
+ enable-cache: true
55
+ cache-dependency-glob: "uv.lock"
56
+
57
+ - name: Pin Python ${{ matrix.python-version }}
58
+ run: uv python pin ${{ matrix.python-version }}
59
+
60
+ - name: Sync dependencies
61
+ run: uv sync --extra dev
62
+
63
+ - name: ruff check
64
+ run: uv run ruff check src tests
65
+
66
+ - name: ruff format check
67
+ run: uv run ruff format --check src tests
68
+
69
+ - name: pytest (parallel, all backends)
70
+ run: uv run python -m pytest -n auto --tb=short
71
+
72
+ docs:
73
+ runs-on: ubuntu-latest
74
+ steps:
75
+ - uses: actions/checkout@v4
76
+ - uses: astral-sh/setup-uv@v3
77
+ with:
78
+ enable-cache: true
79
+ cache-dependency-glob: "uv.lock"
80
+ - run: uv python pin 3.11
81
+ - run: uv sync --extra docs --extra dev
82
+ - run: uv run sphinx-build -b html -W --keep-going docs docs/_build/html
83
+ - uses: actions/upload-artifact@v4
84
+ with:
85
+ name: docs-html
86
+ path: docs/_build/html
87
+
88
+ base-install-smoketest:
89
+ # Catches the kind of regression that broke 0.2.0: an unconditional
90
+ # `from bson import ...` (or any other optional-extra import) at module
91
+ # top level makes `import regstack` fail for any user who didn't opt
92
+ # into the `mongo` extra. The unit test of the same name exercises the
93
+ # equivalent in-process via meta_path blocking; this job verifies the
94
+ # actual built wheel installs and imports against a clean Python.
95
+ runs-on: ubuntu-latest
96
+ steps:
97
+ - uses: actions/checkout@v4
98
+ - uses: astral-sh/setup-uv@v3
99
+ with:
100
+ enable-cache: true
101
+ cache-dependency-glob: "uv.lock"
102
+ - run: uv python pin 3.11
103
+ - name: Build wheel
104
+ run: uv build --wheel
105
+ - name: Install wheel into a clean venv (no extras)
106
+ run: |
107
+ python -m venv /tmp/smoke
108
+ /tmp/smoke/bin/pip install --upgrade pip
109
+ /tmp/smoke/bin/pip install dist/regstack-*.whl
110
+ - name: Import smoketest
111
+ run: |
112
+ /tmp/smoke/bin/python -c "
113
+ import regstack
114
+ from regstack import RegStack, RegStackConfig
115
+ assert RegStack.__module__ == 'regstack.app'
116
+ print('ok', regstack.__version__)
117
+ "
118
+ - name: SQLite end-to-end smoketest
119
+ run: |
120
+ /tmp/smoke/bin/python <<'PY'
121
+ import asyncio, secrets, tempfile
122
+ from pathlib import Path
123
+ from regstack import RegStack, RegStackConfig
124
+ from regstack.models.user import BaseUser
125
+
126
+ async def main() -> None:
127
+ with tempfile.TemporaryDirectory() as td:
128
+ cfg = RegStackConfig.load(
129
+ toml_path=Path("/dev/null"),
130
+ secrets_env_path=Path("/dev/null"),
131
+ jwt_secret=secrets.token_urlsafe(64),
132
+ database_url=f"sqlite+aiosqlite:///{td}/rs.db",
133
+ require_verification=False,
134
+ allow_registration=True,
135
+ rate_limit_disabled=True,
136
+ )
137
+ rs = RegStack(config=cfg)
138
+ await rs.install_schema()
139
+ user = await rs.users.create(
140
+ BaseUser(email="a@b.com", hashed_password="h", is_verified=True)
141
+ )
142
+ assert user.id
143
+ await rs.aclose()
144
+ print("sqlite ok")
145
+
146
+ asyncio.run(main())
147
+ PY
@@ -0,0 +1,42 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project are documented here. The
4
+ authoritative copy lives at
5
+ [`docs/changelog.md`](docs/changelog.md) and is rendered into the
6
+ Sphinx docs.
7
+
8
+ ## 0.2.1 — 2026-04-28
9
+
10
+ Hotfix for 0.2.0: `import regstack` failed on a base install because
11
+ several modules in the import path (`models/_objectid.py`,
12
+ `backends/protocols.py`, four routers, and the SQL `mfa_code_repo`)
13
+ had unconditional `from bson …` / `from regstack.backends.mongo …`
14
+ imports — but `pymongo` became an optional `mongo` extra in 0.2.0.
15
+ Added a CI smoketest that builds the wheel and imports it in a
16
+ no-extras venv, plus an in-process regression test that blocks `bson`
17
+ / `pymongo` via `sys.meta_path`.
18
+
19
+ ## 0.2.0 — 2026-04-28
20
+
21
+ Multi-backend support — SQLite (default), Postgres, MongoDB — switched
22
+ by `database_url` URL scheme. Bundled Alembic migrations for SQL
23
+ backends. Embedding API change: `RegStack(config=, db=)` →
24
+ `RegStack(config=, backend=None)`. README + core docs rewritten for
25
+ less-expert readers (problem framing, hyperlinks to external
26
+ standards, comparison vs Auth0/Clerk/Keycloak/fastapi-users).
27
+
28
+ See [`docs/changelog.md`](docs/changelog.md) for the full per-feature
29
+ breakdown.
30
+
31
+ ## 0.1.1 — 2026-04-27
32
+
33
+ - Rewrite README relative links as absolute URLs so they resolve on the
34
+ PyPI project page. README-only release.
35
+
36
+ ## 0.1.0 — 2026-04-27
37
+
38
+ First tagged release. Bundles M1–M6 from the development plan into a
39
+ single Apache-2.0 package on PyPI.
40
+
41
+ See [`docs/changelog.md`](docs/changelog.md) for the per-milestone
42
+ breakdown of M1 through M6.
@@ -287,6 +287,30 @@ collections.** The full suite must pass under `pytest -n auto` reliably —
287
287
  flaky tests are bugs, not noise. Time-dependent assertions use the
288
288
  `frozen_clock` fixture, never `time.sleep` or wall-clock delays.
289
289
 
290
+ ### A test run that doesn't cover every backend is a failing test
291
+
292
+ The whole point of the SQL + Mongo abstraction is parity. **Reporting
293
+ "all tests passed" when the run only exercised one or two backends is a
294
+ lie.** Treat any partial-backend run as red until it's been re-run
295
+ across the full matrix.
296
+
297
+ Concretely:
298
+
299
+ - `inv test-sqlite` is fine for tight inner-loop iteration, but it is
300
+ **not** a release gate.
301
+ - Before declaring a feature done — and definitely before merging /
302
+ tagging — run `inv test-all`, which exercises sqlite + mongo +
303
+ postgres in one go. Use `inv db-up` first to bring the local services
304
+ up if they aren't already (`inv db-status` reports what's running).
305
+ - The CI matrix runs all three backends on every push. A green local
306
+ `inv test-sqlite` followed by a CI failure on `mongo` or `postgres`
307
+ is the failure pattern this rule exists to prevent.
308
+ - A test that's `mongo`-only or `sqlite`-only by design must
309
+ short-circuit the parametrized fixture (see how
310
+ `tests/integration/test_indexes.py` does it with a local
311
+ `backend_kind = "mongo"` fixture override). Skipping silently because
312
+ a service isn't running doesn't count.
313
+
290
314
  ## Conventions
291
315
 
292
316
  - **Python 3.11+.** Use `from __future__ import annotations` in every module.
@@ -0,0 +1,272 @@
1
+ Metadata-Version: 2.4
2
+ Name: regstack
3
+ Version: 0.2.1
4
+ Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
5
+ Project-URL: Homepage, https://github.com/jdrumgoole/regstack
6
+ Project-URL: Repository, https://github.com/jdrumgoole/regstack
7
+ Author-email: Joe Drumgoole <joe@joedrumgoole.com>
8
+ License-Expression: Apache-2.0
9
+ License-File: LICENSE
10
+ License-File: NOTICE
11
+ Keywords: auth,fastapi,mongodb,registration,users
12
+ Classifier: Development Status :: 3 - Alpha
13
+ Classifier: Framework :: FastAPI
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Programming Language :: Python :: 3.11
16
+ Classifier: Programming Language :: Python :: 3.12
17
+ Classifier: Topic :: Software Development :: Libraries :: Python Modules
18
+ Requires-Python: >=3.11
19
+ Requires-Dist: aiosmtplib>=3.0
20
+ Requires-Dist: aiosqlite>=0.20
21
+ Requires-Dist: alembic>=1.13
22
+ Requires-Dist: click>=8.1
23
+ Requires-Dist: dnspython>=2.6
24
+ Requires-Dist: email-validator>=2.1
25
+ Requires-Dist: fastapi>=0.110
26
+ Requires-Dist: jinja2>=3.1
27
+ Requires-Dist: pwdlib[argon2]>=0.2.1
28
+ Requires-Dist: pydantic-settings>=2.2
29
+ Requires-Dist: pydantic>=2.6
30
+ Requires-Dist: pyjwt>=2.8
31
+ Requires-Dist: python-multipart>=0.0.9
32
+ Requires-Dist: sqlalchemy[asyncio]>=2.0
33
+ Provides-Extra: dev
34
+ Requires-Dist: anyio>=4.3; extra == 'dev'
35
+ Requires-Dist: asyncpg>=0.29; extra == 'dev'
36
+ Requires-Dist: httpx>=0.27; extra == 'dev'
37
+ Requires-Dist: invoke>=2.2; extra == 'dev'
38
+ Requires-Dist: mypy>=1.10; extra == 'dev'
39
+ Requires-Dist: pymongo>=4.9; extra == 'dev'
40
+ Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
41
+ Requires-Dist: pytest-cov>=5.0; extra == 'dev'
42
+ Requires-Dist: pytest-xdist>=3.5; extra == 'dev'
43
+ Requires-Dist: pytest>=8.0; extra == 'dev'
44
+ Requires-Dist: ruff>=0.4; extra == 'dev'
45
+ Requires-Dist: uvicorn[standard]>=0.29; extra == 'dev'
46
+ Provides-Extra: docs
47
+ Requires-Dist: furo>=2024.1; extra == 'docs'
48
+ Requires-Dist: myst-parser>=3.0; extra == 'docs'
49
+ Requires-Dist: sphinx-autobuild>=2024.4; extra == 'docs'
50
+ Requires-Dist: sphinx-autodoc-typehints>=2.0; extra == 'docs'
51
+ Requires-Dist: sphinx-copybutton>=0.5; extra == 'docs'
52
+ Requires-Dist: sphinx>=7.3; extra == 'docs'
53
+ Provides-Extra: mongo
54
+ Requires-Dist: pymongo>=4.9; extra == 'mongo'
55
+ Provides-Extra: postgres
56
+ Requires-Dist: asyncpg>=0.29; extra == 'postgres'
57
+ Provides-Extra: ses
58
+ Requires-Dist: aioboto3>=12.3; extra == 'ses'
59
+ Provides-Extra: sns
60
+ Requires-Dist: aioboto3>=12.3; extra == 'sns'
61
+ Provides-Extra: twilio
62
+ Requires-Dist: twilio>=9.0; extra == 'twilio'
63
+ Description-Content-Type: text/markdown
64
+
65
+ # regstack
66
+
67
+ [![CI](https://github.com/jdrumgoole/regstack/actions/workflows/test.yml/badge.svg)](https://github.com/jdrumgoole/regstack/actions/workflows/test.yml)
68
+ [![Python 3.11+](https://img.shields.io/badge/python-3.11%2B-blue.svg)](https://www.python.org/)
69
+ [![FastAPI](https://img.shields.io/badge/built%20with-FastAPI-009688.svg)](https://fastapi.tiangolo.com/)
70
+ [![License: Apache 2.0](https://img.shields.io/badge/license-Apache%202.0-green.svg)](https://github.com/jdrumgoole/regstack/blob/main/LICENSE)
71
+
72
+ **Production-grade user accounts for your [FastAPI](https://fastapi.tiangolo.com/)
73
+ app — without the vendor lock-in, the second service to run, or the homegrown
74
+ auth bugs.**
75
+
76
+ `pip install regstack`, point it at SQLite (default), [PostgreSQL](https://www.postgresql.org/),
77
+ or [MongoDB](https://www.mongodb.com/), and you have register / login /
78
+ verify-email / reset-password / change-email / delete-account / optional SMS
79
+ two-factor / admin endpoints / themable HTML pages — all behind a small Python
80
+ API and one config file.
81
+
82
+ 📚 **Docs:** <https://regstack.readthedocs.io>
83
+ &nbsp;·&nbsp;
84
+ 🧪 **Try it:** [`examples/sqlite`](https://github.com/jdrumgoole/regstack/tree/main/examples/sqlite)
85
+ &nbsp;·&nbsp;
86
+ 🛡️ **Security model:** [security guide](https://regstack.readthedocs.io/en/latest/security.html)
87
+
88
+ ---
89
+
90
+ ## The problem regstack solves
91
+
92
+ Every web application that has users eventually needs the same dozen
93
+ endpoints: register, log in, log out, verify email, reset a forgotten
94
+ password, change password, change email, delete account, list users for
95
+ the admin panel, lock out brute-force attackers, and ideally a second
96
+ factor. Every one of those endpoints has a well-known way to get
97
+ subtly wrong:
98
+
99
+ - **Password hashing.** Use [Argon2](https://en.wikipedia.org/wiki/Argon2)
100
+ (the [PHC winner](https://www.password-hashing.net/)), not MD5, SHA-1,
101
+ bcrypt-without-pepper, or — somehow still common — plain text.
102
+ - **Token revocation.** A [JWT](https://datatracker.ietf.org/doc/html/rfc7519)
103
+ is signed and self-contained: the server can't "log it out" unless you
104
+ build a revocation list. Forget this and a stolen token works until it
105
+ expires.
106
+ - **Account enumeration.** A login or password-reset endpoint that
107
+ responds differently for "user exists" vs "user doesn't" lets an
108
+ attacker harvest your customer list. See
109
+ [OWASP WSTG-IDNT-04](https://owasp.org/www-project-web-security-testing-guide/v42/4-Web_Application_Security_Testing/03-Identity_Management_Testing/04-Testing_for_Account_Enumeration_and_Guessable_User_Account).
110
+ - **Bulk session invalidation.** When a user changes their password
111
+ because they think they were compromised, every existing token they
112
+ hold should stop working immediately. Most homegrown JWT layers don't
113
+ do this.
114
+ - **One-time tokens.** Verification and password-reset tokens should be
115
+ random, hashed at rest, single-use, and expire fast. Storing the raw
116
+ token in the database is a "now your DB backup is also a credential
117
+ dump" mistake.
118
+ - **Phone numbers.** SMS codes need [E.164](https://en.wikipedia.org/wiki/E.164)-validated
119
+ numbers, attempt limits, and an upstream provider. Wiring all of that
120
+ yourself for a single feature is rarely worth it.
121
+
122
+ Doing all of these correctly, with tests, is two to four weeks of
123
+ engineering for a competent team. Doing them once and embedding the
124
+ result everywhere is what regstack is for.
125
+
126
+ ## What you get
127
+
128
+ ```
129
+ ✔ Email + password registration with email verification
130
+ ✔ JWT login (RFC 7519) with per-token revoke AND bulk revoke
131
+ ✔ Forgot / reset password — anti-enumeration: identical responses
132
+ ✔ Change password (revokes old tokens) / change email (re-verify)
133
+ ✔ Delete account
134
+ ✔ Optional SMS two-factor (TOTP-style 6-digit codes over SMS)
135
+ ✔ Server-side login lockout (HTTP 429 + Retry-After)
136
+ ✔ Admin endpoints (list / disable / delete users, stats)
137
+ ✔ Server-rendered HTML pages, theme with one CSS file
138
+ ✔ Pluggable email (console / SMTP / Amazon SES) and SMS (Amazon SNS / Twilio)
139
+ ✔ Argon2 password hashing, CSP-friendly templates
140
+ ✔ Setup wizard (`regstack init`) and config validator (`regstack doctor`)
141
+ ✔ Three storage backends: SQLite, PostgreSQL, MongoDB — chosen by URL
142
+ ```
143
+
144
+ Every feature is opt-in. Mount only the JSON router for a headless
145
+ backend; flip `enable_ui_router` to also get the bundled SSR pages.
146
+ Skip the SMS extras and you don't pull `twilio` or `aioboto3`.
147
+
148
+ ## Why not just use…?
149
+
150
+ There are real alternatives. Here's why regstack might still be the
151
+ right call.
152
+
153
+ | Alternative | Why you might pick it | Why you might pick regstack instead |
154
+ |---|---|---|
155
+ | **[Auth0](https://auth0.com/) / [Clerk](https://clerk.com/) / [WorkOS](https://workos.com/) / [Stytch](https://stytch.com/)** (hosted SaaS) | Zero ops. Polished UI. Enterprise SSO out of the box. | Cost scales per-user. Your auth lives on someone else's servers. Your customer list is in their database. Vendor lock-in is real and migrations are painful. |
156
+ | **[Keycloak](https://www.keycloak.org/) / [Authentik](https://goauthentik.io/) / [Authelia](https://www.authelia.com/) / [Ory Kratos](https://www.ory.sh/kratos/)** (self-hosted IAM) | Full identity platform. SAML, OIDC, federation. | A separate Java/Go service to run, monitor, back up, upgrade, and reason about. Heavyweight for "let users sign up". Schema lives outside your app. |
157
+ | **[fastapi-users](https://fastapi-users.github.io/fastapi-users/)** | Same language, same framework. Good registration / login primitives. | Doesn't ship verification flows, anti-enumeration, bulk revoke, SMS MFA, admin endpoints, or themable pages — you build those. regstack is the longer tail. |
158
+ | **Roll your own** | Total control. No dependency to learn. | You re-solve every bullet from "The problem" above, including the ones you didn't know existed yet. Two to four engineering weeks, then forever to maintain. |
159
+
160
+ regstack's bet is that for most FastAPI apps the right answer is
161
+ **embed a small Python library that owns the boring 80% correctly, and
162
+ keep the user table in your own database** — not "stand up a separate
163
+ auth product" and not "write the boring 80% from scratch each time".
164
+
165
+ ## 30-second start
166
+
167
+ ```bash
168
+ git clone https://github.com/jdrumgoole/regstack && cd regstack
169
+ uv sync --extra dev
170
+
171
+ # Generate a JWT signing secret. SQLite is the default backend, no DB to install.
172
+ export REGSTACK_JWT_SECRET=$(python -c 'import secrets; print(secrets.token_urlsafe(64))')
173
+
174
+ uv run uvicorn examples.sqlite.main:app --reload
175
+ ```
176
+
177
+ Then visit <http://localhost:8000/account/login> in your browser, or
178
+ register from the command line:
179
+
180
+ ```bash
181
+ curl -X POST http://localhost:8000/api/auth/register \
182
+ -H 'content-type: application/json' \
183
+ -d '{"email":"alice@example.com","password":"hunter2hunter2","full_name":"Alice"}'
184
+ ```
185
+
186
+ The bundled example serves themed SSR pages at `/account/*`, prints
187
+ verification / reset links and SMS codes to stdout (the `console`
188
+ email/SMS backends), and shows how a host overrides regstack's default
189
+ look by serving its own `theme.css`.
190
+
191
+ Want PostgreSQL or MongoDB instead? Set `REGSTACK_DATABASE_URL` to a
192
+ `postgresql+asyncpg://...` or `mongodb://...` URL and install the matching
193
+ extra (`uv sync --extra postgres` or `uv sync --extra mongo`). The
194
+ schema is created on first boot.
195
+
196
+ ## Embed in your own app
197
+
198
+ ```python
199
+ from contextlib import asynccontextmanager
200
+
201
+ from fastapi import FastAPI
202
+
203
+ from regstack import RegStack, RegStackConfig
204
+
205
+
206
+ config = RegStackConfig.load() # env vars + regstack.toml
207
+ regstack = RegStack(config=config)
208
+
209
+
210
+ @asynccontextmanager
211
+ async def lifespan(app: FastAPI):
212
+ await regstack.install_schema() # idempotent: runs migrations / creates indexes
213
+ yield
214
+ await regstack.aclose()
215
+
216
+
217
+ app = FastAPI(lifespan=lifespan)
218
+ app.include_router(regstack.router, prefix=config.api_prefix)
219
+ app.include_router(regstack.ui_router, prefix=config.ui_prefix) # optional
220
+ app.mount(config.static_prefix, regstack.static_files) # optional
221
+ ```
222
+
223
+ That is the whole integration. The rest of the surface area —
224
+ extending the user model, registering hooks (`user_registered`,
225
+ `password_reset`, …), supplying your own email service — is in the
226
+ [embedding guide](https://regstack.readthedocs.io/en/latest/embedding.html).
227
+
228
+ ## Documentation
229
+
230
+ | Page | What's there |
231
+ |---|---|
232
+ | [Quickstart](https://regstack.readthedocs.io/en/latest/quickstart.html) | Install, wizard, minimal embed |
233
+ | [Configuration](https://regstack.readthedocs.io/en/latest/configuration.html) | Every `RegStackConfig` field, env vars, TOML layout |
234
+ | [Architecture](https://regstack.readthedocs.io/en/latest/architecture.html) | Façade, backends, repos, hooks, lifecycle |
235
+ | [Security model](https://regstack.readthedocs.io/en/latest/security.html) | Threat model, JWT scheme, anti-enumeration, MFA |
236
+ | [Embedding](https://regstack.readthedocs.io/en/latest/embedding.html) | Custom backends, hooks, multi-tenant |
237
+ | [Theming](https://regstack.readthedocs.io/en/latest/theming.html) | CSS variables, template overrides |
238
+ | [CLI](https://regstack.readthedocs.io/en/latest/cli.html) | `init`, `create-admin`, `doctor` |
239
+ | [API reference](https://regstack.readthedocs.io/en/latest/api.html) | Public types, generated from source |
240
+
241
+ The same docs are also browsable as Markdown in [`docs/`](https://github.com/jdrumgoole/regstack/tree/main/docs).
242
+
243
+ ## Status
244
+
245
+ Alpha. Single-file SQLite is the default and runs with no infrastructure;
246
+ PostgreSQL and MongoDB backends pass the same parametrized integration
247
+ suite. The next tagged release is `v0.2.0`. See the
248
+ [changelog](https://regstack.readthedocs.io/en/latest/changelog.html)
249
+ for the per-milestone breakdown.
250
+
251
+ ## Contributing
252
+
253
+ Issues and pull requests welcome at
254
+ <https://github.com/jdrumgoole/regstack>. Before opening a PR, please
255
+ run the test suite and the linter — both should be green:
256
+
257
+ ```bash
258
+ uv sync --extra dev
259
+ uv run python -m invoke test-all # SQLite + Mongo + Postgres in parallel
260
+ uv run python -m invoke lint # ruff + format check + mypy
261
+ ```
262
+
263
+ `invoke test-sqlite` is the fast inner-loop variant that needs no
264
+ database services. `invoke test-all` is what CI runs and what gates a
265
+ release. Each pytest-xdist worker isolates its own database, so the
266
+ full suite is safe to re-run while you iterate.
267
+
268
+ Security disclosures: see [SECURITY.md](https://github.com/jdrumgoole/regstack/blob/main/SECURITY.md).
269
+
270
+ ## License
271
+
272
+ [Apache License 2.0](https://github.com/jdrumgoole/regstack/blob/main/LICENSE) © 2026 Joe Drumgoole.