fred.mlambda 0.2.0__tar.gz → 0.4.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.
- {fred_mlambda-0.2.0/src/main/fred.mlambda.egg-info → fred_mlambda-0.4.0}/PKG-INFO +1 -3
- fred_mlambda-0.4.0/src/main/fred/mlambda/__init__.py +0 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred/mlambda/_count.py +1 -7
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred/mlambda/_rand.py +0 -5
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred/mlambda/_strops.py +0 -6
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred/mlambda/catalog.py +0 -5
- fred_mlambda-0.4.0/src/main/fred/mlambda/parser.py +252 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred/mlambda/settings.py +4 -6
- fred_mlambda-0.4.0/src/main/fred/mlambda/version +1 -0
- fred_mlambda-0.4.0/src/main/fred/mlambda/version.py +46 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0/src/main/fred.mlambda.egg-info}/PKG-INFO +1 -3
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred.mlambda.egg-info/SOURCES.txt +0 -1
- fred_mlambda-0.2.0/requirements.txt +0 -2
- fred_mlambda-0.2.0/src/main/fred/mlambda/parser.py +0 -137
- fred_mlambda-0.2.0/src/main/fred/mlambda/version +0 -1
- fred_mlambda-0.2.0/src/main/fred/mlambda/version.py +0 -9
- fred_mlambda-0.2.0/src/main/fred.mlambda.egg-info/requires.txt +0 -1
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/MANIFEST.in +0 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/README.md +0 -0
- /fred_mlambda-0.2.0/src/main/fred/mlambda/__init__.py → /fred_mlambda-0.4.0/requirements.txt +0 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/setup.cfg +0 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/setup.py +0 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred/mlambda/interface.py +0 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred.mlambda.egg-info/dependency_links.txt +0 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred.mlambda.egg-info/entry_points.txt +0 -0
- {fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred.mlambda.egg-info/top_level.txt +0 -0
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred.mlambda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: FRED-MLAMBDA
|
|
5
5
|
Home-page: https://fred.fhr.tools
|
|
6
6
|
Author: Fahera Research, Education, and Development
|
|
7
7
|
Author-email: fred@fahera.mx
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
|
-
Requires-Dist: fred-oss==0.66.0
|
|
11
10
|
Dynamic: author
|
|
12
11
|
Dynamic: author-email
|
|
13
12
|
Dynamic: description
|
|
14
13
|
Dynamic: description-content-type
|
|
15
14
|
Dynamic: home-page
|
|
16
|
-
Dynamic: requires-dist
|
|
17
15
|
Dynamic: requires-python
|
|
18
16
|
Dynamic: summary
|
|
19
17
|
|
|
File without changes
|
|
@@ -1,9 +1,4 @@
|
|
|
1
|
-
from typing import Any
|
|
2
|
-
|
|
3
|
-
from fred.settings import logger_manager
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
logger = logger_manager.get_logger(__name__)
|
|
1
|
+
from typing import Any
|
|
7
2
|
|
|
8
3
|
|
|
9
4
|
def count(value: Any, fail: bool = False) -> int:
|
|
@@ -13,7 +8,6 @@ def count(value: Any, fail: bool = False) -> int:
|
|
|
13
8
|
if isinstance(value, Sized):
|
|
14
9
|
return len(value)
|
|
15
10
|
error = f"Unknown type: {type(value)}"
|
|
16
|
-
logger.warning(error)
|
|
17
11
|
if fail:
|
|
18
12
|
raise ValueError(error)
|
|
19
13
|
return 0
|
|
@@ -1,10 +1,5 @@
|
|
|
1
1
|
from typing import Optional
|
|
2
2
|
|
|
3
|
-
from fred.settings import logger_manager
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
logger = logger_manager.get_logger(__name__)
|
|
7
|
-
|
|
8
3
|
|
|
9
4
|
def strops(string: str, ops: str, fail: bool = False) -> Optional[str]:
|
|
10
5
|
match ops:
|
|
@@ -26,5 +21,4 @@ def strops(string: str, ops: str, fail: bool = False) -> Optional[str]:
|
|
|
26
21
|
msg = f"Unknown operation: {ops}"
|
|
27
22
|
if fail:
|
|
28
23
|
raise ValueError(msg)
|
|
29
|
-
logger.warning(msg)
|
|
30
24
|
return None
|
|
@@ -1,12 +1,9 @@
|
|
|
1
1
|
from enum import Enum
|
|
2
2
|
from typing import Optional
|
|
3
3
|
|
|
4
|
-
from fred.settings import logger_manager
|
|
5
4
|
from fred.mlambda.settings import FRED_MLAMBDA_PARSED_ALIASES
|
|
6
5
|
from fred.mlambda.interface import MLambda
|
|
7
6
|
|
|
8
|
-
logger = logger_manager.get_logger(__name__)
|
|
9
|
-
|
|
10
7
|
|
|
11
8
|
class MLambdaCatalog(Enum):
|
|
12
9
|
STROPS = MLambda(
|
|
@@ -41,7 +38,6 @@ class MLambdaCatalog(Enum):
|
|
|
41
38
|
@classmethod
|
|
42
39
|
def find(cls, alias: str, fail: bool = False, disable_variants: bool = False) -> Optional[MLambda]:
|
|
43
40
|
if "." in alias:
|
|
44
|
-
logger.warning("The target is a dotpath, not an alias. Use MLambdaParser.from_string() instead.")
|
|
45
41
|
return None
|
|
46
42
|
variants: list[str] = [alias, alias.upper(), alias.lower()]
|
|
47
43
|
for variant in variants[:(1 if disable_variants else len(variants))]:
|
|
@@ -56,7 +52,6 @@ class MLambdaCatalog(Enum):
|
|
|
56
52
|
elif variant in cls.keys():
|
|
57
53
|
return cls[variant].value
|
|
58
54
|
error = f"Unknown MLambda: {alias}"
|
|
59
|
-
logger.warning(error)
|
|
60
55
|
if fail:
|
|
61
56
|
raise ValueError(error)
|
|
62
57
|
return None
|
|
@@ -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)
|
|
@@ -1,18 +1,16 @@
|
|
|
1
1
|
import os
|
|
2
2
|
|
|
3
|
-
from fred.settings import get_environ_variable
|
|
4
3
|
|
|
5
|
-
|
|
6
|
-
FRED_MLAMBDA_ALIASES_SEP = get_environ_variable(
|
|
4
|
+
FRED_MLAMBDA_ALIASES_SEP = os.environ.get(
|
|
7
5
|
"FRED_MLAMBDA_ALIASES_SEP",
|
|
8
|
-
|
|
6
|
+
";",
|
|
9
7
|
)
|
|
10
8
|
|
|
11
9
|
FRED_MLAMBDA_ALIASES = [
|
|
12
10
|
alias
|
|
13
|
-
for line in
|
|
11
|
+
for line in os.environ.get(
|
|
14
12
|
"FRED_MLAMBDA_ALIASES",
|
|
15
|
-
|
|
13
|
+
"",
|
|
16
14
|
).split(FRED_MLAMBDA_ALIASES_SEP)
|
|
17
15
|
if "=" in line and (alias := line.strip().split("="))
|
|
18
16
|
]
|
|
@@ -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")
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: fred.mlambda
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.4.0
|
|
4
4
|
Summary: FRED-MLAMBDA
|
|
5
5
|
Home-page: https://fred.fhr.tools
|
|
6
6
|
Author: Fahera Research, Education, and Development
|
|
7
7
|
Author-email: fred@fahera.mx
|
|
8
8
|
Requires-Python: >=3.11
|
|
9
9
|
Description-Content-Type: text/markdown
|
|
10
|
-
Requires-Dist: fred-oss==0.66.0
|
|
11
10
|
Dynamic: author
|
|
12
11
|
Dynamic: author-email
|
|
13
12
|
Dynamic: description
|
|
14
13
|
Dynamic: description-content-type
|
|
15
14
|
Dynamic: home-page
|
|
16
|
-
Dynamic: requires-dist
|
|
17
15
|
Dynamic: requires-python
|
|
18
16
|
Dynamic: summary
|
|
19
17
|
|
|
@@ -6,7 +6,6 @@ src/main/fred.mlambda.egg-info/PKG-INFO
|
|
|
6
6
|
src/main/fred.mlambda.egg-info/SOURCES.txt
|
|
7
7
|
src/main/fred.mlambda.egg-info/dependency_links.txt
|
|
8
8
|
src/main/fred.mlambda.egg-info/entry_points.txt
|
|
9
|
-
src/main/fred.mlambda.egg-info/requires.txt
|
|
10
9
|
src/main/fred.mlambda.egg-info/top_level.txt
|
|
11
10
|
src/main/fred/mlambda/__init__.py
|
|
12
11
|
src/main/fred/mlambda/_count.py
|
|
@@ -1,137 +0,0 @@
|
|
|
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: ${path.to.function: param_line}
|
|
11
|
-
# Group 1 (dotpath): "path.to.function"
|
|
12
|
-
# Group 2 (param_line): "arg1,arg2,kwarg1=value1,..."
|
|
13
|
-
_MLAMBDA_PATTERN = re.compile(
|
|
14
|
-
r"^\$\{\s*(?P<funref>[A-Za-z_][A-Za-z0-9_.]*)\s*:\s*(?P<param_line>[^}]*)\}$"
|
|
15
|
-
)
|
|
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
|
-
@dataclass(frozen=True, slots=True)
|
|
29
|
-
class MLambdaParser:
|
|
30
|
-
mlambda: MLambda
|
|
31
|
-
arguments: Arguments
|
|
32
|
-
|
|
33
|
-
@staticmethod
|
|
34
|
-
def cast(raw: str, disable_autoinfer: bool = False) -> MLAMBDA_TYPES:
|
|
35
|
-
"""
|
|
36
|
-
Parse a raw token string, applying an optional '::type' suffix.
|
|
37
|
-
|
|
38
|
-
Examples:
|
|
39
|
-
"hello" -> "hello" (str)
|
|
40
|
-
"42::int" -> 42 (int)
|
|
41
|
-
"3.14::float" -> 3.14 (float)
|
|
42
|
-
"true::bool" -> True (bool)
|
|
43
|
-
"""
|
|
44
|
-
raw = raw.strip()
|
|
45
|
-
# Early exit for None values IF autoinfer is enabled
|
|
46
|
-
if not disable_autoinfer and raw.lower() in ("null", "none", ""):
|
|
47
|
-
return None
|
|
48
|
-
# Check for type annotation
|
|
49
|
-
if "::" in raw:
|
|
50
|
-
value_part, _, type_name = raw.rpartition("::")
|
|
51
|
-
caster = _TYPE_CASTERS.get(type_name.strip())
|
|
52
|
-
if caster is None:
|
|
53
|
-
raise ValueError(
|
|
54
|
-
f"Unknown type annotation '{type_name}'. "
|
|
55
|
-
f"Supported: {list(_TYPE_CASTERS)}"
|
|
56
|
-
)
|
|
57
|
-
return caster(value_part.strip())
|
|
58
|
-
if not disable_autoinfer and raw.isdigit():
|
|
59
|
-
return int(raw)
|
|
60
|
-
if not disable_autoinfer and raw.replace(".", "", 1).isdigit():
|
|
61
|
-
return float(raw)
|
|
62
|
-
if not disable_autoinfer and raw.lower() in ("true", "false"):
|
|
63
|
-
return raw.lower() == "true"
|
|
64
|
-
return raw
|
|
65
|
-
|
|
66
|
-
@classmethod
|
|
67
|
-
def parse_line(cls, param_line: str) -> tuple[list[MLAMBDA_TYPES], dict[str, MLAMBDA_TYPES]]:
|
|
68
|
-
"""
|
|
69
|
-
Split a CSV-like parameter string into positional args and keyword args.
|
|
70
|
-
|
|
71
|
-
The CSV reader handles:
|
|
72
|
-
- Comma-separated tokens
|
|
73
|
-
- Quoted values (e.g. "hello, world" treated as a single token)
|
|
74
|
-
|
|
75
|
-
Each token is classified as:
|
|
76
|
-
- kwarg if it contains '=' (first '=' is the separator)
|
|
77
|
-
- positional arg otherwise
|
|
78
|
-
|
|
79
|
-
Type coercion via '::type' is applied to every value.
|
|
80
|
-
"""
|
|
81
|
-
args: list[MLAMBDA_TYPES] = []
|
|
82
|
-
kwargs: dict[str, MLAMBDA_TYPES] = {}
|
|
83
|
-
|
|
84
|
-
if not param_line.strip():
|
|
85
|
-
return args, kwargs
|
|
86
|
-
|
|
87
|
-
reader = csv.reader(io.StringIO(param_line), skipinitialspace=True)
|
|
88
|
-
for row in reader:
|
|
89
|
-
for token in row:
|
|
90
|
-
token = token.strip()
|
|
91
|
-
if not token:
|
|
92
|
-
continue
|
|
93
|
-
if "=" in token:
|
|
94
|
-
key, _, raw_value = token.partition("=")
|
|
95
|
-
kwargs[key.strip()] = cls.cast(raw_value)
|
|
96
|
-
else:
|
|
97
|
-
args.append(cls.cast(token))
|
|
98
|
-
|
|
99
|
-
return args, kwargs
|
|
100
|
-
|
|
101
|
-
@classmethod
|
|
102
|
-
def from_string(cls, string: str) -> "MLambdaParser":
|
|
103
|
-
payload = string.strip()
|
|
104
|
-
|
|
105
|
-
match = _MLAMBDA_PATTERN.match(payload)
|
|
106
|
-
if not match:
|
|
107
|
-
raise ValueError(
|
|
108
|
-
f"Invalid MLambda expression: {payload!r}\n"
|
|
109
|
-
"Expected format: ${path.to.function: arg1,arg2,kwarg1=value1,...}"
|
|
110
|
-
)
|
|
111
|
-
# Get the function reference and param_line from the match
|
|
112
|
-
funref: str = match.group("funref")
|
|
113
|
-
param_line: str = match.group("param_line")
|
|
114
|
-
# Parse the CSV-like parameter line
|
|
115
|
-
args, kwargs = cls.parse_line(param_line)
|
|
116
|
-
arguments = Arguments(
|
|
117
|
-
args=args,
|
|
118
|
-
kwargs=kwargs,
|
|
119
|
-
)
|
|
120
|
-
# If the function reference is an alias, get the MLambda from the catalog
|
|
121
|
-
if "." not in funref:
|
|
122
|
-
return cls(
|
|
123
|
-
mlambda=MLambdaCatalog.get_or_create(funref, fail=True),
|
|
124
|
-
arguments=arguments,
|
|
125
|
-
)
|
|
126
|
-
# Split "path.to.function" -> import_pattern="path.to", fname="function"
|
|
127
|
-
import_pattern, fname = funref.rsplit(".", 1)
|
|
128
|
-
return cls(
|
|
129
|
-
mlambda=MLambda(
|
|
130
|
-
name=fname,
|
|
131
|
-
import_pattern=import_pattern
|
|
132
|
-
),
|
|
133
|
-
arguments=arguments,
|
|
134
|
-
)
|
|
135
|
-
|
|
136
|
-
def execute(self) -> Any:
|
|
137
|
-
return self.mlambda.run(arguments=self.arguments)
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
0.2.0
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
fred-oss==0.66.0
|
|
File without changes
|
|
File without changes
|
/fred_mlambda-0.2.0/src/main/fred/mlambda/__init__.py → /fred_mlambda-0.4.0/requirements.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{fred_mlambda-0.2.0 → fred_mlambda-0.4.0}/src/main/fred.mlambda.egg-info/dependency_links.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|