stratae 0.0.1a0__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.
- stratae-0.0.1a0/PKG-INFO +303 -0
- stratae-0.0.1a0/pyproject.toml +117 -0
- stratae-0.0.1a0/readme.md +273 -0
- stratae-0.0.1a0/setup.cfg +4 -0
- stratae-0.0.1a0/stratae/__init__.py +31 -0
- stratae-0.0.1a0/stratae/cache/__init__.py +8 -0
- stratae-0.0.1a0/stratae/cache/cache.py +44 -0
- stratae-0.0.1a0/stratae/cache/memory.py +64 -0
- stratae-0.0.1a0/stratae/cache/thread.py +62 -0
- stratae-0.0.1a0/stratae/cache/util.py +8 -0
- stratae-0.0.1a0/stratae/context/__init__.py +5 -0
- stratae-0.0.1a0/stratae/context/context.py +58 -0
- stratae-0.0.1a0/stratae/depends/__init__.py +13 -0
- stratae-0.0.1a0/stratae/depends/_wrappers.py +124 -0
- stratae-0.0.1a0/stratae/depends/depends.py +59 -0
- stratae-0.0.1a0/stratae/depends/exceptions.py +21 -0
- stratae-0.0.1a0/stratae/depends/inject.py +49 -0
- stratae-0.0.1a0/stratae/depends/resolver.py +161 -0
- stratae-0.0.1a0/stratae/integrations/asgi.py +38 -0
- stratae-0.0.1a0/stratae/lifecycle/__init__.py +9 -0
- stratae-0.0.1a0/stratae/lifecycle/_context.py +63 -0
- stratae-0.0.1a0/stratae/lifecycle/_decorators.py +159 -0
- stratae-0.0.1a0/stratae/lifecycle/_scope.py +73 -0
- stratae-0.0.1a0/stratae/lifecycle/_wrappers.py +173 -0
- stratae-0.0.1a0/stratae/lifecycle/async_lifecycle.py +157 -0
- stratae-0.0.1a0/stratae/lifecycle/exceptions.py +21 -0
- stratae-0.0.1a0/stratae/lifecycle/lifecycle.py +151 -0
- stratae-0.0.1a0/stratae/lifecycle/manage.py +33 -0
- stratae-0.0.1a0/stratae/lifecycle/resource.py +168 -0
- stratae-0.0.1a0/stratae/lifecycle/scope.py +73 -0
- stratae-0.0.1a0/stratae.egg-info/PKG-INFO +303 -0
- stratae-0.0.1a0/stratae.egg-info/SOURCES.txt +33 -0
- stratae-0.0.1a0/stratae.egg-info/dependency_links.txt +1 -0
- stratae-0.0.1a0/stratae.egg-info/requires.txt +15 -0
- stratae-0.0.1a0/stratae.egg-info/top_level.txt +1 -0
stratae-0.0.1a0/PKG-INFO
ADDED
|
@@ -0,0 +1,303 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: stratae
|
|
3
|
+
Version: 0.0.1a0
|
|
4
|
+
Summary: A toolkit of composable tools for dependency injection, lifecycle management, and more.
|
|
5
|
+
Author: Scott Wahl
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/Eudaetics/stratae
|
|
8
|
+
Project-URL: Repository, https://github.com/Eudaetics/stratae
|
|
9
|
+
Project-URL: Issues, https://github.com/Eudaetics/stratae/issues
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Requires-Python: >=3.12
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
Provides-Extra: dev
|
|
19
|
+
Requires-Dist: pre-commit>=4.2.0; extra == "dev"
|
|
20
|
+
Requires-Dist: pyright>=1.1.403; extra == "dev"
|
|
21
|
+
Requires-Dist: ruff>=0.12.2; extra == "dev"
|
|
22
|
+
Provides-Extra: test
|
|
23
|
+
Requires-Dist: pytest>=8.4.1; extra == "test"
|
|
24
|
+
Requires-Dist: pytest-asyncio>=1.0.0; extra == "test"
|
|
25
|
+
Requires-Dist: pytest-cov>=6.1.1; extra == "test"
|
|
26
|
+
Requires-Dist: pytest-mock>=3.14.1; extra == "test"
|
|
27
|
+
Provides-Extra: asgi
|
|
28
|
+
Requires-Dist: httpx>=0.28.1; extra == "asgi"
|
|
29
|
+
Requires-Dist: fastapi>=0.100.0; extra == "asgi"
|
|
30
|
+
|
|
31
|
+
# Stratae
|
|
32
|
+
|
|
33
|
+
**Composable tools for dependency injection in Python. Fast, simple, and framework-agnostic.**
|
|
34
|
+
|
|
35
|
+
Stratae is a toolkit designed of small, focused components that layer together to create more powerful systems. It's built to complement Python's 3.12+ features, leveraging decorators, contexts, and functions to create a system that works anywhere.
|
|
36
|
+
|
|
37
|
+
Write your business logic once, use it everywhere: APIs, CLIs, workers, and tests.
|
|
38
|
+
|
|
39
|
+
```bash
|
|
40
|
+
pip install stratae
|
|
41
|
+
```
|
|
42
|
+
|
|
43
|
+
## Quick Start
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from stratae.depends import Depends, inject
|
|
47
|
+
from stratae.lifecycle import Lifecycle
|
|
48
|
+
|
|
49
|
+
lifecycle = Lifecycle(["application"])
|
|
50
|
+
|
|
51
|
+
type Database = dict[str, list[dict[str, str]]]
|
|
52
|
+
|
|
53
|
+
# Simple database connection (just a dict for demo)
|
|
54
|
+
@lifecycle.cache('application')
|
|
55
|
+
def get_database() -> Database:
|
|
56
|
+
return {"users": []}
|
|
57
|
+
|
|
58
|
+
@inject
|
|
59
|
+
def create_user(name: str, db: Database = Depends(get_database)):
|
|
60
|
+
user = {"name": name}
|
|
61
|
+
db["users"].append(user)
|
|
62
|
+
return user
|
|
63
|
+
|
|
64
|
+
# Use anywhere: APIs, CLIs, workers, tests
|
|
65
|
+
with lifecycle.start('application'):
|
|
66
|
+
user = create_user("Alice")
|
|
67
|
+
print(f"Created user: {user['name']}")
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
## Why Stratae?
|
|
71
|
+
|
|
72
|
+
**Simple by design.** Stratae doesn't impose architectural patterns or force you into a framework. It provides focused tools that solve specific problems: dependency injection, lifecycle management, and context variables. Use what you need, ignore what you don't.
|
|
73
|
+
|
|
74
|
+
**Zero lock-in.** Your business logic is just functions with decorators. Remove Stratae anytime by replacing `Depends()` with actual values. No container to untangle, no framework to escape.
|
|
75
|
+
|
|
76
|
+
**Built for performance.** Stratae keeps overhead minimal through straightforward design:
|
|
77
|
+
|
|
78
|
+
- Analyze dependencies once at decoration time
|
|
79
|
+
- No runtime introspection or provider lookups
|
|
80
|
+
- Direct function calls with minimal indirection
|
|
81
|
+
|
|
82
|
+
The result is a system that minimizes overhead and stays out of your way. If you're using a system that has dependency injection, we encourage you to test it yourself. Change one small piece to use Stratae and see if it works for you.
|
|
83
|
+
|
|
84
|
+
## Features
|
|
85
|
+
|
|
86
|
+
### Dependency Injection
|
|
87
|
+
|
|
88
|
+
Dependency injection in Stratae uses familiar decorator syntax that works with any callable. Use this to send values, objects, or anything into a function.
|
|
89
|
+
|
|
90
|
+
```python
|
|
91
|
+
from stratae.depends import Depends, inject
|
|
92
|
+
|
|
93
|
+
def get_config():
|
|
94
|
+
return {"env": "dev", "mode": "strict"}
|
|
95
|
+
|
|
96
|
+
# Decorate the function with inject to resolve dependencies
|
|
97
|
+
@inject
|
|
98
|
+
# Use Depends(...) to mark parameters for injection
|
|
99
|
+
def endpoint(config: dict[str, str] = Depends(get_config)):
|
|
100
|
+
print(f"Environment: {config['env']}, Mode: {config['mode']}")
|
|
101
|
+
|
|
102
|
+
endpoint()
|
|
103
|
+
# Environment: dev, Mode: strict
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
### Lifecycle Management
|
|
107
|
+
|
|
108
|
+
Use lifecycle management when you want to cache objects or guarantee resource cleanup for context managers. With managed resources, everything is cleaned up automatically at the end of a lifecycle scope.
|
|
109
|
+
|
|
110
|
+
```python
|
|
111
|
+
# Set up the lifecycle with your application scopes
|
|
112
|
+
lifecycle = Lifecycle(['application', 'request'])
|
|
113
|
+
|
|
114
|
+
# Lifecycle will cache the yielded value and return it for all calls within a request
|
|
115
|
+
@lifecycle.cache('request')
|
|
116
|
+
# Mark get_session as a contextmanager that will be auto-entered to get the session
|
|
117
|
+
@managed
|
|
118
|
+
def get_session():
|
|
119
|
+
session = Session()
|
|
120
|
+
try:
|
|
121
|
+
yield session
|
|
122
|
+
session.commit()
|
|
123
|
+
except:
|
|
124
|
+
session.rollback()
|
|
125
|
+
raise
|
|
126
|
+
finally:
|
|
127
|
+
session.close()
|
|
128
|
+
|
|
129
|
+
# Set up your lifecycle boundaries
|
|
130
|
+
with lifecycle.start('application'):
|
|
131
|
+
with lifecycle.start('request'):
|
|
132
|
+
# Session is created at first call and cached automatically
|
|
133
|
+
# All get_session calls in this request will return the same session
|
|
134
|
+
db = get_session()
|
|
135
|
+
db.users.create_user('John')
|
|
136
|
+
with lifecycle.start('request'):
|
|
137
|
+
# New request, new session
|
|
138
|
+
db = get_session()
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Context Variables
|
|
142
|
+
|
|
143
|
+
To enable decoupled systems, Stratae uses context variables for setting values that need to be shared among components. This is particularly useful for setting values at runtime that are needed deep in dependency chains. Change values at runtime, or even whole behavior, without needing to thread parameters or manipulate overrides.
|
|
144
|
+
|
|
145
|
+
```python
|
|
146
|
+
from stratae.context import Context
|
|
147
|
+
|
|
148
|
+
lifecycle = Lifecycle('request')
|
|
149
|
+
user_id = Context[int]("user_id")
|
|
150
|
+
|
|
151
|
+
@lifecycle.cache('request')
|
|
152
|
+
@inject
|
|
153
|
+
def get_current_user(uid: int = Depends(user_id)) -> User:
|
|
154
|
+
return fetch_user(uid)
|
|
155
|
+
|
|
156
|
+
@inject
|
|
157
|
+
def create_post(
|
|
158
|
+
content: str,
|
|
159
|
+
user: User = Depends(get_current_user),
|
|
160
|
+
) -> Post:
|
|
161
|
+
return Post(author=user, content=content)
|
|
162
|
+
|
|
163
|
+
with lifecycle.start('request'), user_id.use(123):
|
|
164
|
+
post = create_post("Hello world!")
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Async Support
|
|
168
|
+
|
|
169
|
+
Stratae is fully async compatible. Injection natively works with sync or async functions. Lifecycle offers versions for sync and async handling of resources.
|
|
170
|
+
|
|
171
|
+
```python
|
|
172
|
+
from stratae.depends import Depends, inject
|
|
173
|
+
from stratae.lifecycle import AsyncLifecycle
|
|
174
|
+
|
|
175
|
+
lifecycle = AsyncLifecycle(['application', 'request'])
|
|
176
|
+
|
|
177
|
+
@lifecycle.cache('application')
|
|
178
|
+
async def get_database() -> Database:
|
|
179
|
+
return await Database(url="postgresql://...")
|
|
180
|
+
|
|
181
|
+
@inject
|
|
182
|
+
async def create_user(
|
|
183
|
+
name: str,
|
|
184
|
+
db: Database = Depends(get_database),
|
|
185
|
+
) -> User:
|
|
186
|
+
return await db.users.create(name=name)
|
|
187
|
+
|
|
188
|
+
# Use anywhere: APIs, CLIs, workers, tests
|
|
189
|
+
async with lifecycle.start('application'):
|
|
190
|
+
async with lifecycle.start('request'):
|
|
191
|
+
user = await create_user("Alice")
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Easy Testing
|
|
195
|
+
|
|
196
|
+
With no complex configuration, testing functions decorated with Stratae is easy. The function signature isn't changed, just pass in the mocks you need.
|
|
197
|
+
|
|
198
|
+
```python
|
|
199
|
+
@inject
|
|
200
|
+
def create_user(name: str, db = Depends(get_db)):
|
|
201
|
+
db.user.create(name=name)
|
|
202
|
+
|
|
203
|
+
# Use normally
|
|
204
|
+
create_user('Steve')
|
|
205
|
+
|
|
206
|
+
# Test
|
|
207
|
+
create_user('Jason', db=MockDB())
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
### Framework Agnostic
|
|
211
|
+
|
|
212
|
+
Stratae doesn't have a complex framework to configure or objects to pass around. Write your business logic once with injection, then simply call those functions anywhere.
|
|
213
|
+
|
|
214
|
+
```python
|
|
215
|
+
# Business logic - framework-independent
|
|
216
|
+
@inject
|
|
217
|
+
async def create_user(
|
|
218
|
+
name: str,
|
|
219
|
+
db: Database = Depends(get_database),
|
|
220
|
+
) -> User:
|
|
221
|
+
return await db.users.create(name=name)
|
|
222
|
+
|
|
223
|
+
# FastAPI
|
|
224
|
+
@app.post("/users")
|
|
225
|
+
async def api_create(name: str):
|
|
226
|
+
return await create_user(name)
|
|
227
|
+
|
|
228
|
+
# CLI
|
|
229
|
+
@click.command()
|
|
230
|
+
def cli_create(name: str):
|
|
231
|
+
asyncio.run(create_user(name))
|
|
232
|
+
|
|
233
|
+
# Tests
|
|
234
|
+
async def test_create():
|
|
235
|
+
user = await create_user("Alice", db=mock_db)
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
### Simple Integrations
|
|
239
|
+
|
|
240
|
+
The design of Stratae means integrating with other tools or frameworks is typically easy. For FastAPI, an ASGI middleware that starts the request lifecycle is enough to add Stratae's lifecycle management.
|
|
241
|
+
|
|
242
|
+
```python
|
|
243
|
+
from fastapi import FastAPI
|
|
244
|
+
from stratae.integrations import RequestLifecycleMiddleware
|
|
245
|
+
from stratae.lifecycle import AsyncLifecycle, managed
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
app = FastAPI()
|
|
249
|
+
lifecycle = AsyncLifecycle(['request'])
|
|
250
|
+
|
|
251
|
+
# Add the middleware that starts a lifecycle request
|
|
252
|
+
app.add_middleware(RequestLifecycleMiddleware, lifecycle, 'request')
|
|
253
|
+
|
|
254
|
+
# Everything that needs the session will get the same session
|
|
255
|
+
@lifecycle.cache('request')
|
|
256
|
+
@managed
|
|
257
|
+
async def get_session():
|
|
258
|
+
session = AsyncSession()
|
|
259
|
+
try:
|
|
260
|
+
yield session
|
|
261
|
+
session.commit()
|
|
262
|
+
except:
|
|
263
|
+
session.rollback()
|
|
264
|
+
raise
|
|
265
|
+
finally:
|
|
266
|
+
session.close()
|
|
267
|
+
|
|
268
|
+
@inject
|
|
269
|
+
async def create_user(
|
|
270
|
+
name: str,
|
|
271
|
+
# Using Stratae Depends
|
|
272
|
+
db: Session = Depends(get_session)
|
|
273
|
+
):
|
|
274
|
+
await db.users.create(name=name)
|
|
275
|
+
|
|
276
|
+
# Every request will get a new session
|
|
277
|
+
@app.post('/users')
|
|
278
|
+
async def post_user(name: str):
|
|
279
|
+
await create_user(name)
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
## Documentation
|
|
283
|
+
|
|
284
|
+
More detailed documentation will be published soon.
|
|
285
|
+
|
|
286
|
+
## Contributing
|
|
287
|
+
|
|
288
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
289
|
+
|
|
290
|
+
Before contributing, please:
|
|
291
|
+
|
|
292
|
+
1. Check for open issues or open a new issue to start a discussion
|
|
293
|
+
2. Fork the repository on GitHub
|
|
294
|
+
3. Install development dependencies with `pip install -e ".[dev]"`
|
|
295
|
+
4. Run pre-commit hooks with `pre-commit install`
|
|
296
|
+
5. Make your changes following the project's coding style
|
|
297
|
+
6. Write tests that cover your changes
|
|
298
|
+
7. Update documentation if needed
|
|
299
|
+
8. Submit a pull request
|
|
300
|
+
|
|
301
|
+
## License
|
|
302
|
+
|
|
303
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=80.9.0", "wheel"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "stratae"
|
|
7
|
+
description = "A toolkit of composable tools for dependency injection, lifecycle management, and more."
|
|
8
|
+
version = "0.0.1a"
|
|
9
|
+
readme = "readme.md"
|
|
10
|
+
requires-python = ">=3.12"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [
|
|
13
|
+
{name = "Scott Wahl"}
|
|
14
|
+
]
|
|
15
|
+
classifiers = [
|
|
16
|
+
"Development Status :: 3 - Alpha",
|
|
17
|
+
"Intended Audience :: Developers",
|
|
18
|
+
"License :: OSI Approved :: MIT License",
|
|
19
|
+
"Programming Language :: Python :: 3",
|
|
20
|
+
"Programming Language :: Python :: 3.12",
|
|
21
|
+
"Programming Language :: Python :: 3.13",
|
|
22
|
+
]
|
|
23
|
+
dependencies = []
|
|
24
|
+
|
|
25
|
+
[project.urls]
|
|
26
|
+
Homepage = "https://github.com/Eudaetics/stratae"
|
|
27
|
+
Repository = "https://github.com/Eudaetics/stratae"
|
|
28
|
+
Issues = "https://github.com/Eudaetics/stratae/issues"
|
|
29
|
+
|
|
30
|
+
[project.optional-dependencies]
|
|
31
|
+
dev = [
|
|
32
|
+
"pre-commit>=4.2.0",
|
|
33
|
+
"pyright>=1.1.403",
|
|
34
|
+
"ruff>=0.12.2",
|
|
35
|
+
]
|
|
36
|
+
test = [
|
|
37
|
+
"pytest>=8.4.1",
|
|
38
|
+
"pytest-asyncio>=1.0.0",
|
|
39
|
+
"pytest-cov>=6.1.1",
|
|
40
|
+
"pytest-mock>=3.14.1",
|
|
41
|
+
]
|
|
42
|
+
asgi = [
|
|
43
|
+
"httpx>=0.28.1",
|
|
44
|
+
"fastapi>=0.100.0"
|
|
45
|
+
]
|
|
46
|
+
|
|
47
|
+
[tool.setuptools.packages.find]
|
|
48
|
+
where = ["."]
|
|
49
|
+
include = ["stratae*"]
|
|
50
|
+
|
|
51
|
+
[tool.ruff]
|
|
52
|
+
lint.select = ["E", "F", "B", "I", "D", "S", "W"]
|
|
53
|
+
lint.ignore = ["D203", "D212", "B008"]
|
|
54
|
+
lint.external = ["S7497"]
|
|
55
|
+
line-length = 100
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
[tool.ruff.lint.per-file-ignores]
|
|
59
|
+
"tests/**/*.py" = [
|
|
60
|
+
# at least these three should be fine in tests:
|
|
61
|
+
"S101", # asserts allowed in tests...
|
|
62
|
+
"ARG", # Unused function args -> fixtures nevertheless are functionally relevant...
|
|
63
|
+
"FBT", # Don't care about booleans as positional arguments in tests, e.g. via @pytest.mark.parametrize()
|
|
64
|
+
"S311", # Not using cryptographic safe randomness in tests, just getting random values
|
|
65
|
+
]
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
minversion = "8.0"
|
|
69
|
+
testpaths = "tests/"
|
|
70
|
+
cache_dir = ".pytest_cache"
|
|
71
|
+
addopts = [
|
|
72
|
+
"--cov=stratae/",
|
|
73
|
+
"--cov-report=html:coverage/cov.html",
|
|
74
|
+
"--no-cov-on-fail"
|
|
75
|
+
]
|
|
76
|
+
asyncio_mode = "auto"
|
|
77
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
78
|
+
markers = [
|
|
79
|
+
"asgi: ASGI framework integration tests (requires fastapi, httpx)",
|
|
80
|
+
]
|
|
81
|
+
|
|
82
|
+
[tool.coverage.run]
|
|
83
|
+
omit = [
|
|
84
|
+
"*/venv/*",
|
|
85
|
+
"*/.venv/*",
|
|
86
|
+
"*/migrations/*",
|
|
87
|
+
"*/alembic/*",
|
|
88
|
+
"*/_version.py",
|
|
89
|
+
]
|
|
90
|
+
|
|
91
|
+
[tool.coverage.report]
|
|
92
|
+
exclude_lines = [
|
|
93
|
+
"@(abc\\.)?abstractmethod",
|
|
94
|
+
"^\\s*\\.\\.\\.$",
|
|
95
|
+
"if TYPE_CHECKING:"
|
|
96
|
+
]
|
|
97
|
+
|
|
98
|
+
[tool.pyright]
|
|
99
|
+
include = ["stratae", "tests"]
|
|
100
|
+
exclude = ["**/__pycache__"]
|
|
101
|
+
|
|
102
|
+
typeCheckingMode = "strict"
|
|
103
|
+
reportMissingImports = true
|
|
104
|
+
reportMissingTypeStubs = false
|
|
105
|
+
reportUnusedImport = true
|
|
106
|
+
reportUnusedClass = true
|
|
107
|
+
reportUnusedFunction = true
|
|
108
|
+
reportUnusedVariable = true
|
|
109
|
+
reportDuplicateImport = true
|
|
110
|
+
reportOptionalSubscript = true
|
|
111
|
+
reportOptionalMemberAccess = true
|
|
112
|
+
reportOptionalCall = true
|
|
113
|
+
reportOptionalIterable = true
|
|
114
|
+
reportUntypedFunctionDecorator = true
|
|
115
|
+
|
|
116
|
+
# Python version
|
|
117
|
+
pythonVersion = "3.12"
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
# Stratae
|
|
2
|
+
|
|
3
|
+
**Composable tools for dependency injection in Python. Fast, simple, and framework-agnostic.**
|
|
4
|
+
|
|
5
|
+
Stratae is a toolkit designed of small, focused components that layer together to create more powerful systems. It's built to complement Python's 3.12+ features, leveraging decorators, contexts, and functions to create a system that works anywhere.
|
|
6
|
+
|
|
7
|
+
Write your business logic once, use it everywhere: APIs, CLIs, workers, and tests.
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pip install stratae
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
## Quick Start
|
|
14
|
+
|
|
15
|
+
```python
|
|
16
|
+
from stratae.depends import Depends, inject
|
|
17
|
+
from stratae.lifecycle import Lifecycle
|
|
18
|
+
|
|
19
|
+
lifecycle = Lifecycle(["application"])
|
|
20
|
+
|
|
21
|
+
type Database = dict[str, list[dict[str, str]]]
|
|
22
|
+
|
|
23
|
+
# Simple database connection (just a dict for demo)
|
|
24
|
+
@lifecycle.cache('application')
|
|
25
|
+
def get_database() -> Database:
|
|
26
|
+
return {"users": []}
|
|
27
|
+
|
|
28
|
+
@inject
|
|
29
|
+
def create_user(name: str, db: Database = Depends(get_database)):
|
|
30
|
+
user = {"name": name}
|
|
31
|
+
db["users"].append(user)
|
|
32
|
+
return user
|
|
33
|
+
|
|
34
|
+
# Use anywhere: APIs, CLIs, workers, tests
|
|
35
|
+
with lifecycle.start('application'):
|
|
36
|
+
user = create_user("Alice")
|
|
37
|
+
print(f"Created user: {user['name']}")
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## Why Stratae?
|
|
41
|
+
|
|
42
|
+
**Simple by design.** Stratae doesn't impose architectural patterns or force you into a framework. It provides focused tools that solve specific problems: dependency injection, lifecycle management, and context variables. Use what you need, ignore what you don't.
|
|
43
|
+
|
|
44
|
+
**Zero lock-in.** Your business logic is just functions with decorators. Remove Stratae anytime by replacing `Depends()` with actual values. No container to untangle, no framework to escape.
|
|
45
|
+
|
|
46
|
+
**Built for performance.** Stratae keeps overhead minimal through straightforward design:
|
|
47
|
+
|
|
48
|
+
- Analyze dependencies once at decoration time
|
|
49
|
+
- No runtime introspection or provider lookups
|
|
50
|
+
- Direct function calls with minimal indirection
|
|
51
|
+
|
|
52
|
+
The result is a system that minimizes overhead and stays out of your way. If you're using a system that has dependency injection, we encourage you to test it yourself. Change one small piece to use Stratae and see if it works for you.
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
### Dependency Injection
|
|
57
|
+
|
|
58
|
+
Dependency injection in Stratae uses familiar decorator syntax that works with any callable. Use this to send values, objects, or anything into a function.
|
|
59
|
+
|
|
60
|
+
```python
|
|
61
|
+
from stratae.depends import Depends, inject
|
|
62
|
+
|
|
63
|
+
def get_config():
|
|
64
|
+
return {"env": "dev", "mode": "strict"}
|
|
65
|
+
|
|
66
|
+
# Decorate the function with inject to resolve dependencies
|
|
67
|
+
@inject
|
|
68
|
+
# Use Depends(...) to mark parameters for injection
|
|
69
|
+
def endpoint(config: dict[str, str] = Depends(get_config)):
|
|
70
|
+
print(f"Environment: {config['env']}, Mode: {config['mode']}")
|
|
71
|
+
|
|
72
|
+
endpoint()
|
|
73
|
+
# Environment: dev, Mode: strict
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
### Lifecycle Management
|
|
77
|
+
|
|
78
|
+
Use lifecycle management when you want to cache objects or guarantee resource cleanup for context managers. With managed resources, everything is cleaned up automatically at the end of a lifecycle scope.
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
# Set up the lifecycle with your application scopes
|
|
82
|
+
lifecycle = Lifecycle(['application', 'request'])
|
|
83
|
+
|
|
84
|
+
# Lifecycle will cache the yielded value and return it for all calls within a request
|
|
85
|
+
@lifecycle.cache('request')
|
|
86
|
+
# Mark get_session as a contextmanager that will be auto-entered to get the session
|
|
87
|
+
@managed
|
|
88
|
+
def get_session():
|
|
89
|
+
session = Session()
|
|
90
|
+
try:
|
|
91
|
+
yield session
|
|
92
|
+
session.commit()
|
|
93
|
+
except:
|
|
94
|
+
session.rollback()
|
|
95
|
+
raise
|
|
96
|
+
finally:
|
|
97
|
+
session.close()
|
|
98
|
+
|
|
99
|
+
# Set up your lifecycle boundaries
|
|
100
|
+
with lifecycle.start('application'):
|
|
101
|
+
with lifecycle.start('request'):
|
|
102
|
+
# Session is created at first call and cached automatically
|
|
103
|
+
# All get_session calls in this request will return the same session
|
|
104
|
+
db = get_session()
|
|
105
|
+
db.users.create_user('John')
|
|
106
|
+
with lifecycle.start('request'):
|
|
107
|
+
# New request, new session
|
|
108
|
+
db = get_session()
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
### Context Variables
|
|
112
|
+
|
|
113
|
+
To enable decoupled systems, Stratae uses context variables for setting values that need to be shared among components. This is particularly useful for setting values at runtime that are needed deep in dependency chains. Change values at runtime, or even whole behavior, without needing to thread parameters or manipulate overrides.
|
|
114
|
+
|
|
115
|
+
```python
|
|
116
|
+
from stratae.context import Context
|
|
117
|
+
|
|
118
|
+
lifecycle = Lifecycle('request')
|
|
119
|
+
user_id = Context[int]("user_id")
|
|
120
|
+
|
|
121
|
+
@lifecycle.cache('request')
|
|
122
|
+
@inject
|
|
123
|
+
def get_current_user(uid: int = Depends(user_id)) -> User:
|
|
124
|
+
return fetch_user(uid)
|
|
125
|
+
|
|
126
|
+
@inject
|
|
127
|
+
def create_post(
|
|
128
|
+
content: str,
|
|
129
|
+
user: User = Depends(get_current_user),
|
|
130
|
+
) -> Post:
|
|
131
|
+
return Post(author=user, content=content)
|
|
132
|
+
|
|
133
|
+
with lifecycle.start('request'), user_id.use(123):
|
|
134
|
+
post = create_post("Hello world!")
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
### Async Support
|
|
138
|
+
|
|
139
|
+
Stratae is fully async compatible. Injection natively works with sync or async functions. Lifecycle offers versions for sync and async handling of resources.
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
from stratae.depends import Depends, inject
|
|
143
|
+
from stratae.lifecycle import AsyncLifecycle
|
|
144
|
+
|
|
145
|
+
lifecycle = AsyncLifecycle(['application', 'request'])
|
|
146
|
+
|
|
147
|
+
@lifecycle.cache('application')
|
|
148
|
+
async def get_database() -> Database:
|
|
149
|
+
return await Database(url="postgresql://...")
|
|
150
|
+
|
|
151
|
+
@inject
|
|
152
|
+
async def create_user(
|
|
153
|
+
name: str,
|
|
154
|
+
db: Database = Depends(get_database),
|
|
155
|
+
) -> User:
|
|
156
|
+
return await db.users.create(name=name)
|
|
157
|
+
|
|
158
|
+
# Use anywhere: APIs, CLIs, workers, tests
|
|
159
|
+
async with lifecycle.start('application'):
|
|
160
|
+
async with lifecycle.start('request'):
|
|
161
|
+
user = await create_user("Alice")
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
### Easy Testing
|
|
165
|
+
|
|
166
|
+
With no complex configuration, testing functions decorated with Stratae is easy. The function signature isn't changed, just pass in the mocks you need.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
@inject
|
|
170
|
+
def create_user(name: str, db = Depends(get_db)):
|
|
171
|
+
db.user.create(name=name)
|
|
172
|
+
|
|
173
|
+
# Use normally
|
|
174
|
+
create_user('Steve')
|
|
175
|
+
|
|
176
|
+
# Test
|
|
177
|
+
create_user('Jason', db=MockDB())
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
### Framework Agnostic
|
|
181
|
+
|
|
182
|
+
Stratae doesn't have a complex framework to configure or objects to pass around. Write your business logic once with injection, then simply call those functions anywhere.
|
|
183
|
+
|
|
184
|
+
```python
|
|
185
|
+
# Business logic - framework-independent
|
|
186
|
+
@inject
|
|
187
|
+
async def create_user(
|
|
188
|
+
name: str,
|
|
189
|
+
db: Database = Depends(get_database),
|
|
190
|
+
) -> User:
|
|
191
|
+
return await db.users.create(name=name)
|
|
192
|
+
|
|
193
|
+
# FastAPI
|
|
194
|
+
@app.post("/users")
|
|
195
|
+
async def api_create(name: str):
|
|
196
|
+
return await create_user(name)
|
|
197
|
+
|
|
198
|
+
# CLI
|
|
199
|
+
@click.command()
|
|
200
|
+
def cli_create(name: str):
|
|
201
|
+
asyncio.run(create_user(name))
|
|
202
|
+
|
|
203
|
+
# Tests
|
|
204
|
+
async def test_create():
|
|
205
|
+
user = await create_user("Alice", db=mock_db)
|
|
206
|
+
```
|
|
207
|
+
|
|
208
|
+
### Simple Integrations
|
|
209
|
+
|
|
210
|
+
The design of Stratae means integrating with other tools or frameworks is typically easy. For FastAPI, an ASGI middleware that starts the request lifecycle is enough to add Stratae's lifecycle management.
|
|
211
|
+
|
|
212
|
+
```python
|
|
213
|
+
from fastapi import FastAPI
|
|
214
|
+
from stratae.integrations import RequestLifecycleMiddleware
|
|
215
|
+
from stratae.lifecycle import AsyncLifecycle, managed
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
app = FastAPI()
|
|
219
|
+
lifecycle = AsyncLifecycle(['request'])
|
|
220
|
+
|
|
221
|
+
# Add the middleware that starts a lifecycle request
|
|
222
|
+
app.add_middleware(RequestLifecycleMiddleware, lifecycle, 'request')
|
|
223
|
+
|
|
224
|
+
# Everything that needs the session will get the same session
|
|
225
|
+
@lifecycle.cache('request')
|
|
226
|
+
@managed
|
|
227
|
+
async def get_session():
|
|
228
|
+
session = AsyncSession()
|
|
229
|
+
try:
|
|
230
|
+
yield session
|
|
231
|
+
session.commit()
|
|
232
|
+
except:
|
|
233
|
+
session.rollback()
|
|
234
|
+
raise
|
|
235
|
+
finally:
|
|
236
|
+
session.close()
|
|
237
|
+
|
|
238
|
+
@inject
|
|
239
|
+
async def create_user(
|
|
240
|
+
name: str,
|
|
241
|
+
# Using Stratae Depends
|
|
242
|
+
db: Session = Depends(get_session)
|
|
243
|
+
):
|
|
244
|
+
await db.users.create(name=name)
|
|
245
|
+
|
|
246
|
+
# Every request will get a new session
|
|
247
|
+
@app.post('/users')
|
|
248
|
+
async def post_user(name: str):
|
|
249
|
+
await create_user(name)
|
|
250
|
+
```
|
|
251
|
+
|
|
252
|
+
## Documentation
|
|
253
|
+
|
|
254
|
+
More detailed documentation will be published soon.
|
|
255
|
+
|
|
256
|
+
## Contributing
|
|
257
|
+
|
|
258
|
+
Contributions are welcome! Please feel free to submit a Pull Request.
|
|
259
|
+
|
|
260
|
+
Before contributing, please:
|
|
261
|
+
|
|
262
|
+
1. Check for open issues or open a new issue to start a discussion
|
|
263
|
+
2. Fork the repository on GitHub
|
|
264
|
+
3. Install development dependencies with `pip install -e ".[dev]"`
|
|
265
|
+
4. Run pre-commit hooks with `pre-commit install`
|
|
266
|
+
5. Make your changes following the project's coding style
|
|
267
|
+
6. Write tests that cover your changes
|
|
268
|
+
7. Update documentation if needed
|
|
269
|
+
8. Submit a pull request
|
|
270
|
+
|
|
271
|
+
## License
|
|
272
|
+
|
|
273
|
+
This project is licensed under the MIT License - see the LICENSE file for details.
|