glpi-python-client 0.3.0__py3-none-any.whl → 0.3.2__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 +1 -1
- glpi_python_client/clients/api/administration/_entity.py +44 -0
- glpi_python_client/clients/api/administration/_user.py +47 -0
- glpi_python_client/clients/api/assistance/_ticket.py +52 -0
- glpi_python_client/clients/async_client.py +3 -1
- glpi_python_client/clients/commons/_async_bridge.py +76 -3
- glpi_python_client/clients/custom/__init__.py +2 -0
- glpi_python_client/clients/custom/_pagination_async.py +164 -0
- glpi_python_client/clients/custom/_statistics.py +431 -18
- glpi_python_client/clients/custom/_statistics_async.py +444 -18
- glpi_python_client/clients/custom/tests/test_statistics.py +368 -0
- glpi_python_client/clients/custom/tests/test_statistics_async.py +232 -0
- glpi_python_client/clients/tests/test_api_coverage.py +175 -0
- glpi_python_client/clients/tests/test_async_branches.py +564 -0
- glpi_python_client/clients/tests/test_parity.py +10 -4
- {glpi_python_client-0.3.0.dist-info → glpi_python_client-0.3.2.dist-info}/METADATA +1 -1
- {glpi_python_client-0.3.0.dist-info → glpi_python_client-0.3.2.dist-info}/RECORD +19 -17
- {glpi_python_client-0.3.0.dist-info → glpi_python_client-0.3.2.dist-info}/WHEEL +0 -0
- {glpi_python_client-0.3.0.dist-info → glpi_python_client-0.3.2.dist-info}/licenses/LICENSE +0 -0
glpi_python_client/__init__.py
CHANGED
|
@@ -7,6 +7,8 @@ client's ``GLPI-Entity`` header so cross-entity lookups remain possible.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from collections.abc import Iterator
|
|
11
|
+
|
|
10
12
|
from glpi_python_client.clients.commons._constants import ENTITY_ENDPOINT, GlpiId
|
|
11
13
|
from glpi_python_client.clients.commons._transport import TransportMixin
|
|
12
14
|
from glpi_python_client.models.api_schema.administration._entity import (
|
|
@@ -54,6 +56,48 @@ class EntityMixin(TransportMixin):
|
|
|
54
56
|
ENTITY_ENDPOINT, GetEntity, params=params, skip_entity=True
|
|
55
57
|
)
|
|
56
58
|
|
|
59
|
+
def iter_search_entities(
|
|
60
|
+
self,
|
|
61
|
+
rsql_filter: str = "",
|
|
62
|
+
*,
|
|
63
|
+
batch_size: int = 50,
|
|
64
|
+
) -> Iterator[list[GetEntity]]:
|
|
65
|
+
"""Yield successive pages of GLPI entities until exhausted.
|
|
66
|
+
|
|
67
|
+
The generator drives pagination automatically by advancing the
|
|
68
|
+
``start`` offset after each batch. Iteration stops when the server
|
|
69
|
+
returns fewer items than ``batch_size``, which signals the last page.
|
|
70
|
+
Entity calls bypass the ``GLPI-Entity`` header so cross-entity
|
|
71
|
+
lookups remain possible.
|
|
72
|
+
|
|
73
|
+
Parameters
|
|
74
|
+
----------
|
|
75
|
+
rsql_filter : str, optional
|
|
76
|
+
Raw RSQL filter forwarded as the ``filter`` query parameter.
|
|
77
|
+
Empty by default, which lists every accessible entity.
|
|
78
|
+
batch_size : int, optional
|
|
79
|
+
Number of records requested per page (default 50).
|
|
80
|
+
|
|
81
|
+
Yields
|
|
82
|
+
------
|
|
83
|
+
list[GetEntity]
|
|
84
|
+
One page of entities per iteration. The last yielded batch may
|
|
85
|
+
be shorter than ``batch_size``.
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
start = 0
|
|
89
|
+
while True:
|
|
90
|
+
batch = self.search_entities(
|
|
91
|
+
rsql_filter,
|
|
92
|
+
limit=batch_size,
|
|
93
|
+
start=start,
|
|
94
|
+
)
|
|
95
|
+
if batch:
|
|
96
|
+
yield batch
|
|
97
|
+
if len(batch) < batch_size:
|
|
98
|
+
break
|
|
99
|
+
start += batch_size
|
|
100
|
+
|
|
57
101
|
def get_entity(self, entity_id: GlpiId) -> GetEntity:
|
|
58
102
|
"""Fetch one GLPI entity by identifier.
|
|
59
103
|
|
|
@@ -8,6 +8,8 @@ the Synchronous transport mixin for HTTP dispatch.
|
|
|
8
8
|
|
|
9
9
|
from __future__ import annotations
|
|
10
10
|
|
|
11
|
+
from collections.abc import Iterator
|
|
12
|
+
|
|
11
13
|
from glpi_python_client.clients.commons._constants import USER_ENDPOINT, GlpiId
|
|
12
14
|
from glpi_python_client.clients.commons._transport import TransportMixin
|
|
13
15
|
from glpi_python_client.models.api_schema.administration._user import (
|
|
@@ -63,6 +65,51 @@ class UserMixin(TransportMixin):
|
|
|
63
65
|
USER_ENDPOINT, GetUser, params=params, skip_entity=skip_entity
|
|
64
66
|
)
|
|
65
67
|
|
|
68
|
+
def iter_search_users(
|
|
69
|
+
self,
|
|
70
|
+
rsql_filter: str = "",
|
|
71
|
+
*,
|
|
72
|
+
batch_size: int = 50,
|
|
73
|
+
skip_entity: bool = False,
|
|
74
|
+
) -> Iterator[list[GetUser]]:
|
|
75
|
+
"""Yield successive pages of GLPI users until exhausted.
|
|
76
|
+
|
|
77
|
+
The generator drives pagination automatically by advancing the
|
|
78
|
+
``start`` offset after each batch. Iteration stops when the server
|
|
79
|
+
returns fewer items than ``batch_size``, which signals the last page.
|
|
80
|
+
|
|
81
|
+
Parameters
|
|
82
|
+
----------
|
|
83
|
+
rsql_filter : str, optional
|
|
84
|
+
Raw RSQL filter forwarded as the ``filter`` query parameter.
|
|
85
|
+
Empty by default, which lists every visible user.
|
|
86
|
+
batch_size : int, optional
|
|
87
|
+
Number of records requested per page (default 50).
|
|
88
|
+
skip_entity : bool, optional
|
|
89
|
+
When ``True`` the ``GLPI-Entity`` header is omitted so the
|
|
90
|
+
search spans every entity the caller has access to.
|
|
91
|
+
|
|
92
|
+
Yields
|
|
93
|
+
------
|
|
94
|
+
list[GetUser]
|
|
95
|
+
One page of users per iteration. The last yielded batch may
|
|
96
|
+
be shorter than ``batch_size``.
|
|
97
|
+
"""
|
|
98
|
+
|
|
99
|
+
start = 0
|
|
100
|
+
while True:
|
|
101
|
+
batch = self.search_users(
|
|
102
|
+
rsql_filter,
|
|
103
|
+
limit=batch_size,
|
|
104
|
+
start=start,
|
|
105
|
+
skip_entity=skip_entity,
|
|
106
|
+
)
|
|
107
|
+
if batch:
|
|
108
|
+
yield batch
|
|
109
|
+
if len(batch) < batch_size:
|
|
110
|
+
break
|
|
111
|
+
start += batch_size
|
|
112
|
+
|
|
66
113
|
def get_user(self, user_id: GlpiId) -> GetUser:
|
|
67
114
|
"""Fetch one GLPI user by identifier.
|
|
68
115
|
|
|
@@ -6,6 +6,8 @@ GLPI ticket resource using the ``api_schema`` Pydantic models.
|
|
|
6
6
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
|
+
from collections.abc import Iterator
|
|
10
|
+
|
|
9
11
|
from glpi_python_client.clients.commons._constants import TICKET_ENDPOINT, GlpiId
|
|
10
12
|
from glpi_python_client.clients.commons._transport import TransportMixin
|
|
11
13
|
from glpi_python_client.models.api_schema.assistance._ticket import (
|
|
@@ -68,6 +70,56 @@ class TicketMixin(TransportMixin):
|
|
|
68
70
|
params["fields"] = ",".join(fields)
|
|
69
71
|
return self._resource_list(TICKET_ENDPOINT, GetTicket, params=params)
|
|
70
72
|
|
|
73
|
+
def iter_search_tickets(
|
|
74
|
+
self,
|
|
75
|
+
rsql_filter: str = "",
|
|
76
|
+
*,
|
|
77
|
+
batch_size: int = 50,
|
|
78
|
+
sort: str | None = None,
|
|
79
|
+
fields: tuple[str, ...] = (),
|
|
80
|
+
) -> Iterator[list[GetTicket]]:
|
|
81
|
+
"""Yield successive pages of GLPI tickets until exhausted.
|
|
82
|
+
|
|
83
|
+
The generator drives pagination automatically by advancing the
|
|
84
|
+
``start`` offset after each batch. Iteration stops when the server
|
|
85
|
+
returns fewer items than ``batch_size``, which signals the last page.
|
|
86
|
+
|
|
87
|
+
Parameters
|
|
88
|
+
----------
|
|
89
|
+
rsql_filter : str, optional
|
|
90
|
+
Raw RSQL filter forwarded as the ``filter`` query parameter.
|
|
91
|
+
Empty by default, which lists every visible ticket.
|
|
92
|
+
batch_size : int, optional
|
|
93
|
+
Number of records requested per page (default 50). Acts as
|
|
94
|
+
the ``limit`` parameter on each underlying
|
|
95
|
+
:meth:`search_tickets` call.
|
|
96
|
+
sort : str | None, optional
|
|
97
|
+
``sort`` query parameter forwarded as-is to each page request.
|
|
98
|
+
fields : tuple[str, ...], optional
|
|
99
|
+
Restricted set of contract field names to request.
|
|
100
|
+
|
|
101
|
+
Yields
|
|
102
|
+
------
|
|
103
|
+
list[GetTicket]
|
|
104
|
+
One page of tickets per iteration. The last yielded batch may
|
|
105
|
+
be shorter than ``batch_size``.
|
|
106
|
+
"""
|
|
107
|
+
|
|
108
|
+
start = 0
|
|
109
|
+
while True:
|
|
110
|
+
batch = self.search_tickets(
|
|
111
|
+
rsql_filter,
|
|
112
|
+
limit=batch_size,
|
|
113
|
+
start=start,
|
|
114
|
+
sort=sort,
|
|
115
|
+
fields=fields,
|
|
116
|
+
)
|
|
117
|
+
if batch:
|
|
118
|
+
yield batch
|
|
119
|
+
if len(batch) < batch_size:
|
|
120
|
+
break
|
|
121
|
+
start += batch_size
|
|
122
|
+
|
|
71
123
|
def get_ticket(self, ticket_id: GlpiId) -> GetTicket:
|
|
72
124
|
"""Fetch one GLPI ticket by identifier.
|
|
73
125
|
|
|
@@ -50,6 +50,7 @@ from glpi_python_client.clients.commons._config import (
|
|
|
50
50
|
build_client_resources,
|
|
51
51
|
)
|
|
52
52
|
from glpi_python_client.clients.commons._transport import TransportMixin
|
|
53
|
+
from glpi_python_client.clients.custom._pagination_async import AsyncPaginationMixin
|
|
53
54
|
from glpi_python_client.clients.custom._statistics_async import AsyncStatisticsMixin
|
|
54
55
|
from glpi_python_client.clients.custom._ticket_context_async import (
|
|
55
56
|
AsyncTicketContextMixin,
|
|
@@ -61,8 +62,9 @@ if TYPE_CHECKING:
|
|
|
61
62
|
logger = logging.getLogger(__name__)
|
|
62
63
|
|
|
63
64
|
|
|
64
|
-
class AsyncGlpiClient(
|
|
65
|
+
class AsyncGlpiClient( # type: ignore[misc]
|
|
65
66
|
AsyncBridge,
|
|
67
|
+
AsyncPaginationMixin,
|
|
66
68
|
TicketMixin,
|
|
67
69
|
TicketTaskMixin,
|
|
68
70
|
FollowupMixin,
|
|
@@ -22,6 +22,23 @@ Concurrency notes
|
|
|
22
22
|
releases the awaiter immediately, but the in-flight HTTP request keeps
|
|
23
23
|
running on the worker thread until ``requests`` returns. This matches
|
|
24
24
|
the behaviour of the original async client.
|
|
25
|
+
|
|
26
|
+
Known limitation — internal ``self``-calls
|
|
27
|
+
------------------------------------------
|
|
28
|
+
The bridge wraps *every* public method on the async client class. As a
|
|
29
|
+
result, when a synchronous body that is running inside a worker thread
|
|
30
|
+
calls another public method through ``self`` (e.g.
|
|
31
|
+
``self.search_tickets(...)``), it resolves to the *bridge-wrapped*
|
|
32
|
+
coroutine, not the synchronous function. Calling a coroutine without
|
|
33
|
+
``await`` produces a dangling coroutine object, not data.
|
|
34
|
+
|
|
35
|
+
Any sync method (or generator) that internally calls other public
|
|
36
|
+
methods through ``self`` must therefore be given a hand-written async
|
|
37
|
+
override that ``await``s (or ``async for``s) those calls on the event
|
|
38
|
+
loop. The convention used in this codebase is to place such overrides
|
|
39
|
+
in a ``_*_async.py`` companion module (e.g.
|
|
40
|
+
``clients/custom/_statistics_async.py``) and wire them into the async
|
|
41
|
+
client's MRO *before* the sync mixin that defines the original method.
|
|
25
42
|
"""
|
|
26
43
|
|
|
27
44
|
from __future__ import annotations
|
|
@@ -33,6 +50,19 @@ from collections.abc import Callable
|
|
|
33
50
|
from concurrent.futures import Executor
|
|
34
51
|
from typing import Any
|
|
35
52
|
|
|
53
|
+
# Sentinel used by the async-generator bridge to signal exhaustion without
|
|
54
|
+
# propagating StopIteration through a coroutine (which PEP 479 forbids).
|
|
55
|
+
_STOPPED: object = object()
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def _next_or_stopped(gen: Any) -> Any:
|
|
59
|
+
"""Return the next item from *gen* or ``_STOPPED`` when exhausted."""
|
|
60
|
+
|
|
61
|
+
try:
|
|
62
|
+
return next(gen)
|
|
63
|
+
except StopIteration:
|
|
64
|
+
return _STOPPED
|
|
65
|
+
|
|
36
66
|
|
|
37
67
|
class AsyncBridge:
|
|
38
68
|
"""Base class that converts inherited sync methods into coroutines.
|
|
@@ -78,14 +108,22 @@ class AsyncBridge:
|
|
|
78
108
|
continue
|
|
79
109
|
if not callable(member) or inspect.iscoroutinefunction(member):
|
|
80
110
|
continue
|
|
111
|
+
if inspect.isasyncgenfunction(member):
|
|
112
|
+
continue
|
|
81
113
|
# Skip if the subclass already overrides the method with
|
|
82
|
-
# a coroutine function (for example async fan-outs).
|
|
114
|
+
# a coroutine function or async generator (for example async fan-outs).
|
|
83
115
|
existing = getattr(cls, name, None)
|
|
84
|
-
if existing is not None and
|
|
116
|
+
if existing is not None and (
|
|
117
|
+
inspect.iscoroutinefunction(existing)
|
|
118
|
+
or inspect.isasyncgenfunction(existing)
|
|
119
|
+
):
|
|
85
120
|
seen.add(name)
|
|
86
121
|
continue
|
|
87
122
|
seen.add(name)
|
|
88
|
-
|
|
123
|
+
if inspect.isgeneratorfunction(member):
|
|
124
|
+
setattr(cls, name, _make_async_generator_wrapper(member))
|
|
125
|
+
else:
|
|
126
|
+
setattr(cls, name, _make_async_wrapper(member))
|
|
89
127
|
|
|
90
128
|
|
|
91
129
|
def _make_async_wrapper(sync_func: Callable[..., Any]) -> Callable[..., Any]:
|
|
@@ -115,4 +153,39 @@ def _make_async_wrapper(sync_func: Callable[..., Any]) -> Callable[..., Any]:
|
|
|
115
153
|
return wrapper
|
|
116
154
|
|
|
117
155
|
|
|
156
|
+
def _make_async_generator_wrapper(sync_func: Callable[..., Any]) -> Callable[..., Any]:
|
|
157
|
+
"""Return an async generator wrapper for a synchronous generator function.
|
|
158
|
+
|
|
159
|
+
Each call to ``next()`` on the underlying sync generator is dispatched
|
|
160
|
+
to a worker thread so that the blocking HTTP call inside the generator
|
|
161
|
+
body does not block the event loop.
|
|
162
|
+
|
|
163
|
+
Parameters
|
|
164
|
+
----------
|
|
165
|
+
sync_func : Callable[..., Any]
|
|
166
|
+
Synchronous generator function inherited from a sync mixin.
|
|
167
|
+
|
|
168
|
+
Returns
|
|
169
|
+
-------
|
|
170
|
+
Callable[..., Any]
|
|
171
|
+
Async generator function that yields the same items as the
|
|
172
|
+
synchronous generator, one batch at a time, off the event loop.
|
|
173
|
+
"""
|
|
174
|
+
|
|
175
|
+
@functools.wraps(sync_func)
|
|
176
|
+
async def wrapper(self: AsyncBridge, *args: Any, **kwargs: Any) -> Any:
|
|
177
|
+
gen = sync_func(self, *args, **kwargs)
|
|
178
|
+
while True:
|
|
179
|
+
if self._executor is not None:
|
|
180
|
+
loop = asyncio.get_running_loop()
|
|
181
|
+
item = await loop.run_in_executor(self._executor, _next_or_stopped, gen)
|
|
182
|
+
else:
|
|
183
|
+
item = await asyncio.to_thread(_next_or_stopped, gen)
|
|
184
|
+
if item is _STOPPED:
|
|
185
|
+
return
|
|
186
|
+
yield item
|
|
187
|
+
|
|
188
|
+
return wrapper
|
|
189
|
+
|
|
190
|
+
|
|
118
191
|
__all__ = ["AsyncBridge"]
|
|
@@ -17,6 +17,7 @@ composed into :class:`~glpi_python_client.clients.AsyncGlpiClient`.
|
|
|
17
17
|
|
|
18
18
|
from __future__ import annotations
|
|
19
19
|
|
|
20
|
+
from glpi_python_client.clients.custom._pagination_async import AsyncPaginationMixin
|
|
20
21
|
from glpi_python_client.clients.custom._statistics import StatisticsMixin
|
|
21
22
|
from glpi_python_client.clients.custom._statistics_async import AsyncStatisticsMixin
|
|
22
23
|
from glpi_python_client.clients.custom._ticket_context import TicketContextMixin
|
|
@@ -25,6 +26,7 @@ from glpi_python_client.clients.custom._ticket_context_async import (
|
|
|
25
26
|
)
|
|
26
27
|
|
|
27
28
|
__all__ = [
|
|
29
|
+
"AsyncPaginationMixin",
|
|
28
30
|
"AsyncStatisticsMixin",
|
|
29
31
|
"AsyncTicketContextMixin",
|
|
30
32
|
"StatisticsMixin",
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
"""Asynchronous overrides for the three paginated search generators.
|
|
2
|
+
|
|
3
|
+
Each of the three ``iter_search_*`` generators in the API mixins
|
|
4
|
+
delegates its pagination loop to a ``self.search_*()`` call. When the
|
|
5
|
+
:class:`~glpi_python_client.clients.commons._async_bridge.AsyncBridge`
|
|
6
|
+
wraps a sync generator it drives ``next()`` on the generator object inside
|
|
7
|
+
a worker thread. Inside that thread ``self`` is still the
|
|
8
|
+
:class:`~glpi_python_client.clients.AsyncGlpiClient` instance, so
|
|
9
|
+
``self.search_tickets(...)`` resolves to the bridge-wrapped coroutine
|
|
10
|
+
function and calling it without ``await`` returns a coroutine object
|
|
11
|
+
instead of the expected list — triggering a
|
|
12
|
+
``RuntimeWarning: coroutine … was never awaited`` and a 500 response.
|
|
13
|
+
|
|
14
|
+
This mixin replaces all three generators with native async generators that
|
|
15
|
+
``await self.search_*(...)`` directly on the event loop.
|
|
16
|
+
|
|
17
|
+
The mixin must be positioned **before** ``TicketMixin``, ``UserMixin``, and
|
|
18
|
+
``EntityMixin`` in the :class:`~glpi_python_client.clients.AsyncGlpiClient`
|
|
19
|
+
base list so that the bridge's ``__init_subclass__`` hook finds the async
|
|
20
|
+
generator via ``getattr`` before it would otherwise wrap the sync version.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
from __future__ import annotations
|
|
24
|
+
|
|
25
|
+
from collections.abc import AsyncIterator
|
|
26
|
+
|
|
27
|
+
from glpi_python_client.models.api_schema.administration._entity import GetEntity
|
|
28
|
+
from glpi_python_client.models.api_schema.administration._user import GetUser
|
|
29
|
+
from glpi_python_client.models.api_schema.assistance._ticket import GetTicket
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class AsyncPaginationMixin:
|
|
33
|
+
"""Async generator overrides for the three paginated search helpers.
|
|
34
|
+
|
|
35
|
+
Each override re-implements the simple ``start``-advancing loop of
|
|
36
|
+
the synchronous counterpart but ``await``\\ s the underlying
|
|
37
|
+
``search_*`` call so it runs on the event loop rather than inside a
|
|
38
|
+
worker-thread-dispatched ``next()`` call where the coroutine would
|
|
39
|
+
be dropped on the floor.
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
async def iter_search_tickets(
|
|
43
|
+
self,
|
|
44
|
+
rsql_filter: str = "",
|
|
45
|
+
*,
|
|
46
|
+
batch_size: int = 50,
|
|
47
|
+
sort: str | None = None,
|
|
48
|
+
fields: tuple[str, ...] = (),
|
|
49
|
+
) -> AsyncIterator[list[GetTicket]]:
|
|
50
|
+
"""Yield successive pages of GLPI tickets until exhausted.
|
|
51
|
+
|
|
52
|
+
Parameters
|
|
53
|
+
----------
|
|
54
|
+
rsql_filter : str, optional
|
|
55
|
+
Raw RSQL filter forwarded as the ``filter`` query parameter.
|
|
56
|
+
Empty by default, which lists every visible ticket.
|
|
57
|
+
batch_size : int, optional
|
|
58
|
+
Number of records requested per page (default 50).
|
|
59
|
+
sort : str | None, optional
|
|
60
|
+
``sort`` query parameter forwarded as-is to each page request.
|
|
61
|
+
fields : tuple[str, ...], optional
|
|
62
|
+
Restricted set of contract field names to request.
|
|
63
|
+
|
|
64
|
+
Yields
|
|
65
|
+
------
|
|
66
|
+
list[GetTicket]
|
|
67
|
+
One page of tickets per iteration. The last yielded batch may
|
|
68
|
+
be shorter than ``batch_size``.
|
|
69
|
+
"""
|
|
70
|
+
|
|
71
|
+
start = 0
|
|
72
|
+
while True:
|
|
73
|
+
batch: list[GetTicket] = await self.search_tickets( # type: ignore[attr-defined]
|
|
74
|
+
rsql_filter,
|
|
75
|
+
limit=batch_size,
|
|
76
|
+
start=start,
|
|
77
|
+
sort=sort,
|
|
78
|
+
fields=fields,
|
|
79
|
+
)
|
|
80
|
+
if batch:
|
|
81
|
+
yield batch
|
|
82
|
+
if len(batch) < batch_size:
|
|
83
|
+
break
|
|
84
|
+
start += batch_size
|
|
85
|
+
|
|
86
|
+
async def iter_search_users(
|
|
87
|
+
self,
|
|
88
|
+
rsql_filter: str = "",
|
|
89
|
+
*,
|
|
90
|
+
batch_size: int = 50,
|
|
91
|
+
skip_entity: bool = False,
|
|
92
|
+
) -> AsyncIterator[list[GetUser]]:
|
|
93
|
+
"""Yield successive pages of GLPI users until exhausted.
|
|
94
|
+
|
|
95
|
+
Parameters
|
|
96
|
+
----------
|
|
97
|
+
rsql_filter : str, optional
|
|
98
|
+
Raw RSQL filter forwarded as the ``filter`` query parameter.
|
|
99
|
+
Empty by default, which lists every visible user.
|
|
100
|
+
batch_size : int, optional
|
|
101
|
+
Number of records requested per page (default 50).
|
|
102
|
+
skip_entity : bool, optional
|
|
103
|
+
When ``True`` the ``GLPI-Entity`` header is omitted so the
|
|
104
|
+
search spans every entity the caller has access to.
|
|
105
|
+
|
|
106
|
+
Yields
|
|
107
|
+
------
|
|
108
|
+
list[GetUser]
|
|
109
|
+
One page of users per iteration. The last yielded batch may
|
|
110
|
+
be shorter than ``batch_size``.
|
|
111
|
+
"""
|
|
112
|
+
|
|
113
|
+
start = 0
|
|
114
|
+
while True:
|
|
115
|
+
batch: list[GetUser] = await self.search_users( # type: ignore[attr-defined]
|
|
116
|
+
rsql_filter,
|
|
117
|
+
limit=batch_size,
|
|
118
|
+
start=start,
|
|
119
|
+
skip_entity=skip_entity,
|
|
120
|
+
)
|
|
121
|
+
if batch:
|
|
122
|
+
yield batch
|
|
123
|
+
if len(batch) < batch_size:
|
|
124
|
+
break
|
|
125
|
+
start += batch_size
|
|
126
|
+
|
|
127
|
+
async def iter_search_entities(
|
|
128
|
+
self,
|
|
129
|
+
rsql_filter: str = "",
|
|
130
|
+
*,
|
|
131
|
+
batch_size: int = 50,
|
|
132
|
+
) -> AsyncIterator[list[GetEntity]]:
|
|
133
|
+
"""Yield successive pages of GLPI entities until exhausted.
|
|
134
|
+
|
|
135
|
+
Parameters
|
|
136
|
+
----------
|
|
137
|
+
rsql_filter : str, optional
|
|
138
|
+
Raw RSQL filter forwarded as the ``filter`` query parameter.
|
|
139
|
+
Empty by default, which lists every accessible entity.
|
|
140
|
+
batch_size : int, optional
|
|
141
|
+
Number of records requested per page (default 50).
|
|
142
|
+
|
|
143
|
+
Yields
|
|
144
|
+
------
|
|
145
|
+
list[GetEntity]
|
|
146
|
+
One page of entities per iteration. The last yielded batch
|
|
147
|
+
may be shorter than ``batch_size``.
|
|
148
|
+
"""
|
|
149
|
+
|
|
150
|
+
start = 0
|
|
151
|
+
while True:
|
|
152
|
+
batch: list[GetEntity] = await self.search_entities( # type: ignore[attr-defined]
|
|
153
|
+
rsql_filter,
|
|
154
|
+
limit=batch_size,
|
|
155
|
+
start=start,
|
|
156
|
+
)
|
|
157
|
+
if batch:
|
|
158
|
+
yield batch
|
|
159
|
+
if len(batch) < batch_size:
|
|
160
|
+
break
|
|
161
|
+
start += batch_size
|
|
162
|
+
|
|
163
|
+
|
|
164
|
+
__all__ = ["AsyncPaginationMixin"]
|