vantage-python 0.3.0__tar.gz → 0.3.2__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.0 → vantage_python-0.3.2}/.github/workflows/pypi-publish.yml +2 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/.github/workflows/test.yml +0 -2
- {vantage_python-0.3.0 → vantage_python-0.3.2}/PKG-INFO +1 -1
- {vantage_python-0.3.0 → vantage_python-0.3.2}/autogen.py +178 -11
- {vantage_python-0.3.0 → vantage_python-0.3.2}/pyproject.toml +1 -1
- {vantage_python-0.3.0 → vantage_python-0.3.2}/src/vantage/_async/client.py +670 -213
- {vantage_python-0.3.0 → vantage_python-0.3.2}/src/vantage/_sync/client.py +670 -213
- {vantage_python-0.3.0 → vantage_python-0.3.2}/tests/test_e2e.py +8 -8
- {vantage_python-0.3.0 → vantage_python-0.3.2}/.gitignore +0 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/LICENSE +0 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/Makefile +0 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/README.md +0 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/src/vantage/__init__.py +0 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/src/vantage/_async/__init__.py +0 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/src/vantage/_base.py +0 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/src/vantage/_sync/__init__.py +0 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/src/vantage/_types.py +0 -0
- {vantage_python-0.3.0 → vantage_python-0.3.2}/src/vantage/py.typed +0 -0
|
@@ -14,7 +14,6 @@ jobs:
|
|
|
14
14
|
with:
|
|
15
15
|
python-version: ${{ matrix.python-version }}
|
|
16
16
|
- run: pip install -e ".[dev]"
|
|
17
|
-
- run: python autogen.py
|
|
18
17
|
- run: pytest tests/test_e2e.py
|
|
19
18
|
env:
|
|
20
19
|
VANTAGE_API_TOKEN: ${{ secrets.VANTAGE_API_TOKEN }}
|
|
@@ -32,7 +31,6 @@ jobs:
|
|
|
32
31
|
with:
|
|
33
32
|
python-version: ${{ matrix.python-version }}
|
|
34
33
|
- run: pip install -e ".[dev]"
|
|
35
|
-
- run: python autogen.py
|
|
36
34
|
- run: python -c "from vantage import Client, AsyncClient"
|
|
37
35
|
|
|
38
36
|
compare-to-api:
|
|
@@ -20,6 +20,17 @@ from typing import Any
|
|
|
20
20
|
OPENAPI_URL = "https://api.vantage.sh/v2/oas_v3.json"
|
|
21
21
|
OUTPUT_DIR = Path(__file__).parent / "src" / "vantage"
|
|
22
22
|
|
|
23
|
+
# Maps a substring found in a response description to the internal client method
|
|
24
|
+
# that should handle it, and the Python return type to emit.
|
|
25
|
+
# Checked against each HTTP status code's description during endpoint parsing.
|
|
26
|
+
RESPONSE_HANDLERS: list[tuple[str, str, str]] = [
|
|
27
|
+
(
|
|
28
|
+
"will be available at the location specified in the Location header",
|
|
29
|
+
"_request_for_location",
|
|
30
|
+
"str",
|
|
31
|
+
),
|
|
32
|
+
]
|
|
33
|
+
|
|
23
34
|
|
|
24
35
|
@dataclass
|
|
25
36
|
class Parameter:
|
|
@@ -48,6 +59,8 @@ class Endpoint:
|
|
|
48
59
|
request_body_type: str | None = None
|
|
49
60
|
response_type: str | None = None
|
|
50
61
|
is_multipart: bool = False
|
|
62
|
+
response_handler: str | None = None # internal client method to call, if not the default
|
|
63
|
+
response_handler_return_type: str | None = None
|
|
51
64
|
|
|
52
65
|
|
|
53
66
|
@dataclass
|
|
@@ -292,13 +305,27 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
|
|
|
292
305
|
|
|
293
306
|
response_type = extract_response_type(spec.get("responses", {}), schemas)
|
|
294
307
|
|
|
308
|
+
description = spec.get("description")
|
|
309
|
+
|
|
310
|
+
response_handler = None
|
|
311
|
+
response_handler_return_type = None
|
|
312
|
+
for resp_desc in spec.get("responses", {}).values():
|
|
313
|
+
text = resp_desc.get("description", "")
|
|
314
|
+
for phrase, handler, return_type in RESPONSE_HANDLERS:
|
|
315
|
+
if phrase in text:
|
|
316
|
+
response_handler = handler
|
|
317
|
+
response_handler_return_type = return_type
|
|
318
|
+
break
|
|
319
|
+
if response_handler:
|
|
320
|
+
break
|
|
321
|
+
|
|
295
322
|
endpoints.append(
|
|
296
323
|
Endpoint(
|
|
297
324
|
path=path,
|
|
298
325
|
method=method.upper(),
|
|
299
326
|
operation_id=operation_id,
|
|
300
327
|
summary=spec.get("summary"),
|
|
301
|
-
description=
|
|
328
|
+
description=description,
|
|
302
329
|
deprecated=spec.get("deprecated", False),
|
|
303
330
|
parameters=parameters,
|
|
304
331
|
request_body_required=request_body.get("required", False)
|
|
@@ -307,6 +334,8 @@ def parse_endpoints(schema: dict[str, Any]) -> list[Endpoint]:
|
|
|
307
334
|
request_body_type=body_type,
|
|
308
335
|
response_type=response_type,
|
|
309
336
|
is_multipart=is_multipart,
|
|
337
|
+
response_handler=response_handler,
|
|
338
|
+
response_handler_return_type=response_handler_return_type,
|
|
310
339
|
)
|
|
311
340
|
)
|
|
312
341
|
|
|
@@ -373,6 +402,70 @@ def generate_method_name(endpoint: Endpoint, resource_name: str) -> str:
|
|
|
373
402
|
return to_snake_case(op_id)
|
|
374
403
|
|
|
375
404
|
|
|
405
|
+
def _extract_inner_type(type_hint: str, generic_prefix: str) -> str | None:
|
|
406
|
+
"""Extract inner type from simple generic forms like Prefix[Inner]."""
|
|
407
|
+
if not type_hint.startswith(generic_prefix) or not type_hint.endswith("]"):
|
|
408
|
+
return None
|
|
409
|
+
return type_hint[len(generic_prefix):-1].strip()
|
|
410
|
+
|
|
411
|
+
|
|
412
|
+
def _extract_dict_value_type(type_hint: str) -> str | None:
|
|
413
|
+
"""Extract value type from Dict[str, ValueType]."""
|
|
414
|
+
if not type_hint.startswith("Dict[") or not type_hint.endswith("]"):
|
|
415
|
+
return None
|
|
416
|
+
inner = type_hint[len("Dict["):-1].strip()
|
|
417
|
+
key_and_value = inner.split(",", 1)
|
|
418
|
+
if len(key_and_value) != 2:
|
|
419
|
+
return None
|
|
420
|
+
key_type = key_and_value[0].strip()
|
|
421
|
+
value_type = key_and_value[1].strip()
|
|
422
|
+
if key_type not in {"str", "Optional[str]"}:
|
|
423
|
+
return None
|
|
424
|
+
return value_type
|
|
425
|
+
|
|
426
|
+
|
|
427
|
+
def _is_model_type(type_hint: str) -> bool:
|
|
428
|
+
"""Return True for generated Pydantic model type names."""
|
|
429
|
+
return bool(re.match(r"^[A-Z][A-Za-z0-9_]*$", type_hint))
|
|
430
|
+
|
|
431
|
+
|
|
432
|
+
def _append_response_mapping(lines: list[str], return_type: str, data_var: str) -> None:
|
|
433
|
+
"""Append generated code that coerces dict payloads into typed models."""
|
|
434
|
+
type_hint = return_type.strip()
|
|
435
|
+
|
|
436
|
+
optional_inner = _extract_inner_type(type_hint, "Optional[")
|
|
437
|
+
if optional_inner:
|
|
438
|
+
type_hint = optional_inner
|
|
439
|
+
|
|
440
|
+
list_inner = _extract_inner_type(type_hint, "List[")
|
|
441
|
+
if list_inner and _is_model_type(list_inner):
|
|
442
|
+
lines.extend(
|
|
443
|
+
[
|
|
444
|
+
f" if isinstance({data_var}, list):",
|
|
445
|
+
f" return [{list_inner}.model_validate(item) if isinstance(item, dict) else item for item in {data_var}]",
|
|
446
|
+
]
|
|
447
|
+
)
|
|
448
|
+
return
|
|
449
|
+
|
|
450
|
+
dict_inner = _extract_dict_value_type(type_hint)
|
|
451
|
+
if dict_inner and _is_model_type(dict_inner):
|
|
452
|
+
lines.extend(
|
|
453
|
+
[
|
|
454
|
+
f" if isinstance({data_var}, dict):",
|
|
455
|
+
f" return {{k: {dict_inner}.model_validate(v) if isinstance(v, dict) else v for k, v in {data_var}.items()}}",
|
|
456
|
+
]
|
|
457
|
+
)
|
|
458
|
+
return
|
|
459
|
+
|
|
460
|
+
if _is_model_type(type_hint):
|
|
461
|
+
lines.extend(
|
|
462
|
+
[
|
|
463
|
+
f" if isinstance({data_var}, dict):",
|
|
464
|
+
f" return {type_hint}.model_validate({data_var})",
|
|
465
|
+
]
|
|
466
|
+
)
|
|
467
|
+
|
|
468
|
+
|
|
376
469
|
def generate_pydantic_models(schema: dict[str, Any]) -> str:
|
|
377
470
|
"""Generate Pydantic models from OpenAPI schemas."""
|
|
378
471
|
schemas = schema.get("components", {}).get("schemas", {})
|
|
@@ -459,6 +552,18 @@ def generate_pydantic_models(schema: dict[str, Any]) -> str:
|
|
|
459
552
|
return "\n".join(lines)
|
|
460
553
|
|
|
461
554
|
|
|
555
|
+
def _collect_handler_routes(resources: dict[str, Resource]) -> dict[str, list[tuple[str, str]]]:
|
|
556
|
+
"""Scan all endpoints and group (method, path) pairs by their response_handler."""
|
|
557
|
+
handler_routes: dict[str, list[tuple[str, str]]] = {}
|
|
558
|
+
for resource in resources.values():
|
|
559
|
+
for endpoint in resource.endpoints:
|
|
560
|
+
if endpoint.response_handler:
|
|
561
|
+
handler_routes.setdefault(endpoint.response_handler, []).append(
|
|
562
|
+
(endpoint.method, endpoint.path)
|
|
563
|
+
)
|
|
564
|
+
return handler_routes
|
|
565
|
+
|
|
566
|
+
|
|
462
567
|
def generate_sync_client(resources: dict[str, Resource]) -> str:
|
|
463
568
|
"""Generate synchronous client code."""
|
|
464
569
|
lines = [
|
|
@@ -554,6 +659,19 @@ def generate_sync_client(resources: dict[str, Resource]) -> str:
|
|
|
554
659
|
" body=response.text,",
|
|
555
660
|
" )",
|
|
556
661
|
"",
|
|
662
|
+
]
|
|
663
|
+
)
|
|
664
|
+
|
|
665
|
+
# Inject generated routing: one if-block per handler, checking (method, path)
|
|
666
|
+
handler_routes = _collect_handler_routes(resources)
|
|
667
|
+
for handler, routes in sorted(handler_routes.items()):
|
|
668
|
+
route_set = "{" + ", ".join(f'("{m}", "{p}")' for m, p in sorted(routes)) + "}"
|
|
669
|
+
lines.append(f" if (method, path) in {route_set}:")
|
|
670
|
+
lines.append(f" return self.{handler}(response)")
|
|
671
|
+
lines.append("")
|
|
672
|
+
|
|
673
|
+
lines.extend(
|
|
674
|
+
[
|
|
557
675
|
" try:",
|
|
558
676
|
" data = response.json()",
|
|
559
677
|
" except Exception:",
|
|
@@ -561,6 +679,10 @@ def generate_sync_client(resources: dict[str, Resource]) -> str:
|
|
|
561
679
|
"",
|
|
562
680
|
" return data",
|
|
563
681
|
"",
|
|
682
|
+
" def _request_for_location(self, response: Any) -> str:",
|
|
683
|
+
' """Extract the Location header from a response."""',
|
|
684
|
+
' return response.headers["Location"]',
|
|
685
|
+
"",
|
|
564
686
|
"",
|
|
565
687
|
]
|
|
566
688
|
)
|
|
@@ -635,7 +757,10 @@ def generate_sync_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
635
757
|
|
|
636
758
|
# Method signature
|
|
637
759
|
param_str = ", ".join(["self"] + params) if params else "self"
|
|
638
|
-
|
|
760
|
+
if endpoint.response_handler:
|
|
761
|
+
return_type = endpoint.response_handler_return_type or "Any"
|
|
762
|
+
else:
|
|
763
|
+
return_type = endpoint.response_type or "None"
|
|
639
764
|
lines.append(f" def {method_name}({param_str}) -> {return_type}:")
|
|
640
765
|
|
|
641
766
|
# Docstring
|
|
@@ -675,10 +800,21 @@ def generate_sync_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
675
800
|
else:
|
|
676
801
|
lines.append(" body_data = None")
|
|
677
802
|
|
|
678
|
-
# Make request
|
|
679
|
-
|
|
680
|
-
|
|
681
|
-
|
|
803
|
+
# Make request and coerce response payload into typed models where possible
|
|
804
|
+
if endpoint.response_handler:
|
|
805
|
+
lines.append(
|
|
806
|
+
f' return self._client.request("{endpoint.method}", path, params=params, body=body_data)'
|
|
807
|
+
)
|
|
808
|
+
elif endpoint.response_type is None:
|
|
809
|
+
lines.append(
|
|
810
|
+
f' self._client.request("{endpoint.method}", path, params=params, body=body_data)'
|
|
811
|
+
)
|
|
812
|
+
else:
|
|
813
|
+
lines.append(
|
|
814
|
+
f' data = self._client.request("{endpoint.method}", path, params=params, body=body_data)'
|
|
815
|
+
)
|
|
816
|
+
_append_response_mapping(lines, return_type, "data")
|
|
817
|
+
lines.append(" return data")
|
|
682
818
|
|
|
683
819
|
return lines
|
|
684
820
|
|
|
@@ -778,6 +914,19 @@ def generate_async_client(resources: dict[str, Resource]) -> str:
|
|
|
778
914
|
" body=response.text,",
|
|
779
915
|
" )",
|
|
780
916
|
"",
|
|
917
|
+
]
|
|
918
|
+
)
|
|
919
|
+
|
|
920
|
+
# Inject generated routing: one if-block per handler, checking (method, path)
|
|
921
|
+
handler_routes = _collect_handler_routes(resources)
|
|
922
|
+
for handler, routes in sorted(handler_routes.items()):
|
|
923
|
+
route_set = "{" + ", ".join(f'("{m}", "{p}")' for m, p in sorted(routes)) + "}"
|
|
924
|
+
lines.append(f" if (method, path) in {route_set}:")
|
|
925
|
+
lines.append(f" return self.{handler}(response)")
|
|
926
|
+
lines.append("")
|
|
927
|
+
|
|
928
|
+
lines.extend(
|
|
929
|
+
[
|
|
781
930
|
" try:",
|
|
782
931
|
" data = response.json()",
|
|
783
932
|
" except Exception:",
|
|
@@ -785,6 +934,10 @@ def generate_async_client(resources: dict[str, Resource]) -> str:
|
|
|
785
934
|
"",
|
|
786
935
|
" return data",
|
|
787
936
|
"",
|
|
937
|
+
" def _request_for_location(self, response: Any) -> str:",
|
|
938
|
+
' """Extract the Location header from a response."""',
|
|
939
|
+
' return response.headers["Location"]',
|
|
940
|
+
"",
|
|
788
941
|
"",
|
|
789
942
|
]
|
|
790
943
|
)
|
|
@@ -859,7 +1012,10 @@ def generate_async_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
859
1012
|
|
|
860
1013
|
# Method signature
|
|
861
1014
|
param_str = ", ".join(["self"] + params) if params else "self"
|
|
862
|
-
|
|
1015
|
+
if endpoint.response_handler:
|
|
1016
|
+
return_type = endpoint.response_handler_return_type or "Any"
|
|
1017
|
+
else:
|
|
1018
|
+
return_type = endpoint.response_type or "None"
|
|
863
1019
|
lines.append(f" async def {method_name}({param_str}) -> {return_type}:")
|
|
864
1020
|
|
|
865
1021
|
# Docstring
|
|
@@ -899,10 +1055,21 @@ def generate_async_method(endpoint: Endpoint, method_name: str) -> list[str]:
|
|
|
899
1055
|
else:
|
|
900
1056
|
lines.append(" body_data = None")
|
|
901
1057
|
|
|
902
|
-
# Make request
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
1058
|
+
# Make request and coerce response payload into typed models where possible
|
|
1059
|
+
if endpoint.response_handler:
|
|
1060
|
+
lines.append(
|
|
1061
|
+
f' return await self._client.request("{endpoint.method}", path, params=params, body=body_data)'
|
|
1062
|
+
)
|
|
1063
|
+
elif endpoint.response_type is None:
|
|
1064
|
+
lines.append(
|
|
1065
|
+
f' await self._client.request("{endpoint.method}", path, params=params, body=body_data)'
|
|
1066
|
+
)
|
|
1067
|
+
else:
|
|
1068
|
+
lines.append(
|
|
1069
|
+
f' data = await self._client.request("{endpoint.method}", path, params=params, body=body_data)'
|
|
1070
|
+
)
|
|
1071
|
+
_append_response_mapping(lines, return_type, "data")
|
|
1072
|
+
lines.append(" return data")
|
|
906
1073
|
|
|
907
1074
|
return lines
|
|
908
1075
|
|