glpi-python-client 0.4.3__tar.gz → 0.6.0__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 (173) hide show
  1. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/.gitignore +6 -1
  2. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/CHANGELOG.md +469 -0
  3. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/CONTRIBUTING.md +17 -1
  4. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/PKG-INFO +14 -11
  5. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/README.md +6 -2
  6. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/api_reference.rst +177 -5
  7. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/conf.py +1 -5
  8. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/development.md +12 -1
  9. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/installation.rst +1 -1
  10. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/publishing_rtd.rst +1 -1
  11. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/user_guide.rst +320 -11
  12. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/__init__.py +45 -1
  13. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/auth/_v1_session.py +3 -3
  14. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/auth/auth.py +3 -3
  15. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/_base_client.py +1 -7
  16. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/__init__.py +13 -3
  17. glpi_python_client-0.6.0/glpi_python_client/_async/clients/api/assets/__init__.py +7 -0
  18. glpi_python_client-0.6.0/glpi_python_client/_async/clients/api/assets/_computer.py +427 -0
  19. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/dropdowns/__init__.py +4 -1
  20. glpi_python_client-0.6.0/glpi_python_client/_async/clients/api/dropdowns/_contract_type.py +220 -0
  21. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/management/__init__.py +2 -1
  22. glpi_python_client-0.6.0/glpi_python_client/_async/clients/api/management/_contract.py +411 -0
  23. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/client.py +7 -6
  24. {glpi_python_client-0.4.3/glpi_python_client/_sync → glpi_python_client-0.6.0/glpi_python_client/_async}/clients/commons/_constants.py +9 -1
  25. {glpi_python_client-0.4.3/glpi_python_client/_sync → glpi_python_client-0.6.0/glpi_python_client/_async}/clients/commons/_payloads.py +8 -6
  26. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_errors.py +36 -4
  27. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/auth/_v1_session.py +3 -3
  28. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/auth/auth.py +3 -3
  29. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/_base_client.py +1 -7
  30. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/__init__.py +13 -3
  31. glpi_python_client-0.6.0/glpi_python_client/_sync/clients/api/assets/__init__.py +7 -0
  32. glpi_python_client-0.6.0/glpi_python_client/_sync/clients/api/assets/_computer.py +427 -0
  33. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/dropdowns/__init__.py +4 -1
  34. glpi_python_client-0.6.0/glpi_python_client/_sync/clients/api/dropdowns/_contract_type.py +220 -0
  35. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/management/__init__.py +2 -1
  36. glpi_python_client-0.6.0/glpi_python_client/_sync/clients/api/management/_contract.py +411 -0
  37. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/client.py +7 -6
  38. {glpi_python_client-0.4.3/glpi_python_client/_async → glpi_python_client-0.6.0/glpi_python_client/_sync}/clients/commons/_constants.py +9 -1
  39. {glpi_python_client-0.4.3/glpi_python_client/_async → glpi_python_client-0.6.0/glpi_python_client/_sync}/clients/commons/_payloads.py +8 -6
  40. glpi_python_client-0.6.0/glpi_python_client/content/conversion.py +675 -0
  41. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/__init__.py +48 -3
  42. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/_base.py +135 -6
  43. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/__init__.py +8 -1
  44. glpi_python_client-0.6.0/glpi_python_client/models/api_schema/_content.py +291 -0
  45. glpi_python_client-0.6.0/glpi_python_client/models/api_schema/assets/__init__.py +25 -0
  46. glpi_python_client-0.6.0/glpi_python_client/models/api_schema/assets/_computer.py +257 -0
  47. glpi_python_client-0.6.0/glpi_python_client/models/api_schema/assets/_contract_item.py +102 -0
  48. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/assistance/_ticket.py +30 -9
  49. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/assistance/timeline/_followup.py +29 -8
  50. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/assistance/timeline/_solution.py +29 -8
  51. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/assistance/timeline/_task.py +29 -8
  52. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/dropdowns/__init__.py +10 -0
  53. glpi_python_client-0.6.0/glpi_python_client/models/api_schema/dropdowns/_contract_type.py +100 -0
  54. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/enums.py +14 -0
  55. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/knowledgebase/_article.py +49 -7
  56. glpi_python_client-0.6.0/glpi_python_client/models/api_schema/knowledgebase/_revision.py +58 -0
  57. glpi_python_client-0.6.0/glpi_python_client/models/api_schema/management/__init__.py +35 -0
  58. glpi_python_client-0.6.0/glpi_python_client/models/api_schema/management/_contract.py +328 -0
  59. glpi_python_client-0.6.0/glpi_python_client/models/api_schema/management/_contract_cost.py +139 -0
  60. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/custom_schema/_ticket_context.py +29 -6
  61. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/rsql.py +74 -14
  62. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/pyproject.toml +15 -12
  63. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/README.md +3 -1
  64. glpi_python_client-0.6.0/skills/glpi-asset-workflow/SKILL.md +89 -0
  65. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/glpi-client-setup/SKILL.md +14 -5
  66. glpi_python_client-0.6.0/skills/glpi-contract-workflow/SKILL.md +123 -0
  67. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/glpi-document-workflow/SKILL.md +2 -2
  68. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/glpi-knowledge-base/SKILL.md +4 -2
  69. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/glpi-plugin-fields/SKILL.md +3 -3
  70. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/glpi-reporting-and-context/SKILL.md +3 -3
  71. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/glpi-team-members/SKILL.md +2 -2
  72. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/glpi-ticket-timeline/SKILL.md +5 -3
  73. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/glpi-ticket-workflow/SKILL.md +6 -3
  74. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/skills/glpi-user-location-provisioning/SKILL.md +2 -2
  75. glpi_python_client-0.4.3/glpi_python_client/content/conversion.py +0 -130
  76. glpi_python_client-0.4.3/glpi_python_client/models/api_schema/_content.py +0 -83
  77. glpi_python_client-0.4.3/glpi_python_client/models/api_schema/knowledgebase/_revision.py +0 -35
  78. glpi_python_client-0.4.3/glpi_python_client/models/api_schema/management/__init__.py +0 -15
  79. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/.pre-commit-config.yaml +0 -0
  80. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/.readthedocs.yaml +0 -0
  81. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/LICENSE +0 -0
  82. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/_static/.gitkeep +0 -0
  83. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/development_rtd.rst +0 -0
  84. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/index.rst +0 -0
  85. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/publishing.md +0 -0
  86. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/docs/sponsoring.rst +0 -0
  87. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/__init__.py +0 -0
  88. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/_concurrency.py +0 -0
  89. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/_testing.py +0 -0
  90. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/auth/__init__.py +0 -0
  91. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/__init__.py +0 -0
  92. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/administration/__init__.py +0 -0
  93. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/administration/_entity.py +0 -0
  94. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/administration/_user.py +0 -0
  95. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/assistance/__init__.py +0 -0
  96. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/assistance/_team.py +0 -0
  97. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/assistance/_ticket.py +0 -0
  98. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/assistance/timeline/__init__.py +0 -0
  99. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/assistance/timeline/_document.py +0 -0
  100. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/assistance/timeline/_followup.py +0 -0
  101. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/assistance/timeline/_solution.py +0 -0
  102. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/assistance/timeline/_task.py +0 -0
  103. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/dropdowns/_location.py +0 -0
  104. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/knowledgebase/__init__.py +0 -0
  105. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/knowledgebase/_article.py +0 -0
  106. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/knowledgebase/_category.py +0 -0
  107. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/knowledgebase/_comment.py +0 -0
  108. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/knowledgebase/_revision.py +0 -0
  109. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/management/_document.py +0 -0
  110. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/plugins/__init__.py +0 -0
  111. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/api/plugins/_fields.py +0 -0
  112. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/commons/__init__.py +0 -0
  113. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/commons/_config.py +0 -0
  114. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/commons/_filters.py +0 -0
  115. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/commons/_http.py +0 -0
  116. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/commons/_transport.py +0 -0
  117. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/custom/__init__.py +0 -0
  118. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/custom/_statistics.py +0 -0
  119. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_async/clients/custom/_ticket_context.py +0 -0
  120. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/__init__.py +0 -0
  121. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/_concurrency.py +0 -0
  122. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/_testing.py +0 -0
  123. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/auth/__init__.py +0 -0
  124. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/__init__.py +0 -0
  125. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/administration/__init__.py +0 -0
  126. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/administration/_entity.py +0 -0
  127. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/administration/_user.py +0 -0
  128. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/assistance/__init__.py +0 -0
  129. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/assistance/_team.py +0 -0
  130. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/assistance/_ticket.py +0 -0
  131. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/assistance/timeline/__init__.py +0 -0
  132. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/assistance/timeline/_document.py +0 -0
  133. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/assistance/timeline/_followup.py +0 -0
  134. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/assistance/timeline/_solution.py +0 -0
  135. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/assistance/timeline/_task.py +0 -0
  136. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/dropdowns/_location.py +0 -0
  137. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/knowledgebase/__init__.py +0 -0
  138. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/knowledgebase/_article.py +0 -0
  139. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/knowledgebase/_category.py +0 -0
  140. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/knowledgebase/_comment.py +0 -0
  141. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/knowledgebase/_revision.py +0 -0
  142. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/management/_document.py +0 -0
  143. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/plugins/__init__.py +0 -0
  144. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/api/plugins/_fields.py +0 -0
  145. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/commons/__init__.py +0 -0
  146. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/commons/_config.py +0 -0
  147. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/commons/_filters.py +0 -0
  148. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/commons/_http.py +0 -0
  149. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/commons/_transport.py +0 -0
  150. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/custom/__init__.py +0 -0
  151. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/custom/_statistics.py +0 -0
  152. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/_sync/clients/custom/_ticket_context.py +0 -0
  153. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/content/__init__.py +0 -0
  154. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/_common.py +0 -0
  155. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/administration/__init__.py +0 -0
  156. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/administration/_entity.py +0 -0
  157. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/administration/_user.py +0 -0
  158. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/assistance/__init__.py +0 -0
  159. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/assistance/_team.py +0 -0
  160. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/assistance/timeline/__init__.py +0 -0
  161. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/assistance/timeline/_document.py +0 -0
  162. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/dropdowns/_location.py +0 -0
  163. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/knowledgebase/__init__.py +0 -0
  164. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/knowledgebase/_category.py +0 -0
  165. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/knowledgebase/_comment.py +0 -0
  166. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/management/_document.py +0 -0
  167. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/plugins/__init__.py +0 -0
  168. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/api_schema/plugins/_fields.py +0 -0
  169. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/models/custom_schema/__init__.py +0 -0
  170. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/py.typed +0 -0
  171. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/testing/__init__.py +0 -0
  172. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/testing/fixtures.py +0 -0
  173. {glpi_python_client-0.4.3 → glpi_python_client-0.6.0}/glpi_python_client/testing/utils.py +0 -0
