regstack 0.2.2__tar.gz → 0.2.3__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 (183) hide show
  1. {regstack-0.2.2 → regstack-0.2.3}/CHANGELOG.md +10 -0
  2. {regstack-0.2.2 → regstack-0.2.3}/PKG-INFO +1 -1
  3. regstack-0.2.3/docs/api.md +278 -0
  4. {regstack-0.2.2 → regstack-0.2.3}/docs/changelog.md +28 -0
  5. {regstack-0.2.2 → regstack-0.2.3}/pyproject.toml +1 -1
  6. regstack-0.2.3/src/regstack/app.py +330 -0
  7. regstack-0.2.3/src/regstack/auth/clock.py +73 -0
  8. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/auth/dependencies.py +47 -5
  9. regstack-0.2.3/src/regstack/auth/jwt.py +256 -0
  10. regstack-0.2.3/src/regstack/auth/lockout.py +119 -0
  11. regstack-0.2.3/src/regstack/auth/password.py +65 -0
  12. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/protocols.py +31 -6
  13. regstack-0.2.3/src/regstack/email/base.py +71 -0
  14. regstack-0.2.3/src/regstack/hooks/events.py +98 -0
  15. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/__init__.py +21 -1
  16. regstack-0.2.3/src/regstack/sms/base.py +67 -0
  17. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/pages.py +49 -8
  18. regstack-0.2.3/src/regstack/version.py +1 -0
  19. {regstack-0.2.2 → regstack-0.2.3}/uv.lock +1 -1
  20. regstack-0.2.2/docs/api.md +0 -184
  21. regstack-0.2.2/src/regstack/app.py +0 -166
  22. regstack-0.2.2/src/regstack/auth/clock.py +0 -29
  23. regstack-0.2.2/src/regstack/auth/jwt.py +0 -145
  24. regstack-0.2.2/src/regstack/auth/lockout.py +0 -59
  25. regstack-0.2.2/src/regstack/auth/password.py +0 -20
  26. regstack-0.2.2/src/regstack/email/base.py +0 -23
  27. regstack-0.2.2/src/regstack/hooks/events.py +0 -59
  28. regstack-0.2.2/src/regstack/sms/base.py +0 -24
  29. regstack-0.2.2/src/regstack/version.py +0 -1
  30. {regstack-0.2.2 → regstack-0.2.3}/.github/workflows/publish.yml +0 -0
  31. {regstack-0.2.2 → regstack-0.2.3}/.github/workflows/test.yml +0 -0
  32. {regstack-0.2.2 → regstack-0.2.3}/.gitignore +0 -0
  33. {regstack-0.2.2 → regstack-0.2.3}/.python-version +0 -0
  34. {regstack-0.2.2 → regstack-0.2.3}/.readthedocs.yaml +0 -0
  35. {regstack-0.2.2 → regstack-0.2.3}/CLAUDE.md +0 -0
  36. {regstack-0.2.2 → regstack-0.2.3}/LICENSE +0 -0
  37. {regstack-0.2.2 → regstack-0.2.3}/NOTICE +0 -0
  38. {regstack-0.2.2 → regstack-0.2.3}/README.md +0 -0
  39. {regstack-0.2.2 → regstack-0.2.3}/SECURITY.md +0 -0
  40. {regstack-0.2.2 → regstack-0.2.3}/docs/_static/.gitkeep +0 -0
  41. {regstack-0.2.2 → regstack-0.2.3}/docs/_templates/.gitkeep +0 -0
  42. {regstack-0.2.2 → regstack-0.2.3}/docs/architecture.md +0 -0
  43. {regstack-0.2.2 → regstack-0.2.3}/docs/cli.md +0 -0
  44. {regstack-0.2.2 → regstack-0.2.3}/docs/conf.py +0 -0
  45. {regstack-0.2.2 → regstack-0.2.3}/docs/configuration.md +0 -0
  46. {regstack-0.2.2 → regstack-0.2.3}/docs/embedding.md +0 -0
  47. {regstack-0.2.2 → regstack-0.2.3}/docs/index.md +0 -0
  48. {regstack-0.2.2 → regstack-0.2.3}/docs/quickstart.md +0 -0
  49. {regstack-0.2.2 → regstack-0.2.3}/docs/security.md +0 -0
  50. {regstack-0.2.2 → regstack-0.2.3}/docs/theming.md +0 -0
  51. {regstack-0.2.2 → regstack-0.2.3}/examples/_common/__init__.py +0 -0
  52. {regstack-0.2.2 → regstack-0.2.3}/examples/_common/app.py +0 -0
  53. {regstack-0.2.2 → regstack-0.2.3}/examples/mongo/README.md +0 -0
  54. {regstack-0.2.2 → regstack-0.2.3}/examples/mongo/branding/theme.css +0 -0
  55. {regstack-0.2.2 → regstack-0.2.3}/examples/mongo/main.py +0 -0
  56. {regstack-0.2.2 → regstack-0.2.3}/examples/mongo/regstack.toml +0 -0
  57. {regstack-0.2.2 → regstack-0.2.3}/examples/postgres/README.md +0 -0
  58. {regstack-0.2.2 → regstack-0.2.3}/examples/postgres/main.py +0 -0
  59. {regstack-0.2.2 → regstack-0.2.3}/examples/postgres/regstack.toml +0 -0
  60. {regstack-0.2.2 → regstack-0.2.3}/examples/sqlite/README.md +0 -0
  61. {regstack-0.2.2 → regstack-0.2.3}/examples/sqlite/main.py +0 -0
  62. {regstack-0.2.2 → regstack-0.2.3}/examples/sqlite/regstack.toml +0 -0
  63. {regstack-0.2.2 → regstack-0.2.3}/regstack.toml.example +0 -0
  64. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/__init__.py +0 -0
  65. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/auth/__init__.py +0 -0
  66. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/auth/mfa.py +0 -0
  67. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/auth/tokens.py +0 -0
  68. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/__init__.py +0 -0
  69. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/base.py +0 -0
  70. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/factory.py +0 -0
  71. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/__init__.py +0 -0
  72. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/backend.py +0 -0
  73. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/client.py +0 -0
  74. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/indexes.py +0 -0
  75. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/__init__.py +0 -0
  76. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/blacklist_repo.py +0 -0
  77. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/login_attempt_repo.py +0 -0
  78. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/mfa_code_repo.py +0 -0
  79. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/pending_repo.py +0 -0
  80. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/mongo/repositories/user_repo.py +0 -0
  81. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/__init__.py +0 -0
  82. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/backend.py +0 -0
  83. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/migrations/__init__.py +0 -0
  84. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/migrations/env.py +0 -0
  85. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/migrations/script.py.mako +0 -0
  86. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/migrations/versions/0001_initial_schema.py +0 -0
  87. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/__init__.py +0 -0
  88. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/blacklist_repo.py +0 -0
  89. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/login_attempt_repo.py +0 -0
  90. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/mfa_code_repo.py +0 -0
  91. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/pending_repo.py +0 -0
  92. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/repositories/user_repo.py +0 -0
  93. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/schema.py +0 -0
  94. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/backends/sql/types.py +0 -0
  95. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/__init__.py +0 -0
  96. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/__main__.py +0 -0
  97. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/_runtime.py +0 -0
  98. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/admin.py +0 -0
  99. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/doctor.py +0 -0
  100. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/init.py +0 -0
  101. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/cli/migrate.py +0 -0
  102. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/config/__init__.py +0 -0
  103. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/config/loader.py +0 -0
  104. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/config/schema.py +0 -0
  105. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/config/secrets.py +0 -0
  106. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/__init__.py +0 -0
  107. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/composer.py +0 -0
  108. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/console.py +0 -0
  109. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/factory.py +0 -0
  110. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/ses.py +0 -0
  111. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/smtp.py +0 -0
  112. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/email_change.html +0 -0
  113. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/email_change.subject.txt +0 -0
  114. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/email_change.txt +0 -0
  115. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/password_reset.html +0 -0
  116. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/password_reset.subject.txt +0 -0
  117. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/password_reset.txt +0 -0
  118. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/sms_login_mfa.txt +0 -0
  119. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/sms_phone_setup.txt +0 -0
  120. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/verification.html +0 -0
  121. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/verification.subject.txt +0 -0
  122. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/email/templates/verification.txt +0 -0
  123. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/hooks/__init__.py +0 -0
  124. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/__init__.py +0 -0
  125. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/_objectid.py +0 -0
  126. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/login_attempt.py +0 -0
  127. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/mfa_code.py +0 -0
  128. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/pending_registration.py +0 -0
  129. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/models/user.py +0 -0
  130. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/_schemas.py +0 -0
  131. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/account.py +0 -0
  132. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/admin.py +0 -0
  133. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/login.py +0 -0
  134. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/logout.py +0 -0
  135. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/password.py +0 -0
  136. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/phone.py +0 -0
  137. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/register.py +0 -0
  138. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/routers/verify.py +0 -0
  139. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/__init__.py +0 -0
  140. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/factory.py +0 -0
  141. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/null.py +0 -0
  142. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/sns.py +0 -0
  143. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/sms/twilio.py +0 -0
  144. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/__init__.py +0 -0
  145. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/static/css/core.css +0 -0
  146. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/static/css/theme.css +0 -0
  147. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/static/js/regstack.js +0 -0
  148. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/email_change_confirm.html +0 -0
  149. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/forgot.html +0 -0
  150. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/login.html +0 -0
  151. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/me.html +0 -0
  152. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/mfa_confirm.html +0 -0
  153. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/register.html +0 -0
  154. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/reset.html +0 -0
  155. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/auth/verify.html +0 -0
  156. {regstack-0.2.2 → regstack-0.2.3}/src/regstack/ui/templates/base.html +0 -0
  157. {regstack-0.2.2 → regstack-0.2.3}/tasks.py +0 -0
  158. {regstack-0.2.2 → regstack-0.2.3}/tests/__init__.py +0 -0
  159. {regstack-0.2.2 → regstack-0.2.3}/tests/conftest.py +0 -0
  160. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/__init__.py +0 -0
  161. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_account_management.py +0 -0
  162. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_admin_router.py +0 -0
  163. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_happy_path.py +0 -0
  164. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_indexes.py +0 -0
  165. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_login_lockout.py +0 -0
  166. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_mfa.py +0 -0
  167. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_password_reset.py +0 -0
  168. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_sql_migrations.py +0 -0
  169. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_ui_router.py +0 -0
  170. {regstack-0.2.2 → regstack-0.2.3}/tests/integration/test_verification.py +0 -0
  171. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/__init__.py +0 -0
  172. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_base_install_imports.py +0 -0
  173. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_cli.py +0 -0
  174. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_config_loader.py +0 -0
  175. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_jwt.py +0 -0
  176. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_lockout.py +0 -0
  177. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_mail_composer.py +0 -0
  178. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_mfa_code_repo.py +0 -0
  179. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_password.py +0 -0
  180. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_ses_backend.py +0 -0
  181. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_sms.py +0 -0
  182. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_smtp_backend.py +0 -0
  183. {regstack-0.2.2 → regstack-0.2.3}/tests/unit/test_ui_env.py +0 -0
