glpi-python-client 0.4.1__tar.gz → 0.4.3__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/CHANGELOG.md +178 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/CONTRIBUTING.md +1 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/PKG-INFO +6 -2
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/README.md +2 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/publishing_rtd.rst +1 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/user_guide.rst +44 -6
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/__init__.py +9 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/_base_client.py +25 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/administration/_user.py +76 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/assistance/_team.py +2 -3
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/assistance/_ticket.py +8 -2
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/dropdowns/_location.py +44 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/knowledgebase/_article.py +59 -2
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/knowledgebase/_category.py +60 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/management/_document.py +92 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/plugins/_fields.py +19 -3
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/commons/_config.py +61 -0
- {glpi_python_client-0.4.1/glpi_python_client/_sync → glpi_python_client-0.4.3/glpi_python_client/_async}/clients/commons/_filters.py +24 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/commons/_http.py +64 -5
- glpi_python_client-0.4.3/glpi_python_client/_async/clients/commons/_payloads.py +95 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/commons/_transport.py +99 -15
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/custom/_statistics.py +32 -22
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/_base_client.py +25 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/administration/_user.py +76 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/assistance/_team.py +2 -3
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/assistance/_ticket.py +8 -2
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/dropdowns/_location.py +44 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/knowledgebase/_article.py +59 -2
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/knowledgebase/_category.py +60 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/management/_document.py +92 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/plugins/_fields.py +19 -3
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/commons/_config.py +61 -0
- {glpi_python_client-0.4.1/glpi_python_client/_async → glpi_python_client-0.4.3/glpi_python_client/_sync}/clients/commons/_filters.py +24 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/commons/_http.py +64 -5
- glpi_python_client-0.4.3/glpi_python_client/_sync/clients/commons/_payloads.py +95 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/commons/_transport.py +99 -15
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/custom/_statistics.py +32 -22
- glpi_python_client-0.4.3/glpi_python_client/content/conversion.py +130 -0
- glpi_python_client-0.4.3/glpi_python_client/models/_base.py +176 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/_content.py +13 -4
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/assistance/_ticket.py +42 -6
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/custom_schema/_ticket_context.py +18 -3
- glpi_python_client-0.4.3/glpi_python_client/rsql.py +188 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/testing/utils.py +1 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/pyproject.toml +9 -1
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/skills/README.md +4 -2
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/skills/glpi-client-setup/SKILL.md +60 -10
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/skills/glpi-document-workflow/SKILL.md +8 -7
- glpi_python_client-0.4.3/skills/glpi-knowledge-base/SKILL.md +169 -0
- glpi_python_client-0.4.3/skills/glpi-plugin-fields/SKILL.md +200 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/skills/glpi-reporting-and-context/SKILL.md +15 -13
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/skills/glpi-team-members/SKILL.md +4 -3
- glpi_python_client-0.4.3/skills/glpi-ticket-timeline/SKILL.md +124 -0
- glpi_python_client-0.4.3/skills/glpi-ticket-workflow/SKILL.md +80 -0
- glpi_python_client-0.4.3/skills/glpi-user-location-provisioning/SKILL.md +109 -0
- glpi_python_client-0.4.1/glpi_python_client/_async/clients/commons/_payloads.py +0 -42
- glpi_python_client-0.4.1/glpi_python_client/_sync/clients/commons/_payloads.py +0 -42
- glpi_python_client-0.4.1/glpi_python_client/content/conversion.py +0 -58
- glpi_python_client-0.4.1/glpi_python_client/models/_base.py +0 -61
- glpi_python_client-0.4.1/skills/glpi-ticket-timeline/SKILL.md +0 -72
- glpi_python_client-0.4.1/skills/glpi-ticket-workflow/SKILL.md +0 -76
- 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.3}/.gitignore +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/.pre-commit-config.yaml +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/.readthedocs.yaml +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/LICENSE +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/_static/.gitkeep +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/api_reference.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/conf.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/development.md +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/development_rtd.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/index.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/installation.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/publishing.md +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/docs/sponsoring.rst +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/_concurrency.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/_testing.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/auth/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/auth/_v1_session.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/auth/auth.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/administration/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/administration/_entity.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/assistance/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/knowledgebase/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/knowledgebase/_comment.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/knowledgebase/_revision.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/management/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/api/plugins/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/client.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/commons/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/commons/_constants.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/custom/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_async/clients/custom/_ticket_context.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_errors.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/_concurrency.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/_testing.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/auth/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/auth/_v1_session.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/auth/auth.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/administration/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/administration/_entity.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/assistance/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/knowledgebase/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/knowledgebase/_comment.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/knowledgebase/_revision.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/management/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/api/plugins/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/client.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/commons/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/commons/_constants.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/custom/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/_sync/clients/custom/_ticket_context.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/content/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/_common.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/administration/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/administration/_entity.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/administration/_user.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/assistance/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/assistance/_team.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/assistance/timeline/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/assistance/timeline/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/assistance/timeline/_followup.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/assistance/timeline/_solution.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/assistance/timeline/_task.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/dropdowns/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/dropdowns/_location.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/enums.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/knowledgebase/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/knowledgebase/_article.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/knowledgebase/_category.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/knowledgebase/_comment.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/knowledgebase/_revision.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/management/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/management/_document.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/plugins/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/api_schema/plugins/_fields.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/models/custom_schema/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/py.typed +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/testing/__init__.py +0 -0
- {glpi_python_client-0.4.1 → glpi_python_client-0.4.3}/glpi_python_client/testing/fixtures.py +0 -0
|
@@ -4,7 +4,178 @@ 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
|
-
##
|
|
7
|
+
## 0.4.3 — 2026-08-13
|
|
8
|
+
|
|
9
|
+
### Changed (breaking)
|
|
10
|
+
|
|
11
|
+
- **`server_timezone` is now a required client argument** (`GLPI_SERVER_TIMEZONE`
|
|
12
|
+
for `from_env`). It takes an IANA zone name — `"Europe/Paris"` — or a
|
|
13
|
+
`tzinfo`.
|
|
14
|
+
|
|
15
|
+
GLPI 11 sends most timestamps with the correct historical offset, but not
|
|
16
|
+
all of them. Measured against a live instance: 19 of the 20 datetime fields
|
|
17
|
+
across every resource are offset-bearing, and `KBArticle.revisions[].date`
|
|
18
|
+
is not. One response therefore carries both kinds, and comparing them raises
|
|
19
|
+
`TypeError: can't compare offset-naive and offset-aware datetimes` — sorting
|
|
20
|
+
an article's revision history against the article's own dates was enough to
|
|
21
|
+
trigger it.
|
|
22
|
+
|
|
23
|
+
There is deliberately **no default**. Every candidate is wrong somewhere:
|
|
24
|
+
against a Europe/Paris instance, assuming UTC shifts the affected timestamps
|
|
25
|
+
by one or two hours *and stops raising*, turning a loud failure into a quiet
|
|
26
|
+
wrong answer. An IANA name is preferred over a fixed offset because a name
|
|
27
|
+
follows DST — the same instance emits both `+01:00` and `+02:00`.
|
|
28
|
+
|
|
29
|
+
An offset already on the wire always wins over the configured zone, and a
|
|
30
|
+
model built outside the client (no validation context) keeps its naive values
|
|
31
|
+
rather than being stamped with a guess.
|
|
32
|
+
|
|
33
|
+
Adds `tzdata` as a dependency on Windows, which ships no system timezone
|
|
34
|
+
database; without it `zoneinfo` resolves on Linux CI and raises on a
|
|
35
|
+
developer machine.
|
|
36
|
+
|
|
37
|
+
- **Search endpoints now raise on a 4xx instead of returning `[]`.** The seven
|
|
38
|
+
`search_*` helpers passed no `failure_message` to `_resource_list`, which
|
|
39
|
+
skipped the status check entirely, so a 400, 401, 403 or 404 came back as an
|
|
40
|
+
empty list — indistinguishable from a filter that legitimately matched
|
|
41
|
+
nothing. (5xx already raised.) It composed badly with the batch iterators:
|
|
42
|
+
they stop on a page shorter than `batch_size`, so a 403 on the first page
|
|
43
|
+
ended the walk having yielded nothing and the caller saw a *successful*
|
|
44
|
+
empty result. This reverses decision D2 of the 0.4.0 error work, which chose
|
|
45
|
+
tolerance deliberately; the silent-empty failure mode has proved worse than
|
|
46
|
+
the exception. An empty list now means the server said the result set is
|
|
47
|
+
empty. **Callers that relied on `[]` after a permission error must catch
|
|
48
|
+
`GlpiStatusError`.**
|
|
49
|
+
|
|
50
|
+
### Added
|
|
51
|
+
|
|
52
|
+
- **`PatchTicket.status`** — ticket status is writable after all, typed
|
|
53
|
+
`GlpiTicketStatus | None`:
|
|
54
|
+
|
|
55
|
+
```python
|
|
56
|
+
client.update_ticket(ticket_id, PatchTicket(status=GlpiTicketStatus.PENDING))
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The field had been excluded on the grounds that GLPI "manages the ticket
|
|
60
|
+
lifecycle through dedicated timeline routes", which is what the contract
|
|
61
|
+
says: it publishes `Ticket.status.id` as `readOnly: true`. Measured against
|
|
62
|
+
a live GLPI 11 instance, that is wrong — `PATCH` with `{"status": 5}`
|
|
63
|
+
answers 200 and the ticket moves. Same failure mode as the `Major` priority
|
|
64
|
+
level the contract omits: observed server behaviour wins.
|
|
65
|
+
|
|
66
|
+
It is declared on `PatchTicket` and **not** on `PostTicket`, because the two
|
|
67
|
+
routes genuinely differ — `POST` with a status answers 201 and creates a
|
|
68
|
+
*New* ticket, dropping the field. A create argument for it would have done
|
|
69
|
+
nothing. `status_id` is likewise ignored; `status` is the spelling that
|
|
70
|
+
works.
|
|
71
|
+
|
|
72
|
+
The annotation is the enum rather than `int` deliberately. GLPI validates
|
|
73
|
+
nothing here: `{"status": 99}` answers 200 and stores it, after which the
|
|
74
|
+
API reports `{"id": 99, "name": "99"}`, the web form displays the ticket as
|
|
75
|
+
*New*, and the ticket vanishes from the ticket list while remaining open in
|
|
76
|
+
the database — only the history records the change. A typo like `55` for
|
|
77
|
+
`5` would lose a ticket in silence, so this model is the only validation on
|
|
78
|
+
the path. `GetTicket.status` stays the permissive `IdNameRef`, since a
|
|
79
|
+
strict enum on the read side would fail a whole search over one bad row.
|
|
80
|
+
|
|
81
|
+
- **`glpi_python_client.rsql`** — public date builders for the v2 filter
|
|
82
|
+
grammar: `created_between`, `date_window` and `changed_since`, all exported
|
|
83
|
+
from the package root. The end-of-day detail on a window's upper bound is
|
|
84
|
+
easy to get wrong and impossible to notice, since GLPI answers a malformed
|
|
85
|
+
filter by ignoring it and returning the whole table.
|
|
86
|
+
|
|
87
|
+
- **`find_user_by_email(email)`** — resolves a person by address. It scans,
|
|
88
|
+
because GLPI exposes addresses as the nested array `User.emails` and the v2
|
|
89
|
+
filter engine cannot join a nested array. Narrow it with `rsql_filter` and
|
|
90
|
+
cache the id; do not hand-roll an RSQL e-mail filter.
|
|
91
|
+
|
|
92
|
+
- **`stream_document_content(document_id, chunk_size=...)`** — yields a
|
|
93
|
+
document body in chunks instead of buffering it whole, as
|
|
94
|
+
`download_document_content` does. Upload still buffers.
|
|
95
|
+
|
|
96
|
+
- **Batch iterators for the four resources that lacked one**:
|
|
97
|
+
`iter_search_kb_articles`, `iter_search_kb_categories`,
|
|
98
|
+
`iter_search_documents` and `iter_search_locations`.
|
|
99
|
+
|
|
100
|
+
### Fixed
|
|
101
|
+
|
|
102
|
+
- **Every `datetime` write raised `TypeError`.** `model_to_payload` dumped in
|
|
103
|
+
Pydantic's python mode, so a request body reached `json.dumps` still holding
|
|
104
|
+
a live `datetime`. The failure landed at the encoder — after the model had
|
|
105
|
+
validated and outside any transport stub — which is why the suite never saw
|
|
106
|
+
it. The dump now runs in JSON mode.
|
|
107
|
+
|
|
108
|
+
- **GLPI discards the offset on every datetime it is sent, so aware values
|
|
109
|
+
were written as the wrong moment.** Measured against a live Europe/Paris
|
|
110
|
+
instance: `2026-08-01T12:30:00` written bare, as `...Z`, and with `+02:00`,
|
|
111
|
+
`+09:00`, `-08:00` and `+14:00` all store 12:30 Paris. The server reads the
|
|
112
|
+
naive prefix, interprets it in its own timezone, and throws the rest away —
|
|
113
|
+
with a 200. It does parse the offset first, since `+99:99` answers HTTP 500,
|
|
114
|
+
which is the worst combination: a malformed offset crashes, a well-formed
|
|
115
|
+
wrong one is silent. `12:30-08:00` is 21:30 in Paris and landed nine hours
|
|
116
|
+
early.
|
|
117
|
+
|
|
118
|
+
An aware `datetime` is now converted onto the server's clock and the offset
|
|
119
|
+
dropped, via a serialisation context mirroring the validation context used
|
|
120
|
+
on the inbound half. Naive values are untouched — they already mean the
|
|
121
|
+
server's clock — and no context means no conversion, so a model dumped
|
|
122
|
+
outside the client is unchanged. This is the second half of the
|
|
123
|
+
`server_timezone` contract above; `mode="json"` alone would have shipped
|
|
124
|
+
writes wrong by up to twelve hours.
|
|
125
|
+
|
|
126
|
+
- **`get_ticket_statistics` silently truncated at 200 tickets**, on an
|
|
127
|
+
instance whose own docstring records 59,690. It issued one `search_tickets`
|
|
128
|
+
call with no paging loop, so every statistic was computed over whichever
|
|
129
|
+
200 tickets came back first and reported as if it covered the corpus. The
|
|
130
|
+
two entity and one user name-resolution sites had the same shape. All four
|
|
131
|
+
now page through `iter_search_*`.
|
|
132
|
+
|
|
133
|
+
- **`from_transport` silently deleted text.** The HTML path was taken whenever
|
|
134
|
+
the content held both `<` and `>`, so `"use the <Enter> key"` became
|
|
135
|
+
`"use the key"` — an unknown tag's markup is dropped and its empty body
|
|
136
|
+
kept, removing the word with nothing left to show it was ever there.
|
|
137
|
+
`"cmd </dev/null > out"` and `"if x<y then z>0"` lost text the same way. The
|
|
138
|
+
decision is now made on the element *name*.
|
|
139
|
+
|
|
140
|
+
- **Fenced code blocks, tables and prose punctuation were mangled.** Without
|
|
141
|
+
the `fenced_code` extension a fence rendered as inline `<code>`, which the
|
|
142
|
+
GLPI web UI shows as one run-on line and which a later read wrote back as
|
|
143
|
+
inline code — so a pasted log degraded further on every edit. Without
|
|
144
|
+
`tables`, a table rendered as literal pipes. Inbound, `markdownify` escaped
|
|
145
|
+
underscores and asterisks, so `snake_case` came back as `snake\_case` and
|
|
146
|
+
accumulated a backslash on every read-modify-write cycle.
|
|
147
|
+
|
|
148
|
+
- **`_MAX_DATETIME` was naive, so `to_markdown()` raised on a mixed-awareness
|
|
149
|
+
timeline.** Sorting events padded absent timestamps with `datetime.max`,
|
|
150
|
+
which cannot be compared against the offset-bearing values GLPI sends. The
|
|
151
|
+
sort key now normalises both sides to UTC. The live probe confirmed the
|
|
152
|
+
mixed population is real, not hypothetical.
|
|
153
|
+
|
|
154
|
+
- **`require_response_int` rejected create responses GLPI actually returns.**
|
|
155
|
+
A numeric-string id, an id nested under a `data` envelope, and a create that
|
|
156
|
+
reports only a `Location` header all raised a protocol error over a
|
|
157
|
+
perfectly usable identifier. It now probes top-level keys, then the
|
|
158
|
+
envelope, then the header.
|
|
159
|
+
|
|
160
|
+
- **`sort="date_mod desc"` — the library's own documented example — is HTTP
|
|
161
|
+
400.** Found while running the wire-format probe. The accepted syntax is
|
|
162
|
+
`field:direction`; a bare `date_mod` is accepted but sorts *ascending*, and
|
|
163
|
+
`order=` is ignored entirely.
|
|
164
|
+
|
|
165
|
+
- **Three client construction examples had `server_timezone` inserted twice
|
|
166
|
+
and misindented**, by the sweep that added it. A repeated keyword argument
|
|
167
|
+
is a `SyntaxError`, so those examples could not be copied at all. Two older
|
|
168
|
+
documentation defects surfaced alongside them: three lines of expected
|
|
169
|
+
output stranded inside a `code-block:: python` (they belong to the example
|
|
170
|
+
above), and an import indented four spaces inside a three-space block. A new
|
|
171
|
+
test compiles all 77 Python snippets in the skills, the guide and the
|
|
172
|
+
README.
|
|
173
|
+
|
|
174
|
+
## 0.4.0 – 0.4.2
|
|
175
|
+
|
|
176
|
+
These three releases were tagged without the changelog ever being
|
|
177
|
+
sectioned, so their notes accumulated under a single `Unreleased`
|
|
178
|
+
heading. They are grouped here rather than split retroactively.
|
|
8
179
|
|
|
9
180
|
### Fixed
|
|
10
181
|
|
|
@@ -314,6 +485,12 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
|
|
|
314
485
|
- The `requests` intersphinx mapping is removed; it survived the transport
|
|
315
486
|
swap and made every docs build fetch an inventory nothing referenced.
|
|
316
487
|
|
|
488
|
+
## Pre-0.4.0 notes
|
|
489
|
+
|
|
490
|
+
Kept for history. Written before the httpx and unasync rewrites, so the
|
|
491
|
+
status they describe is superseded by everything above — the transport is
|
|
492
|
+
no longer `requests`, and tolerant searches no longer swallow a 4xx.
|
|
493
|
+
|
|
317
494
|
### Unchanged (deliberately)
|
|
318
495
|
|
|
319
496
|
- Retry semantics: 5xx retried 3 times with a 3-second fixed wait, 4xx never
|
|
@@ -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
|
-
Metadata-Version: 2.
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
2
|
Name: glpi-python-client
|
|
3
|
-
Version: 0.4.
|
|
3
|
+
Version: 0.4.3
|
|
4
4
|
Summary: A typed Python client for GLPI ITSM APIs.
|
|
5
5
|
Project-URL: Homepage, https://github.com/baraline/glpi_python_client
|
|
6
6
|
Project-URL: Documentation, https://glpi-python-client.readthedocs.io/en/latest/
|
|
@@ -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
|
|
@@ -32,6 +33,7 @@ Requires-Dist: pydantic>=2.8
|
|
|
32
33
|
Requires-Dist: sniffio>=1.3
|
|
33
34
|
Requires-Dist: tenacity>=8.2
|
|
34
35
|
Requires-Dist: typing-extensions>=4.7; python_version < '3.11'
|
|
36
|
+
Requires-Dist: tzdata>=2024.1; platform_system == 'Windows'
|
|
35
37
|
Provides-Extra: dev
|
|
36
38
|
Requires-Dist: build>=1.2; extra == 'dev'
|
|
37
39
|
Requires-Dist: mypy>=1.11; extra == 'dev'
|
|
@@ -113,6 +115,7 @@ from glpi_python_client import GlpiClient, PostTicket
|
|
|
113
115
|
|
|
114
116
|
with GlpiClient(
|
|
115
117
|
glpi_api_url="https://glpi.example.com/api.php/v2",
|
|
118
|
+
server_timezone="Europe/Paris",
|
|
116
119
|
client_id="oauth-client-id",
|
|
117
120
|
client_secret="oauth-client-secret",
|
|
118
121
|
username="api-user",
|
|
@@ -139,6 +142,7 @@ from glpi_python_client import AsyncGlpiClient, PostTicket
|
|
|
139
142
|
async def main() -> None:
|
|
140
143
|
async with AsyncGlpiClient(
|
|
141
144
|
glpi_api_url="https://glpi.example.com/api.php/v2",
|
|
145
|
+
server_timezone="Europe/Paris",
|
|
142
146
|
client_id="oauth-client-id",
|
|
143
147
|
client_secret="oauth-client-secret",
|
|
144
148
|
username="api-user",
|
|
@@ -57,6 +57,7 @@ from glpi_python_client import GlpiClient, PostTicket
|
|
|
57
57
|
|
|
58
58
|
with GlpiClient(
|
|
59
59
|
glpi_api_url="https://glpi.example.com/api.php/v2",
|
|
60
|
+
server_timezone="Europe/Paris",
|
|
60
61
|
client_id="oauth-client-id",
|
|
61
62
|
client_secret="oauth-client-secret",
|
|
62
63
|
username="api-user",
|
|
@@ -83,6 +84,7 @@ from glpi_python_client import AsyncGlpiClient, PostTicket
|
|
|
83
84
|
async def main() -> None:
|
|
84
85
|
async with AsyncGlpiClient(
|
|
85
86
|
glpi_api_url="https://glpi.example.com/api.php/v2",
|
|
87
|
+
server_timezone="Europe/Paris",
|
|
86
88
|
client_id="oauth-client-id",
|
|
87
89
|
client_secret="oauth-client-secret",
|
|
88
90
|
username="api-user",
|
|
@@ -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,
|
|
@@ -68,6 +68,7 @@ pair. The OAuth password grant accepts either ``client_id`` /
|
|
|
68
68
|
|
|
69
69
|
with GlpiClient(
|
|
70
70
|
glpi_api_url="https://glpi.example.com/api.php/v2",
|
|
71
|
+
server_timezone="Europe/Paris",
|
|
71
72
|
client_id="oauth-client-id",
|
|
72
73
|
client_secret="oauth-client-secret",
|
|
73
74
|
username="api-user",
|
|
@@ -92,6 +93,7 @@ The asynchronous client takes the same arguments and is used inside an
|
|
|
92
93
|
async def main() -> None:
|
|
93
94
|
async with AsyncGlpiClient(
|
|
94
95
|
glpi_api_url="https://glpi.example.com/api.php/v2",
|
|
96
|
+
server_timezone="Europe/Paris",
|
|
95
97
|
client_id="oauth-client-id",
|
|
96
98
|
client_secret="oauth-client-secret",
|
|
97
99
|
username="api-user",
|
|
@@ -134,6 +136,11 @@ build the client for you:
|
|
|
134
136
|
* ``GLPI_CLIENT_ID`` and ``GLPI_CLIENT_SECRET``
|
|
135
137
|
* ``GLPI_USERNAME`` and ``GLPI_PASSWORD``
|
|
136
138
|
* ``GLPI_ENTITY``, ``GLPI_PROFILE``, ``GLPI_ENTITY_RECURSIVE``
|
|
139
|
+
* ``GLPI_SERVER_TIMEZONE`` -- **required**. IANA name of the timezone the
|
|
140
|
+
GLPI server runs in (e.g. ``Europe/Paris``). GLPI does not advertise it,
|
|
141
|
+
and it is needed to interpret the timestamps the server sends without an
|
|
142
|
+
offset. There is no default: guessing UTC against a Europe/Paris instance
|
|
143
|
+
shifts those values by an hour or two and never raises.
|
|
137
144
|
* ``GLPI_LANGUAGE``, ``GLPI_VERIFY_SSL``
|
|
138
145
|
* ``GLPI_V1_BASE_URL``, ``GLPI_V1_USER_TOKEN``, ``GLPI_V1_APP_TOKEN``
|
|
139
146
|
|
|
@@ -628,6 +635,9 @@ so the client sets it through a legacy fallback — see
|
|
|
628
635
|
Example output::
|
|
629
636
|
|
|
630
637
|
['Networking']
|
|
638
|
+
42 Reset a Wi-Fi controller
|
|
639
|
+
42 Reset a Wi-Fi controller
|
|
640
|
+
1 revision(s)
|
|
631
641
|
|
|
632
642
|
Assigning categories
|
|
633
643
|
^^^^^^^^^^^^^^^^^^^^^
|
|
@@ -659,9 +669,6 @@ article's full category set; passing an empty list clears every category.
|
|
|
659
669
|
# Or set them explicitly at any time.
|
|
660
670
|
client.set_kb_article_categories(article_id, [14]) # replace the full set
|
|
661
671
|
client.set_kb_article_categories(article_id, []) # clear all
|
|
662
|
-
42 Reset a Wi-Fi controller
|
|
663
|
-
42 Reset a Wi-Fi controller
|
|
664
|
-
1 revision(s)
|
|
665
672
|
|
|
666
673
|
Enums
|
|
667
674
|
~~~~~
|
|
@@ -710,10 +717,11 @@ internal container and field names:
|
|
|
710
717
|
|
|
711
718
|
.. code-block:: python
|
|
712
719
|
|
|
713
|
-
|
|
720
|
+
from glpi_python_client import GlpiClient
|
|
714
721
|
|
|
715
722
|
with GlpiClient(
|
|
716
723
|
glpi_api_url="https://glpi.example.com/api.php/v2",
|
|
724
|
+
server_timezone="Europe/Paris",
|
|
717
725
|
client_id="oauth-client-id",
|
|
718
726
|
client_secret="oauth-client-secret",
|
|
719
727
|
username="api-user",
|
|
@@ -1223,8 +1231,10 @@ Example 4 — Close a ticket with a solution
|
|
|
1223
1231
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1224
1232
|
|
|
1225
1233
|
GLPI moves a ticket to the *Solved* status as soon as a solution is
|
|
1226
|
-
posted, so
|
|
1227
|
-
|
|
1234
|
+
posted, so this both records *how* the ticket was resolved and advances
|
|
1235
|
+
its lifecycle in one call. Prefer it over setting the status directly
|
|
1236
|
+
whenever there is a resolution to record — a status moved on its own
|
|
1237
|
+
leaves no trace of why.
|
|
1228
1238
|
|
|
1229
1239
|
.. code-block:: python
|
|
1230
1240
|
|
|
@@ -1248,6 +1258,34 @@ Expected Markdown (abridged)::
|
|
|
1248
1258
|
|
|
1249
1259
|
Replaced the access point firmware.
|
|
1250
1260
|
|
|
1261
|
+
For the transitions that have no timeline record behind them — putting a
|
|
1262
|
+
ticket on hold, planning it, taking it back from *Solved* — set the
|
|
1263
|
+
status directly. ``PatchTicket`` carries a ``status`` field and
|
|
1264
|
+
``update_ticket`` sends it:
|
|
1265
|
+
|
|
1266
|
+
.. code-block:: python
|
|
1267
|
+
|
|
1268
|
+
from glpi_python_client import GlpiTicketStatus, PatchTicket
|
|
1269
|
+
|
|
1270
|
+
client.update_ticket(ticket_id, PatchTicket(status=GlpiTicketStatus.PENDING))
|
|
1271
|
+
|
|
1272
|
+
The field lives on ``PatchTicket`` and deliberately not on
|
|
1273
|
+
``PostTicket``: GLPI ignores a status sent at creation time and the new
|
|
1274
|
+
ticket comes back as *New*, so a create argument for it would do
|
|
1275
|
+
nothing.
|
|
1276
|
+
|
|
1277
|
+
.. warning::
|
|
1278
|
+
|
|
1279
|
+
``status`` is typed as :class:`glpi_python_client.GlpiTicketStatus`
|
|
1280
|
+
and rejects anything outside it, because the server accepts
|
|
1281
|
+
everything and its interface hides the result. Writing ``99``
|
|
1282
|
+
answers ``200`` and stores it; the API then reports
|
|
1283
|
+
``{"id": 99, "name": "99"}``, the web form displays the ticket as
|
|
1284
|
+
*New*, and the ticket disappears from the ticket list while staying
|
|
1285
|
+
open in the database — only the history records the change. A typo
|
|
1286
|
+
such as ``55`` for ``5`` would lose a ticket with no error anywhere,
|
|
1287
|
+
which is why the enum refuses it here.
|
|
1288
|
+
|
|
1251
1289
|
Example 5 — Upload a document to an existing ticket
|
|
1252
1290
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
|
1253
1291
|
|
|
@@ -103,8 +103,13 @@ from glpi_python_client.models import (
|
|
|
103
103
|
PostUser,
|
|
104
104
|
TicketMarkdownOptions,
|
|
105
105
|
)
|
|
106
|
+
from glpi_python_client.rsql import (
|
|
107
|
+
changed_since,
|
|
108
|
+
created_between,
|
|
109
|
+
date_window,
|
|
110
|
+
)
|
|
106
111
|
|
|
107
|
-
__version__ = "0.4.
|
|
112
|
+
__version__ = "0.4.3"
|
|
108
113
|
|
|
109
114
|
__all__ = [
|
|
110
115
|
"AsyncGlpiClient",
|
|
@@ -190,4 +195,7 @@ __all__ = [
|
|
|
190
195
|
"PostUser",
|
|
191
196
|
"TicketMarkdownOptions",
|
|
192
197
|
"__version__",
|
|
198
|
+
"changed_since",
|
|
199
|
+
"created_between",
|
|
200
|
+
"date_window",
|
|
193
201
|
]
|
|
@@ -25,10 +25,12 @@ from glpi_python_client._async._concurrency import Lock
|
|
|
25
25
|
from glpi_python_client._async.clients.commons._config import (
|
|
26
26
|
build_client_env_config,
|
|
27
27
|
build_client_resources,
|
|
28
|
+
resolve_server_timezone,
|
|
28
29
|
)
|
|
29
30
|
|
|
30
31
|
if TYPE_CHECKING:
|
|
31
32
|
from collections.abc import Mapping
|
|
33
|
+
from datetime import tzinfo
|
|
32
34
|
|
|
33
35
|
logger = logging.getLogger(__name__)
|
|
34
36
|
|
|
@@ -45,6 +47,7 @@ class _BaseGlpiClient:
|
|
|
45
47
|
self,
|
|
46
48
|
*,
|
|
47
49
|
glpi_api_url: str,
|
|
50
|
+
server_timezone: str | tzinfo,
|
|
48
51
|
client_id: str | None = None,
|
|
49
52
|
client_secret: str | None = None,
|
|
50
53
|
username: str | None = None,
|
|
@@ -66,6 +69,25 @@ class _BaseGlpiClient:
|
|
|
66
69
|
glpi_api_url : str
|
|
67
70
|
Base URL of the GLPI v2 REST API, e.g.
|
|
68
71
|
``https://glpi.example.com/api.php/v2``.
|
|
72
|
+
server_timezone : str | tzinfo
|
|
73
|
+
IANA name of the timezone the GLPI server runs in (e.g.
|
|
74
|
+
``"Europe/Paris"``), or a ``tzinfo``. **Required**: GLPI does
|
|
75
|
+
not advertise it, and it governs both directions of every
|
|
76
|
+
timestamp the client exchanges.
|
|
77
|
+
|
|
78
|
+
Reading, it interprets the timestamps the server sends without
|
|
79
|
+
an offset. Writing, it is what makes an aware ``datetime``
|
|
80
|
+
arrive as the moment it names: GLPI reads the naive prefix of a
|
|
81
|
+
timestamp and discards the offset, so the value has to be
|
|
82
|
+
converted onto the server's clock before it is sent. Measured on
|
|
83
|
+
a live instance, offsets from ``-08:00`` to ``+14:00`` written
|
|
84
|
+
to one field all stored the same wall clock.
|
|
85
|
+
|
|
86
|
+
There is no default because every candidate is wrong somewhere
|
|
87
|
+
-- guessing UTC against a Europe/Paris instance shifts those
|
|
88
|
+
timestamps by an hour or two and never raises. Prefer a name
|
|
89
|
+
over a fixed offset: a name follows DST, and one instance emits
|
|
90
|
+
both ``+01:00`` and ``+02:00``.
|
|
69
91
|
client_id : str | None, optional
|
|
70
92
|
OAuth client identifier used to obtain access tokens.
|
|
71
93
|
client_secret : str | None, optional
|
|
@@ -103,6 +125,7 @@ class _BaseGlpiClient:
|
|
|
103
125
|
missing OAuth credentials together with no v1 fallback).
|
|
104
126
|
"""
|
|
105
127
|
|
|
128
|
+
self.server_timezone = resolve_server_timezone(server_timezone)
|
|
106
129
|
resources = build_client_resources(
|
|
107
130
|
glpi_api_url=glpi_api_url,
|
|
108
131
|
client_name=type(self).__name__,
|
|
@@ -142,7 +165,8 @@ class _BaseGlpiClient:
|
|
|
142
165
|
``GLPI_USERNAME``, ``GLPI_PASSWORD``, ``GLPI_VERIFY_SSL``,
|
|
143
166
|
``GLPI_V1_BASE_URL``, ``GLPI_V1_USER_TOKEN``, ``GLPI_V1_APP_TOKEN``,
|
|
144
167
|
``GLPI_ENTITY``, ``GLPI_PROFILE``, ``GLPI_ENTITY_RECURSIVE``,
|
|
145
|
-
``GLPI_LANGUAGE``, ``GLPI_AUTH_TOKEN_REFRESH
|
|
168
|
+
``GLPI_LANGUAGE``, ``GLPI_AUTH_TOKEN_REFRESH``,
|
|
169
|
+
``GLPI_SERVER_TIMEZONE``).
|
|
146
170
|
|
|
147
171
|
Parameters
|
|
148
172
|
----------
|
|
@@ -12,6 +12,7 @@ from collections.abc import AsyncIterator
|
|
|
12
12
|
|
|
13
13
|
from glpi_python_client._async.clients.commons._constants import USER_ENDPOINT, GlpiId
|
|
14
14
|
from glpi_python_client._async.clients.commons._transport import TransportMixin
|
|
15
|
+
from glpi_python_client._errors import GlpiValidationError
|
|
15
16
|
from glpi_python_client.models.api_schema.administration._user import (
|
|
16
17
|
DeleteUser,
|
|
17
18
|
GetUser,
|
|
@@ -110,6 +111,81 @@ class UserMixin(TransportMixin):
|
|
|
110
111
|
break
|
|
111
112
|
start += batch_size
|
|
112
113
|
|
|
114
|
+
async def find_user_by_email(
|
|
115
|
+
self,
|
|
116
|
+
email: str,
|
|
117
|
+
*,
|
|
118
|
+
rsql_filter: str = "",
|
|
119
|
+
batch_size: int = 100,
|
|
120
|
+
skip_entity: bool = True,
|
|
121
|
+
) -> GetUser | None:
|
|
122
|
+
"""Return the first user holding ``email``, or ``None``.
|
|
123
|
+
|
|
124
|
+
**This scans.** GLPI exposes e-mail addresses as ``User.emails``, a
|
|
125
|
+
nested *array*, and the v2 filter engine cannot join a nested array
|
|
126
|
+
-- the structurally identical ``Ticket.team`` answers HTTP 500 for
|
|
127
|
+
its declared subfields and is silently ignored for every other
|
|
128
|
+
spelling. So there is no server-side e-mail filter to use, and the
|
|
129
|
+
addresses have to be compared client-side.
|
|
130
|
+
|
|
131
|
+
Nothing about that is cheap: the scan costs one request per
|
|
132
|
+
``batch_size`` users until it matches, so it is meant for
|
|
133
|
+
occasional resolution, not for a per-request lookup. Narrow it with
|
|
134
|
+
``rsql_filter`` when you can (``"is_active==true"`` is the usual
|
|
135
|
+
one), and cache the resulting id rather than calling this again.
|
|
136
|
+
|
|
137
|
+
A server-side fast path is deliberately *not* attempted. GLPI v2
|
|
138
|
+
ignores a filter field it does not recognise and answers with the
|
|
139
|
+
whole unfiltered table, so a guessed e-mail filter would not fail
|
|
140
|
+
-- it would return a plausible non-empty page whose first row is
|
|
141
|
+
the wrong person. Guessing is the one thing this helper exists to
|
|
142
|
+
stop each caller doing separately.
|
|
143
|
+
|
|
144
|
+
Parameters
|
|
145
|
+
----------
|
|
146
|
+
email : str
|
|
147
|
+
Address to look for. Compared case-insensitively after
|
|
148
|
+
trimming surrounding whitespace, the way mail systems treat it.
|
|
149
|
+
rsql_filter : str, optional
|
|
150
|
+
Raw RSQL filter narrowing the population scanned. Empty by
|
|
151
|
+
default, which scans every visible user.
|
|
152
|
+
batch_size : int, optional
|
|
153
|
+
Users fetched per request while scanning (defaults to 100).
|
|
154
|
+
skip_entity : bool, optional
|
|
155
|
+
When ``True`` (the default) the ``GLPI-Entity`` header is
|
|
156
|
+
omitted so the scan spans every entity the caller can see. A
|
|
157
|
+
user whose account lives outside the client's configured entity
|
|
158
|
+
is invisible otherwise, and the helper would answer ``None``
|
|
159
|
+
for somebody who exists.
|
|
160
|
+
|
|
161
|
+
Returns
|
|
162
|
+
-------
|
|
163
|
+
GetUser | None
|
|
164
|
+
The first user with a matching address, or ``None`` when the
|
|
165
|
+
scanned population holds none.
|
|
166
|
+
|
|
167
|
+
Raises
|
|
168
|
+
------
|
|
169
|
+
GlpiValidationError
|
|
170
|
+
If ``email`` is blank -- which would otherwise scan the whole
|
|
171
|
+
directory and match nothing.
|
|
172
|
+
"""
|
|
173
|
+
|
|
174
|
+
needle = email.strip().casefold()
|
|
175
|
+
if not needle:
|
|
176
|
+
raise GlpiValidationError("find_user_by_email requires a non-empty address")
|
|
177
|
+
|
|
178
|
+
async for batch in self.iter_search_users(
|
|
179
|
+
rsql_filter,
|
|
180
|
+
batch_size=batch_size,
|
|
181
|
+
skip_entity=skip_entity,
|
|
182
|
+
):
|
|
183
|
+
for user in batch:
|
|
184
|
+
for entry in user.emails or ():
|
|
185
|
+
if entry.email and entry.email.strip().casefold() == needle:
|
|
186
|
+
return user
|
|
187
|
+
return None
|
|
188
|
+
|
|
113
189
|
async def get_user(self, user_id: GlpiId) -> GetUser:
|
|
114
190
|
"""Fetch one GLPI user by identifier.
|
|
115
191
|
|
|
@@ -15,7 +15,6 @@ from glpi_python_client._async.clients.commons._constants import (
|
|
|
15
15
|
GlpiId,
|
|
16
16
|
)
|
|
17
17
|
from glpi_python_client._async.clients.commons._http import ensure_response_status
|
|
18
|
-
from glpi_python_client._async.clients.commons._payloads import model_to_payload
|
|
19
18
|
from glpi_python_client._async.clients.commons._transport import TransportMixin
|
|
20
19
|
from glpi_python_client.models.api_schema.assistance._team import (
|
|
21
20
|
GetTeamMember,
|
|
@@ -79,7 +78,7 @@ class TeamMemberMixin(TransportMixin):
|
|
|
79
78
|
"""
|
|
80
79
|
|
|
81
80
|
endpoint = f"{TICKET_ENDPOINT}/{ticket_id}/{TEAM_MEMBER_SUFFIX}"
|
|
82
|
-
response = await self._post_request(endpoint,
|
|
81
|
+
response = await self._post_request(endpoint, self._body(member))
|
|
83
82
|
ensure_response_status(
|
|
84
83
|
response,
|
|
85
84
|
success_statuses=(200, 201),
|
|
@@ -114,7 +113,7 @@ class TeamMemberMixin(TransportMixin):
|
|
|
114
113
|
f"{TICKET_ENDPOINT}/{ticket_id}/{TEAM_MEMBER_SUFFIX}",
|
|
115
114
|
failure_message=f"Failed to remove team member on ticket {ticket_id}",
|
|
116
115
|
log_message=f"GLPI API removed team member on ticket {ticket_id}",
|
|
117
|
-
body=
|
|
116
|
+
body=self._body(member),
|
|
118
117
|
)
|
|
119
118
|
|
|
120
119
|
|
|
@@ -49,7 +49,11 @@ class TicketMixin(TransportMixin):
|
|
|
49
49
|
start : int, optional
|
|
50
50
|
Zero-based offset of the first record returned.
|
|
51
51
|
sort : str | None, optional
|
|
52
|
-
``sort`` query parameter
|
|
52
|
+
``sort`` query parameter, spelled ``field`` or ``field:direction``
|
|
53
|
+
(e.g. ``"date_mod:desc"``). Measured against GLPI 11: a space
|
|
54
|
+
before the direction answers **HTTP 400** ("Invalid property for
|
|
55
|
+
sorting"), a bare ``field`` sorts *ascending*, and a separate
|
|
56
|
+
``order`` parameter is ignored.
|
|
53
57
|
fields : tuple[str, ...], optional
|
|
54
58
|
Restricted set of contract field names to request. Empty
|
|
55
59
|
tuple lets the GLPI server pick its default field set.
|
|
@@ -94,7 +98,9 @@ class TicketMixin(TransportMixin):
|
|
|
94
98
|
the ``limit`` parameter on each underlying
|
|
95
99
|
:meth:`search_tickets` call.
|
|
96
100
|
sort : str | None, optional
|
|
97
|
-
``sort`` query parameter forwarded
|
|
101
|
+
``sort`` query parameter forwarded to each page request,
|
|
102
|
+
spelled ``field`` or ``field:direction`` (e.g. ``"date_mod:desc"``).
|
|
103
|
+
A space before the direction answers HTTP 400.
|
|
98
104
|
fields : tuple[str, ...], optional
|
|
99
105
|
Restricted set of contract field names to request.
|
|
100
106
|
|
|
@@ -7,6 +7,8 @@ models.
|
|
|
7
7
|
|
|
8
8
|
from __future__ import annotations
|
|
9
9
|
|
|
10
|
+
from collections.abc import AsyncIterator
|
|
11
|
+
|
|
10
12
|
from glpi_python_client._async.clients.commons._constants import (
|
|
11
13
|
LOCATION_ENDPOINT,
|
|
12
14
|
GlpiId,
|
|
@@ -52,6 +54,48 @@ class LocationMixin(TransportMixin):
|
|
|
52
54
|
params["filter"] = rsql_filter
|
|
53
55
|
return await self._resource_list(LOCATION_ENDPOINT, GetLocation, params=params)
|
|
54
56
|
|
|
57
|
+
async def iter_search_locations(
|
|
58
|
+
self,
|
|
59
|
+
rsql_filter: str = "",
|
|
60
|
+
*,
|
|
61
|
+
batch_size: int = 50,
|
|
62
|
+
) -> AsyncIterator[list[GetLocation]]:
|
|
63
|
+
"""Yield successive pages of GLPI locations until exhausted.
|
|
64
|
+
|
|
65
|
+
The generator drives pagination automatically by advancing the
|
|
66
|
+
``start`` offset after each batch. Iteration stops when the server
|
|
67
|
+
returns fewer items than ``batch_size``, which signals the last page.
|
|
68
|
+
|
|
69
|
+
Parameters
|
|
70
|
+
----------
|
|
71
|
+
rsql_filter : str, optional
|
|
72
|
+
Raw RSQL filter forwarded as the ``filter`` query parameter.
|
|
73
|
+
Empty by default, which lists every visible record.
|
|
74
|
+
batch_size : int, optional
|
|
75
|
+
Number of records requested per page (default 50). Acts as the
|
|
76
|
+
``limit`` parameter on each underlying :meth:`search_locations`
|
|
77
|
+
call.
|
|
78
|
+
|
|
79
|
+
Yields
|
|
80
|
+
------
|
|
81
|
+
list[GetLocation]
|
|
82
|
+
One page per iteration. The last yielded batch may be shorter
|
|
83
|
+
than ``batch_size``.
|
|
84
|
+
"""
|
|
85
|
+
|
|
86
|
+
start = 0
|
|
87
|
+
while True:
|
|
88
|
+
batch = await self.search_locations(
|
|
89
|
+
rsql_filter,
|
|
90
|
+
limit=batch_size,
|
|
91
|
+
start=start,
|
|
92
|
+
)
|
|
93
|
+
if batch:
|
|
94
|
+
yield batch
|
|
95
|
+
if len(batch) < batch_size:
|
|
96
|
+
break
|
|
97
|
+
start += batch_size
|
|
98
|
+
|
|
55
99
|
async def get_location(self, location_id: GlpiId) -> GetLocation:
|
|
56
100
|
"""Fetch one GLPI location by identifier.
|
|
57
101
|
|