glpi-python-client 0.3.3__tar.gz → 0.3.4__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 (137) hide show
  1. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/PKG-INFO +1 -1
  2. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/api_reference.rst +30 -0
  3. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/user_guide.rst +59 -7
  4. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/__init__.py +9 -1
  5. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/auth/_v1_session.py +173 -49
  6. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/auth/tests/test_v1_session.py +184 -21
  7. glpi_python_client-0.3.3/glpi_python_client/clients/sync_client.py → glpi_python_client-0.3.4/glpi_python_client/clients/_base_client.py +21 -106
  8. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/__init__.py +2 -0
  9. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/management/_document.py +1 -7
  10. glpi_python_client-0.3.4/glpi_python_client/clients/api/plugins/__init__.py +10 -0
  11. glpi_python_client-0.3.4/glpi_python_client/clients/api/plugins/_fields.py +411 -0
  12. glpi_python_client-0.3.4/glpi_python_client/clients/api/plugins/tests/test_fields_mixin.py +377 -0
  13. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/async_client.py +14 -90
  14. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/_config.py +1 -1
  15. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/_http.py +14 -0
  16. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/_transport.py +29 -0
  17. glpi_python_client-0.3.4/glpi_python_client/clients/sync_client.py +129 -0
  18. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/__init__.py +10 -0
  19. glpi_python_client-0.3.4/glpi_python_client/models/api_schema/plugins/__init__.py +19 -0
  20. glpi_python_client-0.3.4/glpi_python_client/models/api_schema/plugins/_fields.py +177 -0
  21. glpi_python_client-0.3.4/glpi_python_client/models/api_schema/plugins/tests/__init__.py +0 -0
  22. glpi_python_client-0.3.4/glpi_python_client/models/api_schema/plugins/tests/test_fields_schemas.py +82 -0
  23. glpi_python_client-0.3.4/glpi_python_client/py.typed +0 -0
  24. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/testing/utils.py +2 -0
  25. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/pyproject.toml +1 -1
  26. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/.gitignore +0 -0
  27. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/.pre-commit-config.yaml +0 -0
  28. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/.readthedocs.yaml +0 -0
  29. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/CONTRIBUTING.md +0 -0
  30. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/LICENSE +0 -0
  31. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/README.md +0 -0
  32. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/_static/.gitkeep +0 -0
  33. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/conf.py +0 -0
  34. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/development.md +0 -0
  35. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/development_rtd.rst +0 -0
  36. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/glpi_api_contract.json +0 -0
  37. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/index.rst +0 -0
  38. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/installation.rst +0 -0
  39. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/publishing.md +0 -0
  40. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/publishing_rtd.rst +0 -0
  41. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/docs/sponsoring.rst +0 -0
  42. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/auth/__init__.py +0 -0
  43. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/auth/auth.py +0 -0
  44. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/auth/tests/test_auth.py +0 -0
  45. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/__init__.py +0 -0
  46. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/administration/__init__.py +0 -0
  47. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/administration/_entity.py +0 -0
  48. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/administration/_user.py +0 -0
  49. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/assistance/__init__.py +0 -0
  50. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/assistance/_team.py +0 -0
  51. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/assistance/_ticket.py +0 -0
  52. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/assistance/timeline/__init__.py +0 -0
  53. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/assistance/timeline/_document.py +0 -0
  54. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/assistance/timeline/_followup.py +0 -0
  55. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/assistance/timeline/_solution.py +0 -0
  56. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/assistance/timeline/_task.py +0 -0
  57. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/dropdowns/__init__.py +0 -0
  58. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/dropdowns/_location.py +0 -0
  59. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/api/management/__init__.py +0 -0
  60. /glpi_python_client-0.3.3/glpi_python_client/py.typed → /glpi_python_client-0.3.4/glpi_python_client/clients/api/plugins/tests/__init__.py +0 -0
  61. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/__init__.py +0 -0
  62. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/_async_bridge.py +0 -0
  63. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/_constants.py +0 -0
  64. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/_errors.py +0 -0
  65. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/_filters.py +0 -0
  66. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/_payloads.py +0 -0
  67. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/tests/__init__.py +0 -0
  68. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/tests/test_errors.py +0 -0
  69. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/tests/test_filters.py +0 -0
  70. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/tests/test_http.py +0 -0
  71. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/tests/test_payloads.py +0 -0
  72. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/commons/tests/test_transport.py +0 -0
  73. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/custom/__init__.py +0 -0
  74. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/custom/_pagination_async.py +0 -0
  75. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/custom/_statistics.py +0 -0
  76. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/custom/_statistics_async.py +0 -0
  77. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/custom/_ticket_context.py +0 -0
  78. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/custom/_ticket_context_async.py +0 -0
  79. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/custom/tests/test_statistics.py +0 -0
  80. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/custom/tests/test_statistics_async.py +0 -0
  81. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/custom/tests/test_ticket_context.py +0 -0
  82. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/tests/__init__.py +0 -0
  83. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/tests/test_api_coverage.py +0 -0
  84. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/tests/test_async_branches.py +0 -0
  85. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/tests/test_async_smoke.py +0 -0
  86. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/tests/test_glpi_client.py +0 -0
  87. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/tests/test_parity.py +0 -0
  88. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/clients/tests/test_smoke.py +0 -0
  89. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/content/__init__.py +0 -0
  90. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/content/conversion.py +0 -0
  91. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/content/tests/__init__.py +0 -0
  92. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/content/tests/test_conversion.py +0 -0
  93. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/_base.py +0 -0
  94. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/__init__.py +0 -0
  95. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/_common.py +0 -0
  96. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/_content.py +0 -0
  97. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/administration/__init__.py +0 -0
  98. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/administration/_entity.py +0 -0
  99. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/administration/_user.py +0 -0
  100. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/administration/tests/__init__.py +0 -0
  101. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/administration/tests/test_administration_schemas.py +0 -0
  102. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/__init__.py +0 -0
  103. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/_team.py +0 -0
  104. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/_ticket.py +0 -0
  105. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/tests/__init__.py +0 -0
  106. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/tests/test_assistance_schemas.py +0 -0
  107. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/tests/test_content_roundtrip.py +0 -0
  108. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/timeline/__init__.py +0 -0
  109. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/timeline/_document.py +0 -0
  110. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/timeline/_followup.py +0 -0
  111. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/timeline/_solution.py +0 -0
  112. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/timeline/_task.py +0 -0
  113. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/timeline/tests/__init__.py +0 -0
  114. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/assistance/timeline/tests/test_timeline_schemas.py +0 -0
  115. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/dropdowns/__init__.py +0 -0
  116. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/dropdowns/_location.py +0 -0
  117. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/dropdowns/tests/__init__.py +0 -0
  118. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/dropdowns/tests/test_dropdowns_schemas.py +0 -0
  119. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/enums.py +0 -0
  120. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/management/__init__.py +0 -0
  121. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/management/_document.py +0 -0
  122. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/management/tests/__init__.py +0 -0
  123. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/api_schema/management/tests/test_management_schemas.py +0 -0
  124. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/custom_schema/__init__.py +0 -0
  125. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/custom_schema/_ticket_context.py +0 -0
  126. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/custom_schema/tests/__init__.py +0 -0
  127. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/models/custom_schema/tests/test_ticket_context.py +0 -0
  128. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/testing/__init__.py +0 -0
  129. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/glpi_python_client/testing/fixtures.py +0 -0
  130. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/skills/README.md +0 -0
  131. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/skills/glpi-client-setup/SKILL.md +0 -0
  132. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/skills/glpi-document-workflow/SKILL.md +0 -0
  133. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/skills/glpi-reporting-and-context/SKILL.md +0 -0
  134. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/skills/glpi-team-members/SKILL.md +0 -0
  135. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/skills/glpi-ticket-timeline/SKILL.md +0 -0
  136. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/skills/glpi-ticket-workflow/SKILL.md +0 -0
  137. {glpi_python_client-0.3.3 → glpi_python_client-0.3.4}/skills/glpi-user-location-provisioning/SKILL.md +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: glpi-python-client
