glpi-python-client 0.3.1__tar.gz → 0.3.3__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 (131) hide show
  1. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/.gitignore +3 -2
  2. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/PKG-INFO +1 -1
  3. glpi_python_client-0.3.3/docs/development_rtd.rst +162 -0
  4. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/user_guide.rst +49 -13
  5. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/__init__.py +1 -1
  6. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/assistance/timeline/_document.py +21 -20
  7. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/async_client.py +3 -1
  8. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/_async_bridge.py +17 -0
  9. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/custom/__init__.py +2 -0
  10. glpi_python_client-0.3.3/glpi_python_client/clients/custom/_pagination_async.py +164 -0
  11. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/custom/_statistics.py +21 -16
  12. glpi_python_client-0.3.3/glpi_python_client/clients/custom/_statistics_async.py +500 -0
  13. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/custom/tests/test_statistics.py +2 -2
  14. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/tests/test_api_coverage.py +2 -2
  15. glpi_python_client-0.3.3/glpi_python_client/clients/tests/test_async_branches.py +701 -0
  16. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/custom_schema/_ticket_context.py +12 -9
  17. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/custom_schema/tests/test_ticket_context.py +7 -7
  18. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/pyproject.toml +1 -1
  19. glpi_python_client-0.3.1/docs/development_rtd.rst +0 -84
  20. glpi_python_client-0.3.1/glpi_python_client/clients/custom/_statistics_async.py +0 -250
  21. glpi_python_client-0.3.1/glpi_python_client/clients/tests/test_async_branches.py +0 -195
  22. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/.pre-commit-config.yaml +0 -0
  23. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/.readthedocs.yaml +0 -0
  24. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/CONTRIBUTING.md +0 -0
  25. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/LICENSE +0 -0
  26. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/README.md +0 -0
  27. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/_static/.gitkeep +0 -0
  28. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/api_reference.rst +0 -0
  29. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/conf.py +0 -0
  30. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/development.md +0 -0
  31. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/glpi_api_contract.json +0 -0
  32. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/index.rst +0 -0
  33. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/installation.rst +0 -0
  34. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/publishing.md +0 -0
  35. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/publishing_rtd.rst +0 -0
  36. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/docs/sponsoring.rst +0 -0
  37. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/auth/__init__.py +0 -0
  38. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/auth/_v1_session.py +0 -0
  39. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/auth/auth.py +0 -0
  40. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/auth/tests/test_auth.py +0 -0
  41. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/auth/tests/test_v1_session.py +0 -0
  42. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/__init__.py +0 -0
  43. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/__init__.py +0 -0
  44. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/administration/__init__.py +0 -0
  45. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/administration/_entity.py +0 -0
  46. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/administration/_user.py +0 -0
  47. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/assistance/__init__.py +0 -0
  48. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/assistance/_team.py +0 -0
  49. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/assistance/_ticket.py +0 -0
  50. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/assistance/timeline/__init__.py +0 -0
  51. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/assistance/timeline/_followup.py +0 -0
  52. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/assistance/timeline/_solution.py +0 -0
  53. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/assistance/timeline/_task.py +0 -0
  54. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/dropdowns/__init__.py +0 -0
  55. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/dropdowns/_location.py +0 -0
  56. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/management/__init__.py +0 -0
  57. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/api/management/_document.py +0 -0
  58. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/__init__.py +0 -0
  59. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/_config.py +0 -0
  60. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/_constants.py +0 -0
  61. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/_errors.py +0 -0
  62. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/_filters.py +0 -0
  63. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/_http.py +0 -0
  64. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/_payloads.py +0 -0
  65. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/_transport.py +0 -0
  66. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/tests/__init__.py +0 -0
  67. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/tests/test_errors.py +0 -0
  68. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/tests/test_filters.py +0 -0
  69. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/tests/test_http.py +0 -0
  70. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/tests/test_payloads.py +0 -0
  71. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/commons/tests/test_transport.py +0 -0
  72. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/custom/_ticket_context.py +0 -0
  73. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/custom/_ticket_context_async.py +0 -0
  74. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/custom/tests/test_statistics_async.py +0 -0
  75. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/custom/tests/test_ticket_context.py +0 -0
  76. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/sync_client.py +0 -0
  77. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/tests/__init__.py +0 -0
  78. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/tests/test_async_smoke.py +0 -0
  79. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/tests/test_glpi_client.py +0 -0
  80. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/tests/test_parity.py +0 -0
  81. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/clients/tests/test_smoke.py +0 -0
  82. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/content/__init__.py +0 -0
  83. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/content/conversion.py +0 -0
  84. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/content/tests/__init__.py +0 -0
  85. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/content/tests/test_conversion.py +0 -0
  86. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/__init__.py +0 -0
  87. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/_base.py +0 -0
  88. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/__init__.py +0 -0
  89. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/_common.py +0 -0
  90. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/_content.py +0 -0
  91. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/administration/__init__.py +0 -0
  92. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/administration/_entity.py +0 -0
  93. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/administration/_user.py +0 -0
  94. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/administration/tests/__init__.py +0 -0
  95. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/administration/tests/test_administration_schemas.py +0 -0
  96. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/__init__.py +0 -0
  97. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/_team.py +0 -0
  98. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/_ticket.py +0 -0
  99. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/tests/__init__.py +0 -0
  100. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/tests/test_assistance_schemas.py +0 -0
  101. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/tests/test_content_roundtrip.py +0 -0
  102. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/timeline/__init__.py +0 -0
  103. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/timeline/_document.py +0 -0
  104. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/timeline/_followup.py +0 -0
  105. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/timeline/_solution.py +0 -0
  106. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/timeline/_task.py +0 -0
  107. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/timeline/tests/__init__.py +0 -0
  108. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/assistance/timeline/tests/test_timeline_schemas.py +0 -0
  109. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/dropdowns/__init__.py +0 -0
  110. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/dropdowns/_location.py +0 -0
  111. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/dropdowns/tests/__init__.py +0 -0
  112. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/dropdowns/tests/test_dropdowns_schemas.py +0 -0
  113. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/enums.py +0 -0
  114. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/management/__init__.py +0 -0
  115. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/management/_document.py +0 -0
  116. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/management/tests/__init__.py +0 -0
  117. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/api_schema/management/tests/test_management_schemas.py +0 -0
  118. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/custom_schema/__init__.py +0 -0
  119. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/models/custom_schema/tests/__init__.py +0 -0
  120. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/py.typed +0 -0
  121. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/testing/__init__.py +0 -0
  122. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/testing/fixtures.py +0 -0
  123. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/glpi_python_client/testing/utils.py +0 -0
  124. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/skills/README.md +0 -0
  125. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/skills/glpi-client-setup/SKILL.md +0 -0
  126. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/skills/glpi-document-workflow/SKILL.md +0 -0
  127. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/skills/glpi-reporting-and-context/SKILL.md +0 -0
  128. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/skills/glpi-team-members/SKILL.md +0 -0
  129. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/skills/glpi-ticket-timeline/SKILL.md +0 -0
  130. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/skills/glpi-ticket-workflow/SKILL.md +0 -0
  131. {glpi_python_client-0.3.1 → glpi_python_client-0.3.3}/skills/glpi-user-location-provisioning/SKILL.md +0 -0
