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.
@@ -0,0 +1,4 @@
1
+ from .api import Api
2
+ from .config import Config
3
+
4
+ __all__ = ["Api", "Config"]
@@ -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