onetick-py 1.162.2__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 +266 -0
  4. locator_parser/common.py +365 -0
  5. locator_parser/io.py +41 -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 +280 -0
  12. onetick/lib/__init__.py +4 -0
  13. onetick/lib/instance.py +138 -0
  14. onetick/py/__init__.py +290 -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 +645 -0
  19. onetick/py/aggregations/_docs.py +912 -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 +427 -0
  26. onetick/py/aggregations/other.py +1014 -0
  27. onetick/py/backports.py +26 -0
  28. onetick/py/cache.py +373 -0
  29. onetick/py/callback/__init__.py +5 -0
  30. onetick/py/callback/callback.py +275 -0
  31. onetick/py/callback/callbacks.py +131 -0
  32. onetick/py/compatibility.py +752 -0
  33. onetick/py/configuration.py +736 -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 +2307 -0
  45. onetick/py/core/_internal/_state_vars.py +87 -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 +810 -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 +270 -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 +1001 -0
  58. onetick/py/core/_source/source_methods/joins.py +1393 -0
  59. onetick/py/core/_source/source_methods/merges.py +566 -0
  60. onetick/py/core/_source/source_methods/misc.py +1325 -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 +702 -0
  68. onetick/py/core/_source/symbol.py +202 -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 +215 -0
  75. onetick/py/core/column_operations/_methods/methods.py +294 -0
  76. onetick/py/core/column_operations/_methods/op_types.py +150 -0
  77. onetick/py/core/column_operations/accessors/__init__.py +0 -0
  78. onetick/py/core/column_operations/accessors/_accessor.py +30 -0
  79. onetick/py/core/column_operations/accessors/decimal_accessor.py +92 -0
  80. onetick/py/core/column_operations/accessors/dt_accessor.py +464 -0
  81. onetick/py/core/column_operations/accessors/float_accessor.py +160 -0
  82. onetick/py/core/column_operations/accessors/str_accessor.py +1374 -0
  83. onetick/py/core/column_operations/base.py +1061 -0
  84. onetick/py/core/cut_builder.py +149 -0
  85. onetick/py/core/db_constants.py +20 -0
  86. onetick/py/core/eval_query.py +244 -0
  87. onetick/py/core/lambda_object.py +442 -0
  88. onetick/py/core/multi_output_source.py +193 -0
  89. onetick/py/core/per_tick_script.py +2253 -0
  90. onetick/py/core/query_inspector.py +465 -0
  91. onetick/py/core/source.py +1663 -0
  92. onetick/py/db/__init__.py +2 -0
  93. onetick/py/db/_inspection.py +1042 -0
  94. onetick/py/db/db.py +1423 -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 +2354 -0
  100. onetick/py/license.py +188 -0
  101. onetick/py/log.py +88 -0
  102. onetick/py/math.py +947 -0
  103. onetick/py/misc.py +437 -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 +211 -0
  108. onetick/py/pyomd_mock.py +47 -0
  109. onetick/py/run.py +841 -0
  110. onetick/py/servers.py +173 -0
  111. onetick/py/session.py +1342 -0
  112. onetick/py/sources/__init__.py +19 -0
  113. onetick/py/sources/cache.py +167 -0
  114. onetick/py/sources/common.py +126 -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 +1049 -0
  119. onetick/py/sources/empty.py +94 -0
  120. onetick/py/sources/odbc.py +337 -0
  121. onetick/py/sources/order_book.py +238 -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 +357 -0
  129. onetick/py/sources/ticks.py +825 -0
  130. onetick/py/sql.py +70 -0
  131. onetick/py/state.py +256 -0
  132. onetick/py/types.py +2056 -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 +499 -0
  141. onetick/py/utils/query.py +49 -0
  142. onetick/py/utils/render.py +1139 -0
  143. onetick/py/utils/script.py +244 -0
  144. onetick/py/utils/temp.py +471 -0
  145. onetick/py/utils/types.py +118 -0
  146. onetick/py/utils/tz.py +82 -0
  147. onetick_py-1.162.2.dist-info/METADATA +148 -0
  148. onetick_py-1.162.2.dist-info/RECORD +152 -0
  149. onetick_py-1.162.2.dist-info/WHEEL +5 -0
  150. onetick_py-1.162.2.dist-info/entry_points.txt +2 -0
  151. onetick_py-1.162.2.dist-info/licenses/LICENSE +21 -0
  152. onetick_py-1.162.2.dist-info/top_level.txt +2 -0
