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.
Files changed (37) hide show
  1. dynamicapiclient-0.1.2/.cursor/rules/test-coverage.mdc +10 -0
  2. dynamicapiclient-0.1.2/.github/workflows/ci.yml +46 -0
  3. dynamicapiclient-0.1.2/.github/workflows/publish-pypi.yml +48 -0
  4. dynamicapiclient-0.1.2/.github/workflows/release-from-version.yml +83 -0
  5. dynamicapiclient-0.1.2/.gitignore +15 -0
  6. dynamicapiclient-0.1.2/.pre-commit-config.yaml +15 -0
  7. dynamicapiclient-0.1.2/PKG-INFO +172 -0
  8. dynamicapiclient-0.1.2/README.md +153 -0
  9. dynamicapiclient-0.1.2/pyproject.toml +46 -0
  10. dynamicapiclient-0.1.2/src/pyapiclient/__init__.py +24 -0
  11. dynamicapiclient-0.1.2/src/pyapiclient/api.py +316 -0
  12. dynamicapiclient-0.1.2/src/pyapiclient/client.py +115 -0
  13. dynamicapiclient-0.1.2/src/pyapiclient/exceptions.py +34 -0
  14. dynamicapiclient-0.1.2/src/pyapiclient/graphql_support.py +505 -0
  15. dynamicapiclient-0.1.2/src/pyapiclient/loader.py +164 -0
  16. dynamicapiclient-0.1.2/src/pyapiclient/models.py +429 -0
  17. dynamicapiclient-0.1.2/src/pyapiclient/routing.py +240 -0
  18. dynamicapiclient-0.1.2/src/pyapiclient/spec.py +126 -0
  19. dynamicapiclient-0.1.2/src/pyapiclient/validation.py +88 -0
  20. dynamicapiclient-0.1.2/tests/conftest.py +22 -0
  21. dynamicapiclient-0.1.2/tests/fixtures/library.graphql +42 -0
  22. dynamicapiclient-0.1.2/tests/fixtures/library_oas3.yaml +116 -0
  23. dynamicapiclient-0.1.2/tests/fixtures/swagger2_library.json +65 -0
  24. dynamicapiclient-0.1.2/tests/test_api.py +169 -0
  25. dynamicapiclient-0.1.2/tests/test_api_errors.py +35 -0
  26. dynamicapiclient-0.1.2/tests/test_api_sniff.py +84 -0
  27. dynamicapiclient-0.1.2/tests/test_api_url.py +69 -0
  28. dynamicapiclient-0.1.2/tests/test_client.py +88 -0
  29. dynamicapiclient-0.1.2/tests/test_coverage_extra.py +337 -0
  30. dynamicapiclient-0.1.2/tests/test_exceptions.py +15 -0
  31. dynamicapiclient-0.1.2/tests/test_graphql.py +340 -0
  32. dynamicapiclient-0.1.2/tests/test_loader.py +124 -0
  33. dynamicapiclient-0.1.2/tests/test_loader_extra.py +57 -0
  34. dynamicapiclient-0.1.2/tests/test_models.py +394 -0
  35. dynamicapiclient-0.1.2/tests/test_routing.py +266 -0
  36. dynamicapiclient-0.1.2/tests/test_spec.py +179 -0
  37. 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
+ .venv/
2
+ __pycache__/
3
+ *.py[cod]
4
+ *$py.class
5
+ .pytest_cache/
6
+ .coverage
7
+ coverage.xml
8
+ htmlcov/
9
+ dist/
10
+ *.egg-info/
11
+ .eggs/
12
+ *.egg
13
+ .mypy_cache/
14
+ .ruff_cache/
15
+ .DS_Store
@@ -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
+ [![CI](https://github.com/stuart23/pyapiclient/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/stuart23/pyapiclient/actions/workflows/ci.yml)
23
+ [![codecov](https://codecov.io/gh/stuart23/pyapiclient/graph/badge.svg?branch=main)](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
+ [![CI](https://github.com/stuart23/pyapiclient/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/stuart23/pyapiclient/actions/workflows/ci.yml)
4
+ [![codecov](https://codecov.io/gh/stuart23/pyapiclient/graph/badge.svg?branch=main)](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