invesytoolbox 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.
@@ -0,0 +1,13 @@
1
+ import importlib
2
+ from pathlib import Path
3
+
4
+ _package_dir = Path(__file__).resolve().parent
5
+
6
+ for _file in sorted(_package_dir.glob('itb_*.py')):
7
+ _module = importlib.import_module(f'.{_file.stem}', package=__name__)
8
+ _module_all = getattr(_module, '__all__', None)
9
+ _names = _module_all if _module_all is not None else [
10
+ name for name in dir(_module) if not name.startswith('_')
11
+ ]
12
+ for _name in _names:
13
+ globals()[_name] = getattr(_module, _name)
@@ -0,0 +1,538 @@
1
+ # -*- coding: utf-8 -*-
2
+ """
3
+ itb_data
4
+ ========
5
+ """
6
+
7
+ __all__ = [
8
+ 'any_2boolean',
9
+ 'create_vcard',
10
+ 'dict_2datatypes',
11
+ 'dict_2unicode',
12
+ 'dict_from_dict_list',
13
+ 'dictlist_2datatypes',
14
+ 'sort_dictlist',
15
+ 'value_2datatype',
16
+ ]
17
+
18
+ import datetime
19
+ from typing import Any, Dict, List, Literal, Optional, Union
20
+
21
+ import DateTime
22
+ import pendulum
23
+ import vobject
24
+ from dateutil.parser import parse
25
+ from .itb_date_time import convert_datetime, get_dateformat, str_to_date, str_to_DT
26
+ from .itb_email_phone import process_phonenumber
27
+
28
+ DataType = Union[str, int, float, bool, datetime.date, datetime.datetime, datetime.time, DateTime.DateTime, pendulum.DateTime]
29
+
30
+ comma_float_chars = set(',1234567890')
31
+ time_chars = set('1234567890:')
32
+
33
+
34
+ def any_2boolean(
35
+ value: Any
36
+ ) -> bool:
37
+ """ return a boolean for any value
38
+
39
+ This function recognizes strings and bytes that signify False, such as "False" or "0"
40
+ """
41
+ if not value:
42
+ return False
43
+ elif isinstance(value, (str, bytes)):
44
+ if value.lower() in ('0', 'false', b'0', b'false'):
45
+ return False
46
+ return True
47
+
48
+
49
+ def dict_from_dict_list(
50
+ dict_list: List[dict],
51
+ key: str,
52
+ single_value: Optional[str] = None,
53
+ include_key: bool = False
54
+ ) -> Dict[str, Any]:
55
+ """ Create a dictionary from a list of dictionaries
56
+
57
+ :param dict_list: the list of dictionaries to be converted. Each item must be a dict.
58
+ :param key: the key of the dictionaries to use as key for the new dictionary.
59
+ The values for this key must be unique across the list. Key must exist in every dict.
60
+ :param single_value: if provided (as string), only this value from each dict is kept.
61
+ It must exist in every dict if specified.
62
+ :param include_key: if True, the value used as key is also included in the value dict.
63
+ :raises ValueError: if inputs are invalid or required keys are missing.
64
+ :return: dictionary keyed by the values of ``key``
65
+ """
66
+ if not isinstance(dict_list, list):
67
+ raise ValueError('dict_list must be a list.')
68
+ if not dict_list:
69
+ return {}
70
+ if not isinstance(key, str) or not key:
71
+ raise ValueError('key must be a non-empty string.')
72
+ if single_value is not None and (not isinstance(single_value, str) or not single_value):
73
+ raise ValueError('single_value must be a non-empty string or None.')
74
+
75
+ # Validate each dict and check key presence and uniqueness
76
+ seen_keys = set()
77
+ for idx, d in enumerate(dict_list):
78
+ if not isinstance(d, dict):
79
+ raise ValueError(f'All items in dict_list must be dicts, but item at index {idx} is {type(d).__name__}.')
80
+ if key not in d:
81
+ raise ValueError(f'Key \'{key}\' must be present in every dict, but missing in item at index {idx}.')
82
+ if single_value is not None and single_value not in d:
83
+ raise ValueError(f'single_value \'{single_value}\' must be present in every dict, but missing in item at index {idx}.')
84
+ dict_key = d[key]
85
+ if dict_key in seen_keys:
86
+ raise ValueError(f'Key \'{key}\' values must be unique, but \'{dict_key}\' is duplicated.')
87
+ seen_keys.add(dict_key)
88
+
89
+ dl = [dict(d) for d in dict_list] # we copy the list because we will change the dictionaries (except for single_value)
90
+
91
+ new_dict = {}
92
+
93
+ for dictionary in dl:
94
+ if single_value:
95
+ new_dict[dictionary.pop(key)] = dictionary.get(single_value)
96
+ elif include_key:
97
+ new_dict[dictionary[key]] = dictionary
98
+ else:
99
+ # slick: pop the id and use it as key for newDict in one line
100
+ new_dict[dictionary.pop(key)] = dictionary
101
+
102
+ return new_dict
103
+
104
+
105
+ def create_vcard(
106
+ data: Dict[str, str],
107
+ returning: Literal['vcard', 'pretty'] = 'vcard'
108
+ ) -> Optional[str]:
109
+ """ create a vCard from a dictionary
110
+
111
+ .. note:: The serialized vcard has Windows line-breaks, which is fine, I guess
112
+ .. note:: colons in notes are escaped in vobject
113
+
114
+ :param data: keys:
115
+
116
+ - prename
117
+ - surname
118
+ - email
119
+ - phone
120
+ - street
121
+ - city
122
+ - region (eg. federal state)
123
+ - zipcode
124
+ - country
125
+
126
+ :param returning: ``'vcard'`` (serialized) or ``'pretty'`` (pretty-printed)
127
+ :return: vCard string, or None if no name is provided
128
+ """
129
+
130
+ prename = data.get('prename', '')
131
+ surname = data.get('surname', '')
132
+ if not prename and not surname:
133
+ return None
134
+
135
+ vcard = vobject.vCard()
136
+ vcard.add('n')
137
+ vcard.n.value = vobject.vcard.Name(
138
+ family=surname,
139
+ given=prename
140
+ )
141
+ vcard.add('fn')
142
+ vcard.fn.value = f'{prename} {surname}'.strip()
143
+ vcard.add('email')
144
+ vcard.email.value = data.get('email', '')
145
+ vcard.email.type_param = 'INTERNET'
146
+ vcard.add('tel')
147
+ phone = process_phonenumber(
148
+ data.get('phone', ''),
149
+ empty_if_invalid=True
150
+ )
151
+ if phone:
152
+ vcard.tel.value = phone
153
+ vcard.tel.type_param = ['CELL', 'VOICE']
154
+ vcard.add('adr')
155
+ vcard.adr.type_param = ['HOME', 'pref']
156
+ vcard.adr.value = vobject.vcard.Address(
157
+ street=data.get('street', ''),
158
+ city=data.get('city', ''),
159
+ region=data.get('region', ''),
160
+ code=data.get('zipcode', ''),
161
+ country=data.get('country', '')
162
+ )
163
+ vcard.add('note')
164
+ vcard.note.value = data.get('note', '')
165
+
166
+ if returning == 'pretty':
167
+ return vcard.prettyPrint()
168
+ else:
169
+ return vcard.serialize()
170
+
171
+
172
+ def dict_2unicode(
173
+ d: Dict[Union[str, bytes], Any],
174
+ encoding: str = 'utf-8'
175
+ ) -> Dict[str, Any]:
176
+ """ Convert all keys and values in a dictionary from bytes to unicode
177
+
178
+ All keys and values are changed from bytes to unicode, if applicable.
179
+ Other data types are left unchanged, including other compound data types.
180
+
181
+ :param d: dictionary
182
+ :param encoding: default is utf-8
183
+ :return: dictionary with all bytes decoded to strings
184
+ """
185
+
186
+ return {
187
+ k.decode(encoding) if isinstance(k, bytes) else k:
188
+ (v.decode(encoding) if isinstance(v, bytes) else v)
189
+ for k, v in d.items()
190
+ }
191
+
192
+
193
+ def dict_2datatypes(
194
+ d: dict,
195
+ metadata: Optional[Dict[str, str]] = None,
196
+ convert_keys: bool = False,
197
+ convert_to_unicode: bool = False,
198
+ dt: str = 'datetime',
199
+ fmt: Optional[str] = None,
200
+ timezone: str = 'local'
201
+ ) -> Dict[Any, Any]:
202
+ """ Convert all data of a dictionary to specific (json metadata) or guessed types
203
+
204
+ :param d: dictionary
205
+ :param metadata: dictionary containing types for the keys:
206
+ - boolean
207
+ - int
208
+ - float
209
+ - date
210
+ - datetime: date and time
211
+ - time: conversion or not based on dt
212
+ :param convert_keys: if True, keys are also converted via value_2datatype
213
+ :param convert_to_unicode: convert all bytes to unicode in the process
214
+ :param dt: - datetime or dt
215
+ - DateTime or DT
216
+ - ignore or string
217
+ - pendulum
218
+ - default: datetime, because it's Python's default
219
+ :param fmt: format for datetime parsing
220
+ :param timezone: default is 'local', other options are 'UTC' or 'Europe/Vienna'
221
+ :return: dictionary with values converted to their proper types
222
+ """
223
+ if metadata is None:
224
+ metadata = {}
225
+
226
+ if convert_keys:
227
+ return {
228
+ value_2datatype(
229
+ value=key,
230
+ convert_to_unicode=convert_to_unicode,
231
+ dt=dt,
232
+ fmt=fmt
233
+ ): value_2datatype(
234
+ value=val,
235
+ typ=metadata.get(key),
236
+ convert_to_unicode=convert_to_unicode,
237
+ dt=dt,
238
+ fmt=fmt,
239
+ timezone=timezone
240
+ )
241
+ for key, val in d.items()
242
+ } # dict comprehension
243
+ else:
244
+ return {
245
+ key: value_2datatype(
246
+ value=val,
247
+ typ=metadata.get(key),
248
+ convert_to_unicode=convert_to_unicode,
249
+ dt=dt,
250
+ fmt=fmt,
251
+ timezone=timezone
252
+ )
253
+ for key, val in d.items()
254
+ } # dict comprehension
255
+
256
+
257
+ def dictlist_2datatypes(
258
+ dictlist: List[Dict[Any, Any]],
259
+ metadata: Optional[Dict[Any, Any]] = None,
260
+ convert_keys: bool = False,
261
+ convert_to_unicode: bool = False,
262
+ dt: str = 'datetime',
263
+ fmt: Optional[str] = None,
264
+ timezone: str = 'local'
265
+ ) -> List[dict]:
266
+ """ Convert all data of a list of dictionaries to specific (json metadata) or guessed types
267
+
268
+ :param dictlist: list of dictionaries
269
+ :param metadata: dictionary containing types for the keys:
270
+ - boolean
271
+ - int
272
+ - float
273
+ - date
274
+ - datetime: date and time
275
+ - time: conversion or not based on dt
276
+ :param convert_keys: if True, keys are also converted via value_2datatype
277
+ :param convert_to_unicode: convert all bytes to unicode in the process
278
+ :param dt: - datetime or dt
279
+ - DateTime or DT
280
+ - ignore or string
281
+ - pendulum
282
+ - default: datetime, because it's Python's default
283
+ :param fmt: format for datetime parsing
284
+ :param timezone: default is 'local', other options are 'UTC' or 'Europe/Vienna'
285
+ :return: list of dictionaries with values converted to their proper types
286
+ """
287
+ return [
288
+ dict_2datatypes(
289
+ d=dic,
290
+ metadata=metadata,
291
+ convert_keys=convert_keys,
292
+ convert_to_unicode=convert_to_unicode,
293
+ dt=dt,
294
+ fmt=fmt,
295
+ timezone=timezone
296
+ )
297
+ for dic
298
+ in dictlist
299
+ ]
300
+
301
+
302
+ def sort_dictlist(
303
+ dictlist: List[dict],
304
+ keys: Union[str, List[str]],
305
+ reverse: bool = False
306
+ ) -> List[dict]:
307
+ """ Sorts a list of dictionaries
308
+
309
+ :param dictlist: list of dictionaries
310
+ :param keys: sort by these keys
311
+ :param reverse: True reverses the sorting order
312
+
313
+ :return: sorted list of dictionaries
314
+
315
+ .. deprecated:: 0.0.14
316
+ install package multisort and use it instead
317
+ """
318
+
319
+ if isinstance(keys, str):
320
+ if ',' in keys:
321
+ keys = keys.replace(
322
+ ' ', ''
323
+ ).split(',')
324
+ else:
325
+ keys = [keys]
326
+
327
+ if len(keys) == 1:
328
+ key = keys[0]
329
+ sorted_dictlist = sorted(
330
+ dictlist,
331
+ key=lambda i: i[key],
332
+ reverse=reverse
333
+ )
334
+ else:
335
+ sorted_dictlist = sorted(
336
+ dictlist,
337
+ key=lambda i: [i[key] for key in keys],
338
+ reverse=reverse
339
+ )
340
+
341
+ return sorted_dictlist
342
+
343
+
344
+ def value_2datatype(
345
+ value: Any,
346
+ typ: Optional[str] = None,
347
+ convert_to_unicode: bool = False,
348
+ encoding: str = 'utf-8',
349
+ dt: str = 'datetime',
350
+ fmt: Optional[str] = None,
351
+ timezone: str = 'local'
352
+ ) -> Optional[DataType]:
353
+ """ Convert a value to a datatype
354
+
355
+ this function has two modes:
356
+
357
+ 1. a type is provided for the conversion
358
+ 2. it makes educated guesses in converting a string
359
+
360
+ :param value:
361
+ :param typ: type for the conversion
362
+ :param convert_to_unicode: convert all bytes to unicode in the process
363
+ :param encoding: if bytes are present, use this encoding to convert them to unicode
364
+ :param dt: - datetime or dt
365
+ - DateTime or DT
366
+ - ignore or string
367
+ - pendulum
368
+ - default: datetime, because it's Python's default
369
+ :param timezone: default is 'local', other options are 'UTC' or 'Europe/Vienna'
370
+ :return: value converted to the appropriate type
371
+ """
372
+ if typ:
373
+ if typ == 'ignore':
374
+ return value
375
+ elif typ == 'string':
376
+ return str(value)
377
+ elif typ == 'boolean':
378
+ return any_2boolean(value=value)
379
+ elif typ == 'int':
380
+ try:
381
+ return int(value)
382
+ except ValueError:
383
+ return value
384
+ elif typ == 'float':
385
+ if isinstance(value, str):
386
+ value = value.replace(',', '.')
387
+ try:
388
+ return float(value)
389
+ except (ValueError, TypeError):
390
+ return value
391
+ elif typ == 'date':
392
+ if dt in ('datetime', 'dt'):
393
+ return convert_datetime(
394
+ value,
395
+ convert_to='date',
396
+ fmt=fmt
397
+ )
398
+ elif dt in ('DateTime', 'DT'):
399
+ if ' ' in value:
400
+ value = value.split()[0] # get rid of time
401
+ return DateTime.DateTime(
402
+ value,
403
+ datefmt='international'
404
+ ) + 0.5 # to avoid day-shifting due to timezones, use 12am
405
+ elif dt == 'pendulum':
406
+ if fmt:
407
+ p_dt = pendulum.from_format(value, fmt)
408
+ else:
409
+ p_dt = pendulum.parse(value)
410
+
411
+ if p_dt.timezone_name == 'UTC' and timezone == 'local':
412
+ return p_dt.in_tz(pendulum.local_timezone())
413
+ elif timezone:
414
+ return p_dt.in_tz(timezone)
415
+ return p_dt.date()
416
+ elif dt in ('ignore', 'string'):
417
+ return value
418
+ else:
419
+ raise ValueError(f'invalid value {dt} for argument dt')
420
+ elif typ == 'datetime':
421
+ if dt in ('datetime', 'dt'):
422
+ if fmt:
423
+ return datetime.datetime.strptime(value, fmt)
424
+ return convert_datetime(value, convert_to='datetime')
425
+ elif dt in ('DateTime', 'DT'):
426
+ value = value.strip()
427
+ try:
428
+ return DateTime.DateTime(
429
+ value,
430
+ datefmt='international'
431
+ )
432
+ except DateTime.interfaces.SyntaxError:
433
+ return value
434
+ elif dt == 'pendulum':
435
+ if fmt:
436
+ p_dt = pendulum.from_format(value, fmt)
437
+ else:
438
+ p_dt = pendulum.parse(value)
439
+
440
+ if p_dt.timezone_name == 'UTC' and timezone == 'local':
441
+ return p_dt.in_tz(pendulum.local_timezone())
442
+ elif timezone:
443
+ return p_dt.in_tz(timezone)
444
+ return p_dt
445
+ elif dt in ('ignore', 'string'):
446
+ return value
447
+ else:
448
+ raise ValueError(f'invalid value {dt} for argument dt')
449
+ elif 'time' in typ:
450
+ if 'datetime' in typ or dt in ('datetime', 'dt'):
451
+ parsed_dt = parse(value)
452
+ return parsed_dt.time()
453
+ else:
454
+ return value # DT: return the string
455
+
456
+ else:
457
+ if isinstance(value, (bool, int, float)):
458
+ return value
459
+
460
+ original_value = value
461
+ if isinstance(value, bytes):
462
+ value = value.decode(encoding)
463
+
464
+ if isinstance(value, str):
465
+ value = value.strip()
466
+
467
+ if value == 'False':
468
+ return False
469
+
470
+ if value == 'True':
471
+ return True
472
+
473
+ try:
474
+ if (
475
+ ':' in value
476
+ and set(value).issubset(time_chars)
477
+ and (
478
+ len(value) == 5 # hh:mm
479
+ or len(value) == 8 # hh:mm:ss
480
+ )
481
+ ):
482
+ if dt in ('datetime', 'dt'):
483
+ return parse(value).time()
484
+ else:
485
+ return value
486
+ elif get_dateformat(
487
+ datestring=value,
488
+ checkonly=True
489
+ ):
490
+ if dt in ('datetime', 'dt'):
491
+ return str_to_date(
492
+ datestring=value
493
+ )
494
+ elif dt in ('DateTime', 'DT'):
495
+ return str_to_DT(
496
+ datestring=value
497
+ )
498
+ elif dt == 'pendulum':
499
+ if fmt:
500
+ p_dt = pendulum.from_format(value, fmt)
501
+ else:
502
+ p_dt = pendulum.parse(value)
503
+
504
+ if p_dt.timezone_name == 'UTC' and timezone == 'local':
505
+ return p_dt.in_tz(pendulum.local_timezone())
506
+ elif timezone:
507
+ return p_dt.in_tz(timezone)
508
+ return p_dt
509
+ else:
510
+ return convert_datetime(
511
+ date=value,
512
+ convert_to=dt
513
+ )
514
+
515
+ except Exception:
516
+ # in case it looks like a date/time string but isn't parseable
517
+ pass
518
+
519
+ try:
520
+ return int(value)
521
+ except (ValueError, TypeError):
522
+ pass
523
+
524
+ if set(value).issubset(comma_float_chars):
525
+ value = value.replace(',', '.')
526
+
527
+ try:
528
+ return float(value)
529
+ except (ValueError, TypeError):
530
+ pass
531
+
532
+ if (
533
+ not convert_to_unicode
534
+ and isinstance(value, str)
535
+ ): # potentially restore bytes
536
+ value = original_value
537
+
538
+ return value