glpi-python-client 0.4.3__py3-none-any.whl → 0.6.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 (54) hide show
  1. glpi_python_client/__init__.py +45 -1
  2. glpi_python_client/_async/auth/_v1_session.py +3 -3
  3. glpi_python_client/_async/auth/auth.py +3 -3
  4. glpi_python_client/_async/clients/_base_client.py +1 -7
  5. glpi_python_client/_async/clients/api/__init__.py +13 -3
  6. glpi_python_client/_async/clients/api/assets/__init__.py +7 -0
  7. glpi_python_client/_async/clients/api/assets/_computer.py +427 -0
  8. glpi_python_client/_async/clients/api/dropdowns/__init__.py +4 -1
  9. glpi_python_client/_async/clients/api/dropdowns/_contract_type.py +220 -0
  10. glpi_python_client/_async/clients/api/management/__init__.py +2 -1
  11. glpi_python_client/_async/clients/api/management/_contract.py +411 -0
  12. glpi_python_client/_async/clients/client.py +7 -6
  13. glpi_python_client/_async/clients/commons/_constants.py +9 -1
  14. glpi_python_client/_async/clients/commons/_payloads.py +8 -6
  15. glpi_python_client/_errors.py +36 -4
  16. glpi_python_client/_sync/auth/_v1_session.py +3 -3
  17. glpi_python_client/_sync/auth/auth.py +3 -3
  18. glpi_python_client/_sync/clients/_base_client.py +1 -7
  19. glpi_python_client/_sync/clients/api/__init__.py +13 -3
  20. glpi_python_client/_sync/clients/api/assets/__init__.py +7 -0
  21. glpi_python_client/_sync/clients/api/assets/_computer.py +427 -0
  22. glpi_python_client/_sync/clients/api/dropdowns/__init__.py +4 -1
  23. glpi_python_client/_sync/clients/api/dropdowns/_contract_type.py +220 -0
  24. glpi_python_client/_sync/clients/api/management/__init__.py +2 -1
  25. glpi_python_client/_sync/clients/api/management/_contract.py +411 -0
  26. glpi_python_client/_sync/clients/client.py +7 -6
  27. glpi_python_client/_sync/clients/commons/_constants.py +9 -1
  28. glpi_python_client/_sync/clients/commons/_payloads.py +8 -6
  29. glpi_python_client/content/conversion.py +622 -77
  30. glpi_python_client/models/__init__.py +48 -3
  31. glpi_python_client/models/_base.py +135 -6
  32. glpi_python_client/models/api_schema/__init__.py +8 -1
  33. glpi_python_client/models/api_schema/_content.py +236 -28
  34. glpi_python_client/models/api_schema/assets/__init__.py +25 -0
  35. glpi_python_client/models/api_schema/assets/_computer.py +257 -0
  36. glpi_python_client/models/api_schema/assets/_contract_item.py +102 -0
  37. glpi_python_client/models/api_schema/assistance/_ticket.py +30 -9
  38. glpi_python_client/models/api_schema/assistance/timeline/_followup.py +29 -8
  39. glpi_python_client/models/api_schema/assistance/timeline/_solution.py +29 -8
  40. glpi_python_client/models/api_schema/assistance/timeline/_task.py +29 -8
  41. glpi_python_client/models/api_schema/dropdowns/__init__.py +10 -0
  42. glpi_python_client/models/api_schema/dropdowns/_contract_type.py +100 -0
  43. glpi_python_client/models/api_schema/enums.py +14 -0
  44. glpi_python_client/models/api_schema/knowledgebase/_article.py +49 -7
  45. glpi_python_client/models/api_schema/knowledgebase/_revision.py +28 -5
  46. glpi_python_client/models/api_schema/management/__init__.py +20 -0
  47. glpi_python_client/models/api_schema/management/_contract.py +328 -0
  48. glpi_python_client/models/api_schema/management/_contract_cost.py +139 -0
  49. glpi_python_client/models/custom_schema/_ticket_context.py +29 -6
  50. glpi_python_client/rsql.py +74 -14
  51. {glpi_python_client-0.4.3.dist-info → glpi_python_client-0.6.0.dist-info}/METADATA +14 -11
  52. {glpi_python_client-0.4.3.dist-info → glpi_python_client-0.6.0.dist-info}/RECORD +54 -40
  53. {glpi_python_client-0.4.3.dist-info → glpi_python_client-0.6.0.dist-info}/WHEEL +1 -1
  54. {glpi_python_client-0.4.3.dist-info → glpi_python_client-0.6.0.dist-info}/licenses/LICENSE +0 -0
