flow-res 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.
- flow_res-0.1.0/LICENSE +21 -0
- flow_res-0.1.0/PKG-INFO +164 -0
- flow_res-0.1.0/README.md +149 -0
- flow_res-0.1.0/pyproject.toml +69 -0
- flow_res-0.1.0/src/flow_res/__init__.py +18 -0
- flow_res-0.1.0/src/flow_res/async_result.py +184 -0
- flow_res-0.1.0/src/flow_res/combinators.py +310 -0
- flow_res-0.1.0/src/flow_res/guards.py +23 -0
- flow_res-0.1.0/src/flow_res/result.py +191 -0
- flow_res-0.1.0/src/flow_res/safe.py +74 -0
flow_res-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Shimae
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
flow_res-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: flow-res
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary:
|
|
5
|
+
Author: Shimae
|
|
6
|
+
Author-email: Shimae <50893541+aiagate@users.noreply.github.com>
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Requires-Python: >=3.13
|
|
12
|
+
Project-URL: Homepage, https://github.com/aiagate/flow-res
|
|
13
|
+
Project-URL: Issues, https://github.com/aiagate/flow-res/issues
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# flow-res
|
|
17
|
+
|
|
18
|
+
Rust 言語の `Result` 型に範を仰いだ、Python 向けの高機能かつ型安全なエラーハンドリングライブラリです。
|
|
19
|
+
|
|
20
|
+
従来の例外駆動型(Exception-driven)から、Result 駆動型(Result-driven)へのパラダイムシフトを強力に支援します。明示的なエラーハンドリングを導入することで、コードの堅牢性と可読性を飛躍的に向上させることが可能です。
|
|
21
|
+
|
|
22
|
+
## 主要な特徴
|
|
23
|
+
|
|
24
|
+
* **厳密な型安全性**: ジェネリクスを活用し、成功値とエラー値の双方に対して静的解析(mypy, pyright 等)を適用可能です。
|
|
25
|
+
* **鉄道指向プログラミング(ROP)**: `map` や `and_then` によるメソッドチェーンにより、宣言的なエラーハンドリングを実現します。
|
|
26
|
+
* **非同期処理のネイティブサポート**: `@async_result` デコレータを通じて、非同期処理を `AwaitableResult` として透過的にチェーン可能です。
|
|
27
|
+
* **メタプログラミングによる統合**: `@safe` デコレータを用いることで、既存の例外送出型関数を容易に Result 型へ変換できます。
|
|
28
|
+
* **高度な結果集約**: `combine`(早期失敗)および `combine_all`(全エラー集約)により、複数の処理結果を合理的に統合します。
|
|
29
|
+
* **軽量設計(ゼロ依存)**: 外部ライブラリへの依存はなく、プロジェクトへの導入障壁が極めて低く抑えられています。
|
|
30
|
+
|
|
31
|
+
## インストール
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
pip install flow_res
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
※ Python 3.13 以上が必要です。
|
|
38
|
+
|
|
39
|
+
## 実装ガイド
|
|
40
|
+
|
|
41
|
+
### 1. 基本的な定義とハンドリング
|
|
42
|
+
|
|
43
|
+
関数の戻り値に `Result` を指定することで、呼び出し側に対してエラー処理の検討を強制(明示)させます。Python 3.10 以降の構造的パターンマッチングを利用することで、エレガントに結果を処理できます。
|
|
44
|
+
|
|
45
|
+
```python
|
|
46
|
+
from flow_res import Result, Ok, Err
|
|
47
|
+
|
|
48
|
+
def divide(a: int, b: int) -> Result[float, ValueError]:
|
|
49
|
+
"""2つの数値の除算を行い、結果を Result 型で返却する"""
|
|
50
|
+
if b == 0:
|
|
51
|
+
return Err(ValueError("Division by zero"))
|
|
52
|
+
return Ok(a / b)
|
|
53
|
+
|
|
54
|
+
result = divide(10, 2)
|
|
55
|
+
match result:
|
|
56
|
+
case Ok(value):
|
|
57
|
+
print(f"Success: {value}")
|
|
58
|
+
case Err(error):
|
|
59
|
+
print(f"Failure: {error}")
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
### 2. 関数型インターフェースによる連鎖処理 (Railroad-Oriented Programming)
|
|
63
|
+
|
|
64
|
+
`map` や `and_then` を用いることで、命令的な条件分岐を排除し、処理のパイプラインを構築できます。
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from flow_res import Result
|
|
68
|
+
|
|
69
|
+
def validate_positive(x: int) -> Result[int, ValueError]:
|
|
70
|
+
if x < 0:
|
|
71
|
+
return Err(ValueError("Must be positive"))
|
|
72
|
+
return Ok(x)
|
|
73
|
+
|
|
74
|
+
# 依存関係のある処理の連結
|
|
75
|
+
result = (
|
|
76
|
+
Ok(5)
|
|
77
|
+
.and_then(validate_positive)
|
|
78
|
+
.map(lambda x: x * 2)
|
|
79
|
+
.map(lambda x: x + 3)
|
|
80
|
+
)
|
|
81
|
+
print(result.unwrap()) # 13
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### 3. @safe デコレータによる例外のラップ
|
|
85
|
+
|
|
86
|
+
既存の例外を発生させる可能性のある関数を、低コストで Result 駆動型へ移行させます。
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from flow_res import safe
|
|
90
|
+
|
|
91
|
+
@safe
|
|
92
|
+
def parse_int(s: str) -> int:
|
|
93
|
+
return int(s)
|
|
94
|
+
|
|
95
|
+
# 例外は送出されず、Err として返却される
|
|
96
|
+
result = parse_int("not_a_number")
|
|
97
|
+
print(result) # Err(error=ValueError("invalid literal for int() with base 10: 'not_a_number'"))
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 4. 非同期処理の統合 (@async_result)
|
|
101
|
+
|
|
102
|
+
`@async_result` デコレータを使用することで、非同期関数の実行結果に対しても await 前にメソッドチェーンを適用できます。
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
import asyncio
|
|
106
|
+
from flow_res import Result, async_result
|
|
107
|
+
|
|
108
|
+
@async_result
|
|
109
|
+
async def fetch_user(user_id: int) -> Result[dict, ValueError]:
|
|
110
|
+
await asyncio.sleep(0.1)
|
|
111
|
+
if user_id < 0:
|
|
112
|
+
return Err(ValueError("Invalid user ID"))
|
|
113
|
+
return Ok({"id": user_id, "name": f"User{user_id}"})
|
|
114
|
+
|
|
115
|
+
async def main():
|
|
116
|
+
# 処理を連結した後に一括で await
|
|
117
|
+
result = await (
|
|
118
|
+
fetch_user(1)
|
|
119
|
+
.map(lambda u: u["name"])
|
|
120
|
+
.map(str.upper)
|
|
121
|
+
)
|
|
122
|
+
print(result.unwrap()) # USER1
|
|
123
|
+
|
|
124
|
+
asyncio.run(main())
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
### 5. 複数結果の集約ロジック (combine / combine_all)
|
|
128
|
+
|
|
129
|
+
バリデーションなど、複数の検証結果を一括で扱うためのインターフェースを提供します。
|
|
130
|
+
|
|
131
|
+
* `combine`: 最初に遭遇した `Err` を返却する(短絡評価・早期失敗)
|
|
132
|
+
* `combine_all`: すべての `Err` を集約して複数の例外を保持する `Err` を返却する(全件チェック)
|
|
133
|
+
|
|
134
|
+
```python
|
|
135
|
+
from flow_res import Result, combine, combine_all, Ok, Err
|
|
136
|
+
|
|
137
|
+
results = (
|
|
138
|
+
Ok(1),
|
|
139
|
+
Err(ValueError("error1")),
|
|
140
|
+
Err(RuntimeError("error2")),
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
# 最初のエラー (error1) のみを返す
|
|
144
|
+
print(combine(results))
|
|
145
|
+
|
|
146
|
+
# すべてのエラーを集約して返す
|
|
147
|
+
match combine_all(results):
|
|
148
|
+
case Err(error):
|
|
149
|
+
for e in error.exceptions:
|
|
150
|
+
print(f"Error: {e}")
|
|
151
|
+
```
|
|
152
|
+
|
|
153
|
+
## 動作環境
|
|
154
|
+
|
|
155
|
+
* **Python バージョン**: 3.13 以上
|
|
156
|
+
* **型ヒント**: 完全対応(Static Type Checking を推奨)
|
|
157
|
+
|
|
158
|
+
## ライセンス
|
|
159
|
+
|
|
160
|
+
本プロジェクトは MIT License の下に公開されています。
|
|
161
|
+
|
|
162
|
+
## 協力・貢献
|
|
163
|
+
|
|
164
|
+
不具合報告や機能拡張の提案は、[GitHub Issues](https://github.com/aiagate/flow-res/issues) にて承っております。
|
flow_res-0.1.0/README.md
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
# flow-res
|
|
2
|
+
|
|
3
|
+
Rust 言語の `Result` 型に範を仰いだ、Python 向けの高機能かつ型安全なエラーハンドリングライブラリです。
|
|
4
|
+
|
|
5
|
+
従来の例外駆動型(Exception-driven)から、Result 駆動型(Result-driven)へのパラダイムシフトを強力に支援します。明示的なエラーハンドリングを導入することで、コードの堅牢性と可読性を飛躍的に向上させることが可能です。
|
|
6
|
+
|
|
7
|
+
## 主要な特徴
|
|
8
|
+
|
|
9
|
+
* **厳密な型安全性**: ジェネリクスを活用し、成功値とエラー値の双方に対して静的解析(mypy, pyright 等)を適用可能です。
|
|
10
|
+
* **鉄道指向プログラミング(ROP)**: `map` や `and_then` によるメソッドチェーンにより、宣言的なエラーハンドリングを実現します。
|
|
11
|
+
* **非同期処理のネイティブサポート**: `@async_result` デコレータを通じて、非同期処理を `AwaitableResult` として透過的にチェーン可能です。
|
|
12
|
+
* **メタプログラミングによる統合**: `@safe` デコレータを用いることで、既存の例外送出型関数を容易に Result 型へ変換できます。
|
|
13
|
+
* **高度な結果集約**: `combine`(早期失敗)および `combine_all`(全エラー集約)により、複数の処理結果を合理的に統合します。
|
|
14
|
+
* **軽量設計(ゼロ依存)**: 外部ライブラリへの依存はなく、プロジェクトへの導入障壁が極めて低く抑えられています。
|
|
15
|
+
|
|
16
|
+
## インストール
|
|
17
|
+
|
|
18
|
+
```bash
|
|
19
|
+
pip install flow_res
|
|
20
|
+
```
|
|
21
|
+
|
|
22
|
+
※ Python 3.13 以上が必要です。
|
|
23
|
+
|
|
24
|
+
## 実装ガイド
|
|
25
|
+
|
|
26
|
+
### 1. 基本的な定義とハンドリング
|
|
27
|
+
|
|
28
|
+
関数の戻り値に `Result` を指定することで、呼び出し側に対してエラー処理の検討を強制(明示)させます。Python 3.10 以降の構造的パターンマッチングを利用することで、エレガントに結果を処理できます。
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from flow_res import Result, Ok, Err
|
|
32
|
+
|
|
33
|
+
def divide(a: int, b: int) -> Result[float, ValueError]:
|
|
34
|
+
"""2つの数値の除算を行い、結果を Result 型で返却する"""
|
|
35
|
+
if b == 0:
|
|
36
|
+
return Err(ValueError("Division by zero"))
|
|
37
|
+
return Ok(a / b)
|
|
38
|
+
|
|
39
|
+
result = divide(10, 2)
|
|
40
|
+
match result:
|
|
41
|
+
case Ok(value):
|
|
42
|
+
print(f"Success: {value}")
|
|
43
|
+
case Err(error):
|
|
44
|
+
print(f"Failure: {error}")
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
### 2. 関数型インターフェースによる連鎖処理 (Railroad-Oriented Programming)
|
|
48
|
+
|
|
49
|
+
`map` や `and_then` を用いることで、命令的な条件分岐を排除し、処理のパイプラインを構築できます。
|
|
50
|
+
|
|
51
|
+
```python
|
|
52
|
+
from flow_res import Result
|
|
53
|
+
|
|
54
|
+
def validate_positive(x: int) -> Result[int, ValueError]:
|
|
55
|
+
if x < 0:
|
|
56
|
+
return Err(ValueError("Must be positive"))
|
|
57
|
+
return Ok(x)
|
|
58
|
+
|
|
59
|
+
# 依存関係のある処理の連結
|
|
60
|
+
result = (
|
|
61
|
+
Ok(5)
|
|
62
|
+
.and_then(validate_positive)
|
|
63
|
+
.map(lambda x: x * 2)
|
|
64
|
+
.map(lambda x: x + 3)
|
|
65
|
+
)
|
|
66
|
+
print(result.unwrap()) # 13
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### 3. @safe デコレータによる例外のラップ
|
|
70
|
+
|
|
71
|
+
既存の例外を発生させる可能性のある関数を、低コストで Result 駆動型へ移行させます。
|
|
72
|
+
|
|
73
|
+
```python
|
|
74
|
+
from flow_res import safe
|
|
75
|
+
|
|
76
|
+
@safe
|
|
77
|
+
def parse_int(s: str) -> int:
|
|
78
|
+
return int(s)
|
|
79
|
+
|
|
80
|
+
# 例外は送出されず、Err として返却される
|
|
81
|
+
result = parse_int("not_a_number")
|
|
82
|
+
print(result) # Err(error=ValueError("invalid literal for int() with base 10: 'not_a_number'"))
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
### 4. 非同期処理の統合 (@async_result)
|
|
86
|
+
|
|
87
|
+
`@async_result` デコレータを使用することで、非同期関数の実行結果に対しても await 前にメソッドチェーンを適用できます。
|
|
88
|
+
|
|
89
|
+
```python
|
|
90
|
+
import asyncio
|
|
91
|
+
from flow_res import Result, async_result
|
|
92
|
+
|
|
93
|
+
@async_result
|
|
94
|
+
async def fetch_user(user_id: int) -> Result[dict, ValueError]:
|
|
95
|
+
await asyncio.sleep(0.1)
|
|
96
|
+
if user_id < 0:
|
|
97
|
+
return Err(ValueError("Invalid user ID"))
|
|
98
|
+
return Ok({"id": user_id, "name": f"User{user_id}"})
|
|
99
|
+
|
|
100
|
+
async def main():
|
|
101
|
+
# 処理を連結した後に一括で await
|
|
102
|
+
result = await (
|
|
103
|
+
fetch_user(1)
|
|
104
|
+
.map(lambda u: u["name"])
|
|
105
|
+
.map(str.upper)
|
|
106
|
+
)
|
|
107
|
+
print(result.unwrap()) # USER1
|
|
108
|
+
|
|
109
|
+
asyncio.run(main())
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
### 5. 複数結果の集約ロジック (combine / combine_all)
|
|
113
|
+
|
|
114
|
+
バリデーションなど、複数の検証結果を一括で扱うためのインターフェースを提供します。
|
|
115
|
+
|
|
116
|
+
* `combine`: 最初に遭遇した `Err` を返却する(短絡評価・早期失敗)
|
|
117
|
+
* `combine_all`: すべての `Err` を集約して複数の例外を保持する `Err` を返却する(全件チェック)
|
|
118
|
+
|
|
119
|
+
```python
|
|
120
|
+
from flow_res import Result, combine, combine_all, Ok, Err
|
|
121
|
+
|
|
122
|
+
results = (
|
|
123
|
+
Ok(1),
|
|
124
|
+
Err(ValueError("error1")),
|
|
125
|
+
Err(RuntimeError("error2")),
|
|
126
|
+
)
|
|
127
|
+
|
|
128
|
+
# 最初のエラー (error1) のみを返す
|
|
129
|
+
print(combine(results))
|
|
130
|
+
|
|
131
|
+
# すべてのエラーを集約して返す
|
|
132
|
+
match combine_all(results):
|
|
133
|
+
case Err(error):
|
|
134
|
+
for e in error.exceptions:
|
|
135
|
+
print(f"Error: {e}")
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
## 動作環境
|
|
139
|
+
|
|
140
|
+
* **Python バージョン**: 3.13 以上
|
|
141
|
+
* **型ヒント**: 完全対応(Static Type Checking を推奨)
|
|
142
|
+
|
|
143
|
+
## ライセンス
|
|
144
|
+
|
|
145
|
+
本プロジェクトは MIT License の下に公開されています。
|
|
146
|
+
|
|
147
|
+
## 協力・貢献
|
|
148
|
+
|
|
149
|
+
不具合報告や機能拡張の提案は、[GitHub Issues](https://github.com/aiagate/flow-res/issues) にて承っております。
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "flow-res"
|
|
3
|
+
version = "0.1.0"
|
|
4
|
+
authors = [
|
|
5
|
+
{ name="Shimae", email="50893541+aiagate@users.noreply.github.com" },
|
|
6
|
+
]
|
|
7
|
+
description = ""
|
|
8
|
+
readme = "README.md"
|
|
9
|
+
requires-python = ">=3.13"
|
|
10
|
+
dependencies = []
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Programming Language :: Python :: 3",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
]
|
|
15
|
+
license = "MIT"
|
|
16
|
+
license-files = ["LICENSE*"]
|
|
17
|
+
|
|
18
|
+
[build-system]
|
|
19
|
+
requires = ["uv_build >= 0.10.10, <0.11.0"]
|
|
20
|
+
build-backend = "uv_build"
|
|
21
|
+
|
|
22
|
+
[project.urls]
|
|
23
|
+
Homepage = "https://github.com/aiagate/flow-res"
|
|
24
|
+
Issues = "https://github.com/aiagate/flow-res/issues"
|
|
25
|
+
|
|
26
|
+
[dependency-groups]
|
|
27
|
+
dev = [
|
|
28
|
+
"anyio>=4.11.0",
|
|
29
|
+
"pre-commit>=4.5.0",
|
|
30
|
+
"pyright>=1.1.407",
|
|
31
|
+
"pytest>=8.3.5",
|
|
32
|
+
"pytest-asyncio>=1.3.0",
|
|
33
|
+
"pytest-cov>=7.0.0",
|
|
34
|
+
"pytest-mock>=3.14.0",
|
|
35
|
+
"ruff>=0.14.6",
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
[tool.coverage.run]
|
|
39
|
+
source = ["src"]
|
|
40
|
+
omit = ["tests/*"]
|
|
41
|
+
|
|
42
|
+
[tool.coverage.report]
|
|
43
|
+
precision = 2
|
|
44
|
+
show_missing = true
|
|
45
|
+
skip_empty = true
|
|
46
|
+
fail_under = 95
|
|
47
|
+
exclude_lines = [
|
|
48
|
+
"pragma: no cover",
|
|
49
|
+
"def __repr__",
|
|
50
|
+
"raise AssertionError",
|
|
51
|
+
"raise NotImplementedError",
|
|
52
|
+
"if __name__ == .__main__.:",
|
|
53
|
+
"if TYPE_CHECKING:",
|
|
54
|
+
"@abstractmethod",
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
[tool.pytest.ini_options]
|
|
58
|
+
pythonpath = ["."]
|
|
59
|
+
testpaths = ["tests"]
|
|
60
|
+
python_files = ["test_*.py"]
|
|
61
|
+
python_classes = ["Test*"]
|
|
62
|
+
python_functions = ["test_*"]
|
|
63
|
+
addopts = [
|
|
64
|
+
"-v",
|
|
65
|
+
"--cov=src",
|
|
66
|
+
"--cov-report=term-missing",
|
|
67
|
+
"--cov-report=html",
|
|
68
|
+
]
|
|
69
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from .async_result import AwaitableResult, async_result
|
|
2
|
+
from .combinators import combine, combine_all
|
|
3
|
+
from .guards import is_err, is_ok
|
|
4
|
+
from .result import Err, Ok, Result
|
|
5
|
+
from .safe import safe
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
"Result",
|
|
9
|
+
"AwaitableResult",
|
|
10
|
+
"Ok",
|
|
11
|
+
"Err",
|
|
12
|
+
"is_err",
|
|
13
|
+
"is_ok",
|
|
14
|
+
"safe",
|
|
15
|
+
"combine",
|
|
16
|
+
"combine_all",
|
|
17
|
+
"async_result",
|
|
18
|
+
]
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import inspect
|
|
2
|
+
from collections.abc import Awaitable, Callable, Coroutine, Generator
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from typing import Any
|
|
5
|
+
|
|
6
|
+
from .result import Err, Ok, Result
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class AwaitableResult[T, E: Exception]:
|
|
10
|
+
"""
|
|
11
|
+
Awaitable wrapper for Result that enables method chaining before await.
|
|
12
|
+
|
|
13
|
+
This allows elegant syntax like:
|
|
14
|
+
message = await Mediator.send_async(query).map(...).unwrap()
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
def __init__(self, coro: Coroutine[Any, Any, Result[T, E]]) -> None:
|
|
18
|
+
"""
|
|
19
|
+
Initialize with a coroutine that returns a Result.
|
|
20
|
+
|
|
21
|
+
Args:
|
|
22
|
+
coro: Coroutine that will return Result[T, E]
|
|
23
|
+
"""
|
|
24
|
+
self._coro = coro
|
|
25
|
+
|
|
26
|
+
def __repr__(self) -> str:
|
|
27
|
+
"""Return a helpful representation for debugging.
|
|
28
|
+
|
|
29
|
+
Shows the wrapped coroutine's repr to aid debugging without awaiting.
|
|
30
|
+
"""
|
|
31
|
+
try:
|
|
32
|
+
coro_repr = repr(self._coro)
|
|
33
|
+
except Exception:
|
|
34
|
+
coro_repr = "<unreprable coroutine>"
|
|
35
|
+
return f"ResultAwaitable(coro={coro_repr})"
|
|
36
|
+
|
|
37
|
+
def __await__(self) -> Generator[Any, None, Result[T, E]]:
|
|
38
|
+
"""Make this object awaitable, returning the underlying Result."""
|
|
39
|
+
return self._coro.__await__()
|
|
40
|
+
|
|
41
|
+
def map[U](self, f: Callable[[T], U]) -> "AwaitableResult[U, E]":
|
|
42
|
+
"""
|
|
43
|
+
Transform the Ok value using the provided function.
|
|
44
|
+
|
|
45
|
+
This method chains onto the coroutine, creating a new coroutine that:
|
|
46
|
+
1. Awaits the current Result
|
|
47
|
+
2. Applies .map() to transform the value
|
|
48
|
+
3. Returns the transformed Result
|
|
49
|
+
|
|
50
|
+
Args:
|
|
51
|
+
f: Function to apply to the Ok value (T -> U)
|
|
52
|
+
|
|
53
|
+
Returns:
|
|
54
|
+
ResultAwaitable[U, E] wrapping the transformed result
|
|
55
|
+
|
|
56
|
+
Example:
|
|
57
|
+
user_id = await Mediator.send_async(cmd).map(lambda v: v.user_id)
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
async def mapped() -> Result[U, E]:
|
|
61
|
+
_result: Result[T, E] = await self
|
|
62
|
+
return _result.map(f)
|
|
63
|
+
|
|
64
|
+
return AwaitableResult(mapped())
|
|
65
|
+
|
|
66
|
+
def and_then[U](
|
|
67
|
+
self, f: Callable[[T], Awaitable[Result[U, E]] | Result[U, E]]
|
|
68
|
+
) -> "AwaitableResult[U, E]":
|
|
69
|
+
"""
|
|
70
|
+
Apply a function (sync or async) that returns a Result, flattening the nested Result.
|
|
71
|
+
|
|
72
|
+
This enables chaining operations that may fail.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
f: Function that takes the Ok value and returns a new Result
|
|
76
|
+
(T -> Awaitable[Result[U, E]] | Result[U, E])
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
ResultAwaitable[U, E] wrapping the result of applying the function
|
|
80
|
+
|
|
81
|
+
Example:
|
|
82
|
+
await (
|
|
83
|
+
Mediator.send_async(create_cmd)
|
|
84
|
+
.and_then(lambda result: Mediator.send_async(GetQuery(result.id)))
|
|
85
|
+
.map(lambda value: format_message(value))
|
|
86
|
+
.unwrap()
|
|
87
|
+
)
|
|
88
|
+
"""
|
|
89
|
+
|
|
90
|
+
async def chained() -> Result[U, E]:
|
|
91
|
+
_result: Result[T, E] = await self
|
|
92
|
+
match _result:
|
|
93
|
+
case Ok(value):
|
|
94
|
+
res = f(value)
|
|
95
|
+
if inspect.isawaitable(res):
|
|
96
|
+
return await res
|
|
97
|
+
return res
|
|
98
|
+
case Err():
|
|
99
|
+
return _result
|
|
100
|
+
|
|
101
|
+
return AwaitableResult(chained())
|
|
102
|
+
|
|
103
|
+
def unwrap(self) -> Awaitable[T]:
|
|
104
|
+
"""
|
|
105
|
+
Return the Ok value or raise the Err as an exception.
|
|
106
|
+
|
|
107
|
+
This is a terminal operation that unwraps the Result.
|
|
108
|
+
|
|
109
|
+
Returns:
|
|
110
|
+
Awaitable[T] that will return the value or raise the error
|
|
111
|
+
|
|
112
|
+
Example:
|
|
113
|
+
message = await Mediator.send_async(query).map(...).unwrap()
|
|
114
|
+
"""
|
|
115
|
+
|
|
116
|
+
async def unwrapped() -> T:
|
|
117
|
+
_result: Result[T, E] = await self
|
|
118
|
+
return _result.unwrap()
|
|
119
|
+
|
|
120
|
+
return unwrapped()
|
|
121
|
+
|
|
122
|
+
def map_err[F: Exception](self, f: Callable[[E], F]) -> "AwaitableResult[T, F]":
|
|
123
|
+
"""
|
|
124
|
+
Transform the error value using the provided function asynchronously.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
f: Function to apply to the error value (E -> F)
|
|
128
|
+
|
|
129
|
+
Returns:
|
|
130
|
+
ResultAwaitable[T, F] wrapping the result with transformed error type
|
|
131
|
+
|
|
132
|
+
Example:
|
|
133
|
+
await (
|
|
134
|
+
Mediator.send_async(cmd)
|
|
135
|
+
.map_err(lambda e: DatabaseError(f"DB error: {e}"))
|
|
136
|
+
.unwrap()
|
|
137
|
+
)
|
|
138
|
+
"""
|
|
139
|
+
|
|
140
|
+
async def mapped() -> Result[T, F]:
|
|
141
|
+
_result = await self
|
|
142
|
+
return _result.map_err(f)
|
|
143
|
+
|
|
144
|
+
return AwaitableResult(mapped())
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def async_result[T, E: Exception](
|
|
148
|
+
func: Callable[..., Coroutine[Any, Any, Result[T, E]]],
|
|
149
|
+
) -> Callable[..., AwaitableResult[T, E]]:
|
|
150
|
+
"""
|
|
151
|
+
Decorator that wraps an async function returning Result in AwaitableResult.
|
|
152
|
+
|
|
153
|
+
This allows async functions to return Result types while automatically
|
|
154
|
+
wrapping them in AwaitableResult for method chaining (.map, .and_then, etc.)
|
|
155
|
+
|
|
156
|
+
The decorated function must be async and return a Result. The decorator
|
|
157
|
+
will wrap the coroutine in AwaitableResult, allowing method chaining without
|
|
158
|
+
an explicit await.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
func: An async function that returns Result[T, E]
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
A wrapped function that returns AwaitableResult[T, E]
|
|
165
|
+
|
|
166
|
+
Example:
|
|
167
|
+
@async_result
|
|
168
|
+
async def fetch_user(user_id: int) -> Result[dict, Exception]:
|
|
169
|
+
if user_id < 0:
|
|
170
|
+
return Err(ValueError("Invalid user ID"))
|
|
171
|
+
return Ok({"id": user_id})
|
|
172
|
+
|
|
173
|
+
# Can now use method chaining without await:
|
|
174
|
+
result = fetch_user(1).map(lambda u: u["id"]).map(str.upper)
|
|
175
|
+
print(await result) # Ok({"id": 1})
|
|
176
|
+
"""
|
|
177
|
+
|
|
178
|
+
@wraps(func)
|
|
179
|
+
def wrapper(*args: Any, **kwargs: Any) -> AwaitableResult[T, E]:
|
|
180
|
+
return AwaitableResult(
|
|
181
|
+
coro=func(*args, **kwargs),
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
return wrapper
|
|
@@ -0,0 +1,310 @@
|
|
|
1
|
+
from collections.abc import Sequence
|
|
2
|
+
from typing import Any, overload
|
|
3
|
+
|
|
4
|
+
from .result import Result, Ok, Err
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@overload
|
|
8
|
+
def combine[E: Exception](results: tuple[()]) -> Result[tuple[()], E]: ...
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@overload
|
|
12
|
+
def combine[T1, E: Exception](
|
|
13
|
+
results: tuple[Result[T1, E]],
|
|
14
|
+
) -> Result[tuple[T1], E]: ...
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
@overload
|
|
18
|
+
def combine[T1, T2, E: Exception](
|
|
19
|
+
results: tuple[Result[T1, E], Result[T2, E]],
|
|
20
|
+
) -> Result[tuple[T1, T2], E]: ...
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@overload
|
|
24
|
+
def combine[T1, T2, T3, E: Exception](
|
|
25
|
+
results: tuple[Result[T1, E], Result[T2, E], Result[T3, E]],
|
|
26
|
+
) -> Result[tuple[T1, T2, T3], E]: ...
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
@overload
|
|
30
|
+
def combine[T1, T2, T3, T4, E: Exception](
|
|
31
|
+
results: tuple[Result[T1, E], Result[T2, E], Result[T3, E], Result[T4, E]],
|
|
32
|
+
) -> Result[tuple[T1, T2, T3, T4], E]: ...
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
@overload
|
|
36
|
+
def combine[T1, T2, T3, T4, T5, E: Exception](
|
|
37
|
+
results: tuple[
|
|
38
|
+
Result[T1, E],
|
|
39
|
+
Result[T2, E],
|
|
40
|
+
Result[T3, E],
|
|
41
|
+
Result[T4, E],
|
|
42
|
+
Result[T5, E],
|
|
43
|
+
],
|
|
44
|
+
) -> Result[tuple[T1, T2, T3, T4, T5], E]: ...
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@overload
|
|
48
|
+
def combine[T1, T2, T3, T4, T5, T6, E: Exception](
|
|
49
|
+
results: tuple[
|
|
50
|
+
Result[T1, E],
|
|
51
|
+
Result[T2, E],
|
|
52
|
+
Result[T3, E],
|
|
53
|
+
Result[T4, E],
|
|
54
|
+
Result[T5, E],
|
|
55
|
+
Result[T6, E],
|
|
56
|
+
],
|
|
57
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6], E]: ...
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
@overload
|
|
61
|
+
def combine[T1, T2, T3, T4, T5, T6, T7, E: Exception](
|
|
62
|
+
results: tuple[
|
|
63
|
+
Result[T1, E],
|
|
64
|
+
Result[T2, E],
|
|
65
|
+
Result[T3, E],
|
|
66
|
+
Result[T4, E],
|
|
67
|
+
Result[T5, E],
|
|
68
|
+
Result[T6, E],
|
|
69
|
+
Result[T7, E],
|
|
70
|
+
],
|
|
71
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7], E]: ...
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
@overload
|
|
75
|
+
def combine[T1, T2, T3, T4, T5, T6, T7, T8, E: Exception](
|
|
76
|
+
results: tuple[
|
|
77
|
+
Result[T1, E],
|
|
78
|
+
Result[T2, E],
|
|
79
|
+
Result[T3, E],
|
|
80
|
+
Result[T4, E],
|
|
81
|
+
Result[T5, E],
|
|
82
|
+
Result[T6, E],
|
|
83
|
+
Result[T7, E],
|
|
84
|
+
Result[T8, E],
|
|
85
|
+
],
|
|
86
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7, T8], E]: ...
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
@overload
|
|
90
|
+
def combine[T1, T2, T3, T4, T5, T6, T7, T8, T9, E: Exception](
|
|
91
|
+
results: tuple[
|
|
92
|
+
Result[T1, E],
|
|
93
|
+
Result[T2, E],
|
|
94
|
+
Result[T3, E],
|
|
95
|
+
Result[T4, E],
|
|
96
|
+
Result[T5, E],
|
|
97
|
+
Result[T6, E],
|
|
98
|
+
Result[T7, E],
|
|
99
|
+
Result[T8, E],
|
|
100
|
+
Result[T9, E],
|
|
101
|
+
],
|
|
102
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9], E]: ...
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
@overload
|
|
106
|
+
def combine[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, E: Exception](
|
|
107
|
+
results: tuple[
|
|
108
|
+
Result[T1, E],
|
|
109
|
+
Result[T2, E],
|
|
110
|
+
Result[T3, E],
|
|
111
|
+
Result[T4, E],
|
|
112
|
+
Result[T5, E],
|
|
113
|
+
Result[T6, E],
|
|
114
|
+
Result[T7, E],
|
|
115
|
+
Result[T8, E],
|
|
116
|
+
Result[T9, E],
|
|
117
|
+
Result[T10, E],
|
|
118
|
+
],
|
|
119
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], E]: ...
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def combine[E: Exception](
|
|
123
|
+
results: Sequence[Result[Any, E]],
|
|
124
|
+
) -> Result[tuple[Any, ...], E]:
|
|
125
|
+
"""
|
|
126
|
+
Aggregates a sequence of Result objects.
|
|
127
|
+
|
|
128
|
+
If all results are Ok, returns an Ok containing a tuple of all success values.
|
|
129
|
+
If any result is an Err, returns the first Err encountered.
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
results: A sequence of Result objects.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
A single Result object. Ok(tuple of success values) or the first Err.
|
|
136
|
+
|
|
137
|
+
Examples:
|
|
138
|
+
Heterogeneous types (use tuple):
|
|
139
|
+
>>> user_id: Result[int, Exception] = Ok(123)
|
|
140
|
+
>>> email: Result[str, Exception] = Ok("user@example.com")
|
|
141
|
+
>>> combine((user_id, email))
|
|
142
|
+
Ok((123, "user@example.com"))
|
|
143
|
+
|
|
144
|
+
Homogeneous types (use list or tuple):
|
|
145
|
+
>>> results = [Ok(1), Ok(2), Ok(3)]
|
|
146
|
+
>>> combine(results)
|
|
147
|
+
Ok((1, 2, 3))
|
|
148
|
+
|
|
149
|
+
Error handling (first error returned):
|
|
150
|
+
>>> results = [Ok(1), Err(Exception("error")), Ok(3)]
|
|
151
|
+
>>> combine(results)
|
|
152
|
+
Err(Exception("error"))
|
|
153
|
+
"""
|
|
154
|
+
values: list[Any] = []
|
|
155
|
+
for r in results:
|
|
156
|
+
if isinstance(r, Err):
|
|
157
|
+
return r # Return the first error found
|
|
158
|
+
values.append(r.unwrap())
|
|
159
|
+
return Ok(tuple(values))
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
@overload
|
|
163
|
+
def combine_all[T1, E: Exception](
|
|
164
|
+
results: tuple[Result[T1, E]],
|
|
165
|
+
) -> Result[tuple[T1], ExceptionGroup]: ...
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
@overload
|
|
169
|
+
def combine_all[T1, T2, E: Exception](
|
|
170
|
+
results: tuple[Result[T1, E], Result[T2, E]],
|
|
171
|
+
) -> Result[tuple[T1, T2], ExceptionGroup]: ...
|
|
172
|
+
|
|
173
|
+
|
|
174
|
+
@overload
|
|
175
|
+
def combine_all[T1, T2, T3, E: Exception](
|
|
176
|
+
results: tuple[Result[T1, E], Result[T2, E], Result[T3, E]],
|
|
177
|
+
) -> Result[tuple[T1, T2, T3], ExceptionGroup]: ...
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
@overload
|
|
181
|
+
def combine_all[T1, T2, T3, T4, E: Exception](
|
|
182
|
+
results: tuple[Result[T1, E], Result[T2, E], Result[T3, E], Result[T4, E]],
|
|
183
|
+
) -> Result[tuple[T1, T2, T3, T4], ExceptionGroup]: ...
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
@overload
|
|
187
|
+
def combine_all[T1, T2, T3, T4, T5, E: Exception](
|
|
188
|
+
results: tuple[
|
|
189
|
+
Result[T1, E],
|
|
190
|
+
Result[T2, E],
|
|
191
|
+
Result[T3, E],
|
|
192
|
+
Result[T4, E],
|
|
193
|
+
Result[T5, E],
|
|
194
|
+
],
|
|
195
|
+
) -> Result[tuple[T1, T2, T3, T4, T5], ExceptionGroup]: ...
|
|
196
|
+
|
|
197
|
+
|
|
198
|
+
@overload
|
|
199
|
+
def combine_all[T1, T2, T3, T4, T5, T6, E: Exception](
|
|
200
|
+
results: tuple[
|
|
201
|
+
Result[T1, E],
|
|
202
|
+
Result[T2, E],
|
|
203
|
+
Result[T3, E],
|
|
204
|
+
Result[T4, E],
|
|
205
|
+
Result[T5, E],
|
|
206
|
+
Result[T6, E],
|
|
207
|
+
],
|
|
208
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6], ExceptionGroup]: ...
|
|
209
|
+
|
|
210
|
+
|
|
211
|
+
@overload
|
|
212
|
+
def combine_all[T1, T2, T3, T4, T5, T6, T7, E: Exception](
|
|
213
|
+
results: tuple[
|
|
214
|
+
Result[T1, E],
|
|
215
|
+
Result[T2, E],
|
|
216
|
+
Result[T3, E],
|
|
217
|
+
Result[T4, E],
|
|
218
|
+
Result[T5, E],
|
|
219
|
+
Result[T6, E],
|
|
220
|
+
Result[T7, E],
|
|
221
|
+
],
|
|
222
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7], ExceptionGroup]: ...
|
|
223
|
+
|
|
224
|
+
|
|
225
|
+
@overload
|
|
226
|
+
def combine_all[T1, T2, T3, T4, T5, T6, T7, T8, E: Exception](
|
|
227
|
+
results: tuple[
|
|
228
|
+
Result[T1, E],
|
|
229
|
+
Result[T2, E],
|
|
230
|
+
Result[T3, E],
|
|
231
|
+
Result[T4, E],
|
|
232
|
+
Result[T5, E],
|
|
233
|
+
Result[T6, E],
|
|
234
|
+
Result[T7, E],
|
|
235
|
+
Result[T8, E],
|
|
236
|
+
],
|
|
237
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7, T8], ExceptionGroup]: ...
|
|
238
|
+
|
|
239
|
+
|
|
240
|
+
@overload
|
|
241
|
+
def combine_all[T1, T2, T3, T4, T5, T6, T7, T8, T9, E: Exception](
|
|
242
|
+
results: tuple[
|
|
243
|
+
Result[T1, E],
|
|
244
|
+
Result[T2, E],
|
|
245
|
+
Result[T3, E],
|
|
246
|
+
Result[T4, E],
|
|
247
|
+
Result[T5, E],
|
|
248
|
+
Result[T6, E],
|
|
249
|
+
Result[T7, E],
|
|
250
|
+
Result[T8, E],
|
|
251
|
+
Result[T9, E],
|
|
252
|
+
],
|
|
253
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9], ExceptionGroup]: ...
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@overload
|
|
257
|
+
def combine_all[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10, E: Exception](
|
|
258
|
+
results: tuple[
|
|
259
|
+
Result[T1, E],
|
|
260
|
+
Result[T2, E],
|
|
261
|
+
Result[T3, E],
|
|
262
|
+
Result[T4, E],
|
|
263
|
+
Result[T5, E],
|
|
264
|
+
Result[T6, E],
|
|
265
|
+
Result[T7, E],
|
|
266
|
+
Result[T8, E],
|
|
267
|
+
Result[T9, E],
|
|
268
|
+
Result[T10, E],
|
|
269
|
+
],
|
|
270
|
+
) -> Result[tuple[T1, T2, T3, T4, T5, T6, T7, T8, T9, T10], ExceptionGroup]: ...
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
def combine_all[E: Exception](
|
|
274
|
+
results: Sequence[Result[Any, E]],
|
|
275
|
+
) -> Result[tuple[Any, ...], ExceptionGroup]:
|
|
276
|
+
"""
|
|
277
|
+
Aggregates a tuple of Results, collecting all errors.
|
|
278
|
+
|
|
279
|
+
If all results are Ok, returns an Ok containing a tuple of all success values.
|
|
280
|
+
If any result is an Err, returns an Err containing an ExceptionGroup with all errors.
|
|
281
|
+
|
|
282
|
+
This is a "fail complete" strategy - useful for validation where you want to show
|
|
283
|
+
all errors to the user at once, rather than one at a time.
|
|
284
|
+
|
|
285
|
+
Args:
|
|
286
|
+
results: A tuple of Result objects.
|
|
287
|
+
|
|
288
|
+
Returns:
|
|
289
|
+
Ok(tuple of success values) if all succeed,
|
|
290
|
+
or Err(ExceptionGroup("Multiple errors occurred", list of errors)) if any fail.
|
|
291
|
+
|
|
292
|
+
Example:
|
|
293
|
+
>>> from app.core.result import combine_all, Ok, Err
|
|
294
|
+
>>> results = (Ok(1), Err(Exception("error1")), Ok(3), Err(Exception("error2")))
|
|
295
|
+
>>> combined = combine_all(results)
|
|
296
|
+
>>> # Returns Err(ExceptionGroup("Multiple errors occurred", [Exception("error1"), Exception("error2")]))
|
|
297
|
+
"""
|
|
298
|
+
values: list[Any] = []
|
|
299
|
+
errors: list[E] = []
|
|
300
|
+
|
|
301
|
+
for r in results:
|
|
302
|
+
if isinstance(r, Err):
|
|
303
|
+
errors.append(r.error)
|
|
304
|
+
else:
|
|
305
|
+
values.append(r.unwrap())
|
|
306
|
+
|
|
307
|
+
if errors:
|
|
308
|
+
return Err(ExceptionGroup("Multiple errors occurred", errors))
|
|
309
|
+
|
|
310
|
+
return Ok(tuple(values))
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from typing import TypeIs
|
|
2
|
+
|
|
3
|
+
from .result import Err, Ok, Result
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def is_ok[T, E: Exception = Exception](result: Result[T, E]) -> TypeIs[Ok[T]]:
|
|
7
|
+
"""
|
|
8
|
+
Return true if the result is ok.
|
|
9
|
+
|
|
10
|
+
Uses TypeIs for bidirectional type narrowing - when this returns False,
|
|
11
|
+
the type checker knows the result must be Err.
|
|
12
|
+
"""
|
|
13
|
+
return isinstance(result, Ok)
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def is_err[T, E: Exception = Exception](result: Result[T, E]) -> TypeIs[Err[E]]:
|
|
17
|
+
"""
|
|
18
|
+
Return true if the result is an error.
|
|
19
|
+
|
|
20
|
+
Uses TypeIs for bidirectional type narrowing - when this returns False,
|
|
21
|
+
the type checker knows the result must be Ok.
|
|
22
|
+
"""
|
|
23
|
+
return isinstance(result, Err)
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
"""Generic Result type, inspired by Rust's Result type."""
|
|
2
|
+
|
|
3
|
+
from collections.abc import Callable
|
|
4
|
+
from dataclasses import dataclass
|
|
5
|
+
from typing import Any, Never
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@dataclass(frozen=True)
|
|
9
|
+
class Ok[T]:
|
|
10
|
+
"""Represents a successful result."""
|
|
11
|
+
|
|
12
|
+
value: T
|
|
13
|
+
__match_args__ = ("value",)
|
|
14
|
+
|
|
15
|
+
def map[V](self, f: Callable[[T], V]) -> "Ok[V]":
|
|
16
|
+
"""
|
|
17
|
+
Transform the Ok value using the provided function.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
f: Function to apply to the Ok value (T -> V)
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
Ok[V] containing the transformed value
|
|
24
|
+
|
|
25
|
+
Example:
|
|
26
|
+
Ok(5).map(lambda x: x * 2) # Returns Ok(10)
|
|
27
|
+
"""
|
|
28
|
+
return Ok(f(self.value))
|
|
29
|
+
|
|
30
|
+
def and_then[V, F_Exc: Exception](
|
|
31
|
+
self, f: Callable[[T], "Result[V, F_Exc]"]
|
|
32
|
+
) -> "Result[V, F_Exc]":
|
|
33
|
+
"""
|
|
34
|
+
Apply a function that returns a Result, flattening the nested Result.
|
|
35
|
+
|
|
36
|
+
This is the monadic bind operation (flatMap in some languages).
|
|
37
|
+
Enables chaining operations that may fail.
|
|
38
|
+
|
|
39
|
+
Args:
|
|
40
|
+
f: Function that takes the Ok value and returns a new Result (T -> Result[V, F])
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
Result[V, F] - The result of applying the function
|
|
44
|
+
|
|
45
|
+
Example:
|
|
46
|
+
Ok(5).and_then(lambda x: Ok(x * 2)) # Returns Ok(10)
|
|
47
|
+
Ok(5).and_then(lambda x: Err(Exception("failed"))) # Returns Err(Exception("failed"))
|
|
48
|
+
"""
|
|
49
|
+
return f(self.value)
|
|
50
|
+
|
|
51
|
+
def unwrap(self) -> T:
|
|
52
|
+
"""
|
|
53
|
+
Return the Ok value.
|
|
54
|
+
|
|
55
|
+
Returns:
|
|
56
|
+
The wrapped value
|
|
57
|
+
"""
|
|
58
|
+
return self.value
|
|
59
|
+
|
|
60
|
+
def expect(self, msg: str) -> T:
|
|
61
|
+
"""
|
|
62
|
+
Return the value.
|
|
63
|
+
|
|
64
|
+
Compatible with Err.expect signature to allow usage without type guards,
|
|
65
|
+
but requires a message explaining why success is expected.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
msg: Message explaining why this Result is expected to be Ok (for consistency)
|
|
69
|
+
|
|
70
|
+
Returns:
|
|
71
|
+
The wrapped value
|
|
72
|
+
"""
|
|
73
|
+
return self.value
|
|
74
|
+
|
|
75
|
+
def map_err[F_Exc: Exception](self, f: Callable[[Any], F_Exc]) -> "Ok[T]":
|
|
76
|
+
"""
|
|
77
|
+
Pass through the Ok value unchanged.
|
|
78
|
+
|
|
79
|
+
Args:
|
|
80
|
+
f: Function to map error (ignored for Ok).
|
|
81
|
+
|
|
82
|
+
Returns:
|
|
83
|
+
Self (unchanged Ok value).
|
|
84
|
+
"""
|
|
85
|
+
return self
|
|
86
|
+
|
|
87
|
+
def unwrap_or[T_Def](self, default: T_Def) -> T | T_Def:
|
|
88
|
+
"""
|
|
89
|
+
Return the Ok value.
|
|
90
|
+
|
|
91
|
+
Args:
|
|
92
|
+
default: Value to return if Err. (Ignored for Ok)
|
|
93
|
+
|
|
94
|
+
Returns:
|
|
95
|
+
The wrapped value.
|
|
96
|
+
"""
|
|
97
|
+
return self.value
|
|
98
|
+
|
|
99
|
+
|
|
100
|
+
@dataclass(frozen=True)
|
|
101
|
+
class Err[E: Exception = Exception]:
|
|
102
|
+
"""Represents a failure result."""
|
|
103
|
+
|
|
104
|
+
error: E
|
|
105
|
+
__match_args__ = ("error",)
|
|
106
|
+
|
|
107
|
+
def map[V](self, f: Callable[[Any], Any]) -> "Err[E]":
|
|
108
|
+
"""
|
|
109
|
+
Pass through the error unchanged (Railway-oriented programming pattern).
|
|
110
|
+
|
|
111
|
+
Args:
|
|
112
|
+
f: Function that would be applied (ignored for Err)
|
|
113
|
+
|
|
114
|
+
Returns:
|
|
115
|
+
Self (unchanged Err)
|
|
116
|
+
"""
|
|
117
|
+
return self
|
|
118
|
+
|
|
119
|
+
def and_then[V, F_Exc: Exception](
|
|
120
|
+
self, f: Callable[[Any], "Result[V, F_Exc]"]
|
|
121
|
+
) -> "Err[E]":
|
|
122
|
+
"""
|
|
123
|
+
Pass through the error unchanged (Railway-oriented programming pattern).
|
|
124
|
+
|
|
125
|
+
Since this is an Err, the function is not called and the error propagates.
|
|
126
|
+
|
|
127
|
+
Args:
|
|
128
|
+
f: Function that would be applied (ignored for Err)
|
|
129
|
+
|
|
130
|
+
Returns:
|
|
131
|
+
Self (unchanged Err)
|
|
132
|
+
|
|
133
|
+
Example:
|
|
134
|
+
Err(Exception("error")).and_then(lambda x: Ok(x * 2)) # Returns Err(Exception("error"))
|
|
135
|
+
"""
|
|
136
|
+
return self
|
|
137
|
+
|
|
138
|
+
def expect(self, msg: str) -> Never:
|
|
139
|
+
"""
|
|
140
|
+
Raise an exception with the provided message.
|
|
141
|
+
|
|
142
|
+
Used when you want to assert that this Result should be Ok.
|
|
143
|
+
Requires a message explaining why the Result was expected to be Ok.
|
|
144
|
+
|
|
145
|
+
Args:
|
|
146
|
+
msg: Message explaining why this was expected to be Ok
|
|
147
|
+
|
|
148
|
+
Raises:
|
|
149
|
+
RuntimeError: Always raised with the provided message and underlying error
|
|
150
|
+
|
|
151
|
+
Example:
|
|
152
|
+
result.expect("User should exist in database")
|
|
153
|
+
"""
|
|
154
|
+
raise RuntimeError(f"{msg}: {self.error}") from self.error
|
|
155
|
+
|
|
156
|
+
def map_err[F_Exc: Exception](self, f: Callable[[E], F_Exc]) -> "Err[F_Exc]":
|
|
157
|
+
"""
|
|
158
|
+
Transform the Err value using the provided function.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
f: Function to apply to the Err value (E -> F).
|
|
162
|
+
|
|
163
|
+
Returns:
|
|
164
|
+
Err[F] containing the transformed error.
|
|
165
|
+
"""
|
|
166
|
+
new_error = f(self.error)
|
|
167
|
+
return Err(new_error)
|
|
168
|
+
|
|
169
|
+
def unwrap(self) -> Never:
|
|
170
|
+
"""
|
|
171
|
+
Raise the underlying error.
|
|
172
|
+
|
|
173
|
+
This ensures that unwrap() can be called on Result (Ok | Err).
|
|
174
|
+
"""
|
|
175
|
+
raise self.error
|
|
176
|
+
|
|
177
|
+
def unwrap_or[T_Def](self, default: T_Def) -> T_Def:
|
|
178
|
+
"""
|
|
179
|
+
Return the default value.
|
|
180
|
+
|
|
181
|
+
Args:
|
|
182
|
+
default: Value to return.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
The default value.
|
|
186
|
+
"""
|
|
187
|
+
return default
|
|
188
|
+
|
|
189
|
+
|
|
190
|
+
# The main Result type alias
|
|
191
|
+
type Result[T, E: Exception = Exception] = Ok[T] | Err[E]
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
from collections.abc import Callable
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Any, overload
|
|
4
|
+
|
|
5
|
+
from .result import Err, Ok, Result
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@overload
|
|
9
|
+
def safe(
|
|
10
|
+
*exceptions: type[Exception],
|
|
11
|
+
) -> Callable[[Callable[..., Any]], Callable[..., Result[Any, Exception]]]: ...
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
@overload
|
|
15
|
+
def safe[T](__func: Callable[..., T]) -> Callable[..., Result[T, Exception]]: ...
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def safe(*args: Any) -> Any:
|
|
19
|
+
"""
|
|
20
|
+
Decorator to convert a function that raises exceptions into one that returns a Result.
|
|
21
|
+
|
|
22
|
+
Can be used without arguments to catch all Exceptions, or with specific exception types
|
|
23
|
+
to only catch those.
|
|
24
|
+
|
|
25
|
+
Args:
|
|
26
|
+
*args: Either a single function (when used as @safe), or one or more Exception types
|
|
27
|
+
(when used as @safe(ValueError, TypeError)).
|
|
28
|
+
|
|
29
|
+
Returns:
|
|
30
|
+
A wrapped function that returns Result[T, Exception] instead of raising
|
|
31
|
+
|
|
32
|
+
Example:
|
|
33
|
+
@safe
|
|
34
|
+
def risky_operation(x: int) -> int:
|
|
35
|
+
if x < 0:
|
|
36
|
+
raise ValueError("Negative number")
|
|
37
|
+
return x * 2
|
|
38
|
+
|
|
39
|
+
@safe(ValueError, TypeError)
|
|
40
|
+
def parse_data(data: dict) -> int:
|
|
41
|
+
return int(data["value"])
|
|
42
|
+
"""
|
|
43
|
+
if (
|
|
44
|
+
len(args) == 1
|
|
45
|
+
and callable(args[0])
|
|
46
|
+
and not (isinstance(args[0], type) and issubclass(args[0], Exception))
|
|
47
|
+
):
|
|
48
|
+
# Called as @safe without parentheses
|
|
49
|
+
func = args[0]
|
|
50
|
+
exceptions = (Exception,)
|
|
51
|
+
|
|
52
|
+
@wraps(func)
|
|
53
|
+
def wrapper(*w_args: Any, **w_kwargs: Any) -> Result[Any, Exception]:
|
|
54
|
+
try:
|
|
55
|
+
return Ok(func(*w_args, **w_kwargs))
|
|
56
|
+
except exceptions as e:
|
|
57
|
+
return Err(e)
|
|
58
|
+
|
|
59
|
+
return wrapper
|
|
60
|
+
|
|
61
|
+
# Called as @safe(ValueError, TypeError)
|
|
62
|
+
exceptions = args if args else (Exception,)
|
|
63
|
+
|
|
64
|
+
def decorator(func: Callable[..., Any]) -> Callable[..., Result[Any, Exception]]:
|
|
65
|
+
@wraps(func)
|
|
66
|
+
def wrapper(*w_args: Any, **w_kwargs: Any) -> Result[Any, Exception]:
|
|
67
|
+
try:
|
|
68
|
+
return Ok(func(*w_args, **w_kwargs))
|
|
69
|
+
except exceptions as e:
|
|
70
|
+
return Err(e)
|
|
71
|
+
|
|
72
|
+
return wrapper
|
|
73
|
+
|
|
74
|
+
return decorator
|