77parent_dir = os .path .join (current_dir , os .pardir )
88
99
10+ def _build_kwargs (sig : inspect .Signature , body : Dict [str , Any ]) -> Dict [str , Any ]:
11+ """Construct kwargs dict for a function signature given the incoming body.
12+
13+ This helper centralizes default handling and reduces nesting in the
14+ dynamically-generated strategy wrapper.
15+ """
16+ kwargs : Dict [str , Any ] = {}
17+ for param , param_obj in sig .parameters .items ():
18+ if param in body :
19+ kwargs [param ] = body [param ]
20+ else :
21+ if param_obj .default != inspect .Parameter .empty :
22+ kwargs [param ] = param_obj .default
23+ return kwargs
24+
25+
1026def create_dynamic_strategy (module_path : str , function_name : str ):
1127 """Dynamically create a strategy function that wraps a library function"""
1228
@@ -15,26 +31,15 @@ def dynamic_strategy(self, body: Dict[str, Any]):
1531 # Import the module and get the function
1632 module = importlib .import_module (module_path )
1733 func = getattr (module , function_name )
18-
19- # Get function signature to extract parameters
34+
35+ # Build kwargs for the function call (extracted to helper to reduce complexity)
2036 sig = inspect .signature (func )
21- parameters = list (sig .parameters .keys ())
22-
23- # Extract arguments from body, handling defaults
24- kwargs = {}
25- for param in parameters :
26- if param in body :
27- kwargs [param ] = body [param ]
28- else :
29- # Check if parameter has a default value
30- param_obj = sig .parameters [param ]
31- if param_obj .default != inspect .Parameter .empty :
32- kwargs [param ] = param_obj .default
33-
34- # Call the function
37+ kwargs = _build_kwargs (sig , body )
38+
39+ # Call the function and publish result
3540 result = func (** kwargs )
3641 self .publish (result )
37-
42+
3843 except Exception as e :
3944 _publish_error (self , e , function_name , module_path )
4045
@@ -43,6 +48,16 @@ def dynamic_strategy(self, body: Dict[str, Any]):
4348 return dynamic_strategy
4449
4550
51+ # Global error publisher so dynamic strategies can report import/call errors
52+ def _publish_error (publisher , exc : Exception , fn : str , mod : str ):
53+ """Publish a consistent error payload for failed dynamic strategies."""
54+ try :
55+ publisher .publish ({"error" : str (exc ), "function" : fn , "module" : mod })
56+ except Exception :
57+ # Ensure exceptions during error reporting don't bubble up
58+ print ("Failed to publish error" , exc )
59+
60+
4661def get_all_library_functions ():
4762 """Discover all functions in the data-scratch-library"""
4863 functions = {}
@@ -95,6 +110,13 @@ def get_all_library_functions():
95110 E1008 = '.e1008_rescaling'
96111 E1009 = '.e1009_dimensionality_reduction'
97112
113+ # Fully qualified working-data module constants to avoid repeated concatenations
114+ WORKING_E1004 = WORKING_DATA_MODULE + E1004
115+ WORKING_E1006 = WORKING_DATA_MODULE + E1006
116+ WORKING_E1007 = WORKING_DATA_MODULE + E1007
117+ WORKING_E1008 = WORKING_DATA_MODULE + E1008
118+ WORKING_E1009 = WORKING_DATA_MODULE + E1009
119+
98120 for module_const , fnames in grouped .items ():
99121 for fn in fnames :
100122 module_mappings [fn ] = module_const
@@ -110,37 +132,89 @@ def get_all_library_functions():
110132 'correlation_matrix' : WORKING_DATA_MODULE + '.e1003_multivariate' ,
111133 'random_normal' : WORKING_DATA_MODULE + '.e1002_bivariate' ,
112134 'demo_deque' : WORKING_DATA_MODULE + '.e1000_circular_buffer' ,
113- 'create_stock_price_namedtuple' : WORKING_DATA_MODULE + E1004 ,
114- 'create_stock_price' : WORKING_DATA_MODULE + E1004 ,
115- 'create_price_dict' : WORKING_DATA_MODULE + E1004 ,
116- 'parse_row' : WORKING_DATA_MODULE + E1006 ,
117- 'try_parse_row' : WORKING_DATA_MODULE + E1006 ,
118- 'process_csv' : WORKING_DATA_MODULE + E1006 ,
119- 'max_stock_price' : WORKING_DATA_MODULE + E1007 ,
120- 'max_prices_by_symbol' : WORKING_DATA_MODULE + E1007 ,
121- 'pct_change' : WORKING_DATA_MODULE + E1007 ,
122- 'day_over_day_changes' : WORKING_DATA_MODULE + E1007 ,
123- 'group_prices_by_symbol' : WORKING_DATA_MODULE + E1007 ,
124- 'find_largest_and_smallest_changes' : WORKING_DATA_MODULE + E1007 ,
125- 'average_daily_change_by_month' : WORKING_DATA_MODULE + E1007 ,
135+ 'create_stock_price_namedtuple' : WORKING_E1004 ,
136+ 'create_stock_price' : WORKING_E1004 ,
137+ 'create_price_dict' : WORKING_E1004 ,
138+ 'parse_row' : WORKING_E1006 ,
139+ 'try_parse_row' : WORKING_E1006 ,
140+ 'process_csv' : WORKING_E1006 ,
141+ 'max_stock_price' : WORKING_E1007 ,
142+ 'max_prices_by_symbol' : WORKING_E1007 ,
143+ 'pct_change' : WORKING_E1007 ,
144+ 'day_over_day_changes' : WORKING_E1007 ,
145+ 'group_prices_by_symbol' : WORKING_E1007 ,
146+ 'find_largest_and_smallest_changes' : WORKING_E1007 ,
147+ 'average_daily_change_by_month' : WORKING_E1007 ,
126148 'create_stock_price_dataclass' : WORKING_DATA_MODULE + '.e1005_dataclass' ,
127- 'vector_mean' : WORKING_DATA_MODULE + E1008 ,
128- 'standard_deviation' : WORKING_DATA_MODULE + E1008 ,
129- 'scale' : WORKING_DATA_MODULE + E1008 ,
130- 'rescale' : WORKING_DATA_MODULE + E1008 ,
131- 'simple_trange' : WORKING_DATA_MODULE + E1009 ,
132- 'de_mean' : WORKING_DATA_MODULE + E1009 ,
133- 'direction' : WORKING_DATA_MODULE + E1009 ,
134- 'directional_variance' : WORKING_DATA_MODULE + E1009 ,
135- 'directional_variance_gradient' : WORKING_DATA_MODULE + E1009 ,
136- 'first_principal_component' : WORKING_DATA_MODULE + E1009 ,
137- 'project' : WORKING_DATA_MODULE + E1009 ,
138- 'remove_projection_from_vector' : WORKING_DATA_MODULE + E1009 ,
139- 'remove_projection' : WORKING_DATA_MODULE + E1009 ,
149+ 'vector_mean' : WORKING_E1008 ,
150+ 'standard_deviation' : WORKING_E1008 ,
151+ 'scale' : WORKING_E1008 ,
152+ 'rescale' : WORKING_E1008 ,
153+ 'simple_trange' : WORKING_E1009 ,
154+ 'de_mean' : WORKING_E1009 ,
155+ 'direction' : WORKING_E1009 ,
156+ 'directional_variance' : WORKING_E1009 ,
157+ 'directional_variance_gradient' : WORKING_E1009 ,
158+ 'first_principal_component' : WORKING_E1009 ,
159+ 'project' : WORKING_E1009 ,
160+ 'remove_projection_from_vector' : WORKING_E1009 ,
161+ 'remove_projection' : WORKING_E1009 ,
140162 }
141163
142164 module_mappings .update (working_map )
143165
166+ # Dynamic discovery: scan the 'dsl' package for additional functions that should
167+ # be exposed as dynamic strategies. This adds any top-level functions found
168+ # under the dsl package to module_mappings if not already present.
169+ try :
170+ import pkgutil
171+ import dsl
172+
173+ for finder , mod_name , ispkg in pkgutil .walk_packages (dsl .__path__ , dsl .__name__ + '.' ):
174+ try :
175+ mod = importlib .import_module (mod_name )
176+ except Exception :
177+ # Ignore individual import failures for optional modules
178+ continue
179+
180+ for obj_name , obj in inspect .getmembers (mod , inspect .isfunction ):
181+ # Only add if not already explicitly mapped
182+ if obj_name not in module_mappings :
183+ module_mappings [obj_name ] = mod_name
184+ except ImportError :
185+ # dsl package not available in this environment; try a filesystem
186+ # fallback to discover functions from the repository's dsl folder.
187+ try :
188+ import ast
189+ # Candidate path relative to this module: go up to project src and
190+ # then into data-scratch-library/dsl
191+ candidate = os .path .join (current_dir , os .pardir , os .pardir , os .pardir , 'data-scratch-library' , 'dsl' )
192+ candidate = os .path .normpath (candidate )
193+ if os .path .isdir (candidate ):
194+ for root , _ , files in os .walk (candidate ):
195+ for fname in files :
196+ if not fname .endswith ('.py' ):
197+ continue
198+ fpath = os .path .join (root , fname )
199+ try :
200+ with open (fpath , 'r' , encoding = 'utf-8' ) as fh :
201+ src = fh .read ()
202+ parsed = ast .parse (src )
203+ except Exception :
204+ continue
205+
206+ # derive module name from file path relative to candidate
207+ rel = os .path .relpath (fpath , candidate )
208+ mod_name = 'dsl.' + rel .replace (os .sep , '.' )[:- 3 ] # strip .py
209+
210+ for node in parsed .body :
211+ if isinstance (node , ast .FunctionDef ):
212+ if node .name not in module_mappings :
213+ module_mappings [node .name ] = mod_name
214+ except Exception :
215+ # If filesystem fallback fails, continue silently — discovery is best-effort
216+ pass
217+
144218 # Utility functions from crash course
145219 module_mappings ['mysqrt' ] = 'dsl.c02_crash_course.e0203_functions'
146220 module_mappings ['strength' ] = 'dsl.c06_probability.e0604_binom'
@@ -149,9 +223,6 @@ def get_all_library_functions():
149223 # string literals across the module (reduces S1192 findings)
150224 WARNING_IMPORT_FMT = 'Warning: Could not import {} from {}: {}'
151225
152- def _publish_error (publisher , exc : Exception , fn : str , mod : str ):
153- publisher .publish ({"error" : str (exc ), "function" : fn , "module" : mod })
154-
155226 # Create dynamic strategies for all functions
156227 for func_name , module_path in module_mappings .items ():
157228 try :
0 commit comments