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.
@@ -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
+ [![PyPI version](https://img.shields.io/pypi/v/apip-sdk-core)](https://pypi.org/project/apip-sdk-core/)
32
+ [![Python versions](https://img.shields.io/pypi/pyversions/apip-sdk-core)](https://pypi.org/project/apip-sdk-core/)
33
+ [![License](https://img.shields.io/badge/license-Apache%202.0-blue)](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.