dukit 0.1.0__py3-none-any.whl
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.
- dukit/__init__.py +108 -0
- dukit/_test_utils.py +374 -0
- dukit/diffing.py +1461 -0
- dukit/excel.py +134 -0
- dukit/interfaces.py +160 -0
- dukit/os.py +280 -0
- dukit/pandas.py +563 -0
- dukit/py.typed +0 -0
- dukit/qlang/__init__.py +61 -0
- dukit/qlang/engine.py +447 -0
- dukit/qlang/symbols.py +6867 -0
- dukit/typing.py +683 -0
- dukit/utils.py +288 -0
- dukit-0.1.0.dist-info/METADATA +113 -0
- dukit-0.1.0.dist-info/RECORD +16 -0
- dukit-0.1.0.dist-info/WHEEL +4 -0
dukit/utils.py
ADDED
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import string as str_module
|
|
4
|
+
import datetime
|
|
5
|
+
import random
|
|
6
|
+
|
|
7
|
+
from collections.abc import Iterable
|
|
8
|
+
from IPython.display import display
|
|
9
|
+
from IPython.core.getipython import get_ipython
|
|
10
|
+
from .typing import str_
|
|
11
|
+
|
|
12
|
+
GREEN = '#6dae51'
|
|
13
|
+
RED = '#f73434'
|
|
14
|
+
|
|
15
|
+
GREY_LIGHT = '#d3d3d3'
|
|
16
|
+
BLUE_LIGHT = '#87ceeb'
|
|
17
|
+
GREEN_LIGHT = '#c0e7b0'
|
|
18
|
+
ORANGE_LIGHT = '#f7d67c'
|
|
19
|
+
RED_LIGHT = '#f7746a'
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
logs = []
|
|
23
|
+
_levels = {
|
|
24
|
+
'TRACE': 5,
|
|
25
|
+
'DEBUG': 4,
|
|
26
|
+
'INFO': 3,
|
|
27
|
+
'WARNING': 2,
|
|
28
|
+
'ERROR': 1,
|
|
29
|
+
}
|
|
30
|
+
_colors = {
|
|
31
|
+
'TRACE': GREY_LIGHT,
|
|
32
|
+
'DEBUG': BLUE_LIGHT,
|
|
33
|
+
'INFO': GREEN_LIGHT,
|
|
34
|
+
'WARNING': ORANGE_LIGHT,
|
|
35
|
+
'ERROR': RED_LIGHT,
|
|
36
|
+
}
|
|
37
|
+
_formats = {
|
|
38
|
+
'TRACE': f'background-color: {_colors["TRACE"]}',
|
|
39
|
+
'DEBUG': f'background-color: {_colors["DEBUG"]}',
|
|
40
|
+
'INFO': f'background-color: {_colors["INFO"]}',
|
|
41
|
+
'WARNING': f'background-color: {_colors["WARNING"]}',
|
|
42
|
+
'ERROR': f'background-color: {_colors["ERROR"]}',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def _build_log_context(
|
|
47
|
+
function: str,
|
|
48
|
+
verbosity=3,
|
|
49
|
+
**kwargs,
|
|
50
|
+
) -> str:
|
|
51
|
+
lines = [f'function: {function}\n']
|
|
52
|
+
|
|
53
|
+
for key, value in kwargs.items():
|
|
54
|
+
|
|
55
|
+
if isinstance(value, pd.DataFrame):
|
|
56
|
+
pass
|
|
57
|
+
elif isinstance(value, pd.Series):
|
|
58
|
+
pass
|
|
59
|
+
|
|
60
|
+
if isinstance(value, (tuple, list, dict)):
|
|
61
|
+
value_str = str_(value)
|
|
62
|
+
elif verbosity <= 3:
|
|
63
|
+
value_str = repr(value)
|
|
64
|
+
else:
|
|
65
|
+
value_str = str(value)
|
|
66
|
+
|
|
67
|
+
lines.append(f'{key}: {value_str}')
|
|
68
|
+
|
|
69
|
+
return '\n'.join(lines)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def log(
|
|
73
|
+
text: str = None,
|
|
74
|
+
context: str = '',
|
|
75
|
+
verbosity: int = None,
|
|
76
|
+
clear: bool = False,
|
|
77
|
+
) -> pd.io.formats.style.Styler | None:
|
|
78
|
+
"""
|
|
79
|
+
A very basic "logger" meant to be used in place
|
|
80
|
+
of print() statements in jupyter notebooks.
|
|
81
|
+
For more extensive logging purposes use a logging module.
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
examples:
|
|
85
|
+
|
|
86
|
+
from dukit import log
|
|
87
|
+
|
|
88
|
+
log('trace: this is a trace entry which will be highlighted grey')
|
|
89
|
+
log('debug: this is a debug entry which will be highlighted blue')
|
|
90
|
+
log('info: this is a info entry which will be highlighted green')
|
|
91
|
+
log('warning: this is a warning entry which will be highlighted orange')
|
|
92
|
+
log('error: this is a error entry which will be highlighted red')
|
|
93
|
+
|
|
94
|
+
log(clear=True) #clear all log entries
|
|
95
|
+
log() #return dataframe of log entries
|
|
96
|
+
dukit.utils.logs #location of list containing log entries
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
if verbosity == 0:
|
|
100
|
+
return None
|
|
101
|
+
|
|
102
|
+
time = datetime.datetime.now()
|
|
103
|
+
|
|
104
|
+
if clear:
|
|
105
|
+
logs.clear()
|
|
106
|
+
if verbosity in (None, 3, 4, 5):
|
|
107
|
+
print('cleared all logs in dukit.util.logs.')
|
|
108
|
+
return None
|
|
109
|
+
|
|
110
|
+
if text is None:
|
|
111
|
+
if len(logs) == 0:
|
|
112
|
+
return pd.DataFrame().style
|
|
113
|
+
df = pd.DataFrame(logs)
|
|
114
|
+
col_format = df['level'].replace(_formats)
|
|
115
|
+
df_style = pd.DataFrame(
|
|
116
|
+
{col: col_format for col in df.columns},
|
|
117
|
+
index=df.index,
|
|
118
|
+
)
|
|
119
|
+
df_styled = (
|
|
120
|
+
df
|
|
121
|
+
.style
|
|
122
|
+
.apply(lambda x: df_style, axis=None)
|
|
123
|
+
.set_properties(None, **{'text-align': 'left'})
|
|
124
|
+
)
|
|
125
|
+
return df_styled
|
|
126
|
+
|
|
127
|
+
color = GREEN_LIGHT
|
|
128
|
+
level = 'INFO'
|
|
129
|
+
level_int = 3
|
|
130
|
+
text_temp = text.upper()
|
|
131
|
+
|
|
132
|
+
#detect logging level
|
|
133
|
+
for level_temp in _levels.keys():
|
|
134
|
+
if text_temp.startswith(level_temp):
|
|
135
|
+
level = level_temp
|
|
136
|
+
level_int = _levels[level]
|
|
137
|
+
color = _colors[level]
|
|
138
|
+
text = text[len(level_temp):].strip()
|
|
139
|
+
if text and text[0] in [':', '-', ' ']:
|
|
140
|
+
text = text[1:].strip()
|
|
141
|
+
break
|
|
142
|
+
|
|
143
|
+
if verbosity is None:
|
|
144
|
+
verbosity = level_int
|
|
145
|
+
|
|
146
|
+
if len(logs) == 0:
|
|
147
|
+
total_ms = 0.0
|
|
148
|
+
delta_ms = 0.0
|
|
149
|
+
else:
|
|
150
|
+
total_ms = datetime.datetime.now() - logs[0]['time']
|
|
151
|
+
delta_ms = datetime.datetime.now() - logs[-1]['time']
|
|
152
|
+
total_ms = total_ms.total_seconds() * 1000
|
|
153
|
+
delta_ms = delta_ms.total_seconds() * 1000
|
|
154
|
+
message = {
|
|
155
|
+
'level': level,
|
|
156
|
+
'text': text,
|
|
157
|
+
'context': context,
|
|
158
|
+
'time': time,
|
|
159
|
+
'total_ms': total_ms,
|
|
160
|
+
'delta_ms': delta_ms,
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
if level_int <= verbosity:
|
|
164
|
+
logs.append(message)
|
|
165
|
+
|
|
166
|
+
#for jupyter
|
|
167
|
+
if get_ipython().__class__.__name__ == 'ZMQInteractiveShell': #pragma: no cover
|
|
168
|
+
|
|
169
|
+
#make html friendly
|
|
170
|
+
message['text'] = (
|
|
171
|
+
message['text']
|
|
172
|
+
.replace('<br>', '\n')
|
|
173
|
+
.replace('<', '<')
|
|
174
|
+
.replace('>', '>')
|
|
175
|
+
.replace('\n', '<br>')
|
|
176
|
+
.replace('\t', ' ')
|
|
177
|
+
)
|
|
178
|
+
message['context'] = (
|
|
179
|
+
message['context']
|
|
180
|
+
.replace('<br>', '\n')
|
|
181
|
+
.replace('<', '<')
|
|
182
|
+
.replace('>', '>')
|
|
183
|
+
.replace('\n', '<br>')
|
|
184
|
+
.replace('\t', ' ')
|
|
185
|
+
)
|
|
186
|
+
message_df = pd.DataFrame(message, index=[len(logs)])
|
|
187
|
+
|
|
188
|
+
kwargs_format = {
|
|
189
|
+
'whitespace': 'normal',
|
|
190
|
+
'text-align': 'left',
|
|
191
|
+
}
|
|
192
|
+
display(
|
|
193
|
+
message_df
|
|
194
|
+
.style
|
|
195
|
+
.hide(axis=1)
|
|
196
|
+
.apply(lambda x: [f'background-color: {color}' for i in x], axis=1)
|
|
197
|
+
.set_properties(None, **kwargs_format)
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
#everywhere else
|
|
201
|
+
else:
|
|
202
|
+
context_formatted = context.replace('\n', '\n ')
|
|
203
|
+
text_formatted = text.replace('\n', '\n ')
|
|
204
|
+
string = (
|
|
205
|
+
f'{level} log message:\n'
|
|
206
|
+
f' time: {time}\n'
|
|
207
|
+
f' time since first log: {total_ms:.2f} ms\n'
|
|
208
|
+
f' time since last log: {delta_ms:.2f} ms\n'
|
|
209
|
+
f' context:{context_formatted}\n'
|
|
210
|
+
f' text:\n """{text_formatted}"""\n'
|
|
211
|
+
)
|
|
212
|
+
print(string)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
|
|
216
|
+
def ensure_unique_string(
|
|
217
|
+
string: str,
|
|
218
|
+
taken: Iterable[str],
|
|
219
|
+
strategy: str = 'increment',
|
|
220
|
+
) -> str:
|
|
221
|
+
"""
|
|
222
|
+
Ensure that a string is unique within a set of taken strings.
|
|
223
|
+
|
|
224
|
+
Parameters
|
|
225
|
+
----------
|
|
226
|
+
string : The original string to be made unique.
|
|
227
|
+
taken : An iterable of strings that are already taken.
|
|
228
|
+
strategy : The strategy to use for making the string unique. Default is 'increment'.
|
|
229
|
+
* "increment": use incrementing numbers appended to the string.
|
|
230
|
+
* "random": use random characters appended to the string.
|
|
231
|
+
* "timestamp": use a timestamp appended to the string.
|
|
232
|
+
* "datestamp": append the current date to the string.
|
|
233
|
+
If date is already taken, incrementing numbers are appended.
|
|
234
|
+
* "prefix=STR": prepend a custom prefix STR to the string.
|
|
235
|
+
Will keep prepending until the string is unique.
|
|
236
|
+
* "suffix=STR": append a custom suffix STR to the string.
|
|
237
|
+
Will keep appending until the string is unique.
|
|
238
|
+
|
|
239
|
+
"""
|
|
240
|
+
|
|
241
|
+
base_string = string
|
|
242
|
+
|
|
243
|
+
if strategy == 'increment':
|
|
244
|
+
counter = 1
|
|
245
|
+
while string in taken:
|
|
246
|
+
string = f"{base_string}{counter}"
|
|
247
|
+
counter += 1
|
|
248
|
+
elif strategy == 'random':
|
|
249
|
+
while string in taken:
|
|
250
|
+
chars = str_module.ascii_letters + str_module.digits
|
|
251
|
+
rand_str = ''.join(random.choices(chars, k=6))
|
|
252
|
+
string = f"{base_string}_{rand_str}"
|
|
253
|
+
elif strategy == 'timestamp':
|
|
254
|
+
while string in taken:
|
|
255
|
+
timestamp = datetime.datetime.now().strftime('%Y_%m_%d_%Hh%Mm%Ss')
|
|
256
|
+
string = f"{base_string}_{timestamp}"
|
|
257
|
+
elif strategy == 'datestamp':
|
|
258
|
+
datestamp = datetime.datetime.now().strftime('%Y_%m_%d')
|
|
259
|
+
string_temp = f"{base_string}_{datestamp}"
|
|
260
|
+
counter = 1
|
|
261
|
+
while string_temp in taken:
|
|
262
|
+
string_temp = f"{base_string}_{datestamp}_v{counter}"
|
|
263
|
+
counter += 1
|
|
264
|
+
string = string_temp
|
|
265
|
+
elif strategy.startswith('prefix='):
|
|
266
|
+
prefix = strategy.split('=')[1]
|
|
267
|
+
while string in taken:
|
|
268
|
+
string = f"{prefix}{string}"
|
|
269
|
+
elif strategy.startswith('suffix='):
|
|
270
|
+
suffix = strategy.split('=')[1]
|
|
271
|
+
while string in taken:
|
|
272
|
+
string = f"{string}{suffix}"
|
|
273
|
+
else:
|
|
274
|
+
raise ValueError(f'Unknown strategy: {strategy}')
|
|
275
|
+
|
|
276
|
+
return string
|
|
277
|
+
|
|
278
|
+
|
|
279
|
+
|
|
280
|
+
def now(fmt='%Y_%m_%d'):
|
|
281
|
+
"""
|
|
282
|
+
alias for datetime.datetime.now().strftime(format_str)
|
|
283
|
+
|
|
284
|
+
common format_str options:
|
|
285
|
+
'%Y_%m_%d_%Hh%Mm%Ss': standard
|
|
286
|
+
%Y_%b_%d: 3 letter month
|
|
287
|
+
"""
|
|
288
|
+
return datetime.datetime.now().strftime(fmt)
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
Metadata-Version: 2.3
|
|
2
|
+
Name: dukit
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: dukit - data utilities kit for exploring, modifying and visualizing small but messy data using pandas
|
|
5
|
+
Author: MartinVoelkl94
|
|
6
|
+
Author-email: MartinVoelkl94 <martinv16248@gmail.com>
|
|
7
|
+
Requires-Dist: ipython>=9.15.0
|
|
8
|
+
Requires-Dist: jinja2>=3.1.6
|
|
9
|
+
Requires-Dist: pandas>=3.0.3
|
|
10
|
+
Requires-Python: >=3.14
|
|
11
|
+
Description-Content-Type: text/markdown
|
|
12
|
+
|
|
13
|
+
# dukit - data utilities kit
|
|
14
|
+
|
|
15
|
+
Provides utilities focused on data exploration, modification and visualization of small but messy datasets in pandas.
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
# qlang
|
|
20
|
+
|
|
21
|
+
A small query language.
|
|
22
|
+
It is implemented as a pandas [accessor extension](https://pandas.pydata.org/docs/development/extending.html), meaning it can be called from a dataframe without any further preparation by calling df.dk.qs() with a valid text string.
|
|
23
|
+
|
|
24
|
+
example:
|
|
25
|
+
```python
|
|
26
|
+
from dukit import get_df
|
|
27
|
+
|
|
28
|
+
df = get_df()
|
|
29
|
+
|
|
30
|
+
#show all patients with ID > 20000
|
|
31
|
+
df.dk.qs(r'id >20000')
|
|
32
|
+
|
|
33
|
+
#select all patients whose name contains "john"
|
|
34
|
+
df.dk.qs(r'name ?john')
|
|
35
|
+
|
|
36
|
+
#select all patients whose name contains "j" and "a"
|
|
37
|
+
df.dk.qs(r'name ?j &&?a')
|
|
38
|
+
|
|
39
|
+
#select all patients whose name contains "j" or "a"
|
|
40
|
+
df.dk.qs(r'name ?j //?a')
|
|
41
|
+
|
|
42
|
+
#select ages between 18 and 80 and highlight them green
|
|
43
|
+
df.dk.qs(r'age >18 &&<80 .color(green)')
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Take a look at "interactive_demo.ipynb" in the github repo for a quick syntax introduction and more examples.
|
|
47
|
+
|
|
48
|
+
The query language allows for arbitrary code execution via eval(), please be aware of the risks.
|
|
49
|
+
|
|
50
|
+
Currently published on [testpypi](https://test.pypi.org/project/dukit/).
|
|
51
|
+
|
|
52
|
+
<br>
|
|
53
|
+
<br>
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
# other utilities
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
## logging with dukit.log()
|
|
62
|
+
|
|
63
|
+
A small logger to be used in notebooks or the REPL. Makes it easier to keep track of outputs in large notebooks by providing color coded output. Does not log to file, but instead to a dataframe which can then be viewed at the end of the notebook.
|
|
64
|
+
<br>
|
|
65
|
+
<br>
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
## dukit.diff()
|
|
69
|
+
|
|
70
|
+
creates colored diff output for two dataframes, see interactive_demo.ipynb for examples. Works with pandas dataframes, csv files, excel files and excel files with multiple sheets.
|
|
71
|
+
<br>
|
|
72
|
+
<br>
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
## "bashlike" wrappers
|
|
76
|
+
|
|
77
|
+
While python has functions to achieve similar results as common bash commands, they are often more verbose, less intuitive if you are already used to the bash names and spread out over different modules and different namespaces in those modules (os, os.path, shutil, sys, ...).
|
|
78
|
+
These wrappers use the same names as the bash commands and offer some additional functionality.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
available wrappers:
|
|
82
|
+
- dukit.ls()
|
|
83
|
+
- dukit.lsr()
|
|
84
|
+
- dukit.pwd()
|
|
85
|
+
- dukit.cd()
|
|
86
|
+
- dukit.cp()
|
|
87
|
+
- dukit.mkdir()
|
|
88
|
+
- dukit.isdir()
|
|
89
|
+
- dukit.isfile()
|
|
90
|
+
- dukit.ispath()
|
|
91
|
+
<br>
|
|
92
|
+
<br>
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
## type conversion
|
|
96
|
+
|
|
97
|
+
Mostly wrappers for pandas functions but with some additional functionality and generally more lenient handling of edge cases.
|
|
98
|
+
|
|
99
|
+
available functions:
|
|
100
|
+
- dukit.int()
|
|
101
|
+
- dukit.float()
|
|
102
|
+
- dukit.num()
|
|
103
|
+
- dukit.bool()
|
|
104
|
+
- dukit.date()
|
|
105
|
+
- dukit.datetime()
|
|
106
|
+
- dukit.na()
|
|
107
|
+
- dukit.nk()
|
|
108
|
+
- dukit.yn()
|
|
109
|
+
- dukit.type()
|
|
110
|
+
- dukit.convert()
|
|
111
|
+
<br>
|
|
112
|
+
<br>
|
|
113
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
dukit/__init__.py,sha256=hcqXqlbc_1cFVTeMwKp43D92JqCxc9cL-5Cm729BMt4,1579
|
|
2
|
+
dukit/_test_utils.py,sha256=-B2po0YjMoIo9DgCKfXdfHSC2NIXqC2jODTXpZPwt_4,10106
|
|
3
|
+
dukit/diffing.py,sha256=zCMLryNvWp1kpwCuZOAzJr9zm-lfh7Ga6xzLAyBbGEU,41718
|
|
4
|
+
dukit/excel.py,sha256=AciWK6rL2G9oWCHLkwqXyt21LeRlnTZvyCefYjJRdvs,3648
|
|
5
|
+
dukit/interfaces.py,sha256=sWgi5IzSUIFDliH7SJPWNi7fFoGmkV4m_gOrfqjLBUw,3283
|
|
6
|
+
dukit/os.py,sha256=Vc7dOOXxwGe1zVhqeik_-Lndk_xOLziql6uaQJaD33k,7233
|
|
7
|
+
dukit/pandas.py,sha256=nSH-7pAUvBqbnz8cF1BRxvNr_OZ-nWo0wMCQ1hYT2g4,13535
|
|
8
|
+
dukit/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
+
dukit/qlang/__init__.py,sha256=mArIX3zbwuJ5_ruRmKIOVadaPVtpjFWlGrOGLJCqr1c,1128
|
|
10
|
+
dukit/qlang/engine.py,sha256=YrJMVD0I_Gpe4Q0n4ovhYAhKDYN2RMrmr-clyGLTiBg,12279
|
|
11
|
+
dukit/qlang/symbols.py,sha256=twCMBOJTt8WAWvjbMcKRSyakMZFv5wvnN1ihRH3dgjs,169665
|
|
12
|
+
dukit/typing.py,sha256=7vCtd6c9JeEtNA403CN-DA9_gRAbnFIWGbGQ4K_Ntyc,18354
|
|
13
|
+
dukit/utils.py,sha256=7e-bCPmVSypN-ZCpXRCsOPMicz8ZB-BA7kxkd2deoR8,8827
|
|
14
|
+
dukit-0.1.0.dist-info/WHEEL,sha256=mydTeHxOpFHo-DnYhAd_3ATePms-g4rrYvM7wJK8P-U,80
|
|
15
|
+
dukit-0.1.0.dist-info/METADATA,sha256=PzZBtt3BJEpo9E-O1lNKQowz77ME12M17FVy94B09-8,3144
|
|
16
|
+
dukit-0.1.0.dist-info/RECORD,,
|