11from __future__ import annotations
22
3- from typing import Any , cast
4-
53import httpx2 as httpx
4+ from pydantic import TypeAdapter , ValidationError
65
76from aai_cli .auth import endpoints
87from aai_cli .errors import APIError , NotAuthenticated
98
109_TIMEOUT = 30.0
10+ _HTTP_ERROR_MIN_STATUS = 400
11+ _JSON_OBJECT : TypeAdapter [dict [str , object ]] = TypeAdapter (dict [str , object ])
12+ _JSON_OBJECTS : TypeAdapter [list [dict [str , object ]]] = TypeAdapter (list [dict [str , object ]])
1113
1214
1315def _detail (resp : httpx .Response ) -> str :
16+ fallback = resp .text or f"HTTP { resp .status_code } "
1417 try :
15- body = resp .json ()
16- if isinstance (body , dict ) and "detail" in body :
17- return str (body ["detail" ])
18- except Exception : # noqa: BLE001,S110 - non-JSON error body; pass is intentional
19- pass
20- return resp .text or f"HTTP { resp .status_code } "
18+ body : object = resp .json ()
19+ mapping = _JSON_OBJECT .validate_python (body )
20+ if "detail" in mapping :
21+ return str (mapping ["detail" ])
22+ except (TypeError , ValueError , ValidationError ):
23+ return fallback
24+ return fallback
2125
2226
2327def _raise_for_error (resp : httpx .Response ) -> None :
2428 if resp .status_code in (401 , 403 ):
2529 raise NotAuthenticated (f"AMS rejected the login ({ resp .status_code } ): { _detail (resp )} " )
26- if resp .status_code >= 400 :
30+ if resp .status_code >= _HTTP_ERROR_MIN_STATUS :
2731 raise APIError (f"AMS request failed ({ resp .status_code } ): { _detail (resp )} " )
2832
2933
30- def _json_or_raise (resp : httpx .Response ) -> Any :
34+ def _json_or_raise (resp : httpx .Response ) -> object :
3135 _raise_for_error (resp )
32- return resp .json ()
36+ data : object = resp .json ()
37+ return data
38+
39+
40+ def _json_object_or_raise (resp : httpx .Response ) -> dict [str , object ]:
41+ data = _json_or_raise (resp )
42+ try :
43+ return _JSON_OBJECT .validate_python (data )
44+ except ValidationError as exc :
45+ raise APIError (
46+ f"AMS request returned unexpected JSON: expected object, got { type (data ).__name__ } ."
47+ ) from exc
48+
49+
50+ def _json_object_list_or_raise (resp : httpx .Response ) -> list [dict [str , object ]]:
51+ data = _json_or_raise (resp )
52+ try :
53+ return _JSON_OBJECTS .validate_python (data )
54+ except ValidationError as exc :
55+ raise APIError (
56+ f"AMS request returned unexpected JSON: expected list of objects, got { type (data ).__name__ } ."
57+ ) from exc
3358
3459
3560def _client (session_jwt : str | None = None ) -> httpx .Client :
@@ -38,17 +63,17 @@ def _client(session_jwt: str | None = None) -> httpx.Client:
3863 return httpx .Client (base_url = endpoints .ams_base (), timeout = _TIMEOUT , cookies = cookies )
3964
4065
41- def discover (token : str ) -> dict [str , Any ]:
66+ def discover (token : str ) -> dict [str , object ]:
4267 """POST /v2/auth/discover with a discovery_oauth token -> {orgs, email, IST}."""
4368 with _client () as client :
4469 resp = client .post (
4570 "/v2/auth/discover" ,
4671 json = {"token" : token , "token_type" : "discovery_oauth" },
4772 )
48- return cast ( dict [ str , Any ], _json_or_raise ( resp ) )
73+ return _json_object_or_raise ( resp )
4974
5075
51- def exchange (intermediate_session_token : str , organization_id : str ) -> dict [str , Any ]:
76+ def exchange (intermediate_session_token : str , organization_id : str ) -> dict [str , object ]:
5277 """POST /v2/auth/exchange -> SignedInResponse {account, session_jwt, session_token}."""
5378 with _client () as client :
5479 resp = client .post (
@@ -58,55 +83,55 @@ def exchange(intermediate_session_token: str, organization_id: str) -> dict[str,
5883 "organization_id" : organization_id ,
5984 },
6085 )
61- return cast ( dict [ str , Any ], _json_or_raise ( resp ) )
86+ return _json_object_or_raise ( resp )
6287
6388
64- def list_projects (account_id : int , session_jwt : str ) -> list [dict [str , Any ]]:
89+ def list_projects (account_id : int , session_jwt : str ) -> list [dict [str , object ]]:
6590 """GET /v1/users/accounts/{id}/projects -> [{project, tokens[]}]."""
6691 with _client (session_jwt ) as client :
6792 resp = client .get (f"/v1/users/accounts/{ account_id } /projects" )
68- return cast ( list [ dict [ str , Any ]], _json_or_raise ( resp ) )
93+ return _json_object_list_or_raise ( resp )
6994
7095
7196def create_token (
7297 account_id : int , project_id : int , token_name : str , session_jwt : str
73- ) -> dict [str , Any ]:
98+ ) -> dict [str , object ]:
7499 """POST /v1/users/accounts/{id}/tokens -> TokenSchema incl. `api_key`."""
75100 with _client (session_jwt ) as client :
76101 resp = client .post (
77102 f"/v1/users/accounts/{ account_id } /tokens" ,
78103 json = {"project_id" : project_id , "token_name" : token_name },
79104 )
80- return cast ( dict [ str , Any ], _json_or_raise ( resp ) )
105+ return _json_object_or_raise ( resp )
81106
82107
83- def get_balance (session_jwt : str ) -> dict [str , Any ]:
108+ def get_balance (session_jwt : str ) -> dict [str , object ]:
84109 """GET /v2/billing/balance -> {account_id, balance_in_cents, ...}."""
85110 with _client (session_jwt ) as client :
86111 resp = client .get ("/v2/billing/balance" )
87- return cast ( dict [ str , Any ], _json_or_raise ( resp ) )
112+ return _json_object_or_raise ( resp )
88113
89114
90115def get_usage (
91116 session_jwt : str ,
92117 starting_on : str ,
93118 ending_before : str ,
94119 window_size : str | None = None ,
95- ) -> dict [str , Any ]:
120+ ) -> dict [str , object ]:
96121 """POST /v2/billing/usage (ISO dates) -> {usage_items: [...]}."""
97- body : dict [str , Any ] = {"starting_on" : starting_on , "ending_before" : ending_before }
122+ body : dict [str , object ] = {"starting_on" : starting_on , "ending_before" : ending_before }
98123 if window_size :
99124 body ["window_size" ] = window_size
100125 with _client (session_jwt ) as client :
101126 resp = client .post ("/v2/billing/usage" , json = body )
102- return cast ( dict [ str , Any ], _json_or_raise ( resp ) )
127+ return _json_object_or_raise ( resp )
103128
104129
105- def get_rate_limits (account_id : int , session_jwt : str ) -> dict [str , Any ]:
130+ def get_rate_limits (account_id : int , session_jwt : str ) -> dict [str , object ]:
106131 """GET /v1/users/accounts/{id}/rate-limits -> {rate_limits: [...]}."""
107132 with _client (session_jwt ) as client :
108133 resp = client .get (f"/v1/users/accounts/{ account_id } /rate-limits" )
109- return cast ( dict [ str , Any ], _json_or_raise ( resp ) )
134+ return _json_object_or_raise ( resp )
110135
111136
112137def rename_token (account_id : int , token_id : int , token_name : str , session_jwt : str ) -> None :
@@ -119,24 +144,28 @@ def rename_token(account_id: int, token_id: int, token_name: str, session_jwt: s
119144 _raise_for_error (resp )
120145
121146
122- def list_streaming (session_jwt : str , ** filters : Any ) -> dict [str , Any ]:
147+ def list_streaming (session_jwt : str , ** filters : object ) -> dict [str , object ]:
123148 """GET /v1/users/streaming -> {page_details, data: [StreamingSessionSchema]}."""
124- params = {k : v for k , v in filters .items () if v is not None }
149+ params = {
150+ key : value for key , value in filters .items () if isinstance (value , str | int | float | bool )
151+ }
125152 with _client (session_jwt ) as client :
126153 resp = client .get ("/v1/users/streaming" , params = params )
127- return cast ( dict [ str , Any ], _json_or_raise ( resp ) )
154+ return _json_object_or_raise ( resp )
128155
129156
130- def get_streaming (session_id : str , session_jwt : str ) -> dict [str , Any ]:
157+ def get_streaming (session_id : str , session_jwt : str ) -> dict [str , object ]:
131158 """GET /v1/users/streaming/{session_id} -> StreamingSessionSchema."""
132159 with _client (session_jwt ) as client :
133160 resp = client .get (f"/v1/users/streaming/{ session_id } " )
134- return cast ( dict [ str , Any ], _json_or_raise ( resp ) )
161+ return _json_object_or_raise ( resp )
135162
136163
137- def list_audit_logs (session_jwt : str , ** filters : Any ) -> dict [str , Any ]:
164+ def list_audit_logs (session_jwt : str , ** filters : object ) -> dict [str , object ]:
138165 """GET /v2/user/audit-logs -> {page_details, data: [AuditLogResponse]}."""
139- params = {k : v for k , v in filters .items () if v is not None }
166+ params = {
167+ key : value for key , value in filters .items () if isinstance (value , str | int | float | bool )
168+ }
140169 with _client (session_jwt ) as client :
141170 resp = client .get ("/v2/user/audit-logs" , params = params )
142- return cast ( dict [ str , Any ], _json_or_raise ( resp ) )
171+ return _json_object_or_raise ( resp )
0 commit comments