onetick-py 1.182.0__py3-none-any.whl → 1.183.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.
locator_parser/locator.py CHANGED
@@ -135,6 +135,11 @@ def parse_locator(reader, writer, action=DoNothing(), recursively=False):
135
135
  cep_tick_servers_p(reader, writer, action)
136
136
  includes_p(reader, writer, action)
137
137
 
138
+ # force reset not full conditions match after processing root tags
139
+ if 0 < list(action.conditions.values()).count(True) < len(action.conditions):
140
+ for k in action.conditions:
141
+ action.conditions[k] = False
142
+
138
143
 
139
144
  # ----------------------------------------- #
140
145
  # set parent
onetick/py/_version.py CHANGED
@@ -1,2 +1,2 @@
1
1
  # This file was generated automatically. DO NOT CHANGE.
2
- VERSION = '1.182.0'
2
+ VERSION = '1.183.0'
@@ -3,7 +3,7 @@ from .functions import (compute, max, min, high_tick, low_tick, high_time, low_t
3
3
  ob_num_levels, ob_size, ob_snapshot, ob_snapshot_wide, ob_snapshot_flat, ob_summary, ob_vwap,
4
4
  generic, correlation, option_price, ranking, variance, percentile, find_value_for_percentile,
5
5
  exp_w_average, exp_tw_average, standardized_moment, portfolio_price, multi_portfolio_price,
6
- return_ep, implied_vol, linear_regression)
6
+ return_ep, implied_vol, linear_regression, partition_evenly_into_groups)
7
7
 
8
8
  try:
9
9
  from .num_distinct import num_distinct
@@ -858,6 +858,44 @@ _expiration_date_field_doc = param_doc(
858
858
  annotation=Union[str, Column],
859
859
  default='',
860
860
  )
861
+ _dependent_variable_field_name_doc = param_doc(
862
+ name='dependent_variable_field_name',
863
+ desc='''
864
+ Specifies the attribute used as the dependent variable in the calculation of the slope and intercept.
865
+ The ticks in the input time series must contain this attribute.
866
+ ''',
867
+ annotation=Union[str, Column],
868
+ )
869
+ _independent_variable_field_name_doc = param_doc(
870
+ name='independent_variable_field_name',
871
+ desc='''
872
+ Specifies the attribute used as the independent variable in the calculation of the slope and intercept.
873
+ The ticks in the input time series must contain this attribute.
874
+ ''',
875
+ annotation=Union[str, Column],
876
+ )
877
+ _field_to_partition_doc = param_doc(
878
+ name='field_to_partition',
879
+ desc='''
880
+ Specifies the tick field that will be partitioned.
881
+ ''',
882
+ annotation=Union[str, Column],
883
+ )
884
+ _weight_field_doc = param_doc(
885
+ name='weight_field',
886
+ desc='''
887
+ Specifies the tick field, the values of which are evaluated as weight; and then, the partitioning is be applied,
888
+ so that the total weight of the groups are as close as possible.
889
+ ''',
890
+ annotation=Union[str, Column],
891
+ )
892
+ _number_of_groups_doc = param_doc(
893
+ name='number_of_groups',
894
+ desc='''
895
+ Specifies the target number of partitions to which the tick should be divided.
896
+ ''',
897
+ annotation=int,
898
+ )
861
899
 
862
900
 
863
901
  class DocMetaclass(type):
@@ -9,7 +9,7 @@ from .other import (First, Last, FirstTime, LastTime, Count, Vwap, FirstTick,
9
9
  Median, Correlation, OptionPrice, Ranking, Variance,
10
10
  Percentile, FindValueForPercentile, ExpWAverage, ExpTwAverage,
11
11
  StandardizedMoment, PortfolioPrice, MultiPortfolioPrice, Return, ImpliedVol,
12
- LinearRegression)
12
+ LinearRegression, PartitionEvenlyIntoGroups)
13
13
  from .order_book import (ObSnapshot, OB_SNAPSHOT_DOC_PARAMS,
14
14
  ObSnapshotWide, OB_SNAPSHOT_WIDE_DOC_PARAMS,
15
15
  ObSnapshotFlat, OB_SNAPSHOT_FLAT_DOC_PARAMS,
@@ -64,7 +64,13 @@ from ._docs import (_column_doc,
64
64
  _strike_price_field_doc,
65
65
  _days_in_year_doc,
66
66
  _days_till_expiration_field_doc,
67
- _expiration_date_field_doc)
67
+ _expiration_date_field_doc,
68
+ _dependent_variable_field_name_doc,
69
+ _independent_variable_field_name_doc,
70
+ _field_to_partition_doc,
71
+ _weight_field_doc,
72
+ _number_of_groups_doc)
73
+
68
74
  from onetick.py.docs.utils import docstring, param_doc
69
75
 
70
76
  # This module mostly focused on providing annotations and documentation for end user
@@ -2183,7 +2189,8 @@ def implied_vol(*args, **kwargs):
2183
2189
  return ImpliedVol(*args, **kwargs)
2184
2190
 
2185
2191
 
