python-ubercode-utils 2.0.3__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.
Files changed (27) hide show
  1. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/PKG-INFO +1 -1
  2. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/python_ubercode_utils.egg-info/PKG-INFO +1 -1
  3. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/setup.py +1 -1
  4. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/test/test_environment.py +58 -5
  5. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/test/test_urls.py +41 -1
  6. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/ubercode/utils/convert.py +5 -5
  7. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/ubercode/utils/environment.py +40 -0
  8. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/ubercode/utils/urls.py +95 -2
  9. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/LICENSE +0 -0
  10. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/MANIFEST.in +0 -0
  11. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/README.md +0 -0
  12. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/python_ubercode_utils.egg-info/SOURCES.txt +0 -0
  13. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/python_ubercode_utils.egg-info/dependency_links.txt +0 -0
  14. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/python_ubercode_utils.egg-info/not-zip-safe +0 -0
  15. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/python_ubercode_utils.egg-info/top_level.txt +0 -0
  16. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/setup.cfg +0 -0
  17. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/test/test_convert.py +0 -0
  18. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/test/test_cursor.py +0 -0
  19. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/test/test_data.py +0 -0
  20. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/test/test_dataframe.py +0 -0
  21. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/test/test_logging.py +0 -0
  22. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/ubercode/__init__.py +0 -0
  23. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/ubercode/utils/__init__.py +0 -0
  24. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/ubercode/utils/cursor.py +0 -0
  25. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/ubercode/utils/data.py +0 -0
  26. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/ubercode/utils/dataframe.py +0 -0
  27. {python_ubercode_utils-2.0.3 → python_ubercode_utils-2.0.4}/ubercode/utils/logging.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python_ubercode_utils
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Core python utilities for all apps
5
5
  Home-page: https://github.com/sstacha/python-ubercode-utils
6
6
  Author: Steve Stacha
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: python-ubercode-utils
3
- Version: 2.0.3
3
+ Version: 2.0.4
4
4
  Summary: Core python utilities for all apps
5
5
  Home-page: https://github.com/sstacha/python-ubercode-utils
6
6
  Author: Steve Stacha
@@ -4,7 +4,7 @@ with open("README.md", "r") as fh:
4
4
  long_description = fh.read()
5
5
 
6
6
  setuptools.setup(name='python_ubercode_utils',
7
- version='2.0.3',
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",
@@ -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
- DATABASES = environment.override_database_variables(DATABASES)
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
- log_output = sout.getvalue()
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 log_output)
108
- self.assertTrue("Test_************sword" in log_output)
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 or None, strip_chars: str = None, left: bool = True, right: bool = True) -> str or None:
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 or bool or str) -> bool:
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 or None:
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 or None = timezone.utc, none_to_now: bool = True, suppress_warnings: bool = True):
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 or None) -> str or None:
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
@@ -7,8 +7,10 @@ import time
7
7
  from datetime import datetime
8
8
  from typing import Any, Tuple
9
9
  from pathlib import Path
10
+
10
11
  from ubercode.utils.logging import ColorLogger
11
12
  from ubercode.utils import convert
13
+ from ubercode.utils.urls import DjUrl
12
14
 
13
15
  _utils_settings_logger = ColorLogger("utils.environment")
14
16
 
@@ -225,6 +227,44 @@ class Environment:
225
227
  f"{db_parts[0]}[{db_parts[1]}][{db_parts[2]}] has a database or property naming issue!")
226
228
  return db_dict
227
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
+
228
268
  class FauxApp:
229
269
  def __init__(self, logger: ColorLogger = None, notebook_path: Path = Path(), default_dict: dict = None) -> None:
230
270
  self._logger = logger if logger else _utils_settings_logger
@@ -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"