opentool-python 0.1.0__py3-none-any.whl

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,62 @@
1
+ """OpenTool SDK for Python."""
2
+
3
+ from .dto import (
4
+ CLI_ARGUMENT_APIKEYS,
5
+ CLI_ARGUMENT_HOST,
6
+ CLI_ARGUMENT_PORT,
7
+ CLI_ARGUMENT_TAG,
8
+ DEFAULT_PREFIX,
9
+ DEFAULT_TOOL_HOST,
10
+ DEFAULT_TOOL_PORT,
11
+ DEFAULT_TOOL_TAG,
12
+ HostType,
13
+ JsonRPCHttpRequestBody,
14
+ JsonRPCHttpResponseBody,
15
+ JsonRPCHttpResponseBodyError,
16
+ StatusInfo,
17
+ StatusType,
18
+ Version,
19
+ )
20
+ from .json_loader import OpenToolJsonLoader
21
+ from .llm.model import FunctionCall, ToolReturn
22
+ from .model.data_type import DataType
23
+ from .model.function_model import FunctionModel
24
+ from .model.info import Info
25
+ from .model.opentool import OpenTool
26
+ from .model.parameter import Parameter
27
+ from .model.return_model import Return
28
+ from .model.schema import Schema, SchemaReferenceResolver, SchemaResolver, SchemaType, SchemaValidationResult
29
+ from .model.server import ServerConfig
30
+
31
+ __all__ = [
32
+ "HostType",
33
+ "Version",
34
+ "JsonRPCHttpRequestBody",
35
+ "JsonRPCHttpResponseBody",
36
+ "JsonRPCHttpResponseBodyError",
37
+ "StatusInfo",
38
+ "StatusType",
39
+ "DEFAULT_PREFIX",
40
+ "DEFAULT_TOOL_TAG",
41
+ "DEFAULT_TOOL_HOST",
42
+ "DEFAULT_TOOL_PORT",
43
+ "CLI_ARGUMENT_TAG",
44
+ "CLI_ARGUMENT_HOST",
45
+ "CLI_ARGUMENT_PORT",
46
+ "CLI_ARGUMENT_APIKEYS",
47
+ "FunctionCall",
48
+ "ToolReturn",
49
+ "DataType",
50
+ "FunctionModel",
51
+ "Info",
52
+ "OpenTool",
53
+ "Parameter",
54
+ "Return",
55
+ "Schema",
56
+ "SchemaType",
57
+ "SchemaReferenceResolver",
58
+ "SchemaResolver",
59
+ "SchemaValidationResult",
60
+ "ServerConfig",
61
+ "OpenToolJsonLoader",
62
+ ]
@@ -0,0 +1,18 @@
1
+ from .exceptions import (
2
+ ErrorNullException,
3
+ OpenToolServerCallException,
4
+ OpenToolServerNoAccessException,
5
+ OpenToolServerUnauthorizedException,
6
+ ResponseNullException,
7
+ )
8
+ from .opentool_client import Client, OpenToolClient
9
+
10
+ __all__ = [
11
+ "Client",
12
+ "OpenToolClient",
13
+ "ResponseNullException",
14
+ "ErrorNullException",
15
+ "OpenToolServerUnauthorizedException",
16
+ "OpenToolServerNoAccessException",
17
+ "OpenToolServerCallException",
18
+ ]
@@ -0,0 +1,48 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class ResponseNullException(Exception):
9
+ code: int
10
+ message: str = "Response is null"
11
+
12
+ def to_json(self) -> dict[str, Any]:
13
+ return {"code": self.code, "message": self.message}
14
+
15
+
16
+ @dataclass
17
+ class ErrorNullException(Exception):
18
+ code: int
19
+ message: str = "Error is null"
20
+
21
+ def to_json(self) -> dict[str, Any]:
22
+ return {"code": self.code, "message": self.message}
23
+
24
+
25
+ @dataclass
26
+ class OpenToolServerUnauthorizedException(Exception):
27
+ code: int = 401
28
+ message: str = "Please check API Key is VALID or NOT"
29
+
30
+ def to_json(self) -> dict[str, Any]:
31
+ return {"code": self.code, "message": self.message}
32
+
33
+
34
+ @dataclass
35
+ class OpenToolServerNoAccessException(Exception):
36
+ code: int = 404
37
+ message: str = "Please check OpenTool Server is RUNNING or NOT"
38
+
39
+ def to_json(self) -> dict[str, Any]:
40
+ return {"code": self.code, "message": self.message}
41
+
42
+
43
+ @dataclass
44
+ class OpenToolServerCallException(Exception):
45
+ message: str
46
+
47
+ def to_json(self) -> dict[str, Any]:
48
+ return {"message": self.message}
@@ -0,0 +1,238 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import socket
5
+ from abc import ABC, abstractmethod
6
+ from collections.abc import Callable, Iterator
7
+ from typing import Any
8
+ from urllib import error, parse, request
9
+
10
+ from ..dto import (
11
+ DEFAULT_PREFIX,
12
+ LOAD_METADATA_NOT_FOUND_CODE,
13
+ JsonRPCHttpRequestBody,
14
+ JsonRPCHttpResponseBody,
15
+ StatusInfo,
16
+ Version,
17
+ )
18
+ from ..llm.model import FunctionCall, ToolReturn
19
+ from ..model.opentool import OpenTool
20
+ from ..model.schema import SchemaReferenceResolver
21
+ from ..tool.model import EventType
22
+ from .exceptions import (
23
+ OpenToolServerCallException,
24
+ OpenToolServerNoAccessException,
25
+ OpenToolServerUnauthorizedException,
26
+ )
27
+
28
+
29
+ class Client(ABC):
30
+ @abstractmethod
31
+ def version(self) -> Version:
32
+ raise NotImplementedError
33
+
34
+ @abstractmethod
35
+ def call(self, function_call: FunctionCall) -> ToolReturn:
36
+ raise NotImplementedError
37
+
38
+ @abstractmethod
39
+ def stream_call(
40
+ self,
41
+ function_call: FunctionCall,
42
+ on_stream: Callable[[str, ToolReturn], None],
43
+ ) -> None:
44
+ raise NotImplementedError
45
+
46
+ def load(self) -> OpenTool | None:
47
+ return None
48
+
49
+ @abstractmethod
50
+ def stop(self) -> StatusInfo | None:
51
+ raise NotImplementedError
52
+
53
+
54
+ class OpenToolClient(Client):
55
+ def __init__(
56
+ self,
57
+ *,
58
+ tool_host: str,
59
+ tool_port: int,
60
+ tool_api_key: str | None = None,
61
+ is_ssl: bool = False,
62
+ schema_reference_resolver: SchemaReferenceResolver | None = None,
63
+ ) -> None:
64
+ self.schema_reference_resolver = schema_reference_resolver
65
+ self.is_ssl = is_ssl
66
+ self.host = tool_host
67
+ self.port = tool_port
68
+ self.prefix = DEFAULT_PREFIX
69
+ self.tool_api_key = tool_api_key
70
+ protocol = "https" if self.is_ssl else "http"
71
+ self.base_url = f"{protocol}://{self.host}:{self.port}{self.prefix}"
72
+
73
+ def version(self) -> Version:
74
+ data = self._request_json("GET", "/version")
75
+ return Version.from_json(data)
76
+
77
+ def call(self, function_call: FunctionCall) -> ToolReturn:
78
+ result = self._call_json_rpc_http(
79
+ function_call.id,
80
+ function_call.name,
81
+ function_call.arguments,
82
+ )
83
+ return ToolReturn(id=function_call.id, result=result)
84
+
85
+ def _call_json_rpc_http(
86
+ self,
87
+ request_id: str,
88
+ method: str,
89
+ params: dict[str, Any],
90
+ ) -> dict[str, Any]:
91
+ body = JsonRPCHttpRequestBody(id=request_id, method=method, params=params)
92
+ data = self._request_json("POST", "/call", body.to_json())
93
+ response_body = JsonRPCHttpResponseBody.from_json(data)
94
+ if response_body.error is not None:
95
+ raise OpenToolServerCallException(response_body.error.message)
96
+ return response_body.result
97
+
98
+ def stream_call(
99
+ self,
100
+ function_call: FunctionCall,
101
+ on_stream: Callable[[str, ToolReturn], None],
102
+ ) -> None:
103
+ body = JsonRPCHttpRequestBody(
104
+ id=function_call.id,
105
+ method=function_call.name,
106
+ params=function_call.arguments,
107
+ )
108
+
109
+ for event_name, event_data in self._stream_call_json_rpc_http(body.to_json()):
110
+ event_name = event_name.lower()
111
+ if event_name == EventType.START:
112
+ on_stream(event_name, ToolReturn(id=function_call.id, result=event_data))
113
+ elif event_name == EventType.DONE:
114
+ break
115
+ elif event_name in (EventType.DATA, EventType.ERROR):
116
+ response_body = JsonRPCHttpResponseBody.from_json(event_data)
117
+ if response_body.error is not None:
118
+ raise OpenToolServerCallException(response_body.error.message)
119
+ if event_name == EventType.ERROR:
120
+ raise OpenToolServerCallException(str(event_data.get("message", event_data)))
121
+ on_stream(event_name, ToolReturn(id=function_call.id, result=response_body.result))
122
+
123
+ on_stream(
124
+ EventType.DONE,
125
+ ToolReturn(id=function_call.id, result={EventType.DONE: function_call.name}),
126
+ )
127
+
128
+ def _stream_call_json_rpc_http(
129
+ self, payload: dict[str, Any]
130
+ ) -> Iterator[tuple[str, dict[str, Any]]]:
131
+ url = f"{self.base_url}/streamCall"
132
+ request_data = json.dumps(payload).encode("utf-8")
133
+ headers = {
134
+ "Content-Type": "application/json",
135
+ "Accept": "text/event-stream",
136
+ }
137
+ if self.tool_api_key:
138
+ headers["Authorization"] = f"Bearer {self.tool_api_key}"
139
+
140
+ req = request.Request(url=url, method="POST", data=request_data, headers=headers)
141
+ try:
142
+ with request.urlopen(req, timeout=60) as response:
143
+ if response.status == 401:
144
+ raise OpenToolServerUnauthorizedException()
145
+ buffer: list[str] = []
146
+ for raw_line in response:
147
+ line = raw_line.decode("utf-8").rstrip("\r\n")
148
+ if line == "":
149
+ event_name, event_data = _parse_sse_block(buffer)
150
+ buffer = []
151
+ if event_name is not None and event_data is not None:
152
+ yield event_name, event_data
153
+ continue
154
+ buffer.append(line)
155
+ if buffer:
156
+ event_name, event_data = _parse_sse_block(buffer)
157
+ if event_name is not None and event_data is not None:
158
+ yield event_name, event_data
159
+ except error.HTTPError as exc:
160
+ if exc.code == 401:
161
+ raise OpenToolServerUnauthorizedException() from exc
162
+ message = exc.read().decode("utf-8", errors="ignore")
163
+ raise OpenToolServerCallException(f"HTTP {exc.code} {message}") from exc
164
+ except (socket.timeout, error.URLError) as exc:
165
+ raise OpenToolServerNoAccessException() from exc
166
+
167
+ def load(self) -> OpenTool | None:
168
+ data = self._request_json("GET", "/load")
169
+ if data.get("code") == LOAD_METADATA_NOT_FOUND_CODE or (
170
+ data.get("code") == 404 and data.get("message") == "Json Parser NOT implement"
171
+ ):
172
+ return None
173
+ return OpenTool.from_json(data, schema_reference_resolver=self.schema_reference_resolver)
174
+
175
+ def stop(self) -> StatusInfo | None:
176
+ try:
177
+ data = self._request_json("POST", "/stop")
178
+ return StatusInfo.from_json(data)
179
+ except Exception:
180
+ return None
181
+
182
+ def _request_json(
183
+ self,
184
+ method: str,
185
+ path: str,
186
+ body: dict[str, Any] | None = None,
187
+ ) -> dict[str, Any]:
188
+ url = parse.urljoin(f"{self.base_url}/", path.lstrip("/"))
189
+ headers = {
190
+ "Content-Type": "application/json",
191
+ "Accept": "application/json",
192
+ }
193
+ if self.tool_api_key:
194
+ headers["Authorization"] = f"Bearer {self.tool_api_key}"
195
+
196
+ data = json.dumps(body).encode("utf-8") if body is not None else None
197
+ req = request.Request(url=url, method=method, data=data, headers=headers)
198
+
199
+ try:
200
+ with request.urlopen(req, timeout=20) as response:
201
+ payload = response.read().decode("utf-8")
202
+ return json.loads(payload) if payload else {}
203
+ except error.HTTPError as exc:
204
+ if exc.code == 401:
205
+ raise OpenToolServerUnauthorizedException() from exc
206
+ message = exc.read().decode("utf-8", errors="replace")
207
+ if path == "/load" and exc.code == 404:
208
+ try:
209
+ data = json.loads(message)
210
+ except json.JSONDecodeError:
211
+ data = None
212
+ if isinstance(data, dict) and data.get("code") == LOAD_METADATA_NOT_FOUND_CODE:
213
+ return data
214
+ raise OpenToolServerCallException(f"HTTP {exc.code} {message}") from exc
215
+ except (socket.timeout, error.URLError) as exc:
216
+ raise OpenToolServerNoAccessException() from exc
217
+
218
+
219
+ def _parse_sse_block(lines: list[str]) -> tuple[str | None, dict[str, Any] | None]:
220
+ event_name: str | None = None
221
+ data_lines: list[str] = []
222
+
223
+ for line in lines:
224
+ if line.startswith("event:"):
225
+ event_name = line[len("event:") :].strip()
226
+ elif line.startswith("data:"):
227
+ data_lines.append(line[len("data:") :].removeprefix(" "))
228
+
229
+ if event_name is None or not data_lines:
230
+ return None, None
231
+
232
+ try:
233
+ payload = json.loads("\n".join(data_lines))
234
+ except json.JSONDecodeError as exc:
235
+ raise OpenToolServerCallException(f"Invalid SSE JSON: {exc}") from exc
236
+ if not isinstance(payload, dict):
237
+ raise OpenToolServerCallException("Expected an SSE JSON object")
238
+ return event_name, payload
opentool_python/dto.py ADDED
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from typing import Any
5
+
6
+ JSONRPC_VERSION = "2.0"
7
+ LOAD_METADATA_NOT_FOUND_CODE = "TOOL_NOT_FOUND"
8
+ LOAD_METADATA_NOT_FOUND_MESSAGE = "Tool metadata not found"
9
+ DEFAULT_PREFIX = "/opentool"
10
+ DEFAULT_TOOL_TAG = "N/A"
11
+ DEFAULT_TOOL_HOST = "0.0.0.0"
12
+ DEFAULT_TOOL_PORT = 17001
13
+ CLI_ARGUMENT_TAG = "opentoolServerTag"
14
+ CLI_ARGUMENT_HOST = "opentoolServerHost"
15
+ CLI_ARGUMENT_PORT = "opentoolServerPort"
16
+ CLI_ARGUMENT_APIKEYS = "opentoolServerApiKeys"
17
+
18
+
19
+ class HostType:
20
+ ANY = "0.0.0.0"
21
+ LOCALHOST = "127.0.0.1"
22
+
23
+
24
+ @dataclass
25
+ class Version:
26
+ version: str
27
+
28
+ @classmethod
29
+ def from_json(cls, data: dict[str, Any]) -> "Version":
30
+ return cls(version=str(data["version"]))
31
+
32
+ def to_json(self) -> dict[str, Any]:
33
+ return {"version": self.version}
34
+
35
+
36
+ @dataclass
37
+ class JsonRPCHttpRequestBody:
38
+ method: str
39
+ params: dict[str, Any] | None
40
+ id: str
41
+ jsonrpc: str = JSONRPC_VERSION
42
+
43
+ @classmethod
44
+ def from_json(cls, data: dict[str, Any]) -> "JsonRPCHttpRequestBody":
45
+ method, request_id = data["method"], data["id"]
46
+ params = data.get("params")
47
+ version = data.get("jsonrpc", JSONRPC_VERSION)
48
+ if not isinstance(method, str) or not isinstance(request_id, str):
49
+ raise ValueError("method and id must be strings")
50
+ if params is not None and not isinstance(params, dict):
51
+ raise ValueError("params must be an object or null")
52
+ if not isinstance(version, str):
53
+ raise ValueError("jsonrpc must be a string")
54
+ return cls(method=method, params=params, id=request_id, jsonrpc=version)
55
+
56
+ def to_json(self) -> dict[str, Any]:
57
+ return {
58
+ "jsonrpc": self.jsonrpc,
59
+ "method": self.method,
60
+ "id": self.id,
61
+ **({"params": self.params} if self.params is not None else {}),
62
+ }
63
+
64
+
65
+ @dataclass
66
+ class JsonRPCHttpResponseBodyError:
67
+ code: int
68
+ message: str
69
+
70
+ @classmethod
71
+ def from_json(cls, data: dict[str, Any]) -> "JsonRPCHttpResponseBodyError":
72
+ return cls(code=int(data["code"]), message=str(data["message"]))
73
+
74
+ def to_json(self) -> dict[str, Any]:
75
+ return {"code": self.code, "message": self.message}
76
+
77
+
78
+ @dataclass
79
+ class JsonRPCHttpResponseBody:
80
+ result: dict[str, Any] = field(default_factory=dict)
81
+ id: str = ""
82
+ error: JsonRPCHttpResponseBodyError | None = None
83
+ jsonrpc: str = JSONRPC_VERSION
84
+
85
+ @classmethod
86
+ def from_json(cls, data: dict[str, Any]) -> "JsonRPCHttpResponseBody":
87
+ error_data = data.get("error")
88
+ error = (
89
+ JsonRPCHttpResponseBodyError.from_json(error_data)
90
+ if isinstance(error_data, dict)
91
+ else None
92
+ )
93
+ return cls(
94
+ result=data.get("result") or {},
95
+ id=str(data.get("id", "")),
96
+ error=error,
97
+ jsonrpc=str(data.get("jsonrpc", JSONRPC_VERSION)),
98
+ )
99
+
100
+ def to_json(self) -> dict[str, Any]:
101
+ payload: dict[str, Any] = {
102
+ "jsonrpc": self.jsonrpc,
103
+ "result": self.result,
104
+ "id": self.id,
105
+ }
106
+ if self.error is not None:
107
+ payload["error"] = self.error.to_json()
108
+ return payload
109
+
110
+
111
+ class StatusType:
112
+ STOPPED = "stopped"
113
+
114
+
115
+ @dataclass
116
+ class StatusInfo:
117
+ status: str
118
+
119
+ @classmethod
120
+ def from_json(cls, data: dict[str, Any]) -> "StatusInfo":
121
+ return cls(status=str(data["status"]))
122
+
123
+ def to_json(self) -> dict[str, Any]:
124
+ return {"status": self.status}
@@ -0,0 +1,23 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from .model.opentool import OpenTool
7
+ from .model.schema import SchemaReferenceResolver
8
+
9
+
10
+ class OpenToolJsonLoader:
11
+ def __init__(self, *, schema_reference_resolver: SchemaReferenceResolver | None = None) -> None:
12
+ self.schema_reference_resolver = schema_reference_resolver
13
+ self.schemas_json: dict[str, object] | None = None
14
+
15
+ def load(self, json_string: str) -> OpenTool:
16
+ opentool_map = json.loads(json_string)
17
+ self.schemas_json = opentool_map.get("schemas")
18
+ return OpenTool.from_json(
19
+ opentool_map, schema_reference_resolver=self.schema_reference_resolver
20
+ )
21
+
22
+ def load_from_file(self, json_path: str) -> OpenTool:
23
+ return self.load(Path(json_path).read_text(encoding="utf-8"))
@@ -0,0 +1,3 @@
1
+ from .model import FunctionCall, ToolReturn
2
+
3
+ __all__ = ["FunctionCall", "ToolReturn"]
@@ -0,0 +1,35 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class FunctionCall:
9
+ id: str
10
+ name: str
11
+ arguments: dict[str, Any]
12
+
13
+ @classmethod
14
+ def from_json(cls, data: dict[str, Any]) -> "FunctionCall":
15
+ return cls(
16
+ id=str(data["id"]),
17
+ name=str(data["name"]),
18
+ arguments=data.get("arguments") or {},
19
+ )
20
+
21
+ def to_json(self) -> dict[str, Any]:
22
+ return {"id": self.id, "name": self.name, "arguments": self.arguments}
23
+
24
+
25
+ @dataclass
26
+ class ToolReturn:
27
+ id: str
28
+ result: dict[str, Any]
29
+
30
+ @classmethod
31
+ def from_json(cls, data: dict[str, Any]) -> "ToolReturn":
32
+ return cls(id=str(data["id"]), result=data.get("result") or {})
33
+
34
+ def to_json(self) -> dict[str, Any]:
35
+ return {"id": self.id, "result": self.result}
@@ -0,0 +1,23 @@
1
+ from .data_type import DataType
2
+ from .function_model import FunctionModel
3
+ from .info import Info
4
+ from .opentool import OpenTool
5
+ from .parameter import Parameter
6
+ from .return_model import Return
7
+ from .schema import Schema, SchemaReferenceResolver, SchemaResolver, SchemaType, SchemaValidationResult
8
+ from .server import ServerConfig
9
+
10
+ __all__ = [
11
+ "DataType",
12
+ "FunctionModel",
13
+ "Info",
14
+ "OpenTool",
15
+ "Parameter",
16
+ "Return",
17
+ "Schema",
18
+ "SchemaType",
19
+ "SchemaReferenceResolver",
20
+ "SchemaResolver",
21
+ "SchemaValidationResult",
22
+ "ServerConfig",
23
+ ]
@@ -0,0 +1,8 @@
1
+ class DataType:
2
+ BOOLEAN = "boolean"
3
+ INTEGER = "integer"
4
+ NUMBER = "number"
5
+ STRING = "string"
6
+ ARRAY = "array"
7
+ OBJECT = "object"
8
+ DYNAMIC = "dynamic"
@@ -0,0 +1,66 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from .parameter import Parameter
7
+ from .return_model import Return
8
+ from .schema import SchemaResolver
9
+
10
+
11
+ @dataclass
12
+ class FunctionModel:
13
+ name: str
14
+ description: str
15
+ parameters: list[Parameter]
16
+ return_: Return | None = None
17
+ stream: dict[str, Return] | None = None
18
+
19
+ @classmethod
20
+ def from_json(cls, data: dict[str, Any], *, schema_resolver: SchemaResolver | None = None) -> "FunctionModel":
21
+ parameters = data["parameters"]
22
+ if not isinstance(parameters, list) or any(not isinstance(item, dict) for item in parameters):
23
+ raise ValueError("parameters: expected an array of objects")
24
+
25
+ raw_return = data.get("return")
26
+ if raw_return is not None and not isinstance(raw_return, dict):
27
+ raise ValueError("return: expected an object")
28
+
29
+ raw_stream = data.get("stream")
30
+ stream: dict[str, Return] | None = None
31
+ if raw_stream is not None:
32
+ if not isinstance(raw_stream, dict) or any(not isinstance(item, dict) for item in raw_stream.values()):
33
+ raise ValueError("stream: expected a map of return objects")
34
+ stream = {
35
+ name: Return.from_json(value, schema_resolver=schema_resolver)
36
+ for name, value in raw_stream.items()
37
+ }
38
+
39
+ return cls(
40
+ name=str(data["name"]),
41
+ description=str(data["description"]),
42
+ parameters=[
43
+ Parameter.from_json(item, schema_resolver=schema_resolver)
44
+ for item in parameters
45
+ ],
46
+ return_=(
47
+ Return.from_json(raw_return, schema_resolver=schema_resolver)
48
+ if raw_return is not None else None
49
+ ),
50
+ stream=stream,
51
+ )
52
+
53
+ def to_json(self) -> dict[str, Any]:
54
+ payload: dict[str, Any] = {
55
+ "name": self.name,
56
+ "description": self.description,
57
+ "parameters": [parameter.to_json() for parameter in self.parameters],
58
+ }
59
+ if self.return_ is not None:
60
+ payload["return"] = self.return_.to_json()
61
+ if self.stream is not None:
62
+ payload["stream"] = {
63
+ event_name: return_model.to_json()
64
+ for event_name, return_model in self.stream.items()
65
+ }
66
+ return payload
@@ -0,0 +1,25 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class Info:
9
+ title: str
10
+ version: str
11
+ description: str | None = None
12
+
13
+ @classmethod
14
+ def from_json(cls, data: dict[str, Any]) -> "Info":
15
+ return cls(
16
+ title=str(data["title"]),
17
+ version=str(data["version"]),
18
+ description=data.get("description"),
19
+ )
20
+
21
+ def to_json(self) -> dict[str, Any]:
22
+ payload: dict[str, Any] = {"title": self.title, "version": self.version}
23
+ if self.description is not None:
24
+ payload["description"] = self.description
25
+ return payload