2186
- @docstring(parameters=[_running_doc, _all_fields_doc, _bucket_interval_doc, _bucket_time_doc,
2192
+ @docstring(parameters=[_dependent_variable_field_name_doc, _independent_variable_field_name_doc,
2193
+ _running_doc, _all_fields_doc, _bucket_interval_doc, _bucket_time_doc,
2187
2194
  _bucket_units_doc, _bucket_end_condition_doc, _end_condition_per_group_doc,
2188
2195
  _boundary_tick_bucket_doc,
2189
2196
  ])
@@ -2214,3 +2221,38 @@ def linear_regression(*args, **kwargs):
2214
2221
  0 2003-12-04 -0.3 6.7
2215
2222
  """
2216
2223
  return LinearRegression(*args, **kwargs)
2224
+
2225
+
2226
+ @docstring(parameters=[_field_to_partition_doc, _weight_field_doc, _number_of_groups_doc,
2227
+ _running_doc, _bucket_interval_doc, _bucket_time_doc,
2228
+ _bucket_units_doc, _bucket_end_condition_doc, _boundary_tick_bucket_doc,
2229
+ ])
2230
+ def partition_evenly_into_groups(*args, **kwargs):
2231
+ """
2232
+ ``PARTITION_EVENLY_INTO_GROUPS`` aggregation.
2233
+
2234
+ For each bucket, this EP breaks ticks into the specified number of groups (``number_of_groups``)
2235
+ by the specified field (``field_to_partition``) in a way that the sums
2236
+ of the specified weight fields (``weight_field``) in each group are as close as possible.
2237
+
2238
+ See also
2239
+ --------
2240
+ **PARTITION_EVENLY_INTO_GROUPS** OneTick event processor
2241
+
2242
+ Examples
2243
+ --------
2244
+
2245
+ >>> data = otp.Ticks(X=['A', 'B', 'A', 'C', 'D'], SIZE=[10, 30, 20, 15, 14])
2246
+ >>> data = data.partition_evenly_into_groups(
2247
+ ... field_to_partition=data['X'],
2248
+ ... weight_field=data['SIZE'],
2249
+ ... number_of_groups=3,
2250
+ ... )
2251
+ >>> otp.run(data)
2252
+ Time FIELD_TO_PARTITION GROUP_ID
2253
+ 0 2003-12-04 A 0
2254
+ 1 2003-12-04 B 1
2255
+ 2 2003-12-04 C 2
2256
+ 3 2003-12-04 D 2
2257
+ """
2258
+ return PartitionEvenlyIntoGroups(*args, **kwargs)
@@ -1012,3 +1012,56 @@ class LinearRegression(_Aggregation, _MultiColumnAggregation):
1012
1012
  'SLOPE': float,
1013
1013
  'INTERCEPT': float,
1014
1014
  }
1015
+
1016
+
1017
+ class PartitionEvenlyIntoGroups(_Aggregation, _MultiColumnAggregation):
1018
+ NAME = 'PARTITION_EVENLY_INTO_GROUPS'
1019
+ EP = otq.PartitionEvenlyIntoGroups
1020
+
1021
+ FIELDS_MAPPING = deepcopy(_Aggregation.FIELDS_MAPPING)
1022
+ FIELDS_MAPPING['field_to_partition'] = 'FIELD_TO_PARTITION'
1023
+ FIELDS_MAPPING['weight_field'] = 'WEIGHT_FIELD'
1024
+ FIELDS_MAPPING['number_of_groups'] = 'NUMBER_OF_GROUPS'
1025
+
1026
+ FIELDS_TO_SKIP = ['column_name', 'output_field_name']
1027
+
1028
+ def __init__(
1029
+ self,
1030
+ field_to_partition: Union[_Column, str, OnetickParameter],
1031
+ weight_field: Union[_Column, str, OnetickParameter],
1032
+ number_of_groups: Union[int, OnetickParameter],
1033
+ *args, **kwargs,
1034
+ ):
1035
+ if isinstance(field_to_partition, _Column):
1036
+ field_to_partition = str(field_to_partition)
1037
+ if isinstance(weight_field, _Column):
1038
+ weight_field = str(weight_field)
1039
+
1040
+ if number_of_groups <= 0:
1041
+ raise ValueError('Parameter `number_of_groups` value should be greater than zero')
1042
+
1043
+ self.field_to_partition = field_to_partition
1044
+ self.weight_field = weight_field
1045
+ self.number_of_groups = number_of_groups
1046
+
1047
+ super().__init__(_Column('TIMESTAMP'), *args, **kwargs)
1048
+
1049
+ def validate_input_columns(self, src: 'Source'):
1050
+ super().validate_input_columns(src)
1051
+
1052
+ for column in [self.field_to_partition, self.weight_field]:
1053
+ if isinstance(column, OnetickParameter):
1054
+ continue
1055
+
1056
+ if column not in src.schema:
1057
+ raise TypeError(f"Aggregation `{self.NAME}` uses column `{column}` as input, which doesn't exist")
1058
+
1059
+ for out_field in ['FIELD_TO_PARTITION', 'GROUP_ID']:
1060
+ if out_field in src.schema:
1061
+ raise TypeError(f"Field `{out_field}`, which is `{self.NAME}` aggregation output column, is in schema.")
1062
+
1063
+ def _get_output_schema(self, src: 'Source', name: Optional[str] = None) -> dict:
1064
+ return {
1065
+ 'FIELD_TO_PARTITION': str,
1066
+ 'GROUP_ID': int,
1067
+ }
@@ -1,14 +1,13 @@
1
1
  import os
2
2
  import warnings
3
3
  from dataclasses import dataclass, astuple
4
- from datetime import datetime
4
+ from datetime import datetime, timezone as dt_timezone
5
5
  from typing import Optional
6
6
 
7
- import pandas as pd
8
7
  from packaging.version import parse as parse_version
9
8
 
10
9
  import onetick.py as otp
11
- from onetick.py.otq import otq, otli
10
+ from onetick.py.otq import otq, otli, pyomd
12
11
  from onetick.py.backports import cache
13
12
 
14
13
 
@@ -104,34 +103,57 @@ def _parse_release_string(release_string: str, build_number: int) -> OnetickVers
104
103
  raise ValueError(f"Unknown release type '{release_type}' in release string '{release_string}'")
105
104
 
106
105
 
107
- def _get_locator_min_date(db_name, context):
106
+ def _get_locator_intervals(db_name, context) -> list[tuple[datetime, datetime]]:
108
107
  graph = otq.GraphQuery(otq.DbShowConfiguredTimeRanges(db_name=db_name).tick_type('ANY')
109
108
  >> otq.Table(fields='long START_DATE, long END_DATE'))
110
109
  symbols = f'{db_name}::'
110
+
111
+ # setting this is important so we don't get access error
112
+ qp = pyomd.QueryProperties()
113
+ qp.set_property_value('IGNORE_TICKS_IN_UNENTITLED_TIME_RANGE', 'TRUE')
114
+
111
115
  result = otq.run(graph,
112
116
  symbols=symbols,
113
117
  # start and end times don't matter for this query, use some constants
114
118
  start=datetime(2003, 12, 1),
115
119
  end=datetime(2003, 12, 1),
116
- # GMT, because start/end timestamp in locator are in GMT
117
- timezone='GMT',
120
+ # timezone is irrelevant, because times are returned as epoch numbers
121
+ timezone='UTC',
122
+ query_properties=qp,
118
123
  context=context)
119
124
  data = result.output(symbols).data
120
- first_date = data['START_DATE'][0]
121
- return datetime.fromtimestamp(first_date / 1000)
125
+ if not data:
126
+ raise RuntimeError(f"Database '{db_name}' doesn't have locations")
127
+ return [
128
+ (
129
+ datetime.fromtimestamp(data['START_DATE'][i] / 1000, dt_timezone.utc).replace(tzinfo=None),
130
+ datetime.fromtimestamp(data['END_DATE'][i] / 1000, dt_timezone.utc).replace(tzinfo=None),
131
+ )
132
+ for i in range(len(data['START_DATE']))
133
+ ]
122
134
 
123
135
 
124
- def _get_onetick_version(symbols, context, start, end):
136
+ def _get_onetick_version(db_name, context, start, end) -> dict:
125
137
  node = otq.TickGenerator(bucket_interval=0,
126
138
  fields='BUILD=GET_ONETICK_VERSION(), RELEASE=GET_ONETICK_RELEASE()')
127
139
  graph = otq.GraphQuery(node.tick_type('DUMMY'))
140
+ symbols = f'{db_name}::'
141
+
142
+ # setting this is important so we don't get access error
143
+ qp = pyomd.QueryProperties()
144
+ qp.set_property_value('IGNORE_TICKS_IN_UNENTITLED_TIME_RANGE', 'TRUE')
145
+
128
146
  result = otq.run(graph,
129
147
  symbols=symbols,
130
148
  start=start,
131
149
  end=end,
132
150
  context=context,
151
+ query_properties=qp,
133
152
  timezone='UTC')
134
- return result
153
+ data = result.output(symbols).data
154
+ if not data:
155
+ raise RuntimeError(f"Can't get OneTick version from database '{db_name}'")
156
+ return data
135
157
 
136
158
 
137
159
  @cache
@@ -166,28 +188,33 @@ def get_onetick_version(db=None, context=None) -> OnetickVersionFromServer:
166
188
 
167
189
  # if otp.config.default_db is set, then we use it to check compatibility
168
190
  # otherwise we use LOCAL database available everywhere
169
- db = db or otp.config.get('default_db', 'LOCAL')
170
- symbols = f'{db}::'
191
+ db_name = db or otp.config.get('default_db', 'LOCAL')
171
192
  context = context or otp.config.context
172
193
 
173
194
  try:
174
- # let's try some default time range first
175
- start = end = datetime(2003, 12, 1)
176
- result = _get_onetick_version(symbols, context, start, end)
177
- except Exception:
178
- if db != 'LOCAL':
195
+ if db_name == 'LOCAL':
196
+ # for LOCAL db any date will do
197
+ start = end = datetime(2003, 12, 1)
198
+ result_data = _get_onetick_version(db_name, context, start, end)
199
+ else:
179
200
  # for real db we need to set time range correctly
180
201
  # otherwise we may get error "Database locator has a gap"
181
- start = end = _get_locator_min_date(db, context)
182
- result = _get_onetick_version(symbols, context, start, end)
183
- else:
184
- raise
202
+ locator_intervals = _get_locator_intervals(db_name, context)
203
+ for i, (start, end) in enumerate(locator_intervals):
204
+ try:
205
+ result_data = _get_onetick_version(db_name, context, start, end)
206
+ break
207
+ except Exception as e:
208
+ if i < len(locator_intervals) - 1:
209
+ continue
210
+ else:
211
+ raise e
185
212
  finally:
186
213
  if s:
187
214
  s.close()
188
215
 
189
- build_number = result[symbols]["BUILD"][0]
190
- release_string = result[symbols]["RELEASE"][0]
216
+ build_number = result_data["BUILD"][0]
217
+ release_string = result_data["RELEASE"][0]
191
218
 
192
219
  try:
193
220
  onetick_version = _parse_release_string(release_string, build_number=build_number)
@@ -615,6 +615,12 @@ def linear_regression(self: 'Source', *args, **kwargs):
615
615
  pass
616
616
 
617
617
 
618
+ @copy_method(aggregations.functions.partition_evenly_into_groups)
619
+ def partition_evenly_into_groups(self: 'Source', *args, **kwargs):
620
+ # method implementation is copied by decorator
621
+ pass
622
+
623
+
618
624
  @inplace_operation
619
625
  def process_by_group(
620
626
  self: 'Source', process_source_func, group_by=None, source_name=None, num_threads=None, inplace=False
@@ -103,7 +103,9 @@ def throw(
103
103
  >>> t = otp.Ticks(A=[1, 2, 3, 4])
104
104
  >>> t = t.throw(message='warning A=1', scope='symbol', error_code=2, where=(t['A']==1))
105
105
  >>> t = t.throw(message='error A=3', scope='symbol', error_code=1502, where=(t['A']==3))
106
- >>> otp.run(t)
106
+ >>> otp.run(t) # doctest: +SKIP
107
+ UserWarning: Symbol error: [2] warning A=1
108
+ UserWarning: Symbol error: [1502] error A=3
107
109
  Time A
108
110
  0 2003-12-01 00:00:00.000 1
109
111
  1 2003-12-01 00:00:00.001 2
@@ -246,7 +248,17 @@ def dump(
246
248
 
247
249
  # print <no data> in case there are 0 ticks
248
250
  if hasattr(otq, 'InsertAtEnd'):
249
- self_c.sink(otq.InsertAtEnd(delimiter_name='AT_END'))
251
+ ep_kwargs = {}
252
+ if {'propagate_ticks', 'insert_if_input_is_empty', 'fields_of_added_tick'}.issubset(
253
+ otq.InsertAtEnd.Parameters.list_parameters()
254
+ ):
255
+ # if supported, fix PY-1433
256
+ ep_kwargs = dict(
257
+ propagate_ticks=True,
258
+ insert_if_input_is_empty=True,
259
+ fields_of_added_tick='AT_END=1'
260
+ )
261
+ self_c.sink(otq.InsertAtEnd(delimiter_name='AT_END', **ep_kwargs))
250
262
  self_c.schema['AT_END'] = int
251
263
  self_c.state_vars['COUNT'] = otp.state.var(0, scope='branch')
252
264
  self_c.state_vars['COUNT'] += 1
onetick/py/core/source.py CHANGED
@@ -5,7 +5,7 @@ import warnings
5
5
  from collections import defaultdict
6
6
  from datetime import datetime, date
7
7
  import pandas as pd
8
- from typing import Optional, Tuple
8
+ from typing import Optional, Tuple, Union
9
9
 
10
10
  from onetick.py.otq import otq
11
11
 
@@ -979,6 +979,8 @@ class Source:
979
979
  render_debug_info: bool = False,
980
980
  debug: bool = False,
981
981
  graphviz_compat_mode: bool = False,
982
+ font_family: Optional[str] = None,
983
+ font_size: Optional[Union[int, float]] = None,
982
984
  **kwargs,
983
985
  ):
984
986
  """
@@ -1011,6 +1013,12 @@ class Source:
1011
1013
  graphviz_compat_mode: bool
1012
1014
  Change internal parameters of result graph for better compatibility with old `Graphviz` versions.
1013
1015
  Could produce larger and less readable graphs.
1016
+ font_family: str, optional
1017
+ Font name
1018
+
1019
+ Default: **Monospace**
1020
+ font_size: int, float, str, optional
1021
+ Font size
1014
1022
  kwargs:
1015
1023
  Additional arguments to be passed to :py:meth:`onetick.py.Source.to_otq` method (except
1016
1024
  ``file_name``, ``file_suffix`` and ``query_name`` parameters)
@@ -1043,7 +1051,7 @@ class Source:
1043
1051
  otq_path = self.to_otq(**kwargs)
1044
1052
  return render_otq(
1045
1053
  otq_path, image_path, output_format, load_external_otqs, view, line_limit, parse_eval_from_params,
1046
- render_debug_info, debug, graphviz_compat_mode,
1054
+ render_debug_info, debug, graphviz_compat_mode, font_family, font_size,
1047
1055
  )
1048
1056
 
1049
1057
  def copy(self, ep=None, columns=None, deep=False) -> 'Source':
@@ -1652,7 +1660,7 @@ class Source:
1652
1660
  ranking, percentile, find_value_for_percentile,
1653
1661
  exp_w_average, exp_tw_average, standardized_moment,
1654
1662
  portfolio_price, multi_portfolio_price, return_ep, implied_vol,
1655
- linear_regression,
1663
+ linear_regression, partition_evenly_into_groups,
1656
1664
  process_by_group,
1657
1665
  )
1658
1666
  from ._source.source_methods.joins import ( # type: ignore[misc]
@@ -165,7 +165,9 @@ class DB:
165
165
  # ACCESS_INFO can return ACL violation error if we use database name as symbol
166
166
  query_properties={'IGNORE_TICKS_IN_UNENTITLED_TIME_RANGE': 'TRUE'},
167
167
  username=username,
168
- context=self.context)
168
+ context=self.context,
169
+ # don't print symbol error from onetick about start/end time adjusted due to entitlement checks
170
+ print_symbol_errors=False)
169
171
  if not df.empty:
170
172
  df = df.drop(columns='Time')
171
173
  if deep_scan:
onetick/py/run.py CHANGED
@@ -60,7 +60,8 @@ def run(query: Union[Callable, Dict, otp.Source, otp.MultiOutputSource, # NOSON
60
60
  max_expected_ticks_per_symbol: Optional[int] = None,
61
61
  log_symbol: Union[bool, Type[utils.default]] = utils.default,
62
62
  encoding: Optional[str] = None,
63
- manual_dataframe_callback: bool = False):
63
+ manual_dataframe_callback: bool = False,
64
+ print_symbol_errors: bool = True):
64
65
  """
65
66
  Executes a query and returns its result.
66
67
 
@@ -214,6 +215,9 @@ def run(query: Union[Callable, Dict, otp.Source, otp.MultiOutputSource, # NOSON
214
215
  Create dataframe manually with ``callback`` mode.
215
216
  Only works if ``output_structure='df'`` is specified and parameter ``callback`` is not.
216
217
  May improve performance in some cases.
218
+ print_symbol_errors_from_onetick: bool
219
+ Applicable only when ``output_structure`` is set to *df*.
220
+ Print symbol errors from OneTick as python warnings.
217
221
 
218
222
  Returns
219
223
  -------
@@ -614,7 +618,8 @@ def run(query: Union[Callable, Dict, otp.Source, otp.MultiOutputSource, # NOSON
614
618
  _process_empty_results(result, query_schema, output_structure)
615
619
 
616
620
  return _format_call_output(result, output_structure=output_structure,
617
- require_dict=require_dict, node_names=node_names)
621
+ require_dict=require_dict, node_names=node_names,
622
+ print_symbol_errors=print_symbol_errors)
618
623
 
619
624
 
620
625
  async def run_async(*args, **kwargs):
@@ -698,7 +703,7 @@ def _filter_returned_map_by_node(result, _node_names):
698
703
 
699
704
  def _filter_returned_list_by_node(result, node_names):
700
705
  """
