stellaspark-utils 0.1__tar.gz

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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 StellaSpark
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.1
2
+ Name: stellaspark-utils
3
+ Version: 0.1
4
+ Summary: A collection of python utilities for StellaSpark Nexus Digital Twin
5
+ Home-page: https://github.com/StellaSpark/stellaspark_utils
6
+ Download-URL: https://github.com/StellaSpark/stellaspark_utils/archive/v0.1.tar.gz
7
+ Author: StellaSpark
8
+ Author-email: support@stellaspark.com
9
+ Maintainer: StellaSpark
10
+ Maintainer-email: support@stellaspark.com
11
+ License: MIT
12
+ Keywords: stellaspark,nexus,utils,calculation,python
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Information Technology
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Natural Language :: English
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.7
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Requires-Python: >=3.7
25
+ Description-Content-Type: text/markdown
26
+ Provides-Extra: test
27
+ License-File: LICENSE
28
+
29
+ [Nexus Digital Twin]: https://www.stellaspark.com/
30
+
31
+ ### Description
32
+ A collection of python utilities for StellaSpark [Nexus Digital Twin] technology.
33
+
34
+
35
+ ### Usage
36
+ ```
37
+ TODO
38
+ ```
39
+
40
+ ### Development
41
+
42
+ ###### Create an environment:
43
+ ```
44
+ # Install virtualenv if you didn't do that already
45
+ pip install virtualenv
46
+
47
+ # Navigate to the project root directory
48
+ cd <project_root>
49
+
50
+ # Create your new environment (called 'venv' here)
51
+ virtualenv venv
52
+
53
+ # Enter the virtual environment
54
+ .\venv\Scripts\activate
55
+
56
+ # Install the requirements in the current environment
57
+ pip install -r requirements.txt
58
+
59
+ # Install the development requirements in the current environment
60
+ pip install -r requirements_dev.txt
61
+ ```
62
+
63
+ ###### Run tests
64
+ ```
65
+ TODO
66
+ ```
67
+
68
+ ###### Autoformat your code with:
69
+ ```
70
+ # Navigate to the project root directory
71
+ cd <project_root>
72
+
73
+ # Enter the virtual environment
74
+ .\venv\Scripts\activate
75
+
76
+ # Make the code look nice
77
+ black .
78
+
79
+ # Sort the import statements
80
+ isort .
81
+
82
+ # Validate the code syntax
83
+ flake8
84
+ ```
85
+
86
+ ###### Prepare release
87
+ ```
88
+ 0. You need a Pypi account with an API token (https://pypi.org/manage/account/)
89
+ 1. Update version and change message in CHANGES.md
90
+ 2. Update the 'version' number in version.txt
91
+ 3. Autoformat code (see above)
92
+ 4. Optionally, create a pull request in a branch "release <version>"
93
+ 5. Optionally, Add commit message "release <version>"
94
+ 6. Optionally, Merge PR in main branch
95
+ 7. Optionally, Checkout main branch and pull
96
+ ```
97
+
98
+ ###### Release automatically
99
+ ```
100
+ cd <project_root>
101
+ .\venv\Scripts\activate
102
+ python release.py
103
+ ```
104
+
105
+ ###### Release manually
106
+ ```
107
+ # Navigate to the project root directory
108
+ cd <project_root>
109
+
110
+ # Enter the virtual environment
111
+ .\venv\Scripts\activate
112
+
113
+ # Create distribution (with a '.tar.gz' in it)
114
+ python setup.py sdist
115
+
116
+ # Validate all distibutions in stellaspark_utils/dist
117
+ twine check dist/*
118
+
119
+ # Upload distribution to pypi.org
120
+ twine upload dist/stellaspark_utils-<version>.tar.gz
121
+
122
+ # You will be prompted for a username and password.
123
+ # - for the username, use __token__ (yes literally '__token__')
124
+ # - for the password, use the pypi token value, including the pypi- prefix
125
+ ```
@@ -0,0 +1,97 @@
1
+ [Nexus Digital Twin]: https://www.stellaspark.com/
2
+
3
+ ### Description
4
+ A collection of python utilities for StellaSpark [Nexus Digital Twin] technology.
5
+
6
+
7
+ ### Usage
8
+ ```
9
+ TODO
10
+ ```
11
+
12
+ ### Development
13
+
14
+ ###### Create an environment:
15
+ ```
16
+ # Install virtualenv if you didn't do that already
17
+ pip install virtualenv
18
+
19
+ # Navigate to the project root directory
20
+ cd <project_root>
21
+
22
+ # Create your new environment (called 'venv' here)
23
+ virtualenv venv
24
+
25
+ # Enter the virtual environment
26
+ .\venv\Scripts\activate
27
+
28
+ # Install the requirements in the current environment
29
+ pip install -r requirements.txt
30
+
31
+ # Install the development requirements in the current environment
32
+ pip install -r requirements_dev.txt
33
+ ```
34
+
35
+ ###### Run tests
36
+ ```
37
+ TODO
38
+ ```
39
+
40
+ ###### Autoformat your code with:
41
+ ```
42
+ # Navigate to the project root directory
43
+ cd <project_root>
44
+
45
+ # Enter the virtual environment
46
+ .\venv\Scripts\activate
47
+
48
+ # Make the code look nice
49
+ black .
50
+
51
+ # Sort the import statements
52
+ isort .
53
+
54
+ # Validate the code syntax
55
+ flake8
56
+ ```
57
+
58
+ ###### Prepare release
59
+ ```
60
+ 0. You need a Pypi account with an API token (https://pypi.org/manage/account/)
61
+ 1. Update version and change message in CHANGES.md
62
+ 2. Update the 'version' number in version.txt
63
+ 3. Autoformat code (see above)
64
+ 4. Optionally, create a pull request in a branch "release <version>"
65
+ 5. Optionally, Add commit message "release <version>"
66
+ 6. Optionally, Merge PR in main branch
67
+ 7. Optionally, Checkout main branch and pull
68
+ ```
69
+
70
+ ###### Release automatically
71
+ ```
72
+ cd <project_root>
73
+ .\venv\Scripts\activate
74
+ python release.py
75
+ ```
76
+
77
+ ###### Release manually
78
+ ```
79
+ # Navigate to the project root directory
80
+ cd <project_root>
81
+
82
+ # Enter the virtual environment
83
+ .\venv\Scripts\activate
84
+
85
+ # Create distribution (with a '.tar.gz' in it)
86
+ python setup.py sdist
87
+
88
+ # Validate all distibutions in stellaspark_utils/dist
89
+ twine check dist/*
90
+
91
+ # Upload distribution to pypi.org
92
+ twine upload dist/stellaspark_utils-<version>.tar.gz
93
+
94
+ # You will be prompted for a username and password.
95
+ # - for the username, use __token__ (yes literally '__token__')
96
+ # - for the password, use the pypi token value, including the pypi- prefix
97
+ ```
@@ -0,0 +1,56 @@
1
+ # Note that you have to use single-quoted strings in TOML for regular expressions.
2
+ # It's the equivalent of r-strings in Python.
3
+ # Multiline strings are treated as verbose regular expressions by Black.
4
+ # Use [ ] to denote a significant space character.
5
+
6
+ [tool.isort]
7
+ atomic = true
8
+ force_alphabetical_sort = true
9
+ force_single_line = true
10
+ include_trailing_comma = true
11
+ line_length = 120
12
+ lines_after_imports = 2
13
+ multi_line_output = 3
14
+ skip = ["venv/"]
15
+ use_parentheses = true
16
+
17
+ [tool.black]
18
+ line-length = 120
19
+ target-version = ['py37', 'py38','py39']
20
+ # include = '\.pyi?$'
21
+ exclude = '''
22
+ /(
23
+ \.eggs
24
+ | \.git
25
+ | \.hg
26
+ | \.mypy_cache
27
+ | \.tox
28
+ | \.venv
29
+ | venv
30
+ | _build
31
+ | buck-out
32
+ | build
33
+ | dist
34
+ )/
35
+ '''
36
+
37
+ # pytest coverage (you need pytest-cov installed, then run 'pytest --cov')
38
+ [tool.coverage.run]
39
+ source = ['.']
40
+ omit = [
41
+ # omit anything in a .local directory anywhere
42
+ '*/.local/*',
43
+ '__init__.py',
44
+ # omit anything that is a test
45
+ 'tests/*',
46
+ 'test/*',
47
+ '*/tests/*',
48
+ '*/tests/*',
49
+ # omit anything in a .venv directory anywhere
50
+ '.venv/*'
51
+ ]
52
+
53
+ # pytest coverage (you need pytest-cov installed, then run 'pytest --cov')
54
+ [tool.coverage.report]
55
+ skip_empty = true
56
+ show_missing = false
@@ -0,0 +1,19 @@
1
+ [aliases]
2
+ test = pytest
3
+
4
+ [tool:pytest]
5
+ python_files = tests.py test_*.py
6
+
7
+ [flake8]
8
+ max-line-length = 120
9
+ max-complexity = 10
10
+ exclude =
11
+ env
12
+ .env
13
+ venv
14
+ .venv
15
+
16
+ [egg_info]
17
+ tag_build =
18
+ tag_date = 0
19
+
@@ -0,0 +1,58 @@
1
+ from constants import ENV_PACKAGE_VERSION
2
+ from constants import MODULE_NAMES
3
+ from constants import PACKAGE_NAME
4
+ from constants import REPO_NAME
5
+ from os import path
6
+ from setuptools import find_packages
7
+ from setuptools import setup
8
+
9
+ import os
10
+
11
+
12
+ # read the contents of your README file
13
+ this_directory = path.abspath(path.dirname(__file__))
14
+ with open(path.join(this_directory, "README.rst"), encoding="utf-8") as f:
15
+ long_description = f.read()
16
+
17
+
18
+ version = os.getenv(ENV_PACKAGE_VERSION)
19
+
20
+ install_requires = ["pytz", "unidecode"]
21
+ tests_require = [
22
+ "pytest",
23
+ ]
24
+
25
+ setup(
26
+ name=PACKAGE_NAME,
27
+ packages=find_packages(include=MODULE_NAMES),
28
+ version=version,
29
+ license="MIT",
30
+ description="A collection of python utilities for StellaSpark Nexus Digital Twin",
31
+ long_description_content_type="text/markdown",
32
+ long_description=long_description,
33
+ author="StellaSpark",
34
+ author_email="support@stellaspark.com",
35
+ maintainer="StellaSpark",
36
+ maintainer_email="support@stellaspark.com",
37
+ url="https://github.com/StellaSpark/stellaspark_utils",
38
+ download_url=f"https://github.com/StellaSpark/{REPO_NAME}/archive/v{version}.tar.gz",
39
+ keywords=["stellaspark", "nexus", "utils", "calculation", "python"],
40
+ zip_safe=False,
41
+ python_requires=">=3.7",
42
+ install_requires=install_requires,
43
+ tests_require=tests_require,
44
+ extras_require={"test": tests_require},
45
+ classifiers=[
46
+ "Development Status :: 4 - Beta",
47
+ "Intended Audience :: Developers",
48
+ "Intended Audience :: Information Technology",
49
+ "Intended Audience :: Science/Research",
50
+ "License :: OSI Approved :: MIT License",
51
+ "Natural Language :: English",
52
+ "Programming Language :: Python :: 3",
53
+ "Programming Language :: Python :: 3.7",
54
+ "Programming Language :: Python :: 3.8",
55
+ "Programming Language :: Python :: 3.9",
56
+ "Topic :: Software Development :: Build Tools",
57
+ ],
58
+ )
File without changes
File without changes
@@ -0,0 +1,236 @@
1
+ from stellaspark_utils.text import q
2
+ from typing import Dict
3
+ from typing import List
4
+
5
+
6
+ def get_indexes(executor, schema: str, table: str, pk: bool = True, unique: bool = True) -> List[Dict]:
7
+ """Return a list of dicts, each dict indicating the index name and definition.
8
+
9
+ Args: executor: Engine, Connection (SQLAlchemy) or DBAPI-like Cursor (Psycopg2, Django)
10
+ Returns a list of dicts, each dict being an index with its details
11
+ """
12
+ sql_filter = ""
13
+
14
+ if not pk:
15
+ sql_filter = sql_filter + " and indexname not ilike '%%_pkey%%'"
16
+ if not unique:
17
+ sql_filter = sql_filter + " and indexdef not ilike '%% unique index %%'"
18
+
19
+ results = executor.execute(
20
+ f"select indexname as name, indexdef as definition "
21
+ f"from pg_indexes "
22
+ f"where schemaname = %s and tablename = %s {sql_filter}",
23
+ (schema, table),
24
+ )
25
+
26
+ if results is None:
27
+ # Python DBAPI Cursor object (Django, Psycopg2)
28
+ indexes = [dict(zip([col[0] for col in executor.description], row)) for row in executor.fetchall()]
29
+ else:
30
+ # Database connection or engine-based query (SQLAlchemy)
31
+ indexes = [dict(row) for row in results.fetchall()]
32
+
33
+ return indexes
34
+
35
+
36
+ def get_constraints(executor, schema: str, table: str, pk: bool = True, child_fks: bool = False) -> List[Dict]:
37
+ """Return a list of dicts, each dict indicating the constraint name, type, definition etc.
38
+
39
+ Args: executor: Engine, Connection (SQLAlchemy) or DBAPI-like Cursor (Psycopg2, Django)
40
+ Returns a list of dicts, each dict being a constraint with its details
41
+ """
42
+ sql_where = "" if pk else "and pgc.contype != 'p'"
43
+
44
+ results = executor.execute(
45
+ f"select pgc.conname as name, "
46
+ f"pg_get_constraintdef(pgc.oid) as definition, "
47
+ f"pgc.contype as type, "
48
+ f"nullif(split_part(pgc.confrelid::regclass::text, '.',2), '') as table_referenced "
49
+ f"from pg_constraint pgc "
50
+ f"join pg_namespace nsp on nsp.oid = pgc.connamespace "
51
+ f"left join pg_class cls on pgc.conrelid = cls.oid "
52
+ f"where nspname = %s and relname = %s "
53
+ f"{sql_where}",
54
+ (schema, table),
55
+ )
56
+ if results is None:
57
+ # Python DBAPI Cursor object (Django, Psycopg2)
58
+ constraints = [dict(zip([col[0] for col in executor.description], row)) for row in executor.fetchall()]
59
+ else:
60
+ # Database connection or engine-based query (SQLAlchemy)
61
+ constraints = [dict(row) for row in results.fetchall()]
62
+
63
+ for constraint in constraints:
64
+ constraint["schema"] = schema
65
+ constraint["table"] = table
66
+ constraint["child"] = False
67
+ constraint["definition"] = f"alter table {schema}.{q(table)} add {constraint['definition']}"
68
+
69
+ if child_fks:
70
+ results = executor.execute(
71
+ "with unnested_confkey as ( "
72
+ " select oid, unnest(confkey) as confkey "
73
+ " from pg_constraint), "
74
+ "unnested_conkey as ( "
75
+ " select oid, unnest(conkey) as conkey "
76
+ " from pg_constraint ) "
77
+ "select c.conname as name, "
78
+ "tbl.relname as table, "
79
+ "col.attname as col, "
80
+ "pg_get_constraintdef(c.oid) as definition "
81
+ "from pg_constraint c "
82
+ "left join unnested_conkey con on c.oid = con.oid "
83
+ "left join pg_class tbl on tbl.oid = c.conrelid "
84
+ "left join pg_attribute col on (col.attrelid = tbl.oid and col.attnum = con.conkey) "
85
+ "left join pg_class referenced_tbl on c.confrelid = referenced_tbl.oid "
86
+ "left join unnested_confkey conf on c.oid = conf.oid "
87
+ "left join pg_attribute referenced_field on (referenced_field.attrelid = c.confrelid and referenced_field.attnum = conf.confkey) " # noqa
88
+ "where c.contype = 'f' "
89
+ "and tbl.relnamespace::regnamespace::text = %s "
90
+ "and referenced_tbl.relname = %s ",
91
+ (schema, table),
92
+ )
93
+ if results is None:
94
+ # Python DBAPI Cursor object (Django, Psycopg2)
95
+ constraints_children = [
96
+ dict(zip([col[0] for col in executor.description], row)) for row in executor.fetchall()
97
+ ]
98
+ else:
99
+ # Database connection or engine-based query (SQLAlchemy)
100
+ constraints_children = [dict(row) for row in results.fetchall()]
101
+
102
+ for child_constraint in constraints_children:
103
+ child_constraint["schema"] = schema
104
+ child_constraint["type"] = "f"
105
+ child_constraint["child"] = True
106
+ child_constraint[
107
+ "definition"
108
+ ] = f"alter table {schema}.{child_constraint['table']} add {child_constraint['definition']}"
109
+
110
+ constraints = constraints + constraints_children
111
+
112
+ return constraints
113
+
114
+
115
+ def get_dependent_views(executor, schema: str, table: str) -> List[Dict]:
116
+ """Get all views that depend on this table.
117
+
118
+ Args:executor: Engine, Connection (SQLAlchemy) or DBAPI-like Cursor (Psycopg2, Django)
119
+ Returns a list of dicts, each dict being a view with its details
120
+ """
121
+ results = executor.execute(
122
+ "select distinct on (objid) objid, "
123
+ "dependent_view.relname as name, "
124
+ "dependent_ns.nspname as schema, "
125
+ "pgv.definition as definition "
126
+ "from pg_depend "
127
+ "join pg_rewrite on pg_depend.objid = pg_rewrite.oid "
128
+ "join pg_class as dependent_view on pg_rewrite.ev_class = dependent_view.oid "
129
+ "join pg_class as source_table on pg_depend.refobjid = source_table.oid "
130
+ "join pg_attribute on pg_depend.refobjid = pg_attribute.attrelid "
131
+ " and pg_depend.refobjsubid = pg_attribute.attnum "
132
+ "join pg_namespace dependent_ns on dependent_ns.oid = dependent_view.relnamespace "
133
+ "join pg_namespace source_ns on source_ns.oid = source_table.relnamespace "
134
+ "join pg_views pgv on pgv.schemaname = dependent_ns.nspname and pgv.viewname = dependent_view.relname "
135
+ "where source_ns.nspname = %s "
136
+ "and source_table.relname = %s "
137
+ "and pg_attribute.attnum > 0",
138
+ (schema, table),
139
+ )
140
+
141
+ if results is None:
142
+ # Python DBAPI Cursor object (Django, Psycopg2)
143
+ dependent_views = [dict(zip([col[0] for col in executor.description], row)) for row in executor.fetchall()]
144
+ else:
145
+ # Database connection or engine-based query (SQLAlchemy)
146
+ dependent_views = [dict(row) for row in results.fetchall()]
147
+
148
+ for view in dependent_views:
149
+ view.pop("objid", None)
150
+ view["definition"] = f"create view {view['schema']}.{view['name']} as {view['definition']}"
151
+
152
+ return dependent_views
153
+
154
+
155
+ def get_dependent_matviews(executor, schema: str, table: str) -> List[Dict]:
156
+ """Get all materialized views that depend on this table.
157
+
158
+ Args:executor: Engine, Connection (SQLAlchemy) or DBAPI-like Cursor (Psycopg2, Django)
159
+ Returns a list of dicts, each dict being a dependent materialized view with its details
160
+ """
161
+ results = executor.execute(
162
+ f"select matviewname as name, schemaname as schema, definition "
163
+ f"from pg_matviews "
164
+ f"where definition ilike '%%{schema}.{table}%%'"
165
+ )
166
+
167
+ if results is None:
168
+ # Python DBAPI Cursor object (Django, Psycopg2)
169
+ dependent_matviews = [dict(zip([col[0] for col in executor.description], row)) for row in executor.fetchall()]
170
+ else:
171
+ # Database connection or engine-based query (SQLAlchemy)
172
+ dependent_matviews = [dict(row) for row in results.fetchall()]
173
+
174
+ for matview in dependent_matviews:
175
+ matview[
176
+ "definition"
177
+ ] = f"create materialized view {matview['schema']}.{matview['name']} as {matview['definition']}"
178
+
179
+ return dependent_matviews
180
+
181
+
182
+ def get_privileges(executor, schema: str, table: str) -> List[Dict]:
183
+ """List user privileges on table.
184
+
185
+ Args: executor: Engine, Connection (SQLAlchemy) or DBAPI-like Cursor (Psycopg2, Django)
186
+ Returns a list of dicts, each dict being a privilege with its details
187
+ """
188
+ results = executor.execute(
189
+ f"select grantee, privilege_type as name "
190
+ f"from information_schema.role_table_grants "
191
+ f"where table_schema = '{schema}' "
192
+ f"and table_name = '{table}'",
193
+ (schema, table),
194
+ )
195
+
196
+ if results is None:
197
+ # Python DBAPI Cursor object (Django, Psycopg2)
198
+ privileges = [dict(zip([col[0] for col in executor.description], row)) for row in executor.fetchall()]
199
+ else:
200
+ # Database connection or engine-based query (SQLAlchemy)
201
+ privileges = [dict(row) for row in results.fetchall()]
202
+
203
+ for privilege in privileges:
204
+ privilege["definition"] = f"grant {privilege['name']} on table {schema}.{q(table)} to {q(privilege['grantee'])}"
205
+
206
+ return privileges
207
+
208
+
209
+ def get_clustered_tables(executor) -> List[Dict]:
210
+ """Get a list of all clustered tables.
211
+
212
+ Args: executor: Engine, Connection (SQLAlchemy) or DBAPI-like Cursor (Psycopg2, Django)
213
+ Returns a list of dicts, each dict being a clustered table with its details
214
+ """
215
+ results = executor.execute(
216
+ "select n.nspname as schema, c.relname as table, split_part(indexrelid::regclass::text, '.', 2) as index "
217
+ "from pg_class c "
218
+ "join pg_namespace n "
219
+ "on n.oid = c.relnamespace "
220
+ "join pg_index i "
221
+ "on i.indrelid = c.oid "
222
+ "where c.relkind = 'r' and c.relhasindex = 't' "
223
+ "and i.indisclustered = 't'"
224
+ )
225
+
226
+ if results is None:
227
+ # Python DBAPI Cursor object (Django, Psycopg2)
228
+ clustered_tables = [dict(zip([col[0] for col in executor.description], row)) for row in executor.fetchall()]
229
+ else:
230
+ # Database connection or engine-based query (SQLAlchemy)
231
+ clustered_tables = [dict(row) for row in results.fetchall()]
232
+
233
+ for table in clustered_tables:
234
+ table["definition"] = f"alter table {table['schema']}.{q(table['table'])} cluster on {table['index']}"
235
+
236
+ return clustered_tables
@@ -0,0 +1,201 @@
1
+ from calendar import monthrange
2
+ from datetime import datetime
3
+ from datetime import timedelta
4
+ from typing import List
5
+ from typing import Union
6
+
7
+ import pytz
8
+ import re
9
+ import unidecode
10
+
11
+
12
+ def q(ids) -> Union[str, List]:
13
+ """Return quoted and sanitized SQL column or table identifiers."""
14
+ if isinstance(ids, (list, tuple)):
15
+ return [f'"{_id}"' for _id in ids]
16
+ else:
17
+ return f'"{ids}"'
18
+
19
+
20
+ def sq(string: Union[str, List]):
21
+ """Return single-quoted strings that are safe for HTML."""
22
+ return (
23
+ [chr(39) + str(elem) + chr(39) for elem in string]
24
+ if isinstance(string, (list, tuple))
25
+ else chr(39) + str(string) + chr(39)
26
+ )
27
+
28
+
29
+ def to_lwu(s, keep_colons=True, keep_minus=False, keep_double_underscores=False):
30
+ """Convert string to lowercase_with_underscores format.
31
+
32
+ Also replace dots and hyphens with underscores for Postgres table name compatibility. Maintaining or removing
33
+ colons is optional, since some Nexus identifiers may contain these (such as GeoServer layers) whereas other
34
+ identifiers are not allowed to have this (e.g. Postgres colnames)
35
+ """
36
+ # Order to_replace matters!
37
+ to_replace = {
38
+ "<>": "_uneq_",
39
+ ">=": "_gte_",
40
+ "=>": "_gte_",
41
+ "<=": "_lte_",
42
+ "=<": "_lte_",
43
+ ">": "_gt_",
44
+ "<": "_lt_",
45
+ "!=": "_uneq_",
46
+ "==": "_eq_",
47
+ "=": "_eq_",
48
+ "%": "_pct_",
49
+ "°": "_deg_",
50
+ "&": "_and_",
51
+ }
52
+
53
+ # Make sure that the space is always replaced between the minuses
54
+ to_underscore = [" "] if keep_minus else [" - ", " ", "-"]
55
+ to_underscore.extend([".", "+", ",", "/"])
56
+
57
+ if not keep_double_underscores:
58
+ to_underscore.append("__")
59
+
60
+ to_underscore.extend(["___", "____"]) # Order here matters, replace duplicate underscores last
61
+ to_remove = [
62
+ "[",
63
+ "]",
64
+ "(",
65
+ ")",
66
+ "{",
67
+ "}",
68
+ "#",
69
+ ";",
70
+ "'",
71
+ '"',
72
+ "?",
73
+ "!",
74
+ "*",
75
+ "@",
76
+ "$",
77
+ "|",
78
+ ]
79
+
80
+ if not keep_colons:
81
+ to_remove.append(":")
82
+
83
+ for substr, replacement in to_replace.items():
84
+ s = s.replace(substr, replacement)
85
+
86
+ for substr in to_underscore:
87
+ s = s.replace(substr, "_")
88
+
89
+ for substr in to_remove:
90
+ s = s.replace(substr, "")
91
+
92
+ # Ensure that final string does not have _ at the end or start
93
+ s = s.strip("_")
94
+ s = unidecode.unidecode(s) # Remove any accents, convert greek, cyrillic or chinese characters
95
+
96
+ return s.lower()
97
+
98
+
99
+ def is_float(string: str) -> bool:
100
+ """Quickly check if string is also a valid number."""
101
+ try:
102
+ float(string)
103
+ return True
104
+ except ValueError:
105
+ return False
106
+
107
+
108
+ def parse_time_placeholders(s: str):
109
+ """Substitute time placeholders.
110
+
111
+ Substitute the following placeholders with current year, month and days respectively (possibly with a subtraction):
112
+ - {-30:%Y-%m-%d}
113
+ - {%Y}
114
+ - {%Y/%m/%d}
115
+
116
+ https://www.programiz.com/python-programming/datetime/strftime
117
+ """
118
+ # Find fields that contain placeholders with percent signs (datetime-like)
119
+ fields = re.findall(r"{([-|%|\d].+?)}", s)
120
+ contains_any = ("%",)
121
+ fields = [field for field in fields if any(substr in field for substr in contains_any)]
122
+ if not fields:
123
+ return s
124
+
125
+ now = datetime.now()
126
+ for field in fields:
127
+ if ":" in field:
128
+ days = 0
129
+ seconds = 0
130
+ if "day" in field:
131
+ field_sanitized = re.sub(r"[a-zA-Z]", "", field.split("day")[0]).strip()
132
+ days = int(eval(field_sanitized))
133
+ elif "year" in field:
134
+ field_sanitized = re.sub(r"[a-zA-Z]", "", field.split("year")[0]).strip()
135
+ years = int(eval(field_sanitized))
136
+ days = get_days_offset(now.year, now.month, years_offset=years)
137
+ elif "second" in field:
138
+ field_sanitized = re.sub(r"[a-zA-Z]", "", field.split("second")[0]).strip()
139
+ seconds = int(eval(field_sanitized))
140
+ else:
141
+ raise ValueError("Unknown time offset")
142
+
143
+ timestamp = now + timedelta(days=days, seconds=seconds)
144
+ pos = field.find(":")
145
+ if field[pos + 1 :] == "%unix": # noqa
146
+ s = s.replace("{" + field + "}", str(int(timestamp.timestamp())))
147
+ else:
148
+ s = s.replace("{" + field + "}", eval(f"{timestamp: f'{field[pos+1:]}'}"))
149
+ else:
150
+ if field == "%unix":
151
+ s = s.replace("{" + field + "}", str(int(now.timestamp())))
152
+ else:
153
+ s = s.replace("{" + field + "}", eval(f"{now: f'{field}'}"))
154
+
155
+ return s
156
+
157
+
158
+ def get_days_offset(
159
+ year: int,
160
+ month: int,
161
+ years_offset: int = 0,
162
+ months_offset: int = 0,
163
+ days_offset: int = 0,
164
+ ) -> int:
165
+ """Get offset in days, counting from the given year and current month.
166
+
167
+ In case of a net-negative offset, the returned number of days will be negative. Vise versa.
168
+ """
169
+ # To account for leap years, also add the years offset to the months, we calculate the amount of days in one go
170
+ months_offset = months_offset + (12 * years_offset)
171
+
172
+ days = 0
173
+ for _ in range(0, abs(months_offset)):
174
+ if months_offset < 0:
175
+ if month == 1:
176
+ year = year - 1
177
+ month = 12
178
+ else:
179
+ month = month - 1
180
+ days = days - monthrange(year, month)[1]
181
+ else:
182
+ if month == 12:
183
+ year = year + 1
184
+ month = 1
185
+ else:
186
+ month = month + 1
187
+ days = days + monthrange(year, month)[1]
188
+
189
+ days = days + days_offset
190
+
191
+ return days
192
+
193
+
194
+ def ensure_tz_aware(datetime_str: str) -> str:
195
+ """Ensure that datetime string is iso-formatted and is timezone aware."""
196
+ datetime_obj = datetime.fromisoformat(datetime_str)
197
+ is_tz_aware = datetime_obj.tzinfo is not None and datetime_obj.tzinfo.utcoffset(datetime_obj) is not None
198
+ if is_tz_aware:
199
+ return datetime_str
200
+ datetime_obj_tz = datetime_obj.replace(tzinfo=pytz.utc) # Make timezone aware (UTC)
201
+ return datetime_obj_tz.isoformat()
@@ -0,0 +1,125 @@
1
+ Metadata-Version: 2.1
2
+ Name: stellaspark-utils
3
+ Version: 0.1
4
+ Summary: A collection of python utilities for StellaSpark Nexus Digital Twin
5
+ Home-page: https://github.com/StellaSpark/stellaspark_utils
6
+ Download-URL: https://github.com/StellaSpark/stellaspark_utils/archive/v0.1.tar.gz
7
+ Author: StellaSpark
8
+ Author-email: support@stellaspark.com
9
+ Maintainer: StellaSpark
10
+ Maintainer-email: support@stellaspark.com
11
+ License: MIT
12
+ Keywords: stellaspark,nexus,utils,calculation,python
13
+ Classifier: Development Status :: 4 - Beta
14
+ Classifier: Intended Audience :: Developers
15
+ Classifier: Intended Audience :: Information Technology
16
+ Classifier: Intended Audience :: Science/Research
17
+ Classifier: License :: OSI Approved :: MIT License
18
+ Classifier: Natural Language :: English
19
+ Classifier: Programming Language :: Python :: 3
20
+ Classifier: Programming Language :: Python :: 3.7
21
+ Classifier: Programming Language :: Python :: 3.8
22
+ Classifier: Programming Language :: Python :: 3.9
23
+ Classifier: Topic :: Software Development :: Build Tools
24
+ Requires-Python: >=3.7
25
+ Description-Content-Type: text/markdown
26
+ Provides-Extra: test
27
+ License-File: LICENSE
28
+
29
+ [Nexus Digital Twin]: https://www.stellaspark.com/
30
+
31
+ ### Description
32
+ A collection of python utilities for StellaSpark [Nexus Digital Twin] technology.
33
+
34
+
35
+ ### Usage
36
+ ```
37
+ TODO
38
+ ```
39
+
40
+ ### Development
41
+
42
+ ###### Create an environment:
43
+ ```
44
+ # Install virtualenv if you didn't do that already
45
+ pip install virtualenv
46
+
47
+ # Navigate to the project root directory
48
+ cd <project_root>
49
+
50
+ # Create your new environment (called 'venv' here)
51
+ virtualenv venv
52
+
53
+ # Enter the virtual environment
54
+ .\venv\Scripts\activate
55
+
56
+ # Install the requirements in the current environment
57
+ pip install -r requirements.txt
58
+
59
+ # Install the development requirements in the current environment
60
+ pip install -r requirements_dev.txt
61
+ ```
62
+
63
+ ###### Run tests
64
+ ```
65
+ TODO
66
+ ```
67
+
68
+ ###### Autoformat your code with:
69
+ ```
70
+ # Navigate to the project root directory
71
+ cd <project_root>
72
+
73
+ # Enter the virtual environment
74
+ .\venv\Scripts\activate
75
+
76
+ # Make the code look nice
77
+ black .
78
+
79
+ # Sort the import statements
80
+ isort .
81
+
82
+ # Validate the code syntax
83
+ flake8
84
+ ```
85
+
86
+ ###### Prepare release
87
+ ```
88
+ 0. You need a Pypi account with an API token (https://pypi.org/manage/account/)
89
+ 1. Update version and change message in CHANGES.md
90
+ 2. Update the 'version' number in version.txt
91
+ 3. Autoformat code (see above)
92
+ 4. Optionally, create a pull request in a branch "release <version>"
93
+ 5. Optionally, Add commit message "release <version>"
94
+ 6. Optionally, Merge PR in main branch
95
+ 7. Optionally, Checkout main branch and pull
96
+ ```
97
+
98
+ ###### Release automatically
99
+ ```
100
+ cd <project_root>
101
+ .\venv\Scripts\activate
102
+ python release.py
103
+ ```
104
+
105
+ ###### Release manually
106
+ ```
107
+ # Navigate to the project root directory
108
+ cd <project_root>
109
+
110
+ # Enter the virtual environment
111
+ .\venv\Scripts\activate
112
+
113
+ # Create distribution (with a '.tar.gz' in it)
114
+ python setup.py sdist
115
+
116
+ # Validate all distibutions in stellaspark_utils/dist
117
+ twine check dist/*
118
+
119
+ # Upload distribution to pypi.org
120
+ twine upload dist/stellaspark_utils-<version>.tar.gz
121
+
122
+ # You will be prompted for a username and password.
123
+ # - for the username, use __token__ (yes literally '__token__')
124
+ # - for the password, use the pypi token value, including the pypi- prefix
125
+ ```
@@ -0,0 +1,15 @@
1
+ LICENSE
2
+ README.rst
3
+ pyproject.toml
4
+ setup.cfg
5
+ setup.py
6
+ stellaspark_utils/__init__.py
7
+ stellaspark_utils/conftest.py
8
+ stellaspark_utils/db.py
9
+ stellaspark_utils/text.py
10
+ stellaspark_utils.egg-info/PKG-INFO
11
+ stellaspark_utils.egg-info/SOURCES.txt
12
+ stellaspark_utils.egg-info/dependency_links.txt
13
+ stellaspark_utils.egg-info/not-zip-safe
14
+ stellaspark_utils.egg-info/requires.txt
15
+ stellaspark_utils.egg-info/top_level.txt
@@ -0,0 +1,5 @@
1
+ pytz
2
+ unidecode
3
+
4
+ [test]
5
+ pytest
@@ -0,0 +1 @@
1
+ stellaspark_utils