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,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,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())
|