@@ -0,0 +1,465 @@
1
+ import os
2
+ import re
3
+ import io
4
+
5
+ from collections import defaultdict
6
+
7
+ from onetick.py.utils import abspath_to_query_by_name
8
+
9
+ MULTIPLE_NAMESAKE_PARAMETERS_ALLOWED = frozenset(
10
+ [
11
+ "BIND_SECURITY",
12
+ # "PARAMETER" isn't on the list as it's handled separately
13
+ ]
14
+ )
15
+
16
+
17
+ class QueryNotFoundError(FileNotFoundError):
18
+ pass
19
+
20
+
21
+ class UncertainQueryName(ValueError):
22
+ pass
23
+
24
+
25
+ def get_queries(otq_path):
26
+ result = []
27
+ with open(otq_path, "r") as fin:
28
+ for line in fin:
29
+ line = line.strip()
30
+
31
+ res = re.match(r"^\[(.*)\]$", line)
32
+ if res:
33
+ name = res.group(1)
34
+ if name != "_meta":
35
+ result.append(name)
36
+
37
+ return result
38
+
39
+
40
+ def is_commented_out(node):
41
+ return hasattr(node, "COMMENTED_OUT") and (node.COMMENTED_OUT == "1")
42
+
43
+
44
+ def get_query_info(otq_path, query_name="", inspected_graphs=None):
45
+ """
46
+ Collects some meta information about an .otq-based query.
47
+
48
+ Parameters
49
+ ----------
50
+ otq_path: str
51
+ Absolute path to an OTQ file.
52
+ query_name: str
53
+ Name of the query to analyze; if it's not specified, the .otq file should have only one query.
54
+ inspected_graphs: None or Dict[tuple of (str, str) : Graph]
55
+ Usually should be kept as the default value (None).
56
+
57
+ Returns
58
+ -------
59
+
60
+ Graph - Instance of an internal class. Contains the following fields:
61
+ nodes: list of Node
62
+ Nodes in the query.
63
+ sources: list of Node
64
+ Nodes without incoming links.
65
+ sinks: list of Node
66
+ Nodes without outcoming links.
67
+ nested_inputs: list of Node
68
+ Nodes with nested inputs.
69
+ nested_outputs: list of Node
70
+ Nodes with nested outputs.
71
+ has_unbound_sources: bool
72
+ Whether the query needs an unbound security list to be provided.
73
+ """
74
+ if inspected_graphs is None:
75
+ inspected_graphs = {}
76
+
77
+ if (otq_path, query_name) in inspected_graphs:
78
+ return inspected_graphs[(otq_path, query_name)]
79
+
80
+ parsed = False
81
+ found = False
82
+
83
+ class Node:
84
+ def __init__(self):
85
+ self.SINK = []
86
+ self.SOURCE = []
87
+
88
+ def _get_ep_name_and_stack_info(self):
89
+ if not hasattr(self, 'EP'):
90
+ return None, None
91
+ # self.EP can be multiline, so using re.DOTALL for dot to match newline too
92
+ res = re.match(r"([A-Z_]+?)(\((.+)\))?$", self.EP, flags=re.DOTALL)
93
+ if not res:
94
+ return None, None
95
+ ep_name = res.group(1)
96
+ ep_parameters = res.group(3)
97
+ stack_info = None
98
+ if ep_parameters and 'STACK_INFO' in ep_parameters:
99
+ res = re.match(r'.*STACK_INFO="?(-?\d+)', ep_parameters)
100
+ if res:
101
+ stack_info = res.group(1)
102
+ return ep_name, stack_info
103
+
104
+ graph = defaultdict(lambda: Node())
105
+
106
+ if not os.path.exists(otq_path):
107
+ raise FileNotFoundError(f'otq "{otq_path}" is not found')
108
+
109
+ if not otq_path.endswith('.py'):
110
+
111
+ multiline_ep = False
112
+ multiline_value = ''
113
+
114
+ with open(otq_path, "r") as fin:
115
+ for line in fin:
116
+ line = line.strip()
117
+
118
+ if line.startswith("[") and line.endswith("]"):
119
+ if ("[" + query_name + "]") == line:
120
+ found = True
121
+ continue
122
+ else:
123
+ if line != "[_meta]" and query_name == "":
124
+ if found is True:
125
+ raise UncertainQueryName(
126
+ "No query name was passed, implying the file has only one query; it has more"
127
+ )
128
+ else:
129
+ found = True
130
+ continue
131
+
132
+ if (found and not parsed) or multiline_ep:
133
+ if line.startswith("TYPE = "):
134
+ parsed = True
135
+ continue
136
+
137
+ if not multiline_ep:
138
+ res = re.match(r"NODE_?(\d+)_?(.*?)\s?=\s?(.*)", line) # NOSONAR
139
+
140
+ if res:
141
+ num = int(res.group(1))
142
+ param = res.group(2)
143
+ value = res.group(3)
144
+ else:
145
+ res = re.match(r"ROOT_?(.*?)\s?=\s?(.*)", line)
146
+
147
+ if res:
148
+ num = 0
149
+ param = res.group(1)
150
+ value = res.group(2)
151
+
152
+ if res and line.endswith('\\'):
153
+ # first line of multiline
154
+ multiline_ep = True
155
+ multiline_value += value.replace('\\', '\n')
156
+ continue
157
+ else:
158
+ # next line of multiline
159
+ multiline_value += line.replace('\\', '\n')
160
+ if line.endswith('\\'):
161
+ continue
162
+ # last line of multiline
163
+ value = multiline_value
164
+ multiline_ep = False
165
+ multiline_value = ''
166
+
167
+ if res:
168
+ param, value = param.strip(), value.strip()
169
+
170
+ if param == "PARAMETER":
171
+ if not hasattr(graph[num], "PARAMETERS"):
172
+ setattr(graph[num], "PARAMETERS", {})
173
+ bits = value.split(" ")
174
+ inner_param_name = bits[0]
175
+ inner_param_value = " ".join(bits[1:])
176
+ graph[num].PARAMETERS[inner_param_name] = inner_param_value
177
+ continue
178
+
179
+ if param == "SOURCE" or param == "SINK":
180
+ sources = []
181
+
182
+ for v in value.split():
183
+ if v == "ROOT":
184
+ sources.append(0)
185
+ else:
186
+ res = re.match(r"NODE_?(\d+)(\.(.*))*", v)
187
+ sources.append(int(res.group(1)))
188
+
189
+ value = sources
190
+ elif param == "":
191
+ param = "EP"
192
+
193
+ if param in MULTIPLE_NAMESAKE_PARAMETERS_ALLOWED:
194
+ # A node can have several parameters with the same name
195
+ if not hasattr(graph[num], param):
196
+ setattr(graph[num], param, [])
197
+
198
+ getattr(graph[num], param).append(value)
199
+ else:
200
+ setattr(graph[num], param, value)
201
+
202
+ # with
203
+
204
+ if not found:
205
+ raise QueryNotFoundError(f'Query "{query_name}" is not found in the {otq_path}')
206
+
207
+ for num, node in graph.items():
208
+ setattr(node, "NUM", num)
209
+ setattr(node, "IS_NESTED", False)
210
+
211
+ if hasattr(node, "NESTED_INPUT"):
212
+ node.NESTED_INPUT = node.NESTED_INPUT.split()[0]
213
+
214
+ if hasattr(node, "NESTED_OUTPUT"):
215
+ node.NESTED_OUTPUT = node.NESTED_OUTPUT.split()[0]
216
+
217
+ for src in node.SOURCE:
218
+ if num not in graph[src].SINK:
219
+ graph[src].SINK.append(num)
220
+
221
+ for src in node.SINK:
222
+ if num not in graph[src].SOURCE:
223
+ graph[src].SOURCE.append(num)
224
+
225
+ if not is_commented_out(node):
226
+ # Commented nodes can affect sources and sinks, but cannot anything else
227
+ if node.EP.startswith("NESTED_OTQ"):
228
+ setattr(node, "IS_NESTED", True)
229
+ if node.EP.strip() == "NESTED_OTQ":
230
+ if "OTQ_PATH" in node.PARAMETERS:
231
+ address = node.PARAMETERS["OTQ_PATH"]
232
+ elif "OTQ_NODE" in node.PARAMETERS:
233
+ address = node.PARAMETERS["OTQ_NODE"]
234
+ elif hasattr(node, "PH_PATH"):
235
+ address = node.PH_PATH
236
+
237
+ node.EP = "NESTED_OTQ " + address
238
+
239
+ setattr(node, "NESTED_GRAPH", _load_nested_query(otq_path, node.EP, inspected_graphs))
240
+
241
+ class Graph:
242
+ def __init__(self, nodes):
243
+ self.nodes = nodes
244
+ self.sources = [node for _, node in graph.items() if len(node.SOURCE) == 0]
245
+ self.sinks = [node for _, node in graph.items() if len(node.SINK) == 0]
246
+
247
+ self.nested_inputs = [node for node in self.sources if hasattr(node, "NESTED_INPUT")]
248
+ self.nested_outputs = [node for node in self.sinks if hasattr(node, "NESTED_OUTPUT")]
249
+
250
+ inspected_graphs[(otq_path, query_name)] = self
251
+
252
+ self.has_unbound_sources = self._check_for_unbound_sources()
253
+
254
+ def _check_for_unbound_sources(self, bound_symbol_info=None):
255
+ if bound_symbol_info is None:
256
+ bound_symbol_info = {}
257
+
258
+ for node in self.sources:
259
+ self._search_for_bound_sink(node.NUM, bound_symbol_info)
260
+
261
+ has_unbound_sources = any((not bound_symbol_info[node.NUM]) for node in self.sources)
262
+ return has_unbound_sources
263
+
264
+ def _search_for_bound_sink(self, root, bound_symbol_info):
265
+ if root in bound_symbol_info:
266
+ return bound_symbol_info[root]
267
+
268
+ if not is_commented_out(self.nodes[root]):
269
+ # A commented node cannot have its own bound symbols, but can have a bound sink
270
+ if hasattr(self.nodes[root], "BIND_SECURITY"):
271
+ for bound_security in self.nodes[root].BIND_SECURITY:
272
+ if (
273
+ ("_SYMBOL_NAME" not in bound_security)
274
+ and ("_SYMBOL_PARAM" not in bound_security)
275
+ and (not bound_security.endswith("No"))
276
+ ):
277
+ # The first two define dependency on lower-bound symbol;
278
+ # The last is whether the bound security is not unchecked in the list
279
+
280
+ bound_symbol_info[root] = True
281
+ break
282
+
283
+ if self.nodes[root].IS_NESTED:
284
+ nested = self.nodes[root].NESTED_GRAPH
285
+ if not nested.has_unbound_sources:
286
+ bound_symbol_info[root] = True
287
+
288
+ if root not in bound_symbol_info:
289
+ for node in self.nodes[root].SINK:
290
+ if node not in bound_symbol_info:
291
+ self._search_for_bound_sink(node, bound_symbol_info)
292
+
293
+ if bound_symbol_info.get(node, None) is True:
294
+ bound_symbol_info[root] = True
295
+ break
296
+
297
+ if root not in bound_symbol_info:
298
+ bound_symbol_info[root] = False
299
+
300
+ def has_unbound_if_pinned(self, in_pins):
301
+ """
302
+ Parameters
303
+ ----------
304
+ in_pins: dict(str: bool)
305
+ mapping of pins to whether the respective source needs unbound symbols
306
+
307
+ Returns
308
+ -------
309
+ True if query with pins bound to graphs with described boundness state would need unbound symbols.
310
+ """
311
+
312
+ if not self.has_unbound_sources:
313
+ # All sources are bound inside the query graph.
314
+ return False
315
+
316
+ in_pins_to_nodes = {node.NESTED_INPUT: node for node in self.nested_inputs}
317
+
318
+ bound_symbol_info = {}
319
+
320
+ for pin, needs_unbound in in_pins.items():
321
+ if not needs_unbound:
322
+ bound_symbol_info[in_pins_to_nodes[pin].NUM] = True
323
+
324
+ return self._check_for_unbound_sources(bound_symbol_info)
325
+
326
+ return Graph(graph)
327
+
328
+
329
+ def _load_nested_query(nesting_file, nested_address, inspected_graphs):
330
+ nested_address = nested_address.split(" ")[1]
331
+ address_bits = nested_address.split("::")
332
+
333
+ if len(address_bits) == 1:
334
+ # Only the path is given, no query name
335
+ # E.g. utils/FindSymbols.otq
336
+ query_file = address_bits[0]
337
+ query = ""
338
+ elif address_bits[-1].endswith(".otq"):
339
+ # remote://DB::utils/FindSymbols.otq
340
+ query_file = address_bits[-1]
341
+ query = ""
342
+ else:
343
+ # remote://DB::utils/FindSymbols.otq::Find
344
+ query_file, query = address_bits[-2:]
345
+
346
+ if query_file == "___ME___":
347
+ otq_path = nesting_file
348
+ else:
349
+ otq_path = abspath_to_query_by_name(query_file)
350
+
351
+ nested_graph = get_query_info(otq_path, query, inspected_graphs)
352
+ return nested_graph
353
+
354
+
355
+ def add_pins(otq_path, query_name, specification):
356
+ """
357
+ The function takes a query and adds pins to the query according to the
358
+ specification, and saves it back to the original file.
359
+
360
+ Parameters
361
+ ----------
362
+ otq_path: str
363
+ Absolute path to an OTQ file.
364
+ query_name: str
365
+ Name of the query to analyze
366
+ specification: List[tuple]
367
+ List of 3-values tuples, where the first value node from the `get_query_info` function,
368
+ the second one is marker, the third one is name.
369
+ A marker values: 1 for input, and 0 for output.
370
+
371
+ Returns
372
+ -------
373
+ Nothing
374
+
375
+ Raises
376
+ ------
377
+ FileNotFoundError
378
+ """
379
+ if not os.path.exists(otq_path):
380
+ raise FileNotFoundError(f'otq "{otq_path}" is not found')
381
+
382
+ found = False
383
+ out = io.StringIO()
384
+
385
+ with open(otq_path, "r") as fin:
386
+
387
+ multiline_ep = False
388
+
389
+ for line in fin:
390
+ line = line.strip()
391
+
392
+ if line.startswith("[") and line.endswith("]"):
393
+ if ("[" + query_name + "]") == line:
394
+ found = True
395
+
396
+ out.write(line + "\n")
397
+
398
+ if found or multiline_ep:
399
+ if not multiline_ep:
400
+ res = re.match(r"NODE_?(\d+)_?(.*?)\s?=\s?(.*)", line) # NOSONAR
401
+
402
+ if res:
403
+ num = int(res.group(1))
404
+ else:
405
+ res = re.match(r"ROOT_?(.*?)\s?=\s?(.*)", line)
406
+
407
+ if res:
408
+ num = 0
409
+
410
+ if res and line.endswith('\\'):
411
+ # first line of multiline
412
+ multiline_ep = True
413
+ continue
414
+ else:
415
+ if line.endswith('\\'):
416
+ continue
417
+ multiline_ep = False
418
+
419
+ if res:
420
+ for inx, v in enumerate(specification):
421
+ node, pin_flag, pin_name = v
422
+ if node.NUM == num:
423
+ if pin_flag is None:
424
+ continue
425
+ node_name = "NODE_" + str(num)
426
+ if num == 0:
427
+ node_name = "ROOT"
428
+
429
+ pin_id = "NESTED_INPUT" if pin_flag == 1 else "NESTED_OUTPUT"
430
+
431
+ out.write(node_name + "_" + pin_id + " = " + pin_name + "\n")
432
+
433
+ # don't use this specification item anymore
434
+ specification[inx] = (node, None, None)
435
+ # with
436
+
437
+ with open(otq_path, "w") as fout:
438
+ fout.write(out.getvalue())
439
+
440
+
441
+ def get_query_parameter_list(otq_path, query_name):
442
+ """Returns a list of query parameter names; can be used for nesting to pass the parameters to the nested query"""
443
+ if not os.path.exists(otq_path):
444
+ raise FileNotFoundError(f'otq "{otq_path}" is not found')
445
+
446
+ found = False
447
+ param_list = []
448
+
449
+ with open(otq_path, "r") as fin:
450
+ for line in fin:
451
+ line = line.strip()
452
+
453
+ if line.startswith("[") and line.endswith("]"):
454
+ if ("[" + query_name + "]") == line:
455
+ found = True
456
+ else:
457
+ found = False
458
+
459
+ if found:
460
+ params = re.findall(r'\$(\w+|(\{(\w+)\}))', line)
461
+ if params:
462
+ param_list.extend([p[2] if p[2] != '' else p[0] for p in params])
463
+
464
+ param_list.sort()
465
+ return param_list