regstack 0.2.0__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 (175) hide show
  1. regstack-0.2.1/.github/workflows/test.yml +147 -0
  2. {regstack-0.2.0 → regstack-0.2.1}/CHANGELOG.md +11 -0
  3. {regstack-0.2.0 → regstack-0.2.1}/PKG-INFO +1 -1
  4. {regstack-0.2.0 → regstack-0.2.1}/docs/changelog.md +30 -0
  5. {regstack-0.2.0 → regstack-0.2.1}/pyproject.toml +1 -1
  6. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +2 -14
  7. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/repositories/pending_repo.py +2 -2
  8. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/repositories/user_repo.py +2 -2
  9. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/protocols.py +24 -6
  10. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/repositories/mfa_code_repo.py +1 -4
  11. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/models/_objectid.py +10 -2
  12. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/account.py +1 -1
  13. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/login.py +1 -1
  14. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/phone.py +1 -1
  15. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/register.py +1 -2
  16. regstack-0.2.1/src/regstack/version.py +1 -0
  17. regstack-0.2.1/tests/unit/test_base_install_imports.py +69 -0
  18. {regstack-0.2.0 → regstack-0.2.1}/uv.lock +1 -1
  19. regstack-0.2.0/.github/workflows/test.yml +0 -86
  20. regstack-0.2.0/src/regstack/version.py +0 -1
  21. {regstack-0.2.0 → regstack-0.2.1}/.github/workflows/publish.yml +0 -0
  22. {regstack-0.2.0 → regstack-0.2.1}/.gitignore +0 -0
  23. {regstack-0.2.0 → regstack-0.2.1}/.python-version +0 -0
  24. {regstack-0.2.0 → regstack-0.2.1}/.readthedocs.yaml +0 -0
  25. {regstack-0.2.0 → regstack-0.2.1}/CLAUDE.md +0 -0
  26. {regstack-0.2.0 → regstack-0.2.1}/LICENSE +0 -0
  27. {regstack-0.2.0 → regstack-0.2.1}/NOTICE +0 -0
  28. {regstack-0.2.0 → regstack-0.2.1}/README.md +0 -0
  29. {regstack-0.2.0 → regstack-0.2.1}/SECURITY.md +0 -0
  30. {regstack-0.2.0 → regstack-0.2.1}/docs/_static/.gitkeep +0 -0
  31. {regstack-0.2.0 → regstack-0.2.1}/docs/_templates/.gitkeep +0 -0
  32. {regstack-0.2.0 → regstack-0.2.1}/docs/api.md +0 -0
  33. {regstack-0.2.0 → regstack-0.2.1}/docs/architecture.md +0 -0
  34. {regstack-0.2.0 → regstack-0.2.1}/docs/cli.md +0 -0
  35. {regstack-0.2.0 → regstack-0.2.1}/docs/conf.py +0 -0
  36. {regstack-0.2.0 → regstack-0.2.1}/docs/configuration.md +0 -0
  37. {regstack-0.2.0 → regstack-0.2.1}/docs/embedding.md +0 -0
  38. {regstack-0.2.0 → regstack-0.2.1}/docs/index.md +0 -0
  39. {regstack-0.2.0 → regstack-0.2.1}/docs/quickstart.md +0 -0
  40. {regstack-0.2.0 → regstack-0.2.1}/docs/security.md +0 -0
  41. {regstack-0.2.0 → regstack-0.2.1}/docs/theming.md +0 -0
  42. {regstack-0.2.0 → regstack-0.2.1}/examples/_common/__init__.py +0 -0
  43. {regstack-0.2.0 → regstack-0.2.1}/examples/_common/app.py +0 -0
  44. {regstack-0.2.0 → regstack-0.2.1}/examples/mongo/README.md +0 -0
  45. {regstack-0.2.0 → regstack-0.2.1}/examples/mongo/branding/theme.css +0 -0
  46. {regstack-0.2.0 → regstack-0.2.1}/examples/mongo/main.py +0 -0
  47. {regstack-0.2.0 → regstack-0.2.1}/examples/mongo/regstack.toml +0 -0
  48. {regstack-0.2.0 → regstack-0.2.1}/examples/postgres/README.md +0 -0
  49. {regstack-0.2.0 → regstack-0.2.1}/examples/postgres/main.py +0 -0
  50. {regstack-0.2.0 → regstack-0.2.1}/examples/postgres/regstack.toml +0 -0
  51. {regstack-0.2.0 → regstack-0.2.1}/examples/sqlite/README.md +0 -0
  52. {regstack-0.2.0 → regstack-0.2.1}/examples/sqlite/main.py +0 -0
  53. {regstack-0.2.0 → regstack-0.2.1}/examples/sqlite/regstack.toml +0 -0
  54. {regstack-0.2.0 → regstack-0.2.1}/regstack.toml.example +0 -0
  55. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/__init__.py +0 -0
  56. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/app.py +0 -0
  57. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/auth/__init__.py +0 -0
  58. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/auth/clock.py +0 -0
  59. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/auth/dependencies.py +0 -0
  60. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/auth/jwt.py +0 -0
  61. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/auth/lockout.py +0 -0
  62. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/auth/mfa.py +0 -0
  63. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/auth/password.py +0 -0
  64. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/auth/tokens.py +0 -0
  65. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/__init__.py +0 -0
  66. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/base.py +0 -0
  67. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/factory.py +0 -0
  68. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/__init__.py +0 -0
  69. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/backend.py +0 -0
  70. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/client.py +0 -0
  71. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/indexes.py +0 -0
  72. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  73. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
  74. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
  75. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/__init__.py +0 -0
  76. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/backend.py +0 -0
  77. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/migrations/__init__.py +0 -0
  78. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/migrations/env.py +0 -0
  79. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  80. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  81. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  82. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  83. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  84. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
  85. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
  86. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/schema.py +0 -0
  87. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/backends/sql/types.py +0 -0
  88. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/cli/__init__.py +0 -0
  89. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/cli/__main__.py +0 -0
  90. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/cli/_runtime.py +0 -0
  91. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/cli/admin.py +0 -0
  92. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/cli/doctor.py +0 -0
  93. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/cli/init.py +0 -0
  94. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/cli/migrate.py +0 -0
  95. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/config/__init__.py +0 -0
  96. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/config/loader.py +0 -0
  97. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/config/schema.py +0 -0
  98. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/config/secrets.py +0 -0
  99. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/__init__.py +0 -0
  100. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/base.py +0 -0
  101. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/composer.py +0 -0
  102. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/console.py +0 -0
  103. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/factory.py +0 -0
  104. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/ses.py +0 -0
  105. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/smtp.py +0 -0
  106. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/email_change.html +0 -0
  107. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/email_change.subject.txt +0 -0
  108. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/email_change.txt +0 -0
  109. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/password_reset.html +0 -0
  110. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  111. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/password_reset.txt +0 -0
  112. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  113. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  114. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/verification.html +0 -0
  115. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/verification.subject.txt +0 -0
  116. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/email/templates/verification.txt +0 -0
  117. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/hooks/__init__.py +0 -0
  118. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/hooks/events.py +0 -0
  119. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/models/__init__.py +0 -0
  120. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/models/login_attempt.py +0 -0
  121. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/models/mfa_code.py +0 -0
  122. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/models/pending_registration.py +0 -0
  123. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/models/user.py +0 -0
  124. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/__init__.py +0 -0
  125. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/_schemas.py +0 -0
  126. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/admin.py +0 -0
  127. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/logout.py +0 -0
  128. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/password.py +0 -0
  129. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/routers/verify.py +0 -0
  130. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/sms/__init__.py +0 -0
  131. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/sms/base.py +0 -0
  132. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/sms/factory.py +0 -0
  133. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/sms/null.py +0 -0
  134. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/sms/sns.py +0 -0
  135. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/sms/twilio.py +0 -0
  136. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/__init__.py +0 -0
  137. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/pages.py +0 -0
  138. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/static/css/core.css +0 -0
  139. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/static/css/theme.css +0 -0
  140. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/static/js/regstack.js +0 -0
  141. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  142. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/templates/auth/forgot.html +0 -0
  143. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/templates/auth/login.html +0 -0
  144. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/templates/auth/me.html +0 -0
  145. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  146. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/templates/auth/register.html +0 -0
  147. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/templates/auth/reset.html +0 -0
  148. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/templates/auth/verify.html +0 -0
  149. {regstack-0.2.0 → regstack-0.2.1}/src/regstack/ui/templates/base.html +0 -0
  150. {regstack-0.2.0 → regstack-0.2.1}/tasks.py +0 -0
  151. {regstack-0.2.0 → regstack-0.2.1}/tests/__init__.py +0 -0
  152. {regstack-0.2.0 → regstack-0.2.1}/tests/conftest.py +0 -0
  153. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/__init__.py +0 -0
  154. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_account_management.py +0 -0
  155. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_admin_router.py +0 -0
  156. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_happy_path.py +0 -0
  157. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_indexes.py +0 -0
  158. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_login_lockout.py +0 -0
  159. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_mfa.py +0 -0
  160. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_password_reset.py +0 -0
  161. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_sql_migrations.py +0 -0
  162. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_ui_router.py +0 -0
  163. {regstack-0.2.0 → regstack-0.2.1}/tests/integration/test_verification.py +0 -0
  164. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/__init__.py +0 -0
  165. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_cli.py +0 -0
  166. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_config_loader.py +0 -0
  167. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_jwt.py +0 -0
  168. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_lockout.py +0 -0
  169. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_mail_composer.py +0 -0
  170. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_mfa_code_repo.py +0 -0
  171. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_password.py +0 -0
  172. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_ses_backend.py +0 -0
  173. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_sms.py +0 -0
  174. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_smtp_backend.py +0 -0
  175. {regstack-0.2.0 → regstack-0.2.1}/tests/unit/test_ui_env.py +0 -0
