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/typing.py
ADDED
|
@@ -0,0 +1,683 @@
|
|
|
1
|
+
|
|
2
|
+
import pandas as pd
|
|
3
|
+
import numpy as np
|
|
4
|
+
import re
|
|
5
|
+
import typing
|
|
6
|
+
import datetime
|
|
7
|
+
from pandas import isna
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
TYPES_INT: tuple[type, ...] = (
|
|
11
|
+
int,
|
|
12
|
+
np.int64,
|
|
13
|
+
np.int32,
|
|
14
|
+
np.int16,
|
|
15
|
+
np.int8,
|
|
16
|
+
pd.Int64Dtype,
|
|
17
|
+
pd.Int32Dtype,
|
|
18
|
+
pd.Int16Dtype,
|
|
19
|
+
pd.Int8Dtype,
|
|
20
|
+
)
|
|
21
|
+
TYPES_FLOAT: tuple[type, ...] = (
|
|
22
|
+
float,
|
|
23
|
+
np.float64,
|
|
24
|
+
np.float32,
|
|
25
|
+
np.float16,
|
|
26
|
+
pd.Float64Dtype,
|
|
27
|
+
pd.Float32Dtype,
|
|
28
|
+
)
|
|
29
|
+
TYPES_NUM: tuple[type, ...] = (
|
|
30
|
+
int,
|
|
31
|
+
float,
|
|
32
|
+
np.int64,
|
|
33
|
+
np.float64,
|
|
34
|
+
np.int32,
|
|
35
|
+
np.float32,
|
|
36
|
+
np.int16,
|
|
37
|
+
np.float16,
|
|
38
|
+
np.int8,
|
|
39
|
+
np.number,
|
|
40
|
+
pd.Int64Dtype,
|
|
41
|
+
pd.Float64Dtype,
|
|
42
|
+
pd.Int32Dtype,
|
|
43
|
+
pd.Float32Dtype,
|
|
44
|
+
pd.Int16Dtype,
|
|
45
|
+
pd.Int8Dtype,
|
|
46
|
+
)
|
|
47
|
+
TYPES_STR: tuple[type, ...] = (
|
|
48
|
+
str,
|
|
49
|
+
pd.StringDtype,
|
|
50
|
+
)
|
|
51
|
+
TYPES_BOOL: tuple[type, ...] = (
|
|
52
|
+
bool,
|
|
53
|
+
np.bool_,
|
|
54
|
+
pd.BooleanDtype,
|
|
55
|
+
)
|
|
56
|
+
TYPES_DATE: tuple[type, ...] = (
|
|
57
|
+
datetime.date,
|
|
58
|
+
pd.Timestamp,
|
|
59
|
+
)
|
|
60
|
+
|
|
61
|
+
VALUES_NA = (
|
|
62
|
+
'',
|
|
63
|
+
'na',
|
|
64
|
+
'<na>',
|
|
65
|
+
'n/a',
|
|
66
|
+
'<n/a>',
|
|
67
|
+
'n.a',
|
|
68
|
+
'n.a.',
|
|
69
|
+
'na.',
|
|
70
|
+
'n.a',
|
|
71
|
+
'nan',
|
|
72
|
+
'n.a.n',
|
|
73
|
+
'n.a.n.',
|
|
74
|
+
'not available',
|
|
75
|
+
'not applicable',
|
|
76
|
+
'not a number',
|
|
77
|
+
'missing',
|
|
78
|
+
'missing.',
|
|
79
|
+
'null',
|
|
80
|
+
'nil',
|
|
81
|
+
'none',
|
|
82
|
+
'void',
|
|
83
|
+
'blank',
|
|
84
|
+
'empty',
|
|
85
|
+
)
|
|
86
|
+
VALUES_NK = (
|
|
87
|
+
'unk',
|
|
88
|
+
'unknown',
|
|
89
|
+
'not known',
|
|
90
|
+
'not known.',
|
|
91
|
+
'nk',
|
|
92
|
+
'n.k.',
|
|
93
|
+
'n.k',
|
|
94
|
+
'n/k',
|
|
95
|
+
'not specified',
|
|
96
|
+
'not specified.',
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
DTYPES_ALLOWED = set([
|
|
100
|
+
'string',
|
|
101
|
+
'Int64',
|
|
102
|
+
'Float64',
|
|
103
|
+
'boolean',
|
|
104
|
+
'datetime64[s]',
|
|
105
|
+
'datetime64[us]',
|
|
106
|
+
'object',
|
|
107
|
+
])
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def str_(x, spacer='\n ') -> str:
|
|
112
|
+
if isinstance(x, str):
|
|
113
|
+
return x
|
|
114
|
+
elif isinstance(x, tuple):
|
|
115
|
+
return _tuple_to_str(x, spacer)
|
|
116
|
+
elif isinstance(x, (list, pd.Series, pd.Index)):
|
|
117
|
+
return _list_to_str(x, spacer)
|
|
118
|
+
elif isinstance(x, dict):
|
|
119
|
+
return _dict_to_str(x, spacer)
|
|
120
|
+
else:
|
|
121
|
+
return str(x)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def _tuple_to_str(tpl: tuple, spacer='\n ') -> str:
|
|
126
|
+
if len(tpl) < 2:
|
|
127
|
+
return str(tpl)
|
|
128
|
+
str_tpl = (
|
|
129
|
+
'('
|
|
130
|
+
+ spacer
|
|
131
|
+
+ spacer.join(f'{item!r}' for item in tpl)
|
|
132
|
+
+ '\n)'
|
|
133
|
+
)
|
|
134
|
+
return str_tpl
|
|
135
|
+
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
def _list_to_str(
|
|
139
|
+
lst: list | pd.Series | pd.Index,
|
|
140
|
+
spacer='\n ',
|
|
141
|
+
) -> str:
|
|
142
|
+
|
|
143
|
+
if isinstance(lst, pd.Series):
|
|
144
|
+
lst = lst.to_list()
|
|
145
|
+
elif isinstance(lst, pd.Index):
|
|
146
|
+
lst = lst.to_list()
|
|
147
|
+
|
|
148
|
+
if len(lst) < 2:
|
|
149
|
+
return str(lst)
|
|
150
|
+
str_lst = (
|
|
151
|
+
'['
|
|
152
|
+
+ spacer
|
|
153
|
+
+ spacer.join(f'{item!r}' for item in lst)
|
|
154
|
+
+ '\n]'
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
return str_lst
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def _dict_to_str(d: dict, spacer='\n ') -> str:
|
|
162
|
+
if len(d) < 2:
|
|
163
|
+
return str(d)
|
|
164
|
+
kvs = (
|
|
165
|
+
f'{k!r}: {v!r}'
|
|
166
|
+
for k, v in
|
|
167
|
+
d.items()
|
|
168
|
+
)
|
|
169
|
+
str_d = (
|
|
170
|
+
'{'
|
|
171
|
+
+ spacer
|
|
172
|
+
+ spacer.join(kvs)
|
|
173
|
+
+ '\n}'
|
|
174
|
+
)
|
|
175
|
+
return str_d
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def int_(x, errors='coerce', na=np.nan) -> typing.Any:
|
|
180
|
+
if x is True or x is False:
|
|
181
|
+
return na
|
|
182
|
+
elif isinstance(x, TYPES_INT):
|
|
183
|
+
return x
|
|
184
|
+
elif isinstance(x, str):
|
|
185
|
+
x = x.replace(',', '.')
|
|
186
|
+
try:
|
|
187
|
+
return round(float(x)) #float first to handle strings like '1.0'
|
|
188
|
+
except Exception as e:
|
|
189
|
+
if errors == 'raise':
|
|
190
|
+
raise ValueError(
|
|
191
|
+
f'could not convert "{x}" to integer.\n'
|
|
192
|
+
'Error handling:\n'
|
|
193
|
+
'errors="raise": raises a ValueError\n'
|
|
194
|
+
'errors="ignore": returns the original value\n'
|
|
195
|
+
'errors="coerce": returns np.nan\n'
|
|
196
|
+
'errors=<any other value>: returns <any other value>\n'
|
|
197
|
+
f'original error:\n{e}'
|
|
198
|
+
)
|
|
199
|
+
elif errors == 'ignore':
|
|
200
|
+
return x
|
|
201
|
+
elif errors == 'coerce':
|
|
202
|
+
return na
|
|
203
|
+
else:
|
|
204
|
+
return errors
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
|
|
208
|
+
def float_(x, errors='coerce', na=np.nan) -> typing.Any:
|
|
209
|
+
if isinstance(x, TYPES_FLOAT):
|
|
210
|
+
return x
|
|
211
|
+
elif isinstance(x, str):
|
|
212
|
+
x = x.replace(',', '.')
|
|
213
|
+
try:
|
|
214
|
+
return float(x)
|
|
215
|
+
except Exception as e:
|
|
216
|
+
if errors == 'raise':
|
|
217
|
+
raise ValueError(
|
|
218
|
+
f'could not convert "{x}" to float.\n'
|
|
219
|
+
'Error handling:\n'
|
|
220
|
+
'errors="raise": raises a ValueError\n'
|
|
221
|
+
'errors="ignore": returns the original value\n'
|
|
222
|
+
'errors="coerce": returns np.nan\n'
|
|
223
|
+
'errors=<any other value>: returns <any other value>\n'
|
|
224
|
+
f'original error:\n{e}'
|
|
225
|
+
)
|
|
226
|
+
elif errors == 'ignore':
|
|
227
|
+
return x
|
|
228
|
+
elif errors == 'coerce':
|
|
229
|
+
return na
|
|
230
|
+
else:
|
|
231
|
+
return errors
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
|
|
235
|
+
def num_(x, errors='coerce', na=np.nan) -> typing.Any:
|
|
236
|
+
if isinstance(x, TYPES_NUM):
|
|
237
|
+
return x
|
|
238
|
+
try:
|
|
239
|
+
return pd.to_numeric(x)
|
|
240
|
+
except Exception as e:
|
|
241
|
+
if errors == 'raise':
|
|
242
|
+
raise ValueError(
|
|
243
|
+
f'could not convert "{x}" to numeric value.\n'
|
|
244
|
+
'Error handling:\n'
|
|
245
|
+
'errors="raise": raises a ValueError\n'
|
|
246
|
+
'errors="ignore": returns the original value\n'
|
|
247
|
+
'errors="coerce": returns np.nan\n'
|
|
248
|
+
'errors=<any other value>: returns <any other value>\n'
|
|
249
|
+
f'original error:\n{e}'
|
|
250
|
+
)
|
|
251
|
+
elif errors == 'ignore':
|
|
252
|
+
return x
|
|
253
|
+
elif errors == 'coerce':
|
|
254
|
+
return na
|
|
255
|
+
else:
|
|
256
|
+
return errors
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
|
|
260
|
+
def bool_(x, errors='coerce', na=None) -> typing.Any:
|
|
261
|
+
if isinstance(x, TYPES_BOOL):
|
|
262
|
+
return x
|
|
263
|
+
elif str(x).lower() in ['y', 'yes', 'true', '1', '1.0', 'positive', 'pos']:
|
|
264
|
+
return True
|
|
265
|
+
elif str(x).lower() in ['n', 'no', 'false', '0', '0.0', 'negative', 'neg']:
|
|
266
|
+
return False
|
|
267
|
+
else:
|
|
268
|
+
if errors == 'raise':
|
|
269
|
+
raise ValueError(
|
|
270
|
+
f'could not convert "{x}" to numeric boolean.\n'
|
|
271
|
+
'Error handling:\n'
|
|
272
|
+
'errors="raise": raises a ValueError\n'
|
|
273
|
+
'errors="ignore": returns the original value\n'
|
|
274
|
+
'errors="coerce": returns np.nan\n'
|
|
275
|
+
'errors=<any other value>: returns <any other value>\n'
|
|
276
|
+
)
|
|
277
|
+
elif errors == 'ignore':
|
|
278
|
+
return x
|
|
279
|
+
elif errors == 'coerce':
|
|
280
|
+
return na
|
|
281
|
+
else:
|
|
282
|
+
return errors
|
|
283
|
+
|
|
284
|
+
|
|
285
|
+
|
|
286
|
+
_months_txt = (
|
|
287
|
+
'Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec|'
|
|
288
|
+
'January|February|March|April|May|June|July|'
|
|
289
|
+
'August|September|October|November|December'
|
|
290
|
+
)
|
|
291
|
+
def date_(x, errors='coerce', na=pd.NaT) -> typing.Any:
|
|
292
|
+
"""
|
|
293
|
+
recognizes and converts potential dates
|
|
294
|
+
between 0001-01-01 and 2999-12-31 in various formats.
|
|
295
|
+
"""
|
|
296
|
+
result = datetime_(x, errors=errors, na=na)
|
|
297
|
+
if result is na:
|
|
298
|
+
return na
|
|
299
|
+
elif isinstance(result, datetime.datetime) or isinstance(result, pd.Timestamp):
|
|
300
|
+
return result.date()
|
|
301
|
+
|
|
302
|
+
#raise and coerce are handled in _datetime and should not be reached here
|
|
303
|
+
elif errors == 'raise': #pragma: no cover
|
|
304
|
+
raise ValueError(
|
|
305
|
+
f'could not convert "{x}" to datetime.\n'
|
|
306
|
+
'Error handling:\n'
|
|
307
|
+
'errors="raise": raises a ValueError\n'
|
|
308
|
+
'errors="ignore": returns the original value\n'
|
|
309
|
+
'errors="coerce": returns np.nan\n'
|
|
310
|
+
'errors=<any other value>: returns <any other value>\n'
|
|
311
|
+
)
|
|
312
|
+
elif errors == 'ignore':
|
|
313
|
+
return x
|
|
314
|
+
elif errors == 'coerce':
|
|
315
|
+
return na #pragma: no cover
|
|
316
|
+
else:
|
|
317
|
+
return errors
|
|
318
|
+
|
|
319
|
+
|
|
320
|
+
|
|
321
|
+
def datetime_(x, errors='coerce', na=pd.NaT) -> typing.Any:
|
|
322
|
+
if isinstance(x, datetime.datetime):
|
|
323
|
+
return x
|
|
324
|
+
elif isinstance(x, datetime.date):
|
|
325
|
+
return pd.to_datetime(x)
|
|
326
|
+
elif isinstance(x, str):
|
|
327
|
+
x = x.replace('.', '-')
|
|
328
|
+
x = x.replace('/', '-')
|
|
329
|
+
x = x.replace('\\', '-')
|
|
330
|
+
x = x.replace('_', '-')
|
|
331
|
+
try:
|
|
332
|
+
|
|
333
|
+
if len(str(x).strip()) < 8:
|
|
334
|
+
raise ValueError(f'could not convert "{x}" to date: too short to be a date')
|
|
335
|
+
|
|
336
|
+
elif re.fullmatch(r'[012]\d\d\d[-\d\s:]+.*', x):
|
|
337
|
+
result = pd.to_datetime(x, dayfirst=False)
|
|
338
|
+
|
|
339
|
+
elif re.match(
|
|
340
|
+
f'(\\d\\d\\d\\d)\\D?({_months_txt})\\D?(\\d\\d)',
|
|
341
|
+
x,
|
|
342
|
+
flags=re.IGNORECASE,
|
|
343
|
+
):
|
|
344
|
+
x = re.sub(
|
|
345
|
+
f'(\\d\\d\\d\\d)\\D?({_months_txt})\\D?(\\d\\d)(.*)',
|
|
346
|
+
r'\3-\2-\1\4',
|
|
347
|
+
x,
|
|
348
|
+
flags=re.IGNORECASE,
|
|
349
|
+
)
|
|
350
|
+
result = pd.to_datetime(x, dayfirst=True)
|
|
351
|
+
|
|
352
|
+
elif re.match(
|
|
353
|
+
f'(\\d\\d)\\D?({_months_txt})\\D?(\\d\\d\\d\\d)',
|
|
354
|
+
x,
|
|
355
|
+
flags=re.IGNORECASE,
|
|
356
|
+
):
|
|
357
|
+
x = re.sub(
|
|
358
|
+
f'(\\d\\d)\\D?({_months_txt})\\D?(\\d\\d\\d\\d)(.*)',
|
|
359
|
+
r'\1-\2-\3\4',
|
|
360
|
+
x,
|
|
361
|
+
flags=re.IGNORECASE,
|
|
362
|
+
)
|
|
363
|
+
result = pd.to_datetime(x, dayfirst=True)
|
|
364
|
+
|
|
365
|
+
elif re.match(
|
|
366
|
+
f'({_months_txt})\\D?(\\d\\d)\\D?(\\d\\d\\d\\d)',
|
|
367
|
+
x,
|
|
368
|
+
flags=re.IGNORECASE,
|
|
369
|
+
):
|
|
370
|
+
x = re.sub(
|
|
371
|
+
f'({_months_txt})\\D?(\\d\\d)\\D?(\\d\\d\\d\\d)(.*)',
|
|
372
|
+
r'\2-\1-\3\4',
|
|
373
|
+
x,
|
|
374
|
+
flags=re.IGNORECASE,
|
|
375
|
+
)
|
|
376
|
+
result = pd.to_datetime(x, dayfirst=True)
|
|
377
|
+
|
|
378
|
+
else:
|
|
379
|
+
result = pd.to_datetime(x, dayfirst=True)
|
|
380
|
+
|
|
381
|
+
if result is pd.NaT:
|
|
382
|
+
raise ValueError(f'could not convert "{x}" to date')
|
|
383
|
+
else:
|
|
384
|
+
return result
|
|
385
|
+
except Exception as e:
|
|
386
|
+
if errors == 'raise':
|
|
387
|
+
raise ValueError(
|
|
388
|
+
f'could not convert "{x}" to datetime.\n'
|
|
389
|
+
'Error handling:\n'
|
|
390
|
+
'errors="raise": raises a ValueError\n'
|
|
391
|
+
'errors="ignore": returns the original value\n'
|
|
392
|
+
'errors="coerce": returns np.nan\n'
|
|
393
|
+
'errors=<any other value>: returns <any other value>\n'
|
|
394
|
+
f'original error:\n{e}'
|
|
395
|
+
)
|
|
396
|
+
elif errors == 'ignore':
|
|
397
|
+
return x
|
|
398
|
+
elif errors == 'coerce':
|
|
399
|
+
return na
|
|
400
|
+
else:
|
|
401
|
+
return errors
|
|
402
|
+
|
|
403
|
+
|
|
404
|
+
|
|
405
|
+
def na_(x, errors='ignore', na=None) -> typing.Any:
|
|
406
|
+
|
|
407
|
+
if str(x).lower().strip() in VALUES_NA:
|
|
408
|
+
return na
|
|
409
|
+
elif isna(x):
|
|
410
|
+
return na
|
|
411
|
+
else:
|
|
412
|
+
if errors == 'raise':
|
|
413
|
+
raise ValueError(
|
|
414
|
+
f'could not convert "{x}" to "{na}".\n'
|
|
415
|
+
'Error handling:\n'
|
|
416
|
+
'errors="raise": raises a ValueError\n'
|
|
417
|
+
'errors="ignore": returns the original value\n'
|
|
418
|
+
'errors="coerce": returns np.nan\n'
|
|
419
|
+
'errors=<any other value>: returns <any other value>\n'
|
|
420
|
+
)
|
|
421
|
+
elif errors == 'ignore':
|
|
422
|
+
return x
|
|
423
|
+
elif errors == 'coerce':
|
|
424
|
+
return None
|
|
425
|
+
else:
|
|
426
|
+
return errors
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
def nk_(x, errors='ignore', nk='unknown', na=None) -> typing.Any:
|
|
431
|
+
if str(x).lower().strip() in VALUES_NK:
|
|
432
|
+
return nk
|
|
433
|
+
else:
|
|
434
|
+
if errors == 'raise':
|
|
435
|
+
raise ValueError(
|
|
436
|
+
f'could not convert "{x}" to "{nk}".\n'
|
|
437
|
+
'Error handling:\n'
|
|
438
|
+
'errors="raise": raises a ValueError\n'
|
|
439
|
+
'errors="ignore": returns the original value\n'
|
|
440
|
+
'errors="coerce": returns np.nan\n'
|
|
441
|
+
'errors=<any other value>: returns <any other value>\n'
|
|
442
|
+
)
|
|
443
|
+
elif errors == 'ignore':
|
|
444
|
+
return x
|
|
445
|
+
elif errors == 'coerce':
|
|
446
|
+
return na
|
|
447
|
+
else:
|
|
448
|
+
return errors
|
|
449
|
+
|
|
450
|
+
|
|
451
|
+
|
|
452
|
+
def yn_(x, errors='coerce', yes='yes', no='no', na=None) -> typing.Any:
|
|
453
|
+
if str(x).lower() in ['y', 'yes', 'true', '1', '1.0', 'positive', 'pos']:
|
|
454
|
+
return yes
|
|
455
|
+
elif str(x).lower() in ['n', 'no', 'false', '0', '0.0', 'negative', 'neg']:
|
|
456
|
+
return no
|
|
457
|
+
else:
|
|
458
|
+
if errors == 'raise':
|
|
459
|
+
raise ValueError(
|
|
460
|
+
f'could not convert "{x}" to "{yes}" or "{no}".\n'
|
|
461
|
+
'Error handling:\n'
|
|
462
|
+
'errors="raise": raises a ValueError\n'
|
|
463
|
+
'errors="ignore": returns the original value\n'
|
|
464
|
+
'errors="coerce": returns np.nan\n'
|
|
465
|
+
'errors=<any other value>: returns <any other value>\n'
|
|
466
|
+
)
|
|
467
|
+
elif errors == 'ignore':
|
|
468
|
+
return x
|
|
469
|
+
elif errors == 'coerce':
|
|
470
|
+
return na
|
|
471
|
+
else:
|
|
472
|
+
return errors
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
def type_(x) -> str:
|
|
477
|
+
"""
|
|
478
|
+
Returns what type something "should" be. e.g.: dk.type('1') == 'int'
|
|
479
|
+
"""
|
|
480
|
+
|
|
481
|
+
if isinstance(x, bool):
|
|
482
|
+
return 'bool'
|
|
483
|
+
elif isinstance(x, TYPES_INT):
|
|
484
|
+
return 'int'
|
|
485
|
+
elif isinstance(x, TYPES_FLOAT):
|
|
486
|
+
return 'float'
|
|
487
|
+
elif datetime_(x) is not pd.NaT:
|
|
488
|
+
x = str(x).strip()
|
|
489
|
+
if re.fullmatch(r'\d*', x):
|
|
490
|
+
return 'int'
|
|
491
|
+
elif re.search(r'\d\d:\d\d', x):
|
|
492
|
+
return 'datetime'
|
|
493
|
+
else:
|
|
494
|
+
return 'date'
|
|
495
|
+
|
|
496
|
+
elif isinstance(x, str):
|
|
497
|
+
x = x.strip()
|
|
498
|
+
if re.fullmatch(r'(true|false)', x, re.IGNORECASE):
|
|
499
|
+
return 'bool'
|
|
500
|
+
elif re.fullmatch(r'-?\d+', x):
|
|
501
|
+
return 'int'
|
|
502
|
+
elif re.fullmatch(r'-?\d+[\.,]\d+', x):
|
|
503
|
+
return 'float'
|
|
504
|
+
elif str(x).lower() in VALUES_NA:
|
|
505
|
+
return 'na'
|
|
506
|
+
else:
|
|
507
|
+
try:
|
|
508
|
+
x = pd.to_numeric(x)
|
|
509
|
+
return 'num'
|
|
510
|
+
except Exception:
|
|
511
|
+
return 'str'
|
|
512
|
+
|
|
513
|
+
elif pd.isna(x):
|
|
514
|
+
return 'na'
|
|
515
|
+
|
|
516
|
+
else:
|
|
517
|
+
return type(x).__name__
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
_conversion_mapping = {
|
|
521
|
+
'int': int_,
|
|
522
|
+
'float': float_,
|
|
523
|
+
'num': num_,
|
|
524
|
+
'bool': bool_,
|
|
525
|
+
'date': date_,
|
|
526
|
+
'datetime': datetime_,
|
|
527
|
+
'na': na_,
|
|
528
|
+
'nk': nk_,
|
|
529
|
+
'yn': yn_,
|
|
530
|
+
'NoneType': lambda x, errors, na: na,
|
|
531
|
+
}
|
|
532
|
+
def convert_(value, errors='coerce', na=None) -> typing.Any:
|
|
533
|
+
"""
|
|
534
|
+
Converts to the type something "should" be according to dk.type().
|
|
535
|
+
e.g.: dk.convert('1') == 1
|
|
536
|
+
"""
|
|
537
|
+
type_name = type_(value)
|
|
538
|
+
if type_name == 'str':
|
|
539
|
+
result = str(value)
|
|
540
|
+
elif type_name in _conversion_mapping:
|
|
541
|
+
result = _conversion_mapping[type_name](value, errors, na)
|
|
542
|
+
else:
|
|
543
|
+
result = value
|
|
544
|
+
return result
|
|
545
|
+
|
|
546
|
+
|
|
547
|
+
def repr_(x) -> str:
|
|
548
|
+
txt = f'{x!r}'
|
|
549
|
+
return txt
|
|
550
|
+
|
|
551
|
+
|
|
552
|
+
def typeinfo_(x) -> str:
|
|
553
|
+
type_inferred = type_(x)
|
|
554
|
+
new = convert_(x)
|
|
555
|
+
#"<{type_inferred}>" would get interpreted as a html tag by the pandas styler
|
|
556
|
+
txt = f'{x!r} [{type_inferred}] {new!r}'
|
|
557
|
+
return txt
|
|
558
|
+
|
|
559
|
+
|
|
560
|
+
def typeinfostrict_(x) -> str:
|
|
561
|
+
type_strict = type(x).__name__
|
|
562
|
+
#"<{type_inferred}>" would get interpreted as a html tag by the pandas styler
|
|
563
|
+
txt = f'{x!r} [{type_strict}]'
|
|
564
|
+
return txt
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
|
|
568
|
+
def list_(x) -> list:
|
|
569
|
+
if x is None:
|
|
570
|
+
return []
|
|
571
|
+
elif isinstance(x, list):
|
|
572
|
+
return x
|
|
573
|
+
elif isinstance(x, str):
|
|
574
|
+
return [x]
|
|
575
|
+
elif hasattr(x, '__iter__'):
|
|
576
|
+
return list(x)
|
|
577
|
+
else:
|
|
578
|
+
return [x]
|
|
579
|
+
|
|
580
|
+
|
|
581
|
+
|
|
582
|
+
class Box:
|
|
583
|
+
"""
|
|
584
|
+
- stores key-value pairs as attributes
|
|
585
|
+
- prevents collisions with class methods and reserved attributes
|
|
586
|
+
- provides dict-like introspection via keys(), values(), items()
|
|
587
|
+
- debug-friendly self referential representation
|
|
588
|
+
"""
|
|
589
|
+
|
|
590
|
+
#essential methods
|
|
591
|
+
|
|
592
|
+
def __init__(
|
|
593
|
+
self,
|
|
594
|
+
**kwargs,
|
|
595
|
+
):
|
|
596
|
+
for key, value in kwargs.items():
|
|
597
|
+
self.__setattr__(key, value)
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
def __setattr__(
|
|
601
|
+
self,
|
|
602
|
+
name: str,
|
|
603
|
+
value: typing.Any,
|
|
604
|
+
):
|
|
605
|
+
if name in dir(Box):
|
|
606
|
+
msg = (
|
|
607
|
+
f'{name!r}'
|
|
608
|
+
' is a reserved attribute/method name.'
|
|
609
|
+
)
|
|
610
|
+
raise AttributeError(msg)
|
|
611
|
+
super().__setattr__(name, value)
|
|
612
|
+
|
|
613
|
+
|
|
614
|
+
def __getitem__(
|
|
615
|
+
self,
|
|
616
|
+
key: str,
|
|
617
|
+
):
|
|
618
|
+
return self.__dict__[key]
|
|
619
|
+
|
|
620
|
+
|
|
621
|
+
#dict-like interface
|
|
622
|
+
|
|
623
|
+
def keys(self):
|
|
624
|
+
return self.__dict__.keys()
|
|
625
|
+
|
|
626
|
+
def values(self):
|
|
627
|
+
return self.__dict__.values()
|
|
628
|
+
|
|
629
|
+
def items(self):
|
|
630
|
+
return self.__dict__.items()
|
|
631
|
+
|
|
632
|
+
|
|
633
|
+
#utility
|
|
634
|
+
|
|
635
|
+
def __repr__(self):
|
|
636
|
+
txt = (
|
|
637
|
+
f'<{self.__class__.__name__};'
|
|
638
|
+
f' {len(self.__dict__)} attributes>'
|
|
639
|
+
)
|
|
640
|
+
return txt
|
|
641
|
+
|
|
642
|
+
def __str__(self):
|
|
643
|
+
kwargs = [(k, v) for k, v in self.__dict__.items()]
|
|
644
|
+
if len(kwargs) == 0:
|
|
645
|
+
txt = f'{self.__class__.__name__}()'
|
|
646
|
+
elif len(kwargs) == 1:
|
|
647
|
+
content = f'{kwargs[0][0]}={kwargs[0][1]!r}'
|
|
648
|
+
txt = f'{self.__class__.__name__}({content})'
|
|
649
|
+
else:
|
|
650
|
+
content = []
|
|
651
|
+
for k, v in kwargs:
|
|
652
|
+
if isinstance(v, Box):
|
|
653
|
+
v = str(v).replace('\n', '\n ')
|
|
654
|
+
content.append(f' {k} = {v},')
|
|
655
|
+
elif isinstance(v, pd.DataFrame):
|
|
656
|
+
cols = len(v.columns)
|
|
657
|
+
rows = len(v.index)
|
|
658
|
+
v = f'<DataFrame; cols={cols}, rows={rows}>'
|
|
659
|
+
content.append(f' {k} = {v},')
|
|
660
|
+
else:
|
|
661
|
+
content.append(f' {k} = {v!r},')
|
|
662
|
+
content = '\n'.join(content)
|
|
663
|
+
txt = f'{self.__class__.__name__}(\n{content}\n )'
|
|
664
|
+
return txt
|
|
665
|
+
|
|
666
|
+
def print(self):
|
|
667
|
+
print(str(self))
|
|
668
|
+
|
|
669
|
+
def __eq__(self, value):
|
|
670
|
+
if isinstance(value, self.__class__):
|
|
671
|
+
return self.__dict__ == value.__dict__
|
|
672
|
+
else:
|
|
673
|
+
return False
|
|
674
|
+
|
|
675
|
+
def clear(self):
|
|
676
|
+
for key in list(self.__dict__.keys()):
|
|
677
|
+
del self.__dict__[key]
|
|
678
|
+
|
|
679
|
+
def copy(self):
|
|
680
|
+
return self.__class__(**self.__dict__)
|
|
681
|
+
|
|
682
|
+
def new(self, **kwargs):
|
|
683
|
+
return self.__class__(**kwargs)
|