devora-python 0.1.0__tar.gz → 0.1.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- devora_python-0.1.1/PKG-INFO +167 -0
- devora_python-0.1.1/README.md +144 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/pyproject.toml +1 -1
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/constants.py +1 -1
- devora_python-0.1.0/PKG-INFO +0 -132
- devora_python-0.1.0/README.md +0 -109
- {devora_python-0.1.0 → devora_python-0.1.1}/.gitignore +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/LICENSE +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/__init__.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/api_url_gen.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/browser_session.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/guard.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/handler.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/hmac.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/models.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/policy.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/py.typed +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/replay.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/sdk.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/security.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/signing.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/transport.py +0 -0
- {devora_python-0.1.0 → devora_python-0.1.1}/src/devora_sdk/utils.py +0 -0
|
@@ -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).
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# devora-python
|
|
2
|
+
|
|
3
|
+
Recording, masking and capture policy are configured in the Devora dashboard and
|
|
4
|
+
authorized server-side for each session. This backend SDK takes no capture
|
|
5
|
+
settings, and `devora_sdk()` ignores keyword arguments it does not recognize,
|
|
6
|
+
so check option names carefully. See
|
|
7
|
+
[capture settings](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
|
|
8
|
+
|
|
9
|
+
Python backend core SDK for Devora customer integrations.
|
|
10
|
+
|
|
11
|
+
## Requirements
|
|
12
|
+
|
|
13
|
+
- Python `>=3.10,<4.0`
|
|
14
|
+
- In production, a replay store shared by every worker (the example below uses
|
|
15
|
+
Redis 6.2 or newer, for `SET ... PXAT`)
|
|
16
|
+
|
|
17
|
+
## Install
|
|
18
|
+
|
|
19
|
+
```bash
|
|
20
|
+
pip install devora-python redis
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
## Quick Start
|
|
24
|
+
|
|
25
|
+
```python
|
|
26
|
+
import os
|
|
27
|
+
from dataclasses import asdict
|
|
28
|
+
|
|
29
|
+
import redis
|
|
30
|
+
from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
|
|
31
|
+
|
|
32
|
+
# Replay protection shared by every worker and instance (required in production).
|
|
33
|
+
redis_client = redis.Redis.from_url(os.environ["REDIS_URL"])
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class RedisReplayStore:
|
|
37
|
+
def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
|
|
38
|
+
# Atomic insert-if-absent kept until expires_at; an exception makes the SDK fail closed (503).
|
|
39
|
+
key = f"devora:replay:{namespace}:{request_id}"
|
|
40
|
+
return bool(redis_client.set(key, b"1", nx=True, pxat=expires_at))
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
sdk = devora_sdk(
|
|
44
|
+
api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
|
|
45
|
+
secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
|
|
46
|
+
org_id=os.environ["DEVORA_ORG_ID"],
|
|
47
|
+
replay_store=RedisReplayStore(),
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
@sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
|
|
52
|
+
def search_users(req):
|
|
53
|
+
return {"users": search_customer_users(req.query.get("term", ""))}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@sdk.register(DEVORA_ENDPOINTS.IMPERSONATE)
|
|
57
|
+
def impersonate(req):
|
|
58
|
+
ctx = req.devora_context
|
|
59
|
+
if not ctx:
|
|
60
|
+
raise ValueError("Missing impersonation context")
|
|
61
|
+
# Store the whole verified context in the token: the scope guard reads every
|
|
62
|
+
# field of it back. Expire the token no later than ctx.expires_at.
|
|
63
|
+
devora = {**asdict(ctx), "is_impersonation": True}
|
|
64
|
+
token = create_customer_token(ctx.target_user["id"], devora)
|
|
65
|
+
return {"token": token}
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
Register every handler, then mount the SDK with the
|
|
69
|
+
[Django](https://pypi.org/project/devora-django/) or
|
|
70
|
+
[FastAPI](https://pypi.org/project/devora-fastapi/) adapter. For any other
|
|
71
|
+
framework, pass the request exactly as received to `process_request` (sync) or
|
|
72
|
+
`await async_process_request` (async):
|
|
73
|
+
|
|
74
|
+
```python
|
|
75
|
+
from devora_sdk import AdapterRequest, process_request
|
|
76
|
+
from devora_sdk.utils import get_error_status_code
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def handle_devora(method: str, path: str, query: str, body: bytes, headers) -> tuple[int, dict]:
|
|
80
|
+
# path: still percent-encoded and relative to your Devora mount, e.g. "/user/search"
|
|
81
|
+
# query: the raw query string without "?"; body: the raw bytes (cap the read yourself)
|
|
82
|
+
# headers: a mapping, or (name, value) pairs so duplicate headers stay visible
|
|
83
|
+
result = process_request(sdk, sdk.get_routes(), AdapterRequest(method, path, query, body, headers))
|
|
84
|
+
status = 200 if result["success"] else get_error_status_code(result.get("errorCode"))
|
|
85
|
+
return status, result # send as JSON with Cache-Control: private, no-store
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
`async_process_request` runs signature verification, the replay store and
|
|
89
|
+
synchronous handlers in a worker thread, so it never blocks the event loop.
|
|
90
|
+
`process_request` cannot run `async def` handlers.
|
|
91
|
+
|
|
92
|
+
### Production replay store
|
|
93
|
+
|
|
94
|
+
Every signed request carries a single-use id. In production the SDK needs a
|
|
95
|
+
shared, atomic `replay_store` so a captured request cannot be replayed against
|
|
96
|
+
another worker or instance. The environment comes from `environment=`, else
|
|
97
|
+
`DEVORA_ENV`, else `NODE_ENV`. Anything other than `development` or `test`
|
|
98
|
+
(including unset) is production, and production refuses to start without a
|
|
99
|
+
`replay_store`. Any store whose `consume` is an atomic insert-if-absent works;
|
|
100
|
+
see the
|
|
101
|
+
[replay-store contract](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#replay-store).
|
|
102
|
+
|
|
103
|
+
**Local development only:** to run without Redis, omit `replay_store` and set
|
|
104
|
+
`DEVORA_ENV=development` (or pass `environment="development"`). The SDK then
|
|
105
|
+
keeps request ids in memory, which protects a single process only. Never use
|
|
106
|
+
this in production.
|
|
107
|
+
|
|
108
|
+
`consume` must be a regular method returning `True` or `False`; the SDK calls
|
|
109
|
+
it synchronously, including from `async_process_request` (in a worker thread).
|
|
110
|
+
Use a synchronous client such as `redis.Redis`: an `async def consume` fails
|
|
111
|
+
closed with a 503. Keep it a single short round trip, and do not let the store
|
|
112
|
+
evict these keys before they expire. A unique-key database insert with an
|
|
113
|
+
expiry column works too.
|
|
114
|
+
|
|
115
|
+
The guard's session-liveness checker caches at most 1,024 results and permits at
|
|
116
|
+
most 64 distinct concurrent lookups per checker. Requests for the same session
|
|
117
|
+
share a lookup. Live/ended results use the configured TTL (five seconds by
|
|
118
|
+
default); unavailable results use at most one second. Cache hits never extend a
|
|
119
|
+
verdict's lifetime. Capacity exhaustion follows the configured unavailable
|
|
120
|
+
policy, which denies access by default.
|
|
121
|
+
|
|
122
|
+
### Cold-start latency
|
|
123
|
+
|
|
124
|
+
The impersonation guard's scope policy is fetched lazily on first use, so the
|
|
125
|
+
first request handled by a freshly started worker process pays for that fetch
|
|
126
|
+
synchronously (and any request racing it gets a `503
|
|
127
|
+
IMPERSONATION_POLICY_UNAVAILABLE` rather than waiting). If your deployment
|
|
128
|
+
runs multiple worker processes (gunicorn, uWSGI, etc.), pass
|
|
129
|
+
`prefetch_scope_config=True` to start that fetch in a background thread as soon
|
|
130
|
+
as `devora_sdk(...)` is constructed, so the cache is usually warm by the first
|
|
131
|
+
request:
|
|
132
|
+
|
|
133
|
+
```python
|
|
134
|
+
sdk = devora_sdk(
|
|
135
|
+
api_key=os.environ["DEVORA_API_KEY"],
|
|
136
|
+
secret_key=os.environ["DEVORA_SECRET_KEY"],
|
|
137
|
+
org_id=os.environ["DEVORA_ORG_ID"],
|
|
138
|
+
replay_store=RedisReplayStore(),
|
|
139
|
+
prefetch_scope_config=True,
|
|
140
|
+
)
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
Options, error codes and the guard are documented in the
|
|
144
|
+
[Python reference](https://docs.devora.sh/reference/python).
|
devora_python-0.1.0/PKG-INFO
DELETED
|
@@ -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
|
-
```
|
devora_python-0.1.0/README.md
DELETED
|
@@ -1,109 +0,0 @@
|
|
|
1
|
-
# devora-python
|
|
2
|
-
|
|
3
|
-
Recording, masking and activity preferences are configured in Devora Settings.
|
|
4
|
-
SDK initialization overrides are ignored. New sessions retain the server's policy
|
|
5
|
-
snapshot across exchange and resume. Developer privacy labels take effect only
|
|
6
|
-
when selected in Settings; sensitive-field protection remains mandatory.
|
|
7
|
-
See [migration details](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
|
|
8
|
-
|
|
9
|
-
Python backend core SDK for Devora customer integrations.
|
|
10
|
-
|
|
11
|
-
## Requirements
|
|
12
|
-
|
|
13
|
-
- Python `>=3.10,<4.0`
|
|
14
|
-
|
|
15
|
-
## Install
|
|
16
|
-
|
|
17
|
-
```bash
|
|
18
|
-
pip install devora-python
|
|
19
|
-
```
|
|
20
|
-
|
|
21
|
-
## Quick Start
|
|
22
|
-
|
|
23
|
-
```python
|
|
24
|
-
from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
|
|
25
|
-
|
|
26
|
-
sdk = devora_sdk(
|
|
27
|
-
api_key="pk_server_live_...",
|
|
28
|
-
secret_key="sk_server_live_...",
|
|
29
|
-
org_id="org_...",
|
|
30
|
-
)
|
|
31
|
-
|
|
32
|
-
@sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
|
|
33
|
-
def search_users(req):
|
|
34
|
-
return {"users": search_customer_users(req.query.get("term", ""))}
|
|
35
|
-
|
|
36
|
-
@sdk.register(DEVORA_ENDPOINTS.IMPERSONATE)
|
|
37
|
-
def impersonate(req):
|
|
38
|
-
context = req.devora_context
|
|
39
|
-
token = create_customer_token(context.target_user["id"], context)
|
|
40
|
-
return {"token": token}
|
|
41
|
-
```
|
|
42
|
-
|
|
43
|
-
Use `process_request()` for sync frameworks, `async_process_request()` for
|
|
44
|
-
async frameworks, or use the Django and FastAPI adapter packages.
|
|
45
|
-
|
|
46
|
-
### Production replay store
|
|
47
|
-
|
|
48
|
-
Every signed request carries a single-use id. In production the SDK needs a
|
|
49
|
-
shared, atomic `replay_store` so a captured request cannot be replayed against
|
|
50
|
-
another worker or instance; the in-memory store is for
|
|
51
|
-
`environment="development"` or `"test"` only, and any other environment refuses
|
|
52
|
-
to start without one.
|
|
53
|
-
|
|
54
|
-
```python
|
|
55
|
-
import os
|
|
56
|
-
|
|
57
|
-
import redis
|
|
58
|
-
from devora_sdk import devora_sdk
|
|
59
|
-
|
|
60
|
-
client = redis.Redis.from_url(os.environ["REDIS_URL"])
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
class RedisReplayStore:
|
|
64
|
-
def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
|
|
65
|
-
# Atomic insert-if-absent that lives until expires_at (Unix ms).
|
|
66
|
-
# Exceptions propagate: the SDK then fails closed with a 503.
|
|
67
|
-
return bool(
|
|
68
|
-
client.set(f"devora:replay:{namespace}:{request_id}", b"1", nx=True, pxat=expires_at)
|
|
69
|
-
)
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
sdk = devora_sdk(
|
|
73
|
-
api_key=os.environ["DEVORA_API_KEY"],
|
|
74
|
-
secret_key=os.environ["DEVORA_SECRET_KEY"],
|
|
75
|
-
org_id=os.environ["DEVORA_ORG_ID"],
|
|
76
|
-
replay_store=RedisReplayStore(),
|
|
77
|
-
)
|
|
78
|
-
```
|
|
79
|
-
|
|
80
|
-
`consume` is called synchronously, including from `async_process_request`;
|
|
81
|
-
keep it a single short round trip. Do not let the store evict these keys
|
|
82
|
-
before they expire. A unique-key database insert with an expiry column works
|
|
83
|
-
too.
|
|
84
|
-
|
|
85
|
-
The guard's session-liveness checker caches at most 1,024 results and permits at
|
|
86
|
-
most 64 distinct concurrent lookups per checker. Requests for the same session
|
|
87
|
-
share a lookup. Live/ended results use the configured TTL (five seconds by
|
|
88
|
-
default); unavailable results use at most one second. Cache hits never extend a
|
|
89
|
-
verdict's lifetime. Capacity exhaustion follows the configured unavailable
|
|
90
|
-
policy, which denies access by default.
|
|
91
|
-
|
|
92
|
-
### Cold-start latency
|
|
93
|
-
|
|
94
|
-
The impersonation guard's scope policy is fetched lazily on first use, so the
|
|
95
|
-
first request handled by a freshly started worker process pays for that fetch
|
|
96
|
-
synchronously (and any request racing it gets a `503
|
|
97
|
-
IMPERSONATION_POLICY_UNAVAILABLE` rather than waiting). If your deployment
|
|
98
|
-
runs multiple worker processes (gunicorn, uWSGI, etc.), pass
|
|
99
|
-
`prefetch_scope_config=True` to warm the cache in the background as soon as
|
|
100
|
-
`devora_sdk(...)` is constructed, before the process starts serving traffic:
|
|
101
|
-
|
|
102
|
-
```python
|
|
103
|
-
sdk = devora_sdk(
|
|
104
|
-
api_key="pk_server_live_...",
|
|
105
|
-
secret_key="sk_server_live_...",
|
|
106
|
-
org_id="org_...",
|
|
107
|
-
prefetch_scope_config=True,
|
|
108
|
-
)
|
|
109
|
-
```
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|