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/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
|