danom 0.5.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.
danom-0.5.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025 Ed Cuss and any other contributors
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.
danom-0.5.0/PKG-INFO ADDED
@@ -0,0 +1,428 @@
1
+ Metadata-Version: 2.4
2
+ Name: danom
3
+ Version: 0.5.0
4
+ Summary: Functional streams and monads
5
+ Author: ed cuss
6
+ Author-email: ed cuss <edcussmusic@gmail.com>
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: attrs>=25.4.0
10
+ Requires-Python: >=3.12
11
+ Description-Content-Type: text/markdown
12
+
13
+ # danom
14
+
15
+ [![PyPI Downloads](https://static.pepy.tech/personalized-badge/danom?period=total&units=INTERNATIONAL_SYSTEM&left_color=BLACK&right_color=BLUE&left_text=downloads)](https://pepy.tech/projects/danom) ![coverage](./coverage.svg)
16
+
17
+ # API Reference
18
+
19
+ ## Ok
20
+
21
+ Frozen instance of an Ok monad used to wrap successful operations.
22
+
23
+ ### `Ok.and_then`
24
+ ```python
25
+ Ok.and_then(self, func: collections.abc.Callable[[~T], danom._result.Result], **kwargs: dict) -> danom._result.Result
26
+ ```
27
+ Pipe another function that returns a monad.
28
+
29
+ ```python
30
+ >>> Ok(1).and_then(add_one) == Ok(2)
31
+ >>> Ok(1).and_then(raise_err) == Err(error=TypeError())
32
+ ```
33
+
34
+
35
+ ### `Ok.is_ok`
36
+ ```python
37
+ Ok.is_ok(self) -> Literal[True]
38
+ ```
39
+ Returns True if the result type is Ok.
40
+
41
+ ```python
42
+ >>> Ok().is_ok() == True
43
+ ```
44
+
45
+
46
+ ### `Ok.match`
47
+ ```python
48
+ Ok.match(self, if_ok_func: collections.abc.Callable[[~T], danom._result.Result], _if_err_func: collections.abc.Callable[[~T], danom._result.Result]) -> danom._result.Result
49
+ ```
50
+ Map Ok func to Ok and Err func to Err
51
+
52
+ ```python
53
+ >>> Ok(1).match(add_one, mock_get_error_type) == Ok(inner=2)
54
+ >>> Ok("ok").match(double, mock_get_error_type) == Ok(inner='okok')
55
+ >>> Err(error=TypeError()).match(double, mock_get_error_type) == Ok(inner='TypeError')
56
+ ```
57
+
58
+
59
+ ### `Ok.unwrap`
60
+ ```python
61
+ Ok.unwrap(self) -> ~T
62
+ ```
63
+ Unwrap the Ok monad and get the inner value.
64
+
65
+ ```python
66
+ >>> Ok().unwrap() == None
67
+ >>> Ok(1).unwrap() == 1
68
+ >>> Ok("ok").unwrap() == 'ok'
69
+ ```
70
+
71
+
72
+ ## Err
73
+
74
+ Frozen instance of an Err monad used to wrap failed operations.
75
+
76
+ ### `Err.and_then`
77
+ ```python
78
+ Err.and_then(self, _: 'Callable[[T], Result]', **_kwargs: 'dict') -> 'Self'
79
+ ```
80
+ Pipe another function that returns a monad. For Err will return original error.
81
+
82
+ ```python
83
+ >>> Err(error=TypeError()).and_then(add_one) == Err(error=TypeError())
84
+ >>> Err(error=TypeError()).and_then(raise_value_err) == Err(error=TypeError())
85
+ ```
86
+
87
+
88
+ ### `Err.is_ok`
89
+ ```python
90
+ Err.is_ok(self) -> 'Literal[False]'
91
+ ```
92
+ Returns False if the result type is Err.
93
+
94
+ ```python
95
+ Err().is_ok() == False
96
+ ```
97
+
98
+
99
+ ### `Err.match`
100
+ ```python
101
+ Err.match(self, _if_ok_func: 'Callable[[T], Result]', if_err_func: 'Callable[[T], Result]') -> 'Result'
102
+ ```
103
+ Map Ok func to Ok and Err func to Err
104
+
105
+ ```python
106
+ >>> Ok(1).match(add_one, mock_get_error_type) == Ok(inner=2)
107
+ >>> Ok("ok").match(double, mock_get_error_type) == Ok(inner='okok')
108
+ >>> Err(error=TypeError()).match(double, mock_get_error_type) == Ok(inner='TypeError')
109
+ ```
110
+
111
+
112
+ ### `Err.unwrap`
113
+ ```python
114
+ Err.unwrap(self) -> 'None'
115
+ ```
116
+ Unwrap the Err monad will raise the inner error.
117
+
118
+ ```python
119
+ >>> Err(error=TypeError()).unwrap() raise TypeError(...)
120
+ ```
121
+
122
+
123
+ ## Stream
124
+
125
+ A lazy iterator with functional operations.
126
+
127
+ ### `Stream.collect`
128
+ ```python
129
+ Stream.collect(self) -> 'tuple'
130
+ ```
131
+ Materialise the sequence from the `Stream`.
132
+
133
+ ```python
134
+ >>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one)
135
+ >>> stream.collect() == (1, 2, 3, 4)
136
+ ```
137
+
138
+
139
+ ### `Stream.filter`
140
+ ```python
141
+ Stream.filter(self, *fns: 'Callable[[T], bool]') -> 'Self'
142
+ ```
143
+ Filter the stream based on a predicate. Will return a new `Stream` with the modified sequence.
144
+
145
+ ```python
146
+ >>> Stream.from_iterable([0, 1, 2, 3]).filter(lambda x: x % 2 == 0).collect() == (0, 2)
147
+ ```
148
+
149
+ Simple functions can be passed in sequence to compose more complex filters
150
+ ```python
151
+ >>> Stream.from_iterable(range(20)).filter(divisible_by_3, divisible_by_5).collect() == (0, 15)
152
+ ```
153
+
154
+
155
+ ### `Stream.from_iterable`
156
+ ```python
157
+ Stream.from_iterable(it: 'Iterable') -> 'Self'
158
+ ```
159
+ This is the recommended way of creating a `Stream` object.
160
+
161
+ ```python
162
+ >>> Stream.from_iterable([0, 1, 2, 3]).collect() == (0, 1, 2, 3)
163
+ ```
164
+
165
+
166
+ ### `Stream.map`
167
+ ```python
168
+ Stream.map(self, *fns: 'Callable[[T], U]') -> 'Self'
169
+ ```
170
+ Map a function to the elements in the `Stream`. Will return a new `Stream` with the modified sequence.
171
+
172
+ ```python
173
+ >>> Stream.from_iterable([0, 1, 2, 3]).map(add_one).collect() == (1, 2, 3, 4)
174
+ ```
175
+
176
+ This can also be mixed with `safe` functions:
177
+ ```python
178
+ >>> Stream.from_iterable([0, 1, 2, 3]).map(add_one).collect() == (Ok(inner=1), Ok(inner=2), Ok(inner=3), Ok(inner=4))
179
+
180
+ >>> @safe
181
+ ... def two_div_value(x: float) -> float:
182
+ ... return 2 / x
183
+
184
+ >>> Stream.from_iterable([0, 1, 2, 4]).map(two_div_value).collect() == (Err(error=ZeroDivisionError('division by zero')), Ok(inner=2.0), Ok(inner=1.0), Ok(inner=0.5))
185
+ ```
186
+
187
+ Simple functions can be passed in sequence to compose more complex transformations
188
+ ```python
189
+ >>> Stream.from_iterable(range(5)).map(mul_two, add_one).collect() == (1, 3, 5, 7, 9)
190
+ ```
191
+
192
+
193
+ ### `Stream.par_collect`
194
+ ```python
195
+ Stream.par_collect(self, workers: 'int' = 4, *, use_threads: 'bool' = False) -> 'tuple'
196
+ ```
197
+ Materialise the sequence from the `Stream` in parallel.
198
+
199
+ ```python
200
+ >>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one)
201
+ >>> stream.par_collect() == (1, 2, 3, 4)
202
+ ```
203
+
204
+ Use the `workers` arg to select the number of workers to use. Use `-1` to use all available processors (except 1).
205
+ Defaults to `4`.
206
+ ```python
207
+ >>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one)
208
+ >>> stream.par_collect(workers=-1) == (1, 2, 3, 4)
209
+ ```
210
+
211
+ For smaller I/O bound tasks use the `use_threads` flag as True.
212
+ If False the processing will use `ProcessPoolExecutor` else it will use `ThreadPoolExecutor`.
213
+ ```python
214
+ >>> stream = Stream.from_iterable([0, 1, 2, 3]).map(add_one)
215
+ >>> stream.par_collect(use_threads=True) == (1, 2, 3, 4)
216
+ ```
217
+
218
+
219
+ ### `Stream.partition`
220
+ ```python
221
+ Stream.partition(self, fn: 'Callable[[T], bool]', *, workers: 'int' = 1, use_threads: 'bool' = False) -> 'tuple[Self, Self]'
222
+ ```
223
+ Similar to `filter` except splits the True and False values. Will return a two new `Stream` with the partitioned sequences.
224
+
225
+ Each partition is independently replayable.
226
+ ```python
227
+ >>> part1, part2 = Stream.from_iterable([0, 1, 2, 3]).partition(lambda x: x % 2 == 0)
228
+ >>> part1.collect() == (0, 2)
229
+ >>> part2.collect() == (1, 3)
230
+ ```
231
+
232
+ As `partition` triggers an action, the parameters will be forwarded to the `collect` call if the `workers` are greater than 1.
233
+ ```python
234
+ >>> Stream.from_iterable(range(10)).map(add_one, add_one).partition(divisible_by_3, workers=4)
235
+ >>> part1.map(add_one).par_collect() == (4, 7, 10)
236
+ >>> part2.collect() == (2, 4, 5, 7, 8, 10, 11)
237
+ ```
238
+
239
+
240
+ ## safe
241
+
242
+ ### `safe`
243
+ ```python
244
+ safe(func: collections.abc.Callable[~P, ~T]) -> collections.abc.Callable[~P, danom._result.Result]
245
+ ```
246
+ Decorator for functions that wraps the function in a try except returns `Ok` on success else `Err`.
247
+
248
+ ```python
249
+ >>> @safe
250
+ ... def add_one(a: int) -> int:
251
+ ... return a + 1
252
+
253
+ >>> add_one(1) == Ok(inner=2)
254
+ ```
255
+
256
+
257
+ ## safe_method
258
+
259
+ ### `safe_method`
260
+ ```python
261
+ safe_method(func: collections.abc.Callable[~P, ~T]) -> collections.abc.Callable[~P, danom._result.Result]
262
+ ```
263
+ The same as `safe` except it forwards on the `self` of the class instance to the wrapped function.
264
+
265
+ ```python
266
+ >>> class Adder:
267
+ ... def __init__(self, result: int = 0) -> None:
268
+ ... self.result = result
269
+ ...
270
+ ... @safe_method
271
+ ... def add_one(self, a: int) -> int:
272
+ ... return self.result + 1
273
+
274
+ >>> Adder.add_one(1) == Ok(inner=1)
275
+ ```
276
+
277
+
278
+ ## compose
279
+
280
+ ### `compose`
281
+ ```python
282
+ compose(*fns: collections.abc.Callable[[T], U]) -> collections.abc.Callable[[T], U]
283
+ ```
284
+ Compose multiple functions into one.
285
+
286
+ The functions will be called in sequence with the result of one being used as the input for the next.
287
+
288
+ ```python
289
+ >>> add_two = compose(add_one, add_one)
290
+ >>> add_two(0) == 2
291
+ ```
292
+
293
+ ```python
294
+ >>> add_two = compose(add_one, add_one, is_even)
295
+ >>> add_two(0) == True
296
+ ```
297
+
298
+
299
+ ## all_of
300
+
301
+ ### `all_of`
302
+ ```python
303
+ all_of(*fns: collections.abc.Callable[[T], bool]) -> collections.abc.Callable[[T], bool]
304
+ ```
305
+ True if all of the given functions return True.
306
+
307
+ ```python
308
+ >>> is_valid_user = all_of(is_subscribed, is_active, has_2fa)
309
+ >>> is_valid_user(user) == True
310
+ ```
311
+
312
+
313
+ ## any_of
314
+
315
+ ### `any_of`
316
+ ```python
317
+ any_of(*fns: collections.abc.Callable[[T], bool]) -> collections.abc.Callable[[T], bool]
318
+ ```
319
+ True if any of the given functions return True.
320
+
321
+ ```python
322
+ >>> is_eligible = any_of(has_coupon, is_vip, is_staff)
323
+ >>> is_eligible(user) == True
324
+ ```
325
+
326
+
327
+ ## identity
328
+
329
+ ### `identity`
330
+ ```python
331
+ identity(x: T) -> T
332
+ ```
333
+ Basic identity function.
334
+
335
+ ```python
336
+ >>> identity("abc") == "abc"
337
+ >>> identity(1) == 1
338
+ >>> identity(ComplexDataType(a=1, b=2, c=3)) == ComplexDataType(a=1, b=2, c=3)
339
+ ```
340
+
341
+
342
+ ## invert
343
+
344
+ ### `invert`
345
+ ```python
346
+ invert(func: collections.abc.Callable[~P, bool]) -> collections.abc.Callable[~P, bool]
347
+ ```
348
+ Invert a boolean function so it returns False where it would've returned True.
349
+
350
+ ```python
351
+ >>> invert(has_len)("abc") == False
352
+ >>> invert(has_len)("") == True
353
+ ```
354
+
355
+
356
+ ## new_type
357
+
358
+ ### `new_type`
359
+ ```python
360
+ new_type(name: 'str', base_type: 'type', validators: 'Callable | Sequence[Callable] | None' = None, converters: 'Callable | Sequence[Callable] | None' = None, *, frozen: 'bool' = True)
361
+ ```
362
+ Create a NewType based on another type.
363
+
364
+ ```python
365
+ >>> def is_positive(value):
366
+ ... return value >= 0
367
+
368
+ >>> ValidBalance = new_type("ValidBalance", float, validators=[is_positive])
369
+ >>> ValidBalance("20") == ValidBalance(inner=20.0)
370
+ ```
371
+
372
+ Unlike an inherited class, the type will not return `True` for an isinstance check.
373
+ ```python
374
+ >>> isinstance(ValidBalance(20.0), ValidBalance) == True
375
+ >>> isinstance(ValidBalance(20.0), float) == False
376
+ ```
377
+
378
+ The methods of the given `base_type` will be forwarded to the specialised type.
379
+ Alternatively the map method can be used to return a new type instance with the transformation.
380
+ ```python
381
+ >>> def has_len(email: str) -> bool:
382
+ ... return len(email) > 0
383
+
384
+ >>> Email = new_type("Email", str, validators=[has_len])
385
+ >>> Email("some_email@domain.com").upper() == "SOME_EMAIL@DOMAIN.COM"
386
+ >>> Email("some_email@domain.com").map(str.upper) == Email(inner='SOME_EMAIL@DOMAIN.COM')
387
+ ```
388
+
389
+ ::
390
+
391
+ # Repo map
392
+ ```
393
+ ├── .github
394
+ │ └── workflows
395
+ │ ├── ci_tests.yaml
396
+ │ └── publish.yaml
397
+ ├── dev_tools
398
+ │ ├── __init__.py
399
+ │ ├── update_cov.py
400
+ │ └── update_readme.py
401
+ ├── src
402
+ │ └── danom
403
+ │ ├── __init__.py
404
+ │ ├── _err.py
405
+ │ ├── _new_type.py
406
+ │ ├── _ok.py
407
+ │ ├── _result.py
408
+ │ ├── _safe.py
409
+ │ ├── _stream.py
410
+ │ └── _utils.py
411
+ ├── tests
412
+ │ ├── __init__.py
413
+ │ ├── conftest.py
414
+ │ ├── test_api.py
415
+ │ ├── test_err.py
416
+ │ ├── test_new_type.py
417
+ │ ├── test_ok.py
418
+ │ ├── test_result.py
419
+ │ ├── test_safe.py
420
+ │ ├── test_stream.py
421
+ │ └── test_utils.py
422
+ ├── .pre-commit-config.yaml
423
+ ├── README.md
424
+ ├── pyproject.toml
425
+ ├── ruff.toml
426
+ └── uv.lock
427
+ ::
428
+ ```