glpi-python-client 0.3.0__py3-none-any.whl → 0.3.1__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.
@@ -72,7 +72,7 @@ from glpi_python_client.models import (
72
72
  TicketMarkdownOptions,
73
73
  )
74
74
 
75
- __version__ = "0.3.0"
75
+ __version__ = "0.3.1"
76
76
 
77
77
  __all__ = [
78
78
  "AsyncGlpiClient",
@@ -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
 
@@ -33,6 +33,19 @@ from collections.abc import Callable
33
33
  from concurrent.futures import Executor
34
34
  from typing import Any
35
35
 
36
+ # Sentinel used by the async-generator bridge to signal exhaustion without
37
+ # propagating StopIteration through a coroutine (which PEP 479 forbids).
38
+ _STOPPED: object = object()
39
+
40
+
41
+ def _next_or_stopped(gen: Any) -> Any:
42
+ """Return the next item from *gen* or ``_STOPPED`` when exhausted."""
43
+
44
+ try:
45
+ return next(gen)
46
+ except StopIteration:
47
+ return _STOPPED
48
+
36
49
 
37
50
  class AsyncBridge:
38
51
  """Base class that converts inherited sync methods into coroutines.
@@ -78,14 +91,22 @@ class AsyncBridge:
78
91
  continue
79
92
  if not callable(member) or inspect.iscoroutinefunction(member):
80
93
  continue
94
+ if inspect.isasyncgenfunction(member):
95
+ continue
81
96
  # Skip if the subclass already overrides the method with
82
- # a coroutine function (for example async fan-outs).
97
+ # a coroutine function or async generator (for example async fan-outs).
83
98
  existing = getattr(cls, name, None)
84
- if existing is not None and inspect.iscoroutinefunction(existing):
99
+ if existing is not None and (
100
+ inspect.iscoroutinefunction(existing)
101
+ or inspect.isasyncgenfunction(existing)
102
+ ):
85
103
  seen.add(name)
86
104
  continue
87
105
  seen.add(name)
88
- setattr(cls, name, _make_async_wrapper(member))
106
+ if inspect.isgeneratorfunction(member):
107
+ setattr(cls, name, _make_async_generator_wrapper(member))
108
+ else:
109
+ setattr(cls, name, _make_async_wrapper(member))
89
110
 
90
111
 
91
112
  def _make_async_wrapper(sync_func: Callable[..., Any]) -> Callable[..., Any]:
@@ -115,4 +136,39 @@ def _make_async_wrapper(sync_func: Callable[..., Any]) -> Callable[..., Any]:
115
136
  return wrapper
116
137
 
117
138
 
139
+ def _make_async_generator_wrapper(sync_func: Callable[..., Any]) -> Callable[..., Any]:
140
+ """Return an async generator wrapper for a synchronous generator function.
141
+
142
+ Each call to ``next()`` on the underlying sync generator is dispatched
143
+ to a worker thread so that the blocking HTTP call inside the generator
144
+ body does not block the event loop.
145
+
146
+ Parameters
147
+ ----------
148
+ sync_func : Callable[..., Any]
149
+ Synchronous generator function inherited from a sync mixin.
150
+
151
+ Returns
152
+ -------
153
+ Callable[..., Any]
154
+ Async generator function that yields the same items as the
155
+ synchronous generator, one batch at a time, off the event loop.
156
+ """
157
+
158
+ @functools.wraps(sync_func)
159
+ async def wrapper(self: AsyncBridge, *args: Any, **kwargs: Any) -> Any:
160
+ gen = sync_func(self, *args, **kwargs)
161
+ while True:
162
+ if self._executor is not None:
163
+ loop = asyncio.get_running_loop()
164
+ item = await loop.run_in_executor(self._executor, _next_or_stopped, gen)
165
+ else:
166
+ item = await asyncio.to_thread(_next_or_stopped, gen)
167
+ if item is _STOPPED:
168
+ return
169
+ yield item
170
+
171
+ return wrapper
172
+
173
+
118
174
  __all__ = ["AsyncBridge"]