@@ -26,5 +26,6 @@ dist/
26
26
  secrets/
27
27
  secrets/*
28
28
 
29
- integration_tests/
30
- integration_tests/*
29
+ .claude/
30
+
31
+ CLAUDE.md
@@ -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.3
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/
@@ -0,0 +1,162 @@
1
+ Development
2
+ ===========
3
+
4
+ Local Setup
5
+ -----------
6
+
7
+ Create a virtual environment and install the development dependencies:
8
+
9
+ .. code-block:: console
10
+
11
+ python -m venv .venv
12
+ .venv\Scripts\activate
13
+ python -m pip install --upgrade pip
14
+ python -m pip install -e .[dev]
15
+ python -m pre_commit install
16
+
17
+ The repository ships a root ``.pre-commit-config.yaml`` that runs Ruff on each
18
+ commit. The lint hook applies safe fixes first, then Ruff formats the touched
19
+ files.
20
+
21
+ Quality Checks
22
+ --------------
23
+
24
+ Run the focused checks before opening a pull request:
25
+
26
+ .. code-block:: console
27
+
28
+ python -m pre_commit run --all-files
29
+ python -m pytest
30
+ python -m ruff check .
31
+ python -m mypy glpi_python_client
32
+ python -m sphinx -b html docs docs/_build/html
33
+
34
+ Integration Tests
35
+ -----------------
36
+
37
+ The ``integration_tests/`` directory holds end-to-end tests that drive a live
38
+ GLPI instance through the synchronous and asynchronous clients. They are
39
+ collected only when secrets resolve to a reachable instance; otherwise each
40
+ test self-skips via ``pytest.skip``. Every test cleans up the records it
41
+ creates in a ``finally`` block, but it is still recommended to point them at
42
+ a non-production GLPI.
43
+
44
+ Markers and CI behaviour
45
+ ~~~~~~~~~~~~~~~~~~~~~~~~
46
+
47
+ All integration tests are tagged with the ``integration`` pytest marker
48
+ declared in ``pyproject.toml``. The ``ci.yml`` workflow runs
49
+ ``pytest -m "not integration"`` for both the test matrix and the coverage
50
+ job, so the public CI never reaches a live GLPI. The default local
51
+ ``python -m pytest`` invocation *does* attempt to collect them, but they
52
+ skip automatically when no secrets are configured.
53
+
54
+ To explicitly opt in or out locally:
55
+
56
+ .. code-block:: console
57
+
58
+ python -m pytest -m integration # run only the live suite
59
+ python -m pytest -m "not integration" # mirror the CI behaviour
60
+
61
+ Configuration
62
+ ~~~~~~~~~~~~~
63
+
64
+ The suite reads each value from a file named after the secret under
65
+ ``secrets/`` at the repository root, falling back to the matching
66
+ environment variable when the file is absent. The ``secrets/`` directory is
67
+ gitignored. Each file contains a single trimmed value.
68
+
69
+ Required:
70
+
71
+ ============================== ========================= ===========================================
72
+ Secret file Environment variable Purpose
73
+ ============================== ========================= ===========================================
74
+ ``glpi_api_url`` ``GLPI_API_URL`` Base URL of the GLPI v2 API.
75
+ ``glpi_client_id_test`` ``GLPI_CLIENT_ID`` OAuth2 client identifier.
76
+ ``glpi_client_secret_test`` ``GLPI_CLIENT_SECRET`` OAuth2 client secret.
77
+ ``glpi_username`` ``GLPI_USERNAME`` GLPI user for the password grant.
78
+ ``glpi_password`` ``GLPI_PASSWORD`` Password for the GLPI user above.
79
+ ============================== ========================= ===========================================
80
+
81
+ Optional:
82
+
83
+ ============================== ============================= ============================================
84
+ Secret file Environment variable Purpose
85
+ ============================== ============================= ============================================
86
+ ``glpi_verify_ssl`` ``GLPI_VERIFY_SSL`` Toggle TLS verification (default ``false``).
87
+ ``glpi_entity`` ``GLPI_ENTITY`` Active entity id sent on every request.
88
+ ``glpi_profile`` ``GLPI_PROFILE`` Active profile id sent on every request.
89
+ ``glpi_entity_recursive`` ``GLPI_ENTITY_RECURSIVE`` Include sub-entities (default ``false``).
90
+ ``glpi_api_v1_url`` ``GLPI_API_V1_URL`` Base URL of the legacy v1 API.
91
+ ``glpi_api_v1_token_user`` ``GLPI_V1_USER_TOKEN`` v1 user token (enables document uploads).
92
+ ``glpi_api_v1_app_token`` ``GLPI_V1_APP_TOKEN`` v1 application token paired with the above.
93
+ ``glpi_team_member_role`` ``GLPI_TEAM_MEMBER_ROLE`` Role used to add the test user to a ticket.
94
+ ============================== ============================= ============================================
95
+
96
+ The v1 secrets are only required for the document-upload test; when they
97
+ are missing that single test skips while the rest of the suite runs.
98
+
99
+ Running the suite
100
+ ~~~~~~~~~~~~~~~~~
101
+
102
+ Once secrets are in place:
103
+
104
+ .. code-block:: console
105
+
106
+ python -m pytest integration_tests -m integration
107
+
108
+ Use a disposable GLPI instance or one whose entity is dedicated to
109
+ automated tests. The suite creates and deletes users, locations, tickets,
110
+ followups, tasks, and solutions on every run.
111
+
112
+ Package Layout
113
+ --------------
114
+
115
+ ``glpi_python_client.__init__``
116
+ Public import surface (``GlpiClient``, ``GlpiTicketContext``, public Pydantic
117
+ models, enums, and ``__version__``).
118
+
119
+ ``glpi_python_client.clients.glpi_client``
120
+ Composition root. ``GlpiClient`` mixes the per-resource async API mixins,
121
+ the OAuth2 token manager, the asynchronous v2 transport, and the optional
122
+ internal v1 session used for document uploads.
123
+
124
+ ``glpi_python_client.clients.api``
125
+ Async API mixins generated from the GLPI v2 OpenAPI contract: tickets,
126
+ ticket timeline (followups, tasks, solutions, documents), team members,
127
+ documents, users, locations, entities, ...
128
+
129
+ ``glpi_python_client.clients.custom``
130
+ Higher-level helpers built on top of the contract mixins:
131
+ ``get_ticket_context``, ``get_ticket_statistics``, ``get_task_statistics``.
132
+
133
+ ``glpi_python_client.clients.commons``
134
+ Shared HTTP transport pieces, including the timeline envelope unwrap that
135
+ reconciles live server behaviour with the OpenAPI contract.
136
+
137
+ ``glpi_python_client.models.api_schema``
138
+ Contract-aligned Pydantic v2 models (``Get``/``Post``/``Patch``/``Delete``)
139
+ for each GLPI v2 resource.
140
+
141
+ ``glpi_python_client.models.custom_schema``
142
+ Composite models such as ``GlpiTicketContext`` returned by the custom
143
+ helpers.
144
+
145
+ Adding Endpoints
146
+ ----------------
147
+
148
+ #. Add or extend the contract-aligned models in
149
+ ``glpi_python_client.models.api_schema``.
150
+ #. Add the async mixin and method under ``glpi_python_client.clients.api``,
151
+ mirroring the OpenAPI path and HTTP verb.
152
+ #. When the live server diverges from the contract, document the choice in the
153
+ module docstring and (when needed) wire an unwrap helper from
154
+ ``glpi_python_client.clients.commons``.
155
+ #. Re-export new public symbols from ``glpi_python_client.__init__``.
156
+ #. Add tests for payload serialization, response parsing, and client behaviour.
157
+ #. Document the workflow in :doc:`user_guide` and the matching skill in
158
+ ``skills/``.
159
+
160
+ Keep organization-specific entity, profile, and category defaults outside the
161
+ library core. Applications can apply their own mapping before calling the
162
+ client.
@@ -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
@@ -752,6 +776,14 @@ batches until the API returns fewer rows than the requested
752
776
  print(ticket.id, ticket.name)
753
777
  print(f"processed {total} tickets")
754
778
 
779
+ .. note::
780
+
781
+ Always pass an RSQL filter to ``iter_search_tickets``. Querying
782
+ without any filter can return very large result sets and may cause
783
+ the GLPI server to return a 500 error on busy instances. The other
784
+ two generators (``iter_search_users``, ``iter_search_entities``) are
785
+ not affected because those collections are typically much smaller.
786
+
755
787
  On the asynchronous client the same helpers are exposed as **async
756
788
  generators** through the bridge, so each ``next()`` call runs off the
757
789
  event loop and the consumer uses ``async for``:
@@ -766,8 +798,10 @@ event loop and the consumer uses ``async for``:
766
798
  ^^^^^^^^^^^^^^^^^^^^^^^^^
767
799
 
768
800
  Counts tickets created within an ISO date window and groups them by
769
- entity, status, priority, and type. Optional filters restrict the
770
- result set on the server side:
801
+ entity, status, priority, and type. The ``start_date`` is inclusive
802
+ from 00:00:00 and the ``end_date`` is inclusive through 23:59:59, so
803
+ tickets created at any time on those days are counted. Optional
804
+ filters restrict the result set on the server side:
771
805
 
772
806
  * ``entity_id`` — restrict to a single entity by numeric identifier.
773
807
  * ``entity_name`` — substring match against the entity ``name`` column;
@@ -869,7 +903,9 @@ then computes per-user and per-entity totals.
869
903
  Available filters:
870
904
 
871
905
  * ``start_date`` / ``end_date`` / ``default_days`` — ISO ``YYYY-MM-DD``
872
- date window; ``default_days`` is used when ``start_date`` is omitted.
906
+ date window; ``start_date`` is inclusive from 00:00:00,
907
+ ``end_date`` is inclusive through 23:59:59, and ``default_days``
908
+ is used when ``start_date`` is omitted.
873
909
  * ``entity_id`` — restrict to a single entity by identifier.
874
910
  * ``entity_name`` — substring match resolved through ``search_entities``;
875
911
  ignored when ``entity_id`` is given.
@@ -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.3"
76
76
 
77
77
  __all__ = [
78
78
  "AsyncGlpiClient",
@@ -7,11 +7,12 @@ Notes
7
7
  -----
8
8
  The live GLPI v2 server returns each entry of the list endpoint wrapped
9
9
  in a ``{"type": "Document_Item", "item": {...}}`` envelope, even though
10
- the OpenAPI contract documents a flat array of ``Document_Item``. Real
11
- behaviour wins over the contract, so :func:`list_ticket_timeline_documents`
12
- unwraps the envelope through the shared
13
- :meth:`~glpi_python_client.clients.commons._transport.TransportMixin._resource_list`
14
- helper and tolerates both shapes.
10
+ the OpenAPI contract documents a flat array of ``Document_Item``. The
11
+ ``item`` value is a full ``Document`` record (matching :class:`GetDocument`),
12
+ not a ``Document_Item`` link record — real behaviour wins over the contract.
13
+ :func:`list_ticket_timeline_documents` unwraps the envelope through the shared
14
+ ``TransportMixin._resource_list`` helper and deserialises each inner object
15
+ as :class:`GetDocument`.
15
16
  """
16
17
 
17
18
  from __future__ import annotations
@@ -24,19 +25,17 @@ from glpi_python_client.clients.commons._constants import (
24
25
  from glpi_python_client.clients.commons._transport import TransportMixin
25
26
  from glpi_python_client.models.api_schema.assistance.timeline._document import (
26
27
  DeleteTimelineDocument,
27
- GetTimelineDocument,
28
28
  PatchTimelineDocument,
29
29
  PostTimelineDocument,
30
30
  )
31
+ from glpi_python_client.models.api_schema.management._document import GetDocument
31
32
 
32
33
 
33
34
  class TimelineDocumentMixin(TransportMixin):
34
35
  """Synchronous CRUD helpers for the ticket document timeline endpoint."""
35
36
 
36
- def list_ticket_timeline_documents(
37
- self, ticket_id: GlpiId
38
- ) -> list[GetTimelineDocument]:
39
- """List all timeline documents linked to one ticket.
37
+ def list_ticket_timeline_documents(self, ticket_id: GlpiId) -> list[GetDocument]:
38
+ """List all documents linked to one ticket timeline.
40
39
 
41
40
  Parameters
42
41
  ----------
@@ -45,14 +44,16 @@ class TimelineDocumentMixin(TransportMixin):
45
44
 
46
45
  Returns
47
46
  -------
48
- list[GetTimelineDocument]
49
- Document links returned by the GLPI server, with the timeline
50
- envelope unwrapped where present.
47
+ list[GetDocument]
48
+ Document records returned by the GLPI server. The live API
49
+ wraps each entry in a ``{"type": "Document_Item", "item": {...}}``
50
+ envelope whose ``item`` value is a full ``Document`` record; the
51
+ envelope is unwrapped automatically.
51
52
  """
52
53
 
53
54
  return self._resource_list(
54
55
  f"{TICKET_ENDPOINT}/{ticket_id}/{TIMELINE_DOCUMENT_SUFFIX}",
55
- GetTimelineDocument,
56
+ GetDocument,
56
57
  failure_message=(
57
58
  f"Failed to list timeline documents for ticket {ticket_id}"
58
59
  ),
@@ -61,20 +62,20 @@ class TimelineDocumentMixin(TransportMixin):
61
62
 
62
63
  def get_ticket_timeline_document(
63
64
  self, ticket_id: GlpiId, document_link_id: GlpiId
64
- ) -> GetTimelineDocument:
65
- """Fetch one timeline document link by identifier.
65
+ ) -> GetDocument:
66
+ """Fetch one document linked to the ticket timeline by its document ID.
66
67
 
67
68
  Parameters
68
69
  ----------
69
70
  ticket_id : GlpiId
70
71
  Numeric identifier of the parent ticket.
71
72
  document_link_id : GlpiId
72
- Numeric identifier of the timeline document link to retrieve.
73
+ Numeric identifier of the linked document to retrieve.
73
74
 
74
75
  Returns
75
76
  -------
76
- GetTimelineDocument
77
- Validated document-link payload.
77
+ GetDocument
78
+ Validated document payload.
78
79
 
79
80
  Raises
80
81
  ------
@@ -85,7 +86,7 @@ class TimelineDocumentMixin(TransportMixin):
85
86
  return self._resource_get(
86
87
  f"{TICKET_ENDPOINT}/{ticket_id}/"
87
88
  f"{TIMELINE_DOCUMENT_SUFFIX}/{document_link_id}",
88
- GetTimelineDocument,
89
+ GetDocument,
89
90
  failure_message=(
90
91
  f"Failed to get timeline document {document_link_id} on "
91
92
  f"ticket {ticket_id}"
@@ -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"]