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.
- build_backend.py +93 -0
- dsgrid/__init__.py +22 -0
- dsgrid/api/__init__.py +0 -0
- dsgrid/api/api_manager.py +179 -0
- dsgrid/api/app.py +419 -0
- dsgrid/api/models.py +60 -0
- dsgrid/api/response_models.py +116 -0
- dsgrid/apps/__init__.py +0 -0
- dsgrid/apps/project_viewer/app.py +216 -0
- dsgrid/apps/registration_gui.py +444 -0
- dsgrid/chronify.py +32 -0
- dsgrid/cli/__init__.py +0 -0
- dsgrid/cli/common.py +120 -0
- dsgrid/cli/config.py +176 -0
- dsgrid/cli/download.py +13 -0
- dsgrid/cli/dsgrid.py +157 -0
- dsgrid/cli/dsgrid_admin.py +92 -0
- dsgrid/cli/install_notebooks.py +62 -0
- dsgrid/cli/query.py +729 -0
- dsgrid/cli/registry.py +1862 -0
- dsgrid/cloud/__init__.py +0 -0
- dsgrid/cloud/cloud_storage_interface.py +140 -0
- dsgrid/cloud/factory.py +31 -0
- dsgrid/cloud/fake_storage_interface.py +37 -0
- dsgrid/cloud/s3_storage_interface.py +156 -0
- dsgrid/common.py +36 -0
- dsgrid/config/__init__.py +0 -0
- dsgrid/config/annual_time_dimension_config.py +194 -0
- dsgrid/config/common.py +142 -0
- dsgrid/config/config_base.py +148 -0
- dsgrid/config/dataset_config.py +907 -0
- dsgrid/config/dataset_schema_handler_factory.py +46 -0
- dsgrid/config/date_time_dimension_config.py +136 -0
- dsgrid/config/dimension_config.py +54 -0
- dsgrid/config/dimension_config_factory.py +65 -0
- dsgrid/config/dimension_mapping_base.py +350 -0
- dsgrid/config/dimension_mappings_config.py +48 -0
- dsgrid/config/dimensions.py +1025 -0
- dsgrid/config/dimensions_config.py +71 -0
- dsgrid/config/file_schema.py +190 -0
- dsgrid/config/index_time_dimension_config.py +80 -0
- dsgrid/config/input_dataset_requirements.py +31 -0
- dsgrid/config/mapping_tables.py +209 -0
- dsgrid/config/noop_time_dimension_config.py +42 -0
- dsgrid/config/project_config.py +1462 -0
- dsgrid/config/registration_models.py +188 -0
- dsgrid/config/representative_period_time_dimension_config.py +194 -0
- dsgrid/config/simple_models.py +49 -0
- dsgrid/config/supplemental_dimension.py +29 -0
- dsgrid/config/time_dimension_base_config.py +192 -0
- dsgrid/data_models.py +155 -0
- dsgrid/dataset/__init__.py +0 -0
- dsgrid/dataset/dataset.py +123 -0
- dsgrid/dataset/dataset_expression_handler.py +86 -0
- dsgrid/dataset/dataset_mapping_manager.py +121 -0
- dsgrid/dataset/dataset_schema_handler_base.py +945 -0
- dsgrid/dataset/dataset_schema_handler_one_table.py +209 -0
- dsgrid/dataset/dataset_schema_handler_two_table.py +322 -0
- dsgrid/dataset/growth_rates.py +162 -0
- dsgrid/dataset/models.py +51 -0
- dsgrid/dataset/table_format_handler_base.py +257 -0
- dsgrid/dataset/table_format_handler_factory.py +17 -0
- dsgrid/dataset/unpivoted_table.py +121 -0
- dsgrid/dimension/__init__.py +0 -0
- dsgrid/dimension/base_models.py +230 -0
- dsgrid/dimension/dimension_filters.py +308 -0
- dsgrid/dimension/standard.py +252 -0
- dsgrid/dimension/time.py +352 -0
- dsgrid/dimension/time_utils.py +103 -0
- dsgrid/dsgrid_rc.py +88 -0
- dsgrid/exceptions.py +105 -0
- dsgrid/filesystem/__init__.py +0 -0
- dsgrid/filesystem/cloud_filesystem.py +32 -0
- dsgrid/filesystem/factory.py +32 -0
- dsgrid/filesystem/filesystem_interface.py +136 -0
- dsgrid/filesystem/local_filesystem.py +74 -0
- dsgrid/filesystem/s3_filesystem.py +118 -0
- dsgrid/loggers.py +132 -0
- dsgrid/minimal_patterns.cp313-win_amd64.pyd +0 -0
- dsgrid/notebooks/connect_to_dsgrid_registry.ipynb +949 -0
- dsgrid/notebooks/registration.ipynb +48 -0
- dsgrid/notebooks/start_notebook.sh +11 -0
- dsgrid/project.py +451 -0
- dsgrid/query/__init__.py +0 -0
- dsgrid/query/dataset_mapping_plan.py +142 -0
- dsgrid/query/derived_dataset.py +388 -0
- dsgrid/query/models.py +728 -0
- dsgrid/query/query_context.py +287 -0
- dsgrid/query/query_submitter.py +994 -0
- dsgrid/query/report_factory.py +19 -0
- dsgrid/query/report_peak_load.py +70 -0
- dsgrid/query/reports_base.py +20 -0
- dsgrid/registry/__init__.py +0 -0
- dsgrid/registry/bulk_register.py +165 -0
- dsgrid/registry/common.py +287 -0
- dsgrid/registry/config_update_checker_base.py +63 -0
- dsgrid/registry/data_store_factory.py +34 -0
- dsgrid/registry/data_store_interface.py +74 -0
- dsgrid/registry/dataset_config_generator.py +158 -0
- dsgrid/registry/dataset_registry_manager.py +950 -0
- dsgrid/registry/dataset_update_checker.py +16 -0
- dsgrid/registry/dimension_mapping_registry_manager.py +575 -0
- dsgrid/registry/dimension_mapping_update_checker.py +16 -0
- dsgrid/registry/dimension_registry_manager.py +413 -0
- dsgrid/registry/dimension_update_checker.py +16 -0
- dsgrid/registry/duckdb_data_store.py +207 -0
- dsgrid/registry/filesystem_data_store.py +150 -0
- dsgrid/registry/filter_registry_manager.py +123 -0
- dsgrid/registry/project_config_generator.py +57 -0
- dsgrid/registry/project_registry_manager.py +1623 -0
- dsgrid/registry/project_update_checker.py +48 -0
- dsgrid/registry/registration_context.py +223 -0
- dsgrid/registry/registry_auto_updater.py +316 -0
- dsgrid/registry/registry_database.py +667 -0
- dsgrid/registry/registry_interface.py +446 -0
- dsgrid/registry/registry_manager.py +558 -0
- dsgrid/registry/registry_manager_base.py +367 -0
- dsgrid/registry/versioning.py +92 -0
- dsgrid/rust_ext/__init__.py +14 -0
- dsgrid/rust_ext/find_minimal_patterns.py +129 -0
- dsgrid/spark/__init__.py +0 -0
- dsgrid/spark/functions.py +589 -0
- dsgrid/spark/types.py +110 -0
- dsgrid/tests/__init__.py +0 -0
- dsgrid/tests/common.py +140 -0
- dsgrid/tests/make_us_data_registry.py +265 -0
- dsgrid/tests/register_derived_datasets.py +103 -0
- dsgrid/tests/utils.py +25 -0
- dsgrid/time/__init__.py +0 -0
- dsgrid/time/time_conversions.py +80 -0
- dsgrid/time/types.py +67 -0
- dsgrid/units/__init__.py +0 -0
- dsgrid/units/constants.py +113 -0
- dsgrid/units/convert.py +71 -0
- dsgrid/units/energy.py +145 -0
- dsgrid/units/power.py +87 -0
- dsgrid/utils/__init__.py +0 -0
- dsgrid/utils/dataset.py +830 -0
- dsgrid/utils/files.py +179 -0
- dsgrid/utils/filters.py +125 -0
- dsgrid/utils/id_remappings.py +100 -0
- dsgrid/utils/py_expression_eval/LICENSE +19 -0
- dsgrid/utils/py_expression_eval/README.md +8 -0
- dsgrid/utils/py_expression_eval/__init__.py +847 -0
- dsgrid/utils/py_expression_eval/tests.py +283 -0
- dsgrid/utils/run_command.py +70 -0
- dsgrid/utils/scratch_dir_context.py +65 -0
- dsgrid/utils/spark.py +918 -0
- dsgrid/utils/spark_partition.py +98 -0
- dsgrid/utils/timing.py +239 -0
- dsgrid/utils/utilities.py +221 -0
- dsgrid/utils/versioning.py +36 -0
- dsgrid_toolkit-0.3.3.dist-info/METADATA +193 -0
- dsgrid_toolkit-0.3.3.dist-info/RECORD +157 -0
- dsgrid_toolkit-0.3.3.dist-info/WHEEL +4 -0
- dsgrid_toolkit-0.3.3.dist-info/entry_points.txt +4 -0
- 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)
|