@@ -33,7 +33,12 @@ CLAUDE.md
33
33
  docs/superpowers/*
34
34
 
35
35
  # Large generated GLPI API contract (kept locally, not tracked)
36
- docs/glpi_api_contract.json
36
+ docs/api_contract/
37
37
  # Coverage data: rewritten by every test run (and by the venv .pth hook).
38
38
  .coverage
39
39
  .coverage.*
40
+
41
+ # One-shot live-instance probe scripts. They are standalone investigations
42
+ # (a main() run by hand against preprod), not collected tests -- the findings
43
+ # get written up in CHANGELOG.md, the scripts stay local.
44
+ integration_tests/probe_*.py
@@ -4,6 +4,450 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
6
6
 
7
+ ## 0.6.0 — 2026-10-01
8
+
9
+ ### Changed (breaking)
10
+
11
+ - **Python 3.10 is no longer supported; 3.11 is the minimum.** The
12
+ `typing-extensions` and `tomli` backports it needed are dropped.
13
+ - **Content conversion is rebuilt on three libraries: markdownify,
14
+ mdformat and cmark-gfm.** `from_transport` reads GLPI's HTML with
15
+ `markdownify`, and `mdformat` re-renders that Markdown from its syntax
16
+ tree, so it keeps only the escapes CommonMark needs. `to_transport`
17
+ renders through `cmark-gfm`, the GitHub reference implementation, in
18
+ place of python-markdown. A thin layer of glue sits on top. Measured on
19
+ 346 real bodies sampled from a GLPI 11 instance:
20
+ - 322 display the same after a round trip, against 299 before;
21
+ - 344 read back as the same Markdown, against 240.
22
+
23
+ Of 205 realistic caller-written Markdown documents, all 205 survive
24
+ Markdown → HTML → Markdown with the same display.
25
+ - **Markdown is rendered as CommonMark with GFM tables.** A newline is a
26
+ line break, as `nl2br` made it before. Raw HTML passes through.
27
+ Differences you may see in Markdown you write:
28
+ - a list or a table written straight after a line now starts a list or a
29
+ table, where python-markdown wanted a blank line first;
30
+ - lists nested by two or three spaces nest;
31
+ - `1)` starts a numbered list;
32
+ - `#Important`, with no space, is text rather than a heading;
33
+ - `*a **b** c*` keeps its bold;
34
+ - a backslash ending a line is a line break, so write `C:\Temp\` at the
35
+ end of a line as `` `C:\Temp\` ``;
36
+ - `<word>` is read as an HTML tag, so put a placeholder such as `<login>`
37
+ in backticks.
38
+ - **`.content` is spelled as canonical CommonMark.** A line break reads
39
+ back as `\` and a newline, a nested list is indented by its bullet's
40
+ width, and a table comes back unpadded. Text that would otherwise read as
41
+ syntax is escaped: `__init__` reads `\_\_init\_\_`, and a `* point` line
42
+ reads `\* point`. Stored digests of `.content` change once.
43
+ - **A plain-text body is literal text on the read path.** A value with no
44
+ HTML element used to come back verbatim and be rendered as Markdown. It is
45
+ now read as GLPI displays it, its lines as lines.
46
+ `GlpiContentConverter.from_transport` takes `plain_text_is_markdown`.
47
+ `True` is what the write models' validator passes: caller-authored
48
+ Markdown passes verbatim unless it starts with an HTML tag, so Markdown
49
+ carrying an inline `<br>` or `<kbd>` stays Markdown.
50
+ - **Dependencies.**
51
+ - Added: `cmarkgfm>=2025.10` (compiled wheels for CPython 3.11–3.14 on
52
+ Linux, macOS and Windows), `mdformat>=0.7.22,<0.8`,
53
+ `mdformat-tables>=1.0` and `markdown-it-py>=3.0`.
54
+ - Dropped: `markdown`. python-markdown 3.11 had broken the previous
55
+ reader.
56
+ - Raised: `beautifulsoup4>=4.15`, which fixed the parser defect that
57
+ dropped the text after a `<br />` in a body that also held a bare
58
+ `<br>`. That removes the workaround.
59
+
60
+ ### Fixed
61
+
62
+ - **Literal text came back as Markdown syntax.** These now read back as the
63
+ text a user typed:
64
+ - `\serveur\compta`, which had lost a backslash;
65
+ - `__init__` and `______`, which had become bold;
66
+ - a `-----` line under text, which had made a heading;
67
+ - `* point` and `> merci` lines, which had become a list and a quote;
68
+ - `[1]: https://...`, which had been consumed as a reference definition;
69
+ - a `|` in a table cell, which had dropped the rest of the row.
70
+ - **A table nested in a table cell lost all its text.** That is the usual
71
+ layout of an e-mail signature. The inner table is now written as its
72
+ cells' text, its line breaks kept as `<br>`.
73
+ - **Nested lists flattened on the first write.** Nested items, the text
74
+ after a nested list, and numbering, `start` included, survive.
75
+ - `<script>`, `<style>` and `<title>` bodies no longer leak into the text.
76
+ - A blank line inside a paragraph (`<br><br>`, or Outlook's
77
+ `<br>&nbsp;<br>`) is kept.
78
+ - A label in bold right before a figure, as in `<b>Total:</b>12`, keeps its
79
+ bold.
80
+ - A code fence keeps its language, and a `|` inside code in a table cell no
81
+ longer splits the row.
82
+ - A long body converts in linear time, and a body nested deeper than the
83
+ stack is read as its text instead of raising.
84
+
85
+ ### Known limitations
86
+
87
+ - A Markdown table needs a header row and the same number of cells in every
88
+ row. A header-less HTML table gains an empty header row, and a row that
89
+ spans the table gains empty cells.
90
+ - Struck-through text is kept as raw `<s>`, since CommonMark has no
91
+ strikethrough.
92
+ - Literal text that looks like syntax is sometimes escaped where
93
+ CommonMark would not need it, for example `5\*3` or `x \<= y`. It
94
+ displays as typed.
95
+
96
+ ## 0.5.0 — 2026-09-08
97
+
98
+ ### Fixed
99
+
100
+ - **Deeply nested HTML raised `RecursionError` while a model was being
101
+ validated.** `markdownify` walks the parsed document recursively and
102
+ spends about two CPython frames per nesting level, so roughly 494 levels
103
+ exhausted the default 1000-frame limit — measured, and the same 494 for
104
+ `<div>`, `<p>`, `<blockquote>` and `<table><tr><td>`, 495 for
105
+ `<ul><li>`, which is what identifies the cost as per-level. An
106
+ unclosed tag counts too: `html.parser` does not auto-close `<p>` or
107
+ `<li>`, so `"<p>" * 5000` really is 5000 levels.
108
+
109
+ Because the converter was wired as a Pydantic `BeforeValidator`, the
110
+ failure landed inside `model_validate` — that is, inside `get_ticket` —
111
+ as a bare builtin from a library whose whole error surface is supposed
112
+ to derive from `GlpiError`.
113
+
114
+ `from_transport` now *attempts* the conversion and answers the
115
+ `RecursionError` by stripping the document to its text instead.
116
+ **It degrades, it never truncates, and it does not raise for depth**:
117
+ every character of prose the converting path would have produced also
118
+ appears in the degraded rendering.
119
+
120
+ Attempting it rather than predicting it is the whole design, and it
121
+ replaced a fixed `MAX_HTML_DEPTH = 200` bound that was wrong in both
122
+ directions. Too low, because the budget is not 1000 frames but whatever
123
+ is left of the stack when the conversion starts, and that belongs to
124
+ the caller — so the bound had to assume the worst and flattened every
125
+ body between 200 and the real cliff of about 494. Measured, a
126
+ 300-level and a 400-level body now come back as **Markdown with their
127
+ links, emphasis and lists intact** where they used to come back as
128
+ plain text, with no error to notice and no way to ask for better. And
129
+ too fragile, because predicting the depth meant reproducing the
130
+ parser's idea of the tree: three rounds of adversarial review found
131
+ seven ways for that estimate to land *under* the real depth, each of
132
+ which sent a document to `markdownify` and into the very
133
+ `RecursionError` the bound existed to prevent.
134
+
135
+ Trying the conversion cannot be wrong about whether the conversion
136
+ fits. `MAX_HTML_DEPTH` and the scan behind it are gone; the constant
137
+ was introduced in this same unreleased cycle and never shipped.
138
+
139
+ Two consequences worth knowing. The outcome now depends on the caller's
140
+ remaining stack, so the same body can convert from one call site and
141
+ degrade from a deeper one — nothing is lost either way, but a caller
142
+ comparing two renderings of one body should know which knob moved it.
143
+ And a body too deep to convert now pays the failed attempt before it
144
+ degrades: measured, 2.0x to 2.6x the old cost at 600 and 5000 levels.
145
+ Ordinary bodies got *faster*, at 0.87x to 0.90x, because the scan they
146
+ used to pay for on every read is gone.
147
+
148
+ A document `html.parser` refuses outright — `<![FOO[`, an unknown
149
+ marked-section keyword, which `bs4` re-raises as
150
+ `ParserRejectedMarkup` — takes the same degraded path, where it used
151
+ to raise and give the caller none of their text.
152
+
153
+ Both halves were fuzzed against the real parser over 15000 documents,
154
+ with zero under-counts, zero over-counts and zero text losses. Getting
155
+ there took several rules that are not the obvious ones:
156
+
157
+ - A closing tag pops by name or is ignored — `bs4` pops nothing when no
158
+ element of that name is open, so `"<div></p>" * 600` really is 600
159
+ deep where a naive counter says 1.
160
+ - An attribute value may contain `<` and `>`, so
161
+ `'<div title="</div>">' * 600` also measured 0 against a real 600
162
+ until the scan learned to skip quoted values.
163
+ - A quote opens a value only as the first character after the `=`,
164
+ which is the parser's own rule, so `<p title=don't>` carries the
165
+ value `don't`. Reading that apostrophe as a quote printed the opening
166
+ tag verbatim at the reader — and an apostrophe needs no malice to
167
+ reach a French ticket body.
168
+ - `tagfind_tolerant` runs a tag *name* to whitespace, `/` or `>`, so
169
+ `<style=>` is an element named `style=` and never enters raw-text
170
+ mode; a self-closed `<script/>` does not either, because
171
+ `parse_starttag` enters it only on the branch that is not
172
+ self-closing. Reading either as raw text swallowed the rest of the
173
+ document: `"<style=>" + "<div>" * 600` measured 1 level against a
174
+ real 601 and raised.
175
+ - A declaration is text on neither path only when it is closed. A
176
+ `<!weird` left unterminated at end of input is flushed as character
177
+ data when the parser closes, so dropping it lost the tail of a body.
178
+ - An unclosed tag counts, a childless node still occupies a level, and
179
+ a bogus comment swallows the tags inside it.
180
+ - The degraded path had to be measured against the converting path
181
+ construct by construct rather than reasoned about. Three answers came
182
+ back the opposite way round: a `<script>`/`<style>` body is *kept*
183
+ (`markdownify`'s `strip=` removes an element's markup and still walks
184
+ its children), so is a `CDATA` body, and so is the inside of any
185
+ `<!`/`<?` construct the parser could not resolve.
186
+
187
+ What the degraded rendering does not reproduce, none of it prose: link
188
+ targets and image alt text, fenced-block and `<pre>` indentation,
189
+ `&nbsp;`-padded alignment, and a processing instruction's `<?`/`>`
190
+ delimiters, which survive as literal text.
191
+
192
+ Character references are resolved by the parser's rule rather than by
193
+ `html.unescape`, which implements HTML5's longest-known-*prefix* rule
194
+ and would rewrite a pasted URL: `?a=1&copyright=2` becomes
195
+ `?a=1©right=2` under `unescape` and is left alone by the parser. A
196
+ semicolon-less reference resolves only when its whole name is known.
197
+
198
+ The ceiling is 200 rather than 494 because the budget is not 1000
199
+ frames, it is whatever is left of the stack when conversion starts, and
200
+ that belongs to the caller. The package's own contribution is small,
201
+ and since conversion moved to the attribute it no longer depends on how
202
+ the record was fetched: measured, 5 frames below the caller when
203
+ `.content` is read — the same 5 whether the model came from
204
+ `model_validate` or from `client.get_ticket` — and 9 on the write path,
205
+ where the renderer runs inside `model_dump`. What is not small is an
206
+ application reading `.content` from inside a request handler or a
207
+ recursive walk. Converting a 200-level document peaks at a measured 412
208
+ frames, so it stays safe until the caller's own stack passes about
209
+ 588 — and no document a human wrote nests 200 elements deep.
210
+
211
+ **`sys.setrecursionlimit` was considered and rejected.** It is
212
+ process-global state belonging to the application, not to a library the
213
+ application imported; and past what the C stack can hold it converts a
214
+ catchable `RecursionError` into a hard interpreter crash — on Windows,
215
+ an access violation with no traceback. It moves the cliff and makes
216
+ falling off it worse. The prohibition is asserted by
217
+ `testing/tests/test_raise_site_audit.py` rather than left as a comment
218
+ for the next person to weigh up again.
219
+
220
+ - **Where a tag *ends* was read with start-tag rules, twice.** Both were
221
+ unbounded depth under-counts, which is the one direction the ceiling
222
+ exists to prevent, and both also deleted prose from the degraded path at
223
+ any depth.
224
+
225
+ `parse_endtag` falls back to `rawdata.find(">")`, so an end tag skips
226
+ nothing — CPython's own comment concedes the case: "this is not 100%
227
+ correct, since we might have things like `</tag attr=">">`". Reading one
228
+ with attribute rules made `'</x a="><div>">' * 600` measure **0**
229
+ against a real 600.
230
+
231
+ `locatestarttagend_tolerant` reaches a quoted value only through an
232
+ attribute *name*, and a name may itself begin with `=`. So in
233
+ `<div ="<p><p>">` the parser reads the name `="<p` and ends the tag at
234
+ the first `>`, where treating any `=` before a quote as a value
235
+ indicator swallowed the rest: `'<div ="' + "<p>" * 600` measured **1**
236
+ against a real 600.
237
+
238
+ The attribute pattern is now a sequence of attributes rather than a run
239
+ of permitted characters, and end tags have their own branch. The
240
+ attribute name carries the parser's own "starts after a quote,
241
+ whitespace or `/`" rule, which is load-bearing twice over: without it
242
+ `<div ="` reads as a value, *and* the pattern backtracks
243
+ catastrophically — a 400-byte `'<div a="' * 50` did not finish.
244
+
245
+ - **A body of nothing but `<div a="` cost O(n²).** 32 KB took 6.6 s. `re`
246
+ restarts at every `<` where `html.parser` buffers an incomplete tag and
247
+ never looks back. No `>` anywhere means no element anywhere, so that is
248
+ now answered in constant time. The pattern's remaining non-linear
249
+ shapes turned out to be exponential rather than quadratic, and are gone
250
+ with the pattern itself — see the entry below.
251
+
252
+ - **The markup scan imitated `html.parser` instead of using it, and was
253
+ wrong in five unbounded ways at once.** A third adversarial round found
254
+ that the pattern reproducing the parser's dispatch disagreed with the
255
+ parser on: a comment closing on `--\s*>` rather than only `-->`; `</
256
+ script>` ending raw text; `<![IGNORE[` opening a marked section; `</ div
257
+ foo>` being a bogus comment rather than an end tag; and `<a href=/>` —
258
+ an ordinary root-relative link — leaving the element *open*, because the
259
+ unquoted value swallows the `/`. Each made a document measure one level
260
+ deep where the real tree was hundreds, so `'<a href=/>' * 494` cleared
261
+ the ceiling and raised. Two further findings were cost: a run of
262
+ whitespace inside a failing tag made the attribute pattern backtrack as
263
+ `(a+)*`, and a 39-byte body took 20.8 s.
264
+
265
+ The pattern is gone. Depth, void-tag canonicalisation and the degraded
266
+ rendering now come from one pass of an `html.parser` subclass — the same
267
+ parser `bs4` uses, so this cannot be wrong about the parser and is not a
268
+ new dependency or a new risk. Every pathology `html.parser` has was
269
+ already in the pipeline: measured on the shapes that made the pattern
270
+ backtrack, the `markdownify` call costs what the scan costs, to within a
271
+ few per cent.
272
+
273
+ Three consequences beyond the five defects:
274
+
275
+ - **A derailed scan silently reinstated the `<br>` data loss fixed
276
+ below**, because the void-tag workaround read the same pattern.
277
+ Measured, `"<p>one<br>two</p><script>x</ script><p>three<br
278
+ />TAIL</p>"` lost `TAIL` outright, and `<img>` and `<hr>` lost their
279
+ tails the same way.
280
+ - **A document the parser rejects now degrades instead of raising.**
281
+ `<![FOO[` makes `_markupbase` raise `AssertionError` on the
282
+ interpreters where that keyword is unknown — 3.10 through 3.12.11 as
283
+ measured, no longer 3.12.14 — and `bs4` re-raises it as
284
+ `ParserRejectedMarkup`. Where it happens, the caller used to get a
285
+ `GlpiContentError` and none of their text, and now gets their words.
286
+
287
+ Note that `html.parser`'s reading of a *malformed* construct is not
288
+ stable across CPython patch releases: the same three builds disagree
289
+ about an unterminated `<script>`, a comment with no `-->` and an end
290
+ tag carrying a quoted `>`. The scan tracks the parser rather than a
291
+ snapshot of it, so the depth decision stays correct on every version,
292
+ but the exact text a broken construct contributes to a degraded body is
293
+ the interpreter's. Well-formed content is unaffected.
294
+ - **A processing instruction no longer leaves `<?` and `>` in the
295
+ degraded text.** The converting path prints the body alone, so this
296
+ does too.
297
+
298
+ Cost, end to end and on identical output: 0.77x to 1.52x of the previous
299
+ implementation on realistic bodies. The exponential shapes are flat: 39
300
+ bytes of the whitespace bomb went from 20.8 s to 0.12 ms, and 20 KB of
301
+ it costs 0.67 ms. The depth scan those defects were found in has since
302
+ been removed altogether — see the entry above — but the same parser now
303
+ backs the void-tag rewrite and the degraded renderer, which inherited
304
+ every one of the misreadings and the backtracking too.
305
+
306
+ The one shape where `html.parser` is worse than linear is a document
307
+ carrying no `>` at all, where `close()` advances a character at a time
308
+ and rescans the tail: 32 KB costs it 13 s. That is answered in constant
309
+ time by the guard already present for the pattern's own O(n²) on the
310
+ same input, and is unreachable from `from_transport`, which needs a `>`
311
+ to find an element at all.
312
+
313
+ Re-fuzzed against a ground-truth walk of the tree `bs4` really builds,
314
+ over an alphabet carrying every construct all three rounds raised —
315
+ including the four whose absence is why the previous 10.5M-document
316
+ corpus could not have found these: `-- >`, `</ script>`, `<![IGNORE[`
317
+ and runs of whitespace and quotes inside a tag. **470000 documents, 0
318
+ depth under-counts, 0 prose losses, 0 crashes**, plus 60000 hostile
319
+ documents through `from_transport` with nothing but `GlpiError`
320
+ escaping.
321
+
322
+ - **`GlpiContentError` did not survive the write path.** Outbound
323
+ conversion runs in a `PlainSerializer`, and pydantic-core catches
324
+ everything a serializer raises and re-raises `PydanticSerializationError`
325
+ — a `ValueError`, not a `GlpiError`, with `__cause__` and `__context__`
326
+ both `None`. So on every `create_*`/`update_*` carrying a body,
327
+ `except GlpiError` did not fire and the underlying fault was
328
+ unrecoverable. The fault is now stashed as it is raised and restored
329
+ around `model_dump`, with its own `__cause__` intact; a serialisation
330
+ failure that is *not* content becomes `GlpiValidationError` rather than
331
+ being mislabelled.
332
+
333
+ - **One field spelled two ways shadowed itself in `model_dump`.** Pydantic
334
+ consumes the first alias and `extra="allow"` files the rest as model
335
+ extras — and an extra named after a field is emitted *instead of* that
336
+ field, so the attribute reported one body and the object's own dump
337
+ reported the other. The redundant spelling is now dropped before
338
+ Pydantic resolves anything, and `content_html` is the first choice, so a
339
+ dump carrying both round-trips back to the raw body.
340
+
341
+ Worth knowing about the read models: `model_copy(update={"content": ...})`
342
+ — the 0.4.x spelling — updates **nothing**, because `content` is now a
343
+ `cached_property`. A caller redacting a body that way gets an object
344
+ whose `.content` still holds the original. Rewrite `content_html`, or
345
+ rebuild through `model_validate`.
346
+
347
+ - **A body that used both spellings of `<br>` lost everything after the
348
+ second one.** `<p>line1<br>line2</p><p>para2<br />line4</p>` converted
349
+ to `line1 \nline2\n\npara2` — `line4` silently gone, no error, on the
350
+ ordinary conversion path.
351
+
352
+ The cause is in `beautifulsoup4` (measured on 4.14.3), not in
353
+ `markdownify`. Its `html.parser` builder auto-closes a bare `<br>` and
354
+ records the name in `already_closed_empty_element` so a later `</br>`
355
+ can be ignored as redundant; when no `</br>` arrives the entry just
356
+ stays. The next `<br />` reaches the builder as `handle_startendtag`,
357
+ opens a real element and closes it itself — and that close finds the
358
+ stale entry, treats the element as already closed, and leaves it open,
359
+ so every following sibling becomes a child of the `<br>`.
360
+ `markdownify`'s `convert_br` ignores an element's children, and the
361
+ text is gone. `get_text` walks children, which is why the tree looks
362
+ intact.
363
+
364
+ Note the paragraph in the example: the two spellings need not be near
365
+ each other, since a name once recorded poisons the rest of the
366
+ document. `<img>` and `<hr>` are the other two converters that discard
367
+ children and lost text the same way.
368
+
369
+ `from_transport` now writes self-closing void tags bare before
370
+ converting, which removes the `handle_startendtag` path where the
371
+ asymmetry lives. Both spellings already built the same node, so nothing
372
+ else moves: measured over 4000 fuzzed documents of each spelling alone,
373
+ not one output changed, and over 4000 mixing them, 102 recovered text
374
+ and none lost any. Only names in the void set are touched, and only in
375
+ real tag position — a `<div/>`, a `<br />` inside an attribute value, a
376
+ comment or a `<script>` body are all left alone.
377
+
378
+ - **`GlpiModel` now recognises validation aliases when it captures unknown
379
+ keys.** `_capture_unknown_fields` runs before Pydantic resolves aliases
380
+ and compared incoming keys against field *names* only, so an aliased key
381
+ was diverted into `extra_payload` before its field could see it — HTTP
382
+ 200, no warning, and the value silently `None`. Latent until this
383
+ release, which introduces the package's first alias.
384
+
385
+ ### Added
386
+
387
+ - **`GlpiContentError`** — a new `GlpiError` leaf for a rich-text body
388
+ that could not be converted, in either direction, with the underlying
389
+ fault attached as `__cause__`. Exported from the package root and
390
+ documented in the API reference.
391
+
392
+ Content conversion previously sat outside the taxonomy altogether: a
393
+ parser fault escaped `except GlpiError` and reached the caller as a bare
394
+ builtin. The depth ceiling above means no ordinary input gets here, so
395
+ this is the backstop — including for the outbound direction, where
396
+ `markdown` has its own cliff at around 500 levels of list indentation.
397
+
398
+ Unlike `GlpiStatusError`, `GlpiValidationError` and `GlpiProtocolError`
399
+ it does **not** inherit `ValueError`. Those three carry it for
400
+ compatibility with releases that raised bare `ValueError` at the same
401
+ sites; there was never a `ValueError` at a conversion site, and a parser
402
+ exhausting the stack is not a value the caller got wrong. Same reasoning
403
+ as `GlpiTransportError`.
404
+
405
+ - **`content_html` on the read models**, holding the wire value verbatim:
406
+ `GetTicket`, `GetFollowup`, `GetTicketTask`, `GetSolution`,
407
+ `GetKBArticleRevision`, and `GetKBArticle` (which also gains
408
+ `description_html`).
409
+
410
+ ### Changed (breaking)
411
+
412
+ - **Read models convert to Markdown on first access instead of during
413
+ validation.** `content` is now a `functools.cached_property` over
414
+ `content_html`:
415
+
416
+ ```python
417
+ ticket = client.get_ticket(42)
418
+ ticket.content_html # '<p>Printer is <strong>offline</strong></p>'
419
+ ticket.content # 'Printer is **offline**' (converted here, once)
420
+ ```
421
+
422
+ **Callers that read `.content` need no change.** The field carries the
423
+ validation alias `content`, so a GLPI payload and a hand-written
424
+ `GetTicket(content=...)` both still populate it, and `.content` still
425
+ returns Markdown. What changes is *when*.
426
+
427
+ Two things follow. A caller who wants only `id` and `date_mod` no longer
428
+ pays HTML-to-Markdown on every record of every page. And a body that
429
+ cannot be converted no longer takes its page-mates with it:
430
+ `TransportMixin._resource_list` builds every item of a page in one
431
+ comprehension, so one unconvertible record used to make the whole page
432
+ unreadable — the failure is now scoped to the record whose body is
433
+ actually read.
434
+
435
+ Write models (`Post*`, `Patch*`) are deliberately unchanged: they keep
436
+ the plain `content` field and convert eagerly, so a caller's own
437
+ Markdown is still checked where it was supplied, and there is no list
438
+ path on a write model to make lazy.
439
+
440
+ What does break: `content` is no longer in `GetTicket.model_fields`, and
441
+ `GetTicket(...).model_dump()` emits `content_html` holding HTML where it
442
+ used to emit `content` holding Markdown (`by_alias=True` gives a dump
443
+ keyed the way GLPI keys it).
444
+
445
+ One sharp edge comes with the cache. Assigning to `content_html` after
446
+ `.content` has been read leaves the stale Markdown in place, and so does
447
+ `model_copy(update={"content_html": ...})` — and neither equality,
448
+ `repr` nor any `model_dump` reveals it. Treat a read model as immutable
449
+ once validated, or rebuild it through `model_validate`.
450
+
7
451
  ## 0.4.3 — 2026-08-13
8
452
 
9
453
  ### Changed (breaking)
@@ -34,6 +478,31 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
34
478
  database; without it `zoneinfo` resolves on Linux CI and raises on a
35
479
  developer machine.
36
480
 
481
+ - **`changed_since` no longer assumes UTC.** An aware `datetime` now needs the
482
+ server's timezone and raises `GlpiValidationError` without it:
483
+
484
+ ```python
485
+ window = changed_since(last_run, tz=client.server_timezone)
486
+ ```
487
+
488
+ The bound this builds is compared against a naive server-local column, so
489
+ converting the caller's moment to UTC asked the server for a different one.
490
+ A 09:33 Paris timestamp became a `07:33` filter. East of UTC that only
491
+ re-reads a few hours on every sweep; west of it the bound moves *forward*
492
+ and modifications are skipped outright — four hours in New York, seven in
493
+ Los Angeles — and the size of the drift changes at each DST transition, so
494
+ a sync that looks correct in January starts losing rows in March.
495
+
496
+ The offset is now spent converting the value onto the server's clock and
497
+ then dropped, which is what the model serialiser already does on the way
498
+ out; the two halves had diverged by exactly the offset. Missing `tz` is
499
+ refused rather than defaulted for the same reason `server_timezone` has no
500
+ default: every guess is wrong somewhere, and being wrong here returns a
501
+ short result set rather than an error.
502
+
503
+ A `date`, an ISO string, or a naive `datetime` is unaffected and needs no
504
+ `tz` — a naive value already means the server's clock.
505
+
37
506
  - **Search endpoints now raise on a 4xx instead of returning `[]`.** The seven
38
507
  `search_*` helpers passed no `failure_message` to `_resource_list`, which
39
508
  skipped the status check entirely, so a 400, 401, 403 or 404 came back as an
@@ -38,7 +38,7 @@ python -m sphinx -W --keep-going -b html docs docs/_build/html
38
38
 
39
39
  ## GitHub Actions
40
40
 
41
- - `.github/workflows/ci.yml` runs tests for Python 3.10 through 3.14 on pull
41
+ - `.github/workflows/ci.yml` runs tests for Python 3.11 through 3.14 on pull
42
42
  requests and pushes to `main`.
43
43
  - The same workflow runs `ruff`, `mypy`, and a warning-free Sphinx build on
44
44
  Python 3.12.
@@ -63,3 +63,19 @@ python -m sphinx -W --keep-going -b html docs docs/_build/html
63
63
  library core.
64
64
  - Add tests for payload serialization and response normalization when adding
65
65
  endpoints.
66
+
67
+ ### The GLPI OpenAPI contract
68
+
69
+ The models under `glpi_python_client/models/api_schema/` mirror
70
+ `components.schemas.*` from the GLPI High-Level REST API document,
71
+ including which fields are `readOnly`. That document is not committed —
72
+ it is ~14 MB and instance-specific. Fetch your instance's copy to the
73
+ path the models cite:
74
+
75
+ ```bash
76
+ curl -sk "<your GLPI base>/api.php/v2.3/doc.json" -o docs/api_contract/api.json
77
+ ```
78
+
79
+ `docs/api_contract/` is gitignored. Nothing in CI checks the models
80
+ against it, so when you add or change a model, diff it against the
81
+ contract by hand.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: glpi-python-client
3
- Version: 0.4.3
3
+ Version: 0.6.0
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/
@@ -15,7 +15,6 @@ Classifier: Intended Audience :: Developers
15
15
  Classifier: License :: OSI Approved :: MIT License
16
16
  Classifier: Operating System :: OS Independent
17
17
  Classifier: Programming Language :: Python :: 3
18
- Classifier: Programming Language :: Python :: 3.10
19
18
  Classifier: Programming Language :: Python :: 3.11
20
19
  Classifier: Programming Language :: Python :: 3.12
21
20
  Classifier: Programming Language :: Python :: 3.13
@@ -23,16 +22,18 @@ Classifier: Programming Language :: Python :: 3.14
23
22
  Classifier: Topic :: Internet :: WWW/HTTP
24
23
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
25
24
  Classifier: Typing :: Typed
26
- Requires-Python: >=3.10
27
- Requires-Dist: beautifulsoup4>=4.12
25
+ Requires-Python: >=3.11
26
+ Requires-Dist: beautifulsoup4>=4.15
27
+ Requires-Dist: cmarkgfm>=2025.10
28
28
  Requires-Dist: httpx>=0.28
29
29
  Requires-Dist: lxml>=4.9
30
- Requires-Dist: markdown>=3.6
31
- Requires-Dist: markdownify>=0.13
30
+ Requires-Dist: markdown-it-py>=3.0
31
+ Requires-Dist: markdownify>=1.2
32
+ Requires-Dist: mdformat-tables>=1.0
33
+ Requires-Dist: mdformat<0.8,>=0.7.22
32
34
  Requires-Dist: pydantic>=2.8
33
35
  Requires-Dist: sniffio>=1.3
34
36
  Requires-Dist: tenacity>=8.2
35
- Requires-Dist: typing-extensions>=4.7; python_version < '3.11'
36
37
  Requires-Dist: tzdata>=2024.1; platform_system == 'Windows'
37
38
  Provides-Extra: dev
38
39
  Requires-Dist: build>=1.2; extra == 'dev'
@@ -45,7 +46,6 @@ Requires-Dist: pytest>=8.0; extra == 'dev'
45
46
  Requires-Dist: ruff>=0.6; extra == 'dev'
46
47
  Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'dev'
47
48
  Requires-Dist: sphinx<8.2,>=7.2; extra == 'dev'
48
- Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'dev'
49
49
  Requires-Dist: twine>=5.1; extra == 'dev'
50
50
  Requires-Dist: unasync>=0.6; extra == 'dev'
51
51
  Requires-Dist: vulture>=2.11; extra == 'dev'
@@ -53,7 +53,6 @@ Provides-Extra: docs
53
53
  Requires-Dist: numpydoc>=1.8; extra == 'docs'
54
54
  Requires-Dist: sphinx-rtd-theme>=2.0; extra == 'docs'
55
55
  Requires-Dist: sphinx<8.2,>=7.2; extra == 'docs'
56
- Requires-Dist: tomli>=2.0; (python_version < '3.11') and extra == 'docs'
57
56
  Description-Content-Type: text/markdown
58
57
 
59
58
  # glpi-python-client
@@ -61,7 +60,7 @@ Description-Content-Type: text/markdown
61
60
  [![CI](https://github.com/baraline/glpi_python_client/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/baraline/glpi_python_client/actions/workflows/ci.yml)
62
61
  [![Coverage](https://codecov.io/gh/baraline/glpi_python_client/branch/main/graph/badge.svg)](https://codecov.io/gh/baraline/glpi_python_client)
63
62
  [![License](https://img.shields.io/github/license/baraline/glpi_python_client)](LICENSE)
64
- [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://github.com/baraline/glpi_python_client)
63
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://github.com/baraline/glpi_python_client)
65
64
  [![Docs](https://readthedocs.org/projects/glpi-python-client/badge/?version=latest)](https://glpi-python-client.readthedocs.io/en/latest/)
66
65
 
67
66
  `glpi-python-client` is a typed Python client for the GLPI REST API.
@@ -70,7 +69,11 @@ The goal is to let GLPI integrations work with domain objects instead of raw
70
69
  JSON payloads. The package exposes Pydantic models for tickets, users,
71
70
  followups, documents, locations, and related records, while converting GLPI
72
71
  HTML content into Markdown for Python-side workflows and rendering Markdown
73
- back to HTML for outgoing payloads.
72
+ back to HTML for outgoing payloads. On a response model that conversion is
73
+ lazy — `.content` converts on first read and caches, so listing records
74
+ costs nothing per body — and it degrades to plain text rather than failing
75
+ on pathologically nested HTML. See
76
+ [Rich-text content](https://glpi-python-client.readthedocs.io/en/latest/user_guide.html#content-conversion).
74
77
 
75
78
  It currently focuses on ticket-centric workflows and exposes two high-level
76
79
  clients built on top of the GLPI v2 REST API:
@@ -3,7 +3,7 @@
3
3
  [![CI](https://github.com/baraline/glpi_python_client/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/baraline/glpi_python_client/actions/workflows/ci.yml)
4
4
  [![Coverage](https://codecov.io/gh/baraline/glpi_python_client/branch/main/graph/badge.svg)](https://codecov.io/gh/baraline/glpi_python_client)
5
5
  [![License](https://img.shields.io/github/license/baraline/glpi_python_client)](LICENSE)
6
- [![Python](https://img.shields.io/badge/python-3.10%2B-blue)](https://github.com/baraline/glpi_python_client)
6
+ [![Python](https://img.shields.io/badge/python-3.11%2B-blue)](https://github.com/baraline/glpi_python_client)
7
7
  [![Docs](https://readthedocs.org/projects/glpi-python-client/badge/?version=latest)](https://glpi-python-client.readthedocs.io/en/latest/)
8
8
 
9
9
  `glpi-python-client` is a typed Python client for the GLPI REST API.
@@ -12,7 +12,11 @@ The goal is to let GLPI integrations work with domain objects instead of raw
12
12
  JSON payloads. The package exposes Pydantic models for tickets, users,
13
13
  followups, documents, locations, and related records, while converting GLPI
14
14
  HTML content into Markdown for Python-side workflows and rendering Markdown
15
- back to HTML for outgoing payloads.
15
+ back to HTML for outgoing payloads. On a response model that conversion is
16
+ lazy — `.content` converts on first read and caches, so listing records
17
+ costs nothing per body — and it degrades to plain text rather than failing
18
+ on pathologically nested HTML. See
19
+ [Rich-text content](https://glpi-python-client.readthedocs.io/en/latest/user_guide.html#content-conversion).
16
20
 
17
21
  It currently focuses on ticket-centric workflows and exposes two high-level
18
22
  clients built on top of the GLPI v2 REST API: