mipi-datamanager 1.4.7__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.
- mipi_datamanager-1.4.7/PKG-INFO +35 -0
- mipi_datamanager-1.4.7/README.md +3 -0
- mipi_datamanager-1.4.7/pyproject.toml +40 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/__init__.py +14 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/connection.py +54 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/__init__.py +0 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/common.py +183 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/data_managers.py +1622 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/docs/__init__.py +0 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/docs/contribute.py +5 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/docs/getting_started.py +261 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/docs/install.py +17 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/docs/patch_notes.py +3 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/docs/setup.py +54 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/file_search.md +64 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/file_search.py +429 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/file_search.tcss +8 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/file_search_gui.py +633 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/jinja/__init__.py +2 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/jinja/filters.py +55 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/jinja/jinja.py +329 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/meta.py +68 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/read_setup.py +313 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/templates/jinja_header.txt +11 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/templates/jinja_repo_header.txt +14 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/core/templates/mipi_summary.txt +0 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/errors.py +38 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/formatters.py +264 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/generate_inserts.py +148 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/query.py +127 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/types.py +6 -0
- mipi_datamanager-1.4.7/src/mipi_datamanager/wrangle.py +133 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: mipi_datamanager
|
|
3
|
+
Version: 1.4.7
|
|
4
|
+
Summary:
|
|
5
|
+
Author: murphy-sean1
|
|
6
|
+
Author-email: murphy-sean1@cooperhealth.edu
|
|
7
|
+
Requires-Python: >=3.12,<4.0
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
10
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
11
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
12
|
+
Requires-Dist: flask (>=3.0.3,<4.0.0)
|
|
13
|
+
Requires-Dist: flask-bcrypt (>=1.0.1,<2.0.0)
|
|
14
|
+
Requires-Dist: flask-restful (>=0.3.10,<0.4.0)
|
|
15
|
+
Requires-Dist: jinja2 (>=3.1.2,<4.0.0)
|
|
16
|
+
Requires-Dist: jinjasql2 (>=0.1.12,<0.2.0)
|
|
17
|
+
Requires-Dist: mkdocs (>=1.6.0,<2.0.0)
|
|
18
|
+
Requires-Dist: mkdocs-material (>=9.5.33,<10.0.0)
|
|
19
|
+
Requires-Dist: mkdocstrings[python] (>=0.26.1,<0.27.0)
|
|
20
|
+
Requires-Dist: openpyxl (>=3.1.5,<4.0.0)
|
|
21
|
+
Requires-Dist: pandas (>=2.2.2,<3.0.0)
|
|
22
|
+
Requires-Dist: pylint (>=3.3.3,<4.0.0)
|
|
23
|
+
Requires-Dist: pymdown-extensions (>=10.11.2,<11.0.0)
|
|
24
|
+
Requires-Dist: pyodbc (>=5.1.0,<6.0.0)
|
|
25
|
+
Requires-Dist: pytest (>=8.3.2,<9.0.0)
|
|
26
|
+
Requires-Dist: pytest-coverage (>=0.0,<0.1)
|
|
27
|
+
Requires-Dist: sqlalchemy (>=2.0.35,<3.0.0)
|
|
28
|
+
Requires-Dist: textual-dev (>=1.7.0,<2.0.0)
|
|
29
|
+
Requires-Dist: textual[syntax] (>=3.2.0,<4.0.0)
|
|
30
|
+
Description-Content-Type: text/markdown
|
|
31
|
+
|
|
32
|
+
# mipi_datamanager
|
|
33
|
+
|
|
34
|
+
Python utilities and pipelines for the MiPi Data Manager. See docs and usage in the repo.
|
|
35
|
+
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
[tool.poetry]
|
|
2
|
+
name = "mipi_datamanager"
|
|
3
|
+
version = "1.4.7"
|
|
4
|
+
description = ""
|
|
5
|
+
authors = ["murphy-sean1 <murphy-sean1@cooperhealth.edu>"]
|
|
6
|
+
readme = "README.md"
|
|
7
|
+
|
|
8
|
+
[tool.poetry.dependencies]
|
|
9
|
+
python = "^3.12"
|
|
10
|
+
pandas = "^2.2.2"
|
|
11
|
+
jinja2 = "^3.1.2"
|
|
12
|
+
pytest = "^8.3.2"
|
|
13
|
+
pyodbc = "^5.1.0"
|
|
14
|
+
openpyxl = "^3.1.5"
|
|
15
|
+
pytest-coverage = "^0.0"
|
|
16
|
+
mkdocs = "^1.6.0"
|
|
17
|
+
mkdocs-material = "^9.5.33"
|
|
18
|
+
flask = "^3.0.3"
|
|
19
|
+
flask-restful = "^0.3.10"
|
|
20
|
+
flask-bcrypt = "^1.0.1"
|
|
21
|
+
sqlalchemy = "^2.0.35"
|
|
22
|
+
mkdocstrings = {extras = ["python"], version = "^0.26.1"}
|
|
23
|
+
pymdown-extensions = "^10.11.2"
|
|
24
|
+
pylint = "^3.3.3"
|
|
25
|
+
textual = {extras = ["syntax"], version = "^3.2.0"}
|
|
26
|
+
textual-dev = "^1.7.0"
|
|
27
|
+
jinjasql2 = "^0.1.12"
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
[tool.poetry.group.dev.dependencies]
|
|
31
|
+
pandas-stubs = "^2.2.3.250308"
|
|
32
|
+
bump-my-version = "^1.2.1"
|
|
33
|
+
pytest-mock = "^3.15.0"
|
|
34
|
+
|
|
35
|
+
[build-system]
|
|
36
|
+
requires = ["poetry-core"]
|
|
37
|
+
build-backend = "poetry.core.masonry.api"
|
|
38
|
+
|
|
39
|
+
[tool.poetry.scripts]
|
|
40
|
+
mipi-app = "mipi_datamanager.core.file_search_gui:main"
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from mipi_datamanager.core.data_managers import DataManager
|
|
2
|
+
from mipi_datamanager.core.jinja import JinjaLibrary, JinjaRepo, exc
|
|
3
|
+
from mipi_datamanager.core.file_search import FileSearch
|
|
4
|
+
|
|
5
|
+
__all__ = ['DataManager', 'JinjaLibrary', 'JinjaRepo', 'FileSearch', "exc"]
|
|
6
|
+
|
|
7
|
+
# import doc pages for mkdocstrings
|
|
8
|
+
from mipi_datamanager.core.docs import (
|
|
9
|
+
setup,
|
|
10
|
+
getting_started,
|
|
11
|
+
contribute,
|
|
12
|
+
patch_notes,
|
|
13
|
+
install
|
|
14
|
+
)
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import datetime as dt
|
|
2
|
+
from sqlalchemy import create_engine
|
|
3
|
+
|
|
4
|
+
class Odbc:
|
|
5
|
+
"""
|
|
6
|
+
Creates an Odbc connection object that can be used in any MiPi function that queries a database. This function
|
|
7
|
+
automatically handles setup and taredown of the connection even if there is an error.
|
|
8
|
+
|
|
9
|
+
Args:
|
|
10
|
+
dsn: The DSN name used to configure the connection
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
def __init__(self, dsn:str = None, driver = None, server = None, database = None, uid = None, pwd = None, trusted_connection = "yes", dialect = "mssql"):
|
|
14
|
+
|
|
15
|
+
self.dsn = dsn
|
|
16
|
+
self.driver = driver
|
|
17
|
+
self.server = server
|
|
18
|
+
self.database = database
|
|
19
|
+
self.uid = uid
|
|
20
|
+
self.pwd = pwd
|
|
21
|
+
self.trusted_connection = trusted_connection
|
|
22
|
+
self.dialect = dialect
|
|
23
|
+
|
|
24
|
+
self.name = self.dsn or self.database
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
@property
|
|
28
|
+
def connection_string(self):
|
|
29
|
+
params = {self.dsn:"?odbc_connect=DSN",
|
|
30
|
+
self.driver:"DRIVER",
|
|
31
|
+
self.server:"SERVER",
|
|
32
|
+
self.database:"DATABASE",
|
|
33
|
+
self.uid:"UID",
|
|
34
|
+
self.pwd:"PWD",
|
|
35
|
+
self.trusted_connection:"Trusted_Connection"}
|
|
36
|
+
con_str = f"mssql+pyodbc:///"
|
|
37
|
+
for param,key in params.items():
|
|
38
|
+
if param:
|
|
39
|
+
con_str += f"{key}={param};"
|
|
40
|
+
|
|
41
|
+
return con_str
|
|
42
|
+
|
|
43
|
+
def __enter__(self):
|
|
44
|
+
self.engine = create_engine(self.connection_string)
|
|
45
|
+
self.con = self.engine.connect()
|
|
46
|
+
self.start = dt.datetime.now()
|
|
47
|
+
print(f"\nConnection Established: {self.name} @ {self.start}")
|
|
48
|
+
return self.con
|
|
49
|
+
|
|
50
|
+
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
51
|
+
self.con.close()
|
|
52
|
+
end = dt.datetime.now()
|
|
53
|
+
print(f"Connection Terminated: {self.name} @ {end}")
|
|
54
|
+
print(f"Connection Open For: {end - self.start}")
|
|
File without changes
|
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
from typing import Any, Sequence
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
def read_text_file(file: str | Path, encoding ="utf-8") -> str:
|
|
5
|
+
with open(file, "r",encoding = encoding) as f:
|
|
6
|
+
contents = f.read()
|
|
7
|
+
return contents
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _maybe_add_lists(list_of_lists):
|
|
11
|
+
if not isinstance(list_of_lists, list):
|
|
12
|
+
_list_of_lists = [list_of_lists]
|
|
13
|
+
|
|
14
|
+
final_list = []
|
|
15
|
+
for list_i in list_of_lists:
|
|
16
|
+
final_list += list_i
|
|
17
|
+
|
|
18
|
+
return final_list
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _maybe_convert_tuple_to_list(val: list | tuple):
|
|
22
|
+
if isinstance(val, tuple):
|
|
23
|
+
return list(val)
|
|
24
|
+
elif isinstance(val, list):
|
|
25
|
+
return val
|
|
26
|
+
else:
|
|
27
|
+
raise TypeError(f'Expected tuple or list, got {type(val)}')
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ensure_list(val: Any) -> list:
|
|
31
|
+
"""
|
|
32
|
+
Wrap a single value in a list, or convert a list-like object into a list if it isnt one
|
|
33
|
+
"""
|
|
34
|
+
if isinstance(val, list):
|
|
35
|
+
return val
|
|
36
|
+
if isinstance(val, Sequence) and not isinstance(val, (str, bytes)):
|
|
37
|
+
return list(val)
|
|
38
|
+
return [val]
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def dict_to_string(dictionary: dict):
|
|
42
|
+
"""
|
|
43
|
+
|
|
44
|
+
creates a clean string representation of a dictionary
|
|
45
|
+
|
|
46
|
+
Args:
|
|
47
|
+
dictionary: any dictionary
|
|
48
|
+
block_pad: int: number of lines to pad top and bottom of output
|
|
49
|
+
|
|
50
|
+
Returns: Clean string representation of the dictionary
|
|
51
|
+
|
|
52
|
+
"""
|
|
53
|
+
|
|
54
|
+
max_key_length = max(len(str(key)) for key in dictionary.keys())
|
|
55
|
+
max_value_length = max(len(str(value)) for value in dictionary.values())
|
|
56
|
+
|
|
57
|
+
output = ""
|
|
58
|
+
for i, (key, value) in enumerate(dictionary.items()):
|
|
59
|
+
if i != 0:
|
|
60
|
+
output += ","
|
|
61
|
+
output += f"{str(key):<{max_key_length}} : {str(value):>{max_value_length}}\n"
|
|
62
|
+
|
|
63
|
+
return output
|
|
64
|
+
|
|
65
|
+
def _maybe_rename_values(value: str|list, rename:dict):
|
|
66
|
+
if rename is None:
|
|
67
|
+
_rename = {}
|
|
68
|
+
else:
|
|
69
|
+
_rename = rename
|
|
70
|
+
|
|
71
|
+
if isinstance(value, str):
|
|
72
|
+
if value in _rename:
|
|
73
|
+
return rename[value]
|
|
74
|
+
else:
|
|
75
|
+
return value
|
|
76
|
+
elif isinstance(value, list):
|
|
77
|
+
return [_rename[i] if i in _rename else i for i in value]
|
|
78
|
+
else:
|
|
79
|
+
raise TypeError(f"expected type string or list got {type(value)}")
|
|
80
|
+
|
|
81
|
+
def _columns_to_dict(df):
|
|
82
|
+
return {f"'{col}'": None for col in df.columns.tolist()}
|
|
83
|
+
|
|
84
|
+
def _dict_to_string(dictionary: dict):
|
|
85
|
+
"""
|
|
86
|
+
|
|
87
|
+
creates a clean string representation of a dictionary
|
|
88
|
+
|
|
89
|
+
Args:
|
|
90
|
+
dictionary: any dictionary
|
|
91
|
+
block_pad: int: number of lines to pad top and bottom of output
|
|
92
|
+
|
|
93
|
+
Returns: Clean string representation of the dictionary
|
|
94
|
+
|
|
95
|
+
"""
|
|
96
|
+
if not isinstance(dictionary, dict):
|
|
97
|
+
raise TypeError(f"expected dict got {type(dictionary)}")
|
|
98
|
+
|
|
99
|
+
if len(dictionary) == 0:
|
|
100
|
+
raise ValueError("Dictionary is empty")
|
|
101
|
+
|
|
102
|
+
max_key_length = max(len(str(key)) for key in dictionary.keys())
|
|
103
|
+
max_value_length = max(len(str(value)) for value in dictionary.values())
|
|
104
|
+
|
|
105
|
+
output = ""
|
|
106
|
+
for i, (key, value) in enumerate(dictionary.items()):
|
|
107
|
+
if i != 0:
|
|
108
|
+
output += ","
|
|
109
|
+
output += f"{str(key):<{max_key_length}} : {str(value):>{max_value_length}}\n"
|
|
110
|
+
|
|
111
|
+
return output
|
|
112
|
+
|
|
113
|
+
class IndexedDict(dict):
|
|
114
|
+
"""
|
|
115
|
+
A dictionary object that allows you to access items by their key index.
|
|
116
|
+
The key index is the suffix of the key.
|
|
117
|
+
"""
|
|
118
|
+
|
|
119
|
+
def __init__(self, *args, **kwargs):
|
|
120
|
+
super().__init__(*args, **kwargs)
|
|
121
|
+
for key in self.keys():
|
|
122
|
+
self._validate_key(key)
|
|
123
|
+
|
|
124
|
+
def _validate_key(self, key):
|
|
125
|
+
if not isinstance(key, str):
|
|
126
|
+
raise TypeError('Key must be a string')
|
|
127
|
+
|
|
128
|
+
suffix = key.split("_")[-1]
|
|
129
|
+
prefix = key.split("_")[0]
|
|
130
|
+
|
|
131
|
+
if not suffix.isnumeric() or "_" not in key or len(prefix) == 0:
|
|
132
|
+
raise ValueError('Key must have a string name followed by an underscored numeric suffix "{keyname}_{index_number}"')
|
|
133
|
+
|
|
134
|
+
if suffix in [k.split("_")[-1] for k in self.keys() if k != key]:
|
|
135
|
+
raise ValueError('A key with that suffix already exists')
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def __setitem__(self, key, value):
|
|
139
|
+
self._validate_key(key)
|
|
140
|
+
super().__setitem__(key, value)
|
|
141
|
+
|
|
142
|
+
def __getitem__(self, key):
|
|
143
|
+
if isinstance(key, str):
|
|
144
|
+
return super().__getitem__(key)
|
|
145
|
+
if isinstance(key, int):
|
|
146
|
+
ks = [k for k in self.keys() if k.endswith(str(key))]
|
|
147
|
+
assert len(ks) == 1, f"Assertion filed, multiple Keys exist for index{key}"
|
|
148
|
+
k = ks[0]
|
|
149
|
+
return super().__getitem__(k)
|
|
150
|
+
def update(self, other, **kwargs):
|
|
151
|
+
if isinstance(other, dict):
|
|
152
|
+
for key in other.keys():
|
|
153
|
+
self._validate_key(key)
|
|
154
|
+
for key in kwargs.keys():
|
|
155
|
+
self._validate_key(key)
|
|
156
|
+
super().update(other, **kwargs)
|
|
157
|
+
def get(self, key, default=None):
|
|
158
|
+
if isinstance(key, str):
|
|
159
|
+
return super().get(key, default)
|
|
160
|
+
if isinstance(key, int):
|
|
161
|
+
ks = [k for k in self.keys() if k.endswith(str(key))]
|
|
162
|
+
if len(ks) == 1:
|
|
163
|
+
return super().__getitem__(ks[0])
|
|
164
|
+
return default
|
|
165
|
+
|
|
166
|
+
class EnsureList:
|
|
167
|
+
def __set_name__(self, owner: type, name: str):
|
|
168
|
+
# called once, per attribute, at class creation time
|
|
169
|
+
self.private_name = f"_{name}"
|
|
170
|
+
|
|
171
|
+
def __get__(self, instance, owner: type):
|
|
172
|
+
if instance is None:
|
|
173
|
+
return self
|
|
174
|
+
# if not yet set, default to empty list
|
|
175
|
+
return getattr(instance, self.private_name, [])
|
|
176
|
+
|
|
177
|
+
def __set__(self, instance, value: Any):
|
|
178
|
+
if value is None:
|
|
179
|
+
set_val = []
|
|
180
|
+
else:
|
|
181
|
+
set_val = ensure_list(value)
|
|
182
|
+
setattr(instance, self.private_name, set_val)
|
|
183
|
+
|