glpi-python-client 0.3.0__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.
- glpi_python_client-0.3.2/.pre-commit-config.yaml +32 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/PKG-INFO +1 -1
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/user_guide.rst +210 -9
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/__init__.py +1 -1
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/administration/_entity.py +44 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/administration/_user.py +47 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/_ticket.py +52 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/async_client.py +3 -1
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_async_bridge.py +76 -3
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/__init__.py +2 -0
- glpi_python_client-0.3.2/glpi_python_client/clients/custom/_pagination_async.py +164 -0
- glpi_python_client-0.3.2/glpi_python_client/clients/custom/_statistics.py +674 -0
- glpi_python_client-0.3.2/glpi_python_client/clients/custom/_statistics_async.py +495 -0
- glpi_python_client-0.3.2/glpi_python_client/clients/custom/tests/test_statistics.py +562 -0
- glpi_python_client-0.3.2/glpi_python_client/clients/custom/tests/test_statistics_async.py +232 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_api_coverage.py +175 -0
- glpi_python_client-0.3.2/glpi_python_client/clients/tests/test_async_branches.py +701 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_parity.py +10 -4
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/pyproject.toml +1 -1
- glpi_python_client-0.3.2/skills/glpi-reporting-and-context/SKILL.md +110 -0
- glpi_python_client-0.3.0/.pre-commit-config.yaml +0 -7
- glpi_python_client-0.3.0/glpi_python_client/clients/custom/_statistics.py +0 -261
- glpi_python_client-0.3.0/glpi_python_client/clients/custom/_statistics_async.py +0 -69
- glpi_python_client-0.3.0/glpi_python_client/clients/custom/tests/test_statistics.py +0 -194
- glpi_python_client-0.3.0/glpi_python_client/clients/tests/test_async_branches.py +0 -137
- glpi_python_client-0.3.0/skills/glpi-reporting-and-context/SKILL.md +0 -72
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/.gitignore +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/.readthedocs.yaml +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/CONTRIBUTING.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/LICENSE +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/README.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/_static/.gitkeep +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/api_reference.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/conf.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/development.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/development_rtd.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/glpi_api_contract.json +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/index.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/installation.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/publishing.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/publishing_rtd.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/docs/sponsoring.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/auth/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/auth/_v1_session.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/auth/auth.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/auth/tests/test_auth.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/auth/tests/test_v1_session.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/administration/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/_team.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/dropdowns/_location.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/management/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/api/management/_document.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_config.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_constants.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_errors.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_filters.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_http.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_payloads.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/_transport.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_errors.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_filters.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_http.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_payloads.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/commons/tests/test_transport.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/_ticket_context.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/_ticket_context_async.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/tests/test_ticket_context.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/sync_client.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_async_smoke.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_glpi_client.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/tests/test_smoke.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/content/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/content/conversion.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/content/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/content/tests/test_conversion.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/_base.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/_common.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/_content.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/_entity.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/_user.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/administration/tests/test_administration_schemas.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/_team.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/_ticket.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/tests/test_assistance_schemas.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/tests/test_content_roundtrip.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/assistance/timeline/tests/test_timeline_schemas.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/dropdowns/_location.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/dropdowns/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/dropdowns/tests/test_dropdowns_schemas.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/enums.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/management/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/management/_document.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/management/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/api_schema/management/tests/test_management_schemas.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/custom_schema/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/custom_schema/_ticket_context.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/custom_schema/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/models/custom_schema/tests/test_ticket_context.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/py.typed +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/testing/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/testing/fixtures.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/testing/utils.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/skills/README.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/skills/glpi-client-setup/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/skills/glpi-document-workflow/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/skills/glpi-team-members/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/skills/glpi-ticket-timeline/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/skills/glpi-ticket-workflow/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/skills/glpi-user-location-provisioning/SKILL.md +0 -0
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
default_install_hook_types: [pre-commit, pre-push]
|
|
2
|
+
|
|
3
|
+
repos:
|
|
4
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
5
|
+
rev: v0.15.13
|
|
6
|
+
hooks:
|
|
7
|
+
- id: ruff-check
|
|
8
|
+
args: [--fix]
|
|
9
|
+
- id: ruff-format
|
|
10
|
+
|
|
11
|
+
- repo: local
|
|
12
|
+
hooks:
|
|
13
|
+
- id: mypy
|
|
14
|
+
name: mypy (strict)
|
|
15
|
+
entry: python -m mypy glpi_python_client
|
|
16
|
+
language: system
|
|
17
|
+
types: [python]
|
|
18
|
+
pass_filenames: false
|
|
19
|
+
|
|
20
|
+
- id: pytest-coverage
|
|
21
|
+
name: pytest with coverage (>=95%)
|
|
22
|
+
entry: python -m pytest -m "not integration" --cov=glpi_python_client --cov-fail-under=95 -q
|
|
23
|
+
language: system
|
|
24
|
+
pass_filenames: false
|
|
25
|
+
stages: [pre-push]
|
|
26
|
+
|
|
27
|
+
- id: sphinx-build
|
|
28
|
+
name: sphinx build (warnings as errors)
|
|
29
|
+
entry: python -m sphinx -b html -W --keep-going docs docs/_build/html_check
|
|
30
|
+
language: system
|
|
31
|
+
pass_filenames: false
|
|
32
|
+
stages: [pre-push]
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: glpi-python-client
|
|
3
|
-
Version: 0.3.
|
|
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,13 +179,51 @@ 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
|
|
183
|
-
|
|
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).
|
|
216
|
+
|
|
217
|
+
Pagination helpers (``iter_search_tickets``, ``iter_search_users``,
|
|
218
|
+
``iter_search_entities``) are exposed as **async generators** on the
|
|
219
|
+
async client. Iterate them with ``async for`` to walk every page
|
|
220
|
+
without blocking the event loop:
|
|
184
221
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
222
|
+
.. code-block:: python
|
|
223
|
+
|
|
224
|
+
async for batch in client.iter_search_tickets("status==1", batch_size=200):
|
|
225
|
+
for ticket in batch:
|
|
226
|
+
...
|
|
189
227
|
|
|
190
228
|
The synchronous versions of the same helpers issue the calls
|
|
191
229
|
sequentially.
|
|
@@ -710,25 +748,79 @@ Example — description and timeline only, no metadata fields:
|
|
|
710
748
|
Reporting helpers
|
|
711
749
|
~~~~~~~~~~~~~~~~~
|
|
712
750
|
|
|
713
|
-
The custom statistics mixin exposes
|
|
751
|
+
The custom statistics mixin exposes several helpers that aggregate the
|
|
714
752
|
ticket and ticket-task records returned by the contract-aligned mixins.
|
|
715
|
-
|
|
753
|
+
They all return plain Python dictionaries so they can be serialised or
|
|
716
754
|
forwarded as-is.
|
|
717
755
|
|
|
756
|
+
Streaming pagination with ``iter_search_*``
|
|
757
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
758
|
+
|
|
759
|
+
The ``search_*`` helpers return one page at a time and require the
|
|
760
|
+
caller to manage the ``start`` cursor. The companion ``iter_search_*``
|
|
761
|
+
generators handle pagination automatically by yielding successive
|
|
762
|
+
batches until the API returns fewer rows than the requested
|
|
763
|
+
``batch_size`` (the natural end-of-stream signal):
|
|
764
|
+
|
|
765
|
+
* :meth:`GlpiClient.iter_search_tickets`
|
|
766
|
+
* :meth:`GlpiClient.iter_search_users`
|
|
767
|
+
* :meth:`GlpiClient.iter_search_entities`
|
|
768
|
+
|
|
769
|
+
.. code-block:: python
|
|
770
|
+
|
|
771
|
+
# Walk every "open" ticket without loading the full result set in memory.
|
|
772
|
+
total = 0
|
|
773
|
+
for batch in client.iter_search_tickets("status==1", batch_size=200):
|
|
774
|
+
total += len(batch)
|
|
775
|
+
for ticket in batch:
|
|
776
|
+
print(ticket.id, ticket.name)
|
|
777
|
+
print(f"processed {total} tickets")
|
|
778
|
+
|
|
779
|
+
On the asynchronous client the same helpers are exposed as **async
|
|
780
|
+
generators** through the bridge, so each ``next()`` call runs off the
|
|
781
|
+
event loop and the consumer uses ``async for``:
|
|
782
|
+
|
|
783
|
+
.. code-block:: python
|
|
784
|
+
|
|
785
|
+
async for batch in async_client.iter_search_users("", batch_size=100):
|
|
786
|
+
for user in batch:
|
|
787
|
+
print(user.id, user.username)
|
|
788
|
+
|
|
718
789
|
``get_ticket_statistics``
|
|
719
790
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
720
791
|
|
|
721
792
|
Counts tickets created within an ISO date window and groups them by
|
|
722
|
-
entity, status, priority, and type.
|
|
793
|
+
entity, status, priority, and type. Optional filters restrict the
|
|
794
|
+
result set on the server side:
|
|
795
|
+
|
|
796
|
+
* ``entity_id`` — restrict to a single entity by numeric identifier.
|
|
797
|
+
* ``entity_name`` — substring match against the entity ``name`` column;
|
|
798
|
+
the helper resolves matching IDs via ``search_entities`` and ORs
|
|
799
|
+
them together. Ignored when ``entity_id`` is provided.
|
|
800
|
+
* ``extra_filter`` — raw RSQL fragment AND-joined with the date window.
|
|
723
801
|
|
|
724
802
|
.. code-block:: python
|
|
725
803
|
|
|
804
|
+
# Tickets created in January 2026 on a specific entity, restricted to
|
|
805
|
+
# priority "HIGH" (5) via an extra raw RSQL fragment.
|
|
726
806
|
stats = client.get_ticket_statistics(
|
|
727
807
|
start_date="2026-01-01",
|
|
728
808
|
end_date="2026-01-31",
|
|
809
|
+
entity_id=3,
|
|
810
|
+
extra_filter="priority==5",
|
|
729
811
|
)
|
|
730
812
|
print(stats)
|
|
731
813
|
|
|
814
|
+
# Resolve the entity by (partial) name instead of by ID:
|
|
815
|
+
stats = client.get_ticket_statistics(
|
|
816
|
+
start_date="2026-01-01",
|
|
817
|
+
end_date="2026-01-31",
|
|
818
|
+
entity_name="Helpdesk",
|
|
819
|
+
)
|
|
820
|
+
|
|
821
|
+
When ``entity_name`` matches no entity the helper short-circuits and
|
|
822
|
+
returns ``{"entities": {}}`` without issuing any ticket search.
|
|
823
|
+
|
|
732
824
|
Returned shape (the outer key is always ``"entities"``; entity keys are
|
|
733
825
|
the GLPI numeric identifier as a string, ``"unknown"`` when missing)::
|
|
734
826
|
|
|
@@ -790,6 +882,115 @@ needed (for example
|
|
|
790
882
|
``client.get_user(22)`` to turn user key ``"22"`` into a full
|
|
791
883
|
:class:`GetUser` model).
|
|
792
884
|
|
|
885
|
+
``get_task_durations``
|
|
886
|
+
^^^^^^^^^^^^^^^^^^^^^^
|
|
887
|
+
|
|
888
|
+
Aggregates task durations over a date window with rich server-side
|
|
889
|
+
filters and an optional per-task detail list. Internally the helper
|
|
890
|
+
iterates :meth:`iter_search_tickets` to collect every matching ticket,
|
|
891
|
+
then computes per-user and per-entity totals.
|
|
892
|
+
|
|
893
|
+
Available filters:
|
|
894
|
+
|
|
895
|
+
* ``start_date`` / ``end_date`` / ``default_days`` — ISO ``YYYY-MM-DD``
|
|
896
|
+
date window; ``default_days`` is used when ``start_date`` is omitted.
|
|
897
|
+
* ``entity_id`` — restrict to a single entity by identifier.
|
|
898
|
+
* ``entity_name`` — substring match resolved through ``search_entities``;
|
|
899
|
+
ignored when ``entity_id`` is given.
|
|
900
|
+
* ``user_id`` — tickets where the user is **either** assignee or
|
|
901
|
+
requester (OR semantics).
|
|
902
|
+
* ``user_editor_id`` — tickets last updated by this user.
|
|
903
|
+
* ``user_recipient_id`` — tickets where this user is the requester.
|
|
904
|
+
* ``extra_filter`` — raw RSQL fragment AND-joined with everything else.
|
|
905
|
+
* ``return_task_details`` — when ``True``, fetch every non-zero ticket's
|
|
906
|
+
task list and include them as ``tasks`` in the result.
|
|
907
|
+
|
|
908
|
+
.. code-block:: python
|
|
909
|
+
|
|
910
|
+
# Sum durations for a tech on a specific entity over the last 30 days.
|
|
911
|
+
summary = client.get_task_durations(
|
|
912
|
+
entity_id=3,
|
|
913
|
+
user_id=42,
|
|
914
|
+
)
|
|
915
|
+
print(summary["total_duration"], summary["task_count"])
|
|
916
|
+
print(summary["duration_by_entity"]) # {"3": 7200}
|
|
917
|
+
|
|
918
|
+
# Same query but ask for the per-task breakdown.
|
|
919
|
+
detailed = client.get_task_durations(
|
|
920
|
+
entity_id=3,
|
|
921
|
+
user_id=42,
|
|
922
|
+
return_task_details=True,
|
|
923
|
+
)
|
|
924
|
+
for task in detailed["tasks"] or []:
|
|
925
|
+
print(task["task_id"], task["ticket_id"], task["duration"])
|
|
926
|
+
|
|
927
|
+
Returned shape::
|
|
928
|
+
|
|
929
|
+
{
|
|
930
|
+
"start_date": "2026-01-01",
|
|
931
|
+
"end_date": "2026-01-31",
|
|
932
|
+
"total_duration": 7200,
|
|
933
|
+
"task_count": 4,
|
|
934
|
+
"duration_by_user": {"42": 7200},
|
|
935
|
+
"duration_by_entity": {"3": 7200},
|
|
936
|
+
"tasks": None, # or a list[dict] when return_task_details=True
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
On the async client the same method is overridden to run the per-ticket
|
|
940
|
+
task fetches concurrently with :func:`asyncio.gather` when
|
|
941
|
+
``return_task_details=True``.
|
|
942
|
+
|
|
943
|
+
``get_user_activity``
|
|
944
|
+
^^^^^^^^^^^^^^^^^^^^^
|
|
945
|
+
|
|
946
|
+
Aggregates per-user activity over a date window: tickets where the
|
|
947
|
+
user appears as technician (``users_id_assign``), tickets where the
|
|
948
|
+
user appears as requester (``users_id_requester``), and the user's
|
|
949
|
+
task duration totals. Multiple users that resolve to the same display
|
|
950
|
+
key (``"<firstname> <realname>"``) are merged into a single bucket.
|
|
951
|
+
|
|
952
|
+
The helper raises ``ValueError`` when no identifier is supplied or
|
|
953
|
+
when the search criteria match no users in the directory.
|
|
954
|
+
|
|
955
|
+
.. code-block:: python
|
|
956
|
+
|
|
957
|
+
# Activity for a single user identified by username (substring match).
|
|
958
|
+
report = client.get_user_activity(
|
|
959
|
+
username="alice",
|
|
960
|
+
start_date="2026-01-01",
|
|
961
|
+
end_date="2026-01-31",
|
|
962
|
+
)
|
|
963
|
+
for display_name, data in report["users"].items():
|
|
964
|
+
print(
|
|
965
|
+
display_name,
|
|
966
|
+
data["tickets_as_technician"],
|
|
967
|
+
data["tickets_as_recipient"],
|
|
968
|
+
data["task_durations"]["total_duration"],
|
|
969
|
+
)
|
|
970
|
+
|
|
971
|
+
# Activity for every user whose last name contains "Smith".
|
|
972
|
+
report = client.get_user_activity(realname="Smith", default_days=90)
|
|
973
|
+
|
|
974
|
+
Returned shape::
|
|
975
|
+
|
|
976
|
+
{
|
|
977
|
+
"users": {
|
|
978
|
+
"Alice Smith": {
|
|
979
|
+
"user_ids": [42],
|
|
980
|
+
"tickets_as_technician": 7,
|
|
981
|
+
"tickets_as_recipient": 2,
|
|
982
|
+
"task_durations": {
|
|
983
|
+
"start_date": "2026-01-01",
|
|
984
|
+
"end_date": "2026-01-31",
|
|
985
|
+
"total_duration": 7200,
|
|
986
|
+
"task_count": 4,
|
|
987
|
+
"duration_by_user": {"42": 7200},
|
|
988
|
+
"duration_by_entity": {"3": 7200},
|
|
989
|
+
},
|
|
990
|
+
}
|
|
991
|
+
}
|
|
992
|
+
}
|
|
993
|
+
|
|
793
994
|
.. _end-to-end-examples:
|
|
794
995
|
|
|
795
996
|
6. End-to-end examples
|
|
@@ -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
|
|
{glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/async_client.py
RENAMED
|
@@ -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"]
|
{glpi_python_client-0.3.0 → glpi_python_client-0.3.2}/glpi_python_client/clients/custom/__init__.py
RENAMED
|
@@ -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",
|