dsgrid-toolkit 0.3.3__cp313-cp313-win_amd64.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 (157) hide show
  1. build_backend.py +93 -0
  2. dsgrid/__init__.py +22 -0
  3. dsgrid/api/__init__.py +0 -0
  4. dsgrid/api/api_manager.py +179 -0
  5. dsgrid/api/app.py +419 -0
  6. dsgrid/api/models.py +60 -0
  7. dsgrid/api/response_models.py +116 -0
  8. dsgrid/apps/__init__.py +0 -0
  9. dsgrid/apps/project_viewer/app.py +216 -0
  10. dsgrid/apps/registration_gui.py +444 -0
  11. dsgrid/chronify.py +32 -0
  12. dsgrid/cli/__init__.py +0 -0
  13. dsgrid/cli/common.py +120 -0
  14. dsgrid/cli/config.py +176 -0
  15. dsgrid/cli/download.py +13 -0
  16. dsgrid/cli/dsgrid.py +157 -0
  17. dsgrid/cli/dsgrid_admin.py +92 -0
  18. dsgrid/cli/install_notebooks.py +62 -0
  19. dsgrid/cli/query.py +729 -0
  20. dsgrid/cli/registry.py +1862 -0
  21. dsgrid/cloud/__init__.py +0 -0
  22. dsgrid/cloud/cloud_storage_interface.py +140 -0
  23. dsgrid/cloud/factory.py +31 -0
  24. dsgrid/cloud/fake_storage_interface.py +37 -0
  25. dsgrid/cloud/s3_storage_interface.py +156 -0
  26. dsgrid/common.py +36 -0
  27. dsgrid/config/__init__.py +0 -0
  28. dsgrid/config/annual_time_dimension_config.py +194 -0
  29. dsgrid/config/common.py +142 -0
  30. dsgrid/config/config_base.py +148 -0
  31. dsgrid/config/dataset_config.py +907 -0
  32. dsgrid/config/dataset_schema_handler_factory.py +46 -0
  33. dsgrid/config/date_time_dimension_config.py +136 -0
  34. dsgrid/config/dimension_config.py +54 -0
  35. dsgrid/config/dimension_config_factory.py +65 -0
  36. dsgrid/config/dimension_mapping_base.py +350 -0
  37. dsgrid/config/dimension_mappings_config.py +48 -0
  38. dsgrid/config/dimensions.py +1025 -0
  39. dsgrid/config/dimensions_config.py +71 -0
  40. dsgrid/config/file_schema.py +190 -0
  41. dsgrid/config/index_time_dimension_config.py +80 -0
  42. dsgrid/config/input_dataset_requirements.py +31 -0
  43. dsgrid/config/mapping_tables.py +209 -0
  44. dsgrid/config/noop_time_dimension_config.py +42 -0
  45. dsgrid/config/project_config.py +1462 -0
  46. dsgrid/config/registration_models.py +188 -0
  47. dsgrid/config/representative_period_time_dimension_config.py +194 -0
  48. dsgrid/config/simple_models.py +49 -0
  49. dsgrid/config/supplemental_dimension.py +29 -0
  50. dsgrid/config/time_dimension_base_config.py +192 -0
  51. dsgrid/data_models.py +155 -0
  52. dsgrid/dataset/__init__.py +0 -0
  53. dsgrid/dataset/dataset.py +123 -0
  54. dsgrid/dataset/dataset_expression_handler.py +86 -0
  55. dsgrid/dataset/dataset_mapping_manager.py +121 -0
  56. dsgrid/dataset/dataset_schema_handler_base.py +945 -0
  57. dsgrid/dataset/dataset_schema_handler_one_table.py +209 -0
  58. dsgrid/dataset/dataset_schema_handler_two_table.py +322 -0
  59. dsgrid/dataset/growth_rates.py +162 -0
  60. dsgrid/dataset/models.py +51 -0
  61. dsgrid/dataset/table_format_handler_base.py +257 -0
  62. dsgrid/dataset/table_format_handler_factory.py +17 -0
  63. dsgrid/dataset/unpivoted_table.py +121 -0
  64. dsgrid/dimension/__init__.py +0 -0
  65. dsgrid/dimension/base_models.py +230 -0
  66. dsgrid/dimension/dimension_filters.py +308 -0
  67. dsgrid/dimension/standard.py +252 -0
  68. dsgrid/dimension/time.py +352 -0
  69. dsgrid/dimension/time_utils.py +103 -0
  70. dsgrid/dsgrid_rc.py +88 -0
  71. dsgrid/exceptions.py +105 -0
  72. dsgrid/filesystem/__init__.py +0 -0
  73. dsgrid/filesystem/cloud_filesystem.py +32 -0
  74. dsgrid/filesystem/factory.py +32 -0
  75. dsgrid/filesystem/filesystem_interface.py +136 -0
  76. dsgrid/filesystem/local_filesystem.py +74 -0
  77. dsgrid/filesystem/s3_filesystem.py +118 -0
  78. dsgrid/loggers.py +132 -0
  79. dsgrid/minimal_patterns.cp313-win_amd64.pyd +0 -0
  80. dsgrid/notebooks/connect_to_dsgrid_registry.ipynb +949 -0
  81. dsgrid/notebooks/registration.ipynb +48 -0
  82. dsgrid/notebooks/start_notebook.sh +11 -0
  83. dsgrid/project.py +451 -0
  84. dsgrid/query/__init__.py +0 -0
  85. dsgrid/query/dataset_mapping_plan.py +142 -0
  86. dsgrid/query/derived_dataset.py +388 -0
  87. dsgrid/query/models.py +728 -0
  88. dsgrid/query/query_context.py +287 -0
  89. dsgrid/query/query_submitter.py +994 -0
  90. dsgrid/query/report_factory.py +19 -0
  91. dsgrid/query/report_peak_load.py +70 -0
  92. dsgrid/query/reports_base.py +20 -0
  93. dsgrid/registry/__init__.py +0 -0
  94. dsgrid/registry/bulk_register.py +165 -0
  95. dsgrid/registry/common.py +287 -0
  96. dsgrid/registry/config_update_checker_base.py +63 -0
  97. dsgrid/registry/data_store_factory.py +34 -0
  98. dsgrid/registry/data_store_interface.py +74 -0
  99. dsgrid/registry/dataset_config_generator.py +158 -0
  100. dsgrid/registry/dataset_registry_manager.py +950 -0
  101. dsgrid/registry/dataset_update_checker.py +16 -0
  102. dsgrid/registry/dimension_mapping_registry_manager.py +575 -0
  103. dsgrid/registry/dimension_mapping_update_checker.py +16 -0
  104. dsgrid/registry/dimension_registry_manager.py +413 -0
  105. dsgrid/registry/dimension_update_checker.py +16 -0
  106. dsgrid/registry/duckdb_data_store.py +207 -0
  107. dsgrid/registry/filesystem_data_store.py +150 -0
  108. dsgrid/registry/filter_registry_manager.py +123 -0
  109. dsgrid/registry/project_config_generator.py +57 -0
  110. dsgrid/registry/project_registry_manager.py +1623 -0
  111. dsgrid/registry/project_update_checker.py +48 -0
  112. dsgrid/registry/registration_context.py +223 -0
  113. dsgrid/registry/registry_auto_updater.py +316 -0
  114. dsgrid/registry/registry_database.py +667 -0
  115. dsgrid/registry/registry_interface.py +446 -0
  116. dsgrid/registry/registry_manager.py +558 -0
  117. dsgrid/registry/registry_manager_base.py +367 -0
  118. dsgrid/registry/versioning.py +92 -0
  119. dsgrid/rust_ext/__init__.py +14 -0
  120. dsgrid/rust_ext/find_minimal_patterns.py +129 -0
  121. dsgrid/spark/__init__.py +0 -0
  122. dsgrid/spark/functions.py +589 -0
  123. dsgrid/spark/types.py +110 -0
  124. dsgrid/tests/__init__.py +0 -0
  125. dsgrid/tests/common.py +140 -0
  126. dsgrid/tests/make_us_data_registry.py +265 -0
  127. dsgrid/tests/register_derived_datasets.py +103 -0
  128. dsgrid/tests/utils.py +25 -0
  129. dsgrid/time/__init__.py +0 -0
  130. dsgrid/time/time_conversions.py +80 -0
  131. dsgrid/time/types.py +67 -0
  132. dsgrid/units/__init__.py +0 -0
  133. dsgrid/units/constants.py +113 -0
  134. dsgrid/units/convert.py +71 -0
  135. dsgrid/units/energy.py +145 -0
  136. dsgrid/units/power.py +87 -0
  137. dsgrid/utils/__init__.py +0 -0
  138. dsgrid/utils/dataset.py +830 -0
  139. dsgrid/utils/files.py +179 -0
  140. dsgrid/utils/filters.py +125 -0
  141. dsgrid/utils/id_remappings.py +100 -0
  142. dsgrid/utils/py_expression_eval/LICENSE +19 -0
  143. dsgrid/utils/py_expression_eval/README.md +8 -0
  144. dsgrid/utils/py_expression_eval/__init__.py +847 -0
  145. dsgrid/utils/py_expression_eval/tests.py +283 -0
  146. dsgrid/utils/run_command.py +70 -0
  147. dsgrid/utils/scratch_dir_context.py +65 -0
  148. dsgrid/utils/spark.py +918 -0
  149. dsgrid/utils/spark_partition.py +98 -0
  150. dsgrid/utils/timing.py +239 -0
  151. dsgrid/utils/utilities.py +221 -0
  152. dsgrid/utils/versioning.py +36 -0
  153. dsgrid_toolkit-0.3.3.dist-info/METADATA +193 -0
  154. dsgrid_toolkit-0.3.3.dist-info/RECORD +157 -0
  155. dsgrid_toolkit-0.3.3.dist-info/WHEEL +4 -0
  156. dsgrid_toolkit-0.3.3.dist-info/entry_points.txt +4 -0
  157. dsgrid_toolkit-0.3.3.dist-info/licenses/LICENSE +29 -0
