Flowfile 0.3.1.2__py3-none-any.whl → 0.3.3__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.
Potentially problematic release.
This version of Flowfile might be problematic. Click here for more details.
- flowfile/__init__.py +2 -1
- flowfile/api.py +5 -3
- flowfile/web/__init__.py +3 -0
- flowfile/web/static/assets/{AirbyteReader-cb0c1d4a.js → AirbyteReader-2b1cf2d8.js} +10 -9
- flowfile/web/static/assets/{CrossJoin-a514fa59.js → CrossJoin-cc3ab73c.js} +8 -8
- flowfile/web/static/assets/{DatabaseConnectionSettings-f2cecf33.js → DatabaseConnectionSettings-307c4652.js} +2 -2
- flowfile/web/static/assets/{DatabaseManager-83ee3c98.js → DatabaseManager-69faa6e1.js} +10 -6
- flowfile/web/static/assets/{DatabaseReader-dc0c6881.js → DatabaseReader-e4134cd0.js} +9 -9
- flowfile/web/static/assets/{DatabaseWriter-5afe9f8d.js → DatabaseWriter-d32d75b1.js} +9 -9
- flowfile/web/static/assets/{ExploreData-c7ee19cf.js → ExploreData-5eb48389.js} +18639 -18629
- flowfile/web/static/assets/{ExternalSource-17b23a01.js → ExternalSource-29489051.js} +8 -21
- flowfile/web/static/assets/{Filter-90856b4f.js → Filter-031332bb.js} +9 -9
- flowfile/web/static/assets/{Formula-38b71e9e.js → Formula-3b900540.js} +15 -15
- flowfile/web/static/assets/{Formula-d60a74f4.css → Formula-b8cefc31.css} +4 -4
- flowfile/web/static/assets/{FuzzyMatch-d0f1fe81.js → FuzzyMatch-dee31153.js} +9 -9
- flowfile/web/static/assets/{GraphSolver-0c86bbc6.js → GraphSolver-ca74eb47.js} +5 -5
- flowfile/web/static/assets/{GroupBy-f2772e9f.js → GroupBy-081b6591.js} +8 -7
- flowfile/web/static/assets/{Join-bc3e1cf7.js → Join-b467376f.js} +11 -10
- flowfile/web/static/assets/{ManualInput-03aa0245.js → ManualInput-ffffb80a.js} +11 -8
- flowfile/web/static/assets/{Output-5b35eee8.js → Output-9a87d4ba.js} +4 -4
- flowfile/web/static/assets/{Pivot-7164087c.js → Pivot-ee3e6093.js} +8 -7
- flowfile/web/static/assets/{PolarsCode-3abf6507.js → PolarsCode-03921254.js} +13 -11
- flowfile/web/static/assets/{PopOver-b37ff9be.js → PopOver-3bdf8951.js} +1 -1
- flowfile/web/static/assets/{Read-65966a3e.js → Read-67fee3a0.js} +6 -6
- flowfile/web/static/assets/{RecordCount-c66c6d6d.js → RecordCount-a2acd02d.js} +7 -6
- flowfile/web/static/assets/{RecordId-826dc095.js → RecordId-0c8bcd77.js} +10 -8
- flowfile/web/static/assets/{Sample-4ed555c8.js → Sample-60594a3a.js} +7 -6
- flowfile/web/static/assets/{SecretManager-eac1e97d.js → SecretManager-bbcec2ac.js} +2 -2
- flowfile/web/static/assets/{Select-085f05cc.js → Select-9540e6ca.js} +8 -8
- flowfile/web/static/assets/{SettingsSection-1f5e79c1.js → SettingsSection-48f28104.js} +1 -1
- flowfile/web/static/assets/{Sort-3e6cb414.js → Sort-6dbe3633.js} +6 -6
- flowfile/web/static/assets/{TextToRows-606349bc.js → TextToRows-27aab4a8.js} +18 -13
- flowfile/web/static/assets/{UnavailableFields-b41976ed.js → UnavailableFields-8143044b.js} +2 -2
- flowfile/web/static/assets/{Union-fca91665.js → Union-52460248.js} +7 -6
- flowfile/web/static/assets/{Unique-a59f830e.js → Unique-f6962644.js} +8 -8
- flowfile/web/static/assets/{Unpivot-c3815565.js → Unpivot-1ff1e938.js} +5 -5
- flowfile/web/static/assets/{api-22b338bd.js → api-3b345d92.js} +1 -1
- flowfile/web/static/assets/{designer-e5bbe26f.js → designer-4736134f.js} +72 -42
- flowfile/web/static/assets/{documentation-08045cf2.js → documentation-b9545eba.js} +1 -1
- flowfile/web/static/assets/{dropDown-5e7e9a5a.js → dropDown-d5a4014c.js} +1 -1
- flowfile/web/static/assets/{dropDownGeneric-50a91b99.js → dropDownGeneric-1f4e32ec.js} +2 -2
- flowfile/web/static/assets/{fullEditor-705c6ccb.js → fullEditor-f4791c23.js} +3 -3
- flowfile/web/static/assets/{genericNodeSettings-65587f20.js → genericNodeSettings-1d456350.js} +3 -3
- flowfile/web/static/assets/{index-552863fd.js → index-f25c9283.js} +2608 -1570
- flowfile/web/static/assets/{nodeTitle-cf9bae3c.js → nodeTitle-cad6fd9d.js} +3 -3
- flowfile/web/static/assets/{secretApi-3ad510e1.js → secretApi-01f07e2c.js} +1 -1
- flowfile/web/static/assets/{selectDynamic-bd644891.js → selectDynamic-f46a4e3f.js} +3 -3
- flowfile/web/static/assets/{vue-codemirror.esm-dd17b478.js → vue-codemirror.esm-eb98fc8b.js} +15 -14
- flowfile/web/static/assets/{vue-content-loader.es-6b36f05e.js → vue-content-loader.es-860c0380.js} +1 -1
- flowfile/web/static/index.html +1 -1
- {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/METADATA +1 -3
- {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/RECORD +97 -88
- flowfile_core/configs/__init__.py +15 -4
- flowfile_core/configs/node_store/nodes.py +2 -4
- flowfile_core/configs/settings.py +5 -3
- flowfile_core/configs/utils.py +18 -0
- flowfile_core/flowfile/FlowfileFlow.py +84 -29
- flowfile_core/flowfile/database_connection_manager/db_connections.py +1 -1
- flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +55 -18
- flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +42 -9
- flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +42 -3
- flowfile_core/flowfile/flow_data_engine/polars_code_parser.py +34 -2
- flowfile_core/flowfile/flow_data_engine/sample_data.py +25 -7
- flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +4 -3
- flowfile_core/flowfile/flow_data_engine/utils.py +1 -0
- flowfile_core/flowfile/flow_graph_utils.py +320 -0
- flowfile_core/flowfile/flow_node/flow_node.py +2 -1
- flowfile_core/flowfile/sources/external_sources/airbyte_sources/models.py +2 -2
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/__init__.py +0 -1
- flowfile_core/flowfile/sources/external_sources/sql_source/sql_source.py +1 -1
- flowfile_core/flowfile/utils.py +34 -3
- flowfile_core/main.py +2 -3
- flowfile_core/routes/secrets.py +1 -1
- flowfile_core/schemas/input_schema.py +12 -14
- flowfile_core/schemas/transform_schema.py +25 -47
- flowfile_frame/__init__.py +11 -4
- flowfile_frame/adding_expr.py +280 -0
- flowfile_frame/config.py +9 -0
- flowfile_frame/expr.py +301 -83
- flowfile_frame/expr.pyi +2174 -0
- flowfile_frame/expr_name.py +258 -0
- flowfile_frame/flow_frame.py +616 -627
- flowfile_frame/flow_frame.pyi +336 -0
- flowfile_frame/flow_frame_methods.py +617 -0
- flowfile_frame/group_frame.py +89 -42
- flowfile_frame/join.py +1 -2
- flowfile_frame/lazy.py +704 -0
- flowfile_frame/lazy_methods.py +201 -0
- flowfile_frame/list_name_space.py +324 -0
- flowfile_frame/selectors.py +3 -0
- flowfile_frame/series.py +70 -0
- flowfile_frame/utils.py +80 -4
- flowfile/web/static/assets/GoogleSheet-854294a4.js +0 -2616
- flowfile/web/static/assets/GoogleSheet-92084da7.css +0 -233
- flowfile_core/flowfile/sources/external_sources/custom_external_sources/google_sheet.py +0 -74
- {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/LICENSE +0 -0
- {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/WHEEL +0 -0
- {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/entry_points.txt +0 -0
- /flowfile_core/{secrets → secret_manager}/__init__.py +0 -0
- /flowfile_core/{secrets/secrets.py → secret_manager/secret_manager.py} +0 -0
|
@@ -166,10 +166,8 @@ class FuzzyMap(JoinMap):
|
|
|
166
166
|
self.output_column_name = f'fuzzy_score_{self.left_col}_{self.right_col}'
|
|
167
167
|
|
|
168
168
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
left_select: SelectInputs = None
|
|
172
|
-
right_select: SelectInputs = None
|
|
169
|
+
class JoinSelectMixin:
|
|
170
|
+
"""Mixin for common join selection functionality"""
|
|
173
171
|
|
|
174
172
|
@staticmethod
|
|
175
173
|
def parse_select(select: List[SelectInput] | List[str] | List[Dict]) -> SelectInputs:
|
|
@@ -184,6 +182,26 @@ class CrossJoinInput:
|
|
|
184
182
|
elif all(isinstance(c, str) for c in select):
|
|
185
183
|
return SelectInputs([SelectInput(s, s) for s in select])
|
|
186
184
|
|
|
185
|
+
def auto_generate_new_col_name(self, old_col_name: str, side: str) -> str:
|
|
186
|
+
current_names = self.left_select.new_cols & self.right_select.new_cols
|
|
187
|
+
if old_col_name not in current_names:
|
|
188
|
+
return old_col_name
|
|
189
|
+
while True:
|
|
190
|
+
if old_col_name not in current_names:
|
|
191
|
+
return old_col_name
|
|
192
|
+
old_col_name = f'{side}_{old_col_name}'
|
|
193
|
+
|
|
194
|
+
def add_new_select_column(self, select_input: SelectInput, side: str):
|
|
195
|
+
selects = self.right_select if side == 'right' else self.left_select
|
|
196
|
+
select_input.new_name = self.auto_generate_new_col_name(select_input.old_name, side=side)
|
|
197
|
+
selects.__add__(select_input)
|
|
198
|
+
|
|
199
|
+
|
|
200
|
+
@dataclass
|
|
201
|
+
class CrossJoinInput(JoinSelectMixin):
|
|
202
|
+
left_select: SelectInputs = None
|
|
203
|
+
right_select: SelectInputs = None
|
|
204
|
+
|
|
187
205
|
def __init__(self, left_select: List[SelectInput] | List[str],
|
|
188
206
|
right_select: List[SelectInput] | List[str]):
|
|
189
207
|
self.left_select = self.parse_select(left_select)
|
|
@@ -201,23 +219,9 @@ class CrossJoinInput:
|
|
|
201
219
|
right_col.new_name = 'right_' + right_col.new_name
|
|
202
220
|
overlapping_records = self.overlapping_records
|
|
203
221
|
|
|
204
|
-
def auto_generate_new_col_name(self, old_col_name: str, side: str) -> str:
|
|
205
|
-
current_names = self.left_select.new_cols & self.right_select.new_cols
|
|
206
|
-
if old_col_name not in current_names:
|
|
207
|
-
return old_col_name
|
|
208
|
-
while True:
|
|
209
|
-
if old_col_name not in current_names:
|
|
210
|
-
return old_col_name
|
|
211
|
-
old_col_name = f'{side}_{old_col_name}'
|
|
212
|
-
|
|
213
|
-
def add_new_select_column(self, select_input: SelectInput, side: str):
|
|
214
|
-
selects = self.right_select if side == 'right' else self.left_select
|
|
215
|
-
select_input.new_name = self.auto_generate_new_col_name(select_input.old_name, side=side)
|
|
216
|
-
selects.__add__(select_input)
|
|
217
|
-
|
|
218
222
|
|
|
219
223
|
@dataclass
|
|
220
|
-
class JoinInput:
|
|
224
|
+
class JoinInput(JoinSelectMixin):
|
|
221
225
|
join_mapping: List[JoinMap]
|
|
222
226
|
left_select: SelectInputs = None
|
|
223
227
|
right_select: SelectInputs = None
|
|
@@ -243,20 +247,8 @@ class JoinInput:
|
|
|
243
247
|
raise Exception('No valid join mapping as input')
|
|
244
248
|
return join_mapping
|
|
245
249
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if all(isinstance(c, SelectInput) for c in select):
|
|
249
|
-
return SelectInputs(select)
|
|
250
|
-
elif all(isinstance(c, dict) for c in select):
|
|
251
|
-
return SelectInputs([SelectInput(**c) for c in select])
|
|
252
|
-
elif isinstance(select, dict):
|
|
253
|
-
renames = select.get('renames')
|
|
254
|
-
if renames:
|
|
255
|
-
return SelectInputs([SelectInput(**c) for c in renames])
|
|
256
|
-
elif all(isinstance(c, str) for c in select):
|
|
257
|
-
return SelectInputs([SelectInput(s, s) for s in select])
|
|
258
|
-
|
|
259
|
-
def __init__(self, join_mapping: List[JoinMap] | Tuple[str, str] | str, left_select: List[SelectInput] | List[str],
|
|
250
|
+
def __init__(self, join_mapping: List[JoinMap] | Tuple[str, str] | str,
|
|
251
|
+
left_select: List[SelectInput] | List[str],
|
|
260
252
|
right_select: List[SelectInput] | List[str],
|
|
261
253
|
how: JoinStrategy = 'inner'):
|
|
262
254
|
self.join_mapping = self.parse_join_mapping(join_mapping)
|
|
@@ -311,20 +303,6 @@ class JoinInput:
|
|
|
311
303
|
)
|
|
312
304
|
return new_mappings
|
|
313
305
|
|
|
314
|
-
def auto_generate_new_col_name(self, old_col_name: str, side: str) -> str:
|
|
315
|
-
current_names = self.left_select.new_cols & self.right_select.new_cols
|
|
316
|
-
if old_col_name not in current_names:
|
|
317
|
-
return old_col_name
|
|
318
|
-
while True:
|
|
319
|
-
if old_col_name not in current_names:
|
|
320
|
-
return old_col_name
|
|
321
|
-
old_col_name = f'{side}_{old_col_name}'
|
|
322
|
-
|
|
323
|
-
def add_new_select_column(self, select_input: SelectInput, side: str):
|
|
324
|
-
selects = self.right_select if side == 'right' else self.left_select
|
|
325
|
-
select_input.new_name = self.auto_generate_new_col_name(select_input.old_name, side=side)
|
|
326
|
-
selects.__add__(select_input)
|
|
327
|
-
|
|
328
306
|
|
|
329
307
|
@dataclass
|
|
330
308
|
class FuzzyMatchInput(JoinInput):
|
flowfile_frame/__init__.py
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
# flowframe/__init__.py
|
|
2
2
|
"""A Polars-like API for building ETL graphs."""
|
|
3
3
|
|
|
4
|
+
from flowfile_core.configs.settings import OFFLOAD_TO_WORKER
|
|
5
|
+
|
|
6
|
+
OFFLOAD_TO_WORKER.value = False
|
|
7
|
+
|
|
4
8
|
# Core classes
|
|
5
9
|
from flowfile_frame.flow_frame import FlowFrame # noqa: F401
|
|
6
10
|
|
|
@@ -10,9 +14,11 @@ from flowfile_frame.utils import create_flow_graph # noqa: F401
|
|
|
10
14
|
from flowfile_frame.expr import ( # noqa: F401
|
|
11
15
|
col, lit, column,
|
|
12
16
|
cum_count, len,
|
|
13
|
-
sum, min, max, mean, count, when
|
|
17
|
+
sum, min, max, mean, count, when, implode, last, corr, cov, first
|
|
14
18
|
)
|
|
15
19
|
|
|
20
|
+
from flowfile_frame.lazy import (fold)
|
|
21
|
+
|
|
16
22
|
# Selector utilities
|
|
17
23
|
from flowfile_frame.selectors import ( # noqa: F401
|
|
18
24
|
numeric, float_, integer, string, temporal,
|
|
@@ -21,10 +27,11 @@ from flowfile_frame.selectors import ( # noqa: F401
|
|
|
21
27
|
by_dtype, contains, starts_with, ends_with, matches
|
|
22
28
|
)
|
|
23
29
|
|
|
30
|
+
from flowfile_frame.series import Series
|
|
31
|
+
|
|
24
32
|
# File I/O
|
|
25
|
-
from flowfile_frame.
|
|
26
|
-
read_csv, read_parquet, from_dict, concat
|
|
27
|
-
)
|
|
33
|
+
from flowfile_frame.flow_frame_methods import ( # noqa: F401
|
|
34
|
+
read_csv, read_parquet, from_dict, concat, scan_csv, scan_parquet)
|
|
28
35
|
|
|
29
36
|
from polars.datatypes import ( # noqa: F401
|
|
30
37
|
# Integer types
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
import polars as pl
|
|
2
|
+
from functools import wraps
|
|
3
|
+
from typing import Callable, TypeVar, Type
|
|
4
|
+
from flowfile_frame.utils import _get_function_source
|
|
5
|
+
from flowfile_frame.config import logger
|
|
6
|
+
|
|
7
|
+
T = TypeVar('T')
|
|
8
|
+
ExprT = TypeVar('ExprT', bound='Expr')
|
|
9
|
+
PASSTHROUGH_METHODS = {"map_elements", "map_batches"}
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def create_expr_method_wrapper(method_name: str, original_method: Callable) -> Callable:
|
|
13
|
+
"""
|
|
14
|
+
Creates a wrapper for a polars Expr method that properly integrates with your custom Expr class.
|
|
15
|
+
|
|
16
|
+
Parameters
|
|
17
|
+
----------
|
|
18
|
+
method_name : str
|
|
19
|
+
Name of the polars Expr method.
|
|
20
|
+
original_method : Callable
|
|
21
|
+
The original polars Expr method.
|
|
22
|
+
|
|
23
|
+
Returns
|
|
24
|
+
-------
|
|
25
|
+
Callable
|
|
26
|
+
A wrapper method appropriate for your Expr class.
|
|
27
|
+
"""
|
|
28
|
+
from flowfile_frame.expr import Expr
|
|
29
|
+
|
|
30
|
+
@wraps(original_method)
|
|
31
|
+
def wrapper(self: Expr, *args, **kwargs):
|
|
32
|
+
from flowfile_frame.expr import Expr
|
|
33
|
+
# Check if we have a valid underlying expression
|
|
34
|
+
if self.expr is None:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Cannot call '{method_name}' on Expr with no underlying polars expression."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
# Collect function sources and build representations
|
|
40
|
+
function_sources = []
|
|
41
|
+
args_representations = []
|
|
42
|
+
kwargs_representations = []
|
|
43
|
+
|
|
44
|
+
# Process positional arguments
|
|
45
|
+
for arg in args:
|
|
46
|
+
if callable(arg) and not isinstance(arg, type):
|
|
47
|
+
# Try to get function source
|
|
48
|
+
try:
|
|
49
|
+
source, is_module_level = _get_function_source(arg)
|
|
50
|
+
if source and hasattr(arg, '__name__') and arg.__name__ != '<lambda>':
|
|
51
|
+
function_sources.append(source)
|
|
52
|
+
# Use the function name in the representation
|
|
53
|
+
args_representations.append(arg.__name__)
|
|
54
|
+
else:
|
|
55
|
+
# Fallback to repr if we can't get the source
|
|
56
|
+
args_representations.append(repr(arg))
|
|
57
|
+
except:
|
|
58
|
+
args_representations.append(repr(arg))
|
|
59
|
+
else:
|
|
60
|
+
args_representations.append(repr(arg))
|
|
61
|
+
|
|
62
|
+
# Process keyword arguments
|
|
63
|
+
for key, value in kwargs.items():
|
|
64
|
+
if callable(value) and not isinstance(value, type):
|
|
65
|
+
# Try to get function source
|
|
66
|
+
try:
|
|
67
|
+
source, is_module_level = _get_function_source(value)
|
|
68
|
+
if source and hasattr(value, '__name__') and value.__name__ != '<lambda>':
|
|
69
|
+
function_sources.append(source)
|
|
70
|
+
# Use the function name in the representation
|
|
71
|
+
kwargs_representations.append(f"{key}={value.__name__}")
|
|
72
|
+
else:
|
|
73
|
+
# Fallback to repr if we can't get the source
|
|
74
|
+
kwargs_representations.append(f"{key}={repr(value)}")
|
|
75
|
+
except:
|
|
76
|
+
kwargs_representations.append(f"{key}={repr(value)}")
|
|
77
|
+
else:
|
|
78
|
+
kwargs_representations.append(f"{key}={repr(value)}")
|
|
79
|
+
|
|
80
|
+
# Call the method on the underlying polars expression
|
|
81
|
+
try:
|
|
82
|
+
result_expr = getattr(self.expr, method_name)(*args, **kwargs)
|
|
83
|
+
except Exception as e:
|
|
84
|
+
logger.debug(f"Warning: Error in {method_name}() call: {e}")
|
|
85
|
+
result_expr = None
|
|
86
|
+
|
|
87
|
+
# Format arguments for repr string
|
|
88
|
+
args_repr = ", ".join(args_representations)
|
|
89
|
+
kwargs_repr = ", ".join(kwargs_representations)
|
|
90
|
+
|
|
91
|
+
if args_repr and kwargs_repr:
|
|
92
|
+
params_repr = f"{args_repr}, {kwargs_repr}"
|
|
93
|
+
elif args_repr:
|
|
94
|
+
params_repr = args_repr
|
|
95
|
+
elif kwargs_repr:
|
|
96
|
+
params_repr = kwargs_repr
|
|
97
|
+
else:
|
|
98
|
+
params_repr = ""
|
|
99
|
+
|
|
100
|
+
# Create the repr string for this method call
|
|
101
|
+
new_repr = f"{self._repr_str}.{method_name}({params_repr})"
|
|
102
|
+
|
|
103
|
+
# Methods that typically change the aggregation status or complexity
|
|
104
|
+
agg_methods = {
|
|
105
|
+
"sum", "mean", "min", "max", "median", "first", "last", "std", "var",
|
|
106
|
+
"count", "n_unique", "quantile", "implode", "explode"
|
|
107
|
+
}
|
|
108
|
+
# Methods that typically make expressions complex
|
|
109
|
+
complex_methods = {
|
|
110
|
+
"filter", "map", "shift", "fill_null", "fill_nan", "round", "abs", "alias",
|
|
111
|
+
"cast", "is_between", "over", "sort", "arg_sort", "arg_unique", "arg_min",
|
|
112
|
+
"arg_max", "rolling", "interpolate", "ewm_mean", "ewm_std", "ewm_var",
|
|
113
|
+
"backward_fill", "forward_fill", "rank", "diff", "clip", "dot", "mode",
|
|
114
|
+
"drop_nulls", "drop_nans", "take", "gather", "filter", "shift_and_fill"
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
# Determine new agg_func status
|
|
118
|
+
new_agg_func = method_name if method_name in agg_methods else self.agg_func
|
|
119
|
+
|
|
120
|
+
# Determine if this makes the expression complex
|
|
121
|
+
is_complex = self.is_complex or method_name in complex_methods
|
|
122
|
+
|
|
123
|
+
# Pass function sources to _create_next_expr
|
|
124
|
+
result = self._create_next_expr(
|
|
125
|
+
*args,
|
|
126
|
+
**kwargs,
|
|
127
|
+
result_expr=result_expr,
|
|
128
|
+
is_complex=is_complex,
|
|
129
|
+
method_name=method_name,
|
|
130
|
+
_function_sources=function_sources # Pass function sources
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
# Set the agg_func if needed
|
|
134
|
+
if new_agg_func != self.agg_func:
|
|
135
|
+
result.agg_func = new_agg_func
|
|
136
|
+
|
|
137
|
+
return result
|
|
138
|
+
|
|
139
|
+
return wrapper
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def add_expr_methods(cls: Type[ExprT]) -> Type[ExprT]:
|
|
143
|
+
"""
|
|
144
|
+
Class decorator that adds all polars Expr methods to a custom Expr class.
|
|
145
|
+
|
|
146
|
+
This adds the methods at class creation time, so they are visible to static type checkers.
|
|
147
|
+
Methods already defined in the class are not overwritten.
|
|
148
|
+
|
|
149
|
+
Parameters
|
|
150
|
+
----------
|
|
151
|
+
cls : Type[ExprT]
|
|
152
|
+
The class to which the methods will be added.
|
|
153
|
+
|
|
154
|
+
Returns
|
|
155
|
+
-------
|
|
156
|
+
Type[ExprT]
|
|
157
|
+
The modified class.
|
|
158
|
+
"""
|
|
159
|
+
# Get methods already defined in the class (including inherited methods)
|
|
160
|
+
existing_methods = set(dir(cls))
|
|
161
|
+
|
|
162
|
+
skip_methods = {
|
|
163
|
+
name for name in dir(pl.Expr)
|
|
164
|
+
if name.startswith('_') or isinstance(getattr(pl.Expr, name, None), property)
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
# Add all public Expr methods that don't already exist
|
|
168
|
+
for name in dir(pl.Expr):
|
|
169
|
+
if name in existing_methods or name in skip_methods:
|
|
170
|
+
continue
|
|
171
|
+
|
|
172
|
+
attr = getattr(pl.Expr, name)
|
|
173
|
+
if callable(attr):
|
|
174
|
+
if name in PASSTHROUGH_METHODS:
|
|
175
|
+
# Create passthrough method that marks the expression as not convertible to code
|
|
176
|
+
def create_passthrough_method(method_name, method_attr):
|
|
177
|
+
@wraps(method_attr)
|
|
178
|
+
def passthrough_method(self, *args, **kwargs):
|
|
179
|
+
if not hasattr(self, "expr") or self.expr is None:
|
|
180
|
+
raise ValueError(
|
|
181
|
+
f"Cannot call '{method_name}' on Expr with no underlying polars expression."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
# Collect function sources and build representations
|
|
185
|
+
function_sources = []
|
|
186
|
+
args_representations = []
|
|
187
|
+
kwargs_representations = []
|
|
188
|
+
convertable_to_code = True
|
|
189
|
+
|
|
190
|
+
# Process positional arguments
|
|
191
|
+
for i, arg in enumerate(args):
|
|
192
|
+
if callable(arg) and not isinstance(arg, type):
|
|
193
|
+
# Try to get function source
|
|
194
|
+
try:
|
|
195
|
+
source, is_module_level = _get_function_source(arg)
|
|
196
|
+
if source and hasattr(arg, '__name__') and arg.__name__ != '<lambda>':
|
|
197
|
+
|
|
198
|
+
function_sources.append(source)
|
|
199
|
+
# Use the function name in the representation
|
|
200
|
+
args_representations.append(arg.__name__)
|
|
201
|
+
arg.__repr__ = lambda: arg.__name__
|
|
202
|
+
|
|
203
|
+
else:
|
|
204
|
+
|
|
205
|
+
# Lambda or unnamed function - not convertible
|
|
206
|
+
logger.warning(
|
|
207
|
+
f"Warning: Using anonymous functions in {method_name} is not convertable to UI code")
|
|
208
|
+
logger.warning(f"Consider using defined functions (def abc(a, b, c): return ...), "
|
|
209
|
+
f"In a separate script")
|
|
210
|
+
convertable_to_code = False
|
|
211
|
+
args_representations.append(repr(arg))
|
|
212
|
+
except:
|
|
213
|
+
args_representations.append(repr(arg))
|
|
214
|
+
else:
|
|
215
|
+
args_representations.append(repr(arg))
|
|
216
|
+
|
|
217
|
+
# Process keyword arguments
|
|
218
|
+
for key, value in kwargs.items():
|
|
219
|
+
if callable(value) and not isinstance(value, type):
|
|
220
|
+
# Try to get function source
|
|
221
|
+
try:
|
|
222
|
+
source, is_module_level = _get_function_source(value)
|
|
223
|
+
if source and hasattr(value, '__name__') and value.__name__ != '<lambda>':
|
|
224
|
+
function_sources.append(source)
|
|
225
|
+
# Use the function name in the representation
|
|
226
|
+
kwargs_representations.append(f"{key}={value.__name__}")
|
|
227
|
+
else:
|
|
228
|
+
# Lambda or unnamed function - not convertible
|
|
229
|
+
convertable_to_code = False
|
|
230
|
+
kwargs_representations.append(f"{key}={repr(value)}")
|
|
231
|
+
except:
|
|
232
|
+
kwargs_representations.append(f"{key}={repr(value)}")
|
|
233
|
+
else:
|
|
234
|
+
kwargs_representations.append(f"{key}={repr(value)}")
|
|
235
|
+
|
|
236
|
+
# Call the underlying polars method
|
|
237
|
+
result_expr = getattr(self.expr, method_name)(*args, **kwargs)
|
|
238
|
+
# Build parameter string
|
|
239
|
+
args_repr = ", ".join(args_representations)
|
|
240
|
+
kwargs_repr = ", ".join(kwargs_representations)
|
|
241
|
+
|
|
242
|
+
if args_repr and kwargs_repr:
|
|
243
|
+
params_repr = f"{args_repr}, {kwargs_repr}"
|
|
244
|
+
elif args_repr:
|
|
245
|
+
params_repr = args_repr
|
|
246
|
+
elif kwargs_repr:
|
|
247
|
+
params_repr = kwargs_repr
|
|
248
|
+
else:
|
|
249
|
+
params_repr = ""
|
|
250
|
+
# Create a representation string
|
|
251
|
+
new_repr = f"{self._repr_str}.{method_name}({params_repr})"
|
|
252
|
+
# self._repr_str = new_repr
|
|
253
|
+
# Return a new expression with the convertable_to_code flag set appropriately
|
|
254
|
+
result = self._create_next_expr(
|
|
255
|
+
*args,
|
|
256
|
+
method_name=method_name,
|
|
257
|
+
result_expr=result_expr,
|
|
258
|
+
is_complex=True,
|
|
259
|
+
convertable_to_code=convertable_to_code,
|
|
260
|
+
_function_sources=function_sources, # Pass function sources
|
|
261
|
+
**kwargs
|
|
262
|
+
)
|
|
263
|
+
return result
|
|
264
|
+
|
|
265
|
+
return passthrough_method
|
|
266
|
+
|
|
267
|
+
setattr(cls, name, create_passthrough_method(name, attr))
|
|
268
|
+
else:
|
|
269
|
+
# Use standard wrapper for other methods
|
|
270
|
+
wrapped_method = create_expr_method_wrapper(name, attr)
|
|
271
|
+
setattr(cls, name, wrapped_method)
|
|
272
|
+
|
|
273
|
+
overlap = {
|
|
274
|
+
name for name in existing_methods
|
|
275
|
+
if name in dir(pl.Expr) and not name.startswith('_') and callable(getattr(pl.Expr, name))
|
|
276
|
+
}
|
|
277
|
+
if overlap:
|
|
278
|
+
logger.debug(f"Preserved existing methods in {cls.__name__}: {', '.join(sorted(overlap))}")
|
|
279
|
+
|
|
280
|
+
return cls
|