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.
- stellaspark-utils-0.1/LICENSE +21 -0
- stellaspark-utils-0.1/PKG-INFO +125 -0
- stellaspark-utils-0.1/README.rst +97 -0
- stellaspark-utils-0.1/pyproject.toml +56 -0
- stellaspark-utils-0.1/setup.cfg +19 -0
- stellaspark-utils-0.1/setup.py +58 -0
- stellaspark-utils-0.1/stellaspark_utils/__init__.py +0 -0
- stellaspark-utils-0.1/stellaspark_utils/conftest.py +0 -0
- stellaspark-utils-0.1/stellaspark_utils/db.py +236 -0
- stellaspark-utils-0.1/stellaspark_utils/text.py +201 -0
- stellaspark-utils-0.1/stellaspark_utils.egg-info/PKG-INFO +125 -0
- stellaspark-utils-0.1/stellaspark_utils.egg-info/SOURCES.txt +15 -0
- stellaspark-utils-0.1/stellaspark_utils.egg-info/dependency_links.txt +1 -0
- stellaspark-utils-0.1/stellaspark_utils.egg-info/not-zip-safe +1 -0
- stellaspark-utils-0.1/stellaspark_utils.egg-info/requires.txt +5 -0
- stellaspark-utils-0.1/stellaspark_utils.egg-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
stellaspark_utils
|