glpi-python-client 0.3.1__tar.gz → 0.3.2__tar.gz

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 (130) hide show
  1. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/PKG-INFO +1 -1
  2. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/user_guide.rst +34 -10
  3. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/__init__.py +1 -1
  4. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/async_client.py +3 -1
  5. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_async_bridge.py +17 -0
  6. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/__init__.py +2 -0
  7. glpi_python_client-0.3.2/glpi_python_client/clients/custom/_pagination_async.py +164 -0
  8. glpi_python_client-0.3.2/glpi_python_client/clients/custom/_statistics_async.py +495 -0
  9. glpi_python_client-0.3.2/glpi_python_client/clients/tests/test_async_branches.py +701 -0
  10. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/pyproject.toml +1 -1
  11. glpi_python_client-0.3.1/glpi_python_client/clients/custom/_statistics_async.py +0 -250
  12. glpi_python_client-0.3.1/glpi_python_client/clients/tests/test_async_branches.py +0 -195
  13. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/.gitignore +0 -0
  14. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/.pre-commit-config.yaml +0 -0
  15. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/.readthedocs.yaml +0 -0
  16. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/CONTRIBUTING.md +0 -0
  17. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/LICENSE +0 -0
  18. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/README.md +0 -0
  19. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/_static/.gitkeep +0 -0
  20. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/api_reference.rst +0 -0
  21. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/conf.py +0 -0
  22. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/development.md +0 -0
  23. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/development_rtd.rst +0 -0
  24. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/glpi_api_contract.json +0 -0
  25. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/index.rst +0 -0
  26. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/installation.rst +0 -0
  27. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/publishing.md +0 -0
  28. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/publishing_rtd.rst +0 -0
  29. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/docs/sponsoring.rst +0 -0
  30. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/auth/__init__.py +0 -0
  31. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/auth/_v1_session.py +0 -0
  32. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/auth/auth.py +0 -0
  33. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/auth/tests/test_auth.py +0 -0
  34. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/auth/tests/test_v1_session.py +0 -0
  35. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/__init__.py +0 -0
  36. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/__init__.py +0 -0
  37. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/administration/__init__.py +0 -0
  38. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/administration/_entity.py +0 -0
  39. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/administration/_user.py +0 -0
  40. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/__init__.py +0 -0
  41. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/_team.py +0 -0
  42. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/_ticket.py +0 -0
  43. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/__init__.py +0 -0
  44. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/_document.py +0 -0
  45. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/_followup.py +0 -0
  46. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/_solution.py +0 -0
  47. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/_task.py +0 -0
  48. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/dropdowns/__init__.py +0 -0
  49. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/dropdowns/_location.py +0 -0
  50. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/management/__init__.py +0 -0
  51. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/management/_document.py +0 -0
  52. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/__init__.py +0 -0
  53. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_config.py +0 -0
  54. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_constants.py +0 -0
  55. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_errors.py +0 -0
  56. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_filters.py +0 -0
  57. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_http.py +0 -0
  58. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_payloads.py +0 -0
  59. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_transport.py +0 -0
  60. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/__init__.py +0 -0
  61. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_errors.py +0 -0
  62. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_filters.py +0 -0
  63. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_http.py +0 -0
  64. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_payloads.py +0 -0
  65. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_transport.py +0 -0
  66. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/_statistics.py +0 -0
  67. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/_ticket_context.py +0 -0
  68. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/_ticket_context_async.py +0 -0
  69. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/tests/test_statistics.py +0 -0
  70. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/tests/test_statistics_async.py +0 -0
  71. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/tests/test_ticket_context.py +0 -0
  72. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/sync_client.py +0 -0
  73. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/__init__.py +0 -0
  74. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_api_coverage.py +0 -0
  75. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_async_smoke.py +0 -0
  76. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_glpi_client.py +0 -0
  77. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_parity.py +0 -0
  78. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_smoke.py +0 -0
  79. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/content/__init__.py +0 -0
  80. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/content/conversion.py +0 -0
  81. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/content/tests/__init__.py +0 -0
  82. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/content/tests/test_conversion.py +0 -0
  83. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/__init__.py +0 -0
  84. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/_base.py +0 -0
  85. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/__init__.py +0 -0
  86. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/_common.py +0 -0
  87. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/_content.py +0 -0
  88. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/__init__.py +0 -0
  89. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/_entity.py +0 -0
  90. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/_user.py +0 -0
  91. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/tests/__init__.py +0 -0
  92. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/tests/test_administration_schemas.py +0 -0
  93. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/__init__.py +0 -0
  94. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/_team.py +0 -0
  95. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/_ticket.py +0 -0
  96. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/tests/__init__.py +0 -0
  97. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/tests/test_assistance_schemas.py +0 -0
  98. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/tests/test_content_roundtrip.py +0 -0
  99. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/__init__.py +0 -0
  100. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/_document.py +0 -0
  101. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/_followup.py +0 -0
  102. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/_solution.py +0 -0
  103. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/_task.py +0 -0
  104. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/tests/__init__.py +0 -0
  105. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/tests/test_timeline_schemas.py +0 -0
  106. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/dropdowns/__init__.py +0 -0
  107. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/dropdowns/_location.py +0 -0
  108. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/dropdowns/tests/__init__.py +0 -0
  109. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/dropdowns/tests/test_dropdowns_schemas.py +0 -0
  110. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/enums.py +0 -0
  111. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/management/__init__.py +0 -0
  112. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/management/_document.py +0 -0
  113. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/management/tests/__init__.py +0 -0
  114. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/management/tests/test_management_schemas.py +0 -0
  115. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/custom_schema/__init__.py +0 -0
  116. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/custom_schema/_ticket_context.py +0 -0
  117. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/custom_schema/tests/__init__.py +0 -0
  118. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/models/custom_schema/tests/test_ticket_context.py +0 -0
  119. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/py.typed +0 -0
  120. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/testing/__init__.py +0 -0
  121. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/testing/fixtures.py +0 -0
  122. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/glpi_python_client/testing/utils.py +0 -0
  123. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/skills/README.md +0 -0
  124. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/skills/glpi-client-setup/SKILL.md +0 -0
  125. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/skills/glpi-document-workflow/SKILL.md +0 -0
  126. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/skills/glpi-reporting-and-context/SKILL.md +0 -0
  127. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/skills/glpi-team-members/SKILL.md +0 -0
  128. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/skills/glpi-ticket-timeline/SKILL.md +0 -0
  129. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/skills/glpi-ticket-workflow/SKILL.md +0 -0
  130. {glpi_python_client-0.3.1 → glpi_python_client-0.3.2}/skills/glpi-user-location-provisioning/SKILL.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glpi-python-client
