pymenderio 0.1.0__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.
- pymenderio/__init__.py +40 -0
- pymenderio/_http.py +417 -0
- pymenderio/api/__init__.py +43 -0
- pymenderio/api/artifacts.py +294 -0
- pymenderio/api/deployments.py +529 -0
- pymenderio/api/deviceconfigure.py +80 -0
- pymenderio/api/deviceconnect.py +284 -0
- pymenderio/api/devicemonitor.py +198 -0
- pymenderio/api/devices.py +423 -0
- pymenderio/api/filters.py +147 -0
- pymenderio/api/inventory.py +507 -0
- pymenderio/api/iot_manager.py +372 -0
- pymenderio/api/releases.py +268 -0
- pymenderio/api/tenantadm.py +280 -0
- pymenderio/api/useradm.py +354 -0
- pymenderio/auth.py +323 -0
- pymenderio/client.py +244 -0
- pymenderio/exceptions.py +69 -0
- pymenderio/models/__init__.py +231 -0
- pymenderio/models/artifact.py +107 -0
- pymenderio/models/common.py +16 -0
- pymenderio/models/connect.py +86 -0
- pymenderio/models/deployment.py +148 -0
- pymenderio/models/device.py +83 -0
- pymenderio/models/filter.py +109 -0
- pymenderio/models/inventory.py +74 -0
- pymenderio/models/iot.py +205 -0
- pymenderio/models/monitor.py +89 -0
- pymenderio/models/tenant.py +221 -0
- pymenderio/models/user.py +139 -0
- pymenderio-0.1.0.dist-info/METADATA +680 -0
- pymenderio-0.1.0.dist-info/RECORD +34 -0
- pymenderio-0.1.0.dist-info/WHEEL +4 -0
- pymenderio-0.1.0.dist-info/licenses/LICENSE +21 -0
pymenderio/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
pymenderio - Python client library for the Mender API.
|
|
5
|
+
|
|
6
|
+
For a complete public SDK reference (methods, classes, and errors), see API.md.
|
|
7
|
+
|
|
8
|
+
Example usage:
|
|
9
|
+
|
|
10
|
+
from pymenderio import MenderClient
|
|
11
|
+
|
|
12
|
+
# Sync usage with mender-cli config and token defaults
|
|
13
|
+
with MenderClient.from_mender_cli() as client:
|
|
14
|
+
devices = client.devices.list(status="accepted")
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
from pymenderio.client import AsyncMenderClient, MenderClient
|
|
18
|
+
from pymenderio.exceptions import (
|
|
19
|
+
AuthenticationError,
|
|
20
|
+
ConflictError,
|
|
21
|
+
MenderError,
|
|
22
|
+
NotFoundError,
|
|
23
|
+
RateLimitError,
|
|
24
|
+
ServerError,
|
|
25
|
+
ValidationError,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
__version__ = "0.1.0"
|
|
29
|
+
|
|
30
|
+
__all__ = [
|
|
31
|
+
"MenderClient",
|
|
32
|
+
"AsyncMenderClient",
|
|
33
|
+
"MenderError",
|
|
34
|
+
"AuthenticationError",
|
|
35
|
+
"NotFoundError",
|
|
36
|
+
"ConflictError",
|
|
37
|
+
"ValidationError",
|
|
38
|
+
"RateLimitError",
|
|
39
|
+
"ServerError",
|
|
40
|
+
]
|
pymenderio/_http.py
ADDED
|
@@ -0,0 +1,417 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""Internal HTTP client wrapper with transparent pagination and error handling."""
|
|
4
|
+
|
|
5
|
+
from __future__ import annotations
|
|
6
|
+
|
|
7
|
+
import re
|
|
8
|
+
from typing import Any, TypeVar, cast
|
|
9
|
+
|
|
10
|
+
import httpx
|
|
11
|
+
|
|
12
|
+
from pymenderio.exceptions import (
|
|
13
|
+
AuthenticationError,
|
|
14
|
+
ConflictError,
|
|
15
|
+
MenderError,
|
|
16
|
+
NotFoundError,
|
|
17
|
+
RateLimitError,
|
|
18
|
+
ServerError,
|
|
19
|
+
ValidationError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
T = TypeVar("T")
|
|
23
|
+
|
|
24
|
+
# Maximum items per page (Mender API limit)
|
|
25
|
+
_PER_PAGE = 500
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _parse_link_header(link_header: str) -> dict[str, str]:
|
|
29
|
+
"""Parse RFC 5988 Link header into a dict of rel -> url."""
|
|
30
|
+
links: dict[str, str] = {}
|
|
31
|
+
for part in link_header.split(","):
|
|
32
|
+
match = re.match(r'\s*<([^>]+)>;\s*rel="([^"]+)"', part.strip())
|
|
33
|
+
if match:
|
|
34
|
+
url, rel = match.groups()
|
|
35
|
+
links[rel] = url
|
|
36
|
+
return links
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _raise_for_status(response: httpx.Response) -> None:
|
|
40
|
+
"""Raise appropriate MenderError based on HTTP status code."""
|
|
41
|
+
if response.status_code < 400:
|
|
42
|
+
return
|
|
43
|
+
|
|
44
|
+
# Try to extract error details from response body
|
|
45
|
+
request_id: str | None = response.headers.get("X-Request-Id")
|
|
46
|
+
body: dict[str, Any] | None = None
|
|
47
|
+
try:
|
|
48
|
+
raw_body: Any = response.json()
|
|
49
|
+
if isinstance(raw_body, dict):
|
|
50
|
+
body = cast(dict[str, Any], raw_body)
|
|
51
|
+
|
|
52
|
+
if body is not None:
|
|
53
|
+
raw_message = body.get("error")
|
|
54
|
+
message = raw_message if isinstance(raw_message, str) else (response.reason_phrase or "Unknown error")
|
|
55
|
+
|
|
56
|
+
# Also check for request_id in the JSON body
|
|
57
|
+
if not request_id:
|
|
58
|
+
raw_request_id = body.get("request_id")
|
|
59
|
+
if isinstance(raw_request_id, str):
|
|
60
|
+
request_id = raw_request_id
|
|
61
|
+
else:
|
|
62
|
+
message = response.reason_phrase or "Unknown error"
|
|
63
|
+
except Exception:
|
|
64
|
+
message = response.reason_phrase or "Unknown error"
|
|
65
|
+
|
|
66
|
+
if response.status_code == 401:
|
|
67
|
+
raise AuthenticationError(
|
|
68
|
+
message,
|
|
69
|
+
status_code=response.status_code,
|
|
70
|
+
request_id=request_id,
|
|
71
|
+
response_body=body,
|
|
72
|
+
)
|
|
73
|
+
elif response.status_code == 404:
|
|
74
|
+
raise NotFoundError(
|
|
75
|
+
message,
|
|
76
|
+
status_code=response.status_code,
|
|
77
|
+
request_id=request_id,
|
|
78
|
+
response_body=body,
|
|
79
|
+
)
|
|
80
|
+
elif response.status_code == 409:
|
|
81
|
+
raise ConflictError(
|
|
82
|
+
message,
|
|
83
|
+
status_code=response.status_code,
|
|
84
|
+
request_id=request_id,
|
|
85
|
+
response_body=body,
|
|
86
|
+
)
|
|
87
|
+
elif response.status_code == 422:
|
|
88
|
+
raise ValidationError(
|
|
89
|
+
message,
|
|
90
|
+
status_code=response.status_code,
|
|
91
|
+
request_id=request_id,
|
|
92
|
+
response_body=body,
|
|
93
|
+
)
|
|
94
|
+
elif response.status_code == 429:
|
|
95
|
+
raise RateLimitError(
|
|
96
|
+
message,
|
|
97
|
+
status_code=response.status_code,
|
|
98
|
+
request_id=request_id,
|
|
99
|
+
response_body=body,
|
|
100
|
+
)
|
|
101
|
+
elif response.status_code >= 500:
|
|
102
|
+
raise ServerError(
|
|
103
|
+
message,
|
|
104
|
+
status_code=response.status_code,
|
|
105
|
+
request_id=request_id,
|
|
106
|
+
response_body=body,
|
|
107
|
+
)
|
|
108
|
+
else:
|
|
109
|
+
raise MenderError(
|
|
110
|
+
message,
|
|
111
|
+
status_code=response.status_code,
|
|
112
|
+
request_id=request_id,
|
|
113
|
+
response_body=body,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class HTTPClient:
|
|
118
|
+
"""
|
|
119
|
+
Synchronous HTTP client wrapper for Mender API.
|
|
120
|
+
|
|
121
|
+
Handles:
|
|
122
|
+
- Bearer token authentication
|
|
123
|
+
- Transparent pagination (fetches all pages, returns complete list)
|
|
124
|
+
- Error translation to MenderError hierarchy
|
|
125
|
+
"""
|
|
126
|
+
|
|
127
|
+
def __init__(
|
|
128
|
+
self,
|
|
129
|
+
base_url: str,
|
|
130
|
+
token: str,
|
|
131
|
+
*,
|
|
132
|
+
verify: bool = True,
|
|
133
|
+
timeout: float = 30.0,
|
|
134
|
+
) -> None:
|
|
135
|
+
self._base_url = base_url.rstrip("/")
|
|
136
|
+
self._token = token
|
|
137
|
+
self._client = httpx.Client(
|
|
138
|
+
base_url=self._base_url,
|
|
139
|
+
headers={
|
|
140
|
+
"Authorization": f"Bearer {token}",
|
|
141
|
+
"Accept": "application/json",
|
|
142
|
+
},
|
|
143
|
+
verify=verify,
|
|
144
|
+
timeout=timeout,
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
def close(self) -> None:
|
|
148
|
+
"""Close the underlying HTTP client."""
|
|
149
|
+
self._client.close()
|
|
150
|
+
|
|
151
|
+
def __enter__(self) -> HTTPClient:
|
|
152
|
+
return self
|
|
153
|
+
|
|
154
|
+
def __exit__(self, *args: Any) -> None:
|
|
155
|
+
self.close()
|
|
156
|
+
|
|
157
|
+
def request(
|
|
158
|
+
self,
|
|
159
|
+
method: str,
|
|
160
|
+
path: str,
|
|
161
|
+
*,
|
|
162
|
+
params: dict[str, Any] | None = None,
|
|
163
|
+
json: Any = None,
|
|
164
|
+
data: Any = None,
|
|
165
|
+
files: Any = None,
|
|
166
|
+
headers: dict[str, str] | None = None,
|
|
167
|
+
) -> httpx.Response:
|
|
168
|
+
"""Make a single HTTP request."""
|
|
169
|
+
response = self._client.request(
|
|
170
|
+
method,
|
|
171
|
+
path,
|
|
172
|
+
params=params,
|
|
173
|
+
json=json,
|
|
174
|
+
data=data,
|
|
175
|
+
files=files,
|
|
176
|
+
headers=headers,
|
|
177
|
+
)
|
|
178
|
+
_raise_for_status(response)
|
|
179
|
+
return response
|
|
180
|
+
|
|
181
|
+
def get(
|
|
182
|
+
self,
|
|
183
|
+
path: str,
|
|
184
|
+
*,
|
|
185
|
+
params: dict[str, Any] | None = None,
|
|
186
|
+
) -> httpx.Response:
|
|
187
|
+
"""Make a GET request."""
|
|
188
|
+
return self.request("GET", path, params=params)
|
|
189
|
+
|
|
190
|
+
def post(
|
|
191
|
+
self,
|
|
192
|
+
path: str,
|
|
193
|
+
*,
|
|
194
|
+
json: Any = None,
|
|
195
|
+
data: Any = None,
|
|
196
|
+
files: Any = None,
|
|
197
|
+
params: dict[str, Any] | None = None,
|
|
198
|
+
) -> httpx.Response:
|
|
199
|
+
"""Make a POST request."""
|
|
200
|
+
return self.request("POST", path, json=json, data=data, files=files, params=params)
|
|
201
|
+
|
|
202
|
+
def put(
|
|
203
|
+
self,
|
|
204
|
+
path: str,
|
|
205
|
+
*,
|
|
206
|
+
json: Any = None,
|
|
207
|
+
) -> httpx.Response:
|
|
208
|
+
"""Make a PUT request."""
|
|
209
|
+
return self.request("PUT", path, json=json)
|
|
210
|
+
|
|
211
|
+
def patch(
|
|
212
|
+
self,
|
|
213
|
+
path: str,
|
|
214
|
+
*,
|
|
215
|
+
json: Any = None,
|
|
216
|
+
) -> httpx.Response:
|
|
217
|
+
"""Make a PATCH request."""
|
|
218
|
+
return self.request("PATCH", path, json=json)
|
|
219
|
+
|
|
220
|
+
def delete(
|
|
221
|
+
self,
|
|
222
|
+
path: str,
|
|
223
|
+
*,
|
|
224
|
+
params: dict[str, Any] | None = None,
|
|
225
|
+
) -> httpx.Response:
|
|
226
|
+
"""Make a DELETE request."""
|
|
227
|
+
return self.request("DELETE", path, params=params)
|
|
228
|
+
|
|
229
|
+
def get_list(
|
|
230
|
+
self,
|
|
231
|
+
path: str,
|
|
232
|
+
*,
|
|
233
|
+
params: dict[str, Any] | None = None,
|
|
234
|
+
) -> list[Any]:
|
|
235
|
+
"""
|
|
236
|
+
Fetch a paginated list endpoint, returning all items.
|
|
237
|
+
|
|
238
|
+
Pagination is handled transparently - all pages are fetched
|
|
239
|
+
and merged into a single list.
|
|
240
|
+
"""
|
|
241
|
+
params = dict(params) if params else {}
|
|
242
|
+
params["per_page"] = _PER_PAGE
|
|
243
|
+
params["page"] = 1
|
|
244
|
+
|
|
245
|
+
all_items: list[Any] = []
|
|
246
|
+
|
|
247
|
+
while True:
|
|
248
|
+
response = self.get(path, params=params)
|
|
249
|
+
items = response.json()
|
|
250
|
+
|
|
251
|
+
if not isinstance(items, list):
|
|
252
|
+
# Some endpoints return a wrapper object
|
|
253
|
+
raise MenderError(f"Expected list response, got {type(items)}")
|
|
254
|
+
|
|
255
|
+
all_items.extend(cast(list[Any], items)) # type: ignore[redundant-cast]
|
|
256
|
+
|
|
257
|
+
# Check for next page via Link header
|
|
258
|
+
link_header = response.headers.get("Link", "")
|
|
259
|
+
links = _parse_link_header(link_header)
|
|
260
|
+
|
|
261
|
+
if "next" not in links:
|
|
262
|
+
break
|
|
263
|
+
|
|
264
|
+
params["page"] += 1
|
|
265
|
+
|
|
266
|
+
return all_items
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
class AsyncHTTPClient:
|
|
270
|
+
"""
|
|
271
|
+
Asynchronous HTTP client wrapper for Mender API.
|
|
272
|
+
|
|
273
|
+
Handles:
|
|
274
|
+
- Bearer token authentication
|
|
275
|
+
- Transparent pagination (fetches all pages, returns complete list)
|
|
276
|
+
- Error translation to MenderError hierarchy
|
|
277
|
+
"""
|
|
278
|
+
|
|
279
|
+
def __init__(
|
|
280
|
+
self,
|
|
281
|
+
base_url: str,
|
|
282
|
+
token: str,
|
|
283
|
+
*,
|
|
284
|
+
verify: bool = True,
|
|
285
|
+
timeout: float = 30.0,
|
|
286
|
+
) -> None:
|
|
287
|
+
self._base_url = base_url.rstrip("/")
|
|
288
|
+
self._token = token
|
|
289
|
+
self._client = httpx.AsyncClient(
|
|
290
|
+
base_url=self._base_url,
|
|
291
|
+
headers={
|
|
292
|
+
"Authorization": f"Bearer {token}",
|
|
293
|
+
"Accept": "application/json",
|
|
294
|
+
},
|
|
295
|
+
verify=verify,
|
|
296
|
+
timeout=timeout,
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
async def close(self) -> None:
|
|
300
|
+
"""Close the underlying HTTP client."""
|
|
301
|
+
await self._client.aclose()
|
|
302
|
+
|
|
303
|
+
async def __aenter__(self) -> AsyncHTTPClient:
|
|
304
|
+
return self
|
|
305
|
+
|
|
306
|
+
async def __aexit__(self, *args: Any) -> None:
|
|
307
|
+
await self.close()
|
|
308
|
+
|
|
309
|
+
async def request(
|
|
310
|
+
self,
|
|
311
|
+
method: str,
|
|
312
|
+
path: str,
|
|
313
|
+
*,
|
|
314
|
+
params: dict[str, Any] | None = None,
|
|
315
|
+
json: Any = None,
|
|
316
|
+
data: Any = None,
|
|
317
|
+
files: Any = None,
|
|
318
|
+
headers: dict[str, str] | None = None,
|
|
319
|
+
) -> httpx.Response:
|
|
320
|
+
"""Make a single HTTP request."""
|
|
321
|
+
response = await self._client.request(
|
|
322
|
+
method,
|
|
323
|
+
path,
|
|
324
|
+
params=params,
|
|
325
|
+
json=json,
|
|
326
|
+
data=data,
|
|
327
|
+
files=files,
|
|
328
|
+
headers=headers,
|
|
329
|
+
)
|
|
330
|
+
_raise_for_status(response)
|
|
331
|
+
return response
|
|
332
|
+
|
|
333
|
+
async def get(
|
|
334
|
+
self,
|
|
335
|
+
path: str,
|
|
336
|
+
*,
|
|
337
|
+
params: dict[str, Any] | None = None,
|
|
338
|
+
) -> httpx.Response:
|
|
339
|
+
"""Make a GET request."""
|
|
340
|
+
return await self.request("GET", path, params=params)
|
|
341
|
+
|
|
342
|
+
async def post(
|
|
343
|
+
self,
|
|
344
|
+
path: str,
|
|
345
|
+
*,
|
|
346
|
+
json: Any = None,
|
|
347
|
+
data: Any = None,
|
|
348
|
+
files: Any = None,
|
|
349
|
+
params: dict[str, Any] | None = None,
|
|
350
|
+
) -> httpx.Response:
|
|
351
|
+
"""Make a POST request."""
|
|
352
|
+
return await self.request("POST", path, json=json, data=data, files=files, params=params)
|
|
353
|
+
|
|
354
|
+
async def put(
|
|
355
|
+
self,
|
|
356
|
+
path: str,
|
|
357
|
+
*,
|
|
358
|
+
json: Any = None,
|
|
359
|
+
) -> httpx.Response:
|
|
360
|
+
"""Make a PUT request."""
|
|
361
|
+
return await self.request("PUT", path, json=json)
|
|
362
|
+
|
|
363
|
+
async def patch(
|
|
364
|
+
self,
|
|
365
|
+
path: str,
|
|
366
|
+
*,
|
|
367
|
+
json: Any = None,
|
|
368
|
+
) -> httpx.Response:
|
|
369
|
+
"""Make a PATCH request."""
|
|
370
|
+
return await self.request("PATCH", path, json=json)
|
|
371
|
+
|
|
372
|
+
async def delete(
|
|
373
|
+
self,
|
|
374
|
+
path: str,
|
|
375
|
+
*,
|
|
376
|
+
params: dict[str, Any] | None = None,
|
|
377
|
+
) -> httpx.Response:
|
|
378
|
+
"""Make a DELETE request."""
|
|
379
|
+
return await self.request("DELETE", path, params=params)
|
|
380
|
+
|
|
381
|
+
async def get_list(
|
|
382
|
+
self,
|
|
383
|
+
path: str,
|
|
384
|
+
*,
|
|
385
|
+
params: dict[str, Any] | None = None,
|
|
386
|
+
) -> list[Any]:
|
|
387
|
+
"""
|
|
388
|
+
Fetch a paginated list endpoint, returning all items.
|
|
389
|
+
|
|
390
|
+
Pagination is handled transparently - all pages are fetched
|
|
391
|
+
and merged into a single list.
|
|
392
|
+
"""
|
|
393
|
+
params = dict(params) if params else {}
|
|
394
|
+
params["per_page"] = _PER_PAGE
|
|
395
|
+
params["page"] = 1
|
|
396
|
+
|
|
397
|
+
all_items: list[Any] = []
|
|
398
|
+
|
|
399
|
+
while True:
|
|
400
|
+
response = await self.get(path, params=params)
|
|
401
|
+
items = response.json()
|
|
402
|
+
|
|
403
|
+
if not isinstance(items, list):
|
|
404
|
+
raise MenderError(f"Expected list response, got {type(items)}")
|
|
405
|
+
|
|
406
|
+
all_items.extend(cast(list[Any], items)) # type: ignore[redundant-cast]
|
|
407
|
+
|
|
408
|
+
# Check for next page via Link header
|
|
409
|
+
link_header = response.headers.get("Link", "")
|
|
410
|
+
links = _parse_link_header(link_header)
|
|
411
|
+
|
|
412
|
+
if "next" not in links:
|
|
413
|
+
break
|
|
414
|
+
|
|
415
|
+
params["page"] += 1
|
|
416
|
+
|
|
417
|
+
return all_items
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# SPDX-License-Identifier: MIT
|
|
2
|
+
|
|
3
|
+
"""API client modules for pymenderio."""
|
|
4
|
+
|
|
5
|
+
from pymenderio.api.artifacts import ArtifactsAPI, AsyncArtifactsAPI
|
|
6
|
+
from pymenderio.api.deployments import AsyncDeploymentsAPI, DeploymentsAPI
|
|
7
|
+
from pymenderio.api.deviceconfigure import AsyncDeviceconfigureAPI, DeviceconfigureAPI
|
|
8
|
+
from pymenderio.api.deviceconnect import AsyncDeviceconnectAPI, DeviceconnectAPI
|
|
9
|
+
from pymenderio.api.devicemonitor import AsyncDevicemonitorAPI, DevicemonitorAPI
|
|
10
|
+
from pymenderio.api.devices import AsyncDevicesAPI, DevicesAPI
|
|
11
|
+
from pymenderio.api.filters import AsyncFiltersAPI, FiltersAPI
|
|
12
|
+
from pymenderio.api.inventory import AsyncInventoryAPI, InventoryAPI
|
|
13
|
+
from pymenderio.api.iot_manager import AsyncIotManagerAPI, IotManagerAPI
|
|
14
|
+
from pymenderio.api.releases import AsyncReleasesAPI, ReleasesAPI
|
|
15
|
+
from pymenderio.api.tenantadm import AsyncTenantadmAPI, TenantadmAPI
|
|
16
|
+
from pymenderio.api.useradm import AsyncUseradmAPI, UseradmAPI
|
|
17
|
+
|
|
18
|
+
__all__ = [
|
|
19
|
+
"DevicesAPI",
|
|
20
|
+
"AsyncDevicesAPI",
|
|
21
|
+
"DeploymentsAPI",
|
|
22
|
+
"AsyncDeploymentsAPI",
|
|
23
|
+
"ArtifactsAPI",
|
|
24
|
+
"AsyncArtifactsAPI",
|
|
25
|
+
"DeviceconnectAPI",
|
|
26
|
+
"AsyncDeviceconnectAPI",
|
|
27
|
+
"DeviceconfigureAPI",
|
|
28
|
+
"AsyncDeviceconfigureAPI",
|
|
29
|
+
"DevicemonitorAPI",
|
|
30
|
+
"AsyncDevicemonitorAPI",
|
|
31
|
+
"IotManagerAPI",
|
|
32
|
+
"AsyncIotManagerAPI",
|
|
33
|
+
"ReleasesAPI",
|
|
34
|
+
"AsyncReleasesAPI",
|
|
35
|
+
"InventoryAPI",
|
|
36
|
+
"AsyncInventoryAPI",
|
|
37
|
+
"FiltersAPI",
|
|
38
|
+
"AsyncFiltersAPI",
|
|
39
|
+
"TenantadmAPI",
|
|
40
|
+
"AsyncTenantadmAPI",
|
|
41
|
+
"UseradmAPI",
|
|
42
|
+
"AsyncUseradmAPI",
|
|
43
|
+
]
|