onetick-py 1.173.0__py3-none-any.whl → 1.175.0__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.
onetick/py/_version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # This file was generated automatically. DO NOT CHANGE.
2
- VERSION = '1.173.0'
2
+ VERSION = '1.175.0'
@@ -547,7 +547,7 @@ class _AggregationTSSelection(_Aggregation):
547
547
 
548
548
  class _FloatAggregation(_Aggregation):
549
549
 
550
- require_type = (int, float, ott._inf)
550
+ require_type = (int, float, ott._inf, ott.decimal)
551
551
 
552
552
  """
553
553
  Aggregation that expect int or float as input
@@ -229,6 +229,9 @@ class ObSnapshot(_OrderBookAggregation):
229
229
  # we don't want to set hard limit on the output of order book aggregations
230
230
  if self.show_full_detail:
231
231
  kwargs['all_fields'] = True
232
+ self._size_type = int
233
+ if self.size_max_fractional_digits > 0:
234
+ self._size_type = float # type: ignore[assignment]
232
235
  super().__init__(*args, **kwargs)
233
236
 
234
237
  def _param_validation(self):
@@ -244,7 +247,7 @@ class ObSnapshot(_OrderBookAggregation):
244
247
  def _get_output_schema(self, src: 'Source', name: Optional[str] = None) -> dict:
245
248
  schema = {
246
249
  'PRICE': float,
247
- 'SIZE': int,
250
+ 'SIZE': self._size_type,
248
251
  'LEVEL': int,
249
252
  'UPDATE_TIME': otp.nsectime,
250
253
  'BUY_SELL_FLAG': int,
@@ -265,10 +268,10 @@ class ObSnapshotWide(ObSnapshot):
265
268
  def _get_output_schema(self, src: 'Source', name: Optional[str] = None) -> dict:
266
269
  schema = {
267
270
  'BID_PRICE': float,
268
- 'BID_SIZE': int,
271
+ 'BID_SIZE': self._size_type,
269
272
  'BID_UPDATE_TIME': otp.nsectime,
270
273
  'ASK_PRICE': float,
271
- 'ASK_SIZE': int,
274
+ 'ASK_SIZE': self._size_type,
272
275
  'ASK_UPDATE_TIME': otp.nsectime,
273
276
  'LEVEL': int,
274
277
  }
@@ -299,10 +302,10 @@ class ObSnapshotFlat(ObSnapshot):
299
302
  for level in range(1, self.max_levels + 1):
300
303
  schema.update({
301
304
  f'BID_PRICE{level}': float,
302
- f'BID_SIZE{level}': int,
305
+ f'BID_SIZE{level}': self._size_type,
303
306
  f'BID_UPDATE_TIME{level}': otp.nsectime,
304
307
  f'ASK_PRICE{level}': float,
305
- f'ASK_SIZE{level}': int,
308
+ f'ASK_SIZE{level}': self._size_type,
306
309
  f'ASK_UPDATE_TIME{level}': otp.nsectime,
307
310
  })
308
311
  return schema
@@ -343,6 +346,9 @@ class ObSummary(_OrderBookAggregation):
343
346
  self.state_key_max_inactivity_sec = state_key_max_inactivity_sec
344
347
  self.size_max_fractional_digits = size_max_fractional_digits
345
348
  self.include_market_order_ticks = include_market_order_ticks
349
+ self._size_type = int
350
+ if self.size_max_fractional_digits > 0:
351
+ self._size_type = float # type: ignore[assignment]
346
352
  super().__init__(*args, **kwargs)
347
353
 
348
354
  def _param_validation(self):
@@ -357,12 +363,12 @@ class ObSummary(_OrderBookAggregation):
357
363
 
358
364
  def _get_output_schema(self, src: 'Source', name: Optional[str] = None) -> dict:
359
365
  schema = {
360
- 'BID_SIZE': int,
366
+ 'BID_SIZE': self._size_type,
361
367
  'BID_VWAP': float,
362
368
  'BEST_BID_PRICE': float,
363
369
  'WORST_BID_PRICE': float,
364
370
  'NUM_BID_LEVELS': int,
365
- 'ASK_SIZE': int,
371
+ 'ASK_SIZE': self._size_type,
366
372
  'ASK_VWAP': float,
367
373
  'BEST_ASK_PRICE': float,
368
374
  'WORST_ASK_PRICE': float,
@@ -100,7 +100,7 @@ class Vwap(_Aggregation):
100
100
  FIELDS_TO_SKIP: List = ['column_name']
101
101
 
102
102
  output_field_type = float
103
- require_type = (int, float, ott.nsectime)
103
+ require_type = (int, float, ott.nsectime, ott.decimal)
104
104
 
105
105
  def __init__(self,
106
106
  price_column: str,
@@ -271,7 +271,7 @@ class Average(_FloatAggregation):
271
271
  class StdDev(_Aggregation): # Stddev does not support inf, so no need to use _FloatAggregation
272
272
  NAME = "STDDEV"
273
273
  EP = otq.Stddev
274
- require_type = (int, float)
274
+ require_type = (int, float, ott.decimal)
275
275
  output_field_type = float
276
276
  FIELDS_MAPPING = deepcopy(_Aggregation.FIELDS_MAPPING)
277
277
  FIELDS_MAPPING['biased'] = 'BIASED'
@@ -618,6 +618,9 @@ def pnl_realized(
618
618
  The name of the field with price, default is **PRICE**.
619
619
  buy_sell_flag_field: str, :py:class:`otp.Column <onetick.py.Column>`
620
620
  The name of the field with buy/sell flag, default is **BUY_SELL_FLAG**.
621
+ If the type of this field is string, then possible values are 'B' or 'b' for buy and 'S' or 's' for sell.
622
+ If the type of this field is integer, then possible values are 0 for buy and 1 for sell.
623
+
621
624
 
622
625
  See also
623
626
  --------
@@ -625,20 +628,63 @@ def pnl_realized(
625
628
 
626
629
  Examples
627
630
  --------
631
+ Let's generate some data:
628
632
 
629
- Simple usage
630
-
631
- >>> data = otp.DataSource('SOME_DATABASE', tick_type='TT', symbols='AA') # doctest: +SKIP
632
- >>> data = data.pnl_realized() # doctest: +SKIP
633
- >>> otp.run(data) # doctest: +SKIP
634
- Time BUY_SELL_FLAG PRICE SIZE PNL_REALIZED
635
- 0 2003-12-01 00:00:00.000 0 5 20 0.0
636
- 1 2003-12-01 00:00:00.001 0 4 30 0.0
637
- 2 2003-12-01 00:00:00.002 1 3 15 -30.0
638
- 3 2003-12-01 00:00:00.003 0 10 50 0.0
639
- 4 2003-12-01 00:00:00.004 1 7 18 49.0
640
- 5 2003-12-01 00:00:00.005 1 9 6 30.0
641
- 6 2003-12-01 00:00:00.006 0 8 40 0.0
633
+ >>> trades = otp.Ticks(
634
+ ... PRICE=[1.0, 2.0, 3.0, 2.5, 4.0, 5.0, 6.0, 7.0, 3.0, 4.0, 1.0],
635
+ ... SIZE=[700, 20, 570, 600, 100, 100, 100, 100, 150, 10, 100],
636
+ ... SELL_FLAG=[0, 0, 0, 1, 1, 1, 1, 1, 0, 0, 1],
637
+ ... SIDE=['B', 'B', 'B', 'S', 'S', 'S', 'S', 'S', 'B', 'B', 'S'],
638
+ ... )
639
+ >>> otp.run(trades)
640
+ Time PRICE SIZE SELL_FLAG SIDE
641
+ 0 2003-12-01 00:00:00.000 1.0 700 0 B
642
+ 1 2003-12-01 00:00:00.001 2.0 20 0 B
643
+ 2 2003-12-01 00:00:00.002 3.0 570 0 B
644
+ 3 2003-12-01 00:00:00.003 2.5 600 1 S
645
+ 4 2003-12-01 00:00:00.004 4.0 100 1 S
646
+ 5 2003-12-01 00:00:00.005 5.0 100 1 S
647
+ 6 2003-12-01 00:00:00.006 6.0 100 1 S
648
+ 7 2003-12-01 00:00:00.007 7.0 100 1 S
649
+ 8 2003-12-01 00:00:00.008 3.0 150 0 B
650
+ 9 2003-12-01 00:00:00.009 4.0 10 0 B
651
+ 10 2003-12-01 00:00:00.010 1.0 100 1 S
652
+
653
+ And then calculate profit and loss metric for it.
654
+
655
+ First let's use string ``buy_sell_flag_field`` field:
656
+
657
+ >>> data = trades.pnl_realized(buy_sell_flag_field='SIDE') # doctest: +SKIP
658
+ >>> otp.run(data)[['Time', 'PRICE', 'SIZE', 'SIDE', 'PNL_REALIZED']] # doctest: +SKIP
659
+ Time PRICE SIZE SIDE PNL_REALIZED
660
+ 0 2003-12-01 00:00:00.000 1.0 700 B 0.0
661
+ 1 2003-12-01 00:00:00.001 2.0 20 B 0.0
662
+ 2 2003-12-01 00:00:00.002 3.0 570 B 0.0
663
+ 3 2003-12-01 00:00:00.003 2.5 600 S 900.0
664
+ 4 2003-12-01 00:00:00.004 4.0 100 S 300.0
665
+ 5 2003-12-01 00:00:00.005 5.0 100 S 220.0
666
+ 6 2003-12-01 00:00:00.006 6.0 100 S 300.0
667
+ 7 2003-12-01 00:00:00.007 7.0 100 S 400.0
668
+ 8 2003-12-01 00:00:00.008 3.0 150 B 0.0
669
+ 9 2003-12-01 00:00:00.009 4.0 10 B 0.0
670
+ 10 2003-12-01 00:00:00.010 1.0 100 S -200.0
671
+
672
+ We can get the same result using integer ``buy_sell_flag_field`` field:
673
+
674
+ >>> data = trades.pnl_realized(buy_sell_flag_field='SELL_FLAG') # doctest: +SKIP
675
+ >>> otp.run(data)[['Time', 'PRICE', 'SIZE', 'SELL_FLAG', 'PNL_REALIZED']] # doctest: +SKIP
676
+ Time PRICE SIZE SELL_FLAG PNL_REALIZED
677
+ 0 2003-12-01 00:00:00.000 1.0 700 0 0.0
678
+ 1 2003-12-01 00:00:00.001 2.0 20 0 0.0
679
+ 2 2003-12-01 00:00:00.002 3.0 570 0 0.0
680
+ 3 2003-12-01 00:00:00.003 2.5 600 1 900.0
681
+ 4 2003-12-01 00:00:00.004 4.0 100 1 300.0
682
+ 5 2003-12-01 00:00:00.005 5.0 100 1 220.0
683
+ 6 2003-12-01 00:00:00.006 6.0 100 1 300.0
684
+ 7 2003-12-01 00:00:00.007 7.0 100 1 400.0
685
+ 8 2003-12-01 00:00:00.008 3.0 150 0 0.0
686
+ 9 2003-12-01 00:00:00.009 4.0 10 0 0.0
687
+ 10 2003-12-01 00:00:00.010 1.0 100 1 -200.0
642
688
  """
