snipepy 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.
- snipepy/__init__.py +73 -0
- snipepy/_config.py +45 -0
- snipepy/_constants.py +15 -0
- snipepy/_http_client.py +382 -0
- snipepy/_whitelist.py +70 -0
- snipepy/models/__init__.py +111 -0
- snipepy/models/_shared.py +125 -0
- snipepy/models/accessories.py +59 -0
- snipepy/models/asset_maintenances.py +55 -0
- snipepy/models/asset_models.py +47 -0
- snipepy/models/assets.py +170 -0
- snipepy/models/categories.py +39 -0
- snipepy/models/companies.py +35 -0
- snipepy/models/components.py +79 -0
- snipepy/models/consumables.py +43 -0
- snipepy/models/custom_fields.py +67 -0
- snipepy/models/departments.py +33 -0
- snipepy/models/groups.py +30 -0
- snipepy/models/licenses.py +99 -0
- snipepy/models/locations.py +41 -0
- snipepy/models/manufacturers.py +39 -0
- snipepy/models/reports.py +68 -0
- snipepy/models/settings.py +25 -0
- snipepy/models/status_labels.py +34 -0
- snipepy/models/suppliers.py +45 -0
- snipepy/models/users.py +60 -0
- snipepy/py.typed +0 -0
- snipepy/services/__init__.py +43 -0
- snipepy/services/_pagination.py +28 -0
- snipepy/services/accessories.py +126 -0
- snipepy/services/asset_maintenances.py +109 -0
- snipepy/services/asset_models.py +97 -0
- snipepy/services/assets.py +517 -0
- snipepy/services/categories.py +80 -0
- snipepy/services/companies.py +68 -0
- snipepy/services/components.py +123 -0
- snipepy/services/consumables.py +106 -0
- snipepy/services/custom_fields.py +118 -0
- snipepy/services/departments.py +91 -0
- snipepy/services/groups.py +66 -0
- snipepy/services/licenses.py +146 -0
- snipepy/services/locations.py +113 -0
- snipepy/services/manufacturers.py +78 -0
- snipepy/services/reports.py +64 -0
- snipepy/services/settings.py +53 -0
- snipepy/services/status_labels.py +110 -0
- snipepy/services/suppliers.py +93 -0
- snipepy/services/users.py +180 -0
- snipepy/snipepy.py +144 -0
- snipepy-0.1.0.dist-info/METADATA +138 -0
- snipepy-0.1.0.dist-info/RECORD +54 -0
- snipepy-0.1.0.dist-info/WHEEL +5 -0
- snipepy-0.1.0.dist-info/licenses/LICENSE +73 -0
- snipepy-0.1.0.dist-info/top_level.txt +1 -0
snipepy/__init__.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"""Snipepy - Python wrapper for the SnipeIT REST API."""
|
|
2
|
+
|
|
3
|
+
__version__ = "0.1.0"
|
|
4
|
+
|
|
5
|
+
from snipepy._config import Config
|
|
6
|
+
from snipepy._http_client import (
|
|
7
|
+
HttpClient,
|
|
8
|
+
HttpClientProtocol,
|
|
9
|
+
SnipepyAuthError,
|
|
10
|
+
SnipepyConflictError,
|
|
11
|
+
SnipepyError,
|
|
12
|
+
SnipepyForbiddenError,
|
|
13
|
+
SnipepyNotFoundError,
|
|
14
|
+
SnipepyRateLimitError,
|
|
15
|
+
SnipepyServerError,
|
|
16
|
+
SnipepyValidationError,
|
|
17
|
+
)
|
|
18
|
+
from snipepy.snipepy import (
|
|
19
|
+
AccessoriesService,
|
|
20
|
+
AssetMaintenancesService,
|
|
21
|
+
AssetModelsService,
|
|
22
|
+
AssetsService,
|
|
23
|
+
CategoriesService,
|
|
24
|
+
CompaniesService,
|
|
25
|
+
ComponentsService,
|
|
26
|
+
ConsumablesService,
|
|
27
|
+
CustomFieldsService,
|
|
28
|
+
DepartmentsService,
|
|
29
|
+
LicensesService,
|
|
30
|
+
LocationsService,
|
|
31
|
+
ManufacturersService,
|
|
32
|
+
ReportsService,
|
|
33
|
+
SettingsService,
|
|
34
|
+
Snipepy,
|
|
35
|
+
StatusLabelsService,
|
|
36
|
+
SuppliersService,
|
|
37
|
+
UserGroupsService,
|
|
38
|
+
UsersService,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
__all__ = [
|
|
42
|
+
"AccessoriesService",
|
|
43
|
+
"AssetMaintenancesService",
|
|
44
|
+
"AssetModelsService",
|
|
45
|
+
"AssetsService",
|
|
46
|
+
"CategoriesService",
|
|
47
|
+
"CompaniesService",
|
|
48
|
+
"ComponentsService",
|
|
49
|
+
"Config",
|
|
50
|
+
"ConsumablesService",
|
|
51
|
+
"CustomFieldsService",
|
|
52
|
+
"DepartmentsService",
|
|
53
|
+
"HttpClient",
|
|
54
|
+
"HttpClientProtocol",
|
|
55
|
+
"LicensesService",
|
|
56
|
+
"LocationsService",
|
|
57
|
+
"ManufacturersService",
|
|
58
|
+
"ReportsService",
|
|
59
|
+
"SettingsService",
|
|
60
|
+
"Snipepy",
|
|
61
|
+
"SnipepyAuthError",
|
|
62
|
+
"SnipepyConflictError",
|
|
63
|
+
"SnipepyError",
|
|
64
|
+
"SnipepyForbiddenError",
|
|
65
|
+
"SnipepyNotFoundError",
|
|
66
|
+
"SnipepyRateLimitError",
|
|
67
|
+
"SnipepyServerError",
|
|
68
|
+
"SnipepyValidationError",
|
|
69
|
+
"StatusLabelsService",
|
|
70
|
+
"SuppliersService",
|
|
71
|
+
"UserGroupsService",
|
|
72
|
+
"UsersService",
|
|
73
|
+
]
|
snipepy/_config.py
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
|
|
6
|
+
from dotenv import load_dotenv
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
@dataclass(frozen=True)
|
|
10
|
+
class Config:
|
|
11
|
+
url: str
|
|
12
|
+
token: str
|
|
13
|
+
timeout: int = 30
|
|
14
|
+
verify_ssl: bool = False
|
|
15
|
+
|
|
16
|
+
@classmethod
|
|
17
|
+
def from_env(cls, dotenv_path: str | None = None) -> Config:
|
|
18
|
+
load_dotenv(dotenv_path, override=True)
|
|
19
|
+
|
|
20
|
+
url: str = os.environ.get("SNIPEPY_URL", "").rstrip("/")
|
|
21
|
+
token: str = os.environ.get("SNIPEPY_API_TOKEN", "")
|
|
22
|
+
|
|
23
|
+
# Collect names of missing required environment variables
|
|
24
|
+
missing: list[str] = [
|
|
25
|
+
name
|
|
26
|
+
for name, val in (
|
|
27
|
+
("SNIPEPY_URL", url),
|
|
28
|
+
("SNIPEPY_API_TOKEN", token),
|
|
29
|
+
)
|
|
30
|
+
if not val
|
|
31
|
+
]
|
|
32
|
+
# Raise ValueError if any required environment variables are missing
|
|
33
|
+
if missing:
|
|
34
|
+
raise ValueError(
|
|
35
|
+
f"Missing required environment variables: {', '.join(missing)}"
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
timeout: int = int(os.environ.get("SNIPEPY_TIMEOUT", "30"))
|
|
39
|
+
verify_ssl: bool = os.environ.get(
|
|
40
|
+
"SNIPEPY_VERIFY_SSL", "false"
|
|
41
|
+
).lower() in ("true", "1", "yes")
|
|
42
|
+
|
|
43
|
+
return cls(
|
|
44
|
+
url=url, token=token, timeout=timeout, verify_ssl=verify_ssl
|
|
45
|
+
)
|
snipepy/_constants.py
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""HTTP status code constants for SnipeIT API error handling."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
# HTTP 4xx Client Error codes
|
|
6
|
+
HTTP_400_BAD_REQUEST = 400
|
|
7
|
+
HTTP_401_UNAUTHORIZED = 401
|
|
8
|
+
HTTP_403_FORBIDDEN = 403
|
|
9
|
+
HTTP_404_NOT_FOUND = 404
|
|
10
|
+
HTTP_409_CONFLICT = 409
|
|
11
|
+
HTTP_429_TOO_MANY_REQUESTS = 429
|
|
12
|
+
|
|
13
|
+
# HTTP 5xx - 6xx Server Error codes
|
|
14
|
+
HTTP_5XX_MIN = 500
|
|
15
|
+
HTTP_5XX_MAX = 600
|
snipepy/_http_client.py
ADDED
|
@@ -0,0 +1,382 @@
|
|
|
1
|
+
"""HTTP client for SnipeIT API calls."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import html
|
|
6
|
+
import time
|
|
7
|
+
from collections.abc import Callable
|
|
8
|
+
from typing import Any, Protocol
|
|
9
|
+
|
|
10
|
+
import requests
|
|
11
|
+
from requests.models import Response
|
|
12
|
+
|
|
13
|
+
from snipepy import Config
|
|
14
|
+
from snipepy._constants import (
|
|
15
|
+
HTTP_5XX_MAX,
|
|
16
|
+
HTTP_5XX_MIN,
|
|
17
|
+
HTTP_401_UNAUTHORIZED,
|
|
18
|
+
HTTP_403_FORBIDDEN,
|
|
19
|
+
HTTP_404_NOT_FOUND,
|
|
20
|
+
HTTP_409_CONFLICT,
|
|
21
|
+
HTTP_429_TOO_MANY_REQUESTS,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
_DEFAULT_MAX_RETRIES: int = 3
|
|
25
|
+
_DEFAULT_RETRY_AFTER: int = 10
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class SnipepyError(Exception):
|
|
29
|
+
"""Base exception for all Snipepy errors."""
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class SnipepyAuthError(SnipepyError):
|
|
33
|
+
"""Raised when authentication fails (HTTP 401)."""
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
class SnipepyForbiddenError(SnipepyError):
|
|
37
|
+
"""Raised when access is forbidden (HTTP 403)."""
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class SnipepyNotFoundError(SnipepyError):
|
|
41
|
+
"""Raised when a resource is not found (HTTP 404)."""
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
class SnipepyConflictError(SnipepyError):
|
|
45
|
+
"""Raised when there is a conflict (HTTP 409)."""
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
class SnipepyValidationError(SnipepyError):
|
|
49
|
+
"""Raised when SnipeIT returns status: error (validation failure)."""
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class SnipepyServerError(SnipepyError):
|
|
53
|
+
"""Raised when server returns 5xx error."""
|
|
54
|
+
|
|
55
|
+
response: Response | None
|
|
56
|
+
|
|
57
|
+
def __init__(self, message: str, response: Response | None = None) -> None:
|
|
58
|
+
super().__init__(message)
|
|
59
|
+
self.response = response
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
class SnipepyRateLimitError(SnipepyError):
|
|
63
|
+
"""Raised when the API rate limit is exceeded and retries exhausted."""
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
# OCP: map status codes to exceptions; extend by adding entries here
|
|
67
|
+
_STATUS_EXCEPTIONS: dict[int, tuple[type[SnipepyError], str]] = {
|
|
68
|
+
HTTP_401_UNAUTHORIZED: (SnipepyAuthError, "Unauthorized"),
|
|
69
|
+
HTTP_403_FORBIDDEN: (SnipepyForbiddenError, "Forbidden"),
|
|
70
|
+
HTTP_404_NOT_FOUND: (SnipepyNotFoundError, "Not found"),
|
|
71
|
+
HTTP_409_CONFLICT: (SnipepyConflictError, "Conflict"),
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def _is_listener_only_failure(response: Response) -> bool:
|
|
76
|
+
"""Check if the 500 error is a listener-only failure.
|
|
77
|
+
|
|
78
|
+
SnipeIT's post-action event listeners (e.g., mail notifications) can
|
|
79
|
+
fail with a 500 error even when the main operation succeeded. The
|
|
80
|
+
stack trace in the error page contains ``Listeners/`` which indicates
|
|
81
|
+
the failure is in a post-action event listener.
|
|
82
|
+
"""
|
|
83
|
+
return "listeners/" in response.text.lower()
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
class HttpClientProtocol(Protocol):
|
|
87
|
+
"""Structural interface for SnipeIT HTTP communication."""
|
|
88
|
+
|
|
89
|
+
def get(self, path: str, params: dict[str, Any] | None = ...) -> Any: ...
|
|
90
|
+
|
|
91
|
+
def post(self, path: str, json: dict[str, Any] | None = ...) -> Any: ...
|
|
92
|
+
|
|
93
|
+
def put(self, path: str, json: dict[str, Any]) -> Any: ...
|
|
94
|
+
|
|
95
|
+
def patch(self, path: str, json: dict[str, Any]) -> Any: ...
|
|
96
|
+
|
|
97
|
+
def delete(self, path: str) -> Any: ...
|
|
98
|
+
|
|
99
|
+
def upload(self, path: str, files: dict[str, Any]) -> Any: ...
|
|
100
|
+
|
|
101
|
+
def post_binary(
|
|
102
|
+
self,
|
|
103
|
+
path: str,
|
|
104
|
+
json: dict[str, Any] | None = ...,
|
|
105
|
+
accept: str = ...,
|
|
106
|
+
) -> bytes: ...
|
|
107
|
+
|
|
108
|
+
def get_binary(
|
|
109
|
+
self,
|
|
110
|
+
path: str,
|
|
111
|
+
params: dict[str, Any] | None = ...,
|
|
112
|
+
accept: str = ...,
|
|
113
|
+
) -> bytes: ...
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class HttpClient:
|
|
117
|
+
"""HTTP client for SnipeIT API calls."""
|
|
118
|
+
|
|
119
|
+
_config: Config
|
|
120
|
+
_session: requests.Session
|
|
121
|
+
_max_retries: int
|
|
122
|
+
_retry_after: int
|
|
123
|
+
|
|
124
|
+
def __init__(
|
|
125
|
+
self,
|
|
126
|
+
config: Config,
|
|
127
|
+
max_retries: int = _DEFAULT_MAX_RETRIES,
|
|
128
|
+
retry_after: int = _DEFAULT_RETRY_AFTER,
|
|
129
|
+
) -> None:
|
|
130
|
+
self._config = config
|
|
131
|
+
self._max_retries = max_retries
|
|
132
|
+
self._retry_after = retry_after
|
|
133
|
+
self._session = requests.Session()
|
|
134
|
+
self._session.headers.update(
|
|
135
|
+
{
|
|
136
|
+
"Authorization": f"Bearer {config.token}",
|
|
137
|
+
"Accept": "application/json",
|
|
138
|
+
}
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
def _execute_request(
|
|
142
|
+
self, make_request: Callable[[], Response]
|
|
143
|
+
) -> Response:
|
|
144
|
+
"""Execute a request callable, retrying on HTTP 429 with Retry-After.
|
|
145
|
+
|
|
146
|
+
Retries up to ``_max_retries`` times. On each 429 response the
|
|
147
|
+
``Retry-After`` header value (seconds) is honoured; when the header is
|
|
148
|
+
absent the configured ``_retry_after`` default is used. After all
|
|
149
|
+
retries are exhausted ``SnipepyRateLimitError`` is raised.
|
|
150
|
+
"""
|
|
151
|
+
for attempt in range(self._max_retries + 1):
|
|
152
|
+
response = make_request()
|
|
153
|
+
if response.status_code != HTTP_429_TOO_MANY_REQUESTS:
|
|
154
|
+
return response
|
|
155
|
+
if attempt < self._max_retries:
|
|
156
|
+
raw_wait = response.headers.get(
|
|
157
|
+
"Retry-After", str(self._retry_after)
|
|
158
|
+
)
|
|
159
|
+
try:
|
|
160
|
+
wait = int(raw_wait)
|
|
161
|
+
except ValueError:
|
|
162
|
+
wait = self._retry_after
|
|
163
|
+
time.sleep(wait)
|
|
164
|
+
retries = self._max_retries
|
|
165
|
+
word = "retry" if retries == 1 else "retries"
|
|
166
|
+
raise SnipepyRateLimitError(
|
|
167
|
+
f"Rate limit exceeded after {retries} {word}"
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
def _raise_for_status(
|
|
171
|
+
self, response: Response, exc: requests.HTTPError
|
|
172
|
+
) -> None:
|
|
173
|
+
"""Translate HTTP error codes into Snipepy exceptions."""
|
|
174
|
+
entry = _STATUS_EXCEPTIONS.get(response.status_code)
|
|
175
|
+
if entry is not None:
|
|
176
|
+
exc_class, message = entry
|
|
177
|
+
raise exc_class(message) from exc
|
|
178
|
+
if HTTP_5XX_MIN <= response.status_code < HTTP_5XX_MAX:
|
|
179
|
+
raise SnipepyServerError(
|
|
180
|
+
"Server error", response=response
|
|
181
|
+
) from exc
|
|
182
|
+
raise exc
|
|
183
|
+
|
|
184
|
+
def _unescape_strings(self, data: Any) -> Any:
|
|
185
|
+
"""Recursively unescape HTML entities in all strings."""
|
|
186
|
+
if isinstance(data, str):
|
|
187
|
+
return html.unescape(data)
|
|
188
|
+
if isinstance(data, dict):
|
|
189
|
+
return {k: self._unescape_strings(v) for k, v in data.items()}
|
|
190
|
+
if isinstance(data, list):
|
|
191
|
+
return [self._unescape_strings(item) for item in data]
|
|
192
|
+
return data
|
|
193
|
+
|
|
194
|
+
def _parse_payload(self, response: Response) -> Any:
|
|
195
|
+
"""Parse JSON; raise SnipepyValidationError on status:error."""
|
|
196
|
+
payload = response.json()
|
|
197
|
+
if not isinstance(payload, dict):
|
|
198
|
+
return payload
|
|
199
|
+
if payload.get("status") == "error":
|
|
200
|
+
messages = payload.get("messages", {})
|
|
201
|
+
raise SnipepyValidationError(str(messages))
|
|
202
|
+
result = payload.get("payload", payload)
|
|
203
|
+
return self._unescape_strings(result)
|
|
204
|
+
|
|
205
|
+
def _build_url(self, path: str, params: dict[str, Any] | None) -> str:
|
|
206
|
+
"""Build a URL with optional query string; None params excluded."""
|
|
207
|
+
path_name: str = path.lstrip("/")
|
|
208
|
+
base_url: str = f"{self._config.url.rstrip('/')}/api/v1/{path_name}"
|
|
209
|
+
|
|
210
|
+
if not params:
|
|
211
|
+
return base_url
|
|
212
|
+
|
|
213
|
+
filtered_params = {k: v for k, v in params.items() if v is not None}
|
|
214
|
+
|
|
215
|
+
if not filtered_params:
|
|
216
|
+
return base_url
|
|
217
|
+
|
|
218
|
+
query_string = "&".join(f"{k}={v}" for k, v in filtered_params.items())
|
|
219
|
+
return f"{base_url}?{query_string}"
|
|
220
|
+
|
|
221
|
+
def _request(self, method: str, path: str) -> Any:
|
|
222
|
+
"""Make a raw HTTP request (no params, no body)."""
|
|
223
|
+
url: str = self._build_url(path, None)
|
|
224
|
+
response = self._execute_request(
|
|
225
|
+
lambda: self._session.request(
|
|
226
|
+
method=method,
|
|
227
|
+
url=url,
|
|
228
|
+
timeout=self._config.timeout,
|
|
229
|
+
verify=self._config.verify_ssl,
|
|
230
|
+
)
|
|
231
|
+
)
|
|
232
|
+
try:
|
|
233
|
+
response.raise_for_status()
|
|
234
|
+
except requests.HTTPError as e:
|
|
235
|
+
self._raise_for_status(response, e)
|
|
236
|
+
return self._parse_payload(response)
|
|
237
|
+
|
|
238
|
+
def get(self, path: str, params: dict[str, Any] | None = None) -> Any:
|
|
239
|
+
"""Make a GET request."""
|
|
240
|
+
url: str = self._build_url(path, params)
|
|
241
|
+
response = self._execute_request(
|
|
242
|
+
lambda: self._session.get(
|
|
243
|
+
url=url,
|
|
244
|
+
timeout=self._config.timeout,
|
|
245
|
+
verify=self._config.verify_ssl,
|
|
246
|
+
)
|
|
247
|
+
)
|
|
248
|
+
try:
|
|
249
|
+
response.raise_for_status()
|
|
250
|
+
except requests.HTTPError as e:
|
|
251
|
+
self._raise_for_status(response, e)
|
|
252
|
+
return self._parse_payload(response)
|
|
253
|
+
|
|
254
|
+
def post(self, path: str, json: dict[str, Any] | None = None) -> Any:
|
|
255
|
+
"""Make a POST request."""
|
|
256
|
+
url: str = self._build_url(path, None)
|
|
257
|
+
response = self._execute_request(
|
|
258
|
+
lambda: self._session.post(
|
|
259
|
+
url=url,
|
|
260
|
+
json=json,
|
|
261
|
+
timeout=self._config.timeout,
|
|
262
|
+
verify=self._config.verify_ssl,
|
|
263
|
+
)
|
|
264
|
+
)
|
|
265
|
+
try:
|
|
266
|
+
response.raise_for_status()
|
|
267
|
+
except requests.HTTPError as e:
|
|
268
|
+
self._raise_for_status(response, e)
|
|
269
|
+
return self._parse_payload(response)
|
|
270
|
+
|
|
271
|
+
def put(self, path: str, json: dict[str, Any]) -> Any:
|
|
272
|
+
"""Make a PUT request."""
|
|
273
|
+
url: str = self._build_url(path, None)
|
|
274
|
+
response = self._execute_request(
|
|
275
|
+
lambda: self._session.put(
|
|
276
|
+
url=url,
|
|
277
|
+
json=json,
|
|
278
|
+
timeout=self._config.timeout,
|
|
279
|
+
verify=self._config.verify_ssl,
|
|
280
|
+
)
|
|
281
|
+
)
|
|
282
|
+
try:
|
|
283
|
+
response.raise_for_status()
|
|
284
|
+
except requests.HTTPError as e:
|
|
285
|
+
self._raise_for_status(response, e)
|
|
286
|
+
return self._parse_payload(response)
|
|
287
|
+
|
|
288
|
+
def patch(self, path: str, json: dict[str, Any]) -> Any:
|
|
289
|
+
"""Make a PATCH request."""
|
|
290
|
+
url: str = self._build_url(path, None)
|
|
291
|
+
response = self._execute_request(
|
|
292
|
+
lambda: self._session.patch(
|
|
293
|
+
url=url,
|
|
294
|
+
json=json,
|
|
295
|
+
timeout=self._config.timeout,
|
|
296
|
+
verify=self._config.verify_ssl,
|
|
297
|
+
)
|
|
298
|
+
)
|
|
299
|
+
try:
|
|
300
|
+
response.raise_for_status()
|
|
301
|
+
except requests.HTTPError as e:
|
|
302
|
+
self._raise_for_status(response, e)
|
|
303
|
+
return self._parse_payload(response)
|
|
304
|
+
|
|
305
|
+
def delete(self, path: str) -> Any:
|
|
306
|
+
"""Make a DELETE request."""
|
|
307
|
+
url: str = self._build_url(path, None)
|
|
308
|
+
response = self._execute_request(
|
|
309
|
+
lambda: self._session.delete(
|
|
310
|
+
url=url,
|
|
311
|
+
timeout=self._config.timeout,
|
|
312
|
+
verify=self._config.verify_ssl,
|
|
313
|
+
)
|
|
314
|
+
)
|
|
315
|
+
try:
|
|
316
|
+
response.raise_for_status()
|
|
317
|
+
except requests.HTTPError as e:
|
|
318
|
+
self._raise_for_status(response, e)
|
|
319
|
+
return self._parse_payload(response)
|
|
320
|
+
|
|
321
|
+
def upload(self, path: str, files: dict[str, Any]) -> Any:
|
|
322
|
+
"""POST a multipart file upload to the SnipeIT API."""
|
|
323
|
+
url: str = self._build_url(path, None)
|
|
324
|
+
response = self._execute_request(
|
|
325
|
+
lambda: self._session.request(
|
|
326
|
+
method="POST",
|
|
327
|
+
url=url,
|
|
328
|
+
files=files,
|
|
329
|
+
timeout=self._config.timeout,
|
|
330
|
+
verify=self._config.verify_ssl,
|
|
331
|
+
)
|
|
332
|
+
)
|
|
333
|
+
try:
|
|
334
|
+
response.raise_for_status()
|
|
335
|
+
except requests.HTTPError as e:
|
|
336
|
+
self._raise_for_status(response, e)
|
|
337
|
+
return self._parse_payload(response)
|
|
338
|
+
|
|
339
|
+
def post_binary(
|
|
340
|
+
self,
|
|
341
|
+
path: str,
|
|
342
|
+
json: dict[str, Any] | None = None,
|
|
343
|
+
accept: str = "application/octet-stream",
|
|
344
|
+
) -> bytes:
|
|
345
|
+
"""POST with JSON body and return binary response content."""
|
|
346
|
+
url: str = self._build_url(path, None)
|
|
347
|
+
response = self._execute_request(
|
|
348
|
+
lambda: self._session.post(
|
|
349
|
+
url=url,
|
|
350
|
+
json=json,
|
|
351
|
+
timeout=self._config.timeout,
|
|
352
|
+
verify=self._config.verify_ssl,
|
|
353
|
+
headers={"Accept": accept},
|
|
354
|
+
)
|
|
355
|
+
)
|
|
356
|
+
try:
|
|
357
|
+
response.raise_for_status()
|
|
358
|
+
except requests.HTTPError as e:
|
|
359
|
+
self._raise_for_status(response, e)
|
|
360
|
+
return bytes(response.content)
|
|
361
|
+
|
|
362
|
+
def get_binary(
|
|
363
|
+
self,
|
|
364
|
+
path: str,
|
|
365
|
+
params: dict[str, Any] | None = None,
|
|
366
|
+
accept: str = "application/octet-stream",
|
|
367
|
+
) -> bytes:
|
|
368
|
+
"""GET a binary response (PDF, PNG, ZIP) from the SnipeIT API."""
|
|
369
|
+
url: str = self._build_url(path, params)
|
|
370
|
+
response = self._execute_request(
|
|
371
|
+
lambda: self._session.get(
|
|
372
|
+
url=url,
|
|
373
|
+
timeout=self._config.timeout,
|
|
374
|
+
verify=self._config.verify_ssl,
|
|
375
|
+
headers={"Accept": accept},
|
|
376
|
+
)
|
|
377
|
+
)
|
|
378
|
+
try:
|
|
379
|
+
response.raise_for_status()
|
|
380
|
+
except requests.HTTPError as e:
|
|
381
|
+
self._raise_for_status(response, e)
|
|
382
|
+
return bytes(response.content)
|
snipepy/_whitelist.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Vulture whitelist - public API surface used by callers outside snipepy/
|
|
2
|
+
from snipepy import Config, Snipepy
|
|
3
|
+
from snipepy._http_client import (
|
|
4
|
+
HttpClientProtocol,
|
|
5
|
+
SnipepyAuthError,
|
|
6
|
+
SnipepyConflictError,
|
|
7
|
+
SnipepyError,
|
|
8
|
+
SnipepyForbiddenError,
|
|
9
|
+
SnipepyNotFoundError,
|
|
10
|
+
SnipepyRateLimitError,
|
|
11
|
+
SnipepyServerError,
|
|
12
|
+
SnipepyValidationError,
|
|
13
|
+
)
|
|
14
|
+
from snipepy.models.components import ComponentAsset, ComponentAssetList
|
|
15
|
+
from snipepy.snipepy import (
|
|
16
|
+
AccessoriesService,
|
|
17
|
+
AssetMaintenancesService,
|
|
18
|
+
AssetModelsService,
|
|
19
|
+
AssetsService,
|
|
20
|
+
CategoriesService,
|
|
21
|
+
CompaniesService,
|
|
22
|
+
ComponentsService,
|
|
23
|
+
ConsumablesService,
|
|
24
|
+
CustomFieldsService,
|
|
25
|
+
DepartmentsService,
|
|
26
|
+
LicensesService,
|
|
27
|
+
LocationsService,
|
|
28
|
+
ManufacturersService,
|
|
29
|
+
ReportsService,
|
|
30
|
+
SettingsService,
|
|
31
|
+
StatusLabelsService,
|
|
32
|
+
SuppliersService,
|
|
33
|
+
UserGroupsService,
|
|
34
|
+
UsersService,
|
|
35
|
+
)
|
|
36
|
+
|
|
37
|
+
_ = (
|
|
38
|
+
ComponentAsset,
|
|
39
|
+
ComponentAssetList,
|
|
40
|
+
Config.from_env,
|
|
41
|
+
Snipepy.from_env,
|
|
42
|
+
HttpClientProtocol,
|
|
43
|
+
SnipepyError,
|
|
44
|
+
SnipepyAuthError,
|
|
45
|
+
SnipepyForbiddenError,
|
|
46
|
+
SnipepyNotFoundError,
|
|
47
|
+
SnipepyConflictError,
|
|
48
|
+
SnipepyValidationError,
|
|
49
|
+
SnipepyServerError,
|
|
50
|
+
SnipepyRateLimitError,
|
|
51
|
+
AssetsService,
|
|
52
|
+
UsersService,
|
|
53
|
+
LicensesService,
|
|
54
|
+
AccessoriesService,
|
|
55
|
+
ComponentsService,
|
|
56
|
+
ConsumablesService,
|
|
57
|
+
LocationsService,
|
|
58
|
+
CompaniesService,
|
|
59
|
+
CategoriesService,
|
|
60
|
+
ManufacturersService,
|
|
61
|
+
AssetModelsService,
|
|
62
|
+
StatusLabelsService,
|
|
63
|
+
DepartmentsService,
|
|
64
|
+
UserGroupsService,
|
|
65
|
+
SuppliersService,
|
|
66
|
+
AssetMaintenancesService,
|
|
67
|
+
SettingsService,
|
|
68
|
+
ReportsService,
|
|
69
|
+
CustomFieldsService,
|
|
70
|
+
)
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
"""Pydantic models for SnipeIT resources."""
|
|
2
|
+
|
|
3
|
+
from snipepy.models._shared import (
|
|
4
|
+
AssignedTo,
|
|
5
|
+
AvailableActions,
|
|
6
|
+
DateField,
|
|
7
|
+
DatetimeField,
|
|
8
|
+
NamedObject,
|
|
9
|
+
StatusLabelRef,
|
|
10
|
+
)
|
|
11
|
+
from snipepy.models.accessories import (
|
|
12
|
+
Accessory,
|
|
13
|
+
AccessoryCheckedOut,
|
|
14
|
+
AccessoryCheckedOutList,
|
|
15
|
+
AccessoryList,
|
|
16
|
+
)
|
|
17
|
+
from snipepy.models.asset_maintenances import (
|
|
18
|
+
AssetMaintenance,
|
|
19
|
+
AssetMaintenanceList,
|
|
20
|
+
AssetRef,
|
|
21
|
+
)
|
|
22
|
+
from snipepy.models.asset_models import AssetModel, AssetModelList
|
|
23
|
+
from snipepy.models.assets import Asset, AssetFile, AssetFileList, AssetList
|
|
24
|
+
from snipepy.models.categories import Category, CategoryList
|
|
25
|
+
from snipepy.models.companies import Company, CompanyList
|
|
26
|
+
from snipepy.models.components import Component, ComponentList
|
|
27
|
+
from snipepy.models.consumables import Consumable, ConsumableList
|
|
28
|
+
from snipepy.models.custom_fields import (
|
|
29
|
+
CustomField,
|
|
30
|
+
CustomFieldList,
|
|
31
|
+
Fieldset,
|
|
32
|
+
FieldsetList,
|
|
33
|
+
)
|
|
34
|
+
from snipepy.models.departments import Department, DepartmentList
|
|
35
|
+
from snipepy.models.groups import Group, GroupList
|
|
36
|
+
from snipepy.models.licenses import (
|
|
37
|
+
License,
|
|
38
|
+
LicenseList,
|
|
39
|
+
LicenseSeat,
|
|
40
|
+
LicenseSeatList,
|
|
41
|
+
)
|
|
42
|
+
from snipepy.models.locations import Location, LocationList
|
|
43
|
+
from snipepy.models.manufacturers import Manufacturer, ManufacturerList
|
|
44
|
+
from snipepy.models.reports import (
|
|
45
|
+
ActivityLog,
|
|
46
|
+
ActivityLogItem,
|
|
47
|
+
DepreciationAsset,
|
|
48
|
+
DepreciationList,
|
|
49
|
+
)
|
|
50
|
+
from snipepy.models.settings import Backup, BackupList
|
|
51
|
+
from snipepy.models.status_labels import StatusLabel, StatusLabelList
|
|
52
|
+
from snipepy.models.suppliers import Supplier, SupplierList
|
|
53
|
+
from snipepy.models.users import User, UserList
|
|
54
|
+
|
|
55
|
+
__all__ = [
|
|
56
|
+
"Accessory",
|
|
57
|
+
"AccessoryCheckedOut",
|
|
58
|
+
"AccessoryCheckedOutList",
|
|
59
|
+
"AccessoryList",
|
|
60
|
+
"ActivityLog",
|
|
61
|
+
"ActivityLogItem",
|
|
62
|
+
"Asset",
|
|
63
|
+
"AssetFile",
|
|
64
|
+
"AssetFileList",
|
|
65
|
+
"AssetList",
|
|
66
|
+
"AssetMaintenance",
|
|
67
|
+
"AssetMaintenanceList",
|
|
68
|
+
"AssetModel",
|
|
69
|
+
"AssetModelList",
|
|
70
|
+
"AssetRef",
|
|
71
|
+
"AssignedTo",
|
|
72
|
+
"AvailableActions",
|
|
73
|
+
"Backup",
|
|
74
|
+
"BackupList",
|
|
75
|
+
"Category",
|
|
76
|
+
"CategoryList",
|
|
77
|
+
"Company",
|
|
78
|
+
"CompanyList",
|
|
79
|
+
"Component",
|
|
80
|
+
"ComponentList",
|
|
81
|
+
"Consumable",
|
|
82
|
+
"ConsumableList",
|
|
83
|
+
"CustomField",
|
|
84
|
+
"CustomFieldList",
|
|
85
|
+
"DateField",
|
|
86
|
+
"DatetimeField",
|
|
87
|
+
"Department",
|
|
88
|
+
"DepartmentList",
|
|
89
|
+
"DepreciationAsset",
|
|
90
|
+
"DepreciationList",
|
|
91
|
+
"Fieldset",
|
|
92
|
+
"FieldsetList",
|
|
93
|
+
"Group",
|
|
94
|
+
"GroupList",
|
|
95
|
+
"License",
|
|
96
|
+
"LicenseList",
|
|
97
|
+
"LicenseSeat",
|
|
98
|
+
"LicenseSeatList",
|
|
99
|
+
"Location",
|
|
100
|
+
"LocationList",
|
|
101
|
+
"Manufacturer",
|
|
102
|
+
"ManufacturerList",
|
|
103
|
+
"NamedObject",
|
|
104
|
+
"StatusLabel",
|
|
105
|
+
"StatusLabelList",
|
|
106
|
+
"StatusLabelRef",
|
|
107
|
+
"Supplier",
|
|
108
|
+
"SupplierList",
|
|
109
|
+
"User",
|
|
110
|
+
"UserList",
|
|
111
|
+
]
|