gazebo 0.1.0__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 (82) hide show
  1. gazebo-0.1.0/.github/dependabot.yml +11 -0
  2. gazebo-0.1.0/.github/workflows/ci.yml +43 -0
  3. gazebo-0.1.0/.github/workflows/docs.yml +73 -0
  4. gazebo-0.1.0/.github/workflows/release.yml +47 -0
  5. gazebo-0.1.0/.gitignore +127 -0
  6. gazebo-0.1.0/.pre-commit-config.yaml +58 -0
  7. gazebo-0.1.0/CHANGELOG.md +27 -0
  8. gazebo-0.1.0/CLAUDE.md +173 -0
  9. gazebo-0.1.0/CONTRIBUTING.md +130 -0
  10. gazebo-0.1.0/LICENSE +21 -0
  11. gazebo-0.1.0/PKG-INFO +239 -0
  12. gazebo-0.1.0/README.md +225 -0
  13. gazebo-0.1.0/docs/core/collections.md +45 -0
  14. gazebo-0.1.0/docs/core/constants.md +34 -0
  15. gazebo-0.1.0/docs/core/context.md +63 -0
  16. gazebo-0.1.0/docs/core/index.md +33 -0
  17. gazebo-0.1.0/docs/core/links.md +50 -0
  18. gazebo-0.1.0/docs/core/ogc.md +34 -0
  19. gazebo-0.1.0/docs/core/problems.md +43 -0
  20. gazebo-0.1.0/docs/di/index.md +52 -0
  21. gazebo-0.1.0/docs/di/providers.md +57 -0
  22. gazebo-0.1.0/docs/di/qualifiers-overrides.md +34 -0
  23. gazebo-0.1.0/docs/di/scopes.md +64 -0
  24. gazebo-0.1.0/docs/example.md +61 -0
  25. gazebo-0.1.0/docs/fastapi/app.md +78 -0
  26. gazebo-0.1.0/docs/fastapi/index.md +43 -0
  27. gazebo-0.1.0/docs/fastapi/proxy.md +64 -0
  28. gazebo-0.1.0/docs/fastapi/routers.md +55 -0
  29. gazebo-0.1.0/docs/getting-started.md +55 -0
  30. gazebo-0.1.0/docs/index.md +51 -0
  31. gazebo-0.1.0/docs/reference.md +31 -0
  32. gazebo-0.1.0/examples/garden/README.md +96 -0
  33. gazebo-0.1.0/examples/garden/garden/__init__.py +1 -0
  34. gazebo-0.1.0/examples/garden/garden/__main__.py +8 -0
  35. gazebo-0.1.0/examples/garden/garden/api.py +106 -0
  36. gazebo-0.1.0/examples/garden/garden/app.py +95 -0
  37. gazebo-0.1.0/examples/garden/garden/models.py +38 -0
  38. gazebo-0.1.0/examples/garden/garden/resources.py +164 -0
  39. gazebo-0.1.0/examples/garden/pyproject.toml +43 -0
  40. gazebo-0.1.0/examples/garden/tests/test_app.py +110 -0
  41. gazebo-0.1.0/pyproject.toml +133 -0
  42. gazebo-0.1.0/src/gazebo/__init__.py +28 -0
  43. gazebo-0.1.0/src/gazebo/__version__.py +24 -0
  44. gazebo-0.1.0/src/gazebo/asgi.py +146 -0
  45. gazebo-0.1.0/src/gazebo/collection.py +48 -0
  46. gazebo-0.1.0/src/gazebo/context.py +107 -0
  47. gazebo-0.1.0/src/gazebo/di/__init__.py +41 -0
  48. gazebo-0.1.0/src/gazebo/di/container.py +366 -0
  49. gazebo-0.1.0/src/gazebo/di/providers.py +193 -0
  50. gazebo-0.1.0/src/gazebo/ext/__init__.py +1 -0
  51. gazebo-0.1.0/src/gazebo/ext/fastapi.py +508 -0
  52. gazebo-0.1.0/src/gazebo/link.py +143 -0
  53. gazebo-0.1.0/src/gazebo/ogc.py +64 -0
  54. gazebo-0.1.0/src/gazebo/pagination.py +65 -0
  55. gazebo-0.1.0/src/gazebo/problems.py +64 -0
  56. gazebo-0.1.0/src/gazebo/py.typed +0 -0
  57. gazebo-0.1.0/src/gazebo/rels.py +47 -0
  58. gazebo-0.1.0/src/gazebo/tags.py +32 -0
  59. gazebo-0.1.0/tests/conftest.py +24 -0
  60. gazebo-0.1.0/tests/examples/__init__.py +7 -0
  61. gazebo-0.1.0/tests/examples/app.py +89 -0
  62. gazebo-0.1.0/tests/examples/collections.py +46 -0
  63. gazebo-0.1.0/tests/examples/constants.py +28 -0
  64. gazebo-0.1.0/tests/examples/context.py +50 -0
  65. gazebo-0.1.0/tests/examples/getting_started.py +67 -0
  66. gazebo-0.1.0/tests/examples/links.py +54 -0
  67. gazebo-0.1.0/tests/examples/ogc.py +46 -0
  68. gazebo-0.1.0/tests/examples/problems.py +27 -0
  69. gazebo-0.1.0/tests/examples/providers.py +87 -0
  70. gazebo-0.1.0/tests/examples/proxy.py +74 -0
  71. gazebo-0.1.0/tests/examples/qualifiers_overrides.py +65 -0
  72. gazebo-0.1.0/tests/examples/routers.py +95 -0
  73. gazebo-0.1.0/tests/examples/scopes.py +43 -0
  74. gazebo-0.1.0/tests/test_asgi.py +87 -0
  75. gazebo-0.1.0/tests/test_collection.py +50 -0
  76. gazebo-0.1.0/tests/test_core_ogc.py +91 -0
  77. gazebo-0.1.0/tests/test_di.py +206 -0
  78. gazebo-0.1.0/tests/test_examples.py +23 -0
  79. gazebo-0.1.0/tests/test_fastapi.py +285 -0
  80. gazebo-0.1.0/tests/test_link.py +56 -0
  81. gazebo-0.1.0/uv.lock +1284 -0
  82. gazebo-0.1.0/zensical.toml +110 -0
