encommon 0.13.1__py3-none-any.whl → 0.15.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.
Files changed (63) hide show
  1. encommon/__init__.py +2 -7
  2. encommon/colors/__init__.py +14 -0
  3. encommon/colors/color.py +518 -0
  4. encommon/colors/test/__init__.py +6 -0
  5. encommon/colors/test/test_color.py +189 -0
  6. encommon/config/config.py +83 -30
  7. encommon/config/files.py +16 -13
  8. encommon/config/logger.py +10 -5
  9. encommon/config/params.py +47 -31
  10. encommon/config/paths.py +15 -12
  11. encommon/config/test/__init__.py +1 -1
  12. encommon/config/test/test_config.py +15 -17
  13. encommon/config/test/test_files.py +8 -7
  14. encommon/config/test/test_logger.py +21 -15
  15. encommon/config/test/test_paths.py +12 -11
  16. encommon/config/utils.py +9 -3
  17. encommon/conftest.py +33 -22
  18. encommon/crypts/params.py +46 -11
  19. encommon/crypts/test/test_crypts.py +5 -5
  20. encommon/crypts/test/test_hashes.py +2 -1
  21. encommon/times/__init__.py +5 -3
  22. encommon/times/common.py +4 -3
  23. encommon/times/params.py +103 -45
  24. encommon/times/parse.py +39 -12
  25. encommon/times/test/test_duration.py +3 -2
  26. encommon/times/test/test_parse.py +16 -9
  27. encommon/times/test/test_time.py +123 -0
  28. encommon/times/test/test_timer.py +5 -4
  29. encommon/times/test/test_timers.py +10 -9
  30. encommon/times/test/test_unitime.py +23 -0
  31. encommon/times/test/test_window.py +4 -3
  32. encommon/times/test/test_windows.py +7 -6
  33. encommon/times/{times.py → time.py} +129 -22
  34. encommon/times/timer.py +10 -10
  35. encommon/times/timers.py +3 -3
  36. encommon/times/unitime.py +57 -0
  37. encommon/times/window.py +31 -31
  38. encommon/times/windows.py +10 -10
  39. encommon/types/__init__.py +20 -2
  40. encommon/types/classes.py +84 -0
  41. encommon/types/lists.py +33 -0
  42. encommon/types/notate.py +2 -1
  43. encommon/types/strings.py +34 -4
  44. encommon/types/test/test_classes.py +74 -0
  45. encommon/types/test/test_empty.py +2 -1
  46. encommon/types/test/test_lists.py +23 -0
  47. encommon/types/test/test_strings.py +15 -3
  48. encommon/types/types.py +20 -0
  49. encommon/utils/__init__.py +4 -0
  50. encommon/utils/paths.py +5 -6
  51. encommon/utils/sample.py +118 -41
  52. encommon/utils/stdout.py +53 -7
  53. encommon/utils/test/test_paths.py +3 -3
  54. encommon/utils/test/test_sample.py +128 -29
  55. encommon/utils/test/test_stdout.py +92 -28
  56. encommon/version.txt +1 -1
  57. {encommon-0.13.1.dist-info → encommon-0.15.0.dist-info}/METADATA +1 -1
  58. encommon-0.15.0.dist-info/RECORD +84 -0
  59. {encommon-0.13.1.dist-info → encommon-0.15.0.dist-info}/WHEEL +1 -1
  60. encommon/times/test/test_times.py +0 -89
  61. encommon-0.13.1.dist-info/RECORD +0 -73
  62. {encommon-0.13.1.dist-info → encommon-0.15.0.dist-info}/LICENSE +0 -0
  63. {encommon-0.13.1.dist-info → encommon-0.15.0.dist-info}/top_level.txt +0 -0
encommon/times/params.py CHANGED
@@ -7,47 +7,61 @@ is permitted, for more information consult the project license file.
7
7
 
8
8
 
9
9
 
10
- from typing import Any
10
+ from typing import Annotated
11
11
  from typing import Optional
12
12
 
13
- from pydantic import BaseModel
13
+ from pydantic import Field
14
14
 
15
15
  from .common import PARSABLE
16
16
  from .common import SCHEDULE
