6262except ImportError :
6363 MiniRacer = None
6464
65-
66- class _JsThreadContext :
67- def __init__ (self ):
68- self ._local = threading .local ()
69-
70- def get_funcs (self ):
71- if not hasattr (self ._local , 'funcs' ):
72- self ._local .funcs = {}
73- return self ._local .funcs
74-
75- def __getstate__ (self ):
76- return {}
77-
78- def __setstate__ (self , state ):
79- self ._local = threading .local ()
80-
81-
82- _js_contexts = _JsThreadContext ()
83-
8465_JS_DATE_ISO_REGEX = re .compile (
8566 r'^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}\.\d{3}Z$' )
8667
@@ -240,10 +221,90 @@ def js_to_py(obj):
240221 return obj
241222
242223
224+ class JsFilterDoFn (beam .DoFn ):
225+ def __init__ (self , udf_code , function_name ):
226+ self .udf_code = udf_code
227+ self .function_name = function_name
228+ self .ctx = None
229+
230+ def setup (self ):
231+ self .ctx = MiniRacer ()
232+ self .ctx .eval (self .udf_code )
233+
234+ def process (self , element ):
235+ row_as_dict = py_value_to_js_dict (element )
236+ result = self .ctx .call (self .function_name , row_as_dict )
237+ result = js_to_py (result )
238+ if result :
239+ yield element
240+
241+
242+ class JsMapToFieldsDoFn (beam .DoFn ):
243+ def __init__ (self , fields , original_fields , input_schema ):
244+ self .fields = fields
245+ self .original_fields = original_fields
246+ self .input_schema = input_schema
247+ self .ctx = None
248+ self .field_funcs = {}
249+ self .passthrough_fields = []
250+
251+ def setup (self ):
252+ self .ctx = MiniRacer ()
253+ script = []
254+ for name , expr in self .fields .items ():
255+ if isinstance (expr , str ) and expr in self .input_schema :
256+ self .passthrough_fields .append ((name , expr ))
257+ continue
258+
259+ if isinstance (expr , str ):
260+ expr = {'expression' : expr }
261+
262+ if 'expression' in expr :
263+ e = expr ['expression' ]
264+ code = f"var func_{ name } = (__row__) => {{ " + " " .join ([
265+ f"const { n } = __row__.{ n } ;" for n in self .original_fields if n in e
266+ ]) + f" return ({ e } ); }}"
267+ script .append (code )
268+ self .field_funcs [name ] = f"func_{ name } "
269+ elif 'callable' in expr :
270+ code = f"var func_{ name } = { expr ['callable' ]} "
271+ script .append (code )
272+ self .field_funcs [name ] = f"func_{ name } "
273+ elif 'path' in expr and 'name' in expr :
274+ path = expr ['path' ]
275+ func_name = expr ['name' ]
276+ udf_code = FileSystems .open (path ).read ().decode ()
277+ script .append (udf_code )
278+ self .field_funcs [name ] = func_name
279+
280+ if script :
281+ self .ctx .eval ("\n " .join (script ))
282+
283+ def process (self , element ):
284+ row_as_dict = py_value_to_js_dict (element )
285+ result_dict = {}
286+
287+ # Handle passthrough fields
288+ for name , src in self .passthrough_fields :
289+ result_dict [name ] = row_as_dict .get (src )
290+
291+ # Handle JS fields
292+ for name , func_name in self .field_funcs .items ():
293+ res = self .ctx .call (func_name , row_as_dict )
294+ result_dict [name ] = js_to_py (res )
295+
296+ yield dicts_to_rows (result_dict )
297+
298+
243299# TODO(yaml) Consider adding optional language version parameter to support
244300# ECMAScript 5 and 6
245- def _expand_javascript_mapping_func (
246- original_fields , expression = None , callable = None , path = None , name = None ):
301+ def _get_javascript_udf_code (
302+ original_fields ,
303+ function_name = "func" ,
304+ expression = None ,
305+ callable = None ,
306+ path = None ,
307+ name = None ):
247308
248309 if MiniRacer is None :
249310 raise ValueError (
@@ -255,39 +316,17 @@ def _expand_javascript_mapping_func(
255316 if not path .endswith ('.js' ):
256317 raise ValueError (f'File "{ path } " is not a valid .js file.' )
257318 udf_code = FileSystems .open (path ).read ().decode ()
319+ return udf_code , name
258320 elif expression :
259- udf_code = f"var func = (__row__) => {{ " + " " .join ([
321+ udf_code = f"var { function_name } = (__row__) => {{ " + " " .join ([
260322 f"const { n } = __row__.{ n } ;" for n in original_fields if n in expression
261323 ]) + f" return ({ expression } ); }}"
324+ return udf_code , function_name
262325 elif callable :
263- udf_code = f"var func = { callable } "
264-
265- udf_key = str (uuid .uuid4 ())
266-
267- def js_wrapper (row ):
268- funcs = _js_contexts .get_funcs ()
269-
270- if udf_key not in funcs :
271- ctx = MiniRacer ()
272- ctx .eval (udf_code )
273- # We use ctx.call for efficiency.
274- # Note: This might return strings for Date objects instead of datetime.
275- if expression or callable :
276- funcs [udf_key ] = lambda x : ctx .call ("func" , x )
277- else :
278- funcs [udf_key ] = lambda x : ctx .call (name , x )
279-
280- func = funcs [udf_key ]
281- row_as_dict = py_value_to_js_dict (row )
282- try :
283- result = func (row_as_dict )
284- except Exception as exn :
285- raise RuntimeError (
286- f"Error evaluating JavaScript expression: { exn } " ) from exn
287- result = js_to_py (result )
288- return dicts_to_rows (result )
289-
290- return js_wrapper
326+ udf_code = f"var { function_name } = { callable } "
327+ return udf_code , function_name
328+ else :
329+ raise ValueError ("Must specify expression, callable, or path." )
291330
292331
293332def _expand_python_mapping_func (
@@ -394,14 +433,10 @@ def _as_callable(original_fields, expr, transform_name, language, input_schema):
394433 explicit_type = expr .pop ('output_type' , None )
395434 _check_mapping_arguments (transform_name , ** expr )
396435
397- if language == "javascript" :
398- func = _expand_javascript_mapping_func (original_fields , ** expr )
399- elif language in ("python" , "generic" , None ):
436+ if language in ("python" , "generic" , None ):
400437 func = _expand_python_mapping_func (original_fields , ** expr )
401438 else :
402- raise ValueError (
403- f'Unknown language for mapping transform: { language } . '
404- 'Supported languages are "javascript" and "python."' )
439+ raise ValueError (f'Language { language } not supported in this context.' )
405440
406441 if explicit_type :
407442 if isinstance (explicit_type , str ):
@@ -640,8 +675,17 @@ def _PyJsFilter(
640675 error_handling: Whether and where to output records that throw errors when
641676 the above expressions are evaluated.
642677 """ # pylint: disable=line-too-long
643- keep_fn = _as_callable_for_pcoll (pcoll , keep , "keep" , language or 'generic' )
644- return pcoll | beam .Filter (keep_fn )
678+ if language == 'javascript' :
679+ if isinstance (keep , str ):
680+ keep = {'expression' : keep }
681+ udf_code , function_name = _get_javascript_udf_code (
682+ [f .name for f in schema_from_element_type (pcoll .element_type ).fields ],
683+ ** keep
684+ )
685+ return pcoll | beam .ParDo (JsFilterDoFn (udf_code , function_name ))
686+ else :
687+ keep_fn = _as_callable_for_pcoll (pcoll , keep , "keep" , language or 'generic' )
688+ return pcoll | beam .Filter (keep_fn )
645689
646690
647691def is_expr (v ):
@@ -713,10 +757,12 @@ def _PyJsMapToFields(
713757 """ # pylint: disable=line-too-long
714758 input_schema , fields = normalize_fields (
715759 pcoll , fields , drop or (), append , language = language or 'generic' )
760+ original_fields = list (input_schema .keys ())
761+
716762 if language == 'javascript' :
717763 options .YamlOptions .check_enabled (pcoll .pipeline , 'javascript' )
718-
719- original_fields = list ( input_schema . keys ( ))
764+ return pcoll | beam . ParDo (
765+ JsMapToFieldsDoFn ( fields , original_fields , input_schema ))
720766
721767 return pcoll | beam .Select (
722768 ** {
0 commit comments