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,88 @@
1
+ """Asynchronous user and location lookup operations for GLPI v2 clients.
2
+
3
+ This module contains the directory-style async search helpers used to query
4
+ users and locations through the GLPI high-level API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from glpi_python_client.clients.v2.common.constants import (
10
+ LOCATION_ENDPOINT,
11
+ USER_ENDPOINT,
12
+ )
13
+ from glpi_python_client.clients.v2.common.filters import rsql_contains_filter
14
+ from glpi_python_client.clients.v2.common.response_payloads import list_payload_records
15
+ from glpi_python_client.content.records.core.scalars import _optional_text
16
+ from glpi_python_client.content.records.parsers.directory import (
17
+ _glpi_location_record,
18
+ _glpi_user_record,
19
+ )
20
+ from glpi_python_client.models import GlpiLocation, GlpiUser
21
+
22
+ from .transport import AsyncTransportMixin
23
+
24
+
25
+ class AsyncDirectoryMixin(AsyncTransportMixin):
26
+ """Asynchronous GLPI user and location lookup helpers.
27
+
28
+ The methods in this mixin return typed directory models while awaiting the
29
+ async transport helpers for remote execution.
30
+ """
31
+
32
+ async def search_users(
33
+ self,
34
+ rsql_filter: str = "",
35
+ *,
36
+ limit: int = 1,
37
+ start: int = 0,
38
+ skip_entity: bool = False,
39
+ ) -> list[GlpiUser]:
40
+ """Search GLPI users with an optional raw RSQL filter.
41
+
42
+ The async helper preserves the same filtering and empty-result behavior
43
+ as the synchronous implementation while awaiting the remote request.
44
+ """
45
+
46
+ params: dict[str, object] = {"limit": limit, "start": start}
47
+ if rsql_filter:
48
+ params["filter"] = rsql_filter
49
+ response = await self._get_request(
50
+ USER_ENDPOINT, params=params, skip_entity=skip_entity
51
+ )
52
+ if response.status_code not in (200, 206):
53
+ return []
54
+ return list_payload_records(
55
+ response.json(),
56
+ record_factory=lambda user: (
57
+ _glpi_user_record(user)
58
+ if _optional_text(user.get("id")) is not None
59
+ else None
60
+ ),
61
+ )
62
+
63
+ async def search_locations(self, name: str) -> list[GlpiLocation]:
64
+ """Search GLPI locations by name.
65
+
66
+ Blank names are rejected locally by returning an empty list, and remote
67
+ non-success statuses are normalized to empty results.
68
+ """
69
+
70
+ location_filter = rsql_contains_filter("name", name)
71
+ if location_filter is None:
72
+ return []
73
+
74
+ response = await self._get_request(
75
+ LOCATION_ENDPOINT,
76
+ params={"filter": location_filter},
77
+ )
78
+ if response.status_code not in (200, 206):
79
+ return []
80
+ return list_payload_records(
81
+ response.json(),
82
+ record_factory=lambda location: (
83
+ _glpi_location_record(location)
84
+ if _optional_text(location.get("id")) is not None
85
+ and _optional_text(location.get("name")) is not None
86
+ else None
87
+ ),
88
+ )
@@ -0,0 +1,144 @@
1
+ """Asynchronous document operations for GLPI v2 clients.
2
+
3
+ This module covers async document listing, metadata lookup, binary download,
4
+ and deletion for ticket-linked GLPI documents.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+ from typing import Any
11
+
12
+ import requests
13
+ from tenacity import RetryError
14
+
15
+ from glpi_python_client.clients.v2.common.constants import (
16
+ DOCUMENT_SUFFIX,
17
+ TICKET_ENDPOINT,
18
+ GlpiId,
19
+ )
20
+ from glpi_python_client.clients.v2.common.errors import remote_error_message
21
+ from glpi_python_client.clients.v2.common.request_http import ensure_response_status
22
+ from glpi_python_client.clients.v2.common.response_payloads import (
23
+ timeline_payload_items,
24
+ )
25
+ from glpi_python_client.content.records.core.scalars import _optional_text
26
+ from glpi_python_client.content.records.parsers.documents import _glpi_document_record
27
+ from glpi_python_client.models import GlpiDocument
28
+
29
+ from .transport import AsyncTransportMixin
30
+
31
+ logger = logging.getLogger(__name__)
32
+
33
+
34
+ class AsyncDocumentMixin(AsyncTransportMixin):
35
+ """Asynchronous GLPI ticket document helpers.
36
+
37
+ These helpers keep document parsing and optional metadata enrichment aligned
38
+ with the synchronous client while exposing awaitable operations.
39
+ """
40
+
41
+ async def get_document_records(
42
+ self,
43
+ ticket_id: GlpiId,
44
+ *,
45
+ enrich_metadata: bool = True,
46
+ ) -> list[GlpiDocument]:
47
+ """Fetch the documents linked to one ticket.
48
+
49
+ When metadata enrichment is enabled, the method performs per-document
50
+ async lookups and logs skipped enrichments without failing the whole
51
+ batch.
52
+ """
53
+
54
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{DOCUMENT_SUFFIX}"
55
+ response = await self._get_request(endpoint)
56
+ if response.status_code not in (200, 206):
57
+ logger.warning(
58
+ "Failed to get documents for ticket %s: %s",
59
+ ticket_id,
60
+ response.status_code,
61
+ )
62
+ return []
63
+
64
+ records: list[GlpiDocument] = []
65
+ for relation in timeline_payload_items(response.json()):
66
+ document_id = _optional_text(
67
+ relation.get("documents_id")
68
+ ) or _optional_text(relation.get("id"))
69
+ if document_id is None:
70
+ continue
71
+ metadata: dict[str, Any] = {}
72
+ if enrich_metadata:
73
+ try:
74
+ document_record = await self.get_document_record(document_id)
75
+ metadata = {
76
+ "id": document_record.document_id,
77
+ "filename": document_record.filename,
78
+ "mime": document_record.mime_type,
79
+ }
80
+ except (RetryError, ValueError, requests.RequestException) as exc:
81
+ logger.warning(
82
+ "Skipping GLPI document %s metadata lookup: %s",
83
+ document_id,
84
+ remote_error_message(exc),
85
+ )
86
+ records.append(_glpi_document_record(relation, metadata=metadata))
87
+ return records
88
+
89
+ async def get_document_record(self, document_id: GlpiId) -> GlpiDocument:
90
+ """Fetch one GLPI document record by identifier.
91
+
92
+ The payload must be a mapping and is converted into a typed
93
+ ``GlpiDocument`` instance before being returned.
94
+ """
95
+
96
+ response = await self._get_request(
97
+ f"Management/Document/{document_id}",
98
+ skip_entity=True,
99
+ )
100
+ if response.status_code not in (200, 206):
101
+ raise ValueError(
102
+ f"Failed to get document {document_id}: "
103
+ f"{response.status_code} {response.text[:200]}"
104
+ )
105
+ payload = response.json()
106
+ if not isinstance(payload, dict):
107
+ raise ValueError(f"Unexpected GLPI document payload for {document_id}")
108
+ return _glpi_document_record(payload)
109
+
110
+ async def download_document_content(self, document_id: GlpiId) -> bytes:
111
+ """Download the raw binary payload for one GLPI document.
112
+
113
+ This helper returns raw response bytes and keeps the async content
114
+ retrieval workflow aligned with the synchronous client.
115
+ """
116
+
117
+ response = await self._get_request(
118
+ f"Management/Document/{document_id}/Download",
119
+ skip_entity=True,
120
+ )
121
+ if response.status_code != 200:
122
+ raise ValueError(
123
+ f"Failed to download document {document_id}: "
124
+ f"{response.status_code} {response.text[:200]}"
125
+ )
126
+ return response.content
127
+
128
+ async def delete_document(self, document_id: GlpiId) -> None:
129
+ """Delete one GLPI document by identifier.
130
+
131
+ Successful deletes return ``None`` and are logged for traceability.
132
+ """
133
+
134
+ response = await self._delete_request(
135
+ f"Management/Document/{document_id}",
136
+ skip_entity=True,
137
+ )
138
+ ensure_response_status(
139
+ response,
140
+ success_statuses=(200, 204),
141
+ failure_message=f"Failed to delete document {document_id}",
142
+ )
143
+ logger.info("GLPI API deleted document %s", document_id)
144
+ return None
@@ -0,0 +1,125 @@
1
+ """Asynchronous team member operations for GLPI v2 clients.
2
+
3
+ This module provides the async ticket team-member helpers that list, add, and
4
+ remove assigned members through the GLPI API.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import logging
10
+
11
+ from glpi_python_client.clients.v2.common.constants import (
12
+ TEAM_MEMBER_SUFFIX,
13
+ TICKET_ENDPOINT,
14
+ GlpiId,
15
+ )
16
+ from glpi_python_client.clients.v2.common.payloads import build_team_member_payload
17
+ from glpi_python_client.clients.v2.common.request_http import ensure_response_status
18
+ from glpi_python_client.clients.v2.common.response_payloads import list_payload_records
19
+ from glpi_python_client.content.records.parsers.team import _glpi_team_member_record
20
+ from glpi_python_client.models import GlpiTeamMember
21
+
22
+ from .transport import AsyncTransportMixin
23
+
24
+ logger = logging.getLogger(__name__)
25
+
26
+
27
+ class AsyncTeamMixin(AsyncTransportMixin):
28
+ """Asynchronous GLPI ticket team member helpers.
29
+
30
+ The mixin keeps team-member payload construction and response handling out
31
+ of the public async client class.
32
+ """
33
+
34
+ async def get_team_member_records(self, ticket_id: GlpiId) -> list[GlpiTeamMember]:
35
+ """Fetch the team members currently linked to one ticket.
36
+
37
+ Non-success list responses are logged and normalized to an empty result
38
+ list rather than raising immediately.
39
+ """
40
+
41
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{TEAM_MEMBER_SUFFIX}"
42
+ response = await self._get_request(endpoint)
43
+ if response.status_code not in (200, 206):
44
+ logger.warning(
45
+ "Failed to get team members for ticket %s: %s",
46
+ ticket_id,
47
+ response.status_code,
48
+ )
49
+ return []
50
+ return list_payload_records(
51
+ response.json(),
52
+ record_factory=_glpi_team_member_record,
53
+ )
54
+
55
+ async def add_team_member(
56
+ self,
57
+ ticket_id: GlpiId,
58
+ member: GlpiTeamMember,
59
+ ) -> None:
60
+ """Add one team member to a GLPI ticket.
61
+
62
+ The typed member object is converted into the payload expected by GLPI,
63
+ and the successful mutation is logged with the affected ticket.
64
+ """
65
+
66
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{TEAM_MEMBER_SUFFIX}"
67
+ payload = build_team_member_payload(
68
+ member_type=member.member_type,
69
+ member_id=member.member_id,
70
+ role=member.role,
71
+ )
72
+ response = await self._post_request(endpoint, payload)
73
+ ensure_response_status(
74
+ response,
75
+ success_statuses=(200, 201),
76
+ failure_message=(
77
+ "Failed to add team member "
78
+ f"{member.member_type}:{member.member_id} "
79
+ f"role={member.role} on ticket {ticket_id}"
80
+ ),
81
+ )
82
+ logger.info(
83
+ "GLPI API added team member %s:%s role=%s on ticket %s",
84
+ member.member_type,
85
+ member.member_id,
86
+ member.role,
87
+ ticket_id,
88
+ )
89
+ return None
90
+
91
+ async def remove_team_member(
92
+ self,
93
+ ticket_id: GlpiId,
94
+ member: GlpiTeamMember,
95
+ ) -> None:
96
+ """Remove one team member from a GLPI ticket.
97
+
98
+ The payload mirrors team-member creation so the same member model can be
99
+ reused for add and remove workflows.
100
+ """
101
+
102
+ endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{TEAM_MEMBER_SUFFIX}"
103
+ payload = build_team_member_payload(
104
+ member_type=member.member_type,
105
+ member_id=member.member_id,
106
+ role=member.role,
107
+ )
108
+ response = await self._delete_request(endpoint, payload)
109
+ ensure_response_status(
110
+ response,
111
+ success_statuses=(200, 204),
112
+ failure_message=(
113
+ "Failed to remove team member "
114
+ f"{member.member_type}:{member.member_id} "
115
+ f"role={member.role} from ticket {ticket_id}"
116
+ ),
117
+ )
118
+ logger.info(
119
+ "GLPI API removed team member %s:%s role=%s from ticket %s",
120
+ member.member_type,
121
+ member.member_id,
122
+ member.role,
123
+ ticket_id,
124
+ )
125
+ return None
@@ -0,0 +1,5 @@
1
+ """Async v2 client tests colocated with async implementation modules.
2
+
3
+ Each test module mirrors the async client file it exercises so sync and async
4
+ coverage can evolve alongside their respective implementations.
5
+ """
@@ -0,0 +1,43 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import pytest
6
+
7
+ from glpi_python_client import AsyncGlpiClient
8
+ from glpi_python_client.testing.utils import SearchResponse
9
+
10
+
11
+ def test_async_search_locations_escapes_query_text(
12
+ monkeypatch: pytest.MonkeyPatch,
13
+ ) -> None:
14
+ async def run_test() -> None:
15
+ client = AsyncGlpiClient(
16
+ glpi_api_url="https://glpi.example.test/api.php/",
17
+ client_id="client-id",
18
+ client_secret="client-secret",
19
+ )
20
+ observed_params: dict[str, object] | None = None
21
+
22
+ async def fake_get_request(
23
+ endpoint: str,
24
+ params: dict[str, object] | None = None,
25
+ skip_entity: bool = False,
26
+ ) -> SearchResponse:
27
+ nonlocal observed_params
28
+ assert endpoint == "Dropdowns/Location"
29
+ assert skip_entity is False
30
+ observed_params = params
31
+ return SearchResponse([])
32
+
33
+ monkeypatch.setattr(client, "_get_request", fake_get_request)
34
+ try:
35
+ await client.search_locations('Paris "HQ" *East\\West')
36
+
37
+ assert observed_params == {
38
+ "filter": 'name=like="*Paris \\"HQ\\" \\*East\\\\West*"'
39
+ }
40
+ finally:
41
+ await client.close()
42
+
43
+ asyncio.run(run_test())
@@ -0,0 +1,41 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import pytest
6
+
7
+ from glpi_python_client import AsyncGlpiClient
8
+ from glpi_python_client.testing.utils import FakeResponse
9
+
10
+
11
+ def _empty_response() -> FakeResponse:
12
+ return FakeResponse(status_code=204, payload={}, text="", content=b"")
13
+
14
+
15
+ def test_async_delete_document_returns_none(
16
+ monkeypatch: pytest.MonkeyPatch,
17
+ ) -> None:
18
+ async def run_test() -> None:
19
+ client = AsyncGlpiClient(
20
+ glpi_api_url="https://glpi.example.test/api.php/",
21
+ client_id="client-id",
22
+ client_secret="client-secret",
23
+ )
24
+
25
+ async def fake_delete_request(
26
+ endpoint: str,
27
+ payload: dict[str, object] | None = None,
28
+ skip_entity: bool = False,
29
+ ) -> FakeResponse:
30
+ assert endpoint == "Management/Document/777"
31
+ assert payload is None
32
+ assert skip_entity is True
33
+ return _empty_response()
34
+
35
+ monkeypatch.setattr(client, "_delete_request", fake_delete_request)
36
+ try:
37
+ await client.delete_document(777)
38
+ finally:
39
+ await client.close()
40
+
41
+ asyncio.run(run_test())
@@ -0,0 +1,44 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import pytest
6
+
7
+ from glpi_python_client import AsyncGlpiClient, GlpiTeamMember
8
+ from glpi_python_client.testing.utils import FakeResponse
9
+
10
+
11
+ def _empty_response() -> FakeResponse:
12
+ return FakeResponse(status_code=204, payload={}, text="", content=b"")
13
+
14
+
15
+ def test_async_remove_team_member_returns_none(
16
+ monkeypatch: pytest.MonkeyPatch,
17
+ ) -> None:
18
+ async def run_test() -> None:
19
+ client = AsyncGlpiClient(
20
+ glpi_api_url="https://glpi.example.test/api.php/",
21
+ client_id="client-id",
22
+ client_secret="client-secret",
23
+ )
24
+
25
+ async def fake_delete_request(
26
+ endpoint: str,
27
+ payload: dict[str, object] | None = None,
28
+ skip_entity: bool = False,
29
+ ) -> FakeResponse:
30
+ assert endpoint == "Assistance/Ticket/321/TeamMember"
31
+ assert payload == {"type": "User", "id": 99, "role": "1"}
32
+ assert skip_entity is False
33
+ return _empty_response()
34
+
35
+ monkeypatch.setattr(client, "_delete_request", fake_delete_request)
36
+ try:
37
+ await client.remove_team_member(
38
+ 321,
39
+ GlpiTeamMember(member_type="User", member_id=99, role="1"),
40
+ )
41
+ finally:
42
+ await client.close()
43
+
44
+ asyncio.run(run_test())
@@ -0,0 +1,174 @@
1
+ from __future__ import annotations
2
+
3
+ import asyncio
4
+
5
+ import pytest
6
+
7
+ from glpi_python_client import AsyncGlpiClient, GlpiTicket
8
+ from glpi_python_client.testing.utils import (
9
+ FakeResponse,
10
+ SearchResponse,
11
+ TicketResponse,
12
+ make_ticket_record,
13
+ )
14
+
15
+
16
+ def _empty_response() -> FakeResponse:
17
+ return FakeResponse(status_code=204, payload={}, text="", content=b"")
18
+
19
+
20
+ def test_async_get_ticket_record_can_include_deleted_ticket(
21
+ monkeypatch: pytest.MonkeyPatch,
22
+ ) -> None:
23
+ async def run_test() -> None:
24
+ client = AsyncGlpiClient(
25
+ glpi_api_url="https://glpi.example.test/api.php/",
26
+ client_id="client-id",
27
+ client_secret="client-secret",
28
+ )
29
+
30
+ async def fake_get_request(
31
+ endpoint: str,
32
+ params: dict[str, object] | None = None,
33
+ skip_entity: bool = False,
34
+ ) -> TicketResponse:
35
+ assert endpoint == "Assistance/Ticket/123"
36
+ assert params is None
37
+ assert skip_entity is False
38
+ return TicketResponse(make_ticket_record(id=123, is_deleted=1))
39
+
40
+ monkeypatch.setattr(client, "_get_request", fake_get_request)
41
+ try:
42
+ ticket = await client.get_ticket_record(
43
+ "123",
44
+ include_deleted_ticket=True,
45
+ )
46
+
47
+ assert ticket.id == "123"
48
+ finally:
49
+ await client.close()
50
+
51
+ asyncio.run(run_test())
52
+
53
+
54
+ def test_async_create_ticket_accepts_public_extra_payload(
55
+ monkeypatch: pytest.MonkeyPatch,
56
+ ) -> None:
57
+ async def run_test() -> None:
58
+ client = AsyncGlpiClient(
59
+ glpi_api_url="https://glpi.example.test/api.php/",
60
+ client_id="client-id",
61
+ client_secret="client-secret",
62
+ )
63
+ payload_data: dict[str, object] | None = None
64
+
65
+ async def fake_post_request(
66
+ endpoint: str,
67
+ payload: dict[str, object] | None = None,
68
+ skip_entity: bool = False,
69
+ ) -> FakeResponse:
70
+ nonlocal payload_data
71
+ assert endpoint == "Assistance/Ticket"
72
+ assert skip_entity is False
73
+ assert payload is not None
74
+ payload_data = payload
75
+ return FakeResponse()
76
+
77
+ monkeypatch.setattr(client, "_post_request", fake_post_request)
78
+ try:
79
+ created = await client.create_ticket(
80
+ GlpiTicket(
81
+ name="Need help",
82
+ extra_payload={"_room_code": "PAR-3F-12"},
83
+ )
84
+ )
85
+
86
+ assert created == "321"
87
+ assert payload_data == {
88
+ "name": "Need help",
89
+ "_room_code": "PAR-3F-12",
90
+ }
91
+ finally:
92
+ await client.close()
93
+
94
+ asyncio.run(run_test())
95
+
96
+
97
+ def test_async_delete_ticket_returns_none(
98
+ monkeypatch: pytest.MonkeyPatch,
99
+ ) -> None:
100
+ async def run_test() -> None:
101
+ client = AsyncGlpiClient(
102
+ glpi_api_url="https://glpi.example.test/api.php/",
103
+ client_id="client-id",
104
+ client_secret="client-secret",
105
+ )
106
+
107
+ async def fake_delete_request(
108
+ endpoint: str,
109
+ payload: dict[str, object] | None = None,
110
+ skip_entity: bool = False,
111
+ ) -> FakeResponse:
112
+ assert endpoint == "Assistance/Ticket/321"
113
+ assert payload is None
114
+ assert skip_entity is False
115
+ return _empty_response()
116
+
117
+ monkeypatch.setattr(client, "_delete_request", fake_delete_request)
118
+ try:
119
+ await client.delete_ticket(321)
120
+ finally:
121
+ await client.close()
122
+
123
+ asyncio.run(run_test())
124
+
125
+
126
+ def test_async_search_ticket_records_returns_full_list_by_default(
127
+ monkeypatch: pytest.MonkeyPatch,
128
+ ) -> None:
129
+ async def run_test() -> None:
130
+ client = AsyncGlpiClient(
131
+ glpi_api_url="https://glpi.example.test/api.php/",
132
+ client_id="client-id",
133
+ client_secret="client-secret",
134
+ )
135
+ requests: list[dict[str, object]] = []
136
+ responses = iter(
137
+ [
138
+ SearchResponse(
139
+ [
140
+ make_ticket_record(id=1, name="Ticket 1"),
141
+ make_ticket_record(id=2, name="Ticket 2"),
142
+ ],
143
+ status_code=206,
144
+ headers={"Content-Range": "0-1/3"},
145
+ ),
146
+ SearchResponse(
147
+ [make_ticket_record(id=3, name="Ticket 3")],
148
+ status_code=200,
149
+ headers={"Content-Range": "2-2/3"},
150
+ ),
151
+ ]
152
+ )
153
+
154
+ async def fake_get_request(
155
+ endpoint: str,
156
+ params: dict[str, object] | None = None,
157
+ skip_entity: bool = False,
158
+ ) -> SearchResponse:
159
+ assert endpoint == "Assistance/Ticket"
160
+ assert skip_entity is False
161
+ requests.append(dict(params or {}))
162
+ return next(responses)
163
+
164
+ monkeypatch.setattr(client, "_get_request", fake_get_request)
165
+ try:
166
+ tickets = await client.search_ticket_records()
167
+
168
+ assert [ticket.id for ticket in tickets] == ["1", "2", "3"]
169
+ assert [request["start"] for request in requests] == [0, 2]
170
+ assert all("limit" not in request for request in requests)
171
+ finally:
172
+ await client.close()
173
+
174
+ asyncio.run(run_test())