@@ -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
@@ -5,6 +5,17 @@ authoritative copy lives at
5
5
  [`docs/changelog.md`](docs/changelog.md) and is rendered into the
6
6
  Sphinx docs.
7
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
+
8
19
  ## 0.2.0 — 2026-04-28
9
20
 
10
21
  Multi-backend support — SQLite (default), Postgres, MongoDB — switched
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regstack
3
- Version: 0.2.0
3
+ Version: 0.2.1
4
4
  Summary: Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB.
5
5
  Project-URL: Homepage, https://github.com/jdrumgoole/regstack
6
6
  Project-URL: Repository, https://github.com/jdrumgoole/regstack
@@ -3,6 +3,36 @@
3
3
  All notable changes to this project are documented here. Versions follow
4
4
  [Semantic Versioning](https://semver.org/) once `1.0.0` ships.
5
5
 
6
+ ## 0.2.1 — 2026-04-28
7
+
8
+ **Hotfix for 0.2.0.** `import regstack` was broken on any install that
9
+ didn't include the new `mongo` extra: `models/_objectid.py` imported
10
+ `bson` unconditionally, and four routers + the SQL MFA repo imported
11
+ shared error / enum types out of `regstack.backends.mongo.*`, which
12
+ in turn imports `pymongo` at module top level.
13
+
14
+ ### Fixed
15
+
16
+ - `models/_objectid.py` now imports `bson.ObjectId` lazily inside a
17
+ `try / except ImportError` and only uses it for `isinstance` checks
18
+ when present.
19
+ - `UserAlreadyExistsError`, `PendingAlreadyExistsError`,
20
+ `MfaVerifyOutcome`, and `MfaVerifyResult` moved from their backend
21
+ modules to `regstack.backends.protocols` (the backend-agnostic
22
+ location). Mongo modules re-export them for backwards compatibility.
23
+ - All consumer modules (`routers/register.py`, `routers/account.py`,
24
+ `routers/login.py`, `routers/phone.py`, the SQL MFA repo) updated to
25
+ import from `regstack.backends.protocols`.
26
+
27
+ ### Added
28
+
29
+ - New `base-install-smoketest` CI job: builds the wheel and runs
30
+ `import regstack` + a SQLite end-to-end RegStack lifecycle in a
31
+ fresh venv with **no extras**. Will catch any future regression.
32
+ - New `tests/unit/test_base_install_imports.py` regression test that
33
+ uses `sys.meta_path` to block `bson` / `pymongo` and confirm
34
+ `import regstack` still succeeds.
35
+
6
36
  ## 0.2.0 — 2026-04-28
7
37
 
8
38
  **Multi-backend support + Alembic migrations.** SQLite is now the
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regstack"
3
- version = "0.2.0"
3
+ version = "0.2.1"
4
4
  description = "Embeddable user registration, login, and account management for FastAPI apps. SQLite / Postgres / MongoDB."
5
5
  readme = "README.md"
6
6
  requires-python = ">=3.11"
@@ -1,11 +1,10 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass
4
3
  from datetime import datetime
5
- from enum import StrEnum
6
4
  from typing import TYPE_CHECKING
7
5
 
8
6
  from regstack.auth.tokens import hash_token
7
+ from regstack.backends.protocols import MfaVerifyOutcome, MfaVerifyResult
9
8
  from regstack.models.mfa_code import MfaCode, MfaKind
10
9
 
11
10
  if TYPE_CHECKING:
@@ -14,18 +13,7 @@ if TYPE_CHECKING:
14
13
  from regstack.auth.clock import Clock
15
14
 
16
15
 
17
- class MfaVerifyOutcome(StrEnum):
18
- OK = "ok"
19
- WRONG = "wrong"
20
- EXPIRED = "expired"
21
- LOCKED = "locked"
22
- MISSING = "missing"
23
-
24
-
25
- @dataclass(slots=True, frozen=True)
26
- class MfaVerifyResult:
27
- outcome: MfaVerifyOutcome
28
- attempts_remaining: int = 0
16
+ __all__ = ["MfaCodeRepo", "MfaVerifyOutcome", "MfaVerifyResult"]
29
17
 
30
18
 
31
19
  class MfaCodeRepo:
@@ -6,14 +6,14 @@ from typing import TYPE_CHECKING, Any
6
6
  from bson import ObjectId
7
7
  from pymongo.errors import DuplicateKeyError
8
8
 
9
+ from regstack.backends.protocols import PendingAlreadyExistsError
9
10
  from regstack.models.pending_registration import PendingRegistration
10
11
 
11
12
  if TYPE_CHECKING:
12
13
  from pymongo.asynchronous.database import AsyncDatabase
13
14
 
14
15
 
15
- class PendingAlreadyExistsError(Exception):
16
- """A pending registration with this email already exists."""
16
+ __all__ = ["PendingAlreadyExistsError", "PendingRepo"]
17
17
 
18
18
 
19
19
  class PendingRepo:
@@ -7,14 +7,14 @@ from bson import ObjectId
7
7
  from pymongo.errors import DuplicateKeyError
8
8
 
9
9
  from regstack.auth.clock import Clock, SystemClock
10
+ from regstack.backends.protocols import UserAlreadyExistsError
10
11
  from regstack.models.user import BaseUser
11
12
 
12
13
  if TYPE_CHECKING:
13
14
  from pymongo.asynchronous.database import AsyncDatabase
14
15
 
15
16
 
16
- class UserAlreadyExistsError(Exception):
17
- """Raised when an attempt is made to insert a user with a duplicate email."""
17
+ __all__ = ["UserAlreadyExistsError", "UserRepo"]
18
18
 
19
19
 
20
20
  def _bulk_revoke_cutoff(now: datetime) -> datetime:
@@ -11,7 +11,9 @@ or Mongo idiom feels "right" to the implementer.
11
11
 
12
12
  from __future__ import annotations
13
13
 
14
+ from dataclasses import dataclass
14
15
  from datetime import datetime, timedelta
16
+ from enum import StrEnum
15
17
  from typing import Protocol, runtime_checkable
16
18
 
17
19
  from regstack.models.mfa_code import MfaCode, MfaKind
@@ -19,6 +21,20 @@ from regstack.models.pending_registration import PendingRegistration
19
21
  from regstack.models.user import BaseUser
20
22
 
21
23
 
24
+ class MfaVerifyOutcome(StrEnum):
25
+ OK = "ok"
26
+ WRONG = "wrong"
27
+ EXPIRED = "expired"
28
+ LOCKED = "locked"
29
+ MISSING = "missing"
30
+
31
+
32
+ @dataclass(slots=True, frozen=True)
33
+ class MfaVerifyResult:
34
+ outcome: MfaVerifyOutcome
35
+ attempts_remaining: int = 0
36
+
37
+
22
38
  class UserAlreadyExistsError(Exception):
23
39
  """Raised when an attempt is made to insert a user with a duplicate email,
24
40
  or to set an email that another user already owns.
@@ -28,6 +44,14 @@ class UserAlreadyExistsError(Exception):
28
44
  """
29
45
 
30
46
 
47
+ class PendingAlreadyExistsError(Exception):
48
+ """A pending registration with this email already exists.
49
+
50
+ Backend-agnostic: each repo raises this on its own duplicate path so
51
+ callers can branch on the type without importing a backend module.
52
+ """
53
+
54
+
31
55
  @runtime_checkable
32
56
  class UserRepoProtocol(Protocol):
33
57
  async def create(self, user: BaseUser) -> BaseUser: ...
@@ -155,9 +179,3 @@ class MfaCodeRepoProtocol(Protocol):
155
179
  async def find(self, *, user_id: str, kind: MfaKind) -> MfaCode | None: ...
156
180
 
157
181
  async def purge_expired(self, now: datetime | None = None) -> int: ...
158
-
159
-
160
- # Re-exported here so callers don't need a deeper import. Lives in the
161
- # Mongo backend module because it predates the SQL backend; the dataclass
162
- # is backend-agnostic, only the import path is historical.
163
- from regstack.backends.mongo.repositories.mfa_code_repo import MfaVerifyResult # noqa: E402
@@ -6,10 +6,7 @@ from typing import TYPE_CHECKING
6
6
  from sqlalchemy import and_, delete, select, update
7
7
 
8
8
  from regstack.auth.tokens import hash_token
9
- from regstack.backends.mongo.repositories.mfa_code_repo import (
10
- MfaVerifyOutcome,
11
- MfaVerifyResult,
12
- )
9
+ from regstack.backends.protocols import MfaVerifyOutcome, MfaVerifyResult
13
10
  from regstack.backends.sql.schema import mfa_codes_table
14
11
  from regstack.models.mfa_code import MfaCode, MfaKind
15
12
 
@@ -17,10 +17,18 @@ from __future__ import annotations
17
17
 
18
18
  from typing import Annotated, Any
19
19
 
20
- from bson import ObjectId
21
20
  from pydantic import GetCoreSchemaHandler
22
21
  from pydantic_core import CoreSchema, core_schema
23
22
 
23
+ # bson is imported lazily so the package remains importable on a base
24
+ # install (the `mongo` extra is what pulls pymongo, and pymongo is the
25
+ # only consumer of bson). When the Mongo backend is in use, bson is
26
+ # always present.
27
+ try:
28
+ from bson import ObjectId as _BsonObjectId # type: ignore[import-not-found]
29
+ except ImportError: # pragma: no cover — exercised only without the mongo extra
30
+ _BsonObjectId = None # type: ignore[assignment,misc]
31
+
24
32
 
25
33
  class _IdValidator:
26
34
  @classmethod
@@ -28,7 +36,7 @@ class _IdValidator:
28
36
  cls, source_type: Any, handler: GetCoreSchemaHandler
29
37
  ) -> CoreSchema:
30
38
  def validate(value: Any) -> str:
31
- if isinstance(value, ObjectId):
39
+ if _BsonObjectId is not None and isinstance(value, _BsonObjectId):
32
40
  return str(value)
33
41
  if isinstance(value, str) and value:
34
42
  return value
@@ -7,7 +7,7 @@ from fastapi import APIRouter, Body, Depends, HTTPException, status
7
7
  from pydantic import BaseModel, ConfigDict, EmailStr, Field
8
8
 
9
9
  from regstack.auth.jwt import TokenError
10
- from regstack.backends.mongo.repositories.user_repo import UserAlreadyExistsError
10
+ from regstack.backends.protocols import UserAlreadyExistsError
11
11
  from regstack.config.secrets import derive_secret
12
12
  from regstack.models.user import BaseUser, UserPublic
13
13
  from regstack.routers._schemas import MessageResponse, PasswordStr
@@ -9,7 +9,7 @@ from pydantic import BaseModel, ConfigDict, Field
9
9
 
10
10
  from regstack.auth.jwt import TokenError
11
11
  from regstack.auth.mfa import generate_mfa_code
12
- from regstack.backends.mongo.repositories.mfa_code_repo import MfaVerifyOutcome
12
+ from regstack.backends.protocols import MfaVerifyOutcome
13
13
  from regstack.models.mfa_code import MfaCode
