stapel-core 0.3.2__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.
- stapel_core-0.3.2/LICENSE +21 -0
- stapel_core-0.3.2/PKG-INFO +294 -0
- stapel_core-0.3.2/README.md +255 -0
- stapel_core-0.3.2/__init__.py +101 -0
- stapel_core-0.3.2/bus/__init__.py +35 -0
- stapel_core-0.3.2/bus/_config.py +81 -0
- stapel_core-0.3.2/bus/backends/__init__.py +0 -0
- stapel_core-0.3.2/bus/backends/kafka.py +194 -0
- stapel_core-0.3.2/bus/backends/memory.py +84 -0
- stapel_core-0.3.2/bus/backends/nats.py +282 -0
- stapel_core-0.3.2/bus/backends/routing.py +140 -0
- stapel_core-0.3.2/bus/base.py +36 -0
- stapel_core-0.3.2/bus/consumer.py +44 -0
- stapel_core-0.3.2/bus/event.py +57 -0
- stapel_core-0.3.2/bus/router.py +68 -0
- stapel_core-0.3.2/captcha/__init__.py +35 -0
- stapel_core-0.3.2/captcha/backends.py +159 -0
- stapel_core-0.3.2/captcha/conf.py +37 -0
- stapel_core-0.3.2/captcha/policy.py +168 -0
- stapel_core-0.3.2/comm/__init__.py +76 -0
- stapel_core-0.3.2/comm/actions.py +135 -0
- stapel_core-0.3.2/comm/config.py +72 -0
- stapel_core-0.3.2/comm/exceptions.py +34 -0
- stapel_core-0.3.2/comm/functions.py +186 -0
- stapel_core-0.3.2/comm/http.py +62 -0
- stapel_core-0.3.2/comm/nats.py +144 -0
- stapel_core-0.3.2/comm/registry.py +120 -0
- stapel_core-0.3.2/comm/schemas.py +66 -0
- stapel_core-0.3.2/comm/tasks.py +333 -0
- stapel_core-0.3.2/conf.py +90 -0
- stapel_core-0.3.2/core/__init__.py +18 -0
- stapel_core-0.3.2/core/config.py +174 -0
- stapel_core-0.3.2/core/jwt_handler.py +448 -0
- stapel_core-0.3.2/core/language.py +125 -0
- stapel_core-0.3.2/core/token_blacklist.py +63 -0
- stapel_core-0.3.2/core/token_manager.py +221 -0
- stapel_core-0.3.2/django/__init__.py +58 -0
- stapel_core-0.3.2/django/admin/__init__.py +1 -0
- stapel_core-0.3.2/django/admin/context.py +53 -0
- stapel_core-0.3.2/django/admin/mixins.py +149 -0
- stapel_core-0.3.2/django/admin/redirect.py +63 -0
- stapel_core-0.3.2/django/api/__init__.py +1 -0
- stapel_core-0.3.2/django/api/errors.py +458 -0
- stapel_core-0.3.2/django/api/pagination.py +336 -0
- stapel_core-0.3.2/django/api/permissions.py +162 -0
- stapel_core-0.3.2/django/api/revision.py +452 -0
- stapel_core-0.3.2/django/api/routers.py +10 -0
- stapel_core-0.3.2/django/api/serializers.py +262 -0
- stapel_core-0.3.2/django/apps.py +57 -0
- stapel_core-0.3.2/django/authentication.py +3 -0
- stapel_core-0.3.2/django/captcha.py +280 -0
- stapel_core-0.3.2/django/cdn/__init__.py +1 -0
- stapel_core-0.3.2/django/cdn/fields.py +295 -0
- stapel_core-0.3.2/django/cdn/ref_sync.py +119 -0
- stapel_core-0.3.2/django/errors.py +59 -0
- stapel_core-0.3.2/django/groups.py +212 -0
- stapel_core-0.3.2/django/jwt/__init__.py +1 -0
- stapel_core-0.3.2/django/jwt/authentication.py +181 -0
- stapel_core-0.3.2/django/jwt/backends.py +73 -0
- stapel_core-0.3.2/django/jwt/login_views.py +92 -0
- stapel_core-0.3.2/django/jwt/middleware.py +333 -0
- stapel_core-0.3.2/django/jwt/provider.py +191 -0
- stapel_core-0.3.2/django/jwt/session.py +59 -0
- stapel_core-0.3.2/django/jwt/utils.py +672 -0
- stapel_core-0.3.2/django/jwt/views.py +181 -0
- stapel_core-0.3.2/django/jwt_provider.py +2 -0
- stapel_core-0.3.2/django/management/__init__.py +0 -0
- stapel_core-0.3.2/django/management/commands/__init__.py +0 -0
- stapel_core-0.3.2/django/management/commands/check_flows.py +34 -0
- stapel_core-0.3.2/django/management/commands/consume_actions.py +57 -0
- stapel_core-0.3.2/django/management/commands/generate_flow_docs.py +39 -0
- stapel_core-0.3.2/django/management/commands/reset_sequences.py +124 -0
- stapel_core-0.3.2/django/management/commands/serve_functions.py +89 -0
- stapel_core-0.3.2/django/management/commands/staff_group.py +174 -0
- stapel_core-0.3.2/django/models.py +213 -0
- stapel_core-0.3.2/django/monitoring/__init__.py +1 -0
- stapel_core-0.3.2/django/monitoring/health.py +177 -0
- stapel_core-0.3.2/django/monitoring/telegram.py +114 -0
- stapel_core-0.3.2/django/openapi/__init__.py +7 -0
- stapel_core-0.3.2/django/openapi/extensions.py +106 -0
- stapel_core-0.3.2/django/openapi/mcp.py +164 -0
- stapel_core-0.3.2/django/openapi/openid.py +91 -0
- stapel_core-0.3.2/django/openapi/schemas.py +399 -0
- stapel_core-0.3.2/django/openapi/swagger.py +494 -0
- stapel_core-0.3.2/django/outbox/__init__.py +0 -0
- stapel_core-0.3.2/django/outbox/apps.py +8 -0
- stapel_core-0.3.2/django/outbox/management/__init__.py +0 -0
- stapel_core-0.3.2/django/outbox/management/commands/__init__.py +0 -0
- stapel_core-0.3.2/django/outbox/management/commands/dispatch_outbox.py +31 -0
- stapel_core-0.3.2/django/outbox/migrations/0001_initial.py +31 -0
- stapel_core-0.3.2/django/outbox/migrations/__init__.py +0 -0
- stapel_core-0.3.2/django/outbox/models.py +31 -0
- stapel_core-0.3.2/django/outbox/relay.py +72 -0
- stapel_core-0.3.2/django/settings.py +510 -0
- stapel_core-0.3.2/django/taskstore/__init__.py +0 -0
- stapel_core-0.3.2/django/taskstore/apps.py +22 -0
- stapel_core-0.3.2/django/taskstore/management/__init__.py +0 -0
- stapel_core-0.3.2/django/taskstore/management/commands/__init__.py +0 -0
- stapel_core-0.3.2/django/taskstore/management/commands/sweep_tasks.py +39 -0
- stapel_core-0.3.2/django/taskstore/migrations/0001_initial.py +37 -0
- stapel_core-0.3.2/django/taskstore/migrations/__init__.py +0 -0
- stapel_core-0.3.2/django/taskstore/models.py +42 -0
- stapel_core-0.3.2/django/templates/admin/base_site.html +229 -0
- stapel_core-0.3.2/django/users/__init__.py +1 -0
- stapel_core-0.3.2/django/users/admin.py +6 -0
- stapel_core-0.3.2/django/users/apps.py +8 -0
- stapel_core-0.3.2/django/users/migrations/0001_initial.py +62 -0
- stapel_core-0.3.2/django/users/migrations/0002_alter_user_managers_alter_user_groups_and_more.py +37 -0
- stapel_core-0.3.2/django/users/migrations/0003_remove_unique_constraints.py +23 -0
- stapel_core-0.3.2/django/users/migrations/0004_alter_user_phone.py +36 -0
- stapel_core-0.3.2/django/users/migrations/0005_alter_user_groups_alter_user_user_permissions.py +24 -0
- stapel_core-0.3.2/django/users/migrations/__init__.py +2 -0
- stapel_core-0.3.2/django/users/models.py +152 -0
- stapel_core-0.3.2/django/utils.py +8 -0
- stapel_core-0.3.2/django/workspaces.py +132 -0
- stapel_core-0.3.2/flows/__init__.py +38 -0
- stapel_core-0.3.2/flows/checks.py +116 -0
- stapel_core-0.3.2/flows/docs.py +192 -0
- stapel_core-0.3.2/flows/registry.py +169 -0
- stapel_core-0.3.2/gdpr.py +197 -0
- stapel_core-0.3.2/kafka/__init__.py +34 -0
- stapel_core-0.3.2/kafka/config.py +63 -0
- stapel_core-0.3.2/kafka/consumer.py +234 -0
- stapel_core-0.3.2/kafka/events.py +63 -0
- stapel_core-0.3.2/kafka/health.py +36 -0
- stapel_core-0.3.2/kafka/producer.py +105 -0
- stapel_core-0.3.2/kafka/topics.py +39 -0
- stapel_core-0.3.2/netintel/__init__.py +160 -0
- stapel_core-0.3.2/netintel/checks.py +47 -0
- stapel_core-0.3.2/netintel/conf.py +38 -0
- stapel_core-0.3.2/netintel/providers.py +314 -0
- stapel_core-0.3.2/netintel/types.py +47 -0
- stapel_core-0.3.2/notifications/__init__.py +17 -0
- stapel_core-0.3.2/notifications/publish.py +90 -0
- stapel_core-0.3.2/notifications/schemas/emits/notification.requested.json +18 -0
- stapel_core-0.3.2/notifications/tokens.py +59 -0
- stapel_core-0.3.2/oauth.py +112 -0
- stapel_core-0.3.2/py.typed +0 -0
- stapel_core-0.3.2/pyproject.toml +142 -0
- stapel_core-0.3.2/setup.cfg +4 -0
- stapel_core-0.3.2/signals.py +48 -0
- stapel_core-0.3.2/stapel_core.egg-info/PKG-INFO +294 -0
- stapel_core-0.3.2/stapel_core.egg-info/SOURCES.txt +344 -0
- stapel_core-0.3.2/stapel_core.egg-info/dependency_links.txt +1 -0
- stapel_core-0.3.2/stapel_core.egg-info/requires.txt +30 -0
- stapel_core-0.3.2/stapel_core.egg-info/top_level.txt +1 -0
- stapel_core-0.3.2/static/admin/js/cdn_image_widget.js +955 -0
- stapel_core-0.3.2/static/admin/js/jwt_session.js +150 -0
- stapel_core-0.3.2/testing.py +78 -0
- stapel_core-0.3.2/tests/test_bus.py +173 -0
- stapel_core-0.3.2/tests/test_bus_config.py +49 -0
- stapel_core-0.3.2/tests/test_bus_nats.py +144 -0
- stapel_core-0.3.2/tests/test_bus_routing.py +139 -0
- stapel_core-0.3.2/tests/test_captcha.py +139 -0
- stapel_core-0.3.2/tests/test_captcha_mixin.py +67 -0
- stapel_core-0.3.2/tests/test_captcha_policy.py +199 -0
- stapel_core-0.3.2/tests/test_captcha_protected.py +254 -0
- stapel_core-0.3.2/tests/test_comm.py +379 -0
- stapel_core-0.3.2/tests/test_comm_tasks.py +196 -0
- stapel_core-0.3.2/tests/test_comm_tasks_gaps.py +185 -0
- stapel_core-0.3.2/tests/test_conf_gaps.py +85 -0
- stapel_core-0.3.2/tests/test_core_hardening.py +155 -0
- stapel_core-0.3.2/tests/test_cov_api_admin_groups.py +324 -0
- stapel_core-0.3.2/tests/test_cov_api_errors_serializers.py +380 -0
- stapel_core-0.3.2/tests/test_cov_api_openapi.py +545 -0
- stapel_core-0.3.2/tests/test_cov_api_pagination.py +285 -0
- stapel_core-0.3.2/tests/test_cov_api_revision.py +343 -0
- stapel_core-0.3.2/tests/test_cov_infra_bus.py +373 -0
- stapel_core-0.3.2/tests/test_cov_infra_captcha.py +79 -0
- stapel_core-0.3.2/tests/test_cov_infra_cdn.py +330 -0
- stapel_core-0.3.2/tests/test_cov_infra_comm.py +480 -0
- stapel_core-0.3.2/tests/test_cov_infra_django_models.py +295 -0
- stapel_core-0.3.2/tests/test_cov_infra_monitoring.py +312 -0
- stapel_core-0.3.2/tests/test_cov_jwt_auth_provider.py +371 -0
- stapel_core-0.3.2/tests/test_cov_jwt_core.py +330 -0
- stapel_core-0.3.2/tests/test_cov_jwt_utils.py +520 -0
- stapel_core-0.3.2/tests/test_cov_jwt_views.py +307 -0
- stapel_core-0.3.2/tests/test_cov_mopup.py +87 -0
- stapel_core-0.3.2/tests/test_errors.py +363 -0
- stapel_core-0.3.2/tests/test_flows.py +112 -0
- stapel_core-0.3.2/tests/test_jwt.py +414 -0
- stapel_core-0.3.2/tests/test_middleware.py +425 -0
- stapel_core-0.3.2/tests/test_netintel.py +490 -0
- stapel_core-0.3.2/tests/test_notification_publish.py +89 -0
- stapel_core-0.3.2/tests/test_notification_tokens.py +54 -0
- stapel_core-0.3.2/tests/test_oauth.py +101 -0
- stapel_core-0.3.2/tests/test_outbox_relay_gaps.py +54 -0
- stapel_core-0.3.2/tests/test_permissions.py +204 -0
- stapel_core-0.3.2/tests/test_public_api.py +107 -0
- stapel_core-0.3.2/tests/test_schemas_autoload.py +71 -0
- stapel_core-0.3.2/tests/test_task_dispatch.py +154 -0
- stapel_core-0.3.2/tests/test_utils.py +346 -0
- stapel_core-0.3.2/tests/test_verification.py +414 -0
- stapel_core-0.3.2/verification/__init__.py +64 -0
- stapel_core-0.3.2/verification/conf.py +27 -0
- stapel_core-0.3.2/verification/decorators.py +154 -0
- stapel_core-0.3.2/verification/errors.py +24 -0
- stapel_core-0.3.2/verification/factors.py +102 -0
- stapel_core-0.3.2/verification/grants.py +166 -0
- stapel_core-0.3.2/verification/policy.py +102 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 usestapel
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,294 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stapel-core
|
|
3
|
+
Version: 0.3.2
|
|
4
|
+
Summary: Core Django utilities for the Stapel framework
|
|
5
|
+
License: MIT
|
|
6
|
+
Keywords: django,stapel,auth,jwt
|
|
7
|
+
Classifier: Framework :: Django
|
|
8
|
+
Classifier: Framework :: Django :: 5.2
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
11
|
+
Requires-Python: >=3.11
|
|
12
|
+
Description-Content-Type: text/markdown
|
|
13
|
+
License-File: LICENSE
|
|
14
|
+
Requires-Dist: Django>=5.1
|
|
15
|
+
Requires-Dist: djangorestframework>=3.14
|
|
16
|
+
Requires-Dist: djangorestframework-dataclasses>=1.2
|
|
17
|
+
Requires-Dist: drf-spectacular>=0.27
|
|
18
|
+
Requires-Dist: django-cors-headers>=4.3
|
|
19
|
+
Requires-Dist: django-redis>=5.4
|
|
20
|
+
Requires-Dist: PyJWT>=2.8
|
|
21
|
+
Requires-Dist: cryptography>=41.0
|
|
22
|
+
Requires-Dist: requests>=2.31
|
|
23
|
+
Requires-Dist: phonenumbers>=8.13
|
|
24
|
+
Provides-Extra: kafka
|
|
25
|
+
Requires-Dist: confluent-kafka>=2.3; extra == "kafka"
|
|
26
|
+
Provides-Extra: nats
|
|
27
|
+
Requires-Dist: nats-py>=2.9; extra == "nats"
|
|
28
|
+
Provides-Extra: sentry
|
|
29
|
+
Requires-Dist: sentry-sdk>=1.40; extra == "sentry"
|
|
30
|
+
Provides-Extra: netintel-maxmind
|
|
31
|
+
Requires-Dist: geoip2>=4.7; extra == "netintel-maxmind"
|
|
32
|
+
Provides-Extra: dev
|
|
33
|
+
Requires-Dist: pytest>=7.4; extra == "dev"
|
|
34
|
+
Requires-Dist: pytest-cov>=4.1; extra == "dev"
|
|
35
|
+
Requires-Dist: celery>=5.3; extra == "dev"
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: stapel-core[kafka,nats,netintel-maxmind,sentry]; extra == "all"
|
|
38
|
+
Dynamic: license-file
|
|
39
|
+
|
|
40
|
+
# stapel_core
|
|
41
|
+
|
|
42
|
+
[](https://github.com/usestapel/stapel-core/actions/workflows/ci.yml)
|
|
43
|
+
[](https://codecov.io/gh/usestapel/stapel-core)
|
|
44
|
+
|
|
45
|
+
Shared Python library for Stapel services. Provides JWT authentication, captcha
|
|
46
|
+
verification, event bus, notifications, and Django utilities used across all
|
|
47
|
+
backend services.
|
|
48
|
+
|
|
49
|
+
Part of the [Stapel framework](https://github.com/usestapel).
|
|
50
|
+
|
|
51
|
+
## Quick start for a new Django service
|
|
52
|
+
|
|
53
|
+
```bash
|
|
54
|
+
pip install -e ../iron-common-python
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
Add to `INSTALLED_APPS`:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
INSTALLED_APPS = [
|
|
61
|
+
...
|
|
62
|
+
'stapel_core.django',
|
|
63
|
+
'stapel_core.django.users', # if using the shared User model
|
|
64
|
+
]
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
## Modules
|
|
68
|
+
|
|
69
|
+
### `stapel_core.captcha` — Pluggable captcha verification
|
|
70
|
+
|
|
71
|
+
Backend-agnostic captcha interface. Supports Cloudflare Turnstile, Google
|
|
72
|
+
reCAPTCHA v2, hCaptcha, and custom backends.
|
|
73
|
+
|
|
74
|
+
**Settings** (per service, in `settings/base.py`):
|
|
75
|
+
|
|
76
|
+
```python
|
|
77
|
+
CAPTCHA_BACKEND = env.str('CAPTCHA_BACKEND', 'turnstile')
|
|
78
|
+
CAPTCHA_SECRET = env.str('CAPTCHA_SECRET', None) # absent → disabled
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Auto-disable**: if `CAPTCHA_SECRET` is `None` or empty, `build_verifier`
|
|
82
|
+
returns `NoopVerifier` regardless of backend. No separate toggle needed.
|
|
83
|
+
|
|
84
|
+
**DRF integration** (add mixin to any serializer):
|
|
85
|
+
|
|
86
|
+
```python
|
|
87
|
+
from stapel_core.django.captcha import CaptchaMixin
|
|
88
|
+
|
|
89
|
+
class MySerializer(CaptchaMixin, serializers.Serializer):
|
|
90
|
+
captcha_token = serializers.CharField(required=False, allow_blank=True)
|
|
91
|
+
|
|
92
|
+
def validate(self, attrs):
|
|
93
|
+
self._require_captcha_if_configured(attrs)
|
|
94
|
+
return attrs
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
**Custom backend** — subclass `CaptchaVerifier` and point to it via a dotted
|
|
98
|
+
import path:
|
|
99
|
+
|
|
100
|
+
```python
|
|
101
|
+
from stapel_core.captcha import CaptchaVerifier
|
|
102
|
+
|
|
103
|
+
class MyCaptchaVerifier(CaptchaVerifier):
|
|
104
|
+
def verify(self, token: str, ip: str | None = None) -> bool:
|
|
105
|
+
return my_service.check(token, self.secret)
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
# settings.py
|
|
110
|
+
CAPTCHA_BACKEND = 'myapp.captcha.MyCaptchaVerifier'
|
|
111
|
+
CAPTCHA_SECRET = 'my-secret'
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
The flat settings above keep working; the namespaced equivalent is
|
|
115
|
+
`STAPEL_CAPTCHA = {"BACKEND": ..., "SECRET": ...}`.
|
|
116
|
+
|
|
117
|
+
**Tiered challenge policy** — instead of a binary on/off, protect a view with
|
|
118
|
+
a strictness level derived from the client's network class (via
|
|
119
|
+
`stapel_core.netintel`):
|
|
120
|
+
|
|
121
|
+
```python
|
|
122
|
+
from stapel_core.django.captcha import captcha_protected
|
|
123
|
+
|
|
124
|
+
class RegisterView(APIView):
|
|
125
|
+
@captcha_protected(action="register")
|
|
126
|
+
def post(self, request): ...
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
Levels: `none < invisible < interactive < interactive+ratelimit < block`.
|
|
130
|
+
The default matrix (overridable via `STAPEL_CAPTCHA["CHALLENGE_MATRIX"]`,
|
|
131
|
+
merged over the defaults) maps residential/unknown → invisible,
|
|
132
|
+
datacenter/vpn → interactive, tor → interactive+ratelimit. Per-action
|
|
133
|
+
overrides: `STAPEL_CAPTCHA["ACTION_OVERRIDES"] = {"register": "+1"}` (bump one
|
|
134
|
+
level) or `{"payout": {"vpn": "block"}}`. The whole policy is swappable via
|
|
135
|
+
`STAPEL_CAPTCHA["CHALLENGE_POLICY"]` (dotted path to a `ChallengePolicy`).
|
|
136
|
+
`block` returns 403 `error.403.network_blocked`; rate limiting is not done
|
|
137
|
+
here — middleware reads `request.stapel_challenge_level`. With no netintel
|
|
138
|
+
provider configured every request classifies as `unknown` → behavior is
|
|
139
|
+
identical to the classic binary captcha.
|
|
140
|
+
|
|
141
|
+
---
|
|
142
|
+
|
|
143
|
+
### `stapel_core.netintel` — IP intelligence (network class + geo)
|
|
144
|
+
|
|
145
|
+
`classify_ip(ip) -> IpProfile{kind, asn, asn_org, country, confidence}` and
|
|
146
|
+
`country_of(ip)`. Kind vocabulary: `residential | datacenter | vpn | tor |
|
|
147
|
+
unknown`. Results are cached in the Django cache; provider errors fail open
|
|
148
|
+
to `unknown` and never raise.
|
|
149
|
+
|
|
150
|
+
```python
|
|
151
|
+
STAPEL_NETINTEL = {
|
|
152
|
+
# dotted path / class / instance of a NetIntelProvider (replace seam)
|
|
153
|
+
"PROVIDER": "stapel_core.netintel.providers.MaxMindProvider",
|
|
154
|
+
"MAXMIND_ASN_DB": "/var/geoip/GeoLite2-ASN.mmdb",
|
|
155
|
+
"MAXMIND_COUNTRY_DB": "/var/geoip/GeoLite2-Country.mmdb",
|
|
156
|
+
"MAXMIND_ANONYMOUS_DB": "/var/geoip/GeoIP2-Anonymous-IP.mmdb",
|
|
157
|
+
}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
Built-in providers: `NullProvider` (default — always `unknown`),
|
|
161
|
+
`MaxMindProvider` (offline mmdb, `pip install stapel-core[netintel-maxmind]`),
|
|
162
|
+
`HttpJsonProvider` (ipinfo/IPQS-style HTTP APIs via `HTTP_URL_TEMPLATE` /
|
|
163
|
+
`HTTP_API_KEY` / `HTTP_RESPONSE_MAPPER`). `client_ip(request)` honors
|
|
164
|
+
`TRUSTED_PROXY_HEADER` (default: `REMOTE_ADDR` only — proxy headers are
|
|
165
|
+
spoofable unless your edge overwrites them).
|
|
166
|
+
|
|
167
|
+
---
|
|
168
|
+
|
|
169
|
+
### `stapel_core.django.jwt` — JWT authentication
|
|
170
|
+
|
|
171
|
+
Unified JWT provider (singleton). Supports HS256 and RS256.
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from stapel_core.django.jwt.provider import jwt_provider
|
|
175
|
+
|
|
176
|
+
access, refresh = jwt_provider.create_tokens(user)
|
|
177
|
+
payload = jwt_provider.validate_token(access_token)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
**Settings**:
|
|
181
|
+
|
|
182
|
+
```python
|
|
183
|
+
JWT_ALGORITHM = 'HS256' # or 'RS256'
|
|
184
|
+
JWT_SECRET_KEY = 'your-secret' # HS256
|
|
185
|
+
JWT_PRIVATE_KEY = '...' # RS256
|
|
186
|
+
JWT_PUBLIC_KEY = '...' # RS256
|
|
187
|
+
JWT_ISSUER = 'https://yourapp.com'
|
|
188
|
+
JWT_AUDIENCE = None
|
|
189
|
+
JWT_ACCESS_TOKEN_LIFETIME = 900 # seconds
|
|
190
|
+
JWT_REFRESH_TOKEN_LIFETIME = 604800 # seconds
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
### `stapel_core.django.authentication` — JWT cookie auth
|
|
196
|
+
|
|
197
|
+
`JWTCookieAuthentication` reads JWT from `access_token` cookie or
|
|
198
|
+
`Authorization: Bearer <token>` header.
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
REST_FRAMEWORK = {
|
|
202
|
+
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
203
|
+
'stapel_core.django.authentication.JWTCookieAuthentication',
|
|
204
|
+
],
|
|
205
|
+
}
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
---
|
|
209
|
+
|
|
210
|
+
### `stapel_core.django.api` — DRF utilities
|
|
211
|
+
|
|
212
|
+
| Symbol | Purpose |
|
|
213
|
+
|---|---|
|
|
214
|
+
| `StapelDataclassSerializer` | Serializer that maps `@dataclass` fields |
|
|
215
|
+
| `StapelResponse(serializer)` | Wraps `.data` automatically |
|
|
216
|
+
| `StapelErrorResponse(status, ERR_KEY)` | Structured error response |
|
|
217
|
+
| `StapelValidationError(ERR_KEY)` | Raises DRF validation error with error key |
|
|
218
|
+
| `register_service_errors(dict)` | Registers error messages for a service |
|
|
219
|
+
| `AnchorPagination` / `CreatedAtAnchorPagination` | Cursor-style paginators |
|
|
220
|
+
|
|
221
|
+
---
|
|
222
|
+
|
|
223
|
+
### `stapel_core.bus` — Event bus
|
|
224
|
+
|
|
225
|
+
Kafka-backed event bus with an in-memory backend for tests.
|
|
226
|
+
|
|
227
|
+
**Publish** (sync, fire-and-forget):
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
from stapel_core.bus import publish, Event
|
|
231
|
+
|
|
232
|
+
publish('user.created', Event(
|
|
233
|
+
event_type='user.created',
|
|
234
|
+
service='auth',
|
|
235
|
+
payload={'user_id': '...'},
|
|
236
|
+
))
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
**Consume** by subclassing the management-command base:
|
|
240
|
+
|
|
241
|
+
```python
|
|
242
|
+
from stapel_core.bus import BaseBusConsumerCommand, Event
|
|
243
|
+
|
|
244
|
+
class ConsumeUsers(BaseBusConsumerCommand):
|
|
245
|
+
topics = ['user.created']
|
|
246
|
+
consumer_group = 'notifications'
|
|
247
|
+
|
|
248
|
+
def handle_event(self, event: Event) -> None:
|
|
249
|
+
...
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
Backend is selected via the `STAPEL_BUS_BACKEND` Django setting
|
|
253
|
+
(`stapel_core.bus.backends.kafka.KafkaBus` in production,
|
|
254
|
+
`stapel_core.bus.backends.memory.MemoryBus` in tests).
|
|
255
|
+
|
|
256
|
+
---
|
|
257
|
+
|
|
258
|
+
### `stapel_core.notifications` — Push notifications
|
|
259
|
+
|
|
260
|
+
```python
|
|
261
|
+
from stapel_core.notifications import request_notification
|
|
262
|
+
|
|
263
|
+
request_notification(
|
|
264
|
+
notification_type='welcome',
|
|
265
|
+
user_id=str(user.id),
|
|
266
|
+
email=user.email,
|
|
267
|
+
variables={'name': user.username},
|
|
268
|
+
source_service='auth',
|
|
269
|
+
)
|
|
270
|
+
```
|
|
271
|
+
|
|
272
|
+
---
|
|
273
|
+
|
|
274
|
+
### `stapel_core.oauth` — OAuth provider registry
|
|
275
|
+
|
|
276
|
+
Provider classes (`GoogleProvider`, `GitHubProvider`, etc.) and registry for
|
|
277
|
+
OAuth consumer flows (when your service accepts OAuth logins from external
|
|
278
|
+
providers).
|
|
279
|
+
|
|
280
|
+
---
|
|
281
|
+
|
|
282
|
+
### `stapel_core.gdpr` — GDPR utilities
|
|
283
|
+
|
|
284
|
+
Account closure requests, data export, re-registration hashes.
|
|
285
|
+
|
|
286
|
+
---
|
|
287
|
+
|
|
288
|
+
## Running tests
|
|
289
|
+
|
|
290
|
+
```bash
|
|
291
|
+
cd iron-common-python
|
|
292
|
+
pip install -e '.[dev]'
|
|
293
|
+
pytest stapel_core/tests/ -v
|
|
294
|
+
```
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
# stapel_core
|
|
2
|
+
|
|
3
|
+
[](https://github.com/usestapel/stapel-core/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/usestapel/stapel-core)
|
|
5
|
+
|
|
6
|
+
Shared Python library for Stapel services. Provides JWT authentication, captcha
|
|
7
|
+
verification, event bus, notifications, and Django utilities used across all
|
|
8
|
+
backend services.
|
|
9
|
+
|
|
10
|
+
Part of the [Stapel framework](https://github.com/usestapel).
|
|
11
|
+
|
|
12
|
+
## Quick start for a new Django service
|
|
13
|
+
|
|
14
|
+
```bash
|
|
15
|
+
pip install -e ../iron-common-python
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
Add to `INSTALLED_APPS`:
|
|
19
|
+
|
|
20
|
+
```python
|
|
21
|
+
INSTALLED_APPS = [
|
|
22
|
+
...
|
|
23
|
+
'stapel_core.django',
|
|
24
|
+
'stapel_core.django.users', # if using the shared User model
|
|
25
|
+
]
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
## Modules
|
|
29
|
+
|
|
30
|
+
### `stapel_core.captcha` — Pluggable captcha verification
|
|
31
|
+
|
|
32
|
+
Backend-agnostic captcha interface. Supports Cloudflare Turnstile, Google
|
|
33
|
+
reCAPTCHA v2, hCaptcha, and custom backends.
|
|
34
|
+
|
|
35
|
+
**Settings** (per service, in `settings/base.py`):
|
|
36
|
+
|
|
37
|
+
```python
|
|
38
|
+
CAPTCHA_BACKEND = env.str('CAPTCHA_BACKEND', 'turnstile')
|
|
39
|
+
CAPTCHA_SECRET = env.str('CAPTCHA_SECRET', None) # absent → disabled
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
**Auto-disable**: if `CAPTCHA_SECRET` is `None` or empty, `build_verifier`
|
|
43
|
+
returns `NoopVerifier` regardless of backend. No separate toggle needed.
|
|
44
|
+
|
|
45
|
+
**DRF integration** (add mixin to any serializer):
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
from stapel_core.django.captcha import CaptchaMixin
|
|
49
|
+
|
|
50
|
+
class MySerializer(CaptchaMixin, serializers.Serializer):
|
|
51
|
+
captcha_token = serializers.CharField(required=False, allow_blank=True)
|
|
52
|
+
|
|
53
|
+
def validate(self, attrs):
|
|
54
|
+
self._require_captcha_if_configured(attrs)
|
|
55
|
+
return attrs
|
|
56
|
+
```
|
|
57
|
+
|
|
58
|
+
**Custom backend** — subclass `CaptchaVerifier` and point to it via a dotted
|
|
59
|
+
import path:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
from stapel_core.captcha import CaptchaVerifier
|
|
63
|
+
|
|
64
|
+
class MyCaptchaVerifier(CaptchaVerifier):
|
|
65
|
+
def verify(self, token: str, ip: str | None = None) -> bool:
|
|
66
|
+
return my_service.check(token, self.secret)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
# settings.py
|
|
71
|
+
CAPTCHA_BACKEND = 'myapp.captcha.MyCaptchaVerifier'
|
|
72
|
+
CAPTCHA_SECRET = 'my-secret'
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The flat settings above keep working; the namespaced equivalent is
|
|
76
|
+
`STAPEL_CAPTCHA = {"BACKEND": ..., "SECRET": ...}`.
|
|
77
|
+
|
|
78
|
+
**Tiered challenge policy** — instead of a binary on/off, protect a view with
|
|
79
|
+
a strictness level derived from the client's network class (via
|
|
80
|
+
`stapel_core.netintel`):
|
|
81
|
+
|
|
82
|
+
```python
|
|
83
|
+
from stapel_core.django.captcha import captcha_protected
|
|
84
|
+
|
|
85
|
+
class RegisterView(APIView):
|
|
86
|
+
@captcha_protected(action="register")
|
|
87
|
+
def post(self, request): ...
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
Levels: `none < invisible < interactive < interactive+ratelimit < block`.
|
|
91
|
+
The default matrix (overridable via `STAPEL_CAPTCHA["CHALLENGE_MATRIX"]`,
|
|
92
|
+
merged over the defaults) maps residential/unknown → invisible,
|
|
93
|
+
datacenter/vpn → interactive, tor → interactive+ratelimit. Per-action
|
|
94
|
+
overrides: `STAPEL_CAPTCHA["ACTION_OVERRIDES"] = {"register": "+1"}` (bump one
|
|
95
|
+
level) or `{"payout": {"vpn": "block"}}`. The whole policy is swappable via
|
|
96
|
+
`STAPEL_CAPTCHA["CHALLENGE_POLICY"]` (dotted path to a `ChallengePolicy`).
|
|
97
|
+
`block` returns 403 `error.403.network_blocked`; rate limiting is not done
|
|
98
|
+
here — middleware reads `request.stapel_challenge_level`. With no netintel
|
|
99
|
+
provider configured every request classifies as `unknown` → behavior is
|
|
100
|
+
identical to the classic binary captcha.
|
|
101
|
+
|
|
102
|
+
---
|
|
103
|
+
|
|
104
|
+
### `stapel_core.netintel` — IP intelligence (network class + geo)
|
|
105
|
+
|
|
106
|
+
`classify_ip(ip) -> IpProfile{kind, asn, asn_org, country, confidence}` and
|
|
107
|
+
`country_of(ip)`. Kind vocabulary: `residential | datacenter | vpn | tor |
|
|
108
|
+
unknown`. Results are cached in the Django cache; provider errors fail open
|
|
109
|
+
to `unknown` and never raise.
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
STAPEL_NETINTEL = {
|
|
113
|
+
# dotted path / class / instance of a NetIntelProvider (replace seam)
|
|
114
|
+
"PROVIDER": "stapel_core.netintel.providers.MaxMindProvider",
|
|
115
|
+
"MAXMIND_ASN_DB": "/var/geoip/GeoLite2-ASN.mmdb",
|
|
116
|
+
"MAXMIND_COUNTRY_DB": "/var/geoip/GeoLite2-Country.mmdb",
|
|
117
|
+
"MAXMIND_ANONYMOUS_DB": "/var/geoip/GeoIP2-Anonymous-IP.mmdb",
|
|
118
|
+
}
|
|
119
|
+
```
|
|
120
|
+
|
|
121
|
+
Built-in providers: `NullProvider` (default — always `unknown`),
|
|
122
|
+
`MaxMindProvider` (offline mmdb, `pip install stapel-core[netintel-maxmind]`),
|
|
123
|
+
`HttpJsonProvider` (ipinfo/IPQS-style HTTP APIs via `HTTP_URL_TEMPLATE` /
|
|
124
|
+
`HTTP_API_KEY` / `HTTP_RESPONSE_MAPPER`). `client_ip(request)` honors
|
|
125
|
+
`TRUSTED_PROXY_HEADER` (default: `REMOTE_ADDR` only — proxy headers are
|
|
126
|
+
spoofable unless your edge overwrites them).
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
### `stapel_core.django.jwt` — JWT authentication
|
|
131
|
+
|
|
132
|
+
Unified JWT provider (singleton). Supports HS256 and RS256.
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from stapel_core.django.jwt.provider import jwt_provider
|
|
136
|
+
|
|
137
|
+
access, refresh = jwt_provider.create_tokens(user)
|
|
138
|
+
payload = jwt_provider.validate_token(access_token)
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
**Settings**:
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
JWT_ALGORITHM = 'HS256' # or 'RS256'
|
|
145
|
+
JWT_SECRET_KEY = 'your-secret' # HS256
|
|
146
|
+
JWT_PRIVATE_KEY = '...' # RS256
|
|
147
|
+
JWT_PUBLIC_KEY = '...' # RS256
|
|
148
|
+
JWT_ISSUER = 'https://yourapp.com'
|
|
149
|
+
JWT_AUDIENCE = None
|
|
150
|
+
JWT_ACCESS_TOKEN_LIFETIME = 900 # seconds
|
|
151
|
+
JWT_REFRESH_TOKEN_LIFETIME = 604800 # seconds
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
---
|
|
155
|
+
|
|
156
|
+
### `stapel_core.django.authentication` — JWT cookie auth
|
|
157
|
+
|
|
158
|
+
`JWTCookieAuthentication` reads JWT from `access_token` cookie or
|
|
159
|
+
`Authorization: Bearer <token>` header.
|
|
160
|
+
|
|
161
|
+
```python
|
|
162
|
+
REST_FRAMEWORK = {
|
|
163
|
+
'DEFAULT_AUTHENTICATION_CLASSES': [
|
|
164
|
+
'stapel_core.django.authentication.JWTCookieAuthentication',
|
|
165
|
+
],
|
|
166
|
+
}
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
### `stapel_core.django.api` — DRF utilities
|
|
172
|
+
|
|
173
|
+
| Symbol | Purpose |
|
|
174
|
+
|---|---|
|
|
175
|
+
| `StapelDataclassSerializer` | Serializer that maps `@dataclass` fields |
|
|
176
|
+
| `StapelResponse(serializer)` | Wraps `.data` automatically |
|
|
177
|
+
| `StapelErrorResponse(status, ERR_KEY)` | Structured error response |
|
|
178
|
+
| `StapelValidationError(ERR_KEY)` | Raises DRF validation error with error key |
|
|
179
|
+
| `register_service_errors(dict)` | Registers error messages for a service |
|
|
180
|
+
| `AnchorPagination` / `CreatedAtAnchorPagination` | Cursor-style paginators |
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
### `stapel_core.bus` — Event bus
|
|
185
|
+
|
|
186
|
+
Kafka-backed event bus with an in-memory backend for tests.
|
|
187
|
+
|
|
188
|
+
**Publish** (sync, fire-and-forget):
|
|
189
|
+
|
|
190
|
+
```python
|
|
191
|
+
from stapel_core.bus import publish, Event
|
|
192
|
+
|
|
193
|
+
publish('user.created', Event(
|
|
194
|
+
event_type='user.created',
|
|
195
|
+
service='auth',
|
|
196
|
+
payload={'user_id': '...'},
|
|
197
|
+
))
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
**Consume** by subclassing the management-command base:
|
|
201
|
+
|
|
202
|
+
```python
|
|
203
|
+
from stapel_core.bus import BaseBusConsumerCommand, Event
|
|
204
|
+
|
|
205
|
+
class ConsumeUsers(BaseBusConsumerCommand):
|
|
206
|
+
topics = ['user.created']
|
|
207
|
+
consumer_group = 'notifications'
|
|
208
|
+
|
|
209
|
+
def handle_event(self, event: Event) -> None:
|
|
210
|
+
...
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
Backend is selected via the `STAPEL_BUS_BACKEND` Django setting
|
|
214
|
+
(`stapel_core.bus.backends.kafka.KafkaBus` in production,
|
|
215
|
+
`stapel_core.bus.backends.memory.MemoryBus` in tests).
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### `stapel_core.notifications` — Push notifications
|
|
220
|
+
|
|
221
|
+
```python
|
|
222
|
+
from stapel_core.notifications import request_notification
|
|
223
|
+
|
|
224
|
+
request_notification(
|
|
225
|
+
notification_type='welcome',
|
|
226
|
+
user_id=str(user.id),
|
|
227
|
+
email=user.email,
|
|
228
|
+
variables={'name': user.username},
|
|
229
|
+
source_service='auth',
|
|
230
|
+
)
|
|
231
|
+
```
|
|
232
|
+
|
|
233
|
+
---
|
|
234
|
+
|
|
235
|
+
### `stapel_core.oauth` — OAuth provider registry
|
|
236
|
+
|
|
237
|
+
Provider classes (`GoogleProvider`, `GitHubProvider`, etc.) and registry for
|
|
238
|
+
OAuth consumer flows (when your service accepts OAuth logins from external
|
|
239
|
+
providers).
|
|
240
|
+
|
|
241
|
+
---
|
|
242
|
+
|
|
243
|
+
### `stapel_core.gdpr` — GDPR utilities
|
|
244
|
+
|
|
245
|
+
Account closure requests, data export, re-registration hashes.
|
|
246
|
+
|
|
247
|
+
---
|
|
248
|
+
|
|
249
|
+
## Running tests
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
cd iron-common-python
|
|
253
|
+
pip install -e '.[dev]'
|
|
254
|
+
pytest stapel_core/tests/ -v
|
|
255
|
+
```
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"""Stapel Core — shared Django utilities for the Stapel framework.
|
|
2
|
+
|
|
3
|
+
The building blocks every Stapel package sits on:
|
|
4
|
+
|
|
5
|
+
- ``comm`` — Action/Function inter-module communication (``emit``,
|
|
6
|
+
``on_action``, ``call``, ``function``) plus long-running tasks
|
|
7
|
+
(``start``, ``status``, ``task_handler``). Transports are deployment
|
|
8
|
+
configuration, not code.
|
|
9
|
+
- ``bus`` — transport-agnostic message bus (``publish``, ``get_bus``,
|
|
10
|
+
``Event``) with Kafka/NATS/in-memory backends.
|
|
11
|
+
- ``conf.AppSettings`` — per-app settings namespaces (the DRF
|
|
12
|
+
``api_settings`` pattern, generalized).
|
|
13
|
+
- ``signals`` — in-process Django signals for business milestones.
|
|
14
|
+
- ``django.api`` — API conventions: ``StapelResponse``,
|
|
15
|
+
``StapelErrorResponse`` and ``StapelDataclassSerializer``.
|
|
16
|
+
- ``gdpr`` — GDPR provider protocol and in-process registry.
|
|
17
|
+
- ``netintel`` — IP intelligence seam (``classify_ip``, ``country_of``,
|
|
18
|
+
``IpProfile``): pluggable provider, cached, fail-open.
|
|
19
|
+
- ``django.users.AbstractStapelUser`` — base user model.
|
|
20
|
+
- ``core`` — framework-agnostic JWT primitives (``JWTHandler``,
|
|
21
|
+
``TokenManager``, ``TokenBlacklist``, ``JWTConfig``).
|
|
22
|
+
|
|
23
|
+
All attributes are exported lazily (PEP 562), so importing
|
|
24
|
+
``stapel_core`` stays cheap and never touches Django until a
|
|
25
|
+
Django-dependent attribute is actually used.
|
|
26
|
+
"""
|
|
27
|
+
|
|
28
|
+
from importlib import import_module
|
|
29
|
+
|
|
30
|
+
try: # single source of truth: pyproject's version via package metadata
|
|
31
|
+
from importlib.metadata import version as _pkg_version
|
|
32
|
+
|
|
33
|
+
__version__ = _pkg_version("stapel-core")
|
|
34
|
+
except Exception: # editable/vendored checkout without dist-info
|
|
35
|
+
__version__ = "0.3.0"
|
|
36
|
+
|
|
37
|
+
# Attribute name -> (relative module, attribute in that module).
|
|
38
|
+
# An attribute of None means the module itself is the export.
|
|
39
|
+
_LAZY_EXPORTS = {
|
|
40
|
+
# comm — Actions, Functions, long-running tasks
|
|
41
|
+
"emit": (".comm", "emit"),
|
|
42
|
+
"on_action": (".comm", "on_action"),
|
|
43
|
+
"call": (".comm", "call"),
|
|
44
|
+
"function": (".comm", "function"),
|
|
45
|
+
"start": (".comm", "start"),
|
|
46
|
+
"status": (".comm", "status"),
|
|
47
|
+
"task_handler": (".comm", "task_handler"),
|
|
48
|
+
# flows — self-documenting business scenarios
|
|
49
|
+
"Flow": (".flows", "Flow"),
|
|
50
|
+
"flow_step": (".flows", "flow_step"),
|
|
51
|
+
"flow_registry": (".flows", "flow_registry"),
|
|
52
|
+
# netintel — IP classification seam (provider dotted-path, cached, fail-open)
|
|
53
|
+
"classify_ip": (".netintel", "classify_ip"),
|
|
54
|
+
"country_of": (".netintel", "country_of"),
|
|
55
|
+
"IpProfile": (".netintel", "IpProfile"),
|
|
56
|
+
# verification — step-up (OTP/TOTP/passkey) on any endpoint
|
|
57
|
+
"requires_verification": (".verification", "requires_verification"),
|
|
58
|
+
"register_factor": (".verification", "register_factor"),
|
|
59
|
+
"VerificationFactor": (".verification", "VerificationFactor"),
|
|
60
|
+
"get_user_policy": (".verification", "get_user_policy"),
|
|
61
|
+
"invalidate_policy_cache": (".verification", "invalidate_policy_cache"),
|
|
62
|
+
# bus — transport-agnostic message bus
|
|
63
|
+
"publish": (".bus", "publish"),
|
|
64
|
+
"get_bus": (".bus", "get_bus"),
|
|
65
|
+
"Event": (".bus", "Event"),
|
|
66
|
+
# conf — per-app settings namespaces
|
|
67
|
+
"AppSettings": (".conf", "AppSettings"),
|
|
68
|
+
# signals — in-process business milestones (module export)
|
|
69
|
+
"signals": (".signals", None),
|
|
70
|
+
# API conventions — responses, errors, serializers
|
|
71
|
+
"StapelResponse": (".django.api.errors", "StapelResponse"),
|
|
72
|
+
"StapelErrorResponse": (".django.api.errors", "StapelErrorResponse"),
|
|
73
|
+
"StapelDataclassSerializer": (".django.api.serializers", "StapelDataclassSerializer"),
|
|
74
|
+
# GDPR — provider protocol + in-process registry
|
|
75
|
+
"GDPRProvider": (".gdpr", "GDPRProvider"),
|
|
76
|
+
"gdpr_registry": (".gdpr", "gdpr_registry"),
|
|
77
|
+
# Users — base user model
|
|
78
|
+
"AbstractStapelUser": (".django.users.models", "AbstractStapelUser"),
|
|
79
|
+
# Framework-agnostic JWT primitives (0.1.x root exports, kept stable)
|
|
80
|
+
"JWTHandler": (".core.jwt_handler", "JWTHandler"),
|
|
81
|
+
"TokenManager": (".core.token_manager", "TokenManager"),
|
|
82
|
+
"TokenBlacklist": (".core.token_blacklist", "TokenBlacklist"),
|
|
83
|
+
"JWTConfig": (".core.config", "JWTConfig"),
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
__all__ = sorted([*_LAZY_EXPORTS, "__version__"])
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def __getattr__(name):
|
|
90
|
+
try:
|
|
91
|
+
module_path, attr = _LAZY_EXPORTS[name]
|
|
92
|
+
except KeyError:
|
|
93
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}") from None
|
|
94
|
+
module = import_module(module_path, __name__)
|
|
95
|
+
value = module if attr is None else getattr(module, attr)
|
|
96
|
+
globals()[name] = value # cache so __getattr__ runs once per name
|
|
97
|
+
return value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
def __dir__():
|
|
101
|
+
return sorted(set(globals()) | set(_LAZY_EXPORTS))
|