3
- Version: 0.3.3
3
+ Version: 0.3.4
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/
@@ -289,6 +289,36 @@ Entities
289
289
  :undoc-members:
290
290
  :show-inheritance:
291
291
 
292
+ Plugin: Fields (custom fields)
293
+ ------------------------------
294
+
295
+ Schemas returned by the GLPI ``Fields`` plugin (legacy v1 REST endpoints).
296
+ The companion mixin methods are exposed on :class:`GlpiClient` /
297
+ :class:`AsyncGlpiClient` as ``list_plugin_fields_containers``,
298
+ ``list_plugin_fields_fields``, ``list_item_plugin_field_rows``,
299
+ ``create_item_plugin_field_row``, ``update_item_plugin_field_row``,
300
+ ``get_ticket_custom_fields`` and ``set_ticket_custom_fields``.
301
+
302
+ .. autoclass:: GetPluginFieldsContainer
303
+ :members:
304
+ :undoc-members:
305
+ :show-inheritance:
306
+
307
+ .. autoclass:: GetPluginFieldsField
308
+ :members:
309
+ :undoc-members:
310
+ :show-inheritance:
311
+
312
+ .. autoclass:: GetPluginFieldsValueRow
313
+ :members:
314
+ :undoc-members:
315
+ :show-inheritance:
316
+
317
+ .. autoclass:: PostPluginFieldsValueRow
318
+ :members:
319
+ :undoc-members:
320
+ :show-inheritance:
321
+
292
322
  Enums
