Skip to content

Commit fe060c7

Browse files
committed
bugfix for Document endpoints and stats date filters, add integration tests
1 parent 598ca3d commit fe060c7

15 files changed

Lines changed: 1371 additions & 78 deletions

File tree

.github/workflows/release.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ jobs:
3838
python -m pip install -e .[dev]
3939
4040
- name: Run tests
41-
run: python -m pytest
41+
run: python -m pytest -m "not integration"
4242

4343
quality:
4444
name: Release quality checks

.gitignore

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,6 @@ dist/
2626
secrets/
2727
secrets/*
2828

29-
integration_tests/
30-
integration_tests/*
29+
.claude/
30+
31+
CLAUDE.md

docs/development_rtd.rst

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,84 @@ Run the focused checks before opening a pull request:
3131
python -m mypy glpi_python_client
3232
python -m sphinx -b html docs docs/_build/html
3333
34+
Integration Tests
35+
-----------------
36+
37+
The ``integration_tests/`` directory holds end-to-end tests that drive a live
38+
GLPI instance through the synchronous and asynchronous clients. They are
39+
collected only when secrets resolve to a reachable instance; otherwise each
40+
test self-skips via ``pytest.skip``. Every test cleans up the records it
41+
creates in a ``finally`` block, but it is still recommended to point them at
42+
a non-production GLPI.
43+
44+
Markers and CI behaviour
45+
~~~~~~~~~~~~~~~~~~~~~~~~
46+
47+
All integration tests are tagged with the ``integration`` pytest marker
48+
declared in ``pyproject.toml``. The ``ci.yml`` workflow runs
49+
``pytest -m "not integration"`` for both the test matrix and the coverage
50+
job, so the public CI never reaches a live GLPI. The default local
51+
``python -m pytest`` invocation *does* attempt to collect them, but they
52+
skip automatically when no secrets are configured.
53+
54+
To explicitly opt in or out locally:
55+
56+
.. code-block:: console
57+
58+
python -m pytest -m integration # run only the live suite
59+
python -m pytest -m "not integration" # mirror the CI behaviour
60+
61+
Configuration
62+
~~~~~~~~~~~~~
63+
64+
The suite reads each value from a file named after the secret under
65+
``secrets/`` at the repository root, falling back to the matching
66+
environment variable when the file is absent. The ``secrets/`` directory is
67+
gitignored. Each file contains a single trimmed value.
68+
69+
Required:
70+
71+
============================== ========================= ===========================================
72+
Secret file Environment variable Purpose
73+
============================== ========================= ===========================================
74+
``glpi_api_url`` ``GLPI_API_URL`` Base URL of the GLPI v2 API.
75+
``glpi_client_id_test`` ``GLPI_CLIENT_ID`` OAuth2 client identifier.
76+
``glpi_client_secret_test`` ``GLPI_CLIENT_SECRET`` OAuth2 client secret.
77+
``glpi_username`` ``GLPI_USERNAME`` GLPI user for the password grant.
78+
``glpi_password`` ``GLPI_PASSWORD`` Password for the GLPI user above.
79+
============================== ========================= ===========================================
80+
81+
Optional:
82+
83+
============================== ============================= ============================================
84+
Secret file Environment variable Purpose
85+
============================== ============================= ============================================
86+
``glpi_verify_ssl`` ``GLPI_VERIFY_SSL`` Toggle TLS verification (default ``false``).
87+
``glpi_entity`` ``GLPI_ENTITY`` Active entity id sent on every request.
88+
``glpi_profile`` ``GLPI_PROFILE`` Active profile id sent on every request.
89+
``glpi_entity_recursive`` ``GLPI_ENTITY_RECURSIVE`` Include sub-entities (default ``false``).
90+
``glpi_api_v1_url`` ``GLPI_API_V1_URL`` Base URL of the legacy v1 API.
91+
``glpi_api_v1_token_user`` ``GLPI_V1_USER_TOKEN`` v1 user token (enables document uploads).
92+
``glpi_api_v1_app_token`` ``GLPI_V1_APP_TOKEN`` v1 application token paired with the above.
93+
``glpi_team_member_role`` ``GLPI_TEAM_MEMBER_ROLE`` Role used to add the test user to a ticket.
94+
============================== ============================= ============================================
95+
96+
The v1 secrets are only required for the document-upload test; when they
97+
are missing that single test skips while the rest of the suite runs.
98+
99+
Running the suite
100+
~~~~~~~~~~~~~~~~~
101+
102+
Once secrets are in place:
103+
104+
.. code-block:: console
105+
106+
python -m pytest integration_tests -m integration
107+
108+
Use a disposable GLPI instance or one whose entity is dedicated to
109+
automated tests. The suite creates and deletes users, locations, tickets,
110+
followups, tasks, and solutions on every run.
111+
34112
Package Layout
35113
--------------
36114

docs/user_guide.rst

Lines changed: 15 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -776,6 +776,14 @@ batches until the API returns fewer rows than the requested
776776
print(ticket.id, ticket.name)
777777
print(f"processed {total} tickets")
778778
779+
.. note::
780+
781+
Always pass an RSQL filter to ``iter_search_tickets``. Querying
782+
without any filter can return very large result sets and may cause
783+
the GLPI server to return a 500 error on busy instances. The other
784+
two generators (``iter_search_users``, ``iter_search_entities``) are
785+
not affected because those collections are typically much smaller.
786+
779787
On the asynchronous client the same helpers are exposed as **async
780788
generators** through the bridge, so each ``next()`` call runs off the
781789
event loop and the consumer uses ``async for``:
@@ -790,8 +798,10 @@ event loop and the consumer uses ``async for``:
790798
^^^^^^^^^^^^^^^^^^^^^^^^^
791799

792800
Counts tickets created within an ISO date window and groups them by
793-
entity, status, priority, and type. Optional filters restrict the
794-
result set on the server side:
801+
entity, status, priority, and type. The ``start_date`` is inclusive
802+
from 00:00:00 and the ``end_date`` is inclusive through 23:59:59, so
803+
tickets created at any time on those days are counted. Optional
804+
filters restrict the result set on the server side:
795805

796806
* ``entity_id`` — restrict to a single entity by numeric identifier.
797807
* ``entity_name`` — substring match against the entity ``name`` column;
@@ -893,7 +903,9 @@ then computes per-user and per-entity totals.
893903
Available filters:
894904

895905
* ``start_date`` / ``end_date`` / ``default_days`` — ISO ``YYYY-MM-DD``
896-
date window; ``default_days`` is used when ``start_date`` is omitted.
906+
date window; ``start_date`` is inclusive from 00:00:00,
907+
``end_date`` is inclusive through 23:59:59, and ``default_days``
908+
is used when ``start_date`` is omitted.
897909
* ``entity_id`` — restrict to a single entity by identifier.
898910
* ``entity_name`` — substring match resolved through ``search_entities``;
899911
ignored when ``entity_id`` is given.

glpi_python_client/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@
7272
TicketMarkdownOptions,
7373
)
7474

75-
__version__ = "0.3.2"
75+
__version__ = "0.3.3"
7676

7777
__all__ = [
7878
"AsyncGlpiClient",

glpi_python_client/clients/api/assistance/timeline/_document.py

Lines changed: 21 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,12 @@
77
-----
88
The live GLPI v2 server returns each entry of the list endpoint wrapped
99
in a ``{"type": "Document_Item", "item": {...}}`` envelope, even though
10-
the OpenAPI contract documents a flat array of ``Document_Item``. Real
11-
behaviour wins over the contract, so :func:`list_ticket_timeline_documents`
12-
unwraps the envelope through the shared
13-
:meth:`~glpi_python_client.clients.commons._transport.TransportMixin._resource_list`
14-
helper and tolerates both shapes.
10+
the OpenAPI contract documents a flat array of ``Document_Item``. The
11+
``item`` value is a full ``Document`` record (matching :class:`GetDocument`),
12+
not a ``Document_Item`` link record — real behaviour wins over the contract.
13+
:func:`list_ticket_timeline_documents` unwraps the envelope through the shared
14+
``TransportMixin._resource_list`` helper and deserialises each inner object
15+
as :class:`GetDocument`.
1516
"""
1617

1718
from __future__ import annotations
@@ -24,19 +25,17 @@
2425
from glpi_python_client.clients.commons._transport import TransportMixin
2526
from glpi_python_client.models.api_schema.assistance.timeline._document import (
2627
DeleteTimelineDocument,
27-
GetTimelineDocument,
2828
PatchTimelineDocument,
2929
PostTimelineDocument,
3030
)
31+
from glpi_python_client.models.api_schema.management._document import GetDocument
3132

3233

3334
class TimelineDocumentMixin(TransportMixin):
3435
"""Synchronous CRUD helpers for the ticket document timeline endpoint."""
3536

36-
def list_ticket_timeline_documents(
37-
self, ticket_id: GlpiId
38-
) -> list[GetTimelineDocument]:
39-
"""List all timeline documents linked to one ticket.
37+
def list_ticket_timeline_documents(self, ticket_id: GlpiId) -> list[GetDocument]:
38+
"""List all documents linked to one ticket timeline.
4039
4140
Parameters
4241
----------
@@ -45,14 +44,16 @@ def list_ticket_timeline_documents(
4544
4645
Returns
4746
-------
48-
list[GetTimelineDocument]
49-
Document links returned by the GLPI server, with the timeline
50-
envelope unwrapped where present.
47+
list[GetDocument]
48+
Document records returned by the GLPI server. The live API
49+
wraps each entry in a ``{"type": "Document_Item", "item": {...}}``
50+
envelope whose ``item`` value is a full ``Document`` record; the
51+
envelope is unwrapped automatically.
5152
"""
5253

5354
return self._resource_list(
5455
f"{TICKET_ENDPOINT}/{ticket_id}/{TIMELINE_DOCUMENT_SUFFIX}",
55-
GetTimelineDocument,
56+
GetDocument,
5657
failure_message=(
5758
f"Failed to list timeline documents for ticket {ticket_id}"
5859
),
@@ -61,20 +62,20 @@ def list_ticket_timeline_documents(
6162

6263
def get_ticket_timeline_document(
6364
self, ticket_id: GlpiId, document_link_id: GlpiId
64-
) -> GetTimelineDocument:
65-
"""Fetch one timeline document link by identifier.
65+
) -> GetDocument:
66+
"""Fetch one document linked to the ticket timeline by its document ID.
6667
6768
Parameters
6869
----------
6970
ticket_id : GlpiId
7071
Numeric identifier of the parent ticket.
7172
document_link_id : GlpiId
72-
Numeric identifier of the timeline document link to retrieve.
73+
Numeric identifier of the linked document to retrieve.
7374
7475
Returns
7576
-------
76-
GetTimelineDocument
77-
Validated document-link payload.
77+
GetDocument
78+
Validated document payload.
7879
7980
Raises
8081
------
@@ -85,7 +86,7 @@ def get_ticket_timeline_document(
8586
return self._resource_get(
8687
f"{TICKET_ENDPOINT}/{ticket_id}/"
8788
f"{TIMELINE_DOCUMENT_SUFFIX}/{document_link_id}",
88-
GetTimelineDocument,
89+
GetDocument,
8990
failure_message=(
9091
f"Failed to get timeline document {document_link_id} on "
9192
f"ticket {ticket_id}"

glpi_python_client/clients/custom/_statistics.py

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -95,10 +95,12 @@ def get_ticket_statistics(
9595
Parameters
9696
----------
9797
start_date : str | None, optional
98-
ISO ``YYYY-MM-DD`` start of the window. Defaults to
99-
``end_date - default_days + 1`` when omitted.
98+
ISO ``YYYY-MM-DD`` start of the window (inclusive from
99+
00:00:00). Defaults to ``end_date - default_days + 1``
100+
when omitted.
100101
end_date : str | None, optional
101-
ISO ``YYYY-MM-DD`` end of the window. Defaults to today.
102+
ISO ``YYYY-MM-DD`` end of the window (inclusive through
103+
23:59:59). Defaults to today.
102104
default_days : int, optional
103105
Span in days used when ``start_date`` is omitted (defaults
104106
to 30 and must be a positive integer).
@@ -147,9 +149,10 @@ def get_ticket_statistics(
147149
entity_filter = rsql_any_filter(
148150
*(f"entities_id=={e.id}" for e in entities if e.id is not None)
149151
)
150-
152+
date_filter = f"date_creation=ge={start.isoformat()};"
153+
date_filter += f"date_creation=le={end.isoformat()} 23:59:59"
151154
query = rsql_all_filter(
152-
f"date_creation=ge={start.isoformat()};date_creation=le={end.isoformat()}",
155+
date_filter,
153156
entity_filter,
154157
extra_filter,
155158
)
@@ -226,10 +229,12 @@ def get_task_durations(
226229
Parameters
227230
----------
228231
start_date : str | None, optional
229-
ISO ``YYYY-MM-DD`` start of the window. Defaults to
230-
``end_date - default_days + 1`` when omitted.
232+
ISO ``YYYY-MM-DD`` start of the window (inclusive from
233+
00:00:00). Defaults to ``end_date - default_days + 1``
234+
when omitted.
231235
end_date : str | None, optional
232-
ISO ``YYYY-MM-DD`` end of the window. Defaults to today.
236+
ISO ``YYYY-MM-DD`` end of the window (inclusive through
237+
23:59:59). Defaults to today.
233238
default_days : int, optional
234239
Span in days used when ``start_date`` is omitted (defaults
235240
to 30 and must be a positive integer).
@@ -269,9 +274,8 @@ def get_task_durations(
269274
end_date=end_date,
270275
default_days=default_days,
271276
)
272-
date_filter = (
273-
f"date_creation=ge={start.isoformat()};date_creation=le={end.isoformat()}"
274-
)
277+
date_filter = f"date_creation=ge={start.isoformat()};"
278+
date_filter += f"date_creation=le={end.isoformat()} 23:59:59"
275279

276280
entity_filter: str | None = None
277281
if entity_id is not None:
@@ -399,9 +403,11 @@ def get_user_activity(
399403
firstname : str | None, optional
400404
Filter by given name (substring match).
401405
start_date : str | None, optional
402-
ISO ``YYYY-MM-DD`` start of the activity window.
406+
ISO ``YYYY-MM-DD`` start of the activity window (inclusive
407+
from 00:00:00).
403408
end_date : str | None, optional
404-
ISO ``YYYY-MM-DD`` end of the activity window. Defaults to today.
409+
ISO ``YYYY-MM-DD`` end of the activity window (inclusive
410+
through 23:59:59). Defaults to today.
405411
default_days : int, optional
406412
Span in days used when ``start_date`` is omitted (default 30).
407413
@@ -460,9 +466,8 @@ def get_user_activity(
460466
if u.id is not None
461467
}
462468

463-
date_range = (
464-
f"date_creation=ge={start.isoformat()};date_creation=le={end.isoformat()}"
465-
)
469+
date_range = f"date_creation=ge={start.isoformat()};"
470+
date_range += f"date_creation=le={end.isoformat()} 23:59:59"
466471

467472
users_output: dict[str, UserActivityEntry] = {}
468473
for uid in resolved_user_ids:

0 commit comments

Comments
 (0)