701
- Here, result has the following format: [(symbol, data_1, data_2, node_name)]
706
+ Here, result has the following format: [(symbol, ticks_data, error_data, node_name)]
702
707
  We need to filter by correct node_names
703
708
  """
704
709
  if not node_names:
@@ -708,12 +713,12 @@ def _filter_returned_list_by_node(result, node_names):
708
713
 
709
714
  res = []
710
715
  empty_result = True
711
- for symbol, data_1, data_2, node, *_ in result:
712
- if len(data_1) > 0:
716
+ for symbol, ticks_data, error_data, node, *_ in result:
717
+ if len(ticks_data) > 0:
713
718
  empty_result = False
714
719
  if node in node_names:
715
720
  node_found = True
716
- res.append((symbol, data_1, data_2, node))
721
+ res.append((symbol, ticks_data, error_data, node))
717
722
 
718
723
  if not empty_result and not node_found:
719
724
  # TODO: Do we even want to raise it?
@@ -721,19 +726,19 @@ def _filter_returned_list_by_node(result, node_names):
721
726
  return res
722
727
 
723
728
 
724
- def _form_dict_from_list(data_list, output_structure):
729
+ def _form_dict_from_list(data_list, output_structure, print_symbol_errors):
725
730
  """
726
- Here, data_list has the following format: [(symbol, data_1, data_2, node_name), ...]
731
+ Here, data_list has the following format: [(symbol, ticks_data, error_data, node_name), ...]
727
732
  We need to create the following result:
728
- either {symbol: DataFrame(data_1)} if there is only one result per symbol
729
- or {symbol: [DataFrame(data_1)]} if there are multiple results for symbol for a single node_name
730
- or {symbol: {node_name: DataFrame(data_1)}} if there are single results for multiple node names for a symbol
731
- or {symbol: {node_name: [DataFrame(data_1)]}} if there are multiple results for multiple node names for a symbol
733
+ either {symbol: DataFrame(ticks_data)} if there is only one result per symbol
734
+ or {symbol: [DataFrame(ticks_data)]} if there are multiple results for symbol for a single node_name
735
+ or {symbol: {node_name: DataFrame(ticks_data)}} if there are single results for multiple node names for a symbol
736
+ or {symbol: {node_name: [DataFrame(ticks_data)]}} if there are multiple results for multiple node names for a symbol
732
737
  """
733
738
 
734
739
  def form_node_name_dict(lst):
735
740
  """
736
- lst is a lit of (node, dataframe)
741
+ lst is a list of (node, dataframe)
737
742
  """
738
743
  d = defaultdict(list)
739
744
  for node, df in lst:
@@ -760,7 +765,12 @@ def _form_dict_from_list(data_list, output_structure):
760
765
  return polars.DataFrame()
761
766
 
762
767
  symbols_dict = defaultdict(list)
763
- for symbol, data, _, node, *_ in data_list:
768
+ for symbol, data, error_data, node, *_ in data_list:
769
+
770
+ if print_symbol_errors:
771
+ for err_code, err_msg, *_ in error_data:
772
+ warnings.warn(f"Symbol error: [{err_code}] {err_msg}")
773
+
764
774
  df = get_dataframe(data)
765
775
 
766
776
  list_item = (node, df)
@@ -772,7 +782,7 @@ def _form_dict_from_list(data_list, output_structure):
772
782
  return dict(symbols_dict)
773
783
 
774
784
 
775
- def _format_call_output(result, output_structure, node_names, require_dict):
785
+ def _format_call_output(result, output_structure, node_names, require_dict, print_symbol_errors):
776
786
  """Formats output of otq.run() according to passed parameters.
777
787
  See parameters' description for more information
778
788
 
@@ -795,6 +805,8 @@ def _format_call_output(result, output_structure, node_names, require_dict):
795
805
  require_dict: bool
796
806
  If True, forces output for output_structure='df' to always be a dictionary, even if only one symbol is returned
797
807
  Has no effect for other values of output_structure
808
+ print_symbol_errors: bool
809
+ Print OneTick symbol errors in when ``output_structure`` is set to 'df' or not.
798
810
 
799
811
  Returns
800
812
  ----------
@@ -811,7 +823,7 @@ def _format_call_output(result, output_structure, node_names, require_dict):
811
823
 
812
824
  # "df" output structure implies that raw results came as a list
813
825
  result_list = _filter_returned_list_by_node(result, node_names)
814
- result_dict = _form_dict_from_list(result_list, output_structure)
826
+ result_dict = _form_dict_from_list(result_list, output_structure, print_symbol_errors)
815
827
 
816
828
  if len(result_dict) == 1 and not require_dict:
817
829
  return list(result_dict.values())[0]
@@ -1212,6 +1212,8 @@ def render_otq(
1212
1212
  render_debug_info: bool = False,
1213
1213
  debug: bool = False,
1214
1214
  graphviz_compat_mode: bool = False,
1215
+ font_family: Optional[str] = None,
1216
+ font_size: Optional[Union[int, float]] = None,
1215
1217
  ) -> str:
1216
1218
  """
1217
1219
  Render queries from .otq files.
@@ -1245,6 +1247,12 @@ def render_otq(
1245
1247
  graphviz_compat_mode: bool
1246
1248
  Change internal parameters of result graph for better compatibility with old `Graphviz` versions.
1247
1249
  Could produce larger and less readable graphs.
1250
+ font_family: str, optional
1251
+ Font name
1252
+
1253
+ Default: **Monospace**
1254
+ font_size: int, float, optional
1255
+ Font size
1248
1256
 
1249
1257
  Returns
1250
1258
  -------
@@ -1268,6 +1276,10 @@ def render_otq(
1268
1276
  Render specific queries from multiple files:
1269
1277
 
1270
1278
  >>> otp.utils.render_otq(["./first.otq", "./second.otq::some_query"]) # doctest: +SKIP
1279
+
1280
+ Change font type to **Times New Roman** and text size to **10**:
1281
+
1282
+ >>> otp.utils.render_otq("./test.otq", font_family="Times-Roman", font_size=10) # doctest: +SKIP
1271
1283
  """
