speedy-utils 1.0.13__tar.gz → 1.0.14__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.
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/PKG-INFO +1 -1
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/pyproject.toml +3 -2
- speedy_utils-1.0.14/src/speedy_utils/scripts/__init__.py +0 -0
- speedy_utils-1.0.14/src/speedy_utils/scripts/openapi_client_codegen.py +258 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/README.md +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/__init__.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/chat_format/__init__.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/chat_format/display.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/chat_format/transform.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/chat_format/utils.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/group_messages.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/lm/__init__.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/lm/alm.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/lm/lm.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/lm/utils.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/scripts/vllm_load_balancer.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/llm_utils/scripts/vllm_serve.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/__init__.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/all.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/common/__init__.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/common/clock.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/common/function_decorator.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/common/logger.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/common/report_manager.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/common/utils_cache.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/common/utils_io.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/common/utils_misc.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/common/utils_print.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/multi_worker/__init__.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/multi_worker/process.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/multi_worker/thread.py +0 -0
- {speedy_utils-1.0.13 → speedy_utils-1.0.14}/src/speedy_utils/scripts/mpython.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "speedy-utils"
|
|
3
|
-
version = "1.0.
|
|
3
|
+
version = "1.0.14"
|
|
4
4
|
description = "Fast and easy-to-use package for data science"
|
|
5
5
|
authors = ["AnhVTH <anhvth.226@gmail.com>"]
|
|
6
6
|
readme = "README.md"
|
|
@@ -11,7 +11,7 @@ packages = [
|
|
|
11
11
|
]
|
|
12
12
|
|
|
13
13
|
[build-system]
|
|
14
|
-
requires = ["poetry-core>=1.0.
|
|
14
|
+
requires = ["poetry-core>=1.0.14"]
|
|
15
15
|
build-backend = "poetry.core.masonry.api"
|
|
16
16
|
|
|
17
17
|
[tool.black]
|
|
@@ -63,3 +63,4 @@ packaging = ">=23.2,<25"
|
|
|
63
63
|
mpython = "speedy_utils.scripts.mpython:main"
|
|
64
64
|
svllm = "llm_utils.scripts.vllm_serve:main"
|
|
65
65
|
svllm-lb = "llm_utils.scripts.vllm_load_balancer:run_load_balancer"
|
|
66
|
+
openapi_client_codegen = "speedy_utils.scripts.openapi_client_codegen:main"
|
|
File without changes
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
"""
|
|
2
|
+
generate_client.py
|
|
3
|
+
|
|
4
|
+
A script to generate a synchronous GeneratedClient from an OpenAPI JSON spec.
|
|
5
|
+
Usage:
|
|
6
|
+
python generate_client.py openapi.json > generated_client.py
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
# Renamed from gen.py to openapi_client_codegen.py
|
|
10
|
+
|
|
11
|
+
import argparse
|
|
12
|
+
import json
|
|
13
|
+
import os
|
|
14
|
+
import signal
|
|
15
|
+
import subprocess
|
|
16
|
+
import sys
|
|
17
|
+
import time
|
|
18
|
+
from typing import Any, Dict, List, Optional
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def pascal_case(s: str) -> str:
|
|
22
|
+
"""Convert snake-case or camelCase to PascalCase"""
|
|
23
|
+
parts = [p for p in s.replace("-", "_").split("_") if p]
|
|
24
|
+
return "".join(p.title() for p in parts)
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
def snake_case(s: str) -> str:
|
|
28
|
+
"""Convert camelCase or PascalCase to snake_case"""
|
|
29
|
+
out = []
|
|
30
|
+
for c in s:
|
|
31
|
+
if c.isupper():
|
|
32
|
+
if out:
|
|
33
|
+
out.append("_")
|
|
34
|
+
out.append(c.lower())
|
|
35
|
+
else:
|
|
36
|
+
out.append(c)
|
|
37
|
+
return "".join(out)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def map_openapi_type(prop: Dict[str, Any]) -> str:
|
|
41
|
+
t = prop.get("type")
|
|
42
|
+
if t == "string":
|
|
43
|
+
fmt = prop.get("format")
|
|
44
|
+
return "datetime" if fmt == "date-time" else "str"
|
|
45
|
+
if t == "integer":
|
|
46
|
+
return "int"
|
|
47
|
+
if t == "boolean":
|
|
48
|
+
return "bool"
|
|
49
|
+
if t == "array":
|
|
50
|
+
items = prop.get("items", {})
|
|
51
|
+
return f"List[{map_openapi_type(items)}]"
|
|
52
|
+
if t == "object":
|
|
53
|
+
return "Dict[str, Any]"
|
|
54
|
+
return "Any"
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def generate_models(components: Dict[str, Any]) -> List[str]:
|
|
58
|
+
lines: List[str] = []
|
|
59
|
+
schemas = components.get("schemas", {})
|
|
60
|
+
for name, schema in schemas.items():
|
|
61
|
+
if "enum" in schema:
|
|
62
|
+
lines.append(f"class {name}(str, Enum):")
|
|
63
|
+
for val in schema["enum"]:
|
|
64
|
+
key = snake_case(val).upper()
|
|
65
|
+
lines.append(f" {key} = '{val}'")
|
|
66
|
+
lines.append("")
|
|
67
|
+
continue
|
|
68
|
+
lines.append(f"class {name}(BaseModel):")
|
|
69
|
+
props = schema.get("properties", {})
|
|
70
|
+
required = set(schema.get("required", []))
|
|
71
|
+
if not props:
|
|
72
|
+
lines.append(" pass")
|
|
73
|
+
else:
|
|
74
|
+
for prop_name, prop in props.items():
|
|
75
|
+
t = map_openapi_type(prop)
|
|
76
|
+
optional = prop_name not in required
|
|
77
|
+
type_hint = f"Optional[{t}]" if optional else t
|
|
78
|
+
default = " = None" if optional else ""
|
|
79
|
+
lines.append(f" {prop_name}: {type_hint}{default}")
|
|
80
|
+
lines.append("")
|
|
81
|
+
return lines
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def generate_client(spec: Dict[str, Any]) -> List[str]:
|
|
85
|
+
paths = spec.get("paths", {})
|
|
86
|
+
models = spec.get("components", {}).get("schemas", {})
|
|
87
|
+
lines: List[str] = []
|
|
88
|
+
lines.append("class GeneratedClient:")
|
|
89
|
+
lines.append(' """Client generated from OpenAPI spec."""')
|
|
90
|
+
lines.append("")
|
|
91
|
+
# __init__
|
|
92
|
+
lines.append(
|
|
93
|
+
" def __init__(self, base_url: Optional[str]=None, api_key: Optional[str]=None, timeout: float=30.0, app: Any=None) -> None:"
|
|
94
|
+
)
|
|
95
|
+
lines.append(' """Initialize the generated client."""')
|
|
96
|
+
lines.append(" from fastapi.testclient import TestClient")
|
|
97
|
+
lines.append(" import httpx")
|
|
98
|
+
lines.append(" from pydantic import BaseModel")
|
|
99
|
+
lines.append(' headers = {"Content-Type": "application/json"}')
|
|
100
|
+
lines.append(" if api_key:")
|
|
101
|
+
lines.append(' headers["Authorization"] = f"Bearer {api_key}"')
|
|
102
|
+
lines.append("")
|
|
103
|
+
lines.append(" if app is not None:")
|
|
104
|
+
lines.append(' self.base_url = "http://testserver"')
|
|
105
|
+
lines.append(" self.client = TestClient(app)")
|
|
106
|
+
lines.append(" self.client.headers.update(headers)")
|
|
107
|
+
lines.append(" else:")
|
|
108
|
+
lines.append(
|
|
109
|
+
' self.base_url = base_url.rstrip("/") if base_url else "http://localhost"'
|
|
110
|
+
)
|
|
111
|
+
lines.append(
|
|
112
|
+
" self.client = httpx.Client(base_url=self.base_url, headers=headers, timeout=timeout)"
|
|
113
|
+
)
|
|
114
|
+
lines.append("")
|
|
115
|
+
|
|
116
|
+
for path, ops in paths.items():
|
|
117
|
+
for method, op in ops.items():
|
|
118
|
+
op_id = op.get("operationId", snake_case(method + path))
|
|
119
|
+
func_name = snake_case(op_id)
|
|
120
|
+
summary = op.get("summary", "").strip()
|
|
121
|
+
# collect parameters
|
|
122
|
+
req_params: List[str] = ["self"]
|
|
123
|
+
opt_params: List[str] = []
|
|
124
|
+
# path params (required)
|
|
125
|
+
path_params = [p for p in op.get("parameters", []) if p.get("in") == "path"]
|
|
126
|
+
for p in path_params:
|
|
127
|
+
req_params.append(f"{p['name']}: str")
|
|
128
|
+
# requestBody props
|
|
129
|
+
body_props = None
|
|
130
|
+
if "requestBody" in op:
|
|
131
|
+
content = op["requestBody"].get("content", {})
|
|
132
|
+
media = content.get("application/json") or next(iter(content.values()))
|
|
133
|
+
schema_ref = media.get("schema", {})
|
|
134
|
+
if "$ref" in schema_ref:
|
|
135
|
+
ref = schema_ref["$ref"].split("/")[-1]
|
|
136
|
+
schema = models.get(ref, {})
|
|
137
|
+
body_props = schema.get("properties", {})
|
|
138
|
+
required = set(schema.get("required", []))
|
|
139
|
+
for name_, prop in body_props.items():
|
|
140
|
+
t = map_openapi_type(prop)
|
|
141
|
+
if name_ in required:
|
|
142
|
+
req_params.append(f"{name_}: {t}")
|
|
143
|
+
else:
|
|
144
|
+
opt_params.append(f"{name_}: Optional[{t}] = None")
|
|
145
|
+
# query params (always optional)
|
|
146
|
+
query_params = [
|
|
147
|
+
p for p in op.get("parameters", []) if p.get("in") == "query"
|
|
148
|
+
]
|
|
149
|
+
for p in query_params:
|
|
150
|
+
t = "str"
|
|
151
|
+
opt_params.append(f"{p['name']}: Optional[{t}] = None")
|
|
152
|
+
# combine signature
|
|
153
|
+
sig = req_params + opt_params
|
|
154
|
+
param_str = ", ".join(sig)
|
|
155
|
+
# determine return type
|
|
156
|
+
ret_type = "Any"
|
|
157
|
+
resp200 = op.get("responses", {}).get("200", {})
|
|
158
|
+
content200 = resp200.get("content", {})
|
|
159
|
+
if content200:
|
|
160
|
+
schema200 = next(iter(content200.values())).get("schema", {})
|
|
161
|
+
if "$ref" in schema200:
|
|
162
|
+
ret_type = schema200["$ref"].split("/")[-1]
|
|
163
|
+
elif "items" in schema200 and "$ref" in schema200["items"]:
|
|
164
|
+
inner = schema200["items"]["$ref"].split("/")[-1]
|
|
165
|
+
ret_type = f"List[{inner}]"
|
|
166
|
+
# method definition
|
|
167
|
+
lines.append(f" def {func_name}({param_str}) -> {ret_type}:")
|
|
168
|
+
if summary:
|
|
169
|
+
lines.append(f' """{summary}."""')
|
|
170
|
+
# build URL
|
|
171
|
+
url = f"f'{path}'" if "{" in path else f"'{path}'"
|
|
172
|
+
m = method.lower()
|
|
173
|
+
# build call
|
|
174
|
+
if body_props and m in ("post", "put", "patch"):
|
|
175
|
+
payload_items = []
|
|
176
|
+
for name_ in body_props:
|
|
177
|
+
payload_items.append(f"'{name_}': {name_}")
|
|
178
|
+
payload = ", ".join(payload_items)
|
|
179
|
+
lines.append(
|
|
180
|
+
f" resp = self.client.{m}({url}, json={{ {payload} }})"
|
|
181
|
+
)
|
|
182
|
+
elif query_params and m == "get":
|
|
183
|
+
qpairs = ", ".join(f"'{p['name']}': {p['name']}" for p in query_params)
|
|
184
|
+
lines.append(
|
|
185
|
+
f" resp = self.client.get({url}, params={{ {qpairs} }})"
|
|
186
|
+
)
|
|
187
|
+
else:
|
|
188
|
+
lines.append(f" resp = self.client.{m}({url})")
|
|
189
|
+
lines.append(" resp.raise_for_status()")
|
|
190
|
+
# parse response
|
|
191
|
+
if ret_type.startswith("List["):
|
|
192
|
+
inner = ret_type[5:-1]
|
|
193
|
+
lines.append(
|
|
194
|
+
f" return [{inner}.model_validate(item) for item in resp.json()]"
|
|
195
|
+
)
|
|
196
|
+
elif ret_type in models:
|
|
197
|
+
lines.append(f" return {ret_type}.model_validate(resp.json())")
|
|
198
|
+
else:
|
|
199
|
+
lines.append(" return resp.json()")
|
|
200
|
+
lines.append("")
|
|
201
|
+
|
|
202
|
+
return lines
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def main() -> None:
|
|
206
|
+
parser = argparse.ArgumentParser(
|
|
207
|
+
description="Generate a synchronous client from OpenAPI spec."
|
|
208
|
+
)
|
|
209
|
+
parser.add_argument(
|
|
210
|
+
"spec",
|
|
211
|
+
type=str,
|
|
212
|
+
help="OpenAPI JSON file path or URL (e.g. openapi.json or http://localhost:8084/openapi.json)",
|
|
213
|
+
)
|
|
214
|
+
parser.add_argument(
|
|
215
|
+
"-o",
|
|
216
|
+
"--output",
|
|
217
|
+
type=str,
|
|
218
|
+
default=None,
|
|
219
|
+
help="Output file path (default: stdout)",
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
args = parser.parse_args()
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
spec_src = args.spec
|
|
226
|
+
if spec_src.startswith("http://") or spec_src.startswith("https://"):
|
|
227
|
+
import httpx
|
|
228
|
+
|
|
229
|
+
response = httpx.get(spec_src)
|
|
230
|
+
spec = response.json()
|
|
231
|
+
else:
|
|
232
|
+
with open(spec_src, "r", encoding="utf-8") as f:
|
|
233
|
+
spec = json.load(f)
|
|
234
|
+
out: List[str] = []
|
|
235
|
+
# imports
|
|
236
|
+
out.append("from typing import Any, Dict, List, Optional")
|
|
237
|
+
out.append("from datetime import datetime")
|
|
238
|
+
out.append("import httpx")
|
|
239
|
+
out.append("from fastapi.testclient import TestClient")
|
|
240
|
+
out.append("from pydantic import BaseModel")
|
|
241
|
+
out.append("from enum import Enum")
|
|
242
|
+
out.append("")
|
|
243
|
+
# models
|
|
244
|
+
out.extend(generate_models(spec.get("components", {})))
|
|
245
|
+
# client
|
|
246
|
+
out.extend(generate_client(spec))
|
|
247
|
+
output_str = "\n".join(out)
|
|
248
|
+
if args.output:
|
|
249
|
+
with open(args.output, "w", encoding="utf-8") as f:
|
|
250
|
+
f.write(output_str)
|
|
251
|
+
print(f"\033[0;32mClient wrapper generated at {args.output}\033[0m")
|
|
252
|
+
else:
|
|
253
|
+
print(output_str)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
print(
|
|
256
|
+
f"\033[0;31mFailed to generate client wrapper: {e}\033[0m", file=sys.stderr
|
|
257
|
+
)
|
|
258
|
+
sys.exit(1)
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|