17
- from .times import Times
17
+ from .time import Time
18
+ from ..types import BaseModel
19
+ from ..types import DictStrAny
20
+
21
+
22
+
23
+ _TIMERS = dict[str, 'TimerParams']
24
+ _WINDOWS = dict[str, 'WindowParams']
18
25
 
19
26
 
20
27
 
21
28
  class TimerParams(BaseModel, extra='forbid'):
22
29
  """
23
30
  Process and validate the core configuration parameters.
24
-
25
- :param timer: Seconds that are used for related timer.
26
- :param start: Optional time for when the timer started.
27
31
  """
28
32
 
29
- timer: float
30
- start: Optional[str] = None
33
+ timer: Annotated[
34
+ float,
35
+ Field(...,
36
+ description='Seconds used for the interval')]
37
+
38
+ start: Annotated[
39
+ str,
40
+ Field(...,
41
+ description='Optional value of timer start',
42
+ min_length=1)]
31
43
 
32
44
 
33
45
  def __init__(
34
46
  self,
35
47
  timer: int | float,
36
- start: Optional[PARSABLE] = None,
48
+ start: PARSABLE = 'now',
37
49
  ) -> None:
38
50
  """
39
51
  Initialize instance for class using provided parameters.
40
52
  """
41
53
 
54
+ data: DictStrAny = {}
55
+
56
+
42
57
  if timer is not None:
43
58
  timer = float(timer)
44
59
 
45
60
  if start is not None:
46
- start = Times(start)
61
+ start = Time(start)
47
62
 
48
63
 
49
- data: dict[str, Any] = {
50
- 'timer': timer}
64
+ data['timer'] = timer
51
65
 
52
66
  if start is not None:
53
67
  data['start'] = start.subsec
@@ -60,67 +74,98 @@ class TimerParams(BaseModel, extra='forbid'):
60
74
  class TimersParams(BaseModel, extra='forbid'):
61
75
  """
62
76
  Process and validate the core configuration parameters.
63
-
64
- :param timers: Seconds that are used for related timer.
65
- :param data: Keyword arguments passed to Pydantic model.
66
- Parameter is picked up by autodoc, please ignore.
67
77
  """
68
78
 
69
- timers: dict[str, TimerParams] = {}
79
+ timers: Annotated[
80
+ _TIMERS,
81
+ Field(...,
82
+ description='Seconds used for the interval',
83
+ min_length=0)]
84
+
85
+
86
+ def __init__(
87
+ self,
88
+ timers: Optional[_TIMERS] = None,
89
+ ) -> None:
90
+ """
91
+ Initialize instance for class using provided parameters.
92
+ """
93
+
94
+ timers = timers or {}
95
+
96
+ super().__init__(**{
97
+ 'timers': timers})
70
98
 
71
99
 
72
100
 
73
101
  class WindowParams(BaseModel, extra='forbid'):
74
102
  """
75
103
  Process and validate the core configuration parameters.
76
-
77
- :param window: Parameters for defining scheduled time.
78
- :param start: Determine the start for scheduling window.
79
- :param stop: Determine the ending for scheduling window.
80
- :param anchor: Optionally define time anchor for window.
81
- :param delay: Period of time schedulng will be delayed.
82
104
  """
83
105
 
84
- window: SCHEDULE
85
-
86
- start: Optional[str] = None
87
- stop: Optional[str] = None
88
- anchor: Optional[str] = None
89
- delay: float = 0.0
106
+ window: Annotated[
107
+ SCHEDULE,
108
+ Field(...,
109
+ description='Period for scheduling window',
110
+ min_length=1)]
111
+
112
+ start: Annotated[
113
+ Optional[str],
114
+ Field(None,
115
+ description='Determine when scope begins',
116
+ min_length=1)]
117
+
118
+ stop: Annotated[
119
+ Optional[str],
120
+ Field(None,
121
+ description='Determine when scope ends',
122
+ min_length=1)]
123
+
124
+ anchor: Annotated[
125
+ Optional[str],
126
+ Field(None,
127
+ description='Optional anchor of the window',
128
+ min_length=1)]
129
+
130
+ delay: Annotated[
131
+ float,
132
+ Field(...,
133
+ description='Time period of schedule delay',
134
+ ge=0)]
90
135
 
91
136
 
