python-ubercode-utils 2.0.0__tar.gz → 2.0.4__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.
- python_ubercode_utils-2.0.4/PKG-INFO +29 -0
- python_ubercode_utils-2.0.4/python_ubercode_utils.egg-info/PKG-INFO +29 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/setup.py +2 -2
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/test/test_data.py +11 -11
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/test/test_environment.py +58 -5
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/test/test_urls.py +41 -1
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/ubercode/utils/convert.py +5 -5
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/ubercode/utils/data.py +4 -4
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/ubercode/utils/dataframe.py +19 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/ubercode/utils/environment.py +99 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/ubercode/utils/logging.py +1 -1
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/ubercode/utils/urls.py +95 -2
- python-ubercode-utils-2.0.0/PKG-INFO +0 -29
- python-ubercode-utils-2.0.0/python_ubercode_utils.egg-info/PKG-INFO +0 -29
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/LICENSE +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/MANIFEST.in +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/README.md +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/python_ubercode_utils.egg-info/SOURCES.txt +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/python_ubercode_utils.egg-info/dependency_links.txt +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/python_ubercode_utils.egg-info/not-zip-safe +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/python_ubercode_utils.egg-info/top_level.txt +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/setup.cfg +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/test/test_convert.py +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/test/test_cursor.py +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/test/test_dataframe.py +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/test/test_logging.py +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/ubercode/__init__.py +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/ubercode/utils/__init__.py +0 -0
- {python-ubercode-utils-2.0.0 → python_ubercode_utils-2.0.4}/ubercode/utils/cursor.py +0 -0
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python_ubercode_utils
|
|
3
|
+
Version: 2.0.4
|
|
4
|
+
Summary: Core python utilities for all apps
|
|
5
|
+
Home-page: https://github.com/sstacha/python-ubercode-utils
|
|
6
|
+
Author: Steve Stacha
|
|
7
|
+
Author-email: sstacha@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
|
|
18
|
+
# python-ubercode-utils
|
|
19
|
+
Extracting common python utilities re-used between all projects. The intent is to have minimal dependencies
|
|
20
|
+
so the library can be used by django settings without circular references. I also have color logging class for
|
|
21
|
+
jupyter notebooks. I will have a couple of libraries that will extend this functionality. Scan the test cases in the
|
|
22
|
+
tests folder for common use cases.
|
|
23
|
+
|
|
24
|
+
python-utils-core:
|
|
25
|
+
- basic conversion helper utilities
|
|
26
|
+
- color logging without dependencies
|
|
27
|
+
- manipulating urls and their parameters
|
|
28
|
+
- helper classes to make working with xml and json data easier
|
|
29
|
+
- minimal helper classes to convert database cursor results to dictionaries or tuples
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
Metadata-Version: 2.1
|
|
2
|
+
Name: python-ubercode-utils
|
|
3
|
+
Version: 2.0.4
|
|
4
|
+
Summary: Core python utilities for all apps
|
|
5
|
+
Home-page: https://github.com/sstacha/python-ubercode-utils
|
|
6
|
+
Author: Steve Stacha
|
|
7
|
+
Author-email: sstacha@gmail.com
|
|
8
|
+
License: MIT
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Programming Language :: Python :: 3
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Topic :: Utilities
|
|
14
|
+
Requires-Python: >=3.8
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
License-File: LICENSE
|
|
17
|
+
|
|
18
|
+
# python-ubercode-utils
|
|
19
|
+
Extracting common python utilities re-used between all projects. The intent is to have minimal dependencies
|
|
20
|
+
so the library can be used by django settings without circular references. I also have color logging class for
|
|
21
|
+
jupyter notebooks. I will have a couple of libraries that will extend this functionality. Scan the test cases in the
|
|
22
|
+
tests folder for common use cases.
|
|
23
|
+
|
|
24
|
+
python-utils-core:
|
|
25
|
+
- basic conversion helper utilities
|
|
26
|
+
- color logging without dependencies
|
|
27
|
+
- manipulating urls and their parameters
|
|
28
|
+
- helper classes to make working with xml and json data easier
|
|
29
|
+
- minimal helper classes to convert database cursor results to dictionaries or tuples
|
|
@@ -3,8 +3,8 @@ import setuptools
|
|
|
3
3
|
with open("README.md", "r") as fh:
|
|
4
4
|
long_description = fh.read()
|
|
5
5
|
|
|
6
|
-
setuptools.setup(name='
|
|
7
|
-
version='2.0.
|
|
6
|
+
setuptools.setup(name='python_ubercode_utils',
|
|
7
|
+
version='2.0.4',
|
|
8
8
|
description='Core python utilities for all apps',
|
|
9
9
|
long_description=long_description,
|
|
10
10
|
long_description_content_type="text/markdown",
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import unittest
|
|
2
2
|
from pathlib import Path
|
|
3
3
|
|
|
4
|
-
from ubercode.utils.data import
|
|
5
|
-
from ubercode.utils.data import
|
|
4
|
+
from ubercode.utils.data import JsonData
|
|
5
|
+
from ubercode.utils.data import XmlData
|
|
6
6
|
|
|
7
7
|
|
|
8
|
-
class
|
|
8
|
+
class TestJsonData(unittest.TestCase):
|
|
9
9
|
|
|
10
10
|
# -------- common usages ----------
|
|
11
11
|
def test_JSON(self):
|
|
@@ -48,10 +48,10 @@ class TestJSON(unittest.TestCase):
|
|
|
48
48
|
}
|
|
49
49
|
"""
|
|
50
50
|
# test we can construct from a json string
|
|
51
|
-
json =
|
|
51
|
+
json = JsonData(json_string=json_string)
|
|
52
52
|
self.assertEqual(len(json.data['people']), 3)
|
|
53
53
|
# test we can construct by chaining and reading file
|
|
54
|
-
json2 =
|
|
54
|
+
json2 = JsonData().from_json_file(str(file_path))
|
|
55
55
|
self.assertEqual(json.data, json2.data)
|
|
56
56
|
# test encoding
|
|
57
57
|
json_string = """
|
|
@@ -89,7 +89,7 @@ class TestJSON(unittest.TestCase):
|
|
|
89
89
|
]
|
|
90
90
|
}
|
|
91
91
|
"""
|
|
92
|
-
json =
|
|
92
|
+
json = JsonData(json_string=json_string, encode_ampersands=True)
|
|
93
93
|
self.assertEqual(len(json.data['people']), 3)
|
|
94
94
|
first_name = json.data['people'][0]['firstName']
|
|
95
95
|
self.assertEqual(first_name, "Joe & Baker")
|
|
@@ -101,7 +101,7 @@ class TestJSON(unittest.TestCase):
|
|
|
101
101
|
self.assertEqual(str(json), result)
|
|
102
102
|
|
|
103
103
|
|
|
104
|
-
class
|
|
104
|
+
class TestXmlData(unittest.TestCase):
|
|
105
105
|
|
|
106
106
|
# -------- common usages ----------
|
|
107
107
|
def test_XML(self):
|
|
@@ -121,15 +121,15 @@ class TestXML(unittest.TestCase):
|
|
|
121
121
|
</contacts>
|
|
122
122
|
"""
|
|
123
123
|
# test we can construct from an xml string
|
|
124
|
-
xml =
|
|
124
|
+
xml = XmlData(xml_string=xml_string)
|
|
125
125
|
# NOTE: because we used a multiline string we need to strip the extra newlines before and after <contacts>
|
|
126
126
|
self.assertEqual(str(xml), xml_string.strip())
|
|
127
127
|
# normal string doesn't need stripping
|
|
128
128
|
xml_compact_string = "<contacts><contact><name>Buggs Bunny</name></contact><contact><name>Daffy Duck</name></contact></contacts>"
|
|
129
|
-
xml2 =
|
|
129
|
+
xml2 = XmlData(xml_compact_string)
|
|
130
130
|
self.assertEqual(str(xml2), xml_compact_string)
|
|
131
131
|
# test we can create using the from_xml_string() method chaining
|
|
132
|
-
xml3 =
|
|
132
|
+
xml3 = XmlData().from_xml_string(xml_compact_string)
|
|
133
133
|
self.assertEqual(str(xml2), str(xml3))
|
|
134
134
|
# test that method chaining after constructor overrides the value in place
|
|
135
135
|
self.assertNotEqual(str(xml), str(xml2))
|
|
@@ -153,7 +153,7 @@ class TestXML(unittest.TestCase):
|
|
|
153
153
|
</contact>
|
|
154
154
|
</contacts>
|
|
155
155
|
"""
|
|
156
|
-
xml =
|
|
156
|
+
xml = XmlData(xml_string=xml_string, encode_ampersands=True)
|
|
157
157
|
xml_dict = xml.to_dict()
|
|
158
158
|
self.assertEqual(xml_dict['contacts']['contact'][0]['@attr'], '1')
|
|
159
159
|
|
|
@@ -80,7 +80,9 @@ class TestEnvironment(unittest.TestCase):
|
|
|
80
80
|
}
|
|
81
81
|
environment = Environment(environment_variable_map=os_vars)
|
|
82
82
|
# test we can override just the database file name
|
|
83
|
-
|
|
83
|
+
with redirect_stdout(StringIO()) as sout:
|
|
84
|
+
DATABASES = environment.override_database_variables(DATABASES)
|
|
85
|
+
print(f"override_vars password log output \n{sout.getvalue()}")
|
|
84
86
|
self.assertEqual(DATABASES['default']['NAME'], BASE_DIR / "sqlite.db")
|
|
85
87
|
# test we can take the default sqlite database and change it to a full mysql connection using env vars
|
|
86
88
|
os_vars = {
|
|
@@ -95,8 +97,7 @@ class TestEnvironment(unittest.TestCase):
|
|
|
95
97
|
# test we can override the default sqllite for dev laptops with a full mysql connection on a deployed server
|
|
96
98
|
with redirect_stdout(StringIO()) as sout:
|
|
97
99
|
DATABASES = environment.override_database_variables(DATABASES)
|
|
98
|
-
|
|
99
|
-
print(log_output)
|
|
100
|
+
print(f"override_vars log output \n{sout.getvalue()}")
|
|
100
101
|
self.assertEqual(DATABASES['default']['ENGINE'], 'django.db.backends.mysql')
|
|
101
102
|
self.assertEqual(DATABASES['default']['HOST'], 'testdb.example.org')
|
|
102
103
|
self.assertEqual(DATABASES['default']['NAME'], 'test')
|
|
@@ -104,11 +105,63 @@ class TestEnvironment(unittest.TestCase):
|
|
|
104
105
|
self.assertEqual(DATABASES['default']['PASSWORD'], 'Test_insecure_password')
|
|
105
106
|
self.assertEqual(DATABASES['default']['PORT'], 3306)
|
|
106
107
|
# test our password is encoded when logged
|
|
107
|
-
self.assertTrue("Test_insecure_password" not in
|
|
108
|
-
self.assertTrue("Test_************sword" in
|
|
108
|
+
self.assertTrue("Test_insecure_password" not in sout.getvalue())
|
|
109
|
+
self.assertTrue("Test_************sword" in sout.getvalue())
|
|
109
110
|
# test a None still works for initialization
|
|
110
111
|
self.assertIsNotNone(Environment())
|
|
111
112
|
|
|
113
|
+
def test_override_database_urls(self):
|
|
114
|
+
# we will start with the default dict for a new django install
|
|
115
|
+
BASE_DIR = Path(__file__).resolve().parent
|
|
116
|
+
DATABASES = {
|
|
117
|
+
'default': {
|
|
118
|
+
'ENGINE': 'django.db.backends.sqlite3',
|
|
119
|
+
'NAME': BASE_DIR / 'db.sqlite3',
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
db_override = BASE_DIR / "sqlite.db"
|
|
123
|
+
os_vars = {
|
|
124
|
+
"DJ_URL_default": f"://@/{db_override}",
|
|
125
|
+
# "DATABASES__default__NAME": BASE_DIR / "sqlite.db",
|
|
126
|
+
"TEST_DATE": "2023-02-21 08:30:00",
|
|
127
|
+
"TEST_INT": "1",
|
|
128
|
+
"PW": "abc1234def",
|
|
129
|
+
"TEST_STRING": "abc1234def"
|
|
130
|
+
}
|
|
131
|
+
environment = Environment(environment_variable_map=os_vars)
|
|
132
|
+
# test we can override just the database file name
|
|
133
|
+
with redirect_stdout(StringIO()) as djurl_out:
|
|
134
|
+
DATABASES = environment.override_database_urls(DATABASES)
|
|
135
|
+
print(f"djurl password log output: \n{djurl_out.getvalue()}")
|
|
136
|
+
|
|
137
|
+
self.assertEqual(DATABASES['default']['NAME'], str(db_override))
|
|
138
|
+
# test we can take the default sqlite database and change it to a full mysql connection using env vars
|
|
139
|
+
# os_vars = {
|
|
140
|
+
# 'DATABASES__default__ENGINE': 'django.db.backends.mysql',
|
|
141
|
+
# 'DATABASES__default__HOST': 'testdb.example.org',
|
|
142
|
+
# 'DATABASES__default__NAME': 'test',
|
|
143
|
+
# 'DATABASES__default__USER': 'testuser',
|
|
144
|
+
# 'DATABASES__default__PASSWORD': 'Test_insecure_password',
|
|
145
|
+
# 'DATABASES__default__PORT': 3306,
|
|
146
|
+
# }
|
|
147
|
+
os_vars = {
|
|
148
|
+
'DJ_URL_default': 'django.db.backends.mysql://testuser:Test_insecure_password@testdb.example.org:3306/test'
|
|
149
|
+
}
|
|
150
|
+
environment = Environment(environment_variable_map=os_vars)
|
|
151
|
+
# test we can override the default sqllite for dev laptops with a full mysql connection on a deployed server
|
|
152
|
+
with redirect_stdout(StringIO()) as djurl_out:
|
|
153
|
+
DATABASES = environment.override_database_urls(DATABASES)
|
|
154
|
+
print(f"djurl full log output \n{djurl_out.getvalue()}")
|
|
155
|
+
self.assertEqual(DATABASES['default']['ENGINE'], 'django.db.backends.mysql')
|
|
156
|
+
self.assertEqual(DATABASES['default']['HOST'], 'testdb.example.org')
|
|
157
|
+
self.assertEqual(DATABASES['default']['NAME'], 'test')
|
|
158
|
+
self.assertEqual(DATABASES['default']['USER'], 'testuser')
|
|
159
|
+
self.assertEqual(DATABASES['default']['PASSWORD'], 'Test_insecure_password')
|
|
160
|
+
self.assertEqual(DATABASES['default']['PORT'], 3306)
|
|
161
|
+
# test our password is encoded when logged
|
|
162
|
+
self.assertTrue("Test_insecure_password" not in djurl_out.getvalue())
|
|
163
|
+
|
|
164
|
+
|
|
112
165
|
def test_timer(self):
|
|
113
166
|
timer = Timer().start()
|
|
114
167
|
time.sleep(3)
|
|
@@ -4,6 +4,7 @@ from io import StringIO
|
|
|
4
4
|
|
|
5
5
|
from ubercode.utils.urls import ParsedUrl
|
|
6
6
|
from ubercode.utils.urls import ParsedQueryString
|
|
7
|
+
from ubercode.utils.urls import DjUrl
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
class TestUrls(unittest.TestCase):
|
|
@@ -49,7 +50,46 @@ class TestUrls(unittest.TestCase):
|
|
|
49
50
|
parsed_url = ParsedUrl(test_uri, default_scheme='http', default_netloc='localhost:8000', default_filepath='/booger/')
|
|
50
51
|
self.assertEqual("tel", str(parsed_url.scheme))
|
|
51
52
|
self.assertEqual(test_uri, parsed_url.url)
|
|
52
|
-
|
|
53
|
+
# test that a full django database url parses to the individual parts correctly
|
|
54
|
+
dj_db_url = DjUrl('django.db.backends.mysql://scott:tiger@localhost:1366/test')
|
|
55
|
+
self.assertEqual(dj_db_url.engine,'django.db.backends.mysql')
|
|
56
|
+
self.assertEqual(dj_db_url.host, 'localhost')
|
|
57
|
+
self.assertEqual(dj_db_url.user, 'scott')
|
|
58
|
+
self.assertEqual(dj_db_url.password, 'tiger')
|
|
59
|
+
self.assertEqual(dj_db_url.port, 1366)
|
|
60
|
+
self.assertEqual(dj_db_url.name, 'test')
|
|
61
|
+
# test that str masks password
|
|
62
|
+
self.assertNotIn(str(dj_db_url), 'tiger')
|
|
63
|
+
# test that a missing one is falsy so it doesn't get overridden at startup
|
|
64
|
+
dj_db_url = DjUrl('://localhost/test')
|
|
65
|
+
self.assertFalse(dj_db_url.engine)
|
|
66
|
+
self.assertFalse(dj_db_url.user)
|
|
67
|
+
self.assertFalse(dj_db_url.password)
|
|
68
|
+
self.assertFalse(dj_db_url.port)
|
|
69
|
+
self.assertTrue(dj_db_url.host)
|
|
70
|
+
self.assertTrue(dj_db_url.name)
|
|
71
|
+
# test that an empty url doesn't break and returns false for everything
|
|
72
|
+
dj_db_url = DjUrl('')
|
|
73
|
+
self.assertFalse(dj_db_url.engine)
|
|
74
|
+
self.assertFalse(dj_db_url.user)
|
|
75
|
+
self.assertFalse(dj_db_url.password)
|
|
76
|
+
self.assertFalse(dj_db_url.port)
|
|
77
|
+
self.assertFalse(dj_db_url.host)
|
|
78
|
+
self.assertFalse(dj_db_url.name)
|
|
79
|
+
# test that str returns an encoded version that may include separators but can be re-initted correctly
|
|
80
|
+
dj_db_url = DjUrl(str(dj_db_url))
|
|
81
|
+
self.assertFalse(dj_db_url.engine)
|
|
82
|
+
self.assertFalse(dj_db_url.user)
|
|
83
|
+
self.assertFalse(dj_db_url.password)
|
|
84
|
+
self.assertFalse(dj_db_url.port)
|
|
85
|
+
self.assertFalse(dj_db_url.host)
|
|
86
|
+
self.assertFalse(dj_db_url.name)
|
|
87
|
+
# test that just password works since this is most common
|
|
88
|
+
dj_db_url = DjUrl(':asdf:!/stuff #')
|
|
89
|
+
self.assertEqual(dj_db_url.password, 'asdf:!/stuff #')
|
|
90
|
+
# test encoded @ for password since that could be needed
|
|
91
|
+
dj_db_url = DjUrl(':asfcasdf23%401:/!?--atencoded')
|
|
92
|
+
self.assertEqual(dj_db_url.password, 'asfcasdf23@1:/!')
|
|
53
93
|
|
|
54
94
|
# --- basic retrieval
|
|
55
95
|
# -------------------
|
|
@@ -13,7 +13,7 @@ FALSE_VALUES = [None, False, 0, "0", "n", "f", "false", "no", "off"]
|
|
|
13
13
|
|
|
14
14
|
|
|
15
15
|
# -------- helper utilities ----------
|
|
16
|
-
def strip(value: str
|
|
16
|
+
def strip(value: str | None, strip_chars: str = None, left: bool = True, right: bool = True) -> str | None:
|
|
17
17
|
"""
|
|
18
18
|
return stripped value if possible or original value
|
|
19
19
|
:param value: str value to be stripped
|
|
@@ -94,7 +94,7 @@ def to_bool(value) -> bool:
|
|
|
94
94
|
return bool(value)
|
|
95
95
|
|
|
96
96
|
|
|
97
|
-
def is_true(value: int
|
|
97
|
+
def is_true(value: int | bool | str) -> bool:
|
|
98
98
|
"""
|
|
99
99
|
Convert <value> to a True boolean value. Useful when you want to convert a passed parameter to True if it matches
|
|
100
100
|
one of the defined TRUE_VALUES above; otherwise False.
|
|
@@ -121,7 +121,7 @@ def to_js_bool(bool_value: bool) -> str:
|
|
|
121
121
|
return "false"
|
|
122
122
|
|
|
123
123
|
|
|
124
|
-
def to_int(value, default: int = 0, none_to_default: bool = True, suppress_warnings: bool = True) -> int
|
|
124
|
+
def to_int(value, default: int | None = 0, none_to_default: bool = True, suppress_warnings: bool = True) -> int | None:
|
|
125
125
|
"""
|
|
126
126
|
Convert <value> to int. Will always return integer or none instead of throwing exception
|
|
127
127
|
@param value: value to be converted
|
|
@@ -201,7 +201,7 @@ def from_iso8601_compact(value: Any = None, tz: timezone = timezone.utc):
|
|
|
201
201
|
return _value
|
|
202
202
|
|
|
203
203
|
|
|
204
|
-
def to_date(value: Any = None, tz: timezone
|
|
204
|
+
def to_date(value: Any = None, tz: timezone | None = timezone.utc, none_to_now: bool = True, suppress_warnings: bool = True):
|
|
205
205
|
"""
|
|
206
206
|
Convert string to python date. Currently, only concerned about iso8601 and db type formats.
|
|
207
207
|
None returns current date by default but can be overridden with none_to_now optional parameter
|
|
@@ -232,7 +232,7 @@ def to_date(value: Any = None, tz: timezone or None = timezone.utc, none_to_now:
|
|
|
232
232
|
|
|
233
233
|
|
|
234
234
|
# -------- helper conversions --------
|
|
235
|
-
def to_mask(value: str
|
|
235
|
+
def to_mask(value: str | None) -> str | None:
|
|
236
236
|
_mask = value
|
|
237
237
|
if isinstance(value, str) and value is not None and len(value) > 0:
|
|
238
238
|
# if we are less than 4 chars then mask the entire string
|
|
@@ -9,7 +9,7 @@ import xml.etree.ElementTree as Etree
|
|
|
9
9
|
from collections import defaultdict
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
class
|
|
12
|
+
class JsonData:
|
|
13
13
|
""" simple json class to encapsulate basic json operations """
|
|
14
14
|
def __init__(self, json_string: str or None = None, encode_ampersands: bool = False):
|
|
15
15
|
# data is core python objects (list, dict, object, etc) from the core python JSON.loads
|
|
@@ -49,7 +49,7 @@ class JSON:
|
|
|
49
49
|
return str(self.data)
|
|
50
50
|
|
|
51
51
|
|
|
52
|
-
class
|
|
52
|
+
class XmlData:
|
|
53
53
|
""" simple xml class to encapsulate basic xml operations using build in python ETree """
|
|
54
54
|
def __init__(self, xml_string: str or None = None, encode_ampersands: bool = False):
|
|
55
55
|
# data is core python ElementTree object
|
|
@@ -95,7 +95,7 @@ class XML:
|
|
|
95
95
|
output to dict
|
|
96
96
|
:return: dict
|
|
97
97
|
"""
|
|
98
|
-
return
|
|
98
|
+
return XmlData.tree_to_dict(self.data)
|
|
99
99
|
|
|
100
100
|
@staticmethod
|
|
101
101
|
def tree_to_dict(t: Etree) -> dict:
|
|
@@ -108,7 +108,7 @@ class XML:
|
|
|
108
108
|
children = list(t)
|
|
109
109
|
if children:
|
|
110
110
|
dd = defaultdict(list)
|
|
111
|
-
for dc in map(
|
|
111
|
+
for dc in map(XmlData.tree_to_dict, children):
|
|
112
112
|
for k, v in dc.items():
|
|
113
113
|
dd[k].append(v)
|
|
114
114
|
d = {t.tag: {k: v[0] if len(v) == 1 else v for k, v in dd.items()}}
|
|
@@ -1,7 +1,26 @@
|
|
|
1
1
|
""" common utilities for working with dataframes"""
|
|
2
2
|
from typing import Any
|
|
3
3
|
from . import logging
|
|
4
|
+
from datetime import datetime
|
|
4
5
|
|
|
6
|
+
default_date_formats = {
|
|
7
|
+
'date': '%Y-%m-%d',
|
|
8
|
+
'datetime': '%Y-%m-%d %H:%M:%S',
|
|
9
|
+
'datetimemilli': '%Y-%m-%d %H:%M:%S.%f'
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
def to_date_str(date_string: str or None, date_col: str, date_field_map: dict, date_formats: dict) -> str or None:
|
|
13
|
+
if date_string == 'None' or date_string == 'NaT' or not date_string:
|
|
14
|
+
return None
|
|
15
|
+
if '.' in date_string:
|
|
16
|
+
dt = datetime.strptime(date_string, date_formats['datetimemilli'])
|
|
17
|
+
elif ':' in date_string:
|
|
18
|
+
dt = datetime.strptime(date_string, date_formats['datetime'])
|
|
19
|
+
else:
|
|
20
|
+
dt = datetime.strptime(date_string, date_formats['date'])
|
|
21
|
+
if not dt:
|
|
22
|
+
return None
|
|
23
|
+
return dt.strftime(date_formats[date_field_map[date_col]])
|
|
5
24
|
|
|
6
25
|
# extend the logging to include log.dataframe()
|
|
7
26
|
# NOTE: making dataframe type Any, so we don't have to include pandas but intended use is dataframe
|
|
@@ -6,8 +6,11 @@ import os
|
|
|
6
6
|
import time
|
|
7
7
|
from datetime import datetime
|
|
8
8
|
from typing import Any, Tuple
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
9
11
|
from ubercode.utils.logging import ColorLogger
|
|
10
12
|
from ubercode.utils import convert
|
|
13
|
+
from ubercode.utils.urls import DjUrl
|
|
11
14
|
|
|
12
15
|
_utils_settings_logger = ColorLogger("utils.environment")
|
|
13
16
|
|
|
@@ -223,3 +226,99 @@ class Environment:
|
|
|
223
226
|
self._logger.warn(
|
|
224
227
|
f"{db_parts[0]}[{db_parts[1]}][{db_parts[2]}] has a database or property naming issue!")
|
|
225
228
|
return db_dict
|
|
229
|
+
|
|
230
|
+
def override_database_urls(self, db_dict: dict) -> dict:
|
|
231
|
+
"""
|
|
232
|
+
Much like above,iterates over environment variables and overrides any database variables. However, instead
|
|
233
|
+
of looking for each variable with a pattern like DATABASES__default__ENGINE it looks for DJ_URL_ prefix and
|
|
234
|
+
parses the url into a DjUrl object. This allows property files to use one line per database config
|
|
235
|
+
instead of one key,value pair per variable like: DJ_URL_default = 'django.db.backends.mysql://scott:tiger@localhost:1366/test'
|
|
236
|
+
NOTE: you can omit everything except what you want to replace like: '://:newpassword@' which
|
|
237
|
+
will only replace the password
|
|
238
|
+
|
|
239
|
+
:param db_dict: settings database dictionary to replace variables in ex: DATABASES
|
|
240
|
+
:return: new dict with updated overridden values
|
|
241
|
+
"""
|
|
242
|
+
items = []
|
|
243
|
+
if hasattr(self._env_map, "items"):
|
|
244
|
+
items = self._env_map.items()
|
|
245
|
+
elif hasattr(os.environ, "items"):
|
|
246
|
+
items = os.environ.items()
|
|
247
|
+
for k, v in items:
|
|
248
|
+
# unlike override_database_variables() we will look for prefix DJ_URL_
|
|
249
|
+
if k and str(k).upper().startswith('DJ_URL_') and v:
|
|
250
|
+
# the actual key is the remaining part left
|
|
251
|
+
key = str(k)[len('DJ_URL_'):]
|
|
252
|
+
djurl = DjUrl(str(v))
|
|
253
|
+
for attr, value in vars(djurl).items():
|
|
254
|
+
if value:
|
|
255
|
+
if not db_dict.get(key):
|
|
256
|
+
self._logger.warn(f'Missing database key [{key}]! creating...')
|
|
257
|
+
db_dict[key] = {}
|
|
258
|
+
_log_from_value = db_dict[key].get(attr.upper(), 'None')
|
|
259
|
+
_log_to_value = value
|
|
260
|
+
if attr.upper() in self._secret_properties:
|
|
261
|
+
_log_to_value = convert.to_mask(value)
|
|
262
|
+
db_dict[key][attr.upper()] = value
|
|
263
|
+
self._logger.info(
|
|
264
|
+
f'set [{key}][{attr.upper()}] from [{_log_from_value}] to [{_log_to_value}]')
|
|
265
|
+
return db_dict
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
class FauxApp:
|
|
269
|
+
def __init__(self, logger: ColorLogger = None, notebook_path: Path = Path(), default_dict: dict = None) -> None:
|
|
270
|
+
self._logger = logger if logger else _utils_settings_logger
|
|
271
|
+
self.notebook_path = notebook_path.resolve()
|
|
272
|
+
self.app_path = os.path.dirname(self.notebook_path)
|
|
273
|
+
self.project_path = os.path.dirname(self.app_path)
|
|
274
|
+
self.instance_path = os.path.join(self.project_path, 'instance')
|
|
275
|
+
self.config = default_dict or dict(
|
|
276
|
+
SECRET_KEY = 'localmachine',
|
|
277
|
+
LOG_LEVEL = 'DEBUG',
|
|
278
|
+
DEBUG = True,
|
|
279
|
+
APP_DIR = self.app_path,
|
|
280
|
+
PROJECT_DIR = self.project_path,
|
|
281
|
+
DATABASE_DEBUG = False,
|
|
282
|
+
SA_URL_APP = f'sqlite+pysqlite:///{os.path.join(self.instance_path, "nbsync.sqlite3")}',
|
|
283
|
+
SA_URL_SRC_LOCAL = f'sqlite+pysqlite:///{os.path.join(self.instance_path, "src.sqlite3")}',
|
|
284
|
+
SA_URL_DST_LOCAL = f'sqlite+pysqlite:///{os.path.join(self.instance_path, "dst.sqlite3")}',
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
def from_mapping(self, mapping: dict) -> None:
|
|
288
|
+
self.config = self.config | mapping
|
|
289
|
+
|
|
290
|
+
def from_pyfile(self, config_file: str = '~/conf/nbsync.cfg') -> None:
|
|
291
|
+
# read the config file into dict if exists then merge
|
|
292
|
+
abs_cfg = os.path.expanduser(config_file)
|
|
293
|
+
try:
|
|
294
|
+
with open(abs_cfg, 'r') as fp:
|
|
295
|
+
for line in fp:
|
|
296
|
+
line = line.strip()
|
|
297
|
+
if line.startswith('#') or not line:
|
|
298
|
+
continue
|
|
299
|
+
# Split only on the first '=' to allow '=' in the value
|
|
300
|
+
try:
|
|
301
|
+
key, val = line.split('=', 1)
|
|
302
|
+
self.config[key.strip().strip("'").strip('"')] = val.strip().strip("'").strip('"')
|
|
303
|
+
except ValueError:
|
|
304
|
+
# Handle lines that might not have an '='
|
|
305
|
+
continue
|
|
306
|
+
except FileNotFoundError:
|
|
307
|
+
self._logger.debug(f'[{config_file}] does not exist')
|
|
308
|
+
|
|
309
|
+
def from_prefixed_env(self, prefix: str = 'UC'):
|
|
310
|
+
# read environment variables with the given prefix and merge into config
|
|
311
|
+
prefix_len = len(prefix) + 1 # +1 for the underscore
|
|
312
|
+
for key, value in os.environ.items():
|
|
313
|
+
if key.startswith(f'{prefix}_'):
|
|
314
|
+
config_key = key[prefix_len:] # remove the prefix and underscore
|
|
315
|
+
self.config[config_key] = value
|
|
316
|
+
# lastly, attempt to convert 'true'/'false' to boolean
|
|
317
|
+
if value.lower() == 'true':
|
|
318
|
+
self.config[config_key] = True
|
|
319
|
+
elif value.lower() == 'false':
|
|
320
|
+
self.config[config_key] = False
|
|
321
|
+
|
|
322
|
+
def __repr__(self):
|
|
323
|
+
return convert.obj_to_str(self)
|
|
324
|
+
|
|
@@ -183,7 +183,7 @@ class ColorLogger:
|
|
|
183
183
|
c_msg = str(msg)
|
|
184
184
|
if self.color_output and color:
|
|
185
185
|
c_msg = color + c_msg + TermColor.ENDC
|
|
186
|
-
if msg == self.repeat_msg:
|
|
186
|
+
if str(msg) == self.repeat_msg:
|
|
187
187
|
# the first time we start repeating track the indent level
|
|
188
188
|
if not self.repeat_cnt:
|
|
189
189
|
if indent is not None:
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import os
|
|
2
2
|
from urllib.parse import urlsplit
|
|
3
|
-
from ubercode.utils.convert import to_str
|
|
3
|
+
from ubercode.utils.convert import to_str, to_int
|
|
4
4
|
from pathlib import PurePath
|
|
5
|
-
|
|
5
|
+
from .convert import to_mask
|
|
6
6
|
|
|
7
7
|
class ParsedQueryString:
|
|
8
8
|
"""
|
|
@@ -216,6 +216,99 @@ class ParsedUrl:
|
|
|
216
216
|
"""
|
|
217
217
|
return self.url
|
|
218
218
|
|
|
219
|
+
class DjUrl:
|
|
220
|
+
engine = None
|
|
221
|
+
user = None
|
|
222
|
+
password = None
|
|
223
|
+
host = None
|
|
224
|
+
port = None
|
|
225
|
+
name = None
|
|
226
|
+
|
|
227
|
+
def __init__(self, dj_url: str = None) -> None:
|
|
228
|
+
"""
|
|
229
|
+
parses a packed "django_url" into its parts following similar rules to SqlAlchemy
|
|
230
|
+
format: engine://user:password@host[:port]/dbname
|
|
231
|
+
example: django.db.backends.mysql://scott:tiger@localhost:1366/test
|
|
232
|
+
|
|
233
|
+
NOTE: asking for the string value will give back the original packed url masking the password
|
|
234
|
+
NOTE: to_dict will give back the dictionary values to be added or replaced in the DATABASES dict
|
|
235
|
+
|
|
236
|
+
:param url: packed django url ex: django.db.backends.mysql://scott:tiger@localhost:1366/test
|
|
237
|
+
"""
|
|
238
|
+
dj_url = dj_url or ""
|
|
239
|
+
dj_url = dj_url.strip()
|
|
240
|
+
encoded = False
|
|
241
|
+
if dj_url.endswith('?--atencoded'):
|
|
242
|
+
encoded = True
|
|
243
|
+
dj_url = dj_url[:-len('?--atencoded')].strip()
|
|
244
|
+
if not dj_url:
|
|
245
|
+
return
|
|
246
|
+
pos = dj_url.find('://')
|
|
247
|
+
if pos > -1:
|
|
248
|
+
self.engine = dj_url[:pos].strip() or None
|
|
249
|
+
constr = dj_url[pos + len('://'):].strip()
|
|
250
|
+
else:
|
|
251
|
+
constr = dj_url
|
|
252
|
+
pos = constr.find("@")
|
|
253
|
+
if pos > -1:
|
|
254
|
+
loginstr = constr[:pos].strip()
|
|
255
|
+
constr = constr[pos + len("@"):].strip()
|
|
256
|
+
pos = loginstr.find(':')
|
|
257
|
+
if pos > -1:
|
|
258
|
+
self.user = loginstr[:pos].strip() or None
|
|
259
|
+
self.password = loginstr[pos + len(':'):].strip() or None
|
|
260
|
+
if encoded:
|
|
261
|
+
self.password = self.password.replace('%40', '@')
|
|
262
|
+
elif len(loginstr) > 0:
|
|
263
|
+
self.user = loginstr or None
|
|
264
|
+
else:
|
|
265
|
+
# since password is the most common replacement look for that specifically next if we didn't have an @
|
|
266
|
+
# NOTE: since password can contain / we will assume the only thing there is the password otherwise use @
|
|
267
|
+
# Ex: DjUrl(':newpassword/newdatabase') -> password=newpassword/newdatabase name=None
|
|
268
|
+
# DjUrl(':newpassword@/newdatabase') -> password=newpassword name=newdatabase
|
|
269
|
+
# DjUrl(':asfcasdf23%401:/!?--atencoded') -> password=asfcasdf23@1:/! name=None port=None
|
|
270
|
+
if constr.startswith(':'):
|
|
271
|
+
self.password = constr.strip(':')
|
|
272
|
+
constr = ''
|
|
273
|
+
if encoded:
|
|
274
|
+
self.password = self.password.replace('%40', '@')
|
|
275
|
+
# NOTE: constr now contains everything after @ - no engine, user, password
|
|
276
|
+
# NOTE: may have port but no db ex: @:8080
|
|
277
|
+
pos = constr.find('/')
|
|
278
|
+
if pos > -1:
|
|
279
|
+
hoststr = constr[:pos].strip()
|
|
280
|
+
self.name = constr[pos + len('/'):].strip() or None
|
|
281
|
+
else:
|
|
282
|
+
hoststr = constr
|
|
283
|
+
# all that is left is hoststr
|
|
284
|
+
pos = hoststr.find(':')
|
|
285
|
+
if pos > -1:
|
|
286
|
+
self.host = hoststr[:pos].strip() or None
|
|
287
|
+
self.port = hoststr[pos + len(':'):].strip() or None
|
|
288
|
+
if self.port:
|
|
289
|
+
self.port = to_int(self.port, default=None, none_to_default=False)
|
|
290
|
+
elif len(hoststr) > 0:
|
|
291
|
+
self.host = hoststr
|
|
292
|
+
|
|
293
|
+
def to_dict(self) -> dict:
|
|
294
|
+
dct = {}
|
|
295
|
+
for attr, value in vars(self).items():
|
|
296
|
+
dct[attr.upper()] = value
|
|
297
|
+
return dct
|
|
298
|
+
|
|
299
|
+
def __str__(self) -> str:
|
|
300
|
+
url = f"{self.engine or ''}://"
|
|
301
|
+
if self.user:
|
|
302
|
+
url += self.user
|
|
303
|
+
if self.password:
|
|
304
|
+
url += f":{to_mask(self.password)}"
|
|
305
|
+
url += f"@{self.host or ''}"
|
|
306
|
+
if self.port:
|
|
307
|
+
url += f":{self.port}"
|
|
308
|
+
if self.name:
|
|
309
|
+
url += f"/{self.name}"
|
|
310
|
+
return url
|
|
311
|
+
|
|
219
312
|
|
|
220
313
|
if __name__ == "__main__":
|
|
221
314
|
# test_uri = "http://localhost:8000/test1/?id=1&x=2"
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: python-ubercode-utils
|
|
3
|
-
Version: 2.0.0
|
|
4
|
-
Summary: Core python utilities for all apps
|
|
5
|
-
Home-page: https://github.com/sstacha/python-ubercode-utils
|
|
6
|
-
Author: Steve Stacha
|
|
7
|
-
Author-email: sstacha@gmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Description: # python-ubercode-utils
|
|
10
|
-
Extracting common python utilities re-used between all projects. The intent is to have minimal dependencies
|
|
11
|
-
so the library can be used by django settings without circular references. I also have color logging class for
|
|
12
|
-
jupyter notebooks. I will have a couple of libraries that will extend this functionality. Scan the test cases in the
|
|
13
|
-
tests folder for common use cases.
|
|
14
|
-
|
|
15
|
-
python-utils-core:
|
|
16
|
-
- basic conversion helper utilities
|
|
17
|
-
- color logging without dependencies
|
|
18
|
-
- manipulating urls and their parameters
|
|
19
|
-
- helper classes to make working with xml and json data easier
|
|
20
|
-
- minimal helper classes to convert database cursor results to dictionaries or tuples
|
|
21
|
-
|
|
22
|
-
Platform: UNKNOWN
|
|
23
|
-
Classifier: Development Status :: 3 - Alpha
|
|
24
|
-
Classifier: Programming Language :: Python :: 3
|
|
25
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
26
|
-
Classifier: Operating System :: OS Independent
|
|
27
|
-
Classifier: Topic :: Utilities
|
|
28
|
-
Requires-Python: >=3.8
|
|
29
|
-
Description-Content-Type: text/markdown
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.1
|
|
2
|
-
Name: python-ubercode-utils
|
|
3
|
-
Version: 2.0.0
|
|
4
|
-
Summary: Core python utilities for all apps
|
|
5
|
-
Home-page: https://github.com/sstacha/python-ubercode-utils
|
|
6
|
-
Author: Steve Stacha
|
|
7
|
-
Author-email: sstacha@gmail.com
|
|
8
|
-
License: MIT
|
|
9
|
-
Description: # python-ubercode-utils
|
|
10
|
-
Extracting common python utilities re-used between all projects. The intent is to have minimal dependencies
|
|
11
|
-
so the library can be used by django settings without circular references. I also have color logging class for
|
|
12
|
-
jupyter notebooks. I will have a couple of libraries that will extend this functionality. Scan the test cases in the
|
|
13
|
-
tests folder for common use cases.
|
|
14
|
-
|
|
15
|
-
python-utils-core:
|
|
16
|
-
- basic conversion helper utilities
|
|
17
|
-
- color logging without dependencies
|
|
18
|
-
- manipulating urls and their parameters
|
|
19
|
-
- helper classes to make working with xml and json data easier
|
|
20
|
-
- minimal helper classes to convert database cursor results to dictionaries or tuples
|
|
21
|
-
|
|
22
|
-
Platform: UNKNOWN
|
|
23
|
-
Classifier: Development Status :: 3 - Alpha
|
|
24
|
-
Classifier: Programming Language :: Python :: 3
|
|
25
|
-
Classifier: License :: OSI Approved :: MIT License
|
|
26
|
-
Classifier: Operating System :: OS Independent
|
|
27
|
-
Classifier: Topic :: Utilities
|
|
28
|
-
Requires-Python: >=3.8
|
|
29
|
-
Description-Content-Type: text/markdown
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|