devora-python 0.1.0__py3-none-any.whl → 0.1.1__py3-none-any.whl

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.
@@ -0,0 +1,167 @@
1
+ Metadata-Version: 2.5
2
+ Name: devora-python
3
+ Version: 0.1.1
4
+ Summary: Devora Python backend SDK for secure impersonation
5
+ Project-URL: Documentation, https://docs.devora.sh
6
+ Project-URL: Repository, https://github.com/getdevora/devora-sdks
7
+ Project-URL: Issues, https://github.com/getdevora/devora-sdks/issues
8
+ Author: Devora
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Keywords: backend,devora,impersonation,sdk,security
12
+ Classifier: Development Status :: 5 - Production/Stable
13
+ Classifier: Intended Audience :: Developers
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: <4.0,>=3.10
22
+ Description-Content-Type: text/markdown
23
+
24
+ # devora-python
25
+
26
+ Recording, masking and capture policy are configured in the Devora dashboard and
27
+ authorized server-side for each session. This backend SDK takes no capture
28
+ settings, and `devora_sdk()` ignores keyword arguments it does not recognize,
29
+ so check option names carefully. See
30
+ [capture settings](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
31
+
32
+ Python backend core SDK for Devora customer integrations.
33
+
34
+ ## Requirements
35
+
36
+ - Python `>=3.10,<4.0`
37
+ - In production, a replay store shared by every worker (the example below uses
38
+ Redis 6.2 or newer, for `SET ... PXAT`)
39
+
40
+ ## Install
41
+
42
+ ```bash
43
+ pip install devora-python redis
44
+ ```
45
+
46
+ ## Quick Start
47
+
48
+ ```python
49
+ import os
50
+ from dataclasses import asdict
51
+
52
+ import redis
53
+ from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
54
+
55
+ # Replay protection shared by every worker and instance (required in production).
56
+ redis_client = redis.Redis.from_url(os.environ["REDIS_URL"])
57
+
58
+
59
+ class RedisReplayStore:
60
+ def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
61
+ # Atomic insert-if-absent kept until expires_at; an exception makes the SDK fail closed (503).
62
+ key = f"devora:replay:{namespace}:{request_id}"
63
+ return bool(redis_client.set(key, b"1", nx=True, pxat=expires_at))
64
+
65
+
66
+ sdk = devora_sdk(
67
+ api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
68
+ secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
69
+ org_id=os.environ["DEVORA_ORG_ID"],
70
+ replay_store=RedisReplayStore(),
71
+ )
72
+
73
+
74
+ @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
75
+ def search_users(req):
76
+ return {"users": search_customer_users(req.query.get("term", ""))}
77
+
78
+
79
+ @sdk.register(DEVORA_ENDPOINTS.IMPERSONATE)
80
+ def impersonate(req):
81
+ ctx = req.devora_context
82
+ if not ctx:
83
+ raise ValueError("Missing impersonation context")
84
+ # Store the whole verified context in the token: the scope guard reads every
85
+ # field of it back. Expire the token no later than ctx.expires_at.
86
+ devora = {**asdict(ctx), "is_impersonation": True}
87
+ token = create_customer_token(ctx.target_user["id"], devora)
88
+ return {"token": token}
89
+ ```
90
+
91
+ Register every handler, then mount the SDK with the
92
+ [Django](https://pypi.org/project/devora-django/) or
93
+ [FastAPI](https://pypi.org/project/devora-fastapi/) adapter. For any other
94
+ framework, pass the request exactly as received to `process_request` (sync) or
95
+ `await async_process_request` (async):
96
+
97
+ ```python
98
+ from devora_sdk import AdapterRequest, process_request
99
+ from devora_sdk.utils import get_error_status_code
100
+
101
+
102
+ def handle_devora(method: str, path: str, query: str, body: bytes, headers) -> tuple[int, dict]:
103
+ # path: still percent-encoded and relative to your Devora mount, e.g. "/user/search"
104
+ # query: the raw query string without "?"; body: the raw bytes (cap the read yourself)
105
+ # headers: a mapping, or (name, value) pairs so duplicate headers stay visible
106
+ result = process_request(sdk, sdk.get_routes(), AdapterRequest(method, path, query, body, headers))
107
+ status = 200 if result["success"] else get_error_status_code(result.get("errorCode"))
108
+ return status, result # send as JSON with Cache-Control: private, no-store
109
+ ```
110
+
111
+ `async_process_request` runs signature verification, the replay store and
112
+ synchronous handlers in a worker thread, so it never blocks the event loop.
113
+ `process_request` cannot run `async def` handlers.
114
+
115
+ ### Production replay store
116
+
117
+ Every signed request carries a single-use id. In production the SDK needs a
118
+ shared, atomic `replay_store` so a captured request cannot be replayed against
119
+ another worker or instance. The environment comes from `environment=`, else
120
+ `DEVORA_ENV`, else `NODE_ENV`. Anything other than `development` or `test`
121
+ (including unset) is production, and production refuses to start without a
122
+ `replay_store`. Any store whose `consume` is an atomic insert-if-absent works;
123
+ see the
124
+ [replay-store contract](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#replay-store).
125
+
126
+ **Local development only:** to run without Redis, omit `replay_store` and set
127
+ `DEVORA_ENV=development` (or pass `environment="development"`). The SDK then
128
+ keeps request ids in memory, which protects a single process only. Never use
129
+ this in production.
130
+
131
+ `consume` must be a regular method returning `True` or `False`; the SDK calls
132
+ it synchronously, including from `async_process_request` (in a worker thread).
133
+ Use a synchronous client such as `redis.Redis`: an `async def consume` fails
134
+ closed with a 503. Keep it a single short round trip, and do not let the store
135
+ evict these keys before they expire. A unique-key database insert with an
136
+ expiry column works too.
137
+
138
+ The guard's session-liveness checker caches at most 1,024 results and permits at
139
+ most 64 distinct concurrent lookups per checker. Requests for the same session
140
+ share a lookup. Live/ended results use the configured TTL (five seconds by
141
+ default); unavailable results use at most one second. Cache hits never extend a
142
+ verdict's lifetime. Capacity exhaustion follows the configured unavailable
143
+ policy, which denies access by default.
144
+
145
+ ### Cold-start latency
146
+
147
+ The impersonation guard's scope policy is fetched lazily on first use, so the
148
+ first request handled by a freshly started worker process pays for that fetch
149
+ synchronously (and any request racing it gets a `503
150
+ IMPERSONATION_POLICY_UNAVAILABLE` rather than waiting). If your deployment
151
+ runs multiple worker processes (gunicorn, uWSGI, etc.), pass
152
+ `prefetch_scope_config=True` to start that fetch in a background thread as soon
153
+ as `devora_sdk(...)` is constructed, so the cache is usually warm by the first
154
+ request:
155
+
156
+ ```python
157
+ sdk = devora_sdk(
158
+ api_key=os.environ["DEVORA_API_KEY"],
159
+ secret_key=os.environ["DEVORA_SECRET_KEY"],
160
+ org_id=os.environ["DEVORA_ORG_ID"],
161
+ replay_store=RedisReplayStore(),
162
+ prefetch_scope_config=True,
163
+ )
164
+ ```
165
+
166
+ Options, error codes and the guard are documented in the
167
+ [Python reference](https://docs.devora.sh/reference/python).
@@ -1,7 +1,7 @@
1
1
  devora_sdk/__init__.py,sha256=X3_377Yyj4fesPUdFmVRG_4MLQCMqaVyjl1uQkXLlUg,2214
2
2
  devora_sdk/api_url_gen.py,sha256=wfYa1I23mbENK9IoDQrw0ZT_6ivkszRI9Jhs48yTcYI,116
3
3
  devora_sdk/browser_session.py,sha256=Get6voUbs4dg9ZvqJtBgiMr4gOqtHZh-Gi9xHX6E-tY,2007
4
- devora_sdk/constants.py,sha256=rj4y8s79eNl1tcWeZljSO16FtKVE3HJFAXW30vLvHvk,1395
4
+ devora_sdk/constants.py,sha256=HBoHYI5dqPL34Eg5pjGCnSw-e8mzjwJ65dLpyprcCDw,1395
5
5
  devora_sdk/guard.py,sha256=idlLUrt-HKstvuLJvwy4oCiW0BQ90n_WD3XiYMS6lCU,18284
6
6
  devora_sdk/handler.py,sha256=Tk98Z7c26gORnlE1CQoKrI9CnwGXuW56Is5oV0cI4K4,10295
7
7
  devora_sdk/hmac.py,sha256=a0OyYEij39j_rWXsgIyKsbgydl0dpVgiIs-BYZR198k,1889
@@ -14,7 +14,7 @@ devora_sdk/security.py,sha256=gfKwz_rF9tGXFejNcwOPBqlczK_g1AZrKZe1SMxdKCA,1437
14
14
  devora_sdk/signing.py,sha256=TzB_DEDX6hkN5cZeJVXXp7Q6Z8OKFXLD8GP9OvopZuA,8906
15
15
  devora_sdk/transport.py,sha256=plTQ0J4B_-LGUs6waOqBk8wL4P06KuS_LilyEYr9sW4,5528
16
16
  devora_sdk/utils.py,sha256=ZC1kmHMB6RVocv1uqpG9srRsQciOO0SzZEKyswY5NLk,5527
17
- devora_python-0.1.0.dist-info/METADATA,sha256=nr33H63Ulv-mhYHk_iGNsS0ZMm7RAEGA9zQxgucakr4,4547
18
- devora_python-0.1.0.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
19
- devora_python-0.1.0.dist-info/licenses/LICENSE,sha256=ZrqlZJezuSZhSOx4R8S5JOwwQ-sLmssj2XW7-3coRjA,1063
20
- devora_python-0.1.0.dist-info/RECORD,,
17
+ devora_python-0.1.1.dist-info/METADATA,sha256=fvnBQv-yW_0I3UWUcVcFumBw-VHH3o71g6Fv6j37Zos,6879
18
+ devora_python-0.1.1.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
19
+ devora_python-0.1.1.dist-info/licenses/LICENSE,sha256=ZrqlZJezuSZhSOx4R8S5JOwwQ-sLmssj2XW7-3coRjA,1063
20
+ devora_python-0.1.1.dist-info/RECORD,,
devora_sdk/constants.py CHANGED
@@ -34,7 +34,7 @@ class SECURITY_HEADERS:
34
34
  SESSION_ID = "x-devora-session-id"
35
35
 
36
36
 
37
- SDK_VERSION = "0.1.0"
37
+ SDK_VERSION = "0.1.1"
38
38
  DEFAULT_API_URL = DEVORA_API_ORIGIN
39
39
  DEFAULT_TIMESTAMP_TOLERANCE_SECONDS = 300
40
40
  DEFAULT_MAX_BODY_SIZE_BYTES = 1024 * 1024
@@ -1,132 +0,0 @@
1
- Metadata-Version: 2.5
2
- Name: devora-python
3
- Version: 0.1.0
4
- Summary: Devora Python backend SDK for secure impersonation
5
- Project-URL: Documentation, https://docs.devora.sh
6
- Project-URL: Repository, https://github.com/getdevora/devora-sdks
7
- Project-URL: Issues, https://github.com/getdevora/devora-sdks/issues
8
- Author: Devora
9
- License-Expression: MIT
10
- License-File: LICENSE
11
- Keywords: backend,devora,impersonation,sdk,security
12
- Classifier: Development Status :: 5 - Production/Stable
13
- Classifier: Intended Audience :: Developers
14
- Classifier: Programming Language :: Python :: 3
15
- Classifier: Programming Language :: Python :: 3.10
16
- Classifier: Programming Language :: Python :: 3.11
17
- Classifier: Programming Language :: Python :: 3.12
18
- Classifier: Programming Language :: Python :: 3.13
19
- Classifier: Programming Language :: Python :: 3.14
20
- Classifier: Typing :: Typed
21
- Requires-Python: <4.0,>=3.10
22
- Description-Content-Type: text/markdown
23
-
24
- # devora-python
25
-
26
- Recording, masking and activity preferences are configured in Devora Settings.
27
- SDK initialization overrides are ignored. New sessions retain the server's policy
28
- snapshot across exchange and resume. Developer privacy labels take effect only
29
- when selected in Settings; sensitive-field protection remains mandatory.
30
- See [migration details](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
31
-
32
- Python backend core SDK for Devora customer integrations.
33
-
34
- ## Requirements
35
-
36
- - Python `>=3.10,<4.0`
37
-
38
- ## Install
39
-
40
- ```bash
41
- pip install devora-python
42
- ```
43
-
44
- ## Quick Start
45
-
46
- ```python
47
- from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
48
-
49
- sdk = devora_sdk(
50
- api_key="pk_server_live_...",
51
- secret_key="sk_server_live_...",
52
- org_id="org_...",
53
- )
54
-
55
- @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
56
- def search_users(req):
57
- return {"users": search_customer_users(req.query.get("term", ""))}
58
-
59
- @sdk.register(DEVORA_ENDPOINTS.IMPERSONATE)
60
- def impersonate(req):
61
- context = req.devora_context
62
- token = create_customer_token(context.target_user["id"], context)
63
- return {"token": token}
64
- ```
65
-
66
- Use `process_request()` for sync frameworks, `async_process_request()` for
67
- async frameworks, or use the Django and FastAPI adapter packages.
68
-
69
- ### Production replay store
70
-
71
- Every signed request carries a single-use id. In production the SDK needs a
72
- shared, atomic `replay_store` so a captured request cannot be replayed against
73
- another worker or instance; the in-memory store is for
74
- `environment="development"` or `"test"` only, and any other environment refuses
75
- to start without one.
76
-
77
- ```python
78
- import os
79
-
80
- import redis
81
- from devora_sdk import devora_sdk
82
-
83
- client = redis.Redis.from_url(os.environ["REDIS_URL"])
84
-
85
-
86
- class RedisReplayStore:
87
- def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
88
- # Atomic insert-if-absent that lives until expires_at (Unix ms).
89
- # Exceptions propagate: the SDK then fails closed with a 503.
90
- return bool(
91
- client.set(f"devora:replay:{namespace}:{request_id}", b"1", nx=True, pxat=expires_at)
92
- )
93
-
94
-
95
- sdk = devora_sdk(
96
- api_key=os.environ["DEVORA_API_KEY"],
97
- secret_key=os.environ["DEVORA_SECRET_KEY"],
98
- org_id=os.environ["DEVORA_ORG_ID"],
99
- replay_store=RedisReplayStore(),
100
- )
101
- ```
102
-
103
- `consume` is called synchronously, including from `async_process_request`;
104
- keep it a single short round trip. Do not let the store evict these keys
105
- before they expire. A unique-key database insert with an expiry column works
106
- too.
107
-
108
- The guard's session-liveness checker caches at most 1,024 results and permits at
109
- most 64 distinct concurrent lookups per checker. Requests for the same session
110
- share a lookup. Live/ended results use the configured TTL (five seconds by
111
- default); unavailable results use at most one second. Cache hits never extend a
112
- verdict's lifetime. Capacity exhaustion follows the configured unavailable
113
- policy, which denies access by default.
114
-
115
- ### Cold-start latency
116
-
117
- The impersonation guard's scope policy is fetched lazily on first use, so the
118
- first request handled by a freshly started worker process pays for that fetch
119
- synchronously (and any request racing it gets a `503
120
- IMPERSONATION_POLICY_UNAVAILABLE` rather than waiting). If your deployment
121
- runs multiple worker processes (gunicorn, uWSGI, etc.), pass
122
- `prefetch_scope_config=True` to warm the cache in the background as soon as
123
- `devora_sdk(...)` is constructed, before the process starts serving traffic:
124
-
125
- ```python
126
- sdk = devora_sdk(
127
- api_key="pk_server_live_...",
128
- secret_key="sk_server_live_...",
129
- org_id="org_...",
130
- prefetch_scope_config=True,
131
- )
132
- ```