Skip to content

Commit f9e19f3

Browse files
authored
Merge pull request #8 from baraline/1-define-an-explicit-sync-client
Rework sync/async
2 parents c593368 + 548f01c commit f9e19f3

67 files changed

Lines changed: 3128 additions & 1336 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.coverage

-20 KB
Binary file not shown.

CONTRIBUTING.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,11 @@ python -m sphinx -W --keep-going -b html docs docs/_build/html
5050

5151
## Design Guidelines
5252

53-
- Keep API calls behind `GlpiClient` methods.
53+
- Keep API calls behind `GlpiClient` / `AsyncGlpiClient` methods. Add
54+
new endpoints to a sync endpoint mixin only; `AsyncGlpiClient`
55+
exposes them as coroutines automatically through `AsyncBridge`. Only
56+
add a dedicated async override when the method needs concurrent
57+
fan-out via `asyncio.gather`.
5458
- Prefer field-validated Pydantic models for request and response payloads.
5559
- Avoid organization-specific category, entity, or profile defaults in the
5660
library core.

README.md

Lines changed: 54 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,14 @@ followups, documents, locations, and related records, while converting GLPI
1414
HTML content into Markdown for Python-side workflows and rendering Markdown
1515
back to HTML for outgoing payloads.
1616

17-
It currently focuses on ticket-centric workflows and exposes a single
18-
asynchronous high-level client built on top of the GLPI v2 REST API.
17+
It currently focuses on ticket-centric workflows and exposes two high-level
18+
clients built on top of the GLPI v2 REST API:
19+
20+
- `GlpiClient` — synchronous, blocking client (single source of truth for
21+
endpoint behaviour).
22+
- `AsyncGlpiClient` — asynchronous facade that wraps every synchronous
23+
method into a coroutine and dispatches it to a worker thread.
24+
1925
Note that all integration tests using this package are made on GLPI 11.
2026
I cannot make any guarantee of the behaviour on previous versions.
2127

@@ -42,14 +48,38 @@ Create a client with your GLPI v2 API URL and at least one complete auth pair:
4248
- `username` and `password`
4349
- both pairs together
4450

51+
### Synchronous client
52+
53+
```python
54+
from glpi_python_client import GlpiClient, PostTicket
55+
56+
with GlpiClient(
57+
glpi_api_url="https://glpi.example.com/api.php/v2",
58+
client_id="oauth-client-id",
59+
client_secret="oauth-client-secret",
60+
username="api-user",
61+
password="api-password",
62+
) as glpi:
63+
ticket_id = glpi.create_ticket(
64+
PostTicket(
65+
name="Printer issue",
66+
content="The printer is not reachable from the office network.",
67+
)
68+
)
69+
ticket = glpi.get_ticket(ticket_id)
70+
print(ticket.id, ticket.name)
71+
```
72+
73+
### Asynchronous client
74+
4575
```python
4676
import asyncio
4777

48-
from glpi_python_client import GlpiClient, PostTicket
78+
from glpi_python_client import AsyncGlpiClient, PostTicket
4979

5080

5181
async def main() -> None:
52-
async with GlpiClient(
82+
async with AsyncGlpiClient(
5383
glpi_api_url="https://glpi.example.com/api.php/v2",
5484
client_id="oauth-client-id",
5585
client_secret="oauth-client-secret",
@@ -69,39 +99,22 @@ async def main() -> None:
6999
asyncio.run(main())
70100
```
71101

72-
If your application already provides `GLPI_` environment variables,
73-
`GlpiClient.from_env()` is also available.
74-
75-
### Calling from synchronous code
76-
For now, I provide only an async client, but if necessary, could
77-
duplicate the code to make a sync client. Until then you can make
78-
it works from sync programs through
79-
`asyncio.run`. Wrap the calls in a coroutine and execute it once:
102+
`GlpiClient.from_env()` and `AsyncGlpiClient.from_env()` are also available
103+
when the credentials are already exposed as `GLPI_`-prefixed environment
104+
variables.
80105