293
323
  -----
294
324
 
@@ -11,8 +11,10 @@ whose surface is built from contract-aligned per-endpoint mixins:
11
11
  it to a worker thread via :func:`asyncio.to_thread`.
12
12
 
13
13
  Both clients speak the GLPI **v2** high-level API and fall back to the
14
- legacy v1 endpoint only for binary document uploads. They expose the
15
- exact same endpoint methods and accept the same constructor arguments.
14
+ legacy v1 API for features that are not exposed by v2, currently
15
+ binary document uploads and the ``Fields`` plugin custom-field
16
+ helpers. They expose the exact same endpoint methods and accept the
17
+ same constructor arguments.
16
18
  Public methods always return Pydantic models (or simple Python types)
17
19
  and never raw dictionaries.
18
20
 
@@ -36,7 +38,8 @@ The guide is split into the following sections:
36
38
  one-to-one to GLPI v2 endpoints (tickets, timeline, team members,
37
39
  users, locations, entities, documents).
38
40
  5. **Added functionalities** — helpers built on top of the API mixins:
39
- the aggregated ticket context view and the reporting helpers.
41
+ the ``Fields`` plugin custom-field helpers, the aggregated ticket
42
+ context view, and the reporting helpers.
40
43
  6. **End-to-end examples** — full workflows that combine the previous
41
44
  building blocks.
42
45
 
@@ -111,7 +114,9 @@ Optional constructor arguments
111
114
  * ``auth_token_refresh`` — number of seconds before token expiry at
112
115
  which the auth manager proactively refreshes the OAuth access token.
113
116
  * ``v1_base_url`` and ``v1_user_token`` — together enable the legacy v1
114
- fallback used by :meth:`GlpiClient.upload_document`.
117
+ fallback used by :meth:`GlpiClient.upload_document` and the
118
+ ``Fields`` plugin helpers such as
119
+ :meth:`GlpiClient.get_ticket_custom_fields`.
115
120
  * ``executor`` (:class:`AsyncGlpiClient` only) — an explicit
116
121
  :class:`concurrent.futures.Executor` used to dispatch the wrapped
117
122
  synchronous calls. Defaults to the standard library thread pool
@@ -616,6 +621,55 @@ Example output::
616
621
  The helpers in this section are not part of the GLPI contract. They are
617
622
  small utilities the client builds on top of the API mixins.
618
623
 
