punctional 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,61 @@
1
+ """
2
+ Punctional - A functional programming framework for Python.
3
+
4
+ Provides composable filters and method chaining for native types and custom dataclasses.
5
+ """
6
+
7
+ from .core import Filter, Functional, Compose
8
+ from .types import FunctionalInt, FunctionalFloat, FunctionalStr, fint, ffloat, fstr
9
+ from .arithmetic import Add, Mult, Sub, Div
10
+ from .logical import AndFilter, OrFilter, NotFilter
11
+ from .comparison import GreaterThan, LessThan, Equals
12
+ from .string import ToUpper, ToLower, Contains
13
+ from .list_filters import Map, FilterList, Reduce
14
+ from .monads import Option, Some, Nothing, Result, Ok, Error, some, try_result
15
+
16
+ __version__ = "0.1.0"
17
+
18
+ __all__ = [
19
+ # Core
20
+ "Filter",
21
+ "Functional",
22
+ # Types
23
+ "FunctionalInt",
24
+ "FunctionalFloat",
25
+ "FunctionalStr",
26
+ "fint",
27
+ "ffloat",
28
+ "fstr",
29
+ # Arithmetic
30
+ "Add",
31
+ "Mult",
32
+ "Sub",
33
+ "Div",
34
+ # Logical
35
+ "AndFilter",
36
+ "OrFilter",
37
+ "NotFilter",
38
+ # Comparison
39
+ "GreaterThan",
40
+ "LessThan",
41
+ "Equals",
42
+ # String
43
+ "ToUpper",
44
+ "ToLower",
45
+ "Contains",
46
+ # Composition
47
+ "Compose",
48
+ # List
49
+ "Map",
50
+ "FilterList",
51
+ "Reduce",
52
+ # Monads
53
+ "Option",
54
+ "Some",
55
+ "Nothing",
56
+ "Result",
57
+ "Ok",
58
+ "Error",
59
+ "some",
60
+ "try_result",
61
+ ]
@@ -0,0 +1,46 @@
1
+ """
2
+ Arithmetic operation filters (Add, Mult, Sub, Div).
3
+ """
4
+
5
+ from typing import Any
6
+ from .core import Filter
7
+
8
+
9
+ class Add(Filter[Any, Any]):
10
+ """Addition filter - adds a value."""
11
+
12
+ def __init__(self, addend: Any):
13
+ self.addend = addend
14
+
15
+ def apply(self, value: Any) -> Any:
16
+ return value + self.addend
17
+
18
+
19
+ class Mult(Filter[Any, Any]):
20
+ """Multiplication filter - multiplies by a value."""
21
+
22
+ def __init__(self, multiplier: Any):
23
+ self.multiplier = multiplier
24
+
25
+ def apply(self, value: Any) -> Any:
26
+ return value * self.multiplier
27
+
28
+
29
+ class Sub(Filter[Any, Any]):
30
+ """Subtraction filter - subtracts a value."""
31
+
32
+ def __init__(self, subtrahend: Any):
33
+ self.subtrahend = subtrahend
34
+
35
+ def apply(self, value: Any) -> Any:
36
+ return value - self.subtrahend
37
+
38
+
39
+ class Div(Filter[Any, Any]):
40
+ """Division filter - divides by a value."""
41
+
42
+ def __init__(self, divisor: Any):
43
+ self.divisor = divisor
44
+
45
+ def apply(self, value: Any) -> Any:
46
+ return value / self.divisor
@@ -0,0 +1,36 @@
1
+ """
2
+ Comparison filters (GreaterThan, LessThan, Equals).
3
+ """
4
+
5
+ from typing import Any
6
+ from .core import Filter
7
+
8
+
9
+ class GreaterThan(Filter[Any, bool]):
10
+ """Check if value is greater than threshold."""
11
+
12
+ def __init__(self, threshold: Any):
13
+ self.threshold = threshold
14
+
15
+ def apply(self, value: Any) -> bool:
16
+ return value > self.threshold
17
+
18
+
19
+ class LessThan(Filter[Any, bool]):
20
+ """Check if value is less than threshold."""
21
+
22
+ def __init__(self, threshold: Any):
23
+ self.threshold = threshold
24
+
25
+ def apply(self, value: Any) -> bool:
26
+ return value < self.threshold
27
+
28
+
29
+ class Equals(Filter[Any, bool]):
30
+ """Check if value equals another value."""
31
+
32
+ def __init__(self, target: Any):
33
+ self.target = target
34
+
35
+ def apply(self, value: Any) -> bool:
36
+ return value == self.target
punctional/core.py ADDED
@@ -0,0 +1,92 @@
1
+ """
2
+ Core abstractions for the Punctional functional programming framework.
3
+ """
4
+
5
+ from __future__ import annotations
6
+ from abc import ABC, abstractmethod
7
+ from typing import Any, TypeVar, Generic
8
+
9
+ T = TypeVar('T')
10
+ U = TypeVar('U')
11
+
12
+
13
+ class Filter(ABC, Generic[T, U]):
14
+ """
15
+ Abstract base class for all functional filters.
16
+ A filter transforms input of type T to output of type U.
17
+ """
18
+
19
+ @abstractmethod
20
+ def apply(self, value: T) -> U:
21
+ """Apply the filter to a value."""
22
+ pass
23
+
24
+ def __call__(self, value: T) -> U:
25
+ """Allow filters to be called as functions."""
26
+ return self.apply(value)
27
+
28
+ def __or__(self, other: Filter) -> Compose:
29
+ """Enable pipe operator for filter composition: filter1 | filter2"""
30
+ # Import here to avoid circular dependency at module level
31
+ # Compose is defined below in this same module
32
+ return Compose(self, other)
33
+
34
+ def __and__(self, other: Filter) -> 'AndFilter':
35
+ """Enable & operator for logical AND"""
36
+ return AndFilter(self, other)
37
+
38
+
39
+ class AndFilter(Filter[T, bool]):
40
+ """Logical AND filter - applies multiple filters and returns True if all are True."""
41
+
42
+ def __init__(self, *filters: Filter):
43
+ self.filters = filters
44
+
45
+ def apply(self, value: T) -> bool:
46
+ return all(f.apply(value) for f in self.filters)
47
+
48
+
49
+ class Compose(Filter[T, Any]):
50
+ """Compose multiple filters into a pipeline."""
51
+
52
+ def __init__(self, *filters: Filter):
53
+ self.filters = filters
54
+
55
+ def apply(self, value: T) -> Any:
56
+ result = value
57
+ for filter in self.filters:
58
+ result = filter.apply(result)
59
+ return result
60
+
61
+
62
+ class Functional:
63
+ """
64
+ Mixin class that adds functional programming capabilities to any class.
65
+ Inherit from this to enable method chaining and filter application.
66
+ """
67
+
68
+ def pipe(self, filter: Filter) -> Any:
69
+ """Apply a filter to this object."""
70
+ result = filter.apply(self)
71
+ return self._wrap_result(result)
72
+
73
+ def __or__(self, filter: Filter) -> Any:
74
+ """Enable pipe operator: obj | filter"""
75
+ result = filter.apply(self)
76
+ return self._wrap_result(result)
77
+
78
+ @staticmethod
79
+ def _wrap_result(result: Any) -> Any:
80
+ """Wrap basic types in functional wrappers for continued chaining."""
81
+ if isinstance(result, Functional):
82
+ return result
83
+ elif isinstance(result, int) and not isinstance(result, bool):
84
+ from .types import FunctionalInt
85
+ return FunctionalInt(result)
86
+ elif isinstance(result, float):
87
+ from .types import FunctionalFloat
88
+ return FunctionalFloat(result)
89
+ elif isinstance(result, str):
90
+ from .types import FunctionalStr
91
+ return FunctionalStr(result)
92
+ return result
@@ -0,0 +1,40 @@
1
+ """
2
+ Filters for List operations (Map, FilterList, Reduce).
3
+ """
4
+
5
+ from typing import Any, List
6
+ from functools import reduce
7
+ from .core import Filter
8
+
9
+
10
+ class Map(Filter[List, List]):
11
+ """Map a filter over a List."""
12
+
13
+ def __init__(self, filter: Filter):
14
+ self.filter = filter
15
+
16
+ def apply(self, values: List) -> List:
17
+ return [self.filter.apply(v) for v in values]
18
+
19
+
20
+ class FilterList(Filter[List, List]):
21
+ """Filter a List based on a predicate."""
22
+
23
+ def __init__(self, predicate: Filter[Any, bool]):
24
+ self.predicate = predicate
25
+
26
+ def apply(self, values: List) -> List:
27
+ return [v for v in values if self.predicate.apply(v)]
28
+
29
+
30
+ class Reduce(Filter[List, Any]):
31
+ """Reduce a List to a single value using a filter."""
32
+
33
+ def __init__(self, filter: Filter, initial: Any = None):
34
+ self.filter = filter
35
+ self.initial = initial
36
+
37
+ def apply(self, values: List) -> Any:
38
+ if self.initial is not None:
39
+ return reduce(lambda acc, x: self.filter.apply((acc, x)), values, self.initial)
40
+ return reduce(lambda acc, x: self.filter.apply((acc, x)), values)
punctional/logical.py ADDED
@@ -0,0 +1,38 @@
1
+ """
2
+ Logical operation filters (And, Or, Not).
3
+ """
4
+
5
+ from typing import TypeVar
6
+ from .core import Filter
7
+
8
+ T = TypeVar('T')
9
+
10
+
11
+ class AndFilter(Filter[T, bool]):
12
+ """Logical AND filter - applies multiple filters and returns True if all are True."""
13
+
14
+ def __init__(self, *filters: Filter):
15
+ self.filters = filters
16
+
17
+ def apply(self, value: T) -> bool:
18
+ return all(f.apply(value) for f in self.filters)
19
+
20
+
21
+ class OrFilter(Filter[T, bool]):
22
+ """Logical OR filter - applies multiple filters and returns True if any is True."""
23
+
24
+ def __init__(self, *filters: Filter):
25
+ self.filters = filters
26
+
27
+ def apply(self, value: T) -> bool:
28
+ return any(f.apply(value) for f in self.filters)
29
+
30
+
31
+ class NotFilter(Filter[T, bool]):
32
+ """Logical NOT filter - negates the result of another filter."""
33
+
34
+ def __init__(self, filter: Filter[T, bool]):
35
+ self.filter = filter
36
+
37
+ def apply(self, value: T) -> bool:
38
+ return not self.filter.apply(value)
punctional/monads.py ADDED
@@ -0,0 +1,357 @@
1
+ """
2
+ Monad implementations for functional programming patterns.
3
+
4
+ Provides Option (Some/Nothing) and Result (Ok/Error) monads with standard
5
+ monadic operations like map, bind, and flat_map.
6
+ """
7
+
8
+ from __future__ import annotations
9
+ from abc import ABC, abstractmethod
10
+ from typing import TypeVar, Generic, Callable, Any, Union, Optional
11
+ from dataclasses import dataclass
12
+
13
+ T = TypeVar('T')
14
+ U = TypeVar('U')
15
+ E = TypeVar('E')
16
+
17
+
18
+ class Option(ABC, Generic[T]):
19
+ """
20
+ Option monad representing the presence (Some) or absence (Nothing) of a value.
21
+
22
+ Useful for handling nullable values without explicit None checks.
23
+ """
24
+
25
+ @abstractmethod
26
+ def map(self, f: Callable[[T], U]) -> Option[U]:
27
+ """Transform the contained value if present."""
28
+ pass
29
+
30
+ @abstractmethod
31
+ def flat_map(self, f: Callable[[T], Option[U]]) -> Option[U]:
32
+ """Chain operations that return Options (also called bind or >>=)."""
33
+ pass
34
+
35
+ @abstractmethod
36
+ def bind(self, f: Callable[[T], Option[U]]) -> Option[U]:
37
+ """Alias for flat_map (monadic bind operation)."""
38
+ pass
39
+
40
+ @abstractmethod
41
+ def get_or_else(self, default: T) -> T:
42
+ """Get the value or return a default."""
43
+ pass
44
+
45
+ @abstractmethod
46
+ def get_or_none(self) -> Optional[T]:
47
+ """Get the value or return None."""
48
+ pass
49
+
50
+ @abstractmethod
51
+ def is_some(self) -> bool:
52
+ """Check if this is Some."""
53
+ pass
54
+
55
+ @abstractmethod
56
+ def is_nothing(self) -> bool:
57
+ """Check if this is Nothing."""
58
+ pass
59
+
60
+ @abstractmethod
61
+ def filter(self, predicate: Callable[[T], bool]) -> Option[T]:
62
+ """Return Nothing if predicate is False, otherwise return self."""
63
+ pass
64
+
65
+ @abstractmethod
66
+ def or_else(self, alternative: Option[T]) -> Option[T]:
67
+ """Return self if Some, otherwise return alternative."""
68
+ pass
69
+
70
+ def __bool__(self) -> bool:
71
+ """Enable truthiness checks."""
72
+ return self.is_some()
73
+
74
+
75
+ @dataclass(frozen=True)
76
+ class Some(Option[T]):
77
+ """Represents the presence of a value."""
78
+
79
+ value: T
80
+
81
+ def map(self, f: Callable[[T], U]) -> Option[U]:
82
+ """Apply function to the contained value."""
83
+ return Some(f(self.value))
84
+
85
+ def flat_map(self, f: Callable[[T], Option[U]]) -> Option[U]:
86
+ """Apply function that returns an Option."""
87
+ return f(self.value)
88
+
89
+ def bind(self, f: Callable[[T], Option[U]]) -> Option[U]:
90
+ """Monadic bind (alias for flat_map)."""
91
+ return self.flat_map(f)
92
+
93
+ def get_or_else(self, default: T) -> T:
94
+ """Return the contained value."""
95
+ return self.value
96
+
97
+ def get_or_none(self) -> Optional[T]:
98
+ """Return the contained value."""
99
+ return self.value
100
+
101
+ def is_some(self) -> bool:
102
+ """Always True for Some."""
103
+ return True
104
+
105
+ def is_nothing(self) -> bool:
106
+ """Always False for Some."""
107
+ return False
108
+
109
+ def filter(self, predicate: Callable[[T], bool]) -> Option[T]:
110
+ """Return self if predicate passes, otherwise Nothing."""
111
+ return self if predicate(self.value) else Nothing()
112
+
113
+ def or_else(self, alternative: Option[T]) -> Option[T]:
114
+ """Return self since we have a value."""
115
+ return self
116
+
117
+ def __repr__(self) -> str:
118
+ return f"Some({self.value!r})"
119
+
120
+
121
+ class NothingType(Option[T]):
122
+ """Represents the absence of a value (singleton pattern)."""
123
+
124
+ _instance = None
125
+
126
+ def __new__(cls):
127
+ if cls._instance is None:
128
+ cls._instance = super().__new__(cls)
129
+ return cls._instance
130
+
131
+ def map(self, f: Callable[[T], U]) -> Option[U]:
132
+ """Nothing remains Nothing."""
133
+ return self
134
+
135
+ def flat_map(self, f: Callable[[T], Option[U]]) -> Option[U]:
136
+ """Nothing remains Nothing."""
137
+ return self
138
+
139
+ def bind(self, f: Callable[[T], Option[U]]) -> Option[U]:
140
+ """Nothing remains Nothing."""
141
+ return self
142
+
143
+ def get_or_else(self, default: T) -> T:
144
+ """Return the default value."""
145
+ return default
146
+
147
+ def get_or_none(self) -> Optional[T]:
148
+ """Return None."""
149
+ return None
150
+
151
+ def is_some(self) -> bool:
152
+ """Always False for Nothing."""
153
+ return False
154
+
155
+ def is_nothing(self) -> bool:
156
+ """Always True for Nothing."""
157
+ return True
158
+
159
+ def filter(self, predicate: Callable[[T], bool]) -> Option[T]:
160
+ """Nothing remains Nothing."""
161
+ return self
162
+
163
+ def or_else(self, alternative: Option[T]) -> Option[T]:
164
+ """Return the alternative since we have no value."""
165
+ return alternative
166
+
167
+ def __repr__(self) -> str:
168
+ return "Nothing"
169
+
170
+ def __bool__(self) -> bool:
171
+ """Nothing is falsy."""
172
+ return False
173
+
174
+
175
+ # Singleton instance
176
+ Nothing = NothingType
177
+
178
+
179
+ def some(value: Optional[T], allow_none: bool = False) -> Option[T]:
180
+ """
181
+ Create an Option from a potentially None value.
182
+
183
+ Args:
184
+ value: The value to wrap
185
+ allow_none: If True, wrap None as Some(None). If False, return Nothing for None.
186
+
187
+ Returns:
188
+ Some(value) if value is not None or allow_none is True
189
+ Nothing if value is None and allow_none is False
190
+
191
+ Examples:
192
+ >>> some(5)
193
+ Some(5)
194
+ >>> some(None)
195
+ Nothing
196
+ >>> some(None, allow_none=True)
197
+ Some(None)
198
+ """
199
+ if value is None and not allow_none:
200
+ return Nothing()
201
+ return Some(value)
202
+
203
+
204
+ class Result(ABC, Generic[T, E]):
205
+ """
206
+ Result monad representing either a success (Ok) or failure (Error).
207
+
208
+ Useful for operations that can fail with an error value.
209
+ """
210
+
211
+ @abstractmethod
212
+ def map(self, f: Callable[[T], U]) -> Result[U, E]:
213
+ """Transform the success value if present."""
214
+ pass
215
+
216
+ @abstractmethod
217
+ def map_error(self, f: Callable[[E], U]) -> Result[T, U]:
218
+ """Transform the error value if present."""
219
+ pass
220
+
221
+ @abstractmethod
222
+ def flat_map(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
223
+ """Chain operations that return Results."""
224
+ pass
225
+
226
+ @abstractmethod
227
+ def bind(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
228
+ """Alias for flat_map (monadic bind)."""
229
+ pass
230
+
231
+ @abstractmethod
232
+ def get_or_else(self, default: T) -> T:
233
+ """Get the success value or return a default."""
234
+ pass
235
+
236
+ @abstractmethod
237
+ def is_ok(self) -> bool:
238
+ """Check if this is Ok."""
239
+ pass
240
+
241
+ @abstractmethod
242
+ def is_error(self) -> bool:
243
+ """Check if this is Error."""
244
+ pass
245
+
246
+ @abstractmethod
247
+ def to_option(self) -> Option[T]:
248
+ """Convert to Option, discarding error information."""
249
+ pass
250
+
251
+ def __bool__(self) -> bool:
252
+ """Enable truthiness checks (Ok is truthy, Error is falsy)."""
253
+ return self.is_ok()
254
+
255
+
256
+ @dataclass(frozen=True)
257
+ class Ok(Result[T, E]):
258
+ """Represents a successful result."""
259
+
260
+ value: T
261
+
262
+ def map(self, f: Callable[[T], U]) -> Result[U, E]:
263
+ """Apply function to the success value."""
264
+ return Ok(f(self.value))
265
+
266
+ def map_error(self, f: Callable[[E], U]) -> Result[T, U]:
267
+ """Leave Ok unchanged."""
268
+ return self
269
+
270
+ def flat_map(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
271
+ """Apply function that returns a Result."""
272
+ return f(self.value)
273
+
274
+ def bind(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
275
+ """Monadic bind (alias for flat_map)."""
276
+ return self.flat_map(f)
277
+
278
+ def get_or_else(self, default: T) -> T:
279
+ """Return the success value."""
280
+ return self.value
281
+
282
+ def is_ok(self) -> bool:
283
+ """Always True for Ok."""
284
+ return True
285
+
286
+ def is_error(self) -> bool:
287
+ """Always False for Ok."""
288
+ return False
289
+
290
+ def to_option(self) -> Option[T]:
291
+ """Convert to Some with the value."""
292
+ return Some(self.value)
293
+
294
+ def __repr__(self) -> str:
295
+ return f"Ok({self.value!r})"
296
+
297
+
298
+ @dataclass(frozen=True)
299
+ class Error(Result[T, E]):
300
+ """Represents a failed result with an error value."""
301
+
302
+ error: E
303
+
304
+ def map(self, f: Callable[[T], U]) -> Result[U, E]:
305
+ """Error remains Error."""
306
+ return self
307
+
308
+ def map_error(self, f: Callable[[E], U]) -> Result[T, U]:
309
+ """Apply function to the error value."""
310
+ return Error(f(self.error))
311
+
312
+ def flat_map(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
313
+ """Error remains Error."""
314
+ return self
315
+
316
+ def bind(self, f: Callable[[T], Result[U, E]]) -> Result[U, E]:
317
+ """Error remains Error."""
318
+ return self
319
+
320
+ def get_or_else(self, default: T) -> T:
321
+ """Return the default value."""
322
+ return default
323
+
324
+ def is_ok(self) -> bool:
325
+ """Always False for Error."""
326
+ return False
327
+
328
+ def is_error(self) -> bool:
329
+ """Always True for Error."""
330
+ return True
331
+
332
+ def to_option(self) -> Option[T]:
333
+ """Convert to Nothing."""
334
+ return Nothing()
335
+
336
+ def __repr__(self) -> str:
337
+ return f"Error({self.error!r})"
338
+
339
+
340
+ # Utility functions for creating Results
341
+ def try_result(f: Callable[[], T], error_handler: Optional[Callable[[Exception], E]] = None) -> Result[T, Union[E, Exception]]:
342
+ """
343
+ Execute a function and wrap the result in Ok or Error.
344
+
345
+ Args:
346
+ f: Function to execute
347
+ error_handler: Optional function to transform exceptions into error values
348
+
349
+ Returns:
350
+ Ok(result) if successful, Error(exception) or Error(error_handler(exception)) if failed
351
+ """
352
+ try:
353
+ return Ok(f())
354
+ except Exception as e:
355
+ if error_handler:
356
+ return Error(error_handler(e))
357
+ return Error(e)
punctional/string.py ADDED
@@ -0,0 +1,29 @@
1
+ """
2
+ String manipulation filters.
3
+ """
4
+
5
+ from .core import Filter
6
+
7
+
8
+ class ToUpper(Filter[str, str]):
9
+ """Convert string to uppercase."""
10
+
11
+ def apply(self, value: str) -> str:
12
+ return value.upper()
13
+
14
+
15
+ class ToLower(Filter[str, str]):
16
+ """Convert string to lowercase."""
17
+
18
+ def apply(self, value: str) -> str:
19
+ return value.lower()
20
+
21
+
22
+ class Contains(Filter[str, bool]):
23
+ """Check if string contains substring."""
24
+
25
+ def __init__(self, substring: str):
26
+ self.substring = substring
27
+
28
+ def apply(self, value: str) -> bool:
29
+ return self.substring in value
punctional/types.py ADDED
@@ -0,0 +1,63 @@
1
+ """
2
+ Functional wrapper types for Python native types (int, float, str).
3
+ """
4
+
5
+ from .core import Functional, Filter
6
+
7
+
8
+ class FunctionalInt(int, Functional):
9
+ """Int with functional programming capabilities."""
10
+
11
+ def __or__(self, other):
12
+ """Override to support Filter piping, fall back to bitwise OR for ints."""
13
+ if isinstance(other, Filter):
14
+ result = other.apply(self)
15
+ return self._wrap_result(result)
16
+ return int.__or__(self, other)
17
+
18
+ def __new__(cls, value):
19
+ return int.__new__(cls, value)
20
+
21
+
22
+ class FunctionalFloat(float, Functional):
23
+ """Float with functional programming capabilities."""
24
+
25
+ def __or__(self, other):
26
+ """Support Filter piping."""
27
+ if isinstance(other, Filter):
28
+ result = other.apply(self)
29
+ return self._wrap_result(result)
30
+ return NotImplemented
31
+
32
+ def __new__(cls, value):
33
+ return float.__new__(cls, value)
34
+
35
+
36
+ class FunctionalStr(str, Functional):
37
+ """String with functional programming capabilities."""
38
+
39
+ def __or__(self, other):
40
+ """Support Filter piping."""
41
+ if isinstance(other, Filter):
42
+ result = other.apply(self)
43
+ return self._wrap_result(result)
44
+ return NotImplemented
45
+
46
+ def __new__(cls, value):
47
+ return str.__new__(cls, value)
48
+
49
+
50
+ # Convenience constructors
51
+ def fint(value: int) -> FunctionalInt:
52
+ """Create a functional integer."""
53
+ return FunctionalInt(value)
54
+
55
+
56
+ def ffloat(value: float) -> FunctionalFloat:
57
+ """Create a functional float."""
58
+ return FunctionalFloat(value)
59
+
60
+
61
+ def fstr(value: str) -> FunctionalStr:
62
+ """Create a functional string."""
63
+ return FunctionalStr(value)
@@ -0,0 +1,445 @@
1
+ Metadata-Version: 2.4
2
+ Name: punctional
3
+ Version: 0.1.0
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
+ > A functional programming framework for Python โ€” enabling composable filters, method chaining, and expressive data pipelines.
28
+
29
+ [![Python 3.12+](https://img.shields.io/badge/python-3.12+-blue.svg)](https://www.python.org/downloads/)
30
+ [![License: MIT](https://img.shields.io/badge/License-MIT-green.svg)](LICENSE)
31
+
32
+ ## ๐ŸŽฏ What is Punctional?
33
+
34
+ **Punctional** is a lightweight functional programming library that brings powerful functional programming patterns to Python. It allows you to compose operations using an intuitive pipe (`|`) operator, create reusable transformation filters, and apply common functional design patterns like Option and Result monads.
35
+
36
+ Whether you're building data pipelines, validation logic, or just want cleaner, more declarative code โ€” Punctional provides the building blocks.
37
+
38
+ ## โœจ Key Features
39
+
40
+ - **๐Ÿ”— Pipe Operator Chaining** โ€” Chain transformations using the intuitive `|` operator
41
+ - **๐Ÿงฑ Composable Filters** โ€” Create reusable, testable transformation units
42
+ - **๐Ÿ“ฆ Functional Wrappers** โ€” Wrap native types (`int`, `float`, `str`) for functional operations
43
+ - **๐ŸŽญ Monads** โ€” Built-in `Option` (Some/Nothing) and `Result` (Ok/Error) monads
44
+ - **๐Ÿ”ง Extensible** โ€” Easily create custom filters for your domain
45
+ - **๐Ÿ“‹ Dataclass Support** โ€” Make any dataclass functional with the `Functional` mixin
46
+
47
+ ## ๐Ÿ“ฆ Installation
48
+
49
+ ```bash
50
+ pip install punctional
51
+ ```
52
+
53
+ Or install from source:
54
+
55
+ ```bash
56
+ git clone https://github.com/peghaz/punctional.git
57
+ cd punctional
58
+ pip install -e .
59
+ ```
60
+
61
+ ## ๐Ÿš€ Quick Start
62
+
63
+ ```python
64
+ from punctional import fint, fstr, Add, Mult, ToUpper, GreaterThan, AndFilter, LessThan
65
+
66
+ # Arithmetic chaining
67
+ result = fint(10) | Add(5) | Mult(2) # (10 + 5) * 2 = 30
68
+
69
+ # String transformations
70
+ text = fstr("hello") | ToUpper() | Add("!") # "HELLO!"
71
+
72
+ # Logical validation
73
+ is_valid = fint(42) | AndFilter(GreaterThan(10), LessThan(100)) # True
74
+ ```
75
+
76
+ ## ๐Ÿ“š Core Concepts
77
+
78
+ ### Filters
79
+
80
+ A **Filter** is the fundamental building block โ€” a transformation that takes an input and produces an output:
81
+
82
+ ```python
83
+ from punctional import Filter, fint
84
+
85
+ class Square(Filter[int, int]):
86
+ def apply(self, value: int) -> int:
87
+ return value ** 2
88
+
89
+ # Use it
90
+ result = fint(5) | Square() # 25
91
+ ```
92
+
93
+ ### Functional Wrappers
94
+
95
+ Wrap native Python types to enable pipe operations:
96
+
97
+ | Function | Type | Description |
98
+ |----------|------|-------------|
99
+ | `fint(x)` | `FunctionalInt` | Functional integer wrapper |
100
+ | `ffloat(x)` | `FunctionalFloat` | Functional float wrapper |
101
+ | `fstr(x)` | `FunctionalStr` | Functional string wrapper |
102
+
103
+ ### The Pipe Operator
104
+
105
+ Chain filters using the `|` operator for readable, left-to-right transformations:
106
+
107
+ ```python
108
+ # Instead of nested calls:
109
+ result = Div(4).apply(Sub(3).apply(Mult(2).apply(Add(5).apply(10))))
110
+
111
+ # Write declaratively:
112
+ result = fint(10) | Add(5) | Mult(2) | Sub(3) | Div(4)
113
+ ```
114
+
115
+ ## ๐Ÿ”ง Built-in Filters
116
+
117
+ ### Arithmetic Filters
118
+
119
+ ```python
120
+ from punctional import Add, Sub, Mult, Div
121
+
122
+ fint(10) | Add(5) # 15
123
+ fint(10) | Sub(3) # 7
124
+ fint(10) | Mult(2) # 20
125
+ fint(10) | Div(4) # 2.5
126
+ ```
127
+
128
+ ### Comparison Filters
129
+
130
+ ```python
131
+ from punctional import GreaterThan, LessThan, Equals
132
+
133
+ fint(42) | GreaterThan(10) # True
134
+ fint(5) | LessThan(10) # True
135
+ fint(42) | Equals(42) # True
136
+ ```
137
+
138
+ ### Logical Filters
139
+
140
+ ```python
141
+ from punctional import AndFilter, OrFilter, NotFilter, GreaterThan, LessThan, Equals
142
+
143
+ # AND: all conditions must be true
144
+ fint(42) | AndFilter(GreaterThan(10), LessThan(100)) # True
145
+
146
+ # OR: at least one condition must be true
147
+ fint(5) | OrFilter(LessThan(10), GreaterThan(100)) # True
148
+
149
+ # NOT: negate a condition
150
+ fint(5) | NotFilter(Equals(0)) # True
151
+ ```
152
+
153
+ ### String Filters
154
+
155
+ ```python
156
+ from punctional import ToUpper, ToLower, Contains, fstr
157
+
158
+ fstr("hello") | ToUpper() # "HELLO"
159
+ fstr("WORLD") | ToLower() # "world"
160
+ fstr("hello world") | Contains("world") # True
161
+ fstr("ha") | Mult(3) # "hahaha"
162
+ ```
163
+
164
+ ### List Filters
165
+
166
+ ```python
167
+ from punctional import Map, FilterList, Reduce, Mult, GreaterThan
168
+
169
+ numbers = [1, 2, 3, 4, 5]
170
+
171
+ # Transform each element
172
+ Map(Mult(2)).apply(numbers) # [2, 4, 6, 8, 10]
173
+
174
+ # Filter elements
175
+ FilterList(GreaterThan(2)).apply(numbers) # [3, 4, 5]
176
+ ```
177
+
178
+ ### Composition
179
+
180
+ Create reusable pipelines with `Compose`:
181
+
182
+ ```python
183
+ from punctional import Compose, Mult, Add, fint
184
+
185
+ # Create a reusable transformation
186
+ double_plus_ten = Compose(Mult(2), Add(10))
187
+
188
+ fint(5) | double_plus_ten # 20
189
+ fint(10) | double_plus_ten # 30
190
+ ```
191
+
192
+ ## ๐ŸŽญ Functional Design Patterns
193
+
194
+ ### Option Monad (Some/Nothing)
195
+
196
+ Handle nullable values without explicit `None` checks:
197
+
198
+ ```python
199
+ from punctional import Some, Nothing, some
200
+
201
+ # Wrap a value
202
+ value = Some(42)
203
+ print(value.map(lambda x: x * 2)) # Some(84)
204
+ print(value.get_or_else(0)) # 42
205
+
206
+ # Handle absence
207
+ empty = Nothing()
208
+ print(empty.map(lambda x: x * 2)) # Nothing
209
+ print(empty.get_or_else(0)) # 0
210
+
211
+ # Auto-convert from potentially None values
212
+ result = some(potentially_none_value) # Returns Nothing if None
213
+ ```
214
+
215
+ #### Option Operations
216
+
217
+ | Method | Description |
218
+ |--------|-------------|
219
+ | `map(f)` | Transform value if present |
220
+ | `flat_map(f)` | Chain operations returning Option |
221
+ | `bind(f)` | Alias for flat_map |
222
+ | `get_or_else(default)` | Get value or default |
223
+ | `get_or_none()` | Get value or None |
224
+ | `filter(predicate)` | Return Nothing if predicate fails |
225
+ | `or_else(alternative)` | Return alternative if Nothing |
226
+ | `is_some()` | Check if value is present |
227
+ | `is_nothing()` | Check if value is absent |
228
+
229
+ ### Result Monad (Ok/Error)
230
+
231
+ Handle operations that can fail with meaningful errors:
232
+
233
+ ```python
234
+ from punctional import Ok, Error, try_result
235
+
236
+ # Successful operation
237
+ success = Ok(42)
238
+ print(success.map(lambda x: x * 2)) # Ok(84)
239
+
240
+ # Failed operation
241
+ failure = Error("Something went wrong")
242
+ print(failure.map(lambda x: x * 2)) # Error("Something went wrong")
243
+
244
+ # Wrap potentially throwing functions
245
+ def divide(a, b):
246
+ return a / b
247
+
248
+ result = try_result(lambda: divide(10, 0))
249
+ # Error(ZeroDivisionError(...))
250
+ ```
251
+
252
+ #### Result Operations
253
+
254
+ | Method | Description |
255
+ |--------|-------------|
256
+ | `map(f)` | Transform success value |
257
+ | `map_error(f)` | Transform error value |
258
+ | `flat_map(f)` | Chain operations returning Result |
259
+ | `bind(f)` | Alias for flat_map |
260
+ | `get_or_else(default)` | Get value or default |
261
+ | `is_ok()` | Check if successful |
262
+ | `is_error()` | Check if failed |
263
+ | `to_option()` | Convert to Option (discards error info) |
264
+
265
+ ## ๐Ÿ—๏ธ Extending the Framework
266
+
267
+ ### Creating Custom Filters
268
+
269
+ #### Simple Filter
270
+
271
+ ```python
272
+ from punctional import Filter
273
+
274
+ class Increment(Filter[int, int]):
275
+ def apply(self, value: int) -> int:
276
+ return value + 1
277
+ ```
278
+
279
+ #### Parameterized Filter
280
+
281
+ ```python
282
+ class Power(Filter[int, int]):
283
+ def __init__(self, exponent: int):
284
+ self.exponent = exponent
285
+
286
+ def apply(self, value: int) -> int:
287
+ return value ** self.exponent
288
+
289
+ fint(2) | Power(3) # 8
290
+ ```
291
+
292
+ #### Stateful Filter
293
+
294
+ ```python
295
+ class Accumulator(Filter[int, int]):
296
+ def __init__(self, initial: int = 0):
297
+ self.total = initial
298
+
299
+ def apply(self, value: int) -> int:
300
+ self.total += value
301
+ return self.total
302
+
303
+ acc = Accumulator()
304
+ acc(5) # 5
305
+ acc(10) # 15
306
+ acc(3) # 18
307
+ ```
308
+
309
+ ### Functional Dataclasses
310
+
311
+ Make any dataclass functional with the `Functional` mixin:
312
+
313
+ ```python
314
+ from dataclasses import dataclass
315
+ from punctional import Functional, Filter
316
+
317
+ @dataclass
318
+ class Point(Functional):
319
+ x: float
320
+ y: float
321
+
322
+ class ScalePoint(Filter[Point, Point]):
323
+ def __init__(self, factor: float):
324
+ self.factor = factor
325
+
326
+ def apply(self, point: Point) -> Point:
327
+ return Point(point.x * self.factor, point.y * self.factor)
328
+
329
+ # Now Point supports piping!
330
+ point = Point(3, 4)
331
+ scaled = point | ScalePoint(2.5) # Point(7.5, 10.0)
332
+ ```
333
+
334
+ ## ๐Ÿ“– Examples
335
+
336
+ The [examples/](examples/) directory contains comprehensive examples:
337
+
338
+ | File | Description |
339
+ |------|-------------|
340
+ | [basics.py](examples/basics.py) | Basic usage and introduction to all features |
341
+ | [extending.py](examples/extending.py) | Guide to creating custom filters and domain-specific extensions |
342
+ | [data_transformation.py](examples/data_transformation.py) | Advanced patterns: validation pipelines, data transformations |
343
+ | [quick_reference.py](examples/quick_reference.py) | Cheat sheet for quick lookup |
344
+
345
+ ### Run the examples:
346
+
347
+ ```bash
348
+ python -m examples.basics
349
+ python -m examples.extending
350
+ python -m examples.data_transformation
351
+ ```
352
+
353
+ ## ๐Ÿงช Common Patterns
354
+
355
+ ### Validation Pipeline
356
+
357
+ ```python
358
+ from punctional import AndFilter, Functional, Filter
359
+ from dataclasses import dataclass
360
+
361
+ @dataclass
362
+ class Person(Functional):
363
+ name: str
364
+ age: int
365
+ email: str
366
+
367
+ class ValidateName(Filter[Person, bool]):
368
+ def apply(self, person: Person) -> bool:
369
+ return 1 <= len(person.name) <= 100
370
+
371
+ class ValidateAge(Filter[Person, bool]):
372
+ def apply(self, person: Person) -> bool:
373
+ return 0 <= person.age <= 150
374
+
375
+ class ValidateEmail(Filter[Person, bool]):
376
+ def apply(self, person: Person) -> bool:
377
+ return "@" in person.email and "." in person.email
378
+
379
+ # Use the validation pipeline
380
+ person = Person("Alice", 30, "alice@example.com")
381
+ is_valid = person | AndFilter(ValidateName(), ValidateAge(), ValidateEmail()) # True
382
+ ```
383
+
384
+ ### Data Transformation Pipeline
385
+
386
+ ```python
387
+ class ApplyBonus(Filter[Person, Person]):
388
+ def __init__(self, percentage: float):
389
+ self.percentage = percentage
390
+
391
+ def apply(self, person: Person) -> Person:
392
+ return Person(person.name, person.age, person.email)
393
+
394
+ person | ApplyBonus(10) | PromoteAge() | SaveToDatabase()
395
+ ```
396
+
397
+ ### List Processing Pipeline
398
+
399
+ ```python
400
+ from punctional import FilterList, Map, Mult
401
+
402
+ class IsEven(Filter[int, bool]):
403
+ def apply(self, value: int) -> bool:
404
+ return value % 2 == 0
405
+
406
+ numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
407
+ result = FilterList(IsEven()).apply(numbers) # [2, 4, 6, 8, 10]
408
+ doubled = Map(Mult(2)).apply(result) # [4, 8, 12, 16, 20]
409
+ ```
410
+
411
+ ### Error Handling with Result
412
+
413
+ ```python
414
+ from punctional import Result, Ok, Error
415
+
416
+ def fetch_user(id: int) -> Result[dict, str]:
417
+ if id < 0:
418
+ return Error("Invalid ID")
419
+ return Ok({"id": id, "name": "Alice"})
420
+
421
+ result = fetch_user(42).map(lambda u: u["name"]).get_or_else("Unknown") # "Alice"
422
+ result = fetch_user(-1).map(lambda u: u["name"]).get_or_else("Unknown") # "Unknown"
423
+ ```
424
+
425
+ ## ๐ŸŽ“ Design Principles
426
+
427
+ 1. **Immutability** โ€” Filters don't modify input; they return new values
428
+ 2. **Composability** โ€” Filters can be combined to create complex transformations
429
+ 3. **Type Safety** โ€” Generic types help catch errors at development time
430
+ 4. **Readability** โ€” Pipe operator makes data flow explicit and easy to follow
431
+ 5. **Extensibility** โ€” Easy to create domain-specific filters
432
+
433
+ ## ๐Ÿค Contributing
434
+
435
+ Contributions are welcome! Please feel free to submit a Pull Request.
436
+
437
+ ## ๐Ÿ“„ License
438
+
439
+ This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details.
440
+
441
+ ---
442
+
443
+ <p align="center">
444
+ Made with โค๏ธ for functional programming enthusiasts
445
+ </p>
@@ -0,0 +1,13 @@
1
+ punctional/__init__.py,sha256=gylyr0Ij6x97I0vc62JzaoRszhcPUdGqwhHl03ooBKY,1271
2
+ punctional/arithmetic.py,sha256=TzU5wJqu0olmEyh-TuHIpMH_2mw-EFoDRxmK6kAFbUU,1071
3
+ punctional/comparison.py,sha256=X9SzAXbNXUR5_lmPaZPaTMqM_0zwlvNPb93pcpipogI,850
4
+ punctional/core.py,sha256=g3VuKNpfbIBoNf8OEAUDtc5bLdr-PAbbMwupnkz8wPE,2838
5
+ punctional/list_filters.py,sha256=Ta9Te7D_qwf32hyQxkThw7nazbSts0jn6gF3qKjgzPk,1127
6
+ punctional/logical.py,sha256=3TR0Bgv-SwUjo4o177hHe8YLTr3jG4Kb13GZBGmEb9w,980
7
+ punctional/monads.py,sha256=zDGr7GMhiGc5whNj81lQTDgxfJtBOuh8IuNGpT7gq5g,9739
8
+ punctional/string.py,sha256=E8Ajo7_Rok55_ZNbDFQgCub0VCM4-fW_bUNswyUqDds,607
9
+ punctional/types.py,sha256=gS8Z3XtHKT0dttuGUMFsZWMoiLKuE51cRZMOVVaNKJw,1724
10
+ punctional-0.1.0.dist-info/METADATA,sha256=bht_JYz963a87ZmZVfSFspxfZAwxd67UrhPqFSOFBls,12337
11
+ punctional-0.1.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
12
+ punctional-0.1.0.dist-info/licenses/LICENSE,sha256=-iDh_gcypGAb576TWRWkMhTMvuAaOML4rZwKV1s8Xoo,1070
13
+ punctional-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -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.