fates 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.
fates-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,112 @@
1
+ Metadata-Version: 2.3
2
+ Name: fates
3
+ Version: 0.1.0
4
+ Summary: A generic Result[T, E] type for explicit error propagation.
5
+ Author: Evgeny Goryachev
6
+ Author-email: Evgeny Goryachev <saladware46@gmail.com>
7
+ Requires-Dist: typing-extensions>=4.0.0 ; python_full_version < '3.12'
8
+ Requires-Python: >=3.7
9
+ Description-Content-Type: text/markdown
10
+
11
+ # fates 🔮
12
+
13
+ A robust, fully-typed, and async-ready Result pattern implementation for Python.
14
+
15
+ `fates` brings expressive, functional error handling to Python, completely removing the need for defensive `try/except` blocks. It helps you build predictable pipelines with absolute type safety for both synchronous and asynchronous operations.
16
+
17
+ ---
18
+
19
+ ## ✨ Features
20
+
21
+ - **🛡️ 100% Type Safe:** Full `mypy` / `pyright` compliance using generic protocols and `TypeVar` covariance.
22
+ - **⚡ Async Native:** First-class support for asynchronous mapping and monadic binding.
23
+ - **🧩 Zero Dependencies:** Lightweight and clean, relying only on standard library primitives (and `typing_extensions` for older Python versions).
24
+
25
+ ---
26
+
27
+ ## 🚀 Installation
28
+
29
+ ```bash
30
+ pip install fates
31
+ ```
32
+
33
+ ---
34
+
35
+ ## 📖 Quick Start
36
+
37
+ ### Synchronous Usage
38
+
39
+ ```python
40
+ from fates import Result, Ok, Err
41
+
42
+ def divide(a: int, b: int) -> Result[float, str]:
43
+ if b == 0:
44
+ return Err("Cannot divide by zero")
45
+ return Ok(a / b)
46
+
47
+ # Monadic binding and mapping
48
+ result = (
49
+ divide(10, 2)
50
+ .map(lambda x: x * 2)
51
+ .unwrap_or(0.0)
52
+ )
53
+ print(result) # Output: 10.0
54
+ ```
55
+
56
+ ### Asynchronous Pipelines
57
+
58
+ `fates` shines when working with async databases or HTTP clients. Use `amap` and `abind` to pipe async operations seamlessly.
59
+
60
+ ```python
61
+ import asyncio
62
+ from fates import Ok, Err, Result
63
+
64
+ async def fetch_user_id(username: str) -> Result[int, str]:
65
+ # Simulating async DB call
66
+ await asyncio.sleep(0.1)
67
+ return Ok(42) if username == "admin" else Err("User not found")
68
+
69
+ async def get_user_role_async(user_id: int) -> Result[str, str]:
70
+ await asyncio.sleep(0.1)
71
+ return Ok("superuser")
72
+
73
+ async def main():
74
+ # Chain async functions using abind
75
+ pipeline = await fetch_user_id("admin").abind(get_user_role_async)
76
+
77
+ print(pipeline) # Output: Ok('superuser')
78
+ print(pipeline.unwrap()) # Output: superuser
79
+
80
+ asyncio.run(main())
81
+ ```
82
+
83
+ ---
84
+
85
+ ## 🛠️ API Reference
86
+
87
+ ### Extracting Values
88
+
89
+ - `.unwrap()` — Returns the success value or raises an `UnwrapError`.
90
+ - `.unwrap_or(default)` — Returns the success value or a fallback value.
91
+ - `.unwrap_err()` — Returns the error value or raises an `UnwrapError`.
92
+ - `.expect(note)` — Returns the success value or crashes with a custom message.
93
+
94
+ ### Transforming Results
95
+
96
+ - `.map(mapper)` — Transforms the success value inside `Ok`.
97
+ - `.map_err(mapper)` — Transforms the error value inside `Err`.
98
+ - `.bind(binder)` — Monadic bind. Chains another operation that returns a `Result`.
99
+ - `.catch(binder)` — Recovers from an error by returning an alternative `Result`.
100
+ - `.resolve(mapper)` — Merges both paths into a single success-type value.
101
+
102
+ ### Async Operations
103
+
104
+ - `.amap(async_mapper)` — Asynchronously transforms the success value.
105
+ - `.amap_err(async_mapper)` — Asynchronously transforms the error value.
106
+ - `.abind(async_binder)` — Asynchronously chains another operation returning a `Result`.
107
+
108
+ ---
109
+
110
+ ## ⚖️ License
111
+
112
+ This project is licensed under the [MIT License](LICENSE).
fates-0.1.0/README.md ADDED
@@ -0,0 +1,102 @@
1
+ # fates 🔮
2
+
3
+ A robust, fully-typed, and async-ready Result pattern implementation for Python.
4
+
5
+ `fates` brings expressive, functional error handling to Python, completely removing the need for defensive `try/except` blocks. It helps you build predictable pipelines with absolute type safety for both synchronous and asynchronous operations.
6
+
7
+ ---
8
+
9
+ ## ✨ Features
10
+
11
+ - **🛡️ 100% Type Safe:** Full `mypy` / `pyright` compliance using generic protocols and `TypeVar` covariance.
12
+ - **⚡ Async Native:** First-class support for asynchronous mapping and monadic binding.
13
+ - **🧩 Zero Dependencies:** Lightweight and clean, relying only on standard library primitives (and `typing_extensions` for older Python versions).
14
+
15
+ ---
16
+
17
+ ## 🚀 Installation
18
+
19
+ ```bash
20
+ pip install fates
21
+ ```
22
+
23
+ ---
24
+
25
+ ## 📖 Quick Start
26
+
27
+ ### Synchronous Usage
28
+
29
+ ```python
30
+ from fates import Result, Ok, Err
31
+
32
+ def divide(a: int, b: int) -> Result[float, str]:
33
+ if b == 0:
34
+ return Err("Cannot divide by zero")
35
+ return Ok(a / b)
36
+
37
+ # Monadic binding and mapping
38
+ result = (
39
+ divide(10, 2)
40
+ .map(lambda x: x * 2)
41
+ .unwrap_or(0.0)
42
+ )
43
+ print(result) # Output: 10.0
44
+ ```
45
+
46
+ ### Asynchronous Pipelines
47
+
48
+ `fates` shines when working with async databases or HTTP clients. Use `amap` and `abind` to pipe async operations seamlessly.
49
+
50
+ ```python
51
+ import asyncio
52
+ from fates import Ok, Err, Result
53
+
54
+ async def fetch_user_id(username: str) -> Result[int, str]:
55
+ # Simulating async DB call
56
+ await asyncio.sleep(0.1)
57
+ return Ok(42) if username == "admin" else Err("User not found")
58
+
59
+ async def get_user_role_async(user_id: int) -> Result[str, str]:
60
+ await asyncio.sleep(0.1)
61
+ return Ok("superuser")
62
+
63
+ async def main():
64
+ # Chain async functions using abind
65
+ pipeline = await fetch_user_id("admin").abind(get_user_role_async)
66
+
67
+ print(pipeline) # Output: Ok('superuser')
68
+ print(pipeline.unwrap()) # Output: superuser
69
+
70
+ asyncio.run(main())
71
+ ```
72
+
73
+ ---
74
+
75
+ ## 🛠️ API Reference
76
+
77
+ ### Extracting Values
78
+
79
+ - `.unwrap()` — Returns the success value or raises an `UnwrapError`.
80
+ - `.unwrap_or(default)` — Returns the success value or a fallback value.
81
+ - `.unwrap_err()` — Returns the error value or raises an `UnwrapError`.
82
+ - `.expect(note)` — Returns the success value or crashes with a custom message.
83
+
84
+ ### Transforming Results
85
+
86
+ - `.map(mapper)` — Transforms the success value inside `Ok`.
87
+ - `.map_err(mapper)` — Transforms the error value inside `Err`.
88
+ - `.bind(binder)` — Monadic bind. Chains another operation that returns a `Result`.
89
+ - `.catch(binder)` — Recovers from an error by returning an alternative `Result`.
90
+ - `.resolve(mapper)` — Merges both paths into a single success-type value.
91
+
92
+ ### Async Operations
93
+
94
+ - `.amap(async_mapper)` — Asynchronously transforms the success value.
95
+ - `.amap_err(async_mapper)` — Asynchronously transforms the error value.
96
+ - `.abind(async_binder)` — Asynchronously chains another operation returning a `Result`.
97
+
98
+ ---
99
+
100
+ ## ⚖️ License
101
+
102
+ This project is licensed under the [MIT License](LICENSE).
@@ -0,0 +1,83 @@
1
+ [project]
2
+ name = "fates"
3
+ version = "0.1.0"
4
+ description = "A generic Result[T, E] type for explicit error propagation."
5
+ readme = "README.md"
6
+ authors = [{ name = "Evgeny Goryachev", email = "saladware46@gmail.com" }]
7
+ requires-python = ">=3.7"
8
+ dependencies = ["typing_extensions>=4.0.0; python_version < '3.12'"]
9
+
10
+ [build-system]
11
+ requires = ["uv_build>=0.11.6,<0.12.0"]
12
+ build-backend = "uv_build"
13
+
14
+ [tool.ruff.lint]
15
+ select = ["ALL"]
16
+ ignore = ["COM812"]
17
+ external = ["WPS"]
18
+
19
+ [tool.ruff.lint.per-file-ignores]
20
+ "**/test_*.py" = ["S101", "D", "PLR2004"]
21
+
22
+ [tool.ty.rules]
23
+ all = "error"
24
+
25
+ [tool.mypy]
26
+ strict = true
27
+ packages = ["fates", "tests"]
28
+
29
+ [tool.pytest.ini_options]
30
+
31
+ asyncio_mode = "auto"
32
+ addopts = [
33
+ "--cov=fates",
34
+ "--cov-report=html",
35
+ "--cov-report=term",
36
+ "--xdoc",
37
+ "--xdoc-global-exec=from fates import Ok, Err, Result",
38
+ ]
39
+
40
+ [tool.coverage.report]
41
+ exclude_also = ["\\A(?s:.*# pragma: exclude file.*)\\Z"]
42
+
43
+ [tool.ruff.lint.pydocstyle]
44
+ convention = "google"
45
+
46
+ [tool.flake8]
47
+ select = ["WPS"]
48
+ ignore = ["WPS214"]
49
+ allowed-domain-names = ["result"]
50
+
51
+ [tool.pyrefly]
52
+ project-includes = ["src/fates"]
53
+
54
+ [tool.taskipy.tasks]
55
+ check = "mypy && ty check && flake8 src && pytest"
56
+ cov = "pytest && python -c 'import webbrowser; webbrowser.open(\"htmlcov/index.html\")'"
57
+
58
+ [dependency-groups]
59
+ dev = [
60
+ "flake8-pyproject>=1.2.4",
61
+ "mypy>=2.1.0",
62
+ "pyrefly>=0.14.0",
63
+ "pytest-asyncio>=1.4.0",
64
+ "pytest-cov>=4.1.0",
65
+ "pytest-cov>=7.1.0",
66
+ "ruff>=0.15.15",
67
+ "taskipy>=1.14.1",
68
+ "ty>=0.0.40",
69
+ "wemake-python-styleguide>=1.6.2",
70
+ "xdoctest>=1.1.6",
71
+ ]
72
+ docs = [
73
+ "furo>=2025.12.19",
74
+ "myst-parser>=5.1.0",
75
+ "sphinx>=9.1.0",
76
+ "sphinx-autobuild>=2025.8.25",
77
+ "sphinx-autodoc-typehints>=3.10.5",
78
+ "sphinx-autodoc2>=0.5.0",
79
+ ]
80
+
81
+ [tool.uv.dependency-groups]
82
+ docs = { requires-python = ">=3.12" }
83
+ dev = { requires-python = ">=3.12" }
@@ -0,0 +1,6 @@
1
+ """fates — A generic Result[T, E] type for explicit error propagation."""
2
+
3
+ from fates._err import Err as Err
4
+ from fates._ok import Ok as Ok
5
+ from fates._result import Result as Result
6
+ from fates.exceptions import UnwrapError as UnwrapError
@@ -0,0 +1,127 @@
1
+ from __future__ import annotations
2
+
3
+ from asyncio import ensure_future
4
+ from typing import TYPE_CHECKING, Generic, cast
5
+
6
+ from fates._typevars import DefaultT, E_co, NewE, NewT, T_co
7
+
8
+ if TYPE_CHECKING:
9
+ from asyncio import Task
10
+ from collections.abc import Awaitable, Generator
11
+
12
+ from fates._result import Result
13
+ from fates._types import AsyncBinder, AsyncMapper, Binder, Mapper
14
+
15
+
16
+ class AsyncResult(Generic[T_co, E_co]):
17
+ def __init__(self, awaitable: Awaitable[Result[T_co, E_co]]) -> None:
18
+ self._awaitable = awaitable
19
+ self._cached_task: Task[Result[T_co, E_co]] | None = None
20
+
21
+ def __await__(self) -> Generator[object, None, Result[T_co, E_co]]:
22
+ if self._cached_task is None:
23
+ self._cached_task = ensure_future(self._awaitable)
24
+ return self._cached_task.__await__()
25
+
26
+ async def unwrap(self) -> T_co:
27
+ res = await self
28
+ return res.unwrap()
29
+
30
+ async def unwrap_or(self, default: DefaultT) -> T_co | DefaultT:
31
+ res = await self
32
+ return res.unwrap_or(default)
33
+
34
+ async def unwrap_err(self) -> E_co:
35
+ res = await self
36
+ return res.unwrap_err()
37
+
38
+ async def expect(self, note: str) -> T_co:
39
+ res = await self
40
+ return res.expect(note)
41
+
42
+ def map(self, mapper: Mapper[T_co, NewT]) -> AsyncResult[NewT, E_co]:
43
+ return AsyncResult(self._map(mapper))
44
+
45
+ def map_err(self, mapper: Mapper[E_co, NewE]) -> AsyncResult[T_co, NewE]:
46
+ return AsyncResult(self._map_err(mapper))
47
+
48
+ def bind(
49
+ self,
50
+ binder: Binder[T_co, NewT, NewE],
51
+ ) -> AsyncResult[NewT, E_co | NewE]:
52
+ return AsyncResult(self._bind(binder))
53
+
54
+ def catch(
55
+ self, binder: Binder[E_co, NewT, NewE]
56
+ ) -> AsyncResult[NewT, NewE] | AsyncResult[T_co, E_co]:
57
+ return cast(
58
+ "AsyncResult[T_co, E_co] | AsyncResult[NewT, NewE]",
59
+ AsyncResult(self._catch(binder)),
60
+ )
61
+
62
+ async def resolve(self, mapper: Mapper[E_co, NewT]) -> T_co | NewT:
63
+ res = await self
64
+ return res.resolve(mapper)
65
+
66
+ def amap(self, mapper: AsyncMapper[T_co, NewT]) -> AsyncResult[NewT, E_co]:
67
+ return AsyncResult(self._amap(mapper))
68
+
69
+ def amap_err(self, mapper: AsyncMapper[E_co, NewE]) -> AsyncResult[T_co, NewE]:
70
+ return AsyncResult(self._amap_err(mapper))
71
+
72
+ def abind(
73
+ self, binder: AsyncBinder[T_co, NewT, NewE]
74
+ ) -> AsyncResult[NewT, E_co | NewE]:
75
+ return AsyncResult(self._abind(binder))
76
+
77
+ def acatch(
78
+ self, binder: AsyncBinder[E_co, NewT, NewE]
79
+ ) -> AsyncResult[T_co, E_co] | AsyncResult[NewT, NewE]:
80
+ return cast(
81
+ "AsyncResult[T_co, E_co] | AsyncResult[NewT, NewE]",
82
+ AsyncResult(self._acatch(binder)),
83
+ )
84
+
85
+ async def aresolve(self, mapper: AsyncMapper[E_co, NewT]) -> T_co | NewT:
86
+ res = await self
87
+ return await res.aresolve(mapper)
88
+
89
+ async def _catch(
90
+ self, binder: Binder[E_co, NewT, NewE]
91
+ ) -> Result[T_co, E_co] | Result[NewT, NewE]:
92
+ res = await self
93
+ return res.catch(binder)
94
+
95
+ async def _acatch(
96
+ self, binder: AsyncBinder[E_co, NewT, NewE]
97
+ ) -> Result[T_co, E_co] | Result[NewT, NewE]:
98
+ res = await self
99
+ return await res.acatch(binder) # ty: ignore[invalid-return-type]
100
+
101
+ async def _map(self, mapper: Mapper[T_co, NewT]) -> Result[NewT, E_co]:
102
+ res = await self
103
+ return res.map(mapper)
104
+
105
+ async def _map_err(self, mapper: Mapper[E_co, NewE]) -> Result[T_co, NewE]:
106
+ res = await self
107
+ return res.map_err(mapper)
108
+
109
+ async def _bind(
110
+ self, binder: Binder[T_co, NewT, NewE]
111
+ ) -> Result[NewT, E_co | NewE]:
112
+ res = await self
113
+ return res.bind(binder)
114
+
115
+ async def _amap(self, mapper: AsyncMapper[T_co, NewT]) -> Result[NewT, E_co]:
116
+ res = await self
117
+ return await res.amap(mapper)
118
+
119
+ async def _amap_err(self, mapper: AsyncMapper[E_co, NewE]) -> Result[T_co, NewE]:
120
+ res = await self
121
+ return await res.amap_err(mapper)
122
+
123
+ async def _abind(
124
+ self, binder: AsyncBinder[T_co, NewT, NewE]
125
+ ) -> Result[NewT, E_co | NewE]:
126
+ res = await self
127
+ return await res.abind(binder)
@@ -0,0 +1,183 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import TYPE_CHECKING, Awaitable, cast, final
5
+
6
+ if sys.version_info >= (3, 12):
7
+ from typing import Never, Self, override # pragma: no cover
8
+ else:
9
+ from typing_extensions import Never, Self, override # pragma: no cover
10
+
11
+
12
+ from fates._async import AsyncResult
13
+ from fates._result import Result
14
+ from fates._typevars import DefaultT, E_co, NewE, NewT
15
+ from fates.exceptions import UnwrapError
16
+
17
+ if TYPE_CHECKING:
18
+ from collections.abc import Generator
19
+
20
+ from fates._types import AsyncBinder, AsyncMapper, Binder, Mapper
21
+
22
+
23
+ @final
24
+ class Err(Result[Never, E_co]):
25
+ __slots__ = ("_boxed_err",)
26
+ __match_args__ = ("_boxed_err",)
27
+
28
+ def __init__(self, boxed_err: E_co) -> None:
29
+ self._boxed_err = boxed_err
30
+
31
+ def __repr__(self) -> str:
32
+ return f"Err({self._boxed_err!r})"
33
+
34
+ def __hash__(self) -> int:
35
+ return self._boxed_err.__hash__() # pragma: no cover
36
+
37
+ def __eq__(self, other: object) -> bool:
38
+ if isinstance(other, Err):
39
+ other = cast("Err[object]", other) # ty: ignore[redundant-cast]
40
+ return self._boxed_err == other._boxed_err
41
+ return False
42
+
43
+ @override
44
+ def unwrap(self) -> Never:
45
+ err = UnwrapError(repr(self._boxed_err))
46
+ if isinstance(self._boxed_err, BaseException):
47
+ raise err from self._boxed_err
48
+ raise err
49
+
50
+ @override
51
+ def unwrap_or(self, default: DefaultT) -> DefaultT:
52
+ return default
53
+
54
+ @override
55
+ def unwrap_err(self) -> E_co:
56
+ return self._boxed_err
57
+
58
+ @override
59
+ def expect(self, note: str) -> Never:
60
+ err_msg = f"{note}\nError details: {self._boxed_err!r}"
61
+ err = UnwrapError(err_msg)
62
+ if isinstance(self._boxed_err, BaseException):
63
+ raise err from self._boxed_err
64
+ raise err
65
+
66
+ @override
67
+ def map(self, mapper: Mapper[Never, object]) -> Self:
68
+ return self
69
+
70
+ @override
71
+ def map_err(self, mapper: Mapper[E_co, NewE]) -> Err[NewE]:
72
+ return Err(mapper(self._boxed_err))
73
+
74
+ @override
75
+ def bind(
76
+ self,
77
+ binder: Binder[Never, object, object],
78
+ ) -> Self:
79
+ return self
80
+
81
+ @override
82
+ def catch(self, binder: Binder[E_co, NewT, NewE]) -> Result[NewT, NewE]:
83
+ return binder(self._boxed_err)
84
+
85
+ @override
86
+ def resolve(self, mapper: Mapper[E_co, NewT]) -> NewT:
87
+ return mapper(self._boxed_err)
88
+
89
+ @override
90
+ def amap(self, mapper: AsyncMapper[Never, object]) -> AsyncErr[E_co]:
91
+ return AsyncErr(self._return_self())
92
+
93
+ @override
94
+ def amap_err(self, mapper: AsyncMapper[E_co, NewE]) -> AsyncErr[NewE]:
95
+ return AsyncErr(self._async_map_err(mapper))
96
+
97
+ @override
98
+ def abind(self, binder: AsyncBinder[Never, object, object]) -> AsyncErr[E_co]:
99
+ return AsyncErr(self._return_self())
100
+
101
+ @override
102
+ def acatch(self, binder: AsyncBinder[E_co, NewT, NewE]) -> AsyncResult[NewT, NewE]:
103
+ return AsyncResult(self._acatch(binder))
104
+
105
+ @override
106
+ async def aresolve(self, mapper: AsyncMapper[E_co, NewT]) -> NewT:
107
+ return await mapper(self._boxed_err)
108
+
109
+ async def _return_self(self) -> Self:
110
+ return self
111
+
112
+ async def _acatch(
113
+ self, binder: AsyncBinder[E_co, NewT, NewE]
114
+ ) -> Result[NewT, NewE]:
115
+ return await binder(self._boxed_err)
116
+
117
+ async def _async_map_err(self, mapper: AsyncMapper[E_co, NewE]) -> Err[NewE]:
118
+ return Err(await mapper(self._boxed_err))
119
+
120
+
121
+ @final
122
+ class AsyncErr(AsyncResult[Never, E_co]):
123
+ @override
124
+ def __init__(self, awaitable: Awaitable[Err[E_co]]) -> None:
125
+ super().__init__(awaitable)
126
+
127
+ @override
128
+ def __await__(self) -> Generator[object, None, Err[E_co]]:
129
+ return cast("Generator[object, None, Err[E_co]]", super().__await__())
130
+
131
+ @override
132
+ def map(self, mapper: Mapper[Never, object]) -> Self:
133
+ return self
134
+
135
+ @override
136
+ def amap(self, mapper: AsyncMapper[Never, object]) -> Self:
137
+ return self
138
+
139
+ @override
140
+ def bind(self, binder: Binder[Never, object, object]) -> Self:
141
+ return self
142
+
143
+ @override
144
+ def abind(self, binder: AsyncBinder[Never, object, object]) -> Self:
145
+ return self
146
+
147
+ @override
148
+ def map_err(self, mapper: Mapper[E_co, NewE]) -> AsyncErr[NewE]:
149
+ return AsyncErr(self._map_err(mapper))
150
+
151
+ @override
152
+ def amap_err(self, mapper: AsyncMapper[E_co, NewE]) -> AsyncErr[NewE]:
153
+ return AsyncErr(self._amap_err(mapper))
154
+
155
+ @override
156
+ def catch(self, binder: Binder[E_co, NewT, NewE]) -> AsyncResult[NewT, NewE]:
157
+ return AsyncResult(self._catch(binder))
158
+
159
+ @override
160
+ def acatch(self, binder: AsyncBinder[E_co, NewT, NewE]) -> AsyncResult[NewT, NewE]:
161
+ return AsyncResult(self._acatch(binder))
162
+
163
+ @override
164
+ async def _map_err(self, mapper: Mapper[E_co, NewE]) -> Err[NewE]:
165
+ res = await self
166
+ return res.map_err(mapper)
167
+
168
+ @override
169
+ async def _amap_err(self, mapper: AsyncMapper[E_co, NewE]) -> Err[NewE]:
170
+ res = await self
171
+ return await res.amap_err(mapper)
172
+
173
+ @override
174
+ async def _catch(self, binder: Binder[E_co, NewT, NewE]) -> Result[NewT, NewE]:
175
+ res = await self
176
+ return res.catch(binder)
177
+
178
+ @override
179
+ async def _acatch(
180
+ self, binder: AsyncBinder[E_co, NewT, NewE]
181
+ ) -> Result[NewT, NewE]:
182
+ res = await self
183
+ return await res.acatch(binder)
@@ -0,0 +1,172 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import TYPE_CHECKING, Awaitable, Generator, cast, final
5
+
6
+ if sys.version_info >= (3, 12):
7
+ from typing import Never, Self, override # pragma: no cover
8
+ else:
9
+ from typing_extensions import Never, Self, override # pragma: no cover
10
+
11
+
12
+ from fates._async import AsyncResult
13
+ from fates._result import Result
14
+ from fates._typevars import NewE, NewT, T_co
15
+ from fates.exceptions import UnwrapError
16
+
17
+ if TYPE_CHECKING:
18
+ from fates._types import AsyncBinder, AsyncMapper, Binder, Mapper
19
+
20
+
21
+ @final
22
+ class Ok(Result[T_co, Never]):
23
+ __slots__ = ("_boxed_val",)
24
+ __match_args__ = ("_boxed_val",)
25
+
26
+ def __init__(self, boxed_val: T_co) -> None:
27
+ self._boxed_val = boxed_val
28
+
29
+ def __repr__(self) -> str:
30
+ return f"Ok({self._boxed_val!r})"
31
+
32
+ def __eq__(self, other: object) -> bool:
33
+ if isinstance(other, Ok):
34
+ other = cast("Ok[object]", other) # ty: ignore[redundant-cast]
35
+ return self._boxed_val == other._boxed_val
36
+ return False
37
+
38
+ def __hash__(self) -> int:
39
+ return self._boxed_val.__hash__() # pragma: no cover
40
+
41
+ @override
42
+ def unwrap(self) -> T_co:
43
+ return self._boxed_val
44
+
45
+ @override
46
+ def unwrap_or(self, default: object) -> T_co:
47
+ return self._boxed_val
48
+
49
+ @override
50
+ def unwrap_err(self) -> Never:
51
+ msg = f"unwrap_err() called on {self}"
52
+ raise UnwrapError(msg)
53
+
54
+ @override
55
+ def expect(self, note: str) -> T_co:
56
+ return self._boxed_val
57
+
58
+ @override
59
+ def map(self, mapper: Mapper[T_co, NewT]) -> Ok[NewT]:
60
+ return Ok(mapper(self._boxed_val))
61
+
62
+ @override
63
+ def map_err(self, mapper: Mapper[Never, object]) -> Self:
64
+ return self
65
+
66
+ @override
67
+ def bind(self, binder: Binder[T_co, NewT, NewE]) -> Result[NewT, NewE]:
68
+ return binder(self._boxed_val)
69
+
70
+ @override
71
+ def catch(self, binder: Binder[Never, object, object]) -> Self:
72
+ return self
73
+
74
+ @override
75
+ def resolve(self, mapper: Mapper[Never, object]) -> T_co:
76
+ return self._boxed_val
77
+
78
+ @override
79
+ def amap(self, mapper: AsyncMapper[T_co, NewT]) -> AsyncOk[NewT]:
80
+ return AsyncOk(self._async_map(mapper))
81
+
82
+ @override
83
+ def amap_err(self, mapper: AsyncMapper[Never, object]) -> AsyncOk[T_co]:
84
+ return AsyncOk(self._return_self())
85
+
86
+ @override
87
+ def abind(self, binder: AsyncBinder[T_co, NewT, NewE]) -> AsyncResult[NewT, NewE]:
88
+ return AsyncResult(self._async_bind(binder))
89
+
90
+ @override
91
+ def acatch(self, binder: AsyncBinder[Never, object, object]) -> AsyncOk[T_co]:
92
+ return AsyncOk(self._return_self())
93
+
94
+ @override
95
+ async def aresolve(self, mapper: AsyncMapper[Never, object]) -> T_co:
96
+ return self._boxed_val
97
+
98
+ async def _async_map(self, mapper: AsyncMapper[T_co, NewT]) -> Ok[NewT]:
99
+ return Ok(await mapper(self._boxed_val))
100
+
101
+ async def _async_bind(
102
+ self, binder: AsyncBinder[T_co, NewT, NewE]
103
+ ) -> Result[NewT, NewE]:
104
+ return await binder(self._boxed_val)
105
+
106
+ async def _return_self(self) -> Self:
107
+ return self
108
+
109
+
110
+ @final
111
+ class AsyncOk(AsyncResult[T_co, Never]):
112
+ @override
113
+ def __init__(self, awaitable: Awaitable[Ok[T_co]]) -> None:
114
+ super().__init__(awaitable)
115
+
116
+ @override
117
+ def __await__(self) -> Generator[object, None, Ok[T_co]]:
118
+ return cast("Generator[object, None, Ok[T_co]]", super().__await__())
119
+
120
+ @override
121
+ async def unwrap_or(self, default: object) -> T_co:
122
+ res = await self
123
+ return res.unwrap()
124
+
125
+ @override
126
+ async def expect(self, note: str) -> T_co:
127
+ res = await self
128
+ return res.unwrap()
129
+
130
+ @override
131
+ async def resolve(self, mapper: Mapper[Never, object]) -> T_co:
132
+ res = await self
133
+ return res.unwrap()
134
+
135
+ @override
136
+ async def aresolve(self, mapper: AsyncMapper[Never, object]) -> T_co:
137
+ res = await self
138
+ return res.unwrap()
139
+
140
+ @override
141
+ def map(self, mapper: Mapper[T_co, NewT]) -> AsyncOk[NewT]:
142
+ return AsyncOk(self._map(mapper))
143
+
144
+ @override
145
+ def amap(self, mapper: AsyncMapper[T_co, NewT]) -> AsyncOk[NewT]:
146
+ return AsyncOk(self._amap(mapper))
147
+
148
+ @override
149
+ def map_err(self, mapper: Mapper[Never, object]) -> Self:
150
+ return self
151
+
152
+ @override
153
+ def catch(self, binder: Binder[Never, object, object]) -> Self:
154
+ return self
155
+
156
+ @override
157
+ def acatch(self, binder: AsyncBinder[Never, object, object]) -> Self:
158
+ return self
159
+
160
+ @override
161
+ def amap_err(self, mapper: AsyncMapper[Never, object]) -> Self:
162
+ return self
163
+
164
+ @override
165
+ async def _map(self, mapper: Mapper[T_co, NewT]) -> Ok[NewT]:
166
+ res = await self
167
+ return res.map(mapper)
168
+
169
+ @override
170
+ async def _amap(self, mapper: AsyncMapper[T_co, NewT]) -> Ok[NewT]:
171
+ res = await self
172
+ return await res.amap(mapper)
@@ -0,0 +1,277 @@
1
+ from __future__ import annotations
2
+
3
+ import sys
4
+ from typing import TYPE_CHECKING, Generic, Protocol
5
+
6
+ from fates._typevars import DefaultT, E_co, NewE, NewT, T_co
7
+
8
+ if sys.version_info >= (3, 11):
9
+ from typing import Self # pragma: no cover
10
+ else:
11
+ from typing_extensions import Self # pragma: no cover
12
+
13
+ if TYPE_CHECKING:
14
+ from fates._async import AsyncResult
15
+ from fates._types import AsyncBinder, AsyncMapper, Binder, Mapper
16
+
17
+
18
+ class Result(Protocol, Generic[T_co, E_co]):
19
+ def unwrap(self) -> T_co:
20
+ """Extracts the success value or raises an exception if it is a failure.
21
+
22
+ Returns:
23
+ T_co: The contained success value.
24
+
25
+ Raises:
26
+ UnwrapError: If the result is an Err.
27
+
28
+ Examples:
29
+ >>> Ok(42).unwrap()
30
+ 42
31
+ >>> Err("error").unwrap()
32
+ Traceback (most recent call last):
33
+ fates.exceptions.UnwrapError: 'error'
34
+ """
35
+ ...
36
+
37
+ def unwrap_or(self, default: DefaultT) -> T_co | DefaultT:
38
+ """Returns the success value or a default value if it is a failure.
39
+
40
+ Args:
41
+ default (DefaultT): The fallback value to return.
42
+
43
+ Returns:
44
+ T_co | DefaultT: The contained value or the default value.
45
+
46
+ Examples:
47
+ >>> Ok(42).unwrap_or(0)
48
+ 42
49
+ >>> Err("error").unwrap_or(0)
50
+ 0
51
+ """
52
+ ...
53
+
54
+ def unwrap_err(self) -> E_co:
55
+ """Extracts the error value or raises an exception if it is a success.
56
+
57
+ Returns:
58
+ E_co: The contained error value.
59
+
60
+ Raises:
61
+ UnwrapError: If the result is an Ok.
62
+
63
+ Examples:
64
+ >>> Err("danger").unwrap_err()
65
+ 'danger'
66
+ >>> Ok(42).unwrap_err()
67
+ Traceback (most recent call last):
68
+ fates.exceptions.UnwrapError: unwrap_err() called on Ok(42)
69
+ """
70
+ ...
71
+
72
+ def expect(self, note: str) -> T_co:
73
+ """Extracts the value or raises an exception with a custom message.
74
+
75
+ Args:
76
+ note (str): The custom error message to include in the exception.
77
+
78
+ Returns:
79
+ T_co: The contained success value.
80
+
81
+ Raises:
82
+ UnwrapError: If the result is an Err, containing the note.
83
+
84
+ Examples:
85
+ >>> Ok(42).expect("Should be a number")
86
+ 42
87
+ >>> Err("db_error").expect("Database connection failed")
88
+ Traceback (most recent call last):
89
+ fates.exceptions.UnwrapError: Database connection failed
90
+ Error details: 'db_error'
91
+ """
92
+ ...
93
+
94
+ def map(
95
+ self,
96
+ mapper: Mapper[T_co, NewT],
97
+ ) -> Result[NewT, E_co]:
98
+ """Applies a function to the success value, leaving an error untouched.
99
+
100
+ Args:
101
+ mapper (Mapper[T_co, NewT]): A function to transform the success value.
102
+
103
+ Returns:
104
+ Result[NewT, E_co]: A new Result with
105
+ the transformed value or original error.
106
+
107
+ Examples:
108
+ >>> Ok(2).map(lambda x: x * 2)
109
+ Ok(4)
110
+ >>> Err("error").map(lambda x: x * 2)
111
+ Err('error')
112
+ """
113
+ ...
114
+
115
+ def map_err(
116
+ self,
117
+ mapper: Mapper[E_co, NewE],
118
+ ) -> Result[T_co, NewE]:
119
+ """Applies a function to the error value, leaving a success untouched.
120
+
121
+ Args:
122
+ mapper (Mapper[E_co, NewE]): A function to transform the error value.
123
+
124
+ Returns:
125
+ Result[T_co, NewE]: A new Result with the
126
+ transformed error or original value.
127
+
128
+ Examples:
129
+ >>> Err("failed").map_err(lambda e: f"Log: {e}")
130
+ Err('Log: failed')
131
+ >>> Ok(42).map_err(lambda e: f"Log: {e}")
132
+ Ok(42)
133
+ """
134
+ ...
135
+
136
+ def bind(
137
+ self,
138
+ binder: Binder[T_co, NewT, NewE],
139
+ ) -> Result[NewT, E_co | NewE]:
140
+ """Monadic bind. Transforms the success value into a new Result.
141
+
142
+ Args:
143
+ binder (Binder[T_co, NewT, NewE]): A function that takes the success
144
+ value and returns a new Result.
145
+
146
+ Returns:
147
+ Result[NewT, E_co | NewE]: The new Result, or the original Err.
148
+
149
+ Examples:
150
+ >>> def check_positive(x: int) -> Result[int, str]:
151
+ ... return Ok(x) if x > 0 else Err("negative")
152
+ >>> Ok(5).bind(check_positive)
153
+ Ok(5)
154
+ >>> Ok(-1).bind(check_positive)
155
+ Err('negative')
156
+ >>> Err("not_a_number").bind(check_positive)
157
+ Err('not_a_number')
158
+ """
159
+ ...
160
+
161
+ def catch(
162
+ self,
163
+ binder: Binder[E_co, NewT, NewE],
164
+ ) -> Self | Result[NewT, NewE]:
165
+ """Handles an error by transforming it into a new Result.
166
+
167
+ Args:
168
+ binder (Binder[E_co, NewT, NewE]): A function that takes the error
169
+ value and returns a alternative Result.
170
+
171
+ Returns:
172
+ Self | Result[NewT, NewE]: The original Ok,
173
+ or the new Result from the binder.
174
+
175
+ Examples:
176
+ >>> def recover(e: str) -> Result[int, str]:
177
+ ... return Ok(0) if e == "recoverable" else Err("fatal")
178
+ >>> Ok(42).catch(recover)
179
+ Ok(42)
180
+ >>> Err("recoverable").catch(recover)
181
+ Ok(0)
182
+ >>> Err("boom").catch(recover)
183
+ Err('fatal')
184
+ """
185
+ ...
186
+
187
+ def resolve(self, mapper: Mapper[E_co, NewT]) -> T_co | NewT:
188
+ """Returns the success value or transforms the error into a success type.
189
+
190
+ Args:
191
+ mapper (Mapper[E_co, NewT]): A function to transform the error value.
192
+
193
+ Returns:
194
+ T_co | NewT: The original success value or the transformed error value.
195
+
196
+ Examples:
197
+ >>> Ok(100).resolve(lambda e: 0)
198
+ 100
199
+ >>> Err("missing").resolve(lambda e: 0)
200
+ 0
201
+ """
202
+ ...
203
+
204
+ def amap(
205
+ self,
206
+ mapper: AsyncMapper[T_co, NewT],
207
+ ) -> AsyncResult[NewT, E_co]:
208
+ """Asynchronously maps the success value using an async function.
209
+
210
+ Args:
211
+ mapper (AsyncMapper[T_co, NewT]): An async function to transform the value.
212
+
213
+ Returns:
214
+ AsyncResult[NewT, E_co]: An async result wrapper.
215
+
216
+ Examples:
217
+ >>> async def async_double(x: int) -> int:
218
+ ... return x * 2
219
+ >>> await Ok(2).amap(async_double)
220
+ Ok(4)
221
+ >>> await Err("error").amap(async_double)
222
+ Err('error')
223
+ """
224
+ ...
225
+
226
+ def amap_err(
227
+ self,
228
+ mapper: AsyncMapper[E_co, NewE],
229
+ ) -> AsyncResult[T_co, NewE]:
230
+ """Asynchronously maps the error value using an async function.
231
+
232
+ Args:
233
+ mapper (AsyncMapper[E_co, NewE]): An async function to transform the error.
234
+
235
+ Returns:
236
+ AsyncResult[T_co, NewE]: An async result wrapper.
237
+
238
+ Examples:
239
+ >>> async def async_log(e: str) -> str:
240
+ ... return f"logged_{e}"
241
+ >>> await Err("fail").amap_err(async_log)
242
+ Err('logged_fail')
243
+ >>> await Ok(42).amap_err(async_log)
244
+ Ok(42)
245
+ """
246
+ ...
247
+
248
+ def abind(
249
+ self, binder: AsyncBinder[T_co, NewT, NewE]
250
+ ) -> AsyncResult[NewT, E_co | NewE]:
251
+ """
252
+ Asynchronously binds the success value to an async
253
+ function returning a Result.
254
+
255
+ Args:
256
+ binder (AsyncBinder[T_co, NewT, NewE]): An async
257
+ function returning a Result.
258
+
259
+ Returns:
260
+ AsyncResult[NewT, E_co | NewE]: An async result wrapper.
261
+
262
+ Examples:
263
+ >>> async def async_check(x: int) -> Result[int, str]:
264
+ ... return Ok(x) if x > 0 else Err("negative")
265
+ >>> await Ok(5).abind(async_check)
266
+ Ok(5)
267
+ >>> await Err("error").abind(async_check)
268
+ Err('error')
269
+ """
270
+ ...
271
+
272
+ def acatch(
273
+ self,
274
+ binder: AsyncBinder[E_co, NewT, NewE],
275
+ ) -> AsyncResult[T_co, E_co] | AsyncResult[NewT, NewE]: ...
276
+
277
+ async def aresolve(self, mapper: AsyncMapper[E_co, NewT]) -> T_co | NewT: ...
@@ -0,0 +1,20 @@
1
+ # pragma: exclude file
2
+
3
+ import sys
4
+ from collections.abc import Awaitable, Callable
5
+
6
+ if sys.version_info >= (3, 10):
7
+ from typing import TypeAlias
8
+ else:
9
+ from typing_extensions import TypeAlias
10
+
11
+ from fates._result import Result
12
+ from fates._typevars import New, NewE, NewT, Old, T_co
13
+
14
+ Mapper: TypeAlias = Callable[[Old], New]
15
+
16
+ AsyncMapper: TypeAlias = Callable[[Old], Awaitable[New]]
17
+
18
+ Binder: TypeAlias = Callable[[T_co], Result[NewT, NewE]]
19
+
20
+ AsyncBinder: TypeAlias = Callable[[T_co], Awaitable[Result[NewT, NewE]]]
@@ -0,0 +1,9 @@
1
+ from typing import TypeVar
2
+
3
+ T_co = TypeVar("T_co", covariant=True)
4
+ E_co = TypeVar("E_co", covariant=True)
5
+ NewT = TypeVar("NewT")
6
+ NewE = TypeVar("NewE")
7
+ DefaultT = TypeVar("DefaultT")
8
+ Old = TypeVar("Old")
9
+ New = TypeVar("New")
@@ -0,0 +1,2 @@
1
+ class UnwrapError(Exception):
2
+ """Raises when unwrap() called on Err or unwrap_err() called on Ok."""
File without changes