624
+ Ticket custom fields via the Fields plugin
625
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
626
+
627
+ The `Fields plugin <https://github.com/pluginsGLPI/fields>`_ exposes
628
+ ticket custom fields through the legacy v1 API rather than the GLPI v2
629
+ contract. Configure the client with ``v1_base_url`` and
630
+ ``v1_user_token`` (or the matching ``GLPI_V1_*`` environment
631
+ variables), then use the discovery helpers when you need the plugin's
632
+ internal container and field names:
633
+
634
+ .. code-block:: python
635
+
636
+ from glpi_python_client import GlpiClient
637
+
638
+ with GlpiClient(
639
+ glpi_api_url="https://glpi.example.com/api.php/v2",
640
+ client_id="oauth-client-id",
641
+ client_secret="oauth-client-secret",
642
+ username="api-user",
643
+ password="api-password",
644
+ v1_base_url="https://glpi.example.com/apirest.php",
645
+ v1_user_token="legacy-user-token",
646
+ ) as client:
647
+ containers = client.list_plugin_fields_containers(itemtype="Ticket")
648
+ for container in containers:
649
+ print(container.id, container.name)
650
+ fields = client.list_plugin_fields_fields(container_id=container.id)
651
+ print([field.name for field in fields])
652
+
653
+ custom_fields = client.get_ticket_custom_fields(ticket_id)
654
+ print(custom_fields)
655
+
656
+ client.set_ticket_custom_fields(
657
+ ticket_id,
658
+ {
659
+ "aidelarsolution": {
660
+ "aidelarsolutionfield": "<p>Handled by the NOC shift</p>",
661
+ }
662
+ },
663
+ )
664
+
665
+ The high-level ``get_ticket_custom_fields`` /
666
+ ``set_ticket_custom_fields`` pair uses the mapping
667
+ ``{container_name: {field_name: value}}`` and automatically decides
668
+ whether the v1 plugin needs a row creation or an in-place update. Drop
669
+ to ``list_item_plugin_field_rows``, ``create_item_plugin_field_row``,
670
+ or ``update_item_plugin_field_row`` only when you need the raw v1 row
671
+ shape.
672
+
619
673
  Aggregated ticket context
620
674
  ~~~~~~~~~~~~~~~~~~~~~~~~~
621
675
 
@@ -780,9 +834,7 @@ batches until the API returns fewer rows than the requested
780
834
 
781
835
  Always pass an RSQL filter to ``iter_search_tickets``. Querying
782
836
  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.
837
+ the GLPI server to return a 500 errors.
786
838
 
787
839
  On the asynchronous client the same helpers are exposed as **async
788
840
  generators** through the bridge, so each ``next()`` call runs off the