81-
```python
82-
import asyncio
83-
84-
from glpi_python_client import GlpiClient
85-
86-
87-
def fetch_open_tickets() -> list[int]:
88-
async def _run() -> list[int]:
89-
async with GlpiClient.from_env() as glpi:
90-
tickets = await glpi.search_tickets("status==1", limit=10)
91-
return [ticket.id for ticket in tickets]
92-
93-
return asyncio.run(_run())
106+
### Sync or async?
94107

95-
96-
if __name__ == "__main__":
97-
print(fetch_open_tickets())
98-
```
99-
100-
For long-lived sync services that need many calls, run a dedicated
101-
event loop on a background thread and dispatch with
102-
`asyncio.run_coroutine_threadsafe`. See the
103-
[user guide](https://glpi-python-client.readthedocs.io/en/latest/user_guide.html#calling-the-client-from-synchronous-code)
104-
for the full pattern.
108+
Both clients expose the exact same endpoint surface and accept the same
109+
constructor arguments. The async client is a thin facade that wraps each
110+
synchronous method into a coroutine dispatched to a worker thread via
111+
`asyncio.to_thread` (or a caller-supplied `concurrent.futures.Executor`).
112+
A shared `threading.Lock` serialises OAuth token acquisition so concurrent
113+
`asyncio.gather(...)` fan-outs cannot race. Pick `GlpiClient` for plain
114+
scripts, CLI tools, and synchronous services; pick `AsyncGlpiClient` when
115+
your application already runs an event loop or when you need concurrent
116+
fan-out (the aggregated `get_ticket_context` and per-ticket
117+
`get_task_statistics` helpers use `asyncio.gather` on the async client).
105118

106119
## Documentation
107120

@@ -116,3 +129,9 @@ To build the Sphinx documentation locally:
116129
python -m pip install -e .[docs]
117130
python -m sphinx -b html docs docs/_build/html
118131
```
132+
133+
## Sponsoring & Professional services
134+
The development of this package is indirectly supported by [Novahé](https://www.novahe.fr/) & [Constellation](https://www.constellation.fr/).
135+
136+
If you need professional help or services around GLPI, we offer consulting and engineering services to install, maintain or upgarde GLPI instance, as an [official GLPI partner](https://www.glpi-project.org/fr/new-glpi-silver-partner-in-france-novahe/).
137+

docs/api_reference.rst

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,27 @@ underscore-prefixed helpers are intentionally omitted.
77

88
.. currentmodule:: glpi_python_client
99

10-
Client
11-
------
10+
Clients
11+
-------
12+
13+
The package exposes two clients with identical endpoint surfaces. The
14+
synchronous one is the single source of truth for endpoint behaviour;
15+
the asynchronous one wraps each synchronous method into a coroutine.
1216

1317
.. autoclass:: GlpiClient
1418
:members:
1519
:inherited-members:
1620
:show-inheritance:
1721

22+
.. autoclass:: AsyncGlpiClient
23+
:members:
24+
:inherited-members:
25+
:show-inheritance:
26+
27+
.. autoclass:: glpi_python_client.clients.commons._async_bridge.AsyncBridge
28+
:members:
29+
:show-inheritance:
30+
1831
Aggregated Models
1932
-----------------
2033

docs/development.md

Lines changed: 61 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -40,57 +40,70 @@ python -m pytest
4040

4141
## Package Layout
4242

43-
- `glpi_python_client.__init__` exposes the public import surface.
44-
- `glpi_python_client.clients.api_v2_client.GlpiClient` owns synchronous API
45-
configuration, authentication, context-manager cleanup, and the small
46-
user/location/document provisioning surface.
47-
- `glpi_python_client.clients.async_api_v2_client.AsyncGlpiClient` owns the
48-
matching awaitable client surface and keeps blocking requests behind
49-
`asyncio.to_thread()` boundaries.
50-
- `glpi_python_client.clients.v2` contains the internal v2 implementation
51-
packages.
52-
- `glpi_python_client.clients.v2.common` holds reusable setup, endpoint,
53-
request, pagination, payload, filter, and error helpers shared by both
54-
execution models.
55-
- `glpi_python_client.clients.v2.sync` contains the synchronous endpoint mixins:
56-
`transport`, `tickets`, `timeline`, `documents`, `team`, and `directory`.
57-
`sync.api` assembles those mixins.
58-
- `glpi_python_client.clients.v2.async_` contains the matching asynchronous
59-
endpoint mixins and keeps `asyncio.to_thread()` at the blocking request and
60-
v1-session boundaries. `async_.api` assembles those mixins.
61-
- `glpi_python_client.clients._shared` is a compatibility module that re-exports
62-
the scoped v2 helper modules for older internal imports.
63-
- `glpi_python_client.clients.api_v1_session` contains the legacy v1 session
64-
used for document operations.
65-
- `glpi_python_client.models` contains typed request and response models.
66-
- `glpi_python_client.content.records` is a compatibility package for raw GLPI
67-
payload conversion.
68-
- `glpi_python_client.content.records.core` contains shared normalization,
69-
scalar coercion, nested-reference parsing, and timeline document-link
70-
helpers.
71-
- `glpi_python_client.content.records.parsers` contains model-specific parsers
72-
for tickets, timeline items, documents, team members, users, and locations.
43+
- `glpi_python_client.__init__` exposes the public import surface,
44+
including both client classes and the Pydantic models.
45+
- `glpi_python_client.clients.sync_client.GlpiClient` is the
46+
synchronous, blocking client. It is the single source of truth for
47+
endpoint behaviour: each public method lives on one of the sync
48+
endpoint mixins under `glpi_python_client.clients.api.*` and
49+
`glpi_python_client.clients.custom.*`.
50+
- `glpi_python_client.clients.async_client.AsyncGlpiClient` is the
51+
asynchronous facade. It inherits the same endpoint mixins and uses
52+
`glpi_python_client.clients.commons._async_bridge.AsyncBridge` to wrap
53+
every inherited public sync method into a coroutine dispatched on a
54+
worker thread (`asyncio.to_thread` by default, or a caller-supplied
55+
`concurrent.futures.Executor`).
56+
- `glpi_python_client.clients.commons` holds the reusable building
57+
blocks shared by every endpoint mixin: configuration helpers
58+
(`_config`), constants (`_constants`), errors (`_errors`), filters
59+
(`_filters`), HTTP helpers (`_http`), payload builders (`_payloads`),
60+
the synchronous `TransportMixin` (`_transport`), and the
61+
`AsyncBridge` (`_async_bridge`). A shared `threading.Lock` in the
62+
transport serialises OAuth token acquisition so concurrent
63+
`asyncio.gather` fan-outs on the async client cannot race.
64+
- `glpi_python_client.clients.api.*` contains the contract-aligned
65+
synchronous endpoint mixins, grouped by GLPI subtree (administration,
66+
assistance, assistance/timeline, dropdowns, management).
67+
- `glpi_python_client.clients.custom` contains custom helpers built on
68+
top of the API mixins. Each helper has a synchronous implementation
69+
(`_ticket_context.py`, `_statistics.py`) plus an optional async
70+
override (`_ticket_context_async.py`, `_statistics_async.py`) that
71+
fans the underlying calls out concurrently with `asyncio.gather`.
72+
- `glpi_python_client.auth._v1_session` contains the legacy v1
73+
session used for binary document uploads.
74+
- `glpi_python_client.models` contains typed request and response
75+
models.
76+
- `glpi_python_client.content` handles HTML/Markdown conversion for
77+
ticket descriptions, followups, tasks, and solutions.
78+
- `glpi_python_client.testing` exposes `make_client` and
79+
`make_async_client` factories that produce in-memory clients with no
80+
real HTTP plumbing for downstream test suites.
7381
- `docs` contains the Read the Docs/Sphinx documentation source.
74-
- `skills` contains contributor-facing Agent Skills for repository workflows.
75-
The source distribution includes them for source consumers and contributors,
76-
but the wheel still installs only the `glpi_python_client` runtime package.
82+
- `skills` contains contributor-facing Agent Skills for repository
83+
workflows. The source distribution includes them for source consumers
84+
and contributors, but the wheel still installs only the
85+
`glpi_python_client` runtime package.
7786

7887
## Adding Endpoints
7988

8089
1. Add or extend a model in `glpi_python_client.models`.
81-
2. Add response parsing in the matching
82-
`glpi_python_client.content.records.parsers` module when the endpoint returns
83-
structured data, and put shared parsing helpers in
84-
`glpi_python_client.content.records.core` only when multiple parsers need
85-
them.
86-
3. Add the client method in the matching
87-
`glpi_python_client.clients.v2.sync` module and the matching
88-
`glpi_python_client.clients.v2.async_` module when applicable.
89-
4. Put reusable endpoint names, payload builders, response handling, or
90-
pagination logic in the focused `glpi_python_client.clients.v2.common`
91-
helper module named for that responsibility.
92-
5. Add tests for payload serialization, response parsing, and client behavior.
93-
6. Document the new workflow in `docs/usage.md` or the README.
90+
2. Add the client method on the matching **synchronous** endpoint mixin
91+
under `glpi_python_client.clients.api.*` (or
92+
`glpi_python_client.clients.custom.*` for derived helpers). The
93+
async client picks the new method up automatically through the
94+
`AsyncBridge` — do not duplicate the method on a parallel async
95+
mixin unless you genuinely need concurrent fan-out (`asyncio.gather`)
96+
inside the method body.
97+
3. Put reusable endpoint names, payload builders, response handling, or
98+
pagination logic in the focused
99+
`glpi_python_client.clients.commons` helper module named for that
100+
responsibility.
101+
4. Add unit tests for payload serialization, response parsing, and
102+
client behavior. The parity test in
103+
`glpi_python_client/clients/tests/test_parity.py` will fail if the
104+
sync and async surfaces diverge.
105+
5. Document the new workflow in `docs/user_guide.rst` or the README.
94106

95-
Keep organization-specific defaults outside the package core. Applications can
96-
map their own entities, profiles, and categories before calling the client.
107+
Keep organization-specific defaults outside the package core.
108+
Applications can map their own entities, profiles, and categories
109+
before calling the client.

docs/index.rst

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ handling, and helpers for ticket, user, location, and document workflows.
2424

2525
development_rtd
2626
publishing_rtd
27+
sponsoring
2728

2829
Indices and Tables
2930
==================

docs/sponsoring.rst

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
Sponsoring & Professional Services
2+
===================================
3+
4+
The development of this package is indirectly supported by
5+
`Novahé <https://www.novahe.fr/>`_ & `Constellation <https://www.constellation.fr/>`_.
6+
7+
If you need professional help or services around GLPI, we offer consulting and
8+
engineering services to install, maintain, or upgrade GLPI instances, as an
9+
`official GLPI partner <https://www.glpi-project.org/fr/new-glpi-silver-partner-in-france-novahe/>`_.

0 commit comments

Comments
 (0)