azul-client 9.0.24__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.
- azul_client/__init__.py +4 -0
- azul_client/api/__init__.py +74 -0
- azul_client/api/base_api.py +163 -0
- azul_client/api/binaries_data.py +513 -0
- azul_client/api/binaries_meta.py +510 -0
- azul_client/api/features.py +175 -0
- azul_client/api/plugins.py +49 -0
- azul_client/api/purge.py +71 -0
- azul_client/api/security.py +29 -0
- azul_client/api/sources.py +51 -0
- azul_client/api/statistics.py +23 -0
- azul_client/api/users.py +29 -0
- azul_client/client.py +510 -0
- azul_client/config.py +116 -0
- azul_client/exceptions.py +30 -0
- azul_client/oidc/__init__.py +5 -0
- azul_client/oidc/callback.py +73 -0
- azul_client/oidc/oidc.py +215 -0
- azul_client-9.0.24.dist-info/METADATA +102 -0
- azul_client-9.0.24.dist-info/RECORD +23 -0
- azul_client-9.0.24.dist-info/WHEEL +5 -0
- azul_client-9.0.24.dist-info/entry_points.txt +2 -0
- azul_client-9.0.24.dist-info/top_level.txt +1 -0
azul_client/__init__.py
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
"""Api wrapper."""
|
|
2
|
+
|
|
3
|
+
import logging
|
|
4
|
+
import sys
|
|
5
|
+
|
|
6
|
+
from azul_client import config, oidc
|
|
7
|
+
|
|
8
|
+
from . import (
|
|
9
|
+
binaries_data,
|
|
10
|
+
binaries_meta,
|
|
11
|
+
features,
|
|
12
|
+
plugins,
|
|
13
|
+
purge,
|
|
14
|
+
security,
|
|
15
|
+
sources,
|
|
16
|
+
statistics,
|
|
17
|
+
users,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
logging.basicConfig(stream=sys.stdout, level=logging.WARNING)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class Api:
|
|
24
|
+
"""Contains api implementation instances.
|
|
25
|
+
|
|
26
|
+
This is so we can chain different API calls without having to
|
|
27
|
+
juggle multiple objects.
|
|
28
|
+
"""
|
|
29
|
+
|
|
30
|
+
def __init__(self, conf: config.Config | None = None) -> None:
|
|
31
|
+
# api support
|
|
32
|
+
self.config = conf if conf else config.get_config()
|
|
33
|
+
self.auth = oidc.OIDC(self.config)
|
|
34
|
+
|
|
35
|
+
self._api_implementations = []
|
|
36
|
+
# api implementations
|
|
37
|
+
self.binaries_data = binaries_data.BinariesData(self.config, self.auth.get_client)
|
|
38
|
+
self._api_implementations.append(self.binaries_data)
|
|
39
|
+
|
|
40
|
+
self.binaries_meta = binaries_meta.BinariesMeta(self.config, self.auth.get_client)
|
|
41
|
+
self._api_implementations.append(self.binaries_meta)
|
|
42
|
+
|
|
43
|
+
self.features = features.Features(self.config, self.auth.get_client)
|
|
44
|
+
self._api_implementations.append(self.features)
|
|
45
|
+
|
|
46
|
+
self.plugins = plugins.Plugins(self.config, self.auth.get_client)
|
|
47
|
+
self._api_implementations.append(self.plugins)
|
|
48
|
+
|
|
49
|
+
self.purge = purge.Purge(self.config, self.auth.get_client)
|
|
50
|
+
self._api_implementations.append(self.purge)
|
|
51
|
+
|
|
52
|
+
self.security = security.Security(self.config, self.auth.get_client)
|
|
53
|
+
self._api_implementations.append(self.security)
|
|
54
|
+
|
|
55
|
+
self.sources = sources.Sources(self.config, self.auth.get_client)
|
|
56
|
+
self._api_implementations.append(self.sources)
|
|
57
|
+
|
|
58
|
+
self.statistics = statistics.Statistics(self.config, self.auth.get_client)
|
|
59
|
+
self._api_implementations.append(self.statistics)
|
|
60
|
+
|
|
61
|
+
self.users = users.Users(self.config, self.auth.get_client)
|
|
62
|
+
self._api_implementations.append(self.users)
|
|
63
|
+
|
|
64
|
+
def get_excluded_security(self) -> list[str]:
|
|
65
|
+
"""Get excluded security."""
|
|
66
|
+
return self._excluded_security
|
|
67
|
+
|
|
68
|
+
def set_excluded_security(self, security_list: list[str]):
|
|
69
|
+
"""Set excluded security's to the provided values."""
|
|
70
|
+
self._excluded_security = security_list
|
|
71
|
+
|
|
72
|
+
# Set excluded security for all API's
|
|
73
|
+
for api in self._api_implementations:
|
|
74
|
+
api._excluded_security = self._excluded_security
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
"""Base API class used by all other Azul API classes."""
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import logging
|
|
5
|
+
from http import HTTPMethod
|
|
6
|
+
from typing import Any, Callable, TypeVar
|
|
7
|
+
|
|
8
|
+
import httpx
|
|
9
|
+
from azul_bedrock import models_restapi
|
|
10
|
+
from pydantic import BaseModel, TypeAdapter
|
|
11
|
+
|
|
12
|
+
from azul_client import config, exceptions
|
|
13
|
+
|
|
14
|
+
T = TypeVar("T", bound=BaseModel)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseApiHandler:
|
|
18
|
+
"""Base class for handling all restapi calls with generic functionality."""
|
|
19
|
+
|
|
20
|
+
def __init__(self, cfg: config.Config, get_client: Callable[[], httpx.Client]):
|
|
21
|
+
self.logger = logging.getLogger(type(self).__name__)
|
|
22
|
+
self.cfg = cfg
|
|
23
|
+
self.__get_client = get_client
|
|
24
|
+
self._last_meta = None
|
|
25
|
+
self.__excluded_security = None
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def _excluded_security(self) -> list[str]:
|
|
29
|
+
"""Get excluded security (recommended to get this at the API level)."""
|
|
30
|
+
return self.__excluded_security
|
|
31
|
+
|
|
32
|
+
@_excluded_security.setter
|
|
33
|
+
def _excluded_security(self, security_list: list[str]):
|
|
34
|
+
"""Set excluded security's to the provided values (internal use only get directly get from API level)."""
|
|
35
|
+
self.__excluded_security = security_list
|
|
36
|
+
|
|
37
|
+
def get_meta_from_last_request(self) -> models_restapi.Meta | None:
|
|
38
|
+
"""Get the metadata from the last request that was made, if there is no metadata return None."""
|
|
39
|
+
if self._last_meta:
|
|
40
|
+
return models_restapi.Meta.model_validate(self._last_meta)
|
|
41
|
+
return None
|
|
42
|
+
|
|
43
|
+
def __request_to_client(
|
|
44
|
+
self,
|
|
45
|
+
method: HTTPMethod,
|
|
46
|
+
url: str,
|
|
47
|
+
params: httpx.QueryParams | dict | None = None,
|
|
48
|
+
json: Any = None,
|
|
49
|
+
timeout: int | None = None,
|
|
50
|
+
) -> httpx.Response:
|
|
51
|
+
if timeout is None:
|
|
52
|
+
timeout = httpx.USE_CLIENT_DEFAULT
|
|
53
|
+
if self._excluded_security:
|
|
54
|
+
if not params:
|
|
55
|
+
params = dict()
|
|
56
|
+
params["x"] = self._excluded_security
|
|
57
|
+
if method == HTTPMethod.GET:
|
|
58
|
+
if json is not None:
|
|
59
|
+
raise ValueError("Get request cannot accept a body parameter.")
|
|
60
|
+
return self.__get_client().get(url, params=params, timeout=timeout)
|
|
61
|
+
elif method == HTTPMethod.POST:
|
|
62
|
+
return self.__get_client().post(url, params=params, json=json, timeout=timeout)
|
|
63
|
+
elif method == HTTPMethod.DELETE:
|
|
64
|
+
if json is not None:
|
|
65
|
+
raise ValueError("Delete request cannot accept a body parameter.")
|
|
66
|
+
return self.__get_client().delete(url, params=params, timeout=timeout)
|
|
67
|
+
raise ValueError(
|
|
68
|
+
f"The provided method '{method}' is invalid it must be one of "
|
|
69
|
+
+ f"{', '.join([HTTPMethod.POST, HTTPMethod.POST])}"
|
|
70
|
+
)
|
|
71
|
+
|
|
72
|
+
def _request_with_pydantic_model_response(
|
|
73
|
+
self,
|
|
74
|
+
*,
|
|
75
|
+
method: HTTPMethod,
|
|
76
|
+
url: str,
|
|
77
|
+
response_model: type[T],
|
|
78
|
+
params: httpx.QueryParams | dict | None = None,
|
|
79
|
+
json: Any = None,
|
|
80
|
+
get_data_only: bool = False,
|
|
81
|
+
timeout: int | None = None,
|
|
82
|
+
) -> T:
|
|
83
|
+
"""Generic Handler for requests.
|
|
84
|
+
|
|
85
|
+
:param HTTPMethod method: HTTP method to use only GET and POST are supported.
|
|
86
|
+
:param str url: url to post or get to.
|
|
87
|
+
:param BaseModel response_model: Expected Pydantic response model.
|
|
88
|
+
:param httpx.QueryParams params: parameters for the request.
|
|
89
|
+
:param Any json: raw body that will be json encoded and sent with POST request, doesn't work with get requests.
|
|
90
|
+
:param bool get_data_only: will extract out the 'data' key from the response and throws an
|
|
91
|
+
exception if there is no data field.
|
|
92
|
+
"""
|
|
93
|
+
# API requests should always clear last requests metadata
|
|
94
|
+
self._last_meta = None
|
|
95
|
+
resp = self.__request_to_client(method=method, url=url, params=params, json=json, timeout=timeout)
|
|
96
|
+
|
|
97
|
+
if resp.status_code != 200 and resp.status_code != 206:
|
|
98
|
+
raise exceptions.bad_response(resp)
|
|
99
|
+
|
|
100
|
+
raw_content: str | bytes = resp.content
|
|
101
|
+
if get_data_only:
|
|
102
|
+
raw_content = self._get_response_data(resp)
|
|
103
|
+
|
|
104
|
+
try:
|
|
105
|
+
if isinstance(response_model, TypeAdapter):
|
|
106
|
+
return response_model.validate_json(raw_content)
|
|
107
|
+
return response_model.model_validate_json(raw_content)
|
|
108
|
+
except Exception:
|
|
109
|
+
self.logger.error(f"Failed to deserialize pydantic model {type(response_model)}.")
|
|
110
|
+
self.logger.error(f"Response started with {raw_content[:500]}")
|
|
111
|
+
raise
|
|
112
|
+
|
|
113
|
+
def _request(
|
|
114
|
+
self,
|
|
115
|
+
*,
|
|
116
|
+
method: HTTPMethod,
|
|
117
|
+
url: str,
|
|
118
|
+
params: httpx.QueryParams = None,
|
|
119
|
+
json: Any = None,
|
|
120
|
+
timeout: int | None = None,
|
|
121
|
+
) -> httpx.Response:
|
|
122
|
+
"""Send a http request with the provided method to azul, and provide the json response."""
|
|
123
|
+
# API requests should always clear last requests metadata
|
|
124
|
+
self._last_meta = None
|
|
125
|
+
resp = self.__request_to_client(method=method, url=url, params=params, json=json, timeout=timeout)
|
|
126
|
+
|
|
127
|
+
if resp.status_code != 200 and resp.status_code != 206:
|
|
128
|
+
raise exceptions.bad_response(resp)
|
|
129
|
+
return resp
|
|
130
|
+
|
|
131
|
+
def _request_upload(self, *, url: str, params: dict, files: dict, data: dict, timeout: int) -> httpx.Response:
|
|
132
|
+
"""Special request type for uploading files."""
|
|
133
|
+
# API requests should always clear last requests metadata
|
|
134
|
+
self._last_meta = None
|
|
135
|
+
return self.__get_client().post(url=url, params=params, files=files, data=data, timeout=timeout)
|
|
136
|
+
|
|
137
|
+
def _generic_head_request(self, url: str) -> bool:
|
|
138
|
+
"""Generic request to check if a resource exists."""
|
|
139
|
+
# API requests should always clear last requests metadata
|
|
140
|
+
self._last_meta = None
|
|
141
|
+
resp = self.__get_client().head(url)
|
|
142
|
+
if resp.status_code == 404:
|
|
143
|
+
return False
|
|
144
|
+
if resp.status_code != 200 and resp.status_code != 206:
|
|
145
|
+
raise exceptions.bad_response(resp)
|
|
146
|
+
return True
|
|
147
|
+
|
|
148
|
+
def _get_response_data(self, resp: httpx.Response) -> str:
|
|
149
|
+
"""Get the 'data' key from a response when it may also have a 'metadata' field."""
|
|
150
|
+
json_response = resp.json()
|
|
151
|
+
data = json_response.get("data", None)
|
|
152
|
+
self._last_meta = json_response.get("meta", None)
|
|
153
|
+
if data is None:
|
|
154
|
+
raise Exception("Response has no 'data' key and 'data cannot be extracted.")
|
|
155
|
+
|
|
156
|
+
return json.dumps(data)
|
|
157
|
+
|
|
158
|
+
def filter_none_values(self, params: dict) -> dict:
|
|
159
|
+
"""Takes a dictionary and filters out all keys with None values."""
|
|
160
|
+
for k, v in list(params.items()):
|
|
161
|
+
if v is None:
|
|
162
|
+
params.pop(k)
|
|
163
|
+
return params
|