glpi-python-client 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.
Files changed (98) hide show
  1. glpi_python_client/__init__.py +36 -0
  2. glpi_python_client/auth/__init__.py +11 -0
  3. glpi_python_client/auth/auth.py +310 -0
  4. glpi_python_client/auth/tests/test_auth.py +189 -0
  5. glpi_python_client/clients/__init__.py +18 -0
  6. glpi_python_client/clients/api_v1_session.py +460 -0
  7. glpi_python_client/clients/api_v2_client.py +317 -0
  8. glpi_python_client/clients/async_api_v2_client.py +236 -0
  9. glpi_python_client/clients/tests/__init__.py +5 -0
  10. glpi_python_client/clients/tests/test_api_v1_session.py +85 -0
  11. glpi_python_client/clients/tests/test_api_v2_client.py +349 -0
  12. glpi_python_client/clients/tests/test_async_api_v2_client.py +257 -0
  13. glpi_python_client/clients/v2/__init__.py +8 -0
  14. glpi_python_client/clients/v2/async_/__init__.py +12 -0
  15. glpi_python_client/clients/v2/async_/api.py +29 -0
  16. glpi_python_client/clients/v2/async_/directory.py +88 -0
  17. glpi_python_client/clients/v2/async_/documents.py +144 -0
  18. glpi_python_client/clients/v2/async_/team.py +125 -0
  19. glpi_python_client/clients/v2/async_/tests/__init__.py +5 -0
  20. glpi_python_client/clients/v2/async_/tests/test_directory.py +43 -0
  21. glpi_python_client/clients/v2/async_/tests/test_documents.py +41 -0
  22. glpi_python_client/clients/v2/async_/tests/test_team.py +44 -0
  23. glpi_python_client/clients/v2/async_/tests/test_tickets.py +174 -0
  24. glpi_python_client/clients/v2/async_/tests/test_timeline.py +126 -0
  25. glpi_python_client/clients/v2/async_/tickets.py +312 -0
  26. glpi_python_client/clients/v2/async_/timeline.py +312 -0
  27. glpi_python_client/clients/v2/async_/transport.py +251 -0
  28. glpi_python_client/clients/v2/common/__init__.py +6 -0
  29. glpi_python_client/clients/v2/common/client_config.py +219 -0
  30. glpi_python_client/clients/v2/common/constants.py +45 -0
  31. glpi_python_client/clients/v2/common/errors.py +23 -0
  32. glpi_python_client/clients/v2/common/filters.py +30 -0
  33. glpi_python_client/clients/v2/common/payloads.py +57 -0
  34. glpi_python_client/clients/v2/common/request_http.py +195 -0
  35. glpi_python_client/clients/v2/common/response_payloads.py +76 -0
  36. glpi_python_client/clients/v2/common/ticket_search.py +113 -0
  37. glpi_python_client/clients/v2/sync/__init__.py +12 -0
  38. glpi_python_client/clients/v2/sync/api.py +29 -0
  39. glpi_python_client/clients/v2/sync/directory.py +90 -0
  40. glpi_python_client/clients/v2/sync/documents.py +144 -0
  41. glpi_python_client/clients/v2/sync/team.py +125 -0
  42. glpi_python_client/clients/v2/sync/tests/__init__.py +5 -0
  43. glpi_python_client/clients/v2/sync/tests/test_directory.py +57 -0
  44. glpi_python_client/clients/v2/sync/tests/test_documents.py +99 -0
  45. glpi_python_client/clients/v2/sync/tests/test_team.py +64 -0
  46. glpi_python_client/clients/v2/sync/tests/test_tickets.py +430 -0
  47. glpi_python_client/clients/v2/sync/tests/test_timeline.py +77 -0
  48. glpi_python_client/clients/v2/sync/tests/test_transport.py +89 -0
  49. glpi_python_client/clients/v2/sync/tickets.py +312 -0
  50. glpi_python_client/clients/v2/sync/timeline.py +308 -0
  51. glpi_python_client/clients/v2/sync/transport.py +246 -0
  52. glpi_python_client/content/__init__.py +11 -0
  53. glpi_python_client/content/conversion.py +58 -0
  54. glpi_python_client/content/records/__init__.py +84 -0
  55. glpi_python_client/content/records/core/__init__.py +6 -0
  56. glpi_python_client/content/records/core/document_links.py +100 -0
  57. glpi_python_client/content/records/core/normalization.py +53 -0
  58. glpi_python_client/content/records/core/references.py +98 -0
  59. glpi_python_client/content/records/core/scalars.py +83 -0
  60. glpi_python_client/content/records/parsers/__init__.py +6 -0
  61. glpi_python_client/content/records/parsers/directory.py +62 -0
  62. glpi_python_client/content/records/parsers/documents.py +49 -0
  63. glpi_python_client/content/records/parsers/team.py +58 -0
  64. glpi_python_client/content/records/parsers/tests/__init__.py +5 -0
  65. glpi_python_client/content/records/parsers/tests/test_tickets.py +35 -0
  66. glpi_python_client/content/records/parsers/tests/test_timeline.py +20 -0
  67. glpi_python_client/content/records/parsers/tickets.py +96 -0
  68. glpi_python_client/content/records/parsers/timeline.py +119 -0
  69. glpi_python_client/content/tests/__init__.py +5 -0
  70. glpi_python_client/content/tests/test_conversion.py +13 -0
  71. glpi_python_client/models/__init__.py +30 -0
  72. glpi_python_client/models/_base.py +22 -0
  73. glpi_python_client/models/_payload.py +79 -0
  74. glpi_python_client/models/_shared.py +37 -0
  75. glpi_python_client/models/glpi/__init__.py +27 -0
  76. glpi_python_client/models/glpi/_document.py +59 -0
  77. glpi_python_client/models/glpi/_followup.py +77 -0
  78. glpi_python_client/models/glpi/_location.py +53 -0
  79. glpi_python_client/models/glpi/_solution.py +57 -0
  80. glpi_python_client/models/glpi/_task.py +41 -0
  81. glpi_python_client/models/glpi/_team_member.py +33 -0
  82. glpi_python_client/models/glpi/_ticket.py +303 -0
  83. glpi_python_client/models/glpi/_user.py +92 -0
  84. glpi_python_client/models/glpi/tests/__init__.py +5 -0
  85. glpi_python_client/models/glpi/tests/test__document.py +12 -0
  86. glpi_python_client/models/glpi/tests/test__followup.py +31 -0
  87. glpi_python_client/models/glpi/tests/test__location.py +12 -0
  88. glpi_python_client/models/glpi/tests/test__solution.py +11 -0
  89. glpi_python_client/models/glpi/tests/test__ticket.py +59 -0
  90. glpi_python_client/models/glpi/tests/test__user.py +29 -0
  91. glpi_python_client/py.typed +0 -0
  92. glpi_python_client/testing/__init__.py +27 -0
  93. glpi_python_client/testing/fixtures.py +52 -0
  94. glpi_python_client/testing/utils.py +149 -0
  95. glpi_python_client-0.1.0.dist-info/METADATA +144 -0
  96. glpi_python_client-0.1.0.dist-info/RECORD +98 -0
  97. glpi_python_client-0.1.0.dist-info/WHEEL +4 -0
  98. glpi_python_client-0.1.0.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,312 @@
