importastimportinspectimporttextwrapimporttypesimportoperatorimporttokenizeimportwarningsfromtypingimportCallable,Union,Any,Optional,Iterable,Type,Tuple,Dict,Listfromcopyimportdeepcopyfromfunctoolsimportwrapsfromonetick.py.backportsimportastunparse,cached_propertyfromcollectionsimportdequefromcontextlibimportcontextmanagerfrom..importtypesasottfrom.column_operations.baseimport_Operationfrom.columnimport_Columnfrom.lambda_objectimport_EmulateObject,_LambdaIfElse,_default_by_type,_EmulateStateVars,_CompareTrackScopefrom._internal._state_objectsimport(_TickSequence,_TickSequenceTickBase,_TickListTick,_TickSetTick,_TickDequeTick,_DynamicTick)classStatic:""" Class for declaring static local variable in per-tick script. Static variables are defined once and save their values between arrival of the input ticks. """def__init__(self,value):self.value=value# these functions needed mostly for lintersdef__getattr__(self,item):returnself.value.__getattr__(item)def__getitem__(self,item):returnoperator.getitem(self.value,item)def__setitem__(self,key,value):returnoperator.setitem(self.value,key,value)classLocalVariable(_Operation):""" Class for inner representation of local variable in per-tick script. Only simple values are supported, tick sequences are represented by another class. """def__init__(self,name,dtype=None):super().__init__(op_str=f'LOCAL::{name}',dtype=dtype)self.name=name
[docs]classTickDescriptorFields(_TickSequence):""" Class for declaring tick descriptor fields in per-tick script. Can only be iterated, doesn't have methods and parameters. See also -------- :py:class:`TickDescriptorField <onetick.py.core.per_tick_script.TickDescriptorField>` Examples -------- >>> t = otp.Tick(A=1) >>> def fun(tick): ... for field in otp.tick_descriptor_fields(): ... tick['NAME'] = field.get_name() >>> t = t.script(fun) >>> otp.run(t) Time A NAME 0 2003-12-01 1 A """def__init__(self):passdef__str__(self):return'LOCAL::INPUT_TICK_DESCRIPTOR_FIELDS'@propertydef_tick_class(self):returnTickDescriptorField
[docs]deftick_list_tick():""" Can be used only in per-tick script function to define a tick list tick local variable. Tick list ticks can be used with some methods of tick lists :py:class:`onetick.py.state.tick_list`. See also -------- :py:class:`onetick.py.state.tick_list`. Note ---- Note that :py:class:`onetick.py.static` value is returned. You should not define tick variable as static manually. Examples -------- >>> def fun(tick): ... t = otp.tick_list_tick() ... tick.state_vars['LIST'].push_back(t) """returnStatic(_TickListTick(None))
[docs]deftick_set_tick():""" Can be used only in per-tick script function to define a tick set tick local variable. Tick set ticks can be used with some methods of tick sets :py:class:`onetick.py.state.tick_set`. See also -------- :py:class:`onetick.py.state.tick_set`. Note ---- Note that :py:class:`onetick.py.static` value is returned. You should not define tick variable as static manually. Examples -------- >>> def fun(tick): ... t = otp.tick_set_tick() ... if tick.state_vars['SET'].find(t, -1): ... tick['RES'] = '-1' """returnStatic(_TickSetTick(None))
[docs]deftick_deque_tick():""" Can be used only in per-tick script function to define a tick deque tick local variable. Tick deque ticks can be used with some methods of tick deques :py:class:`onetick.py.state.tick_deque`. See also -------- :py:class:`onetick.py.state.tick_deque`. Note ---- Note that :py:class:`onetick.py.static` value is returned. You should not define tick variable as static manually. Examples -------- >>> def fun(tick): ... t = otp.tick_deque_tick() ... tick.state_vars['DEQUE'].get_tick(0, t) """returnStatic(_TickDequeTick(None))
[docs]defdynamic_tick():""" Can be used only in per-tick script function to define a dynamic tick local variable. Dynamic ticks can be used with some methods of all tick sequences. See also -------- :py:class:`onetick.py.state.tick_list` :py:class:`onetick.py.state.tick_set` :py:class:`onetick.py.state.tick_deque` Note ---- Note that :py:class:`onetick.py.static` value is returned. You should not define tick variable as static manually. Examples -------- >>> def fun(tick): ... t = otp.dynamic_tick() ... t['X'] = tick['SUM'] """returnStatic(_DynamicTick(None))
[docs]classTickDescriptorField(_TickSequenceTickBase):""" Tick descriptor field object. Can be accessed only while iterating over :py:class:`otp.tick_descriptor_fields <onetick.py.core.per_tick_script.TickDescriptorFields>` in per-tick script. Examples -------- >>> t = otp.Tick(A=2, B='B', C=1.2345) >>> def fun(tick): ... tick['NAMES'] = '' ... tick['TYPES'] = '' ... tick['SIZES'] = '' ... for field in otp.tick_descriptor_fields(): ... tick['NAMES'] += field.get_name() + ',' ... tick['TYPES'] += field.get_type() + ',' ... tick['SIZES'] += field.get_size().apply(str) + ',' >>> t = t.script(fun) >>> otp.run(t) Time A B C NAMES TYPES SIZES 0 2003-12-01 2 B 1.2345 A,B,C, long,string,double, 8,64,8, """_definition='TICK_DESCRIPTOR_FIELD'
[docs]defget_field_name(self):""" Get the name of the field. Returns ------- onetick.py.Operation """return_Operation(op_str=f'{self}.GET_FIELD_NAME()',dtype=str)
[docs]defget_name(self):""" Get the name of the field. Returns ------- onetick.py.Operation """returnself.get_field_name()
[docs]defget_size(self):""" Get the size of the type of the field. Returns ------- onetick.py.Operation """return_Operation(op_str=f'{self}.GET_SIZE()',dtype=int)
[docs]defget_type(self):""" Get the name of the type of the field. Returns ------- onetick.py.Operation """return_Operation(op_str=f'{self}.GET_TYPE()',dtype=str)
classExpression:""" Class to save per-tick-script expressions along with their possible values. Parameters ---------- expr string expression that will be saved to per tick script values: values that this expression can take. For example, bool operation can take many values. lhs: True if expression is left hand expression. In this case value of expression must be callable. Calling it with right hand expression value as an argument should be the same as execute the whole expression. """def__init__(self,*values:Any,expr:Optional[str]=None,lhs:bool=False):self.values=valuesself._expr=exprself.lhs=lhsifself.lhs:assertisinstance(self.value,Callable)assertself.expr@propertydefexpr(self):ifself._expr:returnself._exprifself.is_emulator:self._expr='LOCAL::INPUT_TICK'elifself.is_column:self._expr=str(self.value)elifself.values:self._expr=self.value_to_onetick(self.value)returnself._expr@propertydefvalue(self):length=len(self.values)iflength==0:raiseValueError(f"Expression '{self}' doesn't have values.")iflength>1:raiseValueError(f"Expression '{self}' have more than one value.")returnself.values[0]@cached_propertydefdtype(self):returnott.get_type_by_objects(self.values)@propertydefis_emulator(self)->bool:try:returnisinstance(self.value,_EmulateObject)exceptValueError:returnFalse@propertydefis_state_vars(self)->bool:try:returnisinstance(self.value,_EmulateStateVars)exceptValueError:returnFalse@propertydefis_static(self)->bool:try:returnisinstance(self.value,Static)exceptValueError:returnFalse@propertydefis_dynamic_tick(self)->bool:try:returntype(self.value)is_DynamicTickexceptValueError:returnFalse@propertydefis_tick(self)->bool:try:returnisinstance(self.value,_TickSequenceTickBase)exceptValueError:returnFalse@propertydefis_column(self)->bool:try:returnisinstance(self.value,_Column)exceptValueError:returnFalse@propertydefis_local_variable(self)->bool:try:returnisinstance(self.value,LocalVariable)exceptValueError:returnFalse@propertydefis_operation(self)->bool:try:returnisinstance(self.value,_Operation)exceptValueError:returnFalse@propertydefpredefined(self)->bool:"""Check if the value of expression is known before the execution of query"""returnnotself.is_operation@propertydefexpressible(self)->bool:returnbool(self.expr)def__str__(self):ifnotself.expressible:raiseValueError("This Expression can't be expressed in OneTick or is undefined yet")returnself.exprdefconvert_to_operation(self):""" Convert otp.Column to otp.Operation. Needed to convert expressions like: if tick['X']: to if (X != 0) { """ifself.is_column:self.values=[self.value._make_python_way_bool_expression()]self._expr=str(self.value)@staticmethoddefvalue_to_onetick(value:Union[str,int,float,bool,None,_Operation])->str:""" Python value will be converted accordingly to OneTick syntax (lowercase boolean values, string in quotes, etc.) """ifvalueisNone:returnstr(ott.nan)ifisinstance(value,bool):returnstr(value).lower()returnott.value2str(value)classCaseOperatorParser:""" Class with methods to convert ast operators to their string or python representations. Only ast operators that can be used in OneTick's CASE function are accepted. """@staticmethoddefpy_operator(op:Union[ast.operator,ast.cmpop,ast.unaryop,ast.boolop])->Callable:""" Convert ast operator to python function for this operator. Parameters ---------- op ast operator object """return{# binaryast.Add:operator.add,ast.Sub:operator.sub,ast.Mult:operator.mul,ast.Div:operator.truediv,ast.BitAnd:operator.and_,ast.BitOr:operator.or_,ast.Mod:operator.mod,# unaryast.UAdd:operator.pos,ast.USub:operator.neg,ast.Not:operator.not_,ast.Invert:operator.invert,# compareast.Lt:operator.lt,ast.LtE:operator.le,ast.Gt:operator.gt,ast.GtE:operator.ge,ast.Eq:operator.eq,ast.NotEq:operator.ne,# boolast.And:lambdax,y:xandy,ast.Or:lambdax,y:xory,}[type(op)]@staticmethoddefoperator(op:Union[ast.operator,ast.cmpop,ast.unaryop,ast.boolop])->str:""" Convert ast operator to OneTick's string representation. Parameters ---------- op ast operator object """return{# binaryast.Add:'+',ast.Sub:'-',ast.Mult:'*',ast.Div:'/',ast.Mod:'%',# unaryast.UAdd:'+',ast.USub:'-',# compareast.Lt:'<',ast.LtE:'<=',ast.Gt:'>',ast.GtE:'>=',ast.Eq:'=',ast.NotEq:'!=',# boolast.And:'AND',ast.Or:'OR',}[type(op)]classOperatorParser(CaseOperatorParser):""" Class with methods to convert ast operators to their string or python representations. Only ast operators that can be used in OneTick's per tick script are accepted. """@staticmethoddefpy_operator(op:Union[ast.operator,ast.cmpop,ast.unaryop,ast.boolop],aug:bool=False,**kwargs)->Callable:""" Convert ast operator to python function for this operator. Parameters ---------- op ast operator object aug ast don't have separate inplace operators (+=, -=, etc.) If this parameter is True then operator is inplace and otherwise if False. """ifaug:return{ast.Add:operator.iadd,ast.Sub:operator.isub,ast.Mult:operator.imul,ast.Div:operator.itruediv,}[type(op)]returnCaseOperatorParser.py_operator(op,**kwargs)@staticmethoddefoperator(op:Union[ast.operator,ast.cmpop,ast.unaryop,ast.boolop],aug:bool=False)->str:""" Convert ast operator to its string representation. Parameters ---------- op ast operator object aug ast don't have separate inplace binary operators (+=, -=, etc.) If this parameter is True then parameter is inplace and otherwise if False. """ifaug:return{ast.Add:'+=',ast.Sub:'-=',ast.Mult:'*=',ast.Div:'/=',}[type(op)]try:return{ast.Eq:'==',ast.And:'&&',ast.Or:'||',}[type(op)]exceptKeyError:returnCaseOperatorParser.operator(op)classExpressionParser:""" Class with methods to convert ast expressions to OneTick's script or function syntax. """def__init__(self,fun:'FunctionParser'):self.fun=funself.operator_parser=OperatorParser()@contextmanagerdef_replace_context(self,closure_vars:inspect.ClosureVars):""" Temporarily change closure variables in self.fun. Variables will be replaced with those from closure_vars parameter. """nonlocals,globals,*_=closure_varssaved_globals=self.fun.closure_vars.globals.copy()saved_nonlocals=self.fun.closure_vars.nonlocals.copy()self.fun.closure_vars.globals.update(globals)self.fun.closure_vars.nonlocals.update(nonlocals)yieldself.fun.closure_vars.globals.update(saved_globals)self.fun.closure_vars.nonlocals.update(saved_nonlocals)defconstant(self,expr:ast.Constant)->Expression:"""Some basic constant value: string, integer, float."""returnExpression(expr.value)defstring(self,expr:ast.Str)->Expression:"""String (for backward compatibility with Python 3.7)."""returnExpression(expr.s)defnumber(self,expr:ast.Str)->Expression:"""Number (for backward compatibility with Python 3.7)."""returnExpression(expr.n)defname(self,expr:ast.Name)->Expression:""" Name of the variable. Every variable in per-tick script function, if defined correctly, is considered to be local per-tick script variable. If variable with this name is not found it will be captured from function context. """ifself.fun.arg_nameandexpr.id==self.fun.arg_name:value=self.fun.emulatorifself.fun.emulatorisnotNoneelseexpr.idreturnExpression(value)iftype(expr.ctx)isnotast.Load:# local variable, left-hand sidereturnExpression(LocalVariable(expr.id))fordict_namein('LOCAL_VARS','STATIC_VARS'):# local or static variable, right-hand sidevars=getattr(self.fun.emulator,dict_name,{})ifexpr.idinvars:dtype=ott.get_type_by_objects([vars[expr.id]])ifissubclass(dtype,_TickSequenceTickBase):# ticks have schema, owner and methods, so using saved valuereturnExpression(vars[expr.id])returnExpression(LocalVariable(expr.id,dtype))value=eval(expr.id,self.fun.closure_vars.globals,self.fun.closure_vars.nonlocals)returnExpression(value)defindex(self,expr:ast.Index)->Expression:"""Proxy object in ast.Subscript in python < 3.9"""returnself.expression(expr.value)defslice(self,expr:ast.Slice)->Expression:""" Slice of the list. For example: a = [1, 2, 3, 4] a[2:4] Here, 2:4 is the slice. """lower=self.expression(expr.lower).valueifexpr.lowerelseNoneupper=self.expression(expr.upper).valueifexpr.upperelseNonestep=self.expression(expr.step).valueifexpr.stepelseNonereturnExpression(slice(lower,upper,step))defsubscript(self,expr:ast.Subscript)->Expression:""" Expression like: tick['X']. Setting items of ticks and state variables is supported. Getting items supported for any captured variable. """val=self.expression(expr.value)item=self.expression(expr.slice)iftype(expr.ctx)isast.Load:v=val.value[item.value]returnExpression(v)# index of per tick script function parameter or tick sequence tick is column nameifnot(val.is_emulatororval.is_tickorval.is_state_vars):raiseValueError(f"Setting items supported only for "f"'{self.fun.arg_name}' function argument, "f"tick sequences' ticks and state variables object")returnExpression(lambdarhs:val.value.__setitem__(item.value,rhs),expr=item.value,lhs=True,)defattribute(self,expr:ast.Attribute)->Expression:""" Expression like: tick.X For now we only support setting attributes of first function parameter. Getting attributes supported for any captured variable. """val=self.expression(expr.value)attr=expr.attriftype(expr.ctx)isast.Load:v=getattr(val.value,attr)returnExpression(v)# attribute of per tick script function parameter or tick sequence tick is column nameifnot(val.is_emulatororval.is_tick):raiseValueError(f"Setting attributes supported only for "f"'{self.fun.arg_name}' function argument and tick sequences' ticks")returnExpression(lambdarhs:val.value.__setattr__(attr,rhs),expr=attr,lhs=True,)defbin_op(self,expr:ast.BinOp)->Expression:""" Binary operation expression: 2 + 2, tick['X'] * 2, etc. """left=self.expression(expr.left)py_op=self.operator_parser.py_operator(expr.op)right=self.expression(expr.right)value=py_op(left.value,right.value)returnExpression(value)defunary_op(self,expr:ast.UnaryOp)->Expression:""" Unary operation expression: -1, -tick['X'], not tick['X'], ~tick['X'], etc. """py_op=self.operator_parser.py_operator(expr.op)operand=self.expression(expr.operand)ifoperand.is_operation:# special case for negative otp.Columns and otp.Operationsifisinstance(expr.op,(ast.Not,ast.Invert)):operand.convert_to_operation()ifisinstance(expr.op,ast.Not):py_op=self.operator_parser.py_operator(ast.Invert())value=py_op(operand.value)returnExpression(value)defbool_op(self,expr:ast.BoolOp)->Expression:""" Bool operation expression: True and tick['X'], etc. Note that * all python values will be checked inplace and will not be written to the script * short-circuit logic will work for python values For example: True and 0 and tick['X'] == 1 -------> false 'true' or False or tick['X'] == 1 -------> true True and True and tick['X'] == 1 -------> X == 1 """value=Noneforeinexpr.values:expression=self.expression(e)expression.convert_to_operation()v=expression.valueifnotexpression.is_operation:# short-circuit logic, return as early as possibleifisinstance(expr.op,ast.And)andnotv:# TODO: return v, not True or False# TODO: there can be many values if operations are presentvalue=Falsebreakifisinstance(expr.op,ast.Or)andv:value=TruebreakcontinueifvalueisNone:value=vcontinueifisinstance(value,_Operation)orexpression.is_operation:# change operator for operationspy_op=self.operator_parser.py_operator({ast.And:ast.BitAnd(),ast.Or:ast.BitOr(),}[type(expr.op)])else:py_op=self.operator_parser.py_operator(expr.op)value=py_op(value,v)returnExpression(value)def_convert_in_to_bool_op(self,expr:ast.Compare)->Union[ast.Compare,ast.BoolOp]:""" Convert expressions like: tick['X'] in [1, 2] -----> tick['X'] == 1 or tick['X'] == 2 tick['X'] not in [1, 2] -----> tick['X'] != 1 and tick['X'] != 2 """left,op,right=expr.left,expr.ops[0],expr.comparators[0]ifnotisinstance(op,(ast.In,ast.NotIn)):returnexprassertlen(expr.ops)==1assertlen(expr.comparators)==1right_values=[ast.Constant(r)forrinself.expression(right).value]ifisinstance(op,ast.In):bool_op,compare_op=ast.Or(),ast.Eq()else:bool_op,compare_op=ast.And(),ast.NotEq()values=[ast.Compare(left=left,ops=[compare_op],comparators=[r])forrinright_values]returnast.BoolOp(op=bool_op,values=values)def_convert_many_comparators_to_bool_op(self,expr:ast.Compare)->Union[ast.Compare,ast.BoolOp]:""" OneTick don't support compare expressions with many comparators so replacing them with several simple expressions. For example: 1 < tick['X'] < 3, -----> tick['X'] > 1 AND tick['X'] < 3 """iflen(expr.comparators)==1andlen(expr.ops)==1:returnexprcomparators=[]comparators.append(expr.left)ops=[]forop,rightinzip(expr.ops,expr.comparators):ops.append(op)comparators.append(right)bool_operands=[]foriinrange(len(comparators)-1):left,op,right=comparators[i],ops[i],comparators[i+1]bool_operands.append(ast.Compare(left=left,ops=[op],comparators=[right]))returnast.BoolOp(op=ast.And(),values=bool_operands)defcompare(self,expr:ast.Compare)->Expression:""" Compare operation expression: tick['X'] > 1, 1 < 2 < 3, tick['X'] in [1, 2] etc. """iflen(expr.ops)>1:returnself.expression(self._convert_many_comparators_to_bool_op(expr))op=expr.ops[0]ifisinstance(op,(ast.In,ast.NotIn)):returnself.expression(self._convert_in_to_bool_op(expr))left=self.expression(expr.left)right=self.expression(expr.comparators[0])py_op=self.operator_parser.py_operator(op)value=py_op(left.value,right.value)returnExpression(value)defkeyword(self,expr:ast.keyword)->Tuple[str,Any]:""" Keyword argument expression from function call: func(key=value). Not converted to per tick script in any way, needed only in self.call() function. """arg=expr.argval=self.expression(expr.value)returnarg,val.valuedefcall(self,expr:ast.Call)->Expression:""" Any call expression, like otp.nsectime(). The returned value of the call will be inserted in script. """func=self.expression(expr.func)args=[]forarginexpr.args:# TODO: support starred in CaseExpressionParser.call()ifisinstance(arg,ast.Starred):args.extend(self.expression(arg.value).value)else:args.append(self.expression(arg).value)keywords=dict(self.keyword(keyword)forkeywordinexpr.keywords)value=func.value(*args,**keywords)returnExpression(value)defformatted_value(self,expr:ast.FormattedValue)->Expression:""" Block from the f-string in curly brackets, e.g. {tick['A']} and {123} in f"{tick['A']} {123}" """returnself.expression(expr.value)defjoined_str(self,expr:ast.JoinedStr)->Expression:""" F-string expression, like: f"{tick['A']} {123}" """expressions=[self.expression(value)forvalueinexpr.values]value=Noneforexpressioninexpressions:v=expression.valueifexpression.is_operation:v=v.apply(str)else:v=str(v)ifvalueisNone:value=velse:value=value+vreturnExpression(value)deflist(self,expr:ast.List)->Expression:""" List expression, like: [1, 2, 3, 4, 5] """value=[]foreinexpr.elts:ifisinstance(e,ast.Starred):value.extend(self.expression(e.value).value)else:value.append(self.expression(e).value)returnExpression(value,expr=None)deftuple(self,expr:ast.Tuple)->Expression:""" Tuple expression, like: (1, 2, 3, 4, 5) """expression=self.list(expr)expression.values=[tuple(expression.value)]returnexpression@propertydef_expression(self)->dict:"""Mapping from ast expression to parser functions"""return{ast.Constant:self.constant,ast.NameConstant:self.constant,ast.Str:self.string,ast.Num:self.number,ast.Name:self.name,ast.Attribute:self.attribute,ast.Index:self.index,ast.Subscript:self.subscript,ast.BinOp:self.bin_op,ast.UnaryOp:self.unary_op,ast.BoolOp:self.bool_op,ast.Compare:self.compare,ast.Call:self.call,ast.FormattedValue:self.formatted_value,ast.JoinedStr:self.joined_str,ast.List:self.list,ast.Tuple:self.tuple,ast.Slice:self.slice,}defexpression(self,expr:ast.expr)->Expression:"""Return parsed expression according to its type."""returnself._expression[type(expr)](expr)classCaseExpressionParser(ExpressionParser):""" Class with methods to convert ast expressions to CASE function. """def__init__(self,fun:'FunctionParser'):super().__init__(fun)self.operator_parser=CaseOperatorParser()def_convert_bool_op_to_if_expr(self,expr:ast.expr)->ast.expr:""" Special case to convert bool operation to if expression. For example: lambda row: row['A'] or -1 will be converted to: case(A != 0, 1, A, -1) """ifnotisinstance(expr,ast.BoolOp):returnexprdefget_if_expr(first,second):ifisinstance(expr.op,ast.Or):returnast.IfExp(test=first,body=first,orelse=second)ifisinstance(expr.op,ast.And):returnast.IfExp(test=first,body=second,orelse=first)first=Noneforiinrange(len(expr.values)-1):iffirstisNone:first=expr.values[i]first=self._convert_bool_op_to_if_expr(first)second=expr.values[i+1]second=self._convert_bool_op_to_if_expr(second)first=get_if_expr(first,second)returnfirstdefif_expr(self,expr:ast.IfExp)->Expression:""" If expression: 'A' if tick['X'] > 0 else 'B'. Do not confuse with if statement. Will be converted to OneTick case function: case(X > 0, 1, 'A', 'B'). If condition value can be deduced before execution of script, then if or else value will be returned without using case() function. For example: tick['A'] if False else 3 -----------> 3 """test=self.expression(expr.test)iftest.predefined:# we can remove unnecessary branch if condition value is already knowniftest.value:returnself.expression(expr.body)returnself.expression(expr.orelse)body=self.expression(expr.body)orelse=self.expression(expr.orelse)test.convert_to_operation()str_expr=f'case({test}, 1, {body}, {orelse})'value=_LambdaIfElse(str_expr,ott.get_type_by_objects([*body.values,*orelse.values]))returnExpression(value,expr=str_expr)defcall(self,expr:ast.Call)->Expression:""" For case() function we support using inner functions that return valid case expression. """func=self.expression(expr.func)need_to_parse=Falseifnotisinstance(func.value,types.BuiltinMethodType):fornodeinexpr.args+[kw.valueforkwinexpr.keywords]:ifisinstance(node,ast.Name)andnode.id==self.fun.arg_name:# we will parse inner function call to OneTick expression# only if one of the function call arguments is# 'tick' or 'row' parameter of the original functionneed_to_parse=Truebreakifnotneed_to_parse:with_CompareTrackScope(emulation_enabled=False):try:returnsuper().call(expr)exceptException:warnings.warn(f"Function '{astunparse(expr)}' can't be called in python, ""will try to parse it to OneTick expression. "f"Use '{self.fun.arg_name}' in function's signature to indicate ""that this function can be parsed to OneTick expression.")fp=FunctionParser(func.value,check_arg_name=False)kwargs={}args=fp.ast_node.args.argsiffp.is_method:args=args[1:]forarg,defaultinzip(reversed(args),reversed(fp.ast_node.args.defaults)):kwargs[arg.arg]=defaultkwargs.update({keyword.arg:keyword.valueforkeywordinexpr.keywords})forarg,arg_valueinzip(args,expr.args):kwargs[arg.arg]=arg_valuetry:value=fp.compress()exceptExceptionaserr:try:returnsuper().call(expr)exceptException:raiseValueError(f"Can't convert function '{astunparse(expr)}' to case() expression.")fromerrwithself._replace_context(fp.closure_vars):# replace function parameters with calculated valuesvalue=fp.case_statement_parser._replace_nodes(value,replace_name=kwargs)returnself.expression(value)@propertydef_expression(self)->dict:returndict(super()._expression.items()|{ast.IfExp:self.if_expr,}.items())classCaseStatementParser:""" Class with methods to convert ast statements to CASE function. """def__init__(self,fun:'FunctionParser'):self.fun=funself.expression_parser=CaseExpressionParser(fun=fun)self.operator_parser=CaseOperatorParser()@staticmethoddef_replace_nodes(node:ast.AST,replace_name:Dict[str,ast.expr]=None,replace_break:Union[ast.stmt,Exception,Type[Exception]]=None,inplace:bool=False)->ast.AST:""" Function to replace expressions and statements inside ast.For node. Parameters ---------- node ast node in which expressions and statements will be replaced inplace if True `node` object will be modified else it will be copied and the copy will be returned replace_name mapping from ast.Name ids to ast expressions. ast.Name nodes with these ids will be replaced with corresponding expressions. replace_break replace break statement with another statement. We can't execute for loop on real data here so we can't allow break statements at all. So we will replace them with statements from code after the for loop. If replace_break is Exception then exception will be raised when visiting ast.Break nodes. """classRewriteName(ast.NodeTransformer):defvisit_Name(self,n:ast.Name):return(replace_nameor{}).get(n.id)orndefvisit_Continue(self,n:ast.Continue):# TODO: pass is not continue, we must allow only bodies with one statement in this casereturnast.Pass()defvisit_Break(self,n:ast.Break):ifreplace_breakisNone:returnnifinspect.isclass(replace_break)andissubclass(replace_break,Exception):raisereplace_break("Break is found in for loop and replacer is not provided")ifisinstance(replace_break,Exception):raisereplace_breakreturnCaseStatementParser._replace_nodes(replace_break,replace_name=replace_name)ifnotinplace:node=deepcopy(node)RewriteName().visit(node)returnnodedef_flatten_for_stmt(self,stmt:ast.For,replace_break:Union[ast.stmt,Exception,Type[Exception]]=None,stmt_after_for:ast.stmt=None)->List[ast.stmt]:""" Convert for statement to list of copy-pasted statements from the body for each iteration. """stmts=[]target=stmt.targetassertisinstance(target,(ast.Name,ast.Tuple)),(f"Unsupported expression '{astunparse(target)}' is used in for statement."" Please, use variable or tuple of variables instead.")targets=[target]ifisinstance(target,ast.Tuple):targets=target.eltsfortintargets:assertisinstance(t,ast.Name)replace_name={}iter=self.expression_parser.expression(stmt.iter)foriter_valueiniter.value:ifnotisinstance(iter_value,Iterable)orisinstance(iter_value,str):iter_value=[iter_value]replace_name={target.id:ast.Constant(value)fortarget,valueinzip(targets,iter_value)}forsinstmt.body:stmts.append(self._replace_nodes(s,replace_name=replace_name,replace_break=replace_break))ifstmt_after_forandreplace_name:stmts.append(self._replace_nodes(stmt_after_for,replace_name=replace_name))returnstmtsdef_flatten_for_stmts(self,stmts:List[ast.stmt])->List[Union[ast.If,ast.Return,ast.Pass]]:""" Find ast.For statements in list of statements and flatten them. Return list of statements without ast.For. Additionally raise exception if unsupported statement is found. """# TODO: support ast.For statements on deeper levelsres_stmts=[]fori,stmtinenumerate(stmts):ifnotisinstance(stmt,(ast.If,ast.Return,ast.For,ast.Pass)):raiseValueError("this function can't be converted to CASE function, ""only for, if, return and pass statements are allowed")ifisinstance(stmt,ast.For):try:res_stmts.extend(self._flatten_for_stmt(stmt,replace_break=ValueError))exceptValueError:stmts_left=len(stmts[i+1:])assertstmts_leftin(0,1),"Can't be more than one statement after break"ifstmts_left==0:stmt_after_for=Nonereplace_break=ast.Pass()else:stmt_after_for=stmts[i+1]assertisinstance(stmt_after_for,(ast.Return,ast.Pass)),('Can only use pass and return statements after for loop with break')replace_break=stmt_after_forres_stmts.extend(self._flatten_for_stmt(stmt,replace_break=replace_break,stmt_after_for=stmt_after_for))breakelse:res_stmts.append(stmt)returnres_stmtsdef_compress_stmts_to_one_stmt(self,stmts:List[Union[ast.If,ast.Return,ast.Pass]],filler=None,)->Union[ast.If,ast.Return]:""" List of if statements will be converted to one if statement. For example: if tick['X'] <= 1: if tick['X'] > 0: return 1 else: pass else: if tick['X'] < 3: return 2 if tick['X'] <= 3: return 3 return 4 will be converted to: if tick['X'] <= 1: if tick['X'] > 0: return 1 else: if tick['X'] <= 3: return 3 else: return 4 else: if tick['X'] < 3: return 2 else: if tick['X'] <= 3: return 3 else: return 4 """filler=fillerorast.Pass()ifnotstmts:returnfillerstmt,*others=stmtsifisinstance(stmt,ast.Return):returnstmtifisinstance(stmt,ast.Pass):returnfillerfiller=self._compress_stmts_to_one_stmt(others,filler=filler)ifisinstance(stmt,ast.If):stmt.body=[self._compress_stmts_to_one_stmt(stmt.body,filler=filler)]ifstmt.orelse:stmt.orelse=[self._compress_stmts_to_one_stmt(stmt.orelse,filler=filler)]eliffiller:stmt.orelse=[filler]assertstmt.orelsereturnstmtraiseValueError("this function can't be converted to CASE function, ""only for, if, return and pass statements are allowed")def_replace_local_variables(self,stmts:List[ast.stmt])->List[ast.stmt]:""" We support local variables only by calculating their value and replacing all it's occurrences in the code after variable definition. For example: a = 12345 if a: return a return 0 will be converted to: if 12345: return 12345 return 0 """replace_name={}res_stmts=[]forstmtinstmts:ifisinstance(stmt,ast.Assign):assertlen(stmt.targets)==1,'Unpacking local variables is not supported yet'var,val=stmt.targets[0],stmt.valueassertisinstance(var,ast.Name)replace_name[var.id]=valcontinueres_stmts.append(self._replace_nodes(stmt,replace_name=replace_name))returnres_stmtsdefif_stmt(self,stmt:ast.If)->ast.IfExp:""" Classic if statement with limited set of allowed statements in the body: * only one statement in the body * statement can be return or another if statement with same rules as above For example: if tick['X'] > 0: return 'POS' elif tick['X'] == 0: return 'ZERO' else: return 'NEG' will be converted to OneTick's CASE function: CASE(X > 0, 1, 'POS', CASE(X = 0, 1, 'ZERO', 'NEG')) """# TODO: support many statements in bodyiflen(stmt.body)!=1:raiseValueError("this function can't be converted to CASE function, ""too many statements in if body")body=self.statement(stmt.body[0])iflen(stmt.orelse)>1:raiseValueError("this function can't be converted to CASE function, ""too many statements in else body")ifstmt.orelseandnotisinstance(stmt.orelse[0],ast.Pass):orelse=self.statement(stmt.orelse[0])else:e=self.expression_parser.expression(body)orelse=ast.Constant(_default_by_type(ott.get_type_by_objects(e.values)))returnast.IfExp(test=stmt.test,body=body,orelse=orelse)defreturn_stmt(self,stmt:ast.Return)->ast.expr:""" Return statement. Will be converted to value according to OneTick's syntax. """returnstmt.valuedefpass_stmt(self,stmt:ast.Pass)->ast.Constant:""" Pass statement. Will be converted to None according to OneTick's syntax. """returnast.Constant(None)defcompress(self,stmts:List[ast.stmt])->ast.stmt:""" Compress list of statements to single statement. This is possible only if simple if and return statements are used. """stmts=self._replace_local_variables(stmts)stmts=self._flatten_for_stmts(stmts)stmt=self._compress_stmts_to_one_stmt(stmts)returnstmtdefstatement(self,stmt:ast.stmt)->ast.expr:"""Return statement converted to expression."""return{ast.If:self.if_stmt,ast.Return:self.return_stmt,ast.Pass:self.pass_stmt,}[type(stmt)](stmt)classStatementParser(CaseStatementParser):""" Class with methods to convert ast statements to per tick script lines. """def__init__(self,fun:'FunctionParser'):super().__init__(fun)self.expression_parser=ExpressionParser(fun=fun)self.operator_parser=OperatorParser()self._for_counter=0@staticmethoddef_transform_if_expr_to_if_stmt(stmt:Union[ast.Assign,ast.AugAssign])->ast.If:""" Per tick script do not support if expressions, so converting it to if statement. For example: tick['X'] = 'A' if tick['S'] > 0 else 'B' will be converted to: if (S > 0) { X = 'A'; } else { X = 'B'; } """if_expr:ast.IfExp=stmt.valuebody,orelse=deepcopy(stmt),deepcopy(stmt)body.value=if_expr.bodyorelse.value=if_expr.orelsereturnast.If(test=if_expr.test,body=[body],orelse=[orelse],)defassign(self,stmt:ast.Assign)->str:""" Assign statement: tick['X'] = 1 Will be converted to OneTick syntax: X = 1; """assertlen(stmt.targets)==1,'Unpacking variables is not yet supported'ifisinstance(stmt.value,ast.IfExp):if_stmt=self._transform_if_expr_to_if_stmt(stmt)returnself.statement(if_stmt)var=self.expression_parser.expression(stmt.targets[0])val=self.expression_parser.expression(stmt.value)default_expr=f'{var} = {val};'ifvar.lhs:expr=var.value(val.value)returnexprordefault_exprifvar.is_local_variable:var_name=var.value.nameifval.is_static:val=Expression(val.value.value)ifvar_nameinself.fun.emulator.STATIC_VARS:raiseValueError(f"Trying to define static variable '{var_name}' more than once")ifvar_nameinself.fun.emulator.LOCAL_VARS:raiseValueError(f"Can't redefine variable '{var_name}' as static")ifself.fun.emulator.NEW_VALUES:raiseValueError('Mixed definition of static variables and new columns is not supported')ifval.is_tick:# recreating tick object here, because it doesn't have name yetself.fun.emulator.STATIC_VARS[var_name]=val.dtype(var_name)returnf'static {val.value._definition}{var};'self.fun.emulator.STATIC_VARS[var_name]=val.valuereturnf'static {ott.type2str(val.dtype)}{var} = {val};'vars=Noneifvar_nameinself.fun.emulator.STATIC_VARS:vars=self.fun.emulator.STATIC_VARSelifvar_nameinself.fun.emulator.LOCAL_VARS:vars=self.fun.emulator.LOCAL_VARSifvarsisNone:ifval.is_tick:raiseValueError('Only primitive types are allowed for non static local variables.')ifself.fun.emulator.NEW_VALUES:raiseValueError('Mixed definition of local variables and new columns is not supported')self.fun.emulator.LOCAL_VARS[var_name]=val.valuereturnf'{ott.type2str(val.dtype)}{var} = {val};'dtype=ott.get_type_by_objects([vars[var_name]])ifval.dtype!=dtype:raiseValueError(f"Wrong type for variable '{var_name}': should be {dtype}, got {val.dtype}")returndefault_exprdefaug_assign(self,stmt:ast.AugAssign)->str:""" Assign with inplace operation statement: tick['X'] += 1. Will be converted to OneTick syntax: X = X + 1; """target=deepcopy(stmt.target)target.ctx=ast.Load()returnself.assign(ast.Assign(targets=[stmt.target],value=ast.BinOp(left=target,op=stmt.op,right=stmt.value,)))defif_stmt(self,stmt:ast.If)->str:""" Classic if statement: if tick['X'] > 0: tick['Y'] = 1 elif tick['X'] == 0: tick['Y'] = 0 else: tick['Y'] = -1 Will be converted to: if (X > 0) { Y = 1; } else { if (X == 0) { Y = 0; } else { Y = -1; } } """test=self.expression_parser.expression(stmt.test)test.convert_to_operation()body=[self.statement(s)forsinstmt.body]orelse=[self.statement(s)forsinstmt.orelse]iftest.predefined:iftest.value:return'\n'.join(body)return'\n'.join(orelse)lines=[]lines.append('if (%s) {'%test)lines.extend(body)lines.append('}')iforelse:lines.append('else {')lines.extend(orelse)lines.append('}')return'\n'.join(lines)defreturn_stmt(self,stmt:ast.Return)->str:""" Return statement. For now we support returning only boolean values or nothing. Will be converted to: return true; """# if return is empty then it is not filterv=stmt.valueifstmt.valueisnotNoneelseast.Constant(value=True)value=self.expression_parser.expression(v)dtype=ott.get_object_type(value.value)ifdtypeisnotbool:raiseTypeError(f"Not supported return type {dtype}")ifstmt.valueisnotNone:_EmulateObject.IS_FILTER=Truereturnf'return {value};'defwhile_stmt(self,stmt:ast.While)->str:""" Classic while statement: while tick['X'] > 0: tick['Y'] = 1 Will be converted to: while (X > 0) { Y = 1; } """test=self.expression_parser.expression(stmt.test)test.convert_to_operation()body=[self.statement(s)forsinstmt.body]iftest.predefined:raiseValueError(f'The condition of while statement always evaluates to {bool(test.value)}.'' That will result in infinite loop.'' Change condition to include some of the onetick variables.')lines=[]lines.append('while (%s) {'%test)lines.extend(body)lines.append('}')return'\n'.join(lines)deffor_stmt(self,stmt:ast.For)->str:""" For now for statement will not be converted to per tick script's for statement. Instead, the statements from the body of the for statement will be duplicated for each iteration. For example: for i in range(1, 4): tick['X'] += i will be converted to: tick['X'] += 1 tick['X'] += 2 tick['X'] += 3 """lines=[]iter=self.expression_parser.expression(stmt.iter)ifisinstance(iter.value,_TickSequence):target=stmt.targetassertisinstance(target,ast.Name),"Tuples can't be used while iterating on tick sequences"state_tick=iter.value._tick_obj(target.id)# TODO: uglystate_tick_name=f"_______state_tick_{self._for_counter}_______"self._for_counter+=1ast_tick=ast.Name(state_tick_name,ctx=ast.Load())lines.append('for (%s%s : %s) {'%(state_tick._definition,state_tick,iter.value))withself.expression_parser._replace_context(inspect.ClosureVars({},{state_tick_name:state_tick},{},set())):forsinstmt.body:s=self._replace_nodes(s,replace_name={target.id:ast_tick})lines.append(self.statement(s))lines.append('}')else:lines=[self.statement(s)forsinself._flatten_for_stmt(stmt)]return'\n'.join(lines)defbreak_stmt(self,stmt:ast.Break)->str:return'break;'defcontinue_stmt(self,stmt:ast.Continue)->str:return'continue;'defpass_stmt(self,stmt:ast.Pass)->str:"""Pass statement is not converted to anything"""return''defyield_expr(self,expr:ast.Yield)->Expression:""" Yield expression, like: yield Values for yield are not supported. Will be translated to PROPAGATE_TICK() function. Can be used only as a statement, so this function is here and not in ExpressionParser. """ifexpr.valueisnotNone:raiseValueError("Passing value with yield expression is not supported.")returnExpression('PROPAGATE_TICK();')defexpression(self,stmt:ast.Expr)->str:""" Here goes raw strings and yield expression. For example: if tick['A'] == 0: 'return 0;' Here 'return 0;' is used as a statement and an expression. Expression's returned value *must* be a string and this string will be injected in per tick script directly. """ifisinstance(stmt.value,ast.Yield):expression=self.yield_expr(stmt.value)else:expression=self.expression_parser.expression(stmt.value)assertisinstance(expression.value,(str,_Operation)),(f"The statement '{astunparse(stmt)}' can't be used here"" because the value of such statement can be string only"" as it's value will be injected directly in per tick script.")value=str(expression.value)ifvalueandvalue[-1]!=';':value+=';'returnvaluedefwith_stmt(self,stmt:ast.With)->str:""" Used only with special context managers. Currently only `_ONCE` is supported. """iflen(stmt.items)!=1:raiseValueError('Currently it is possible to use only one context manager in single with statement')with_item=stmt.items[0]ifwith_item.optional_vars:raiseValueError('It is not allowed to use "as" in with statements for per-tick script')context_expr=with_item.context_exprifisinstance(context_expr,ast.Call):expr=self.expression_parser.expression(context_expr.func)else:raiseValueError(f'{context_expr} is not called')ifexpr.valueisnotOnce:raiseValueError(f'{expr.value} is not supported in per-tick script with statements')returnexpr.value().get_str('\n'.join([self.statement(s)forsinstmt.body]))defstatement(self,stmt:ast.stmt)->str:"""Return parsed statement according to its type."""return{ast.Assign:self.assign,ast.AugAssign:self.aug_assign,ast.If:self.if_stmt,ast.Return:self.return_stmt,ast.While:self.while_stmt,ast.For:self.for_stmt,ast.Break:self.break_stmt,ast.Continue:self.continue_stmt,ast.Pass:self.pass_stmt,ast.Expr:self.expression,ast.With:self.with_stmt,}[type(stmt)](stmt)classEndOfBlock(Exception):passclassLambdaBlockFinder:""" This is simplified version of inspect.BlockFinder that supports multiline lambdas. """OPENING_BRACKETS={'[':']','(':')','{':'}',}CLOSING_BRACKETS={c:oforo,cinOPENING_BRACKETS.items()}BRACKETS_MATCHING=dict(OPENING_BRACKETS.items()|CLOSING_BRACKETS.items())def__init__(self):# current indentation levelself.indent=0# row and column index for the start of lambda expressionself.start=None# row and column index for the end of lambda expressionself.end=None# stack with bracketsself.brackets=deque()self.prev=Noneself.current=Nonedeftokeneater(self,type,token,srowcol,erowcol,line,start_row=0):srowcol=(srowcol[0]+start_row,srowcol[1])erowcol=(erowcol[0]+start_row,erowcol[1])self.prev=self.currentself.current=tokenize.TokenInfo(type,token,srowcol,erowcol,line)self.end=erowcoliftoken=='lambda':self.start=srowcoleliftype==tokenize.INDENT:self.indent+=1eliftype==tokenize.DEDENT:self.indent-=1# the end of matching indent/dedent pairs ends a blockifself.indent<=0:raiseEndOfBlockelifnotself.start:self.indent=0eliftype==tokenize.NEWLINE:ifself.indent==0or(# if lambda is the argument of the functionself.prevandself.prev.type==tokenize.OPandself.prev.string==','):raiseEndOfBlockeliftokeninself.OPENING_BRACKETS:self.brackets.append(token)eliftokeninself.CLOSING_BRACKETS:try:assertself.brackets.pop()==self.CLOSING_BRACKETS[token]except(IndexError,AssertionError):self.end=self.prev.endraiseEndOfBlock# noqa: W0707defget_lambda_source(lines):"""Extract the block of lambda code at the top of the given list of lines."""blockfinder=LambdaBlockFinder()start_row=0whileTrue:try:tokens=tokenize.generate_tokens(iter(lines[start_row:]).__next__)for_tokenintokens:blockfinder.tokeneater(*_token,start_row=start_row)breakexceptIndentationErrorase:# indentation errors are possible because# we started eating tokens from line with lambda# not from the start of the statement# trying to eat again from the current row in this casestart_row=e.args[1][1]-1continueexceptEndOfBlock:breakstart_row,start_column=blockfinder.startend_row,end_column=blockfinder.end# crop block to get rid of tokens from the context around lambdalines=lines[start_row-1:end_row]lines[-1]=lines[-1][:end_column]lines[0]=lines[0][start_column:]# add brackets around lambda in case it is multiline lambdareturn''.join(['(',*lines,')'])defis_lambda(lambda_f)->bool:returnisinstance(lambda_f,types.LambdaType)andlambda_f.__name__=='<lambda>'defget_source(lambda_f)->str:""" Get source code of the function or lambda. """ifis_lambda(lambda_f):# that's a hack for multiline lambdas in brackets# inspect.getsource parse them wrongsource_lines,lineno=inspect.findsource(lambda_f)if'lambda'notinsource_lines[lineno]:# inspect.findsource fails sometimes toolineno=lambda_f.__code__.co_firstlineno+1while'lambda'notinsource_lines[lineno]:lineno-=1source=get_lambda_source(source_lines[lineno:])else:source=inspect.getsource(lambda_f)# doing dedent because self.ast_node do not like indented source codereturntextwrap.dedent(source)classFunctionParser:""" Class to parse callable objects (lambdas and functions) to OneTick's per tick script or case functions. Only simple functions corresponding to OneTick syntax supported (without inner functions, importing modules, etc.) You can call simple functions inside, do operations with captured variables (without assigning to them), but using non-pure functions is not recommended because the code in function may not be executed in the order you expect. """SOURCE_CODE_ATTRIBUTE='___SOURCE_CODE___'def__init__(self,lambda_f,emulator=None,check_arg_name=True):""" Parameters ---------- emulator otp.Source emulator that will be tracking changes made to source check_arg_name if True, only callables with zero or one parameter will be allowed """assertisinstance(lambda_f,(types.LambdaType,types.FunctionType,types.MethodType)),(f"It is expected to get a function, method or lambda, but got '{type(lambda_f)}'")self.lambda_f=lambda_fself.emulator=emulatorself.check_arg_name=check_arg_nameself.statement_parser=StatementParser(fun=self)self.expression_parser=ExpressionParser(fun=self)self.case_expression_parser=CaseExpressionParser(fun=self)self.case_statement_parser=CaseStatementParser(fun=self)# calling property here, so we can raise exception as early as possible_=self.arg_name@cached_propertydefis_method(self)->bool:returnisinstance(self.lambda_f,types.MethodType)@cached_propertydefsource_code(self)->str:""" Get source code of the function or lambda. """# first try to get code from special attribute else get code the usual wayreturngetattr(self.lambda_f,self.SOURCE_CODE_ATTRIBUTE,None)orget_source(self.lambda_f)@cached_propertydefclosure_vars(self)->inspect.ClosureVars:""" Get closure variables of the function. These are variables that were captured from the context before function definition. For example: A = 12345 def a(): print(A + 1) In this function variable A is the captured variable. We need closure variables, so we can use them when parsing ast tree. """returninspect.getclosurevars(self.lambda_f)@cached_propertydefast_node(self)->Union[ast.FunctionDef,ast.Lambda]:""" Convert function or lambda to ast module statement. """source_code=self.source_codetree=ast.parse(source_code)fornodeinast.walk(tree):ifisinstance(node,(ast.FunctionDef,ast.Lambda)):ifisinstance(node,ast.FunctionDef)andast.get_docstring(node):# remove comment section from function bodynode.body.pop(0)returnnode@cached_propertydefarg_name(self)->Optional[str]:"""Get name of the first function or lambda argument."""node=self.ast_nodeargv=list(node.args.args)argc=len(argv)ifargc>1andargv[0].arg=='self'andself.is_method:argv.pop(0)argc-=1ifself.check_arg_nameandargc>1:raiseValueError("It is allowed to pass only functions or lambdas that take either one or"f" zero parameters, but got {argc}")returnargv[0].argifargvelseNonedefper_tick_script(self)->str:""" Convert function to OneTick's per tick script. """node=self.ast_nodelines=[]assertisinstance(node,ast.FunctionDef),'lambdas are not supported in per-tick-script yet'function_def:ast.FunctionDef=nodeforstmtinfunction_def.body:line=self.statement_parser.statement(stmt)ifline:lines.append(line)if_EmulateObject.IS_FILTER:# if there were return statement anywhere in the code# then we add default return at the endlines.append(self.statement_parser.statement(ast.Return(ast.Constant(False))))ifself.emulatorisnotNone:# per tick script syntax demand that we declare variables before using them# so we get all new variables from emulator and declare them.new_columns=[]defvar_definition(key,values):dtype=ott.get_type_by_objects(values)returnf'{ott.type2str(dtype)}{key} = {ott.value2str(_default_by_type(dtype))};'forkey,valuesinself.emulator.NEW_VALUES.items():new_columns.append(var_definition(key,values))lines=new_columns+linesifnotlines:raiseValueError("The resulted body of PER TICK SCRIPT is empty")return'\n'.join(lines)+'\n'defcompress(self)->ast.expr:""" Convert lambda or function to AST expression. """node=self.ast_nodeifisinstance(node,ast.Lambda):returnnode.bodystmt=self.case_statement_parser.compress(node.body)returnself.case_statement_parser.statement(stmt)defcase(self)->str:""" Convert lambda or function to OneTick's CASE() function. """expr=self.compress()expr=self.case_expression_parser._convert_bool_op_to_if_expr(expr)expression=self.case_expression_parser.expression(expr)# this will raise type error if type of the expression is not supported_default_by_type(ott.get_type_by_objects(expression.values))returnstr(expression),expression.values
[docs]defremote(fun):""" This decorator is needed in case function ``fun`` is used in :py:meth:`~onetick.py.Source.apply` method in a `Remote OTP with Ray` context. We want to get source code of the function locally because we will not be able to get source code on the remote server. See also -------- :ref:`Remote OTP with Ray <ray-remote>`. """# see PY-424@wraps(fun)defwrapper(*args,**kwargs):returnfun(*args,**kwargs)setattr(wrapper,FunctionParser.SOURCE_CODE_ATTRIBUTE,get_source(fun))returnwrapper
classOnce:""" Used with a statement or a code block to make it run only once (the first time control reaches to the statement). """def__enter__(self):passdef__exit__(self,exc_type,exc_val,exc_tb):passdefget_str(self,string:str)->str:returnf"_ONCE\n{{\n{string}\n}}"