eyeriss-plugin-sdk 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.
- eyeriss_plugin_sdk-0.1.0/PKG-INFO +20 -0
- eyeriss_plugin_sdk-0.1.0/eyeriss_plugin_sdk/__init__.py +36 -0
- eyeriss_plugin_sdk-0.1.0/eyeriss_plugin_sdk/plugin.py +105 -0
- eyeriss_plugin_sdk-0.1.0/eyeriss_plugin_sdk/types.py +168 -0
- eyeriss_plugin_sdk-0.1.0/eyeriss_plugin_sdk.egg-info/PKG-INFO +20 -0
- eyeriss_plugin_sdk-0.1.0/eyeriss_plugin_sdk.egg-info/SOURCES.txt +10 -0
- eyeriss_plugin_sdk-0.1.0/eyeriss_plugin_sdk.egg-info/dependency_links.txt +1 -0
- eyeriss_plugin_sdk-0.1.0/eyeriss_plugin_sdk.egg-info/top_level.txt +1 -0
- eyeriss_plugin_sdk-0.1.0/pyproject.toml +40 -0
- eyeriss_plugin_sdk-0.1.0/setup.cfg +4 -0
- eyeriss_plugin_sdk-0.1.0/tests/test_plugin.py +83 -0
- eyeriss_plugin_sdk-0.1.0/tests/test_types.py +316 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eyeriss-plugin-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Eyeriss API Gateway Plugin SDK for Python
|
|
5
|
+
Author-email: Eyeriss <dev@eyeriss.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/eyeriss/eyeriss-sdk
|
|
8
|
+
Project-URL: Documentation, https://docs.eyeriss.io/plugins/sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/eyeriss/eyeriss-sdk
|
|
10
|
+
Keywords: eyeriss,api-gateway,plugin,sdk
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
18
|
+
Classifier: Topic :: System :: Networking
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Eyeriss Plugin SDK for Python
|
|
3
|
+
|
|
4
|
+
Provides the base class and data types for building Python plugins
|
|
5
|
+
for the Eyeriss API Gateway.
|
|
6
|
+
|
|
7
|
+
Usage:
|
|
8
|
+
from eyeriss_plugin_sdk import EyerissPlugin
|
|
9
|
+
from eyeriss_plugin_sdk.types import PluginContext, PluginResult, ErrorResponse
|
|
10
|
+
|
|
11
|
+
class Plugin(EyerissPlugin):
|
|
12
|
+
@property
|
|
13
|
+
def plugin_id(self) -> str:
|
|
14
|
+
return "my-plugin"
|
|
15
|
+
|
|
16
|
+
async def pre_request(self, context: PluginContext) -> PluginResult:
|
|
17
|
+
return PluginResult(proceed=True)
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from eyeriss_plugin_sdk.plugin import EyerissPlugin
|
|
21
|
+
from eyeriss_plugin_sdk.types import (
|
|
22
|
+
ErrorResponse,
|
|
23
|
+
PluginContext,
|
|
24
|
+
PluginResult,
|
|
25
|
+
SerializedRequest,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__all__ = [
|
|
29
|
+
"EyerissPlugin",
|
|
30
|
+
"ErrorResponse",
|
|
31
|
+
"PluginContext",
|
|
32
|
+
"PluginResult",
|
|
33
|
+
"SerializedRequest",
|
|
34
|
+
]
|
|
35
|
+
|
|
36
|
+
__version__ = "0.1.0"
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
"""
|
|
2
|
+
EyerissPlugin Abstract Base Class
|
|
3
|
+
|
|
4
|
+
Base class for all Python plugins in the Eyeriss Gateway. Plugin authors
|
|
5
|
+
subclass this and implement the hooks they need. Default implementations
|
|
6
|
+
return proceed=True so plugins only need to override hooks they care about.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from abc import ABC, abstractmethod
|
|
10
|
+
|
|
11
|
+
from eyeriss_plugin_sdk.types import PluginContext, PluginResult
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class EyerissPlugin(ABC):
|
|
15
|
+
"""
|
|
16
|
+
Base class for all Python plugins in the Eyeriss Gateway.
|
|
17
|
+
|
|
18
|
+
Plugin authors must:
|
|
19
|
+
1. Subclass this class
|
|
20
|
+
2. Implement the `plugin_id` property (must match manifest.json id)
|
|
21
|
+
3. Override one or more hook methods
|
|
22
|
+
|
|
23
|
+
Example:
|
|
24
|
+
class Plugin(EyerissPlugin):
|
|
25
|
+
@property
|
|
26
|
+
def plugin_id(self) -> str:
|
|
27
|
+
return "my-custom-plugin"
|
|
28
|
+
|
|
29
|
+
async def pre_request(self, context: PluginContext) -> PluginResult:
|
|
30
|
+
return PluginResult(
|
|
31
|
+
proceed=True,
|
|
32
|
+
modified_headers={"X-Plugin-Processed": "true"},
|
|
33
|
+
)
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
@property
|
|
37
|
+
@abstractmethod
|
|
38
|
+
def plugin_id(self) -> str:
|
|
39
|
+
"""Unique plugin identifier (must match manifest.json 'id' field)."""
|
|
40
|
+
...
|
|
41
|
+
|
|
42
|
+
async def pre_request(self, context: PluginContext) -> PluginResult:
|
|
43
|
+
"""
|
|
44
|
+
Called after auth/RBAC pass, before proxying to backend.
|
|
45
|
+
|
|
46
|
+
Return PluginResult with proceed=False to block the request.
|
|
47
|
+
Return modified_headers to add/override outbound headers.
|
|
48
|
+
Return modified_body to replace the request body.
|
|
49
|
+
|
|
50
|
+
Default: proceed without modification.
|
|
51
|
+
"""
|
|
52
|
+
return PluginResult(proceed=True)
|
|
53
|
+
|
|
54
|
+
async def post_response(self, context: PluginContext) -> PluginResult:
|
|
55
|
+
"""
|
|
56
|
+
Called after backend response, before returning to client.
|
|
57
|
+
|
|
58
|
+
Return modified_headers to add/override response headers.
|
|
59
|
+
Return modified_body to replace the response body.
|
|
60
|
+
proceed=False is ignored for post_response (response already received).
|
|
61
|
+
|
|
62
|
+
Default: pass-through without modification.
|
|
63
|
+
"""
|
|
64
|
+
return PluginResult(proceed=True)
|
|
65
|
+
|
|
66
|
+
async def on_error(self, context: PluginContext, error_info: dict) -> None:
|
|
67
|
+
"""
|
|
68
|
+
Called on gateway error (auth failure, 5xx, timeout, etc.).
|
|
69
|
+
|
|
70
|
+
Observational only — return value is ignored. Cannot modify the
|
|
71
|
+
error response. Use for logging, alerting, or metrics.
|
|
72
|
+
|
|
73
|
+
Default: no-op.
|
|
74
|
+
"""
|
|
75
|
+
pass
|
|
76
|
+
|
|
77
|
+
async def initialize(self) -> None:
|
|
78
|
+
"""
|
|
79
|
+
Called once when the plugin is loaded at gateway startup.
|
|
80
|
+
Override for setup tasks (initialize state, validate config, etc.).
|
|
81
|
+
|
|
82
|
+
Default: no-op.
|
|
83
|
+
"""
|
|
84
|
+
pass
|
|
85
|
+
|
|
86
|
+
async def configure(self, config: dict) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Called after initialize() with the plugin's configuration.
|
|
89
|
+
|
|
90
|
+
Override to apply settings from the admin UI / database config.
|
|
91
|
+
Use this instead of reading environment variables directly so
|
|
92
|
+
that configuration changes via the UI take effect immediately.
|
|
93
|
+
|
|
94
|
+
Default: no-op.
|
|
95
|
+
"""
|
|
96
|
+
pass
|
|
97
|
+
|
|
98
|
+
async def cleanup(self) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Called when the plugin is unloaded (gateway shutdown or plugin removal).
|
|
101
|
+
Override for cleanup tasks (close connections, flush buffers, etc.).
|
|
102
|
+
|
|
103
|
+
Default: no-op.
|
|
104
|
+
"""
|
|
105
|
+
pass
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Eyeriss Plugin SDK — Data Types
|
|
3
|
+
|
|
4
|
+
Serializable data types for plugin hook arguments and return values.
|
|
5
|
+
All types are fully JSON-serializable for WASM compatibility.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from dataclasses import dataclass
|
|
9
|
+
from typing import Optional
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
@dataclass
|
|
13
|
+
class SerializedRequest:
|
|
14
|
+
"""
|
|
15
|
+
Serialized representation of an HTTP request for plugin consumption.
|
|
16
|
+
Body is size-limited to prevent memory issues with large uploads.
|
|
17
|
+
"""
|
|
18
|
+
method: str
|
|
19
|
+
path: str
|
|
20
|
+
headers: dict # {header_name: value} — lowercase keys
|
|
21
|
+
query_params: dict # {param_name: value}
|
|
22
|
+
content_type: str = ""
|
|
23
|
+
body: Optional[bytes] = None
|
|
24
|
+
|
|
25
|
+
def to_dict(self) -> dict:
|
|
26
|
+
"""Serialize to a JSON-compatible dict (body as base64)."""
|
|
27
|
+
import base64
|
|
28
|
+
d = {
|
|
29
|
+
"method": self.method,
|
|
30
|
+
"path": self.path,
|
|
31
|
+
"headers": self.headers,
|
|
32
|
+
"query_params": self.query_params,
|
|
33
|
+
"content_type": self.content_type,
|
|
34
|
+
}
|
|
35
|
+
if self.body is not None:
|
|
36
|
+
d["body"] = base64.b64encode(self.body).decode("ascii")
|
|
37
|
+
d["body_encoding"] = "base64"
|
|
38
|
+
else:
|
|
39
|
+
d["body"] = None
|
|
40
|
+
return d
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def from_dict(cls, d: dict) -> "SerializedRequest":
|
|
44
|
+
"""Deserialize from a JSON-compatible dict."""
|
|
45
|
+
import base64
|
|
46
|
+
body = None
|
|
47
|
+
if d.get("body") is not None:
|
|
48
|
+
if d.get("body_encoding") == "base64":
|
|
49
|
+
body = base64.b64decode(d["body"])
|
|
50
|
+
elif isinstance(d["body"], bytes):
|
|
51
|
+
body = d["body"]
|
|
52
|
+
else:
|
|
53
|
+
body = d["body"].encode("utf-8")
|
|
54
|
+
return cls(
|
|
55
|
+
method=d["method"],
|
|
56
|
+
path=d["path"],
|
|
57
|
+
headers=d.get("headers", {}),
|
|
58
|
+
query_params=d.get("query_params", {}),
|
|
59
|
+
content_type=d.get("content_type", ""),
|
|
60
|
+
body=body,
|
|
61
|
+
)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
@dataclass
|
|
65
|
+
class ErrorResponse:
|
|
66
|
+
"""Error response returned by a plugin when blocking a request."""
|
|
67
|
+
status_code: int
|
|
68
|
+
body: dict
|
|
69
|
+
headers: Optional[dict] = None
|
|
70
|
+
|
|
71
|
+
def to_dict(self) -> dict:
|
|
72
|
+
d = {"status_code": self.status_code, "body": self.body}
|
|
73
|
+
if self.headers:
|
|
74
|
+
d["headers"] = self.headers
|
|
75
|
+
return d
|
|
76
|
+
|
|
77
|
+
@classmethod
|
|
78
|
+
def from_dict(cls, d: dict) -> "ErrorResponse":
|
|
79
|
+
return cls(
|
|
80
|
+
status_code=d["status_code"],
|
|
81
|
+
body=d["body"],
|
|
82
|
+
headers=d.get("headers"),
|
|
83
|
+
)
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
@dataclass
|
|
87
|
+
class PluginResult:
|
|
88
|
+
"""
|
|
89
|
+
Result returned by a plugin hook execution.
|
|
90
|
+
|
|
91
|
+
- proceed=True: request continues to next plugin or to backend
|
|
92
|
+
- proceed=False: request is blocked; error_response is returned to client
|
|
93
|
+
- modified_headers: headers to merge into outbound request (pre_request)
|
|
94
|
+
or response (post_response)
|
|
95
|
+
- modified_body: replacement body bytes, or None to keep original
|
|
96
|
+
"""
|
|
97
|
+
proceed: bool = True
|
|
98
|
+
modified_headers: Optional[dict] = None
|
|
99
|
+
modified_body: Optional[bytes] = None
|
|
100
|
+
error_response: Optional[ErrorResponse] = None
|
|
101
|
+
|
|
102
|
+
def to_dict(self) -> dict:
|
|
103
|
+
import base64
|
|
104
|
+
d = {"proceed": self.proceed}
|
|
105
|
+
if self.modified_headers is not None:
|
|
106
|
+
d["modified_headers"] = self.modified_headers
|
|
107
|
+
if self.modified_body is not None:
|
|
108
|
+
d["modified_body"] = base64.b64encode(self.modified_body).decode("ascii")
|
|
109
|
+
d["modified_body_encoding"] = "base64"
|
|
110
|
+
if self.error_response is not None:
|
|
111
|
+
d["error_response"] = self.error_response.to_dict()
|
|
112
|
+
return d
|
|
113
|
+
|
|
114
|
+
@classmethod
|
|
115
|
+
def from_dict(cls, d: dict) -> "PluginResult":
|
|
116
|
+
import base64
|
|
117
|
+
modified_body = None
|
|
118
|
+
if d.get("modified_body") is not None:
|
|
119
|
+
if d.get("modified_body_encoding") == "base64":
|
|
120
|
+
modified_body = base64.b64decode(d["modified_body"])
|
|
121
|
+
elif isinstance(d["modified_body"], bytes):
|
|
122
|
+
modified_body = d["modified_body"]
|
|
123
|
+
error_response = None
|
|
124
|
+
if d.get("error_response"):
|
|
125
|
+
error_response = ErrorResponse.from_dict(d["error_response"])
|
|
126
|
+
return cls(
|
|
127
|
+
proceed=d.get("proceed", True),
|
|
128
|
+
modified_headers=d.get("modified_headers"),
|
|
129
|
+
modified_body=modified_body,
|
|
130
|
+
error_response=error_response,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
@dataclass
|
|
135
|
+
class PluginContext:
|
|
136
|
+
"""
|
|
137
|
+
Context passed to plugin hooks.
|
|
138
|
+
|
|
139
|
+
Contains serialized request data and auth information. Must be fully
|
|
140
|
+
JSON-serializable for WASM compatibility.
|
|
141
|
+
"""
|
|
142
|
+
request: SerializedRequest
|
|
143
|
+
application_id: int
|
|
144
|
+
endpoint_id: int
|
|
145
|
+
auth_result: dict
|
|
146
|
+
request_id: str
|
|
147
|
+
client_ip: str
|
|
148
|
+
|
|
149
|
+
def to_dict(self) -> dict:
|
|
150
|
+
return {
|
|
151
|
+
"request": self.request.to_dict(),
|
|
152
|
+
"application_id": self.application_id,
|
|
153
|
+
"endpoint_id": self.endpoint_id,
|
|
154
|
+
"auth_result": self.auth_result,
|
|
155
|
+
"request_id": self.request_id,
|
|
156
|
+
"client_ip": self.client_ip,
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
@classmethod
|
|
160
|
+
def from_dict(cls, d: dict) -> "PluginContext":
|
|
161
|
+
return cls(
|
|
162
|
+
request=SerializedRequest.from_dict(d["request"]),
|
|
163
|
+
application_id=d["application_id"],
|
|
164
|
+
endpoint_id=d["endpoint_id"],
|
|
165
|
+
auth_result=d.get("auth_result", {}),
|
|
166
|
+
request_id=d["request_id"],
|
|
167
|
+
client_ip=d["client_ip"],
|
|
168
|
+
)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: eyeriss-plugin-sdk
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Eyeriss API Gateway Plugin SDK for Python
|
|
5
|
+
Author-email: Eyeriss <dev@eyeriss.io>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/eyeriss/eyeriss-sdk
|
|
8
|
+
Project-URL: Documentation, https://docs.eyeriss.io/plugins/sdk
|
|
9
|
+
Project-URL: Repository, https://github.com/eyeriss/eyeriss-sdk
|
|
10
|
+
Keywords: eyeriss,api-gateway,plugin,sdk
|
|
11
|
+
Classifier: Development Status :: 3 - Alpha
|
|
12
|
+
Classifier: Intended Audience :: Developers
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Topic :: Internet :: WWW/HTTP :: HTTP Servers
|
|
18
|
+
Classifier: Topic :: System :: Networking
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Python: >=3.10
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
pyproject.toml
|
|
2
|
+
eyeriss_plugin_sdk/__init__.py
|
|
3
|
+
eyeriss_plugin_sdk/plugin.py
|
|
4
|
+
eyeriss_plugin_sdk/types.py
|
|
5
|
+
eyeriss_plugin_sdk.egg-info/PKG-INFO
|
|
6
|
+
eyeriss_plugin_sdk.egg-info/SOURCES.txt
|
|
7
|
+
eyeriss_plugin_sdk.egg-info/dependency_links.txt
|
|
8
|
+
eyeriss_plugin_sdk.egg-info/top_level.txt
|
|
9
|
+
tests/test_plugin.py
|
|
10
|
+
tests/test_types.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
eyeriss_plugin_sdk
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=68.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "eyeriss-plugin-sdk"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Eyeriss API Gateway Plugin SDK for Python"
|
|
9
|
+
license = {text = "MIT"}
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
authors = [
|
|
12
|
+
{ name = "Eyeriss", email = "dev@eyeriss.io" },
|
|
13
|
+
]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"Programming Language :: Python :: 3",
|
|
18
|
+
"Programming Language :: Python :: 3.10",
|
|
19
|
+
"Programming Language :: Python :: 3.11",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Topic :: Internet :: WWW/HTTP :: HTTP Servers",
|
|
22
|
+
"Topic :: System :: Networking",
|
|
23
|
+
"Typing :: Typed",
|
|
24
|
+
]
|
|
25
|
+
keywords = ["eyeriss", "api-gateway", "plugin", "sdk"]
|
|
26
|
+
|
|
27
|
+
[project.urls]
|
|
28
|
+
Homepage = "https://github.com/eyeriss/eyeriss-sdk"
|
|
29
|
+
Documentation = "https://docs.eyeriss.io/plugins/sdk"
|
|
30
|
+
Repository = "https://github.com/eyeriss/eyeriss-sdk"
|
|
31
|
+
|
|
32
|
+
[tool.pytest.ini_options]
|
|
33
|
+
asyncio_mode = "auto"
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
python_files = ["test_*.py"]
|
|
36
|
+
python_functions = ["test_*"]
|
|
37
|
+
|
|
38
|
+
[tool.setuptools.packages.find]
|
|
39
|
+
where = ["."]
|
|
40
|
+
include = ["eyeriss_plugin_sdk*"]
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
"""Tests for eyeriss_plugin_sdk.plugin — EyerissPlugin ABC."""
|
|
2
|
+
|
|
3
|
+
import pytest
|
|
4
|
+
|
|
5
|
+
from eyeriss_plugin_sdk import EyerissPlugin
|
|
6
|
+
from eyeriss_plugin_sdk.types import PluginContext, PluginResult, SerializedRequest
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def _make_context():
|
|
10
|
+
return PluginContext(
|
|
11
|
+
request=SerializedRequest(
|
|
12
|
+
method="GET", path="/test",
|
|
13
|
+
headers={}, query_params={},
|
|
14
|
+
),
|
|
15
|
+
application_id=1, endpoint_id=1,
|
|
16
|
+
auth_result={}, request_id="test-id",
|
|
17
|
+
client_ip="127.0.0.1",
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class _MinimalPlugin(EyerissPlugin):
|
|
22
|
+
@property
|
|
23
|
+
def plugin_id(self) -> str:
|
|
24
|
+
return "test-minimal-plugin"
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TestEyerissPluginABC:
|
|
28
|
+
|
|
29
|
+
def test_cannot_instantiate_directly(self):
|
|
30
|
+
with pytest.raises(TypeError):
|
|
31
|
+
EyerissPlugin()
|
|
32
|
+
|
|
33
|
+
def test_must_implement_plugin_id(self):
|
|
34
|
+
class Incomplete(EyerissPlugin):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
with pytest.raises(TypeError):
|
|
38
|
+
Incomplete()
|
|
39
|
+
|
|
40
|
+
def test_minimal_subclass_instantiates(self):
|
|
41
|
+
plugin = _MinimalPlugin()
|
|
42
|
+
assert plugin.plugin_id == "test-minimal-plugin"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class TestDefaultHooks:
|
|
46
|
+
|
|
47
|
+
@pytest.mark.asyncio
|
|
48
|
+
async def test_pre_request_returns_proceed(self):
|
|
49
|
+
plugin = _MinimalPlugin()
|
|
50
|
+
result = await plugin.pre_request(_make_context())
|
|
51
|
+
assert isinstance(result, PluginResult)
|
|
52
|
+
assert result.proceed is True
|
|
53
|
+
|
|
54
|
+
@pytest.mark.asyncio
|
|
55
|
+
async def test_post_response_returns_proceed(self):
|
|
56
|
+
plugin = _MinimalPlugin()
|
|
57
|
+
result = await plugin.post_response(_make_context())
|
|
58
|
+
assert isinstance(result, PluginResult)
|
|
59
|
+
assert result.proceed is True
|
|
60
|
+
|
|
61
|
+
@pytest.mark.asyncio
|
|
62
|
+
async def test_on_error_returns_none(self):
|
|
63
|
+
plugin = _MinimalPlugin()
|
|
64
|
+
result = await plugin.on_error(_make_context(), {"error": "timeout"})
|
|
65
|
+
assert result is None
|
|
66
|
+
|
|
67
|
+
@pytest.mark.asyncio
|
|
68
|
+
async def test_initialize_returns_none(self):
|
|
69
|
+
plugin = _MinimalPlugin()
|
|
70
|
+
result = await plugin.initialize()
|
|
71
|
+
assert result is None
|
|
72
|
+
|
|
73
|
+
@pytest.mark.asyncio
|
|
74
|
+
async def test_configure_returns_none(self):
|
|
75
|
+
plugin = _MinimalPlugin()
|
|
76
|
+
result = await plugin.configure({"key": "value"})
|
|
77
|
+
assert result is None
|
|
78
|
+
|
|
79
|
+
@pytest.mark.asyncio
|
|
80
|
+
async def test_cleanup_returns_none(self):
|
|
81
|
+
plugin = _MinimalPlugin()
|
|
82
|
+
result = await plugin.cleanup()
|
|
83
|
+
assert result is None
|
|
@@ -0,0 +1,316 @@
|
|
|
1
|
+
"""Tests for eyeriss_plugin_sdk.types — serialization, deserialization, roundtrips."""
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
|
|
5
|
+
from eyeriss_plugin_sdk.types import (
|
|
6
|
+
ErrorResponse,
|
|
7
|
+
PluginContext,
|
|
8
|
+
PluginResult,
|
|
9
|
+
SerializedRequest,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
# ---------------------------------------------------------------------------
|
|
14
|
+
# SerializedRequest
|
|
15
|
+
# ---------------------------------------------------------------------------
|
|
16
|
+
|
|
17
|
+
class TestSerializedRequest:
|
|
18
|
+
|
|
19
|
+
def test_to_dict_with_body(self):
|
|
20
|
+
req = SerializedRequest(
|
|
21
|
+
method="POST", path="/api/v1/data",
|
|
22
|
+
headers={"content-type": "application/json"},
|
|
23
|
+
query_params={"page": "1"},
|
|
24
|
+
content_type="application/json",
|
|
25
|
+
body=b'{"key": "value"}',
|
|
26
|
+
)
|
|
27
|
+
d = req.to_dict()
|
|
28
|
+
assert d["method"] == "POST"
|
|
29
|
+
assert d["path"] == "/api/v1/data"
|
|
30
|
+
assert d["headers"] == {"content-type": "application/json"}
|
|
31
|
+
assert d["query_params"] == {"page": "1"}
|
|
32
|
+
assert d["content_type"] == "application/json"
|
|
33
|
+
assert d["body_encoding"] == "base64"
|
|
34
|
+
assert base64.b64decode(d["body"]) == b'{"key": "value"}'
|
|
35
|
+
|
|
36
|
+
def test_to_dict_without_body(self):
|
|
37
|
+
req = SerializedRequest(
|
|
38
|
+
method="GET", path="/healthz",
|
|
39
|
+
headers={}, query_params={},
|
|
40
|
+
)
|
|
41
|
+
d = req.to_dict()
|
|
42
|
+
assert d["body"] is None
|
|
43
|
+
assert "body_encoding" not in d
|
|
44
|
+
|
|
45
|
+
def test_from_dict_base64_body(self):
|
|
46
|
+
encoded = base64.b64encode(b"hello").decode("ascii")
|
|
47
|
+
d = {
|
|
48
|
+
"method": "PUT", "path": "/items/1",
|
|
49
|
+
"headers": {}, "query_params": {},
|
|
50
|
+
"body": encoded, "body_encoding": "base64",
|
|
51
|
+
}
|
|
52
|
+
req = SerializedRequest.from_dict(d)
|
|
53
|
+
assert req.body == b"hello"
|
|
54
|
+
|
|
55
|
+
def test_from_dict_string_body(self):
|
|
56
|
+
d = {
|
|
57
|
+
"method": "POST", "path": "/data",
|
|
58
|
+
"headers": {}, "query_params": {},
|
|
59
|
+
"body": "plain text",
|
|
60
|
+
}
|
|
61
|
+
req = SerializedRequest.from_dict(d)
|
|
62
|
+
assert req.body == b"plain text"
|
|
63
|
+
|
|
64
|
+
def test_from_dict_bytes_body(self):
|
|
65
|
+
d = {
|
|
66
|
+
"method": "POST", "path": "/data",
|
|
67
|
+
"headers": {}, "query_params": {},
|
|
68
|
+
"body": b"raw bytes",
|
|
69
|
+
}
|
|
70
|
+
req = SerializedRequest.from_dict(d)
|
|
71
|
+
assert req.body == b"raw bytes"
|
|
72
|
+
|
|
73
|
+
def test_from_dict_none_body(self):
|
|
74
|
+
d = {
|
|
75
|
+
"method": "GET", "path": "/",
|
|
76
|
+
"headers": {}, "query_params": {},
|
|
77
|
+
"body": None,
|
|
78
|
+
}
|
|
79
|
+
req = SerializedRequest.from_dict(d)
|
|
80
|
+
assert req.body is None
|
|
81
|
+
|
|
82
|
+
def test_from_dict_missing_optional_fields(self):
|
|
83
|
+
d = {"method": "GET", "path": "/test"}
|
|
84
|
+
req = SerializedRequest.from_dict(d)
|
|
85
|
+
assert req.headers == {}
|
|
86
|
+
assert req.query_params == {}
|
|
87
|
+
assert req.content_type == ""
|
|
88
|
+
assert req.body is None
|
|
89
|
+
|
|
90
|
+
def test_roundtrip(self):
|
|
91
|
+
original = SerializedRequest(
|
|
92
|
+
method="PATCH", path="/users/42",
|
|
93
|
+
headers={"authorization": "Bearer tok"},
|
|
94
|
+
query_params={"fields": "name,email"},
|
|
95
|
+
content_type="application/json",
|
|
96
|
+
body=b'{"name": "Alice"}',
|
|
97
|
+
)
|
|
98
|
+
restored = SerializedRequest.from_dict(original.to_dict())
|
|
99
|
+
assert restored.method == original.method
|
|
100
|
+
assert restored.path == original.path
|
|
101
|
+
assert restored.headers == original.headers
|
|
102
|
+
assert restored.query_params == original.query_params
|
|
103
|
+
assert restored.content_type == original.content_type
|
|
104
|
+
assert restored.body == original.body
|
|
105
|
+
|
|
106
|
+
def test_roundtrip_no_body(self):
|
|
107
|
+
original = SerializedRequest(
|
|
108
|
+
method="DELETE", path="/items/7",
|
|
109
|
+
headers={}, query_params={},
|
|
110
|
+
)
|
|
111
|
+
restored = SerializedRequest.from_dict(original.to_dict())
|
|
112
|
+
assert restored.body is None
|
|
113
|
+
|
|
114
|
+
def test_defaults(self):
|
|
115
|
+
req = SerializedRequest(method="GET", path="/", headers={}, query_params={})
|
|
116
|
+
assert req.content_type == ""
|
|
117
|
+
assert req.body is None
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
# ---------------------------------------------------------------------------
|
|
121
|
+
# ErrorResponse
|
|
122
|
+
# ---------------------------------------------------------------------------
|
|
123
|
+
|
|
124
|
+
class TestErrorResponse:
|
|
125
|
+
|
|
126
|
+
def test_to_dict_without_headers(self):
|
|
127
|
+
err = ErrorResponse(status_code=403, body={"error": "forbidden"})
|
|
128
|
+
d = err.to_dict()
|
|
129
|
+
assert d == {"status_code": 403, "body": {"error": "forbidden"}}
|
|
130
|
+
assert "headers" not in d
|
|
131
|
+
|
|
132
|
+
def test_to_dict_with_headers(self):
|
|
133
|
+
err = ErrorResponse(
|
|
134
|
+
status_code=429, body={"error": "rate limited"},
|
|
135
|
+
headers={"Retry-After": "60"},
|
|
136
|
+
)
|
|
137
|
+
d = err.to_dict()
|
|
138
|
+
assert d["headers"] == {"Retry-After": "60"}
|
|
139
|
+
|
|
140
|
+
def test_from_dict(self):
|
|
141
|
+
d = {"status_code": 500, "body": {"error": "internal"}, "headers": {"X-Req-Id": "abc"}}
|
|
142
|
+
err = ErrorResponse.from_dict(d)
|
|
143
|
+
assert err.status_code == 500
|
|
144
|
+
assert err.body == {"error": "internal"}
|
|
145
|
+
assert err.headers == {"X-Req-Id": "abc"}
|
|
146
|
+
|
|
147
|
+
def test_from_dict_no_headers(self):
|
|
148
|
+
d = {"status_code": 401, "body": {"error": "unauthorized"}}
|
|
149
|
+
err = ErrorResponse.from_dict(d)
|
|
150
|
+
assert err.headers is None
|
|
151
|
+
|
|
152
|
+
def test_roundtrip(self):
|
|
153
|
+
original = ErrorResponse(status_code=422, body={"detail": "invalid"}, headers={"X-Error": "yes"})
|
|
154
|
+
restored = ErrorResponse.from_dict(original.to_dict())
|
|
155
|
+
assert restored.status_code == original.status_code
|
|
156
|
+
assert restored.body == original.body
|
|
157
|
+
assert restored.headers == original.headers
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
# ---------------------------------------------------------------------------
|
|
161
|
+
# PluginResult
|
|
162
|
+
# ---------------------------------------------------------------------------
|
|
163
|
+
|
|
164
|
+
class TestPluginResult:
|
|
165
|
+
|
|
166
|
+
def test_defaults(self):
|
|
167
|
+
result = PluginResult()
|
|
168
|
+
assert result.proceed is True
|
|
169
|
+
assert result.modified_headers is None
|
|
170
|
+
assert result.modified_body is None
|
|
171
|
+
assert result.error_response is None
|
|
172
|
+
|
|
173
|
+
def test_to_dict_minimal(self):
|
|
174
|
+
result = PluginResult()
|
|
175
|
+
assert result.to_dict() == {"proceed": True}
|
|
176
|
+
|
|
177
|
+
def test_to_dict_with_headers(self):
|
|
178
|
+
result = PluginResult(modified_headers={"X-Custom": "val"})
|
|
179
|
+
d = result.to_dict()
|
|
180
|
+
assert d["modified_headers"] == {"X-Custom": "val"}
|
|
181
|
+
|
|
182
|
+
def test_to_dict_with_body(self):
|
|
183
|
+
result = PluginResult(modified_body=b"new body")
|
|
184
|
+
d = result.to_dict()
|
|
185
|
+
assert d["modified_body_encoding"] == "base64"
|
|
186
|
+
assert base64.b64decode(d["modified_body"]) == b"new body"
|
|
187
|
+
|
|
188
|
+
def test_to_dict_with_error_response(self):
|
|
189
|
+
err = ErrorResponse(status_code=403, body={"msg": "blocked"})
|
|
190
|
+
result = PluginResult(proceed=False, error_response=err)
|
|
191
|
+
d = result.to_dict()
|
|
192
|
+
assert d["proceed"] is False
|
|
193
|
+
assert d["error_response"]["status_code"] == 403
|
|
194
|
+
|
|
195
|
+
def test_from_dict_minimal(self):
|
|
196
|
+
result = PluginResult.from_dict({"proceed": True})
|
|
197
|
+
assert result.proceed is True
|
|
198
|
+
assert result.modified_body is None
|
|
199
|
+
assert result.error_response is None
|
|
200
|
+
|
|
201
|
+
def test_from_dict_defaults_proceed_true(self):
|
|
202
|
+
result = PluginResult.from_dict({})
|
|
203
|
+
assert result.proceed is True
|
|
204
|
+
|
|
205
|
+
def test_from_dict_with_body(self):
|
|
206
|
+
encoded = base64.b64encode(b"modified").decode("ascii")
|
|
207
|
+
result = PluginResult.from_dict({
|
|
208
|
+
"proceed": True,
|
|
209
|
+
"modified_body": encoded,
|
|
210
|
+
"modified_body_encoding": "base64",
|
|
211
|
+
})
|
|
212
|
+
assert result.modified_body == b"modified"
|
|
213
|
+
|
|
214
|
+
def test_from_dict_with_raw_bytes_body(self):
|
|
215
|
+
result = PluginResult.from_dict({
|
|
216
|
+
"proceed": True,
|
|
217
|
+
"modified_body": b"raw",
|
|
218
|
+
})
|
|
219
|
+
assert result.modified_body == b"raw"
|
|
220
|
+
|
|
221
|
+
def test_from_dict_with_error_response(self):
|
|
222
|
+
result = PluginResult.from_dict({
|
|
223
|
+
"proceed": False,
|
|
224
|
+
"error_response": {"status_code": 429, "body": {"error": "throttled"}},
|
|
225
|
+
})
|
|
226
|
+
assert result.proceed is False
|
|
227
|
+
assert result.error_response.status_code == 429
|
|
228
|
+
|
|
229
|
+
def test_roundtrip_full(self):
|
|
230
|
+
err = ErrorResponse(status_code=503, body={"error": "unavailable"}, headers={"Retry-After": "30"})
|
|
231
|
+
original = PluginResult(
|
|
232
|
+
proceed=False,
|
|
233
|
+
modified_headers={"X-Plugin": "test"},
|
|
234
|
+
modified_body=b"replacement",
|
|
235
|
+
error_response=err,
|
|
236
|
+
)
|
|
237
|
+
restored = PluginResult.from_dict(original.to_dict())
|
|
238
|
+
assert restored.proceed == original.proceed
|
|
239
|
+
assert restored.modified_headers == original.modified_headers
|
|
240
|
+
assert restored.modified_body == original.modified_body
|
|
241
|
+
assert restored.error_response.status_code == original.error_response.status_code
|
|
242
|
+
assert restored.error_response.body == original.error_response.body
|
|
243
|
+
assert restored.error_response.headers == original.error_response.headers
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
# ---------------------------------------------------------------------------
|
|
247
|
+
# PluginContext
|
|
248
|
+
# ---------------------------------------------------------------------------
|
|
249
|
+
|
|
250
|
+
class TestPluginContext:
|
|
251
|
+
|
|
252
|
+
def _make_request(self):
|
|
253
|
+
return SerializedRequest(
|
|
254
|
+
method="POST", path="/api/test",
|
|
255
|
+
headers={"content-type": "application/json"},
|
|
256
|
+
query_params={"q": "search"},
|
|
257
|
+
content_type="application/json",
|
|
258
|
+
body=b'{"data": 1}',
|
|
259
|
+
)
|
|
260
|
+
|
|
261
|
+
def test_to_dict(self):
|
|
262
|
+
ctx = PluginContext(
|
|
263
|
+
request=self._make_request(),
|
|
264
|
+
application_id=10, endpoint_id=20,
|
|
265
|
+
auth_result={"user_id": "u1", "role": "admin"},
|
|
266
|
+
request_id="req-abc-123",
|
|
267
|
+
client_ip="192.168.1.100",
|
|
268
|
+
)
|
|
269
|
+
d = ctx.to_dict()
|
|
270
|
+
assert d["application_id"] == 10
|
|
271
|
+
assert d["endpoint_id"] == 20
|
|
272
|
+
assert d["auth_result"]["user_id"] == "u1"
|
|
273
|
+
assert d["request_id"] == "req-abc-123"
|
|
274
|
+
assert d["client_ip"] == "192.168.1.100"
|
|
275
|
+
assert d["request"]["method"] == "POST"
|
|
276
|
+
|
|
277
|
+
def test_from_dict(self):
|
|
278
|
+
req_dict = self._make_request().to_dict()
|
|
279
|
+
d = {
|
|
280
|
+
"request": req_dict,
|
|
281
|
+
"application_id": 5, "endpoint_id": 15,
|
|
282
|
+
"auth_result": {"authenticated": True},
|
|
283
|
+
"request_id": "req-xyz",
|
|
284
|
+
"client_ip": "10.0.0.1",
|
|
285
|
+
}
|
|
286
|
+
ctx = PluginContext.from_dict(d)
|
|
287
|
+
assert ctx.application_id == 5
|
|
288
|
+
assert ctx.request.method == "POST"
|
|
289
|
+
assert ctx.request.body == b'{"data": 1}'
|
|
290
|
+
|
|
291
|
+
def test_from_dict_default_auth_result(self):
|
|
292
|
+
req_dict = self._make_request().to_dict()
|
|
293
|
+
d = {
|
|
294
|
+
"request": req_dict,
|
|
295
|
+
"application_id": 1, "endpoint_id": 2,
|
|
296
|
+
"request_id": "r1", "client_ip": "0.0.0.0",
|
|
297
|
+
}
|
|
298
|
+
ctx = PluginContext.from_dict(d)
|
|
299
|
+
assert ctx.auth_result == {}
|
|
300
|
+
|
|
301
|
+
def test_roundtrip(self):
|
|
302
|
+
original = PluginContext(
|
|
303
|
+
request=self._make_request(),
|
|
304
|
+
application_id=42, endpoint_id=99,
|
|
305
|
+
auth_result={"scope": "read:all"},
|
|
306
|
+
request_id="roundtrip-id",
|
|
307
|
+
client_ip="203.0.113.50",
|
|
308
|
+
)
|
|
309
|
+
restored = PluginContext.from_dict(original.to_dict())
|
|
310
|
+
assert restored.application_id == original.application_id
|
|
311
|
+
assert restored.endpoint_id == original.endpoint_id
|
|
312
|
+
assert restored.auth_result == original.auth_result
|
|
313
|
+
assert restored.request_id == original.request_id
|
|
314
|
+
assert restored.client_ip == original.client_ip
|
|
315
|
+
assert restored.request.method == original.request.method
|
|
316
|
+
assert restored.request.body == original.request.body
|