apip-sdk-core 0.1.0__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.
- apip_sdk_core-0.1.0/.gitignore +25 -0
- apip_sdk_core-0.1.0/CHANGELOG.md +36 -0
- apip_sdk_core-0.1.0/PKG-INFO +543 -0
- apip_sdk_core-0.1.0/README.md +515 -0
- apip_sdk_core-0.1.0/pyproject.toml +53 -0
- apip_sdk_core-0.1.0/src/apip_sdk_core/__init__.py +23 -0
- apip_sdk_core-0.1.0/src/apip_sdk_core/policy/__init__.py +6 -0
- apip_sdk_core-0.1.0/src/apip_sdk_core/policy/v1alpha2/__init__.py +103 -0
- apip_sdk_core-0.1.0/src/apip_sdk_core/policy/v1alpha2/actions.py +116 -0
- apip_sdk_core-0.1.0/src/apip_sdk_core/policy/v1alpha2/policy.py +139 -0
- apip_sdk_core-0.1.0/src/apip_sdk_core/policy/v1alpha2/types.py +225 -0
- apip_sdk_core-0.1.0/src/apip_sdk_core/py.typed +0 -0
- apip_sdk_core-0.1.0/tests/__init__.py +1 -0
- apip_sdk_core-0.1.0/tests/test_public_api.py +39 -0
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Python build artefacts
|
|
2
|
+
dist/
|
|
3
|
+
build/
|
|
4
|
+
*.egg-info/
|
|
5
|
+
|
|
6
|
+
# Bytecode
|
|
7
|
+
__pycache__/
|
|
8
|
+
*.py[cod]
|
|
9
|
+
*.pyo
|
|
10
|
+
|
|
11
|
+
# Virtual environments
|
|
12
|
+
.venv/
|
|
13
|
+
venv/
|
|
14
|
+
env/
|
|
15
|
+
|
|
16
|
+
# mypy cache
|
|
17
|
+
.mypy_cache/
|
|
18
|
+
|
|
19
|
+
# pytest / coverage
|
|
20
|
+
.pytest_cache/
|
|
21
|
+
.coverage
|
|
22
|
+
htmlcov/
|
|
23
|
+
|
|
24
|
+
# macOS metadata
|
|
25
|
+
.DS_Store
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to **apip-sdk-core** will be documented here.
|
|
4
|
+
|
|
5
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
6
|
+
This project uses [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## [0.1.0] — 2026-04-23
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- Initial release of the `apip-sdk-core` Python SDK.
|
|
15
|
+
- `policy.v1alpha2` contract — a Python mirror of the Go `sdk/core/policy/v1alpha2` package.
|
|
16
|
+
- **Policy interfaces**: `Policy`, `RequestHeaderPolicy`, `RequestPolicy`,
|
|
17
|
+
`ResponseHeaderPolicy`, `ResponsePolicy`, `StreamingRequestPolicy`,
|
|
18
|
+
`StreamingResponsePolicy`.
|
|
19
|
+
- **Action types**: `UpstreamRequestHeaderModifications`,
|
|
20
|
+
`UpstreamRequestModifications`, `DownstreamResponseHeaderModifications`,
|
|
21
|
+
`DownstreamResponseModifications`, `ImmediateResponse`, `ForwardRequestChunk`,
|
|
22
|
+
`ForwardResponseChunk`, `TerminateResponseChunk`.
|
|
23
|
+
- **Context types**: `RequestHeaderContext`, `RequestContext`,
|
|
24
|
+
`ResponseHeaderContext`, `ResponseContext`, `RequestStreamContext`,
|
|
25
|
+
`ResponseStreamContext`, `SharedContext`, `AuthContext`, `ExecutionContext`,
|
|
26
|
+
`PolicyMetadata`.
|
|
27
|
+
- **Value types**: `Headers` (case-insensitive, read-only, multi-value),
|
|
28
|
+
`Body`, `StreamBody`, `ProcessingMode`, `HeaderProcessingMode`,
|
|
29
|
+
`BodyProcessingMode`, `ExecutionPhase`.
|
|
30
|
+
- `py.typed` marker for full PEP 561 type-checker support.
|
|
31
|
+
- Top-level re-exports so `from apip_sdk_core import RequestPolicy` works
|
|
32
|
+
without importing the versioned subpackage.
|
|
33
|
+
- `__version__` attribute on the root package.
|
|
34
|
+
- Zero runtime dependencies — standard library only.
|
|
35
|
+
- Python 3.10–3.13 support.
|
|
36
|
+
- Apache 2.0 licence.
|
|
@@ -0,0 +1,543 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: apip-sdk-core
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Typed Python interfaces for API Platform gateway policies
|
|
5
|
+
Project-URL: Homepage, https://wso2.com/api-platform/policy-hub
|
|
6
|
+
Project-URL: Repository, https://github.com/wso2/api-platform
|
|
7
|
+
Project-URL: Issues, https://github.com/wso2/api-platform/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/wso2/api-platform/blob/main/sdk-python/CHANGELOG.md
|
|
9
|
+
Author: WSO2
|
|
10
|
+
Keywords: api-platform,gateway,policy,sdk,wso2,wso2-api-platform
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: License :: OSI Approved :: Apache Software License
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.10
|
|
23
|
+
Provides-Extra: dev
|
|
24
|
+
Requires-Dist: build>=1.0; extra == 'dev'
|
|
25
|
+
Requires-Dist: mypy>=1.9; extra == 'dev'
|
|
26
|
+
Requires-Dist: twine>=5.0; extra == 'dev'
|
|
27
|
+
Description-Content-Type: text/markdown
|
|
28
|
+
|
|
29
|
+
# APIP SDK Core
|
|
30
|
+
|
|
31
|
+
[](https://pypi.org/project/apip-sdk-core/)
|
|
32
|
+
[](https://pypi.org/project/apip-sdk-core/)
|
|
33
|
+
[](https://github.com/wso2/api-platform/blob/main/LICENSE)
|
|
34
|
+
|
|
35
|
+
Python interfaces for writing **gateway policies** on the [WSO2 API Platform](https://github.com/wso2/api-platform).
|
|
36
|
+
|
|
37
|
+
A policy is a plain Python class that the gateway executor loads at runtime. It receives a rich, fully-typed execution context and returns a structured action that the gateway applies to the live request or response—no configuration files, no generated stubs, no runtime dependencies.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
## Contents
|
|
42
|
+
|
|
43
|
+
- [Requirements](#requirements)
|
|
44
|
+
- [Installation](#installation)
|
|
45
|
+
- [Quick start](#quick-start)
|
|
46
|
+
- [Policy types](#policy-types)
|
|
47
|
+
- [RequestHeaderPolicy](#requestheaderpolicy)
|
|
48
|
+
- [RequestPolicy](#requestpolicy)
|
|
49
|
+
- [ResponseHeaderPolicy](#responseheaderpolicy)
|
|
50
|
+
- [ResponsePolicy](#responsepolicy)
|
|
51
|
+
- [StreamingRequestPolicy](#streamingrequestpolicy)
|
|
52
|
+
- [StreamingResponsePolicy](#streamingresponsepolicy)
|
|
53
|
+
- [Processing mode](#processing-mode)
|
|
54
|
+
- [Actions reference](#actions-reference)
|
|
55
|
+
- [Context types](#context-types)
|
|
56
|
+
- [Versioned imports](#versioned-imports)
|
|
57
|
+
- [Development](#development)
|
|
58
|
+
- [License](#license)
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## Requirements
|
|
63
|
+
|
|
64
|
+
- Python **3.10** or newer
|
|
65
|
+
- No third-party runtime dependencies
|
|
66
|
+
|
|
67
|
+
---
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
```bash
|
|
72
|
+
pip install apip-sdk-core
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
---
|
|
76
|
+
|
|
77
|
+
## Quick start
|
|
78
|
+
|
|
79
|
+
### 1 · Add a request header
|
|
80
|
+
|
|
81
|
+
```python
|
|
82
|
+
from typing import Any
|
|
83
|
+
from apip_sdk_core import (
|
|
84
|
+
ExecutionContext,
|
|
85
|
+
HeaderProcessingMode,
|
|
86
|
+
ProcessingMode,
|
|
87
|
+
RequestHeaderAction,
|
|
88
|
+
RequestHeaderContext,
|
|
89
|
+
RequestHeaderPolicy,
|
|
90
|
+
UpstreamRequestHeaderModifications,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
class AddCorrelationId(RequestHeaderPolicy):
|
|
95
|
+
"""Injects an X-Correlation-ID header into every upstream request."""
|
|
96
|
+
|
|
97
|
+
def mode(self) -> ProcessingMode:
|
|
98
|
+
return ProcessingMode(
|
|
99
|
+
request_header_mode=HeaderProcessingMode.PROCESS,
|
|
100
|
+
)
|
|
101
|
+
|
|
102
|
+
def on_request_headers(
|
|
103
|
+
self,
|
|
104
|
+
execution_ctx: ExecutionContext,
|
|
105
|
+
ctx: RequestHeaderContext,
|
|
106
|
+
params: dict[str, Any],
|
|
107
|
+
) -> RequestHeaderAction:
|
|
108
|
+
return UpstreamRequestHeaderModifications(
|
|
109
|
+
headers_to_set={"x-correlation-id": execution_ctx.request_id},
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
### 2 · Validate and rewrite a buffered request body
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
import json
|
|
117
|
+
from typing import Any
|
|
118
|
+
from apip_sdk_core import (
|
|
119
|
+
BodyProcessingMode,
|
|
120
|
+
ExecutionContext,
|
|
121
|
+
ImmediateResponse,
|
|
122
|
+
ProcessingMode,
|
|
123
|
+
RequestAction,
|
|
124
|
+
RequestContext,
|
|
125
|
+
RequestPolicy,
|
|
126
|
+
UpstreamRequestModifications,
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
class EnforceJsonSchema(RequestPolicy):
|
|
131
|
+
"""Rejects requests that are not valid JSON objects."""
|
|
132
|
+
|
|
133
|
+
def mode(self) -> ProcessingMode:
|
|
134
|
+
return ProcessingMode(request_body_mode=BodyProcessingMode.BUFFER)
|
|
135
|
+
|
|
136
|
+
def on_request_body(
|
|
137
|
+
self,
|
|
138
|
+
execution_ctx: ExecutionContext,
|
|
139
|
+
ctx: RequestContext,
|
|
140
|
+
params: dict[str, Any],
|
|
141
|
+
) -> RequestAction:
|
|
142
|
+
raw = ctx.body.content if ctx.body else None
|
|
143
|
+
try:
|
|
144
|
+
data = json.loads(raw or b"{}")
|
|
145
|
+
except json.JSONDecodeError:
|
|
146
|
+
return ImmediateResponse(
|
|
147
|
+
status_code=400,
|
|
148
|
+
body=b'{"error":"request body must be valid JSON"}',
|
|
149
|
+
headers={"content-type": "application/json"},
|
|
150
|
+
)
|
|
151
|
+
# Optionally rewrite the body before forwarding
|
|
152
|
+
return UpstreamRequestModifications(body=json.dumps(data).encode())
|
|
153
|
+
```
|
|
154
|
+
|
|
155
|
+
### 3 · Stream and log response chunks
|
|
156
|
+
|
|
157
|
+
```python
|
|
158
|
+
import logging
|
|
159
|
+
from typing import Any
|
|
160
|
+
from apip_sdk_core import (
|
|
161
|
+
BodyProcessingMode,
|
|
162
|
+
ExecutionContext,
|
|
163
|
+
ForwardResponseChunk,
|
|
164
|
+
ProcessingMode,
|
|
165
|
+
ResponseContext,
|
|
166
|
+
ResponseAction,
|
|
167
|
+
ResponseStreamContext,
|
|
168
|
+
StreamBody,
|
|
169
|
+
StreamingResponseAction,
|
|
170
|
+
StreamingResponsePolicy,
|
|
171
|
+
UpstreamRequestModifications,
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
logger = logging.getLogger(__name__)
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class StreamingLogger(StreamingResponsePolicy):
|
|
178
|
+
"""Logs every streamed response chunk and passes it through unchanged."""
|
|
179
|
+
|
|
180
|
+
def mode(self) -> ProcessingMode:
|
|
181
|
+
return ProcessingMode(response_body_mode=BodyProcessingMode.STREAM)
|
|
182
|
+
|
|
183
|
+
def needs_more_response_data(self, accumulated: bytes) -> bool:
|
|
184
|
+
return False # dispatch every chunk immediately
|
|
185
|
+
|
|
186
|
+
def on_response_body(
|
|
187
|
+
self,
|
|
188
|
+
execution_ctx: ExecutionContext,
|
|
189
|
+
ctx: ResponseContext,
|
|
190
|
+
params: dict[str, Any],
|
|
191
|
+
) -> ResponseAction:
|
|
192
|
+
return None # fallback – never called in STREAM mode
|
|
193
|
+
|
|
194
|
+
def on_response_body_chunk(
|
|
195
|
+
self,
|
|
196
|
+
execution_ctx: ExecutionContext,
|
|
197
|
+
ctx: ResponseStreamContext,
|
|
198
|
+
chunk: StreamBody,
|
|
199
|
+
params: dict[str, Any],
|
|
200
|
+
) -> StreamingResponseAction:
|
|
201
|
+
logger.debug(
|
|
202
|
+
"chunk #%d eos=%s bytes=%d",
|
|
203
|
+
chunk.index,
|
|
204
|
+
chunk.end_of_stream,
|
|
205
|
+
len(chunk.chunk),
|
|
206
|
+
)
|
|
207
|
+
return ForwardResponseChunk(body=chunk.chunk)
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
---
|
|
211
|
+
|
|
212
|
+
## Policy types
|
|
213
|
+
|
|
214
|
+
All policy interfaces live in `apip_sdk_core` (re-exported from `apip_sdk_core.policy.v1alpha2`).
|
|
215
|
+
|
|
216
|
+
| Interface | Hook called | Typical use |
|
|
217
|
+
|---|---|---|
|
|
218
|
+
| `RequestHeaderPolicy` | `on_request_headers` | Auth, routing, header injection |
|
|
219
|
+
| `RequestPolicy` | `on_request_body` | Body validation, transformation |
|
|
220
|
+
| `ResponseHeaderPolicy` | `on_response_headers` | CORS, response header rewriting |
|
|
221
|
+
| `ResponsePolicy` | `on_response_body` | Response body transformation |
|
|
222
|
+
| `StreamingRequestPolicy` | `on_request_body_chunk` | Streaming body inspection |
|
|
223
|
+
| `StreamingResponsePolicy` | `on_response_body_chunk` | Streaming response inspection / SSE |
|
|
224
|
+
|
|
225
|
+
Every interface extends the base `Policy` class:
|
|
226
|
+
|
|
227
|
+
```python
|
|
228
|
+
class Policy(ABC):
|
|
229
|
+
@abstractmethod
|
|
230
|
+
def mode(self) -> ProcessingMode: ...
|
|
231
|
+
|
|
232
|
+
def close(self) -> None: ... # optional – release resources
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
### RequestHeaderPolicy
|
|
236
|
+
|
|
237
|
+
Called **before** the request body is read.
|
|
238
|
+
|
|
239
|
+
```python
|
|
240
|
+
class RequestHeaderPolicy(Policy, ABC):
|
|
241
|
+
@abstractmethod
|
|
242
|
+
def on_request_headers(
|
|
243
|
+
self,
|
|
244
|
+
execution_ctx: ExecutionContext,
|
|
245
|
+
ctx: RequestHeaderContext,
|
|
246
|
+
params: dict[str, Any],
|
|
247
|
+
) -> RequestHeaderAction: ...
|
|
248
|
+
```
|
|
249
|
+
|
|
250
|
+
**Returns** `UpstreamRequestHeaderModifications | ImmediateResponse | None`
|
|
251
|
+
|
|
252
|
+
### RequestPolicy
|
|
253
|
+
|
|
254
|
+
Called once with the **complete buffered** request body.
|
|
255
|
+
|
|
256
|
+
```python
|
|
257
|
+
class RequestPolicy(Policy, ABC):
|
|
258
|
+
@abstractmethod
|
|
259
|
+
def on_request_body(
|
|
260
|
+
self,
|
|
261
|
+
execution_ctx: ExecutionContext,
|
|
262
|
+
ctx: RequestContext,
|
|
263
|
+
params: dict[str, Any],
|
|
264
|
+
) -> RequestAction: ...
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
**Returns** `UpstreamRequestModifications | ImmediateResponse | None`
|
|
268
|
+
|
|
269
|
+
### ResponseHeaderPolicy
|
|
270
|
+
|
|
271
|
+
Called **before** the response body is read.
|
|
272
|
+
|
|
273
|
+
```python
|
|
274
|
+
class ResponseHeaderPolicy(Policy, ABC):
|
|
275
|
+
@abstractmethod
|
|
276
|
+
def on_response_headers(
|
|
277
|
+
self,
|
|
278
|
+
execution_ctx: ExecutionContext,
|
|
279
|
+
ctx: ResponseHeaderContext,
|
|
280
|
+
params: dict[str, Any],
|
|
281
|
+
) -> ResponseHeaderAction: ...
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
**Returns** `DownstreamResponseHeaderModifications | ImmediateResponse | None`
|
|
285
|
+
|
|
286
|
+
### ResponsePolicy
|
|
287
|
+
|
|
288
|
+
Called once with the **complete buffered** response body.
|
|
289
|
+
|
|
290
|
+
```python
|
|
291
|
+
class ResponsePolicy(Policy, ABC):
|
|
292
|
+
@abstractmethod
|
|
293
|
+
def on_response_body(
|
|
294
|
+
self,
|
|
295
|
+
execution_ctx: ExecutionContext,
|
|
296
|
+
ctx: ResponseContext,
|
|
297
|
+
params: dict[str, Any],
|
|
298
|
+
) -> ResponseAction: ...
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
**Returns** `DownstreamResponseModifications | ImmediateResponse | None`
|
|
302
|
+
|
|
303
|
+
### StreamingRequestPolicy
|
|
304
|
+
|
|
305
|
+
Extends `RequestPolicy`. Called **chunk-by-chunk** for the request body.
|
|
306
|
+
|
|
307
|
+
```python
|
|
308
|
+
class StreamingRequestPolicy(RequestPolicy, ABC):
|
|
309
|
+
@abstractmethod
|
|
310
|
+
def needs_more_request_data(self, accumulated: bytes) -> bool: ...
|
|
311
|
+
|
|
312
|
+
@abstractmethod
|
|
313
|
+
def on_request_body_chunk(
|
|
314
|
+
self,
|
|
315
|
+
execution_ctx: ExecutionContext,
|
|
316
|
+
ctx: RequestStreamContext,
|
|
317
|
+
chunk: StreamBody,
|
|
318
|
+
params: dict[str, Any],
|
|
319
|
+
) -> StreamingRequestAction: ...
|
|
320
|
+
```
|
|
321
|
+
|
|
322
|
+
**Returns** `ForwardRequestChunk | None`
|
|
323
|
+
|
|
324
|
+
### StreamingResponsePolicy
|
|
325
|
+
|
|
326
|
+
Extends `ResponsePolicy`. Called **chunk-by-chunk** for the response body.
|
|
327
|
+
|
|
328
|
+
```python
|
|
329
|
+
class StreamingResponsePolicy(ResponsePolicy, ABC):
|
|
330
|
+
@abstractmethod
|
|
331
|
+
def needs_more_response_data(self, accumulated: bytes) -> bool: ...
|
|
332
|
+
|
|
333
|
+
@abstractmethod
|
|
334
|
+
def on_response_body_chunk(
|
|
335
|
+
self,
|
|
336
|
+
execution_ctx: ExecutionContext,
|
|
337
|
+
ctx: ResponseStreamContext,
|
|
338
|
+
chunk: StreamBody,
|
|
339
|
+
params: dict[str, Any],
|
|
340
|
+
) -> StreamingResponseAction: ...
|
|
341
|
+
```
|
|
342
|
+
|
|
343
|
+
**Returns** `ForwardResponseChunk | TerminateResponseChunk | None`
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Processing mode
|
|
348
|
+
|
|
349
|
+
The `mode()` method declares which pipeline phases your policy wants to be called for. The gateway uses this to skip unnecessary work.
|
|
350
|
+
|
|
351
|
+
```python
|
|
352
|
+
@dataclass(slots=True)
|
|
353
|
+
class ProcessingMode:
|
|
354
|
+
request_header_mode: HeaderProcessingMode = HeaderProcessingMode.SKIP
|
|
355
|
+
request_body_mode: BodyProcessingMode = BodyProcessingMode.SKIP
|
|
356
|
+
response_header_mode: HeaderProcessingMode = HeaderProcessingMode.SKIP
|
|
357
|
+
response_body_mode: BodyProcessingMode = BodyProcessingMode.SKIP
|
|
358
|
+
```
|
|
359
|
+
|
|
360
|
+
### `HeaderProcessingMode`
|
|
361
|
+
|
|
362
|
+
| Value | Meaning |
|
|
363
|
+
|---|---|
|
|
364
|
+
| `SKIP` | Do not invoke the header hook for this policy |
|
|
365
|
+
| `PROCESS` | Invoke the header hook |
|
|
366
|
+
|
|
367
|
+
### `BodyProcessingMode`
|
|
368
|
+
|
|
369
|
+
| Value | Meaning |
|
|
370
|
+
|---|---|
|
|
371
|
+
| `SKIP` | Do not read the body for this policy |
|
|
372
|
+
| `BUFFER` | Buffer the entire body and call the body hook once |
|
|
373
|
+
| `STREAM` | Call the chunk hook for each body segment |
|
|
374
|
+
|
|
375
|
+
---
|
|
376
|
+
|
|
377
|
+
## Actions reference
|
|
378
|
+
|
|
379
|
+
Return an action dataclass (or `None` to pass through unchanged) from your policy hook.
|
|
380
|
+
|
|
381
|
+
### Request actions
|
|
382
|
+
|
|
383
|
+
| Dataclass | Effect |
|
|
384
|
+
|---|---|
|
|
385
|
+
| `UpstreamRequestHeaderModifications` | Modify headers / path / method / query params before forwarding to upstream |
|
|
386
|
+
| `UpstreamRequestModifications` | Modify body **and** headers / path / method / query params before forwarding |
|
|
387
|
+
| `ImmediateResponse` | Short-circuit the request and return a response to the client immediately |
|
|
388
|
+
| `None` | Pass through without modifications |
|
|
389
|
+
|
|
390
|
+
### Response actions
|
|
391
|
+
|
|
392
|
+
| Dataclass | Effect |
|
|
393
|
+
|---|---|
|
|
394
|
+
| `DownstreamResponseHeaderModifications` | Modify response headers before sending to client |
|
|
395
|
+
| `DownstreamResponseModifications` | Modify response body and headers before sending to client |
|
|
396
|
+
| `ImmediateResponse` | Replace the upstream response with a synthetic one |
|
|
397
|
+
| `None` | Pass through without modifications |
|
|
398
|
+
|
|
399
|
+
### Streaming actions
|
|
400
|
+
|
|
401
|
+
| Dataclass | Effect |
|
|
402
|
+
|---|---|
|
|
403
|
+
| `ForwardRequestChunk` | Forward the (optionally rewritten) chunk upstream |
|
|
404
|
+
| `ForwardResponseChunk` | Forward the (optionally rewritten) chunk downstream |
|
|
405
|
+
| `TerminateResponseChunk` | Send a final chunk downstream and end the stream |
|
|
406
|
+
| `None` | Drop the chunk (use with care) |
|
|
407
|
+
|
|
408
|
+
### `ImmediateResponse` fields
|
|
409
|
+
|
|
410
|
+
```python
|
|
411
|
+
@dataclass(slots=True)
|
|
412
|
+
class ImmediateResponse:
|
|
413
|
+
status_code: int = 500
|
|
414
|
+
headers: dict[str, str] = field(default_factory=dict)
|
|
415
|
+
body: bytes | None = None
|
|
416
|
+
analytics_metadata: dict[str, Any] = field(default_factory=dict)
|
|
417
|
+
dynamic_metadata: dict[str, dict[str, Any]] = field(default_factory=dict)
|
|
418
|
+
analytics_header_filter: DropHeaderAction = field(default_factory=DropHeaderAction)
|
|
419
|
+
```
|
|
420
|
+
|
|
421
|
+
---
|
|
422
|
+
|
|
423
|
+
## Context types
|
|
424
|
+
|
|
425
|
+
### `ExecutionContext`
|
|
426
|
+
|
|
427
|
+
Injected into every hook. Contains tracing information and a cancellation check.
|
|
428
|
+
|
|
429
|
+
```python
|
|
430
|
+
@dataclass(slots=True)
|
|
431
|
+
class ExecutionContext:
|
|
432
|
+
request_id: str
|
|
433
|
+
phase: ExecutionPhase
|
|
434
|
+
deadline: datetime | None = None
|
|
435
|
+
route_name: str = ""
|
|
436
|
+
policy_name: str = ""
|
|
437
|
+
policy_version: str = ""
|
|
438
|
+
trace_id: str | None = None
|
|
439
|
+
span_id: str | None = None
|
|
440
|
+
|
|
441
|
+
def is_cancelled(self) -> bool: ... # check before expensive operations
|
|
442
|
+
```
|
|
443
|
+
|
|
444
|
+
### `SharedContext`
|
|
445
|
+
|
|
446
|
+
Accessible via `ctx.shared` on every context object.
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
@dataclass(slots=True)
|
|
450
|
+
class SharedContext:
|
|
451
|
+
project_id: str
|
|
452
|
+
request_id: str
|
|
453
|
+
api_id: str
|
|
454
|
+
api_name: str
|
|
455
|
+
api_version: str
|
|
456
|
+
api_kind: str
|
|
457
|
+
api_context: str
|
|
458
|
+
operation_path: str
|
|
459
|
+
auth_context: AuthContext | None
|
|
460
|
+
metadata: dict[str, Any] # cross-policy mutable bag
|
|
461
|
+
```
|
|
462
|
+
|
|
463
|
+
### `AuthContext`
|
|
464
|
+
|
|
465
|
+
Populated when authentication has run upstream of your policy.
|
|
466
|
+
|
|
467
|
+
```python
|
|
468
|
+
@dataclass(slots=True)
|
|
469
|
+
class AuthContext:
|
|
470
|
+
authenticated: bool
|
|
471
|
+
authorized: bool
|
|
472
|
+
auth_type: str # e.g. "JWT", "APIKey"
|
|
473
|
+
subject: str
|
|
474
|
+
issuer: str
|
|
475
|
+
audience: list[str]
|
|
476
|
+
scopes: dict[str, bool]
|
|
477
|
+
credential_id: str
|
|
478
|
+
properties: dict[str, str]
|
|
479
|
+
previous: AuthContext | None # previous auth layer result
|
|
480
|
+
```
|
|
481
|
+
|
|
482
|
+
### `Headers`
|
|
483
|
+
|
|
484
|
+
Read-only multi-value header wrapper. All lookups are **case-insensitive**.
|
|
485
|
+
|
|
486
|
+
```python
|
|
487
|
+
headers = ctx.headers
|
|
488
|
+
|
|
489
|
+
headers.get("content-type") # -> list[str]
|
|
490
|
+
headers.has("authorization") # -> bool
|
|
491
|
+
headers.get_all() # -> dict[str, list[str]]
|
|
492
|
+
|
|
493
|
+
for name, values in headers.iterate():
|
|
494
|
+
print(name, values)
|
|
495
|
+
```
|
|
496
|
+
|
|
497
|
+
---
|
|
498
|
+
|
|
499
|
+
## Versioned imports
|
|
500
|
+
|
|
501
|
+
The top-level package always re-exports the **latest stable contract**. Use these imports in most cases:
|
|
502
|
+
|
|
503
|
+
```python
|
|
504
|
+
from apip_sdk_core import RequestPolicy, ProcessingMode, ImmediateResponse
|
|
505
|
+
```
|
|
506
|
+
|
|
507
|
+
If you need to pin to a specific contract version (e.g. in a library):
|
|
508
|
+
|
|
509
|
+
```python
|
|
510
|
+
from apip_sdk_core.policy.v1alpha2 import RequestPolicy, ProcessingMode
|
|
511
|
+
```
|
|
512
|
+
|
|
513
|
+
The current stable contract is `v1alpha2`.
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
## Development
|
|
518
|
+
|
|
519
|
+
### Build
|
|
520
|
+
|
|
521
|
+
```bash
|
|
522
|
+
python -m build
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
### Run tests
|
|
526
|
+
|
|
527
|
+
```bash
|
|
528
|
+
python -m unittest discover -s tests
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
### Type-check
|
|
532
|
+
|
|
533
|
+
```bash
|
|
534
|
+
mypy src/
|
|
535
|
+
```
|
|
536
|
+
|
|
537
|
+
---
|
|
538
|
+
|
|
539
|
+
## License
|
|
540
|
+
|
|
541
|
+
Apache License 2.0 — see [LICENSE](https://github.com/wso2/api-platform/blob/main/LICENSE) for details.
|
|
542
|
+
|
|
543
|
+
© 2026 WSO2 LLC.
|