dynamicapiclient 0.1.2__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- dynamicapiclient-0.1.2/.cursor/rules/test-coverage.mdc +10 -0
- dynamicapiclient-0.1.2/.github/workflows/ci.yml +46 -0
- dynamicapiclient-0.1.2/.github/workflows/publish-pypi.yml +48 -0
- dynamicapiclient-0.1.2/.github/workflows/release-from-version.yml +83 -0
- dynamicapiclient-0.1.2/.gitignore +15 -0
- dynamicapiclient-0.1.2/.pre-commit-config.yaml +15 -0
- dynamicapiclient-0.1.2/PKG-INFO +172 -0
- dynamicapiclient-0.1.2/README.md +153 -0
- dynamicapiclient-0.1.2/pyproject.toml +46 -0
- dynamicapiclient-0.1.2/src/pyapiclient/__init__.py +24 -0
- dynamicapiclient-0.1.2/src/pyapiclient/api.py +316 -0
- dynamicapiclient-0.1.2/src/pyapiclient/client.py +115 -0
- dynamicapiclient-0.1.2/src/pyapiclient/exceptions.py +34 -0
- dynamicapiclient-0.1.2/src/pyapiclient/graphql_support.py +505 -0
- dynamicapiclient-0.1.2/src/pyapiclient/loader.py +164 -0
- dynamicapiclient-0.1.2/src/pyapiclient/models.py +429 -0
- dynamicapiclient-0.1.2/src/pyapiclient/routing.py +240 -0
- dynamicapiclient-0.1.2/src/pyapiclient/spec.py +126 -0
- dynamicapiclient-0.1.2/src/pyapiclient/validation.py +88 -0
- dynamicapiclient-0.1.2/tests/conftest.py +22 -0
- dynamicapiclient-0.1.2/tests/fixtures/library.graphql +42 -0
- dynamicapiclient-0.1.2/tests/fixtures/library_oas3.yaml +116 -0
- dynamicapiclient-0.1.2/tests/fixtures/swagger2_library.json +65 -0
- dynamicapiclient-0.1.2/tests/test_api.py +169 -0
- dynamicapiclient-0.1.2/tests/test_api_errors.py +35 -0
- dynamicapiclient-0.1.2/tests/test_api_sniff.py +84 -0
- dynamicapiclient-0.1.2/tests/test_api_url.py +69 -0
- dynamicapiclient-0.1.2/tests/test_client.py +88 -0
- dynamicapiclient-0.1.2/tests/test_coverage_extra.py +337 -0
- dynamicapiclient-0.1.2/tests/test_exceptions.py +15 -0
- dynamicapiclient-0.1.2/tests/test_graphql.py +340 -0
- dynamicapiclient-0.1.2/tests/test_loader.py +124 -0
- dynamicapiclient-0.1.2/tests/test_loader_extra.py +57 -0
- dynamicapiclient-0.1.2/tests/test_models.py +394 -0
- dynamicapiclient-0.1.2/tests/test_routing.py +266 -0
- dynamicapiclient-0.1.2/tests/test_spec.py +179 -0
- dynamicapiclient-0.1.2/tests/test_validation.py +145 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
---
|
|
2
|
+
description: Keep total test coverage at or above 90% for pyapiclient
|
|
3
|
+
alwaysApply: true
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Test coverage
|
|
7
|
+
|
|
8
|
+
- **Target:** total coverage for the `pyapiclient` package must stay **≥ 90%** (pytest-cov with branch coverage enabled, same settings as `pyproject.toml`).
|
|
9
|
+
- **Scope:** all modules under `src/pyapiclient/` — when adding or changing code, add or extend tests so new branches and lines remain covered. Do not lower `--cov-fail-under` to “make CI green” without restoring coverage.
|
|
10
|
+
- **Check locally:** `pytest -q --cov=pyapiclient --cov-fail-under=90` (or `pre-commit run --all-files` after hooks are installed).
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
name: CI
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
push:
|
|
5
|
+
branches: [main]
|
|
6
|
+
pull_request:
|
|
7
|
+
branches: [main]
|
|
8
|
+
|
|
9
|
+
permissions:
|
|
10
|
+
contents: read
|
|
11
|
+
id-token: write
|
|
12
|
+
|
|
13
|
+
jobs:
|
|
14
|
+
test:
|
|
15
|
+
runs-on: ubuntu-latest
|
|
16
|
+
strategy:
|
|
17
|
+
fail-fast: false
|
|
18
|
+
matrix:
|
|
19
|
+
python-version: ["3.10", "3.11", "3.12", "3.13"]
|
|
20
|
+
|
|
21
|
+
steps:
|
|
22
|
+
- uses: actions/checkout@v4
|
|
23
|
+
|
|
24
|
+
- uses: actions/setup-python@v5
|
|
25
|
+
with:
|
|
26
|
+
python-version: ${{ matrix.python-version }}
|
|
27
|
+
|
|
28
|
+
- name: Install dependencies
|
|
29
|
+
run: pip install --upgrade pip && pip install -e ".[dev]"
|
|
30
|
+
|
|
31
|
+
- name: Run tests with coverage
|
|
32
|
+
run: >
|
|
33
|
+
pytest -q
|
|
34
|
+
--cov=pyapiclient
|
|
35
|
+
--cov-report=xml
|
|
36
|
+
--cov-report=term-missing
|
|
37
|
+
--cov-fail-under=90
|
|
38
|
+
|
|
39
|
+
- name: Upload coverage to Codecov
|
|
40
|
+
uses: codecov/codecov-action@v5
|
|
41
|
+
with:
|
|
42
|
+
use_oidc: true
|
|
43
|
+
files: coverage.xml
|
|
44
|
+
flags: py${{ matrix.python-version }}
|
|
45
|
+
name: Python-${{ matrix.python-version }}
|
|
46
|
+
fail_ci_if_error: true
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
# Publish dynamicapiclient to PyPI when a GitHub Release is published.
|
|
2
|
+
# Auto-created releases use release-from-version.yml, which publishes to PyPI
|
|
3
|
+
# in the same job (GitHub does not chain workflows for GITHUB_TOKEN events).
|
|
4
|
+
#
|
|
5
|
+
# The value of [project].name in pyproject.toml must match the PyPI project name
|
|
6
|
+
# configured for trusted publishing, or uploads fail with HTTP 400.
|
|
7
|
+
#
|
|
8
|
+
# Trusted publishing (recommended): https://docs.pypi.org/trusted-publishers/
|
|
9
|
+
# PyPI → Your project → Publishing → Add a pending publisher
|
|
10
|
+
# Provider: GitHub, repo, workflow file: publish-pypi.yml (environment name optional).
|
|
11
|
+
#
|
|
12
|
+
# Token fallback: add a repo secret PYPI_API_TOKEN (PyPI → Account → API tokens) and set:
|
|
13
|
+
# with:
|
|
14
|
+
# password: ${{ secrets.PYPI_API_TOKEN }}
|
|
15
|
+
#
|
|
16
|
+
# Manual run: Actions → this workflow → Run workflow (pick branch; default main).
|
|
17
|
+
|
|
18
|
+
name: Publish to PyPI
|
|
19
|
+
|
|
20
|
+
on:
|
|
21
|
+
release:
|
|
22
|
+
types: [published]
|
|
23
|
+
workflow_dispatch:
|
|
24
|
+
|
|
25
|
+
permissions:
|
|
26
|
+
contents: read
|
|
27
|
+
id-token: write
|
|
28
|
+
|
|
29
|
+
jobs:
|
|
30
|
+
publish:
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
steps:
|
|
33
|
+
- uses: actions/checkout@v4
|
|
34
|
+
|
|
35
|
+
- uses: actions/setup-python@v5
|
|
36
|
+
with:
|
|
37
|
+
python-version: "3.12"
|
|
38
|
+
|
|
39
|
+
- name: Install build tools
|
|
40
|
+
run: pip install --upgrade build
|
|
41
|
+
|
|
42
|
+
- name: Build sdist and wheel
|
|
43
|
+
run: python -m build
|
|
44
|
+
|
|
45
|
+
- name: Publish to PyPI
|
|
46
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
47
|
+
with:
|
|
48
|
+
skip-existing: true
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
# When [project].version changes on main (any edit to pyproject.toml), create a
|
|
2
|
+
# GitHub Release and tag v{version} if that tag does not exist yet, then publish
|
|
3
|
+
# to PyPI in this same workflow.
|
|
4
|
+
#
|
|
5
|
+
# GitHub does not run other workflows in response to events raised with the
|
|
6
|
+
# default GITHUB_TOKEN (e.g. creating a release here), so publish-pypi.yml would
|
|
7
|
+
# never fire for bot-created releases. Manual releases from the UI still trigger
|
|
8
|
+
# publish-pypi.yml normally.
|
|
9
|
+
|
|
10
|
+
name: Release from pyproject version
|
|
11
|
+
|
|
12
|
+
on:
|
|
13
|
+
push:
|
|
14
|
+
branches: [main]
|
|
15
|
+
paths:
|
|
16
|
+
- "pyproject.toml"
|
|
17
|
+
|
|
18
|
+
permissions:
|
|
19
|
+
contents: write
|
|
20
|
+
id-token: write
|
|
21
|
+
|
|
22
|
+
jobs:
|
|
23
|
+
maybe-release:
|
|
24
|
+
runs-on: ubuntu-latest
|
|
25
|
+
steps:
|
|
26
|
+
- uses: actions/checkout@v4
|
|
27
|
+
with:
|
|
28
|
+
fetch-depth: 0
|
|
29
|
+
|
|
30
|
+
- name: Read version from pyproject.toml
|
|
31
|
+
id: version
|
|
32
|
+
run: |
|
|
33
|
+
VERSION=$(python3 <<'PY'
|
|
34
|
+
import tomllib
|
|
35
|
+
from pathlib import Path
|
|
36
|
+
with Path("pyproject.toml").open("rb") as f:
|
|
37
|
+
print(tomllib.load(f)["project"]["version"])
|
|
38
|
+
PY
|
|
39
|
+
)
|
|
40
|
+
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
|
|
41
|
+
|
|
42
|
+
- name: Fetch tags
|
|
43
|
+
run: git fetch origin --tags
|
|
44
|
+
|
|
45
|
+
- name: Check if tag already exists
|
|
46
|
+
id: tag
|
|
47
|
+
env:
|
|
48
|
+
VERSION: ${{ steps.version.outputs.version }}
|
|
49
|
+
run: |
|
|
50
|
+
TAG="v${VERSION}"
|
|
51
|
+
if git rev-parse -q --verify "refs/tags/${TAG}" >/dev/null; then
|
|
52
|
+
echo "exists=true" >> "$GITHUB_OUTPUT"
|
|
53
|
+
echo "Tag ${TAG} already exists; skipping release."
|
|
54
|
+
else
|
|
55
|
+
echo "exists=false" >> "$GITHUB_OUTPUT"
|
|
56
|
+
fi
|
|
57
|
+
|
|
58
|
+
- name: Create GitHub Release
|
|
59
|
+
if: steps.tag.outputs.exists == 'false'
|
|
60
|
+
uses: softprops/action-gh-release@153bb8e04406b158c6c84fc1615b65b24149a1fe
|
|
61
|
+
with:
|
|
62
|
+
tag_name: v${{ steps.version.outputs.version }}
|
|
63
|
+
name: v${{ steps.version.outputs.version }}
|
|
64
|
+
generate_release_notes: true
|
|
65
|
+
|
|
66
|
+
- uses: actions/setup-python@v5
|
|
67
|
+
if: steps.tag.outputs.exists == 'false'
|
|
68
|
+
with:
|
|
69
|
+
python-version: "3.12"
|
|
70
|
+
|
|
71
|
+
- name: Install build tools
|
|
72
|
+
if: steps.tag.outputs.exists == 'false'
|
|
73
|
+
run: pip install --upgrade build
|
|
74
|
+
|
|
75
|
+
- name: Build sdist and wheel
|
|
76
|
+
if: steps.tag.outputs.exists == 'false'
|
|
77
|
+
run: python -m build
|
|
78
|
+
|
|
79
|
+
- name: Publish to PyPI
|
|
80
|
+
if: steps.tag.outputs.exists == 'false'
|
|
81
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
82
|
+
with:
|
|
83
|
+
skip-existing: true
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# See https://pre-commit.com for usage: pip install pre-commit && pre-commit install
|
|
2
|
+
|
|
3
|
+
repos:
|
|
4
|
+
- repo: local
|
|
5
|
+
hooks:
|
|
6
|
+
- id: pytest
|
|
7
|
+
name: pytest (coverage ≥90%)
|
|
8
|
+
entry: python -m pytest
|
|
9
|
+
language: system
|
|
10
|
+
pass_filenames: false
|
|
11
|
+
always_run: true
|
|
12
|
+
args:
|
|
13
|
+
- -q
|
|
14
|
+
- --cov=pyapiclient
|
|
15
|
+
- --cov-fail-under=90
|
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: dynamicapiclient
|
|
3
|
+
Version: 0.1.2
|
|
4
|
+
Summary: pyAPIClient: Django-like dynamic ORM models generated from OpenAPI 2/3 or GraphQL schemas
|
|
5
|
+
Author: pyAPIClient contributors
|
|
6
|
+
License: MIT
|
|
7
|
+
Requires-Python: >=3.10
|
|
8
|
+
Requires-Dist: httpx<1,>=0.27
|
|
9
|
+
Requires-Dist: pyyaml>=6
|
|
10
|
+
Provides-Extra: dev
|
|
11
|
+
Requires-Dist: graphql-core<4,>=3.2; extra == 'dev'
|
|
12
|
+
Requires-Dist: pre-commit>=4; extra == 'dev'
|
|
13
|
+
Requires-Dist: pytest-cov>=5; extra == 'dev'
|
|
14
|
+
Requires-Dist: pytest>=8; extra == 'dev'
|
|
15
|
+
Requires-Dist: respx>=0.21; extra == 'dev'
|
|
16
|
+
Provides-Extra: graphql
|
|
17
|
+
Requires-Dist: graphql-core<4,>=3.2; extra == 'graphql'
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# pyAPIClient
|
|
21
|
+
|
|
22
|
+
[](https://github.com/stuart23/pyapiclient/actions/workflows/ci.yml)
|
|
23
|
+
[](https://codecov.io/gh/stuart23/pyapiclient)
|
|
24
|
+
|
|
25
|
+
Generate **Django-like** model classes from an **OpenAPI 2** or **OpenAPI 3** document, or a **GraphQL schema** (SDL or introspection JSON), as a URL or local file. OpenAPI schemas under `definitions` (v2) or `components.schemas` (v3) become models with `.objects` wired to REST paths. GraphQL **object types** become models whose `.objects` issues `query` / `mutation` operations against a single HTTP endpoint (default `POST /graphql`).
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
The **PyPI distribution** is [`dynamicapiclient`](https://pypi.org/project/dynamicapiclient/) (`pip install dynamicapiclient`). The **Python import package** remains `pyapiclient` (`import pyapiclient`). This project is referred to as **pyAPIClient** in documentation.
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
pip install -e .
|
|
33
|
+
# or, with dev dependencies:
|
|
34
|
+
pip install -e ".[dev]"
|
|
35
|
+
# GraphQL support (graphql-core):
|
|
36
|
+
pip install -e ".[graphql]"
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
Requires Python 3.10+. GraphQL requires the optional `graphql` extra (`graphql-core`).
|
|
40
|
+
|
|
41
|
+
### Tests, coverage, and pre-commit
|
|
42
|
+
|
|
43
|
+
CI-style checks use **≥90%** coverage on `pyapiclient` (see `pyproject.toml`). Run:
|
|
44
|
+
|
|
45
|
+
```bash
|
|
46
|
+
pytest -q --cov=pyapiclient --cov-fail-under=90
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
[GitHub Actions](https://github.com/stuart23/pyapiclient/actions/workflows/ci.yml) runs the same suite on Python 3.10–3.13 and uploads coverage to [**Codecov**](https://codecov.io/gh/stuart23/pyapiclient) via **OIDC** (no `CODECOV_TOKEN` needed on the main repo). Add the project in Codecov once so the badge and graphs populate. Forks or private mirrors may need a **`CODECOV_TOKEN`** secret—see [Codecov’s docs](https://docs.codecov.com/docs/codecov-tokens).
|
|
50
|
+
|
|
51
|
+
With a **Git** checkout, install [`pre-commit`](https://pre-commit.com/) (`pip install pre-commit` or use the `dev` extra) and run `pre-commit install` so commits run the same pytest command via [`.pre-commit-config.yaml`](.pre-commit-config.yaml).
|
|
52
|
+
|
|
53
|
+
## Quick start (fixture spec)
|
|
54
|
+
|
|
55
|
+
This repo includes a sample OpenAPI 3 spec at [`tests/fixtures/library_oas3.yaml`](tests/fixtures/library_oas3.yaml). It describes a small “library” API with `Author` and `Book` schemas and paths under `https://api.example.com/v1`.
|
|
56
|
+
|
|
57
|
+
Load the spec from disk and build the API object:
|
|
58
|
+
|
|
59
|
+
```python
|
|
60
|
+
from pathlib import Path
|
|
61
|
+
|
|
62
|
+
from pyapiclient import api_make # or: from pyapiclient import apiMake
|
|
63
|
+
|
|
64
|
+
spec_path = Path("tests/fixtures/library_oas3.yaml")
|
|
65
|
+
# Or an absolute path on your machine.
|
|
66
|
+
|
|
67
|
+
MyAPI = api_make(spec_path)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Discover generated models (works well in a REPL):
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
dir(MyAPI.models) # ['Author', 'Book']
|
|
74
|
+
list(MyAPI.models) # model classes
|
|
75
|
+
MyAPI.models.model_names() # ('Author', 'Book')
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
The spec’s `servers[0].url` is used as the HTTP base URL unless you override it:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
MyAPI = api_make(spec_path, base_url="http://localhost:8000/v1")
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Creating and reading resources
|
|
85
|
+
|
|
86
|
+
The fixture marks `name` and `email` as required on `Author`, and `title` and `author_id` on `Book`. **Your server must actually implement** the described paths (`POST /authors`, `GET /authors/{author_id}`, `POST /books`, etc.); the YAML file is only the contract.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
author = MyAPI.models.Author.objects.create(
|
|
90
|
+
name="J.R.R. Tolkien",
|
|
91
|
+
email="tolkien@example.com",
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
book = MyAPI.models.Book.objects.create(
|
|
95
|
+
title="The Hobbit",
|
|
96
|
+
author_id=author.pk,
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
**Note:** In `library_oas3.yaml`, `Book` only defines `id`, `title`, and `author_id`. Pass only fields that appear in the schema (extra fields raise validation errors). This fixture uses an integer `author_id`, not a nested `author=` relation object.
|
|
101
|
+
|
|
102
|
+
Other useful calls:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
same = MyAPI.models.Author.objects.get(pk=author.pk)
|
|
106
|
+
for a in MyAPI.models.Author.objects.filter(name="J.R.R. Tolkien"):
|
|
107
|
+
print(a.pk, a._data["email"])
|
|
108
|
+
|
|
109
|
+
MyAPI.models.Author.objects.update(same, email="tolkien@tolkien.estate")
|
|
110
|
+
MyAPI.models.Author.objects.delete(same)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Close the underlying HTTP client when you are done (optional but good practice):
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
MyAPI.close()
|
|
117
|
+
# or: with api_make(...) as MyAPI: ...
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Headers (e.g. auth)
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
MyAPI = api_make(
|
|
124
|
+
spec_path,
|
|
125
|
+
headers={"Authorization": "Bearer YOUR_TOKEN"},
|
|
126
|
+
)
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
### Loading from a URL
|
|
130
|
+
|
|
131
|
+
```python
|
|
132
|
+
MyAPI = api_make("https://example.com/openapi.yaml")
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
### Swagger 2 example
|
|
136
|
+
|
|
137
|
+
[`tests/fixtures/swagger2_library.json`](tests/fixtures/swagger2_library.json) defines a `Widget` model. Load it the same way; Swagger 2 uses `host` + `basePath` + `schemes` for the base URL, or pass `base_url=...` explicitly.
|
|
138
|
+
|
|
139
|
+
## GraphQL schema (SDL or introspection JSON)
|
|
140
|
+
|
|
141
|
+
Install `graphql-core` (`pip install "dynamicapiclient[graphql]"`). Point `api_make` at a `.graphql` / `.gql` file, a JSON introspection export (`data.__schema` or bare `__schema`), or a URL whose body looks like GraphQL SDL or introspection. You **must** pass `base_url=` to the HTTP server root; SDL does not carry a server URL.
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
from pathlib import Path
|
|
145
|
+
|
|
146
|
+
from pyapiclient import api_make
|
|
147
|
+
|
|
148
|
+
schema_path = Path("tests/fixtures/library.graphql")
|
|
149
|
+
GQL = api_make(
|
|
150
|
+
schema_path,
|
|
151
|
+
base_url="https://api.example.com",
|
|
152
|
+
graphql_path="/graphql", # default; POST JSON { "query", "variables" }
|
|
153
|
+
)
|
|
154
|
+
author = GQL.models.Author.objects.create(name="Ada", email="ada@example.com")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
pyAPIClient infers operations using common patterns:
|
|
158
|
+
|
|
159
|
+
- **List**: a `Query` field whose return type is a list of the object type (e.g. `authors: [Author!]!`), optional `filter()` args match declared GraphQL arguments on that field.
|
|
160
|
+
- **Get**: a `Query` field returning the type with an `id: ID!` (or `authorId`-style) argument.
|
|
161
|
+
- **Create / update / delete**: `Mutation` fields whose names start with `create` / `add`, `update` / `edit`, or `delete` / `remove`, with `input` arguments for writes and `ID` arguments where needed.
|
|
162
|
+
|
|
163
|
+
If your API uses different names, pyAPIClient may not find an operation; you will get a clear `PyAPIClientModelError`.
|
|
164
|
+
|
|
165
|
+
## How it works (short)
|
|
166
|
+
|
|
167
|
+
- Schemas become Python types on `api.models.<Name>`.
|
|
168
|
+
- **OpenAPI**: CRUD routes are **inferred** from paths whose bodies or responses reference that schema.
|
|
169
|
+
- **GraphQL**: CRUD maps to `query` / `mutation` documents sent to `graphql_path`, using the heuristics described above.
|
|
170
|
+
- If the spec does not expose a clear operation for a model, calling the missing operation raises a clear `PyAPIClientModelError`.
|
|
171
|
+
|
|
172
|
+
For full behavior and edge cases, see the test suite under `tests/`.
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# pyAPIClient
|
|
2
|
+
|
|
3
|
+
[](https://github.com/stuart23/pyapiclient/actions/workflows/ci.yml)
|
|
4
|
+
[](https://codecov.io/gh/stuart23/pyapiclient)
|
|
5
|
+
|
|
6
|
+
Generate **Django-like** model classes from an **OpenAPI 2** or **OpenAPI 3** document, or a **GraphQL schema** (SDL or introspection JSON), as a URL or local file. OpenAPI schemas under `definitions` (v2) or `components.schemas` (v3) become models with `.objects` wired to REST paths. GraphQL **object types** become models whose `.objects` issues `query` / `mutation` operations against a single HTTP endpoint (default `POST /graphql`).
|
|
7
|
+
|
|
8
|
+
## Install
|
|
9
|
+
|
|
10
|
+
The **PyPI distribution** is [`dynamicapiclient`](https://pypi.org/project/dynamicapiclient/) (`pip install dynamicapiclient`). The **Python import package** remains `pyapiclient` (`import pyapiclient`). This project is referred to as **pyAPIClient** in documentation.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
pip install -e .
|
|
14
|
+
# or, with dev dependencies:
|
|
15
|
+
pip install -e ".[dev]"
|
|
16
|
+
# GraphQL support (graphql-core):
|
|
17
|
+
pip install -e ".[graphql]"
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
Requires Python 3.10+. GraphQL requires the optional `graphql` extra (`graphql-core`).
|
|
21
|
+
|
|
22
|
+
### Tests, coverage, and pre-commit
|
|
23
|
+
|
|
24
|
+
CI-style checks use **≥90%** coverage on `pyapiclient` (see `pyproject.toml`). Run:
|
|
25
|
+
|
|
26
|
+
```bash
|
|
27
|
+
pytest -q --cov=pyapiclient --cov-fail-under=90
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
[GitHub Actions](https://github.com/stuart23/pyapiclient/actions/workflows/ci.yml) runs the same suite on Python 3.10–3.13 and uploads coverage to [**Codecov**](https://codecov.io/gh/stuart23/pyapiclient) via **OIDC** (no `CODECOV_TOKEN` needed on the main repo). Add the project in Codecov once so the badge and graphs populate. Forks or private mirrors may need a **`CODECOV_TOKEN`** secret—see [Codecov’s docs](https://docs.codecov.com/docs/codecov-tokens).
|
|
31
|
+
|
|
32
|
+
With a **Git** checkout, install [`pre-commit`](https://pre-commit.com/) (`pip install pre-commit` or use the `dev` extra) and run `pre-commit install` so commits run the same pytest command via [`.pre-commit-config.yaml`](.pre-commit-config.yaml).
|
|
33
|
+
|
|
34
|
+
## Quick start (fixture spec)
|
|
35
|
+
|
|
36
|
+
This repo includes a sample OpenAPI 3 spec at [`tests/fixtures/library_oas3.yaml`](tests/fixtures/library_oas3.yaml). It describes a small “library” API with `Author` and `Book` schemas and paths under `https://api.example.com/v1`.
|
|
37
|
+
|
|
38
|
+
Load the spec from disk and build the API object:
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from pathlib import Path
|
|
42
|
+
|
|
43
|
+
from pyapiclient import api_make # or: from pyapiclient import apiMake
|
|
44
|
+
|
|
45
|
+
spec_path = Path("tests/fixtures/library_oas3.yaml")
|
|
46
|
+
# Or an absolute path on your machine.
|
|
47
|
+
|
|
48
|
+
MyAPI = api_make(spec_path)
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
Discover generated models (works well in a REPL):
|
|
52
|
+
|
|
53
|
+
```python
|
|
54
|
+
dir(MyAPI.models) # ['Author', 'Book']
|
|
55
|
+
list(MyAPI.models) # model classes
|
|
56
|
+
MyAPI.models.model_names() # ('Author', 'Book')
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The spec’s `servers[0].url` is used as the HTTP base URL unless you override it:
|
|
60
|
+
|
|
61
|
+
```python
|
|
62
|
+
MyAPI = api_make(spec_path, base_url="http://localhost:8000/v1")
|
|
63
|
+
```
|
|
64
|
+
|
|
65
|
+
### Creating and reading resources
|
|
66
|
+
|
|
67
|
+
The fixture marks `name` and `email` as required on `Author`, and `title` and `author_id` on `Book`. **Your server must actually implement** the described paths (`POST /authors`, `GET /authors/{author_id}`, `POST /books`, etc.); the YAML file is only the contract.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
author = MyAPI.models.Author.objects.create(
|
|
71
|
+
name="J.R.R. Tolkien",
|
|
72
|
+
email="tolkien@example.com",
|
|
73
|
+
)
|
|
74
|
+
|
|
75
|
+
book = MyAPI.models.Book.objects.create(
|
|
76
|
+
title="The Hobbit",
|
|
77
|
+
author_id=author.pk,
|
|
78
|
+
)
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
**Note:** In `library_oas3.yaml`, `Book` only defines `id`, `title`, and `author_id`. Pass only fields that appear in the schema (extra fields raise validation errors). This fixture uses an integer `author_id`, not a nested `author=` relation object.
|
|
82
|
+
|
|
83
|
+
Other useful calls:
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
same = MyAPI.models.Author.objects.get(pk=author.pk)
|
|
87
|
+
for a in MyAPI.models.Author.objects.filter(name="J.R.R. Tolkien"):
|
|
88
|
+
print(a.pk, a._data["email"])
|
|
89
|
+
|
|
90
|
+
MyAPI.models.Author.objects.update(same, email="tolkien@tolkien.estate")
|
|
91
|
+
MyAPI.models.Author.objects.delete(same)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
Close the underlying HTTP client when you are done (optional but good practice):
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
MyAPI.close()
|
|
98
|
+
# or: with api_make(...) as MyAPI: ...
|
|
99
|
+
```
|
|
100
|
+
|
|
101
|
+
### Headers (e.g. auth)
|
|
102
|
+
|
|
103
|
+
```python
|
|
104
|
+
MyAPI = api_make(
|
|
105
|
+
spec_path,
|
|
106
|
+
headers={"Authorization": "Bearer YOUR_TOKEN"},
|
|
107
|
+
)
|
|
108
|
+
```
|
|
109
|
+
|
|
110
|
+
### Loading from a URL
|
|
111
|
+
|
|
112
|
+
```python
|
|
113
|
+
MyAPI = api_make("https://example.com/openapi.yaml")
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
### Swagger 2 example
|
|
117
|
+
|
|
118
|
+
[`tests/fixtures/swagger2_library.json`](tests/fixtures/swagger2_library.json) defines a `Widget` model. Load it the same way; Swagger 2 uses `host` + `basePath` + `schemes` for the base URL, or pass `base_url=...` explicitly.
|
|
119
|
+
|
|
120
|
+
## GraphQL schema (SDL or introspection JSON)
|
|
121
|
+
|
|
122
|
+
Install `graphql-core` (`pip install "dynamicapiclient[graphql]"`). Point `api_make` at a `.graphql` / `.gql` file, a JSON introspection export (`data.__schema` or bare `__schema`), or a URL whose body looks like GraphQL SDL or introspection. You **must** pass `base_url=` to the HTTP server root; SDL does not carry a server URL.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
from pathlib import Path
|
|
126
|
+
|
|
127
|
+
from pyapiclient import api_make
|
|
128
|
+
|
|
129
|
+
schema_path = Path("tests/fixtures/library.graphql")
|
|
130
|
+
GQL = api_make(
|
|
131
|
+
schema_path,
|
|
132
|
+
base_url="https://api.example.com",
|
|
133
|
+
graphql_path="/graphql", # default; POST JSON { "query", "variables" }
|
|
134
|
+
)
|
|
135
|
+
author = GQL.models.Author.objects.create(name="Ada", email="ada@example.com")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
pyAPIClient infers operations using common patterns:
|
|
139
|
+
|
|
140
|
+
- **List**: a `Query` field whose return type is a list of the object type (e.g. `authors: [Author!]!`), optional `filter()` args match declared GraphQL arguments on that field.
|
|
141
|
+
- **Get**: a `Query` field returning the type with an `id: ID!` (or `authorId`-style) argument.
|
|
142
|
+
- **Create / update / delete**: `Mutation` fields whose names start with `create` / `add`, `update` / `edit`, or `delete` / `remove`, with `input` arguments for writes and `ID` arguments where needed.
|
|
143
|
+
|
|
144
|
+
If your API uses different names, pyAPIClient may not find an operation; you will get a clear `PyAPIClientModelError`.
|
|
145
|
+
|
|
146
|
+
## How it works (short)
|
|
147
|
+
|
|
148
|
+
- Schemas become Python types on `api.models.<Name>`.
|
|
149
|
+
- **OpenAPI**: CRUD routes are **inferred** from paths whose bodies or responses reference that schema.
|
|
150
|
+
- **GraphQL**: CRUD maps to `query` / `mutation` documents sent to `graphql_path`, using the heuristics described above.
|
|
151
|
+
- If the spec does not expose a clear operation for a model, calling the missing operation raises a clear `PyAPIClientModelError`.
|
|
152
|
+
|
|
153
|
+
For full behavior and edge cases, see the test suite under `tests/`.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "dynamicapiclient"
|
|
7
|
+
version = "0.1.2"
|
|
8
|
+
description = "pyAPIClient: Django-like dynamic ORM models generated from OpenAPI 2/3 or GraphQL schemas"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.10"
|
|
11
|
+
license = { text = "MIT" }
|
|
12
|
+
authors = [{ name = "pyAPIClient contributors" }]
|
|
13
|
+
dependencies = [
|
|
14
|
+
"httpx>=0.27,<1",
|
|
15
|
+
"PyYAML>=6",
|
|
16
|
+
]
|
|
17
|
+
|
|
18
|
+
[project.optional-dependencies]
|
|
19
|
+
graphql = [
|
|
20
|
+
"graphql-core>=3.2,<4",
|
|
21
|
+
]
|
|
22
|
+
dev = [
|
|
23
|
+
"pytest>=8",
|
|
24
|
+
"pytest-cov>=5",
|
|
25
|
+
"respx>=0.21",
|
|
26
|
+
"graphql-core>=3.2,<4",
|
|
27
|
+
"pre-commit>=4",
|
|
28
|
+
]
|
|
29
|
+
|
|
30
|
+
[tool.hatch.build.targets.wheel]
|
|
31
|
+
packages = ["src/pyapiclient"]
|
|
32
|
+
|
|
33
|
+
[tool.pytest.ini_options]
|
|
34
|
+
testpaths = ["tests"]
|
|
35
|
+
pythonpath = ["src"]
|
|
36
|
+
addopts = "-q --cov=pyapiclient --cov-report=term-missing --cov-fail-under=90"
|
|
37
|
+
|
|
38
|
+
[tool.coverage.run]
|
|
39
|
+
branch = true
|
|
40
|
+
source_pkgs = ["pyapiclient"]
|
|
41
|
+
|
|
42
|
+
[tool.coverage.report]
|
|
43
|
+
exclude_lines = [
|
|
44
|
+
"pragma: no cover",
|
|
45
|
+
"if TYPE_CHECKING:",
|
|
46
|
+
]
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
"""Dynamic ORM-style API clients from OpenAPI 2/3 or GraphQL schemas."""
|
|
2
|
+
|
|
3
|
+
from pyapiclient.api import api_make
|
|
4
|
+
from pyapiclient.exceptions import (
|
|
5
|
+
PyAPIClientConfigurationError,
|
|
6
|
+
PyAPIClientError,
|
|
7
|
+
PyAPIClientHTTPError,
|
|
8
|
+
PyAPIClientModelError,
|
|
9
|
+
PyAPIClientSpecError,
|
|
10
|
+
PyAPIClientValidationError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
__all__ = [
|
|
14
|
+
"api_make",
|
|
15
|
+
"PyAPIClientError",
|
|
16
|
+
"PyAPIClientSpecError",
|
|
17
|
+
"PyAPIClientConfigurationError",
|
|
18
|
+
"PyAPIClientHTTPError",
|
|
19
|
+
"PyAPIClientValidationError",
|
|
20
|
+
"PyAPIClientModelError",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
# Django-style alias (user-facing example uses camelCase apiMake)
|
|
24
|
+
apiMake = api_make
|