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,303 @@
1
+ """Typed GLPI ticket model.
2
+
3
+ The ticket model is the richest object in the package and can hold both the
4
+ core ticket fields and related timeline, document, and team data.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Mapping, Sequence
10
+ from datetime import datetime
11
+ from typing import Any
12
+
13
+ from glpi_python_client.content.conversion import GlpiContentConverter
14
+ from glpi_python_client.models._base import GlpiModel
15
+ from glpi_python_client.models._payload import (
16
+ ApiPayloadMixin,
17
+ drop_empty_payload_values,
18
+ )
19
+ from glpi_python_client.models._shared import _model_data
20
+ from glpi_python_client.models.glpi._document import GlpiDocument
21
+ from glpi_python_client.models.glpi._followup import GlpiFollowup
22
+ from glpi_python_client.models.glpi._solution import GlpiSolution
23
+ from glpi_python_client.models.glpi._task import GlpiTask
24
+ from glpi_python_client.models.glpi._team_member import GlpiTeamMember
25
+ from glpi_python_client.models.glpi._user import GlpiUser
26
+
27
+
28
+ def _glpi_user_from_value(value: object | None) -> GlpiUser | None:
29
+ if value is None:
30
+ return None
31
+ if isinstance(value, GlpiUser):
32
+ return value
33
+ return GlpiUser.model_validate(_model_data(value))
34
+
35
+
36
+ def _datetime_from_data(value: object) -> datetime | None:
37
+ return value if isinstance(value, datetime) else None
38
+
39
+
40
+ def _glpi_followup_from_value(value: object) -> GlpiFollowup:
41
+ if isinstance(value, GlpiFollowup):
42
+ return value
43
+ data = _model_data(value)
44
+ author = _glpi_user_from_value(data.get("author"))
45
+ author_id = data.get("author_id")
46
+ if author is None and author_id is not None:
47
+ author = GlpiUser(user_id=str(author_id))
48
+ return GlpiFollowup(
49
+ followup_id=_optional_text(data.get("followup_id")),
50
+ content=GlpiContentConverter.from_transport(data.get("content")),
51
+ created_at=_datetime_from_data(data.get("created_at")),
52
+ updated_at=_datetime_from_data(data.get("updated_at")),
53
+ author=author,
54
+ editor=_glpi_user_from_value(data.get("editor")),
55
+ is_private=bool(data.get("is_private", False)),
56
+ attachment_document_ids=tuple(
57
+ str(document_id) for document_id in data.get("attachment_document_ids", ())
58
+ ),
59
+ )
60
+
61
+
62
+ def _glpi_solution_from_value(value: object) -> GlpiSolution:
63
+ if isinstance(value, GlpiSolution):
64
+ return value
65
+ data = _model_data(value)
66
+ author = _glpi_user_from_value(data.get("author"))
67
+ author_id = data.get("author_id")
68
+ if author is None and author_id is not None:
69
+ author = GlpiUser(user_id=str(author_id))
70
+ return GlpiSolution(
71
+ solution_id=_optional_text(data.get("solution_id")),
72
+ content=GlpiContentConverter.from_transport(data.get("content")),
73
+ created_at=_datetime_from_data(data.get("created_at")),
74
+ updated_at=_datetime_from_data(data.get("updated_at")),
75
+ author=author,
76
+ attachment_document_ids=tuple(
77
+ str(document_id) for document_id in data.get("attachment_document_ids", ())
78
+ ),
79
+ )
80
+
81
+
82
+ def _glpi_task_from_value(value: object) -> GlpiTask:
83
+ if isinstance(value, GlpiTask):
84
+ return value
85
+ data = _model_data(value)
86
+ author = _glpi_user_from_value(data.get("author"))
87
+ author_id = data.get("author_id")
88
+ if author is None and author_id is not None:
89
+ author = GlpiUser(user_id=str(author_id))
90
+ editor = _glpi_user_from_value(data.get("editor"))
91
+ editor_id = data.get("editor_id")
92
+ if editor is None and editor_id is not None:
93
+ editor = GlpiUser(user_id=str(editor_id))
94
+ return GlpiTask(
95
+ task_id=_optional_text(data.get("task_id")),
96
+ content=GlpiContentConverter.from_transport(data.get("content")),
97
+ created_at=_datetime_from_data(data.get("created_at")),
98
+ updated_at=_datetime_from_data(data.get("updated_at")),
99
+ author=author,
100
+ editor=editor,
101
+ is_private=bool(data.get("is_private", False)),
102
+ )
103
+
104
+
105
+ def _glpi_document_from_value(value: object) -> GlpiDocument:
106
+ if isinstance(value, GlpiDocument):
107
+ return value
108
+ return GlpiDocument.model_validate(_model_data(value))
109
+
110
+
111
+ def _glpi_team_member_from_value(value: object) -> GlpiTeamMember:
112
+ if isinstance(value, GlpiTeamMember):
113
+ return value
114
+ return GlpiTeamMember.model_validate(_model_data(value))
115
+
116
+
117
+ def _normalize_glpi_ticket_updates(updates: Mapping[str, Any]) -> dict[str, Any]:
118
+ normalized: dict[str, Any] = {}
119
+ for key, value in updates.items():
120
+ if key in {"user_recipient", "user_editor"}:
121
+ normalized[key] = _glpi_user_from_value(value)
122
+ elif key == "followups":
123
+ normalized[key] = tuple(_glpi_followup_from_value(item) for item in value)
124
+ elif key == "tasks":
125
+ normalized[key] = tuple(_glpi_task_from_value(item) for item in value)
126
+ elif key == "solutions":
127
+ normalized[key] = tuple(_glpi_solution_from_value(item) for item in value)
128
+ elif key == "documents":
129
+ normalized[key] = tuple(_glpi_document_from_value(item) for item in value)
130
+ elif key == "team":
131
+ normalized[key] = tuple(
132
+ _glpi_team_member_from_value(item) for item in value
133
+ )
134
+ else:
135
+ normalized[key] = value
136
+ return normalized
137
+
138
+
139
+ class GlpiTicket(ApiPayloadMixin, GlpiModel):
140
+ """GLPI ticket record.
141
+
142
+ Textual ``content`` is stored as canonical Markdown in Python. It is rendered
143
+ to GLPI HTML only when :meth:`to_api_payload` builds an outgoing payload.
144
+ """
145
+
146
+ id: str | None = None
147
+ name: str | None = None
148
+ content: str | None = None
149
+ status: int | dict[str, object] | None = None
150
+ urgency: int | None = None
151
+ impact: int | None = None
152
+ priority: int | None = None
153
+ type: int | None = None
154
+ external_id: str | None = None
155
+ category: int | dict[str, object] | None = None
156
+ entity: int | dict[str, object] | None = None
157
+ location: str | int | dict[str, object] | None = None
158
+ request_type: str | dict[str, object] | None = None
159
+ date_creation: datetime | None = None
160
+ date_mod: datetime | None = None
161
+ date_close: datetime | None = None
162
+ user_recipient: GlpiUser | None = None
163
+ user_editor: GlpiUser | None = None
164
+ followups: tuple[GlpiFollowup, ...] = ()
165
+ tasks: tuple[GlpiTask, ...] = ()
166
+ solutions: tuple[GlpiSolution, ...] = ()
167
+ documents: tuple[GlpiDocument, ...] = ()
168
+ team: tuple[GlpiTeamMember, ...] = ()
169
+
170
+ @classmethod
171
+ def from_record(
172
+ cls, record: GlpiTicket | Mapping[str, Any], **updates: Any
173
+ ) -> GlpiTicket:
174
+ """Build a ticket instance from an existing record and override values.
175
+
176
+ The helper accepts either an existing ``GlpiTicket`` or a normalized
177
+ mapping and then applies any requested updates through the same model
178
+ validation path as direct construction.
179
+ """
180
+
181
+ if isinstance(record, cls):
182
+ base_data = record.model_dump(mode="python")
183
+ else:
184
+ base_data = dict(record)
185
+ if updates:
186
+ base_data.update(_normalize_glpi_ticket_updates(updates))
187
+ return cls.model_validate(base_data)
188
+
189
+ def updated_at(self) -> datetime:
190
+ """Return the best available update timestamp for the ticket.
191
+
192
+ When GLPI does not provide ``date_mod``, the current local timestamp is
193
+ used as a fallback so caller code always receives a ``datetime`` value.
194
+ """
195
+
196
+ return self.date_mod or datetime.now().astimezone()
197
+
198
+ def _build_api_payload(
199
+ self,
200
+ *,
201
+ entity_id: int | None = None,
202
+ include_entity: bool = False,
203
+ field_mask: tuple[str, ...] = (),
204
+ category_defaults: Mapping[str, int] | Sequence[int] | None = None,
205
+ default_status: int | None = None,
206
+ default_priority: int | None = None,
207
+ default_type: int | None = None,
208
+ ) -> dict[str, object]:
209
+ """Build the raw GLPI API request body for the ticket.
210
+
211
+ The payload builder applies the package's defaulting rules for entity,
212
+ status, priority, type, and category fields before optionally trimming
213
+ the result with a field mask.
214
+ """
215
+
216
+ status = (
217
+ _glpi_field_id(self.status)
218
+ if self.status is not None
219
+ else (default_status if include_entity else None)
220
+ )
221
+ priority = (
222
+ self.priority
223
+ if self.priority is not None
224
+ else (default_priority if include_entity else None)
225
+ )
226
+ glpi_type = (
227
+ self.type
228
+ if self.type is not None
229
+ else (default_type if include_entity else None)
230
+ )
231
+ category = _glpi_field_id(self.category) or _category_from_defaults(
232
+ category_defaults, _glpi_field_name(self.category)
233
+ )
234
+ payload = drop_empty_payload_values(
235
+ {
236
+ "name": self.name,
237
+ "content": GlpiContentConverter.to_transport(self.content)
238
+ if self.content is not None
239
+ else None,
240
+ "status": _id_object(status),
241
+ "urgency": self.urgency,
242
+ "impact": self.impact,
243
+ "priority": priority,
244
+ "type": glpi_type,
245
+ "external_id": self.external_id,
246
+ "category": _id_object(category),
247
+ "location": _id_object(_glpi_field_id(self.location)),
248
+ "entity": _id_object(
249
+ entity_id if entity_id is not None else _glpi_field_id(self.entity)
250
+ )
251
+ if include_entity
252
+ else None,
253
+ }
254
+ )
255
+ if not field_mask:
256
+ return payload
257
+ return {key: value for key, value in payload.items() if key in field_mask}
258
+
259
+
260
+ def _optional_text(value: object) -> str | None:
261
+ text = str(value).strip() if value is not None else ""
262
+ return text or None
263
+
264
+
265
+ def _id_object(value: object) -> dict[str, object] | None:
266
+ if value is None:
267
+ return None
268
+ return {"id": value}
269
+
270
+
271
+ def _glpi_field_id(value: object) -> object | None:
272
+ if isinstance(value, Mapping):
273
+ return value.get("id")
274
+ return value
275
+
276
+
277
+ def _glpi_field_name(value: object) -> str | None:
278
+ if not isinstance(value, Mapping):
279
+ return None
280
+ text = str(value.get("name", "")).strip()
281
+ return text or None
282
+
283
+
284
+ def _category_from_defaults(
285
+ category_defaults: Mapping[str, int] | Sequence[int] | None,
286
+ category_label: str | None,
287
+ ) -> object | None:
288
+ if not category_defaults:
289
+ return None
290
+ if isinstance(category_defaults, Mapping):
291
+ if category_label:
292
+ expected = category_label.casefold()
293
+ for key, value in category_defaults.items():
294
+ if str(key).casefold() == expected:
295
+ return value
296
+ return None
297
+ for value in category_defaults:
298
+ if value is not None:
299
+ return value
300
+ return None
301
+
302
+
303
+ __all__ = ["GlpiTicket"]
@@ -0,0 +1,92 @@
1
+ """Typed GLPI user model.
2
+
3
+ The user model is used for parsed directory records, embedded ticket users, and
4
+ outgoing user-creation payloads.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from glpi_python_client.models._base import GlpiModel
10
+ from glpi_python_client.models._payload import (
11
+ ApiPayloadMixin,
12
+ drop_empty_payload_values,
13
+ )
14
+
15
+
16
+ class GlpiUser(ApiPayloadMixin, GlpiModel):
17
+ """GLPI user rich object.
18
+
19
+ Parameters
20
+ ----------
21
+ user_id : str | None, optional
22
+ Native GLPI user identifier.
23
+ email : str | None, optional
24
+ User email address.
25
+ firstname : str | None, optional
26
+ First name.
27
+ realname : str | None, optional
28
+ Last name.
29
+ name : str | None, optional
30
+ Display name.
31
+ entity_id : int | None, optional
32
+ Owning GLPI entity.
33
+ default_is_notifications_enabled : bool | None, optional
34
+ Default GLPI user notification preference.
35
+ """
36
+
37
+ user_id: str | None = None
38
+ email: str | None = None
39
+ firstname: str | None = None
40
+ realname: str | None = None
41
+ name: str | None = None
42
+ entity_id: int | None = None
43
+ default_is_notifications_enabled: bool | None = None
44
+
45
+ def _build_api_payload(self) -> dict[str, object]:
46
+ """Return the raw GLPI user-create request body.
47
+
48
+ Returns
49
+ -------
50
+ dict[str, object]
51
+ Raw GLPI user-create request body.
52
+
53
+ Raises
54
+ ------
55
+ ValueError
56
+ Raised when neither a name nor an email is available.
57
+ """
58
+
59
+ username = _first_not_none(self.email, self.name, self.user_id)
60
+ if username is None:
61
+ raise ValueError("GLPI user creation requires at least a name or email")
62
+ firstname = self.firstname
63
+ realname = self.realname
64
+ fallback_identity = _first_not_none(self.name, self.email, username)
65
+ if firstname is None and realname is None:
66
+ firstname = fallback_identity
67
+ realname = ""
68
+ else:
69
+ firstname = firstname or ""
70
+ realname = realname or ""
71
+ payload: dict[str, object] = {
72
+ "id": self.user_id,
73
+ "username": username,
74
+ "email": self.email,
75
+ "realname": realname,
76
+ "firstname": firstname,
77
+ "name": self.name if self.name is not None else username,
78
+ }
79
+ if self.entity_id is not None:
80
+ payload["default_entity"] = {"id": self.entity_id}
81
+ if self.default_is_notifications_enabled is not None:
82
+ payload["default_is_notifications_enabled"] = (
83
+ self.default_is_notifications_enabled
84
+ )
85
+ return drop_empty_payload_values(payload)
86
+
87
+
88
+ def _first_not_none(*values: str | None) -> str | None:
89
+ for value in values:
90
+ if value is not None:
91
+ return value
92
+ return None
@@ -0,0 +1,5 @@
1
+ """Model tests colocated with the concrete GLPI model package.
2
+
3
+ Keeping these tests near the model definitions helps track validation and
4
+ serialization behavior as the typed objects evolve.
5
+ """
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from glpi_python_client import GlpiDocument
4
+
5
+
6
+ def test_document_upload_payload_defaults_mime_type() -> None:
7
+ document = GlpiDocument(ticket_id=123, filename="trace.txt", content=b"trace")
8
+
9
+ payload = document.to_api_payload()
10
+
11
+ assert payload["mime_type"] == "application/octet-stream"
12
+ assert payload["content"] == b"trace"
@@ -0,0 +1,31 @@
1
+ from __future__ import annotations
2
+
3
+ from glpi_python_client import GlpiFollowup, GlpiUser
4
+
5
+
6
+ def test_followup_model_copy_returns_updated_model() -> None:
7
+ followup = GlpiFollowup(content="hello")
8
+
9
+ updated = followup.model_copy(update={"followup_id": "42"})
10
+
11
+ assert followup.followup_id is None
12
+ assert updated.followup_id == "42"
13
+
14
+
15
+ def test_followup_payload_renders_markdown_to_html() -> None:
16
+ followup = GlpiFollowup(content="Observed **again**")
17
+
18
+ assert followup.to_api_payload()["content"] == (
19
+ "<p>Observed <strong>again</strong></p>"
20
+ )
21
+
22
+
23
+ def test_followup_payload_preserves_raw_user_ids() -> None:
24
+ followup = GlpiFollowup(
25
+ content="Observed **again**",
26
+ author=GlpiUser(user_id="0012"),
27
+ editor=GlpiUser(user_id=" user-7 "),
28
+ )
29
+
30
+ assert followup.to_api_payload()["user"] == {"id": "0012"}
31
+ assert followup.to_api_payload()["user_editor"] == {"id": " user-7 "}
@@ -0,0 +1,12 @@
1
+ from __future__ import annotations
2
+
3
+ from glpi_python_client import GlpiLocation
4
+
5
+
6
+ def test_location_payload_preserves_raw_location_id() -> None:
7
+ location = GlpiLocation(location_id="0012", name="Paris")
8
+
9
+ payload = location.to_api_payload()
10
+
11
+ assert payload["id"] == "0012"
12
+ assert payload["name"] == "Paris"
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from glpi_python_client import GlpiSolution
4
+
5
+
6
+ def test_solution_payload_renders_markdown_to_html() -> None:
7
+ solution = GlpiSolution(content="Rebooted and **validated**")
8
+
9
+ assert solution.to_api_payload()["content"] == (
10
+ "<p>Rebooted and <strong>validated</strong></p>"
11
+ )
@@ -0,0 +1,59 @@
1
+ from __future__ import annotations
2
+
3
+ from glpi_python_client import GlpiTicket
4
+
5
+
6
+ def test_ticket_payload_uses_glpi_high_level_shapes() -> None:
7
+ ticket = GlpiTicket(
8
+ name="Printer issue",
9
+ content="Cannot print from accounting",
10
+ status=2,
11
+ urgency=3,
12
+ impact=4,
13
+ priority=5,
14
+ type=1,
15
+ category=10,
16
+ location="12",
17
+ )
18
+
19
+ payload = ticket.to_api_payload(entity_id=7, include_entity=True)
20
+
21
+ assert payload == {
22
+ "name": "Printer issue",
23
+ "content": "<p>Cannot print from accounting</p>",
24
+ "status": {"id": 2},
25
+ "urgency": 3,
26
+ "impact": 4,
27
+ "priority": 5,
28
+ "type": 1,
29
+ "category": {"id": 10},
30
+ "location": {"id": "12"},
31
+ "entity": {"id": 7},
32
+ }
33
+
34
+
35
+ def test_ticket_payload_field_mask_accepts_model_field_names() -> None:
36
+ ticket = GlpiTicket(name="Updated", status=3, priority=4)
37
+
38
+ payload = ticket.to_api_payload(field_mask=("name", "status"))
39
+
40
+ assert payload == {"name": "Updated", "status": {"id": 3}}
41
+
42
+
43
+ def test_ticket_payload_merges_public_extra_payload() -> None:
44
+ ticket = GlpiTicket(
45
+ name="Access badge reader offline",
46
+ extra_payload={
47
+ "_room_code": "PAR-3F-12",
48
+ "_asset_tag": "BADGE-READER-044",
49
+ "_ignored": None,
50
+ },
51
+ )
52
+
53
+ payload = ticket.to_api_payload()
54
+
55
+ assert payload == {
56
+ "name": "Access badge reader offline",
57
+ "_room_code": "PAR-3F-12",
58
+ "_asset_tag": "BADGE-READER-044",
59
+ }
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+
3
+ from glpi_python_client import GlpiUser
4
+
5
+
6
+ def test_user_payload_uses_email_as_username() -> None:
7
+ user = GlpiUser(email="jane@example.test", firstname="Jane", realname="Doe")
8
+
9
+ payload = user.to_api_payload()
10
+
11
+ assert payload["username"] == "jane@example.test"
12
+ assert payload["name"] == "jane@example.test"
13
+
14
+
15
+ def test_user_payload_preserves_raw_input_text() -> None:
16
+ user = GlpiUser(
17
+ user_id="0012",
18
+ email=" jane@example.test ",
19
+ firstname=" Jane ",
20
+ realname=" Doe ",
21
+ )
22
+
23
+ payload = user.to_api_payload()
24
+
25
+ assert payload["id"] == "0012"
26
+ assert payload["username"] == " jane@example.test "
27
+ assert payload["firstname"] == " Jane "
28
+ assert payload["realname"] == " Doe "
29
+ assert payload["name"] == " jane@example.test "
File without changes
@@ -0,0 +1,27 @@
1
+ """Shared testing exports for the GLPI client package.
2
+
3
+ This package gathers reusable fake responses, record builders, and client
4
+ factories so unit tests can share realistic test data without repeating setup.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from glpi_python_client.testing.utils import (
10
+ FakeResponse,
11
+ SearchResponse,
12
+ TicketResponse,
13
+ TokenResponse,
14
+ make_client,
15
+ make_followup_record,
16
+ make_ticket_record,
17
+ )
18
+
19
+ __all__ = [
20
+ "FakeResponse",
21
+ "SearchResponse",
22
+ "TicketResponse",
23
+ "TokenResponse",
24
+ "make_client",
25
+ "make_followup_record",
26
+ "make_ticket_record",
27
+ ]
@@ -0,0 +1,52 @@
1
+ """Pytest fixtures backed by shared glpi_python_client test utilities.
2
+
3
+ These fixtures provide common sample clients and payloads for unit tests while
4
+ delegating the concrete data construction to the shared testing utilities.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from collections.abc import Callable
10
+ from typing import Any
11
+
12
+ import pytest
13
+
14
+ from glpi_python_client import GlpiClient
15
+ from glpi_python_client.testing.utils import (
16
+ make_client,
17
+ make_followup_record,
18
+ make_ticket_record,
19
+ )
20
+
21
+
22
+ @pytest.fixture
23
+ def client_factory() -> Callable[..., GlpiClient]:
24
+ """Return the reusable GLPI client factory fixture.
25
+
26
+ Tests can call the returned factory with overrides to create focused client
27
+ instances without duplicating base configuration.
28
+ """
29
+
30
+ return make_client
31
+
32
+
33
+ @pytest.fixture
34
+ def sample_followup_record() -> dict[str, Any]:
35
+ """Return a representative raw GLPI followup payload fixture.
36
+
37
+ The payload mirrors the structure expected by the content parsing tests and
38
+ can be overridden in higher-level helpers when needed.
39
+ """
40
+
41
+ return make_followup_record()
42
+
43
+
44
+ @pytest.fixture
45
+ def sample_ticket_record() -> dict[str, Any]:
46
+ """Return a representative raw GLPI ticket payload fixture.
47
+
48
+ The payload captures the common ticket fields exercised by the parsing and
49
+ client tests.
50
+ """
51
+
52
+ return make_ticket_record()