92
137
  def __init__(
93
138
  self,
94
139
  window: SCHEDULE | int,
140
+ *,
95
141
  start: Optional[PARSABLE] = None,
96
142
  stop: Optional[PARSABLE] = None,
97
143
  anchor: Optional[PARSABLE] = None,
98
- delay: Optional[int | float] = None,
144
+ delay: int | float = 0.0,
99
145
  ) -> None:
100
146
  """
101
147
  Initialize instance for class using provided parameters.
102
148
  """
103
149
 
150
+ data: DictStrAny = {}
151
+
104
152
 
105
153
  if isinstance(window, int):
106
154
  window = {'seconds': window}
107
155
 
108
-
109
156
  if start is not None:
110
- start = Times(start)
157
+ start = Time(start)
111
158
 
112
159
  if stop is not None:
113
- stop = Times(stop)
160
+ stop = Time(stop)
114
161
 
115
162
  if anchor is not None:
116
- anchor = Times(anchor)
163
+ anchor = Time(anchor)
117
164
 
118
- if delay is not None:
119
- delay = float(delay)
165
+ delay = float(delay)
120
166
 
121
167
 
122
- data: dict[str, Any] = {
123
- 'window': window}
168
+ data['window'] = window
124
169
 
125
170
  if start is not None:
126
171
  data['start'] = start.subsec
@@ -131,8 +176,7 @@ class WindowParams(BaseModel, extra='forbid'):
131
176
  if anchor is not None:
132
177
  data['anchor'] = anchor.subsec
133
178
 
134
- if delay is not None:
135
- data['delay'] = delay
179
+ data['delay'] = delay
136
180
 
137
181
 
138
182
  super().__init__(**data)
@@ -142,10 +186,24 @@ class WindowParams(BaseModel, extra='forbid'):
142
186
  class WindowsParams(BaseModel, extra='forbid'):
143
187
  """
144
188
  Process and validate the core configuration parameters.
145
-
146
- :param windows: Parameters for defining scheduled time.
147
- :param data: Keyword arguments passed to Pydantic model.
148
- Parameter is picked up by autodoc, please ignore.
149
189
  """
150
190
 
151
- windows: dict[str, WindowParams] = {}
191
+ windows: Annotated[
192
+ _WINDOWS,
193
+ Field(...,
194
+ description='Period for scheduling windows',
195
+ min_length=0)]
196
+
197
+
198
+ def __init__(
199
+ self,
200
+ windows: Optional[_WINDOWS] = None,
201
+ ) -> None:
202
+ """
203
+ Initialize instance for class using provided parameters.
204
+ """
205
+
206
+ windows = windows or {}
207
+
208
+ super().__init__(**{
209
+ 'windows': windows})
encommon/times/parse.py CHANGED
@@ -9,7 +9,6 @@ is permitted, for more information consult the project license file.
9
9
 
10
10
  from contextlib import suppress
11
11
  from datetime import datetime
12
- from datetime import timezone
13
12
  from re import match as re_match
14
13
  from typing import Optional
15
14
  from typing import TYPE_CHECKING
@@ -27,11 +26,11 @@ from .utils import strptime
27
26
  from .utils import utcdatetime
28
27
 
29
28
  if TYPE_CHECKING:
30
- from .times import Times # noqa: F401
29
+ from .time import Time # noqa: F401
31
30
 
32
31
 
33
32
 