@@ -30,6 +30,9 @@ from glpi_python_client.models import (
30
30
  GetEntity,
31
31
  GetFollowup,
32
32
  GetLocation,
33
+ GetPluginFieldsContainer,
34
+ GetPluginFieldsField,
35
+ GetPluginFieldsValueRow,
33
36
  GetSolution,
34
37
  GetTeamMember,
35
38
  GetTicket,
@@ -63,6 +66,7 @@ from glpi_python_client.models import (
63
66
  PostEntity,
64
67
  PostFollowup,
65
68
  PostLocation,
69
+ PostPluginFieldsValueRow,
66
70
  PostSolution,
67
71
  PostTeamMember,
68
72
  PostTicket,
@@ -72,7 +76,7 @@ from glpi_python_client.models import (
72
76
  TicketMarkdownOptions,
73
77
  )
74
78
 
75
- __version__ = "0.3.3"
79
+ __version__ = "0.3.4"
76
80
 
77
81
  __all__ = [
78
82
  "AsyncGlpiClient",
@@ -90,6 +94,9 @@ __all__ = [
90
94
  "GetEntity",
91
95
  "GetFollowup",
92
96
  "GetLocation",
97
+ "GetPluginFieldsContainer",
98
+ "GetPluginFieldsField",
99
+ "GetPluginFieldsValueRow",
93
100
  "GetSolution",
94
101
  "GetTeamMember",
95
102
  "GetTicket",
@@ -124,6 +131,7 @@ __all__ = [
124
131
  "PostEntity",
125
132
  "PostFollowup",
126
133
  "PostLocation",
134
+ "PostPluginFieldsValueRow",
127
135
  "PostSolution",
128
136
  "PostTeamMember",
129
137
  "PostTicket",
@@ -1,10 +1,27 @@
1
- """GLPI v1 REST session used exclusively for document uploads.
2
-
3
- The high-level async ``GlpiClient`` only relies on the legacy v1 API for the
4
- ``POST /Document`` multipart upload endpoint. The session wrapper below owns
5
- the authenticated v1 lifecycle (init, refresh, kill) and exposes a single
6
- ``upload_document`` operation that the management mixin calls through
7
- ``asyncio.to_thread`` at the blocking HTTP boundary.
1
+ """GLPI v1 REST session used for legacy endpoints not exposed by v2.
2
+
3
+ Two consumers currently share this session:
4
+
5
+ * the management :class:`DocumentMixin` for the multipart
6
+ ``POST /Document`` upload (the v2 API does not advertise a binary
7
+ upload route), and
8
+ * the :class:`PluginFieldsMixin` for the GLPI "Fields" plugin endpoints
9
+ (``PluginFieldsContainer``, ``PluginFieldsField`` and the per-item
10
+ value itemtypes), which the v2 contract does not surface at all.
11
+
12
+ The session wrapper owns the authenticated v1 lifecycle (init, refresh,
13
+ kill) and exposes the typed ``upload_document`` helper plus the generic
14
+ ``request_json`` JSON-only HTTP helper that newer mixins build on.
15
+
16
+ Retry policy
17
+ ------------
18
+ Every public dispatch helper (``_init_session``, ``request_json``,
19
+ ``upload_document``) carries the same :mod:`tenacity` retry decorator
20
+ used by the v2 transport: three attempts spaced by three seconds,
21
+ triggered exclusively by :class:`requests.RequestException` (which
22
+ :func:`finalize_request_response` raises for 5xx server errors).
23
+ :class:`ValueError` raised by status-code or payload checks does not
24
+ trigger a retry — client-side or 4xx failures are surfaced immediately.
8
25
  """
9
26
 
10
27
  from __future__ import annotations
@@ -12,15 +29,26 @@ from __future__ import annotations
12
29
  import json
13
30
  import logging
14
31
  from datetime import datetime, timedelta, timezone
15
- from typing import cast
32
+ from typing import Any, cast
16
33
 
17
34
  import requests
18
- from tenacity import retry, stop_after_attempt, wait_fixed
35
+ from tenacity import retry, retry_if_exception_type, stop_after_attempt, wait_fixed
36
+
37
+ from glpi_python_client.clients.commons._http import (
38
+ ensure_response_status,
39
+ finalize_request_response,
40
+ response_json_or_empty,
41
+ )
19
42
 
20
43
  logger = logging.getLogger(__name__)
21
44
 
22
45
  _DEFAULT_SESSION_REFRESH_INTERVAL_SECONDS = 15 * 60
23
46
  _AUTH_FAILURE_STATUS_CODES = frozenset({401, 403})
47
+ _RETRY_ON_NETWORK_ERRORS = retry(
48
+ retry=retry_if_exception_type(requests.RequestException),
49
+ stop=stop_after_attempt(3),
50
+ wait=wait_fixed(3),
51
+ )
24
52
 
25
53
 
26
54
  class GLPIV1Session:
@@ -36,7 +64,7 @@ class GLPIV1Session:
36
64
  *,
37
65
  base_url: str,
38
66
  user_token: str,
39
- app_token: str,
67
+ app_token: str | None = None,
40
68
  verify_ssl: bool = True,
41
69
  session_refresh_interval_seconds: int = (
42
70
  _DEFAULT_SESSION_REFRESH_INTERVAL_SECONDS
@@ -59,12 +87,14 @@ class GLPIV1Session:
59
87
  self._session_token: str | None = None
60
88
  self._session_started_at: datetime | None = None
61
89
 
62
- @retry(stop=stop_after_attempt(3), wait=wait_fixed(3))
90
+ @_RETRY_ON_NETWORK_ERRORS
63
91
  def _init_session(self) -> None:
64
92
  """Acquire one fresh GLPI v1 session token via ``GET /initSession``.
65
93
 
66
94
  The call replaces any existing session state and stores the
67
95
  authentication timestamp used by the refresh-interval check.
96
+ Network errors and 5xx responses are retried; 4xx and payload
97
+ errors propagate immediately as :class:`ValueError`.
68
98
  """
69
99
 
70
100
  headers: dict[str, str] = {
@@ -75,16 +105,20 @@ class GLPIV1Session:
75
105
  if self._app_token:
76
106
  headers["App-Token"] = self._app_token
77
107
 
78
- response = self._http.get(
79
- f"{self._base_url}/initSession",
80
- headers=headers,
81
- timeout=30,
108
+ url = f"{self._base_url}/initSession"
109
+ response = self._http.get(url, headers=headers, timeout=30)
110
+ finalize_request_response(
111
+ response,
112
+ method="get",
113
+ url=url,
114
+ success_statuses=(200,),
115
+ logger=logger,
116
+ )
117
+ ensure_response_status(
118
+ response,
119
+ success_statuses=(200,),
120
+ failure_message="GLPI v1 initSession failed",
82
121
  )
83
- if response.status_code != 200:
84
- raise ValueError(
85
- "GLPI v1 initSession failed: "
86
- f"{response.status_code} {response.text[:300]}"
87
- )
88
122
 
89
123
  token = response.json().get("session_token")
90
124
  if not token:
@@ -140,11 +174,12 @@ class GLPIV1Session:
140
174
  """Drop the current GLPI v1 session token and acquire a new one.
141
175
 
142
176
  The previous token is best-effort killed so the GLPI server can release
143
- the associated session state immediately.
177
+ the associated session state immediately. ``_init_session`` will set
178
+ the new token on success or raise, leaving the existing state
179
+ untouched on failure (the retry decorator handles transients).
144
180
  """
145
181
 
146
- old_token = self._session_token
147
- if old_token is not None:
182
+ if self._session_token is not None:
148
183
  try:
149
184
  self._http.get(
150
185
  f"{self._base_url}/killSession",
@@ -153,8 +188,6 @@ class GLPIV1Session:
153
188
  )
154
189
  except Exception:
155
190
  logger.warning("Failed to kill stale GLPI v1 session.", exc_info=True)
156
- self._session_token = None
157
- self._session_started_at = None
158
191
  self._init_session()
159
192
 
160
193
  def _headers(self) -> dict[str, str]:
@@ -172,13 +205,19 @@ class GLPIV1Session:
172
205
  method: str,
173
206
  url: str,
174
207
  *,
208
+ success_statuses: tuple[int, ...],
175
209
  headers: dict[str, str] | None = None,
176
- **kwargs: object,
210
+ **kwargs: Any,
177
211
  ) -> requests.Response:
178
- """Send one authenticated GLPI v1 request with one auth-failure retry.
179
-
180
- When the GLPI server rejects the current token, the helper renews the
181
- session and retries the request once before returning the response.
212
+ """Send one authenticated GLPI v1 request and finalize the response.
213
+
214
+ When the GLPI server rejects the current token the helper renews
215
+ the session and retries the request once. The returned response
216
+ has already been passed through :func:`finalize_request_response`
217
+ so 5xx errors surface as :class:`requests.HTTPError` for the
218
+ outer tenacity retry to catch; non-success statuses outside the
219
+ ``success_statuses`` set are logged but otherwise returned for
220
+ the caller to validate with :func:`ensure_response_status`.
182
221
  """
183
222
 
184
223
  request_headers = {**self._headers(), **(headers or {})}
@@ -187,18 +226,23 @@ class GLPIV1Session:
187
226
  requests.Response,
188
227
  request_method(url, headers=request_headers, **kwargs),
189
228
  )
190
- if not _is_auth_failure_response(response):
191
- return response
192
-
193
- logger.warning(
194
- "GLPI v1 session token was rejected; refreshing session and retrying "
195
- "request once."
196
- )
197
- self._renew_session()
198
- request_headers = {**self._headers(), **(headers or {})}
199
- return cast(
200
- requests.Response,
201
- request_method(url, headers=request_headers, **kwargs),
229
+ if _is_auth_failure_response(response):
230
+ logger.warning(
231
+ "GLPI v1 session token was rejected; refreshing session and "
232
+ "retrying request once."
233
+ )
234
+ self._renew_session()
235
+ request_headers = {**self._headers(), **(headers or {})}
236
+ response = cast(
237
+ requests.Response,
238
+ request_method(url, headers=request_headers, **kwargs),
239
+ )
240
+ return finalize_request_response(
241
+ response,
242
+ method=method,
243
+ url=url,
244
+ success_statuses=success_statuses,
245
+ logger=logger,
202
246
  )
203
247
 
204
248
  def close(self) -> None:
@@ -223,7 +267,84 @@ class GLPIV1Session:
223
267
  self._session_started_at = None
224
268
  self._http.close()
225
269
 
226
- @retry(stop=stop_after_attempt(3), wait=wait_fixed(3))
270
+ @_RETRY_ON_NETWORK_ERRORS
271
+ def request_json(
272
+ self,
273
+ method: str,
274
+ path: str,
275
+ *,
276
+ params: dict[str, object] | None = None,
277
+ json_body: dict[str, object] | None = None,
278
+ success_statuses: tuple[int, ...] = (200, 201, 204, 206),
279
+ failure_message: str | None = None,
280
+ ) -> object:
281
+ """Send one JSON-only authenticated request to the GLPI v1 API.
282
+
283
+ The helper centralises session-token handling, the one-shot retry
284
+ on token rejection, status validation and JSON parsing so callers
285
+ can stay focused on their endpoint semantics. Network errors and
286
+ 5xx responses are retried; 4xx and payload errors propagate
287
+ immediately as :class:`ValueError`.
288
+
289
+ Parameters
290
+ ----------
291
+ method : str
292
+ HTTP verb (``"GET"``, ``"POST"``, ``"PUT"``, ``"DELETE"``).
293
+ path : str
294
+ Resource path appended to the v1 base URL (without leading
295
+ slash, e.g. ``"PluginFieldsContainer"``).
296
+ params : dict[str, object] | None, optional
297
+ Query-string parameters forwarded to ``requests``.
298
+ json_body : dict[str, object] | None, optional
299
+ JSON body serialised into the request when set. The
300
+ ``Content-Type: application/json`` header is added
301
+ automatically.
302
+ success_statuses : tuple[int, ...], optional
303
+ HTTP status codes considered successful (default covers the
304
+ CRUD codes returned by the v1 API).
305
+ failure_message : str | None, optional
306
+ Prefix used in the :class:`ValueError` raised on a
307
+ non-success status. Defaults to ``"GLPI v1 {METHOD} {path}
308
+ failed"``.
309
+
310
+ Returns
311
+ -------
312
+ object
313
+ Parsed JSON body for non-empty responses; an empty ``dict``
314
+ when the body is empty or contains only whitespace.
315
+
316
+ Raises
317
+ ------
318
+ ValueError
319
+ If the v1 server returns a non-success HTTP status outside
320
+ the 5xx range (which surfaces as :class:`requests.HTTPError`
321
+ and is retried).
322
+ """
323
+
324
+ url = f"{self._base_url}/{path.lstrip('/')}"
325
+ kwargs: dict[str, Any] = {"timeout": 30}
326
+ if params is not None:
327
+ kwargs["params"] = params
328
+ headers: dict[str, str] = {}
329
+ if json_body is not None:
330
+ kwargs["data"] = json.dumps(json_body)
331
+ headers["Content-Type"] = "application/json"
332
+ response = self._authenticated_request(
333
+ method,
334
+ url,
335
+ success_statuses=success_statuses,
336
+ headers=headers or None,
337
+ **kwargs,
338
+ )
339
+ ensure_response_status(
340
+ response,
341
+ success_statuses=success_statuses,
342
+ failure_message=failure_message
343
+ or f"GLPI v1 {method.upper()} {path} failed",
344
+ )
345
+ return response_json_or_empty(response)
346
+
347
+ @_RETRY_ON_NETWORK_ERRORS
227
348
  def upload_document(
228
349
  self,
229
350
  filename: str,
@@ -238,7 +359,9 @@ class GLPIV1Session:
238
359
 
239
360
  The legacy v1 endpoint uses a multipart upload manifest so the GLPI
240
361
  server can create the document, link it to the optional parent ticket,
241
- and assign it to the provided entity in a single round-trip.
362
+ and assign it to the provided entity in a single round-trip. Network
363
+ errors and 5xx responses are retried; 4xx and payload errors
364
+ propagate immediately as :class:`ValueError`.
242
365
  """
243
366
 
244
367
  manifest_input: dict[str, object] = {
@@ -255,17 +378,18 @@ class GLPIV1Session:
255
378
  response = self._authenticated_request(
256
379
  "POST",
257
380
  f"{self._base_url}/Document",
381
+ success_statuses=(200, 201),
258
382
  files=[
259
383
  ("uploadManifest", (None, manifest, "application/json")),
260
384
  ("filename[]", (filename, content, mime_type)),
261
385
  ],
262
386
  timeout=60,
263
387
  )
264
- if response.status_code not in (200, 201):
265
- raise ValueError(
266
- "GLPI v1 document upload failed: "
267
- f"{response.status_code} {response.text[:300]}"
268
- )
388
+ ensure_response_status(
389
+ response,
390
+ success_statuses=(200, 201),
391
+ failure_message="GLPI v1 document upload failed",
392
+ )
269
393
  payload = response.json()
270
394
  if not isinstance(payload, dict):
271
395
  raise ValueError(