Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Binary file modified .coverage
Binary file not shown.
6 changes: 5 additions & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,11 @@ python -m sphinx -W --keep-going -b html docs docs/_build/html

## Design Guidelines

- Keep API calls behind `GlpiClient` methods.
- Keep API calls behind `GlpiClient` / `AsyncGlpiClient` methods. Add
new endpoints to a sync endpoint mixin only; `AsyncGlpiClient`
exposes them as coroutines automatically through `AsyncBridge`. Only
add a dedicated async override when the method needs concurrent
fan-out via `asyncio.gather`.
- Prefer field-validated Pydantic models for request and response payloads.
- Avoid organization-specific category, entity, or profile defaults in the
library core.
Expand Down
89 changes: 54 additions & 35 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,14 @@ followups, documents, locations, and related records, while converting GLPI
HTML content into Markdown for Python-side workflows and rendering Markdown
back to HTML for outgoing payloads.

It currently focuses on ticket-centric workflows and exposes a single
asynchronous high-level client built on top of the GLPI v2 REST API.
It currently focuses on ticket-centric workflows and exposes two high-level
clients built on top of the GLPI v2 REST API:

- `GlpiClient` — synchronous, blocking client (single source of truth for
endpoint behaviour).
- `AsyncGlpiClient` — asynchronous facade that wraps every synchronous
method into a coroutine and dispatches it to a worker thread.

Note that all integration tests using this package are made on GLPI 11.
I cannot make any guarantee of the behaviour on previous versions.

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

### Synchronous client

```python
from glpi_python_client import GlpiClient, PostTicket

with GlpiClient(
glpi_api_url="https://glpi.example.com/api.php/v2",
client_id="oauth-client-id",
client_secret="oauth-client-secret",
username="api-user",
password="api-password",
) as glpi:
ticket_id = glpi.create_ticket(
PostTicket(
name="Printer issue",
content="The printer is not reachable from the office network.",
)
)
ticket = glpi.get_ticket(ticket_id)
print(ticket.id, ticket.name)
```

### Asynchronous client

```python
import asyncio

from glpi_python_client import GlpiClient, PostTicket
from glpi_python_client import AsyncGlpiClient, PostTicket


async def main() -> None:
async with GlpiClient(
async with AsyncGlpiClient(
glpi_api_url="https://glpi.example.com/api.php/v2",
client_id="oauth-client-id",
client_secret="oauth-client-secret",
Expand All @@ -69,39 +99,22 @@ async def main() -> None:
asyncio.run(main())
```

If your application already provides `GLPI_` environment variables,
`GlpiClient.from_env()` is also available.

### Calling from synchronous code
For now, I provide only an async client, but if necessary, could
duplicate the code to make a sync client. Until then you can make
it works from sync programs through
`asyncio.run`. Wrap the calls in a coroutine and execute it once:
`GlpiClient.from_env()` and `AsyncGlpiClient.from_env()` are also available
when the credentials are already exposed as `GLPI_`-prefixed environment
variables.

```python
import asyncio

from glpi_python_client import GlpiClient


def fetch_open_tickets() -> list[int]:
async def _run() -> list[int]:
async with GlpiClient.from_env() as glpi:
tickets = await glpi.search_tickets("status==1", limit=10)
return [ticket.id for ticket in tickets]

return asyncio.run(_run())
### Sync or async?


if __name__ == "__main__":
print(fetch_open_tickets())
```

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

## Documentation

Expand All @@ -116,3 +129,9 @@ To build the Sphinx documentation locally:
python -m pip install -e .[docs]
python -m sphinx -b html docs docs/_build/html
```

## Sponsoring & Professional services
The development of this package is indirectly supported by [Novahé](https://www.novahe.fr/) & [Constellation](https://www.constellation.fr/).

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/).

17 changes: 15 additions & 2 deletions docs/api_reference.rst
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,27 @@ underscore-prefixed helpers are intentionally omitted.

.. currentmodule:: glpi_python_client

Client
------
Clients
-------

The package exposes two clients with identical endpoint surfaces. The
synchronous one is the single source of truth for endpoint behaviour;
the asynchronous one wraps each synchronous method into a coroutine.

.. autoclass:: GlpiClient
:members:
:inherited-members:
:show-inheritance:

.. autoclass:: AsyncGlpiClient
:members:
:inherited-members:
:show-inheritance:

.. autoclass:: glpi_python_client.clients.commons._async_bridge.AsyncBridge
:members:
:show-inheritance:

Aggregated Models
-----------------

Expand Down
109 changes: 61 additions & 48 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -40,57 +40,70 @@ python -m pytest

## Package Layout

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

## Adding Endpoints

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

Keep organization-specific defaults outside the package core. Applications can
map their own entities, profiles, and categories before calling the client.
Keep organization-specific defaults outside the package core.
Applications can map their own entities, profiles, and categories
before calling the client.
1 change: 1 addition & 0 deletions docs/index.rst
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ handling, and helpers for ticket, user, location, and document workflows.

development_rtd
publishing_rtd
sponsoring

Indices and Tables
==================
Expand Down
9 changes: 9 additions & 0 deletions docs/sponsoring.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
Sponsoring & Professional Services
===================================

The development of this package is indirectly supported by
`Novahé <https://www.novahe.fr/>`_ & `Constellation <https://www.constellation.fr/>`_.

If you need professional help or services around GLPI, we offer consulting and
engineering services to install, maintain, or upgrade GLPI instances, as an
`official GLPI partner <https://www.glpi-project.org/fr/new-glpi-silver-partner-in-france-novahe/>`_.
Loading
Loading