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.

Files changed (100) hide show
  1. flowfile/__init__.py +2 -1
  2. flowfile/api.py +5 -3
  3. flowfile/web/__init__.py +3 -0
  4. flowfile/web/static/assets/{AirbyteReader-cb0c1d4a.js → AirbyteReader-2b1cf2d8.js} +10 -9
  5. flowfile/web/static/assets/{CrossJoin-a514fa59.js → CrossJoin-cc3ab73c.js} +8 -8
  6. flowfile/web/static/assets/{DatabaseConnectionSettings-f2cecf33.js → DatabaseConnectionSettings-307c4652.js} +2 -2
  7. flowfile/web/static/assets/{DatabaseManager-83ee3c98.js → DatabaseManager-69faa6e1.js} +10 -6
  8. flowfile/web/static/assets/{DatabaseReader-dc0c6881.js → DatabaseReader-e4134cd0.js} +9 -9
  9. flowfile/web/static/assets/{DatabaseWriter-5afe9f8d.js → DatabaseWriter-d32d75b1.js} +9 -9
  10. flowfile/web/static/assets/{ExploreData-c7ee19cf.js → ExploreData-5eb48389.js} +18639 -18629
  11. flowfile/web/static/assets/{ExternalSource-17b23a01.js → ExternalSource-29489051.js} +8 -21
  12. flowfile/web/static/assets/{Filter-90856b4f.js → Filter-031332bb.js} +9 -9
  13. flowfile/web/static/assets/{Formula-38b71e9e.js → Formula-3b900540.js} +15 -15
  14. flowfile/web/static/assets/{Formula-d60a74f4.css → Formula-b8cefc31.css} +4 -4
  15. flowfile/web/static/assets/{FuzzyMatch-d0f1fe81.js → FuzzyMatch-dee31153.js} +9 -9
  16. flowfile/web/static/assets/{GraphSolver-0c86bbc6.js → GraphSolver-ca74eb47.js} +5 -5
  17. flowfile/web/static/assets/{GroupBy-f2772e9f.js → GroupBy-081b6591.js} +8 -7
  18. flowfile/web/static/assets/{Join-bc3e1cf7.js → Join-b467376f.js} +11 -10
  19. flowfile/web/static/assets/{ManualInput-03aa0245.js → ManualInput-ffffb80a.js} +11 -8
  20. flowfile/web/static/assets/{Output-5b35eee8.js → Output-9a87d4ba.js} +4 -4
  21. flowfile/web/static/assets/{Pivot-7164087c.js → Pivot-ee3e6093.js} +8 -7
  22. flowfile/web/static/assets/{PolarsCode-3abf6507.js → PolarsCode-03921254.js} +13 -11
  23. flowfile/web/static/assets/{PopOver-b37ff9be.js → PopOver-3bdf8951.js} +1 -1
  24. flowfile/web/static/assets/{Read-65966a3e.js → Read-67fee3a0.js} +6 -6
  25. flowfile/web/static/assets/{RecordCount-c66c6d6d.js → RecordCount-a2acd02d.js} +7 -6
  26. flowfile/web/static/assets/{RecordId-826dc095.js → RecordId-0c8bcd77.js} +10 -8
  27. flowfile/web/static/assets/{Sample-4ed555c8.js → Sample-60594a3a.js} +7 -6
  28. flowfile/web/static/assets/{SecretManager-eac1e97d.js → SecretManager-bbcec2ac.js} +2 -2
  29. flowfile/web/static/assets/{Select-085f05cc.js → Select-9540e6ca.js} +8 -8
  30. flowfile/web/static/assets/{SettingsSection-1f5e79c1.js → SettingsSection-48f28104.js} +1 -1
  31. flowfile/web/static/assets/{Sort-3e6cb414.js → Sort-6dbe3633.js} +6 -6
  32. flowfile/web/static/assets/{TextToRows-606349bc.js → TextToRows-27aab4a8.js} +18 -13
  33. flowfile/web/static/assets/{UnavailableFields-b41976ed.js → UnavailableFields-8143044b.js} +2 -2
  34. flowfile/web/static/assets/{Union-fca91665.js → Union-52460248.js} +7 -6
  35. flowfile/web/static/assets/{Unique-a59f830e.js → Unique-f6962644.js} +8 -8
  36. flowfile/web/static/assets/{Unpivot-c3815565.js → Unpivot-1ff1e938.js} +5 -5
  37. flowfile/web/static/assets/{api-22b338bd.js → api-3b345d92.js} +1 -1
  38. flowfile/web/static/assets/{designer-e5bbe26f.js → designer-4736134f.js} +72 -42
  39. flowfile/web/static/assets/{documentation-08045cf2.js → documentation-b9545eba.js} +1 -1
  40. flowfile/web/static/assets/{dropDown-5e7e9a5a.js → dropDown-d5a4014c.js} +1 -1
  41. flowfile/web/static/assets/{dropDownGeneric-50a91b99.js → dropDownGeneric-1f4e32ec.js} +2 -2
  42. flowfile/web/static/assets/{fullEditor-705c6ccb.js → fullEditor-f4791c23.js} +3 -3
  43. flowfile/web/static/assets/{genericNodeSettings-65587f20.js → genericNodeSettings-1d456350.js} +3 -3
  44. flowfile/web/static/assets/{index-552863fd.js → index-f25c9283.js} +2608 -1570
  45. flowfile/web/static/assets/{nodeTitle-cf9bae3c.js → nodeTitle-cad6fd9d.js} +3 -3
  46. flowfile/web/static/assets/{secretApi-3ad510e1.js → secretApi-01f07e2c.js} +1 -1
  47. flowfile/web/static/assets/{selectDynamic-bd644891.js → selectDynamic-f46a4e3f.js} +3 -3
  48. flowfile/web/static/assets/{vue-codemirror.esm-dd17b478.js → vue-codemirror.esm-eb98fc8b.js} +15 -14
  49. flowfile/web/static/assets/{vue-content-loader.es-6b36f05e.js → vue-content-loader.es-860c0380.js} +1 -1
  50. flowfile/web/static/index.html +1 -1
  51. {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/METADATA +1 -3
  52. {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/RECORD +97 -88
  53. flowfile_core/configs/__init__.py +15 -4
  54. flowfile_core/configs/node_store/nodes.py +2 -4
  55. flowfile_core/configs/settings.py +5 -3
  56. flowfile_core/configs/utils.py +18 -0
  57. flowfile_core/flowfile/FlowfileFlow.py +84 -29
  58. flowfile_core/flowfile/database_connection_manager/db_connections.py +1 -1
  59. flowfile_core/flowfile/flow_data_engine/flow_data_engine.py +55 -18
  60. flowfile_core/flowfile/flow_data_engine/flow_file_column/main.py +42 -9
  61. flowfile_core/flowfile/flow_data_engine/flow_file_column/utils.py +42 -3
  62. flowfile_core/flowfile/flow_data_engine/polars_code_parser.py +34 -2
  63. flowfile_core/flowfile/flow_data_engine/sample_data.py +25 -7
  64. flowfile_core/flowfile/flow_data_engine/subprocess_operations/subprocess_operations.py +4 -3
  65. flowfile_core/flowfile/flow_data_engine/utils.py +1 -0
  66. flowfile_core/flowfile/flow_graph_utils.py +320 -0
  67. flowfile_core/flowfile/flow_node/flow_node.py +2 -1
  68. flowfile_core/flowfile/sources/external_sources/airbyte_sources/models.py +2 -2
  69. flowfile_core/flowfile/sources/external_sources/custom_external_sources/__init__.py +0 -1
  70. flowfile_core/flowfile/sources/external_sources/sql_source/sql_source.py +1 -1
  71. flowfile_core/flowfile/utils.py +34 -3
  72. flowfile_core/main.py +2 -3
  73. flowfile_core/routes/secrets.py +1 -1
  74. flowfile_core/schemas/input_schema.py +12 -14
  75. flowfile_core/schemas/transform_schema.py +25 -47
  76. flowfile_frame/__init__.py +11 -4
  77. flowfile_frame/adding_expr.py +280 -0
  78. flowfile_frame/config.py +9 -0
  79. flowfile_frame/expr.py +301 -83
  80. flowfile_frame/expr.pyi +2174 -0
  81. flowfile_frame/expr_name.py +258 -0
  82. flowfile_frame/flow_frame.py +616 -627
  83. flowfile_frame/flow_frame.pyi +336 -0
  84. flowfile_frame/flow_frame_methods.py +617 -0
  85. flowfile_frame/group_frame.py +89 -42
  86. flowfile_frame/join.py +1 -2
  87. flowfile_frame/lazy.py +704 -0
  88. flowfile_frame/lazy_methods.py +201 -0
  89. flowfile_frame/list_name_space.py +324 -0
  90. flowfile_frame/selectors.py +3 -0
  91. flowfile_frame/series.py +70 -0
  92. flowfile_frame/utils.py +80 -4
  93. flowfile/web/static/assets/GoogleSheet-854294a4.js +0 -2616
  94. flowfile/web/static/assets/GoogleSheet-92084da7.css +0 -233
  95. flowfile_core/flowfile/sources/external_sources/custom_external_sources/google_sheet.py +0 -74
  96. {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/LICENSE +0 -0
  97. {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/WHEEL +0 -0
  98. {flowfile-0.3.1.2.dist-info → flowfile-0.3.3.dist-info}/entry_points.txt +0 -0
  99. /flowfile_core/{secrets → secret_manager}/__init__.py +0 -0
  100. /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
- @dataclass
170
- class CrossJoinInput:
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
- @staticmethod
247
- def parse_select(select: List[SelectInput] | List[str] | List[Dict]) -> SelectInputs:
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):
@@ -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.flow_frame import ( # noqa: F401
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
@@ -0,0 +1,9 @@
1
+ import logging
2
+
3
+ logging.basicConfig(
4
+ level=logging.INFO,
5
+ format='[%(levelname)s] %(message)s'
6
+ )
7
+
8
+ # Create and export the logger
9
+ logger = logging.getLogger('flow_frame')