glpi-utils 1.2.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.
glpi_utils/__init__.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ glpi_utils
3
+ ~~~~~~~~~~
4
+
5
+ Python library for the GLPI 11 REST API.
6
+
7
+ Clients
8
+ -------
9
+ * :class:`GlpiAPI` – Sync client, legacy API (``/apirest.php``)
10
+ * :class:`AsyncGlpiAPI` – Async client, legacy API (``/apirest.php``)
11
+ * :class:`GlpiOAuthClient` – Sync client, high-level API (``/api.php``, OAuth2)
12
+ * :class:`AsyncGlpiOAuthClient`– Async client, high-level API (``/api.php``, OAuth2)
13
+
14
+ Exceptions
15
+ ----------
16
+ * :exc:`GlpiError` – Base exception
17
+ * :exc:`GlpiAPIError` – API-level error (includes status_code, error_code)
18
+ * :exc:`GlpiAuthError` – Authentication / session errors
19
+ * :exc:`GlpiNotFoundError` – 404 Not Found
20
+ * :exc:`GlpiPermissionError` – 403 Forbidden
21
+ * :exc:`GlpiConnectionError` – Network/connectivity errors
22
+
23
+ Utilities
24
+ ---------
25
+ * :class:`GLPIVersion` – Comparable version helper
26
+ * :class:`SensitiveFilter` – Logging filter that masks credentials
27
+ * :class:`EmptyHandler` – Silent handler (library default)
28
+ """
29
+
30
+ from .api import GlpiAPI
31
+ from .aio import AsyncGlpiAPI
32
+ from .oauth import AsyncGlpiOAuthClient, GlpiOAuthClient
33
+ from .exceptions import (
34
+ GlpiAPIError,
35
+ GlpiAuthError,
36
+ GlpiConnectionError,
37
+ GlpiError,
38
+ GlpiNotFoundError,
39
+ GlpiPermissionError,
40
+ )
41
+ from .logger import EmptyHandler, SensitiveFilter
42
+ from .version import GLPIVersion
43
+
44
+ __version__ = "1.2.0"
45
+
46
+ __all__ = [
47
+ # Clients
48
+ "GlpiAPI",
49
+ "AsyncGlpiAPI",
50
+ "GlpiOAuthClient",
51
+ "AsyncGlpiOAuthClient",
52
+ # Exceptions
53
+ "GlpiError",
54
+ "GlpiAPIError",
55
+ "GlpiAuthError",
56
+ "GlpiNotFoundError",
57
+ "GlpiPermissionError",
58
+ "GlpiConnectionError",
59
+ # Utilities
60
+ "GLPIVersion",
61
+ "SensitiveFilter",
62
+ "EmptyHandler",
63
+ ]
@@ -0,0 +1,162 @@
1
+ """
2
+ glpi_utils._resource
3
+ ~~~~~~~~~~~~~~~~~~~~
4
+
5
+ ItemProxy and AsyncItemProxy – thin accessor objects that bind an item-type
6
+ name to a session so callers can write ``api.ticket.get(1)`` instead of
7
+ calling lower-level methods directly.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from typing import TYPE_CHECKING, Any, AsyncIterator, Iterator
13
+
14
+ if TYPE_CHECKING:
15
+ from .api import GlpiAPI
16
+ from .aio import AsyncGlpiAPI
17
+
18
+
19
+ class ItemProxy:
20
+ """Synchronous resource accessor for a single GLPI item-type."""
21
+
22
+ def __init__(self, session: "GlpiAPI", itemtype: str) -> None:
23
+ self._session = session
24
+ self._itemtype = itemtype
25
+
26
+ def get(self, item_id: int, **kwargs: Any) -> dict:
27
+ """Return a single item by *item_id*."""
28
+ return self._session.get_item(self._itemtype, item_id, **kwargs)
29
+
30
+ def get_all(self, **kwargs: Any) -> list:
31
+ """Return a single page of items (default range ``0-49``).
32
+
33
+ Use :meth:`get_all_pages` to fetch every item automatically.
34
+ """
35
+ return self._session.get_all_items(self._itemtype, **kwargs)
36
+
37
+ def get_all_pages(self, page_size: int = 50, **kwargs: Any) -> list:
38
+ """Fetch **all** items across all pages automatically.
39
+
40
+ Examples
41
+ --------
42
+ ::
43
+
44
+ all_tickets = api.ticket.get_all_pages()
45
+ open_tickets = api.ticket.get_all_pages(searchText={"status": "1"})
46
+ """
47
+ return self._session.get_all_pages(self._itemtype, page_size=page_size, **kwargs)
48
+
49
+ def iter_pages(self, page_size: int = 50, **kwargs: Any) -> Iterator[list]:
50
+ """Yield one page at a time — memory-efficient for large datasets.
51
+
52
+ Examples
53
+ --------
54
+ ::
55
+
56
+ for page in api.ticket.iter_pages(page_size=100):
57
+ for ticket in page:
58
+ process(ticket)
59
+ """
60
+ return self._session.iter_pages(self._itemtype, page_size=page_size, **kwargs)
61
+
62
+ def search(self, **kwargs: Any) -> dict:
63
+ """Run the GLPI search engine against this item-type."""
64
+ return self._session.search(self._itemtype, **kwargs)
65
+
66
+ def create(self, input_data: Any, **kwargs: Any) -> Any:
67
+ """Create one or several items."""
68
+ return self._session.create_item(self._itemtype, input_data, **kwargs)
69
+
70
+ def update(self, input_data: Any, **kwargs: Any) -> list:
71
+ """Update one or several items. Each dict must contain an ``"id"`` key."""
72
+ return self._session.update_item(self._itemtype, input_data, **kwargs)
73
+
74
+ def delete(
75
+ self,
76
+ input_data: Any,
77
+ force_purge: bool = False,
78
+ history: bool = True,
79
+ ) -> list:
80
+ """Delete one or several items."""
81
+ return self._session.delete_item(
82
+ self._itemtype, input_data,
83
+ force_purge=force_purge, history=history,
84
+ )
85
+
86
+ def get_sub_items(self, item_id: int, sub_itemtype: str, **kwargs: Any) -> list:
87
+ """Return sub-items of *sub_itemtype* for the given parent *item_id*."""
88
+ return self._session.get_sub_items(
89
+ self._itemtype, item_id, sub_itemtype, **kwargs
90
+ )
91
+
92
+ def add_sub_item(
93
+ self, item_id: int, sub_itemtype: str, input_data: dict, **kwargs: Any
94
+ ) -> dict:
95
+ """Add a sub-item (e.g. a followup or task) to a parent item."""
96
+ return self._session.add_sub_item(
97
+ self._itemtype, item_id, sub_itemtype, input_data, **kwargs
98
+ )
99
+
100
+ def __repr__(self) -> str:
101
+ return f"ItemProxy(itemtype={self._itemtype!r})"
102
+
103
+
104
+ class AsyncItemProxy:
105
+ """Asynchronous resource accessor for a single GLPI item-type."""
106
+
107
+ def __init__(self, session: "AsyncGlpiAPI", itemtype: str) -> None:
108
+ self._session = session
109
+ self._itemtype = itemtype
110
+
111
+ async def get(self, item_id: int, **kwargs: Any) -> dict:
112
+ return await self._session.get_item(self._itemtype, item_id, **kwargs)
113
+
114
+ async def get_all(self, **kwargs: Any) -> list:
115
+ """Return a single page of items (default range ``0-49``)."""
116
+ return await self._session.get_all_items(self._itemtype, **kwargs)
117
+
118
+ async def get_all_pages(self, page_size: int = 50, **kwargs: Any) -> list:
119
+ """Fetch **all** items across all pages automatically."""
120
+ return await self._session.get_all_pages(self._itemtype, page_size=page_size, **kwargs)
121
+
122
+ async def iter_pages(self, page_size: int = 50, **kwargs: Any) -> AsyncIterator[list]:
123
+ """Yield one page at a time asynchronously."""
124
+ async for page in self._session.iter_pages(self._itemtype, page_size=page_size, **kwargs):
125
+ yield page
126
+
127
+ async def search(self, **kwargs: Any) -> dict:
128
+ return await self._session.search(self._itemtype, **kwargs)
129
+
130
+ async def create(self, input_data: Any, **kwargs: Any) -> Any:
131
+ return await self._session.create_item(self._itemtype, input_data, **kwargs)
132
+
133
+ async def update(self, input_data: Any, **kwargs: Any) -> list:
134
+ return await self._session.update_item(self._itemtype, input_data, **kwargs)
135
+
136
+ async def delete(
137
+ self,
138
+ input_data: Any,
139
+ force_purge: bool = False,
140
+ history: bool = True,
141
+ ) -> list:
142
+ return await self._session.delete_item(
143
+ self._itemtype, input_data,
144
+ force_purge=force_purge, history=history,
145
+ )
146
+
147
+ async def get_sub_items(
148
+ self, item_id: int, sub_itemtype: str, **kwargs: Any
149
+ ) -> list:
150
+ return await self._session.get_sub_items(
151
+ self._itemtype, item_id, sub_itemtype, **kwargs
152
+ )
153
+
154
+ async def add_sub_item(
155
+ self, item_id: int, sub_itemtype: str, input_data: dict, **kwargs: Any
156
+ ) -> dict:
157
+ return await self._session.add_sub_item(
158
+ self._itemtype, item_id, sub_itemtype, input_data, **kwargs
159
+ )
160
+
161
+ def __repr__(self) -> str:
162
+ return f"AsyncItemProxy(itemtype={self._itemtype!r})"
glpi_utils/aio.py ADDED
@@ -0,0 +1,463 @@
1
+ """
2
+ glpi_utils.aio
3
+ ~~~~~~~~~~~~~~
4
+
5
+ Asynchronous GLPI REST API client powered by ``aiohttp``.
6
+
7
+ The public interface mirrors :class:`~glpi_utils.api.GlpiAPI` exactly so
8
+ callers can swap between sync and async with minimal changes.
9
+
10
+ Requires the optional ``aiohttp`` dependency::
11
+
12
+ pip install glpi-utils[async]
13
+ """
14
+
15
+ from __future__ import annotations
16
+
17
+ import logging
18
+ import os
19
+ from base64 import b64encode
20
+ from typing import Any, AsyncIterator, Optional
21
+
22
+ from ._resource import AsyncItemProxy
23
+ from .api import DEFAULT_PAGE_SIZE, _ITEMTYPE_MAP, _boolify_params, _parse_content_range, _raise_for_glpi_error
24
+ from .exceptions import GlpiAuthError, GlpiConnectionError
25
+ from .logger import EmptyHandler, SensitiveFilter
26
+ from .version import GLPIVersion
27
+
28
+ log = logging.getLogger(__name__)
29
+ log.addHandler(EmptyHandler())
30
+ log.addFilter(SensitiveFilter())
31
+
32
+
33
+ class AsyncGlpiAPI:
34
+ """Asynchronous client for the GLPI 11 legacy REST API (``/apirest.php``).
35
+
36
+ Must be used with ``await``::
37
+
38
+ import asyncio
39
+ from glpi_utils import AsyncGlpiAPI
40
+
41
+ async def main():
42
+ api = AsyncGlpiAPI(url="https://glpi.example.com")
43
+ await api.login(username="glpi", password="glpi")
44
+
45
+ # Single page
46
+ tickets = await api.ticket.get_all()
47
+
48
+ # All pages automatically
49
+ all_tickets = await api.ticket.get_all_pages()
50
+
51
+ # Memory-efficient iteration
52
+ async for page in api.ticket.iter_pages(page_size=100):
53
+ for ticket in page:
54
+ process(ticket)
55
+
56
+ await api.logout()
57
+
58
+ asyncio.run(main())
59
+
60
+ Can also be used as an async context manager::
61
+
62
+ async with AsyncGlpiAPI(url="https://glpi.example.com") as api:
63
+ await api.login(username="glpi", password="glpi")
64
+ version = await api.get_version()
65
+
66
+ Parameters
67
+ ----------
68
+ url : str or None
69
+ app_token : str or None
70
+ verify_ssl : bool
71
+ timeout : int
72
+ """
73
+
74
+ def __init__(
75
+ self,
76
+ url: Optional[str] = None,
77
+ app_token: Optional[str] = None,
78
+ verify_ssl: bool = True,
79
+ timeout: int = 30,
80
+ ) -> None:
81
+ try:
82
+ import aiohttp # noqa: F401
83
+ except ImportError as exc:
84
+ raise ImportError(
85
+ "AsyncGlpiAPI requires 'aiohttp'. "
86
+ "Install it with: pip install glpi-utils[async]"
87
+ ) from exc
88
+
89
+ self._url = (url or os.environ.get("GLPI_URL", "")).rstrip("/")
90
+ if not self._url:
91
+ raise ValueError("A GLPI URL is required. Pass url= or set GLPI_URL.")
92
+ self._app_token = app_token or os.environ.get("GLPI_APP_TOKEN")
93
+ self._verify_ssl = verify_ssl
94
+ self._timeout = timeout
95
+
96
+ self._session_token: Optional[str] = None
97
+ self._http: Any = None
98
+ self._version: Optional[GLPIVersion] = None
99
+ self._proxies: dict = {}
100
+
101
+ # ------------------------------------------------------------------
102
+ # Async context manager
103
+ # ------------------------------------------------------------------
104
+
105
+ async def __aenter__(self) -> "AsyncGlpiAPI":
106
+ return self
107
+
108
+ async def __aexit__(self, *_: Any) -> None:
109
+ if self._session_token:
110
+ try:
111
+ await self.logout()
112
+ except Exception:
113
+ pass
114
+ if self._http and not self._http.closed:
115
+ await self._http.close()
116
+
117
+ # ------------------------------------------------------------------
118
+ # Fluent accessors
119
+ # ------------------------------------------------------------------
120
+
121
+ def __getattr__(self, name: str) -> AsyncItemProxy:
122
+ lower = name.lower()
123
+ if lower in _ITEMTYPE_MAP:
124
+ if lower not in self._proxies:
125
+ self._proxies[lower] = AsyncItemProxy(self, _ITEMTYPE_MAP[lower])
126
+ return self._proxies[lower]
127
+ raise AttributeError(
128
+ f"{self.__class__.__name__!r} has no attribute {name!r}. "
129
+ "Use api.item('YourItemtype') for non-standard item types."
130
+ )
131
+
132
+ def item(self, itemtype: str) -> AsyncItemProxy:
133
+ """Return an :class:`~glpi_utils._resource.AsyncItemProxy` for any itemtype."""
134
+ if itemtype not in self._proxies:
135
+ self._proxies[itemtype] = AsyncItemProxy(self, itemtype)
136
+ return self._proxies[itemtype]
137
+
138
+ # ------------------------------------------------------------------
139
+ # Internal HTTP helpers
140
+ # ------------------------------------------------------------------
141
+
142
+ @property
143
+ def _base_url(self) -> str:
144
+ return f"{self._url}/apirest.php"
145
+
146
+ def _get_http(self) -> Any:
147
+ """Lazily create the aiohttp ClientSession."""
148
+ import aiohttp
149
+
150
+ if self._http is None or self._http.closed:
151
+ connector = aiohttp.TCPConnector(ssl=self._verify_ssl)
152
+ self._http = aiohttp.ClientSession(connector=connector)
153
+ return self._http
154
+
155
+ def _default_headers(self) -> dict:
156
+ headers: dict = {"Content-Type": "application/json"}
157
+ if self._app_token:
158
+ headers["App-Token"] = self._app_token
159
+ if self._session_token:
160
+ headers["Session-Token"] = self._session_token
161
+ return headers
162
+
163
+ async def _request(
164
+ self,
165
+ method: str,
166
+ path: str,
167
+ *,
168
+ headers: Optional[dict] = None,
169
+ params: Optional[dict] = None,
170
+ json: Any = None,
171
+ ) -> Any:
172
+ body, _ = await self._request_with_headers(
173
+ method, path, headers=headers, params=params, json=json
174
+ )
175
+ return body
176
+
177
+ async def _request_with_headers(
178
+ self,
179
+ method: str,
180
+ path: str,
181
+ *,
182
+ headers: Optional[dict] = None,
183
+ params: Optional[dict] = None,
184
+ json: Any = None,
185
+ ) -> tuple:
186
+ """Like ``_request`` but returns ``(body, response_headers)``."""
187
+ import aiohttp
188
+
189
+ url = f"{self._base_url}/{path.lstrip('/')}"
190
+ merged_headers = {**self._default_headers(), **(headers or {})}
191
+
192
+ log.debug("ASYNC %s %s params=%s", method.upper(), url, params)
193
+
194
+ http = self._get_http()
195
+ timeout = aiohttp.ClientTimeout(total=self._timeout)
196
+
197
+ try:
198
+ async with http.request(
199
+ method, url,
200
+ headers=merged_headers,
201
+ params=params,
202
+ json=json,
203
+ timeout=timeout,
204
+ ) as response:
205
+ status = response.status
206
+ resp_headers = dict(response.headers)
207
+ log.debug("Response %s from %s", status, url)
208
+
209
+ if status == 204 or response.content_length == 0:
210
+ return None, resp_headers
211
+
212
+ body = await response.json(content_type=None)
213
+
214
+ class _FakeResponse:
215
+ status_code = status
216
+ content = True
217
+
218
+ def json(self_):
219
+ return body
220
+
221
+ text = str(body)
222
+
223
+ _raise_for_glpi_error(_FakeResponse()) # type: ignore[arg-type]
224
+ return body, resp_headers
225
+
226
+ except aiohttp.ClientConnectorError as exc:
227
+ raise GlpiConnectionError(f"Cannot reach GLPI at {self._url}: {exc}") from exc
228
+ except aiohttp.ServerTimeoutError as exc:
229
+ raise GlpiConnectionError(f"Request timed out: {exc}") from exc
230
+
231
+ # ------------------------------------------------------------------
232
+ # Authentication
233
+ # ------------------------------------------------------------------
234
+
235
+ async def login(
236
+ self,
237
+ username: Optional[str] = None,
238
+ password: Optional[str] = None,
239
+ user_token: Optional[str] = None,
240
+ ) -> None:
241
+ """Authenticate and obtain a session token."""
242
+ username = username or os.environ.get("GLPI_USER")
243
+ password = password or os.environ.get("GLPI_PASSWORD")
244
+ user_token = user_token or os.environ.get("GLPI_USER_TOKEN")
245
+
246
+ auth_headers: dict = {}
247
+
248
+ if user_token:
249
+ auth_headers["Authorization"] = f"user_token {user_token}"
250
+ elif username and password:
251
+ credentials = b64encode(f"{username}:{password}".encode()).decode()
252
+ auth_headers["Authorization"] = f"Basic {credentials}"
253
+ else:
254
+ raise GlpiAuthError("Provide username+password or user_token.")
255
+
256
+ data = await self._request("GET", "initSession", headers=auth_headers)
257
+ self._session_token = data["session_token"]
258
+ log.debug("Async session established.")
259
+
260
+ async def logout(self) -> None:
261
+ """Terminate the active GLPI session."""
262
+ if self._session_token:
263
+ try:
264
+ await self._request("GET", "killSession")
265
+ finally:
266
+ self._session_token = None
267
+ log.debug("Async session terminated.")
268
+
269
+ # ------------------------------------------------------------------
270
+ # Version
271
+ # ------------------------------------------------------------------
272
+
273
+ @property
274
+ def version(self) -> Optional[GLPIVersion]:
275
+ """Return cached version (call ``await api.get_version()`` to fetch)."""
276
+ return self._version
277
+
278
+ async def get_version(self) -> GLPIVersion:
279
+ """Fetch and cache the GLPI server version."""
280
+ data = await self._request("GET", "getGlpiVersion")
281
+ self._version = GLPIVersion(data.get("glpi_version", "0.0.0"))
282
+ return self._version
283
+
284
+ # ------------------------------------------------------------------
285
+ # Session utilities
286
+ # ------------------------------------------------------------------
287
+
288
+ async def get_my_profiles(self) -> list:
289
+ return (await self._request("GET", "getMyProfiles"))["myprofiles"]
290
+
291
+ async def get_active_profile(self) -> dict:
292
+ return (await self._request("GET", "getActiveProfile"))["active_profile"]
293
+
294
+ async def set_active_profile(self, profile_id: int) -> None:
295
+ await self._request("POST", "changeActiveProfile", json={"profiles_id": profile_id})
296
+
297
+ async def get_my_entities(self, is_recursive: bool = False) -> list:
298
+ return (
299
+ await self._request(
300
+ "GET", "getMyEntities", params={"is_recursive": int(is_recursive)}
301
+ )
302
+ )["myentities"]
303
+
304
+ async def get_active_entities(self) -> dict:
305
+ return (await self._request("GET", "getActiveEntities"))["active_entity"]
306
+
307
+ async def set_active_entity(self, entity_id: int, is_recursive: bool = False) -> None:
308
+ await self._request(
309
+ "POST", "changeActiveEntities",
310
+ json={"entities_id": entity_id, "is_recursive": int(is_recursive)},
311
+ )
312
+
313
+ async def get_full_session(self) -> dict:
314
+ return (await self._request("GET", "getFullSession"))["session"]
315
+
316
+ # ------------------------------------------------------------------
317
+ # Item CRUD
318
+ # ------------------------------------------------------------------
319
+
320
+ async def get_item(self, itemtype: str, item_id: int, **kwargs: Any) -> dict:
321
+ params = _boolify_params(kwargs)
322
+ return await self._request("GET", f"{itemtype}/{item_id}", params=params)
323
+
324
+ async def get_all_items(self, itemtype: str, **kwargs: Any) -> list:
325
+ """Return a single page of items (default range ``0-49``).
326
+
327
+ Use :meth:`get_all_pages` to retrieve all items automatically.
328
+ """
329
+ params = _boolify_params(kwargs)
330
+ if "range" not in params:
331
+ params["range"] = f"0-{DEFAULT_PAGE_SIZE - 1}"
332
+ return await self._request("GET", itemtype, params=params)
333
+
334
+ async def get_all_pages(
335
+ self,
336
+ itemtype: str,
337
+ page_size: int = DEFAULT_PAGE_SIZE,
338
+ **kwargs: Any,
339
+ ) -> list:
340
+ """Fetch **all** items of *itemtype* by iterating pages automatically.
341
+
342
+ Parameters
343
+ ----------
344
+ itemtype : str
345
+ page_size : int
346
+ Items per request (default: 50).
347
+ **kwargs
348
+ Extra GLPI parameters: ``sort``, ``order``, ``searchText``,
349
+ ``is_deleted``, ``expand_dropdowns``, etc.
350
+
351
+ Returns
352
+ -------
353
+ list
354
+ All matching items as a flat list of dicts.
355
+ """
356
+ results: list = []
357
+ async for page in self.iter_pages(itemtype, page_size=page_size, **kwargs):
358
+ results.extend(page)
359
+ return results
360
+
361
+ async def iter_pages(
362
+ self,
363
+ itemtype: str,
364
+ page_size: int = DEFAULT_PAGE_SIZE,
365
+ **kwargs: Any,
366
+ ) -> AsyncIterator[list]:
367
+ """Yield one page of items at a time asynchronously.
368
+
369
+ Parameters
370
+ ----------
371
+ itemtype : str
372
+ page_size : int
373
+ **kwargs
374
+ Same as :meth:`get_all_pages`.
375
+
376
+ Yields
377
+ ------
378
+ list
379
+ One page per iteration.
380
+ """
381
+ params = _boolify_params(kwargs)
382
+ start = 0
383
+ fetched = 0
384
+
385
+ while True:
386
+ end = start + page_size - 1
387
+ params["range"] = f"{start}-{end}"
388
+
389
+ page_items, resp_headers = await self._request_with_headers(
390
+ "GET", itemtype, params=params
391
+ )
392
+
393
+ if not page_items:
394
+ return
395
+
396
+ fetched += len(page_items)
397
+ yield page_items
398
+
399
+ total = _parse_content_range(resp_headers.get("Content-Range", ""))
400
+
401
+ if total is not None and fetched >= total:
402
+ return
403
+ if len(page_items) < page_size:
404
+ return
405
+
406
+ start += page_size
407
+
408
+ async def search(self, itemtype: str, **kwargs: Any) -> dict:
409
+ params = _boolify_params(kwargs)
410
+ return await self._request("GET", f"search/{itemtype}", params=params)
411
+
412
+ async def create_item(self, itemtype: str, input_data: Any, **kwargs: Any) -> Any:
413
+ payload: dict = {"input": input_data}
414
+ payload.update(kwargs)
415
+ return await self._request("POST", itemtype, json=payload)
416
+
417
+ async def update_item(self, itemtype: str, input_data: Any, **kwargs: Any) -> list:
418
+ payload: dict = {"input": input_data}
419
+ payload.update(kwargs)
420
+ return await self._request("PUT", itemtype, json=payload)
421
+
422
+ async def delete_item(
423
+ self,
424
+ itemtype: str,
425
+ input_data: Any,
426
+ force_purge: bool = False,
427
+ history: bool = True,
428
+ ) -> list:
429
+ payload: dict = {
430
+ "input": input_data,
431
+ "force_purge": int(force_purge),
432
+ "history": int(history),
433
+ }
434
+ return await self._request("DELETE", itemtype, json=payload)
435
+
436
+ # ------------------------------------------------------------------
437
+ # Sub-items
438
+ # ------------------------------------------------------------------
439
+
440
+ async def get_sub_items(
441
+ self, itemtype: str, item_id: int, sub_itemtype: str, **kwargs: Any
442
+ ) -> list:
443
+ params = _boolify_params(kwargs)
444
+ return await self._request(
445
+ "GET", f"{itemtype}/{item_id}/{sub_itemtype}", params=params
446
+ )
447
+
448
+ async def add_sub_item(
449
+ self,
450
+ itemtype: str,
451
+ item_id: int,
452
+ sub_itemtype: str,
453
+ input_data: dict,
454
+ **kwargs: Any,
455
+ ) -> dict:
456
+ payload: dict = {"input": input_data}
457
+ payload.update(kwargs)
458
+ return await self._request(
459
+ "POST", f"{itemtype}/{item_id}/{sub_itemtype}", json=payload
460
+ )
461
+
462
+ async def list_item_types(self) -> list:
463
+ return await self._request("GET", "listItemtypes")