14
14
  from regstack.routers._schemas import LoginRequest, TokenResponse
15
15
  from regstack.sms.base import SmsMessage
@@ -8,7 +8,7 @@ from pydantic import BaseModel, ConfigDict, Field
8
8
 
9
9
  from regstack.auth.jwt import TokenError
10
10
  from regstack.auth.mfa import generate_mfa_code
11
- from regstack.backends.mongo.repositories.mfa_code_repo import MfaVerifyOutcome
11
+ from regstack.backends.protocols import MfaVerifyOutcome
12
12
  from regstack.models.mfa_code import MfaCode
13
13
  from regstack.models.user import BaseUser, UserPublic
14
14
  from regstack.routers._schemas import MessageResponse
@@ -6,8 +6,7 @@ from typing import TYPE_CHECKING
6
6
  from fastapi import APIRouter, HTTPException, status
7
7
 
8
8
  from regstack.auth.tokens import generate_verification_token
9
- from regstack.backends.mongo.repositories.pending_repo import PendingAlreadyExistsError
10
- from regstack.backends.mongo.repositories.user_repo import UserAlreadyExistsError
9
+ from regstack.backends.protocols import PendingAlreadyExistsError, UserAlreadyExistsError
11
10
  from regstack.models.pending_registration import PendingRegistration