@@ -20,6 +20,7 @@ from __future__ import annotations
20
20
  from glpi_python_client._async.clients import AsyncGlpiClient
21
21
  from glpi_python_client._errors import (
22
22
  GlpiAuthError,
23
+ GlpiContentError,
23
24
  GlpiError,
24
25
  GlpiNotFoundError,
25
26
  GlpiProtocolError,
@@ -31,6 +32,11 @@ from glpi_python_client._errors import (
31
32
  )
32
33
  from glpi_python_client._sync.clients import GlpiClient
33
34
  from glpi_python_client.models import (
35
+ DeleteComputer,
36
+ DeleteContract,
37
+ DeleteContractCost,
38
+ DeleteContractItem,
39
+ DeleteContractType,
34
40
  DeleteDocument,
35
41
  DeleteEntity,
36
42
  DeleteFollowup,
@@ -44,6 +50,11 @@ from glpi_python_client.models import (
44
50
  DeleteTicketTask,
45
51
  DeleteTimelineDocument,
46
52
  DeleteUser,
53
+ GetComputer,
54
+ GetContract,
55
+ GetContractCost,
56
+ GetContractItem,
57
+ GetContractType,
47
58
  GetDocument,
48
59
  GetEntity,
49
60
  GetFollowup,
@@ -61,6 +72,7 @@ from glpi_python_client.models import (
61
72
  GetTicketTask,
62
73
  GetTimelineDocument,
63
74
  GetUser,
75
+ GlpiContractRenewalType,
64
76
  GlpiEnum,
65
77
  GlpiGlobalValidation,
66
78
  GlpiPriority,
@@ -74,6 +86,11 @@ from glpi_python_client.models import (
74
86
  IdNameCompletenameRef,
75
87
  IdNameRef,
76
88
  IdRef,
89
+ PatchComputer,
90
+ PatchContract,
91
+ PatchContractCost,
92
+ PatchContractItem,
93
+ PatchContractType,
77
94
  PatchDocument,
78
95
  PatchEntity,
79
96
  PatchFollowup,
@@ -87,6 +104,11 @@ from glpi_python_client.models import (
87
104
  PatchTicketTask,
88
105
  PatchTimelineDocument,
89
106
  PatchUser,
107
+ PostComputer,
108
+ PostContract,
109
+ PostContractCost,
110
+ PostContractItem,
111
+ PostContractType,
90
112
  PostDocument,
91
113
  PostEntity,
92
114
  PostFollowup,
@@ -109,10 +131,15 @@ from glpi_python_client.rsql import (
109
131
  date_window,
110
132
  )
111
133
 
112
- __version__ = "0.4.3"
134
+ __version__ = "0.6.0"
113
135
 
114
136
  __all__ = [
115
137
  "AsyncGlpiClient",
138
+ "DeleteComputer",
139
+ "DeleteContract",
140
+ "DeleteContractCost",
141
+ "DeleteContractItem",
142
+ "DeleteContractType",
116
143
  "DeleteDocument",
117
144
  "DeleteEntity",
118
145
  "DeleteFollowup",
@@ -126,6 +153,11 @@ __all__ = [
126
153
  "DeleteTicketTask",
127
154
  "DeleteTimelineDocument",
128
155
  "DeleteUser",
156
+ "GetComputer",
157
+ "GetContract",
158
+ "GetContractCost",
159
+ "GetContractItem",
160
+ "GetContractType",
129
161
  "GetDocument",
130
162
  "GetEntity",
131
163
  "GetFollowup",
@@ -145,6 +177,8 @@ __all__ = [
145
177
  "GetUser",
146
178
  "GlpiAuthError",
147
179
  "GlpiClient",
180
+ "GlpiContentError",
181
+ "GlpiContractRenewalType",
148
182
  "GlpiEnum",
149
183
  "GlpiError",
150
184
  "GlpiGlobalValidation",
@@ -166,6 +200,11 @@ __all__ = [
166
200
  "IdNameCompletenameRef",
167
201
  "IdNameRef",
168
202
  "IdRef",
203
+ "PatchComputer",
204
+ "PatchContract",
205
+ "PatchContractCost",
206
+ "PatchContractItem",
207
+ "PatchContractType",
169
208
  "PatchDocument",
170
209
  "PatchEntity",
171
210
  "PatchFollowup",
@@ -179,6 +218,11 @@ __all__ = [
179
218
  "PatchTicketTask",
180
219
  "PatchTimelineDocument",
181
220
  "PatchUser",
221
+ "PostComputer",
222
+ "PostContract",
223
+ "PostContractCost",
224
+ "PostContractItem",
225
+ "PostContractType",
182
226
  "PostDocument",
183
227
  "PostEntity",
184
228
  "PostFollowup",
@@ -37,7 +37,7 @@ from __future__ import annotations
37
37
 
38
38
  import json
39
39
  import logging
40
- from datetime import datetime, timedelta, timezone
40
+ from datetime import UTC, datetime, timedelta
41
41
  from typing import Any, cast
42
42
 
43
43
  import httpx
@@ -170,7 +170,7 @@ class GLPIV1Session:
170
170
  raise GlpiProtocolError("GLPI v1 initSession returned no session_token")
171
171
 
172
172
  self._session_token = str(token)
173
- self._session_started_at = datetime.now(tz=timezone.utc)
173
+ self._session_started_at = datetime.now(tz=UTC)
174
174
  logger.info("GLPI v1 session initialised.")
175
175
 
176
176
  async def _ensure_session(self) -> None:
@@ -196,7 +196,7 @@ class GLPIV1Session:
196
196
 
197
197
  if self._session_started_at is None:
198
198
  return True
199
- return datetime.now(tz=timezone.utc) >= (
199
+ return datetime.now(tz=UTC) >= (
200
200
  self._session_started_at + self._session_refresh_interval
201
201
  )
202
202
 
@@ -8,7 +8,7 @@ share the same authenticated session state.
8
8
  from __future__ import annotations
9
9
 
10
10
  import logging
11
- from datetime import datetime, timedelta, timezone
11
+ from datetime import UTC, datetime, timedelta
12
12
 
13
13
  import httpx
14
14
  from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
@@ -241,7 +241,7 @@ class GLPITokenManager:
241
241
  if refresh_token:
242
242
  self.refresh_token = refresh_token
243
243
  expires_in = int(str(token_data.get("expires_in") or 3600))
244
- now = datetime.now(tz=timezone.utc)
244
+ now = datetime.now(tz=UTC)
245
245
  self.token_updated_at = now
246
246
  self.token_expires_at = now + timedelta(seconds=expires_in)
247
247
  logger.info("GLPI OAuth token %s successfully.", label)
@@ -399,7 +399,7 @@ class GLPITokenManager:
399
399
  await self._acquire_token()
400
400
  return
401
401
 
402
- now = datetime.now(tz=timezone.utc)
402
+ now = datetime.now(tz=UTC)
403
403
  token_expired = (
404
404
  self.token_expires_at is not None and now >= self.token_expires_at
405
405
  )
@@ -13,13 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import logging
15
15
  import os
16
- import sys
17
- from typing import TYPE_CHECKING
18
-
19
- if sys.version_info >= (3, 11):
20
- from typing import Self
21
- else: # pragma: no cover - fallback for Python 3.10
22
- from typing_extensions import Self
16
+ from typing import TYPE_CHECKING, Self
23
17
 
24
18
  from glpi_python_client._async._concurrency import Lock
25
19
  from glpi_python_client._async.clients.commons._config import (
@@ -1,7 +1,7 @@
1
1
  """Per-endpoint API mixins backed by the ``api_schema`` Pydantic models.
2
2
 
3
3
  The mixins under this package mirror the endpoints documented in
4
- ``docs/glpi_api_contract.json`` one for one. They wrap the
4
+ ``docs/api_contract/api.json`` one for one. They wrap the
5
5
  transport helpers from :mod:`glpi_python_client._async.clients.commons` and exchange
6
6
  typed ``Get<Name>``, ``Post<Name>``, ``Patch<Name>``, and ``Delete<Name>``
7
7
  models with the GLPI API.
@@ -13,6 +13,7 @@ from glpi_python_client._async.clients.api.administration import (
13
13
  EntityMixin,
14
14
  UserMixin,
15
15
  )
16
+ from glpi_python_client._async.clients.api.assets import ComputerMixin
16
17
  from glpi_python_client._async.clients.api.assistance import (
17
18
  TeamMemberMixin,
18
19
  TicketMixin,
@@ -23,19 +24,28 @@ from glpi_python_client._async.clients.api.assistance.timeline import (
23
24
  TicketTaskMixin,
24
25
  TimelineDocumentMixin,
25
26
  )
26
- from glpi_python_client._async.clients.api.dropdowns import LocationMixin
27
+ from glpi_python_client._async.clients.api.dropdowns import (
28
+ ContractTypeMixin,
29
+ LocationMixin,
30
+ )
27
31
  from glpi_python_client._async.clients.api.knowledgebase import (
28
32
  KBArticleCommentMixin,
29
33
  KBArticleMixin,
30
34
  KBArticleRevisionMixin,
31
35
  KBCategoryMixin,
32
36
  )
33
- from glpi_python_client._async.clients.api.management import DocumentMixin
37
+ from glpi_python_client._async.clients.api.management import (
38
+ ContractMixin,
39
+ DocumentMixin,
40
+ )
34
41
  from glpi_python_client._async.clients.api.plugins import (
35
42
  PluginFieldsMixin,
36
43
  )
37
44
 
38
45
  __all__ = [
46
+ "ComputerMixin",
47
+ "ContractMixin",
48
+ "ContractTypeMixin",
39
49
  "DocumentMixin",
40
50
  "EntityMixin",
41
51
  "FollowupMixin",
@@ -0,0 +1,7 @@
1
+ """GLPI ``/Assets`` mixins for the GLPI client."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from glpi_python_client._async.clients.api.assets._computer import ComputerMixin
6
+
7
+ __all__ = ["ComputerMixin"]
@@ -0,0 +1,427 @@
1
+ """GLPI ``/Assets/Computer`` mixin.
2
+
3
+ The mixin exposes search, fetch, create, update, and delete helpers for the
4
+ GLPI computer resource using the contract-aligned ``api_schema`` models. It
5
+ also exposes CRUD helpers for the ``/Assets/Computer/{id}/Contract``
6
+ sub-resource, the join answering which contracts cover a given computer.
7
+
8
+ ``link_computer_contract`` and ``update_computer_contract`` set ``itemtype``
9
+ and ``items_id`` themselves rather than trusting the caller-supplied values
10
+ on the request body; see ``models/api_schema/assets/_contract_item.py`` for
11
+ why.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ from collections.abc import AsyncIterator
17
+
18
+ from glpi_python_client._async.clients.commons._constants import (
19
+ COMPUTER_ENDPOINT,
20
+ GlpiId,
21
+ )
22
+ from glpi_python_client._async.clients.commons._transport import TransportMixin
23
+ from glpi_python_client.models.api_schema.assets._computer import (
24
+ DeleteComputer,
25
+ GetComputer,
26
+ PatchComputer,
27
+ PostComputer,
28
+ )
29
+ from glpi_python_client.models.api_schema.assets._contract_item import (
30
+ DeleteContractItem,
31
+ GetContractItem,
32
+ PatchContractItem,
33
+ PostContractItem,
34
+ )
35
+
36
+
37
+ class ComputerMixin(TransportMixin):
38
+ """CRUD helpers for ``/Assets/Computer``."""
39
+
40
+ async def search_computers(
41
+ self,
42
+ rsql_filter: str = "",
43
+ *,
44
+ limit: int = 50,
45
+ start: int = 0,
46
+ sort: str | None = None,
47
+ ) -> list[GetComputer]:
48
+ """Search GLPI computers with an optional RSQL filter.
49
+
50
+ Parameters
51
+ ----------
52
+ rsql_filter : str, optional
53
+ Raw RSQL filter forwarded as the ``filter`` query parameter.
54
+ limit : int, optional
55
+ Maximum number of records returned by the GLPI server.
56
+ start : int, optional
57
+ Zero-based offset of the first record returned.
58
+ sort : str | None, optional
59
+ Server-side ordering expressed as ``"<field>:<direction>"``,
60
+ for example ``"date_mod:desc"``. Omitted when :data:`None`,
61
+ leaving the server default ordering in place.
62
+
63
+ Returns
64
+ -------
65
+ list[GetComputer]
66
+ Computers matching the filter.
67
+ """
68
+
69
+ params: dict[str, object] = {"limit": limit, "start": start}
70
+ if rsql_filter:
71
+ params["filter"] = rsql_filter
72
+ if sort is not None:
73
+ params["sort"] = sort
74
+ return await self._resource_list(COMPUTER_ENDPOINT, GetComputer, params=params)
75
+
76
+ async def iter_search_computers(
77
+ self,
78
+ rsql_filter: str = "",
79
+ *,
80
+ batch_size: int = 50,
81
+ sort: str | None = None,
82
+ ) -> AsyncIterator[list[GetComputer]]:
83
+ """Yield successive pages of GLPI computers until exhausted.
84
+
85
+ The generator drives pagination automatically by advancing the
86
+ ``start`` offset after each batch. Iteration stops when the server
87
+ returns fewer items than ``batch_size``, which signals the last page.
88
+
89
+ Parameters
90
+ ----------
91
+ rsql_filter : str, optional
92
+ Raw RSQL filter forwarded as the ``filter`` query parameter.
93
+ Empty by default, which lists every visible record.
94
+ batch_size : int, optional
95
+ Number of records requested per page (default 50). Acts as the
96
+ ``limit`` parameter on each underlying :meth:`search_computers`
97
+ call.
98
+ sort : str | None, optional
99
+ Server-side ordering expressed as ``"<field>:<direction>"``,
100
+ for example ``"date_mod:desc"``. Forwarded to each page
101
+ request; omitted when :data:`None`, leaving the server default
102
+ ordering in place.
103
+
104
+ Yields
105
+ ------
106
+ list[GetComputer]
107
+ One page per iteration. The last yielded batch may be shorter
108
+ than ``batch_size``.
109
+ """
110
+
111
+ start = 0
112
+ while True:
113
+ batch = await self.search_computers(
114
+ rsql_filter,
115
+ limit=batch_size,
116
+ start=start,
117
+ sort=sort,
118
+ )
119
+ if batch:
120
+ yield batch
121
+ if len(batch) < batch_size:
122
+ break
123
+ start += batch_size
124
+
125
+ async def get_computer(self, computer_id: GlpiId) -> GetComputer:
126
+ """Fetch one GLPI computer by identifier.
127
+
128
+ Parameters
129
+ ----------
130
+ computer_id : GlpiId
131
+ Numeric identifier of the computer to retrieve.
132
+
133
+ Returns
134
+ -------
135
+ GetComputer
136
+ Validated computer payload.
137
+
138
+ Raises
139
+ ------
140
+ GlpiStatusError
141
+ If the GLPI server returns a non-success HTTP status.
142
+ """
143
+
144
+ return await self._resource_get(
145
+ f"{COMPUTER_ENDPOINT}/{computer_id}",
146
+ GetComputer,
147
+ failure_message=f"Failed to get computer {computer_id}",
148
+ )
149
+
150
+ async def create_computer(self, computer: PostComputer) -> int:
151
+ """Create one GLPI computer.
152
+
153
+ Parameters
154
+ ----------
155
+ computer : PostComputer
156
+ Request body describing the computer to create.
157
+
158
+ Returns
159
+ -------
160
+ int
161
+ Identifier assigned by the GLPI server.
162
+
163
+ Raises
164
+ ------
165
+ GlpiStatusError
166
+ If the GLPI server returns a non-success HTTP status.
167
+ GlpiProtocolError
168
+ If the create response is missing the ``id`` field.
169
+ """
170
+
171
+ return await self._resource_create(
172
+ COMPUTER_ENDPOINT,
173
+ computer,
174
+ failure_message="Failed to create computer",
175
+ missing_message="GLPI computer create response did not include an ID",
176
+ log_message_factory=(lambda new_id: f"GLPI API created computer {new_id}"),
177
+ )
178
+
179
+ async def update_computer(
180
+ self, computer_id: GlpiId, computer: PatchComputer
181
+ ) -> None:
182
+ """Update one GLPI computer with a partial body.
183
+
184
+ Parameters
185
+ ----------
186
+ computer_id : GlpiId
187
+ Numeric identifier of the computer to update.
188
+ computer : PatchComputer
189
+ Partial request body.
190
+
191
+ Returns
192
+ -------
193
+ None
194
+
195
+ Raises
196
+ ------
197
+ GlpiStatusError
198
+ If the GLPI server returns a non-success HTTP status.
199
+ """
200
+
201
+ await self._resource_update(
202
+ f"{COMPUTER_ENDPOINT}/{computer_id}",
203
+ computer,
204
+ failure_message=f"Failed to update computer {computer_id}",
205
+ log_message=f"GLPI API updated computer {computer_id}",
206
+ )
207
+
208
+ async def delete_computer(
209
+ self, computer_id: GlpiId, *, force: bool | None = None
210
+ ) -> None:
211
+ """Delete one GLPI computer by identifier.
212
+
213
+ Parameters
214
+ ----------
215
+ computer_id : GlpiId
216
+ Numeric identifier of the computer to delete.
217
+ force : bool | None, optional
218
+ When ``True`` the computer is permanently deleted instead of
219
+ being moved to the trash.
220
+
221
+ Returns
222
+ -------
223
+ None
224
+
225
+ Raises
226
+ ------
227
+ GlpiStatusError
228
+ If the GLPI server returns a non-success HTTP status.
229
+ """
230
+
231
+ await self._resource_delete(
232
+ f"{COMPUTER_ENDPOINT}/{computer_id}",
233
+ failure_message=f"Failed to delete computer {computer_id}",
234
+ log_message=f"GLPI API deleted computer {computer_id}",
235
+ force=force,
236
+ delete_model_cls=DeleteComputer,
237
+ )
238
+
239
+ async def list_computer_contracts(
240
+ self,
241
+ computer_id: GlpiId,
242
+ *,
243
+ limit: int = 50,
244
+ start: int = 0,
245
+ ) -> list[GetContractItem]:
246
+ """List the contracts linked to one GLPI computer.
247
+
248
+ Parameters
249
+ ----------
250
+ computer_id : GlpiId
251
+ Numeric identifier of the owning computer.
252
+ limit : int, optional
253
+ Maximum number of records returned by the GLPI server.
254
+ start : int, optional
255
+ Zero-based offset of the first record returned.
256
+
257
+ Returns
258
+ -------
259
+ list[GetContractItem]
260
+ Contract links belonging to the computer.
261
+ """
262
+
263
+ params: dict[str, object] = {"limit": limit, "start": start}
264
+ return await self._resource_list(
265
+ f"{COMPUTER_ENDPOINT}/{computer_id}/Contract",
266
+ GetContractItem,
267
+ params=params,
268
+ )
269
+
270
+ async def get_computer_contract(
271
+ self, computer_id: GlpiId, link_id: GlpiId
272
+ ) -> GetContractItem:
273
+ """Fetch one contract link recorded against a GLPI computer.
274
+
275
+ Parameters
276
+ ----------
277
+ computer_id : GlpiId
278
+ Numeric identifier of the owning computer.
279
+ link_id : GlpiId
280
+ Numeric identifier of the contract link to retrieve.
281
+
282
+ Returns
283
+ -------
284
+ GetContractItem
285
+ Validated contract link payload.
286
+
287
+ Raises
288
+ ------
289
+ GlpiStatusError
290
+ If the GLPI server returns a non-success HTTP status.
291
+ """
292
+
293
+ return await self._resource_get(
294
+ f"{COMPUTER_ENDPOINT}/{computer_id}/Contract/{link_id}",
295
+ GetContractItem,
296
+ failure_message=(
297
+ f"Failed to get contract link {link_id} for computer {computer_id}"
298
+ ),
299
+ )
300
+
301
+ async def link_computer_contract(
302
+ self, computer_id: GlpiId, link: PostContractItem
303
+ ) -> int:
304
+ """Link one GLPI contract to one computer.
305
+
306
+ The ``itemtype`` and ``items_id`` fields are set from
307
+ ``computer_id`` rather than taken from ``link``. The GLPI contract
308
+ types ``itemtype`` as a free string, so a caller-supplied value is
309
+ a silent mis-link waiting to happen; this helper already knows
310
+ which asset it is working on.
311
+
312
+ Parameters
313
+ ----------
314
+ computer_id : GlpiId
315
+ Numeric identifier of the computer the contract covers.
316
+ link : PostContractItem
317
+ Request body naming the contract to link. Any ``itemtype`` or
318
+ ``items_id`` set on it is replaced.
319
+
320
+ Returns
321
+ -------
322
+ int
323
+ Identifier assigned to the new link by the GLPI server.
324
+
325
+ Raises
326
+ ------
327
+ GlpiStatusError
328
+ If the GLPI server returns a non-success HTTP status.
329
+ GlpiProtocolError
330
+ If the create response is missing the ``id`` field.
331
+ """
332
+
333
+ body = link.model_copy(update={"itemtype": "Computer", "items_id": computer_id})
334
+ return await self._resource_create(
335
+ f"{COMPUTER_ENDPOINT}/{computer_id}/Contract",
336
+ body,
337
+ failure_message=f"Failed to link a contract to computer {computer_id}",
338
+ missing_message=(
339
+ "GLPI contract link create response did not include an ID"
340
+ ),
341
+ log_message_factory=(
342
+ lambda new_id: (
343
+ f"GLPI API linked contract item {new_id} to computer {computer_id}"
344
+ )
345
+ ),
346
+ )
347
+
348
+ async def update_computer_contract(
349
+ self, computer_id: GlpiId, link_id: GlpiId, link: PatchContractItem
350
+ ) -> None:
351
+ """Update one computer-contract link with a partial body.
352
+
353
+ The ``itemtype`` and ``items_id`` fields are set from
354
+ ``computer_id`` rather than taken from ``link``, for the same
355
+ reason as :meth:`link_computer_contract`.
356
+
357
+ Parameters
358
+ ----------
359
+ computer_id : GlpiId
360
+ Numeric identifier of the owning computer.
361
+ link_id : GlpiId
362
+ Numeric identifier of the contract link to update.
363
+ link : PatchContractItem
364
+ Partial request body. Any ``itemtype`` or ``items_id`` set on
365
+ it is replaced.
366
+
367
+ Returns
368
+ -------
369
+ None
370
+
371
+ Raises
372
+ ------
373
+ GlpiStatusError
374
+ If the GLPI server returns a non-success HTTP status.
375
+ """
376
+
377
+ body = link.model_copy(update={"itemtype": "Computer", "items_id": computer_id})
378
+ await self._resource_update(
379
+ f"{COMPUTER_ENDPOINT}/{computer_id}/Contract/{link_id}",
380
+ body,
381
+ failure_message=(
382
+ f"Failed to update contract link {link_id} for computer {computer_id}"
383
+ ),
384
+ log_message=(
385
+ f"GLPI API updated contract link {link_id} for computer {computer_id}"
386
+ ),
387
+ )
388
+
389
+ async def unlink_computer_contract(
390
+ self, computer_id: GlpiId, link_id: GlpiId, *, force: bool | None = None
391
+ ) -> None:
392
+ """Remove one contract link from a GLPI computer.
393
+
394
+ Parameters
395
+ ----------
396
+ computer_id : GlpiId
397
+ Numeric identifier of the owning computer.
398
+ link_id : GlpiId
399
+ Numeric identifier of the contract link to remove.
400
+ force : bool | None, optional
401
+ When ``True`` the link is permanently deleted instead of
402
+ being moved to the trash.
403
+
404
+ Returns
405
+ -------
406
+ None
407
+
408
+ Raises
409
+ ------
410
+ GlpiStatusError
411
+ If the GLPI server returns a non-success HTTP status.
412
+ """
413
+
414
+ await self._resource_delete(
415
+ f"{COMPUTER_ENDPOINT}/{computer_id}/Contract/{link_id}",
416
+ failure_message=(
417
+ f"Failed to unlink contract link {link_id} from computer {computer_id}"
418
+ ),
419
+ log_message=(
420
+ f"GLPI API unlinked contract link {link_id} from computer {computer_id}"
421
+ ),
422
+ force=force,
423
+ delete_model_cls=DeleteContractItem,
424
+ )
425
+
426
+
427
+ __all__ = ["ComputerMixin"]
@@ -2,6 +2,9 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
+ from glpi_python_client._async.clients.api.dropdowns._contract_type import (
6
+ ContractTypeMixin,
7
+ )
5
8
  from glpi_python_client._async.clients.api.dropdowns._location import LocationMixin
6
9
 
7
- __all__ = ["LocationMixin"]
10
+ __all__ = ["ContractTypeMixin", "LocationMixin"]