fred.mlambda 0.4.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.
File without changes
fred/mlambda/_count.py ADDED
@@ -0,0 +1,13 @@
1
+ from typing import Any
2
+
3
+
4
+ def count(value: Any, fail: bool = False) -> int:
5
+ if hasattr(value, "__len__"):
6
+ return len(value)
7
+ from collections.abc import Sized
8
+ if isinstance(value, Sized):
9
+ return len(value)
10
+ error = f"Unknown type: {type(value)}"
11
+ if fail:
12
+ raise ValueError(error)
13
+ return 0
fred/mlambda/_rand.py ADDED
@@ -0,0 +1,12 @@
1
+ import random
2
+ from typing import Any
3
+
4
+
5
+ def rand(*args, k=1, disable_autoflat: bool = False) -> list[Any]:
6
+ if not disable_autoflat and k == 1:
7
+ out, *_ = rand(*args, k=k, disable_autoflat=True)
8
+ return out
9
+ return random.choices(
10
+ population=args,
11
+ k=k,
12
+ )
@@ -0,0 +1,24 @@
1
+ from typing import Optional
2
+
3
+
4
+ def strops(string: str, ops: str, fail: bool = False) -> Optional[str]:
5
+ match ops:
6
+ case "lower":
7
+ return string.lower()
8
+ case "upper":
9
+ return string.upper()
10
+ case "title":
11
+ return string.title()
12
+ case "capitalize":
13
+ return string.capitalize()
14
+ case "strip":
15
+ return string.strip()
16
+ case "lstrip":
17
+ return string.lstrip()
18
+ case "rstrip":
19
+ return string.rstrip()
20
+ case _:
21
+ msg = f"Unknown operation: {ops}"
22
+ if fail:
23
+ raise ValueError(msg)
24
+ return None
@@ -0,0 +1,57 @@
1
+ from enum import Enum
2
+ from typing import Optional
3
+
4
+ from fred.mlambda.settings import FRED_MLAMBDA_PARSED_ALIASES
5
+ from fred.mlambda.interface import MLambda
6
+
7
+
8
+ class MLambdaCatalog(Enum):
9
+ STROPS = MLambda(
10
+ name="strops",
11
+ import_pattern="fred.mlambda._strops",
12
+ )
13
+ RAND = MLambda(
14
+ name="rand",
15
+ import_pattern="fred.mlambda._rand",
16
+ )
17
+
18
+ @classmethod
19
+ def keys(cls) -> list[str]:
20
+ return [
21
+ mem.name
22
+ for mem in cls
23
+ ]
24
+
25
+ @classmethod
26
+ def get_or_create(cls, target: str, fail: bool = False) -> Optional[MLambda]:
27
+ if "." in target:
28
+ *import_path, function_name = target.split(".")
29
+ return MLambda(
30
+ name=function_name,
31
+ import_pattern=".".join(import_path),
32
+ )
33
+ return cls.find(
34
+ alias=target,
35
+ fail=fail,
36
+ )
37
+
38
+ @classmethod
39
+ def find(cls, alias: str, fail: bool = False, disable_variants: bool = False) -> Optional[MLambda]:
40
+ if "." in alias:
41
+ return None
42
+ variants: list[str] = [alias, alias.upper(), alias.lower()]
43
+ for variant in variants[:(1 if disable_variants else len(variants))]:
44
+ # Check if the target is an alias registered in the environment or defaults
45
+ if variant in FRED_MLAMBDA_PARSED_ALIASES:
46
+ *import_path, function_name = FRED_MLAMBDA_PARSED_ALIASES[variant].split(".")
47
+ return MLambda(
48
+ name=function_name,
49
+ import_pattern=".".join(import_path),
50
+ )
51
+ # Check if the alias is a registered MLambdaCatalog Enum
52
+ elif variant in cls.keys():
53
+ return cls[variant].value
54
+ error = f"Unknown MLambda: {alias}"
55
+ if fail:
56
+ raise ValueError(error)
57
+ return None
@@ -0,0 +1,36 @@
1
+ from dataclasses import dataclass
2
+ from typing import Callable
3
+
4
+
5
+ @dataclass(frozen=True, slots=True)
6
+ class Arguments:
7
+ args: list
8
+ kwargs: dict
9
+
10
+
11
+ @dataclass(frozen=True, slots=True)
12
+ class MLambda:
13
+ name: str
14
+ import_pattern: str
15
+
16
+ @property
17
+ def function(self) -> Callable:
18
+ import importlib
19
+ # Import the module
20
+ module = importlib.import_module(self.import_pattern)
21
+ # Get the function from the module
22
+ if not (fn := getattr(module, self.name, None)):
23
+ raise ValueError(f"Function {self.name} not found in module {self.import_pattern}")
24
+ return fn
25
+
26
+ def run(self, arguments: Arguments):
27
+ return self.function(*arguments.args, **arguments.kwargs)
28
+
29
+ def __call__(self, *args, **kwargs):
30
+ arguments = Arguments(
31
+ args=args,
32
+ kwargs=kwargs
33
+ )
34
+ return self.run(
35
+ arguments=arguments
36
+ )
fred/mlambda/parser.py ADDED
@@ -0,0 +1,252 @@
1
+ import re
2
+ import io
3
+ import csv
4
+ from dataclasses import dataclass
5
+ from typing import Any, Callable, Union, Optional
6
+
7
+ from fred.mlambda.interface import Arguments, MLambda
8
+ from fred.mlambda.catalog import MLambdaCatalog
9
+
10
+ # Matches innermost ${...} — i.e. no $, {, or } inside the braces.
11
+ # Used by _resolve_nested to find leaf-level expressions to evaluate first.
12
+ _INNER_PATTERN = re.compile(r"\$\{[^${}]*\}")
13
+
14
+ # Validates the funref portion: "path.to.function" or "ALIAS"
15
+ _FUNREF_RE = re.compile(r"^[A-Za-z_][A-Za-z0-9_.]*$")
16
+
17
+ # Supported type annotations via the "::" syntax, e.g. "42::int"
18
+ MLAMBDA_TYPES = Optional[Union[int, float, bool, str]]
19
+ _NULL_VALUES = ("null", "none", "")
20
+ _TYPE_CASTERS: dict[str, Callable] = {
21
+ "int": int,
22
+ "float": float,
23
+ "bool": lambda v: v.strip().lower() not in ("false", "0", "no", ""),
24
+ "str": str,
25
+ }
26
+
27
+
28
+ def _serialize(value: Any) -> str:
29
+ """
30
+ Convert an execution result back into a string token that cast() can
31
+ handle — used when embedding a nested result into its parent param_line.
32
+
33
+ Examples:
34
+ None -> "null"
35
+ True -> "true"
36
+ False -> "false"
37
+ 42 -> "42"
38
+ 3.14 -> "3.14"
39
+ "alice" -> "alice"
40
+ """
41
+ if value is None:
42
+ return "null"
43
+ if isinstance(value, bool):
44
+ return "true" if value else "false"
45
+ return str(value)
46
+
47
+
48
+ def _extract_outer(string: str) -> tuple[str, str]:
49
+ """
50
+ Parse the outermost ${funref: param_line} shell using a brace-depth
51
+ counter so that nested '}' inside param_line are handled correctly.
52
+
53
+ Returns:
54
+ (funref, raw_param_line)
55
+
56
+ Raises:
57
+ ValueError: if the string is not a valid outer MLambda expression.
58
+ """
59
+ s = string.strip()
60
+
61
+ if not s.startswith("${"):
62
+ raise ValueError(
63
+ f"Invalid MLambda expression: {s!r}\n"
64
+ "Expected format: ${funref: arg1,arg2,kwarg=value,...}"
65
+ )
66
+
67
+ # Walk from position 1 (the '{') counting brace depth
68
+ depth = 0
69
+ closing = -1
70
+ for i in range(1, len(s)):
71
+ if s[i] == "{":
72
+ depth += 1
73
+ elif s[i] == "}":
74
+ depth -= 1
75
+ if depth == 0:
76
+ closing = i
77
+ break
78
+
79
+ if closing == -1:
80
+ raise ValueError(f"Unmatched '{{' in MLambda expression: {s!r}")
81
+ if closing != len(s) - 1:
82
+ raise ValueError(
83
+ f"Unexpected characters after closing '}}' in: {s!r}"
84
+ )
85
+
86
+ inner = s[2:closing] # content between ${ and }
87
+
88
+ colon_idx = inner.find(":")
89
+ if colon_idx == -1:
90
+ raise ValueError(
91
+ f"Missing ':' separator in MLambda expression: {s!r}\n"
92
+ "Expected format: ${funref: param_line}"
93
+ )
94
+
95
+ funref = inner[:colon_idx].strip()
96
+ param_line = inner[colon_idx + 1:]
97
+
98
+ if not _FUNREF_RE.match(funref):
99
+ raise ValueError(
100
+ f"Invalid function reference {funref!r}. "
101
+ "Must be an identifier or dotted path (e.g. 'MY_ALIAS' or 'path.to.func')."
102
+ )
103
+
104
+ return funref, param_line
105
+
106
+
107
+ @dataclass(frozen=True, slots=True)
108
+ class MLambdaParser:
109
+ mlambda: MLambda
110
+ arguments: Arguments
111
+
112
+ @staticmethod
113
+ def cast(raw: str, disable_autoinfer: bool = False) -> MLAMBDA_TYPES:
114
+ """
115
+ Parse a raw token string, applying an optional '::type' suffix.
116
+
117
+ Examples:
118
+ "hello" -> "hello" (str)
119
+ "42::int" -> 42 (int)
120
+ "3.14::float" -> 3.14 (float)
121
+ "true::bool" -> True (bool)
122
+ """
123
+ raw = raw.strip()
124
+ # Early exit for None values IF autoinfer is enabled
125
+ if not disable_autoinfer and raw.lower() in ("null", "none", ""):
126
+ return None
127
+ # Check for explicit type annotation
128
+ if "::" in raw:
129
+ value_part, _, type_name = raw.rpartition("::")
130
+ caster = _TYPE_CASTERS.get(type_name.strip())
131
+ if caster is None:
132
+ raise ValueError(
133
+ f"Unknown type annotation '{type_name}'. "
134
+ f"Supported: {list(_TYPE_CASTERS)}"
135
+ )
136
+ return caster(value_part.strip())
137
+ if not disable_autoinfer and raw.isdigit():
138
+ return int(raw)
139
+ if not disable_autoinfer and raw.replace(".", "", 1).isdigit():
140
+ return float(raw)
141
+ if not disable_autoinfer and raw.lower() in ("true", "false"):
142
+ return raw.lower() == "true"
143
+ return raw
144
+
145
+ @classmethod
146
+ def parse_line(cls, param_line: str) -> tuple[list[MLAMBDA_TYPES], dict[str, MLAMBDA_TYPES]]:
147
+ """
148
+ Split a CSV-like parameter string into positional args and keyword args.
149
+
150
+ The CSV reader handles:
151
+ - Comma-separated tokens
152
+ - Quoted values (e.g. "hello, world" treated as a single token)
153
+
154
+ Each token is classified as:
155
+ - kwarg if it contains '=' (first '=' is the separator)
156
+ - positional arg otherwise
157
+
158
+ Type coercion via '::type' is applied to every value.
159
+ """
160
+ args: list[MLAMBDA_TYPES] = []
161
+ kwargs: dict[str, MLAMBDA_TYPES] = {}
162
+
163
+ if not param_line.strip():
164
+ return args, kwargs
165
+
166
+ reader = csv.reader(io.StringIO(param_line), skipinitialspace=True)
167
+ for row in reader:
168
+ for token in row:
169
+ token = token.strip()
170
+ if not token:
171
+ continue
172
+ if "=" in token:
173
+ key, _, raw_value = token.partition("=")
174
+ kwargs[key.strip()] = cls.cast(raw_value)
175
+ else:
176
+ args.append(cls.cast(token))
177
+
178
+ return args, kwargs
179
+
180
+ @classmethod
181
+ def _resolve_nested(cls, param_line: str) -> str:
182
+ """
183
+ Resolve all nested ${...} expressions within a param_line string,
184
+ evaluating innermost expressions first and working outward.
185
+
186
+ For each iteration, _INNER_PATTERN finds expressions with no nested
187
+ braces (guaranteed to be fully flat), evaluates them via from_string,
188
+ and serializes the result back as a plain token string. Repeats until
189
+ no ${...} remain.
190
+
191
+ Example:
192
+ "${RAND: alice, bob, carol}"
193
+ -> (evaluates RAND) -> "alice"
194
+
195
+ "${STROPS: ${RAND: hello, world}, upper}"
196
+ -> pass 1: "${RAND: hello, world}" -> "world"
197
+ -> param becomes "world, upper" (no more ${)
198
+ -> parse_line sees: args=["world"], kwargs={"upper": ...}
199
+ ... Wait: that's positional, so: args=["world", "upper"]
200
+ """
201
+ while "${" in param_line:
202
+ resolved = _INNER_PATTERN.sub(
203
+ lambda m: _serialize(cls.from_string(m.group(0)).execute()),
204
+ param_line,
205
+ )
206
+ if resolved == param_line:
207
+ # No substitution made — malformed inner expression
208
+ raise ValueError(
209
+ f"Could not resolve nested MLambda expression in: {param_line!r}"
210
+ )
211
+ param_line = resolved
212
+ return param_line
213
+
214
+ @classmethod
215
+ def from_string(cls, string: str) -> "MLambdaParser":
216
+ """
217
+ Parse a (potentially nested) MLambda expression string.
218
+
219
+ Supports expressions at arbitrary nesting depth, e.g.:
220
+ ${COUNT: ${RAND: alice, bob, carol}}
221
+ ${STROPS: ${RAND: hello, world}, upper}
222
+ ${A: ${B: ${C: x}}}
223
+ """
224
+ payload = string.strip()
225
+
226
+ # Step 1: extract the outer ${funref: raw_param_line} shell
227
+ # using a stack-based approach that correctly handles nested '}'
228
+ funref, raw_param_line = _extract_outer(payload)
229
+
230
+ # Step 2: resolve any nested ${...} within the param_line
231
+ resolved_param_line = cls._resolve_nested(raw_param_line)
232
+
233
+ # Step 3: parse the now-flat param_line
234
+ args, kwargs = cls.parse_line(resolved_param_line)
235
+ arguments = Arguments(args=args, kwargs=kwargs)
236
+
237
+ # Step 4: resolve the function reference
238
+ if "." not in funref:
239
+ # Bare alias — look up in catalog / settings
240
+ return cls(
241
+ mlambda=MLambdaCatalog.get_or_create(funref, fail=True),
242
+ arguments=arguments,
243
+ )
244
+ # Dotted path — construct MLambda directly
245
+ import_pattern, fname = funref.rsplit(".", 1)
246
+ return cls(
247
+ mlambda=MLambda(name=fname, import_pattern=import_pattern),
248
+ arguments=arguments,
249
+ )
250
+
251
+ def execute(self) -> Any:
252
+ return self.mlambda.run(arguments=self.arguments)
@@ -0,0 +1,24 @@
1
+ import os
2
+
3
+
4
+ FRED_MLAMBDA_ALIASES_SEP = os.environ.get(
5
+ "FRED_MLAMBDA_ALIASES_SEP",
6
+ ";",
7
+ )
8
+
9
+ FRED_MLAMBDA_ALIASES = [
10
+ alias
11
+ for line in os.environ.get(
12
+ "FRED_MLAMBDA_ALIASES",
13
+ "",
14
+ ).split(FRED_MLAMBDA_ALIASES_SEP)
15
+ if "=" in line and (alias := line.strip().split("="))
16
+ ]
17
+
18
+ FRED_MLAMBDA_PARSED_ALIASES = {
19
+ "count": "fred.mlambda._count.count",
20
+ **{
21
+ key: val
22
+ for key, val in FRED_MLAMBDA_ALIASES
23
+ }
24
+ }
fred/mlambda/version ADDED
@@ -0,0 +1 @@
1
+ 0.4.0
@@ -0,0 +1,46 @@
1
+ import os
2
+ from dataclasses import dataclass
3
+
4
+
5
+ @dataclass(frozen=True, slots=True)
6
+ class Version:
7
+ name: str
8
+ value: str
9
+
10
+ def components(self, as_int: bool = False) -> list:
11
+ return [int(val) if as_int else val for val in self.value.split(".")]
12
+
13
+ @property
14
+ def major(self) -> int:
15
+ component, *_ = self.components(as_int=True)
16
+ return component
17
+
18
+ @property
19
+ def minor(self) -> int:
20
+ _, component, *_ = self.components(as_int=True)
21
+ return component
22
+
23
+ @property
24
+ def patch(self) -> int:
25
+ *_, component = self.components(as_int=True)
26
+ return component
27
+
28
+ @classmethod
29
+ def from_path(cls, dirpath: str, name: str):
30
+ for file in os.listdir(dirpath):
31
+ if file.lower().endswith("version"):
32
+ filepath = os.path.join(dirpath, file)
33
+ break
34
+ else:
35
+ raise ValueError("Version file not found for package name: " + name)
36
+
37
+ with open(filepath, "r") as version_file:
38
+ version_value = version_file.readline().strip() # TODO: Validate version pattern via regex
39
+ return cls(name=name, value=version_value)
40
+
41
+
42
+ try:
43
+ version = Version.from_path(name="fred.mlambda", dirpath=os.path.dirname(__file__))
44
+ except Exception:
45
+ print("Version file not found for package name: fred.mlambda, using 0.0.0")
46
+ version = Version(name="fred.mlambda", value="0.0.0")
@@ -0,0 +1,18 @@
1
+ Metadata-Version: 2.4
2
+ Name: fred.mlambda
3
+ Version: 0.4.0
4
+ Summary: FRED-MLAMBDA
5
+ Home-page: https://fred.fhr.tools
6
+ Author: Fahera Research, Education, and Development
7
+ Author-email: fred@fahera.mx
8
+ Requires-Python: >=3.11
9
+ Description-Content-Type: text/markdown
10
+ Dynamic: author
11
+ Dynamic: author-email
12
+ Dynamic: description
13
+ Dynamic: description-content-type
14
+ Dynamic: home-page
15
+ Dynamic: requires-python
16
+ Dynamic: summary
17
+
18
+ # FRED MLAMBDA
@@ -0,0 +1,15 @@
1
+ fred/mlambda/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ fred/mlambda/_count.py,sha256=nm9FYAPJoQkbfLfyfKGCaUUcaAmGBRYomdPt5sJuLbA,333
3
+ fred/mlambda/_rand.py,sha256=413iDWAK70Wf-5i4C_EFNPaW90WXh7UWAqUmAFQPKZw,294
4
+ fred/mlambda/_strops.py,sha256=oGWM_5vgQcykojxNFOYkF-644R6pTY0sfBOMUtpYCzA,668
5
+ fred/mlambda/catalog.py,sha256=db2a6-973eMv_bd5gA87_u6dIBx7JFH_hSW3tTZrtQk,1873
6
+ fred/mlambda/interface.py,sha256=sc9gHHWUZyc8HtOWkTPHN4_oH6Tr_o4J20WSipq4zBg,927
7
+ fred/mlambda/parser.py,sha256=NwzIEvA__Ly9c02GKnY5M8AHafve6KJSLuZ3cjxOBKM,8620
8
+ fred/mlambda/settings.py,sha256=x5mLnvXI9w1pqyhgvhgwqbwoyqCkY5dPCX0j9hhqquA,461
9
+ fred/mlambda/version,sha256=zXhTTtFa1GkStxM50UF9DQQ9gwnCuUQV8-0bnR_frtA,5
10
+ fred/mlambda/version.py,sha256=pfhriCzyq166o5KVK2r44i0TY90iqC3l4jOjUHIN90c,1417
11
+ fred_mlambda-0.4.0.dist-info/METADATA,sha256=ADNdHstgRBWmSFTa4z4jTuNv2F0tM9HzBvbs9rBU30g,427
12
+ fred_mlambda-0.4.0.dist-info/WHEEL,sha256=aeYiig01lYGDzBgS8HxWXOg3uV61G9ijOsup-k9o1sk,91
13
+ fred_mlambda-0.4.0.dist-info/entry_points.txt,sha256=vxpaOh6wOm4RgK0O1sG3_9DGUH9bsJFImbnShS-BUas,68
14
+ fred_mlambda-0.4.0.dist-info/top_level.txt,sha256=amdkZ5b-sD33wi769YgrltKPSlpOVT-7V5pBT3wWcb4,5
15
+ fred_mlambda-0.4.0.dist-info/RECORD,,
@@ -0,0 +1,5 @@
1
+ Wheel-Version: 1.0
2
+ Generator: setuptools (82.0.1)
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
5
+
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ fred-mlambda = fred.mlambda.cli.main:CLI.cli_exec
@@ -0,0 +1 @@
1
+ fred