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.
- glpi_python_client/__init__.py +36 -0
- glpi_python_client/auth/__init__.py +11 -0
- glpi_python_client/auth/auth.py +310 -0
- glpi_python_client/auth/tests/test_auth.py +189 -0
- glpi_python_client/clients/__init__.py +18 -0
- glpi_python_client/clients/api_v1_session.py +460 -0
- glpi_python_client/clients/api_v2_client.py +317 -0
- glpi_python_client/clients/async_api_v2_client.py +236 -0
- glpi_python_client/clients/tests/__init__.py +5 -0
- glpi_python_client/clients/tests/test_api_v1_session.py +85 -0
- glpi_python_client/clients/tests/test_api_v2_client.py +349 -0
- glpi_python_client/clients/tests/test_async_api_v2_client.py +257 -0
- glpi_python_client/clients/v2/__init__.py +8 -0
- glpi_python_client/clients/v2/async_/__init__.py +12 -0
- glpi_python_client/clients/v2/async_/api.py +29 -0
- glpi_python_client/clients/v2/async_/directory.py +88 -0
- glpi_python_client/clients/v2/async_/documents.py +144 -0
- glpi_python_client/clients/v2/async_/team.py +125 -0
- glpi_python_client/clients/v2/async_/tests/__init__.py +5 -0
- glpi_python_client/clients/v2/async_/tests/test_directory.py +43 -0
- glpi_python_client/clients/v2/async_/tests/test_documents.py +41 -0
- glpi_python_client/clients/v2/async_/tests/test_team.py +44 -0
- glpi_python_client/clients/v2/async_/tests/test_tickets.py +174 -0
- glpi_python_client/clients/v2/async_/tests/test_timeline.py +126 -0
- glpi_python_client/clients/v2/async_/tickets.py +312 -0
- glpi_python_client/clients/v2/async_/timeline.py +312 -0
- glpi_python_client/clients/v2/async_/transport.py +251 -0
- glpi_python_client/clients/v2/common/__init__.py +6 -0
- glpi_python_client/clients/v2/common/client_config.py +219 -0
- glpi_python_client/clients/v2/common/constants.py +45 -0
- glpi_python_client/clients/v2/common/errors.py +23 -0
- glpi_python_client/clients/v2/common/filters.py +30 -0
- glpi_python_client/clients/v2/common/payloads.py +57 -0
- glpi_python_client/clients/v2/common/request_http.py +195 -0
- glpi_python_client/clients/v2/common/response_payloads.py +76 -0
- glpi_python_client/clients/v2/common/ticket_search.py +113 -0
- glpi_python_client/clients/v2/sync/__init__.py +12 -0
- glpi_python_client/clients/v2/sync/api.py +29 -0
- glpi_python_client/clients/v2/sync/directory.py +90 -0
- glpi_python_client/clients/v2/sync/documents.py +144 -0
- glpi_python_client/clients/v2/sync/team.py +125 -0
- glpi_python_client/clients/v2/sync/tests/__init__.py +5 -0
- glpi_python_client/clients/v2/sync/tests/test_directory.py +57 -0
- glpi_python_client/clients/v2/sync/tests/test_documents.py +99 -0
- glpi_python_client/clients/v2/sync/tests/test_team.py +64 -0
- glpi_python_client/clients/v2/sync/tests/test_tickets.py +430 -0
- glpi_python_client/clients/v2/sync/tests/test_timeline.py +77 -0
- glpi_python_client/clients/v2/sync/tests/test_transport.py +89 -0
- glpi_python_client/clients/v2/sync/tickets.py +312 -0
- glpi_python_client/clients/v2/sync/timeline.py +308 -0
- glpi_python_client/clients/v2/sync/transport.py +246 -0
- glpi_python_client/content/__init__.py +11 -0
- glpi_python_client/content/conversion.py +58 -0
- glpi_python_client/content/records/__init__.py +84 -0
- glpi_python_client/content/records/core/__init__.py +6 -0
- glpi_python_client/content/records/core/document_links.py +100 -0
- glpi_python_client/content/records/core/normalization.py +53 -0
- glpi_python_client/content/records/core/references.py +98 -0
- glpi_python_client/content/records/core/scalars.py +83 -0
- glpi_python_client/content/records/parsers/__init__.py +6 -0
- glpi_python_client/content/records/parsers/directory.py +62 -0
- glpi_python_client/content/records/parsers/documents.py +49 -0
- glpi_python_client/content/records/parsers/team.py +58 -0
- glpi_python_client/content/records/parsers/tests/__init__.py +5 -0
- glpi_python_client/content/records/parsers/tests/test_tickets.py +35 -0
- glpi_python_client/content/records/parsers/tests/test_timeline.py +20 -0
- glpi_python_client/content/records/parsers/tickets.py +96 -0
- glpi_python_client/content/records/parsers/timeline.py +119 -0
- glpi_python_client/content/tests/__init__.py +5 -0
- glpi_python_client/content/tests/test_conversion.py +13 -0
- glpi_python_client/models/__init__.py +30 -0
- glpi_python_client/models/_base.py +22 -0
- glpi_python_client/models/_payload.py +79 -0
- glpi_python_client/models/_shared.py +37 -0
- glpi_python_client/models/glpi/__init__.py +27 -0
- glpi_python_client/models/glpi/_document.py +59 -0
- glpi_python_client/models/glpi/_followup.py +77 -0
- glpi_python_client/models/glpi/_location.py +53 -0
- glpi_python_client/models/glpi/_solution.py +57 -0
- glpi_python_client/models/glpi/_task.py +41 -0
- glpi_python_client/models/glpi/_team_member.py +33 -0
- glpi_python_client/models/glpi/_ticket.py +303 -0
- glpi_python_client/models/glpi/_user.py +92 -0
- glpi_python_client/models/glpi/tests/__init__.py +5 -0
- glpi_python_client/models/glpi/tests/test__document.py +12 -0
- glpi_python_client/models/glpi/tests/test__followup.py +31 -0
- glpi_python_client/models/glpi/tests/test__location.py +12 -0
- glpi_python_client/models/glpi/tests/test__solution.py +11 -0
- glpi_python_client/models/glpi/tests/test__ticket.py +59 -0
- glpi_python_client/models/glpi/tests/test__user.py +29 -0
- glpi_python_client/py.typed +0 -0
- glpi_python_client/testing/__init__.py +27 -0
- glpi_python_client/testing/fixtures.py +52 -0
- glpi_python_client/testing/utils.py +149 -0
- glpi_python_client-0.1.0.dist-info/METADATA +144 -0
- glpi_python_client-0.1.0.dist-info/RECORD +98 -0
- glpi_python_client-0.1.0.dist-info/WHEEL +4 -0
- 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,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()
|