Skip to content

Architecture

The conventions shared with aioscaleway are in the Business Agile conventions (backend/08-python-api-clients.md, draft in /opt/businessagile/libraries/conventions-clients-api.md). This file covers what is specific to OVHcloud.

Two layers

  • Operations (src/aioovh/_operations/v1/<api>/, .../v2/<api>/), private: one typed method per operation of each schema, with its models, generated by scripts/operations.py. Method names derive from the path and the HTTP method, since the schemas have no operation identifiers.
  • Objects (src/aioovh/<area>/), public: business areas, collections, references and loaded objects, generated by scripts/objects.py from the operations and mapping.yaml.

The hand-written runtime is _client.py (signature, clock offset, OAuth2, retries), _objects.py (collections, references, binding, bounded loading), _root.py (OVH), _settings.py, _endpoints.py, _errors.py, _models.py and _enums.py.

The mapping

mapping.yaml lists every API root, as a path:

  • a collection root (/me/bill: {namespace: billing, name: bills}) starts a collection named name in the namespace;
  • an area (/me: {namespace: account, area: true}) is not a resource: its sub-paths become collections and methods of the namespace, and a GET on the area itself is what await ovh.account returns;
  • a scope (scope: {serviceName: project}) turns a path parameter of the root into a keyword with a default from the settings: the Public Cloud project;
  • a prefix puts a word in front of an area's names when two areas of a namespace would clash (managed for the v2 managed databases next to the 1.0 ones, public_cloud for the v2 Public Cloud areas, in BETA).

The longest matching root wins, so /me/bill goes to billing while the rest of /me goes to account. A root left out of the mapping stops the generator.

Placement of each operation

Under a root, a static segment followed by a parameter is a resource (/me/bill/{billId}). Then:

  • on the item path: GET loads, PUT is update, DELETE is delete; a second GET (from the other API version) is details;
  • on the collection path: a GET returning a list is the iteration, POST is create;
  • a static sub-path with items is a sub-collection (bill.details); one without items is a method named after it (service_infos(), update_service_infos(), reboot()), or an iterable listing when it returns a list;
  • deprecated operations stay in the operations layer only.

A GET that needs more than the identifier (a required query parameter) is a method, not the load.

Lists of identifiers

Most 1.0 lists return identifiers. Iterating a collection loads each object, max_concurrency at a time in a task group that ends before the next batch: no task outlives the async for. ids() skips the loading.

Types and dates

datetime fields are whenever.Instant, date fields whenever.Date; a datetime query filter also accepts a Date. Path parameters typed as an enum, a date or an integer are converted from the key, which holds strings. Every model is rebuilt at the end of its module, so that loaded classes in another module inherit resolved fields.

Each schema is self-contained, so shared models such as order.Price exist once per schema that uses them; an area's types module prefixes a name with its API (MeOrderPrice) when two schemas of the area define it.

Requests

Each request is built, then signed on its exact URL and body; each retry is signed again. The clock offset is read once from /auth/time. With client_id and client_secret, an OAuth2 token is used instead and renewed 30 seconds before it expires.

Retries

Idempotent methods are retried on 429, 502, 503, 504 and transport errors; a POST only on 429, 503 or a connection that never opened.