Lapidary is a client library for remote APIs described with the OpenAPI specification, and a program to generate models and clients stubs for those APIs.
Together, the generated client and the library, let developers use remote APIs as if they were local libraries.
Leverages Pydantic as the base classes for models, HTTPX as the HTTP client and Typer as the CLI library.
See render readme for command line interface to generate and update models and stubs.
-
Schema objects of object type are source to generate model classes.
-
Enums:
Currently only primitive values are supported, but OpenAPI specifies no such limitation - any valid JSON value should be supported (#30).
-
oneOf, allOf, anyOf, not: see discussion and concepts below.
Currently, oneOf is implemented as
typing.Union
, which is not ideal. Also, Pydantic doesn't allow models to have both __root__ and own properties, which isn't compatible with OpenAPI. -
Recursive references between schemas.
- $ref to self: Schema A having a property, which schema is a $ref: A
- Indirect circular ref - schema A has a property which schema is a $ref: B and schema B has a property whose schema is $ref: A
This could translate to a following Python code:
class A: b: 'B' class B: a: 'A'
Both constructs are supported by their languages and Lapidary.
Recursive composing or inheritance is not supported:
components: B: schema: oneOf: - $ref: /components/schemas/C # invalid C: schema: oneOf: - $ref: /components/schemas/B # invalid
-
Parameter names
Operation parameters are uniquely identified by their name and the value of the
in
attribute. It is possible to have parameter namedparam
in all of: path, query, cookies and headers.Lapidary uses Hungarian notation for method parameter names. See #29
- References to other schemas.
- Read- and writeOnly properties (Currently rendered as not-required fields).
Lapidary applies JSONPatch files from src/patches directory to the OpenAPI before generating the client code.
Planned: Plug-ins
Lapidary should generate compatible client code as long as you're using
- compatible OpenAPI spec, which includes the patches, and
- compatible version of lapidary.
There are a couple of issues that need to be solved:
-
Some unnamed elements in OpenAPI need to be named in Python.
Examples:
- enum values (list elements)
- schema objects under requestBody and responses
-
Some names may not be valid in Python
Examples:
- names of all objects under /components/* names must match this regex:
^[a-zA-Z0-9\.\-_]+$
, which means characters.-
and_
if it's on the beginning of string must be escaped. - the above limitation doesn't apply to any other names, so nearly full unicode character set needs to be handled by the character escaping algorithm.
- names of all objects under /components/* names must match this regex:
To achieve this, naming classes and variables should be independent of its siblings. E.g.
schemas:
A:
type: object
schema:
oneOf:
- schema:
type: object
properties:
a:
type: string
- schema:
type: object
properties:
a: integer
A code generator could naively generate such python classes:
class A1:
a: str
class A2:
a: int
A = typing.Union[A1, A2]
We'll forget for a moment the problem of whether typing.Union
is a proper representation of oneOf
schema.
The problem here is that if the OpenAPI author changes order of the two sub-schemas, that change would be backwards compatible. On the Python side however, the generated code would have changed in an incompatible way, with properties in both A1 and A2 classes having different types than before.
In this particular case, both classes would need to be either $refs or explicitly named using an extended attribute in OpenAPI.
OpenAPI's schema objects is a means of defining validations for JSON values, just like JSONSchema, from which it's derived.
On the other hand, Python type hints is a language feature to declare variables' (class attributes' and function parameters') types as a help for developers through the use of static code analysis tools.
While those two goals are, to an extent, overlapping, they're not identical.
For example, a static code analysis tool is unable to check if string's length is within specified bounds. Only the application itself can check it at run-time (perhaps by using a library, like Pydantic).
Consider these examples:
schemas:
A:
properties:
b:
type: string
Could be represented as:
class A:
b: str
but:
schemas:
A:
properties:
b:
type: string
maxLength: 1
class A(pydantic.BaseModel):
b: typing.Annotated[str, pydantic.Field(max_length=1)]
Static analysis can, in the right circumstances, ensure that b
is a string, but validating its length is only possible at run-time, in this case, by Pydantic.
In case of composite types: anyOf
, oneOf
, allOf
and not
, it may be possible to create generic type annotations for them, but it might require
implementing static analysis tools to support it, or at least a plug-in for an existing tool, like Mypy.
On the other hand, validating it in run-time is relatively simple.
Any Operation or global response of type array
can be returned as a collections.abc.AsyncIterator
. Use x-lapidary-modelType: iterator
OpenAPI extended
attribute.
Example:
get:
operationId: get_data
responses:
'200':
description: example response
content:
application/json:
schema:
type: array
items:
type: str
x-lapidary-modelType: iterator
results with the following python stub:
import collections.abc
import lapidary.runtime
class ApiClient(lapidary.runtime.ClientBase):
...
async def get_data(self) -> collections.abc.AsyncIterator[str]:
...
Only top-level schemas, that is those directly under, or referenced by responses or global-responses, may be marked with x-lapidary-modelType
.
At runtime, if the response body is an array, it's parsed as a list and then returned as an AsyncGenerator, for compatibility with paging plugin.
Paging allows to automatically get paged result as an iterator over either the pages or the paged items.
By default, an operation method returns the parsed body of a single response.
When a paging plugin is used, it returns an async iterator over the paged responses.
When the plugin is combined with x-lapidary-modelType: iterator
and the response body is an array, the result is an async iterator over all items in all
pages.
The paging plugin has an option to convert the response body object for cases when the array is wrapped in another object.
Paging is implemented using AsyncGenerator. Pages are fetched lazily, when the first item of the next page is accessed.