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,126 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import asyncio
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from glpi_python_client import AsyncGlpiClient, GlpiFollowup, GlpiSolution
|
|
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_update_followup_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_update_request(
|
|
26
|
+
endpoint: str,
|
|
27
|
+
payload: dict[str, object] | None = None,
|
|
28
|
+
skip_entity: bool = False,
|
|
29
|
+
) -> FakeResponse:
|
|
30
|
+
assert endpoint == "Assistance/Ticket/321/Timeline/Followup/654"
|
|
31
|
+
assert skip_entity is False
|
|
32
|
+
assert payload == {
|
|
33
|
+
"content": "<p>Updated followup</p>",
|
|
34
|
+
"is_private": False,
|
|
35
|
+
}
|
|
36
|
+
response = FakeResponse()
|
|
37
|
+
response.status_code = 204
|
|
38
|
+
response.text = ""
|
|
39
|
+
response.content = b""
|
|
40
|
+
return response
|
|
41
|
+
|
|
42
|
+
monkeypatch.setattr(client, "_update_request", fake_update_request)
|
|
43
|
+
try:
|
|
44
|
+
await client.update_followup(
|
|
45
|
+
"321",
|
|
46
|
+
"654",
|
|
47
|
+
GlpiFollowup(content="Updated followup"),
|
|
48
|
+
)
|
|
49
|
+
finally:
|
|
50
|
+
await client.close()
|
|
51
|
+
|
|
52
|
+
asyncio.run(run_test())
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def test_async_create_solution_accepts_solution_id_response_key(
|
|
56
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
57
|
+
) -> None:
|
|
58
|
+
async def run_test() -> None:
|
|
59
|
+
client = AsyncGlpiClient(
|
|
60
|
+
glpi_api_url="https://glpi.example.test/api.php/",
|
|
61
|
+
client_id="client-id",
|
|
62
|
+
client_secret="client-secret",
|
|
63
|
+
)
|
|
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
|
+
assert endpoint == "Assistance/Ticket/321/Timeline/Solution"
|
|
71
|
+
assert skip_entity is False
|
|
72
|
+
assert payload == {"content": "<p>Resolved</p>"}
|
|
73
|
+
return FakeResponse(payload={"solution_id": "987"})
|
|
74
|
+
|
|
75
|
+
monkeypatch.setattr(client, "_post_request", fake_post_request)
|
|
76
|
+
try:
|
|
77
|
+
created = await client.create_solution(
|
|
78
|
+
321,
|
|
79
|
+
GlpiSolution(content="Resolved"),
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
assert created == "987"
|
|
83
|
+
finally:
|
|
84
|
+
await client.close()
|
|
85
|
+
|
|
86
|
+
asyncio.run(run_test())
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@pytest.mark.parametrize(
|
|
90
|
+
("method_name", "args", "expected_endpoint"),
|
|
91
|
+
[
|
|
92
|
+
("delete_followup", (321, 654), "Assistance/Ticket/321/Timeline/Followup/654"),
|
|
93
|
+
("delete_solution", (321, 987), "Assistance/Ticket/321/Timeline/Solution/987"),
|
|
94
|
+
],
|
|
95
|
+
)
|
|
96
|
+
def test_async_delete_timeline_operations_return_none(
|
|
97
|
+
monkeypatch: pytest.MonkeyPatch,
|
|
98
|
+
method_name: str,
|
|
99
|
+
args: tuple[object, ...],
|
|
100
|
+
expected_endpoint: str,
|
|
101
|
+
) -> None:
|
|
102
|
+
async def run_test() -> None:
|
|
103
|
+
client = AsyncGlpiClient(
|
|
104
|
+
glpi_api_url="https://glpi.example.test/api.php/",
|
|
105
|
+
client_id="client-id",
|
|
106
|
+
client_secret="client-secret",
|
|
107
|
+
)
|
|
108
|
+
|
|
109
|
+
async def fake_delete_request(
|
|
110
|
+
endpoint: str,
|
|
111
|
+
payload: dict[str, object] | None = None,
|
|
112
|
+
skip_entity: bool = False,
|
|
113
|
+
) -> FakeResponse:
|
|
114
|
+
assert endpoint == expected_endpoint
|
|
115
|
+
assert payload is None
|
|
116
|
+
assert skip_entity is False
|
|
117
|
+
return _empty_response()
|
|
118
|
+
|
|
119
|
+
monkeypatch.setattr(client, "_delete_request", fake_delete_request)
|
|
120
|
+
try:
|
|
121
|
+
operation = getattr(client, method_name)
|
|
122
|
+
await operation(*args)
|
|
123
|
+
finally:
|
|
124
|
+
await client.close()
|
|
125
|
+
|
|
126
|
+
asyncio.run(run_test())
|
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
"""Asynchronous ticket operations for GLPI v2 clients.
|
|
2
|
+
|
|
3
|
+
This module contains the async ticket search, fetch, create, update, and
|
|
4
|
+
delete helpers used by the asynchronous high-level client.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
from collections.abc import AsyncIterator
|
|
11
|
+
from typing import Any, cast, overload
|
|
12
|
+
|
|
13
|
+
from glpi_python_client.clients.v2.common.constants import TICKET_ENDPOINT, GlpiId
|
|
14
|
+
from glpi_python_client.clients.v2.common.request_http import (
|
|
15
|
+
ensure_response_status,
|
|
16
|
+
require_non_empty_text,
|
|
17
|
+
require_response_text,
|
|
18
|
+
)
|
|
19
|
+
from glpi_python_client.clients.v2.common.ticket_search import (
|
|
20
|
+
advance_ticket_search_pagination,
|
|
21
|
+
build_ticket_search_params,
|
|
22
|
+
filter_ticket_search_batch,
|
|
23
|
+
is_deleted_ticket,
|
|
24
|
+
merge_list_ticket_fields,
|
|
25
|
+
)
|
|
26
|
+
from glpi_python_client.content.records.core.normalization import (
|
|
27
|
+
_normalize_ticket_record,
|
|
28
|
+
)
|
|
29
|
+
from glpi_python_client.content.records.parsers.tickets import (
|
|
30
|
+
_glpi_ticket_record,
|
|
31
|
+
)
|
|
32
|
+
from glpi_python_client.models import GlpiTicket
|
|
33
|
+
|
|
34
|
+
from .transport import AsyncTransportMixin
|
|
35
|
+
|
|
36
|
+
logger = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
class AsyncTicketMixin(AsyncTransportMixin):
|
|
40
|
+
"""Asynchronous GLPI ticket search and mutation helpers.
|
|
41
|
+
|
|
42
|
+
The mixin exposes the typed async ticket operations while delegating shared
|
|
43
|
+
field and pagination rules to the common helper modules.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
@overload
|
|
47
|
+
async def search_ticket_records(
|
|
48
|
+
self,
|
|
49
|
+
query: str | None = None,
|
|
50
|
+
*,
|
|
51
|
+
fields: tuple[str, ...] = (),
|
|
52
|
+
sort: str | None = None,
|
|
53
|
+
batch_size: None = None,
|
|
54
|
+
include_deleted_ticket: bool = False,
|
|
55
|
+
) -> list[GlpiTicket]: ...
|
|
56
|
+
|
|
57
|
+
@overload
|
|
58
|
+
async def search_ticket_records(
|
|
59
|
+
self,
|
|
60
|
+
query: str | None = None,
|
|
61
|
+
*,
|
|
62
|
+
fields: tuple[str, ...] = (),
|
|
63
|
+
sort: str | None = None,
|
|
64
|
+
batch_size: int,
|
|
65
|
+
include_deleted_ticket: bool = False,
|
|
66
|
+
) -> AsyncIterator[list[GlpiTicket]]: ...
|
|
67
|
+
|
|
68
|
+
async def search_ticket_records(
|
|
69
|
+
self,
|
|
70
|
+
query: str | None = None,
|
|
71
|
+
*,
|
|
72
|
+
fields: tuple[str, ...] = (),
|
|
73
|
+
sort: str | None = None,
|
|
74
|
+
batch_size: int | None = None,
|
|
75
|
+
include_deleted_ticket: bool = False,
|
|
76
|
+
) -> list[GlpiTicket] | AsyncIterator[list[GlpiTicket]]:
|
|
77
|
+
"""Search GLPI tickets and return either a full list or record batches.
|
|
78
|
+
|
|
79
|
+
Passing ``batch_size`` switches the method into streaming mode and
|
|
80
|
+
returns an async iterator of typed ticket batches instead of a fully
|
|
81
|
+
materialized list.
|
|
82
|
+
"""
|
|
83
|
+
|
|
84
|
+
if batch_size is not None and batch_size < 1:
|
|
85
|
+
raise ValueError("batch_size must be a positive integer or None")
|
|
86
|
+
|
|
87
|
+
merged_fields = merge_list_ticket_fields(list(fields) or None)
|
|
88
|
+
batches = self._iter_ticket_record_batches(
|
|
89
|
+
query=query,
|
|
90
|
+
fields=merged_fields,
|
|
91
|
+
sort=sort,
|
|
92
|
+
batch_size=batch_size,
|
|
93
|
+
include_deleted_ticket=include_deleted_ticket,
|
|
94
|
+
)
|
|
95
|
+
if batch_size is not None:
|
|
96
|
+
return batches
|
|
97
|
+
|
|
98
|
+
records: list[GlpiTicket] = []
|
|
99
|
+
async for page in batches:
|
|
100
|
+
records.extend(page)
|
|
101
|
+
return records
|
|
102
|
+
|
|
103
|
+
async def _iter_ticket_record_batches(
|
|
104
|
+
self,
|
|
105
|
+
*,
|
|
106
|
+
query: str | None = None,
|
|
107
|
+
fields: list[str] | None = None,
|
|
108
|
+
sort: str | None = None,
|
|
109
|
+
batch_size: int | None = None,
|
|
110
|
+
include_deleted_ticket: bool = False,
|
|
111
|
+
) -> AsyncIterator[list[GlpiTicket]]:
|
|
112
|
+
"""Yield typed ticket batches produced from paginated raw payloads.
|
|
113
|
+
|
|
114
|
+
This helper sits between raw payload pagination and public return types,
|
|
115
|
+
ensuring each yielded batch already contains parsed ``GlpiTicket``
|
|
116
|
+
objects.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
async for page in self._yield_ticket_payloads(
|
|
120
|
+
query=query,
|
|
121
|
+
fields=fields,
|
|
122
|
+
sort=sort,
|
|
123
|
+
batch_size=batch_size,
|
|
124
|
+
include_deleted_ticket=include_deleted_ticket,
|
|
125
|
+
):
|
|
126
|
+
yield [
|
|
127
|
+
_glpi_ticket_record(raw_ticket)
|
|
128
|
+
for raw_ticket in page
|
|
129
|
+
if isinstance(raw_ticket, dict)
|
|
130
|
+
]
|
|
131
|
+
|
|
132
|
+
async def get_ticket_record(
|
|
133
|
+
self,
|
|
134
|
+
ticket_id: GlpiId,
|
|
135
|
+
*,
|
|
136
|
+
include_deleted_ticket: bool = False,
|
|
137
|
+
) -> GlpiTicket:
|
|
138
|
+
"""Fetch one GLPI ticket by identifier.
|
|
139
|
+
|
|
140
|
+
Deleted tickets are rejected by default so single-record fetch behavior
|
|
141
|
+
stays aligned with the default async search behavior.
|
|
142
|
+
"""
|
|
143
|
+
|
|
144
|
+
response = await self._get_request(f"{TICKET_ENDPOINT}/{ticket_id}")
|
|
145
|
+
if response.status_code not in (200, 206):
|
|
146
|
+
raise ValueError(
|
|
147
|
+
f"Failed to get ticket {ticket_id}: {response.status_code}"
|
|
148
|
+
)
|
|
149
|
+
payload = _normalize_ticket_record(response.json())
|
|
150
|
+
if not isinstance(payload, dict):
|
|
151
|
+
raise ValueError(f"Unexpected GLPI ticket payload for {ticket_id}")
|
|
152
|
+
if not include_deleted_ticket and is_deleted_ticket(payload):
|
|
153
|
+
raise ValueError(
|
|
154
|
+
f"Ticket {ticket_id} is deleted and excluded from fetch results"
|
|
155
|
+
)
|
|
156
|
+
return _glpi_ticket_record(payload)
|
|
157
|
+
|
|
158
|
+
async def create_ticket(self, ticket: GlpiTicket) -> str:
|
|
159
|
+
"""Create a GLPI ticket and return the identifier assigned by GLPI.
|
|
160
|
+
|
|
161
|
+
The ticket name precondition is enforced locally before the awaited
|
|
162
|
+
request is sent, and the response must contain a created ticket ID.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
require_non_empty_text(
|
|
166
|
+
ticket.name,
|
|
167
|
+
error_message="GLPI ticket creation requires a name",
|
|
168
|
+
)
|
|
169
|
+
|
|
170
|
+
payload_data = ticket.to_api_payload(
|
|
171
|
+
entity_id=self.glpi_entity,
|
|
172
|
+
include_entity=True,
|
|
173
|
+
)
|
|
174
|
+
response = await self._post_request(TICKET_ENDPOINT, payload_data)
|
|
175
|
+
ensure_response_status(
|
|
176
|
+
response,
|
|
177
|
+
success_statuses=(200, 201),
|
|
178
|
+
failure_message="Failed to create ticket",
|
|
179
|
+
)
|
|
180
|
+
ticket_id = require_response_text(
|
|
181
|
+
response,
|
|
182
|
+
keys=("id",),
|
|
183
|
+
missing_message="GLPI create response did not include a ticket ID",
|
|
184
|
+
)
|
|
185
|
+
logger.info(
|
|
186
|
+
"GLPI API created ticket %s fields=%s",
|
|
187
|
+
ticket_id,
|
|
188
|
+
sorted(payload_data),
|
|
189
|
+
)
|
|
190
|
+
return ticket_id
|
|
191
|
+
|
|
192
|
+
async def update_ticket(
|
|
193
|
+
self,
|
|
194
|
+
ticket_id: GlpiId,
|
|
195
|
+
ticket: GlpiTicket,
|
|
196
|
+
*,
|
|
197
|
+
field_mask: tuple[str, ...] = (),
|
|
198
|
+
) -> None:
|
|
199
|
+
"""Update one GLPI ticket with the provided field changes.
|
|
200
|
+
|
|
201
|
+
Optional field masks are forwarded to the model serializer so callers
|
|
202
|
+
can restrict the update payload to a specific subset of fields.
|
|
203
|
+
"""
|
|
204
|
+
|
|
205
|
+
payload_data = ticket.to_api_payload(
|
|
206
|
+
entity_id=self.glpi_entity,
|
|
207
|
+
include_entity=False,
|
|
208
|
+
field_mask=field_mask,
|
|
209
|
+
)
|
|
210
|
+
response = await self._update_request(
|
|
211
|
+
f"{TICKET_ENDPOINT}/{ticket_id}",
|
|
212
|
+
payload_data,
|
|
213
|
+
)
|
|
214
|
+
ensure_response_status(
|
|
215
|
+
response,
|
|
216
|
+
success_statuses=(200, 204),
|
|
217
|
+
failure_message=f"Failed to update ticket {ticket_id}",
|
|
218
|
+
)
|
|
219
|
+
logger.info(
|
|
220
|
+
"GLPI API updated ticket %s fields=%s",
|
|
221
|
+
ticket_id,
|
|
222
|
+
sorted(payload_data),
|
|
223
|
+
)
|
|
224
|
+
return None
|
|
225
|
+
|
|
226
|
+
async def delete_ticket(self, ticket_id: GlpiId) -> None:
|
|
227
|
+
"""Delete one GLPI ticket by identifier.
|
|
228
|
+
|
|
229
|
+
The method logs successful deletion and returns ``None`` to match the
|
|
230
|
+
package's async mutation-helper conventions.
|
|
231
|
+
"""
|
|
232
|
+
|
|
233
|
+
response = await self._delete_request(f"{TICKET_ENDPOINT}/{ticket_id}")
|
|
234
|
+
ensure_response_status(
|
|
235
|
+
response,
|
|
236
|
+
success_statuses=(200, 204),
|
|
237
|
+
failure_message=f"Failed to delete ticket {ticket_id}",
|
|
238
|
+
)
|
|
239
|
+
logger.info("GLPI API deleted ticket %s", ticket_id)
|
|
240
|
+
return None
|
|
241
|
+
|
|
242
|
+
async def _yield_ticket_payloads(
|
|
243
|
+
self,
|
|
244
|
+
*,
|
|
245
|
+
query: str | None = None,
|
|
246
|
+
fields: list[str] | None = None,
|
|
247
|
+
sort: str | None = None,
|
|
248
|
+
batch_size: int | None = None,
|
|
249
|
+
include_deleted_ticket: bool = False,
|
|
250
|
+
) -> AsyncIterator[list[dict[str, Any]]]:
|
|
251
|
+
"""Yield paginated raw ticket payload batches from the GLPI API.
|
|
252
|
+
|
|
253
|
+
Pagination continues until the server indicates no more content or the
|
|
254
|
+
observed page-size heuristic shows that iteration is complete.
|
|
255
|
+
"""
|
|
256
|
+
|
|
257
|
+
params = build_ticket_search_params(
|
|
258
|
+
query=query,
|
|
259
|
+
fields=fields,
|
|
260
|
+
sort=sort,
|
|
261
|
+
batch_size=batch_size,
|
|
262
|
+
)
|
|
263
|
+
|
|
264
|
+
observed_page_size = batch_size
|
|
265
|
+
while True:
|
|
266
|
+
current_start = cast(int, params["start"])
|
|
267
|
+
response = await self._get_request(TICKET_ENDPOINT, params)
|
|
268
|
+
if response.status_code not in (200, 206):
|
|
269
|
+
logger.info(
|
|
270
|
+
"GLPI ticket search returned status %s (start=%d)",
|
|
271
|
+
response.status_code,
|
|
272
|
+
current_start,
|
|
273
|
+
)
|
|
274
|
+
return
|
|
275
|
+
|
|
276
|
+
batch = response.json()
|
|
277
|
+
if not isinstance(batch, list) or not batch:
|
|
278
|
+
logger.info(
|
|
279
|
+
"GLPI ticket search returned empty batch (start=%d)",
|
|
280
|
+
current_start,
|
|
281
|
+
)
|
|
282
|
+
return
|
|
283
|
+
|
|
284
|
+
logger.info(
|
|
285
|
+
"GLPI ticket search: batch of %d tickets (start=%d)",
|
|
286
|
+
len(batch),
|
|
287
|
+
current_start,
|
|
288
|
+
)
|
|
289
|
+
result_batch, deleted_count = filter_ticket_search_batch(
|
|
290
|
+
batch,
|
|
291
|
+
include_deleted_ticket=include_deleted_ticket,
|
|
292
|
+
)
|
|
293
|
+
if deleted_count:
|
|
294
|
+
logger.info(
|
|
295
|
+
"GLPI ticket search: excluded %d deleted tickets (start=%d)",
|
|
296
|
+
deleted_count,
|
|
297
|
+
current_start,
|
|
298
|
+
)
|
|
299
|
+
if result_batch:
|
|
300
|
+
yield result_batch
|
|
301
|
+
|
|
302
|
+
next_start, observed_page_size, should_continue = (
|
|
303
|
+
advance_ticket_search_pagination(
|
|
304
|
+
current_start=current_start,
|
|
305
|
+
page_size=len(batch),
|
|
306
|
+
content_range=response.headers.get("Content-Range", ""),
|
|
307
|
+
observed_page_size=observed_page_size,
|
|
308
|
+
)
|
|
309
|
+
)
|
|
310
|
+
params["start"] = next_start
|
|
311
|
+
if not should_continue:
|
|
312
|
+
return
|