cdxcore 0.1.5__py3-none-any.whl → 0.1.9__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.

Potentially problematic release.


This version of cdxcore might be problematic. Click here for more details.

cdxcore/util.py CHANGED
@@ -1,5 +1,17 @@
1
1
  """
2
- Basic utilities for Python such as type management, formatting, some trivial timers
2
+ Overview
3
+ --------
4
+
5
+ Basic utilities for Python such as type management, formatting, some trivial timers.
6
+
7
+ Import
8
+ ------
9
+ .. code-block:: python
10
+
11
+ import cdxcore.util as util
12
+
13
+ Documentation
14
+ -------------
3
15
  """
4
16
 
5
17
  import datetime as datetime
@@ -12,9 +24,7 @@ from collections import OrderedDict
12
24
  from sortedcontainers import SortedDict
13
25
  import numpy as np
14
26
  import pandas as pd
15
- import os as os
16
- import warnings as warnings
17
- from collections.abc import Callable, Collection
27
+ from .err import fmt, _fmt, verify, error, warn_if, warn #NOQA
18
28
 
19
29
  # =============================================================================
20
30
  # basic indentification short cuts
@@ -22,187 +32,83 @@ from collections.abc import Callable, Collection
22
32
 
23
33
  __types_functions = None
24
34
 
25
- def types_functions():
26
- """ Returns all types.* considered function """
35
+ #: a set of all ``types`` considered functions
36
+ def types_functions() -> tuple[type]:
37
+ """ Returns a set of all ``types`` considered functions """
27
38
  global __types_functions
28
39
  if __types_functions is None:
29
- __types_functions = set()
30
- try: __types_functions.add(types.FunctionType)
40
+ fs = set()
41
+ try: fs.add(types.FunctionType)
31
42
  except: pass
32
- try: __types_functions.add(types.LambdaType)
43
+ try: fs.add(types.LambdaType)
33
44
  except: pass
34
- try: __types_functions.add(types.CodeType)
45
+ try: fs.add(types.CodeType)
35
46
  except: pass
36
47
  #types.MappingProxyType
37
48
  #types.SimpleNamespace
38
- try: __types_functions.add(types.GeneratorType)
49
+ try: fs.add(types.GeneratorType)
39
50
  except: pass
40
- try: __types_functions.add(types.CoroutineType)
51
+ try: fs.add(types.CoroutineType)
41
52
  except: pass
42
- try: __types_functions.add(types.AsyncGeneratorType)
53
+ try: fs.add(types.AsyncGeneratorType)
43
54
  except: pass
44
- try: __types_functions.add(types.MethodType)
55
+ try: fs.add(types.MethodType)
45
56
  except: pass
46
- try: __types_functions.add(types.BuiltinFunctionType)
57
+ try: fs.add(types.BuiltinFunctionType)
47
58
  except: pass
48
- try: __types_functions.add(types.BuiltinMethodType)
59
+ try: fs.add(types.BuiltinMethodType)
49
60
  except: pass
50
- try: __types_functions.add(types.WrapperDescriptorType)
61
+ try: fs.add(types.WrapperDescriptorType)
51
62
  except: pass
52
- try: __types_functions.add(types.MethodWrapperType)
63
+ try: fs.add(types.MethodWrapperType)
53
64
  except: pass
54
- try: __types_functions.add(types.MethodDescriptorType)
65
+ try: fs.add(types.MethodDescriptorType)
55
66
  except: pass
56
- try: __types_functions.add(types.ClassMethodDescriptorType)
67
+ try: fs.add(types.ClassMethodDescriptorType)
57
68
  except: pass
58
69
  #types.ModuleType,
59
70
  #types.TracebackType,
60
71
  #types.FrameType,
61
- try: __types_functions.add(types.GetSetDescriptorType)
72
+ try: fs.add(types.GetSetDescriptorType)
62
73
  except: pass
63
- try: __types_functions.add(types.MemberDescriptorType)
74
+ try: fs.add(types.MemberDescriptorType)
64
75
  except: pass
65
- try: __types_functions.add(types.DynamicClassAttribute)
76
+ try: fs.add(types.DynamicClassAttribute)
66
77
  except: pass
67
- __types_functions = tuple(__types_functions)
78
+ __types_functions = tuple(fs)
68
79
  return __types_functions
69
80
 
70
- def isFunction(f) -> bool:
81
+ def is_function(f) -> bool:
71
82
  """
72
- Checks whether 'f' is a function in an extended sense.
73
- Check 'types_functions' for what is tested against.
74
- In particular it does not test positive for properties.
83
+ Checks whether ``f`` is a function in an extended sense.
84
+
85
+ Check :func:`cdxcore.util.types_functions` for what is tested against.
86
+ In particular ``is_function`` does not test positive for properties.
75
87
  """
76
88
  return isinstance(f,types_functions())
77
89
 
78
- def isAtomic( o ):
79
- """ Returns true if 'o' is a string, int, float, date or bool, or a numpy generic """
90
+ def is_atomic( o ):
91
+ """
92
+ Whether an element is atomic.
93
+
94
+ Returns ``True`` if ``o`` is a
95
+ ``string``, ``int``, ``float``, :class:`datedatime.date`, ``bool``,
96
+ or a :class:`numpy.generic`
97
+ """
80
98
  if type(o) in [str,int,bool,float,datetime.date]:
81
99
  return True
82
100
  if isinstance(o,np.generic):
83
101
  return True
84
102
  return False
85
103
 
86
- def isFloat( o ):
87
- """ Checks whether a type is a float """
104
+ def is_float( o ):
105
+ """ Checks whether a type is a ``float`` which includes numpy floating types """
88
106
  if type(o) is float:
89
107
  return True
90
108
  if isinstance(o,np.floating):
91
109
  return True
92
110
  return False
93
111
 
