usecaseapi 1.0.0__tar.gz → 1.1.1__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.
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/.gitignore +5 -0
- usecaseapi-1.1.1/PKG-INFO +295 -0
- usecaseapi-1.1.1/README.md +258 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/RELEASE.md +6 -5
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/docs/api-reference.md +23 -11
- usecaseapi-1.1.1/docs/assets/brand/favicon-128.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/favicon-16.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/favicon-180.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/favicon-192.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/favicon-256.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/favicon-32.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/favicon-48.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/favicon-512.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/favicon-64.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/favicon.ico +0 -0
- usecaseapi-1.1.1/docs/assets/brand/icon-circle-dark.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/icon-circle-light.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/icon-horizontal-on-dark.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/icon-square-dark.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/icon-square-light.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/logo-on-dark.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/logo.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/mark-circle-light.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/mark-circle.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/mark-square-dark.png +0 -0
- usecaseapi-1.1.1/docs/assets/brand/mark-square.png +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/docs/design.md +6 -6
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/docs/integrations.md +13 -4
- usecaseapi-1.1.1/docs/llm-manifest-prompt.md +183 -0
- usecaseapi-1.1.1/docs/manifest.md +290 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/docs/pypi-publishing.md +6 -4
- usecaseapi-1.1.1/docs/quickstart.md +102 -0
- usecaseapi-1.1.1/docs/scaffold.md +55 -0
- usecaseapi-1.1.1/docs/testing.md +43 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/docs/versioning.md +6 -6
- usecaseapi-1.1.1/examples/basic/README.md +16 -0
- usecaseapi-1.1.1/examples/basic/src/commerce/__init__.py +1 -0
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/__init__.py +1 -0
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/check_availability/__init__.py +1 -0
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/check_availability/v1/__init__.py +1 -0
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/check_availability/v1/check_availability_contract.py +51 -0
- usecaseapi-1.0.0/examples/basic/app/usecases/inventory/check_availability.py → usecaseapi-1.1.1/examples/basic/src/commerce/usecases/check_availability/v1/check_availability_usecase.py +12 -8
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/checkout/__init__.py +1 -0
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/checkout/v1/__init__.py +1 -0
- usecaseapi-1.0.0/examples/basic/app/contracts/checkout/checkout/v1.py → usecaseapi-1.1.1/examples/basic/src/commerce/usecases/checkout/v1/checkout_contract.py +9 -9
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/checkout/v1/checkout_usecase.py +38 -0
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/place_order/__init__.py +1 -0
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/place_order/v1/__init__.py +1 -0
- usecaseapi-1.0.0/examples/basic/app/contracts/orders/place_order/v1.py → usecaseapi-1.1.1/examples/basic/src/commerce/usecases/place_order/v1/place_order_contract.py +11 -11
- usecaseapi-1.1.1/examples/basic/src/commerce/usecases/place_order/v1/place_order_usecase.py +29 -0
- usecaseapi-1.1.1/examples/basic/src/composition.py +44 -0
- usecaseapi-1.1.1/examples/basic/tests/commerce/usecases/check_availability/v1/test_check_availability.py +24 -0
- usecaseapi-1.1.1/examples/basic/tests/commerce/usecases/checkout/v1/test_checkout.py +20 -0
- usecaseapi-1.1.1/examples/basic/tests/commerce/usecases/place_order/v1/test_place_order.py +19 -0
- usecaseapi-1.1.1/examples/basic/tests/conftest.py +9 -0
- usecaseapi-1.1.1/examples/basic/usecaseapi.ucase.yaml +185 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/pyproject.toml +28 -19
- usecaseapi-1.1.1/schema/usecaseapi.manifest.v1.schema.yaml +190 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/scripts/verify_distribution.py +18 -10
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/src/usecaseapi/__init__.py +34 -9
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/src/usecaseapi/api.py +149 -80
- usecaseapi-1.1.1/src/usecaseapi/cli.py +253 -0
- usecaseapi-1.1.1/src/usecaseapi/manifest.py +1537 -0
- usecaseapi-1.1.1/src/usecaseapi/scaffold.py +407 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/tests/test_call.py +1 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/tests/test_coverage_pr_comment.py +9 -3
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/tests/test_full_service_validation.py +154 -178
- usecaseapi-1.1.1/tests/test_manifest.py +954 -0
- usecaseapi-1.1.1/tests/test_release_workflow.py +37 -0
- usecaseapi-1.1.1/tests/test_snapshot_docs_scaffold.py +159 -0
- usecaseapi-1.0.0/PKG-INFO +0 -192
- usecaseapi-1.0.0/README.md +0 -158
- usecaseapi-1.0.0/docs/agent-tools.md +0 -15
- usecaseapi-1.0.0/docs/quickstart.md +0 -91
- usecaseapi-1.0.0/docs/scaffold.md +0 -67
- usecaseapi-1.0.0/docs/testing.md +0 -37
- usecaseapi-1.0.0/docs/v1-scope.md +0 -36
- usecaseapi-1.0.0/examples/basic/README.md +0 -9
- usecaseapi-1.0.0/examples/basic/app/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/composition.py +0 -38
- usecaseapi-1.0.0/examples/basic/app/contracts/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/contracts/checkout/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/contracts/checkout/checkout/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/contracts/inventory/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/contracts/inventory/check_availability/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/contracts/inventory/check_availability/v1.py +0 -41
- usecaseapi-1.0.0/examples/basic/app/contracts/orders/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/contracts/orders/place_order/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/usecases/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/usecases/checkout/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/usecases/checkout/checkout.py +0 -38
- usecaseapi-1.0.0/examples/basic/app/usecases/inventory/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/usecases/orders/__init__.py +0 -1
- usecaseapi-1.0.0/examples/basic/app/usecases/orders/place_order.py +0 -28
- usecaseapi-1.0.0/src/usecaseapi/cli.py +0 -182
- usecaseapi-1.0.0/src/usecaseapi/docs.py +0 -88
- usecaseapi-1.0.0/src/usecaseapi/scaffold.py +0 -298
- usecaseapi-1.0.0/src/usecaseapi/snapshot.py +0 -173
- usecaseapi-1.0.0/tests/test_snapshot_docs_scaffold.py +0 -176
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/CODE_OF_CONDUCT.md +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/CONTRIBUTING.md +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/LICENSE +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/SECURITY.md +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/scripts/write_coverage_pr_comment.py +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/scripts/write_coverage_summary.py +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/src/usecaseapi/contracts.py +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/src/usecaseapi/errors.py +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/src/usecaseapi/model.py +0 -0
- {usecaseapi-1.0.0 → usecaseapi-1.1.1}/src/usecaseapi/py.typed +0 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: usecaseapi
|
|
3
|
+
Version: 1.1.1
|
|
4
|
+
Summary: FastAPI-style contracts for Python application use cases.
|
|
5
|
+
Project-URL: Homepage, https://github.com/Wisteria30/usecaseapi
|
|
6
|
+
Project-URL: Documentation, https://github.com/Wisteria30/usecaseapi/tree/main/docs
|
|
7
|
+
Project-URL: Repository, https://github.com/Wisteria30/usecaseapi
|
|
8
|
+
Project-URL: Issues, https://github.com/Wisteria30/usecaseapi/issues
|
|
9
|
+
Author: UseCaseAPI Contributors
|
|
10
|
+
Maintainer: UseCaseAPI Contributors
|
|
11
|
+
License-Expression: MIT
|
|
12
|
+
License-File: LICENSE
|
|
13
|
+
Keywords: ai-agents,application-api,contracts,protocol,pydantic,usecase,workflow
|
|
14
|
+
Classifier: Development Status :: 4 - Beta
|
|
15
|
+
Classifier: Intended Audience :: Developers
|
|
16
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
17
|
+
Classifier: Operating System :: OS Independent
|
|
18
|
+
Classifier: Programming Language :: Python :: 3
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Typing :: Typed
|
|
24
|
+
Requires-Python: <3.15,>=3.12
|
|
25
|
+
Requires-Dist: pydantic<3,>=2.12.4
|
|
26
|
+
Requires-Dist: pyyaml<7,>=6.0.2
|
|
27
|
+
Requires-Dist: typer>=0.25.1
|
|
28
|
+
Provides-Extra: dev
|
|
29
|
+
Requires-Dist: build>=1.3.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: coverage>=7.12.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: mypy>=1.18.2; extra == 'dev'
|
|
32
|
+
Requires-Dist: pytest>=9.0.1; extra == 'dev'
|
|
33
|
+
Requires-Dist: ruff>=0.14.6; extra == 'dev'
|
|
34
|
+
Requires-Dist: twine>=6.2.0; extra == 'dev'
|
|
35
|
+
Requires-Dist: types-pyyaml>=6.0.12; extra == 'dev'
|
|
36
|
+
Description-Content-Type: text/markdown
|
|
37
|
+
|
|
38
|
+
# UseCaseAPI
|
|
39
|
+
|
|
40
|
+
<p align="center">
|
|
41
|
+
<img src="https://raw.githubusercontent.com/Wisteria30/usecaseapi/main/docs/assets/brand/logo.png" alt="UseCaseAPI" width="720">
|
|
42
|
+
</p>
|
|
43
|
+
|
|
44
|
+
<p align="center">
|
|
45
|
+
<em>FastAPI-style contracts for same-process Python application use cases.</em>
|
|
46
|
+
</p>
|
|
47
|
+
|
|
48
|
+
<p align="center">
|
|
49
|
+
<a href="https://github.com/Wisteria30/usecaseapi/actions/workflows/ci.yml">
|
|
50
|
+
<img src="https://github.com/Wisteria30/usecaseapi/actions/workflows/ci.yml/badge.svg" alt="CI">
|
|
51
|
+
</a>
|
|
52
|
+
<a href="https://pypi.org/project/usecaseapi/">
|
|
53
|
+
<img src="https://img.shields.io/pypi/v/usecaseapi.svg" alt="PyPI">
|
|
54
|
+
</a>
|
|
55
|
+
<a href="https://pypi.org/project/usecaseapi/">
|
|
56
|
+
<img src="https://img.shields.io/pypi/pyversions/usecaseapi.svg" alt="Python versions">
|
|
57
|
+
</a>
|
|
58
|
+
<a href="https://github.com/Wisteria30/usecaseapi/blob/main/LICENSE">
|
|
59
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT">
|
|
60
|
+
</a>
|
|
61
|
+
</p>
|
|
62
|
+
|
|
63
|
+
---
|
|
64
|
+
|
|
65
|
+
**Documentation**: <a href="https://github.com/Wisteria30/usecaseapi/tree/main/docs">github.com/Wisteria30/usecaseapi/tree/main/docs</a>
|
|
66
|
+
|
|
67
|
+
**Source Code**: <a href="https://github.com/Wisteria30/usecaseapi">github.com/Wisteria30/usecaseapi</a>
|
|
68
|
+
|
|
69
|
+
---
|
|
70
|
+
|
|
71
|
+
UseCaseAPI gives internal application use cases stable, versioned contracts.
|
|
72
|
+
|
|
73
|
+
Define a contract with a Python `Protocol`, Pydantic v2 models, and domain exceptions. Bind that contract to an implementation explicitly. Call it in the same process without turning HTTP, RPC, serialization, dependency injection containers, or transaction managers into runtime requirements.
|
|
74
|
+
|
|
75
|
+
Add it when your application has internal use case nodes that need stable names, versions, input/output schemas, declared dependencies, and a canonical YAML contract catalog. It helps teams and AI coding agents see what can be called, what can fail, and whether a change broke an application-layer contract before that break ships.
|
|
76
|
+
|
|
77
|
+
It is designed for applications with many internal use case nodes: workflow-like systems, AI-agent tool surfaces, CLIs, batch workers, FastAPI/Django backends, and codebases where accidental application-layer breaking changes are costly.
|
|
78
|
+
|
|
79
|
+
## Key features
|
|
80
|
+
|
|
81
|
+
- **Code-first contracts**: use `Protocol`, `Model`, `UseCase`, `UseCaseRef`, and normal Python exceptions.
|
|
82
|
+
- **Same-process calls**: call implementations directly, with no JSON serialization in the call path.
|
|
83
|
+
- **Explicit bindings**: connect a contract token to an implementation factory in one place.
|
|
84
|
+
- **Declared dependencies**: expose and validate which use cases can call which other use cases.
|
|
85
|
+
- **Versioned identity**: identify contracts as stable names such as `commerce.place_order@v1`.
|
|
86
|
+
- **Manifest artifacts**: generate a YAML catalog, conservative diffs, Markdown docs, and Mermaid graphs.
|
|
87
|
+
- **CLI scaffolding**: create a contract, implementation, and test layout from one command.
|
|
88
|
+
- **Typed distribution**: ships `py.typed` for downstream type checkers.
|
|
89
|
+
|
|
90
|
+
## Requirements
|
|
91
|
+
|
|
92
|
+
UseCaseAPI requires:
|
|
93
|
+
|
|
94
|
+
- Python `>=3.12,<3.15`
|
|
95
|
+
- Pydantic v2
|
|
96
|
+
- PyYAML
|
|
97
|
+
- Typer
|
|
98
|
+
|
|
99
|
+
## Installation
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
uv add usecaseapi
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
## Example
|
|
106
|
+
|
|
107
|
+
### Create a contract
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
# src/commerce/usecases/place_order/v1/place_order_contract.py
|
|
111
|
+
from __future__ import annotations
|
|
112
|
+
|
|
113
|
+
from typing import ClassVar, Protocol
|
|
114
|
+
|
|
115
|
+
from usecaseapi import Contract, Model, UseCase, UseCaseError, UseCaseRef, define_usecase
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class PlaceOrderUseCaseInput(Model):
|
|
119
|
+
user_id: str
|
|
120
|
+
sku_id: str
|
|
121
|
+
quantity: int
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
class PlaceOrderUseCaseOutput(Model):
|
|
125
|
+
order_id: str
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class PlaceOrderError(UseCaseError):
|
|
129
|
+
code: ClassVar[str] = "commerce.place_order"
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
class PlaceOrder(UseCase[PlaceOrderUseCaseInput, PlaceOrderUseCaseOutput], Protocol):
|
|
133
|
+
async def __call__(self, input: PlaceOrderUseCaseInput, /) -> PlaceOrderUseCaseOutput:
|
|
134
|
+
...
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
PLACE_ORDER_USECASE: UseCaseRef[PlaceOrderUseCaseInput, PlaceOrderUseCaseOutput] = define_usecase(
|
|
138
|
+
PlaceOrder,
|
|
139
|
+
Contract(
|
|
140
|
+
name="commerce.place_order",
|
|
141
|
+
version=1,
|
|
142
|
+
input=PlaceOrderUseCaseInput,
|
|
143
|
+
output=PlaceOrderUseCaseOutput,
|
|
144
|
+
raises=(PlaceOrderError,),
|
|
145
|
+
),
|
|
146
|
+
)
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Implement it
|
|
150
|
+
|
|
151
|
+
```python
|
|
152
|
+
# src/commerce/usecases/place_order/v1/place_order_usecase.py
|
|
153
|
+
from commerce.usecases.place_order.v1.place_order_contract import (
|
|
154
|
+
PlaceOrderUseCaseInput,
|
|
155
|
+
PlaceOrderUseCaseOutput,
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
class PlaceOrderUseCase:
|
|
160
|
+
async def __call__(
|
|
161
|
+
self,
|
|
162
|
+
input: PlaceOrderUseCaseInput,
|
|
163
|
+
/,
|
|
164
|
+
) -> PlaceOrderUseCaseOutput:
|
|
165
|
+
return PlaceOrderUseCaseOutput(order_id="ord_123")
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
The implementation class is ordinary Python. Instantiate it in your composition root
|
|
169
|
+
or in tests with the dependencies that project actually uses.
|
|
170
|
+
|
|
171
|
+
### Bind and call it
|
|
172
|
+
|
|
173
|
+
```python
|
|
174
|
+
from dataclasses import dataclass
|
|
175
|
+
|
|
176
|
+
from usecaseapi import UseCaseAPI
|
|
177
|
+
|
|
178
|
+
from commerce.usecases.place_order.v1.place_order_contract import (
|
|
179
|
+
PLACE_ORDER_USECASE,
|
|
180
|
+
PlaceOrderUseCaseInput,
|
|
181
|
+
)
|
|
182
|
+
from commerce.usecases.place_order.v1.place_order_usecase import PlaceOrderUseCase
|
|
183
|
+
|
|
184
|
+
|
|
185
|
+
@dataclass(frozen=True)
|
|
186
|
+
class AppContext:
|
|
187
|
+
tenant_id: str
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
usecases = UseCaseAPI[AppContext]()
|
|
191
|
+
usecases.bind(PLACE_ORDER_USECASE, lambda caller: PlaceOrderUseCase())
|
|
192
|
+
|
|
193
|
+
caller = usecases.caller(AppContext(tenant_id="tenant_a"))
|
|
194
|
+
output = await caller.call(
|
|
195
|
+
PLACE_ORDER_USECASE,
|
|
196
|
+
PlaceOrderUseCaseInput(user_id="u1", sku_id="s1", quantity=1),
|
|
197
|
+
)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
The call is same-process and direct. UseCaseAPI does not serialize the input or output.
|
|
201
|
+
|
|
202
|
+
## CLI scaffolding
|
|
203
|
+
|
|
204
|
+
Create the first contract version:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
usecaseapi scaffold commerce place_order --output-root src
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Create the next major version from the latest existing contract:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
usecaseapi scaffold commerce place_order --output-root src --next
|
|
214
|
+
```
|
|
215
|
+
|
|
216
|
+
The first command creates:
|
|
217
|
+
|
|
218
|
+
```text
|
|
219
|
+
src/commerce/usecases/place_order/v1/place_order_contract.py
|
|
220
|
+
src/commerce/usecases/place_order/v1/place_order_usecase.py
|
|
221
|
+
tests/commerce/usecases/place_order/v1/test_place_order.py
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
See [docs/scaffold.md](docs/scaffold.md) for the generated layout and options.
|
|
225
|
+
|
|
226
|
+
## Manifest
|
|
227
|
+
|
|
228
|
+
Export the canonical contract catalog:
|
|
229
|
+
|
|
230
|
+
```bash
|
|
231
|
+
usecaseapi manifest export composition:usecases --output usecaseapi.ucase.yaml
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
Validate it and check that code still matches it:
|
|
235
|
+
|
|
236
|
+
```bash
|
|
237
|
+
usecaseapi manifest validate usecaseapi.ucase.yaml
|
|
238
|
+
usecaseapi manifest check-sync composition:usecases usecaseapi.ucase.yaml
|
|
239
|
+
```
|
|
240
|
+
|
|
241
|
+
Generate Python contract and implementation skeletons from the catalog:
|
|
242
|
+
|
|
243
|
+
```bash
|
|
244
|
+
usecaseapi manifest scaffold usecaseapi.ucase.yaml --root .
|
|
245
|
+
```
|
|
246
|
+
|
|
247
|
+
Render derived outputs:
|
|
248
|
+
|
|
249
|
+
```bash
|
|
250
|
+
usecaseapi docs usecaseapi.ucase.yaml --output usecaseapi.md
|
|
251
|
+
usecaseapi graph usecaseapi.ucase.yaml --output usecaseapi.mmd
|
|
252
|
+
usecaseapi diff old.ucase.yaml new.ucase.yaml
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
See [docs/scaffold.md](docs/scaffold.md), [docs/manifest.md](docs/manifest.md), and
|
|
256
|
+
[docs/llm-manifest-prompt.md](docs/llm-manifest-prompt.md) for generated layouts,
|
|
257
|
+
Manifest syntax, and an LLM prompt for converging requirements into `usecaseapi.ucase.yaml`.
|
|
258
|
+
|
|
259
|
+
## Manifest docs and graph
|
|
260
|
+
|
|
261
|
+
UseCaseAPI can turn a composed application into inspectable artifacts:
|
|
262
|
+
|
|
263
|
+
- a canonical YAML Manifest;
|
|
264
|
+
- a conservative diff between Manifests;
|
|
265
|
+
- Markdown documentation for use cases and dependencies;
|
|
266
|
+
- Mermaid graph output for the dependency graph.
|
|
267
|
+
|
|
268
|
+
See [docs/api-reference.md](docs/api-reference.md), [docs/versioning.md](docs/versioning.md), and [docs/integrations.md](docs/integrations.md).
|
|
269
|
+
|
|
270
|
+
## What UseCaseAPI is not
|
|
271
|
+
|
|
272
|
+
UseCaseAPI is not a web framework and does not add HTTP, RPC, serialization, service locators, dependency injection containers, or transaction managers to your application runtime.
|
|
273
|
+
|
|
274
|
+
You can write plain classes and functions for small projects. UseCaseAPI becomes useful when an application has enough internal use case nodes that stable names, versions, declared dependencies, documented errors, Manifest catalogs, and generated docs start paying for themselves.
|
|
275
|
+
|
|
276
|
+
## Development
|
|
277
|
+
|
|
278
|
+
```bash
|
|
279
|
+
uv sync --extra dev
|
|
280
|
+
uv run pytest
|
|
281
|
+
uv run mypy
|
|
282
|
+
uv run ruff check .
|
|
283
|
+
uv run ruff format --check .
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
For release validation:
|
|
287
|
+
|
|
288
|
+
```bash
|
|
289
|
+
uv build
|
|
290
|
+
uv run twine check dist/*
|
|
291
|
+
uv run --isolated --no-project --with dist/*.whl scripts/verify_distribution.py
|
|
292
|
+
uv run --isolated --no-project --with dist/*.tar.gz scripts/verify_distribution.py
|
|
293
|
+
```
|
|
294
|
+
|
|
295
|
+
See [docs/pypi-publishing.md](docs/pypi-publishing.md) for the publishing workflow.
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# UseCaseAPI
|
|
2
|
+
|
|
3
|
+
<p align="center">
|
|
4
|
+
<img src="https://raw.githubusercontent.com/Wisteria30/usecaseapi/main/docs/assets/brand/logo.png" alt="UseCaseAPI" width="720">
|
|
5
|
+
</p>
|
|
6
|
+
|
|
7
|
+
<p align="center">
|
|
8
|
+
<em>FastAPI-style contracts for same-process Python application use cases.</em>
|
|
9
|
+
</p>
|
|
10
|
+
|
|
11
|
+
<p align="center">
|
|
12
|
+
<a href="https://github.com/Wisteria30/usecaseapi/actions/workflows/ci.yml">
|
|
13
|
+
<img src="https://github.com/Wisteria30/usecaseapi/actions/workflows/ci.yml/badge.svg" alt="CI">
|
|
14
|
+
</a>
|
|
15
|
+
<a href="https://pypi.org/project/usecaseapi/">
|
|
16
|
+
<img src="https://img.shields.io/pypi/v/usecaseapi.svg" alt="PyPI">
|
|
17
|
+
</a>
|
|
18
|
+
<a href="https://pypi.org/project/usecaseapi/">
|
|
19
|
+
<img src="https://img.shields.io/pypi/pyversions/usecaseapi.svg" alt="Python versions">
|
|
20
|
+
</a>
|
|
21
|
+
<a href="https://github.com/Wisteria30/usecaseapi/blob/main/LICENSE">
|
|
22
|
+
<img src="https://img.shields.io/badge/license-MIT-blue.svg" alt="License: MIT">
|
|
23
|
+
</a>
|
|
24
|
+
</p>
|
|
25
|
+
|
|
26
|
+
---
|
|
27
|
+
|
|
28
|
+
**Documentation**: <a href="https://github.com/Wisteria30/usecaseapi/tree/main/docs">github.com/Wisteria30/usecaseapi/tree/main/docs</a>
|
|
29
|
+
|
|
30
|
+
**Source Code**: <a href="https://github.com/Wisteria30/usecaseapi">github.com/Wisteria30/usecaseapi</a>
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
UseCaseAPI gives internal application use cases stable, versioned contracts.
|
|
35
|
+
|
|
36
|
+
Define a contract with a Python `Protocol`, Pydantic v2 models, and domain exceptions. Bind that contract to an implementation explicitly. Call it in the same process without turning HTTP, RPC, serialization, dependency injection containers, or transaction managers into runtime requirements.
|
|
37
|
+
|
|
38
|
+
Add it when your application has internal use case nodes that need stable names, versions, input/output schemas, declared dependencies, and a canonical YAML contract catalog. It helps teams and AI coding agents see what can be called, what can fail, and whether a change broke an application-layer contract before that break ships.
|
|
39
|
+
|
|
40
|
+
It is designed for applications with many internal use case nodes: workflow-like systems, AI-agent tool surfaces, CLIs, batch workers, FastAPI/Django backends, and codebases where accidental application-layer breaking changes are costly.
|
|
41
|
+
|
|
42
|
+
## Key features
|
|
43
|
+
|
|
44
|
+
- **Code-first contracts**: use `Protocol`, `Model`, `UseCase`, `UseCaseRef`, and normal Python exceptions.
|
|
45
|
+
- **Same-process calls**: call implementations directly, with no JSON serialization in the call path.
|
|
46
|
+
- **Explicit bindings**: connect a contract token to an implementation factory in one place.
|
|
47
|
+
- **Declared dependencies**: expose and validate which use cases can call which other use cases.
|
|
48
|
+
- **Versioned identity**: identify contracts as stable names such as `commerce.place_order@v1`.
|
|
49
|
+
- **Manifest artifacts**: generate a YAML catalog, conservative diffs, Markdown docs, and Mermaid graphs.
|
|
50
|
+
- **CLI scaffolding**: create a contract, implementation, and test layout from one command.
|
|
51
|
+
- **Typed distribution**: ships `py.typed` for downstream type checkers.
|
|
52
|
+
|
|
53
|
+
## Requirements
|
|
54
|
+
|
|
55
|
+
UseCaseAPI requires:
|
|
56
|
+
|
|
57
|
+
- Python `>=3.12,<3.15`
|
|
58
|
+
- Pydantic v2
|
|
59
|
+
- PyYAML
|
|
60
|
+
- Typer
|
|
61
|
+
|
|
62
|
+
## Installation
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
uv add usecaseapi
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
## Example
|
|
69
|
+
|
|
70
|
+
### Create a contract
|
|
71
|
+
|
|
72
|
+
```python
|
|
73
|
+
# src/commerce/usecases/place_order/v1/place_order_contract.py
|
|
74
|
+
from __future__ import annotations
|
|
75
|
+
|
|
76
|
+
from typing import ClassVar, Protocol
|
|
77
|
+
|
|
78
|
+
from usecaseapi import Contract, Model, UseCase, UseCaseError, UseCaseRef, define_usecase
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
class PlaceOrderUseCaseInput(Model):
|
|
82
|
+
user_id: str
|
|
83
|
+
sku_id: str
|
|
84
|
+
quantity: int
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
class PlaceOrderUseCaseOutput(Model):
|
|
88
|
+
order_id: str
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class PlaceOrderError(UseCaseError):
|
|
92
|
+
code: ClassVar[str] = "commerce.place_order"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class PlaceOrder(UseCase[PlaceOrderUseCaseInput, PlaceOrderUseCaseOutput], Protocol):
|
|
96
|
+
async def __call__(self, input: PlaceOrderUseCaseInput, /) -> PlaceOrderUseCaseOutput:
|
|
97
|
+
...
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
PLACE_ORDER_USECASE: UseCaseRef[PlaceOrderUseCaseInput, PlaceOrderUseCaseOutput] = define_usecase(
|
|
101
|
+
PlaceOrder,
|
|
102
|
+
Contract(
|
|
103
|
+
name="commerce.place_order",
|
|
104
|
+
version=1,
|
|
105
|
+
input=PlaceOrderUseCaseInput,
|
|
106
|
+
output=PlaceOrderUseCaseOutput,
|
|
107
|
+
raises=(PlaceOrderError,),
|
|
108
|
+
),
|
|
109
|
+
)
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### Implement it
|
|
113
|
+
|
|
114
|
+
```python
|
|
115
|
+
# src/commerce/usecases/place_order/v1/place_order_usecase.py
|
|
116
|
+
from commerce.usecases.place_order.v1.place_order_contract import (
|
|
117
|
+
PlaceOrderUseCaseInput,
|
|
118
|
+
PlaceOrderUseCaseOutput,
|
|
119
|
+
)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class PlaceOrderUseCase:
|
|
123
|
+
async def __call__(
|
|
124
|
+
self,
|
|
125
|
+
input: PlaceOrderUseCaseInput,
|
|
126
|
+
/,
|
|
127
|
+
) -> PlaceOrderUseCaseOutput:
|
|
128
|
+
return PlaceOrderUseCaseOutput(order_id="ord_123")
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
The implementation class is ordinary Python. Instantiate it in your composition root
|
|
132
|
+
or in tests with the dependencies that project actually uses.
|
|
133
|
+
|
|
134
|
+
### Bind and call it
|
|
135
|
+
|
|
136
|
+
```python
|
|
137
|
+
from dataclasses import dataclass
|
|
138
|
+
|
|
139
|
+
from usecaseapi import UseCaseAPI
|
|
140
|
+
|
|
141
|
+
from commerce.usecases.place_order.v1.place_order_contract import (
|
|
142
|
+
PLACE_ORDER_USECASE,
|
|
143
|
+
PlaceOrderUseCaseInput,
|
|
144
|
+
)
|
|
145
|
+
from commerce.usecases.place_order.v1.place_order_usecase import PlaceOrderUseCase
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
@dataclass(frozen=True)
|
|
149
|
+
class AppContext:
|
|
150
|
+
tenant_id: str
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
usecases = UseCaseAPI[AppContext]()
|
|
154
|
+
usecases.bind(PLACE_ORDER_USECASE, lambda caller: PlaceOrderUseCase())
|
|
155
|
+
|
|
156
|
+
caller = usecases.caller(AppContext(tenant_id="tenant_a"))
|
|
157
|
+
output = await caller.call(
|
|
158
|
+
PLACE_ORDER_USECASE,
|
|
159
|
+
PlaceOrderUseCaseInput(user_id="u1", sku_id="s1", quantity=1),
|
|
160
|
+
)
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The call is same-process and direct. UseCaseAPI does not serialize the input or output.
|
|
164
|
+
|
|
165
|
+
## CLI scaffolding
|
|
166
|
+
|
|
167
|
+
Create the first contract version:
|
|
168
|
+
|
|
169
|
+
```bash
|
|
170
|
+
usecaseapi scaffold commerce place_order --output-root src
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
Create the next major version from the latest existing contract:
|
|
174
|
+
|
|
175
|
+
```bash
|
|
176
|
+
usecaseapi scaffold commerce place_order --output-root src --next
|
|
177
|
+
```
|
|
178
|
+
|
|
179
|
+
The first command creates:
|
|
180
|
+
|
|
181
|
+
```text
|
|
182
|
+
src/commerce/usecases/place_order/v1/place_order_contract.py
|
|
183
|
+
src/commerce/usecases/place_order/v1/place_order_usecase.py
|
|
184
|
+
tests/commerce/usecases/place_order/v1/test_place_order.py
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
See [docs/scaffold.md](docs/scaffold.md) for the generated layout and options.
|
|
188
|
+
|
|
189
|
+
## Manifest
|
|
190
|
+
|
|
191
|
+
Export the canonical contract catalog:
|
|
192
|
+
|
|
193
|
+
```bash
|
|
194
|
+
usecaseapi manifest export composition:usecases --output usecaseapi.ucase.yaml
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
Validate it and check that code still matches it:
|
|
198
|
+
|
|
199
|
+
```bash
|
|
200
|
+
usecaseapi manifest validate usecaseapi.ucase.yaml
|
|
201
|
+
usecaseapi manifest check-sync composition:usecases usecaseapi.ucase.yaml
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
Generate Python contract and implementation skeletons from the catalog:
|
|
205
|
+
|
|
206
|
+
```bash
|
|
207
|
+
usecaseapi manifest scaffold usecaseapi.ucase.yaml --root .
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Render derived outputs:
|
|
211
|
+
|
|
212
|
+
```bash
|
|
213
|
+
usecaseapi docs usecaseapi.ucase.yaml --output usecaseapi.md
|
|
214
|
+
usecaseapi graph usecaseapi.ucase.yaml --output usecaseapi.mmd
|
|
215
|
+
usecaseapi diff old.ucase.yaml new.ucase.yaml
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
See [docs/scaffold.md](docs/scaffold.md), [docs/manifest.md](docs/manifest.md), and
|
|
219
|
+
[docs/llm-manifest-prompt.md](docs/llm-manifest-prompt.md) for generated layouts,
|
|
220
|
+
Manifest syntax, and an LLM prompt for converging requirements into `usecaseapi.ucase.yaml`.
|
|
221
|
+
|
|
222
|
+
## Manifest docs and graph
|
|
223
|
+
|
|
224
|
+
UseCaseAPI can turn a composed application into inspectable artifacts:
|
|
225
|
+
|
|
226
|
+
- a canonical YAML Manifest;
|
|
227
|
+
- a conservative diff between Manifests;
|
|
228
|
+
- Markdown documentation for use cases and dependencies;
|
|
229
|
+
- Mermaid graph output for the dependency graph.
|
|
230
|
+
|
|
231
|
+
See [docs/api-reference.md](docs/api-reference.md), [docs/versioning.md](docs/versioning.md), and [docs/integrations.md](docs/integrations.md).
|
|
232
|
+
|
|
233
|
+
## What UseCaseAPI is not
|
|
234
|
+
|
|
235
|
+
UseCaseAPI is not a web framework and does not add HTTP, RPC, serialization, service locators, dependency injection containers, or transaction managers to your application runtime.
|
|
236
|
+
|
|
237
|
+
You can write plain classes and functions for small projects. UseCaseAPI becomes useful when an application has enough internal use case nodes that stable names, versions, declared dependencies, documented errors, Manifest catalogs, and generated docs start paying for themselves.
|
|
238
|
+
|
|
239
|
+
## Development
|
|
240
|
+
|
|
241
|
+
```bash
|
|
242
|
+
uv sync --extra dev
|
|
243
|
+
uv run pytest
|
|
244
|
+
uv run mypy
|
|
245
|
+
uv run ruff check .
|
|
246
|
+
uv run ruff format --check .
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
For release validation:
|
|
250
|
+
|
|
251
|
+
```bash
|
|
252
|
+
uv build
|
|
253
|
+
uv run twine check dist/*
|
|
254
|
+
uv run --isolated --no-project --with dist/*.whl scripts/verify_distribution.py
|
|
255
|
+
uv run --isolated --no-project --with dist/*.tar.gz scripts/verify_distribution.py
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
See [docs/pypi-publishing.md](docs/pypi-publishing.md) for the publishing workflow.
|
|
@@ -34,9 +34,10 @@ The `src/usecaseapi/__init__.py` module intentionally does not define
|
|
|
34
34
|
Publishing is handled by `.github/workflows/release.yml`.
|
|
35
35
|
|
|
36
36
|
On `main`, the workflow detects changes to `pyproject.toml` `[project].version`,
|
|
37
|
-
validates the package, creates the annotated `v{version}` tag,
|
|
38
|
-
|
|
39
|
-
the
|
|
37
|
+
validates the package, creates the annotated `v{version}` tag, creates a GitHub
|
|
38
|
+
Release with generated release notes and distribution artifacts, and publishes
|
|
39
|
+
to PyPI. Pushing a matching `v*` tag or running the workflow manually also
|
|
40
|
+
publishes the current package version.
|
|
40
41
|
|
|
41
42
|
The workflow expects PyPI Trusted Publishing:
|
|
42
43
|
|
|
@@ -55,5 +56,5 @@ The workflow uses GitHub OIDC and `uv publish`; no PyPI API token should be stor
|
|
|
55
56
|
4. Create the `pypi` GitHub environment and require approval if desired.
|
|
56
57
|
5. Review the generated package metadata and README rendering locally.
|
|
57
58
|
6. Update `pyproject.toml` `[project].version` and changelog or GitHub release notes.
|
|
58
|
-
7. Merge the version bump to `main`; GitHub Actions creates the annotated tag and publishes.
|
|
59
|
-
8. Confirm the GitHub Actions release workflow completed and the PyPI project page is correct.
|
|
59
|
+
7. Merge the version bump to `main`; GitHub Actions creates the annotated tag, creates the GitHub Release, and publishes.
|
|
60
|
+
8. Confirm the GitHub Actions release workflow completed, the GitHub Release is correct, and the PyPI project page is correct.
|
|
@@ -10,7 +10,7 @@ Base class for domain errors that are part of public usecase contracts. Subclass
|
|
|
10
10
|
|
|
11
11
|
```python
|
|
12
12
|
class PlaceOrderError(UseCaseError):
|
|
13
|
-
code: ClassVar[str] = "
|
|
13
|
+
code: ClassVar[str] = "commerce.place_order"
|
|
14
14
|
```
|
|
15
15
|
|
|
16
16
|
## `UseCase[InputT, OutputT]`
|
|
@@ -18,8 +18,8 @@ class PlaceOrderError(UseCaseError):
|
|
|
18
18
|
Structural Protocol for async callable usecase implementations.
|
|
19
19
|
|
|
20
20
|
```python
|
|
21
|
-
class PlaceOrder(UseCase[
|
|
22
|
-
async def __call__(self, input:
|
|
21
|
+
class PlaceOrder(UseCase[PlaceOrderUseCaseInput, PlaceOrderUseCaseOutput], Protocol):
|
|
22
|
+
async def __call__(self, input: PlaceOrderUseCaseInput, /) -> PlaceOrderUseCaseOutput:
|
|
23
23
|
...
|
|
24
24
|
```
|
|
25
25
|
|
|
@@ -49,8 +49,8 @@ Registry and runtime.
|
|
|
49
49
|
|
|
50
50
|
```python
|
|
51
51
|
api = UseCaseAPI[AppContext]()
|
|
52
|
-
api.register(
|
|
53
|
-
api.bind(
|
|
52
|
+
api.register(PLACE_ORDER_USECASE)
|
|
53
|
+
api.bind(PLACE_ORDER_USECASE, lambda caller: PlaceOrderUseCase())
|
|
54
54
|
api.validate()
|
|
55
55
|
caller = api.caller(context)
|
|
56
56
|
```
|
|
@@ -60,20 +60,32 @@ caller = api.caller(context)
|
|
|
60
60
|
Context-bound call surface.
|
|
61
61
|
|
|
62
62
|
```python
|
|
63
|
-
output = await caller.call(
|
|
63
|
+
output = await caller.call(PLACE_ORDER_USECASE, input)
|
|
64
64
|
results = await caller.gather(caller.call(A, a), caller.call(B, b))
|
|
65
65
|
```
|
|
66
66
|
|
|
67
67
|
`Caller.gather` uses `asyncio.TaskGroup`, so multiple failures preserve Python `ExceptionGroup` behavior.
|
|
68
68
|
|
|
69
|
-
## `
|
|
69
|
+
## `manifest_from_api(api)`
|
|
70
70
|
|
|
71
|
-
Creates a
|
|
71
|
+
Creates a YAML-friendly Manifest catalog from a registered `UseCaseAPI` instance.
|
|
72
72
|
|
|
73
|
-
## `
|
|
73
|
+
## `load_manifest(path)` / `dump_manifest(manifest, path)`
|
|
74
|
+
|
|
75
|
+
Loads and writes validated `.ucase.yaml` Manifest files.
|
|
76
|
+
|
|
77
|
+
## `validate_manifest(manifest)`
|
|
78
|
+
|
|
79
|
+
Validates Manifest shape, type expression syntax, error boundaries, model references, and usecase keys.
|
|
80
|
+
|
|
81
|
+
## `scaffold_from_manifest(manifest)`
|
|
82
|
+
|
|
83
|
+
Generates Python contract and implementation skeletons from Manifest entries.
|
|
84
|
+
|
|
85
|
+
## `diff_manifests(old, new)`
|
|
74
86
|
|
|
75
87
|
Performs conservative breaking-change detection.
|
|
76
88
|
|
|
77
|
-
## `
|
|
89
|
+
## `render_manifest_markdown(manifest)` / `render_manifest_graph(manifest)`
|
|
78
90
|
|
|
79
|
-
Renders human-readable docs and graph diagrams.
|
|
91
|
+
Renders human-readable docs and graph diagrams from Manifest data.
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|