onetick-py 1.177.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.
Files changed (152) hide show
  1. locator_parser/__init__.py +0 -0
  2. locator_parser/acl.py +73 -0
  3. locator_parser/actions.py +262 -0
  4. locator_parser/common.py +368 -0
  5. locator_parser/io.py +43 -0
  6. locator_parser/locator.py +150 -0
  7. onetick/__init__.py +101 -0
  8. onetick/doc_utilities/__init__.py +3 -0
  9. onetick/doc_utilities/napoleon.py +40 -0
  10. onetick/doc_utilities/ot_doctest.py +140 -0
  11. onetick/doc_utilities/snippets.py +279 -0
  12. onetick/lib/__init__.py +4 -0
  13. onetick/lib/instance.py +141 -0
  14. onetick/py/__init__.py +293 -0
  15. onetick/py/_stack_info.py +89 -0
  16. onetick/py/_version.py +2 -0
  17. onetick/py/aggregations/__init__.py +11 -0
  18. onetick/py/aggregations/_base.py +648 -0
  19. onetick/py/aggregations/_docs.py +948 -0
  20. onetick/py/aggregations/compute.py +286 -0
  21. onetick/py/aggregations/functions.py +2216 -0
  22. onetick/py/aggregations/generic.py +104 -0
  23. onetick/py/aggregations/high_low.py +80 -0
  24. onetick/py/aggregations/num_distinct.py +83 -0
  25. onetick/py/aggregations/order_book.py +501 -0
  26. onetick/py/aggregations/other.py +1014 -0
  27. onetick/py/backports.py +26 -0
  28. onetick/py/cache.py +374 -0
  29. onetick/py/callback/__init__.py +5 -0
  30. onetick/py/callback/callback.py +276 -0
  31. onetick/py/callback/callbacks.py +131 -0
  32. onetick/py/compatibility.py +798 -0
  33. onetick/py/configuration.py +771 -0
  34. onetick/py/core/__init__.py +0 -0
  35. onetick/py/core/_csv_inspector.py +93 -0
  36. onetick/py/core/_internal/__init__.py +0 -0
  37. onetick/py/core/_internal/_manually_bound_value.py +6 -0
  38. onetick/py/core/_internal/_nodes_history.py +250 -0
  39. onetick/py/core/_internal/_op_utils/__init__.py +0 -0
  40. onetick/py/core/_internal/_op_utils/every_operand.py +9 -0
  41. onetick/py/core/_internal/_op_utils/is_const.py +10 -0
  42. onetick/py/core/_internal/_per_tick_scripts/tick_list_sort_template.script +121 -0
  43. onetick/py/core/_internal/_proxy_node.py +140 -0
  44. onetick/py/core/_internal/_state_objects.py +2312 -0
  45. onetick/py/core/_internal/_state_vars.py +93 -0
  46. onetick/py/core/_source/__init__.py +0 -0
  47. onetick/py/core/_source/_symbol_param.py +95 -0
  48. onetick/py/core/_source/schema.py +97 -0
  49. onetick/py/core/_source/source_methods/__init__.py +0 -0
  50. onetick/py/core/_source/source_methods/aggregations.py +809 -0
  51. onetick/py/core/_source/source_methods/applyers.py +296 -0
  52. onetick/py/core/_source/source_methods/columns.py +141 -0
  53. onetick/py/core/_source/source_methods/data_quality.py +301 -0
  54. onetick/py/core/_source/source_methods/debugs.py +272 -0
  55. onetick/py/core/_source/source_methods/drops.py +120 -0
  56. onetick/py/core/_source/source_methods/fields.py +619 -0
  57. onetick/py/core/_source/source_methods/filters.py +1002 -0
  58. onetick/py/core/_source/source_methods/joins.py +1413 -0
  59. onetick/py/core/_source/source_methods/merges.py +605 -0
  60. onetick/py/core/_source/source_methods/misc.py +1455 -0
  61. onetick/py/core/_source/source_methods/pandases.py +155 -0
  62. onetick/py/core/_source/source_methods/renames.py +356 -0
  63. onetick/py/core/_source/source_methods/sorts.py +183 -0
  64. onetick/py/core/_source/source_methods/switches.py +142 -0
  65. onetick/py/core/_source/source_methods/symbols.py +117 -0
  66. onetick/py/core/_source/source_methods/times.py +627 -0
  67. onetick/py/core/_source/source_methods/writes.py +986 -0
  68. onetick/py/core/_source/symbol.py +205 -0
  69. onetick/py/core/_source/tmp_otq.py +222 -0
  70. onetick/py/core/column.py +209 -0
  71. onetick/py/core/column_operations/__init__.py +0 -0
  72. onetick/py/core/column_operations/_methods/__init__.py +4 -0
  73. onetick/py/core/column_operations/_methods/_internal.py +28 -0
  74. onetick/py/core/column_operations/_methods/conversions.py +216 -0
  75. onetick/py/core/column_operations/_methods/methods.py +292 -0
  76. onetick/py/core/column_operations/_methods/op_types.py +160 -0
  77. onetick/py/core/column_operations/accessors/__init__.py +0 -0
  78. onetick/py/core/column_operations/accessors/_accessor.py +28 -0
  79. onetick/py/core/column_operations/accessors/decimal_accessor.py +104 -0
  80. onetick/py/core/column_operations/accessors/dt_accessor.py +537 -0
  81. onetick/py/core/column_operations/accessors/float_accessor.py +184 -0
  82. onetick/py/core/column_operations/accessors/str_accessor.py +1367 -0
  83. onetick/py/core/column_operations/base.py +1121 -0
  84. onetick/py/core/cut_builder.py +150 -0
  85. onetick/py/core/db_constants.py +20 -0
  86. onetick/py/core/eval_query.py +245 -0
  87. onetick/py/core/lambda_object.py +441 -0
  88. onetick/py/core/multi_output_source.py +232 -0
  89. onetick/py/core/per_tick_script.py +2256 -0
  90. onetick/py/core/query_inspector.py +464 -0
  91. onetick/py/core/source.py +1744 -0
  92. onetick/py/db/__init__.py +2 -0
  93. onetick/py/db/_inspection.py +1128 -0
  94. onetick/py/db/db.py +1327 -0
  95. onetick/py/db/utils.py +64 -0
  96. onetick/py/docs/__init__.py +0 -0
  97. onetick/py/docs/docstring_parser.py +112 -0
  98. onetick/py/docs/utils.py +81 -0
  99. onetick/py/functions.py +2398 -0
  100. onetick/py/license.py +190 -0
  101. onetick/py/log.py +88 -0
  102. onetick/py/math.py +935 -0
  103. onetick/py/misc.py +470 -0
  104. onetick/py/oqd/__init__.py +22 -0
  105. onetick/py/oqd/eps.py +1195 -0
  106. onetick/py/oqd/sources.py +325 -0
  107. onetick/py/otq.py +216 -0
  108. onetick/py/pyomd_mock.py +47 -0
  109. onetick/py/run.py +916 -0
  110. onetick/py/servers.py +173 -0
  111. onetick/py/session.py +1347 -0
  112. onetick/py/sources/__init__.py +19 -0
  113. onetick/py/sources/cache.py +167 -0
  114. onetick/py/sources/common.py +128 -0
  115. onetick/py/sources/csv.py +642 -0
  116. onetick/py/sources/custom.py +85 -0
  117. onetick/py/sources/data_file.py +305 -0
  118. onetick/py/sources/data_source.py +1045 -0
  119. onetick/py/sources/empty.py +94 -0
  120. onetick/py/sources/odbc.py +337 -0
  121. onetick/py/sources/order_book.py +271 -0
  122. onetick/py/sources/parquet.py +168 -0
  123. onetick/py/sources/pit.py +191 -0
  124. onetick/py/sources/query.py +495 -0
  125. onetick/py/sources/snapshots.py +419 -0
  126. onetick/py/sources/split_query_output_by_symbol.py +198 -0
  127. onetick/py/sources/symbology_mapping.py +123 -0
  128. onetick/py/sources/symbols.py +374 -0
  129. onetick/py/sources/ticks.py +825 -0
  130. onetick/py/sql.py +70 -0
  131. onetick/py/state.py +251 -0
  132. onetick/py/types.py +2131 -0
  133. onetick/py/utils/__init__.py +70 -0
  134. onetick/py/utils/acl.py +93 -0
  135. onetick/py/utils/config.py +186 -0
  136. onetick/py/utils/default.py +49 -0
  137. onetick/py/utils/file.py +38 -0
  138. onetick/py/utils/helpers.py +76 -0
  139. onetick/py/utils/locator.py +94 -0
  140. onetick/py/utils/perf.py +498 -0
  141. onetick/py/utils/query.py +49 -0
  142. onetick/py/utils/render.py +1374 -0
  143. onetick/py/utils/script.py +244 -0
  144. onetick/py/utils/temp.py +471 -0
  145. onetick/py/utils/types.py +120 -0
  146. onetick/py/utils/tz.py +84 -0
  147. onetick_py-1.177.0.dist-info/METADATA +137 -0
  148. onetick_py-1.177.0.dist-info/RECORD +152 -0
  149. onetick_py-1.177.0.dist-info/WHEEL +5 -0
  150. onetick_py-1.177.0.dist-info/entry_points.txt +2 -0
  151. onetick_py-1.177.0.dist-info/licenses/LICENSE +21 -0
  152. onetick_py-1.177.0.dist-info/top_level.txt +2 -0
onetick/py/session.py ADDED
@@ -0,0 +1,1347 @@
1
+ import os
2
+ import re
3
+ import warnings
4
+ import shutil
5
+ from datetime import datetime
6
+ from typing import List
7
+
8
+ from locator_parser.io import FileReader, FileWriter, PrintWriter
9
+ from locator_parser.actions import Add, GetAll, Delete
10
+ from locator_parser.common import apply_actions
11
+ from locator_parser import locator as _locator
12
+ from locator_parser import acl as _acl
13
+ from abc import ABC, abstractmethod
14
+ from onetick.py.otq import otli
15
+ from . import utils
16
+ from . import license as _license
17
+ from . import db as _db
18
+ from . import servers as _servers
19
+ from . import configuration
20
+ from .db._inspection import databases as _databases
21
+
22
+
23
+ class EntityOperationFailed(Exception):
24
+ """
25
+ Raise when operation with an entity for a config
26
+ in this module failed
27
+ """
28
+
29
+
30
+ class MultipleSessionsException(Exception):
31
+ """
32
+ Raises when user tries to initiate a new one Session
33
+ when another one is already used
34
+ """
35
+
36
+
37
+ def _check_entities(entities):
38
+ if not entities:
39
+ raise ValueError("At least one argument in parameter 'entities' is expected.")
40
+
41
+
42
+ def _apply_to_entities(cfg, operations):
43
+ """
44
+ Function generalizes operations for locators and ACLs:
45
+ tries to apply ``operations``, and does rollback in case
46
+ OneTick config is invalid.
47
+ """
48
+ if not operations:
49
+ return
50
+
51
+ for entities, func, roll_back_func in operations:
52
+ _check_entities(entities)
53
+ result = func(entities)
54
+ if not result:
55
+ entity_name = entities[0].__class__.__name__
56
+ entities = list(map(str, entities))
57
+ raise EntityOperationFailed(
58
+ f'Operation {func.__name__} for {entity_name}s {entities}'
59
+ f' for {cfg.__class__.__name__} "{cfg.path}" has failed'
60
+ )
61
+ try:
62
+ cfg.reload()
63
+ except Exception:
64
+ for entities, func, roll_back_func in reversed(operations):
65
+ roll_back_func(entities)
66
+ raise
67
+
68
+
69
+ class _FileHandler(ABC):
70
+ def __init__(self, file_h=None, clean_up=utils.default, copied=True, session_ref=None):
71
+ self._file = file_h
72
+ self._clean_up = clean_up
73
+ # flag to understand whether we work with externally passed files;
74
+ # it is set and affects logic, when copy=False
75
+ self._copied = copied
76
+ self._session_ref = session_ref
77
+
78
+ @property
79
+ def path(self):
80
+ return self._file.path
81
+
82
+ @property
83
+ def file(self):
84
+ return self._file
85
+
86
+ def copy(self, clean_up=utils.default, copy=True, session_ref=None):
87
+ return self.__class__(self.path, clean_up=clean_up, copy=copy, session_ref=session_ref) # pylint: disable=E1123
88
+
89
+ @abstractmethod
90
+ def cleanup(self):
91
+ pass
92
+
93
+ @staticmethod
94
+ def _db_in_dbs_case_insensitive(db_id: str, databases: List[str]):
95
+ for db_name in databases:
96
+ if db_id.upper() == db_name.upper():
97
+ return True
98
+ return False
99
+
100
+
101
+ class _CommonBuilder(ABC):
102
+ def __init__(self, src=None, clean_up=utils.default, copy=True, session_ref=None):
103
+ self.src = src
104
+ self.clean_up = clean_up
105
+ self.copy = copy
106
+ self.session_ref = session_ref
107
+
108
+ @abstractmethod
109
+ def build(self):
110
+ pass
111
+
112
+
113
+ class ACL(_FileHandler):
114
+ class User(str):
115
+ """
116
+ Subclass represents an ACL user
117
+ """
118
+
119
+ pass
120
+
121
+ def __init__(self, path=None, clean_up=utils.default, copy=True, session_ref=None):
122
+ """
123
+ Class representing OneTick database access list file.
124
+ ACL is the file that describes the list of the users
125
+ that are allowed to access the database and what permissions do they have.
126
+
127
+ Parameters
128
+ ----------
129
+ path: str
130
+ A path to custom acl file. Default is `None`, that means to generate a temporary acl file.
131
+ clean_up: bool
132
+ If `True`, then temporary acl file will be removed when ACL object will be destroyed. It is
133
+ helpful for debug purpose.
134
+
135
+ By default,
136
+ :py:attr:`otp.config.clean_up_tmp_files<onetick.py.configuration.Config.clean_up_tmp_files>` is used.
137
+ copy: bool
138
+ If `True`, then the passed custom acl file by the ``path`` parameter will be copied first before
139
+ usage. It might be used when you want to work with a custom acl file, but don't want to change
140
+ the original file; in that case a custom acl file will be copied into a temporary file and
141
+ every request for modification will be executed for that temporary file. Default is `True`.
142
+ """
143
+
144
+ copied = True
145
+
146
+ # TODO: implement this logic later
147
+ # if copy is None and path is not None:
148
+ # # if copy rule is not specified, but path is specified
149
+ # # then we set copy to True with safety goal, otherwise
150
+ # # we would might change a permanent file without user
151
+ # # acknowledgment
152
+ # copy = True
153
+ # raise Warning("You set the ACL file, but have not specify a copy rule. "
154
+ # "We copy it with safety goal, and it means you will work "
155
+ # "with copied file instead of passed. If you want to work "
156
+ # "with passed file directly, then you could set the 'copy' "
157
+ # "parameter to True.")
158
+
159
+ # if path is set, then copy file, we should not work directly
160
+ # with externally passed files
161
+ if copy:
162
+ if path:
163
+ file_h = utils.TmpFile(suffix=".acl", clean_up=clean_up)
164
+ shutil.copyfile(path, file_h.path)
165
+ else:
166
+ file_h = utils.tmp_acl(clean_up=clean_up)
167
+ else:
168
+ if path:
169
+ file_h = utils.PermanentFile(path)
170
+ copied = False
171
+ else:
172
+ file_h = utils.tmp_acl(clean_up=clean_up)
173
+
174
+ assert file_h is not None
175
+
176
+ super().__init__(file_h, clean_up=clean_up, copied=copied, session_ref=session_ref)
177
+
178
+ self._added_dbs = []
179
+
180
+ def cleanup(self):
181
+ self._remove_db(self._added_dbs)
182
+ self._added_dbs = []
183
+ self.reload()
184
+
185
+ def _apply_actions(self, actions, print_writer=False):
186
+ writer = PrintWriter() if print_writer else FileWriter(self.path)
187
+ flush = False if print_writer else True
188
+ return apply_actions(_acl.parse_acl, FileReader(self.path), writer, actions, flush=flush)
189
+
190
+ def _add_db(self, dbs):
191
+ actions = []
192
+ for db in dbs:
193
+ actions.append(Add(_acl.DB(id=db.id, read_access="true")))
194
+
195
+ permissions = {}
196
+ if db._write:
197
+ permissions["write_access"] = "true"
198
+ if hasattr(db, "_destroy_access") and db._destroy_access:
199
+ permissions["destroy_access"] = "true"
200
+ if db._minimum_start_date:
201
+ permissions['minimum_start_date'] = db._minimum_start_date.strftime('%Y%m%d')
202
+ if db._maximum_end_date:
203
+ permissions['maximum_end_date'] = db._maximum_end_date.strftime('%Y%m%d')
204
+
205
+ if permissions:
206
+ action = Add(_acl.Allow(role="Admin", **permissions))
207
+ action.add_where(_acl.DB, id=db.id)
208
+ actions.append(action)
209
+
210
+ return self._apply_actions(actions)
211
+
212
+ def _remove_db(self, dbs):
213
+ actions = []
214
+ for db in dbs:
215
+ action = Delete()
216
+ action.add_where(_acl.DB, id=db.id)
217
+ actions.append(action)
218
+
219
+ return self._apply_actions(actions)
220
+
221
+ def _add_user(self, users):
222
+ actions = []
223
+ for user in users:
224
+ action = Add(_acl.User(name=user))
225
+ action.add_where(_acl.Role, name="Admin")
226
+ actions.append(action)
227
+
228
+ return self._apply_actions(actions)
229
+
230
+ def _remove_user(self, users):
231
+ actions = []
232
+ for user in users:
233
+ action = Delete()
234
+ action.add_where(_acl.Role, name="Admin")
235
+ action.add_where(_acl.User, name=user)
236
+ actions.append(action)
237
+
238
+ return self._apply_actions(actions)
239
+
240
+ def add(self, *entities):
241
+ """
242
+ Add entities to the ACL and reload it.
243
+ If it fails, then tries to roll back to the original state.
244
+
245
+ Parameters
246
+ ----------
247
+ entities: DB or ACL.User
248
+
249
+ Raises
250
+ ------
251
+ TypeError, EntityOperationFailed
252
+ """
253
+ _check_entities(entities)
254
+
255
+ dbs = []
256
+ users = []
257
+ for entity in entities:
258
+ if isinstance(entity, _db.DB):
259
+ if self._db_in_dbs_case_insensitive(entity.id, self.databases):
260
+ if '//' not in entity.name:
261
+ warnings.warn(f"Database '{entity.id}' is already added to the ACL"
262
+ " and will not be rewritten with this command."
263
+ f" Notice that databases' names are case insensitive.",
264
+ stacklevel=2)
265
+ continue
266
+ dbs.append(entity)
267
+ elif isinstance(entity, ACL.User):
268
+ users.append(entity)
269
+ else:
270
+ raise TypeError(f'Entity of type "{type(entity)}" is not supported')
271
+
272
+ operations = []
273
+ if dbs:
274
+ operations.append((dbs, self._add_db, self._remove_db))
275
+ if users:
276
+ operations.append((users, self._add_user, self._remove_user))
277
+
278
+ _apply_to_entities(self, operations)
279
+
280
+ self._added_dbs.extend(dbs)
281
+
282
+ def remove(self, *entities):
283
+ """
284
+ Remove entities from the ACL and reload it.
285
+ If it fails, then tries to roll back to the original state.
286
+
287
+ Parameters
288
+ ----------
289
+ entities: DB or ACL.User
290
+
291
+ Raises
292
+ ------
293
+ ValueError, TypeError, EntityOperationFailed
294
+ """
295
+ _check_entities(entities)
296
+
297
+ dbs = []
298
+ users = []
299
+ for entity in entities:
300
+ if isinstance(entity, _db.DB):
301
+ if entity not in self._added_dbs:
302
+ raise ValueError(f'DB "{entity}" was not added')
303
+ dbs.append(entity)
304
+ elif isinstance(entity, ACL.User):
305
+ users.append(entity)
306
+ else:
307
+ raise TypeError(f'Entity of type "{type(entity)}" is not supported')
308
+
309
+ operations = []
310
+ if dbs:
311
+ operations.append((dbs, self._remove_db, self._add_db))
312
+ if users:
313
+ operations.append((users, self._remove_user, self._add_user))
314
+
315
+ _apply_to_entities(self, operations)
316
+
317
+ for db in dbs:
318
+ self._added_dbs.remove(db)
319
+
320
+ def reload(self, db=None):
321
+ if self._session_ref is not None:
322
+ return utils.reload_config(db, config_type='ACCESS_LIST')
323
+ return None
324
+
325
+ def _read_dbs(self):
326
+ get_db = GetAll()
327
+ get_db.add_where(_acl.DB)
328
+ self._apply_actions([get_db], print_writer=True)
329
+ return list(map(lambda x: x.id, get_db.result))
330
+
331
+ def _dbs(self):
332
+ action = GetAll()
333
+ action.add_where(_acl.DB)
334
+ self._apply_actions([action], print_writer=True)
335
+ return list(map(lambda x: x.id, action.result))
336
+
337
+ def _users(self):
338
+ action = GetAll()
339
+ action.add_where(_acl.Role, name="Admin")
340
+ action.add_where(_acl.User)
341
+ self._apply_actions([action], print_writer=True)
342
+ return list(map(lambda x: x.name, action.result))
343
+
344
+ @property
345
+ def databases(self):
346
+ return self._dbs()
347
+
348
+ @property
349
+ def users(self):
350
+ return self._users()
351
+
352
+
353
+ class ACLBuilder(_CommonBuilder):
354
+ def build(self):
355
+ params = {"clean_up": self.clean_up, "copy": self.copy, "session_ref": self.session_ref}
356
+
357
+ if isinstance(self.src, str):
358
+ return ACL(self.src, **params)
359
+ elif isinstance(self.src, utils.File):
360
+ return ACL(self.src.path, **params)
361
+ elif isinstance(self.src, ACL):
362
+ return self.src.copy(**params)
363
+ elif self.src is None:
364
+ return ACL(**params)
365
+
366
+ raise ValueError(f'It is not allowed to build ACL from the object of type "{type(self.src)}"')
367
+
368
+
369
+ class Locator(_FileHandler):
370
+ def __init__(self, path=None, clean_up=utils.default, copy=True, empty=False, session_ref=None):
371
+ """
372
+ Class representing OneTick database locator.
373
+ Locator is the file that describes database name, location and other options.
374
+
375
+ Parameters
376
+ ----------
377
+ path: str
378
+ A path to custom locator file. Default is `None`, that means to generate a temporary locator.
379
+ clean_up: bool
380
+ If True, then temporary locator will be removed when Locator object will be destroyed. It is
381
+ helpful for debug purpose.
382
+
383
+ By default,
384
+ :py:attr:`otp.config.clean_up_tmp_files<onetick.py.configuration.Config.clean_up_tmp_files>` is used.
385
+ copy: bool
386
+ If `True`, then the passed custom locator by the ``path`` parameter will be copied firstly before
387
+ usage. It might be used when you want to work with a custom locator, but don't want to change
388
+ the original file; in that case a custom locator will be copied into a temporary locator and
389
+ every request for modification will be executed for that temporary locator. Default is `True`.
390
+ empty: bool
391
+ If `True`, then a temporary locator will have no databases, otherwise it will have default
392
+ otp.config.default_db and COMMON databases. Default is `False`.
393
+ """
394
+ copied = True
395
+
396
+ # if path is set, then copy file, we should not work directly
397
+ # with externally passed files
398
+ if copy:
399
+ if path:
400
+ file_h = utils.TmpFile(".locator", clean_up=clean_up)
401
+ shutil.copyfile(path, file_h)
402
+ else:
403
+ file_h = utils.tmp_locator(clean_up=clean_up, empty=empty)
404
+
405
+ else:
406
+ if path:
407
+ file_h = utils.PermanentFile(path)
408
+ copied = False
409
+ else:
410
+ file_h = utils.tmp_locator(clean_up=clean_up, empty=empty)
411
+
412
+ assert file_h is not None
413
+
414
+ super().__init__(file_h, clean_up=clean_up, copied=copied, session_ref=session_ref)
415
+
416
+ self._added_dbs = []
417
+ self._added_ts = []
418
+
419
+ def cleanup(self):
420
+ self._remove_db(self._added_dbs)
421
+ self._remove_ts(str(server) for server in self._added_ts)
422
+ self._added_dbs = []
423
+ self._added_ts = []
424
+ self.reload()
425
+
426
+ @property
427
+ def databases(self):
428
+ return self._dbs()
429
+
430
+ @property
431
+ def tick_servers(self):
432
+ return self._ts()
433
+
434
+ def reload(self, db_=None):
435
+ if self._session_ref is not None:
436
+ return utils.reload_config(db_, config_type='LOCATOR')
437
+ return None
438
+
439
+ def _apply_actions(self, actions, print_writer=False):
440
+ writer = PrintWriter() if print_writer else FileWriter(self.path)
441
+ flush = False if print_writer else True
442
+ return apply_actions(_locator.parse_locator, FileReader(self.path), writer, actions, flush=flush)
443
+
444
+ def _dbs(self):
445
+ action = GetAll()
446
+ action.add_where(_locator.DB)
447
+ self._apply_actions([action], print_writer=True)
448
+ return list(map(lambda x: x.id, action.result))
449
+
450
+ def _ts(self):
451
+ get_ts = GetAll()
452
+ get_ts.add_where(_locator.TickServers)
453
+ get_ts.add_where(_locator.ServerLocation)
454
+ self._apply_actions([get_ts], print_writer=True)
455
+ return [location.location for location in get_ts.result]
456
+
457
+ def _add_db(self, dbs):
458
+ actions = []
459
+ for db in dbs:
460
+ actions.append(Add(_locator.DB(id=db.id, **db.properties)))
461
+ for location in db.locations:
462
+ action = Add(_locator.Location(**location))
463
+ action.add_where(_locator.DB, id=db.id)
464
+ actions.append(action)
465
+
466
+ for raw_db in db.raw_data:
467
+ common = {k: v for k, v in raw_db.items() if k not in {'id', 'locations'}}
468
+ action = Add(_locator.RawDB(id=raw_db['id'], **common))
469
+ action.add_where(_locator.DB, id=db.id)
470
+ actions.append(action)
471
+ for location in raw_db['locations']:
472
+ action = Add(_locator.Location(**location))
473
+ action.add_where(_locator.DB, id=db.id)
474
+ action.add_where(_locator.RawDB, id=raw_db['id'])
475
+ actions.append(action)
476
+
477
+ if db.feed:
478
+ options = {k: v for k, v in db.feed.items() if k != 'type'}
479
+ action = Add(_locator.Feed(type=db.feed['type']))
480
+ action.add_where(_locator.DB, id=db.id)
481
+ actions.append(action)
482
+ action = Add(_locator.FeedOptions(**options))
483
+ action.add_where(_locator.DB, id=db.id)
484
+ action.add_where(_locator.Feed, type=db.feed['type'])
485
+ actions.append(action)
486
+
487
+ return self._apply_actions(actions)
488
+
489
+ def _remove_db(self, dbs):
490
+ actions = []
491
+ for db in dbs:
492
+ action = Delete()
493
+ action.add_where(_locator.DB, id=db.id)
494
+ actions.append(action)
495
+
496
+ return self._apply_actions(actions)
497
+
498
+ def _add_ts(self, servers):
499
+ """
500
+ Add servers to locator file (without reloading)
501
+
502
+ Parameters
503
+ ----------
504
+ servers: RemoteTS
505
+ Servers to be added to locator.
506
+ """
507
+ actions = []
508
+ for server in servers:
509
+ if server.cep:
510
+ actions.append(Add(_locator.CEPServerLocation(location=str(server))))
511
+ else:
512
+ actions.append(Add(_locator.ServerLocation(location=str(server))))
513
+
514
+ return self._apply_actions(actions)
515
+
516
+ def _remove_ts(self, servers):
517
+ """
518
+ Remove servers from locator file (without reloading)
519
+
520
+ Parameters
521
+ ----------
522
+ servers: RemoteTS
523
+ Servers to remove from locator
524
+ """
525
+ actions = []
526
+ for server in servers:
527
+ action = Delete()
528
+ action.add_where(_locator.ServerLocation, location=str(server))
529
+ actions.append(action)
530
+
531
+ return self._apply_actions(actions)
532
+
533
+ def _add_locator(self, locators):
534
+ """
535
+ Add references to locators
536
+
537
+ Parameters
538
+ ----------
539
+ locators: Locator
540
+ """
541
+ actions = []
542
+ for locator in locators:
543
+ actions.append(Add(_locator.Include(path=locator.path)))
544
+
545
+ return self._apply_actions(actions)
546
+
547
+ def _remove_locator(self, locators):
548
+ """
549
+ Remove references for locators
550
+
551
+ Parameters
552
+ ----------
553
+ locators: Locator
554
+ """
555
+ actions = []
556
+ for locator in locators:
557
+ action = Delete()
558
+ action.add_where(_locator.Include, path=locator.path)
559
+ actions.append(action)
560
+
561
+ return self._apply_actions(actions)
562
+
563
+ def add(self, *entities):
564
+ """
565
+ Add entities to the locator and reload it.
566
+ If it fails, then tries to roll back to the original state.
567
+
568
+ Parameters
569
+ ----------
570
+ entities: DB, RemoteTS or Locator
571
+
572
+ Raises
573
+ ------
574
+ TypeError, EntityOperationFailed
575
+ """
576
+ _check_entities(entities)
577
+
578
+ dbs = []
579
+ servers = []
580
+ locators = []
581
+ for entity in entities:
582
+ if isinstance(entity, _db.db._DB):
583
+ if self._db_in_dbs_case_insensitive(entity.id, self.databases):
584
+ if '//' not in entity.name:
585
+ warnings.warn(f"Database '{entity.id}' is already added to the Locator"
586
+ " and will not be rewritten with this command."
587
+ f" Notice that databases' names are case insensitive.", stacklevel=2)
588
+ continue
589
+ dbs.append(entity)
590
+ elif isinstance(entity, _servers.RemoteTS):
591
+ servers.append(entity)
592
+ elif isinstance(entity, Locator):
593
+ locators.append(entity)
594
+ else:
595
+ raise TypeError(f'Entity of type "{type(entity)}" is not supported')
596
+
597
+ operations = []
598
+ if dbs:
599
+ operations.append((dbs, self._add_db, self._remove_db))
600
+ if servers:
601
+ operations.append((servers, self._add_ts, self._remove_ts))
602
+ if locators:
603
+ operations.append((locators, self._add_locator, self._remove_locator))
604
+
605
+ _apply_to_entities(self, operations)
606
+
607
+ self._added_dbs.extend(dbs)
608
+ self._added_ts.extend(servers)
609
+
610
+ def remove(self, *entities):
611
+ """
612
+ Remove entities from the locator and reload it.
613
+ If it fails, then tries to roll back to the original state.
614
+
615
+ Raises
616
+ ------
617
+ ValueError, TypeError, EntityOperationFailed
618
+ """
619
+ _check_entities(entities)
620
+
621
+ dbs = []
622
+ servers = []
623
+ locators = []
624
+ for entity in entities:
625
+ if isinstance(entity, _db.db._DB):
626
+ if entity not in self._added_dbs:
627
+ raise ValueError(f'DB "{entity}" was not added')
628
+ dbs.append(entity)
629
+ elif isinstance(entity, _servers.RemoteTS):
630
+ if entity not in self._added_ts:
631
+ raise ValueError(f'Tick server "{entity}" was not added')
632
+ servers.append(entity)
633
+ elif isinstance(entity, Locator):
634
+ locators.append(entity)
635
+ else:
636
+ raise TypeError(f'Entity of type "{type(entity)}" is not supported')
637
+
638
+ operations = []
639
+ if dbs:
640
+ operations.append((dbs, self._remove_db, self._add_db))
641
+ if servers:
642
+ operations.append((servers, self._remove_ts, self._add_ts))
643
+ if locators:
644
+ operations.append((locators, self._remove_locator, self._add_locator))
645
+
646
+ _apply_to_entities(self, operations)
647
+
648
+ for db in dbs:
649
+ self._added_dbs.remove(db)
650
+ for server in servers:
651
+ self._added_ts.remove(server)
652
+
653
+ def __contains__(self, item):
654
+ if str(item) in self.databases:
655
+ return True
656
+ return False
657
+
658
+
659
+ class LocatorBuilder(_CommonBuilder):
660
+ def build(self):
661
+ params = {"clean_up": self.clean_up, "copy": self.copy, "session_ref": self.session_ref}
662
+
663
+ if isinstance(self.src, str):
664
+ return Locator(self.src, **params)
665
+ elif isinstance(self.src, utils.File):
666
+ return Locator(self.src.path, **params)
667
+ elif isinstance(self.src, Locator):
668
+ return self.src.copy(**params)
669
+ elif isinstance(self.src, _servers.RemoteTS):
670
+ locator = Locator(empty=True, **params)
671
+ locator.add(self.src)
672
+ return locator
673
+ elif self.src is None:
674
+ return Locator(**params)
675
+
676
+ raise ValueError(f'It is not allowed to build Locator from the object of type "{type(self.src)}"')
677
+
678
+
679
+ class Config(_FileHandler):
680
+
681
+ _CONFIG_VARIABLES_PASSED_VIA_THEIR_OWN_PARAMETER = {
682
+ 'ACCESS_CONTROL_FILE': 'acl',
683
+ 'DB_LOCATOR.DEFAULT': 'locator',
684
+ 'OTQ_FILE_PATH': 'otq_path',
685
+ 'CSV_FILE_PATH': 'csv_path',
686
+ 'LICENSE_REPOSITORY_DIR': 'license',
687
+ 'ONE_TICK_LICENSE_FILE': 'license',
688
+ }
689
+
690
+ def __init__(
691
+ self,
692
+ config=None,
693
+ locator=None,
694
+ acl=None,
695
+ otq_path=None,
696
+ csv_path=None,
697
+ clean_up=utils.default,
698
+ copy=True,
699
+ session_ref=None,
700
+ license=None,
701
+ variables=None,
702
+ ):
703
+ """
704
+ Parameters
705
+ ----------
706
+ config: path or Config
707
+ Allows to specify a custom config. None is to use temporary generated config. Default is None.
708
+ locator: Locator
709
+ Allows to specify a custom locator file. None is to use temporary generated locator. Default is None.
710
+ acl: ACL
711
+ Allows to specify a custom acl file. None is to use temporary generated acl. Default is None.
712
+ otq_path: list of paths to lookup queries
713
+ OTQ_PATH parameter in the OneTick config file. Default is None, that is equal to the empty list.
714
+ csv_path: list of paths to lookup csv files
715
+ CSV_PATH parameter in the OneTick config file. Default is None, that is equal to the empty list.
716
+ clean_up: bool
717
+ If True, then temporary config file will be removed when the Config instance will be destroyed.
718
+ It is helpful for debug purpose.
719
+
720
+ By default,
721
+ :py:attr:`otp.config.clean_up_tmp_files<onetick.py.configuration.Config.clean_up_tmp_files>` is used.
722
+ copy: bool
723
+ If True, then the passed custom config file will be copied firstly before any usage with it.
724
+ It might be used when you want to work with a custom config file, but don't want to change to
725
+ change the original file; in that case a custom config will be copied into a temporary config
726
+ file and every request for modification will be executed for that temporary config. Default
727
+ is True.
728
+ license: instance from the onetick.py.license module
729
+ License to use. If it is not set, then onetick.py.license.Default is used.
730
+ variables: dict
731
+ Other values to pass to config.
732
+ """
733
+ if config and (locator or acl):
734
+ raise ValueError("It is not allowed to use 'config' parameter along with 'locator' or 'acl'")
735
+
736
+ # builders that construct locator and acl based on parameters
737
+ acl_builder = ACLBuilder(src=acl, clean_up=clean_up, copy=copy, session_ref=session_ref)
738
+ locator_builder = LocatorBuilder(src=locator, clean_up=clean_up, copy=copy, session_ref=session_ref)
739
+ config_copied = True
740
+
741
+ if config:
742
+ # copy passed file, we should not work with externally passed files
743
+ if copy and not os.getenv('OTP_WEBAPI_TEST_MODE'):
744
+ self._file = utils.TmpFile(".cfg", clean_up=clean_up)
745
+ config_path = config.path if isinstance(config, Config) else config
746
+
747
+ shutil.copyfile(config_path, self._file.path)
748
+ else:
749
+ self._file = utils.PermanentFile(config)
750
+ config_copied = False
751
+
752
+ if utils.is_param_in_config(self._file.path, "ACCESS_CONTROL_FILE"):
753
+ acl_builder.src = utils.get_config_param(self._file.path, "ACCESS_CONTROL_FILE")
754
+
755
+ if utils.is_param_in_config(self._file.path, "DB_LOCATOR.DEFAULT"):
756
+ locator_builder.src = utils.get_config_param(self._file.path, "DB_LOCATOR.DEFAULT")
757
+
758
+ else:
759
+ self._file = utils.tmp_config(clean_up=clean_up)
760
+
761
+ self._acl = acl_builder.build()
762
+ # it is used in onetick-py-test
763
+ os.environ["ONE_TICK_SESSION_ACL_PATH"] = self._acl.path
764
+ self._locator = locator_builder.build()
765
+ # it is used in onetick-py-test
766
+ os.environ["ONE_TICK_SESSION_LOCATOR_PATH"] = self._locator.path
767
+
768
+ super().__init__(self._file, clean_up=clean_up, copied=config_copied, session_ref=session_ref)
769
+
770
+ # Here we can start to modify files - they are either copied or generated
771
+ # ------------------------------------------------------------------------
772
+
773
+ utils.modify_config_param(self.path, "ACCESS_CONTROL_FILE", self._acl.path, throw_on_missing=False)
774
+ utils.modify_config_param(self.path, "DB_LOCATOR.DEFAULT", self._locator.path, throw_on_missing=False)
775
+
776
+ if otq_path:
777
+ otq_path = map(str, otq_path)
778
+ utils.modify_config_param(self.path, "OTQ_FILE_PATH", ",".join(otq_path), throw_on_missing=False)
779
+ if csv_path:
780
+ csv_path = map(str, csv_path)
781
+ utils.modify_config_param(self.path, "CSV_FILE_PATH", ",".join(csv_path), throw_on_missing=False)
782
+ variables = variables or {}
783
+ for parameter_name, parameter_value in variables.items():
784
+ if parameter_name in self._CONFIG_VARIABLES_PASSED_VIA_THEIR_OWN_PARAMETER:
785
+ raise ValueError(f'Variable {parameter_name} should be set via '
786
+ f'{self._CONFIG_VARIABLES_PASSED_VIA_THEIR_OWN_PARAMETER[parameter_name]} parameter')
787
+ if isinstance(parameter_value, list):
788
+ parameter_value = ",".join(map(str, parameter_value))
789
+ utils.modify_config_param(self.path, parameter_name, parameter_value, throw_on_missing=False)
790
+
791
+ # set license
792
+ # ---------------------------
793
+ custom_license = utils.is_param_in_config(self.path, "LICENSE_REPOSITORY_DIR")
794
+ custom_license &= utils.is_param_in_config(self.path, "ONE_TICK_LICENSE_FILE")
795
+ if license:
796
+ self._license = license
797
+ else:
798
+ if custom_license:
799
+ lic_file = utils.get_config_param(self.path, "ONE_TICK_LICENSE_FILE")
800
+ lic_dir = utils.get_config_param(self.path, "LICENSE_REPOSITORY_DIR")
801
+ self._license = _license.Custom(lic_file, lic_dir)
802
+ else:
803
+ if isinstance(locator, _servers.RemoteTS):
804
+ self._license = _license.Remote()
805
+ else:
806
+ self._license = _license.Default()
807
+
808
+ if not custom_license: # no need to set already defined custom values
809
+ if self._license.dir:
810
+ utils.modify_config_param(self.path,
811
+ "LICENSE_REPOSITORY_DIR",
812
+ self._license.dir,
813
+ throw_on_missing=False)
814
+ if self._license.file:
815
+ utils.modify_config_param(self.path,
816
+ "ONE_TICK_LICENSE_FILE",
817
+ self._license.file,
818
+ throw_on_missing=False)
819
+
820
+ @property
821
+ def acl(self):
822
+ return self._acl
823
+
824
+ @property
825
+ def locator(self):
826
+ return self._locator
827
+
828
+ @property
829
+ def license(self):
830
+ return self._license
831
+
832
+ def copy(self, clean_up=utils.default, copy=True, session_ref=None):
833
+ """ overridden version of copy """
834
+ return self.__class__(self.path, clean_up=clean_up, copy=copy, session_ref=session_ref, license=self._license)
835
+
836
+ @staticmethod
837
+ def build(obj=None, clean_up=utils.default, copy=True, session_ref=None):
838
+ params = {"clean_up": clean_up, "copy": copy, "session_ref": session_ref}
839
+
840
+ if isinstance(obj, str):
841
+ return Config(obj, **params)
842
+ elif isinstance(obj, utils.File):
843
+ return Config(obj.path, **params)
844
+ elif isinstance(obj, Config):
845
+ return obj.copy(**params)
846
+ elif obj is None:
847
+ return Config(**params)
848
+
849
+ raise ValueError(f'It is not allowed to build Config from the object of type "{type(obj)}"')
850
+
851
+ def cleanup(self):
852
+ # no logic to clean up content
853
+
854
+ self._acl.cleanup()
855
+ self._locator.cleanup()
856
+
857
+ @property
858
+ def otq_path(self):
859
+ try:
860
+ return utils.get_config_param(self.path, "OTQ_FILE_PATH")
861
+ except AttributeError:
862
+ return None
863
+
864
+
865
+ class PerformanceMetricsParser:
866
+ metrics_fields = {
867
+ "user_time": float,
868
+ "system_time": float,
869
+ "elapsed_time": float,
870
+ "virtual_memory": int,
871
+ "virtual_memory_peak": int,
872
+ "working_set": int,
873
+ "working_set_peak": int,
874
+ "disk_read": int,
875
+ "disk_write": int,
876
+ }
877
+
878
+ metrics_units = {
879
+ "user_time": "s",
880
+ "system_time": "s",
881
+ "elapsed_time": "s",
882
+ "virtual_memory": "bytes",
883
+ "virtual_memory_peak": "bytes",
884
+ "working_set": "bytes",
885
+ "working_set_peak": "bytes",
886
+ "disk_read": "bytes",
887
+ "disk_write": "bytes",
888
+ }
889
+
890
+ def __init__(self):
891
+ self._metrics = {}
892
+
893
+ def parse(self, log_file: str):
894
+ with open(log_file, "r") as log_file_io:
895
+ for log_line in log_file_io:
896
+ result = re.search(r"Performance Metrics FINAL ([\w\s]+) \((\w+)\): (.+)$", log_line.strip())
897
+ if not result:
898
+ continue
899
+
900
+ metric_name, metric_units, metric_value = result.groups()
901
+ metric_key = "_".join(metric_name.lower().split())
902
+
903
+ if metric_key not in self.metrics_fields:
904
+ raise ValueError(
905
+ f"Unexpected performance metric `{metric_key}` was found in the log: \"{log_line.strip()}\""
906
+ )
907
+
908
+ if metric_units != self.metrics_units[metric_key]:
909
+ raise ValueError(
910
+ f"Unexpected units for metric `{metric_key}`: "
911
+ f"expected `{self.metrics_units[metric_key]}`, got `{metric_units}`."
912
+ )
913
+
914
+ if metric_key in self._metrics:
915
+ raise KeyError(f"Metric `{metric_key}` was already saved, but another entry was found in the log.")
916
+
917
+ try:
918
+ metric_value = self.metrics_fields[metric_key](metric_value)
919
+ except Exception as exc:
920
+ raise ValueError(
921
+ f"Failed to parse and convert metric {metric_key} "
922
+ f"from `str` to `{self.metrics_fields[metric_key].__name__}`: \"{metric_value}\""
923
+ ) from exc
924
+
925
+ self._metrics[metric_key] = {
926
+ "name": metric_name,
927
+ "value": metric_value,
928
+ "units": self.metrics_units[metric_key],
929
+ }
930
+
931
+ not_found_keys = set(self.metrics_fields.keys()).difference(self._metrics.keys())
932
+ if not_found_keys:
933
+ raise RuntimeError("Expected performance metrics not found in the log: " + ", ".join(not_found_keys))
934
+
935
+ @property
936
+ def metrics(self):
937
+ return self._metrics.copy()
938
+
939
+
940
+ class Session:
941
+ """
942
+ A class for setting up working OneTick session. It keeps configuration files during
943
+ the session and allows to manage them. When instance is out of scope, then instance
944
+ cleans up config files and configuration.
945
+ You can leave the scope manually with method :py:meth:`close`.
946
+ Also, session is closed automatically if this object is used as a context manager.
947
+
948
+ .. note::
949
+ It is allowed to have only one alive session instance in the process.
950
+
951
+ If you don't use Session's instance, then ``ONE_TICK_CONFIG`` environment variable
952
+ should be set to be able to work with OneTick.
953
+
954
+ If config file is not set then temporary is generated.
955
+ Config includes locator and acl file, and if they are not set, then they are generated.
956
+
957
+ Parameters
958
+ ----------
959
+ config : str, :py:class:`onetick.py.session.Config`, optional
960
+ Path to an existing OneTick config file; if it is not set, then config will be generated.
961
+ If config is not set, then temporary config is generated. Default is None.
962
+ clean_up : bool, optional
963
+ A flag to control cleaning up process: if it is True then all temporary generated files will
964
+ be automatically removed. It is helpful for debugging. The flag affects only generated files, but
965
+ does not externally passed.
966
+
967
+ By default,
968
+ :py:attr:`otp.config.clean_up_tmp_files<onetick.py.configuration.Config.clean_up_tmp_files>` is used.
969
+ copy : bool, optional
970
+ A flag to control file copy process: if it is True then all externally passed files will be
971
+ copied before usage, otherwise all modifications during an existing session happen directly with
972
+ passed config files. NOTE: we suggest to set this flag only when you fully understand it's effect.
973
+ Default is True.
974
+ override_env : bool, optional
975
+ If flag is True, then unconditionally ``ONE_TICK_CONFIG`` environment variable will be overridden
976
+ with a config that belongs to a Session. Otherwise ``ONE_TICK_CONFIG``
977
+ will be defined in the scope of session only when it is not defined externally.
978
+ For example, it is helpful when you test ascii_loader that uses 'ONE_TICK_CONFIG' only.
979
+
980
+ Default is False ( default is False, because overriding external environment variable
981
+ might be not obvious and desirable )
982
+ redirect_logs: bool, optional
983
+ If flag is True, then OneTick logs will be redirected into a temporary log file. Otherwise
984
+ logs will be mixed with output. Default is True.
985
+ gather_performance_metrics: bool, optional
986
+ If flag is True, then enables performance metrics gathering, by setting ``DUMP_PERF_METRICS`` config parameter.
987
+ Sets ``redirect_logs`` flag to ``True``.
988
+
989
+ Metrics are available after closing a session via ``session.performance_metrics`` property.
990
+
991
+ .. warning::
992
+ Due to current limitations, metrics are cumulative. So if you run multiple queries in the same process
993
+ (even with different session objects), you'll get metrics for the whole process since it's start
994
+ till end of a session with ``gather_performance_metrics=True``.
995
+
996
+ To avoid this, you can create a session in a separate process, either by using Python's ``multiprocessing``
997
+ or by moving required code to a separate Python script and running it in a new process.
998
+
999
+ .. note::
1000
+ Metrics are gathered for all operations in the session between its creation and closing.
1001
+
1002
+ Examples
1003
+ --------
1004
+
1005
+ If session is defined with environment, OneTick can be used right away:
1006
+
1007
+ >>> 'ONE_TICK_CONFIG' in os.environ
1008
+ True
1009
+ >>> list(otp.databases()) # doctest: +ELLIPSIS
1010
+ ['COMMON', 'DEMO_L1', ..., 'SOME_DB', 'SOME_DB_2'...
1011
+ >>> data = otp.DataSource('SOME_DB', symbol='S1', tick_type='TT')
1012
+ >>> otp.run(data)
1013
+ Time X
1014
+ 0 2003-12-01 00:00:00.000 1
1015
+ 1 2003-12-01 00:00:00.001 2
1016
+ 2 2003-12-01 00:00:00.002 3
1017
+
1018
+ Collecting performance metrics with ``gather_performance_metrics`` parameter:
1019
+
1020
+ >>> with otp.Session(gather_performance_metrics=True) as session: # doctest: +SKIP
1021
+ >>> data_a = otp.DataSource('DB_A', symbol='S1', tick_type='TT') # doctest: +SKIP
1022
+ >>> data_b = otp.DataSource('DB_B', symbol='S1', tick_type='TT') # doctest: +SKIP
1023
+ >>> _ = otp.run(otp.merge([data_a, data_b])) # doctest: +SKIP
1024
+ >>>
1025
+ >>> session.performance_metrics # doctest: +SKIP
1026
+ {
1027
+ 'user_time': {'name': 'User Time', 'value': 3.39063, 'units': 's'},
1028
+ 'system_time': {'name': 'System Time', 'value': 1.07813, 'units': 's'},
1029
+ 'elapsed_time': {'name': 'Elapsed Time', 'value': 6.78816, 'units': 's'},
1030
+ 'virtual_memory': {'name': 'Virtual Memory', 'value': 6261944320, 'units': 'bytes'},
1031
+ 'virtual_memory_peak': {'name': 'Virtual Memory Peak', 'value': 6271926272, 'units': 'bytes'},
1032
+ 'working_set': {'name': 'Working Set', 'value': 228110336, 'units': 'bytes'},
1033
+ 'working_set_peak': {'name': 'Working Set Peak', 'value': 228126720, 'units': 'bytes'},
1034
+ 'disk_read': {'name': 'Disk Read', 'value': 32289438, 'units': 'bytes'},
1035
+ 'disk_write': {'name': 'Disk Write', 'value': 172906, 'units': 'bytes'}
1036
+ }
1037
+ """
1038
+ # TODO: create article for Session in Guides or Concepts
1039
+
1040
+ _instance = None
1041
+
1042
+ def __init__(
1043
+ self, config=None, clean_up=utils.default, copy=True, override_env=False, redirect_logs=True,
1044
+ gather_performance_metrics=False,
1045
+ ):
1046
+ self._construct(config, clean_up, copy, override_env, redirect_logs, gather_performance_metrics)
1047
+
1048
+ def _construct(
1049
+ self, config=None, clean_up=utils.default, copy=True, override_env=False, redirect_logs=True,
1050
+ gather_performance_metrics=False,
1051
+ ):
1052
+
1053
+ if Session._instance:
1054
+ raise MultipleSessionsException(
1055
+ "It is forbidden to use multiple active sessions simultaniously in one process"
1056
+ )
1057
+
1058
+ def onetick_cfg_rollback(var):
1059
+ """
1060
+ function to rollback ONE_TICK_CONFIG state
1061
+ """
1062
+
1063
+ def _impl():
1064
+ if var is None:
1065
+ if "ONE_TICK_CONFIG" in os.environ:
1066
+ del os.environ["ONE_TICK_CONFIG"]
1067
+ else:
1068
+ os.environ["ONE_TICK_CONFIG"] = var
1069
+
1070
+ return _impl
1071
+
1072
+ self._lib = None
1073
+ self._env_rollback = onetick_cfg_rollback(os.environ.get("ONE_TICK_CONFIG", None))
1074
+ self._override_env = override_env
1075
+
1076
+ self._config = Config.build(config, clean_up=clean_up, copy=copy, session_ref=self)
1077
+ # it is used in onetick-py-test
1078
+ os.environ["ONE_TICK_SESSION_CFG_PATH"] = self._config.path
1079
+
1080
+ try:
1081
+ if "ONE_TICK_CONFIG" not in os.environ:
1082
+ os.environ["ONE_TICK_CONFIG"] = self._config.path
1083
+ else:
1084
+ if override_env:
1085
+ os.environ["ONE_TICK_CONFIG"] = self._config.path
1086
+ else:
1087
+ warnings.warn(
1088
+ UserWarning(
1089
+ "ONE_TICK_CONFIG env variable has been set before a session, "
1090
+ "and in the session scope it is not related to the session config. "
1091
+ "If you want to make ONE_TICK_CONFIG env variable be consistent "
1092
+ "with the session, then look at the override_env flag "
1093
+ "for the Session constructor"
1094
+ )
1095
+ )
1096
+
1097
+ otli.OneTickLib().cleanup()
1098
+
1099
+ self._performance_metrics_parser = None
1100
+ if gather_performance_metrics:
1101
+ self._performance_metrics_parser = PerformanceMetricsParser()
1102
+ utils.modify_config_param(
1103
+ self._config.path, "DUMP_PERF_METRICS", "ON", throw_on_missing=False,
1104
+ )
1105
+ redirect_logs = True
1106
+
1107
+ self._log_file = log_file = None
1108
+ if redirect_logs:
1109
+ self._log_file = utils.TmpFile(suffix=".onetick.log", clean_up=clean_up)
1110
+ log_file = self._log_file.path
1111
+
1112
+ self._lib = otli.OneTickLib(self._config.path, log_file=log_file)
1113
+ except Exception:
1114
+ self._env_rollback()
1115
+ # TODO: rollback, but need to wait BDS-91
1116
+ raise
1117
+
1118
+ # force reload cfg/locator/acl, because it is not reloaded after each time session if re-created
1119
+ if os.getenv('OTP_WEBAPI_TEST_MODE'):
1120
+ utils.reload_config(None, config_type='MAIN_CONFIG')
1121
+ self.locator.reload()
1122
+ self.acl.reload()
1123
+
1124
+ self._ts_dbs = {}
1125
+
1126
+ # PY-1352: this line must be the last
1127
+ # in case we get exception anywhere before, we shouldn't set Session class variable,
1128
+ # because it will affect the creation of all the future Session objects
1129
+ Session._instance = self
1130
+
1131
+ def use(self, *items):
1132
+ """
1133
+ Makes DB or TS available inside the session.
1134
+
1135
+ Parameters
1136
+ ----------
1137
+ items : :py:class:`~onetick.py.DB` or :py:class:`~onetick.py.servers.RemoteTS` objects
1138
+ Items to be added to session.
1139
+
1140
+ Examples
1141
+ --------
1142
+
1143
+ (note that ``session`` is created before this example)
1144
+
1145
+ >>> list(otp.databases()) # doctest: +ELLIPSIS
1146
+ ['COMMON', 'DEMO_L1', ...]
1147
+ >>> new_db = otp.DB('ZZZZ')
1148
+ >>> session.use(new_db)
1149
+ >>> list(otp.databases()) # doctest: +ELLIPSIS
1150
+ ['COMMON', 'DEMO_L1', ..., 'ZZZZ']
1151
+ """
1152
+ self.locator.add(*items)
1153
+ dbs = []
1154
+ for item in items:
1155
+ if isinstance(item, _db.db._DB):
1156
+ dbs.append(item)
1157
+ try:
1158
+ if dbs:
1159
+ self.acl.add(*dbs)
1160
+ except Exception:
1161
+ self.locator.remove(*items)
1162
+ raise
1163
+
1164
+ def use_stub(self, stub_name):
1165
+ """
1166
+ Adds stub-DB into the session.
1167
+ The shortcut for ``.use(otp.DB(stub_name))``
1168
+
1169
+ Parameters
1170
+ ----------
1171
+ stub_name : str
1172
+ name of the stub
1173
+ """
1174
+ return self.use(_db.DB(stub_name))
1175
+
1176
+ def __enter__(self):
1177
+ return self
1178
+
1179
+ def __exit__(self, exc_type, exc_val, exc_tb):
1180
+ if exc_type:
1181
+ # session will be closed due to exception, no need to parse performance metrics
1182
+ self._performance_metrics_parser = None
1183
+
1184
+ self.close()
1185
+
1186
+ @staticmethod
1187
+ def _available_dbs():
1188
+ return _databases()
1189
+
1190
+ def _get_ts_dbs(self):
1191
+ locator_dbs = self.locator.databases
1192
+
1193
+ all_dbs = self._available_dbs()
1194
+
1195
+ for db_name in all_dbs:
1196
+ if db_name not in locator_dbs and db_name not in self._ts_dbs:
1197
+ self._ts_dbs[db_name] = _db.db._DB(db_name)
1198
+
1199
+ def close(self):
1200
+ """
1201
+ Close session
1202
+ """
1203
+ if Session._instance == self:
1204
+ try:
1205
+ if self._config:
1206
+ del self._config
1207
+ self._config = None
1208
+
1209
+ finally:
1210
+ if self._lib:
1211
+ self._lib.cleanup()
1212
+ self._lib = None
1213
+
1214
+ self._env_rollback()
1215
+
1216
+ Session._instance = None
1217
+
1218
+ if self._performance_metrics_parser:
1219
+ self._performance_metrics_parser.parse(self._log_file)
1220
+
1221
+ def __del__(self):
1222
+ self.close()
1223
+
1224
+ @property
1225
+ def config(self):
1226
+ """
1227
+ A reference to the underlying Config object that represents OneTick config file.
1228
+
1229
+ Returns
1230
+ -------
1231
+ :py:class:`onetick.py.session.Config`
1232
+ """
1233
+ return self._config
1234
+
1235
+ @config.setter
1236
+ def config(self, cfg):
1237
+ self.close()
1238
+ self._construct(cfg, override_env=self._override_env)
1239
+
1240
+ @property
1241
+ def acl(self):
1242
+ """
1243
+ A reference to the underlying ACL object that represents OneTick access control list file.
1244
+
1245
+ Returns
1246
+ -------
1247
+ :py:class:`onetick.py.session.ACL`
1248
+ """
1249
+ return self._config.acl
1250
+
1251
+ @property
1252
+ def locator(self):
1253
+ """
1254
+ A reference to the underlying Locator that represents OneTick locator file.
1255
+
1256
+ Returns
1257
+ -------
1258
+ :py:class:`onetick.py.session.Locator`
1259
+ """
1260
+ return self._config.locator
1261
+
1262
+ @property
1263
+ def license(self):
1264
+ return self._config.license
1265
+
1266
+ @property
1267
+ def ts_databases(self):
1268
+ self._get_ts_dbs()
1269
+ return self._ts_dbs
1270
+
1271
+ @property
1272
+ def databases(self):
1273
+ return self._available_dbs()
1274
+
1275
+ @property
1276
+ def performance_metrics(self):
1277
+ if self._instance:
1278
+ raise RuntimeError("Trying to get performance metrics before closing the session.")
1279
+
1280
+ if not self._performance_metrics_parser:
1281
+ raise RuntimeError(
1282
+ "Trying to get performance metrics, "
1283
+ "however session was created without `gather_performance_metrics=True`."
1284
+ )
1285
+
1286
+ return self._performance_metrics_parser.metrics
1287
+
1288
+
1289
+ class TestSession(Session):
1290
+ def __init__(self, *args, **kwargs):
1291
+ """
1292
+ This class does the same as :py:class:`onetick.py.session.Session`,
1293
+ but also defines default required :py:attr:`otp.config <onetick.py.configuration.Config>` values.
1294
+
1295
+ Using this session object is the equivalent of defining these configuration values:
1296
+
1297
+ ::
1298
+
1299
+ otp.config['tz'] = 'EST5EDT'
1300
+ otp.config['default_db'] = 'DEMO_L1'
1301
+ otp.config['default_symbol'] = 'AAPL'
1302
+ otp.config['default_start_time'] = datetime(2003, 12, 1, 0, 0, 0)
1303
+ otp.config['default_end_time'] = datetime(2003, 12, 4, 0, 0, 0)
1304
+
1305
+ ``DEMO_L1`` is the database defined in the default locator generated by onetick.py
1306
+ and it is also a name of the database commonly used in the OneTick ecosystem, academy courses, etc.
1307
+ """
1308
+ configuration.config['tz'] = 'EST5EDT'
1309
+ configuration.config['default_db'] = 'DEMO_L1'
1310
+ configuration.config['default_symbol'] = 'AAPL'
1311
+ configuration.config['default_start_time'] = datetime(2003, 12, 1, 0, 0, 0)
1312
+ configuration.config['default_end_time'] = datetime(2003, 12, 4, 0, 0, 0)
1313
+ super().__init__(*args, **kwargs)
1314
+
1315
+
1316
+ class HTTPSession:
1317
+ param_list = ['http_address', 'http_username', 'http_password', 'access_token', 'http_proxy', 'https_proxy']
1318
+
1319
+ def __init__(self,
1320
+ http_address,
1321
+ http_username=None,
1322
+ http_password=None,
1323
+ access_token=None,
1324
+ http_proxy=None,
1325
+ https_proxy=None):
1326
+ """
1327
+ This class must be used only for WebAPI connection,
1328
+ to set HTTP connection parameters.
1329
+ """
1330
+ import onetick.py as otp # noqa
1331
+ self._restore_config = {}
1332
+ for param in self.param_list:
1333
+ if locals()[param]:
1334
+ self._restore_config[param] = otp.config.get(param)
1335
+ otp.config.__setattr__(param, locals()[param])
1336
+
1337
+ def close(self):
1338
+ # restore config
1339
+ import onetick.py as otp # noqa
1340
+ for param, value in self._restore_config.items():
1341
+ otp.config.__setattr__(param, value)
1342
+
1343
+ def __enter__(self):
1344
+ return self
1345
+
1346
+ def __exit__(self, exc_type, exc_val, exc_tb):
1347
+ self.close()