vantage-python 0.3.2__tar.gz → 0.4.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.
- {vantage_python-0.3.2 → vantage_python-0.4.0}/PKG-INFO +1 -1
- {vantage_python-0.3.2 → vantage_python-0.4.0}/autogen.py +153 -19
- {vantage_python-0.3.2 → vantage_python-0.4.0}/pyproject.toml +1 -1
- {vantage_python-0.3.2 → vantage_python-0.4.0}/src/vantage/_async/client.py +219 -190
- {vantage_python-0.3.2 → vantage_python-0.4.0}/src/vantage/_base.py +1 -1
- {vantage_python-0.3.2 → vantage_python-0.4.0}/src/vantage/_sync/client.py +219 -190
- {vantage_python-0.3.2 → vantage_python-0.4.0}/src/vantage/_types.py +19 -2
- {vantage_python-0.3.2 → vantage_python-0.4.0}/.github/workflows/pypi-publish.yml +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/.github/workflows/test.yml +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/.gitignore +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/LICENSE +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/Makefile +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/README.md +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/src/vantage/__init__.py +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/src/vantage/_async/__init__.py +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/src/vantage/_sync/__init__.py +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/src/vantage/py.typed +0 -0
- {vantage_python-0.3.2 → vantage_python-0.4.0}/tests/test_e2e.py +0 -0
|
@@ -31,6 +31,12 @@ RESPONSE_HANDLERS: list[tuple[str, str, str]] = [
|
|
|
31
31
|
),
|
|
32
32
|
]
|
|
33
33
|
|
|
34
|
+
# Endpoints that return bool based on HTTP status: 404 -> False, 2xx -> True, else raise.
|
|
35
|
+
# Each entry is (METHOD, openapi_path_template).
|
|
36
|
+
BOOLEAN_STATUS_ROUTES: list[tuple[str, str]] = [
|
|
37
|
+
("GET", "/virtual_tag_configs/async/{request_id}"),
|
|
38
|
+
]
|
|
39
|
+
|
|
34
40
|
|
|
35
41
|
@dataclass
|
|
36
42
|
class Parameter:
|
|
@@ -61,6 +67,7 @@ class Endpoint:
|
|
|
61
67
|
is_multipart: bool = False
|
|
62
68
|
response_handler: str | None = None # internal client method to call, if not the default
|
|
63
69
|
response_handler_return_type: str | None = None
|
|
70
|
+
boolean_status: bool = False # 404->False, 2xx->True, else raise VantageAPIError
|
|
64
71
|
|
|
65
72
|
|
|
66
73
|
@dataclass
|
|
@@ -196,10 +203,16 @@ def preprocess_inline_models(schemas: dict[str, Any]) -> None:
|
|
|
196
203
|
existing_names.add(model_name)
|
|
197
204
|
|
|
198
205
|
|
|
199
|
-
def openapi_type_to_python(
|
|
206
|
+
def openapi_type_to_python(
|
|
207
|
+
schema: dict[str, Any],
|
|
208
|
+
schemas: dict[str, Any],
|
|
209
|
+
name_map: dict[str, str] | None = None,
|
|
210
|
+
) -> str:
|
|
200
211
|
"""Convert OpenAPI type to Python type hint."""
|
|
201
212
|
if "$ref" in schema:
|
|
202
213
|
ref_name = schema["$ref"].split("/")[-1]
|
|
214
|
+
if name_map and ref_name in name_map:
|
|
215
|
+
return name_map[ref_name]
|
|
203
216
|
return to_pascal_case(ref_name)
|
|
204
217
|
|
|
205
218
|
schema_type = schema.get("type", "any")
|
|
@@ -216,12 +229,12 @@ def openapi_type_to_python(schema: dict[str, Any], schemas: dict[str, Any]) -> s
|
|
|
216
229
|
return "bool"
|
|
217
230
|
elif schema_type == "array":
|
|
218
231
|
items = schema.get("items", {})
|
|
219
|
-
item_type = openapi_type_to_python(items, schemas)
|
|
232
|
+
item_type = openapi_type_to_python(items, schemas, name_map)
|
|
220
233
|
return f"List[{item_type}]"
|
|
221
234
|
elif schema_type == "object":
|
|
222
235
|
additional = schema.get("additionalProperties")
|
|
223
236
|
if additional:
|
|
224
|
-
value_type = openapi_type_to_python(additional, schemas)
|
|
237
|
+
value_type = openapi_type_to_python(additional, schemas, name_map)
|
|
225
238
|
return f"Dict[str, {value_type}]"
|
|
226
239
|
# Check if inline properties match an existing named schema
|
|
227
240
|
inline_props = schema.get("properties")
|
|
@@ -230,6 +243,8 @@ def openapi_type_to_python(schema: dict[str, Any], schemas: dict[str, Any]) -> s
|
|
|
230
243
|
for schema_name, schema_def in schemas.items():
|
|
231
244
|
defined_keys = sorted(schema_def.get("properties", {}).keys())
|
|
232
245
|
if defined_keys and inline_keys == defined_keys:
|
|
246
|
+
if name_map and schema_name in name_map:
|
|
247
|
+
return name_map[schema_name]
|
|
233
248
|
return to_pascal_case(schema_name)
|
|
234
249
|
return "Dict[str, Any]"
|
|
235
250
|
else:
|
|
@@ -237,7 +252,9 @@ def openapi_type_to_python(schema: dict[str, Any], schemas: dict[str, Any]) -> s
|
|
|
237
252
|
|
|
238
253
|
|
|
239
254
|
def extract_request_body_type(
|
|
240
|
-
request_body: dict[str, Any] | None,
|
|
255
|
+
request_body: dict[str, Any] | None,
|
|
256
|
+
schemas: dict[str, Any],
|
|
257
|
+
name_map: dict[str, str] | None = None,
|
|
241
258
|
) -> tuple[str | None, bool]:
|
|
242
259
|
"""Extract request body type and whether it's multipart."""
|
|
243
260
|
if not request_body:
|
|
@@ -252,13 +269,15 @@ def extract_request_body_type(
|
|
|
252
269
|
# Check for JSON
|
|
253
270
|
if "application/json" in content:
|
|
254
271
|
schema = content["application/json"].get("schema", {})
|
|
255
|
-
return openapi_type_to_python(schema, schemas), False
|
|
272
|
+
return openapi_type_to_python(schema, schemas, name_map), False
|
|
256
273
|
|
|
257
274
|
return None, False
|
|
258
275
|
|
|
259
276
|
|
|
260
277
|
def extract_response_type(
|
|
261
|
-
responses: dict[str, Any],
|
|
278
|
+
responses: dict[str, Any],
|
|
279
|
+
schemas: dict[str, Any],
|
|
280
|
+
name_map: dict[str, str] | None = None,
|
|
262
281
|
) -> str | None:
|
|
263
282
|
"""Extract successful response type."""
|
|
264
283
|
for code in ["200", "201", "202", "203"]:
|
|
@@ -268,15 +287,59 @@ def extract_response_type(
|
|
|
268
287
|
content = response.get("content", {})
|
|
269
288
|
if "application/json" in content:
|
|
270
289
|
schema = content["application/json"].get("schema", {})
|
|
271
|
-
return openapi_type_to_python(schema, schemas)
|
|
290
|
+
return openapi_type_to_python(schema, schemas, name_map)
|
|
272
291
|
return None
|
|
273
292
|
|
|
274
293
|
|
|
294
|
+
def find_request_body_schemas(schema: dict[str, Any]) -> set[str]:
|
|
295
|
+
"""Return the set of schema names referenced as request bodies in any endpoint."""
|
|
296
|
+
result = set()
|
|
297
|
+
paths = schema.get("paths", {})
|
|
298
|
+
for path_item in paths.values():
|
|
299
|
+
for method, spec in path_item.items():
|
|
300
|
+
if method in ("parameters", "servers", "summary", "description"):
|
|
301
|
+
continue
|
|
302
|
+
request_body = spec.get("requestBody", {})
|
|
303
|
+
content = request_body.get("content", {})
|
|
304
|
+
for media_type in content.values():
|
|
305
|
+
ref_schema = media_type.get("schema", {})
|
|
306
|
+
if "$ref" in ref_schema:
|
|
307
|
+
name = ref_schema["$ref"].split("/")[-1]
|
|
308
|
+
result.add(name)
|
|
309
|
+
return result
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
def build_class_name_map(schemas: dict[str, Any], request_body_schemas: set[str]) -> dict[str, str]:
|
|
313
|
+
"""Build a mapping from raw schema names to Python class names, resolving conflicts.
|
|
314
|
+
|
|
315
|
+
If two schema names map to the same PascalCase name, the one used as a
|
|
316
|
+
request body is suffixed with 'Request'.
|
|
317
|
+
"""
|
|
318
|
+
initial = {name: to_pascal_case(name) for name in schemas}
|
|
319
|
+
|
|
320
|
+
by_class_name: dict[str, list[str]] = {}
|
|
321
|
+
for raw_name, class_name in initial.items():
|
|
322
|
+
by_class_name.setdefault(class_name, []).append(raw_name)
|
|
323
|
+
|
|
324
|
+
result: dict[str, str] = {}
|
|
325
|
+
for class_name, raw_names in by_class_name.items():
|
|
326
|
+
if len(raw_names) == 1:
|
|
327
|
+
result[raw_names[0]] = class_name
|
|
328
|
+
else:
|
|
329
|
+
for raw_name in raw_names:
|
|
330
|
+
if raw_name in request_body_schemas:
|
|
331
|
+
result[raw_name] = class_name + "Request"
|
|
332
|
+
else:
|
|
333
|
+
result[raw_name] = class_name
|
|
334
|
+
return result
|
|
335
|
+
|
|
336
|
+
|
|
275
337
|
def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
|
|
276
338
|
"""Parse all endpoints from OpenAPI schema."""
|
|
277
339
|
endpoints = []
|
|
278
340
|
paths = schema.get("paths", {})
|
|
279
341
|
schemas = schema.get("components", {}).get("schemas", {})
|
|
342
|
+
name_map = build_class_name_map(schemas, find_request_body_schemas(schema))
|
|
280
343
|
|
|
281
344
|
for path, methods in paths.items():
|
|
282
345
|
for method, spec in methods.items():
|
|
@@ -288,7 +351,7 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
|
|
|
288
351
|
parameters = []
|
|
289
352
|
for param in spec.get("parameters", []):
|
|
290
353
|
param_schema = param.get("schema", {})
|
|
291
|
-
param_type = openapi_type_to_python(param_schema, schemas)
|
|
354
|
+
param_type = openapi_type_to_python(param_schema, schemas, name_map)
|
|
292
355
|
parameters.append(
|
|
293
356
|
Parameter(
|
|
294
357
|
name=param["name"],
|
|
@@ -301,9 +364,9 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
|
|
|
301
364
|
)
|
|
302
365
|
|
|
303
366
|
request_body = spec.get("requestBody")
|
|
304
|
-
body_type, is_multipart = extract_request_body_type(request_body, schemas)
|
|
367
|
+
body_type, is_multipart = extract_request_body_type(request_body, schemas, name_map)
|
|
305
368
|
|
|
306
|
-
response_type = extract_response_type(spec.get("responses", {}), schemas)
|
|
369
|
+
response_type = extract_response_type(spec.get("responses", {}), schemas, name_map)
|
|
307
370
|
|
|
308
371
|
description = spec.get("description")
|
|
309
372
|
|
|
@@ -319,6 +382,10 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
|
|
|
319
382
|
if response_handler:
|
|
320
383
|
break
|
|
321
384
|
|
|
385
|
+
boolean_status = (method.upper(), path) in {
|
|
386
|
+
(m.upper(), p) for m, p in BOOLEAN_STATUS_ROUTES
|
|
387
|
+
}
|
|
388
|
+
|
|
322
389
|
endpoints.append(
|
|
323
390
|
Endpoint(
|
|
324
391
|
path=path,
|
|
@@ -336,6 +403,7 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
|
|
|
336
403
|
is_multipart=is_multipart,
|
|
337
404
|
response_handler=response_handler,
|
|
338
405
|
response_handler_return_type=response_handler_return_type,
|
|
406
|
+
boolean_status=boolean_status,
|
|
339
407
|
)
|
|
340
408
|
)
|
|
341
409
|
|
|
@@ -469,6 +537,7 @@ def _append_response_mapping(lines: list[str], return_type: str, data_var: str)
|
|
|
469
537
|
def generate_pydantic_models(schema: dict[str, Any]) -> str:
|
|
470
538
|
"""Generate Pydantic models from OpenAPI schemas."""
|
|
471
539
|
schemas = schema.get("components", {}).get("schemas", {})
|
|
540
|
+
name_map = build_class_name_map(schemas, find_request_body_schemas(schema))
|
|
472
541
|
lines = [
|
|
473
542
|
'"""Auto-generated Pydantic models from OpenAPI schema."""',
|
|
474
543
|
"",
|
|
@@ -482,7 +551,7 @@ def generate_pydantic_models(schema: dict[str, Any]) -> str:
|
|
|
482
551
|
]
|
|
483
552
|
|
|
484
553
|
for name, spec in schemas.items():
|
|
485
|
-
class_name = to_pascal_case(name)
|
|
554
|
+
class_name = name_map.get(name, to_pascal_case(name))
|
|
486
555
|
description = spec.get("description", "")
|
|
487
556
|
|
|
488
557
|
lines.append(f"class {class_name}(BaseModel):")
|
|
@@ -508,7 +577,7 @@ def generate_pydantic_models(schema: dict[str, Any]) -> str:
|
|
|
508
577
|
python_name = python_name + "_"
|
|
509
578
|
needs_alias = True
|
|
510
579
|
|
|
511
|
-
prop_type = openapi_type_to_python(prop_spec, schemas)
|
|
580
|
+
prop_type = openapi_type_to_python(prop_spec, schemas, name_map)
|
|
512
581
|
|
|
513
582
|
# Handle nullable
|
|
514
583
|
if prop_spec.get("x-nullable") or prop_spec.get("nullable"):
|
|
@@ -559,11 +628,26 @@ def _collect_handler_routes(resources: dict[str, Resource]) -> dict[str, list[tu
|
|
|
559
628
|
for endpoint in resource.endpoints:
|
|
560
629
|
if endpoint.response_handler:
|
|
561
630
|
handler_routes.setdefault(endpoint.response_handler, []).append(
|
|
562
|
-
(endpoint.method, endpoint.path)
|
|
631
|
+
(endpoint.method, "/v2" + endpoint.path)
|
|
563
632
|
)
|
|
564
633
|
return handler_routes
|
|
565
634
|
|
|
566
635
|
|
|
636
|
+
def _collect_boolean_status_prefixes(resources: dict[str, Resource]) -> list[tuple[str, str]]:
|
|
637
|
+
"""Collect (method, path_prefix) pairs for boolean-status endpoints.
|
|
638
|
+
|
|
639
|
+
The prefix is derived by taking everything before the first path parameter
|
|
640
|
+
so it can be matched with str.startswith() at runtime.
|
|
641
|
+
"""
|
|
642
|
+
result = []
|
|
643
|
+
for resource in resources.values():
|
|
644
|
+
for endpoint in resource.endpoints:
|
|
645
|
+
if endpoint.boolean_status:
|
|
646
|
+
prefix = ("/v2" + endpoint.path).split("{")[0]
|
|
647
|
+
result.append((endpoint.method, prefix))
|
|
648
|
+
return result
|
|
649
|
+
|
|
650
|
+
|
|
567
651
|
def generate_sync_client(resources: dict[str, Resource]) -> str:
|
|
568
652
|
"""Generate synchronous client code."""
|
|
569
653
|
lines = [
|
|
@@ -652,6 +736,29 @@ def generate_sync_client(resources: dict[str, Resource]) -> str:
|
|
|
652
736
|
" json=body,",
|
|
653
737
|
" )",
|
|
654
738
|
"",
|
|
739
|
+
]
|
|
740
|
+
)
|
|
741
|
+
|
|
742
|
+
# Inject boolean-status path checks (before the generic error check)
|
|
743
|
+
boolean_prefixes = _collect_boolean_status_prefixes(resources)
|
|
744
|
+
for method, prefix in boolean_prefixes:
|
|
745
|
+
lines.extend([
|
|
746
|
+
f' if method.upper() == "{method}" and path.startswith("{prefix}"):',
|
|
747
|
+
" if response.status_code == 404:",
|
|
748
|
+
" return False",
|
|
749
|
+
" elif response.is_success:",
|
|
750
|
+
" return True",
|
|
751
|
+
" else:",
|
|
752
|
+
" raise VantageAPIError(",
|
|
753
|
+
" status=response.status_code,",
|
|
754
|
+
" status_text=response.reason_phrase,",
|
|
755
|
+
" body=response.text,",
|
|
756
|
+
" )",
|
|
757
|
+
"",
|
|
758
|
+
])
|
|
759
|
+
|
|
760
|
+
lines.extend(
|
|
761
|
+
[
|
|
655
762
|
" if not response.is_success:",
|
|
656
763
|
" raise VantageAPIError(",
|
|
657
764
|
" status=response.status_code,",
|
|
@@ -757,7 +864,9 @@ def generate_sync_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
757
864
|
|
|
758
865
|
# Method signature
|
|
759
866
|
param_str = ", ".join(["self"] + params) if params else "self"
|
|
760
|
-
if endpoint.
|
|
867
|
+
if endpoint.boolean_status:
|
|
868
|
+
return_type = "bool"
|
|
869
|
+
elif endpoint.response_handler:
|
|
761
870
|
return_type = endpoint.response_handler_return_type or "Any"
|
|
762
871
|
else:
|
|
763
872
|
return_type = endpoint.response_type or "None"
|
|
@@ -774,7 +883,7 @@ def generate_sync_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
774
883
|
lines.append(' """')
|
|
775
884
|
|
|
776
885
|
# Build path with parameters (URL-encode each path arg)
|
|
777
|
-
path = endpoint.path
|
|
886
|
+
path = "/v2" + endpoint.path
|
|
778
887
|
for pp in path_params:
|
|
779
888
|
path = path.replace(f"{{{pp.name}}}", f"{{quote(str({pp.python_name}), safe='')}}")
|
|
780
889
|
|
|
@@ -801,7 +910,7 @@ def generate_sync_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
801
910
|
lines.append(" body_data = None")
|
|
802
911
|
|
|
803
912
|
# Make request and coerce response payload into typed models where possible
|
|
804
|
-
if endpoint.response_handler:
|
|
913
|
+
if endpoint.boolean_status or endpoint.response_handler:
|
|
805
914
|
lines.append(
|
|
806
915
|
f' return self._client.request("{endpoint.method}", path, params=params, body=body_data)'
|
|
807
916
|
)
|
|
@@ -907,6 +1016,29 @@ def generate_async_client(resources: dict[str, Resource]) -> str:
|
|
|
907
1016
|
" json=body,",
|
|
908
1017
|
" )",
|
|
909
1018
|
"",
|
|
1019
|
+
]
|
|
1020
|
+
)
|
|
1021
|
+
|
|
1022
|
+
# Inject boolean-status path checks (before the generic error check)
|
|
1023
|
+
boolean_prefixes = _collect_boolean_status_prefixes(resources)
|
|
1024
|
+
for method, prefix in boolean_prefixes:
|
|
1025
|
+
lines.extend([
|
|
1026
|
+
f' if method.upper() == "{method}" and path.startswith("{prefix}"):',
|
|
1027
|
+
" if response.status_code == 404:",
|
|
1028
|
+
" return False",
|
|
1029
|
+
" elif response.is_success:",
|
|
1030
|
+
" return True",
|
|
1031
|
+
" else:",
|
|
1032
|
+
" raise VantageAPIError(",
|
|
1033
|
+
" status=response.status_code,",
|
|
1034
|
+
" status_text=response.reason_phrase,",
|
|
1035
|
+
" body=response.text,",
|
|
1036
|
+
" )",
|
|
1037
|
+
"",
|
|
1038
|
+
])
|
|
1039
|
+
|
|
1040
|
+
lines.extend(
|
|
1041
|
+
[
|
|
910
1042
|
" if not response.is_success:",
|
|
911
1043
|
" raise VantageAPIError(",
|
|
912
1044
|
" status=response.status_code,",
|
|
@@ -1012,7 +1144,9 @@ def generate_async_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
1012
1144
|
|
|
1013
1145
|
# Method signature
|
|
1014
1146
|
param_str = ", ".join(["self"] + params) if params else "self"
|
|
1015
|
-
if endpoint.
|
|
1147
|
+
if endpoint.boolean_status:
|
|
1148
|
+
return_type = "bool"
|
|
1149
|
+
elif endpoint.response_handler:
|
|
1016
1150
|
return_type = endpoint.response_handler_return_type or "Any"
|
|
1017
1151
|
else:
|
|
1018
1152
|
return_type = endpoint.response_type or "None"
|
|
@@ -1029,7 +1163,7 @@ def generate_async_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
1029
1163
|
lines.append(' """')
|
|
1030
1164
|
|
|
1031
1165
|
# Build path with parameters (URL-encode each path arg)
|
|
1032
|
-
path = endpoint.path
|
|
1166
|
+
path = "/v2" + endpoint.path
|
|
1033
1167
|
for pp in path_params:
|
|
1034
1168
|
path = path.replace(f"{{{pp.name}}}", f"{{quote(str({pp.python_name}), safe='')}}")
|
|
1035
1169
|
|
|
@@ -1056,7 +1190,7 @@ def generate_async_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
1056
1190
|
lines.append(" body_data = None")
|
|
1057
1191
|
|
|
1058
1192
|
# Make request and coerce response payload into typed models where possible
|
|
1059
|
-
if endpoint.response_handler:
|
|
1193
|
+
if endpoint.boolean_status or endpoint.response_handler:
|
|
1060
1194
|
lines.append(
|
|
1061
1195
|
f' return await self._client.request("{endpoint.method}", path, params=params, body=body_data)'
|
|
1062
1196
|
)
|