punctional 0.1.1__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.
- punctional-0.1.1/.gitignore +10 -0
- punctional-0.1.1/LICENSE +21 -0
- punctional-0.1.1/PKG-INFO +426 -0
- punctional-0.1.1/README.md +402 -0
- punctional-0.1.1/punctional/__init__.py +61 -0
- punctional-0.1.1/punctional/arithmetic.py +46 -0
- punctional-0.1.1/punctional/comparison.py +36 -0
- punctional-0.1.1/punctional/core.py +92 -0
- punctional-0.1.1/punctional/list_filters.py +40 -0
- punctional-0.1.1/punctional/logical.py +38 -0
- punctional-0.1.1/punctional/monads.py +357 -0
- punctional-0.1.1/punctional/string.py +29 -0
- punctional-0.1.1/punctional/types.py +63 -0
- punctional-0.1.1/pyproject.toml +60 -0
punctional-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Mehdi Yaminli
|
|
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.
|
|
@@ -0,0 +1,426 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: punctional
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: A functional programming framework for Python — composable filters, method chaining, and expressive data pipelines with Option and Result monads.
|
|
5
|
+
Project-URL: Homepage, https://github.com/peghaz/punctional
|
|
6
|
+
Project-URL: Repository, https://github.com/peghaz/punctional
|
|
7
|
+
Project-URL: Documentation, https://github.com/peghaz/punctional#readme
|
|
8
|
+
Project-URL: Issues, https://github.com/peghaz/punctional/issues
|
|
9
|
+
Author-email: Mehdi Peghaz <peghaz@example.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: composition,data-pipeline,filters,functional-programming,method-chaining,monads,option,pipe-operator,result
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
20
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
21
|
+
Classifier: Typing :: Typed
|
|
22
|
+
Requires-Python: >=3.12
|
|
23
|
+
Description-Content-Type: text/markdown
|
|
24
|
+
|
|
25
|
+
# Punctional
|
|
26
|
+
|
|
27
|
+
> Functional programming for Python — monads, composable filters, and expressive data pipelines.
|
|
28
|
+
|
|
29
|
+
[](https://www.python.org/downloads/)
|
|
30
|
+
[](LICENSE)
|
|
31
|
+
|
|
32
|
+
## What is Punctional?
|
|
33
|
+
|
|
34
|
+
**Punctional** brings robust functional programming patterns to Python. It provides:
|
|
35
|
+
|
|
36
|
+
- **Monads** for safe error handling and null-safety (`Option`, `Result`)
|
|
37
|
+
- **Composable filters** that chain with the pipe (`|`) operator
|
|
38
|
+
- **Functional wrappers** for native types enabling method chaining
|
|
39
|
+
|
|
40
|
+
No dependencies. Pure Python. Type-safe.
|
|
41
|
+
|
|
42
|
+
## Installation
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
pip install punctional
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Or from source:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
git clone https://github.com/peghaz/punctional.git
|
|
52
|
+
cd punctional
|
|
53
|
+
pip install -e .
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
---
|
|
57
|
+
|
|
58
|
+
## Core Features
|
|
59
|
+
|
|
60
|
+
### 1. Option Monad — Safe Null Handling
|
|
61
|
+
|
|
62
|
+
The `Option` type represents a value that may or may not exist. Use `Some` to wrap a value and `Nothing` to represent absence — eliminating `None` checks and `AttributeError` exceptions.
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
from punctional import Some, Nothing, some
|
|
66
|
+
|
|
67
|
+
# Wrap existing values
|
|
68
|
+
user_name = Some("Alice")
|
|
69
|
+
print(user_name.map(str.upper)) # Some('ALICE')
|
|
70
|
+
print(user_name.get_or_else("Unknown")) # 'Alice'
|
|
71
|
+
|
|
72
|
+
# Handle missing values safely
|
|
73
|
+
missing = Nothing()
|
|
74
|
+
print(missing.map(str.upper)) # Nothing
|
|
75
|
+
print(missing.get_or_else("Unknown")) # 'Unknown'
|
|
76
|
+
|
|
77
|
+
# Auto-convert from potentially None values
|
|
78
|
+
def find_user(user_id):
|
|
79
|
+
return {"alice": "Alice"}.get(user_id)
|
|
80
|
+
|
|
81
|
+
result = some(find_user("bob")) # Nothing (because get returns None)
|
|
82
|
+
result = some(find_user("alice")) # Some('Alice')
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
**Option Methods:**
|
|
86
|
+
|
|
87
|
+
| Method | Description |
|
|
88
|
+
|--------|-------------|
|
|
89
|
+
| `map(f)` | Transform the value if present |
|
|
90
|
+
| `flat_map(f)` / `bind(f)` | Chain operations that return `Option` |
|
|
91
|
+
| `filter(predicate)` | Return `Nothing` if predicate fails |
|
|
92
|
+
| `get_or_else(default)` | Get value or return default |
|
|
93
|
+
| `get_or_none()` | Get value or `None` |
|
|
94
|
+
| `or_else(alternative)` | Return alternative `Option` if `Nothing` |
|
|
95
|
+
| `is_some()` / `is_nothing()` | Check presence |
|
|
96
|
+
|
|
97
|
+
**Chaining with Option:**
|
|
98
|
+
|
|
99
|
+
```python
|
|
100
|
+
from punctional import Some, Nothing
|
|
101
|
+
|
|
102
|
+
def parse_int(s: str) -> Option[int]:
|
|
103
|
+
try:
|
|
104
|
+
return Some(int(s))
|
|
105
|
+
except ValueError:
|
|
106
|
+
return Nothing()
|
|
107
|
+
|
|
108
|
+
def double(x: int) -> Option[int]:
|
|
109
|
+
return Some(x * 2)
|
|
110
|
+
|
|
111
|
+
# Chain operations safely
|
|
112
|
+
result = Some("42").flat_map(parse_int).flat_map(double) # Some(84)
|
|
113
|
+
result = Some("abc").flat_map(parse_int).flat_map(double) # Nothing
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
### 2. Result Monad — Explicit Error Handling
|
|
119
|
+
|
|
120
|
+
The `Result` type represents either success (`Ok`) or failure (`Error`). Unlike exceptions, errors are explicit in the type signature and must be handled.
|
|
121
|
+
|
|
122
|
+
```python
|
|
123
|
+
from punctional import Ok, Error, try_result
|
|
124
|
+
|
|
125
|
+
# Explicit success and failure
|
|
126
|
+
success = Ok(42)
|
|
127
|
+
failure = Error("Something went wrong")
|
|
128
|
+
|
|
129
|
+
print(success.map(lambda x: x * 2)) # Ok(84)
|
|
130
|
+
print(failure.map(lambda x: x * 2)) # Error('Something went wrong')
|
|
131
|
+
|
|
132
|
+
print(success.get_or_else(0)) # 42
|
|
133
|
+
print(failure.get_or_else(0)) # 0
|
|
134
|
+
```
|
|
135
|
+
|
|
136
|
+
**Wrapping exceptions with `try_result`:**
|
|
137
|
+
|
|
138
|
+
```python
|
|
139
|
+
from punctional import try_result
|
|
140
|
+
|
|
141
|
+
def divide(a, b):
|
|
142
|
+
return a / b
|
|
143
|
+
|
|
144
|
+
result = try_result(lambda: divide(10, 2)) # Ok(5.0)
|
|
145
|
+
result = try_result(lambda: divide(10, 0)) # Error(ZeroDivisionError(...))
|
|
146
|
+
|
|
147
|
+
# With custom error handler
|
|
148
|
+
result = try_result(
|
|
149
|
+
lambda: divide(10, 0),
|
|
150
|
+
error_handler=lambda e: f"Division failed: {e}"
|
|
151
|
+
) # Error('Division failed: division by zero')
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
**Result Methods:**
|
|
155
|
+
|
|
156
|
+
| Method | Description |
|
|
157
|
+
|--------|-------------|
|
|
158
|
+
| `map(f)` | Transform success value |
|
|
159
|
+
| `map_error(f)` | Transform error value |
|
|
160
|
+
| `flat_map(f)` / `bind(f)` | Chain operations returning `Result` |
|
|
161
|
+
| `get_or_else(default)` | Get value or default |
|
|
162
|
+
| `is_ok()` / `is_error()` | Check success/failure |
|
|
163
|
+
| `to_option()` | Convert to `Option` (discards error info) |
|
|
164
|
+
|
|
165
|
+
**Railway-oriented programming:**
|
|
166
|
+
|
|
167
|
+
```python
|
|
168
|
+
from punctional import Result, Ok, Error
|
|
169
|
+
|
|
170
|
+
def validate_age(age: int) -> Result[int, str]:
|
|
171
|
+
if age < 0:
|
|
172
|
+
return Error("Age cannot be negative")
|
|
173
|
+
if age > 150:
|
|
174
|
+
return Error("Age seems unrealistic")
|
|
175
|
+
return Ok(age)
|
|
176
|
+
|
|
177
|
+
def validate_name(name: str) -> Result[str, str]:
|
|
178
|
+
if not name.strip():
|
|
179
|
+
return Error("Name cannot be empty")
|
|
180
|
+
return Ok(name.strip())
|
|
181
|
+
|
|
182
|
+
# Chain validations
|
|
183
|
+
age_result = validate_age(25).map(lambda a: f"Age: {a}") # Ok('Age: 25')
|
|
184
|
+
age_result = validate_age(-5).map(lambda a: f"Age: {a}") # Error('Age cannot be negative')
|
|
185
|
+
```
|
|
186
|
+
|
|
187
|
+
---
|
|
188
|
+
|
|
189
|
+
### 3. Pipe Operator & Filters
|
|
190
|
+
|
|
191
|
+
Chain transformations using the `|` operator for readable, left-to-right data flow.
|
|
192
|
+
|
|
193
|
+
```python
|
|
194
|
+
from punctional import fint, fstr, ffloat, Add, Mult, Sub, Div, ToUpper
|
|
195
|
+
|
|
196
|
+
# Arithmetic chaining
|
|
197
|
+
result = fint(10) | Add(5) | Mult(2) | Sub(3) # (10 + 5) * 2 - 3 = 27
|
|
198
|
+
|
|
199
|
+
# String transformations
|
|
200
|
+
text = fstr("hello") | ToUpper() | Add(" WORLD!") # 'HELLO WORLD!'
|
|
201
|
+
|
|
202
|
+
# Float operations
|
|
203
|
+
value = ffloat(10.0) | Div(4) | Mult(2) # 5.0
|
|
204
|
+
```
|
|
205
|
+
|
|
206
|
+
**Functional Wrappers:**
|
|
207
|
+
|
|
208
|
+
| Function | Type | Description |
|
|
209
|
+
|----------|------|-------------|
|
|
210
|
+
| `fint(x)` | `FunctionalInt` | Enables piping for integers |
|
|
211
|
+
| `ffloat(x)` | `FunctionalFloat` | Enables piping for floats |
|
|
212
|
+
| `fstr(x)` | `FunctionalStr` | Enables piping for strings |
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
### 4. Built-in Filters
|
|
217
|
+
|
|
218
|
+
**Arithmetic:**
|
|
219
|
+
```python
|
|
220
|
+
from punctional import fint, Add, Sub, Mult, Div
|
|
221
|
+
|
|
222
|
+
fint(10) | Add(5) # 15
|
|
223
|
+
fint(10) | Sub(3) # 7
|
|
224
|
+
fint(10) | Mult(2) # 20
|
|
225
|
+
fint(10) | Div(4) # 2.5
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
**Comparison:**
|
|
229
|
+
```python
|
|
230
|
+
from punctional import fint, GreaterThan, LessThan, Equals
|
|
231
|
+
|
|
232
|
+
fint(42) | GreaterThan(10) # True
|
|
233
|
+
fint(5) | LessThan(10) # True
|
|
234
|
+
fint(42) | Equals(42) # True
|
|
235
|
+
```
|
|
236
|
+
|
|
237
|
+
**Logical:**
|
|
238
|
+
```python
|
|
239
|
+
from punctional import fint, AndFilter, OrFilter, NotFilter, GreaterThan, LessThan
|
|
240
|
+
|
|
241
|
+
# All conditions must pass
|
|
242
|
+
fint(42) | AndFilter(GreaterThan(10), LessThan(100)) # True
|
|
243
|
+
|
|
244
|
+
# At least one condition must pass
|
|
245
|
+
fint(5) | OrFilter(LessThan(3), GreaterThan(3)) # True
|
|
246
|
+
|
|
247
|
+
# Negate a condition
|
|
248
|
+
fint(5) | NotFilter(Equals(0)) # True
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
**String:**
|
|
252
|
+
```python
|
|
253
|
+
from punctional import fstr, ToUpper, ToLower, Contains, Mult
|
|
254
|
+
|
|
255
|
+
fstr("hello") | ToUpper() # 'HELLO'
|
|
256
|
+
fstr("WORLD") | ToLower() # 'world'
|
|
257
|
+
fstr("hello world") | Contains("world") # True
|
|
258
|
+
fstr("ha") | Mult(3) # 'hahaha'
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**List Operations:**
|
|
262
|
+
```python
|
|
263
|
+
from punctional import Map, FilterList, Mult, GreaterThan
|
|
264
|
+
|
|
265
|
+
numbers = [1, 2, 3, 4, 5]
|
|
266
|
+
|
|
267
|
+
Map(Mult(2)).apply(numbers) # [2, 4, 6, 8, 10]
|
|
268
|
+
FilterList(GreaterThan(2)).apply(numbers) # [3, 4, 5]
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
---
|
|
272
|
+
|
|
273
|
+
### 5. Composition
|
|
274
|
+
|
|
275
|
+
Create reusable pipelines with `Compose`:
|
|
276
|
+
|
|
277
|
+
```python
|
|
278
|
+
from punctional import Compose, Mult, Add, fint
|
|
279
|
+
|
|
280
|
+
# Define a reusable transformation
|
|
281
|
+
double_then_add_ten = Compose(Mult(2), Add(10))
|
|
282
|
+
|
|
283
|
+
fint(5) | double_then_add_ten # 20 (5 * 2 + 10)
|
|
284
|
+
fint(10) | double_then_add_ten # 30 (10 * 2 + 10)
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
---
|
|
288
|
+
|
|
289
|
+
### 6. Custom Filters
|
|
290
|
+
|
|
291
|
+
Create your own filters by extending the `Filter` base class:
|
|
292
|
+
|
|
293
|
+
```python
|
|
294
|
+
from punctional import Filter, fint
|
|
295
|
+
|
|
296
|
+
class Square(Filter[int, int]):
|
|
297
|
+
def apply(self, value: int) -> int:
|
|
298
|
+
return value ** 2
|
|
299
|
+
|
|
300
|
+
class Power(Filter[int, int]):
|
|
301
|
+
def __init__(self, exponent: int):
|
|
302
|
+
self.exponent = exponent
|
|
303
|
+
|
|
304
|
+
def apply(self, value: int) -> int:
|
|
305
|
+
return value ** self.exponent
|
|
306
|
+
|
|
307
|
+
fint(5) | Square() # 25
|
|
308
|
+
fint(2) | Power(10) # 1024
|
|
309
|
+
```
|
|
310
|
+
|
|
311
|
+
---
|
|
312
|
+
|
|
313
|
+
### 7. Functional Dataclasses
|
|
314
|
+
|
|
315
|
+
Add the `Functional` mixin to any dataclass to enable piping:
|
|
316
|
+
|
|
317
|
+
```python
|
|
318
|
+
from dataclasses import dataclass
|
|
319
|
+
from punctional import Functional, Filter
|
|
320
|
+
|
|
321
|
+
@dataclass
|
|
322
|
+
class Point(Functional):
|
|
323
|
+
x: float
|
|
324
|
+
y: float
|
|
325
|
+
|
|
326
|
+
class Scale(Filter[Point, Point]):
|
|
327
|
+
def __init__(self, factor: float):
|
|
328
|
+
self.factor = factor
|
|
329
|
+
|
|
330
|
+
def apply(self, p: Point) -> Point:
|
|
331
|
+
return Point(p.x * self.factor, p.y * self.factor)
|
|
332
|
+
|
|
333
|
+
class Translate(Filter[Point, Point]):
|
|
334
|
+
def __init__(self, dx: float, dy: float):
|
|
335
|
+
self.dx, self.dy = dx, dy
|
|
336
|
+
|
|
337
|
+
def apply(self, p: Point) -> Point:
|
|
338
|
+
return Point(p.x + self.dx, p.y + self.dy)
|
|
339
|
+
|
|
340
|
+
# Chain transformations on custom types
|
|
341
|
+
point = Point(3, 4)
|
|
342
|
+
result = point | Scale(2) | Translate(10, 10) # Point(16, 18)
|
|
343
|
+
```
|
|
344
|
+
|
|
345
|
+
---
|
|
346
|
+
|
|
347
|
+
## Complete API Reference
|
|
348
|
+
|
|
349
|
+
### Monads
|
|
350
|
+
|
|
351
|
+
| Type | Description |
|
|
352
|
+
|------|-------------|
|
|
353
|
+
| `Option[T]` | Abstract base for optional values |
|
|
354
|
+
| `Some(value)` | Contains a value |
|
|
355
|
+
| `Nothing()` | Represents absence |
|
|
356
|
+
| `some(value)` | Creates `Some` or `Nothing` based on `None` check |
|
|
357
|
+
| `Result[T, E]` | Abstract base for success/failure |
|
|
358
|
+
| `Ok(value)` | Successful result |
|
|
359
|
+
| `Error(error)` | Failed result |
|
|
360
|
+
| `try_result(fn)` | Wraps a function, catching exceptions as `Error` |
|
|
361
|
+
|
|
362
|
+
### Filters
|
|
363
|
+
|
|
364
|
+
| Filter | Input → Output | Description |
|
|
365
|
+
|--------|----------------|-------------|
|
|
366
|
+
| `Add(n)` | `number → number` | Addition |
|
|
367
|
+
| `Sub(n)` | `number → number` | Subtraction |
|
|
368
|
+
| `Mult(n)` | `number → number` | Multiplication |
|
|
369
|
+
| `Div(n)` | `number → number` | Division |
|
|
370
|
+
| `GreaterThan(n)` | `number → bool` | Greater than comparison |
|
|
371
|
+
| `LessThan(n)` | `number → bool` | Less than comparison |
|
|
372
|
+
| `Equals(n)` | `any → bool` | Equality check |
|
|
373
|
+
| `AndFilter(*filters)` | `T → bool` | Logical AND |
|
|
374
|
+
| `OrFilter(*filters)` | `T → bool` | Logical OR |
|
|
375
|
+
| `NotFilter(filter)` | `T → bool` | Logical NOT |
|
|
376
|
+
| `ToUpper()` | `str → str` | Uppercase |
|
|
377
|
+
| `ToLower()` | `str → str` | Lowercase |
|
|
378
|
+
| `Contains(s)` | `str → bool` | Substring check |
|
|
379
|
+
| `Map(filter)` | `list → list` | Apply filter to each element |
|
|
380
|
+
| `FilterList(pred)` | `list → list` | Filter elements by predicate |
|
|
381
|
+
| `Compose(*filters)` | `T → U` | Compose multiple filters |
|
|
382
|
+
|
|
383
|
+
### Core Classes
|
|
384
|
+
|
|
385
|
+
| Class | Description |
|
|
386
|
+
|-------|-------------|
|
|
387
|
+
| `Filter[T, U]` | Abstract base class for all filters |
|
|
388
|
+
| `Functional` | Mixin that enables `\|` operator on any class |
|
|
389
|
+
| `FunctionalInt` | Wrapper for `int` with pipe support |
|
|
390
|
+
| `FunctionalFloat` | Wrapper for `float` with pipe support |
|
|
391
|
+
| `FunctionalStr` | Wrapper for `str` with pipe support |
|
|
392
|
+
|
|
393
|
+
---
|
|
394
|
+
|
|
395
|
+
## Examples
|
|
396
|
+
|
|
397
|
+
Run the included examples:
|
|
398
|
+
|
|
399
|
+
```bash
|
|
400
|
+
python -m examples.basics # Introduction to all features
|
|
401
|
+
python -m examples.extending # Creating custom filters
|
|
402
|
+
python -m examples.data_transformation # Real-world patterns
|
|
403
|
+
python -m examples.quick_reference # Quick lookup cheatsheet
|
|
404
|
+
```
|
|
405
|
+
|
|
406
|
+
---
|
|
407
|
+
|
|
408
|
+
## Design Principles
|
|
409
|
+
|
|
410
|
+
1. **Explicit over implicit** — Errors are values, not exceptions
|
|
411
|
+
2. **Composability** — Small, reusable units that combine easily
|
|
412
|
+
3. **Type safety** — Generics provide IDE support and catch bugs early
|
|
413
|
+
4. **Immutability** — Filters return new values, never mutate
|
|
414
|
+
5. **Zero dependencies** — Pure Python, works everywhere
|
|
415
|
+
|
|
416
|
+
---
|
|
417
|
+
|
|
418
|
+
## License
|
|
419
|
+
|
|
420
|
+
MIT License — see [LICENSE](LICENSE) for details.
|
|
421
|
+
|
|
422
|
+
---
|
|
423
|
+
|
|
424
|
+
<p align="center">
|
|
425
|
+
Made with ❤️ for functional programming in Python
|
|
426
|
+
</p>
|