94
- # =============================================================================
95
- # exceptions
96
- # =============================================================================
97
-
98
- def _verify( cond : bool, msgf : Callable, exception : Exception = Exception, **msg_kwargs ):
99
- """
100
- Verifies 'cond' and raises an exception of type 'exception' with message 'msgf()' if cond is not True.
101
- The message itself is generated by calling msgf() if it is a callable, or by str.fomrat(msgf,**msg_kwargs) if it is a string.
102
- That means that the message is only formatted if the conditon was not met and an exception is to be thrown:
103
-
104
- Functional use case:
105
- x=1
106
- verify(x==1, lambda : f"Error: x is {x}") # <- the use of 'lambda' delays generation of the error message
107
-
108
- If 'msgf' is a string, then str.format(msgf, **msg_kwargs) is called:
109
- x=1
110
- verify(x==1, "Error: x is {x}", x=x ) # <- do *not* use f-string !
111
-
112
- Basically
113
- verify(cond, msgf, exception, **msg_kwargs)
114
- is functionally equivalent to
115
- if not cond: raise exception(msgf())
116
- or, if 'msgf' is a string:
117
- if not cond: raise exception(str.format(msgf,**msg_kwargs))
118
-
119
- Parameters
120
- ----------
121
- cond:
122
- condition to be tested. An exception is thrown if it is not true
123
- msgf:
124
- function to call to generate the error message if 'cond' is False,
125
- or str.format formatting string using {} [this is *not* an f-string].
126
- In both case the message is only generated if the conditon 'cond' is False.
127
-
128
- Two main use cases:
129
- For complicated formatting, use a lambda function which returns an f-string.
130
- For simple formatting, use a non-fstring.
131
- exception:
132
-
133
- Anything the function msfg() returns is passed to the constructor
134
- of the 'exception', except if msgf() returns itself an Exception
135
- object. In that case that will be raised.
136
- The function msgf cannot return None.
137
- exception:
138
- Exception type to raise.
139
- msg_kwargs :
140
- Keywords for msgf if msgf is a string; must be empty otherwise.
141
- """
142
- if not bool(cond):
143
- if not isinstance( msgf, str ):
144
- assert len(msg_kwargs) == 0, ("Superflous arguments passed", str(msg_kwargs)[:100] )
145
- msg = msgf()
146
- assert not msg is None, ("'msgf' returned None")
147
- if isinstance(msg, Exception):
148
- raise msg
149
- else:
150
- msg = str.format(msgf,**msg_kwargs)
151
- raise exception( msg )
152
-
153
- _warn_skips = (os.path.dirname(__file__),)
154
-
155
- def _warn( message : str, category : Warning = RuntimeWarning, stack_level : int = 1 ):
156
- """ Standard warning """
157
- warnings.warn( message=str(message), category=category, stacklevel=stack_level, skip_file_prefixes=_warn_skips )
158
-
159
- def _warn_if( cond : bool, msgf : Callable, *, category : Warning = RuntimeWarning, stack_level : int = 1, **msg_kwargs ):
160
- """
161
- Tests 'cond' and issues a warning with message 'msgf()'.
162
- The message itself is generated by calling msgf() if it is a callable, or by str.fomrat(msgf,**msg_kwargs) if it is a string.
163
- That means that the message is only formatted if the conditon was met and the warning is printed.
164
-
165
- Functional use case:
166
- x=1
167
- warn_if(x!=0, lambda : f"Warn: x is {x}") # <- the use of 'lambda' delays generation of the warning message
168
-
169
- If 'msgf' is a string, then str.format(msgf, **msg_kwargs) is called:
170
- x=1
171
- verify(x!=1, "Warn: x is {x}", x=x ) # <- do *not* use f-string !
172
-
173
- Basically
174
- warn_if(cond, msgf, exception, **msg_kwargs)
175
- is functionally equivalent to
176
- if cond: warn(msgf())
177
- or, if 'msgf' is a string:
178
- if cond: warn(str.format(msgf,**msg_kwargs))
179
-
180
- Parameters
181
- ----------
182
- cond:
183
- condition to be tested. An exception is thrown if it is not true
184
- msgf:
185
- function to call to generate the error message if 'cond' is False,
186
- or str.format formatting string using {} [this is *not* an f-string].
187
- In both case the message is only generated if the conditon 'cond' is False.
188
-
189
- Two main use cases:
190
- For complicated formatting, use a lambda function which returns an f-string.
191
- For simple formatting, use a non-fstring.
192
- exception:
193
- exception:
194
- Exception type to raise.
195
- msg_kwargs :
196
- Keywords for msgf if msgf is a string; must be empty otherwise.
197
- """
198
- if bool(cond):
199
- if not isinstance( msgf, str ):
200
- assert len(msg_kwargs) == 0, ("Superflous arguments passed?", str(msg_kwargs)[:100] )
201
- msg = msgf()
202
- else:
203
- msg = str.format(msgf,**msg_kwargs)
204
- _warn( msg, category=category, stack_level=stack_level )
205
-
206
112
  # =============================================================================
207
113
  # python basics
208
114
  # =============================================================================