643
689
  if computation_method not in ['fifo']:
644
690
  raise ValueError(
@@ -31,7 +31,7 @@ def float_to_str(prev_op, dtype=str):
31
31
  return MethodResult(op_str, dtype)
32
32
 
33
33
 
34
- def float_to_decimal(prev_op):
34
+ def num_to_decimal(prev_op):
35
35
  return MethodResult(f'decimal({str(prev_op)})', ott.decimal)
36
36
 
37
37
 
@@ -192,7 +192,7 @@ class _ConversionsDict(UserDict):
192
192
 
193
193
  CONVERSIONS = _ConversionsDict({(float, int): float_to_int,
194
194
  (float, str): float_to_str,
195
- (float, ott.decimal): float_to_decimal,
195
+ (float, ott.decimal): num_to_decimal,
196
196
  (ott.decimal, int): float_to_int,
197
197
  (ott.decimal, str): decimal_to_str,
198
198
  (ott.decimal, float): decimal_to_float,
@@ -206,6 +206,7 @@ CONVERSIONS = _ConversionsDict({(float, int): float_to_int,
206
206
  (int, float): int_to_float,
207
207
  (int, ott.nsectime): int_to_nsectime,
208
208
  (int, ott.msectime): int_to_msectime,
209
+ (int, ott.decimal): num_to_decimal,
209
210
  (str, float): str_to_float,
210
211
  (str, ott.decimal): str_to_decimal,
211
212
  (str, int): str_to_int,
@@ -8,9 +8,12 @@ from onetick.py import types as ott
8
8
 
9
9
 
10
10
  def are_numerics(*dtypes):
11
- return all(inspect.isclass(dtype)
12
- and (issubclass(dtype, (float, int)) or np.issubdtype(dtype, np.integer))
13
- and not issubclass(dtype, (ott.nsectime, ott.msectime)) for dtype in dtypes)
11
+ return all(
12
+ inspect.isclass(dtype)
13
+ and (issubclass(dtype, (float, int)) or np.issubdtype(dtype, np.integer) or issubclass(dtype, ott.decimal))
14
+ and not issubclass(dtype, (ott.nsectime, ott.msectime))
15
+ for dtype in dtypes
16
+ )
14
17
 
15
18
 
16
19
  def are_ints_not_time(*dtypes):
@@ -109,6 +112,12 @@ def _get_widest_type(left, right):
109
112
  (<class '...MyTime'>, None, <class '...MyNSec'>)
110
113
  """
111
114
 
115
+ # decimal takes precedence before integer and floating point types
116
+ if issubclass(left, ott.decimal) and are_numerics(right):
117
+ return left
118
+ if are_numerics(left) and issubclass(right, ott.decimal):
119
+ return right
120
+
112
121
  if issubclass(left, float) and issubclass(right, float):
113
122
  # between np.float and float we choose base float
114
123
  if left is not float and np.issubdtype(left, np.floating):
@@ -476,7 +476,7 @@ class Operation:
476
476
  Property that provides access to
477
477
  methods specific to float type.
478
478
  """
479
- if issubclass(self.dtype, float) and self.dtype is not ott.decimal:
479
+ if issubclass(self.dtype, float):
480
480
  from onetick.py.core.column_operations.accessors.float_accessor import _FloatAccessor
481
481
  return _FloatAccessor(self)
482
482
  else:
@@ -1043,7 +1043,7 @@ class Raw(Operation):
1043
1043
  def __init__(self, raw, dtype):
1044
1044
  if dtype is str:
1045
1045
  warnings.warn(
1046
- f'Be careful, default string length in OneTick is {ott.string.DEFAULT_LENGTH}.'
1046
+ f'Be careful, default string length in OneTick is {ott.string.DEFAULT_LENGTH}. '
1047
1047
  "Length of the result raw expression can't be calculated automatically, "
1048
1048
  "so you'd better use onetick.py.string type.",
1049
1049
  stacklevel=2,
onetick/py/core/source.py CHANGED
@@ -947,7 +947,9 @@ class Source:
947
947
  view: bool = False,
948
948
  line_limit: Optional[Tuple[int, int]] = (10, 30),
949
949
  parse_eval_from_params: bool = False,
950
+ render_debug_info: bool = False,
950
951
  debug: bool = False,
952
+ graphviz_compat_mode: bool = False,
951
953
  **kwargs,
952
954
  ):
953
955
  """
@@ -973,8 +975,13 @@ class Source:
973
975
  If one of tuple values set to zero the corresponding limit disabled.
974
976
  parse_eval_from_params: bool
975
977
  Enable parsing and printing `eval` sub-queries from EP parameters.
978
+ render_debug_info: bool
979
+ Render additional debug information.
976
980
  debug: bool
977
981
  Allow to print stdout or stderr from `Graphviz` render.
982
+ graphviz_compat_mode: bool
983
+ Change internal parameters of result graph for better compatibility with old `Graphviz` versions.
984
+ Could produce larger and less readable graphs.
978
985
  kwargs:
979
986
  Additional arguments to be passed to :py:meth:`onetick.py.Source.to_otq` method (except
980
987
  ``file_name``, ``file_suffix`` and ``query_name`` parameters)
@@ -1006,7 +1013,8 @@ class Source:
1006
1013
 
1007
1014
  otq_path = self.to_otq(**kwargs)
1008
1015
  return render_otq(
1009
- otq_path, image_path, output_format, load_external_otqs, view, line_limit, parse_eval_from_params, debug,
1016
+ otq_path, image_path, output_format, load_external_otqs, view, line_limit, parse_eval_from_params,
1017
+ render_debug_info, debug, graphviz_compat_mode,
1010
1018
  )
1011
1019
 
1012
1020
  def copy(self, ep=None, columns=None, deep=False) -> 'Source':
onetick/py/functions.py CHANGED
@@ -327,6 +327,7 @@ def merge(sources, align_schema=True, symbols=None, identify_input_ts=False,
327
327
 
328
328
  if enforce_order:
329
329
  result.drop('OMDSEQ', inplace=True)
330
+ merged_columns.pop('OMDSEQ')
330
331
 
331
332
  if identify_input_ts:
332
333
  result.schema['SYMBOL_NAME' + added_field_name_suffix] = str
@@ -2030,7 +2031,7 @@ def _add_element(cur_res, element, format_spec_additional=None):
2030
2031
  if isinstance(element, Operation):
2031
2032
  if format_spec_additional is None:
2032
2033
  cur_res += element.apply(str)
2033
- elif issubclass(element.dtype, float) and re.fullmatch(r'\.\d+f', format_spec_additional):
2034
+ elif issubclass(element.dtype, (float, ott.decimal)) and re.fullmatch(r'\.\d+f', format_spec_additional):
2034
2035
  # float has strange behavior when precision=0
2035
2036
  decimal_elem = element.apply(ott.decimal)
2036
2037
  precision_str = re.findall(r'\d+', format_spec_additional)[0]
@@ -2047,7 +2048,7 @@ def _add_element(cur_res, element, format_spec_additional=None):
2047
2048
  else:
2048
2049
  if format_spec_additional is None:
2049
2050
  cur_res += str(element)
2050
- elif isinstance(element, float):
2051
+ elif isinstance(element, (float, ott.decimal)):
2051
2052
  formatting = f'{{:{format_spec_additional}}}'
2052
2053
  cur_res += formatting.format(element)
2053
2054
  else:
onetick/py/types.py CHANGED
@@ -2,6 +2,7 @@ import ctypes
2
2
  import functools
3
3
  import inspect
4
4
  import warnings
5
+ import decimal as _decimal
5
6
  from typing import Optional, Type, Union
6
7
  from datetime import date as _date
7
8
  from datetime import datetime as _datetime
@@ -707,18 +708,23 @@ class _inf(float, metaclass=_nan_base):
707
708
  inf = _inf()
708
709
 
709
710
 
710
- class _decimal_str(type):
711
- def __str__(cls):
712
- return 'decimal'
713
-
714
-
715
- class decimal(float, metaclass=_decimal_str):
711
+ class decimal:
716
712
  """
717
713
  Object that represents decimal OneTick value.
718
714
  Decimal is 128 bit base 10 floating point number.
719
715
 
716
+ Parameters
717
+ ----------
718
+ value: int, float, str
719
+ The value to initialize decimal from.
720
+ Note that float values may be converted with precision lost.
721
+
720
722
  Examples
721
723
  --------
724
+
725
+ :py:class:`~onetick.py.types.decimal` objects can be used in tick generators
726
+ and column operations as any other onetick-py type:
727
+
722
728
  >>> t = otp.Ticks({'A': [otp.decimal(1), otp.decimal(2)]})
723
729
  >>> t['B'] = otp.decimal(1.23456789)
724
730
  >>> t['C'] = t['A'] / 0
@@ -727,43 +733,107 @@ class decimal(float, metaclass=_decimal_str):
727
733
  Time A B C D
728
734
  0 2003-12-01 00:00:00.000 1.0 1.234568 inf NaN
729
735
  1 2003-12-01 00:00:00.001 2.0 1.234568 inf NaN
730
- """
731
- def __wrap(self, res):
732
- # if parent class doesn't support some operation, it returns NotImplemented and so do we
733
- # In other case we wrap float result with our decimal class
734
- if isinstance(res, type(NotImplemented)):
735
- return NotImplemented
736
- return self.__class__(res)
737
-
738
- def __add__(self, other):
739
- return self.__wrap(super().__add__(other))
740
736
 
741
- def __radd__(self, other):
742
- return self.__wrap(super().__radd__(other))
743
-
744
- def __sub__(self, other):
745
- return self.__wrap(super().__sub__(other))
737
+ Additionally, any arithmetic operation with :py:class:`~onetick.py.types.decimal` object will return
738
+ an :py:class:`~onetick.py.Operation` object:
746
739
 
747
- def __rsub__(self, other):
748
- return self.__wrap(super().__rsub__(other))
740
+ >>> t = otp.Tick(A=1)
741
+ >>> t['X'] = otp.decimal(1) / 0
742
+ >>> otp.run(t)
743
+ Time A X
744
+ 0 2003-12-01 1 inf
745
+
746
+ Note that converting from float (first row) may result in losing precision.
747
+ :py:class:`~onetick.py.types.decimal` objects are created from strings or integers, so they don't lose precision:
748
+
749
+ >>> t0 = otp.Tick(A=0.1)
750
+ >>> t1 = otp.Tick(A=otp.decimal(0.01))
751
+ >>> t2 = otp.Tick(A=otp.decimal('0.001'))
752
+ >>> t3 = otp.Tick(A=otp.decimal(1) / otp.decimal(10_000))
753
+ >>> t = otp.merge([t0, t1, t2, t3], enforce_order=True)
754
+ >>> t['STR_A'] = t['A'].decimal.str(34)
755
+ >>> otp.run(t)
756
+ Time A STR_A
757
+ 0 2003-12-01 0.1000 0.1000000000000000055511151231257827
758
+ 1 2003-12-01 0.0100 0.0100000000000000000000000000000000
759
+ 2 2003-12-01 0.0010 0.0010000000000000000000000000000000
760
+ 3 2003-12-01 0.0001 0.0001000000000000000000000000000000
749
761
 
750
- def __mul__(self, other):
751
- return self.__wrap(super().__mul__(other))
762
+ Note that :py:class:`otp.Ticks <onetick.py.Ticks>` will convert everything from string under the hood,
763
+ so even the float values will not lose precision:
752
764
 
753
- def __rmul__(self, other):
754
- return self.__wrap(super().__rmul__(other))
765
+ >>> t = otp.Ticks({'A': [0.1, otp.decimal(0.01), otp.decimal('0.001'), otp.decimal(1e-4)]})
766
+ >>> t['STR_A'] = t['A'].decimal.str(34)
767
+ >>> otp.run(t)
768
+ Time A STR_A
769
+ 0 2003-12-01 00:00:00.000 0.1000 0.1000000000000000000000000000000000
770
+ 1 2003-12-01 00:00:00.001 0.0100 0.0100000000000000000000000000000000
771
+ 2 2003-12-01 00:00:00.002 0.0010 0.0010000000000000000000000000000000
772
+ 3 2003-12-01 00:00:00.003 0.0001 0.0001000000000000000000000000000000
773
+ """
774
+ def __new__(cls, *args, **kwargs):
775
+ # this method dynamically adds properties and methods
776
+ # from otp.Operation class to this one
777
+
778
+ # otp.decimal class doesn't fit well in onetick-py type system,
779
+ # so this class is a mix of both type and Operation logic
780
+
781
+ # Basically it works like this:
782
+ # otp.decimal is a OneTick type
783
+ # otp.decimal(1) is a decimal type object
784
+ # Doing anything with this object returns an otp.Operation:
785
+ # otp.decimal(1) / 2
786
+
787
+ def proxy_wrap(attr, value):
788
+ if callable(value):
789
+ @functools.wraps(value)
790
+ def f(self, *args, **kwargs):
791
+ op = self.to_operation()
792
+ return getattr(op, attr)(*args, **kwargs)
793
+ return f
794
+ else:
795
+ @functools.wraps(value)
796
+ def f(self):
797
+ op = self.to_operation()
798
+ return getattr(op, attr)
799
+ return property(f)
800
+
801
+ for attr, value in inspect.getmembers(otp.Operation):
802
+ # comparison methods are defined by default for some reason,
803
+ # but we want to get them from otp.Operation
804
+ if not hasattr(cls, attr) or attr in ('__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__'):
805
+ setattr(cls, attr, proxy_wrap(attr, value))
806
+
807
+ return super().__new__(cls)
808
+
809
+ def __init__(self, value):
810
+ supported_types = (str, int, float)
811
+ if not isinstance(value, supported_types):
812
+ raise TypeError("Parameter 'value' must be one of these types: {supported_types}")
813
+ self.__value = value
814
+
815
+ @classmethod
816
+ def _to_onetick_type_string(cls):
817
+ # called by ott.type2str
818
+ return 'decimal'
755
819
 
756
- def __truediv__(self, other):
757
- return self.__wrap(super().__truediv__(other))
820
+ def _to_onetick_string(self):
821
+ # called by ott.value2str
822
+ value = str(self.__value)
823
+ return f'STRING_TO_DECIMAL({value2str(value)})'
758
824
 
759
- def __rtruediv__(self, other):
760
- return self.__wrap(super().__rtruediv__(other))
825
+ def to_operation(self):
826
+ return otp.Operation(op_str=self._to_onetick_string(), dtype=decimal)
761
827
 
762
828
  def __str__(self):
763
- return super().__repr__()
829
+ # called by otp.CSV, we don't need to convert the value with OneTick functions in this case
830
+ return str(self.__value)
764
831
 
765
832
  def __repr__(self):
766
- return f"{self.__class__.__name__}({self})"
833
+ return f"{self.__class__.__name__}({value2str(self.__value)})"
834
+
835
+ def __format__(self, __format_spec: str) -> str:
836
+ return _decimal.Decimal(self.__value).__format__(__format_spec)
767
837
 
768
838
  # --------------------------------------------------------------- #
769
839
  # AUXILIARY FUNCTIONS
@@ -809,7 +879,7 @@ def get_source_base_type(value):
809
879
  value_type = nsectime
810
880
 
811
881
  # check valid value type
812
- if get_base_type(value_type) not in [int, float, str, bool]:
882
+ if get_base_type(value_type) not in [int, float, str, bool, decimal]:
813
883
  raise TypeError(f'Type "{repr(value_type)}" is not supported.')
814
884
 
815
885
  if not is_type_basic(value_type):
@@ -818,7 +888,7 @@ def get_source_base_type(value):
818
888
 
819
889
 
820
890
  def is_type_supported(dtype):
821
- return get_base_type(dtype) in [int, float, str, bool] or issubclass(dtype, (datetime, date))
891
+ return get_base_type(dtype) in [int, float, str, bool, decimal] or issubclass(dtype, (datetime, date))
822
892
 
823
893
 
824
894
  def get_base_type(obj):
@@ -830,6 +900,8 @@ def get_base_type(obj):
830
900
  return int
831
901
  elif issubclass(obj, float):
832
902
  return float
903
+ elif issubclass(obj, decimal):
904
+ return decimal
833
905
 
834
906
  return type(None)
835
907
 
@@ -1686,6 +1758,8 @@ def type2str(t):
1686
1758
  return "double"
1687
1759
  if t is None:
1688
1760
  return ''
1761
+ if t is decimal:
1762
+ return t._to_onetick_type_string()
1689
1763
  return str(t)
1690
1764
 
1691
1765
 
@@ -1841,15 +1915,16 @@ def value2str(v):
1841
1915
  # there is no escape, so replacing double quotes with concatenation with it
1842
1916
  return '"' + str(v).replace('"', '''"+'"'+"''') + '"'
1843
1917
 
1844
- if isinstance(v, (float, decimal)) and not (isinstance(v, (_inf, _nan))):
1918
+ if isinstance(v, decimal):
1919
+ return v._to_onetick_string()
1920
+
1921
+ if isinstance(v, float) and not (isinstance(v, (_inf, _nan))):
1845
1922
  # PY-286: support science notation
1846
1923
  s = str(v)
1847
1924
  if "e" in s:
1848
- s = f"{v:.20f}".rstrip("0")
1925
+ return f'atof({value2str(s)})'
1849
1926
  if s == "nan":
1850
1927
  return str(nan)
1851
- if isinstance(v, decimal):
1852
- return f'DECIMAL({s})'
1853
1928
  return s
1854
1929
 
1855
1930
  if is_time_type(v):
@@ -1960,7 +2035,7 @@ def default_by_type(dtype):
1960
2035
  >>> otp.default_by_type(float)
1961
2036
  nan
1962
2037
  >>> otp.default_by_type(otp.decimal)
1963
- decimal(0.0)
2038
+ decimal(0)
1964
2039
  >>> otp.default_by_type(int)
1965
2040
  0
1966
2041
  >>> otp.default_by_type(otp.ulong)
@@ -1,6 +1,7 @@
1
1
  import os
2
2
  import re
3
3
  import html
4
+ import textwrap
4
5
  import graphviz as gv
5
6
  from collections import defaultdict, deque
6
7
  from datetime import datetime
@@ -29,6 +30,82 @@ IF_ELSE_EPS = {
29
30
  }
30
31
 
31
32
 
33
+ def _parse_table_fields(line: str) -> list:
34
+ result = line.strip().split(',')
35
+ for idx in range(0, len(result) - 1):
36
+ result[idx] = result[idx] + ','
37
+
38
+ return result
39
+
40
+
41
+ def _light_function_splitter(line: str, sep=',') -> list:
42
+ lines = []
43
+ current_line: list = []
44
+ parentheses_stack = 0
45
+ quotes_stack = 0
46
+ lead_quote_type = None
47
+
48
+ for ch in line:
49
+ if ch == sep and not parentheses_stack and not quotes_stack:
50
+ lines.append(''.join(current_line) + sep)
51
+ current_line = []
52
+ continue
53
+
54
+ current_line.append(ch)
55
+
56
+ if ch == '(' and not quotes_stack:
57
+ parentheses_stack += 1
58
+ continue
59
+
60
+ if ch == ')' and not quotes_stack:
61
+ parentheses_stack -= 1
62
+ if parentheses_stack < 0:
63
+ break
64
+
65
+ if ch in ["\"", "'"]:
66
+ if lead_quote_type is None:
67
+ lead_quote_type = ch
68
+ quotes_stack = 1
69
+ elif ch == lead_quote_type:
70
+ lead_quote_type = None
71
+ quotes_stack = 0
72
+
73
+ if parentheses_stack != 0:
74
+ raise ValueError(f'Incorrect parentheses count in function: `{line}`')
75
+
76
+ if quotes_stack != 0:
77
+ raise ValueError(f'Incorrect quotes count in function: `{line}`')
78
+
79
+ lines.append(''.join(current_line))
80
+
81
+ return lines
82
+
83
+
84
+ EP_TO_MULTILINE_ATTRS: dict = {
85
+ "ADD_FIELDS": {
86
+ "set": _light_function_splitter,
87
+ },
88
+ "UPDATE_FIELDS": {
89
+ "set": _light_function_splitter,
90
+ },
91
+ "TABLE": {
92
+ "fields": _parse_table_fields,
93
+ },
94
+ "PASSTHROUGH": {
95
+ "fields": _parse_table_fields,
96
+ },
97
+ "COMPUTE": {
98
+ "compute": _light_function_splitter,
99
+ },
100
+ "DECLARE_STATE_VARIABLES": {
101
+ "variables": _light_function_splitter,
102
+ },
103
+ "RENAME_FIELDS": {
104
+ "rename_fields": _parse_table_fields,
105
+ }
106
+ }
107
+
108
+
32
109
  @dataclass
33
110
  class NestedQuery:
34
111
  name: str
@@ -53,6 +130,14 @@ class NestedQuery:
53
130
  return "::".join(i for i in [self.file_path, self.query] if i)
54
131
 
55
132
 
133
+ @dataclass
134
+ class Config:
135
+ height: int = field(default=0)
136
+ width: int = field(default=0)
137
+ render_debug_info: bool = field(default=False)
138
+ constraint_edges: str = field(default="true")
139
+
140
+
56
141
  @dataclass
57
142
  class EP:
58
143
  name: str
@@ -341,11 +426,14 @@ def _parse_function_params(func_params: str) -> Tuple[list, dict]:
341
426
  return args, kwargs
342
427
 
343
428
 
344
- def _parse_function(expression: str) -> Tuple[Optional[str], list, dict]:
429
+ def _parse_function(expression: str, pattern: Optional[str] = None) -> Tuple[Optional[str], list, dict]:
345
430
  # EP_NAME(PARAM_NAME=PARAM_VALUE,...)
346
431
  # [a-zA-Z_:] is EP_NAME, can contain letters, underscore and colon
347
432
  # [\s\S] is any symbol including newline (because . doesn't include newline by default)
348
- m = re.search(r"^([a-zA-Z_:]*)\s*\(([\s\S]*)\)\s*$", expression)
433
+ if not pattern:
434
+ pattern = r"^([a-zA-Z_:]*)\s*\(([\s\S]*)\)\s*$"
435
+
436
+ m = re.search(pattern, expression)
349
437
 
350
438
  if not m:
351
439
  return None, [], {}
@@ -412,9 +500,9 @@ def _parse_ep(ep_string: str, parse_eval_from_params: bool = False) -> Union[EP,
412
500
  is_query_found = True
413
501
 
414
502
  if kwargs_key in kwargs:
415
- query_path = kwargs.pop(kwargs_key)[1]
503
+ query_path = kwargs[kwargs_key][1]
416
504
  elif 0 <= args_idx < len(args):
417
- query_path = args.pop(args_idx)
505
+ query_path = args[args_idx]
418
506
  else:
419
507
  # don't do anything, just process as EP
420
508
  is_query_found = False
@@ -697,32 +785,59 @@ def read_otq(path: str, parse_eval_from_params: bool = False) -> Optional[Graph]
697
785
  return graph
698
786
 
699
787
 
700
- def truncate_param_value(ep: Any, param, value, line_limit: Optional[Tuple[int, int]] = None):
701
- if line_limit is None:
702
- return value
788
+ def _truncate_param_value(value, height, width):
789
+ lines = [
790
+ line if len(line) <= width or not width else line[:width] + "..."
791
+ for line in value.splitlines()
792
+ ]
703
793
 
704
- height, width = line_limit
705
- if height < 0 or width < 0:
706
- raise ValueError("line_limit values should not be negative")
794
+ if height and len(lines) > height:
795
+ lines = lines[:height] + ["..."]
796
+
797
+ return "\n".join(lines)
798
+
799
+
800
+ def _split_long_value_to_lines(value, height, width, indent=0, escape=False) -> list:
801
+ if len(value) <= width:
802
+ return [value]
803
+
804
+ result = []
805
+ lines = value.splitlines()
806
+
807
+ # textwrap.wrap replaces newline character to whitespace and brakes multiline strings
808
+ # If replace_whitespace=False, it preserves newline, but not use it for result array line splitting
809
+ for line in lines:
810
+ result.extend(textwrap.wrap(line, width=width, replace_whitespace=False))
811
+
812
+ if escape:
813
+ result = [html.escape(s) for s in result]
814
+
815
+ if indent:
816
+ indent_str = "&nbsp;" * indent
817
+ for i in range(1, len(result)):
818
+ result[i] = indent_str + result[i]
707
819
 
820
+ if height and len(result) > height:
821
+ result = result[:height] + ['...']
822
+ return result
823
+
824
+
825
+ def transform_param_value(ep: Any, param, value, height, width):
708
826
  if isinstance(ep, EP) and (
709
827
  ep.name == "PER_TICK_SCRIPT" and param.lower() == "script" or
710
828
  ep.name == "CSV_FILE_LISTING" and param.lower() == "file_contents"
711
829
  ):
712
- lines = [
713
- line if len(line) <= width or not width else line[:width] + "..."
714
- for line in value.split("\n")
715
- ]
716
-
717
- if height and len(lines) > height:
718
- lines = lines[:height] + ["..."]
830
+ return _truncate_param_value(value, height, width)
719
831
 
720
- return "\n".join(lines)
832
+ if not (isinstance(ep, EP) and EP_TO_MULTILINE_ATTRS.get(ep.name, {}).get(param.lower())):
833
+ return "\n".join(_split_long_value_to_lines(value, height, width))
721
834
 
722
835
  return value
723
836
 
724
837
 
725
- def build_symbols(symbols, gr_nested, gr_static, graphs: GraphStorage, graph_node, reverse=False):
838
+ def build_symbols(
839
+ symbols, gr_nested, gr_static, graphs: GraphStorage, graph_node, config: Config, reverse=False, graph_file=None,
840
+ ):
726
841
  table = GVTable()
727
842
 
728
843
  for symbol_data in symbols:
@@ -732,11 +847,17 @@ def build_symbols(symbols, gr_nested, gr_static, graphs: GraphStorage, graph_nod
732
847
  if symbol.query:
733
848
  if symbol.is_local:
734
849
  # reversed directions here brakes everything
850
+
851
+ if graph_file is None:
852
+ raise ValueError('`graph_file` parameter required for this case')
853
+
854
+ nested_cluster_id = graphs.get_query_unique_id(symbol.query, graph_file)
855
+
735
856
  gr_nested.edge(
736
- f"cluster__{symbol.query}__footer",
857
+ f"{nested_cluster_id}__footer",
737
858
  f"{graph_node}:symbols",
738
- ltail=f"cluster__{symbol.query}",
739
- style="dashed", constraint="false",
859
+ ltail=f"{nested_cluster_id}",
860
+ style="dashed", dir="both", constraint=config.constraint_edges,
740
861
  )
741
862
  continue
742
863
 
@@ -747,7 +868,7 @@ def build_symbols(symbols, gr_nested, gr_static, graphs: GraphStorage, graph_nod
747
868
  f"{nested_cluster_id}__footer",
748
869
  f"{graph_node}:symbols",
749
870
  ltail=nested_cluster_id,
750
- style="dashed", constraint="false",
871
+ style="dashed", dir="both", constraint=config.constraint_edges,
751
872
  )
752
873
  continue
753
874
 
@@ -764,11 +885,48 @@ def build_symbols(symbols, gr_nested, gr_static, graphs: GraphStorage, graph_nod
764
885
  gr_static.edge(
765
886
  f"{graph_node}__symbols" if not reverse else f"{graph_node}:symbols",
766
887
  f"{graph_node}:symbols" if not reverse else f"{graph_node}__symbols",
767
- style="dashed", constraint="false" if not reverse else "true",
888
+ style="dashed", constraint=config.constraint_edges,
768
889
  )
769
890
 
770
891
 
771
- def build_node(graphs: GraphStorage, node: Node, line_limit: Optional[Tuple[int, int]] = None):
892
+ def _parse_special_attribute(param_name, param_lines, parser, height, width, cols=4):
893
+ """
894
+ Builds better param representation for selected parameters and EPs
895
+ """
896
+ def generate_row_string(_line: list) -> list:
897
+ sep = "&nbsp;&nbsp;&nbsp;&nbsp;"
898
+
899
+ # only in this case line could be longer than width
900
+ if len(_line) == 1 and len(_line[0]) > width:
901
+ _lines = _split_long_value_to_lines(_line[0], height, width, indent=4, escape=True)
902
+ else:
903
+ _lines = [sep.join(html.escape(s) for s in _line)]
904
+
905
+ return ["&nbsp;" * 2 + s for s in _lines]
906
+
907
+ param_value = ' '.join(param_lines)
908
+ params = parser(param_value)
909
+
910
+ params_table = [f"{param_name}:"]
911
+ current_line = []
912
+ current_width = 0
913
+
914
+ for param in params:
915
+ if width and current_line and current_width + len(param) >= width or len(current_line) == cols:
916
+ params_table.extend(generate_row_string(current_line))
917
+ current_line = []
918
+ current_width = 0
919
+
920
+ current_line.append(param)
921
+ current_width += len(param)
922
+
923
+ if current_line:
924
+ params_table.extend(generate_row_string(current_line))
925
+
926
+ return [(params_table, {"ALIGN": "LEFT", "BALIGN": "LEFT"})]
927
+
928
+
929
+ def build_node(graphs: GraphStorage, node: Node, config: Config):
772
930
  if node.ep is None:
773
931
  raise ValueError(f"EP of node {node.id} could not be None")
774
932
 
@@ -786,6 +944,9 @@ def build_node(graphs: GraphStorage, node: Node, line_limit: Optional[Tuple[int,
786
944
  if node.tick_type:
787
945
  table.cell([node.tick_type])
788
946
 
947
+ if config.render_debug_info:
948
+ table.cell([node.id])
949
+
789
950
  if node.symbols:
790
951
  table.cell([("[■]", {"port": "symbols"})])
791
952
 
@@ -793,6 +954,10 @@ def build_node(graphs: GraphStorage, node: Node, line_limit: Optional[Tuple[int,
793
954
  params: List[Tuple[Optional[str], Union[str, NestedQuery]]] = \
794
955
  [(None, v) for v in node.ep.args] + list(node.ep.kwargs.values())
795
956
 
957
+ param_args_lines = []
958
+ param_kwargs_lines = []
959
+ special_params = []
960
+
796
961
  for idx, data in enumerate(params):
797
962
  k, v = data
798
963
  attrs = {"port": k}
@@ -806,34 +971,53 @@ def build_node(graphs: GraphStorage, node: Node, line_limit: Optional[Tuple[int,
806
971
  else:
807
972
  param_value = v
808
973
 
809
- param_value = truncate_param_value(node.ep, k, param_value, line_limit)
810
- param_value = html.escape(param_value).replace("\t", "&nbsp;" * 4)
811
- param_lines = param_value.split("\n")
974
+ is_special_attribute = k and EP_TO_MULTILINE_ATTRS.get(node.ep.name, {}).get(k.lower())
812
975
 
813
- if k:
814
- if len(param_lines) == 1:
815
- param_lines[0] = f"{html.escape(k)}={param_lines[0]}"
816
- else:
817
- param_lines = [f"{html.escape(k)}="] + param_lines
976
+ param_value = transform_param_value(node.ep, k, param_value, config.height, config.width)
818
977
 
819
- if len(param_lines) > 1:
820
- # Add idents disable default horizontal central align
821
- # if there are multiline parameter for EP.
822
- # Align change affects all parameters for EP.
823
- for i in range(len(param_lines)):
824
- if i > 0:
825
- param_lines[i] = "&nbsp;" * 2 + param_lines[i]
978
+ if not is_special_attribute:
979
+ param_value = html.escape(param_value)
826
980
 
827
- attrs.update({"ALIGN": "LEFT", "BALIGN": "LEFT"})
981
+ param_value = param_value.replace("\t", "&nbsp;" * 4)
982
+ param_lines = param_value.splitlines()
828
983
 
984
+ # additional k check required by mypy
985
+ if is_special_attribute and k:
986
+ special_params.extend(
987
+ _parse_special_attribute(
988
+ k, param_lines, EP_TO_MULTILINE_ATTRS[node.ep.name][k.lower()], config.height, config.width,
989
+ )
990
+ )
991
+ else:
992
+ if k:
993
+ if len(param_lines) == 1:
994
+ param_lines[0] = f"{html.escape(k)}={param_lines[0]}"
995
+ else:
996
+ param_lines = [f"{html.escape(k)}:"] + param_lines
997
+
998
+ if len(param_lines) > 1:
999
+ # Add idents disable default horizontal central align
1000
+ # if there are multiline parameter for EP.
1001
+ # Align change affects all parameters for EP.
1002
+ for i in range(len(param_lines)):
1003
+ if i > 0:
1004
+ param_lines[i] = "&nbsp;" * 2 + param_lines[i]
1005
+
1006
+ attrs.update({"ALIGN": "LEFT", "BALIGN": "LEFT"})
1007
+
1008
+ if k:
1009
+ param_kwargs_lines.append((param_lines, attrs))
1010
+ else:
1011
+ param_args_lines.append((param_lines, attrs))
1012
+
1013
+ for param_lines, attrs in param_args_lines + special_params + param_kwargs_lines:
829
1014
  table.row([param_lines], attrs=attrs)
830
1015
 
831
1016
  if node.params:
832
- table.row([[f"{html.escape(k)}={html.escape(v)}" for k, v in node.params.items()]])
833
-
834
- if isinstance(node.ep, NestedQuery):
835
- if not (node.ep.is_local and node.ep.query or graphs.get_query(node.ep.file_path, node.ep.query)):
836
- table.row([node.ep.to_string()])
1017
+ table.row([[
1018
+ f"{html.escape(k)}={html.escape(_truncate_param_value(v, config.height, config.width))}"
1019
+ for k, v in node.params.items()
1020
+ ]])
837
1021
 
838
1022
  if isinstance(node.ep, IfElseEP):
839
1023
  table.row([
@@ -886,7 +1070,7 @@ def _get_nested_query(nested_query: NestedQuery, local_graph: Graph, graphs: Gra
886
1070
 
887
1071
 
888
1072
  def _render_graph(
889
- gr_root, gr, graphs: GraphStorage, graph_name: str, queries: set, line_limit: Optional[Tuple[int, int]] = None,
1073
+ gr_root, gr, graphs: GraphStorage, graph_name: str, queries: set, config: Config,
890
1074
  ):
891
1075
  graph = graphs[graph_name]
892
1076
 
@@ -947,15 +1131,18 @@ def _render_graph(
947
1131
 
948
1132
  gr_sub.edge(
949
1133
  f"{footer_id}:params", f"{query_id}__params",
950
- style="dashed", constraint="true",
1134
+ style="dashed", constraint=config.constraint_edges,
951
1135
  )
952
1136
 
953
1137
  if query.symbols:
954
- build_symbols(query.symbols, gr, gr_sub, graphs, f"{query_id}__footer", reverse=True)
1138
+ build_symbols(
1139
+ query.symbols, gr, gr_sub, graphs, f"{query_id}__footer", config,
1140
+ reverse=True, graph_file=graph.file_path,
1141
+ )
955
1142
 
956
1143
  for node_id, node in query.nodes.items():
957
1144
  node_unique_id = _get_node_unique_id(node, query)
958
- gr_sub.node(node_unique_id, build_node(graphs, node, line_limit), group=query_name)
1145
+ gr_sub.node(node_unique_id, build_node(graphs, node, config), group=query_name)
959
1146
 
960
1147
  for sink in node.sinks:
961
1148
  if "OUT" in node.labels:
@@ -989,11 +1176,11 @@ def _render_graph(
989
1176
  f"{node_unique_id}:{param_name}",
990
1177
  _get_node_unique_id(nested_cluster.roots[0], nested_cluster),
991
1178
  lhead=nested_cluster.get_id(),
992
- style="dashed", dir="both",
1179
+ style="dashed", dir="both", constraint=config.constraint_edges,
993
1180
  )
994
1181
 
995
1182
  if node.symbols:
996
- build_symbols(node.symbols, gr, gr_sub, graphs, node_unique_id)
1183
+ build_symbols(node.symbols, gr, gr_sub, graphs, node_unique_id, config, graph_file=graph.file_path)
997
1184
 
998
1185
  if isinstance(node.ep, NestedQuery):
999
1186
  nested_cluster = _get_nested_query(node.ep, graph, graphs)
@@ -1004,7 +1191,7 @@ def _render_graph(
1004
1191
  node_unique_id,
1005
1192
  _get_node_unique_id(nested_cluster.roots[0], nested_cluster),
1006
1193
  lhead=nested_cluster.get_id(),
1007
- style="dashed", dir="both",
1194
+ style="dashed", dir="both", constraint=config.constraint_edges,
1008
1195
  )
1009
1196
 
1010
1197
 
@@ -1014,9 +1201,11 @@ def render_otq(
1014
1201
  output_format: Optional[str] = None,
1015
1202
  load_external_otqs: bool = True,
1016
1203
  view: bool = False,
1017
- line_limit: Optional[Tuple[int, int]] = (10, 30),
1204
+ line_limit: Optional[Tuple[int, int]] = (10, 60),
1018
1205
  parse_eval_from_params: bool = False,
1206
+ render_debug_info: bool = False,
1019
1207
  debug: bool = False,
1208
+ graphviz_compat_mode: bool = False,
1020
1209
  ) -> str:
1021
1210
  """
1022
1211
  Render queries from .otq files.
@@ -1029,7 +1218,7 @@ def render_otq(
1029
1218
  image_path: str, None
1030
1219
  Path for generated image. If omitted, image will be saved in a temp dir
1031
1220
  output_format: str, None
1032
- `Graphviz` rendering format. Default: `png`.
1221
+ `Graphviz` rendering format. Default: `svg`.
1033
1222
  If `image_path` contains one of next extensions, `output_format` will be set automatically: `png`, `svg`, `dot`.
1034
1223
  load_external_otqs: bool
1035
1224
  If set to `True` (default) dependencies from external .otq files (not listed in ``path`` param)
@@ -1043,8 +1232,13 @@ def render_otq(
1043
1232
  If one of tuple values set to zero the corresponding limit disabled.
1044
1233
  parse_eval_from_params: bool
1045
1234
  Enable parsing and printing `eval` sub-queries from EP parameters.
1235
+ render_debug_info: bool
1236
+ Render additional debug information.
1046
1237
  debug: bool
1047
1238
  Allow to print stdout or stderr from `Graphviz` render.
1239
+ graphviz_compat_mode: bool
1240
+ Change internal parameters of result graph for better compatibility with old `Graphviz` versions.
1241
+ Could produce larger and less readable graphs.
1048
1242
 
1049
1243
  Returns
1050
1244
  -------
@@ -1069,6 +1263,19 @@ def render_otq(
1069
1263
 
1070
1264
  >>> otp.utils.render_otq(["./first.otq", "./second.otq::some_query"]) # doctest: +SKIP
1071
1265
  """
1266
+ if line_limit is None:
1267
+ line_limit = (0, 0)
1268
+
1269
+ height, width = line_limit
1270
+ if height < 0 or width < 0:
1271
+ raise ValueError("line_limit values should not be negative")
1272
+
1273
+ config_kwargs = {}
1274
+ if graphviz_compat_mode:
1275
+ config_kwargs["constraint_edges"] = "false"
1276
+
1277
+ config = Config(height=height, width=width, render_debug_info=render_debug_info, **config_kwargs)
1278
+
1072
1279
  if not isinstance(path, list):
1073
1280
  path = [path]
1074
1281
 
@@ -1137,7 +1344,7 @@ def render_otq(
1137
1344
  output_format = extension
1138
1345
 
1139
1346
  if not output_format:
1140
- output_format = "png"
1347
+ output_format = "svg"
1141
1348
 
1142
1349
  if not image_path:
1143
1350
  image_path = TmpFile().path
@@ -1154,8 +1361,14 @@ def render_otq(
1154
1361
  with gr.subgraph(name=f"cluster__graph__{idx}", node_attr={"shape": "plaintext"}) as gr_otq:
1155
1362
  gr_otq.attr(label=otq_path)
1156
1363
  gr_otq.attr(margin="16")
1157
- _render_graph(gr, gr_otq, graphs, otq_path, queries_to_render[otq_path], line_limit)
1364
+ _render_graph(gr, gr_otq, graphs, otq_path, queries_to_render[otq_path], config)
1158
1365
 
1159
1366
  idx += 1
1160
1367
 
1161
- return gr.render(view=view, quiet=not debug)
1368
+ try:
1369
+ return gr.render(view=view, quiet=not debug)
1370
+ except Exception as exc:
1371
+ raise RuntimeError(
1372
+ "Graphviz render failed. Try to set parameter `graphviz_compat_mode=True` "
1373
+ "for better compatibility if you use old Graphviz version"
1374
+ ) from exc
onetick/py/utils/types.py CHANGED
@@ -8,6 +8,8 @@ def get_type_that_includes(types):
8
8
  if b_type1 != b_type2:
9
9
  if {b_type1, b_type2} == {int, float}:
10
10
  dtype = float
11
+ elif {b_type1, b_type2} == {ott.decimal, float} or {b_type1, b_type2} == {ott.decimal, int}:
12
+ dtype = ott.decimal
11
13
  elif {b_type1, b_type2} == {ott.nsectime, ott.msectime}:
12
14
  dtype = ott.nsectime
13
15
  else:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onetick-py
3
- Version: 1.173.0
3
+ Version: 1.175.0
4
4
  Summary: Python package that allows you to work with OneTick
5
5
  Author-email: solutions <solutions@onetick.com>
6
6
  License-Expression: MIT
@@ -36,7 +36,7 @@ Requires-Dist: numpy==1.26.4; python_version == "3.12" and extra == "strict"
36
36
  Requires-Dist: pandas==1.5.2; python_version == "3.9" and extra == "strict"
37
37
  Requires-Dist: pandas==1.5.3; python_version == "3.10" and extra == "strict"
38
38
  Requires-Dist: pandas==1.5.3; python_version == "3.11" and extra == "strict"
39
- Requires-Dist: pandas==2.2.0; python_version == "3.12" and extra == "strict"
39
+ Requires-Dist: pandas==2.2.1; python_version == "3.12" and extra == "strict"
40
40
  Requires-Dist: pandas==2.3.0; python_version == "3.13" and extra == "strict"
41
41
  Requires-Dist: pandas==2.3.3; python_version == "3.14" and extra == "strict"
42
42
  Provides-Extra: webapi
@@ -13,12 +13,12 @@ onetick/lib/__init__.py,sha256=Rp7CIDoA4E6LIm1f2mNvl_5b_n-0U3suA3FmBXbmKoU,114
13
13
  onetick/lib/instance.py,sha256=3FJB8PWs2ap-EGb6DzsnLRL2meTMUViTdy343m6tHvM,4825
14
14
  onetick/py/__init__.py,sha256=Ge1Ekl6OHRjm5KrXojq3AzLCaUGvLNWuaIK0C9DdwWU,11164
15
15
  onetick/py/_stack_info.py,sha256=PHZOkW_fK7Fbl4YEj5CaYK9L6vh4j-bUU7_cSYOWZ30,2546
16
- onetick/py/_version.py,sha256=xgAl00OwXSmPbqAEjJJgqhprH9I04svQhlQygpZzYfQ,76
16
+ onetick/py/_version.py,sha256=T2BdtqEAyiIrXPUtz7ueDQGJEIwu9w3KvGz9NNm-yjE,76
17
17
  onetick/py/backports.py,sha256=mR00mxe7E7UgBljf-Wa93Mo6lpi-C4Op561uhPUoEt8,815
18
18
  onetick/py/cache.py,sha256=BBZg8n0AGjZzZapg4752LkSZdX5C6DGf7vU9sAStv6A,12798
19
19
  onetick/py/compatibility.py,sha256=PyQVs4h8vs_9qKocb117jyu1sNmUvqHj9o9r9NqI-9E,31036
20
20
  onetick/py/configuration.py,sha256=KCX44v_nEOZDvo-rItIrNVKKvqyM73QUwIivez2VHvY,28448
21
- onetick/py/functions.py,sha256=yJKL4thjNh_u-DKY08rCDMGJO_IMAeN2Odu70Q9U3Ew,97937
21
+ onetick/py/functions.py,sha256=Kqromia64o1K4fgbLbijw64o5pFGBZRog52824wOxlA,98004
22
22
  onetick/py/license.py,sha256=50dsFrE-NKsPOdkAoyxHr44bH8DzFCr_6TabX0JH6tQ,6140
23
23
  onetick/py/log.py,sha256=Els2drZcVjJrD6kvbwgFGEpg6jAacoUEtyN6rCaauuk,2723
24
24
  onetick/py/math.py,sha256=MZvlyUjxOWGJvmxK8pfMkCr4THM52AE6NyGEidgDMyE,26311
@@ -30,17 +30,17 @@ onetick/py/servers.py,sha256=h6l0X55kcnpI25bZcYaBpPAAGBZjqk9mt1SQe4xZrh0,6840
30
30
  onetick/py/session.py,sha256=AqoS7BFN_Sz3JU2CAaZm0Bro8CmUy6VjmoDjwOU1wYM,49591
31
31
  onetick/py/sql.py,sha256=kJ0732bZ0OGF8x3VzcOUs38zEGsxcd1nZOSN75VKXhA,2993
32
32
  onetick/py/state.py,sha256=giQpUUXWwuz_CL_VvxNQDaM71LAMPqeRgHfoGhS1gTk,10330
33
- onetick/py/types.py,sha256=U4FSaMN_y1OUrcN6reVidbew-X2jIWj6S_lS6GvYsUo,64599
33
+ onetick/py/types.py,sha256=jzGyG01UmjEy_ISCzgj2S7LzGx35oesQRSjYqVdg_D0,68079
34
34
  onetick/py/aggregations/__init__.py,sha256=02WnTc9ocTp34OkGyXRHY2QAZi_QjDMHClE5tvx6Gtk,736
35
- onetick/py/aggregations/_base.py,sha256=YQWlFYlq_QvGwTIaNGuyoU8rQsYfx-Yd-lNNq4R_o6o,25702
35
+ onetick/py/aggregations/_base.py,sha256=HbiicjQI6nYmfGZxI1Hx9QdDJoQ4kcCmOI8E-3VZrjc,25715
36
36
  onetick/py/aggregations/_docs.py,sha256=7pz6XIhRuP_1vuOYO9UfTlzl6zGD2_jwbPMfJPOX-M8,34798
37
37
  onetick/py/aggregations/compute.py,sha256=GJpR8AWlEmfNsnl-L2oJPuY5rFb6PC-VwLZnnRGtCys,11854
38
38
  onetick/py/aggregations/functions.py,sha256=MnjINLAxoLehgInxhY-xSI1xw8TtCGzT7APD5HdFmjE,80801
39
39
  onetick/py/aggregations/generic.py,sha256=7mWRRAPS-t-e_umQc2T3GPPuW1Pia7mKhgjaVB2sSBM,3896
40
40
  onetick/py/aggregations/high_low.py,sha256=B7_Y6KhJjIqJthK0msW0JRkTpxINpnKPuSQjQa2QyZs,2508
41
41
  onetick/py/aggregations/num_distinct.py,sha256=s-VrU92kBKnJ6LIAGq2-j-t8amnolIF2EHkEt78A7_s,2830
42
- onetick/py/aggregations/order_book.py,sha256=RLzQk0HOG4Why7tNag3pT1GG5nBL_bBnJtID0kFr2y4,19070
43
- onetick/py/aggregations/other.py,sha256=wCXrb3BCUJy5e39i35V2tslp0bVR2-8Fa8E1lf45hO4,39410
42
+ onetick/py/aggregations/order_book.py,sha256=4_NPXWy5GmtGK4W3fmT089sq3jIROn3mcgc-hX8fqVw,19439
43
+ onetick/py/aggregations/other.py,sha256=0FZw8AIqo73duVsH7Jw7Sx7TcMABoUZAq866P-eII04,39436
44
44
  onetick/py/callback/__init__.py,sha256=lF6u1jDy0dv-2qBvonqmSaQAU7dWQAuq5FWyR5LzvLI,108
45
45
  onetick/py/callback/callback.py,sha256=i8yVsbceqhVxNTcXU2ie0-nBRyC0nS2LVDFFtqfCMlw,8618
46
46
  onetick/py/callback/callbacks.py,sha256=_8Yw_b-OITXUtNTxGJIXm-tz-KfHZYe6SdoyW7-QX4w,5011
@@ -54,7 +54,7 @@ onetick/py/core/lambda_object.py,sha256=ayob_2c2XYSG7vFsqp-2b9FSPD-3TFReLbx3de3t
54
54
  onetick/py/core/multi_output_source.py,sha256=f3dXr9kwmED77oWDFVr9cn44aYiJxC81loq2pX4u50Q,8766
55
55
  onetick/py/core/per_tick_script.py,sha256=I3pfidriS1TrFPc3DAABt91GKMgby9P_8KBatiFQx6U,83322
56
56
  onetick/py/core/query_inspector.py,sha256=Jwnw7qUVvjvaT63mQ5PkUSy8x36knoxWrNll3aXuLZw,16440
57
- onetick/py/core/source.py,sha256=AX4DUK1frV_FUCG5sywsn42xYX7Y2sYC7vMCOq6TeRg,63998
57
+ onetick/py/core/source.py,sha256=h-J7Fkl5AyGiW7c0gPoUgjrZxD-6RXO6DHbJLgikByk,64421
58
58
  onetick/py/core/_internal/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
59
59
  onetick/py/core/_internal/_manually_bound_value.py,sha256=a4JF63g9MtXsIwpDqm_PCLBH51_SaG1-T6jWV5-IJ1k,191
60
60
  onetick/py/core/_internal/_nodes_history.py,sha256=fCpJ4FWFDzM-bmi-YyCHmNYuFM-CL_lE2bXySOi-TPY,7194
@@ -81,7 +81,7 @@ onetick/py/core/_source/source_methods/fields.py,sha256=6GozvRornClXfEu6oY9-JpN3
81
81
  onetick/py/core/_source/source_methods/filters.py,sha256=WuSnK-tQsP3fL2bqVfMZpbpPgk_LG4L8lrYULSSGoKE,33930
82
82
  onetick/py/core/_source/source_methods/joins.py,sha256=edLIJ-BTWbwvxga1YCzsN6NdhIwtJyZIksZawvPWCbU,61175
83
83
  onetick/py/core/_source/source_methods/merges.py,sha256=efk-R4WOjB2pcAIGWPsZsDzYuitADztH5xtRWTBfutk,23115
84
- onetick/py/core/_source/source_methods/misc.py,sha256=iWXre-Dker16oAnv6kGxyFk2FU_apMGiZQRppcUhr0c,57026
84
+ onetick/py/core/_source/source_methods/misc.py,sha256=tPkyYc7aS6JxUOuPonFeDJ-bO0FKyhT7Zn0EuGvh5lg,59689
85
85
  onetick/py/core/_source/source_methods/pandases.py,sha256=BbvIlw6ipqC2dOiKSyyZ5jhAtE4ZtdL30OZXV_e_FuE,3670
86
86
  onetick/py/core/_source/source_methods/renames.py,sha256=T5EXq-oHq5NwApPZfYRDOJPVjclqLqAofRAP-Ykzu2M,13749
87
87
  onetick/py/core/_source/source_methods/sorts.py,sha256=tDFt3rObNHP-S_HOtDd6SLpgkCCGJnSDcMUxLake080,5583
@@ -90,12 +90,12 @@ onetick/py/core/_source/source_methods/symbols.py,sha256=MG0dgVVVJ6fD_-GJkkXEgTe
90
90
  onetick/py/core/_source/source_methods/times.py,sha256=2kbLHG-3RQM6uWmw8pBGDiAz4R83KK46Tp1smTY6SR8,27665
91
91
  onetick/py/core/_source/source_methods/writes.py,sha256=fPUbiqlm30Kb4QzXZLtWCsQiTFBDxK5oOWuVyfVXbYM,42717
92
92
  onetick/py/core/column_operations/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
93
- onetick/py/core/column_operations/base.py,sha256=Kcwfd4PErCGLSbkqtFKY6SZL1-pksWHB-Pc-5A8I3WM,36135
93
+ onetick/py/core/column_operations/base.py,sha256=gn-4rz4FMAR2pp-7e9DulKc6L05YAMkGkIiEwzHmYmo,36102
94
94
  onetick/py/core/column_operations/_methods/__init__.py,sha256=_C5hpQG51MingtWW2N8vYzSVFpR314xIrtukBfEfPMk,207
95
95
  onetick/py/core/column_operations/_methods/_internal.py,sha256=M04DbbD9DjA3NrrbibuQPje8ylkaYnVB5Nmlwx1RvaE,878
96
- onetick/py/core/column_operations/_methods/conversions.py,sha256=QB8ZKJi5qlV8ydPn43nRHa3HMkaXCDV2Fkrq2gnVQ6U,7001
96
+ onetick/py/core/column_operations/_methods/conversions.py,sha256=V7VREn8fFd1tanRO7x0mltrJtWFnc4ltTXTrxv1Lxpc,7065
97
97
  onetick/py/core/column_operations/_methods/methods.py,sha256=OaXiDcdPYXnPlWhwD-BCac5UAecZFnEKCkxBpQOVB5E,11054
98
- onetick/py/core/column_operations/_methods/op_types.py,sha256=rUMrtIPCp7TYYJIK4f836KxZIcvW60MYv3cYTGMpfRA,5004
98
+ onetick/py/core/column_operations/_methods/op_types.py,sha256=HfoFQLVcfsAjDBigMOUxCLGoQMqEecS8IFHIE6yb2rQ,5283
99
99
  onetick/py/core/column_operations/accessors/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
100
100
  onetick/py/core/column_operations/accessors/_accessor.py,sha256=b5K4OQmnO4NeFK1QsKjgZ1WfEyBjxKWMkb12RBbnGXQ,983
101
101
  onetick/py/core/column_operations/accessors/decimal_accessor.py,sha256=DwgJ-as0p_gex6UGxhxhsn9chSpne5YV-t1ntvr-saw,3345
@@ -139,14 +139,14 @@ onetick/py/utils/helpers.py,sha256=XY2PaMUSzFNIpiuiAY_gPQ0TnYTmfoPAGNAFh_6aB14,2
139
139
  onetick/py/utils/locator.py,sha256=YUOXU0yh0ZZZOJpJniAq9WNn0_u3X_M5q2tEwt1cp_4,2887
140
140
  onetick/py/utils/perf.py,sha256=g0r-9YUc2mx9Lj4PjG6Fk1keO3m9nC3t2cpHx3M-AeQ,19299
141
141
  onetick/py/utils/query.py,sha256=b60JmfJDx3mpP74rTQC3qnlCb6t4fYFEauVaH9ulwL8,1330
142
- onetick/py/utils/render.py,sha256=9SBKfKsHCrdF-FOZFGon7TalzXydCdPHINouRUwHMKM,38289
142
+ onetick/py/utils/render.py,sha256=wTSUqkzrzrFeGQgeU5hEt1ckfW2i1hQjiEX1apUHAzs,44910
143
143
  onetick/py/utils/script.py,sha256=Y8NujEo2_5QaP6KDnLKJiKQ7SmMjw8_dv8sYL9rHDCE,11184
144
144
  onetick/py/utils/temp.py,sha256=j-BC155dE46k0zfKTTs26KTF0CK6WA1hO2GD54iunyM,17380
145
- onetick/py/utils/types.py,sha256=_tYf66r9JVgjtiCJfxIxrg_BQEjHkjlnck_i86dBYEY,3606
145
+ onetick/py/utils/types.py,sha256=7u9s9uN1jlkgud8_TSLy6iFzQFBtKJ0v3yamgOVitrs,3747
146
146
  onetick/py/utils/tz.py,sha256=sYUKigaORonp7Xa6x806xVYJ69lYJHd6NrLxQHB5AZo,2878
147
- onetick_py-1.173.0.dist-info/licenses/LICENSE,sha256=Yhu7lKNFS0fsaN-jSattEMRtCOPueP58Eu5BPH8ZGjM,1075
148
- onetick_py-1.173.0.dist-info/METADATA,sha256=LDGWo1_fYz7O3MZnNC_hPpHaMy0QtQ5J_1LY__KmG4o,7218
149
- onetick_py-1.173.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
150
- onetick_py-1.173.0.dist-info/entry_points.txt,sha256=QmK_tFswIN-SQRmtnTSBEi8GvT0TVq-66IzXXZIsV3U,81
151
- onetick_py-1.173.0.dist-info/top_level.txt,sha256=Na1jSJmVMyYGOndaswt554QKIUwQjcYh6th2ATsmw0U,23
152
- onetick_py-1.173.0.dist-info/RECORD,,
147
+ onetick_py-1.175.0.dist-info/licenses/LICENSE,sha256=Yhu7lKNFS0fsaN-jSattEMRtCOPueP58Eu5BPH8ZGjM,1075
148
+ onetick_py-1.175.0.dist-info/METADATA,sha256=Xhk0Xhbx2mZ5VTLonvn-LFab_pint8OxKYU5VR_-7XU,7218
149
+ onetick_py-1.175.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
150
+ onetick_py-1.175.0.dist-info/entry_points.txt,sha256=QmK_tFswIN-SQRmtnTSBEi8GvT0TVq-66IzXXZIsV3U,81
151
+ onetick_py-1.175.0.dist-info/top_level.txt,sha256=Na1jSJmVMyYGOndaswt554QKIUwQjcYh6th2ATsmw0U,23
152
+ onetick_py-1.175.0.dist-info/RECORD,,