1
+ """Asynchronous timeline operations for GLPI v2 clients.
2
+
3
+ This module groups async followup, task, solution, and attachment-lookup
4
+ helpers for ticket timeline data.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+
12
+ from tenacity import RetryError
13
+
14
+ from glpi_python_client.clients.v2.common.constants import (
15
+ FOLLOWUP_SUFFIX,
16
+ SOLUTION_SUFFIX,
17
+ TASK_SUFFIX,
18
+ TICKET_ENDPOINT,
19
+ GlpiId,
20
+ )
21
+ from glpi_python_client.clients.v2.common.errors import remote_error_message
22
+ from glpi_python_client.clients.v2.common.request_http import (
23
+ ensure_response_status,
24
+ require_response_text,
25
+ )
26
+ from glpi_python_client.clients.v2.common.response_payloads import (
27
+ timeline_records_from_response,
28
+ )
29
+ from glpi_python_client.content.records.core.scalars import _optional_text
30
+ from glpi_python_client.content.records.parsers.timeline import (
31
+ _glpi_followup_record,
32
+ _glpi_solution_record,
33
+ _glpi_task_record,
34
+ )
35
+ from glpi_python_client.models import GlpiFollowup, GlpiSolution, GlpiTask
36
+
37
+ from .transport import AsyncTransportMixin
38
+
39
+ logger = logging.getLogger(__name__)
40
+
41
+
42
+ class AsyncTimelineMixin(AsyncTransportMixin):
43
+ """Asynchronous followup, task, and solution helpers.
44
+
45
+ The mixin keeps ticket timeline behavior together because these endpoints
46
+ share parsing, logging, and optional legacy attachment lookup rules.
47
+ """
48
+
49
+ async def get_followup_records(self, ticket_id: GlpiId) -> list[GlpiFollowup]:
50
+ """Fetch the followups associated with one ticket.
51
+
52
+ Non-success responses are logged and normalized to an empty list so the
53
+ helper behaves like the other async timeline list operations.
54
+ """
55
+
56
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{FOLLOWUP_SUFFIX}"
57
+ response = await self._get_request(endpoint)
58
+ if response.status_code not in (200, 206):
59
+ logger.warning(
60
+ "Failed to get followups for ticket %s: %s",
61
+ ticket_id,
62
+ response.status_code,
63
+ )
64
+ return []
65
+ return timeline_records_from_response(
66
+ response,
67
+ record_factory=_glpi_followup_record,
68
+ )
69
+
70
+ async def get_task_records(self, ticket_id: GlpiId) -> list[GlpiTask]:
71
+ """Fetch the tasks associated with one ticket.
72
+
73
+ Returned records are parsed into typed ``GlpiTask`` instances using the
74
+ shared timeline response handling helpers.
75
+ """
76
+
77
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{TASK_SUFFIX}"
78
+ response = await self._get_request(endpoint)
79
+ if response.status_code not in (200, 206):
80
+ logger.warning(
81
+ "Failed to get tasks for ticket %s: %s",
82
+ ticket_id,
83
+ response.status_code,
84
+ )
85
+ return []
86
+ return timeline_records_from_response(
87
+ response,
88
+ record_factory=_glpi_task_record,
89
+ )
90
+
91
+ async def get_followup_attachment_document_ids(
92
+ self, followup_id: GlpiId
93
+ ) -> tuple[str, ...]:
94
+ """Fetch document IDs linked directly to one followup through v1.
95
+
96
+ Attachment lookup is best-effort because it depends on the optional
97
+ legacy v1 session and runs through ``asyncio.to_thread`` at that narrow
98
+ blocking boundary.
99
+ """
100
+
101
+ v1_session = self._v1
102
+ if v1_session is None:
103
+ return ()
104
+
105
+ document_ids: list[str] = []
106
+ seen_document_ids: set[str] = set()
107
+ try:
108
+ relations = await asyncio.to_thread(
109
+ v1_session.get_sub_items,
110
+ "ITILFollowup",
111
+ followup_id,
112
+ "Document_Item",
113
+ )
114
+ except (RetryError, ValueError) as exc:
115
+ logger.warning(
116
+ "Skipping GLPI followup %s attachment lookup after v1 API failure: %s",
117
+ followup_id,
118
+ remote_error_message(exc),
119
+ )
120
+ return ()
121
+ for relation in relations:
122
+ document_id = _optional_text(relation.get("documents_id"))
123
+ if document_id is None or document_id in seen_document_ids:
124
+ continue
125
+ seen_document_ids.add(document_id)
126
+ document_ids.append(document_id)
127
+ return tuple(document_ids)
128
+
129
+ async def get_solution_attachment_document_ids(
130
+ self, solution_id: GlpiId
131
+ ) -> tuple[str, ...]:
132
+ """Fetch document IDs linked directly to one solution through v1.
133
+
134
+ This mirrors followup attachment lookup and preserves first-seen order
135
+ while removing duplicate document identifiers.
136
+ """
137
+
138
+ v1_session = self._v1
139
+ if v1_session is None:
140
+ return ()
141
+
142
+ document_ids: list[str] = []
143
+ seen_document_ids: set[str] = set()
144
+ try:
145
+ relations = await asyncio.to_thread(
146
+ v1_session.get_sub_items,
147
+ "ITILSolution",
148
+ solution_id,
149
+ "Document_Item",
150
+ )
151
+ except (RetryError, ValueError) as exc:
152
+ logger.warning(
153
+ "Skipping GLPI solution %s attachment lookup after v1 API failure: %s",
154
+ solution_id,
155
+ remote_error_message(exc),
156
+ )
157
+ return ()
158
+ for relation in relations:
159
+ document_id = _optional_text(relation.get("documents_id"))
160
+ if document_id is None or document_id in seen_document_ids:
161
+ continue
162
+ seen_document_ids.add(document_id)
163
+ document_ids.append(document_id)
164
+ return tuple(document_ids)
165
+
166
+ async def get_solution_records(self, ticket_id: GlpiId) -> list[GlpiSolution]:
167
+ """Fetch the solutions associated with one ticket.
168
+
169
+ Each returned item is parsed into a typed ``GlpiSolution`` record using
170
+ the shared timeline parsing helpers.
171
+ """
172
+
173
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{SOLUTION_SUFFIX}"
174
+ response = await self._get_request(endpoint)
175
+ if response.status_code not in (200, 206):
176
+ logger.warning(
177
+ "Failed to get solutions for ticket %s: %s",
178
+ ticket_id,
179
+ response.status_code,
180
+ )
181
+ return []
182
+ return timeline_records_from_response(
183
+ response,
184
+ record_factory=_glpi_solution_record,
185
+ )
186
+
187
+ async def create_followup(
188
+ self,
189
+ ticket_id: GlpiId,
190
+ followup: GlpiFollowup,
191
+ ) -> str:
192
+ """Create a GLPI followup and return the identifier assigned by GLPI.
193
+
194
+ The response may expose the created ID under different keys, so the
195
+ helper normalizes that lookup before returning to callers.
196
+ """
197
+
198
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{FOLLOWUP_SUFFIX}"
199
+ payload_data = followup.to_api_payload()
200
+ response = await self._post_request(endpoint, payload_data)
201
+ ensure_response_status(
202
+ response,
203
+ success_statuses=(200, 201),
204
+ failure_message=f"Failed to post followup on ticket {ticket_id}",
205
+ )
206
+ followup_id = require_response_text(
207
+ response,
208
+ keys=("id", "followup_id"),
209
+ missing_message="GLPI followup create response did not include an ID",
210
+ )
211
+ logger.info(
212
+ "GLPI API created %s followup %s on ticket %s",
213
+ "private" if payload_data.get("is_private") else "public",
214
+ followup_id,
215
+ ticket_id,
216
+ )
217
+ return followup_id
218
+
219
+ async def update_followup(
220
+ self,
221
+ ticket_id: GlpiId,
222
+ followup_id: GlpiId,
223
+ followup: GlpiFollowup,
224
+ ) -> None:
225
+ """Update one GLPI followup on a ticket.
226
+
227
+ The followup model is serialized as-is and the method returns ``None``
228
+ once GLPI accepts the update.
229
+ """
230
+
231
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{FOLLOWUP_SUFFIX}/{followup_id}"
232
+ payload_data = followup.to_api_payload()
233
+ response = await self._update_request(endpoint, payload_data)
234
+ ensure_response_status(
235
+ response,
236
+ success_statuses=(200, 204),
237
+ failure_message=(
238
+ f"Failed to patch followup {followup_id} on ticket {ticket_id}"
239
+ ),
240
+ )
241
+ logger.info(
242
+ "GLPI API updated followup %s on ticket %s fields=%s",
243
+ followup_id,
244
+ ticket_id,
245
+ sorted(payload_data),
246
+ )
247
+ return None
248
+
249
+ async def delete_followup(self, ticket_id: GlpiId, followup_id: GlpiId) -> None:
250
+ """Delete one GLPI ticket followup by identifier.
251
+
252
+ Successful deletes are logged with both ticket and followup context to
253
+ help trace timeline mutations.
254
+ """
255
+
256
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{FOLLOWUP_SUFFIX}/{followup_id}"
257
+ response = await self._delete_request(endpoint)
258
+ ensure_response_status(
259
+ response,
260
+ success_statuses=(200, 204),
261
+ failure_message=(
262
+ f"Failed to delete followup {followup_id} on ticket {ticket_id}"
263
+ ),
264
+ )
265
+ logger.info("GLPI API deleted followup %s on ticket %s", followup_id, ticket_id)
266
+ return None
267
+
268
+ async def create_solution(
269
+ self,
270
+ ticket_id: GlpiId,
271
+ solution: GlpiSolution,
272
+ ) -> str:
273
+ """Create a GLPI solution and return the identifier assigned by GLPI.
274
+
275
+ As with followups, the created solution identifier is normalized from
276
+ the response payload before being returned.
277
+ """
278
+
279
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{SOLUTION_SUFFIX}"
280
+ payload_data = solution.to_api_payload()
281
+ response = await self._post_request(endpoint, payload_data)
282
+ ensure_response_status(
283
+ response,
284
+ success_statuses=(200, 201),
285
+ failure_message=f"Failed to post solution on ticket {ticket_id}",
286
+ )
287
+ solution_id = require_response_text(
288
+ response,
289
+ keys=("id", "solution_id"),
290
+ missing_message="GLPI solution create response did not include an ID",
291
+ )
292
+ logger.info("GLPI API created solution %s on ticket %s", solution_id, ticket_id)
293
+ return solution_id
294
+
295
+ async def delete_solution(self, ticket_id: GlpiId, solution_id: GlpiId) -> None:
296
+ """Delete one GLPI ticket solution by identifier.
297
+
298
+ The helper returns ``None`` after a successful delete response and logs
299
+ the mutation for troubleshooting and auditability.
300
+ """
301
+
302
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{SOLUTION_SUFFIX}/{solution_id}"
303
+ response = await self._delete_request(endpoint)
304
+ ensure_response_status(
305
+ response,
306
+ success_statuses=(200, 204),
307
+ failure_message=(
308
+ f"Failed to delete solution {solution_id} on ticket {ticket_id}"
309
+ ),
310
+ )
311
+ logger.info("GLPI API deleted solution %s on ticket %s", solution_id, ticket_id)
312
+ return None
@@ -0,0 +1,251 @@
1
+ """Asynchronous GLPI v2 transport methods.
2
+
3
+ This module owns the awaitable transport helpers that wrap the shared
4
+ ``requests`` session and keep blocking calls behind narrow thread boundaries.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import asyncio
10
+ import logging
11
+ from typing import TYPE_CHECKING, cast
12
+
13
+ import requests
14
+ from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
15
+
16
+ from glpi_python_client.clients.v2.common.request_http import (
17
+ build_request_headers,
18
+ build_request_url,
19
+ finalize_request_response,
20
+ request_params,
21
+ require_access_token,
22
+ )
23
+
24
+ if TYPE_CHECKING:
25
+ from glpi_python_client.auth.auth import GLPITokenManager
26
+ from glpi_python_client.clients.api_v1_session import GLPIV1Session
27
+
28
+ logger = logging.getLogger(__name__)
29
+
30
+
31
+ class AsyncTransportMixin:
32
+ """Asynchronous GLPI API transport helpers.
33
+
34
+ The transport mixin keeps token handling, header construction, retries, and
35
+ request dispatch out of the endpoint-specific async mixins.
36
+ """
37
+
38
+ _auth: GLPITokenManager
39
+ _auth_lock: asyncio.Lock
40
+ _closed: bool = False
41
+ _session: requests.Session
42
+ _v1: GLPIV1Session | None
43
+ entity_recursive: bool
44
+ glpi_api_url: str
45
+ glpi_entity: int | None
46
+ glpi_profile: int | None
47
+ language: str
48
+
49
+ def _ensure_open(self) -> None:
50
+ """Raise when the client has already been closed.
51
+
52
+ All async transport helpers call this guard before touching the shared
53
+ HTTP session.
54
+ """
55
+
56
+ if self._closed:
57
+ raise RuntimeError("GLPI client is closed")
58
+
59
+ async def _ensure_token(self) -> None:
60
+ """Ensure that a valid GLPI access token is available.
61
+
62
+ Token refresh is serialized by the async lock so concurrent awaited
63
+ calls do not race while updating shared authentication state.
64
+ """
65
+
66
+ self._ensure_open()
67
+ async with self._auth_lock:
68
+ await asyncio.to_thread(self._auth.ensure_token)
69
+
70
+ def _get_headers(
71
+ self,
72
+ *,
73
+ include_content_type: bool = False,
74
+ skip_entity: bool = False,
75
+ ) -> dict[str, str]:
76
+ """Build GLPI request headers for the current client state.
77
+
78
+ This convenience wrapper forwards the current transport state to the
79
+ shared header builder used across sync and async implementations.
80
+ """
81
+
82
+ return build_request_headers(
83
+ access_token=self._auth.access_token,
84
+ language=self.language,
85
+ glpi_entity=self.glpi_entity,
86
+ glpi_profile=self.glpi_profile,
87
+ entity_recursive=self.entity_recursive,
88
+ include_content_type=include_content_type,
89
+ skip_entity=skip_entity,
90
+ )
91
+
92
+ async def _send_request(
93
+ self,
94
+ method: str,
95
+ url: str,
96
+ **kwargs: object,
97
+ ) -> requests.Response:
98
+ request_method = getattr(self._session, method)
99
+ return cast(
100
+ requests.Response,
101
+ await asyncio.to_thread(request_method, url, **kwargs),
102
+ )
103
+
104
+ async def _execute_request(
105
+ self,
106
+ *,
107
+ method: str,
108
+ endpoint: str,
109
+ success_statuses: tuple[int, ...],
110
+ params: dict[str, object] | None = None,
111
+ json_body: dict[str, object] | None = None,
112
+ skip_entity: bool = False,
113
+ include_content_type: bool = False,
114
+ ) -> requests.Response:
115
+ """Execute one authenticated GLPI request asynchronously.
116
+
117
+ The helper normalizes the endpoint URL, headers, timeout, and payload
118
+ placement before dispatching the blocking HTTP call through the async
119
+ transport wrapper.
120
+ """
121
+
122
+ await self._ensure_token()
123
+ access_token = require_access_token(self._auth.access_token)
124
+ url = build_request_url(self.glpi_api_url, endpoint)
125
+
126
+ request_kwargs: dict[str, object] = {
127
+ "headers": build_request_headers(
128
+ access_token=access_token,
129
+ language=self.language,
130
+ glpi_entity=self.glpi_entity,
131
+ glpi_profile=self.glpi_profile,
132
+ entity_recursive=self.entity_recursive,
133
+ include_content_type=include_content_type,
134
+ skip_entity=skip_entity,
135
+ ),
136
+ "timeout": 30,
137
+ }
138
+ if method == "get":
139
+ request_kwargs["params"] = request_params(params)
140
+ else:
141
+ request_kwargs["json"] = json_body
142
+
143
+ response = await self._send_request(method, url, **request_kwargs)
144
+ return finalize_request_response(
145
+ response,
146
+ method=method,
147
+ url=url,
148
+ success_statuses=success_statuses,
149
+ logger=logger,
150
+ )
151
+
152
+ @retry(
153
+ retry=retry_if_exception_type(requests.RequestException),
154
+ stop=stop_after_attempt(3),
155
+ wait=wait_fixed(3),
156
+ )
157
+ async def _get_request(
158
+ self,
159
+ endpoint: str,
160
+ params: dict[str, object] | None = None,
161
+ skip_entity: bool = False,
162
+ ) -> requests.Response:
163
+ """Execute one authenticated GLPI ``GET`` request asynchronously.
164
+
165
+ Network-level request exceptions are retried according to the transport
166
+ retry policy before the response is returned to the caller.
167
+ """
168
+
169
+ return await self._execute_request(
170
+ method="get",
171
+ endpoint=endpoint,
172
+ success_statuses=(200, 206),
173
+ params=params,
174
+ skip_entity=skip_entity,
175
+ )
176
+
177
+ @retry(
178
+ retry=retry_if_exception_type(requests.RequestException),
179
+ stop=stop_after_attempt(3),
180
+ wait=wait_fixed(3),
181
+ )
182
+ async def _post_request(
183
+ self,
184
+ endpoint: str,
185
+ json_body: dict[str, object] | None = None,
186
+ skip_entity: bool = False,
187
+ ) -> requests.Response:
188
+ """Execute one authenticated GLPI ``POST`` request asynchronously.
189
+
190
+ JSON request bodies automatically include the content-type header needed
191
+ by the GLPI API.
192
+ """
193
+
194
+ return await self._execute_request(
195
+ method="post",
196
+ endpoint=endpoint,
197
+ success_statuses=(200, 201),
198
+ json_body=json_body,
199
+ skip_entity=skip_entity,
200
+ include_content_type=True,
201
+ )
202
+
203
+ @retry(
204
+ retry=retry_if_exception_type(requests.RequestException),
205
+ stop=stop_after_attempt(3),
206
+ wait=wait_fixed(3),
207
+ )
208
+ async def _update_request(
209
+ self,
210
+ endpoint: str,
211
+ json_body: dict[str, object] | None = None,
212
+ ) -> requests.Response:
213
+ """Execute one authenticated GLPI ``PATCH`` request asynchronously.
214
+
215
+ The helper uses the same authenticated execution path as the other HTTP
216
+ verbs while targeting the success codes expected from update calls.
217
+ """
218
+
219
+ return await self._execute_request(
220
+ method="patch",
221
+ endpoint=endpoint,
222
+ success_statuses=(200, 204),
223
+ json_body=json_body,
224
+ include_content_type=True,
225
+ )
226
+
227
+ @retry(
228
+ retry=retry_if_exception_type(requests.RequestException),
229
+ stop=stop_after_attempt(3),
230
+ wait=wait_fixed(3),
231
+ )
232
+ async def _delete_request(
233
+ self,
234
+ endpoint: str,
235
+ json_body: dict[str, object] | None = None,
236
+ skip_entity: bool = False,
237
+ ) -> requests.Response:
238
+ """Execute one authenticated GLPI ``DELETE`` request asynchronously.
239
+
240
+ Some delete endpoints accept a JSON body, so the content-type header is
241
+ enabled automatically when a body is supplied.
242
+ """
243
+
244
+ return await self._execute_request(
245
+ method="delete",
246
+ endpoint=endpoint,
247
+ success_statuses=(200, 204),
248
+ json_body=json_body,
249
+ skip_entity=skip_entity,
250
+ include_content_type=json_body is not None,
251
+ )
@@ -0,0 +1,6 @@
1
+ """Shared helper modules for the GLPI v2 clients.
2
+
3
+ This package holds deterministic logic such as endpoint constants, request
4
+ normalization, payload building, and response extraction that is reused by
5
+ both the synchronous and asynchronous client implementations.
6
+ """