@@ -210,6 +116,7 @@ def _warn_if( cond : bool, msgf : Callable, *, category : Warning = RuntimeWarni
210
116
  def _get_recursive_size(obj, seen=None):
211
117
  """
212
118
  Recursive helper for sizeof
119
+ :meta private:
213
120
  """
214
121
  if seen is None:
215
122
  seen = set() # Keep track of seen objects to avoid double-counting
@@ -222,7 +129,7 @@ def _get_recursive_size(obj, seen=None):
222
129
  return 0
223
130
  seen.add(id(obj))
224
131
 
225
- if isinstance( obj, np.ndarray ):
132
+ if isinstance( obj, (np.ndarray, pd.DataFrame) ):
226
133
  size += obj.nbytes
227
134
  elif isinstance(obj, Mapping):
228
135
  for key, value in obj.items():
@@ -244,8 +151,11 @@ def _get_recursive_size(obj, seen=None):
244
151
 
245
152
  def getsizeof(obj):
246
153
  """
247
- Approximates the size of 'obj'.
248
- In addition to sys.getsizeof this function also iterates through embedded containers.
154
+ Approximates the size of an object.
155
+
156
+ In addition to calling :func:`sys.getsizeof` this function
157
+ also iterates embedded containers, numpy arrays, and panda dataframes.
158
+ :meta private:
249
159
  """
250
160
  return _get_recursive_size(obj,None)
251
161
 
@@ -253,38 +163,22 @@ def getsizeof(obj):
253
163
  # string formatting
254
164
  # =============================================================================
255
165
 
256
- def _fmt( text : str, args = None, kwargs = None ) -> str:
257
- """ Utility function. See fmt() """
258
- if text.find('%') == -1:
259
- return text
260
- if not args is None and len(args) > 0:
261
- assert kwargs is None or len(kwargs) == 0, "Cannot specify both 'args' and 'kwargs'"
262
- return text % tuple(args)
263
- if not kwargs is None and len(kwargs) > 0:
264
- return text % kwargs
265
- return text
266
-
267
- def fmt(text : str,*args,**kwargs) -> str:
268
- """
269
- String formatting made easy
270
- text - pattern
271
- Examples
272
- fmt("The is one = %ld", 1)
273
- fmt("The is text = %s", 1.3)
274
- fmt("Using keywords: one=%(one)d, two=%(two)d", two=2, one=1)
166
+ def fmt_seconds( seconds : float, *, eps : float = 1E-8 ) -> str:
275
167
  """
276
- return _fmt(text,args,kwargs)
277
-
278
- def prnt(text : str,*args,**kwargs) -> str:
279
- """ Prints a fmt() string. """
280
- print(_fmt(text,args,kwargs))
281
-
282
- def write(text : str,*args,**kwargs) -> str:
283
- """ Prints a fmt() string without EOL, e.g. uses print(fmt(..),end='') """
284
- print(_fmt(text,args,kwargs),end='')
168
+ Generate format string for seconds, e.g. "23s"" for ``seconds=23``, or "1:10" for ``seconds=70``.
169
+
170
+ Parameters
171
+ ----------
172
+ seconds : float
173
+ Seconds as a float.
174
+
175
+ eps : float
176
+ anything below ``eps`` is considered zero. Default ``1E-8``.
285
177
 
286
- def fmt_seconds( seconds : float, *, eps : float = 1E-8 ) -> str:
287
- """ Print nice format string for seconds, e.g. '23s' for seconds=23, or 1:10 for seconds=70 """
178
+ Returns
179
+ -------
180
+ Seconds : string
181
+ """
288
182
  assert eps>=0., ("'eps' must not be negative")
289
183
  if seconds < -eps:
290
184
  return "-" + fmt_seconds(-seconds, eps=eps)
@@ -304,19 +198,25 @@ def fmt_seconds( seconds : float, *, eps : float = 1E-8 ) -> str:
304
198
 
305
199
  def fmt_list( lst : list, *, none : str = "-", link : str = "and", sort : bool = False ) -> str:
306
200
  """
307
- Returns a nicely formatted list of string with commas
308
-
201
+ Returns a formatted string of a list, its elements separated by commas and (by default) a final 'and'.
202
+
203
+ If the list is ``[1,2,3]`` then the function will return ``"1, 2 and 3"``.
204
+
309
205
  Parameters
310
206
  ----------
311
- lst : list. The list() operator is applied to it, so it will resolve dictionaries and generators.
312
- none : string used when list was empty
313
- link : string used to connect the last item. Default is 'and'
314
- If the list is [1,2,3] then the function will return 1, 2 and 3
315
- sort : whether to sort the list
207
+ lst : list.
208
+ The ``list()`` operator is applied to ``lst``, so it will resolve dictionaries and generators.
209
+ none : str, optional
210
+ String to be used when ``list`` is empty. Default is ``"-"``.
211
+ link : str, optional
212
+ String to be used to connect the last item. Default is ``"and"``.
213
+ sort : bool, optional
214
+ Whether to sort the list. Default is ``False``.
316
215
 
317
216
  Returns
318
217
  -------
319
- String of the list.
218
+ Text : str
219
+ String.
320
220
  """
321
221
  if lst is None:
322
222
  return str(none)
@@ -343,21 +243,27 @@ def fmt_list( lst : list, *, none : str = "-", link : str = "and", sort : bool =
343
243
 
344
244
  def fmt_dict( dct : dict, *, sort : bool = False, none : str = "-", link : str = "and" ) -> str:
345
245
  """
346
- Return a nice readable representation of a dictionary
347
- This assumes that the elements of the dictionary itself can be formatted well with 'str()'
348
-
349
- For a dictionary dict(a=1,b=2,c=3) this function will return a: 1, b: 2, and c: 3
246
+ Return a readable representation of a dictionary.
247
+
248
+ This assumes that the elements of the dictionary itself can be formatted well with :func:`str()`.
249
+
250
+ For a dictionary ``dict(a=1,b=2,c=3)`` this function will return ``"a: 1, b: 2, and c: 3"``.
350
251
 
351
252
  Parameters
352
253
  ----------
353
- x : dict
354
- sort : whether to sort the keys
355
- none : string to be used if dictionary is empty
356
- link : string to be used to link the last element to the previous string
254
+ dct : dict
255
+ The dictionary to format.
256
+ sort : bool, optional
257
+ Whether to sort the keys. Default is ``False``.
258
+ none : str, optional
259
+ String to be used if dictionary is empty. Default is ``"-"``.
260
+ link : str, optional
261
+ String to be used to link the last element to the previous string. Default is ``"and"``.
357
262
 
358
263
  Returns
359
264
  -------
360
- String
265
+ Text : str
266
+ String.
361
267
  """
362
268
  if len(dct) == 0:
363
269
  return str(none)
@@ -368,46 +274,52 @@ def fmt_dict( dct : dict, *, sort : bool = False, none : str = "-", link : str =
368
274
  strs = [ str(k) + ": " + str(dct[k]) for k in keys ]
369
275
  return fmt_list( strs, none=none, link=link, sort=False )
370
276
 
371
- def fmt_digits( uint : int, sep : str = "," ):
277
+ def fmt_digits( integer : int, sep : str = "," ):
372
278
  """
373
- String representation of 'uint' with 1000 separators
374
- So 10000 becomes "10,000".
279
+ String representation of an integer with 1000 separators: 10000 becomes "10,000".
375
280
 
376
281
  Parameters
377
282
  --------
378
- uint : integer
379
- The number. The function will int() the input which allows
380
- for processing of a number of inputs (such as strings) but
381
- might cut off floating point numbers.
382
- sep : str
383
- Separator, ","" by default
283
+ integer : int
284
+ The number. The function will :func:`int()` the input which allows
285
+ for processing of a number of inputs (such as strings) but
286
+ might cut off floating point numbers.
287
+
288
+ sep : str
289
+ Separator; ``","`` by default.
290
+
384
291
  Returns
385
292
  -------
386
- String
387
- """
388
- if isinstance( uint, float ):
389
- raise ValueError("float value provided", uint)
390
- uint = int(uint)
391
- if uint < 0:
392
- return "-" + fmt_digits( -uint, sep )
393
- assert uint >= 0
394
- if uint < 1000:
395
- return "%ld" % uint
293
+ Text : str
294
+ String.
295
+ """
296
+ if isinstance( integer, float ):
297
+ raise ValueError("float value provided", integer)
298
+ integer = int(integer)
299
+ if integer < 0:
300
+ return "-" + fmt_digits( -integer, sep )
301
+ assert integer >= 0
302
+ if integer < 1000:
303
+ return "%ld" % integer
396
304
  else:
397
- return fmt_digits(uint//1000, sep) + ( sep + "%03ld" % (uint % 1000) )
305
+ return fmt_digits(integer//1000, sep) + ( sep + "%03ld" % (integer % 1000) )
398
306
 
399
307
  def fmt_big_number( number : int ) -> str:
400
308
  """
401
309
  Return a formatted big number string, e.g. 12.35M instead of all digits.
310
+
402
311
  Uses decimal system and "B" for billions.
403
- Use fmt_big_byte_number for byte sizes ie 1024 units.
312
+ Use :func:`cdxcore.util.fmt_big_byte_number` for byte sizes i.e. 1024 units.
404
313
 
405
314
  Parameters
406
315
  ----------
407
- number : int
316
+ number : int
317
+ Number to format.
318
+
408
319
  Returns
409
320
  -------
410
- String number
321
+ Text : str
322
+ String.
411
323
  """
412
324
  if isinstance( number, float ):
413
325
  raise ValueError("float value provided", number)
@@ -439,20 +351,31 @@ def fmt_big_number( number : int ) -> str:
439
351
  return "%gK" % number
440
352
  return str(number)
441
353
 
442
- def fmt_big_byte_number( byte_cnt : int, str_B = True ) -> str:
354
+ def fmt_big_byte_number( byte_cnt : int, str_B : bool = True ) -> str:
443
355
  """
444
- Return a formatted big number string, e.g. 12.35M instead of all digits.
356
+ Return a formatted big byte string, e.g. 12.35MB.
357
+ Uses 1024 as base for KB.
358
+
359
+ Use :func:`cdxcore.util.fmt_big_number` for converting general numbers
360
+ using 1000 blocks instead.
445
361
 
446
362
  Parameters
447
363
  ----------
448
- byte_cnt : int
449
- str_B : bool
450
- If true, return GB, MB and KB. If False, return G, M, K
451
- If 'byte_cnt' is less than 10KB, then this will add 'bytes'
452
- e.g. '1024 bytes'
364
+ byte_cnt : int
365
+ Number of bytes.
366
+
367
+ str_B : bool
368
+ If ``True``, return ``"GB"``, ``"MB"`` and ``"KB"`` units.
369
+ Moreover, if ``byte_cnt` is less than 10KB, then this will add ``"bytes"``
370
+ e.g. ``"1024 bytes"``.
371
+
372
+ If ``False``, return ``"G"``, ``"M"`` and ``"K"`` only, and do not
373
+ add ``"bytes"`` to smaller ``byte_cnt``.
374
+
453
375
  Returns
454
376
  -------
455
- String number
377
+ Text : str
378
+ String.
456
379
  """
457
380
  if isinstance( byte_cnt, float ):
458
381
  raise ValueError("float value provided", byte_cnt)
@@ -487,36 +410,45 @@ def fmt_big_byte_number( byte_cnt : int, str_B = True ) -> str:
487
410
  return str(byte_cnt) if not str_B else f"{byte_cnt} bytes"
488
411
  return s if not str_B else s+"B"
489
412
 
490
- def fmt_datetime(dt : datetime.datetime, *,
413
+ def fmt_datetime(dt : datetime.datetime|datetime.date|datetime.time, *,
491
414
  sep : str = ':',
492
415
  ignore_ms : bool = False,
493
416
  ignore_tz : bool = True
494
417
  ) -> str:
495
418
  """
496
- Returns string for 'dt' of the form "YYYY-MM-DD HH:MM:SS" if 'dt' is a datetime,
497
- or a the respective version for time or date.
419
+ Convert :class:`datetime.datetime` to a string of the form "YYYY-MM-DD HH:MM:SS".
498
420
 
499
- Microseconds are added as digits:
500
- "YYYY-MM-DD HH:MM:SS,MICROSECONDS"
421
+ If present, microseconds are added as digits::
422
+
423
+ YYYY-MM-DD HH:MM:SS,MICROSECONDS
424
+
425
+ Optinally a time zone is added via::
501
426
 
502
- Optinally a time zone is added via:
503
- "YYYY-MM-DD HH:MM:SS+HH"
504
- "YYYY-MM-DD HH:MM:SS+HH:MM"
427
+ YYYY-MM-DD HH:MM:SS+HH
428
+ YYYY-MM-DD HH:MM:SS+HH:MM
505
429
 
430
+ Output is reduced accordingly if ``dt`` is a :class:`datetime.time`
431
+ or :class:`datetime.date`.
432
+
506
433
  Parameters
507
434
  ----------
508
- dt : datetime, date, or time
509
- String represent this.
510
- sep : str
511
- Seperator for hours, minutes, seconds. The default ':' looks better
512
- but is not suitable for filenames
513
- ignore_ms : bool
514
- Whether to ignore microseconds. Default False
515
- ignore_tz : bool
516
- Whether to ignore the time zone. Default True
435
+ dt : :class:`datetime.datetime`, :class:`datetime.date`, or :class:`datetime.time`
436
+ Input.
437
+
438
+ sep : str, optional
439
+ Seperator for hours, minutes, seconds. The default ``':'`` is most appropriate for viusalization
440
+ but is not suitable for filenames.
441
+
442
+ ignore_ms : bool, optional
443
+ Whether to ignore microseconds. Default ``False``.
444
+
445
+ ignore_tz : bool, optional
446
+ Whether to ignore the time zone. Default ``True``.
447
+
517
448
  Returns
518
449
  -------
519
- String, see above.
450
+ Text : str
451
+ String.
520
452
  """
521
453
  if not isinstance(dt, datetime.datetime):
522
454
  if isinstance(dt, datetime.date):
@@ -552,12 +484,13 @@ def fmt_datetime(dt : datetime.datetime, *,
552
484
 
553
485
  def fmt_date(dt : datetime.date) -> str:
554
486
  """
555
- Returns string representation for date 'dt' of the form YYYY-MM-DD
556
- If passed a datetime, it will extract its date().
487
+ Returns string representation for a date of the form "YYYY-MM-DD".
488
+
489
+ If passed a :class:`datetime.datetime`, it will format its :func:`datetime.datetime.date`.
557
490
  """
558
491
  if isinstance(dt, datetime.datetime):
559
492
  dt = dt.date()
560
- assert isinstance(dt, datetime.date), "'dt' must be datetime.date. Found %s" % type(dt)
493
+ assert isinstance(dt, datetime.date), "'dt' must be :class:`datetime.date`. Found %s" % type(dt)
561
494
  return f"{dt.year:04d}-{dt.month:02d}-{dt.day:02d}"
562
495
 
563
496
  def fmt_time(dt : datetime.time, *,
@@ -565,34 +498,46 @@ def fmt_time(dt : datetime.time, *,
565
498
  ignore_ms : bool = False
566
499
  ) -> str:
567
500
  """
568
- Returns string for 'dt' of the form "HH:MM:SS" if 'dt'.
501
+ Convers a time to a string with format "HH:MM:SS".
569
502
 
570
- Microseconds are added as digits:
571
- "HH:MM:SS,MICROSECONDS"
503
+ Microseconds are added as digits::
504
+
505
+ HH:MM:SS,MICROSECONDS
572
506
 
573
- Optinally a time zone is added via:
574
- "HH:MM:SS+HH"
575
-
576
- If passed a datetime, it will extract its time().
577
- Note that while datetime.time objects may carry a tzinfo object,
578
- the corresponding otcoffset() function returns None without
579
- providing a 'dt' parameter, see https://docs.python.org/3/library/datetime.html#tzinfo-objects
580
- We bypass this inconsistency by only allowing datetime to process time zones.
507
+ If passed a :class:`datetime.datetime`, then this function will format
508
+ only its :func:`datetime.datetime.time` part.
509
+
510
+ **Time Zones**
511
+
512
+ Note that while :class:`datetime.time` objects may carry a ``tzinfo`` time zone object,
513
+ the corresponding :func:`datetime.time.otcoffset` function returns ``None`` if we donot
514
+ provide a ``dt`` parameter, see
515
+ `tzinfo documentation <https://docs.python.org/3/library/datetime.html#tzinfo-objects>`__.
516
+ That means :func:`datetime.time.otcoffset` is only useful if we have :class:`datetime.datetime`
517
+ object at hand.
518
+ That makes sense as a time zone can chnage date as well.
581
519
 
520
+ We therefore here do not allow ``dt`` to contain
521
+ a time zone.
522
+
523
+ Use :func:`cdxcore.util.fmt_datetime` for time zone support
582
524
 
583
525
  Parameters
584
526
  ----------
585
- dt : time
586
- String represent this.
587
- sep : str
588
- Seperator for hours, minutes, seconds. The default ':' looks better
589
- but is not suitable for filenames
590
- ignore_ms : bool
591
- Whether to ignore microseconds. Default False
527
+ dt : :class:`datetime.time`
528
+ Input.
529
+ sep : str, optional
530
+
531
+ Seperator for hours, minutes, seconds. The default ``':'`` is most appropriate for viusalization
532
+ but is not suitable for filenames.
533
+
534
+ ignore_ms : bool
535
+ Whether to ignore microseconds. Default is ``False``.
592
536
 
593
537
  Returns
594
538
  -------
595
- String, see above.
539
+ Text : str
540
+ String.
596
541
  """
597
542
  if isinstance(dt, datetime.datetime):
598
543
  dt = dt.timetz()
@@ -606,28 +551,33 @@ def fmt_time(dt : datetime.time, *,
606
551
  def fmt_timedelta(dt : datetime.timedelta, *,
607
552
  sep : str = "" ) -> str:
608
553
  """
609
- Returns string representation for a time delta in the form DD:HH:MM:SS,MS
610
-
554
+ Returns string representation for a time delta in the form "DD:HH:MM:SS,MS".
611
555
 
612
556
  Parameters
613
557
  ----------
614
- dt : timedelta
615
- Timedelta.
616
- sep :
617
- Identify the three separators: between days and HMS and between microseconds:
618
- DD*HH*MM*SS*MS
619
- 0 1 1 2
620
- 'sep' can be a string, in which case:
621
- * If it is an empty string, all separators are ''
622
- * A single character will be reused for all separators
623
- * If the string has length 2, then the last character is used for '2'
624
- * If the string has length 3, then the chracters are used accordingly
625
- 'sep' can also be a collection ie a tuple or list. In this case each element
626
- is used accordingly.
558
+ dt : :class:`datetime.timedelta`
559
+ Timedelta.
560
+
561
+ sep :
562
+ Identify the three separators: between days, and HMS and between microseconds:
563
+
564
+ .. code-block:: python
565
+
566
+ DD*HH*MM*SS*MS
567
+ 0 1 1 2
568
+
569
+ * ``sep`` can be a string, in which case:
570
+ * If it is an empty string, all separators are ``''``.
571
+ * A single character will be reused for all separators.
572
+ * If the string has length 2, then the last character is used for ``'2'``.
573
+ * If the string has length 3, then the chracters are used accordingly.
574
+
575
+ * ``sep`` can also be a collection ie a ``tuple`` or ``list``. In this case each element is used accordingly.
627
576
 
628
577
  Returns
629
578
  -------
630
- String with leading sign. Returns "" if timedelta is 0.
579
+ Text : str
580
+ String with leading sign. Returns "" if ``timedelta`` is 0.
631
581
  """
632
582
  assert isinstance(dt, datetime.timedelta), "'dt' must be datetime.timedelta. Found %s" % type(dt)
633
583
 
@@ -690,10 +640,10 @@ def fmt_timedelta(dt : datetime.timedelta, *,
690
640
  return f"{sign}{days}d{rest}"
691
641
 
692
642
  def fmt_now() -> str:
693
- """ Returns string for 'now' """
643
+ """ Returns the :func:`cdxcore.util.fmt_datetime` applied to :func:`datetime.datetime.now` """
694
644
  return fmt_datetime(datetime.datetime.now())
695
645
 
696
- DEF_FILE_NAME_MAP = {
646
+ DEF_FILE_NAME_MAP = {
697
647
  '/' : "_",
698
648
  '\\': "_",
699
649
  '|' : "_",
@@ -703,144 +653,109 @@ DEF_FILE_NAME_MAP = {
703
653
  '?' : "!",
704
654
  '*' : "@",
705
655
  }
706
- INVALID_FILE_NAME_CHARCTERS = set(DEF_FILE_NAME_MAP)
656
+ """
657
+ Default map from characters which cannot be used for filenames under either
658
+ Windows or Linux to valid characters.
659
+ """
707
660
 
708
- def fmt_filename( s : str , by : str = DEF_FILE_NAME_MAP ):
709
- """
710
- Replaces invalid filename characters by a differnet character.
661
+ def fmt_filename( filename : str , by : str | Mapping = "default" ) -> str:
662
+ r"""
663
+ Replaces invalid filename characters such as `\\', ':', or '/' by a differnet character.
711
664
  The returned string is technically a valid file name under both windows and linux.
712
665
 
713
666
  However, that does not prevent the filename to be a reserved name, for example "." or "..".
714
667
 
715
668
  Parameters
716
669
  ----------
717
- s : str
718
- Input string
719
- by :
720
- Either a single character or a dictionary with elements.
670
+ filename : str
671
+ Input string.
672
+
673
+ by : str | Mapping, optional.
674
+ A dictionary of characters and their replacement.
675
+ The default value ``"default"`` leads to using :data:`cdxcore.util.DEF_FILE_NAME_MAP`.
676
+
677
+ Returns
678
+ -------
679
+ Text : str
680
+ Filename
721
681
  """
682
+ if not isinstance(by, Mapping):
683
+ if not isinstance(by, str):
684
+ raise ValueError(f"'by': must be a Mapping or 'default'. Found type {type(by).__qualname__}")
685
+ if by != "default":
686
+ raise ValueError(f"'by': must be a Mapping or 'default'. Found string '{by}'")
687
+ by = DEF_FILE_NAME_MAP
722
688
 
723
- if isinstance(by, Mapping):
724
- for c in INVALID_FILE_NAME_CHARCTERS:
725
- s = s.replace(c, by[c])
726
- else:
727
- assert isinstance(by, str), ("by: 'str' or mapping expected", type(by))
728
- for c in INVALID_FILE_NAME_CHARCTERS:
729
- s = s.replace(c, by)
730
- return s
689
+ for c, cby in by.items():
690
+ filename = filename.replace(c, cby)
691
+ return filename
692
+ fmt_filename.DEF_FILE_NAME_MAP = DEF_FILE_NAME_MAP
731
693
 
732
- class WriteLine(object):
694
+ def is_filename( filename : str , by : str | Collection = "default" ) -> bool:
733
695
  """
734
- Class to manage the current text output line.
735
- This class is a thin wrapper around print(text + '\r', end='') or IPython.display.display()
736
- to ensure the current line is cleared correctly when replaced with the next line.
737
-
738
- Example 1 (how to use \r and \n)
739
- write = WriteLine("Initializing...")
740
- import time
741
- for i in range(10):
742
- time.sleep(1)
743
- write("\rRunning %g%% ...", round(float(i+1)/float(10)*100,0))
744
- write(" done.\nProcess finished.\n")
745
-
746
- Example 2 (line length is getting shorter)
747
- write = WriteLine("Initializing...")
748
- import time
749
- for i in range(10):
750
- time.sleep(1)
751
- write("\r" + ("#" * (9-i)))
752
- write("\rProcess finished.\n")
753
- """
754
-
755
- def __init__(self, text : str = "", *kargs, **kwargs):
756
- """
757
- Creates a new WriteLine object which manages the current print output line.
758
- Subsequent calls to __call__() will replace the text in the current line using `\r` in text mode, or a display() object in jupyter
759
-
760
- Parameters
761
- ----------
762
- text : str
763
- Classic formatting text. 'text' may not contain newlines (\n) except at the end.
764
- kargs, kwargs:
765
- Formatting arguments.
766
- """
767
- self._last_len = 0
768
- if text != "":
769
- self(text,*kargs,**kwargs)
696
+ Tests whether a filename is indeed a valid filename.
770
697
 
771
- def __call__(self, text : str, *kargs, **kwargs ):
772
- """
773
- Print lines of text.
774
- The last line of 'text' becomes the current line and will be overwritten by the next line.
775
-
776
- Parameters
777
- ----------
778
- text : str
779
- Classic formatting text. 'text' may not contain newlines (\n) except at the end.
780
- kargs, kwargs:
781
- Formatting arguments.
782
- """
783
- text = _fmt(text,kargs,kwargs)
784
- lines = text.split("\n")
785
- assert len(lines) > 0, "Internal error"
786
-
787
- for line in lines[:-1]:
788
- self._write_line(line)
789
- self.cr()
790
- if len(lines[-1]) > 0:
791
- self._write_line(lines[-1])
792
- sys.stdout.flush()
793
-
794
- def cr(self):
795
- """ Creates a new line. """
796
- sys.stdout.write("\n")
797
- sys.stdout.flush()
798
- self._last_len = 0
799
-
800
- def _write_line(self, line):
801
- """ Write a line; no newlines """
802
- assert not '\n' in line, "Error: found newline in '%s'" % line
803
- if line == "":
804
- return
805
- i = line.rfind('\r')
806
- if i == -1:
807
- # no `\r': append text to current line
808
- sys.stdout.write(line)
809
- self._last_len += len(line)
810
- else:
811
- # found '\r': clear previous line and print new line
812
- line = line[i+1:]
813
- if len(line) < self._last_len:
814
- sys.stdout.write("\r" + (" " * self._last_len)) # clear current line
815
- sys.stdout.write("\r" + line)
816
- self._last_len = len(line)
698
+ Parameters
699
+ ----------
700
+ filename : str
701
+ Supposed filename.
702
+
703
+ by : str | Collection, optional
704
+ A collection of invalid characters.
705
+ The default value ``"default"`` leads to using
706
+ they keys of :data:`cdxcore.util.DEF_FILE_NAME_MAP`.
707
+
708
+ Returns
709
+ -------
710
+ Validity : vool
711
+ ``True`` if ``filename`` does not contain any invalid characters contained in ``by``.
712
+ """
713
+
714
+ if not isinstance(by, Mapping):
715
+ if not isinstance(by, str):
716
+ raise ValueError(f"'by': must be a Mapping or 'default'. Found type {type(by).__qualname__}")
717
+ if by != "default":
718
+ raise ValueError(f"'by': must be a Mapping or 'default'. Found string '{by}'")
719
+ by = DEF_FILE_NAME_MAP
720
+
721
+ for c in by:
722
+ if c in filename:
723
+ return False
724
+ return True
817
725
 
818
726
  # =============================================================================
819
727
  # Conversion of arbitrary python elements into re-usable versions
820
728
  # =============================================================================
821
729
 
822
- # deprecated
823
730
  def plain( inn, *, sorted_dicts : bool = False,
824
731
  native_np : bool = False,
825
732
  dt_to_str : bool = False):
826
733
  """
827
734
  Converts a python structure into a simple atomic/list/dictionary collection such
828
735
  that it can be read without the specific imports used inside this program.
829
- or example, objects are converted into dictionaries of their data fields.
736
+
737
+ For example, objects are converted into dictionaries of their data fields.
830
738
 
831
739
  Parameters
832
740
  ----------
833
- inn : some object
834
- sorted_dicts : Use SortedDicts instead of dicts.
835
- native_np : convert numpy to Python natives.
836
- dt_to_str : convert date times to strings
741
+ inn :
742
+ some object.
743
+ sorted_dicts : bool, optional
744
+ use SortedDicts instead of dicts. Since Python 3.7 all dictionaries are sorted anyway.
745
+ native_np : bool, optional
746
+ convert numpy to Python natives.
747
+ dt_to_str : bool, optional
748
+ convert dates, times, and datetimes to strings.
837
749
 
838
- Hans Buehler, Dec 2013
750
+ Returns
751
+ -------
752
+ Text : str
753
+ Filename
839
754
  """
840
755
  def rec_plain( x ):
841
756
  return plain( x, sorted_dicts=sorted_dicts, native_np=native_np, dt_to_str=dt_to_str )
842
757
  # basics
843
- if isAtomic(inn) or inn is None:
758
+ if is_atomic(inn) or inn is None:
844
759
  return inn
845
760
  if isinstance(inn,(datetime.time,datetime.date,datetime.datetime)):
846
761
  return fmt_datetime(inn) if dt_to_str else inn
@@ -852,11 +767,11 @@ def plain( inn, *, sorted_dicts : bool = False,
852
767
  elif isinstance(inn, np.floating):
853
768
  return float(inn)
854
769
  # can't handle functions --> return None
855
- if isFunction(inn) or isinstance(inn,property):
770
+ if is_function(inn) or isinstance(inn,property):
856
771
  return None
857
772
  # dictionaries
858
773
  if isinstance(inn,Mapping):
859
- r = { k: rec_plain(v) for k, v in inn.items() if not isFunction(v) and not isinstance(v,property) }
774
+ r = { k: rec_plain(v) for k, v in inn.items() if not is_function(v) and not isinstance(v,property) }
860
775
  return r if not sorted_dicts else SortedDict(r)
861
776
  # pandas
862
777
  if not pd is None and isinstance(inn,pd.DataFrame):
@@ -877,10 +792,12 @@ def plain( inn, *, sorted_dicts : bool = False,
877
792
  # Misc Jupyter
878
793
  # =============================================================================
879
794
 
880
- def is_jupyter():
795
+ def is_jupyter() -> bool:
881
796
  """
882
- Wheher we operate in a jupter session
883
- Somewhat unreliable function. Use with care
797
+ Whether we operate in a jupter session.
798
+ Somewhat unreliable function. Use with care.
799
+
800
+ :meta private:
884
801
  """
885
802
  parent_process = psutil.Process().parent().cmdline()[-1]
886
803
  return 'jupyter' in parent_process
@@ -892,8 +809,10 @@ def is_jupyter():
892
809
  class TrackTiming(object):
893
810
  """
894
811
  Simplistic class to track the time it takes to run sequential tasks.
895
- Usage:
896
812
 
813
+ Usage::
814
+
815
+ from cdxcore.util import TrackTiming
897
816
  timer = TrackTiming() # clock starts
898
817
 
899
818
  # do job 1
@@ -944,23 +863,28 @@ class TrackTiming(object):
944
863
  """ Returns dictionary of tracked texts """
945
864
  return self._tracked
946
865
 
947
- def summary(self, frmat : str = "%(text)s: %(fmt_seconds)s", jn_fmt : str = ", " ) -> str:
866
+ def summary(self, fmat : str = "%(text)s: %(fmt_seconds)s", jn_fmt : str = ", " ) -> str:
948
867
  """
949
868
  Generate summary string by applying some formatting
950
869
 
951
870
  Parameters
952
871
  ----------
953
- format : str
954
- Format string. Arguments are 'text', 'seconds' (as int) and 'fmt_seconds' (as text, see fmt_seconds())
955
- jn_fmt : str
956
- String to be used between two texts
872
+ fmat : str, optional
873
+ Format string using ``%()``. Arguments are ```text``, ``seconds`` (as int) and ``fmt_seconds`` (a string).
874
+
875
+ Default is ``"%(text)s: %(fmt_seconds)s"``.
876
+
877
+ jn_fmt : str, optional
878
+ String to be used between two texts. Default ``", " ``.
879
+
957
880
  Returns
958
881
  -------
882
+ Summary : str
959
883
  The combined summary string
960
884
  """
961
885
  s = ""
962
886
  for text, seconds in self._tracked.items():
963
- tr_txt = frmat % dict( text=text, seconds=seconds, fmt_seconds=fmt_seconds(seconds))
887
+ tr_txt = fmat % dict( text=text, seconds=seconds, fmt_seconds=fmt_seconds(seconds))
964
888
  s = tr_txt if s=="" else s+jn_fmt+tr_txt
965
889
  return s
966
890
 
@@ -970,11 +894,14 @@ class TrackTiming(object):
970
894
 
971
895
  class Timer(object):
972
896
  """
973
- Micro utility which allows keeing track of time using 'with'
974
-
975
- with Timer() as t:
976
- .... do somthing ...
977
- print(f"This took {t}.")
897
+ Micro utility to measure passage of time.
898
+
899
+ Example::
900
+
901
+ from cdxcore.util import Timer
902
+ with Timer() as t:
903
+ .... do somthing ...
904
+ print(f"This took {t}.")
978
905
  """
979
906
 
980
907
  def __init__(self):
@@ -982,6 +909,7 @@ class Timer(object):
982
909
  self.intv = None
983
910
 
984
911
  def reset(self):
912
+ """ Resets the timer. """
985
913
  self.time = time.time()
986
914
  self.intv = None
987
915
 
@@ -990,19 +918,26 @@ class Timer(object):
990
918
  return self
991
919
 
992
920
  def __str__(self):
921
+ """
922
+ Seconds elapsed since construction or :meth:`cdxcore.util.Timer.reset`,
923
+ formatted using :func:`cdxcore.util.Timer.fmt_seconds`
924
+ """
993
925
  return self.fmt_seconds
994
926
 
995
- def interval_test( self, interval : float ):
996
- """
997
- Tests if 'interval' seconds have passed.
998
- If yes, reset timer and return True. Otherwise return False
927
+ def interval_test( self, interval : float ) -> bool:
928
+ r"""
929
+ Tests if `interval` seconds have passed.
930
+ If yes, reset timer and return True. Otherwise return False.
999
931
 
1000
- Usage:
1001
- ------
932
+ Usage::
933
+
934
+ from cdxcore.util import Timer
1002
935
  tme = Timer()
1003
936
  for i in range(n):
1004
- if tme.test_dt_seconds(2.): print(f"\r{i+1}/{n} done. Time taken so far {tme}.", end='', flush=True)
937
+ if tme.test_dt_seconds(2.):
938
+ print(f"\r{i+1}/{n} done. Time taken so far {tme}.", end='', flush=True)
1005
939
  print("\rDone. This took {tme}.")
940
+
1006
941
  """
1007
942
  if interval is None:
1008
943
  self.intv = self.seconds
@@ -1017,18 +952,24 @@ class Timer(object):
1017
952
 
1018
953
  @property
1019
954
  def fmt_seconds(self):
955
+ """
956
+ Seconds elapsed since construction or :meth:`cdxcore.util.Timer.reset`, formatted using :func:`cdxcore.util.fmt_seconds`
957
+ """
1020
958
  return fmt_seconds(self.seconds)
1021
959
 
1022
960
  @property
1023
- def seconds(self):
961
+ def seconds(self) -> float:
962
+ """ Seconds elapsed since construction or :meth:`cdxcore.util.Timer.reset` """
1024
963
  return time.time() - self.time
1025
964
 
1026
965
  @property
1027
- def minutes(self):
966
+ def minutes(self) -> float:
967
+ """ Minutes passed since construction or :meth:`cdxcore.util.Timer.reset` """
1028
968
  return self.seconds / 60.
1029
969
 
1030
970
  @property
1031
- def hours(self):
971
+ def hours(self) -> float:
972
+ """ Hours passed since construction or :meth:`cdxcore.util.Timer.reset` """
1032
973
  return self.minutes / 60.
1033
974
 
1034
975
  def __exit__(self, *kargs, **wargs):