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.
- locator_parser/__init__.py +0 -0
- locator_parser/acl.py +73 -0
- locator_parser/actions.py +262 -0
- locator_parser/common.py +368 -0
- locator_parser/io.py +43 -0
- locator_parser/locator.py +150 -0
- onetick/__init__.py +101 -0
- onetick/doc_utilities/__init__.py +3 -0
- onetick/doc_utilities/napoleon.py +40 -0
- onetick/doc_utilities/ot_doctest.py +140 -0
- onetick/doc_utilities/snippets.py +279 -0
- onetick/lib/__init__.py +4 -0
- onetick/lib/instance.py +141 -0
- onetick/py/__init__.py +293 -0
- onetick/py/_stack_info.py +89 -0
- onetick/py/_version.py +2 -0
- onetick/py/aggregations/__init__.py +11 -0
- onetick/py/aggregations/_base.py +648 -0
- onetick/py/aggregations/_docs.py +948 -0
- onetick/py/aggregations/compute.py +286 -0
- onetick/py/aggregations/functions.py +2216 -0
- onetick/py/aggregations/generic.py +104 -0
- onetick/py/aggregations/high_low.py +80 -0
- onetick/py/aggregations/num_distinct.py +83 -0
- onetick/py/aggregations/order_book.py +501 -0
- onetick/py/aggregations/other.py +1014 -0
- onetick/py/backports.py +26 -0
- onetick/py/cache.py +374 -0
- onetick/py/callback/__init__.py +5 -0
- onetick/py/callback/callback.py +276 -0
- onetick/py/callback/callbacks.py +131 -0
- onetick/py/compatibility.py +798 -0
- onetick/py/configuration.py +771 -0
- onetick/py/core/__init__.py +0 -0
- onetick/py/core/_csv_inspector.py +93 -0
- onetick/py/core/_internal/__init__.py +0 -0
- onetick/py/core/_internal/_manually_bound_value.py +6 -0
- onetick/py/core/_internal/_nodes_history.py +250 -0
- onetick/py/core/_internal/_op_utils/__init__.py +0 -0
- onetick/py/core/_internal/_op_utils/every_operand.py +9 -0
- onetick/py/core/_internal/_op_utils/is_const.py +10 -0
- onetick/py/core/_internal/_per_tick_scripts/tick_list_sort_template.script +121 -0
- onetick/py/core/_internal/_proxy_node.py +140 -0
- onetick/py/core/_internal/_state_objects.py +2312 -0
- onetick/py/core/_internal/_state_vars.py +93 -0
- onetick/py/core/_source/__init__.py +0 -0
- onetick/py/core/_source/_symbol_param.py +95 -0
- onetick/py/core/_source/schema.py +97 -0
- onetick/py/core/_source/source_methods/__init__.py +0 -0
- onetick/py/core/_source/source_methods/aggregations.py +809 -0
- onetick/py/core/_source/source_methods/applyers.py +296 -0
- onetick/py/core/_source/source_methods/columns.py +141 -0
- onetick/py/core/_source/source_methods/data_quality.py +301 -0
- onetick/py/core/_source/source_methods/debugs.py +272 -0
- onetick/py/core/_source/source_methods/drops.py +120 -0
- onetick/py/core/_source/source_methods/fields.py +619 -0
- onetick/py/core/_source/source_methods/filters.py +1002 -0
- onetick/py/core/_source/source_methods/joins.py +1413 -0
- onetick/py/core/_source/source_methods/merges.py +605 -0
- onetick/py/core/_source/source_methods/misc.py +1455 -0
- onetick/py/core/_source/source_methods/pandases.py +155 -0
- onetick/py/core/_source/source_methods/renames.py +356 -0
- onetick/py/core/_source/source_methods/sorts.py +183 -0
- onetick/py/core/_source/source_methods/switches.py +142 -0
- onetick/py/core/_source/source_methods/symbols.py +117 -0
- onetick/py/core/_source/source_methods/times.py +627 -0
- onetick/py/core/_source/source_methods/writes.py +986 -0
- onetick/py/core/_source/symbol.py +205 -0
- onetick/py/core/_source/tmp_otq.py +222 -0
- onetick/py/core/column.py +209 -0
- onetick/py/core/column_operations/__init__.py +0 -0
- onetick/py/core/column_operations/_methods/__init__.py +4 -0
- onetick/py/core/column_operations/_methods/_internal.py +28 -0
- onetick/py/core/column_operations/_methods/conversions.py +216 -0
- onetick/py/core/column_operations/_methods/methods.py +292 -0
- onetick/py/core/column_operations/_methods/op_types.py +160 -0
- onetick/py/core/column_operations/accessors/__init__.py +0 -0
- onetick/py/core/column_operations/accessors/_accessor.py +28 -0
- onetick/py/core/column_operations/accessors/decimal_accessor.py +104 -0
- onetick/py/core/column_operations/accessors/dt_accessor.py +537 -0
- onetick/py/core/column_operations/accessors/float_accessor.py +184 -0
- onetick/py/core/column_operations/accessors/str_accessor.py +1367 -0
- onetick/py/core/column_operations/base.py +1121 -0
- onetick/py/core/cut_builder.py +150 -0
- onetick/py/core/db_constants.py +20 -0
- onetick/py/core/eval_query.py +245 -0
- onetick/py/core/lambda_object.py +441 -0
- onetick/py/core/multi_output_source.py +232 -0
- onetick/py/core/per_tick_script.py +2256 -0
- onetick/py/core/query_inspector.py +464 -0
- onetick/py/core/source.py +1744 -0
- onetick/py/db/__init__.py +2 -0
- onetick/py/db/_inspection.py +1128 -0
- onetick/py/db/db.py +1327 -0
- onetick/py/db/utils.py +64 -0
- onetick/py/docs/__init__.py +0 -0
- onetick/py/docs/docstring_parser.py +112 -0
- onetick/py/docs/utils.py +81 -0
- onetick/py/functions.py +2398 -0
- onetick/py/license.py +190 -0
- onetick/py/log.py +88 -0
- onetick/py/math.py +935 -0
- onetick/py/misc.py +470 -0
- onetick/py/oqd/__init__.py +22 -0
- onetick/py/oqd/eps.py +1195 -0
- onetick/py/oqd/sources.py +325 -0
- onetick/py/otq.py +216 -0
- onetick/py/pyomd_mock.py +47 -0
- onetick/py/run.py +916 -0
- onetick/py/servers.py +173 -0
- onetick/py/session.py +1347 -0
- onetick/py/sources/__init__.py +19 -0
- onetick/py/sources/cache.py +167 -0
- onetick/py/sources/common.py +128 -0
- onetick/py/sources/csv.py +642 -0
- onetick/py/sources/custom.py +85 -0
- onetick/py/sources/data_file.py +305 -0
- onetick/py/sources/data_source.py +1045 -0
- onetick/py/sources/empty.py +94 -0
- onetick/py/sources/odbc.py +337 -0
- onetick/py/sources/order_book.py +271 -0
- onetick/py/sources/parquet.py +168 -0
- onetick/py/sources/pit.py +191 -0
- onetick/py/sources/query.py +495 -0
- onetick/py/sources/snapshots.py +419 -0
- onetick/py/sources/split_query_output_by_symbol.py +198 -0
- onetick/py/sources/symbology_mapping.py +123 -0
- onetick/py/sources/symbols.py +374 -0
- onetick/py/sources/ticks.py +825 -0
- onetick/py/sql.py +70 -0
- onetick/py/state.py +251 -0
- onetick/py/types.py +2131 -0
- onetick/py/utils/__init__.py +70 -0
- onetick/py/utils/acl.py +93 -0
- onetick/py/utils/config.py +186 -0
- onetick/py/utils/default.py +49 -0
- onetick/py/utils/file.py +38 -0
- onetick/py/utils/helpers.py +76 -0
- onetick/py/utils/locator.py +94 -0
- onetick/py/utils/perf.py +498 -0
- onetick/py/utils/query.py +49 -0
- onetick/py/utils/render.py +1374 -0
- onetick/py/utils/script.py +244 -0
- onetick/py/utils/temp.py +471 -0
- onetick/py/utils/types.py +120 -0
- onetick/py/utils/tz.py +84 -0
- onetick_py-1.177.0.dist-info/METADATA +137 -0
- onetick_py-1.177.0.dist-info/RECORD +152 -0
- onetick_py-1.177.0.dist-info/WHEEL +5 -0
- onetick_py-1.177.0.dist-info/entry_points.txt +2 -0
- onetick_py-1.177.0.dist-info/licenses/LICENSE +21 -0
- onetick_py-1.177.0.dist-info/top_level.txt +2 -0
onetick/py/utils/temp.py
ADDED
|
@@ -0,0 +1,471 @@
|
|
|
1
|
+
import abc
|
|
2
|
+
import datetime
|
|
3
|
+
import errno
|
|
4
|
+
import getpass
|
|
5
|
+
import os
|
|
6
|
+
import shutil
|
|
7
|
+
import tempfile
|
|
8
|
+
import weakref
|
|
9
|
+
from collections import defaultdict
|
|
10
|
+
from typing import Dict, List
|
|
11
|
+
from .types import default
|
|
12
|
+
|
|
13
|
+
import coolname
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
WEBAPI_TEST_MODE_SHARED_CONFIG = os.getenv('WEBAPI_TEST_MODE_SHARED_CONFIG')
|
|
17
|
+
if os.getenv('OTP_WEBAPI_TEST_MODE') and not WEBAPI_TEST_MODE_SHARED_CONFIG:
|
|
18
|
+
raise ValueError("WEBAPI_TEST_MODE_SHARED_CONFIG is not set, but it is required with OTP_WEBAPI_TEST_MODE.")
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def default_clean_up(clean_up):
|
|
22
|
+
from ..configuration import config
|
|
23
|
+
if clean_up is default:
|
|
24
|
+
return config.clean_up_tmp_files
|
|
25
|
+
return clean_up
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def get_logger(*args):
|
|
29
|
+
from .. import log
|
|
30
|
+
return log.get_logger(__name__, *args)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class CleanUpFinalizer:
|
|
34
|
+
"""
|
|
35
|
+
Class that manages destruction of the object,
|
|
36
|
+
setting up proper finalizer callback and clean_up boolean flag.
|
|
37
|
+
"""
|
|
38
|
+
|
|
39
|
+
def __init__(self, clean_up=default, *args):
|
|
40
|
+
clean_up = default_clean_up(clean_up)
|
|
41
|
+
# we need to use reference type here so finalizer can get the latest clean_up value,
|
|
42
|
+
# even if this value is changed after finalizer is created
|
|
43
|
+
# (because weakref.finalize can't use object's fields)
|
|
44
|
+
self._clean_up_ref = [clean_up]
|
|
45
|
+
self._finalizer = weakref.finalize(self, self._cleanup, self._clean_up_ref, *args)
|
|
46
|
+
|
|
47
|
+
@classmethod
|
|
48
|
+
def _cleanup(cls, clean_up_ref, *args):
|
|
49
|
+
raise NotImplementedError()
|
|
50
|
+
|
|
51
|
+
@property
|
|
52
|
+
def need_to_cleanup(self):
|
|
53
|
+
return self._clean_up_ref[0]
|
|
54
|
+
|
|
55
|
+
@need_to_cleanup.setter
|
|
56
|
+
def need_to_cleanup(self, value):
|
|
57
|
+
self._clean_up_ref[0] = value
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
class File(abc.ABC):
|
|
61
|
+
def __init__(self, path):
|
|
62
|
+
self._path = path
|
|
63
|
+
|
|
64
|
+
@property
|
|
65
|
+
def path(self):
|
|
66
|
+
return self._path
|
|
67
|
+
|
|
68
|
+
def __str__(self):
|
|
69
|
+
return self._path
|
|
70
|
+
|
|
71
|
+
def __fspath__(self):
|
|
72
|
+
"""
|
|
73
|
+
If children inherit os.PathLike, then this method is required and
|
|
74
|
+
makes files behave compatible with 'os' module
|
|
75
|
+
"""
|
|
76
|
+
return self._path
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def __name_generator():
|
|
80
|
+
"""
|
|
81
|
+
Returns tuple with initialized name generator and
|
|
82
|
+
the number of unique names this generator will produce
|
|
83
|
+
before it starts to repeat itself.
|
|
84
|
+
"""
|
|
85
|
+
def cool_gen():
|
|
86
|
+
while True:
|
|
87
|
+
yield coolname.generate_slug(2)
|
|
88
|
+
return cool_gen(), coolname.get_combinations_count(2)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
def mktemp(fun, dir, prefix='', suffix='', **kwargs):
|
|
92
|
+
names, max_unique_values = __name_generator()
|
|
93
|
+
for _ in range(max_unique_values):
|
|
94
|
+
name = next(names)
|
|
95
|
+
filename = os.path.join(dir, prefix + name + suffix)
|
|
96
|
+
try:
|
|
97
|
+
return fun(filename, **kwargs), filename
|
|
98
|
+
except FileExistsError:
|
|
99
|
+
continue
|
|
100
|
+
except PermissionError:
|
|
101
|
+
# This exception is thrown when a directory with the chosen name already exists on Windows
|
|
102
|
+
if os.name == 'nt' and os.path.isdir(dir) and os.access(dir, os.W_OK):
|
|
103
|
+
continue
|
|
104
|
+
else:
|
|
105
|
+
raise
|
|
106
|
+
raise FileExistsError(errno.EEXIST, 'No usable temporary file name found')
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def mkstemp(dir, prefix='', suffix=''):
|
|
110
|
+
flags = os.O_RDWR | os.O_CREAT | os.O_EXCL
|
|
111
|
+
if hasattr(os, 'O_NOFOLLOW'):
|
|
112
|
+
flags |= os.O_NOFOLLOW
|
|
113
|
+
return mktemp(os.open, dir, prefix, suffix, flags=flags, mode=0o600)
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def mkdtemp(dir, prefix='', suffix=''):
|
|
117
|
+
_, filename = mktemp(os.mkdir, dir, prefix, suffix, mode=0o700)
|
|
118
|
+
return filename
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
_TMP_CONFIGS_DIR_BASE = os.environ.get('OTP_BASE_FOLDER_FOR_GENERATED_RESOURCE',
|
|
122
|
+
os.path.join(tempfile.gettempdir(), "test_" + getpass.getuser()))
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class TmpFile(File, os.PathLike, CleanUpFinalizer):
|
|
126
|
+
|
|
127
|
+
ALL: Dict[str, List['TmpFile']] = defaultdict(list)
|
|
128
|
+
keep_everything_generated = False
|
|
129
|
+
|
|
130
|
+
def __init__(self, suffix="", name="", clean_up=default, force=False, base_dir=None):
|
|
131
|
+
"""
|
|
132
|
+
Class to create a temporary file.
|
|
133
|
+
By default, this file will be deleted automatically after all references to it are gone.
|
|
134
|
+
Base path where temporary files are created could be set using the ``ONE_TICK_TMP_DIR``.
|
|
135
|
+
By default they are created under the ``tempfile.gettempdir()`` folder.
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
Parameters
|
|
139
|
+
----------
|
|
140
|
+
name: str
|
|
141
|
+
name of the temporary file without suffix.
|
|
142
|
+
By default some random name will be generated.
|
|
143
|
+
suffix: str
|
|
144
|
+
suffix of the name of the temporary file.
|
|
145
|
+
clean_up: bool
|
|
146
|
+
Controls whether this temporary file will be deleted automatically
|
|
147
|
+
after all references to it are gone.
|
|
148
|
+
|
|
149
|
+
By default,
|
|
150
|
+
:py:attr:`otp.config.clean_up_tmp_files<onetick.py.configuration.Config.clean_up_tmp_files>` is used.
|
|
151
|
+
force: bool
|
|
152
|
+
Rewrite temporary file if it exists and parameter ``name`` is set.
|
|
153
|
+
base_dir: str
|
|
154
|
+
Absolute path of the directory where temporary file will be created.
|
|
155
|
+
|
|
156
|
+
See also
|
|
157
|
+
--------
|
|
158
|
+
|
|
159
|
+
The testing framework has a ``--keep-generated`` flag that controls clean up for all related instances
|
|
160
|
+
:ref:`onetick py test features`
|
|
161
|
+
"""
|
|
162
|
+
clean_up = default_clean_up(clean_up)
|
|
163
|
+
clean_up = clean_up and not TmpFile.keep_everything_generated
|
|
164
|
+
fd, self._path = self._create(clean_up, suffix=suffix, name=name, force=force, base_dir=base_dir)
|
|
165
|
+
# we only needed to create file, so closing opened file descriptor
|
|
166
|
+
os.close(fd)
|
|
167
|
+
self._logger = get_logger(self.__class__.__name__)
|
|
168
|
+
CleanUpFinalizer.__init__(self, clean_up, self.path, self._logger)
|
|
169
|
+
self._logger.debug(f'created {self._path}, clean_up={self.need_to_cleanup}')
|
|
170
|
+
|
|
171
|
+
def __repr__(self):
|
|
172
|
+
return f"TmpFile({self.path})"
|
|
173
|
+
|
|
174
|
+
@classmethod
|
|
175
|
+
def _cleanup(cls, clean_up_ref, path, logger):
|
|
176
|
+
if clean_up_ref[0] and os.path.exists(path):
|
|
177
|
+
logger.debug(f'removing {path}')
|
|
178
|
+
try:
|
|
179
|
+
os.remove(path)
|
|
180
|
+
except Exception:
|
|
181
|
+
# TODO: remove try-except block when BDS-116 will be fixed
|
|
182
|
+
pass
|
|
183
|
+
|
|
184
|
+
def _create(self, clean_up, suffix="", name="", force=False, base_dir=None):
|
|
185
|
+
|
|
186
|
+
self._parent_dir = None
|
|
187
|
+
|
|
188
|
+
if ONE_TICK_TMP_DIR() and not base_dir:
|
|
189
|
+
# allows to set a test name suffix for when we use it in
|
|
190
|
+
# tests with goal to distinguish tests content (configs, databases, etc)
|
|
191
|
+
# from each other
|
|
192
|
+
dir_path = ONE_TICK_TMP_DIR()
|
|
193
|
+
# save to access them from the teardown
|
|
194
|
+
TmpFile.ALL[dir_path].append(self)
|
|
195
|
+
|
|
196
|
+
if not os.path.exists(dir_path):
|
|
197
|
+
# If it does not exist, then lets create it. Note that it goes up
|
|
198
|
+
# recursively until finds an existing folder.
|
|
199
|
+
# We store it as a field of the instance to guarantee that it will
|
|
200
|
+
# be destroyed later than a temporary file.
|
|
201
|
+
self._parent_dir = GeneratedDir(dir_path, clean_up=clean_up)
|
|
202
|
+
elif dir_path in GeneratedDir.ALL:
|
|
203
|
+
self._parent_dir = GeneratedDir.ALL[dir_path]
|
|
204
|
+
else:
|
|
205
|
+
if base_dir and not os.path.exists(base_dir):
|
|
206
|
+
# If it does not exist, then lets create it. Note that it goes up
|
|
207
|
+
# recursively until finds an existing folder.
|
|
208
|
+
# We store it as a field of the instance to guarantee that it will
|
|
209
|
+
# be destroyed later than a temporary file.
|
|
210
|
+
self._parent_dir = GeneratedDir(base_dir, clean_up=clean_up)
|
|
211
|
+
dir_path = base_dir if base_dir else TMP_CONFIGS_DIR()
|
|
212
|
+
TmpFile.ALL[dir_path].append(self)
|
|
213
|
+
if dir_path in GeneratedDir.ALL:
|
|
214
|
+
self._parent_dir = GeneratedDir.ALL[dir_path]
|
|
215
|
+
|
|
216
|
+
# let know the parent generated folder to not destroy itself
|
|
217
|
+
if self._parent_dir is not None and not clean_up:
|
|
218
|
+
self._parent_dir.need_to_cleanup = False
|
|
219
|
+
|
|
220
|
+
if name:
|
|
221
|
+
path = os.path.normpath(os.path.join(dir_path, name)) + suffix
|
|
222
|
+
flags = os.O_RDWR | os.O_CREAT
|
|
223
|
+
if not force:
|
|
224
|
+
flags = flags | os.O_EXCL
|
|
225
|
+
if hasattr(os, 'O_NOFOLLOW'):
|
|
226
|
+
flags |= os.O_NOFOLLOW
|
|
227
|
+
mode = 0o600
|
|
228
|
+
if os.getenv('OTP_WEBAPI_TEST_MODE'):
|
|
229
|
+
mode = 0o644
|
|
230
|
+
return os.open(path, flags=flags, mode=mode), path
|
|
231
|
+
else:
|
|
232
|
+
return mkstemp(dir=dir_path, suffix=suffix, prefix="")
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
class TmpDir(os.PathLike, CleanUpFinalizer):
|
|
236
|
+
|
|
237
|
+
ALL: Dict[str, List['TmpDir']] = defaultdict(list)
|
|
238
|
+
keep_everything_generated = False
|
|
239
|
+
|
|
240
|
+
def __init__(self, rel_path="", *, suffix="", clean_up=default, base_dir=""):
|
|
241
|
+
"""
|
|
242
|
+
Class to create a temporary directory.
|
|
243
|
+
By default, this directory will be deleted automatically after all references to it are gone.
|
|
244
|
+
All files and directories under this one will be deleted too.
|
|
245
|
+
|
|
246
|
+
Base path where directories are created could be set using the ``ONE_TICK_TMP_DIR``.
|
|
247
|
+
By default they are created under the ``tempfile.gettempdir()`` folder.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
rel_path: str
|
|
252
|
+
relative path to the temporary directory.
|
|
253
|
+
If empty, then the name will be auto-generated.
|
|
254
|
+
suffix: str
|
|
255
|
+
suffix of the name of the temporary directory.
|
|
256
|
+
base_dir: str
|
|
257
|
+
relative path of the directory where temporary directory will be created.
|
|
258
|
+
clean_up: bool
|
|
259
|
+
Controls whether this temporary directory will be deleted automatically
|
|
260
|
+
after all references to it are gone.
|
|
261
|
+
|
|
262
|
+
By default,
|
|
263
|
+
:py:attr:`otp.config.clean_up_tmp_files<onetick.py.configuration.Config.clean_up_tmp_files>` is used.
|
|
264
|
+
|
|
265
|
+
See also
|
|
266
|
+
--------
|
|
267
|
+
|
|
268
|
+
The testing framework has a ``--keep-generated`` flag that controls clean up for all related instances
|
|
269
|
+
:ref:`onetick py test features`
|
|
270
|
+
"""
|
|
271
|
+
clean_up = default_clean_up(clean_up)
|
|
272
|
+
clean_up = clean_up and not TmpDir.keep_everything_generated
|
|
273
|
+
self._parent_dir = None
|
|
274
|
+
|
|
275
|
+
if os.path.isabs(rel_path):
|
|
276
|
+
raise ValueError("Absolute paths are not supported in 'rel_path' parameter.")
|
|
277
|
+
|
|
278
|
+
if ONE_TICK_TMP_DIR():
|
|
279
|
+
# allows to set a test name suffix for when we use it in
|
|
280
|
+
# tests with goal to distinguish tests content (configs, databases, etc)
|
|
281
|
+
# from each other
|
|
282
|
+
dir_path = os.path.normpath(os.path.join(ONE_TICK_TMP_DIR(), base_dir))
|
|
283
|
+
|
|
284
|
+
if not os.path.exists(dir_path):
|
|
285
|
+
# If it does not exist, then lets create it. Note that it goes up
|
|
286
|
+
# recursively until finds an existing folder.
|
|
287
|
+
# We store it as a field of the instance to guarantee that it will
|
|
288
|
+
# be destroyed later than a temporary file.
|
|
289
|
+
self._parent_dir = GeneratedDir(dir_path, clean_up=clean_up)
|
|
290
|
+
elif not clean_up:
|
|
291
|
+
if dir_path in GeneratedDir.ALL: # NOSONAR
|
|
292
|
+
# let know the parent generated folder to not destroy itself
|
|
293
|
+
GeneratedDir.ALL[dir_path].need_to_cleanup = False
|
|
294
|
+
|
|
295
|
+
self._parent_dir = GeneratedDir.ALL[dir_path]
|
|
296
|
+
|
|
297
|
+
else:
|
|
298
|
+
dir_path = os.path.normpath(os.path.join(TMP_CONFIGS_DIR(), base_dir))
|
|
299
|
+
|
|
300
|
+
if rel_path:
|
|
301
|
+
path = os.path.normpath(os.path.join(dir_path, rel_path)) + suffix
|
|
302
|
+
# dir_path should be the parent directory of path
|
|
303
|
+
dir_path = os.path.dirname(path)
|
|
304
|
+
for tmp_dir in TmpDir.ALL[dir_path]:
|
|
305
|
+
# check if path already exists
|
|
306
|
+
if tmp_dir.path == path:
|
|
307
|
+
self.path = tmp_dir.path
|
|
308
|
+
if not clean_up:
|
|
309
|
+
tmp_dir.need_to_cleanup = clean_up
|
|
310
|
+
return
|
|
311
|
+
|
|
312
|
+
os.mkdir(path, mode=0o700)
|
|
313
|
+
self.path = path
|
|
314
|
+
else:
|
|
315
|
+
self.path = mkdtemp(dir=dir_path, suffix=suffix, prefix="")
|
|
316
|
+
|
|
317
|
+
# save to access them in the teardown
|
|
318
|
+
TmpDir.ALL[dir_path].append(self)
|
|
319
|
+
|
|
320
|
+
self._logger = get_logger(self.__class__.__name__)
|
|
321
|
+
CleanUpFinalizer.__init__(self, clean_up, self.path, self._logger)
|
|
322
|
+
self._logger.debug(f'created {self.path}, clean_up={self.need_to_cleanup}')
|
|
323
|
+
|
|
324
|
+
@CleanUpFinalizer.need_to_cleanup.setter # type: ignore
|
|
325
|
+
def need_to_cleanup(self, value):
|
|
326
|
+
super(TmpDir, type(self)).need_to_cleanup.fset(self, value)
|
|
327
|
+
children = self.ALL[self.path]
|
|
328
|
+
for child in children:
|
|
329
|
+
child.need_to_cleanup = value
|
|
330
|
+
|
|
331
|
+
@classmethod
|
|
332
|
+
def _cleanup(cls, clean_up_ref, path, logger):
|
|
333
|
+
if clean_up_ref[0]:
|
|
334
|
+
logger.debug(f'removing {path}')
|
|
335
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
336
|
+
|
|
337
|
+
def __str__(self):
|
|
338
|
+
return self.path
|
|
339
|
+
|
|
340
|
+
def __repr__(self):
|
|
341
|
+
return f"TmpDir({self.path})"
|
|
342
|
+
|
|
343
|
+
def __fspath__(self):
|
|
344
|
+
return self.path
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
class GeneratedDir(os.PathLike, CleanUpFinalizer):
|
|
348
|
+
|
|
349
|
+
ALL: Dict[str, 'GeneratedDir'] = {} # store all created dir with goal to set cleanup=False if it requires
|
|
350
|
+
keep_everything_generated = False
|
|
351
|
+
|
|
352
|
+
def __init__(self, dir_path, clean_up=default):
|
|
353
|
+
"""
|
|
354
|
+
Class to create generated temporary directory.
|
|
355
|
+
By default, this directory will be deleted automatically after all references to it are gone.
|
|
356
|
+
All files and directories under this one will be deleted too.
|
|
357
|
+
|
|
358
|
+
The main difference from TmpDir class is that also this directory's parent directory will be deleted!
|
|
359
|
+
|
|
360
|
+
The exact path to this directory will be: ``dir_path``.
|
|
361
|
+
|
|
362
|
+
Parameters
|
|
363
|
+
----------
|
|
364
|
+
dir_path: str
|
|
365
|
+
path to the temporary directory.
|
|
366
|
+
clean_up: bool
|
|
367
|
+
Controls whether this temporary directory will be deleted automatically
|
|
368
|
+
after all references to it are gone.
|
|
369
|
+
|
|
370
|
+
By default,
|
|
371
|
+
:py:attr:`otp.config.clean_up_tmp_files<onetick.py.configuration.Config.clean_up_tmp_files>` is used.
|
|
372
|
+
"""
|
|
373
|
+
|
|
374
|
+
parent = os.path.normpath(os.path.dirname(dir_path))
|
|
375
|
+
|
|
376
|
+
clean_up = default_clean_up(clean_up)
|
|
377
|
+
clean_up = clean_up and not GeneratedDir.keep_everything_generated
|
|
378
|
+
|
|
379
|
+
self._parent_dir = None
|
|
380
|
+
if not os.path.exists(parent):
|
|
381
|
+
# Do the same thing for the parent folder.
|
|
382
|
+
# Save it as a field to guarantee that child folder is destroyed
|
|
383
|
+
# earlier than parent
|
|
384
|
+
self._parent_dir = GeneratedDir(parent, clean_up=clean_up)
|
|
385
|
+
elif not clean_up:
|
|
386
|
+
if parent in GeneratedDir.ALL: # NOSONAR
|
|
387
|
+
GeneratedDir.ALL[parent].need_to_cleanup = False
|
|
388
|
+
|
|
389
|
+
if self._parent_dir is None and parent in GeneratedDir.ALL:
|
|
390
|
+
self._parent_dir = GeneratedDir.ALL[parent]
|
|
391
|
+
|
|
392
|
+
self.path = dir_path
|
|
393
|
+
|
|
394
|
+
if os.path.exists(dir_path):
|
|
395
|
+
# Do not create and even handle it, because it has been created externally.
|
|
396
|
+
# It mgiht happen in the concurrent runs.
|
|
397
|
+
return
|
|
398
|
+
|
|
399
|
+
# We need to set exist_ok=True, because it still can be created externally in the concurrent runs
|
|
400
|
+
os.makedirs(dir_path, exist_ok=True)
|
|
401
|
+
|
|
402
|
+
self._logger = get_logger(self.__class__.__name__)
|
|
403
|
+
CleanUpFinalizer.__init__(self, clean_up, self.path, self._logger)
|
|
404
|
+
self._logger.debug(f'created {self.path}, clean_up={self.need_to_cleanup}')
|
|
405
|
+
|
|
406
|
+
GeneratedDir.ALL[dir_path] = self
|
|
407
|
+
|
|
408
|
+
@CleanUpFinalizer.need_to_cleanup.setter # type: ignore
|
|
409
|
+
def need_to_cleanup(self, value):
|
|
410
|
+
super(GeneratedDir, type(self)).need_to_cleanup.fset(self, value)
|
|
411
|
+
|
|
412
|
+
if self._parent_dir:
|
|
413
|
+
self._parent_dir.need_to_cleanup = value
|
|
414
|
+
|
|
415
|
+
@classmethod
|
|
416
|
+
def _cleanup(cls, clean_up_ref, path, logger):
|
|
417
|
+
if clean_up_ref[0]:
|
|
418
|
+
logger.debug(f'removing {path}')
|
|
419
|
+
shutil.rmtree(path, ignore_errors=True)
|
|
420
|
+
|
|
421
|
+
def __str__(self):
|
|
422
|
+
return self.path
|
|
423
|
+
|
|
424
|
+
def __repr__(self):
|
|
425
|
+
return f"GeneratedDir({self.path})"
|
|
426
|
+
|
|
427
|
+
def __fspath__(self):
|
|
428
|
+
return self.path
|
|
429
|
+
|
|
430
|
+
|
|
431
|
+
# ----------------------------------------------------
|
|
432
|
+
# We set TMP_CONFIGS_DIR to a function, that returns
|
|
433
|
+
# only the first result.
|
|
434
|
+
# We need to support multiprocessing environment, where
|
|
435
|
+
# every process should use it's own base temporary folder
|
|
436
|
+
# but generate it only when something is required, because
|
|
437
|
+
# otherwise parent process could construct it with its pid
|
|
438
|
+
# and other processes would use the same location.
|
|
439
|
+
|
|
440
|
+
|
|
441
|
+
def _gen_root_path_():
|
|
442
|
+
generated = []
|
|
443
|
+
|
|
444
|
+
def _inner_():
|
|
445
|
+
if not generated:
|
|
446
|
+
generated.append(
|
|
447
|
+
GeneratedDir(
|
|
448
|
+
os.path.join(
|
|
449
|
+
_TMP_CONFIGS_DIR_BASE,
|
|
450
|
+
f'run_{datetime.datetime.now().strftime("%Y%m%d_%H%M%S")}_{os.getpid()}',
|
|
451
|
+
)
|
|
452
|
+
).path
|
|
453
|
+
)
|
|
454
|
+
return generated[0]
|
|
455
|
+
|
|
456
|
+
return _inner_
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
TMP_CONFIGS_DIR = _gen_root_path_()
|
|
460
|
+
def ONE_TICK_TMP_DIR(): # noqa # NOSONAR
|
|
461
|
+
if os.getenv('OTP_WEBAPI_TEST_MODE'):
|
|
462
|
+
return WEBAPI_TEST_MODE_SHARED_CONFIG
|
|
463
|
+
if 'ONE_TICK_TMP_DIR' not in os.environ:
|
|
464
|
+
return None
|
|
465
|
+
return os.path.normpath(os.path.join(TMP_CONFIGS_DIR(), os.environ['ONE_TICK_TMP_DIR']))
|
|
466
|
+
# ----------------------------------------------------
|
|
467
|
+
|
|
468
|
+
|
|
469
|
+
class PermanentFile(File):
|
|
470
|
+
def copy(self):
|
|
471
|
+
return TmpFile(self._path)
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
def get_type_that_includes(types):
|
|
2
|
+
import onetick.py.types as ott
|
|
3
|
+
|
|
4
|
+
def merge_two(type1, type2):
|
|
5
|
+
type_changed = False
|
|
6
|
+
|
|
7
|
+
b_type1, b_type2 = ott.get_base_type(type1), ott.get_base_type(type2)
|
|
8
|
+
if b_type1 != b_type2:
|
|
9
|
+
if {b_type1, b_type2} == {int, float}:
|
|
10
|
+
dtype = float
|
|
11
|
+
elif {b_type1, b_type2} == {ott.decimal, float} or {b_type1, b_type2} == {ott.decimal, int}:
|
|
12
|
+
dtype = ott.decimal
|
|
13
|
+
elif {b_type1, b_type2} == {ott.nsectime, ott.msectime}:
|
|
14
|
+
dtype = ott.nsectime
|
|
15
|
+
else:
|
|
16
|
+
raise ValueError(f"Incompatible types: {type1}, {type2}")
|
|
17
|
+
|
|
18
|
+
type_changed = True
|
|
19
|
+
elif issubclass(b_type1, str):
|
|
20
|
+
t1_length = ott.string.DEFAULT_LENGTH if type1 is str or type1.length is None else type1.length
|
|
21
|
+
t2_length = ott.string.DEFAULT_LENGTH if type2 is str or type2.length is None else type2.length
|
|
22
|
+
|
|
23
|
+
if t1_length is Ellipsis or t2_length is Ellipsis:
|
|
24
|
+
dtype = ott.varstring
|
|
25
|
+
else:
|
|
26
|
+
dtype = type2 if t1_length < t2_length else type1
|
|
27
|
+
|
|
28
|
+
if t1_length != t2_length:
|
|
29
|
+
type_changed = True # TODO: test
|
|
30
|
+
|
|
31
|
+
else:
|
|
32
|
+
dtype = type1
|
|
33
|
+
|
|
34
|
+
return dtype, type_changed
|
|
35
|
+
|
|
36
|
+
dtype = types[0]
|
|
37
|
+
type_changed = False
|
|
38
|
+
|
|
39
|
+
for other_dtype in types[1:]:
|
|
40
|
+
dtype, new_type_change = merge_two(dtype, other_dtype)
|
|
41
|
+
type_changed |= new_type_change
|
|
42
|
+
|
|
43
|
+
return dtype, type_changed
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
class adaptive:
|
|
47
|
+
"""
|
|
48
|
+
This class is mostly used as the default value for the functions' parameters
|
|
49
|
+
when the value of ``None`` has some other meaning
|
|
50
|
+
or when the meaning of the parameter depends on the other parameter's values,
|
|
51
|
+
:ref:`otp.config <static/configuration/root:configuration>` options or the context.
|
|
52
|
+
|
|
53
|
+
Examples
|
|
54
|
+
--------
|
|
55
|
+
|
|
56
|
+
For example, setting :py:class:`~onetick.py.DataSource` ``symbols`` parameter
|
|
57
|
+
to ``otp.adaptive`` allows to set symbols when running the query later.
|
|
58
|
+
|
|
59
|
+
>>> data = otp.DataSource('SOME_DB', tick_type='TT', symbols=otp.adaptive)
|
|
60
|
+
>>> otp.run(data, symbols='S1')
|
|
61
|
+
Time X
|
|
62
|
+
0 2003-12-01 00:00:00.000 1
|
|
63
|
+
1 2003-12-01 00:00:00.001 2
|
|
64
|
+
2 2003-12-01 00:00:00.002 3
|
|
65
|
+
|
|
66
|
+
This is the default value of ``symbols`` parameter, so omitting it also works:
|
|
67
|
+
|
|
68
|
+
>>> data = otp.DataSource('SOME_DB', tick_type='TT')
|
|
69
|
+
>>> otp.run(data, symbols='S1')
|
|
70
|
+
Time X
|
|
71
|
+
0 2003-12-01 00:00:00.000 1
|
|
72
|
+
1 2003-12-01 00:00:00.001 2
|
|
73
|
+
2 2003-12-01 00:00:00.002 3
|
|
74
|
+
"""
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class adaptive_to_default(adaptive):
|
|
78
|
+
"""
|
|
79
|
+
If something is not specified and can not be deduced, then use the
|
|
80
|
+
default one
|
|
81
|
+
"""
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class default:
|
|
85
|
+
"""
|
|
86
|
+
Used when you need to specify a default without evaluating it (e.g. default timezone)
|
|
87
|
+
"""
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
class range:
|
|
91
|
+
"""
|
|
92
|
+
Class that expresses OneTick ranges.
|
|
93
|
+
For example, if you want to express a range in the .split() method,
|
|
94
|
+
then you can use this range.
|
|
95
|
+
|
|
96
|
+
It has start and stop fields that allow you to define a range.
|
|
97
|
+
|
|
98
|
+
See also
|
|
99
|
+
--------
|
|
100
|
+
:py:meth:`~onetick.py.Source.split`.
|
|
101
|
+
|
|
102
|
+
Examples
|
|
103
|
+
--------
|
|
104
|
+
>>> data = otp.Ticks(X=[0.33, -5.1, otp.nan, 9.4])
|
|
105
|
+
>>> r1, r2, r3 = data.split(data['X'], [otp.nan, otp.range(0, 100)], default=True)
|
|
106
|
+
>>> otp.run(r1)
|
|
107
|
+
Time X
|
|
108
|
+
0 2003-12-01 00:00:00.002 NaN
|
|
109
|
+
>>> otp.run(r2)
|
|
110
|
+
Time X
|
|
111
|
+
0 2003-12-01 00:00:00.000 0.33
|
|
112
|
+
1 2003-12-01 00:00:00.003 9.40
|
|
113
|
+
>>> otp.run(r3)
|
|
114
|
+
Time X
|
|
115
|
+
0 2003-12-01 00:00:00.001 -5.1
|
|
116
|
+
"""
|
|
117
|
+
|
|
118
|
+
def __init__(self, start, stop):
|
|
119
|
+
self.start = start
|
|
120
|
+
self.stop = stop
|
onetick/py/utils/tz.py
ADDED
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import datetime
|
|
2
|
+
import sys
|
|
3
|
+
import warnings
|
|
4
|
+
|
|
5
|
+
from typing import Optional
|
|
6
|
+
from contextlib import suppress
|
|
7
|
+
|
|
8
|
+
import dateutil.tz
|
|
9
|
+
import tzlocal
|
|
10
|
+
|
|
11
|
+
import onetick.py as otp
|
|
12
|
+
from onetick.py.backports import zoneinfo
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_tzfile_by_name(timezone):
|
|
16
|
+
if isinstance(timezone, str):
|
|
17
|
+
try:
|
|
18
|
+
timezone = zoneinfo.ZoneInfo(timezone)
|
|
19
|
+
except zoneinfo.ZoneInfoNotFoundError:
|
|
20
|
+
timezone = dateutil.tz.gettz(timezone)
|
|
21
|
+
return timezone
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_local_timezone_name():
|
|
25
|
+
tz = tzlocal.get_localzone()
|
|
26
|
+
try:
|
|
27
|
+
return tz.zone # type: ignore
|
|
28
|
+
except AttributeError:
|
|
29
|
+
return tz.key # type: ignore
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_timezone_from_datetime(dt) -> Optional[str]:
|
|
33
|
+
tzinfo = getattr(dt, 'tzinfo', None)
|
|
34
|
+
if tzinfo is None:
|
|
35
|
+
return None
|
|
36
|
+
if tzinfo is datetime.timezone.utc:
|
|
37
|
+
return 'UTC'
|
|
38
|
+
with suppress(ModuleNotFoundError):
|
|
39
|
+
import pytz
|
|
40
|
+
if isinstance(tzinfo, pytz.BaseTzInfo):
|
|
41
|
+
return tzinfo.zone
|
|
42
|
+
if isinstance(tzinfo, zoneinfo.ZoneInfo):
|
|
43
|
+
return tzinfo.key
|
|
44
|
+
if isinstance(tzinfo, dateutil.tz.tzlocal):
|
|
45
|
+
return get_local_timezone_name()
|
|
46
|
+
if isinstance(tzinfo, dateutil.tz.tzstr) and hasattr(tzinfo, '_s'):
|
|
47
|
+
return tzinfo._s
|
|
48
|
+
if isinstance(tzinfo, dateutil.tz.tzfile):
|
|
49
|
+
if sys.platform == 'win32':
|
|
50
|
+
warnings.warn(
|
|
51
|
+
"It's not recommended to use dateutil.tz timezones on Windows platform. "
|
|
52
|
+
"Function 'get_timezone_from_datetime' can't guarantee correct results in this case. "
|
|
53
|
+
"Please, use zoneinfo timezones instead."
|
|
54
|
+
)
|
|
55
|
+
if hasattr(tzinfo, '_filename'):
|
|
56
|
+
if tzinfo._filename == '/etc/localtime':
|
|
57
|
+
return get_local_timezone_name()
|
|
58
|
+
for timezone in zoneinfo.available_timezones():
|
|
59
|
+
if tzinfo._filename.endswith(timezone):
|
|
60
|
+
return timezone
|
|
61
|
+
if sys.platform == 'win32':
|
|
62
|
+
if isinstance(tzinfo, dateutil.tz.win.tzwin) and hasattr(tzinfo, '_name'):
|
|
63
|
+
return tzinfo._name
|
|
64
|
+
raise ValueError(f"Can't get timezone name from datetime '{dt}' with tzinfo {tzinfo}")
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
def convert_timezone(dt, src_timezone, dest_timezone) -> datetime.datetime:
|
|
68
|
+
"""
|
|
69
|
+
Converting timezone-naive ``dt`` object localized in ``src_timezone`` timezone
|
|
70
|
+
to the specified ``dest_timezone`` and returning also timezone-naive object.
|
|
71
|
+
"""
|
|
72
|
+
if src_timezone is None:
|
|
73
|
+
src_timezone = get_local_timezone_name()
|
|
74
|
+
# using pandas, because stdlib datetime has some bug around epoch on Windows
|
|
75
|
+
dt = otp.datetime(dt).ts
|
|
76
|
+
# change timezone-naive to timezone-aware
|
|
77
|
+
dt = dt.tz_localize(src_timezone)
|
|
78
|
+
# convert timezone
|
|
79
|
+
dt = dt.tz_convert(dest_timezone)
|
|
80
|
+
# change timezone-aware to timezone-naive
|
|
81
|
+
dt = dt.tz_localize(None)
|
|
82
|
+
# convert to datetime
|
|
83
|
+
dt = dt.to_pydatetime()
|
|
84
|
+
return dt
|