effecton 0.1.0__tar.gz → 0.2.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 (43) hide show
  1. {effecton-0.1.0 → effecton-0.2.0}/.gitignore +3 -0
  2. effecton-0.2.0/CHANGELOG.md +19 -0
  3. effecton-0.2.0/PKG-INFO +389 -0
  4. effecton-0.2.0/README.md +373 -0
  5. {effecton-0.1.0 → effecton-0.2.0}/pyproject.toml +8 -13
  6. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/__init__.py +20 -5
  7. effecton-0.2.0/src/effecton/attempt.py +42 -0
  8. effecton-0.2.0/src/effecton/catch.py +36 -0
  9. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/effect.py +64 -13
  10. effecton-0.2.0/src/effecton/exit.py +63 -0
  11. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/implicit_requirement.py +1 -9
  12. effecton-0.2.0/src/effecton/provide.py +36 -0
  13. effecton-0.2.0/src/effecton/run_async.py +237 -0
  14. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/run_sync.py +58 -20
  15. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/suspend.py +1 -3
  16. effecton-0.1.0/PKG-INFO +0 -307
  17. effecton-0.1.0/README.md +0 -291
  18. effecton-0.1.0/src/effecton/attempt.py +0 -24
  19. effecton-0.1.0/src/effecton/exit.py +0 -21
  20. effecton-0.1.0/src/effecton/provide.py +0 -60
  21. effecton-0.1.0/tests/std/test_logger.py +0 -132
  22. effecton-0.1.0/tests/std/test_pretty_logger.py +0 -104
  23. effecton-0.1.0/tests/std/test_scope.py +0 -225
  24. effecton-0.1.0/tests/std/test_types_logger.py +0 -46
  25. effecton-0.1.0/tests/std/test_types_scope.py +0 -138
  26. effecton-0.1.0/tests/test_attempt.py +0 -81
  27. effecton-0.1.0/tests/test_gen.py +0 -168
  28. effecton-0.1.0/tests/test_implicit_requirement.py +0 -111
  29. effecton-0.1.0/tests/test_run_sync.py +0 -489
  30. effecton-0.1.0/tests/test_sample_program.py +0 -63
  31. effecton-0.1.0/tests/test_suspend.py +0 -171
  32. effecton-0.1.0/tests/test_types_attempt.py +0 -22
  33. effecton-0.1.0/tests/test_types_gen.py +0 -123
  34. effecton-0.1.0/tests/test_types_implicit_requirement.py +0 -84
  35. effecton-0.1.0/tests/test_types_run_sync.py +0 -266
  36. effecton-0.1.0/tests/test_types_suspend.py +0 -66
  37. {effecton-0.1.0 → effecton-0.2.0}/LICENSE +0 -0
  38. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/gen.py +0 -0
  39. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/py.typed +0 -0
  40. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/std/__init__.py +0 -0
  41. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/std/logger.py +30 -30
  42. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/std/pretty_logger.py +0 -0
  43. {effecton-0.1.0 → effecton-0.2.0}/src/effecton/std/scope.py +0 -0
@@ -8,3 +8,6 @@ wheels/
8
8
 
9
9
  # Virtual environments
10
10
  .venv