1272
1284
  if line_limit is None:
1273
1285
  line_limit = (0, 0)
@@ -1355,9 +1367,31 @@ def render_otq(
1355
1367
  if not image_path:
1356
1368
  image_path = TmpFile().path
1357
1369
 
1370
+ graph_kwargs = {}
1371
+ node_kwargs = {}
1372
+ edge_kwargs = {}
1373
+
1374
+ if font_size is not None:
1375
+ if font_size <= 0:
1376
+ raise ValueError("Parameter `font_size` should be greater than zero")
1377
+
1378
+ font_size_str = str(font_size)
1379
+
1380
+ graph_kwargs["fontsize"] = font_size_str
1381
+ node_kwargs["fontsize"] = font_size_str
1382
+ edge_kwargs["fontsize"] = font_size_str
1383
+
1384
+ if font_family is None:
1385
+ font_family = "Monospace"
1386
+
1387
+ graph_kwargs["fontname"] = font_family
1388
+ node_kwargs["fontname"] = font_family
1389
+ edge_kwargs["fontname"] = font_family
1390
+
1358
1391
  gr = gv.Digraph(format=output_format, filename=image_path, engine="dot")
1359
- gr.attr("graph", compound="true")
1360
- gr.attr("node", shape="plaintext", margin="0")
1392
+ gr.attr("graph", compound="true", **graph_kwargs)
1393
+ gr.attr("node", shape="plaintext", margin="0", **node_kwargs)
1394
+ gr.attr("edge", **edge_kwargs)
1361
1395
 
1362
1396
  idx = 0
1363
1397
  for otq_path, graph in graphs.items():
@@ -44,6 +44,14 @@ def parse_args():
44
44
  help='Change internal parameters of result graph for better compatibility with old `Graphviz` versions',
45
45
  action=argparse.BooleanOptionalAction,
46
46
  )
47
+ parser.add_argument(
48
+ '--font-family', help='Font family',
49
+ type=str,
50
+ )
51
+ parser.add_argument(
52
+ '--font-size', help='Font size',
53
+ type=int,
54
+ )
47
55
 
48
56
  args = parser.parse_args()
49
57
  return args
