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/excel.py ADDED
@@ -0,0 +1,134 @@
1
+
2
+ import os
3
+ import openpyxl
4
+ import pandas as pd
5
+ from .typing import list_
6
+ from .utils import (
7
+ log,
8
+ )
9
+
10
+
11
+ def format(
12
+ path,
13
+ sheet=None,
14
+ freeze_panes='B2',
15
+ hide_sheets=None,
16
+ hide_cols=None,
17
+ col_width_max=70,
18
+ col_width_padding=2,
19
+ align_vertical='top',
20
+ align_horizontal='left',
21
+ wrap_text=True,
22
+ openpyxl_workbook=None,
23
+ verbosity=3,
24
+ ): #pragma: no cover (does not affect reading of xlsx files)
25
+ """
26
+ applies formatting to an Excel file:
27
+ - adjust col width to max length of cell content (accounts for linebreaks)
28
+ - set cell alignment to top-left and wrap text
29
+ - hide specified columns
30
+ """
31
+
32
+ if openpyxl_workbook:
33
+ wb = openpyxl_workbook
34
+ else:
35
+ wb = openpyxl.load_workbook(path)
36
+ if sheet:
37
+ sheetnames = list_(sheet)
38
+ else:
39
+ sheetnames = [sheetname for sheetname in wb.sheetnames]
40
+ sheets_hide = list_(hide_sheets)
41
+ cols_hide = list_(hide_cols)
42
+
43
+
44
+ for sheetname in sheetnames:
45
+
46
+ data = pd.read_excel(path, sheet_name=sheetname)
47
+ sheet = wb[sheetname]
48
+
49
+ if sheetname in sheets_hide:
50
+ sheet.sheet_state = 'hidden'
51
+
52
+ if freeze_panes:
53
+ sheet.freeze_panes = freeze_panes
54
+
55
+ #adjust column widths, cell alignment and text wrapping
56
+ for col in sheet.columns:
57
+ colname = col[0].value
58
+ col_letter = col[0].column_letter #type:ignore
59
+
60
+ if colname is None:
61
+ text = (
62
+ 'warning: skipping column with'
63
+ f' no header in sheet "{sheet.title}"'
64
+ )
65
+ log(text, 'dk.format_excel()', verbosity)
66
+ continue
67
+
68
+ if colname in cols_hide:
69
+ sheet.column_dimensions[col_letter].hidden = True #type:ignore
70
+
71
+
72
+ #multiline cells are split by newline,
73
+ #expanded into new rows,
74
+ #and the maximum length is calculated
75
+ #these changes are not applied to the actual data,
76
+ #only the column width is adjusted
77
+ max_length = (
78
+ data[colname] #type:ignore
79
+ .apply(str)
80
+ .apply(lambda x: x.split('\n'))
81
+ .explode()
82
+ .str
83
+ .len()
84
+ .max()
85
+ )
86
+ max_length = max(max_length, len(str(colname)))
87
+ max_length = min(max_length, col_width_max) + col_width_padding
88
+
89
+ sheet.column_dimensions[col_letter].width = max_length
90
+
91
+ for cell in col:
92
+ cell.alignment = openpyxl.styles.Alignment( #type:ignore
93
+ vertical=align_vertical,
94
+ horizontal=align_horizontal,
95
+ wrap_text=wrap_text
96
+ )
97
+
98
+ wb.save(path)
99
+ wb.close()
100
+
101
+
102
+
103
+
104
+ def save(
105
+ df: pd.DataFrame,
106
+ path: str,
107
+ sheet_name='df',
108
+ index=False,
109
+ format_excel=True,
110
+ **kwargs,
111
+ ):
112
+
113
+ if os.path.exists(path):
114
+ writer = pd.ExcelWriter(
115
+ path,
116
+ mode='a',
117
+ if_sheet_exists='replace',
118
+ )
119
+ else:
120
+ writer = pd.ExcelWriter(
121
+ path,
122
+ mode='w',
123
+ )
124
+
125
+ with writer:
126
+ df.to_excel(
127
+ writer,
128
+ sheet_name=sheet_name,
129
+ index=index,
130
+ **kwargs,
131
+ )
132
+
133
+ if format_excel:
134
+ format(path, sheet=sheet_name)
dukit/interfaces.py ADDED
@@ -0,0 +1,160 @@
1
+
2
+ import pandas as pd
3
+ from .excel import save
4
+ from .qlang import (
5
+ q,
6
+ qr,
7
+ qs,
8
+ )
9
+ from .pandas import (
10
+ flatten,
11
+ stagger,
12
+ embed,
13
+ collapse,
14
+ transpose,
15
+ )
16
+
17
+
18
+
19
+ @pd.api.extensions.register_dataframe_accessor('dk')
20
+ class DukitAccessor():
21
+
22
+ def __init__(
23
+ self,
24
+ df: pd.DataFrame,
25
+ ):
26
+ self.df = df
27
+
28
+
29
+ def q(
30
+ self,
31
+ code='',
32
+ verbosity=3,
33
+ ):
34
+ return q(self.df, code, verbosity)
35
+
36
+
37
+ def qs(
38
+ self,
39
+ code='',
40
+ verbosity=3,
41
+ ):
42
+ return qs(self.df, code, verbosity)
43
+
44
+
45
+ def qr(
46
+ self,
47
+ code='',
48
+ verbosity=3,
49
+ ):
50
+ return qr(self.df, code, verbosity)
51
+
52
+
53
+ def flatten(
54
+ self,
55
+ on: str,
56
+ template='{colname}_#{counter}',
57
+ ):
58
+ df_new = flatten(
59
+ self.df,
60
+ on=on,
61
+ template=template,
62
+ )
63
+ return df_new
64
+
65
+
66
+ def stagger(
67
+ self,
68
+ on: str,
69
+ template='#{counter}_{colname}',
70
+ separator_col='#{counter}',
71
+ ):
72
+ df_new = stagger(
73
+ self.df,
74
+ on=on,
75
+ template=template,
76
+ separator_col=separator_col,
77
+ )
78
+ return df_new
79
+
80
+
81
+ def embed(
82
+ self,
83
+ on: str,
84
+ colname='',
85
+ template='{colname}_#{counter}',
86
+ line_start='',
87
+ separator=':',
88
+ spacer='\u00A0', #non-breaking space
89
+ line_stop='\n',
90
+ ):
91
+ df_new = embed(
92
+ self.df,
93
+ on=on,
94
+ colname=colname,
95
+ template=template,
96
+ line_start=line_start,
97
+ separator=separator,
98
+ spacer=spacer,
99
+ line_stop=line_stop,
100
+ )
101
+ return df_new
102
+
103
+
104
+ def collapse(
105
+ self,
106
+ on: str,
107
+ template='{colname}',
108
+ line_start='#',
109
+ line_stop='\n',
110
+ ):
111
+ df_new = collapse(
112
+ self.df,
113
+ on=on,
114
+ template=template,
115
+ line_start=line_start,
116
+ line_stop=line_stop,
117
+ )
118
+ return df_new
119
+
120
+
121
+ def transpose(
122
+ self,
123
+ header='uid',
124
+ ) -> pd.DataFrame:
125
+ df_new = transpose(
126
+ self.df,
127
+ header=header,
128
+ )
129
+ return df_new
130
+
131
+
132
+ def save(
133
+ self,
134
+ path: str,
135
+ sheet_name='df',
136
+ index=False,
137
+ format_excel=True,
138
+ **kwargs,
139
+ ):
140
+ save(
141
+ df=self.df,
142
+ path=path,
143
+ sheet_name=sheet_name,
144
+ index=index,
145
+ format_excel=format_excel,
146
+ **kwargs,
147
+ )
148
+ return None
149
+
150
+
151
+ def style(self):
152
+ code = """
153
+ .replace("\n", "<br>")
154
+ .wrap(normal)
155
+ .mono()
156
+ .align(left)
157
+ %.align(center)
158
+ """
159
+ df_styled = qs(self.df, code)
160
+ return df_styled
dukit/os.py ADDED
@@ -0,0 +1,280 @@
1
+
2
+ import pandas as pd
3
+ import os
4
+ import shutil
5
+ import datetime
6
+ from .utils import log
7
+ from .typing import date_, datetime_
8
+
9
+
10
+
11
+ def pwd():
12
+ """
13
+ print working directory
14
+ """
15
+ return os.getcwd()
16
+
17
+
18
+
19
+ def ls(path='', recursive=False) -> pd.DataFrame:
20
+ """
21
+ list files directory
22
+ """
23
+
24
+ if path == '':
25
+ path = os.getcwd()
26
+
27
+ if recursive is True:
28
+ filepaths = []
29
+ for root, dirs, filenames in os.walk(path):
30
+ for filename in filenames:
31
+ filepaths.append(f'{root}\\{filename}')
32
+ else:
33
+ filepaths = [f'{path}\\{filename}' for filename in os.listdir(path)]
34
+
35
+ files = pd.DataFrame()
36
+ files['_path'] = filepaths
37
+ files['name'] = files['_path'].apply(lambda x: os.path.basename(x))
38
+ files['size'] = files['_path'].apply(
39
+ lambda x: (
40
+ os.path.getsize(x)
41
+ if os.path.isfile(x)
42
+ else None
43
+ )
44
+ )
45
+ files['created'] = files['_path'].apply(
46
+ lambda x: (
47
+ datetime
48
+ .datetime
49
+ .fromtimestamp(os.path.getctime(x))
50
+ .strftime('%Y-%m-%d %H:%M:%S')
51
+ )
52
+ )
53
+ files['last modified'] = files['_path'].apply(
54
+ lambda x: (
55
+ datetime
56
+ .datetime
57
+ .fromtimestamp(os.path.getmtime(x))
58
+ .strftime('%Y-%m-%d %H:%M:%S')
59
+ )
60
+ )
61
+ files['last accessed'] = files['_path'].apply(
62
+ lambda x: (
63
+ datetime
64
+ .datetime
65
+ .fromtimestamp(os.path.getatime(x))
66
+ .strftime('%Y-%m-%d %H:%M:%S')
67
+ )
68
+ )
69
+ files['permissions'] = files['_path'].apply(
70
+ lambda x: (
71
+ oct(os.stat(x).st_mode)[-3:]
72
+ if os.path.isfile(x)
73
+ else None
74
+ )
75
+ )
76
+ files['path'] = files['_path']
77
+ files['folder'] = files['_path'].apply(lambda x: os.path.dirname(x))
78
+ files['type'] = files['_path'].apply(
79
+ lambda x: (
80
+ 'dir'
81
+ if os.path.isdir(x)
82
+ else os.path.splitext(x)[1]
83
+ )
84
+ )
85
+
86
+ files.drop(columns='_path', inplace=True)
87
+
88
+ return files
89
+
90
+
91
+
92
+ def lsr(path='') -> pd.DataFrame:
93
+ return ls(path, recursive=True)
94
+
95
+
96
+
97
+ def cd(path=None, verbosity=3):
98
+ """
99
+ change directory
100
+ """
101
+
102
+ if path is None:
103
+ path = os.getcwd()
104
+ elif path in ('', '.'):
105
+ path = os.getcwd()
106
+ elif path == '..':
107
+ path = os.path.dirname(os.getcwd())
108
+
109
+ dir_old = os.getcwd()
110
+ if dir_old.endswith(path):
111
+ log(f'INFO: already in "{path}"', f'dk.cd("{path}")', verbosity)
112
+ return
113
+
114
+ os.chdir(path)
115
+ dir_new = os.getcwd()
116
+ text = f'INFO: moved from<br>"{dir_old}"<br>to<br>"{dir_new}"'
117
+ log(text, f'dk.cd("{path}")', verbosity)
118
+ return
119
+
120
+
121
+
122
+ def cp(src, dest, verbosity=3):
123
+ """
124
+ copy file or directory
125
+ """
126
+
127
+ if os.path.isdir(dest):
128
+ dest = os.path.join(dest, os.path.basename(src))
129
+
130
+ if os.path.exists(dest):
131
+ text = f'WARNING: "{dest}" already exists and will be overwritten'
132
+ log(text, 'dk.cp()', verbosity)
133
+
134
+ if os.path.isdir(src):
135
+ shutil.copytree(src, dest)
136
+ else:
137
+ shutil.copy(src, dest)
138
+
139
+ log(f'INFO: copied<br>{src}<br>to<br>{dest}', 'dk.cp()', verbosity)
140
+ return
141
+
142
+
143
+
144
+ def mv(src, dest, verbosity=3):
145
+ """
146
+ move file or directory
147
+ """
148
+
149
+ if os.path.isdir(dest):
150
+ dest = os.path.join(dest, os.path.basename(src))
151
+
152
+ if os.path.exists(dest):
153
+ text = f'WARNING: "{dest}" already exists and will be overwritten'
154
+ log(text, 'dk.mv()', verbosity)
155
+
156
+ shutil.move(src, dest)
157
+
158
+ text = f'INFO: moved<br>"{src}"<br>to<br>"{dest}"'
159
+ log(text, 'dk.mv()', verbosity)
160
+ return
161
+
162
+
163
+
164
+ def mkdir(name, verbosity=3):
165
+ """
166
+ create directory
167
+ """
168
+ if os.path.isdir(name):
169
+ text = f'INFO: directory "{name}" already exists'
170
+ log(text, f'dk.mkdir("{name}")', verbosity)
171
+ else:
172
+ os.mkdir(name)
173
+ text = f'INFO: created directory "{name}"'
174
+ log(text, f'dk.mkdir("{name}")', verbosity)
175
+ return
176
+
177
+
178
+
179
+ def isdir(name):
180
+ return os.path.isdir(name)
181
+
182
+
183
+
184
+ def isfile(name):
185
+ return os.path.isfile(name)
186
+
187
+
188
+
189
+ def ispath(name):
190
+ return os.path.exists(name)
191
+
192
+
193
+
194
+ nums_str = [
195
+ '0',
196
+ '1',
197
+ '2',
198
+ '3',
199
+ '4',
200
+ '5',
201
+ '6',
202
+ '7',
203
+ '8',
204
+ '9',
205
+ ]
206
+ def fetch(path, before='now', verbosity=3):
207
+ """
208
+ returns the path to the most recent version of a file
209
+ assuming that a date is part of the filename.
210
+
211
+ "before" defines recency of the file:
212
+ - now: most recent version
213
+ - today: most recent version before today
214
+ - this day: most recent version before today
215
+ - this week: ...
216
+ - this month: ...
217
+ - this year: ...
218
+ - '2024_01_01': most recent version before 2024_01_01 (accepts many date formats)
219
+ """
220
+
221
+ if os.path.isfile(path):
222
+ log(f'INFO: found file "{path}"', 'dk.fetch()', verbosity)
223
+ return path
224
+
225
+ today = datetime.date.today()
226
+
227
+
228
+ if before == 'now':
229
+ cutoff = today + datetime.timedelta(days=1)
230
+ elif before == 'today':
231
+ cutoff = today
232
+ elif before == 'this day':
233
+ cutoff = today
234
+ elif before == 'this week':
235
+ cutoff = today - datetime.timedelta(days=today.weekday())
236
+ elif before == 'this month':
237
+ cutoff = today - datetime.timedelta(days=today.day - 1)
238
+ elif before == 'this year':
239
+ cutoff = pd.to_datetime(f'{today.year}0101').date()
240
+ else:
241
+ cutoff = date_(before)
242
+
243
+
244
+ name = os.path.basename(path)
245
+ folder = os.path.dirname(path)
246
+ extension = ''
247
+
248
+
249
+ if folder == '':
250
+ folder = os.getcwd()
251
+
252
+ timestamps = pd.Series([])
253
+ for file in os.listdir(folder):
254
+ #check if file starts with name and is a file
255
+ if os.path.isfile(f'{folder}\\{file}') and file.startswith(name):
256
+ try:
257
+ timestamp_str_full = file.split(name)[-1]
258
+ if timestamp_str_full[0] not in nums_str:
259
+ continue
260
+ extension = '.' + timestamp_str_full.split('.')[-1]
261
+ timestamp_str = timestamp_str_full.replace(f'{extension}', '')
262
+ timestamp = datetime_(timestamp_str)
263
+ if timestamp < datetime_(cutoff):
264
+ timestamps[timestamp] = (timestamp_str, extension)
265
+ except Exception: #pragma: no cover
266
+ pass
267
+ if len(timestamps) == 0:
268
+ text = (
269
+ 'ERROR: no timestamped files starting with'
270
+ f' "{name}" found in "{folder}" before {cutoff}'
271
+ )
272
+ log(text, 'dk.fetch()', verbosity)
273
+ raise FileNotFoundError(text)
274
+ else:
275
+ timestamps = timestamps.sort_index()
276
+ latest = timestamps.iloc[len(timestamps) - 1][0]
277
+ extension = timestamps.iloc[len(timestamps) - 1][1]
278
+ path = f'{folder}\\{name}{latest}{extension}'
279
+ log(f'INFO: found file "{path}"', 'dk.fetch()', verbosity)
280
+ return path