11
+
12
+ # Copied from the repo root by the release workflow
13
+ packages/effecton/LICENSE
@@ -0,0 +1,19 @@
1
+ # effecton
2
+
3
+ ## 0.2.0
4
+
5
+ ### Minor Changes
6
+
7
+ - Add `run_async`, `coroutine` and `attempt_async` for awaitable-backed effects ([#12](https://github.com/krzkaczor/effecton/pull/12))
8
+ - Split the runners: `run_sync` and `run_async` now return the value and raise on failure (the error, the defect, or `UnhandledDefect` for a non-exception defect); `run_sync_exit`, `run_async_exit` and the coroutine `run_async_coroutine` return the `Exit` ([#13](https://github.com/krzkaczor/effecton/pull/13))
9
+
10
+ ### Patch Changes
11
+
12
+ - Add `Effect.catch`: handle one error type and subtract it from the error channel. ([#10](https://github.com/krzkaczor/effecton/pull/10))
13
+ - Add `Interrupt` as a third `Cause` state, produced when a cancellation unwinds `run_async` ([#12](https://github.com/krzkaczor/effecton/pull/12))
14
+
15
+ ## 0.1.1
16
+
17
+ ### Patch Changes
18
+
19
+ - Migrate from mypy to ty. Simplify api where possible. ([#1](https://github.com/krzkaczor/effecton/pull/1))
@@ -0,0 +1,389 @@
1
+ Metadata-Version: 2.5
2
+ Name: effecton
3
+ Version: 0.2.0
4
+ Summary: A typed effect system for Python, inspired by Effect-TS
5
+ Project-URL: Repository, https://github.com/krzkaczor/effecton
6
+ Author-email: Krzysztof Kaczor <chris@kaczor.io>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Keywords: dependency-injection,effect-system,effect-ts,functional,typed
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Programming Language :: Python :: 3.14
12
+ Classifier: Typing :: Typed
13
+ Requires-Python: >=3.14
14
+ Requires-Dist: typing-extensions>=4.16.0
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Effecton
18
+
19
+ [![PyPI](https://img.shields.io/pypi/v/effecton?logo=pypi&logoColor=white)](https://pypi.org/project/effecton/)
20
+ [![Discord](https://img.shields.io/badge/Discord-join%20chat-5865F2?logo=discord&logoColor=white)](https://discord.gg/fNhY7AxMyh)
21
+
22
+ A typed effect system for Python, inspired by [Effect-TS](https://effect.website/). Early stage and experimental.
23
+
24
+ An `Effect[A, E, R]` is a description of a computation that succeeds with `A`, fails with a typed error `E` and requires `R` dependencies.
25
+
26
+ ```python
27
+ from dataclasses import dataclass
28
+ from typing import final
29
+
30
+ import effecton as E
31
+
32
+
33
+ # Custom errors extend EffectonError and are final: one leaf class per cause
34
+ @final
35
+ @dataclass(frozen=True)
36
+ class SecretInvalidError(E.EffectonError):
37
+ actual: str
38
+
39
+
40
+ # signature means that it succeeds with str, fails with SecretInvalidError or HttpError and it requires HttpClient
41
+ @E.gen
42
+ def check_secret() -> E.EffectGen[
43
+ str, SecretInvalidError | HttpError, HttpClient.Protocol
44
+ ]:
45
+ http = yield from E.require(HttpClient.Protocol) # requires HttpClient.Protocol
46
+
47
+ secret = yield from http.get_text(
48
+ "https://example.com/secret"
49
+ ) # secret is a str; HttpError joins the error channel
50
+ if secret != "hunter2":
51
+ yield from E.fail(
52
+ SecretInvalidError(secret)
53
+ ) # SecretInvalidError joins the error channel
54
+ return secret
55
+
56
+
57
+ # program can be executed only after its requirements are provided
58
+ program = check_secret().provide(HttpClient.Protocol)(HttpClient.Live())
59
+
60
+ match E.run_sync_exit(program):
61
+ case E.Succeeded(value):
62
+ print(value) # "hunter2"
63
+ case E.Failure(cause):
64
+ print(cause) # Fail(SecretInvalidError(...)) or Fail(HttpStatusError(...))
65
+ ```
66
+
67
+ ## Installation
68
+
69
+ Requires Python 3.14 or later.
70
+
71
+ ```sh
72
+ uv add effecton
73
+ # or
74
+ pip install effecton
75
+ ```
76
+
77
+ ## Features
78
+
79
+ * *Type-safe errors* -- stop guessing what a given function throws; implement surgical error handling to build reliable systems.
80
+ * *Dependency injection* -- with requirements, dependencies become visible. In tests, another implementation can be trivially injected. Forgetting to do so is a type error.
81
+ * *Finalizers* -- granular resource management.
82
+ * *Ergonomic* -- generator-based syntax with `@E.gen` and functional-style `flat_map`, `map`, and friends.
83
+
84
+
85
+ ## Motivation
86
+
87
+ Effect based systems provide programmers with building blocks that might be difficult at first but yield benefits in the future. Handling edge cases and thorough testing might be optional in the prototype stage but becomes critical in production.
88
+
89
+ Furthermore, *agents love* strict type systems and building blocks.
90
+
91
+ *Full example*: [skills-cli](https://github.com/krzkaczor/effecton/tree/main/packages/examples/skills-cli), a small CLI for installing agent skills built entirely on effecton services.
92
+
93
+ ## Overview
94
+
95
+ ### Building effects
96
+
97
+ ```python
98
+ E.success(21).map(lambda x: x * 2) # Effect[int]
99
+
100
+ E.sync(lambda: print("hi")) # Effect[None] — defers a side effect until the effect runs
101
+
102
+
103
+ # Custom errors extend EffectonError and are final: one leaf class per cause
104
+ @final
105
+ @dataclass(frozen=True)
106
+ class OopsError(E.EffectonError):
107
+ msg: str
108
+
109
+
110
+ E.fail(OopsError(msg="oops")) # Effect[Never, OopsError]
111
+ ```
112
+
113
+ `suspend` defers building an effect. The thunk form wraps one effect; as a decorator on a function with parameters, each call captures its arguments and defers the body until the effect runs:
114
+
115
+ ```python
116
+ E.suspend(lambda: E.fail(OopsError(msg="later"))) # Effect[Never, OopsError]
117
+
118
+
119
+ @E.suspend
120
+ def find_user(user_id: int) -> E.Effect[str, OopsError]:
121
+ print("runs only when the effect is interpreted")
122
+ return E.success(f"user-{user_id}")
123
+
124
+
125
+ find_user(1) # Effect[str, OopsError] — nothing printed yet
126
+ ```
127
+
128
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_run_sync.py), [`test_suspend.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_suspend.py).
129
+
130
+ ### Running effects
131
+
132
+ Effects are inert values; a runner interprets one. Each runner comes in a throwing form that returns the value and an `_exit` form that returns an `Exit`:
133
+
134
+ | Runner | Runs | Returns |
135
+ |---|---|---|
136
+ | `run_sync(effect)` | synchronously | the value, raising on failure |
137
+ | `run_sync_exit(effect)` | synchronously | `Exit[A, E]` |
138
+ | `run_async(effect)` | on a fresh asyncio loop (`asyncio.run` inside) | the value, raising on failure |
139
+ | `run_async_exit(effect)` | on a fresh asyncio loop (`asyncio.run` inside) | `Exit[A, E]` |
140
+ | `await run_async_coroutine(effect)` | inside a loop you already own | `Exit[A, E]` |
141
+
142
+ The throwing forms raise a typed failure as the error itself (every `EffectonError` is an `Exception`), re-raise an exception defect as it is, wrap any other defect in `UnhandledDefect`, and re-raise the exception carried by an interruption:
143
+
144
+ ```python
145
+ try:
146
+ value = E.run_sync(effect) # A
147
+ except HttpStatusError as e: # a typed failure
148
+ ...
149
+ ```
150
+
151
+ The `_exit` forms never raise; they return an `Exit` to match on:
152
+
153
+ ```python
154
+ match E.run_sync_exit(effect): # Exit[A, E] = Succeeded[A] | Failure[E]
155
+ case E.Succeeded(value):
156
+ ...
157
+ case E.Failure(cause):
158
+ ... # cause is Fail(error) for typed failures, Die(defect) for unexpected exceptions, Interrupt(exception) for cancellations
159
+ ```
160
+
161
+ `run_async` and `run_async_exit` interpret the same effect under asyncio, awaiting every `coroutine` effect they reach. They own the event loop through `asyncio.run`, so they cannot be called from a running loop; `run_async_coroutine` is the coroutine underneath, for a caller that already has one:
162
+
163
+ ```python
164
+ exit = await E.run_async_coroutine(
165
+ effect
166
+ ) # Exit[A, E], awaiting coroutine effects along the way
167
+ ```
168
+
169
+ Running an effect that contains a `coroutine` effect synchronously doesn't await it: `run_sync_exit` settles as `Failure(Die(AsyncEffectInSyncRun()))`, `run_sync` raises `AsyncEffectInSyncRun`, and finalizers still run in both cases.
170
+
171
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_run_sync.py), [`test_run_async.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_run_async.py).
172
+
173
+ ### Error handling
174
+
175
+ Use `catch_all` to handle errors:
176
+
177
+ ```python
178
+ p = E.fail(OopsError(msg="oops")).catch_all(
179
+ lambda e: E.success(f"recovered from {e.msg}")
180
+ ) # Effect[str] — the error channel is now Never
181
+
182
+ E.run_sync(p) # "recovered from oops"
183
+ ```
184
+
185
+ Use `catch` to handle one error type and leave the rest in the error channel. The handler receives the narrowed error, and defects (`Die`) pass through untouched:
186
+
187
+ ```python
188
+ n = random.randint(1, 4)
189
+
190
+ p = E.success(n).flat_map(
191
+ lambda r: E.fail(FatalError()) if r == 2 else E.fail(RecoverableError())
192
+ ) # Effect[Never, FatalError | RecoverableError]
193
+
194
+ p2 = p.catch(RecoverableError)(lambda e: E.success(42)) # Effect[int, FatalError]
195
+ ```
196
+
197
+ `catch` is curried like `provide`: the error class is bound first so the type checker can subtract it from the union. Catching a class the effect cannot fail with is a well-typed no-op.
198
+
199
+ Error classes are leaves: mark every error `@final` and never subclass one. `catch` matches by class, and `@final` keeps its runtime `isinstance` check and the static subtraction in agreement, because the type checker cannot distinguish a subclass from its base when subtracting from a union.
200
+
201
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_run_sync.py).
202
+
203
+ ### Requirements and providing them
204
+
205
+ `require(T)` reads a dependency and records it in the `R` channel; composing effects unions their requirements, exactly like errors. the runners only accept `Effect[A, E]`, so running an effect with unmet requirements is a type error, not a runtime surprise.
206
+
207
+ ```python
208
+ @dataclass(frozen=True)
209
+ class Db:
210
+ url: str
211
+
212
+
213
+ needs_db = E.require(Db).map(lambda db: db.url) # Effect[str, Never, Db]
214
+
215
+ program = needs_db.provide(Db)(Db("postgres://x")) # Effect[str] — runnable
216
+
217
+ E.run_sync(program) # "postgres://x"
218
+ ```
219
+
220
+ `provide(T)(impl)` subtracts the provided type from `R`, so requirements can be provided one at a time, anywhere in the program — a partially provided effect is an ordinary value carrying the remainder in `R`, and the runners accept it only once `R` reaches `Never`.
221
+
222
+ More examples: [`test_run_sync.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_run_sync.py).
223
+
224
+ ### Implicit requirements
225
+
226
+ Some dependencies, such as a logger or a log level, should work out of the box yet stay overridable. An implicit requirement is a class that extends the `ImplicitRequirement` protocol with a `default()` classmethod. Reading one with `require_implicit(X)` types as `Effect[X]`: it never enters `R`, so a program that only uses implicit requirements runs bare. If the lookup misses, the interpreter falls back to `X.default()`, computed once per process and memoized, so defaults must be immutable values.
227
+
228
+ ```python
229
+ @final
230
+ @dataclass(frozen=True)
231
+ class Greeting(E.ImplicitRequirement):
232
+ text: str
233
+
234
+ @classmethod
235
+ def default(cls) -> Greeting:
236
+ return Greeting("hello")
237
+
238
+
239
+ E.run_sync(E.require_implicit(Greeting)) # Greeting("hello") — nothing provided
240
+
241
+ # override for a sub-effect only; the env is restored when it settles
242
+ E.run_sync(E.provide_implicit(E.require_implicit(Greeting), Greeting("hi")))
243
+ ```
244
+
245
+ `provide_implicit(effect, value)` is keyed by `type(value)`, so mark implicit requirement classes `@final`. Overrides also compose in a `provide` chain: `effect.provide(Greeting)(Greeting("hi"))`. `require_implicit` is a separate accessor rather than an overload on `require` because the overload pair silently drops requirements from `R` in some inference positions (pinned in `test_types_implicit_requirement.py`). One footgun: the runtime check only tests that a `default` attribute exists, so a plain requirement class that defines one gets the default fallback instead of a `MissingRequirement` defect.
246
+
247
+ More examples: [`test_implicit_requirement.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_implicit_requirement.py).
248
+
249
+ ### Resource management with `on_exit` and Scope
250
+
251
+ `on_exit` attaches a finalizer that runs when the effect settles, on success and failure alike. A `Scope` collects finalizers from a whole sub-tree: `acquire_and_release` registers a release for an acquired resource, and `.scoped()` provides the `Scope` and runs the collected finalizers in reverse order when the wrapped effect settles. It composes with `provide` chains — `program.provide(Db)(db).scoped()` — and is a no-op on effects that never acquired a `Scope`, so it can uniformly terminate a chain.
252
+
253
+ ```python
254
+ E.success(21).on_exit(E.log_info("done")) # finalizer runs on success and failure alike
255
+
256
+ conn = E.acquire_and_release(
257
+ E.sync(lambda: pool.connect()), # acquire
258
+ lambda c: E.sync(c.close), # release, guaranteed by the enclosing scope
259
+ ) # Effect[Connection, Never, Scope]
260
+
261
+ program = conn.flat_map(
262
+ run_queries
263
+ ).scoped() # Scope discharged; close() runs when program settles
264
+ ```
265
+
266
+ A finalizer that dies doesn't skip the remaining finalizers; its defect surfaces in the final `Exit`.
267
+
268
+ More examples: [`test_scope.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/std/test_scope.py).
269
+
270
+ ### Generator syntax
271
+
272
+ `@E.gen` turns a generator function into a factory of effects: the interpreter runs each yielded effect and sends its success value back into the generator, and the generator's return value becomes the effect's success value. Write `x = yield from effect`, not `x = yield effect` — `Effect.__iter__` is typed so `yield from` gives `x` the effect's success type, while a bare `yield` types as `Any`.
273
+
274
+ ```python
275
+ @E.gen
276
+ def total(n: int) -> E.EffectGen[int, OopsError]:
277
+ a = yield from E.success(20) # a: int — yield from types the sent-back value
278
+
279
+ if n < 0:
280
+ yield from E.fail(
281
+ OopsError(msg="negative")
282
+ ) # OopsError joins the error channel
283
+ return a + n
284
+
285
+
286
+ E.run_sync(total(22)) # 42
287
+ ```
288
+
289
+ A failing yielded effect abandons the generator, so `try/except` around a `yield` never observes effect failures — use `catch` or `catch_all` on the resulting effect instead.
290
+
291
+ More examples: [`test_gen.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_gen.py).
292
+
293
+ ### Wrapping third party code
294
+
295
+ `attempt` runs an exception-throwing thunk lazily and maps expected exceptions into the typed error channel. Re-raise unexpected exceptions from the mapper so they stay defects:
296
+
297
+ ```python
298
+ @final
299
+ @dataclass(frozen=True)
300
+ class InvalidJson(E.EffectonError):
301
+ text: str
302
+
303
+
304
+ def parse_json(text: str) -> E.Effect[Any, InvalidJson]:
305
+ def to_error(e: Exception) -> InvalidJson:
306
+ if isinstance(e, json.JSONDecodeError):
307
+ return InvalidJson(text)
308
+ raise e # anything else stays a defect
309
+
310
+ return E.attempt(lambda: json.loads(text), to_error)
311
+ ```
312
+
313
+ More examples: [`test_attempt.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_attempt.py).
314
+
315
+ ### Wrapping async code
316
+
317
+ `coroutine` defers an awaitable the way `sync` defers a thunk: the thunk builds a fresh awaitable on every run, because a coroutine object can be awaited only once. Exceptions become defects. `attempt_async` is the `attempt` counterpart that maps expected exceptions into the error channel. Only the `run_async` family can interpret either:
318
+
319
+ ```python
320
+ client = httpx.AsyncClient()
321
+
322
+ E.coroutine(lambda: client.get(url)) # Effect[httpx.Response] — nothing awaited yet
323
+
324
+
325
+ def get_text(url: str) -> E.Effect[str, HttpStatusError]:
326
+ async def go() -> str:
327
+ response = await client.get(url)
328
+ response.raise_for_status()
329
+ return response.text
330
+
331
+ def to_error(e: Exception) -> HttpStatusError:
332
+ if isinstance(e, httpx.HTTPStatusError):
333
+ return HttpStatusError(url=url, status_code=e.response.status_code)
334
+ raise e # anything else stays a defect
335
+
336
+ return E.attempt_async(go, to_error)
337
+
338
+
339
+ E.run_async(get_text("https://example.com")) # text, or raises HttpStatusError
340
+
341
+ E.run_async_exit(
342
+ get_text("https://example.com")
343
+ ) # Succeeded(text) or Failure(Fail(HttpStatusError(...)))
344
+ ```
345
+
346
+ Inside `@E.gen` bodies, `yield from E.coroutine(...)` works like any other effect; the generator itself stays synchronous. If the task running `run_async_coroutine` is cancelled, the effect unwinds with an `Interrupt` cause, which `catch_all` and `catch` skip like a `Die`: finalizers and scope releases run, and the run settles as `Failure(Interrupt(exception))`, so an `asyncio.timeout` around `run_async_coroutine` doesn't leak resources and completes normally with that `Exit`. The cancellation is consumed by `run_async_coroutine`; a caller whose task should stop re-raises the carried exception. A finalizer that is mid-await when the cancellation arrives is shielded and runs to completion, and a cancellation raised from a synchronous thunk or callback, such as a cancelled future's `result()`, unwinds the same way.
347
+
348
+ More examples: [`test_run_async.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/test_run_async.py).
349
+
350
+ ## Standard library
351
+
352
+ ### Logger
353
+
354
+ Effecton comes with pretty logger out of the box.
355
+
356
+ ```python
357
+ E.run_sync(E.log_info("user created", 42)) # pretty-printed to stderr, no setup needed
358
+
359
+ program = E.annotate_logs(
360
+ handle_request(), request_id="r-1"
361
+ ) # every log inside carries request_id=r-1
362
+
363
+ captured: list[E.LogData] = []
364
+ E.run_sync(
365
+ E.provide_implicit(
366
+ program, E.CurrentLoggers((E.EffectonLogger(log=captured.append),))
367
+ )
368
+ )
369
+ ```
370
+
371
+ More examples: [`test_logger.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/std/test_logger.py), [`test_pretty_logger.py`](https://github.com/krzkaczor/effecton/blob/main/packages/effecton/src/effecton/std/test_pretty_logger.py).
372
+
373
+ ## Roadmap
374
+
375
+ - [x] `ty` support
376
+ - [x] Support for async/sync code
377
+ - [ ] Retries
378
+ - [ ] Timeouts
379
+ - [ ] `Random` implicit service
380
+ - [ ] More examples of integrations with existing ecosystem (fastapi, pydantic etc.)
381
+
382
+ ## Inspirations
383
+
384
+ * Effect-TS/ZIO
385
+ * stateless
386
+
387
+ ## Contributing
388
+
389
+ See [CONTRIBUTING.md](https://github.com/krzkaczor/effecton/blob/main/CONTRIBUTING.md) for repo setup and development commands.