devora-fastapi 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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: devora-fastapi
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: FastAPI adapter for the Devora Python backend SDK
5
5
  Project-URL: Documentation, https://docs.devora.sh
6
6
  Project-URL: Repository, https://github.com/getdevora/devora-sdks
@@ -21,7 +21,7 @@ Classifier: Programming Language :: Python :: 3.14
21
21
  Classifier: Typing :: Typed
22
22
  Requires-Python: <4.0,>=3.10
23
23
  Requires-Dist: anyio>=4.14.2
24
- Requires-Dist: devora-python<0.2.0,>=0.1.0
24
+ Requires-Dist: devora-python<0.2.0,>=0.1.1
25
25
  Requires-Dist: fastapi<1.0.0,>=0.133.0
26
26
  Requires-Dist: idna>=3.15
27
27
  Requires-Dist: starlette>=1.3.1
@@ -29,36 +29,53 @@ Description-Content-Type: text/markdown
29
29
 
30
30
  # devora-fastapi
31
31
 
32
- Recording, masking and activity preferences are configured in Devora Settings.
33
- SDK initialization overrides are ignored. New sessions retain the server's policy
34
- snapshot across exchange and resume. Developer privacy labels take effect only
35
- when selected in Settings; sensitive-field protection remains mandatory.
36
- See [migration details](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
32
+ Recording, masking and capture policy are configured in the Devora dashboard and
33
+ authorized server-side for each session. This backend SDK takes no capture
34
+ settings, and `devora_sdk()` ignores keyword arguments it does not recognize,
35
+ so check option names carefully. See
36
+ [capture settings](https://github.com/getdevora/devora-sdks/blob/main/SETTINGS.md).
37
37
 
38
38
  FastAPI adapter for the Devora Python backend SDK.
39
39
 
40
40
  ## Requirements
41
41
 
42
42
  - Python `>=3.10,<4.0`
43
- - FastAPI `>=0.100.0,<1.0.0`
43
+ - FastAPI `>=0.133.0,<1.0.0`
44
+ - In production, a replay store shared by every worker (the example below uses
45
+ Redis 6.2 or newer, for `SET ... PXAT`)
44
46
 
45
47
  ## Install
46
48
 
47
49
  ```bash
48
- pip install devora-python devora-fastapi
50
+ pip install devora-python devora-fastapi redis
49
51
  ```
50
52
 
51
53
  ## Quick Start
52
54
 
53
55
  ```python
56
+ import os
57
+
58
+ import redis
54
59
  from fastapi import FastAPI
55
60
  from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
56
61
  from devora_sdk_fastapi import fastapi_router
57
62
 
63
+ # Replay protection shared by every worker and instance (required in production).
64
+ redis_client = redis.Redis.from_url(os.environ["REDIS_URL"])
65
+
66
+
67
+ class RedisReplayStore:
68
+ def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
69
+ # Atomic insert-if-absent kept until expires_at; an exception makes the SDK fail closed (503).
70
+ key = f"devora:replay:{namespace}:{request_id}"
71
+ return bool(redis_client.set(key, b"1", nx=True, pxat=expires_at))
72
+
73
+
58
74
  sdk = devora_sdk(
59
- api_key="pk_server_live_...",
60
- secret_key="sk_server_live_...",
61
- org_id="org_...",
75
+ api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
76
+ secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
77
+ org_id=os.environ["DEVORA_ORG_ID"],
78
+ replay_store=RedisReplayStore(),
62
79
  )
63
80
 
64
81
 
@@ -71,30 +88,36 @@ app = FastAPI()
71
88
  app.include_router(fastapi_router(sdk), prefix="/devora")
72
89
  ```
73
90
 
74
- For protected application routes, install the guard middleware and extract a
75
- trusted impersonation context from authenticated request state.
91
+ Register every handler before calling `fastapi_router(sdk)`; routes registered
92
+ later are not mounted. Handlers may be sync or async; sync handlers run in a
93
+ worker thread.
94
+
95
+ The environment comes from `environment=`, else `DEVORA_ENV`, else `NODE_ENV`.
96
+ Anything other than `development` or `test` (including unset) is production,
97
+ and production refuses to start without a `replay_store`. Any store whose
98
+ `consume` is an atomic insert-if-absent shared by every worker works; see the
99
+ [replay-store contract](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#replay-store).
100
+ `consume` must be a regular method: the SDK calls it from a worker thread, so
101
+ use the synchronous `redis.Redis` client even in an async app. An
102
+ `async def consume` fails closed with a 503.
103
+
104
+ **Local development only:** to run without Redis, omit `replay_store` and set
105
+ `DEVORA_ENV=development` (or pass `environment="development"`). The SDK then
106
+ keeps request ids in memory, which protects a single process only. Never use
107
+ this in production.
108
+
109
+ For protected application routes, install the guard middleware and return the
110
+ context dict your `IMPERSONATE` handler stored, read back from your
111
+ authenticated request state.
76
112
 
77
113
  ```python
78
- from devora_sdk import ImpersonationContext
79
114
  from devora_sdk_fastapi import DevoraImpersonationGuardMiddleware
80
115
 
81
116
 
82
- def get_impersonation_context(request) -> ImpersonationContext | None:
83
- devora = getattr(request.state, "devora", None)
84
- if not devora:
85
- return None
86
- return ImpersonationContext(
87
- is_impersonation=devora["isImpersonation"],
88
- scope=devora["scope"],
89
- session_id=devora["sessionId"],
90
- expires_at=devora["expiresAt"],
91
- actor=devora["actor"],
92
- subject=devora["subject"],
93
- auth_method=devora["authMethod"],
94
- authorization_source=devora["authorizationSource"],
95
- recording_allowed=devora["recordingAllowed"],
96
- impersonator=devora.get("impersonator"),
97
- )
117
+ def get_impersonation_context(request) -> dict | None:
118
+ # YOU IMPLEMENT: return the dict your IMPERSONATE handler stored, read from
119
+ # your auth middleware (never from headers or JSON the browser can set), or None.
120
+ return getattr(request.state, "devora", None)
98
121
 
99
122
 
100
123
  app.add_middleware(
@@ -120,3 +143,6 @@ seconds, multiply by `1000` when building the impersonation context. `actor`,
120
143
  `subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
121
144
  all required — the guard rejects the context as invalid without them, even
122
145
  though the dataclass marks them optional for construction convenience.
146
+
147
+ Options, error codes and the browser-session bridge are documented in the
148
+ [Python reference](https://docs.devora.sh/reference/python).
@@ -0,0 +1,119 @@
1
+ # devora-fastapi
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
+ FastAPI adapter for the Devora Python backend SDK.
10
+
11
+ ## Requirements
12
+
13
+ - Python `>=3.10,<4.0`
14
+ - FastAPI `>=0.133.0,<1.0.0`
15
+ - In production, a replay store shared by every worker (the example below uses
16
+ Redis 6.2 or newer, for `SET ... PXAT`)
17
+
18
+ ## Install
19
+
20
+ ```bash
21
+ pip install devora-python devora-fastapi redis
22
+ ```
23
+
24
+ ## Quick Start
25
+
26
+ ```python
27
+ import os
28
+
29
+ import redis
30
+ from fastapi import FastAPI
31
+ from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
32
+ from devora_sdk_fastapi import fastapi_router
33
+
34
+ # Replay protection shared by every worker and instance (required in production).
35
+ redis_client = redis.Redis.from_url(os.environ["REDIS_URL"])
36
+
37
+
38
+ class RedisReplayStore:
39
+ def consume(self, namespace: str, request_id: str, expires_at: int) -> bool:
40
+ # Atomic insert-if-absent kept until expires_at; an exception makes the SDK fail closed (503).
41
+ key = f"devora:replay:{namespace}:{request_id}"
42
+ return bool(redis_client.set(key, b"1", nx=True, pxat=expires_at))
43
+
44
+
45
+ sdk = devora_sdk(
46
+ api_key=os.environ["DEVORA_API_KEY"], # pk_server_live_...
47
+ secret_key=os.environ["DEVORA_SECRET_KEY"], # sk_server_live_...
48
+ org_id=os.environ["DEVORA_ORG_ID"],
49
+ replay_store=RedisReplayStore(),
50
+ )
51
+
52
+
53
+ @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
54
+ async def search_users(req):
55
+ return {"users": await search_customer_users(req.query.get("term", ""))}
56
+
57
+
58
+ app = FastAPI()
59
+ app.include_router(fastapi_router(sdk), prefix="/devora")
60
+ ```
61
+
62
+ Register every handler before calling `fastapi_router(sdk)`; routes registered
63
+ later are not mounted. Handlers may be sync or async; sync handlers run in a
64
+ worker thread.
65
+
66
+ The environment comes from `environment=`, else `DEVORA_ENV`, else `NODE_ENV`.
67
+ Anything other than `development` or `test` (including unset) is production,
68
+ and production refuses to start without a `replay_store`. Any store whose
69
+ `consume` is an atomic insert-if-absent shared by every worker works; see the
70
+ [replay-store contract](https://github.com/getdevora/devora-sdks/blob/main/SIGNING.md#replay-store).
71
+ `consume` must be a regular method: the SDK calls it from a worker thread, so
72
+ use the synchronous `redis.Redis` client even in an async app. An
73
+ `async def consume` fails closed with a 503.
74
+
75
+ **Local development only:** to run without Redis, omit `replay_store` and set
76
+ `DEVORA_ENV=development` (or pass `environment="development"`). The SDK then
77
+ keeps request ids in memory, which protects a single process only. Never use
78
+ this in production.
79
+
80
+ For protected application routes, install the guard middleware and return the
81
+ context dict your `IMPERSONATE` handler stored, read back from your
82
+ authenticated request state.
83
+
84
+ ```python
85
+ from devora_sdk_fastapi import DevoraImpersonationGuardMiddleware
86
+
87
+
88
+ def get_impersonation_context(request) -> dict | None:
89
+ # YOU IMPLEMENT: return the dict your IMPERSONATE handler stored, read from
90
+ # your auth middleware (never from headers or JSON the browser can set), or None.
91
+ return getattr(request.state, "devora", None)
92
+
93
+
94
+ app.add_middleware(
95
+ DevoraImpersonationGuardMiddleware,
96
+ sdk=sdk,
97
+ get_impersonation_context=get_impersonation_context,
98
+ )
99
+ ```
100
+
101
+ Register your own authentication middleware **after** this call — Starlette
102
+ runs middleware in the reverse of its registration order, so it must be the
103
+ outer layer that runs first for `request.state` to carry verified claims by
104
+ the time the guard reads them.
105
+
106
+ Resolve method overrides and route rewrites **before** the guard as well
107
+ (register that middleware after this call too, so it runs first). The guard
108
+ also judges every `X-HTTP-Method-Override`, `X-HTTP-Method` and
109
+ `X-Method-Override` value and any `_method` query parameter, but it cannot see a
110
+ `_method` field inside a request body, and it judges the path it receives.
111
+
112
+ `expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
113
+ seconds, multiply by `1000` when building the impersonation context. `actor`,
114
+ `subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
115
+ all required — the guard rejects the context as invalid without them, even
116
+ though the dataclass marks them optional for construction convenience.
117
+
118
+ Options, error codes and the browser-session bridge are documented in the
119
+ [Python reference](https://docs.devora.sh/reference/python).
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "devora-fastapi"
7
- version = "0.1.0"
7
+ version = "0.1.1"
8
8
  description = "FastAPI adapter for the Devora Python backend SDK"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10,<4.0"
@@ -25,7 +25,7 @@ classifiers = [
25
25
  "Typing :: Typed",
26
26
  ]
27
27
  dependencies = [
28
- "devora-python>=0.1.0,<0.2.0",
28
+ "devora-python>=0.1.1,<0.2.0",
29
29
  # Security floors: the lowest versions without known advisories (audited 2026-09-28).
30
30
  "fastapi>=0.133.0,<1.0.0",
31
31
  "starlette>=1.3.1",
@@ -1,93 +0,0 @@
1
- # devora-fastapi
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
- FastAPI adapter for the Devora Python backend SDK.
10
-
11
- ## Requirements
12
-
13
- - Python `>=3.10,<4.0`
14
- - FastAPI `>=0.100.0,<1.0.0`
15
-
16
- ## Install
17
-
18
- ```bash
19
- pip install devora-python devora-fastapi
20
- ```
21
-
22
- ## Quick Start
23
-
24
- ```python
25
- from fastapi import FastAPI
26
- from devora_sdk import DEVORA_ENDPOINTS, devora_sdk
27
- from devora_sdk_fastapi import fastapi_router
28
-
29
- sdk = devora_sdk(
30
- api_key="pk_server_live_...",
31
- secret_key="sk_server_live_...",
32
- org_id="org_...",
33
- )
34
-
35
-
36
- @sdk.register(DEVORA_ENDPOINTS.USER_SEARCH)
37
- async def search_users(req):
38
- return {"users": await search_customer_users(req.query.get("term", ""))}
39
-
40
-
41
- app = FastAPI()
42
- app.include_router(fastapi_router(sdk), prefix="/devora")
43
- ```
44
-
45
- For protected application routes, install the guard middleware and extract a
46
- trusted impersonation context from authenticated request state.
47
-
48
- ```python
49
- from devora_sdk import ImpersonationContext
50
- from devora_sdk_fastapi import DevoraImpersonationGuardMiddleware
51
-
52
-
53
- def get_impersonation_context(request) -> ImpersonationContext | None:
54
- devora = getattr(request.state, "devora", None)
55
- if not devora:
56
- return None
57
- return ImpersonationContext(
58
- is_impersonation=devora["isImpersonation"],
59
- scope=devora["scope"],
60
- session_id=devora["sessionId"],
61
- expires_at=devora["expiresAt"],
62
- actor=devora["actor"],
63
- subject=devora["subject"],
64
- auth_method=devora["authMethod"],
65
- authorization_source=devora["authorizationSource"],
66
- recording_allowed=devora["recordingAllowed"],
67
- impersonator=devora.get("impersonator"),
68
- )
69
-
70
-
71
- app.add_middleware(
72
- DevoraImpersonationGuardMiddleware,
73
- sdk=sdk,
74
- get_impersonation_context=get_impersonation_context,
75
- )
76
- ```
77
-
78
- Register your own authentication middleware **after** this call — Starlette
79
- runs middleware in the reverse of its registration order, so it must be the
80
- outer layer that runs first for `request.state` to carry verified claims by
81
- the time the guard reads them.
82
-
83
- Resolve method overrides and route rewrites **before** the guard as well
84
- (register that middleware after this call too, so it runs first). The guard
85
- also judges every `X-HTTP-Method-Override`, `X-HTTP-Method` and
86
- `X-Method-Override` value and any `_method` query parameter, but it cannot see a
87
- `_method` field inside a request body, and it judges the path it receives.
88
-
89
- `expires_at` must be a Unix timestamp in milliseconds. If your JWT stores Unix
90
- seconds, multiply by `1000` when building the impersonation context. `actor`,
91
- `subject`, `auth_method`, `authorization_source`, and `recording_allowed` are
92
- all required — the guard rejects the context as invalid without them, even
93
- though the dataclass marks them optional for construction convenience.
File without changes