@@ -0,0 +1,11 @@
1
+ version: 2
2
+ updates:
3
+ # Keep the SHA-pinned GitHub Actions (and their version comments) up to date.
4
+ - package-ecosystem: "github-actions"
5
+ directory: "/"
6
+ schedule:
7
+ interval: "weekly"
8
+ groups:
9
+ github-actions:
10
+ patterns:
11
+ - "*"
@@ -0,0 +1,43 @@
1
+ name: "Continuous integration"
2
+
3
+ concurrency:
4
+ group: ${{ github.ref }}
5
+ cancel-in-progress: false
6
+
7
+ on:
8
+ push:
9
+ branches:
10
+ - main
11
+ pull_request:
12
+
13
+ jobs:
14
+ ci:
15
+ name: Continuous integration
16
+ runs-on: ubuntu-latest
17
+ strategy:
18
+ matrix:
19
+ python-version:
20
+ - "3.12"
21
+ - "3.13"
22
+ - "3.14"
23
+ steps:
24
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
25
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
26
+ with:
27
+ python-version: ${{ matrix.python-version }}
28
+ - name: Sync
29
+ run: uv sync --locked --all-extras --all-packages --no-editable
30
+ - name: Pre-Commit Hooks
31
+ run: uv run pre-commit run --all-files
32
+ - name: Test
33
+ run: uv run pytest
34
+ - name: Test example
35
+ run: uv run --no-sync pytest
36
+ working-directory: examples/garden
37
+ - name: "Upload coverage to Codecov"
38
+ uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0
39
+ env:
40
+ CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
41
+ with:
42
+ fail_ci_if_error: false
43
+ verbose: true
@@ -0,0 +1,73 @@
1
+ name: Documentation
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ release:
9
+ types:
10
+ - published
11
+ workflow_dispatch:
12
+
13
+ # Allow only one concurrent deployment, to avoid mike/gh-pages races.
14
+ concurrency:
15
+ group: docs-${{ github.ref }}
16
+ cancel-in-progress: false
17
+
18
+ permissions:
19
+ contents: write
20
+
21
+ jobs:
22
+ # On pull requests, just confirm the docs build cleanly (no deploy).
23
+ build:
24
+ if: github.event_name == 'pull_request'
25
+ runs-on: ubuntu-latest
26
+ steps:
27
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
28
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
29
+ with:
30
+ python-version: "3.14"
31
+ - name: Sync
32
+ run: uv sync --locked --all-extras --group docs
33
+ - name: Build (strict)
34
+ run: uv run zensical build --strict --clean
35
+
36
+ deploy:
37
+ if: github.event_name != 'pull_request'
38
+ runs-on: ubuntu-latest
39
+ steps:
40
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
41
+ with:
42
+ fetch-depth: 0 # full history so mike can update the gh-pages branch
43
+
44
+ - name: Configure git
45
+ run: |
46
+ git config user.name "github-actions[bot]"
47
+ git config user.email "github-actions[bot]@users.noreply.github.com"
48
+
49
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
50
+ with:
51
+ python-version: "3.14"
52
+
53
+ - name: Sync
54
+ run: uv sync --locked --all-extras --group docs
55
+
56
+ # mike (squidfunk's Zensical-aware fork) builds via `zensical build` and
57
+ # commits each version to the gh-pages branch.
58
+ - name: Deploy dev version
59
+ if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
60
+ run: uv run mike deploy --push --update-aliases dev latest
61
+
62
+ - name: Deploy release version
63
+ if: github.event_name == 'release' && !github.event.release.prerelease
64
+ run: |
65
+ VERSION="${{ github.event.release.tag_name }}"
66
+ uv run mike deploy --push --update-aliases "$VERSION" stable
67
+ uv run mike set-default --push stable
68
+
69
+ - name: Deploy pre-release version
70
+ if: github.event_name == 'release' && github.event.release.prerelease
71
+ run: |
72
+ VERSION="${{ github.event.release.tag_name }}"
73
+ uv run mike deploy --push "$VERSION"
@@ -0,0 +1,47 @@
1
+ name: Build and release
2
+
3
+ on:
4
+ push:
5
+ branches:
6
+ - main
7
+ pull_request:
8
+ branches:
9
+ - main
10
+ release:
11
+ types:
12
+ - published
13
+
14
+ jobs:
15
+ build-package:
16
+ runs-on: ubuntu-latest
17
+ steps:
18
+ - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
19
+ - uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
20
+ - name: Build package
21
+ run: uv build
22
+ - name: Upload artifact
23
+ uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
24
+ if: startsWith(github.ref, 'refs/tags')
25
+ with:
26
+ name: dist-${{ github.ref_name }}
27
+ path: dist/
28
+ overwrite: true
29
+ if-no-files-found: error
30
+
31
+ release-package:
32
+ if: startsWith(github.ref, 'refs/tags')
33
+ needs: build-package
34
+ runs-on: ubuntu-latest
35
+ environment:
36
+ name: pypi
37
+ url: https://pypi.org/p/gazebo
38
+ permissions:
39
+ id-token: write
40
+ steps:
41
+ - name: Download artifact
42
+ uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v5.0.0
43
+ with:
44
+ name: dist-${{ github.ref_name }}
45
+ path: dist/
46
+ - name: Upload release
47
+ uses: pypa/gh-action-pypi-publish@cef221092ed1bacb1cc03d23a2d87d1d172e277b # v1.14.0
@@ -0,0 +1,127 @@
1
+ __version__.py
2
+
3
+ # Byte-compiled / optimized / DLL files
4
+ __pycache__/
5
+ *.py[cod]
6
+ *$py.class
7
+
8
+ # C extensions
9
+ *.so
10
+
11
+ # Distribution / packaging
12
+ .Python
13
+ build/
14
+ develop-eggs/
15
+ dist/
16
+ downloads/
17
+ eggs/
18
+ .eggs/
19
+ lib/
20
+ lib64/
21
+ parts/
22
+ sdist/
23
+ var/
24
+ wheels/
25
+ share/python-wheels/
26
+ *.egg-info/
27
+ .installed.cfg
28
+ *.egg
29
+ MANIFEST
30
+
31
+ # PyInstaller
32
+ # Usually these files are written by a python script from a template
33
+ # before PyInstaller builds the exe, so as to inject date/other infos into it.
34
+ *.manifest
35
+ *.spec
36
+
37
+ # Installer logs
38
+ pip-log.txt
39
+ pip-delete-this-directory.txt
40
+
41
+ # Unit test / coverage reports
42
+ htmlcov/
43
+ .tox/
44
+ .nox/
45
+ .coverage
46
+ .coverage.*
47
+ .cache
48
+ nosetests.xml
49
+ coverage.xml
50
+ *.cover
51
+ *.py,cover
52
+ .hypothesis/
53
+ .pytest_cache/
54
+ cover/
55
+
56
+ # Translations
57
+ *.mo
58
+ *.pot
59
+
60
+ # Scrapy stuff:
61
+ .scrapy
62
+
63
+ # Sphinx documentation
64
+ docs/_build/
65
+
66
+ # PyBuilder
67
+ .pybuilder/
68
+ target/
69
+
70
+ # Jupyter Notebook
71
+ .ipynb_checkpoints
72
+
73
+ # IPython
74
+ profile_default/
75
+ ipython_config.py
76
+
77
+ # pyenv
78
+ # For a library or package, you might want to ignore these files since the code is
79
+ # intended to run in multiple environments; otherwise, check them in:
80
+ .python-version
81
+
82
+ # pdm
83
+ # Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
84
+ #pdm.lock
85
+ # pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
86
+ # in version control.
87
+ # https://pdm.fming.dev/latest/usage/project/#working-with-version-control
88
+ .pdm.toml
89
+ .pdm-python
90
+ .pdm-build/
91
+
92
+ # PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
93
+ __pypackages__/
94
+
95
+ # Environments
96
+ .env
97
+ .env.*
98
+ .venv
99
+ env/
100
+ venv/
101
+ ENV/
102
+ env.bak/
103
+ venv.bak/
104
+
105
+ # Spyder project settings
106
+ .spyderproject
107
+ .spyproject
108
+
109
+ # Rope project settings
110
+ .ropeproject
111
+
112
+ # mkdocs documentation
113
+ /site
114
+
115
+ # mypy
116
+ .mypy_cache/
117
+ .dmypy.json
118
+ dmypy.json
119
+
120
+ # Pyre type checker
121
+ .pyre/
122
+
123
+ # pytype static type analyzer
124
+ .pytype/
125
+
126
+ # Cython debug symbols
127
+ cython_debug/
@@ -0,0 +1,58 @@
1
+ repos:
2
+ - repo: local
3
+ hooks:
4
+ - id: ruff_check
5
+ name: ruff check
6
+ entry: ruff check --force-exclude
7
+ language: python
8
+ 'types_or': [python, pyi]
9
+ args: [--fix, --exit-non-zero-on-fix]
10
+ require_serial: true
11
+ - id: ruff_format
12
+ name: ruff format
13
+ entry: ruff format --force-exclude
14
+ language: python
15
+ 'types_or': [python, pyi]
16
+ args: []
17
+ require_serial: true
18
+ - id: check-added-large-files
19
+ name: Check for added large files
20
+ entry: check-added-large-files
21
+ language: system
22
+ - id: check-toml
23
+ name: Check Toml
24
+ entry: check-toml
25
+ language: system
26
+ types: [toml]
27
+ - id: check-yaml
28
+ name: Check Yaml
29
+ entry: check-yaml
30
+ language: system
31
+ types: [yaml]
32
+ - id: end-of-file-fixer
33
+ name: Fix End of Files
34
+ entry: end-of-file-fixer
35
+ language: system
36
+ types: [text]
37
+ stages: [pre-commit, pre-push, manual]
38
+ - id: trailing-whitespace
39
+ name: Trim Trailing Whitespace
40
+ entry: trailing-whitespace-fixer
41
+ language: system
42
+ types: [text]
43
+ stages: [pre-commit, pre-push, manual]
44
+ - id: mypy
45
+ name: mypy
46
+ entry: mypy
47
+ language: python
48
+ 'types_or': [python, pyi]
49
+ args: []
50
+ require_serial: true
51
+ - id: pyright
52
+ name: pyright
53
+ entry: pyright
54
+ language: system
55
+ 'types_or': [python, pyi]
56
+ pass_filenames: false
57
+ args: [src, tests]
58
+ require_serial: true
@@ -0,0 +1,27 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
7
+
8
+ ## [unreleased]
9
+
10
+ ### Added
11
+
12
+ ### Changed
13
+
14
+ ### Deprecated
15
+
16
+ ### Removed
17
+
18
+ ### Fixed
19
+
20
+ ### Security
21
+
22
+ ## [0.1.0] - XXXX-XX-XX
23
+
24
+ Initial release 🎉
25
+
26
+ [unreleased]: https://github.com/jkeifer/gazebo/compare/v0.1.0...HEAD
27
+ [0.1.0]: https://github.com/jkeifer/gazebo/releases/tag/v0.1.0
gazebo-0.1.0/CLAUDE.md ADDED
@@ -0,0 +1,173 @@
1
+ # CLAUDE.md
2
+
3
+ This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4
+
5
+ ## What this is
6
+
7
+ gazebo packages the recurring machinery of OGC-style REST APIs (deferred links,
8
+ collection envelopes, RFC 7807 problems, proxy-aware URLs, a typed DI container, and
9
+ FastAPI glue) so it isn't re-implemented per project. The core depends only
10
+ on `pydantic`; framework integration is opt-in via extras. Requires Python 3.12+.
11
+
12
+ ## Commands
13
+
14
+ The project is `uv`-managed and is a `uv` workspace whose only member is
15
+ `examples/garden`. Run everything through `uv`.
16
+
17
+ ```sh
18
+ uv sync --all-extras --all-packages # install (CI also uses --locked --no-editable)
19
+ uv run pytest # run the library test suite (tests/)
20
+ uv run pytest tests/test_link.py::test_name # a single test
21
+ uv run pre-commit run --all-files # ruff check+format, mypy, pyright, file hygiene
22
+ ```
23
+
24
+ `uv run pytest` always reports coverage on the `gazebo` package (`addopts=--cov=gazebo`
25
+ in `pyproject.toml`) and treats warnings as errors (`filterwarnings = ['error']`).
26
+
27
+ The example app is its **own** project under `examples/garden` with its own test suite
28
+ and entry point; run them from that directory:
29
+
30
+ ```sh
31
+ cd examples/garden
32
+ uv run garden # serve Gazebo Gardens on http://127.0.0.1:8000
33
+ uv run pytest # the example's tests
34
+ ```
35
+
36
+ Type checking is enforced by **both** mypy and pyright (pyright runs over `src` and
37
+ `tests`); both run in pre-commit and CI must be green on Python 3.12–3.14.
38
+
39
+ ## Architecture
40
+
41
+ The codebase is strictly layered and **dependencies only ever point downward**. The
42
+ two load-bearing ideas are *deferred links* and a *scoped DI container*; understand
43
+ those two seams and the rest follows.
44
+
45
+ ### Layers (`src/gazebo/`)
46
+
47
+ 1. **Core** (`context.py`, `link.py`, `collection.py`, `pagination.py`, `rels.py`,
48
+ `problems.py`, `ogc.py`) — pydantic + stdlib only. **Never imports a web
49
+ framework.** Pure models and the context seam.
50
+ 2. **DI core** (`di/`) — stdlib only, no web framework. A standalone, extraction-ready
51
+ container behind a `Providers` interface.
52
+ 3. **Pure ASGI** (`asgi.py`) — proxy-header middleware and context-setting middleware;
53
+ no framework import.
54
+ 4. **Framework glue** (`ext/fastapi.py`) — the only module that imports `fastapi`.
55
+ Wires the DI container and the link context into a real app. Importing it requires
56
+ the `gazebo[fastapi]` extra.
57
+
58
+ When adding code, respect the layer: anything a lower layer needs must not import
59
+ upward, and the core must stay framework-free.
60
+
61
+ ### Deferred links (the central trick)
62
+
63
+ A `Link.href` may be a plain URL **or** a callable taking a `RequestContext` and
64
+ returning a URL. Callables are resolved at **JSON serialization time**, so links (and
65
+ whole `LinkedCollection`s) are fully constructible in business logic with no request in
66
+ hand.
67
+
68
+ - `gazebo/context.py` defines the `RequestContext` Protocol (the minimal surface link
69
+ factories need: `base_url`, `url`, `query_params`, `url_for`) and delivers it
70
+ ambiently via the `link_context` ContextVar.
71
+ - The framework glue sets that ContextVar per request (`use_context`). For manual dumps
72
+ / tests there is a fallback: `model_dump(context={'request': ctx})`, resolved by
73
+ `resolve_context`.
74
+ - `Link` factories (`self_link`, `root_link`, `to_route`) build callable hrefs that
75
+ call back into the context — they stay framework-agnostic.
76
+
77
+ A consequence: a callable-href link only serializes correctly inside an active request
78
+ (or with an explicit dump context). Serializing one with no context raises a clear
79
+ `ValueError` by design.
80
+
81
+ ### DI container (`di/`)
82
+
83
+ - `providers.py` — registration. A **recipe** is a callable that builds a value, keyed
84
+ by the type it produces; it may be colocated as a `__provide__` classmethod on the
85
+ type, or supplied standalone for external types. **Scope is a wiring decision bound
86
+ at registration, never a property of the type** (`providers.app(T)` /
87
+ `providers.request(T)`). Recipes may be sync/async functions, (async) generators, or
88
+ (async) context managers; generators are auto-wrapped as CMs. `Qualify` disambiguates
89
+ duplicate types; `Overrides` is a typed replacement layer (the test-override
90
+ mechanism — by parameter, never by mutating a global).
91
+ - `container.py` — the resolution engine. Resolves a recipe's dependencies by the
92
+ **types of its parameters**; a parameter typed as a scope's *root* (e.g. the request
93
+ object) receives that root. Each entered scope owns a resolution cache and an
94
+ `AsyncExitStack` for teardown. Errors are specific: `UnresolvedDependencyError`,
95
+ `ScopeMismatchError`, `CircularDependencyError`.
96
+
97
+ ### How the glue ties it together (`ext/fastapi.py`)
98
+
99
+ `GazeboApp` enters the **app** scope in its lifespan and opens a **request** scope per
100
+ request, publishing the link `RequestContext` for that request. Routes opt into
101
+ bare-type injection by living on a `GazeboRouter` (or the app directly): any parameter
102
+ whose type carries `__provide__`, or is marked `Annotated[T, Inject]`, is resolved from
103
+ the per-request scope by rewriting the route signature into FastAPI `Depends`.
104
+
105
+ `GazeboApp` + `GazeboRouter` are an **intended pair**. Putting an injectable-typed route
106
+ on a plain `APIRouter` fails loudly at startup (naming the route) rather than silently
107
+ treating the parameter as a request body. To add gazebo behavior to an app you didn't
108
+ construct, use `upgrade(app, providers)` instead of subclassing; to mount a `GazeboApp`
109
+ under a root app, forward its lifespan with `forward_lifespans`.
110
+
111
+ ## Docs
112
+
113
+ - `working-docs/` — design specs and drafts: `design.md` (the OGC/web shapes),
114
+ `design-di.md` (the injection system), `roadmap.md` (post-v1 feature backlog). These
115
+ are the authoritative rationale for why things are shaped as they are; read the
116
+ relevant one before reworking a subsystem.
117
+ - `docs/` — the published documentation site (zensical/mkdocs, versioned with mike).
118
+ - `examples/garden/` — a complete standalone OGC-style API that exercises every feature;
119
+ the best end-to-end reference for how the pieces fit.
120
+
121
+ ```sh
122
+ uv run --group docs zensical build --strict --clean # build (CI gate; fails on issues)
123
+ uv run --group docs zensical serve # live preview while writing
124
+ ```
125
+
126
+ ### Documentation style & guidelines
127
+
128
+ - **Split by document type; never duplicate across the boundary.** *Reference* (what
129
+ each symbol is — signatures, params) lives in **docstrings** and is autogenerated into
130
+ `docs/reference.md` via mkdocstrings. *Explanation/how-to* (why you'd reach for it, how
131
+ pieces combine) lives in **handwritten Markdown**. Keep docstrings Google-style and
132
+ current — they are the single source of truth for the reference layer.
133
+ - **Narrative pages must not restate the API.** No retyped signatures or param tables in
134
+ prose; link into the reference anchor instead (e.g. `reference.md#gazebo.link`, or a
135
+ per-symbol anchor like `#gazebo.link.Link.self_link`). Each page ends with a
136
+ **Reference** link.
137
+ - **Structure is layered by architecture** (Core → DI → FastAPI integration), one page
138
+ per module, mirroring `src/gazebo/`. The nav lives in `zensical.toml`; update it when
139
+ adding a page.
140
+ - **Page shape:** open with a one-line *why* blockquote, lead with the rationale (the
141
+ *why*) before the *how*, then concept sections. Prefer small, self-contained,
142
+ copy-pasteable snippets over one large example; the garden example is the
143
+ "see it all together" reference.
144
+ - **All code snippets are tested.** Example code lives in `tests/examples/<page>.py` as
145
+ runnable modules with module-level `assert`s; `tests/test_examples.py` executes each
146
+ via `runpy`, so a broken snippet fails CI. Docs **include** the clean region with
147
+ pymdownx.snippets — ` ```python\n--8<-- "tests/examples/links.py:self_link"\n``` ` —
148
+ rather than pasting code, so what readers see is exactly what runs. Wrap the rendered
149
+ region in `# --8<-- [start:name]` / `# --8<-- [end:name]` markers and keep the driving
150
+ `assert`/`TestClient` code *outside* the markers so it doesn't render. `check_paths`
151
+ is on, so a bad include path or region name fails the strict build.
152
+
153
+ ## Landing a feature
154
+
155
+ A new feature or behavior change is not complete until all three of these land with it:
156
+
157
+ 1. **Tests** in `tests/` covering the new behavior (coverage and warnings-as-errors are
158
+ enforced; CI runs the suite on Python 3.12–3.14).
159
+ 2. **Use in the garden example** — `examples/garden` is meant to exercise *every*
160
+ feature, so wire the new capability into the example app and its tests. CI runs the
161
+ garden suite separately, so this is load-bearing, not decorative.
162
+ 3. **Documentation** — update the relevant page under `docs/` (and the design spec in
163
+ `working-docs/` if the feature changes a subsystem's rationale or shape).
164
+
165
+ ## Conventions
166
+
167
+ - Ruff is configured with single quotes and a 99-char line length, with a broad lint
168
+ rule set (see `[tool.ruff.lint]` in `pyproject.toml`). Match the surrounding style.
169
+ - Every module starts with a docstring explaining its role in the layering; keep that
170
+ up to date when a module's responsibility shifts.
171
+ </content>
172
+ </invoke>
173
+ </invoke>
@@ -0,0 +1,130 @@
1
+ # Contributing
2
+
3
+ gazebo packages the recurring machinery of OGC-style REST APIs (deferred links,
4
+ collection envelopes, RFC 7807 problems, proxy-aware URLs, a typed DI container,
5
+ and FastAPI glue). The core depends only on `pydantic`; framework integration is
6
+ opt-in via extras. **Requires Python 3.12+.**
7
+
8
+ See [`CLAUDE.md`](CLAUDE.md) for a deeper tour of the architecture and layering.
9
+
10
+ ## Project Setup
11
+
12
+ This project uses [uv](https://docs.astral.sh/uv) for project management. On a
13
+ Mac it can be installed globally with `brew install uv`. `uv` manages its own
14
+ virtual environment, so there is no separate venv step.
15
+
16
+ The project is a `uv` workspace whose only member is `examples/garden`. Sync
17
+ everything (all extras and all workspace packages) with:
18
+
19
+ ```commandline
20
+ uv sync --all-extras --all-packages
21
+ ```
22
+
23
+ CI installs with `--locked --no-editable` in addition to the flags above.
24
+
25
+ ## Pre-commit
26
+
27
+ Install the pre-commit hooks so they run on every `git commit`:
28
+
29
+ ```commandline
30
+ uv run pre-commit install
31
+ ```
32
+
33
+ You can also run them explicitly against all files:
34
+
35
+ ```commandline
36
+ uv run pre-commit run --all-files
37
+ ```
38
+
39
+ The hooks cover `ruff` (lint + format), `mypy`, `pyright`, and a set of
40
+ file-hygiene checks. The same hooks run in CI, so it's worth keeping them green
41
+ locally.
42
+
43
+ If for some reason you need to commit code that does not pass the pre-commit
44
+ checks, this can be done with:
45
+
46
+ ```commandline
47
+ git commit -m "message" --no-verify
48
+ ```
49
+
50
+ ## Type Checking
51
+
52
+ Type checking is enforced by **both** mypy and pyright (pyright runs over `src`
53
+ and `tests`). Both run in pre-commit and must be green in CI on Python
54
+ 3.12–3.14.
55
+
56
+ ## Testing
57
+
58
+ Tests are run with `pytest`. Put test modules and resources in the `tests/`
59
+ directory.
60
+
61
+ ```commandline
62
+ uv run pytest
63
+ ```
64
+
65
+ Run a single test by node id:
66
+
67
+ ```commandline
68
+ uv run pytest tests/test_link.py::test_name
69
+ ```
70
+
71
+ `uv run pytest` always reports coverage on the `gazebo` package
72
+ (`addopts=--cov=gazebo` in `pyproject.toml`) and treats warnings as errors
73
+ (`filterwarnings = ['error']`), so a stray warning fails the suite.
74
+
75
+ ### Tested documentation snippets
76
+
77
+ All code snippets in the docs are tested. Example code lives in
78
+ `tests/examples/<page>.py` as runnable modules with module-level `assert`s, and
79
+ `tests/test_examples.py` executes each via `runpy`. The docs **include** the
80
+ clean region with `pymdownx.snippets` rather than pasting code, so a broken
81
+ snippet fails CI. See the documentation guidelines in `CLAUDE.md` for the marker
82
+ conventions.
83
+
84
+ ## The Garden Example
85
+
86
+ `examples/garden` is a complete, standalone OGC-style API that is meant to
87
+ exercise *every* feature — it is the best end-to-end reference and is **load
88
+ bearing**: CI runs its suite separately. It is its own project with its own
89
+ entry point, so run it from that directory:
90
+
91
+ ```commandline
92
+ cd examples/garden
93
+ uv run garden # serve Gazebo Gardens on http://127.0.0.1:8000
94
+ uv run pytest # the example's tests
95
+ ```
96
+
97
+ ## Documentation
98
+
99
+ The published docs site is built with zensical/mkdocs (versioned with `mike`).
100
+
101
+ ```commandline
102
+ uv run --group docs zensical serve # live preview while writing
103
+ uv run --group docs zensical build --strict --clean # build (CI gate; fails on issues)
104
+ ```
105
+
106
+ *Reference* docs (signatures, params) are autogenerated from Google-style
107
+ docstrings into `docs/reference.md` via mkdocstrings — keep docstrings as the
108
+ single source of truth. *Explanation/how-to* lives in handwritten Markdown under
109
+ `docs/`, one page per module, mirroring `src/gazebo/`. The nav lives in
110
+ `zensical.toml`; update it when adding a page. See `CLAUDE.md` for the full
111
+ documentation style guide.
112
+
113
+ ## Modifying Dependencies
114
+
115
+ With `uv`, add a dependency by running `uv add` (add `--dev` for a dev
116
+ dependency). Upgrade dependencies with `uv sync --upgrade`, optionally passing a
117
+ package name to upgrade just one; by default it upgrades everything.
118
+
119
+ ## Landing a Feature
120
+
121
+ A new feature or behavior change is not complete until all three of these land
122
+ with it:
123
+
124
+ 1. **Tests** in `tests/` covering the new behavior (coverage and
125
+ warnings-as-errors are enforced; CI runs the suite on Python 3.12–3.14).
126
+ 2. **Use in the garden example** — wire the new capability into
127
+ `examples/garden` and its tests.
128
+ 3. **Documentation** — update the relevant page under `docs/` (and the design
129
+ spec in `working-docs/` if the feature changes a subsystem's rationale or
130
+ shape).