glpi-python-client 0.4.1__tar.gz → 0.4.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/CONTRIBUTING.md +1 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/PKG-INFO +2 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/publishing_rtd.rst +1 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/__init__.py +1 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/pyproject.toml +2 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/skills/README.md +4 -2
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/skills/glpi-client-setup/SKILL.md +57 -10
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/skills/glpi-document-workflow/SKILL.md +4 -3
- glpi_python_client-0.4.2/skills/glpi-knowledge-base/SKILL.md +168 -0
- glpi_python_client-0.4.2/skills/glpi-plugin-fields/SKILL.md +199 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/skills/glpi-reporting-and-context/SKILL.md +13 -11
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/skills/glpi-team-members/SKILL.md +4 -3
- glpi_python_client-0.4.2/skills/glpi-ticket-timeline/SKILL.md +124 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/skills/glpi-ticket-workflow/SKILL.md +7 -6
- glpi_python_client-0.4.2/skills/glpi-user-location-provisioning/SKILL.md +106 -0
- glpi_python_client-0.4.1/skills/glpi-ticket-timeline/SKILL.md +0 -72
- glpi_python_client-0.4.1/skills/glpi-user-location-provisioning/SKILL.md +0 -77
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/.gitignore +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/.pre-commit-config.yaml +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/.readthedocs.yaml +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/CHANGELOG.md +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/LICENSE +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/README.md +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/_static/.gitkeep +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/api_reference.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/conf.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/development.md +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/development_rtd.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/index.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/installation.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/publishing.md +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/sponsoring.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/docs/user_guide.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/_concurrency.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/_testing.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/auth/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/auth/_v1_session.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/auth/auth.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/_base_client.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/administration/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/administration/_entity.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/administration/_user.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/assistance/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/assistance/_team.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/assistance/_ticket.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/dropdowns/_location.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/knowledgebase/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/knowledgebase/_article.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/knowledgebase/_category.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/knowledgebase/_comment.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/knowledgebase/_revision.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/management/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/management/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/plugins/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/api/plugins/_fields.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/client.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/commons/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/commons/_config.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/commons/_constants.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/commons/_filters.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/commons/_http.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/commons/_payloads.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/commons/_transport.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/custom/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/custom/_statistics.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_async/clients/custom/_ticket_context.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_errors.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/_concurrency.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/_testing.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/auth/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/auth/_v1_session.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/auth/auth.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/_base_client.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/administration/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/administration/_entity.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/administration/_user.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/assistance/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/assistance/_team.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/assistance/_ticket.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/dropdowns/_location.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/knowledgebase/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/knowledgebase/_article.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/knowledgebase/_category.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/knowledgebase/_comment.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/knowledgebase/_revision.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/management/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/management/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/plugins/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/api/plugins/_fields.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/client.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/commons/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/commons/_config.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/commons/_constants.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/commons/_filters.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/commons/_http.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/commons/_payloads.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/commons/_transport.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/custom/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/custom/_statistics.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/_sync/clients/custom/_ticket_context.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/content/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/content/conversion.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/_base.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/_common.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/_content.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/administration/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/administration/_entity.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/administration/_user.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/assistance/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/assistance/_team.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/assistance/_ticket.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/dropdowns/_location.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/enums.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/knowledgebase/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/knowledgebase/_article.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/knowledgebase/_category.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/knowledgebase/_comment.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/knowledgebase/_revision.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/management/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/management/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/plugins/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/api_schema/plugins/_fields.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/custom_schema/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/models/custom_schema/_ticket_context.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/py.typed +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/testing/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/testing/fixtures.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/glpi_python_client/testing/utils.py +0 -0
|
@@ -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.
|
|
41
|
+
- `.github/workflows/ci.yml` runs tests for Python 3.10 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.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: glpi-python-client
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.2
|
|
4
4
|
Summary: A typed Python client for GLPI ITSM APIs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/baraline/glpi_python_client
|
|
6
6
|
Project-URL: Documentation, https://glpi-python-client.readthedocs.io/en/latest/
|
|
@@ -19,6 +19,7 @@ Classifier: Programming Language :: Python :: 3.10
|
|
|
19
19
|
Classifier: Programming Language :: Python :: 3.11
|
|
20
20
|
Classifier: Programming Language :: Python :: 3.12
|
|
21
21
|
Classifier: Programming Language :: Python :: 3.13
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
23
|
Classifier: Topic :: Internet :: WWW/HTTP
|
|
23
24
|
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
24
25
|
Classifier: Typing :: Typed
|
|
@@ -92,7 +92,7 @@ GitHub Actions and Read the Docs
|
|
|
92
92
|
The repository ships with two GitHub Actions workflows:
|
|
93
93
|
|
|
94
94
|
* ``.github/workflows/ci.yml`` runs on pull requests and pushes to ``main``.
|
|
95
|
-
It executes ``pytest`` on Python 3.10 through 3.
|
|
95
|
+
It executes ``pytest`` on Python 3.10 through 3.14, then runs ``ruff``,
|
|
96
96
|
``mypy``, and the Sphinx build on Python 3.12.
|
|
97
97
|
* ``.github/workflows/release.yml`` runs on published GitHub releases. It
|
|
98
98
|
repeats the quality checks, builds the source and wheel distributions,
|
|
@@ -35,7 +35,7 @@ exclude = [
|
|
|
35
35
|
|
|
36
36
|
[project]
|
|
37
37
|
name = "glpi-python-client"
|
|
38
|
-
version = "0.4.
|
|
38
|
+
version = "0.4.2"
|
|
39
39
|
description = "A typed Python client for GLPI ITSM APIs."
|
|
40
40
|
readme = "README.md"
|
|
41
41
|
requires-python = ">=3.10"
|
|
@@ -52,6 +52,7 @@ classifiers = [
|
|
|
52
52
|
"Programming Language :: Python :: 3.11",
|
|
53
53
|
"Programming Language :: Python :: 3.12",
|
|
54
54
|
"Programming Language :: Python :: 3.13",
|
|
55
|
+
"Programming Language :: Python :: 3.14",
|
|
55
56
|
"Topic :: Internet :: WWW/HTTP",
|
|
56
57
|
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
57
58
|
"Typing :: Typed",
|
|
@@ -10,11 +10,13 @@ These skills are source-tree project material. They are included in source distr
|
|
|
10
10
|
| --- | --- | --- |
|
|
11
11
|
| `glpi-client-setup` | Build and configure an authenticated client | `GlpiClient`, `AsyncGlpiClient`, `.from_env()` |
|
|
12
12
|
| `glpi-ticket-workflow` | Search, fetch, create, update, or delete tickets | `GetTicket`, `PostTicket`, `PatchTicket`, `DeleteTicket` |
|
|
13
|
-
| `glpi-ticket-timeline` | Read timeline records or write followups, tasks, solutions, and document links | `PostFollowup`, `PostTicketTask`, `PostSolution`, `PostTimelineDocument` (plus matching Get/Patch/Delete) |
|
|
13
|
+
| `glpi-ticket-timeline` | Read timeline records or write followups, tasks, solutions, and document links | `PostFollowup`, `PostTicketTask`, `PostSolution`, `PostTimelineDocument` (plus matching Get/Patch/Delete for followups/tasks/solutions; document reads return `GetDocument`, not a `GetTimelineDocument`) |
|
|
14
14
|
| `glpi-document-workflow` | Manage document metadata, upload binary content, download binaries | `GetDocument`, `PostDocument`, `PatchDocument`, `DeleteDocument` |
|
|
15
15
|
| `glpi-user-location-provisioning` | Search and provision users, locations, and entities | `GetUser`, `PostUser`, `GetLocation`, `PostLocation`, `GetEntity`, `PostEntity` |
|
|
16
16
|
| `glpi-reporting-and-context` | Aggregate ticket statistics, aggregate task durations, or load one ticket context bundle | `GlpiClient`, `GlpiTicketContext`, public enums |
|
|
17
17
|
| `glpi-team-members` | List, add, or remove ticket team members | `GetTeamMember`, `PostTeamMember` |
|
|
18
|
+
| `glpi-knowledge-base` | Search, read, or write KB articles, categories, comments, and revisions | `GetKBArticle`, `PostKBArticle`, `GetKBCategory`, `GetKBArticleComment`, `GetKBArticleRevision` |
|
|
19
|
+
| `glpi-plugin-fields` | Discover and read/write Fields-plugin custom fields | `GetPluginFieldsContainer`, `GetPluginFieldsField`, `GetPluginFieldsValueRow` |
|
|
18
20
|
|
|
19
21
|
## Sync and async
|
|
20
22
|
|
|
@@ -23,6 +25,6 @@ The package ships two clients with identical endpoint surfaces:
|
|
|
23
25
|
- `GlpiClient` — synchronous. `with GlpiClient(...) as client`, no `await`.
|
|
24
26
|
- `AsyncGlpiClient` — asynchronous, performing real non-blocking I/O. `async with AsyncGlpiClient(...) as client`, `await` every method.
|
|
25
27
|
|
|
26
|
-
Neither wraps the other: the async tree is hand-written and the synchronous one is generated from it by `unasync_build.py`, so the two cannot drift apart.
|
|
28
|
+
Neither wraps the other: the async tree is hand-written and the synchronous one is generated from it by `unasync_build.py`, so the two cannot drift apart. Every skill opens with a note telling you how to read its snippets across the two surfaces. For eight of the nine that note says the same thing -- the snippets are written against `AsyncGlpiClient`, so drop the `await` and the `async` for `GlpiClient`. `glpi-client-setup` is the exception and says so in its own note: choosing between the two clients is what that skill is *for*, so it shows both directly, side by side, and neither surface is a translation of the other.
|
|
27
29
|
|
|
28
30
|
When fanning out concurrently on the async client, bound the fan-out with an `asyncio.Semaphore` — see `glpi-client-setup`. An unbounded fan-out is slower, not faster.
|
|
@@ -1,15 +1,21 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: glpi-client-setup
|
|
3
|
-
description: "Create and configure the synchronous glpi_python_client.GlpiClient or the asynchronous glpi_python_client.AsyncGlpiClient, including from_env, OAuth credential pairs, entity/profile headers, SSL settings, and the optional legacy v1 document-
|
|
3
|
+
description: "Create and configure the synchronous glpi_python_client.GlpiClient or the asynchronous glpi_python_client.AsyncGlpiClient, including from_env, OAuth credential pairs, entity/profile headers, SSL settings, and the optional legacy v1 session (v1_base_url / v1_user_token) that backs document uploads, the Fields plugin helpers, KB category writes and actor-based statistics. Use before calling GLPI APIs, when configuring the v1 session for any of those features, or when the user asks how to connect to GLPI with glpi_python_client."
|
|
4
4
|
license: MIT
|
|
5
5
|
compatibility: "Requires Python 3.10+, glpi-python-client, network access to a GLPI v2 API, and valid GLPI credentials."
|
|
6
6
|
metadata:
|
|
7
7
|
package: glpi-python-client
|
|
8
|
-
version: "0.4.
|
|
8
|
+
version: "0.4.1"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# GLPI Client Setup
|
|
12
12
|
|
|
13
|
+
> Unlike the other skills in this package, the snippets below are not written
|
|
14
|
+
> against one client and translated for the other: `with GlpiClient(...)` and
|
|
15
|
+
> `async with AsyncGlpiClient(...)` examples both appear directly, side by
|
|
16
|
+
> side, because choosing between the two surfaces is what this skill is for.
|
|
17
|
+
> Read each example as written for the client it names.
|
|
18
|
+
|
|
13
19
|
The package exposes two clients with identical endpoint surfaces:
|
|
14
20
|
|
|
15
21
|
- `glpi_python_client.GlpiClient` — synchronous, blocking client. Use it from
|
|
@@ -44,8 +50,22 @@ call `client.close()` (or `await client.close()`) when finished.
|
|
|
44
50
|
`client_secret`, `username`/`password`, or both pairs together.
|
|
45
51
|
5. Add `glpi_entity`, `glpi_profile`, and `entity_recursive=True` only
|
|
46
52
|
when the operation must run in a specific GLPI scope.
|
|
47
|
-
6. Add `v1_base_url` and `v1_user_token`
|
|
48
|
-
uploads are
|
|
53
|
+
6. Add `v1_base_url` and `v1_user_token` whenever a v1-backed feature is
|
|
54
|
+
used, not only for uploads. They are required by: binary document
|
|
55
|
+
uploads (`upload_document`); the Fields plugin helpers
|
|
56
|
+
(`get_ticket_custom_fields`, `set_ticket_custom_fields`,
|
|
57
|
+
`list_plugin_fields_containers`, `list_plugin_fields_fields`,
|
|
58
|
+
`list_item_plugin_field_rows`, `create_item_plugin_field_row`,
|
|
59
|
+
`update_item_plugin_field_row`); KB category writes
|
|
60
|
+
(`set_kb_article_categories`, and `PostKBArticle.categories` /
|
|
61
|
+
`PatchKBArticle.categories` passed to `create_kb_article` /
|
|
62
|
+
`update_kb_article`); and actor-based statistics
|
|
63
|
+
(`get_user_activity`, `get_task_durations(user_id=...)`) — v2 cannot
|
|
64
|
+
filter on a ticket's actors at all, so those resolve through the v1
|
|
65
|
+
search engine. The same session also switches `get_task_durations`
|
|
66
|
+
to a bulk v1 task sweep once a run covers 25 tickets or more. Any of
|
|
67
|
+
these raises `RuntimeError` when the v1 session is absent.
|
|
68
|
+
`v1_app_token` is optional.
|
|
49
69
|
7. Keep `verify_ssl=True` unless the user explicitly confirms a test or
|
|
50
70
|
internal endpoint that cannot validate TLS.
|
|
51
71
|
8. Bound any large async fan-out with an `asyncio.Semaphore` on the
|
|
@@ -130,17 +150,25 @@ with GlpiClient.from_env() as glpi:
|
|
|
130
150
|
Environment setup, asynchronous:
|
|
131
151
|
|
|
132
152
|
```python
|
|
153
|
+
import asyncio
|
|
154
|
+
|
|
133
155
|
from glpi_python_client import AsyncGlpiClient
|
|
134
156
|
|
|
135
|
-
|
|
136
|
-
|
|
157
|
+
|
|
158
|
+
async def main() -> None:
|
|
159
|
+
async with AsyncGlpiClient.from_env() as glpi:
|
|
160
|
+
tickets = await glpi.search_tickets("status==1")
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
asyncio.run(main())
|
|
137
164
|
```
|
|
138
165
|
|
|
139
|
-
|
|
166
|
+
Legacy v1 session setup — enables every v1-backed feature from step 6,
|
|
167
|
+
not only uploads (works on either client):
|
|
140
168
|
|
|
141
169
|
```python
|
|
142
170
|
with GlpiClient.from_env(
|
|
143
|
-
v1_base_url="https://glpi.example.com/
|
|
171
|
+
v1_base_url="https://glpi.example.com/api.php/v1",
|
|
144
172
|
v1_user_token="legacy-user-token",
|
|
145
173
|
) as glpi:
|
|
146
174
|
...
|
|
@@ -154,10 +182,29 @@ with GlpiClient.from_env(
|
|
|
154
182
|
- The package no longer exports `GLPIV1Session`. Configure
|
|
155
183
|
`v1_base_url`/`v1_user_token` on the client and call
|
|
156
184
|
`upload_document` instead.
|
|
157
|
-
- Use `glpi_api_url` for the v2 API; `v1_base_url`
|
|
158
|
-
|
|
185
|
+
- Use `glpi_api_url` for the v2 API; `v1_base_url` additionally enables
|
|
186
|
+
every v1-backed feature listed in step 6 — document uploads, the
|
|
187
|
+
Fields plugin helpers, KB category writes and actor-based statistics.
|
|
159
188
|
- Closing the client matters because it owns one or two HTTP sessions
|
|
160
189
|
plus an OAuth token manager. Prefer the context-manager form.
|
|
190
|
+
- Every **API** failure the library raises derives from `GlpiError`,
|
|
191
|
+
exported from the package root. Construction raises
|
|
192
|
+
`GlpiValidationError` for a missing `glpi_api_url`, a half-supplied
|
|
193
|
+
credential pair, or a `v1_base_url` without a `v1_user_token`; calls
|
|
194
|
+
raise `GlpiAuthError` (401/403), `GlpiNotFoundError` (404),
|
|
195
|
+
`GlpiServerError` (persistent 5xx),
|
|
196
|
+
`GlpiTransportError`/`GlpiTimeoutError` (network fault) or
|
|
197
|
+
`GlpiProtocolError` (unusable 2xx body). Do not catch `requests`
|
|
198
|
+
exceptions — `requests` is not a dependency — and do not catch
|
|
199
|
+
`tenacity.RetryError`; the retry decorators re-raise the real error.
|
|
200
|
+
- A small set of raise sites is deliberately **outside** that hierarchy,
|
|
201
|
+
so `except GlpiError:` will not catch them. Plain `RuntimeError`:
|
|
202
|
+
using a closed client; a v1-backed call on a client built without
|
|
203
|
+
`v1_base_url`; and a `create_kb_article` whose category fallback
|
|
204
|
+
failed *after* the article was already created (the article exists,
|
|
205
|
+
its categories were not applied). Plain `TypeError`: an environment
|
|
206
|
+
value that is neither a string nor the expected scalar when `from_env`
|
|
207
|
+
parses an integer or boolean setting.
|
|
161
208
|
- Concurrent callers cannot stampede the token endpoint: the client
|
|
162
209
|
holds a lock around OAuth acquisition, so it is safe to launch a
|
|
163
210
|
fan-out on `AsyncGlpiClient` before the token has ever been fetched.
|
{glpi_python_client-0.4.1 → glpi_python_client-0.4.2}/skills/glpi-document-workflow/SKILL.md
RENAMED
|
@@ -5,7 +5,7 @@ license: MIT
|
|
|
5
5
|
compatibility: "Requires Python 3.10+, glpi-python-client, network access to the GLPI v2 API, and v1 credentials configured on the client for binary uploads."
|
|
6
6
|
metadata:
|
|
7
7
|
package: glpi-python-client
|
|
8
|
-
version: "0.4.
|
|
8
|
+
version: "0.4.1"
|
|
9
9
|
---
|
|
10
10
|
|
|
11
11
|
# GLPI Document Workflow
|
|
@@ -25,7 +25,7 @@ The `GLPIV1Session` class is no longer part of the public surface; the v1 sessio
|
|
|
25
25
|
6. Delete with `await client.delete_document(document_id, force=True|False|None)`.
|
|
26
26
|
7. Download bytes with `content = await client.download_document_content(document_id)`.
|
|
27
27
|
8. Upload bytes with `await client.upload_document(filename=..., content=..., mime_type=..., ticket_id=..., entity_id=...)`.
|
|
28
|
-
9. To
|
|
28
|
+
9. To put a file on a ticket timeline, use `upload_document(..., ticket_id=...)` -- it creates the document *and* the ticket link in one call. `link_ticket_timeline_document` from the timeline skill cannot be told **which** existing document to link: `PostTimelineDocument` declares only `extra_payload` and `timeline_position`, and the POST URL carries only the ticket id, so there is no typed slot for a document id. See the timeline skill for the `extra_payload` escape hatch and its caveat.
|
|
29
29
|
|
|
30
30
|
## Examples
|
|
31
31
|
|
|
@@ -64,9 +64,10 @@ document_id = await client.create_document(PostDocument(name="Diagnostic notes")
|
|
|
64
64
|
|
|
65
65
|
## Gotchas
|
|
66
66
|
|
|
67
|
+
- **`search_documents` swallows 4xx and returns `[]`.** This is a library-wide contract, not a document peculiarity: `_resource_list` checks the response status only when the caller passes a `failure_message`, and none of the seven `search_*` helpers (`search_documents`, `search_tickets`, `search_users`, `search_locations`, `search_entities`, `search_kb_articles`, `search_kb_categories`) passes one -- a GLPI error body is not a JSON list, so it is coerced to `[]`. A malformed RSQL filter, a 403 on `/Management/Document`, a missing route and "no such document" all look identical. `get_document`, `download_document_content` and every `list_*` helper do pass a `failure_message` and raise `GlpiStatusError` (narrowed to `GlpiAuthError` / `GlpiNotFoundError` / `GlpiServerError`) normally. So never conclude from an empty `search_documents` that a file is not on the server and re-upload it -- that is how duplicate documents get created; corroborate with a call that raises first.
|
|
67
68
|
- `upload_document` raises `RuntimeError` when the v1 session is not configured. Pass `v1_base_url` and `v1_user_token` to the client constructor or `from_env`.
|
|
68
69
|
- `upload_document` requires a non-empty `filename`. On the async client the multipart POST is awaited like any other call, so the event loop is not blocked.
|
|
69
70
|
- `download_document_content` returns `bytes` and raises on non-200 responses.
|
|
70
71
|
- `mime_type` defaults to `application/octet-stream` when omitted on `upload_document`.
|
|
71
|
-
-
|
|
72
|
+
- The snippets above use `AsyncGlpiClient`, so every call is awaited. The same methods on the synchronous `GlpiClient` are plain blocking calls -- drop the `await`.
|
|
72
73
|
- The `delete_document(force=True)` flag permanently deletes; omit (or `False`) to move to the trash.
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: glpi-knowledge-base
|
|
3
|
+
description: "Search, read, create, update, and delete GLPI knowledge base articles, categories, comments, and revisions with the synchronous glpi_python_client.GlpiClient or the asynchronous AsyncGlpiClient, and the GetKBArticle/PostKBArticle/GetKBCategory/GetKBArticleComment/GetKBArticleRevision models. Use for GLPI knowledge base content, FAQ articles, article categories, article comments, article revision history, or assigning categories to a KB article."
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: "Requires Python 3.10+, glpi-python-client, network access to the GLPI v2 API, and — for category writes only — a legacy v1 session (v1_base_url + v1_user_token)."
|
|
6
|
+
metadata:
|
|
7
|
+
package: glpi-python-client
|
|
8
|
+
version: "0.4.1"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# GLPI Knowledge Base
|
|
12
|
+
> The snippets below use `AsyncGlpiClient` (`async with` + `await`). Every method shown also exists on the synchronous `GlpiClient` with the same signature -- replace `async with` with `with`, drop the `await` keyword, and skip the surrounding `async def`/`asyncio.run` scaffolding.
|
|
13
|
+
|
|
14
|
+
The GLPI knowledge base lives under `/Knowledgebase/*` on the v2 API and covers four resources: articles, their categories, article comments, and article revisions. Eighteen methods expose them, present on both `GlpiClient` and `AsyncGlpiClient` with identical signatures. One operation -- assigning categories to an article -- is not a v2 call at all and needs a legacy v1 session; everything else in the family is pure v2.
|
|
15
|
+
|
|
16
|
+
## Procedure
|
|
17
|
+
|
|
18
|
+
1. Create a client from the `glpi-client-setup` skill. Add `v1_base_url` and `v1_user_token` **only** if you will write article categories.
|
|
19
|
+
2. Articles: `search_kb_articles(rsql_filter, limit=..., start=..., sort=..., language=...)` for lists and `get_kb_article(article_id)` for one. Write with `create_kb_article(PostKBArticle(...))` (returns the new id), `update_kb_article(article_id, PatchKBArticle(...))` and `delete_kb_article(article_id, force=...)` (both return `None`).
|
|
20
|
+
3. Article categories: `set_kb_article_categories(article_id, category_ids)`. The ids **replace** the whole set; an empty sequence clears it. Ids are not validated against the server -- an unknown id is simply not linked.
|
|
21
|
+
4. Categories: `search_kb_categories(...)` (same parameters as the article search), `get_kb_category(category_id)`, `create_kb_category(PostKBCategory(...))`, `update_kb_category(category_id, PatchKBCategory(...))`, `delete_kb_category(category_id, force=...)`. `completename` and `level` are server-managed and absent from the write models.
|
|
22
|
+
5. Comments: `list_kb_article_comments(article_id)`, `get_kb_article_comment(article_id, comment_id)`, `create_kb_article_comment(article_id, PostKBArticleComment(...))` (returns the new id), `update_kb_article_comment(article_id, comment_id, PatchKBArticleComment(...))`, `delete_kb_article_comment(article_id, comment_id, force=...)`. The parent article comes from the URL, so `PostKBArticleComment` has no `kbarticle` field.
|
|
23
|
+
6. Revisions, read-only: `list_kb_article_revisions(article_id, language=...)` then `get_kb_article_revision(article_id, revision, language=...)`.
|
|
24
|
+
7. Refetch with `get_kb_article()` when the task needs a populated model after a write.
|
|
25
|
+
|
|
26
|
+
## Examples
|
|
27
|
+
|
|
28
|
+
Create a category and a categorised article. `v1_base_url`/`v1_user_token` are required here **only** because the article carries `categories`:
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from glpi_python_client import AsyncGlpiClient, IdNameRef, PostKBArticle, PostKBCategory
|
|
32
|
+
|
|
33
|
+
async with AsyncGlpiClient(
|
|
34
|
+
glpi_api_url="https://glpi.example.com/api.php/v2",
|
|
35
|
+
client_id="oauth-client-id",
|
|
36
|
+
client_secret="oauth-client-secret",
|
|
37
|
+
v1_base_url="https://glpi.example.com/api.php/v1",
|
|
38
|
+
v1_user_token="legacy-user-token",
|
|
39
|
+
) as client:
|
|
40
|
+
category_id = await client.create_kb_category(
|
|
41
|
+
PostKBCategory(name="Network", comment="Networking runbooks")
|
|
42
|
+
)
|
|
43
|
+
# content/description are Markdown here and HTML on the wire.
|
|
44
|
+
article_id = await client.create_kb_article(
|
|
45
|
+
PostKBArticle(
|
|
46
|
+
name="Reset a password",
|
|
47
|
+
content="Run **passwd**, then check `logs`.",
|
|
48
|
+
description="A *short* summary.",
|
|
49
|
+
is_faq=True,
|
|
50
|
+
categories=[IdNameRef(id=category_id)], # IdRef would not validate
|
|
51
|
+
)
|
|
52
|
+
)
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Recover from the non-atomic create. There is no rollback: on failure the article exists and its id is only available from the message text:
|
|
56
|
+
|
|
57
|
+
```python
|
|
58
|
+
import re
|
|
59
|
+
|
|
60
|
+
from glpi_python_client import AsyncGlpiClient, PostKBArticle
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def create_with_categories(client: AsyncGlpiClient, article: PostKBArticle) -> int:
|
|
64
|
+
"""Create `article`, re-linking its categories if the fallback failed.
|
|
65
|
+
|
|
66
|
+
The retry presupposes a configured v1 session: when the missing session
|
|
67
|
+
is itself the cause, `set_kb_article_categories` raises the same
|
|
68
|
+
`RuntimeError` again. It helps only for a transient legacy failure.
|
|
69
|
+
"""
|
|
70
|
+
try:
|
|
71
|
+
return await client.create_kb_article(article)
|
|
72
|
+
except RuntimeError as exc: # plain builtin, NOT a GlpiError
|
|
73
|
+
match = re.search(r"KB article (\d+) was created", str(exc))
|
|
74
|
+
if match is None:
|
|
75
|
+
raise
|
|
76
|
+
article_id = int(match.group(1))
|
|
77
|
+
# The v2 article is intact; retry only the legacy category link.
|
|
78
|
+
# Derive the ids from the model -- they are the same list that
|
|
79
|
+
# triggered the fallback, so the retry cannot link nothing.
|
|
80
|
+
await client.set_kb_article_categories(
|
|
81
|
+
article_id, [c.id for c in article.categories or [] if c.id is not None]
|
|
82
|
+
)
|
|
83
|
+
return article_id
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Search, and disambiguate an empty result. `language` is a query parameter on both search helpers:
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from glpi_python_client import GlpiNotFoundError
|
|
90
|
+
|
|
91
|
+
faq = await client.search_kb_articles(
|
|
92
|
+
"is_faq==1", limit=25, start=0, sort="date_mod desc", language="fr_FR"
|
|
93
|
+
)
|
|
94
|
+
categories = await client.search_kb_categories("name==Network", limit=10)
|
|
95
|
+
print([(c.id, c.completename) for c in categories])
|
|
96
|
+
|
|
97
|
+
# A search never raises on a 4xx -- it returns []. To tell 'no matches'
|
|
98
|
+
# from 'this GLPI serves no /Knowledgebase routes', probe an article you
|
|
99
|
+
# know exists. Only the SUCCEEDING branch is informative: a 404 is raised
|
|
100
|
+
# both by an absent route and by an absent id, so it proves nothing.
|
|
101
|
+
if not faq:
|
|
102
|
+
known_article_id = 1 # an article known to exist on this instance
|
|
103
|
+
try:
|
|
104
|
+
await client.get_kb_article(known_article_id)
|
|
105
|
+
except GlpiNotFoundError:
|
|
106
|
+
print("inconclusive: no such article, or no /Knowledgebase routes")
|
|
107
|
+
else:
|
|
108
|
+
print("the endpoint is served -- the filter simply matched nothing")
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
Comments and revisions on one article:
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
from glpi_python_client import PatchKBArticleComment, PostKBArticleComment
|
|
115
|
+
|
|
116
|
+
# `comment` is plain text -- no Markdown conversion, unlike article content.
|
|
117
|
+
comment_id = await client.create_kb_article_comment(
|
|
118
|
+
5, PostKBArticleComment(comment="Confirmed on GLPI 11.")
|
|
119
|
+
)
|
|
120
|
+
await client.update_kb_article_comment(
|
|
121
|
+
5, comment_id, PatchKBArticleComment(comment="Edited.")
|
|
122
|
+
)
|
|
123
|
+
one = await client.get_kb_article_comment(5, comment_id)
|
|
124
|
+
for listed in await client.list_kb_article_comments(5):
|
|
125
|
+
print(listed.id, listed.comment)
|
|
126
|
+
await client.delete_kb_article_comment(5, comment_id, force=True)
|
|
127
|
+
|
|
128
|
+
# `language` is a PATH SEGMENT here: Knowledgebase/Article/5/fr_FR/Revision
|
|
129
|
+
revisions = await client.list_kb_article_revisions(5, language="fr_FR")
|
|
130
|
+
if revisions and revisions[0].revision is not None:
|
|
131
|
+
revision = await client.get_kb_article_revision(
|
|
132
|
+
5, revisions[0].revision, language="fr_FR" # the revision NUMBER
|
|
133
|
+
)
|
|
134
|
+
print(revision.revision, revision.content) # content comes back as Markdown
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
Category maintenance is pure v2 -- no legacy session is involved:
|
|
138
|
+
|
|
139
|
+
```python
|
|
140
|
+
from glpi_python_client import IdNameRef, PatchKBCategory, PostKBCategory
|
|
141
|
+
|
|
142
|
+
parent_id = await client.create_kb_category(PostKBCategory(name="IT"))
|
|
143
|
+
child_id = await client.create_kb_category(
|
|
144
|
+
PostKBCategory(name="Network", parent=IdNameRef(id=parent_id), is_recursive=True)
|
|
145
|
+
)
|
|
146
|
+
await client.update_kb_category(child_id, PatchKBCategory(comment="Moved"))
|
|
147
|
+
category = await client.get_kb_category(child_id)
|
|
148
|
+
print(category.completename, category.level) # both server-managed
|
|
149
|
+
|
|
150
|
+
await client.delete_kb_category(child_id, force=True) # omit force to trash it
|
|
151
|
+
await client.delete_kb_article(42, force=True)
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
## Gotchas
|
|
155
|
+
|
|
156
|
+
- Assigning categories to an article is the only KB operation that needs the legacy v1 session, and it is not a v2 call at all: `set_kb_article_categories` issues a legacy `PUT KnowbaseItem/{article_id}` with the body `{"input": {"_categories": [ids]}}`. Every category CRUD call, every comment call and every revision call is pure v2.
|
|
157
|
+
- When no v1 session is configured, that path raises a plain builtin `RuntimeError`, **not** a `GlpiError` -- `except GlpiError:` will not catch it. The message is `GLPI knowledge base category assignments require the legacy v1 session to be configured (set v1_base_url and v1_user_token).` The session is only built when *both* `v1_base_url` and `v1_user_token` are supplied; exactly one of the pair raises `GlpiValidationError` at client construction.
|
|
158
|
+
- `create_kb_article` is not atomic and there is no rollback. If the v2 POST succeeds and the category fallback then fails, the article stays on the server: you get an error *and* an uncategorised article. The failure is a `RuntimeError` shaped `KB article 88 was created but assigning its categories failed: ...`, chaining the original as `__cause__` -- so the new id is recoverable only from the message text, and `except GlpiValidationError:` around a create catches nothing.
|
|
159
|
+
- `update_kb_article` does not wrap the failure the way create does -- the raw error propagates (`GlpiValidationError` for a category reference with no `id`, `RuntimeError` for a missing v1 session, `GlpiStatusError` for a legacy non-success). The v2 field changes are already applied and are not reverted.
|
|
160
|
+
- `categories=[]` means opposite things on create and update. On create it is skipped entirely: no v1 call, no v1 session needed. On update it *clears* every category, which is a legacy write and does need v1. Only `categories=None` (the default) is a no-op on both.
|
|
161
|
+
- `categories` is still sent inside the v2 POST/PATCH body and GLPI silently ignores it; the body is never stripped. So a create-with-categories against a client with no v1 session yields an uncategorised article *and* an error, not a clean rejection -- and no code path persists a category through v2 alone.
|
|
162
|
+
- **Every `search_*` helper in the library swallows 4xx and returns `[]`. This is a library-wide contract, not a KB peculiarity.** `_resource_list` checks the response status only when the caller passes a `failure_message`, and none of the seven searches -- `search_kb_articles`, `search_kb_categories`, `search_tickets`, `search_users`, `search_locations`, `search_entities`, `search_documents` -- passes one. A GLPI error body is not a JSON list, so it is coerced to `[]`. Every `list_*` and `get_*` helper does pass a `failure_message` and raises normally: here that is `list_kb_article_comments`, `list_kb_article_revisions`, `get_kb_article`, `get_kb_category` and `get_kb_article_revision` (`GlpiNotFoundError` on a 404). So an empty list from a search means "no matches" *or* "bad RSQL filter" *or* "403" *or* "this GLPI serves no `/Knowledgebase` routes at all" (they need High-Level API >= 2.2.0), indistinguishably; an empty list from a list helper is unambiguous. Never treat `[]` from a search as proof a record is absent before creating one.
|
|
163
|
+
- `language` has two different mechanics in this family. On `search_kb_articles`/`search_kb_categories` it is a **query parameter**. On `list_kb_article_revisions`/`get_kb_article_revision` it is a **path segment** between the id and `Revision` (`Knowledgebase/Article/5/fr_FR/Revision`). `get_kb_article`, the comment helpers and every write helper take no `language` at all; they inherit the client-level value, sent as `Accept-Language` (default `en_GB`).
|
|
164
|
+
- KB write models use `IdNameRef` for every foreign key -- `categories[]`, `entity`, `user`, `parent` -- not `IdRef`. Passing `IdRef(id=4)` raises a pydantic `ValidationError`. (`GetKBArticleComment.parent` is the one KB field genuinely typed `IdRef`, and it is read-only.)
|
|
165
|
+
- Article `content`/`description` and revision `content` are Markdown on the Python side and HTML on the wire; the conversion is automatic, so never author HTML. Comment `comment` is a plain `str` with no conversion at all -- the inconsistency is real, not an omission here.
|
|
166
|
+
- `force` on `delete_kb_article`, `delete_kb_category` and `delete_kb_article_comment` is keyword-only and is serialised into the JSON request **body** via the matching `Delete*` model, not sent as a query parameter. `force=True` deletes permanently; omitting it or passing `False` moves the record to the GLPI trash.
|
|
167
|
+
- Revisions are read-only: there is no create/update/delete helper and no Post/Patch/Delete revision model. A revision appears as a side effect of updating an article. `get_kb_article_revision(article_id, revision)` takes the revision **number** (`GetKBArticleRevision.revision`), not the row `id` -- the two differ on the model.
|
|
168
|
+
- `GetKBArticle.revisions` and `.translations` hold two *different* private ref classes (leading underscore, not exported from the package root). Read their attributes; never import them. The two field sets are not interchangeable: a `revisions` entry has `.id`, `.revision`, `.language`, `.date`, while a `translations` entry has `.id`, `.language`, `.name`. `revisions[0].name` raises `AttributeError` -- the models allow unknown keys from the server, but that does not synthesise an attribute that was never sent.
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: glpi-plugin-fields
|
|
3
|
+
description: "Discover and read/write GLPI Fields-plugin custom fields with the synchronous glpi_python_client.GlpiClient or the asynchronous AsyncGlpiClient — list_plugin_fields_containers, list_plugin_fields_fields, list_item_plugin_field_rows, create_item_plugin_field_row, update_item_plugin_field_row, and the Ticket-only get_ticket_custom_fields/set_ticket_custom_fields. Use for GLPI custom fields, the Fields plugin, per-instance extra ticket attributes, or reading a ticket's custom-field values."
|
|
4
|
+
license: MIT
|
|
5
|
+
compatibility: "Requires Python 3.10+, glpi-python-client, the GLPI Fields plugin installed server-side, and a legacy v1 session (v1_base_url + v1_user_token) — every method in this family goes over the v1 API."
|
|
6
|
+
metadata:
|
|
7
|
+
package: glpi-python-client
|
|
8
|
+
version: "0.4.1"
|
|
9
|
+
---
|
|
10
|
+
|
|
11
|
+
# GLPI Plugin Fields
|
|
12
|
+
> The snippets below use `AsyncGlpiClient` (`async with` + `await`). Every method shown also exists on the synchronous `GlpiClient` with the same signature -- replace `async with` with `with`, drop the `await` keyword, and skip the surrounding `async def`/`asyncio.run` scaffolding.
|
|
13
|
+
|
|
14
|
+
The GLPI `Fields` plugin adds user-defined custom fields to any itemtype: a *container* is a block of fields attached to one or more itemtypes, and each container stores one value row per item. None of it exists in the GLPI v2 contract, so all seven methods -- `list_plugin_fields_containers`, `list_plugin_fields_fields`, `list_item_plugin_field_rows`, `create_item_plugin_field_row`, `update_item_plugin_field_row`, and the Ticket-only `get_ticket_custom_fields`/`set_ticket_custom_fields` -- talk to the legacy v1 REST API. They are present on both `GlpiClient` and `AsyncGlpiClient` with identical signatures.
|
|
15
|
+
|
|
16
|
+
Two constraints decide whether any of it works, so settle them first:
|
|
17
|
+
|
|
18
|
+
- **Every one of the seven methods needs the legacy v1 session.** Build the client with `v1_base_url` *and* `v1_user_token` (or `GLPI_V1_BASE_URL`/`GLPI_V1_USER_TOKEN` for `from_env`). Without them the call raises a plain builtin `RuntimeError`, **not** a `GlpiError` -- `except GlpiError:` will not catch it. The message is `GLPI Fields plugin helpers require the legacy v1 session to be configured (set v1_base_url and v1_user_token).` Supplying exactly one of the pair raises `GlpiValidationError` at client construction instead.
|
|
19
|
+
- **Discovery is mandatory, not advisory.** Container and field names are chosen by whoever configured the plugin on that instance, and the plugin itself is optional. There is nothing to hardcode: read `container.name` and `field.name` off the server and reuse them verbatim.
|
|
20
|
+
|
|
21
|
+
```text
|
|
22
|
+
list_plugin_fields_containers(itemtype) → list_plugin_fields_fields(container_id)
|
|
23
|
+
→ list_item_plugin_field_rows(itemtype, items_id, container_name)
|
|
24
|
+
→ create_item_plugin_field_row(...) / update_item_plugin_field_row(...)
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
The family takes two different `values` shapes and they are not interchangeable -- flat for the low-level row helpers, nested for the two Ticket helpers:
|
|
28
|
+
|
|
29
|
+
```python
|
|
30
|
+
# create_item_plugin_field_row / update_item_plugin_field_row -- FLAT,
|
|
31
|
+
# one level, keyed by field.name:
|
|
32
|
+
values = {"extrainfofield": "<p>x</p>"}
|
|
33
|
+
|
|
34
|
+
# get_ticket_custom_fields / set_ticket_custom_fields -- NESTED,
|
|
35
|
+
# outer key is container.name:
|
|
36
|
+
values = {"extrainfo": {"extrainfofield": "<p>new</p>"}}
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
## Procedure
|
|
40
|
+
|
|
41
|
+
1. Create a client from the `glpi-client-setup` skill, adding `v1_base_url` and `v1_user_token`.
|
|
42
|
+
2. Discover containers with `list_plugin_fields_containers(itemtype="Ticket")`. `itemtype` is optional and filtered client-side. An uninstalled plugin does **not** return `[]`: it raises `GlpiStatusError` with `.status_code == 400` and `ERROR_RESOURCE_NOT_FOUND_NOR_COMMONDBTM` in `.response_text`. An installed-but-unused plugin returns `[]`.
|
|
43
|
+
3. Discover fields with `list_plugin_fields_fields(container_id=container.id)`. `field.name` is the key you put in a `values` dict; `field.type` (`string`, `text`, `richtext`, `dropdown`, `yesno`, `date`, `datetime`, `number`, `url`, `header`) tells you the value format.
|
|
44
|
+
4. Read the stored row with `list_item_plugin_field_rows(itemtype, items_id, container_name)` -- ordinary parameters, passable positionally or by keyword. It returns zero or one `GetPluginFieldsValueRow`; the values live in `row.extra_payload` and `row.id` is the `row_id` an update needs.
|
|
45
|
+
5. Write with `update_item_plugin_field_row(itemtype=..., container_name=..., row_id=..., values=...)` when a row exists, otherwise `create_item_plugin_field_row(itemtype=..., items_id=..., container_id=..., container_name=..., values=..., entities_id=...)`, which returns the new row id. Both are keyword-only.
|
|
46
|
+
6. On Tickets only, `get_ticket_custom_fields(ticket_id)` and `set_ticket_custom_fields(ticket_id, values)` fold steps 2-5 into one call each, using the nested mapping. For every other itemtype, drive steps 2-5 yourself.
|
|
47
|
+
|
|
48
|
+
## Examples
|
|
49
|
+
|
|
50
|
+
Discovery, including the branch that tells "plugin absent" apart from "plugin configured but empty":
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
import asyncio
|
|
54
|
+
|
|
55
|
+
from glpi_python_client import AsyncGlpiClient, GlpiStatusError
|
|
56
|
+
|
|
57
|
+
# GLPI answers 400 with this marker when the itemtype in the URL is not a
|
|
58
|
+
# known CommonDBTM subclass -- which is what an uninstalled plugin looks
|
|
59
|
+
# like from the outside.
|
|
60
|
+
PLUGIN_ABSENT = "ERROR_RESOURCE_NOT_FOUND_NOR_COMMONDBTM"
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
async def main() -> None:
|
|
64
|
+
async with AsyncGlpiClient(
|
|
65
|
+
glpi_api_url="https://glpi.example.com/api.php/v2",
|
|
66
|
+
client_id="oauth-client-id",
|
|
67
|
+
client_secret="oauth-client-secret",
|
|
68
|
+
v1_base_url="https://glpi.example.com/api.php/v1",
|
|
69
|
+
v1_user_token="legacy-user-token",
|
|
70
|
+
) as client:
|
|
71
|
+
try:
|
|
72
|
+
containers = await client.list_plugin_fields_containers(itemtype="Ticket")
|
|
73
|
+
except GlpiStatusError as exc:
|
|
74
|
+
if exc.status_code == 400 and PLUGIN_ABSENT in (exc.response_text or ""):
|
|
75
|
+
print("the GLPI Fields plugin is not installed on this instance")
|
|
76
|
+
return
|
|
77
|
+
raise
|
|
78
|
+
|
|
79
|
+
for container in containers:
|
|
80
|
+
if container.id is None or not container.name:
|
|
81
|
+
continue
|
|
82
|
+
# container.name is the internal key you reuse verbatim;
|
|
83
|
+
# container.label is the UI label and is never a valid key.
|
|
84
|
+
print(container.id, container.name, container.label, container.is_active)
|
|
85
|
+
fields = await client.list_plugin_fields_fields(container_id=container.id)
|
|
86
|
+
for field in fields:
|
|
87
|
+
print(" ", field.name, field.type, field.is_active, field.is_readonly)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
asyncio.run(main())
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
Read one ticket's custom fields. The result is the nested mapping, and a container with nothing saved is missing from it entirely:
|
|
94
|
+
|
|
95
|
+
```python
|
|
96
|
+
from glpi_python_client import AsyncGlpiClient
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
async def ticket_note(client: AsyncGlpiClient, ticket_id: int) -> str | None:
|
|
100
|
+
"""Return one ticket's `extrainfo.extrainfofield` value, if it has one."""
|
|
101
|
+
values = await client.get_ticket_custom_fields(ticket_id)
|
|
102
|
+
# {'extrainfo': {'extrainfofield': '<p>test</p>'}}
|
|
103
|
+
|
|
104
|
+
# Containers with no persisted row are ABSENT, not empty: `.get`, never [].
|
|
105
|
+
note = values.get("extrainfo", {}).get("extrainfofield")
|
|
106
|
+
|
|
107
|
+
# The inner dict is the row's extra_payload -- dynamic columns only, so
|
|
108
|
+
# it carries no row id. For that, drop to the low-level row listing:
|
|
109
|
+
rows = await client.list_item_plugin_field_rows("Ticket", ticket_id, "extrainfo")
|
|
110
|
+
if rows:
|
|
111
|
+
print(rows[0].id, rows[0].items_id, rows[0].extra_payload)
|
|
112
|
+
return note
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
Write to a ticket with the high-level upsert. Values go over the wire verbatim, so a `richtext` field takes raw HTML:
|
|
116
|
+
|
|
117
|
+
```python
|
|
118
|
+
from glpi_python_client import GlpiValidationError
|
|
119
|
+
|
|
120
|
+
await client.set_ticket_custom_fields(
|
|
121
|
+
1234, {"extrainfo": {"extrainfofield": "<p>Handled by the NOC shift</p>"}}
|
|
122
|
+
)
|
|
123
|
+
# GET containers, GET fields, GET rows, then PUT {"input": {"id": 1, ...}}
|
|
124
|
+
# -- or POST with items_id/itemtype/plugin_fields_containers_id if no row exists.
|
|
125
|
+
|
|
126
|
+
await client.set_ticket_custom_fields(1234, {}) # empty mapping: zero HTTP calls
|
|
127
|
+
|
|
128
|
+
# Container names are matched EXACT-CASE against container.name.
|
|
129
|
+
try:
|
|
130
|
+
await client.set_ticket_custom_fields(1234, {"ExtraInfo": {"extrainfofield": "x"}})
|
|
131
|
+
except GlpiValidationError as exc:
|
|
132
|
+
print(exc) # Unknown plugin-fields container(s) for Ticket: ExtraInfo
|
|
133
|
+
|
|
134
|
+
# The call is not atomic across containers, so write one per call when a
|
|
135
|
+
# rejected field name must not leave the earlier container already written.
|
|
136
|
+
payload = {
|
|
137
|
+
"extrainfo": {"extrainfofield": "<p>a</p>"},
|
|
138
|
+
"secondary": {"othercolumn": "b"},
|
|
139
|
+
}
|
|
140
|
+
for container_name, columns in payload.items():
|
|
141
|
+
await client.set_ticket_custom_fields(1234, {container_name: columns})
|
|
142
|
+
```
|
|
143
|
+
|
|
144
|
+
Upsert on any other itemtype, with the flat `values` dict and the low-level helpers:
|
|
145
|
+
|
|
146
|
+
```python
|
|
147
|
+
from glpi_python_client import AsyncGlpiClient
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
async def upsert_plugin_field_row(
|
|
151
|
+
client: AsyncGlpiClient,
|
|
152
|
+
itemtype: str, # "Computer", "Problem", "Change", ...
|
|
153
|
+
items_id: int,
|
|
154
|
+
container_id: int, # container.id -- create needs it in the body
|
|
155
|
+
container_name: str, # container.name -- it builds the URL itemtype
|
|
156
|
+
values: dict[str, object], # FLAT: {field.name: value}
|
|
157
|
+
) -> int:
|
|
158
|
+
"""Update this container's row for one item, creating it when absent."""
|
|
159
|
+
# Positional is allowed here (ordinary parameters); the two writers
|
|
160
|
+
# below are keyword-only and raise TypeError if called positionally.
|
|
161
|
+
rows = await client.list_item_plugin_field_rows(itemtype, items_id, container_name)
|
|
162
|
+
if rows and rows[0].id is not None:
|
|
163
|
+
await client.update_item_plugin_field_row(
|
|
164
|
+
itemtype=itemtype,
|
|
165
|
+
container_name=container_name,
|
|
166
|
+
row_id=rows[0].id,
|
|
167
|
+
values=values, # only these columns are touched
|
|
168
|
+
)
|
|
169
|
+
return rows[0].id
|
|
170
|
+
return await client.create_item_plugin_field_row(
|
|
171
|
+
itemtype=itemtype,
|
|
172
|
+
items_id=items_id,
|
|
173
|
+
container_id=container_id,
|
|
174
|
+
container_name=container_name,
|
|
175
|
+
values=values,
|
|
176
|
+
# `entities_id` is create-only and is omitted from the body unless
|
|
177
|
+
# you pass it, letting the server apply its default scope. Do NOT
|
|
178
|
+
# hardcode 0 here: 0 is not None, so it would pin every row you
|
|
179
|
+
# create to entity 0. Pass a real entity id only when you mean one.
|
|
180
|
+
)
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
## Gotchas
|
|
184
|
+
|
|
185
|
+
- **The two `values` shapes are the main trap.** `create_item_plugin_field_row` and `update_item_plugin_field_row` take a **flat** `dict[str, object]` of field name to value -- `values={"extrainfofield": "<p>x</p>"}`. `get_ticket_custom_fields` returns, and `set_ticket_custom_fields` accepts, a **nested** `dict[str, dict[str, Any]]` keyed by container name -- `{"extrainfo": {"extrainfofield": "<p>new</p>"}}`. Passing the nested shape to the low-level create sends the inner dict as a column value; passing the flat shape to `set_ticket_custom_fields` makes field names look like container names and raises `GlpiValidationError: Unknown plugin-fields container(s) for Ticket: ...`.
|
|
186
|
+
- `container_name` and `container_id` are not interchangeable, and `create_item_plugin_field_row` needs **both**. The name builds the URL itemtype, lowercased (`Ticket` + `extrainfo` gives `PluginFieldsTicketextrainfo`, and `Ticket/1234/PluginFieldsTicketextrainfo` for the row list). The id is a body column, `plugin_fields_containers_id`, and it is also what `list_plugin_fields_fields(container_id=...)` filters on. `update_item_plugin_field_row` needs only the name plus a `row_id`, which identifies the record on its own. `list_item_plugin_field_rows` needs only the name too, and has **no** `row_id` parameter -- its signature is exactly `(itemtype, items_id, container_name)`; it is what you call *to obtain* a `row_id`.
|
|
187
|
+
- Container-name matching in `set_ticket_custom_fields` is **exact-case** against `container.name`, while the URL derivation lowercases. So `{"ExtraInfo": {...}}` raises `GlpiValidationError` when the container is actually named `extrainfo`, even though the derived URL would have been identical. Copy `container.name` verbatim from discovery; never retype it and never substitute `container.label`.
|
|
188
|
+
- `set_ticket_custom_fields` is **not atomic across multiple containers**, despite a docstring claiming validation happens "before any write to keep the call atomic". Only the unknown-*container* check runs up front for the whole payload; the unknown-*field* check runs per container inside the write loop. With two containers where the second has a typo, the first is already written when the error raises. Write one container per call when you need all-or-nothing.
|
|
189
|
+
- `get_ticket_custom_fields` returns **only `extra_payload`** -- every *undeclared* key of the row, not a curated list of the plugin's fields. So the row's `id`, `items_id`, `itemtype`, `plugin_fields_containers_id` and `entities_id` are absent (use `list_item_plugin_field_rows` when you need the `row_id`), and any other bookkeeping column the v1 server returns appears alongside real values. Intersect against `list_plugin_fields_fields` names if you need only declared fields.
|
|
190
|
+
- A container that has never had a value saved for that ticket is **silently absent** from the `get_ticket_custom_fields` result -- you do not get `{"container": {}}`. Use `result.get(name, {})`, never `result[name]`. An empty overall dict is **ambiguous**: `get_ticket_custom_fields` builds its result by skipping every container with no persisted row, so `{}` comes back both when the instance declares no Ticket containers at all and when it declares several but this ticket has saved nothing in any of them. The return value cannot tell you which -- call `list_plugin_fields_containers(itemtype="Ticket")` if you need to know.
|
|
191
|
+
- Both discovery listings fetch **one fixed page, `range=0-999`**, with no pagination and no server-side filtering. The `itemtype` and `container_id` narrowing happens client-side *after* that cap, so an instance with more than 1000 containers or field declarations silently loses the tail -- possibly including the container you are looking for.
|
|
192
|
+
- Neither listing filters on `is_active`, so **disabled containers and disabled or read-only fields come back from discovery looking exactly like live ones**. `set_ticket_custom_fields` accepts a field whose `is_readonly` is `True`, because its guard only checks that the name is declared. Check `container.is_active`, `field.is_active` and `field.is_readonly` yourself.
|
|
193
|
+
- Values are transmitted **verbatim -- there is no HTML/Markdown conversion on this path**, unlike ticket and KB article content. A `richtext` field takes raw HTML (`"<p>test</p>"`). Convert yourself if you want Markdown: `GlpiContentConverter` is not exported from the package root, import it from `glpi_python_client.content`.
|
|
194
|
+
- The two convenience helpers are **Ticket-only** -- the itemtype is hardcoded. There is no `get_item_custom_fields` and no `set_item_custom_fields`. For Computer, Problem, Change and the rest, drive the generic row helpers.
|
|
195
|
+
- Parameter-passing style is inconsistent across the family, and only one half of it is a rule. `list_item_plugin_field_rows(itemtype, items_id, container_name)` declares ordinary `POSITIONAL_OR_KEYWORD` parameters, so both `("Ticket", 1234, "extrainfo")` and `(itemtype="Ticket", items_id=1234, container_name="extrainfo")` are legal -- the examples above pass them positionally by choice, not by requirement. `create_item_plugin_field_row` and `update_item_plugin_field_row` are genuinely **keyword-only** (`*` in the signature): calling either writer positionally is a `TypeError`.
|
|
196
|
+
- Error taxonomy on the write path: an unknown container name or an unknown field name raises `GlpiValidationError`; a container the server returned without an `id`, or a create whose v1 reply carries no numeric row id, raises `GlpiProtocolError`. Both inherit `ValueError`. A missing v1 session raises a plain `RuntimeError`, and a non-success v1 status raises `GlpiStatusError` (with `.status_code`, `.url` and `.response_text`).
|
|
197
|
+
- `entities_id` exists on **create only**, and is left out of the body unless explicitly passed. `update_item_plugin_field_row` has no such parameter -- its body is exactly `{"input": {"id": row_id, **values}}`. `set_ticket_custom_fields` never passes it, so rows it creates take the GLPI server's default scope.
|
|
198
|
+
- Cost is **linear and sequential**; there is no concurrent fan-out in this family. `get_ticket_custom_fields` costs one container list plus one row list per Ticket container. `set_ticket_custom_fields` costs one container list plus, per container in the payload, one field list, one row list and one write -- and it re-reads discovery on every call, with no caching. Cache the container and field listings yourself when writing many tickets in a loop.
|
|
199
|
+
- `PostPluginFieldsValueRow` is exported at the package root but is **dead surface for callers**: no client method takes or returns it. `create_item_plugin_field_row` assembles the request body itself from a plain dict. Building this model and handing it to the client neither type-checks nor works.
|