@@ -0,0 +1,283 @@
1
+ #! /usr/bin/env python
2
+ # -*- coding: utf-8 -*-
3
+ # Author: AxiaCore S.A.S. http://axiacore.com
4
+ #
5
+ # Based on js-expression-eval, by Matthew Crumley (email@matthewcrumley.com, http://silentmatt.com/)
6
+ # https://github.com/silentmatt/js-expression-eval
7
+ #
8
+ # Ported to Python and modified by Vera Mazhuga (ctrl-alt-delete@live.com, http://vero4ka.info/)
9
+ #
10
+ # You are free to use and modify this code in anyway you find useful. Please leave this comment in the code
11
+ # to acknowledge its original source. If you feel like it, I enjoy hearing about projects that use my code,
12
+ # but don't feel like you have to let me know or ask permission.
13
+
14
+ import unittest
15
+
16
+ from dsgrid.utils.py_expression_eval import Parser
17
+
18
+
19
+ class ParserTestCase(unittest.TestCase):
20
+ def setUp(self):
21
+ self.parser = Parser()
22
+
23
+ def assertExactEqual(self, a, b):
24
+ self.assertEqual(type(a), type(b))
25
+ self.assertEqual(a, b)
26
+
27
+ def test_parser(self):
28
+ parser = Parser()
29
+ # parser and variables
30
+ self.assertEqual(parser.parse('pow(x,y)').variables(), ['x', 'y'])
31
+ self.assertEqual(parser.parse('pow(x,y)').symbols(), ['pow', 'x', 'y'])
32
+
33
+ # but '"a b"' can *not* be used as a variable
34
+ self.assertEqual(parser.parse('"a b"*2').evaluate({'"a b"': 2}), "a ba b")
35
+ # unless parse configured to allow double quoted variables (i.e. allow multi-word vars)
36
+ parser2 = Parser(string_literal_quotes=("'")) # only single, not double!
37
+ self.assertEqual(parser2.parse('"a b"*2').evaluate({'"a b"':2}),4)
38
+
39
+ # evaluate
40
+ self.assertExactEqual(parser.parse('1').evaluate({}), 1)
41
+ self.assertExactEqual(parser.parse('a').evaluate({'a': 2}), 2)
42
+ self.assertExactEqual(parser.parse('2 * 3').evaluate({}), 6)
43
+ self.assertExactEqual(parser.parse(u'2 \u2219 3').evaluate({}), 6)
44
+ self.assertExactEqual(parser.parse(u'2 \u2022 3').evaluate({}), 6)
45
+ self.assertExactEqual(parser.parse('2 ^ x').evaluate({'x': 3}), 8)
46
+ self.assertExactEqual(parser.parse('2 ** x').evaluate({'x': 3}), 8)
47
+ self.assertExactEqual(parser.parse('-1.E2 ** x + 2.0E2').evaluate({'x': 1}), 100.0)
48
+ self.assertEqual(parser.parse('x < 3').evaluate({'x': 3}), False)
49
+ self.assertEqual(parser.parse('x < 3').evaluate({'x': 2}), True)
50
+ self.assertEqual(parser.parse('x <= 3').evaluate({'x': 3}), True)
51
+ self.assertEqual(parser.parse('x <= 3').evaluate({'x': 4}), False)
52
+ self.assertEqual(parser.parse('x > 3').evaluate({'x': 4}), True)
53
+ self.assertEqual(parser.parse('x >= 3').evaluate({'x': 3}), True)
54
+ self.assertExactEqual(parser.parse('2 * x + 1').evaluate({'x': 3}), 7)
55
+ self.assertExactEqual(parser.parse('2 + 3 * x').evaluate({'x': 4}), 14)
56
+ self.assertExactEqual(parser.parse('(2 + 3) * x').evaluate({'x': 4}), 20)
57
+ self.assertExactEqual(parser.parse('2-3.0^x').evaluate({'x': 4}), -79.0)
58
+ self.assertExactEqual(parser.parse('-2-3.0^x').evaluate({'x': 4}), -83.0)
59
+ self.assertExactEqual(parser.parse('-3^x').evaluate({'x': 4}), -81)
60
+ self.assertExactEqual(parser.parse('(-3)^x').evaluate({'x': 4}), 81)
61
+ self.assertExactEqual(parser.parse('2-3**x').evaluate({'x': 4}), -79)
62
+ self.assertExactEqual(parser.parse('-2-3**x').evaluate({'x': 4}), -83)
63
+ self.assertExactEqual(parser.parse('-3.0**x').evaluate({'x': 4}), -81.0)
64
+ self.assertExactEqual(parser.parse('(-3.0)**x').evaluate({'x': 4}), 81.0)
65
+ self.assertExactEqual(parser.parse('2*x + y').evaluate({'x': 4, 'y': 1}), 9)
66
+ self.assertEqual(parser.parse("x||y").evaluate({'x': 'hello ', 'y': 'world'}), 'hello world')
67
+ self.assertEqual(parser.parse("'x'||'y'").evaluate({}), 'xy')
68
+ self.assertEqual(parser.parse("'x'=='x'").evaluate({}), True)
69
+ self.assertEqual(parser.parse("(a+b)==c").evaluate({'a': 1, 'b': 2, 'c': 3}), True)
70
+ self.assertEqual(parser.parse("(a+b)!=c").evaluate({'a': 1, 'b': 2, 'c': 3}), False)
71
+ self.assertEqual(parser.parse("(a^2-b^2)==((a+b)*(a-b))").evaluate({'a': 4859, 'b': 13150}), True)
72
+ self.assertEqual(parser.parse("(a^2-b^2+1)==((a+b)*(a-b))").evaluate({'a': 4859, 'b': 13150}), False)
73
+ self.assertEqual(parser.parse("(a**2-b**2)==((a+b)*(a-b))").evaluate({'a': 4859, 'b': 13150}), True)
74
+ self.assertEqual(parser.parse("(a**2-b**2+1)==((a+b)*(a-b))").evaluate({'a': 4859, 'b': 13150}), False)
75
+ self.assertExactEqual(parser.parse("x/((x+y))").simplify({}).evaluate({'x': 1, 'y': 1}), 0.5)
76
+ self.assertExactEqual(parser.parse('origin+2.0').evaluate({'origin': 1.0}), 3.0)
77
+ assert parser.parse('a | b').evaluate({'a': set([1, 2]), 'b': set([3, 4])}) == set([1, 2, 3, 4])
78
+ assert parser.parse('a | b').evaluate({'a': True, 'b': False})
79
+
80
+ # logical expressions
81
+ self.assertExactEqual(parser.parse('a and b').evaluate({'a': True, 'b': False}), False)
82
+ self.assertExactEqual(parser.parse('a and not b').evaluate({'a': True, 'b': False}), True)
83
+ self.assertExactEqual(parser.parse('a or b').evaluate({'a': True, 'b': False}), True)
84
+ self.assertExactEqual(parser.parse('a xor b').evaluate({'a': True, 'b': True}), False)
85
+
86
+ # check precedents: AND should evaluate before OR
87
+ self.assertExactEqual(parser.parse('a or b and not a').evaluate({'a': True, 'b': False}), True)
88
+
89
+ # in operations
90
+ self.assertExactEqual(parser.parse('"ab" in ("ab", "cd")').evaluate({}), True)
91
+ self.assertExactEqual(parser.parse('"ee" in ("ab", "cd")').evaluate({}), False)
92
+ self.assertExactEqual(parser.parse('1 in (1, 2, 3)').evaluate({}), True)
93
+ self.assertExactEqual(parser.parse('"ab" in ("ab", "cd") and 1 in (1,2,3)').evaluate({}), True)
94
+ self.assertExactEqual(parser.parse('"word" in "word in sentence"').evaluate({}), True)
95
+
96
+ # functions
97
+ self.assertExactEqual(parser.parse('pyt(2 , 0)').evaluate({}), 2.0)
98
+ self.assertEqual(parser.parse("concat('Hello',' ','world')").evaluate({}), 'Hello world')
99
+ self.assertExactEqual(parser.parse('if(a>b,5,6)').evaluate({'a': 8, 'b': 3}), 5)
100
+ self.assertExactEqual(parser.parse('if(a,b,c)').evaluate({'a': None, 'b': 1, 'c': 3}), 3)
101
+ self.assertExactEqual(parser.parse('if(random(1)>1,1,0)').evaluate({}), 0)
102
+
103
+ # log with base or natural log
104
+ self.assertExactEqual(parser.parse('log(16,2)').evaluate({}), 4.0)
105
+ self.assertExactEqual(parser.parse('log(E^100)').evaluate({}), 100.0)
106
+ self.assertExactEqual(parser.parse('log(E**100)').evaluate({}), 100.0)
107
+
108
+ # test substitute
109
+ expr = parser.parse('2 * x + 1')
110
+ expr2 = expr.substitute('x', '4 * x') # ((2*(4*x))+1)
111
+ self.assertExactEqual(expr2.evaluate({'x': 3}), 25)
112
+
113
+ # test simplify
114
+ expr = parser.parse('x * (y * atan(1))').simplify({'y': 4})
115
+ self.assertIn('x*3.141592', expr.toString())
116
+ self.assertExactEqual(expr.evaluate({'x': 2}), 6.283185307179586)
117
+
118
+ # test toString with string constant
119
+ expr = parser.parse("'a'=='b'")
120
+ self.assertIn("'a'=='b'", expr.toString())
121
+ self.assertIn("'a'=='b'", "%s" % expr)
122
+ expr = parser.parse("concat('a\n','\n','\rb')=='a\n\n\rb'")
123
+ self.assertEqual(expr.evaluate({}), True)
124
+ expr = parser.parse("a==''")
125
+ self.assertEqual(expr.evaluate({'a': ''}), True)
126
+
127
+ # test toString with an external function
128
+ expr = parser.parse("myExtFn(a,b,c,1.51,'ok')")
129
+ self.assertEqual(expr.substitute("a", 'first').toString(), "myExtFn(first,b,c,1.51,'ok')")
130
+
131
+ # test variables
132
+ expr = parser.parse('x * (y * atan(1))')
133
+ self.assertEqual(expr.variables(), ['x', 'y'])
134
+ self.assertEqual(expr.simplify({'y': 4}).variables(), ['x'])
135
+
136
+ # list operations
137
+ self.assertEqual(parser.parse('a, 3').evaluate({'a': [1, 2]}), [1, 2, 3])
138
+
139
+ def test_consts(self):
140
+ # self.assertEqual(self.parser.parse("PI ").variables(), [""])
141
+ self.assertEqual(self.parser.parse("PI").variables(), [])
142
+ self.assertEqual(self.parser.parse("PI ").variables(), [])
143
+ self.assertEqual(self.parser.parse("E ").variables(), [])
144
+ self.assertEqual(self.parser.parse(" E").variables(), [])
145
+ self.assertEqual(self.parser.parse("E").variables(), [])
146
+ self.assertEqual(self.parser.parse("E+1").variables(), [])
147
+ self.assertEqual(self.parser.parse("E / 1").variables(), [])
148
+ self.assertEqual(self.parser.parse("sin(PI)+E").variables(), [])
149
+
150
+ def test_parsing_e_and_pi(self):
151
+ self.assertEqual(self.parser.parse('Pie').variables(), ["Pie"])
152
+ self.assertEqual(self.parser.parse('PIe').variables(), ["PIe"])
153
+ self.assertEqual(self.parser.parse('Eval').variables(), ["Eval"])
154
+ self.assertEqual(self.parser.parse('Eval1').variables(), ["Eval1"])
155
+ self.assertEqual(self.parser.parse('EPI').variables(), ["EPI"])
156
+ self.assertEqual(self.parser.parse('PIE').variables(), ["PIE"])
157
+ self.assertEqual(self.parser.parse('Engage').variables(), ["Engage"])
158
+ self.assertEqual(self.parser.parse('Engage * PIE').variables(), ["Engage", "PIE"])
159
+ self.assertEqual(self.parser.parse('Engage_').variables(), ["Engage_"])
160
+ self.assertEqual(self.parser.parse('Engage1').variables(), ["Engage1"])
161
+ self.assertEqual(self.parser.parse('E1').variables(), ["E1"])
162
+ self.assertEqual(self.parser.parse('PI2').variables(), ["PI2"])
163
+ self.assertEqual(self.parser.parse('(E1 + PI)').variables(), ["E1"])
164
+ self.assertEqual(self.parser.parse('E1_').variables(), ["E1_"])
165
+ self.assertEqual(self.parser.parse('E_').variables(), ["E_"])
166
+
167
+ def test_evaluating_consts(self):
168
+ self.assertExactEqual(self.parser.evaluate("Engage1", variables={"Engage1": 2}), 2)
169
+ self.assertExactEqual(self.parser.evaluate("Engage1 + 1", variables={"Engage1": 1}), 2)
170
+
171
+ def test_custom_functions(self):
172
+ parser = Parser()
173
+
174
+ def testFunction0():
175
+ return 13
176
+
177
+ def testFunction1(a):
178
+ return 2 * a + 9
179
+
180
+ def testFunction2(a, b):
181
+ return 2 * a + 3 * b
182
+
183
+ # zero argument functions don't currently work
184
+ # self.assertEqual(parser
185
+ # .parse('testFunction()')
186
+ # .evaluate({"testFunction":testFunction0}),13)
187
+ self.assertExactEqual(parser
188
+ .parse('testFunction(x)')
189
+ .evaluate({"x": 2, "testFunction": testFunction1}), 13)
190
+ self.assertExactEqual(parser
191
+ .parse('testFunction(x , y)')
192
+ .evaluate({"x": 2, "y": 3, "testFunction": testFunction2}), 13)
193
+
194
+ # Add some "built-in" functions
195
+ def mean(*xs):
196
+ return sum(xs) / len(xs)
197
+
198
+ parser.functions['mean'] = mean
199
+
200
+ def counter(initial):
201
+ class nonlocals:
202
+ x = initial
203
+
204
+ def count(increment):
205
+ nonlocals.x += increment
206
+ return nonlocals.x
207
+
208
+ return count
209
+
210
+ parser.functions['count'] = counter(0)
211
+
212
+ self.assertEqual(parser.parse("mean(xs)").variables(), ["xs"])
213
+ self.assertEqual(parser.parse("mean(xs)").symbols(), ["mean", "xs"])
214
+ self.assertEqual(parser.evaluate("mean(xs)", variables={"xs": [1, 2, 3]}), 2)
215
+ self.assertExactEqual(parser.evaluate("count(num)", variables={"num": 5}), 5)
216
+ self.assertExactEqual(parser.evaluate("count(num)", variables={"num": 5}), 10)
217
+
218
+ def test_custom_functions_with_inline_strings(self):
219
+ parser = Parser()
220
+ expr = parser.parse("func(1, \"func(2, 4)\")")
221
+ self.assertEqual(expr.variables(), ['func'])
222
+
223
+ expr = parser.parse("func(1, 'func(2, 4)')")
224
+ self.assertEqual(expr.variables(), ['func'])
225
+
226
+ parser2 = Parser(string_literal_quotes=("'"))
227
+ expr = parser2.parse("func(1, \"func(2, 4)\")")
228
+ self.assertEqual(expr.variables(), ['func', "\"func(2, 4)\""])
229
+
230
+ expr = parser2.parse("func(1, 'func(2, 4)')")
231
+ self.assertEqual(expr.variables(), ['func'])
232
+
233
+ def test_custom_functions_substitute_strings(self):
234
+ def func(var, str):
235
+ if str == "custom text":
236
+ return 1
237
+ if str == "foo":
238
+ return 2
239
+ return 0
240
+
241
+ parser = Parser()
242
+ expr = parser.parse("func(1, \"custom text\")")
243
+ self.assertEqual(expr.evaluate({"func": func}), 1)
244
+
245
+ parser = Parser(string_literal_quotes=("'"))
246
+ expr = parser.parse("func(1, \"custom text\")")
247
+ self.assertEqual(expr.evaluate({"func": func, "\"custom text\"": "foo" }), 2)
248
+
249
+ def test_decimals(self):
250
+ parser = Parser()
251
+
252
+ self.assertExactEqual(parser.parse(".1").evaluate({}), parser.parse("0.1").evaluate({}))
253
+ self.assertExactEqual(parser.parse(".1*.2").evaluate({}), parser.parse("0.1*0.2").evaluate({}))
254
+ self.assertExactEqual(parser.parse(".5^3").evaluate({}), float(0.125))
255
+ self.assertExactEqual(parser.parse("16^.5").evaluate({}), 4.0)
256
+ self.assertExactEqual(parser.parse(".5**3").evaluate({}), float(0.125))
257
+ self.assertExactEqual(parser.parse("16**.5").evaluate({}), 4.0)
258
+ self.assertExactEqual(parser.parse("8300*.8").evaluate({}), 6640.0)
259
+ self.assertExactEqual(parser.parse("1E3*2.0").evaluate({}), 2000.0)
260
+ self.assertExactEqual(parser.parse("-1e3*2.0").evaluate({}), -2000.0)
261
+ self.assertExactEqual(parser.parse("-1E3*2.E2").evaluate({}), -200000.0)
262
+
263
+ with self.assertRaises(ValueError):
264
+ parser.parse("..5").evaluate({})
265
+
266
+ def test_hexadecimal(self):
267
+ parser = Parser()
268
+
269
+ self.assertExactEqual(parser.parse("0x4000").evaluate({}), 0x4000)
270
+ self.assertExactEqual(parser.parse("0x4000").evaluate({}), 16384)
271
+ self.assertExactEqual(parser.parse("0x3FF").evaluate({}), 0x3FF)
272
+ self.assertExactEqual(parser.parse("0x1000 * 2").evaluate({}), 0x2000)
273
+ self.assertExactEqual(parser.parse("1000 * 0x2").evaluate({}), 2000)
274
+ self.assertExactEqual(parser.parse("0x3F + 0x10 * 0x20 + 32").evaluate({}), 0x25F)
275
+
276
+ def test_to_string(self):
277
+ parser = Parser()
278
+
279
+ self.assertEqual(parser.parse("-12 * a + -2").toString(), '(((-12)*a)+(-2))')
280
+
281
+
282
+ if __name__ == '__main__':
283
+ unittest.main()
@@ -0,0 +1,70 @@
1
+ """Contains helper functions to run commands as subprocesses."""
2
+
3
+ import logging
4
+ import shlex
5
+ import subprocess
6
+ import sys
7
+
8
+ from dsgrid.exceptions import DSGRuntimeError
9
+
10
+
11
+ logger = logging.getLogger(__name__)
12
+
13
+
14
+ def check_run_command(*args, **kwargs):
15
+ """Same as run_command except that it raises an exception on failure.
16
+
17
+ Raises
18
+ ------
19
+ DSGRuntimeError
20
+ Raised if the command returns a non-zero return code.
21
+
22
+ """
23
+ ret = run_command(*args, **kwargs)
24
+ if ret != 0:
25
+ msg = f"command returned error code: {ret}"
26
+ raise DSGRuntimeError(msg)
27
+
28
+
29
+ def run_command(cmd: str, output=None, cwd=None):
30
+ """Runs a command as a subprocess.
31
+
32
+ Caution: Capturing stdout and stderr in memory can be hazardous with
33
+ long-running processes that output lots of text. In those cases consider
34
+ running subprocess.Popen with stdout and/or stderr set to a pre-configured
35
+ file descriptor.
36
+
37
+ Parameters
38
+ ----------
39
+ cmd : str
40
+ command to run
41
+ output : None | dict
42
+ If a dict is passed then return stdout and stderr as keys.
43
+ cwd: str | default None
44
+ Change the working directory to cwd before executing the process.
45
+
46
+ Returns
47
+ -------
48
+ int
49
+ return code from system; usually zero is good, non-zero is error
50
+
51
+ """
52
+ logger.debug(cmd)
53
+ # Disable posix if on Windows.
54
+ command = shlex.split(cmd, posix="win" not in sys.platform)
55
+
56
+ if output is not None:
57
+ pipe = subprocess.Popen(command, stdout=subprocess.PIPE, stderr=subprocess.PIPE, cwd=cwd)
58
+ out, err = pipe.communicate()
59
+ output["stdout"] = out.decode("utf-8")
60
+ output["stderr"] = err.decode("utf-8")
61
+ ret = pipe.returncode
62
+ else:
63
+ ret = subprocess.call(command, cwd=cwd)
64
+
65
+ if ret != 0:
66
+ logger.debug("Command [%s] failed: %s", cmd, ret)
67
+ if output:
68
+ logger.debug(output["stderr"])
69
+
70
+ return ret
@@ -0,0 +1,65 @@
1
+ import logging
2
+ import os
3
+ from pathlib import Path
4
+ from tempfile import NamedTemporaryFile
5
+
6
+ from dsgrid.utils.files import delete_if_exists
7
+
8
+
9
+ logger = logging.getLogger(__name__)
10
+
11
+
12
+ class ScratchDirContext:
13
+ """Manages the lifetime of files in a scratch directory."""
14
+
15
+ def __init__(self, scratch_dir: Path):
16
+ self._scratch_dir = scratch_dir
17
+ self._paths: set[Path] = set()
18
+ if not self._scratch_dir.exists():
19
+ self._scratch_dir.mkdir()
20
+
21
+ @property
22
+ def scratch_dir(self) -> Path:
23
+ """Return the scratch directory."""
24
+ return self._scratch_dir
25
+
26
+ def add_tracked_path(self, path: Path) -> None:
27
+ """Add tracking of a path in the scratch directory."""
28
+ self._paths.add(path)
29
+
30
+ def list_tracked_paths(self) -> list[Path]:
31
+ """Return a list of paths being tracked."""
32
+ return list(self._paths)
33
+
34
+ def get_temp_filename(self, prefix=None, suffix=None, add_tracked_path=True) -> Path:
35
+ """Return a temporary filename based in the scratch directory.
36
+
37
+ Parameters
38
+ ----------
39
+ prefix : str | None
40
+ Forwarded to NamedTemporaryFile.
41
+ suffix : str | None
42
+ Forwarded to NamedTemporaryFile.
43
+ add_tracked_path : bool
44
+ If True, add tracking of the path
45
+ """
46
+ self._scratch_dir.mkdir(parents=True, exist_ok=True)
47
+ with NamedTemporaryFile(dir=self._scratch_dir, prefix=prefix, suffix=suffix) as f:
48
+ path = Path(f.name)
49
+ if add_tracked_path:
50
+ self._paths.add(path)
51
+ return Path(f.name)
52
+
53
+ def finalize(self) -> None:
54
+ """Remove all tracked paths once use of them is complete."""
55
+ for path in self._paths:
56
+ delete_if_exists(path)
57
+ logger.info("Deleted temporary path %s", path)
58
+
59
+ def __enter__(self):
60
+ return self
61
+
62
+ def __exit__(self, *args, **kwargs):
63
+ self.finalize()
64
+ if self._scratch_dir.exists() and not list(self._scratch_dir.iterdir()):
65
+ os.rmdir(self._scratch_dir)