34
- def parse_time( # noqa: CFQ004
33
+ def parse_time(
35
34
  source: Optional[PARSABLE] = None,
36
35
  *,
37
36
  anchor: Optional[PARSABLE] = None,
@@ -52,7 +51,7 @@ def parse_time( # noqa: CFQ004
52
51
  +----------------------+----------------------------+
53
52
  | 'max', float('inf') | Uses the maximum time |
54
53
  +----------------------+----------------------------+
55
- | object | Provide datetime or Times |
54
+ | object | Provide datetime or Time |
56
55
  +----------------------+----------------------------+
57
56
  | 2000-01-01T00:00:00Z | Uses strptime and dateutil |
58
57
  +----------------------+----------------------------+
@@ -79,14 +78,20 @@ def parse_time( # noqa: CFQ004
79
78
  :returns: Python datetime object containing related time.
80
79
  """
81
80
 
81
+
82
82
  if (source is not None
83
83
  and hasattr(source, 'source')):
84
+
84
85
  source = source.source
86
+ assert isinstance(
87
+ source, datetime)
88
+
85
89
 
86
90
  if (isinstance(source, str)
87
91
  and re_match(NUMERISH, source)):
88
92
  source = float(source)
89
93
 
94
+
90
95
  tzinfo = findtz(tzname)
91
96
 
92
97
 
@@ -94,8 +99,11 @@ def parse_time( # noqa: CFQ004
94
99
  source = datetime.fromtimestamp(
95
100
  float(source), tz=tzinfo)
96
101
 
97
- if str(source) in STRINGNOW:
98
- return utcdatetime()
102
+
103
+ if source in STRINGNOW:
104
+ source = (
105
+ utcdatetime()
106
+ .astimezone(tzinfo))
99
107
 
100
108
  if source in ['max', float('inf')]:
101
109
  source = datetime.max
@@ -116,11 +124,15 @@ def parse_time( # noqa: CFQ004
116
124
  formats=format,
117
125
  tzname=tzname)
118
126
 
119
- if isinstance(source, datetime):
120
- return (
121
- source.replace(tzinfo=tzinfo)
122
- .astimezone(timezone.utc))
123
127
 
128
+ if (isinstance(source, datetime)
129
+ and not source.tzinfo):
130
+ source = source.replace(
131
+ tzinfo=findtz(tzname))
132
+
133
+
134
+ if isinstance(source, datetime):
135
+ return source
124
136
 
125
137
  raise ValueError('source') # NOCVR
126
138
 
@@ -155,7 +167,12 @@ def shift_time(
155
167
  anchor = parse_time(
156
168
  anchor, tzname=tzname)
157
169
 
158
- return snap(anchor, notate)
170
+ parsed = snap(anchor, notate)
171
+
172
+ assert parsed.tzinfo
173
+
174
+ return parse_time(
175
+ parsed, tzname=tzname)
159
176
 
160
177
 
161
178
 
@@ -186,11 +203,21 @@ def string_time(
186
203
  """
187
204
 
188
205
  if formats is not None:
206
+
189
207
  with suppress(ValueError):
190
- return strptime(source, formats)
208
+
209
+ return strptime(
210
+ source, formats)
191
211
 
192
212
  parsed = parser.parse(source)
193
213
 
214
+ if parsed.tzinfo is None:
215
+
216
+ tzinfo = findtz(tzname)
217
+
218
+ parsed = parsed.replace(
219
+ tzinfo=tzinfo)
220
+
194
221
  return parse_time(
195
222
  parsed, tzname=tzname)
196
223
 
@@ -9,6 +9,7 @@ is permitted, for more information consult the project license file.
9
9
 
10
10
  from ..duration import Duration
11
11
  from ...types import inrepr
12
+ from ...types import lattrs
12
13
  from ...types.strings import COMMAS
13
14
 
14
15
 
@@ -21,7 +22,7 @@ def test_Duration() -> None:
21
22
  durate = Duration(95401)
22
23
 
23
24
 
24
- attrs = list(durate.__dict__)
25
+ attrs = lattrs(durate)
25
26
 
26
27
  assert attrs == [
27
28
  '_Duration__source',
@@ -58,7 +59,7 @@ def test_Duration() -> None:
58
59
 
59
60
  assert durate.source == 95401
60
61
 
61
- assert durate.smart is True
62
+ assert durate.smart
62
63
 
63
64
  assert durate.groups == 7
64
65
 
@@ -8,6 +8,7 @@ is permitted, for more information consult the project license file.
8
8
 
9
9
 
10
10
  from datetime import timedelta
11
+ from datetime import timezone
11
12
 
12
13
  from pytest import mark
13
14
 
@@ -41,10 +42,10 @@ def test_parse_time() -> None:
41
42
  '12/31/1969 6:00pm',
42
43
  tzname='US/Central')
43
44
 
44
- assert parsed.year == 1970
45
- assert parsed.month == 1
46
- assert parsed.day == 1
47
- assert parsed.hour == 0
45
+ assert parsed.year == 1969
46
+ assert parsed.month == 12
47
+ assert parsed.day == 31
48
+ assert parsed.hour == 18
48
49
 
49
50
 
50
51
  parsed = parse_time(0)
@@ -139,7 +140,10 @@ def test_string_time() -> None:
139
140
  Perform various tests associated with relevant routines.
140
141
  """
141
142
 
142
- expect = utcdatetime(1980, 1, 1)
143
+ utc = timezone.utc
144
+ expect = (
145
+ utcdatetime(1980, 1, 1)
146
+ .astimezone(None))
143
147
 
144
148
 
145
149
  strings = [
@@ -151,7 +155,9 @@ def test_string_time() -> None:
151
155
 
152
156
  for string in strings:
153
157
 
154
- parsed = string_time(string)
158
+ parsed = (
159
+ string_time(string)
160
+ .astimezone(utc))
155
161
 
156
162
  assert parsed == expect
157
163
 
@@ -170,9 +176,10 @@ def test_string_time() -> None:
170
176
  assert parsed == expect
171
177
 
172
178
 
173
- parsed = string_time(
174
- '1979-12-31 18:00:00',
175
- tzname='US/Central')
179
+ parsed = (
180
+ string_time(
181
+ '1979-12-31 18:00:00',
182
+ tzname='US/Central'))
176
183
 
177
184
  assert parsed == expect
178
185
 
@@ -0,0 +1,123 @@
1
+ """
2
+ Functions and routines associated with Enasis Network Common Library.
3
+
4
+ This file is part of Enasis Network software eco-system. Distribution
5
+ is permitted, for more information consult the project license file.
6
+ """
7
+
8
+
9
+
10
+ from ..common import STAMP_SIMPLE
11
+ from ..common import UNIXEPOCH
12
+ from ..common import UNIXHPOCH
13
+ from ..common import UNIXMPOCH
14
+ from ..time import Time
15
+ from ...types import inrepr
16
+ from ...types import instr
17
+ from ...types import lattrs
18
+
19
+
20
+
21
+ def test_Time() -> None:
22
+ """
23
+ Perform various tests associated with relevant routines.
24
+ """
25
+
26
+ time = Time(
27
+ UNIXEPOCH,
28
+ format=STAMP_SIMPLE)
29
+
30
+
31
+ attrs = lattrs(time)
32
+
33
+ assert attrs == [
34
+ '_Time__source']
35
+
36
+
37
+ assert inrepr(
38
+ "Time('1970-01-01T00:00",
39
+ time)
40
+
41
+ assert hash(time) > 0
42
+
43
+ assert instr(
44
+ '1970-01-01T00:00:00.000',
45
+ time)
46
+
47
+
48
+ assert int(time) == 0
49
+ assert float(time) == 0.0
50
+
51
+ assert time + 1 == Time(1)
52
+ assert time - 1 == Time(-1)
53
+
54
+ assert time == Time(0)
55
+ assert time != Time(-1)
56
+ assert time != 'invalid'
57
+
58
+ assert time > Time(-1)
59
+ assert time >= Time(0)
60
+ assert time < Time(1)
61
+ assert time <= Time(0)
62
+
63
+
64
+ assert time.source.year == 1970
65
+
66
+ assert time.epoch == 0.0
67
+
68
+ assert time.spoch == 0
69
+
70
+ assert time.mpoch == 0.0
71
+
72
+ assert str(time.time) == '00:00:00'
73
+
74
+ assert time.simple == UNIXEPOCH
75
+
76
+ assert time.subsec == UNIXMPOCH
77
+
78
+ assert time.human == UNIXHPOCH
79
+
80
+ assert time.elapsed >= 1672531200
81
+
82
+ assert time.since >= 1672531200
83
+
84
+ assert time.before == (
85
+ '1969-12-31T23:59:59.999999Z')
86
+
87
+ assert time.after == (
88
+ '1970-01-01T00:00:00.000001Z')
89
+
90
+ assert time.stamp() == UNIXMPOCH
91
+
92
+ time = time.shift('+1y')
93
+ assert time == '1971-01-01'
94
+
95
+ time = time.shifz('UTC-1')
96
+ assert time == (
97
+ '12/31/1970 23:00 -0100')
98
+
99
+
100
+
101
+ def test_Time_tzname() -> None:
102
+ """
103
+ Perform various tests associated with relevant routines.
104
+ """
105
+
106
+ time1 = Time(
107
+ '1970-01-01T00:00:00Z')
108
+
109
+ time2 = time1.shifz(
110
+ 'US/Central')
111
+
112
+
113
+ delta = time1 - time2
114
+
115
+ assert -1 < delta < 1
116
+
117
+
118
+ stamp1 = time1.stamp(
119
+ tzname='US/Central')
120
+
121
+ stamp2 = time2.stamp()
122
+
123
+ assert stamp1 == stamp2
@@ -11,10 +11,11 @@ from time import sleep
11
11
 
12
12
  from pytest import fixture
13
13
 
14
+ from ..time import Time
14
15
  from ..timer import Timer
15
- from ..times import Times
16
16
  from ...types import inrepr
17
17
  from ...types import instr
18
+ from ...types import lattrs
18
19
 
19
20
 
20
21
 
@@ -40,11 +41,11 @@ def test_Timer(
40
41
  """
41
42
 
42
43
 
43
- attrs = list(timer.__dict__)
44
+ attrs = lattrs(timer)
44
45
 
45
46
  assert attrs == [
46
47
  '_Timer__timer',
47
- '_Timer__times']
48
+ '_Timer__time']
48
49
 
49
50
 
50
51
  assert inrepr(
@@ -60,7 +61,7 @@ def test_Timer(
60
61
 
61
62
  assert timer.timer == 1
62
63
 
63
- assert timer.times >= Times('-1s')
64
+ assert timer.time >= Time('-1s')
64
65
 
65
66
  assert timer.since <= 1
66
67
 
@@ -9,18 +9,19 @@ is permitted, for more information consult the project license file.
9
9
 
10
10
  from pathlib import Path
11
11
  from time import sleep
12
- from typing import Any
13
12
 
14
13
  from pytest import fixture
15
14
  from pytest import raises
16
15
 
17
16
  from ..params import TimerParams
18
17
  from ..params import TimersParams
18
+ from ..time import Time
19
19
  from ..timers import Timers
20
20
  from ..timers import TimersTable
21
- from ..times import Times
21
+ from ...types import DictStrAny
22
22
  from ...types import inrepr
23
23
  from ...types import instr
24
+ from ...types import lattrs
24
25
 
25
26
 
26
27
 
@@ -36,7 +37,7 @@ def timers(
36
37
  """
37
38
 
38
39
 
39
- source: dict[str, Any] = {
40
+ source: DictStrAny = {
40
41
  'one': {'timer': 1},
41
42
  'two': {'timer': 1}}
42
43
 
@@ -93,7 +94,7 @@ def test_Timers(
93
94
  """
94
95
 
95
96
 
96
- attrs = list(timers.__dict__)
97
+ attrs = lattrs(timers)
97
98
 
98
99
  assert attrs == [
99
100
  '_Timers__params',
@@ -115,27 +116,27 @@ def test_Timers(
115
116
  timers)
116
117
 
117
118
 
118
- assert timers.params is not None
119
+ assert timers.params
119
120
 
120
121
  assert timers.store[:6] == 'sqlite'
121
122
 
122
123
  assert timers.group == 'default'
123
124
 
124
- assert timers.store_engine is not None
125
+ assert timers.store_engine
125
126
 
126
- assert timers.store_session is not None
127
+ assert timers.store_session
127
128
 
128
129
  assert len(timers.children) == 2
129
130
 
130
131
 
131
132
  timer = timers.children['one']
132
133
 
133
- assert timer.times >= Times('-1s')
134
+ assert timer.time >= Time('-1s')
134
135
 
135
136
 
136
137
  timer = timers.children['two']
137
138
 
138
- assert timer.times == '1970-01-01'
139
+ assert timer.time == '1970-01-01'
139
140
 
140
141
 
141
142
 
@@ -0,0 +1,23 @@
1
+ """
2
+ Functions and routines associated with Enasis Network Common Library.
3
+
4
+ This file is part of Enasis Network software eco-system. Distribution
5
+ is permitted, for more information consult the project license file.
6
+ """
7
+
8
+
9
+
10
+ from ..unitime import unitime
11
+
12
+
13
+
14
+ def test_unitime() -> None:
15
+ """
16
+ Perform various tests associated with relevant routines.
17
+ """
18
+
19
+ assert unitime('1s') == 1
20
+ assert unitime('1h') == 3600
21
+ assert unitime('1') == 1
22
+ assert unitime(1) == 1
23
+ assert unitime('1.0') == 1