@@ -70,7 +78,7 @@ def render_otq(
70
78
 
71
79
  for param_name in [
72
80
  'output_format', 'load_external_otqs', 'view', 'parse_eval_from_params', 'render_debug_info',
73
- 'debug', 'graphviz_compat_mode',
81
+ 'debug', 'graphviz_compat_mode', 'font_family', 'font_size',
74
82
  ]:
75
83
  if kwargs.get(param_name) is not None:
76
84
  call_kwargs[param_name] = kwargs[param_name]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: onetick-py
3
- Version: 1.182.0
3
+ Version: 1.183.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
@@ -3,7 +3,7 @@ locator_parser/acl.py,sha256=DO39SykYtVbC0c4WieoFL-q08lIfi1rbM4woHwhSRfE,1347
3
3
  locator_parser/actions.py,sha256=-t4apkXg3Bt2xss5m60Y3pO21TggSCcrZiyyUHm1xDw,7716
4
4
  locator_parser/common.py,sha256=afuNGELploha13vYKCQ4SIAmfRaS89ESQLHsr6XM66w,10105
5
5
  locator_parser/io.py,sha256=47ADbz9jRr8BqPvHtQrRfFc_Gt6NnI4TOp-1M_q811k,986
6
- locator_parser/locator.py,sha256=1CcjAQIi1A6gRL556RySeB4f82bkfCFRbIsl3eiKf1w,3173
6
+ locator_parser/locator.py,sha256=evlVmaINPRwiR51V_mgAYnqqa_6pPtPzP0gO4R0y9n8,3420
7
7
  onetick/__init__.py,sha256=yeeF6TaLX9B8fwqr8RyOxZQYMIrTOI4oBuGh_rkwf2Q,3543
8
8
  onetick/doc_utilities/__init__.py,sha256=4DptlEjzG-Jt16c-gX8pgQnDlSVd7VVPw8vSO4emXsk,132
9
9
  onetick/doc_utilities/napoleon.py,sha256=CE2r7xJ2Oz_MHDbMQx5hOE6JxkRPXQE55QE4bq3dYvg,1540
@@ -13,10 +13,10 @@ 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=JVXbKakzScRnwpJVEkerJkX-UAX7Surdp4PCImvvieA,11183
15
15
  onetick/py/_stack_info.py,sha256=PHZOkW_fK7Fbl4YEj5CaYK9L6vh4j-bUU7_cSYOWZ30,2546
16
- onetick/py/_version.py,sha256=6duBuEKq7UU6QULm1owR251o9fbS3vwJM7vn-lYoaoY,76
16
+ onetick/py/_version.py,sha256=95ejWplFoPpneDvc5NydLYDkTsFPqDB7B3Ea7oUDkyY,76
17
17
  onetick/py/backports.py,sha256=mR00mxe7E7UgBljf-Wa93Mo6lpi-C4Op561uhPUoEt8,815
18
18
  onetick/py/cache.py,sha256=BBZg8n0AGjZzZapg4752LkSZdX5C6DGf7vU9sAStv6A,12798
19
- onetick/py/compatibility.py,sha256=rBLoszCoSZrl9SnBYiPzWuoudZwhOP6d4exshy3BJTw,34530
19
+ onetick/py/compatibility.py,sha256=lZG_jzoClqrT5GR0VO10wuNmAlQI2EhygH2PtwcFAfI,35751
20
20
  onetick/py/configuration.py,sha256=wOmGBaaT89uz6Eny3Pp_tFpATto-qZXcqNRyy4rzmQw,28655
21
21
  onetick/py/functions.py,sha256=Kqromia64o1K4fgbLbijw64o5pFGBZRog52824wOxlA,98004
22
22
  onetick/py/license.py,sha256=50dsFrE-NKsPOdkAoyxHr44bH8DzFCr_6TabX0JH6tQ,6140
@@ -25,22 +25,22 @@ onetick/py/math.py,sha256=MZvlyUjxOWGJvmxK8pfMkCr4THM52AE6NyGEidgDMyE,26311
25
25
  onetick/py/misc.py,sha256=mD-EJslLGjQ0ROEez76HvOY0tsX8_1bC1QOkyHY9FZ8,13934
26
26
  onetick/py/otq.py,sha256=TAOFw8e_p0mfTEV92qb_AzHplbv_H71hMwnT3qNmksg,8780
27
27
  onetick/py/pyomd_mock.py,sha256=A7IIUraN9yCsPhvnHXQUZrmwFjlh7vofSnYm4G7bdsc,2006
28
- onetick/py/run.py,sha256=Ql-hN0R9GZvxf0ukRUgVpqxk69Dez6T0CA1Cn2Fq1dY,40805
28
+ onetick/py/run.py,sha256=rWxM3mSglhboJ01CUHzjtoX9iIrCzigLgBoPvYnGlaA,41490
29
29
  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
33
  onetick/py/types.py,sha256=9qrqmk8NDqZFlGM_R1TAkoOAgaJ-n3wdYi74GH9cCNs,69360
34
- onetick/py/aggregations/__init__.py,sha256=02WnTc9ocTp34OkGyXRHY2QAZi_QjDMHClE5tvx6Gtk,736
34
+ onetick/py/aggregations/__init__.py,sha256=jDBCqnjkScH1nL7TU2cKk0RVKs8OVAf0fkktwHLPG9o,766
35
35
  onetick/py/aggregations/_base.py,sha256=HbiicjQI6nYmfGZxI1Hx9QdDJoQ4kcCmOI8E-3VZrjc,25715
36
- onetick/py/aggregations/_docs.py,sha256=XQH1F8hakS7FHRDLhOtv8ocDsqfG0YpeXBZz0bfGtIA,35179
36
+ onetick/py/aggregations/_docs.py,sha256=F6RlVdWx0aX6FY5auPzcQjJIhUnQmfnn5WtC29svTg4,36505
37
37
  onetick/py/aggregations/compute.py,sha256=GJpR8AWlEmfNsnl-L2oJPuY5rFb6PC-VwLZnnRGtCys,11854
38
- onetick/py/aggregations/functions.py,sha256=MnjINLAxoLehgInxhY-xSI1xw8TtCGzT7APD5HdFmjE,80801
38
+ onetick/py/aggregations/functions.py,sha256=ilxILvIwR10skZqU8hlo2EDRFujR4EivavBjB87Vj0o,82525
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
42
  onetick/py/aggregations/order_book.py,sha256=6bueLtvblvc84ZJ8guHfNPzGrr_1kUGgQe49l_feyP8,20286
43
- onetick/py/aggregations/other.py,sha256=0FZw8AIqo73duVsH7Jw7Sx7TcMABoUZAq866P-eII04,39436
43
+ onetick/py/aggregations/other.py,sha256=p-baVFQYVqb7_xC2Sz-jgSUAXTSCSKOUOx0GzUEVmXQ,41480
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=duFut5tYCzsTHPXwLGzQGwpRF_h4d83cqfWdWYIJlR0,65837
57
+ onetick/py/core/source.py,sha256=p_vJ6KuXWCdpLZuln7BXH5kqq9HoqRXNLeb4bstZcJE,66156
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
@@ -71,11 +71,11 @@ onetick/py/core/_source/schema.py,sha256=WWaa2HtSNNp1G4Z4Hx85fnUJpTvpBnX7xmDq3Jc
71
71
  onetick/py/core/_source/symbol.py,sha256=H274Xd-f_0IqF6jMKmxIvQj1lGD-3F54E27y8A-lSYA,8269
72
72
  onetick/py/core/_source/tmp_otq.py,sha256=6HRbUZK_INF7BB29Ej2q_tulkR2SXlOua1--0pnsW4I,10779
73
73
  onetick/py/core/_source/source_methods/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
74
- onetick/py/core/_source/source_methods/aggregations.py,sha256=EBdHV8IFwdptPDxQncHjtXkNr6lOCvp06OmjBzFs4AU,27526
74
+ onetick/py/core/_source/source_methods/aggregations.py,sha256=d436lFiAdKwqrXzRNvs4VAQPAd2FfW8Zypo4oNAjO_s,27721
75
75
  onetick/py/core/_source/source_methods/applyers.py,sha256=UlVJznASMfuEzUSWEQj7tcR-fhztU0iFOTe0JHf4u4c,10104
76
76
  onetick/py/core/_source/source_methods/columns.py,sha256=JJNr0Wqf8B9igLVoCNtFeNE5sHhBUPWyy-i38YkgqFY,4691
77
77
  onetick/py/core/_source/source_methods/data_quality.py,sha256=iz8e-60se-rNH72k5fRxX3cm3ahfbBZ7R7E46Be5zhs,9343
78
- onetick/py/core/_source/source_methods/debugs.py,sha256=Hq08HYjF2sEZQAcBlwNMVKpu0muCWNIYEhmPqbKkSZw,10176
78
+ onetick/py/core/_source/source_methods/debugs.py,sha256=bnJa9t35hYiQGIsG39NM3CywDEQjv9bN_hN34_OmThM,10703
79
79
  onetick/py/core/_source/source_methods/drops.py,sha256=3ZXfQWXsl0-ZLHdp6D-wTu2saqCYHZ-Gn777aMRSRp0,4403
80
80
  onetick/py/core/_source/source_methods/fields.py,sha256=10_4HlRJOEMmuHsEOzM71MZ8wx8ppndQQNieLlNKf0E,23398
81
81
  onetick/py/core/_source/source_methods/filters.py,sha256=l2nazixNNH_0zjmE4Il90ZX0scPn7pmIbjOKnjZkEfo,36400
@@ -103,7 +103,7 @@ onetick/py/core/column_operations/accessors/dt_accessor.py,sha256=UeY7UNUiaLdrX0
103
103
  onetick/py/core/column_operations/accessors/float_accessor.py,sha256=KEbmyt8l91DJBkPVmPUCJXwRkUcP0lRzOakW2HCQIVo,7378
104
104
  onetick/py/core/column_operations/accessors/str_accessor.py,sha256=kpCQQQJd02yWsTVwkAMYBfJMTePSffzDiT-6utRqNB4,55248
105
105
  onetick/py/db/__init__.py,sha256=-TKRO0suXTR-mLAppYt-Vegu9HQn7LU_JcObXpS3N9M,61
106
- onetick/py/db/_inspection.py,sha256=4oPUGAxwair-lELHSdTyPOwVnG9aZi3xsKdrR5qT2ls,50556
106
+ onetick/py/db/_inspection.py,sha256=ClKt4vZEY8BAXcruZC_oXvo4zxzR_MWeN13nOIZlbQY,50721
107
107
  onetick/py/db/db.py,sha256=3K1X9RQ2ObXGxyxSi-jSSl6b7p0OuGZJJkx9SGzkBsc,55930
108
108
  onetick/py/db/utils.py,sha256=gt-KmbGgcp1tH81LH-cLO08n2Hx8GaBPbvfEN22mgLg,2309
109
109
  onetick/py/docs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -141,15 +141,15 @@ onetick/py/utils/helpers.py,sha256=XY2PaMUSzFNIpiuiAY_gPQ0TnYTmfoPAGNAFh_6aB14,2
141
141
  onetick/py/utils/locator.py,sha256=YUOXU0yh0ZZZOJpJniAq9WNn0_u3X_M5q2tEwt1cp_4,2887
142
142
  onetick/py/utils/perf.py,sha256=g0r-9YUc2mx9Lj4PjG6Fk1keO3m9nC3t2cpHx3M-AeQ,19299
143
143
  onetick/py/utils/query.py,sha256=b60JmfJDx3mpP74rTQC3qnlCb6t4fYFEauVaH9ulwL8,1330
144
- onetick/py/utils/render.py,sha256=WCWkbcPfUBr9AFT3Rg4A--Xd7fZjIZMQbTWX71d_znE,45124
145
- onetick/py/utils/render_cli.py,sha256=Np4omSvGwVhqnKSPII5Raz_t1chFF980oTDdyEJE0aQ,3006
144
+ onetick/py/utils/render.py,sha256=dGhV-EnBN63VjKDBGc1Da4cO81t7GksFEaj8Glwra2U,46170
145
+ onetick/py/utils/render_cli.py,sha256=qc5RicJ2UgMTxx21ZTeRt5iXd0ddG4fa8pF5To_09sc,3218
146
146
  onetick/py/utils/script.py,sha256=Y8NujEo2_5QaP6KDnLKJiKQ7SmMjw8_dv8sYL9rHDCE,11184
147
147
  onetick/py/utils/temp.py,sha256=j-BC155dE46k0zfKTTs26KTF0CK6WA1hO2GD54iunyM,17380
148
148
  onetick/py/utils/types.py,sha256=7u9s9uN1jlkgud8_TSLy6iFzQFBtKJ0v3yamgOVitrs,3747
149
149
  onetick/py/utils/tz.py,sha256=sYUKigaORonp7Xa6x806xVYJ69lYJHd6NrLxQHB5AZo,2878
150
- onetick_py-1.182.0.dist-info/licenses/LICENSE,sha256=Yhu7lKNFS0fsaN-jSattEMRtCOPueP58Eu5BPH8ZGjM,1075
151
- onetick_py-1.182.0.dist-info/METADATA,sha256=WDHkCg3Xu1RmZFMZygSdREaz8_ZGqRAhbg_OuV36cOI,7290
152
- onetick_py-1.182.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
153
- onetick_py-1.182.0.dist-info/entry_points.txt,sha256=bafJo_C7lPHz5pAdlIryLzxAlJ8F-We5cC7psdwcx4Q,131
154
- onetick_py-1.182.0.dist-info/top_level.txt,sha256=Na1jSJmVMyYGOndaswt554QKIUwQjcYh6th2ATsmw0U,23
155
- onetick_py-1.182.0.dist-info/RECORD,,
150
+ onetick_py-1.183.0.dist-info/licenses/LICENSE,sha256=Yhu7lKNFS0fsaN-jSattEMRtCOPueP58Eu5BPH8ZGjM,1075
151
+ onetick_py-1.183.0.dist-info/METADATA,sha256=ehgWunoVU5qIPZAslrSGvjVS5Fl6T8etySEaVBwksbo,7290
152
+ onetick_py-1.183.0.dist-info/WHEEL,sha256=wUyA8OaulRlbfwMtmQsvNngGrxQHAvkKcvRmdizlJi0,92
153
+ onetick_py-1.183.0.dist-info/entry_points.txt,sha256=bafJo_C7lPHz5pAdlIryLzxAlJ8F-We5cC7psdwcx4Q,131
154
+ onetick_py-1.183.0.dist-info/top_level.txt,sha256=Na1jSJmVMyYGOndaswt554QKIUwQjcYh6th2ATsmw0U,23
155
+ onetick_py-1.183.0.dist-info/RECORD,,
@@ -1,5 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: setuptools (80.9.0)
2
+ Generator: setuptools (80.10.2)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
5