12
11
  from regstack.models.user import BaseUser, UserCreate, UserPublic
13
12
  from regstack.routers._schemas import PendingResponse
@@ -0,0 +1 @@
1
+ __version__ = "0.2.1"
@@ -0,0 +1,69 @@
1
+ """Regression test: ``import regstack`` must not require optional extras.
2
+
3
+ regstack 0.2.0 shipped to PyPI broken because several modules in the
4
+ hot import path (`routers/account.py`, `routers/login.py`,
5
+ `models/_objectid.py`, …) had unconditional ``from bson …`` /
6
+ ``from pymongo …`` / ``from regstack.backends.mongo …`` statements.
7
+ On a base install (no ``mongo`` extra) ``import regstack`` raised
8
+ ``ModuleNotFoundError: No module named 'bson'``.
9
+
10
+ This test runs ``import regstack`` in a subprocess with ``bson`` and
11
+ ``pymongo`` blocked, so a future regression is caught even when the
12
+ test environment happens to have pymongo installed for other reasons.
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import subprocess
18
+ import sys
19
+ import textwrap
20
+
21
+
22
+ def test_import_regstack_without_pymongo() -> None:
23
+ """``import regstack`` and ``from regstack import RegStack, RegStackConfig``
24
+ must work on a base install — no optional extras.
25
+
26
+ We can't actually uninstall pymongo from the test venv, so we
27
+ simulate "not installed" by inserting a finder that raises
28
+ ImportError for ``bson`` and ``pymongo`` at the top of sys.meta_path.
29
+ """
30
+ program = textwrap.dedent(
31
+ """
32
+ import importlib.abc, importlib.machinery, sys
33
+
34
+ BLOCKED = {"bson", "pymongo"}
35
+
36
+ class BlockMongo(importlib.abc.MetaPathFinder):
37
+ def find_spec(self, name, path, target=None):
38
+ root = name.split(".", 1)[0]
39
+ if root in BLOCKED:
40
+ raise ModuleNotFoundError(f"No module named {name!r}")
41
+ return None
42
+
43
+ sys.meta_path.insert(0, BlockMongo())
44
+ # Drop anything pymongo-touching that may have been pre-imported
45
+ # by pytest's collection of sibling test modules.
46
+ for mod in list(sys.modules):
47
+ root = mod.split(".", 1)[0]
48
+ if root in BLOCKED or mod.startswith("regstack"):
49
+ del sys.modules[mod]
50
+
51
+ import regstack
52
+ from regstack import RegStack, RegStackConfig
53
+
54
+ assert RegStack.__module__ == "regstack.app"
55
+ assert RegStackConfig.__module__ == "regstack.config.schema"
56
+ print("ok", regstack.__version__)
57
+ """
58
+ )
59
+ result = subprocess.run(
60
+ [sys.executable, "-c", program],
61
+ capture_output=True,
62
+ text=True,
63
+ timeout=30,
64
+ )
65
+ assert result.returncode == 0, (
66
+ f"import regstack failed without pymongo:\nstdout={result.stdout!r}"
67
+ f"\nstderr={result.stderr!r}"
68
+ )
69
+ assert result.stdout.startswith("ok "), result.stdout
@@ -1978,7 +1978,7 @@ wheels = [
1978
1978
 
1979
1979
  [[package]]
1980
1980
  name = "regstack"
1981
- version = "0.2.0"
1981
+ version = "0.2.1"
1982
1982
  source = { editable = "." }
1983
1983
  dependencies = [
1984
1984
  { name = "aiosmtplib" },
@@ -1,86 +0,0 @@
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
@@ -1 +0,0 @@
1
- __version__ = "0.2.0"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes