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,349 @@
1
+ from __future__ import annotations
2
+
3
+ from collections.abc import Callable
4
+ from typing import Any, cast
5
+
6
+ import pytest
7
+
8
+ from glpi_python_client import GlpiClient, GlpiDocument, GlpiLocation, GlpiUser
9
+ from glpi_python_client.testing.utils import FakeResponse
10
+
11
+ _V1_BASE_URLS = (
12
+ pytest.param("https://glpi.example.test/api.php/v1", id="api.php-v1"),
13
+ pytest.param("https://glpi.example.test/apirest.php", id="apirest.php"),
14
+ )
15
+
16
+
17
+ def _empty_response() -> FakeResponse:
18
+ return FakeResponse(status_code=204, payload={}, text="", content=b"")
19
+
20
+
21
+ def test_client_rejects_legacy_base_url_keyword() -> None:
22
+ with pytest.raises(TypeError, match="base_url"):
23
+ GlpiClient(
24
+ base_url="https://glpi.example.test/api.php", # type: ignore[call-arg]
25
+ client_id="client-id",
26
+ client_secret="client-secret",
27
+ )
28
+
29
+
30
+ def test_client_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
31
+ monkeypatch.setenv("GLPI_API_URL", "https://glpi.example.test/api.php")
32
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
33
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
34
+ monkeypatch.setenv("GLPI_USERNAME", "api-user")
35
+ monkeypatch.setenv("GLPI_PASSWORD", "api-password")
36
+ monkeypatch.setenv("GLPI_ENTITY", "4")
37
+ monkeypatch.setenv("GLPI_PROFILE", "5")
38
+ monkeypatch.setenv("GLPI_ENTITY_RECURSIVE", "yes")
39
+ monkeypatch.setenv("GLPI_VERIFY_SSL", "false")
40
+ monkeypatch.setenv("GLPI_AUTH_TOKEN_REFRESH", "120")
41
+
42
+ client = GlpiClient.from_env()
43
+ try:
44
+ assert client.glpi_api_url == "https://glpi.example.test/api.php"
45
+ assert client.glpi_entity == 4
46
+ assert client.glpi_profile == 5
47
+ assert client.entity_recursive is True
48
+ assert client._session.verify is False
49
+ assert client._auth.auth_token_refresh == 120
50
+ finally:
51
+ client.close()
52
+
53
+
54
+ def test_client_from_env_none_overrides_clear_optional_env_values(
55
+ monkeypatch: pytest.MonkeyPatch,
56
+ ) -> None:
57
+ monkeypatch.setenv("GLPI_API_URL", "https://glpi.example.test/api.php")
58
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
59
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
60
+ monkeypatch.setenv("GLPI_AUTH_TOKEN_REFRESH", "120")
61
+ monkeypatch.setenv("GLPI_V1_BASE_URL", "https://glpi.example.test/apirest.php")
62
+ monkeypatch.setenv("GLPI_V1_USER_TOKEN", "user-token")
63
+
64
+ client = GlpiClient.from_env(
65
+ auth_token_refresh=None,
66
+ v1_base_url=None,
67
+ v1_user_token=None,
68
+ )
69
+ try:
70
+ assert client._auth.auth_token_refresh is None
71
+ assert client._v1 is None
72
+ finally:
73
+ client.close()
74
+
75
+
76
+ def test_client_from_env_rejects_invalid_integer_values(
77
+ monkeypatch: pytest.MonkeyPatch,
78
+ ) -> None:
79
+ monkeypatch.setenv("GLPI_API_URL", "https://glpi.example.test/api.php")
80
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
81
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
82
+ monkeypatch.setenv("GLPI_ENTITY", "abc")
83
+
84
+ with pytest.raises(ValueError):
85
+ GlpiClient.from_env()
86
+
87
+
88
+ def test_client_from_env_rejects_invalid_boolean_values(
89
+ monkeypatch: pytest.MonkeyPatch,
90
+ ) -> None:
91
+ monkeypatch.setenv("GLPI_API_URL", "https://glpi.example.test/api.php")
92
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
93
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
94
+ monkeypatch.setenv("GLPI_VERIFY_SSL", " definitely ")
95
+
96
+ with pytest.raises(ValueError, match="Invalid boolean"):
97
+ GlpiClient.from_env()
98
+
99
+
100
+ def test_client_context_manager_logs_out_and_rejects_future_calls(
101
+ client_factory: Callable[..., GlpiClient],
102
+ monkeypatch: pytest.MonkeyPatch,
103
+ ) -> None:
104
+ client = client_factory()
105
+ events: list[str] = []
106
+ original_logout = client._auth.logout
107
+ original_session_close = client._session.close
108
+
109
+ def logout() -> None:
110
+ events.append("logout")
111
+ original_logout()
112
+
113
+ def close_session() -> None:
114
+ events.append("session_close")
115
+ original_session_close()
116
+
117
+ monkeypatch.setattr(client._auth, "logout", logout)
118
+ monkeypatch.setattr(client._session, "close", close_session)
119
+
120
+ client._auth.access_token = "access-token"
121
+
122
+ with client as managed_client:
123
+ assert managed_client is client
124
+
125
+ assert events == ["logout", "session_close"]
126
+ assert cast(str | None, client._auth.access_token) is None
127
+ with pytest.raises(RuntimeError, match="closed"):
128
+ client.get_ticket_record("123")
129
+
130
+
131
+ def test_client_from_env_requires_glpi_api_url(monkeypatch: pytest.MonkeyPatch) -> None:
132
+ monkeypatch.setenv("GLPI_BASE_URL", "https://glpi.example.test/api.php")
133
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
134
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
135
+
136
+ with pytest.raises(ValueError, match="glpi_api_url"):
137
+ GlpiClient.from_env()
138
+
139
+
140
+ @pytest.mark.parametrize("v1_base_url", _V1_BASE_URLS)
141
+ def test_client_preserves_explicit_v1_base_url(v1_base_url: str) -> None:
142
+ client = GlpiClient(
143
+ glpi_api_url="https://glpi.example.test/api.php",
144
+ client_id="client-id",
145
+ client_secret="client-secret",
146
+ v1_base_url=v1_base_url,
147
+ v1_user_token="user-token",
148
+ )
149
+
150
+ try:
151
+ assert client._v1 is not None
152
+ assert client._v1._base_url == v1_base_url
153
+ finally:
154
+ client.close()
155
+
156
+
157
+ @pytest.mark.parametrize(
158
+ "v1_kwargs",
159
+ [
160
+ {"v1_base_url": "https://glpi.example.test/apirest.php"},
161
+ {"v1_user_token": "user-token"},
162
+ ],
163
+ )
164
+ def test_client_rejects_partial_v1_document_config(
165
+ v1_kwargs: dict[str, Any],
166
+ ) -> None:
167
+ with pytest.raises(ValueError, match=r"v1_base_url.*v1_user_token"):
168
+ GlpiClient(
169
+ glpi_api_url="https://glpi.example.test/api.php",
170
+ client_id="client-id",
171
+ client_secret="client-secret",
172
+ **v1_kwargs,
173
+ )
174
+
175
+
176
+ @pytest.mark.parametrize(
177
+ "env_values",
178
+ [
179
+ {
180
+ "GLPI_USERNAME": "api-user",
181
+ "GLPI_PASSWORD": "api-password",
182
+ },
183
+ {
184
+ "GLPI_CLIENT_ID": "client-id",
185
+ "GLPI_CLIENT_SECRET": "client-secret",
186
+ },
187
+ ],
188
+ )
189
+ def test_client_from_env_accepts_each_supported_auth_pair(
190
+ monkeypatch: pytest.MonkeyPatch,
191
+ env_values: dict[str, str],
192
+ ) -> None:
193
+ monkeypatch.setenv("GLPI_API_URL", "https://glpi.example.test/api.php")
194
+ for key, value in env_values.items():
195
+ monkeypatch.setenv(key, value)
196
+
197
+ client = GlpiClient.from_env()
198
+ try:
199
+ assert client.glpi_api_url == "https://glpi.example.test/api.php"
200
+ finally:
201
+ client.close()
202
+
203
+
204
+ def test_create_user_uses_sync_transport(
205
+ client_factory: Callable[..., GlpiClient],
206
+ monkeypatch: pytest.MonkeyPatch,
207
+ ) -> None:
208
+ client = client_factory()
209
+ requests: list[dict[str, object]] = []
210
+
211
+ def ensure_token() -> None:
212
+ client._auth.access_token = "token"
213
+
214
+ def post(url: str, **kwargs: object) -> FakeResponse:
215
+ requests.append({"url": url, **kwargs})
216
+ return FakeResponse()
217
+
218
+ monkeypatch.setattr(client._auth, "ensure_token", ensure_token)
219
+ monkeypatch.setattr(client._session, "post", post)
220
+ try:
221
+ created = client.create_user(GlpiUser(email="ada@example.test"))
222
+
223
+ assert created == "321"
224
+ assert requests[0]["url"] == (
225
+ "https://glpi.example.test/api.php/Administration/User"
226
+ )
227
+ assert cast(dict[str, object], requests[0]["json"])["email"] == (
228
+ "ada@example.test"
229
+ )
230
+ finally:
231
+ client.close()
232
+
233
+
234
+ def test_delete_user_returns_none(
235
+ client_factory: Callable[..., GlpiClient],
236
+ monkeypatch: pytest.MonkeyPatch,
237
+ ) -> None:
238
+ client = client_factory()
239
+
240
+ def fake_delete_request(
241
+ endpoint: str,
242
+ payload: dict[str, object] | None = None,
243
+ skip_entity: bool = False,
244
+ ) -> FakeResponse:
245
+ assert endpoint == "Administration/User/42"
246
+ assert payload is None
247
+ assert skip_entity is False
248
+ return _empty_response()
249
+
250
+ monkeypatch.setattr(client, "_delete_request", fake_delete_request)
251
+ try:
252
+ client.delete_user(42)
253
+ finally:
254
+ client.close()
255
+
256
+
257
+ def test_create_location_requires_name(
258
+ client_factory: Callable[..., GlpiClient],
259
+ ) -> None:
260
+ client = client_factory()
261
+ try:
262
+ with pytest.raises(ValueError, match="location creation requires a name"):
263
+ client.create_location(GlpiLocation(entity_id=7))
264
+ finally:
265
+ client.close()
266
+
267
+
268
+ def test_delete_location_returns_none(
269
+ client_factory: Callable[..., GlpiClient],
270
+ monkeypatch: pytest.MonkeyPatch,
271
+ ) -> None:
272
+ client = client_factory()
273
+
274
+ def fake_delete_request(
275
+ endpoint: str,
276
+ payload: dict[str, object] | None = None,
277
+ skip_entity: bool = False,
278
+ ) -> FakeResponse:
279
+ assert endpoint == "Dropdowns/Location/9"
280
+ assert payload is None
281
+ assert skip_entity is False
282
+ return _empty_response()
283
+
284
+ monkeypatch.setattr(client, "_delete_request", fake_delete_request)
285
+ try:
286
+ client.delete_location(9)
287
+ finally:
288
+ client.close()
289
+
290
+
291
+ def test_upload_document_to_ticket_uses_v1_upload_session(
292
+ client_factory: Callable[..., GlpiClient],
293
+ ) -> None:
294
+ client = client_factory(glpi_entity=7)
295
+ upload_calls: list[dict[str, object]] = []
296
+
297
+ class FakeV1Session:
298
+ def close(self) -> None:
299
+ return None
300
+
301
+ def upload_document(
302
+ self,
303
+ filename: str,
304
+ content: bytes,
305
+ mime_type: str,
306
+ *,
307
+ document_name: str,
308
+ ticket_id: int,
309
+ entity_id: int | None,
310
+ ) -> dict[str, object]:
311
+ upload_calls.append(
312
+ {
313
+ "filename": filename,
314
+ "content": content,
315
+ "mime_type": mime_type,
316
+ "document_name": document_name,
317
+ "ticket_id": ticket_id,
318
+ "entity_id": entity_id,
319
+ }
320
+ )
321
+ return {"id": "654"}
322
+
323
+ client._v1 = cast(Any, FakeV1Session())
324
+ try:
325
+ uploaded = client.upload_document_to_ticket(
326
+ GlpiDocument(
327
+ ticket_id=123,
328
+ filename="trace.txt",
329
+ content=b"trace",
330
+ mime_type="text/plain",
331
+ )
332
+ )
333
+
334
+ assert upload_calls == [
335
+ {
336
+ "filename": "trace.txt",
337
+ "content": b"trace",
338
+ "mime_type": "text/plain",
339
+ "document_name": "Document ticket 123",
340
+ "ticket_id": 123,
341
+ "entity_id": 7,
342
+ }
343
+ ]
344
+ assert uploaded.ticket_id == 123
345
+ assert uploaded.document_id == "654"
346
+ assert uploaded.document_name == "Document ticket 123"
347
+ assert uploaded.filename == "trace.txt"
348
+ finally:
349
+ client.close()
@@ -0,0 +1,257 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+ from typing import Any, cast
5
+
6
+ import pytest
7
+
8
+ from glpi_python_client import AsyncGlpiClient, GlpiUser
9
+ from glpi_python_client.testing.utils import FakeResponse
10
+
11
+ _V1_BASE_URLS = (
12
+ pytest.param("https://glpi.example.test/api.php/v1", id="api.php-v1"),
13
+ pytest.param("https://glpi.example.test/apirest.php", id="apirest.php"),
14
+ )
15
+
16
+
17
+ def _empty_response() -> FakeResponse:
18
+ return FakeResponse(status_code=204, payload={}, text="", content=b"")
19
+
20
+
21
+ def test_async_client_rejects_legacy_base_url_keyword() -> None:
22
+ with pytest.raises(TypeError, match="base_url"):
23
+ AsyncGlpiClient(
24
+ base_url="https://glpi.example.test/api.php", # type: ignore[call-arg]
25
+ client_id="client-id",
26
+ client_secret="client-secret",
27
+ )
28
+
29
+
30
+ def test_async_client_rejects_constructor_batch_size_keyword() -> None:
31
+ with pytest.raises(TypeError, match="batch_size"):
32
+ AsyncGlpiClient(
33
+ glpi_api_url="https://glpi.example.test/api.php",
34
+ client_id="client-id",
35
+ client_secret="client-secret",
36
+ batch_size=100, # type: ignore[call-arg]
37
+ )
38
+
39
+
40
+ def test_async_client_from_env(monkeypatch: pytest.MonkeyPatch) -> None:
41
+ monkeypatch.setenv("GLPI_API_URL", "https://glpi.example.test/api.php")
42
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
43
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
44
+ monkeypatch.setenv("GLPI_AUTH_TOKEN_REFRESH", "90")
45
+
46
+ client = AsyncGlpiClient.from_env()
47
+ try:
48
+ assert client.glpi_api_url == "https://glpi.example.test/api.php"
49
+ assert client._auth.auth_token_refresh == 90
50
+ finally:
51
+ asyncio.run(client.close())
52
+
53
+
54
+ def test_async_client_from_env_none_overrides_clear_optional_env_values(
55
+ monkeypatch: pytest.MonkeyPatch,
56
+ ) -> None:
57
+ monkeypatch.setenv("GLPI_API_URL", "https://glpi.example.test/api.php")
58
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
59
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
60
+ monkeypatch.setenv("GLPI_AUTH_TOKEN_REFRESH", "90")
61
+ monkeypatch.setenv("GLPI_V1_BASE_URL", "https://glpi.example.test/apirest.php")
62
+ monkeypatch.setenv("GLPI_V1_USER_TOKEN", "user-token")
63
+
64
+ client = AsyncGlpiClient.from_env(
65
+ auth_token_refresh=None,
66
+ v1_base_url=None,
67
+ v1_user_token=None,
68
+ )
69
+ try:
70
+ assert client._auth.auth_token_refresh is None
71
+ assert client._v1 is None
72
+ finally:
73
+ asyncio.run(client.close())
74
+
75
+
76
+ def test_async_client_from_env_rejects_invalid_integer_values(
77
+ monkeypatch: pytest.MonkeyPatch,
78
+ ) -> None:
79
+ monkeypatch.setenv("GLPI_API_URL", "https://glpi.example.test/api.php")
80
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
81
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
82
+ monkeypatch.setenv("GLPI_AUTH_TOKEN_REFRESH", "abc")
83
+
84
+ with pytest.raises(ValueError):
85
+ AsyncGlpiClient.from_env()
86
+
87
+
88
+ def test_async_client_from_env_rejects_invalid_boolean_values(
89
+ monkeypatch: pytest.MonkeyPatch,
90
+ ) -> None:
91
+ monkeypatch.setenv("GLPI_API_URL", "https://glpi.example.test/api.php")
92
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
93
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
94
+ monkeypatch.setenv("GLPI_ENTITY_RECURSIVE", " definitely ")
95
+
96
+ with pytest.raises(ValueError, match="Invalid boolean"):
97
+ AsyncGlpiClient.from_env()
98
+
99
+
100
+ def test_async_client_context_manager_logs_out_and_rejects_future_calls(
101
+ monkeypatch: pytest.MonkeyPatch,
102
+ ) -> None:
103
+ async def run_test() -> None:
104
+ client = AsyncGlpiClient(
105
+ glpi_api_url="https://glpi.example.test/api.php/",
106
+ client_id="client-id",
107
+ client_secret="client-secret",
108
+ )
109
+ events: list[str] = []
110
+ original_logout = client._auth.logout
111
+ original_session_close = client._session.close
112
+
113
+ def logout() -> None:
114
+ events.append("logout")
115
+ original_logout()
116
+
117
+ def close_session() -> None:
118
+ events.append("session_close")
119
+ original_session_close()
120
+
121
+ monkeypatch.setattr(client._auth, "logout", logout)
122
+ monkeypatch.setattr(client._session, "close", close_session)
123
+
124
+ client._auth.access_token = "access-token"
125
+
126
+ async with client as managed_client:
127
+ assert managed_client is client
128
+
129
+ assert events == ["logout", "session_close"]
130
+ assert cast(str | None, client._auth.access_token) is None
131
+ with pytest.raises(RuntimeError, match="closed"):
132
+ await client.get_ticket_record("123")
133
+
134
+ asyncio.run(run_test())
135
+
136
+
137
+ def test_async_client_from_env_requires_glpi_api_url(
138
+ monkeypatch: pytest.MonkeyPatch,
139
+ ) -> None:
140
+ monkeypatch.setenv("GLPI_BASE_URL", "https://glpi.example.test/api.php")
141
+ monkeypatch.setenv("GLPI_CLIENT_ID", "client-id")
142
+ monkeypatch.setenv("GLPI_CLIENT_SECRET", "client-secret")
143
+
144
+ with pytest.raises(ValueError, match="glpi_api_url"):
145
+ AsyncGlpiClient.from_env()
146
+
147
+
148
+ @pytest.mark.parametrize("v1_base_url", _V1_BASE_URLS)
149
+ def test_async_client_preserves_explicit_v1_base_url(v1_base_url: str) -> None:
150
+ client = AsyncGlpiClient(
151
+ glpi_api_url="https://glpi.example.test/api.php",
152
+ client_id="client-id",
153
+ client_secret="client-secret",
154
+ v1_base_url=v1_base_url,
155
+ v1_user_token="user-token",
156
+ )
157
+
158
+ try:
159
+ assert client._v1 is not None
160
+ assert client._v1._base_url == v1_base_url
161
+ finally:
162
+ asyncio.run(client.close())
163
+
164
+
165
+ @pytest.mark.parametrize(
166
+ "v1_kwargs",
167
+ [
168
+ {"v1_base_url": "https://glpi.example.test/apirest.php"},
169
+ {"v1_user_token": "user-token"},
170
+ ],
171
+ )
172
+ def test_async_client_rejects_partial_v1_document_config(
173
+ v1_kwargs: dict[str, Any],
174
+ ) -> None:
175
+ with pytest.raises(ValueError, match=r"v1_base_url.*v1_user_token"):
176
+ AsyncGlpiClient(
177
+ glpi_api_url="https://glpi.example.test/api.php",
178
+ client_id="client-id",
179
+ client_secret="client-secret",
180
+ **v1_kwargs,
181
+ )
182
+
183
+
184
+ def test_async_create_user_uses_async_transport(
185
+ monkeypatch: pytest.MonkeyPatch,
186
+ ) -> None:
187
+ async def run_test() -> None:
188
+ client = AsyncGlpiClient(
189
+ glpi_api_url="https://glpi.example.test/api.php/",
190
+ client_id="client-id",
191
+ client_secret="client-secret",
192
+ )
193
+ requests: list[dict[str, object]] = []
194
+
195
+ def ensure_token() -> None:
196
+ client._auth.access_token = "token"
197
+
198
+ def post(url: str, **kwargs: object) -> FakeResponse:
199
+ requests.append({"url": url, **kwargs})
200
+ return FakeResponse()
201
+
202
+ monkeypatch.setattr(client._auth, "ensure_token", ensure_token)
203
+ monkeypatch.setattr(client._session, "post", post)
204
+ try:
205
+ created = await client.create_user(GlpiUser(email="ada@example.test"))
206
+
207
+ assert created == "321"
208
+ assert requests[0]["url"] == (
209
+ "https://glpi.example.test/api.php/Administration/User"
210
+ )
211
+ assert cast(dict[str, object], requests[0]["json"])["email"] == (
212
+ "ada@example.test"
213
+ )
214
+ finally:
215
+ await client.close()
216
+
217
+ asyncio.run(run_test())
218
+
219
+
220
+ @pytest.mark.parametrize(
221
+ ("method_name", "args", "expected_endpoint"),
222
+ [
223
+ ("delete_user", (42,), "Administration/User/42"),
224
+ ("delete_location", (9,), "Dropdowns/Location/9"),
225
+ ],
226
+ )
227
+ def test_async_provisioning_delete_operations_return_none(
228
+ monkeypatch: pytest.MonkeyPatch,
229
+ method_name: str,
230
+ args: tuple[object, ...],
231
+ expected_endpoint: str,
232
+ ) -> None:
233
+ async def run_test() -> None:
234
+ client = AsyncGlpiClient(
235
+ glpi_api_url="https://glpi.example.test/api.php/",
236
+ client_id="client-id",
237
+ client_secret="client-secret",
238
+ )
239
+
240
+ async def fake_delete_request(
241
+ endpoint: str,
242
+ payload: dict[str, object] | None = None,
243
+ skip_entity: bool = False,
244
+ ) -> FakeResponse:
245
+ assert endpoint == expected_endpoint
246
+ assert payload is None
247
+ assert skip_entity is False
248
+ return _empty_response()
249
+
250
+ monkeypatch.setattr(client, "_delete_request", fake_delete_request)
251
+ try:
252
+ operation = getattr(client, method_name)
253
+ await operation(*args)
254
+ finally:
255
+ await client.close()
256
+
257
+ asyncio.run(run_test())
@@ -0,0 +1,8 @@
1
+ """Internal GLPI v2 client implementation packages.
2
+
3
+ The v2 client implementation is split by responsibility: shared helpers live
4
+ under ``common`` while execution-model-specific mixins live under ``sync`` and
5
+ ``async_``.
6
+ """
7
+
8
+ from __future__ import annotations
@@ -0,0 +1,12 @@
1
+ """Asynchronous GLPI v2 implementation exports.
2
+
3
+ The asynchronous client is assembled from mixins grouped by endpoint area.
4
+ This package exposes the combined async API mixin used by
5
+ ``AsyncGlpiClient``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ from glpi_python_client.clients.v2.async_.api import AsyncGlpiApiClientMixin
11
+
12
+ __all__ = ["AsyncGlpiApiClientMixin"]
@@ -0,0 +1,29 @@
1
+ """Asynchronous GLPI v2 API mixin assembly.
2
+
3
+ This module combines the scope-focused async mixins into the single API surface
4
+ consumed by the public ``AsyncGlpiClient`` class.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from .directory import AsyncDirectoryMixin
10
+ from .documents import AsyncDocumentMixin
11
+ from .team import AsyncTeamMixin
12
+ from .tickets import AsyncTicketMixin
13
+ from .timeline import AsyncTimelineMixin
14
+ from .transport import AsyncTransportMixin
15
+
16
+
17
+ class AsyncGlpiApiClientMixin(
18
+ AsyncTicketMixin,
19
+ AsyncTimelineMixin,
20
+ AsyncDocumentMixin,
21
+ AsyncTeamMixin,
22
+ AsyncDirectoryMixin,
23
+ AsyncTransportMixin,
24
+ ):
25
+ """Combined asynchronous GLPI v2 client behavior.
26
+
27
+ The mixin class exists as the composition point for the async endpoint and
28
+ transport helpers used by ``AsyncGlpiClient``.
29
+ """