glpi-python-client 0.3.0__tar.gz → 0.3.1__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.1/.pre-commit-config.yaml +32 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/PKG-INFO +1 -1
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/user_guide.rst +180 -3
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/__init__.py +1 -1
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/administration/_entity.py +44 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/administration/_user.py +47 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/assistance/_ticket.py +52 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/_async_bridge.py +59 -3
- glpi_python_client-0.3.1/glpi_python_client/clients/custom/_statistics.py +674 -0
- glpi_python_client-0.3.1/glpi_python_client/clients/custom/_statistics_async.py +250 -0
- glpi_python_client-0.3.1/glpi_python_client/clients/custom/tests/test_statistics.py +562 -0
- glpi_python_client-0.3.1/glpi_python_client/clients/custom/tests/test_statistics_async.py +232 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/tests/test_api_coverage.py +175 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/tests/test_async_branches.py +58 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/tests/test_parity.py +10 -4
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/pyproject.toml +1 -1
- glpi_python_client-0.3.1/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/skills/glpi-reporting-and-context/SKILL.md +0 -72
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/.gitignore +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/.readthedocs.yaml +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/CONTRIBUTING.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/LICENSE +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/README.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/_static/.gitkeep +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/api_reference.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/conf.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/development.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/development_rtd.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/glpi_api_contract.json +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/index.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/installation.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/publishing.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/publishing_rtd.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/docs/sponsoring.rst +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/auth/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/auth/_v1_session.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/auth/auth.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/auth/tests/test_auth.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/auth/tests/test_v1_session.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/administration/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/assistance/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/assistance/_team.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/dropdowns/_location.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/management/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/api/management/_document.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/async_client.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/_config.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/_constants.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/_errors.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/_filters.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/_http.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/_payloads.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/_transport.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/tests/test_errors.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/tests/test_filters.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/tests/test_http.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/tests/test_payloads.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/commons/tests/test_transport.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/custom/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/custom/_ticket_context.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/custom/_ticket_context_async.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/custom/tests/test_ticket_context.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/sync_client.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/tests/test_async_smoke.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/tests/test_glpi_client.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/clients/tests/test_smoke.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/content/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/content/conversion.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/content/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/content/tests/test_conversion.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/_base.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/_common.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/_content.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/administration/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/administration/_entity.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/administration/_user.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/administration/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/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.1}/glpi_python_client/models/api_schema/assistance/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/assistance/_team.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/assistance/_ticket.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/assistance/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/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.1}/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.1}/glpi_python_client/models/api_schema/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/assistance/timeline/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/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.1}/glpi_python_client/models/api_schema/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/dropdowns/_location.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/dropdowns/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/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.1}/glpi_python_client/models/api_schema/enums.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/management/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/management/_document.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/api_schema/management/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/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.1}/glpi_python_client/models/custom_schema/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/custom_schema/_ticket_context.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/custom_schema/tests/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/models/custom_schema/tests/test_ticket_context.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/py.typed +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/testing/__init__.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/testing/fixtures.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/glpi_python_client/testing/utils.py +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/skills/README.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/skills/glpi-client-setup/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/skills/glpi-document-workflow/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/skills/glpi-team-members/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/skills/glpi-ticket-timeline/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/skills/glpi-ticket-workflow/SKILL.md +0 -0
- {glpi_python_client-0.3.0 → glpi_python_client-0.3.1}/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.1
|
|
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/
|
|
@@ -186,6 +186,20 @@ need real concurrency:
|
|
|
186
186
|
GLPI calls out concurrently with :func:`asyncio.gather`.
|
|
187
187
|
* :meth:`AsyncGlpiClient.get_task_statistics` fans the per-ticket task
|
|
188
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``.
|
|
192
|
+
|
|
193
|
+
Pagination helpers (``iter_search_tickets``, ``iter_search_users``,
|
|
194
|
+
``iter_search_entities``) are exposed as **async generators** on the
|
|
195
|
+
async client. Iterate them with ``async for`` to walk every page
|
|
196
|
+
without blocking the event loop:
|
|
197
|
+
|
|
198
|
+
.. code-block:: python
|
|
199
|
+
|
|
200
|
+
async for batch in client.iter_search_tickets("status==1", batch_size=200):
|
|
201
|
+
for ticket in batch:
|
|
202
|
+
...
|
|
189
203
|
|
|
190
204
|
The synchronous versions of the same helpers issue the calls
|
|
191
205
|
sequentially.
|
|
@@ -710,25 +724,79 @@ Example — description and timeline only, no metadata fields:
|
|
|
710
724
|
Reporting helpers
|
|
711
725
|
~~~~~~~~~~~~~~~~~
|
|
712
726
|
|
|
713
|
-
The custom statistics mixin exposes
|
|
727
|
+
The custom statistics mixin exposes several helpers that aggregate the
|
|
714
728
|
ticket and ticket-task records returned by the contract-aligned mixins.
|
|
715
|
-
|
|
729
|
+
They all return plain Python dictionaries so they can be serialised or
|
|
716
730
|
forwarded as-is.
|
|
717
731
|
|
|
732
|
+
Streaming pagination with ``iter_search_*``
|
|
733
|
+
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
734
|
+
|
|
735
|
+
The ``search_*`` helpers return one page at a time and require the
|
|
736
|
+
caller to manage the ``start`` cursor. The companion ``iter_search_*``
|
|
737
|
+
generators handle pagination automatically by yielding successive
|
|
738
|
+
batches until the API returns fewer rows than the requested
|
|
739
|
+
``batch_size`` (the natural end-of-stream signal):
|
|
740
|
+
|
|
741
|
+
* :meth:`GlpiClient.iter_search_tickets`
|
|
742
|
+
* :meth:`GlpiClient.iter_search_users`
|
|
743
|
+
* :meth:`GlpiClient.iter_search_entities`
|
|
744
|
+
|
|
745
|
+
.. code-block:: python
|
|
746
|
+
|
|
747
|
+
# Walk every "open" ticket without loading the full result set in memory.
|
|
748
|
+
total = 0
|
|
749
|
+
for batch in client.iter_search_tickets("status==1", batch_size=200):
|
|
750
|
+
total += len(batch)
|
|
751
|
+
for ticket in batch:
|
|
752
|
+
print(ticket.id, ticket.name)
|
|
753
|
+
print(f"processed {total} tickets")
|
|
754
|
+
|
|
755
|
+
On the asynchronous client the same helpers are exposed as **async
|
|
756
|
+
generators** through the bridge, so each ``next()`` call runs off the
|
|
757
|
+
event loop and the consumer uses ``async for``:
|
|
758
|
+
|
|
759
|
+
.. code-block:: python
|
|
760
|
+
|
|
761
|
+
async for batch in async_client.iter_search_users("", batch_size=100):
|
|
762
|
+
for user in batch:
|
|
763
|
+
print(user.id, user.username)
|
|
764
|
+
|
|
718
765
|
``get_ticket_statistics``
|
|
719
766
|
^^^^^^^^^^^^^^^^^^^^^^^^^
|
|
720
767
|
|
|
721
768
|
Counts tickets created within an ISO date window and groups them by
|
|
722
|
-
entity, status, priority, and type.
|
|
769
|
+
entity, status, priority, and type. Optional filters restrict the
|
|
770
|
+
result set on the server side:
|
|
771
|
+
|
|
772
|
+
* ``entity_id`` — restrict to a single entity by numeric identifier.
|
|
773
|
+
* ``entity_name`` — substring match against the entity ``name`` column;
|
|
774
|
+
the helper resolves matching IDs via ``search_entities`` and ORs
|
|
775
|
+
them together. Ignored when ``entity_id`` is provided.
|
|
776
|
+
* ``extra_filter`` — raw RSQL fragment AND-joined with the date window.
|
|
723
777
|
|
|
724
778
|
.. code-block:: python
|
|
725
779
|
|
|
780
|
+
# Tickets created in January 2026 on a specific entity, restricted to
|
|
781
|
+
# priority "HIGH" (5) via an extra raw RSQL fragment.
|
|
726
782
|
stats = client.get_ticket_statistics(
|
|
727
783
|
start_date="2026-01-01",
|
|
728
784
|
end_date="2026-01-31",
|
|
785
|
+
entity_id=3,
|
|
786
|
+
extra_filter="priority==5",
|
|
729
787
|
)
|
|
730
788
|
print(stats)
|
|
731
789
|
|
|
790
|
+
# Resolve the entity by (partial) name instead of by ID:
|
|
791
|
+
stats = client.get_ticket_statistics(
|
|
792
|
+
start_date="2026-01-01",
|
|
793
|
+
end_date="2026-01-31",
|
|
794
|
+
entity_name="Helpdesk",
|
|
795
|
+
)
|
|
796
|
+
|
|
797
|
+
When ``entity_name`` matches no entity the helper short-circuits and
|
|
798
|
+
returns ``{"entities": {}}`` without issuing any ticket search.
|
|
799
|
+
|
|
732
800
|
Returned shape (the outer key is always ``"entities"``; entity keys are
|
|
733
801
|
the GLPI numeric identifier as a string, ``"unknown"`` when missing)::
|
|
734
802
|
|
|
@@ -790,6 +858,115 @@ needed (for example
|
|
|
790
858
|
``client.get_user(22)`` to turn user key ``"22"`` into a full
|
|
791
859
|
:class:`GetUser` model).
|
|
792
860
|
|
|
861
|
+
``get_task_durations``
|
|
862
|
+
^^^^^^^^^^^^^^^^^^^^^^
|
|
863
|
+
|
|
864
|
+
Aggregates task durations over a date window with rich server-side
|
|
865
|
+
filters and an optional per-task detail list. Internally the helper
|
|
866
|
+
iterates :meth:`iter_search_tickets` to collect every matching ticket,
|
|
867
|
+
then computes per-user and per-entity totals.
|
|
868
|
+
|
|
869
|
+
Available filters:
|
|
870
|
+
|
|
871
|
+
* ``start_date`` / ``end_date`` / ``default_days`` — ISO ``YYYY-MM-DD``
|
|
872
|
+
date window; ``default_days`` is used when ``start_date`` is omitted.
|
|
873
|
+
* ``entity_id`` — restrict to a single entity by identifier.
|
|
874
|
+
* ``entity_name`` — substring match resolved through ``search_entities``;
|
|
875
|
+
ignored when ``entity_id`` is given.
|
|
876
|
+
* ``user_id`` — tickets where the user is **either** assignee or
|
|
877
|
+
requester (OR semantics).
|
|
878
|
+
* ``user_editor_id`` — tickets last updated by this user.
|
|
879
|
+
* ``user_recipient_id`` — tickets where this user is the requester.
|
|
880
|
+
* ``extra_filter`` — raw RSQL fragment AND-joined with everything else.
|
|
881
|
+
* ``return_task_details`` — when ``True``, fetch every non-zero ticket's
|
|
882
|
+
task list and include them as ``tasks`` in the result.
|
|
883
|
+
|
|
884
|
+
.. code-block:: python
|
|
885
|
+
|
|
886
|
+
# Sum durations for a tech on a specific entity over the last 30 days.
|
|
887
|
+
summary = client.get_task_durations(
|
|
888
|
+
entity_id=3,
|
|
889
|
+
user_id=42,
|
|
890
|
+
)
|
|
891
|
+
print(summary["total_duration"], summary["task_count"])
|
|
892
|
+
print(summary["duration_by_entity"]) # {"3": 7200}
|
|
893
|
+
|
|
894
|
+
# Same query but ask for the per-task breakdown.
|
|
895
|
+
detailed = client.get_task_durations(
|
|
896
|
+
entity_id=3,
|
|
897
|
+
user_id=42,
|
|
898
|
+
return_task_details=True,
|
|
899
|
+
)
|
|
900
|
+
for task in detailed["tasks"] or []:
|
|
901
|
+
print(task["task_id"], task["ticket_id"], task["duration"])
|
|
902
|
+
|
|
903
|
+
Returned shape::
|
|
904
|
+
|
|
905
|
+
{
|
|
906
|
+
"start_date": "2026-01-01",
|
|
907
|
+
"end_date": "2026-01-31",
|
|
908
|
+
"total_duration": 7200,
|
|
909
|
+
"task_count": 4,
|
|
910
|
+
"duration_by_user": {"42": 7200},
|
|
911
|
+
"duration_by_entity": {"3": 7200},
|
|
912
|
+
"tasks": None, # or a list[dict] when return_task_details=True
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
On the async client the same method is overridden to run the per-ticket
|
|
916
|
+
task fetches concurrently with :func:`asyncio.gather` when
|
|
917
|
+
``return_task_details=True``.
|
|
918
|
+
|
|
919
|
+
``get_user_activity``
|
|
920
|
+
^^^^^^^^^^^^^^^^^^^^^
|
|
921
|
+
|
|
922
|
+
Aggregates per-user activity over a date window: tickets where the
|
|
923
|
+
user appears as technician (``users_id_assign``), tickets where the
|
|
924
|
+
user appears as requester (``users_id_requester``), and the user's
|
|
925
|
+
task duration totals. Multiple users that resolve to the same display
|
|
926
|
+
key (``"<firstname> <realname>"``) are merged into a single bucket.
|
|
927
|
+
|
|
928
|
+
The helper raises ``ValueError`` when no identifier is supplied or
|
|
929
|
+
when the search criteria match no users in the directory.
|
|
930
|
+
|
|
931
|
+
.. code-block:: python
|
|
932
|
+
|
|
933
|
+
# Activity for a single user identified by username (substring match).
|
|
934
|
+
report = client.get_user_activity(
|
|
935
|
+
username="alice",
|
|
936
|
+
start_date="2026-01-01",
|
|
937
|
+
end_date="2026-01-31",
|
|
938
|
+
)
|
|
939
|
+
for display_name, data in report["users"].items():
|
|
940
|
+
print(
|
|
941
|
+
display_name,
|
|
942
|
+
data["tickets_as_technician"],
|
|
943
|
+
data["tickets_as_recipient"],
|
|
944
|
+
data["task_durations"]["total_duration"],
|
|
945
|
+
)
|
|
946
|
+
|
|
947
|
+
# Activity for every user whose last name contains "Smith".
|
|
948
|
+
report = client.get_user_activity(realname="Smith", default_days=90)
|
|
949
|
+
|
|
950
|
+
Returned shape::
|
|
951
|
+
|
|
952
|
+
{
|
|
953
|
+
"users": {
|
|
954
|
+
"Alice Smith": {
|
|
955
|
+
"user_ids": [42],
|
|
956
|
+
"tickets_as_technician": 7,
|
|
957
|
+
"tickets_as_recipient": 2,
|
|
958
|
+
"task_durations": {
|
|
959
|
+
"start_date": "2026-01-01",
|
|
960
|
+
"end_date": "2026-01-31",
|
|
961
|
+
"total_duration": 7200,
|
|
962
|
+
"task_count": 4,
|
|
963
|
+
"duration_by_user": {"42": 7200},
|
|
964
|
+
"duration_by_entity": {"3": 7200},
|
|
965
|
+
},
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
}
|
|
969
|
+
|
|
793
970
|
.. _end-to-end-examples:
|
|
794
971
|
|
|
795
972
|
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
|
|
|
@@ -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
|
|
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
|
-
|
|
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"]
|