@@ -5,6 +5,16 @@ 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.3 — 2026-04-28
9
+
10
+ Docs-only release. Restructured the API reference around the current
11
+ package layout (post multi-backend refactor) and added Google-style
12
+ docstrings (Args / Returns / Raises) to the public surface — RegStack,
13
+ JwtCodec, PasswordHasher, LockoutService, AuthDependencies,
14
+ HookRegistry, EmailService, SmsService, the router builders, and the
15
+ Clock implementations. Dataclass field docs moved to PEP 258
16
+ attribute docstrings. Sphinx builds clean under `-W` again.
17
+
8
18
  ## 0.2.2 — 2026-04-28
9
19
 
10
20
  Docs-only release. The README and Sphinx docs landing page now lead
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: regstack
3
- Version: 0.2.2
3
+ Version: 0.2.3
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
@@ -0,0 +1,278 @@
1
+ # API reference
2
+
3
+ Auto-generated from docstrings. The most useful entry point is
4
+ [`regstack.RegStack`](#regstack.app.RegStack); everything else hangs
5
+ off it.
6
+
7
+ This page is organized by what you'd reach for, not by package
8
+ hierarchy. Each section starts with a one-paragraph orientation,
9
+ then the ``autoclass`` / ``autofunction`` directives pull the
10
+ docstrings, signatures, and parameter docs straight off the package
11
+ source.
12
+
13
+ ## Top-level
14
+
15
+ The handful of things you import from `regstack` directly:
16
+
17
+ - [`RegStack`](#regstack.app.RegStack) — the embeddable façade.
18
+ - [`RegStackConfig`](#regstack.config.schema.RegStackConfig) — top-level config.
19
+ - [`EmailConfig`](#regstack.config.schema.EmailConfig) — email-backend sub-config.
20
+ - [`SmsConfig`](#regstack.config.schema.SmsConfig) — SMS-backend sub-config.
21
+
22
+ Most embeddings need only `RegStack` and `RegStackConfig`.
23
+
24
+ ## Façade
25
+
26
+ `RegStack` is the embeddable façade. One per FastAPI application;
27
+ hosts mount its `router` and (optionally) `ui_router`. All collaborators
28
+ — password hasher, JWT codec, repos, hooks bus, email and SMS
29
+ services — hang off the instance.
30
+
31
+ ```{eval-rst}
32
+ .. autoclass:: regstack.app.RegStack
33
+ :members:
34
+ :show-inheritance:
35
+ ```
36
+
37
+ ## Configuration
38
+
39
+ `RegStackConfig` is a `pydantic-settings` model loaded from
40
+ environment variables (`REGSTACK_*`), an optional
41
+ `regstack.secrets.env`, an optional `regstack.toml`, and programmatic
42
+ kwargs — in that priority order. See the
43
+ [Configuration guide](configuration.md) for every field with its
44
+ default.
45
+
46
+ ```{eval-rst}
47
+ .. autoclass:: regstack.config.schema.RegStackConfig
48
+ :members:
49
+ :show-inheritance:
50
+ :exclude-members: model_config, model_fields, model_computed_fields
51
+
52
+ .. autoclass:: regstack.config.schema.EmailConfig
53
+ :members:
54
+ :show-inheritance:
55
+ :exclude-members: model_config, model_fields, model_computed_fields
56
+
57
+ .. autoclass:: regstack.config.schema.SmsConfig
58
+ :members:
59
+ :show-inheritance:
60
+ :exclude-members: model_config, model_fields, model_computed_fields
61
+
62
+ .. autofunction:: regstack.config.loader.load_config
63
+ ```
64
+
65
+ ## Auth primitives
66
+
67
+ The pieces that make authentication work: password hashing (Argon2id),
68
+ JWT issuance and validation with per-purpose derived keys, login
69
+ lockout, and the FastAPI dependency factory. Hosts rarely instantiate
70
+ these directly — they're built and wired by the `RegStack` constructor
71
+ — but the docstrings on the public methods explain the contract.
72
+
73
+ ```{eval-rst}
74
+ .. autoclass:: regstack.auth.password.PasswordHasher
75
+ :members:
76
+
77
+ .. autoclass:: regstack.auth.jwt.JwtCodec
78
+ :members:
79
+
80
+ .. autoclass:: regstack.auth.jwt.TokenPayload
81
+ :members:
82
+
83
+ .. autoexception:: regstack.auth.jwt.TokenError
84
+
85
+ .. autofunction:: regstack.auth.jwt.is_payload_bulk_revoked
86
+
87
+ .. autoclass:: regstack.auth.lockout.LockoutService
88
+ :members:
89
+
90
+ .. autoclass:: regstack.auth.lockout.LockoutDecision
91
+ :members:
92
+
93
+ .. autoclass:: regstack.auth.dependencies.AuthDependencies
94
+ :members:
95
+ ```
96
+
97
+ ### Time
98
+
99
+ Every time-sensitive operation reads `now()` through a `Clock`. Tests
100
+ inject `FrozenClock` to make assertions deterministic.
101
+
102
+ ```{eval-rst}
103
+ .. autoclass:: regstack.auth.clock.Clock
104
+ :members:
105
+
106
+ .. autoclass:: regstack.auth.clock.SystemClock
107
+ :members:
108
+
109
+ .. autoclass:: regstack.auth.clock.FrozenClock
110
+ :members:
111
+ ```
112
+
113
+ ## Backends
114
+
115
+ regstack ships three storage backends behind one set of `Protocol`
116
+ classes: SQLite (default, no infrastructure), PostgreSQL, and MongoDB.
117
+ The active backend is auto-built from `config.database_url`'s URL
118
+ scheme via `build_backend`. Hosts that need to share a connection pool
119
+ with their own application can pass an explicit `Backend` to the
120
+ `RegStack` constructor.
121
+
122
+ ```{eval-rst}
123
+ .. autoclass:: regstack.backends.base.Backend
124
+ :members:
125
+ :show-inheritance:
126
+
127
+ .. autoclass:: regstack.backends.base.BackendKind
128
+ :members:
129
+
130
+ .. autofunction:: regstack.backends.factory.build_backend
131
+
132
+ .. autoclass:: regstack.backends.mongo.MongoBackend
133
+ :members:
134
+ :show-inheritance:
135
+
136
+ .. autoclass:: regstack.backends.sql.SqlBackend
137
+ :members:
138
+ :show-inheritance:
139
+ ```
140
+
141
+ ### Repository protocols
142
+
143
+ The five repository protocols are the contract every backend
144
+ implements. Routers and services depend only on these — switching
145
+ backends is a wiring change, not a code change.
146
+
147
+ ```{eval-rst}
148
+ .. autoclass:: regstack.backends.protocols.UserRepoProtocol
149
+ :members:
150
+
151
+ .. autoclass:: regstack.backends.protocols.PendingRepoProtocol
152
+ :members:
153
+
154
+ .. autoclass:: regstack.backends.protocols.BlacklistRepoProtocol
155
+ :members:
156
+
157
+ .. autoclass:: regstack.backends.protocols.LoginAttemptRepoProtocol
158
+ :members:
159
+
160
+ .. autoclass:: regstack.backends.protocols.MfaCodeRepoProtocol
161
+ :members:
162
+
163
+ .. autoclass:: regstack.backends.protocols.MfaVerifyOutcome
164
+ :members:
165
+
166
+ .. autoclass:: regstack.backends.protocols.MfaVerifyResult
167
+ :members:
168
+
169
+ .. autoexception:: regstack.backends.protocols.UserAlreadyExistsError
170
+
171
+ .. autoexception:: regstack.backends.protocols.PendingAlreadyExistsError
172
+ ```
173
+
174
+ ## Models
175
+
176
+ The persisted data shapes. `BaseUser` is the canonical user document;
177
+ `UserCreate` validates registration input; `UserPublic` is what the
178
+ API returns (omits the password hash). The other models drive
179
+ verification, lockout, and SMS MFA.
180
+
181
+ ```{eval-rst}
182
+ .. autoclass:: regstack.models.user.BaseUser
183
+ :members:
184
+ :exclude-members: model_config, model_fields, model_computed_fields
185
+
186
+ .. autoclass:: regstack.models.user.UserCreate
187
+ :members:
188
+ :exclude-members: model_config, model_fields, model_computed_fields
189
+
190
+ .. autoclass:: regstack.models.user.UserPublic
191
+ :members:
192
+ :exclude-members: model_config, model_fields, model_computed_fields
193
+
194
+ .. autoclass:: regstack.models.pending_registration.PendingRegistration
195
+ :members:
196
+ :exclude-members: model_config, model_fields, model_computed_fields
197
+
198
+ .. autoclass:: regstack.models.login_attempt.LoginAttempt
199
+ :members:
200
+ :exclude-members: model_config, model_fields, model_computed_fields
201
+
202
+ .. autoclass:: regstack.models.mfa_code.MfaCode
203
+ :members:
204
+ :exclude-members: model_config, model_fields, model_computed_fields
205
+ ```
206
+
207
+ ## Email + SMS
208
+
209
+ Pluggable transports for the verification / reset / change-email
210
+ emails and the SMS MFA codes. Implement `EmailService` or `SmsService`
211
+ to plug in a provider that isn't bundled (Postmark, SendGrid,
212
+ MessageBird, …) and pass the instance to `regstack.set_email_backend`
213
+ / `set_sms_backend`.
214
+
215
+ ### Email
216
+
217
+ ```{eval-rst}
218
+ .. autoclass:: regstack.email.base.EmailMessage
219
+ :members:
220
+
221
+ .. autoclass:: regstack.email.base.EmailService
222
+ :members:
223
+
224
+ .. autoclass:: regstack.email.console.ConsoleEmailService
225
+ :members:
226
+
227
+ .. autoclass:: regstack.email.composer.MailComposer
228
+ :members:
229
+
230
+ .. autofunction:: regstack.email.factory.build_email_service
231
+ ```
232
+
233
+ ### SMS
234
+
235
+ ```{eval-rst}
236
+ .. autoclass:: regstack.sms.base.SmsMessage
237
+ :members:
238
+
239
+ .. autoclass:: regstack.sms.base.SmsService
240
+ :members:
241
+
242
+ .. autofunction:: regstack.sms.base.is_valid_e164
243
+
244
+ .. autoclass:: regstack.sms.null.NullSmsService
245
+ :members:
246
+
247
+ .. autofunction:: regstack.sms.factory.build_sms_service
248
+ ```
249
+
250
+ ## Hooks
251
+
252
+ The event bus regstack uses to fire side-effect notifications
253
+ (`user_registered`, `password_changed`, etc.) without coupling auth
254
+ code to host concerns like CRMs or analytics. See the
255
+ [Embedding guide](embedding.md#subscribing-to-events) for examples.
256
+
257
+ ```{eval-rst}
258
+ .. autoclass:: regstack.hooks.events.HookRegistry
259
+ :members:
260
+
261
+ .. autodata:: regstack.hooks.events.KNOWN_EVENTS
262
+ ```
263
+
264
+ ## Routers
265
+
266
+ Hosts normally access these via the `regstack.router` and
267
+ `regstack.ui_router` properties; the builder functions are public for
268
+ hosts that want to compose differently.
269
+
270
+ ```{eval-rst}
271
+ .. autofunction:: regstack.routers.build_router
272
+
273
+ .. autofunction:: regstack.ui.pages.build_ui_router
274
+
275
+ .. autofunction:: regstack.ui.pages.build_ui_environment
276
+
277
+ .. autofunction:: regstack.ui.pages.default_static_dir
278
+ ```
@@ -3,6 +3,34 @@
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.3 — 2026-04-28
7
+
8
+ **Docs-only release.** API reference rewritten around the current
9
+ package layout, public surface gained proper Google-style docstrings.
10
+
11
+ ### Changed
12
+
13
+ - ``docs/api.md`` restructured around the post-multi-backend package
14
+ layout (``regstack.backends.{base,protocols,factory,mongo,sql}`` and
15
+ friends). Each section now opens with a one-paragraph orientation
16
+ before the autodoc directives. The pre-refactor
17
+ ``regstack.db.repositories.*`` references that rendered empty are
18
+ gone.
19
+ - Added Google-style docstrings (purpose summary + Args / Returns /
20
+ Raises) to the most-touched public methods on ``RegStack``,
21
+ ``JwtCodec``, ``PasswordHasher``, ``LockoutService``,
22
+ ``AuthDependencies``, ``HookRegistry``, ``EmailService``,
23
+ ``SmsService``, ``build_router``, ``build_ui_router``,
24
+ ``build_ui_environment``, ``default_static_dir``, ``Clock`` /
25
+ ``SystemClock`` / ``FrozenClock``.
26
+ - Dataclass field documentation moved to PEP 258 attribute docstrings
27
+ on ``TokenPayload``, ``LockoutDecision``, ``EmailMessage``,
28
+ ``SmsMessage``, ``MfaVerifyResult`` — autodoc now renders each field
29
+ with its description without the "duplicate object description"
30
+ warnings the napoleon ``Attributes:`` block was triggering.
31
+ - ``MfaVerifyOutcome`` enum docstring reformatted as a bullet list
32
+ (the napoleon ``Members:`` block isn't a recognised section).
33
+
6
34
  ## 0.2.2 — 2026-04-28
7
35
 
8
36
  **Docs-only release.**
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "regstack"
3
- version = "0.2.2"
3
+ version = "0.2.3"
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"
@@ -0,0 +1,330 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import TYPE_CHECKING
5
+
6
+ from fastapi.staticfiles import StaticFiles
7
+
8
+ from regstack.auth.clock import Clock, SystemClock
9
+ from regstack.auth.dependencies import AuthDependencies
10
+ from regstack.auth.jwt import JwtCodec
11
+ from regstack.auth.lockout import LockoutService
12
+ from regstack.auth.password import PasswordHasher
13
+ from regstack.backends.factory import build_backend
14
+ from regstack.config.schema import RegStackConfig
15
+ from regstack.email.base import EmailService
16
+ from regstack.email.composer import MailComposer
17
+ from regstack.email.factory import build_email_service
18
+ from regstack.hooks.events import HookRegistry
19
+ from regstack.models.user import BaseUser
20
+ from regstack.routers import build_router
21
+ from regstack.sms.base import SmsService
22
+ from regstack.sms.factory import build_sms_service
23
+ from regstack.ui.pages import build_ui_environment, build_ui_router, default_static_dir
24
+
25
+ if TYPE_CHECKING:
26
+ from collections.abc import Awaitable, Callable
27
+
28
+ from fastapi import APIRouter
29
+ from jinja2 import Environment
30
+
31
+ from regstack.backends.base import Backend
32
+
33
+
34
+ class RegStack:
35
+ """Embeddable account-management façade.
36
+
37
+ One ``RegStack`` is constructed per FastAPI application. The host
38
+ then mounts the JSON router (and optionally the SSR router) and
39
+ regstack owns user accounts, authentication, password reset, email
40
+ verification, and (optionally) SMS two-factor.
41
+
42
+ The persistence story is owned by a
43
+ :class:`~regstack.backends.base.Backend` selected by
44
+ ``config.database_url``'s URL scheme:
45
+
46
+ - ``mongodb://`` / ``mongodb+srv://`` → MongoDB
47
+ - ``sqlite+aiosqlite://`` → SQLite
48
+ - ``postgresql+asyncpg://`` → PostgreSQL
49
+
50
+ Hosts that need to share a connection pool with their own code can
51
+ pass an explicit ``backend=`` argument and the URL is ignored.
52
+
53
+ Typical embed::
54
+
55
+ config = RegStackConfig.load()
56
+ regstack = RegStack(config=config)
57
+
58
+ @asynccontextmanager
59
+ async def lifespan(app):
60
+ await regstack.install_schema()
61
+ yield
62
+ await regstack.aclose()
63
+
64
+ app = FastAPI(lifespan=lifespan)
65
+ app.include_router(regstack.router, prefix=config.api_prefix)
66
+
67
+ Notable instance attributes (all set during ``__init__``):
68
+
69
+ - ``config`` — the loaded :class:`~regstack.config.schema.RegStackConfig`.
70
+ - ``clock`` — the injected :class:`~regstack.auth.clock.Clock`
71
+ (``SystemClock`` in production, ``FrozenClock`` in tests).
72
+ - ``backend`` — the active backend (Mongo / SQLite / Postgres).
73
+ - ``users``, ``pending``, ``blacklist``, ``attempts``, ``mfa_codes``
74
+ — repositories conforming to the protocols in
75
+ :mod:`regstack.backends.protocols`.
76
+ - ``password_hasher`` — Argon2id wrapper.
77
+ - ``jwt`` — :class:`~regstack.auth.jwt.JwtCodec` for session tokens.
78
+ - ``lockout`` — :class:`~regstack.auth.lockout.LockoutService`.
79
+ - ``email``, ``sms`` — the active transports.
80
+ - ``mail`` — the :class:`~regstack.email.composer.MailComposer`.
81
+ - ``hooks`` — the :class:`~regstack.hooks.events.HookRegistry`
82
+ event bus.
83
+ - ``deps`` — :class:`~regstack.auth.dependencies.AuthDependencies`
84
+ factory.
85
+ """
86
+
87
+ def __init__(
88
+ self,
89
+ *,
90
+ config: RegStackConfig,
91
+ backend: Backend | None = None,
92
+ clock: Clock | None = None,
93
+ email_service: EmailService | None = None,
94
+ mail_composer: MailComposer | None = None,
95
+ sms_service: SmsService | None = None,
96
+ ) -> None:
97
+ """Construct the façade and wire its collaborators.
98
+
99
+ Args:
100
+ config: Loaded configuration (see
101
+ :func:`~regstack.config.schema.RegStackConfig.load`).
102
+ backend: Optional pre-built backend. When ``None``, the
103
+ backend is built from ``config.database_url`` via
104
+ :func:`~regstack.backends.factory.build_backend`. Pass an
105
+ explicit backend if you want to share a connection pool
106
+ with the host application.
107
+ clock: Optional clock. Defaults to
108
+ :class:`~regstack.auth.clock.SystemClock`. Tests pass a
109
+ ``FrozenClock`` to make timing-sensitive assertions
110
+ deterministic.
111
+ email_service: Optional pre-built email backend. When
112
+ ``None``, one is built from ``config.email`` via
113
+ :func:`~regstack.email.factory.build_email_service`.
114
+ mail_composer: Optional pre-built mail composer. When
115
+ ``None``, one is built from ``config.email`` and
116
+ ``config.app_name``.
117
+ sms_service: Optional pre-built SMS backend. When ``None``,
118
+ one is built from ``config.sms`` via
119
+ :func:`~regstack.sms.factory.build_sms_service`.
120
+ """
121
+ self.config = config
122
+ self.clock: Clock = clock or SystemClock()
123
+ self.backend: Backend = backend or build_backend(config, clock=self.clock)
124
+ self.password_hasher = PasswordHasher()
125
+ self.jwt = JwtCodec(config, self.clock)
126
+
127
+ # Repos come straight off the backend so they're always in sync
128
+ # with whatever implementation is configured.
129
+ self.users = self.backend.users
130
+ self.pending = self.backend.pending
131
+ self.blacklist = self.backend.blacklist
132
+ self.attempts = self.backend.attempts
133
+ self.mfa_codes = self.backend.mfa_codes
134
+
135
+ self.lockout = LockoutService(attempts=self.attempts, config=config, clock=self.clock)
136
+ self.email: EmailService = email_service or build_email_service(config.email)
137
+ self.sms: SmsService = sms_service or build_sms_service(config.sms)
138
+ self.mail = mail_composer or MailComposer(
139
+ email_config=config.email,
140
+ app_name=config.app_name,
141
+ )
142
+ self.hooks = HookRegistry()
143
+ self.deps = AuthDependencies(jwt=self.jwt, users=self.users, blacklist=self.blacklist)
144
+ self._template_dirs: list[Path] = list(config.extra_template_dirs)
145
+ self._ui_env: Environment | None = None
146
+ self._router: APIRouter | None = None
147
+ self._ui_router: APIRouter | None = None
148
+ self._static_files: StaticFiles | None = None
149
+
150
+ @property
151
+ def router(self) -> APIRouter:
152
+ """The composite JSON ``APIRouter``.
153
+
154
+ Mount with
155
+ ``app.include_router(regstack.router, prefix=config.api_prefix)``.
156
+ Includes ``register``, ``verify``, ``login``, ``logout``,
157
+ ``account`` always; conditionally adds ``password``
158
+ (forgot/reset), ``phone`` + MFA, and ``admin`` based on
159
+ ``config.enable_*`` flags.
160
+
161
+ Built lazily on first access.
162
+ """
163
+ if self._router is None:
164
+ self._router = build_router(self)
165
+ return self._router
166
+
167
+ @property
168
+ def ui_env(self) -> Environment:
169
+ """The Jinja2 environment that renders the SSR pages.
170
+
171
+ Built lazily on first access; rebuilt automatically after every
172
+ :meth:`add_template_dir` call so host overrides take effect.
173
+ """
174
+ if self._ui_env is None:
175
+ self._ui_env = build_ui_environment(self._template_dirs)
176
+ return self._ui_env
177
+
178
+ @property
179
+ def ui_router(self) -> APIRouter:
180
+ """The SSR ``APIRouter`` for the bundled HTML pages.
181
+
182
+ Mount with ``app.include_router(regstack.ui_router,
183
+ prefix=config.ui_prefix)``. Only meaningful when
184
+ ``config.enable_ui_router=True`` — building it on a host that
185
+ won't mount it is harmless but pointless.
186
+ """
187
+ if self._ui_router is None:
188
+ self._ui_router = build_ui_router(self)
189
+ return self._ui_router
190
+
191
+ @property
192
+ def static_files(self) -> StaticFiles:
193
+ """Bundled CSS / JS as a Starlette ``StaticFiles`` app.
194
+
195
+ Mount with
196
+ ``app.mount(config.static_prefix, regstack.static_files)``.
197
+ Serves ``core.css``, the default ``theme.css``, and
198
+ ``regstack.js`` — the assets the SSR pages link to.
199
+ """
200
+ if self._static_files is None:
201
+ self._static_files = StaticFiles(directory=str(default_static_dir()))
202
+ return self._static_files
203
+
204
+ # --- Lifecycle -------------------------------------------------------
205
+
206
+ async def install_schema(self) -> None:
207
+ """Bring the database schema to head — idempotent.
208
+
209
+ On Mongo this means ensuring every required index exists. On
210
+ SQL backends it runs Alembic migrations to head, which creates
211
+ tables on a fresh database and applies any new revisions on an
212
+ existing one. Safe to call on every application boot.
213
+ """
214
+ await self.backend.install_schema()
215
+
216
+ async def install_indexes(self) -> None:
217
+ """Backwards-compatible alias for :meth:`install_schema`.
218
+
219
+ Kept for the 0.1.x ``install_indexes()`` name. New callers
220
+ should use :meth:`install_schema`.
221
+ """
222
+ await self.install_schema()
223
+
224
+ async def aclose(self) -> None:
225
+ """Tear down the backend's connection pool.
226
+
227
+ Call from your FastAPI lifespan teardown so background
228
+ connections are closed cleanly when the application shuts down.
229
+ """
230
+ await self.backend.aclose()
231
+
232
+ async def bootstrap_admin(self, email: str, password: str) -> BaseUser:
233
+ """Create or promote a verified superuser. Idempotent.
234
+
235
+ If a user with this email already exists, they are promoted to
236
+ ``is_superuser=True`` if they weren't already (their password is
237
+ not changed). Otherwise a new active, verified, superuser
238
+ account is created with the given password.
239
+
240
+ Args:
241
+ email: The admin's email address. Must be valid for the
242
+ user model's ``email`` validator.
243
+ password: The plaintext password to hash and store on a
244
+ newly-created admin. Ignored when promoting an existing
245
+ user.
246
+
247
+ Returns:
248
+ The persisted (and now-superuser) :class:`~regstack.models.user.BaseUser`.
249
+
250
+ Raises:
251
+ UserAlreadyExistsError: If the create path races against
252
+ another writer for the same email.
253
+ """
254
+ existing = await self.users.get_by_email(email)
255
+ if existing is not None:
256
+ if not existing.is_superuser:
257
+ assert existing.id is not None
258
+ await self.users.set_superuser(existing.id, is_superuser=True)
259
+ existing.is_superuser = True
260
+ return existing
261
+ user = BaseUser(
262
+ email=email,
263
+ hashed_password=self.password_hasher.hash(password),
264
+ is_active=True,
265
+ is_verified=True,
266
+ is_superuser=True,
267
+ )
268
+ return await self.users.create(user)
269
+
270
+ # --- Extension surface ------------------------------------------------
271
+
272
+ def set_email_backend(self, service: EmailService) -> None:
273
+ """Replace the active email backend at runtime.
274
+
275
+ Useful for hosts that want a backend not in the bundled set
276
+ (Postmark, SendGrid, MessageBird, …). See
277
+ :class:`~regstack.email.base.EmailService` for the contract.
278
+
279
+ Args:
280
+ service: An :class:`EmailService` implementation.
281
+ """
282
+ self.email = service
283
+
284
+ def set_sms_backend(self, service: SmsService) -> None:
285
+ """Replace the active SMS backend at runtime.
286
+
287
+ Args:
288
+ service: A :class:`~regstack.sms.base.SmsService` implementation.
289
+ """
290
+ self.sms = service
291
+
292
+ def add_template_dir(self, path: str | Path) -> None:
293
+ """Prepend a host template directory to the override chain.
294
+
295
+ Host templates win over regstack defaults via Jinja2's
296
+ ``ChoiceLoader`` for **both** the email composer and the SSR UI
297
+ pages. To override the verification email, drop a
298
+ ``verification.html`` file in the directory; to override the
299
+ login page, drop ``auth/login.html``.
300
+
301
+ Args:
302
+ path: Filesystem directory to search before regstack's
303
+ bundled templates. Must exist when templates are
304
+ rendered.
305
+ """
306
+ path_obj = Path(path)
307
+ self.mail.add_template_dir(path_obj)
308
+ if path_obj not in self._template_dirs:
309
+ self._template_dirs.insert(0, path_obj)
310
+ # Force the UI environment to rebuild on next access so the new
311
+ # directory takes effect even if the env was already touched.
312
+ self._ui_env = None
313
+
314
+ def on(self, event: str, handler: Callable[..., Awaitable[None] | None]) -> None:
315
+ """Register an event handler. Sync and async handlers both work.
316
+
317
+ Forwards to :meth:`HookRegistry.on
318
+ <regstack.hooks.events.HookRegistry.on>`. Handlers fire
319
+ concurrently when an event happens; exceptions are logged but
320
+ never break the primary auth flow. See
321
+ :data:`~regstack.hooks.events.KNOWN_EVENTS` for the set of
322
+ events regstack itself fires.
323
+
324
+ Args:
325
+ event: The event name (e.g. ``"user_registered"``,
326
+ ``"password_changed"``).
327
+ handler: A callable invoked with the event's keyword
328
+ arguments. Can be sync or async.
329
+ """
330
+ self.hooks.on(event, handler)