3
- Version: 0.3.1
3
+ Version: 0.3.2
4
4
  Summary: A typed Python client for GLPI ITSM APIs.
5
5
  Project-URL: Homepage, https://github.com/baraline/glpi_python_client
6
6
  Project-URL: Documentation, https://glpi-python-client.readthedocs.io/en/latest/
@@ -179,16 +179,40 @@ fan-outs cannot race the auth manager, while the HTTP requests
179
179
  themselves execute outside the lock through the thread-safe
180
180
  :class:`requests.Session`.
181
181
 
182
- A small number of helpers exist in async-only variants because they
183
- need real concurrency:
184
-
185
- * :meth:`AsyncGlpiClient.get_ticket_context` fans the five underlying
186
- GLPI calls out concurrently with :func:`asyncio.gather`.
187
- * :meth:`AsyncGlpiClient.get_task_statistics` fans the per-ticket task
188
- list calls out concurrently with :func:`asyncio.gather`.
189
- * :meth:`AsyncGlpiClient.get_task_durations` fans the per-ticket task
190
- fetches out concurrently with :func:`asyncio.gather` when
191
- ``return_task_details=True``.
182
+ A number of helpers ship with hand-written async overrides rather than
183
+ relying solely on the bridge. There are two reasons a method needs its
184
+ own async variant:
185
+
186
+ 1. **Concurrency** — the method benefits from fanning multiple GLPI
187
+ calls out concurrently with :func:`asyncio.gather`.
188
+ 2. **Internal self-calls** — the method calls another public method
189
+ through ``self`` (e.g. ``self.search_tickets(...)`` inside a
190
+ pagination loop). When the bridge runs the synchronous body in a
191
+ worker thread, ``self.method`` resolves to the bridge-wrapped
192
+ *coroutine*, which returns a coroutine object instead of data when
193
+ called without ``await``. The async override replaces the body so
194
+ every internal call is properly awaited on the event loop.
195
+
196
+ Helpers with async overrides:
197
+
198
+ * :meth:`AsyncGlpiClient.get_ticket_context` — fans the five underlying
199
+ GLPI calls out concurrently (reason: concurrency).
200
+ * :meth:`AsyncGlpiClient.get_task_statistics` — fans the per-ticket
201
+ task-list calls out concurrently (reason: concurrency).
202
+ * :meth:`AsyncGlpiClient.get_task_durations` — fans the per-ticket task
203
+ fetches out concurrently when ``return_task_details=True``, and
204
+ properly awaits ``iter_search_tickets`` and ``search_entities``
205
+ internally (reasons: concurrency + internal self-calls).
206
+ * :meth:`AsyncGlpiClient.get_ticket_statistics` — properly awaits
207
+ ``search_tickets`` and ``search_entities`` internally (reason:
208
+ internal self-calls).
209
+ * :meth:`AsyncGlpiClient.get_user_activity` — properly awaits
210
+ ``search_users``, ``iter_search_tickets``, and ``get_task_durations``
211
+ internally (reason: internal self-calls).
212
+ * ``iter_search_tickets``, ``iter_search_users``,
213
+ ``iter_search_entities`` — each pagination loop body calls
214
+ ``self.search_*(...)``; the async variants are native async generators
215
+ that ``await`` those calls directly (reason: internal self-calls).
192
216
 
193
217
  Pagination helpers (``iter_search_tickets``, ``iter_search_users``,
194
218
  ``iter_search_entities``) are exposed as **async generators** on the
@@ -72,7 +72,7 @@ from glpi_python_client.models import (
72
72
  TicketMarkdownOptions,
73
73
  )
74
74
 
75
- __version__ = "0.3.1"
75
+ __version__ = "0.3.2"
76
76
 
77
77
  __all__ = [
78
78
  "AsyncGlpiClient",
@@ -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
@@ -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"]