datec 0.2__tar.gz → 0.3__tar.gz
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.
- {datec-0.2 → datec-0.3}/PKG-INFO +19 -10
- {datec-0.2 → datec-0.3}/README.md +18 -9
- {datec-0.2 → datec-0.3}/datec/__init__.py +52 -30
- {datec-0.2 → datec-0.3}/datec/__main__.py +17 -5
- datec-0.3/datec/py.typed +0 -0
- {datec-0.2 → datec-0.3}/tests/datec_test.py +36 -0
- {datec-0.2 → datec-0.3}/.gitignore +0 -0
- {datec-0.2 → datec-0.3}/LICENSE +0 -0
- {datec-0.2 → datec-0.3}/pyproject.toml +0 -0
- {datec-0.2 → datec-0.3}/requirements-dev.in +0 -0
{datec-0.2 → datec-0.3}/PKG-INFO
RENAMED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: datec
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.3
|
|
4
4
|
Summary: Date Command
|
|
5
5
|
Project-URL: Homepage, https://github.com/isaacto/datec
|
|
6
6
|
Project-URL: Repository, https://github.com/isaacto/datec.git
|
|
@@ -40,15 +40,16 @@ to them, like this:
|
|
|
40
40
|
A date command can be parsed from strings using the parse() function,
|
|
41
41
|
which create a command from a string representation. This forms the
|
|
42
42
|
basis of the datec command, which is a command-line program to output
|
|
43
|
-
datetime after applying date commands
|
|
44
|
-
|
|
45
|
-
parts are omitted
|
|
46
|
-
the
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
43
|
+
datetime after applying date commands, or sleep until that time if
|
|
44
|
+
"-w" is given. In general the date representation is
|
|
45
|
+
NxYYYY-mm-ddTHH:MM:SS.ffffff, where unspecified parts are omitted
|
|
46
|
+
leaving the symbols intact, like "2x-2-29T3::." (see the following for
|
|
47
|
+
the meaning). If the fractional part is not specified the "." may be
|
|
48
|
+
omitted, if all time parts are not specified the "T::." can be
|
|
49
|
+
omitted, if all date parts are not specified the "--T" can be omitted,
|
|
50
|
+
and if Nx may be omitted in some cases for setting a partial datetime
|
|
51
|
+
or weekday. There are a couple other more formats like +3week and
|
|
52
|
+
-2wed for shifting by period and weekday.
|
|
52
53
|
|
|
53
54
|
Date commands are in two forms: period shifting commands and partial
|
|
54
55
|
datetime shifting commands. The first type is more familiar: they
|
|
@@ -78,6 +79,11 @@ They are represented by either a Weekday object or a PartialDate
|
|
|
78
79
|
object with a count. A count of 0 means setting instead of shifting.
|
|
79
80
|
Only integer counts are acceptable.
|
|
80
81
|
|
|
82
|
+
A trailing "/" on a partial date command sets all fields after the
|
|
83
|
+
last specified field to zero. For example, `12::/` sets the hour to
|
|
84
|
+
12 and the minute, second and microsecond to 0, whereas `12::` would
|
|
85
|
+
leave those unchanged.
|
|
86
|
+
|
|
81
87
|
It is an error to set to an invalid date (e.g., --31 applied on
|
|
82
88
|
2019-06-25 is an error). The datetime parts which are specified must
|
|
83
89
|
be consecutive (it is an error to specify 12::05). It is also an
|
|
@@ -95,6 +101,9 @@ will cause the date to moved backwards until the date is valid. E.g.,
|
|
|
95
101
|
if you shift by -6- with count 1 (next June) from 2019-05-31, you get
|
|
96
102
|
2019-06-30. With count 2 you get 2020-06-30.
|
|
97
103
|
|
|
104
|
+
All these functionalities are available in the constructors too. Read
|
|
105
|
+
their docstring to find how to use them.
|
|
106
|
+
|
|
98
107
|
This library is grown out of frustration that it is tedious to have a
|
|
99
108
|
shell script or program to get a datetime like "the next 6pm from now"
|
|
100
109
|
or "the next 3rd of any month from two days ago". With this module
|
|
@@ -16,15 +16,16 @@ to them, like this:
|
|
|
16
16
|
A date command can be parsed from strings using the parse() function,
|
|
17
17
|
which create a command from a string representation. This forms the
|
|
18
18
|
basis of the datec command, which is a command-line program to output
|
|
19
|
-
datetime after applying date commands
|
|
20
|
-
|
|
21
|
-
parts are omitted
|
|
22
|
-
the
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
19
|
+
datetime after applying date commands, or sleep until that time if
|
|
20
|
+
"-w" is given. In general the date representation is
|
|
21
|
+
NxYYYY-mm-ddTHH:MM:SS.ffffff, where unspecified parts are omitted
|
|
22
|
+
leaving the symbols intact, like "2x-2-29T3::." (see the following for
|
|
23
|
+
the meaning). If the fractional part is not specified the "." may be
|
|
24
|
+
omitted, if all time parts are not specified the "T::." can be
|
|
25
|
+
omitted, if all date parts are not specified the "--T" can be omitted,
|
|
26
|
+
and if Nx may be omitted in some cases for setting a partial datetime
|
|
27
|
+
or weekday. There are a couple other more formats like +3week and
|
|
28
|
+
-2wed for shifting by period and weekday.
|
|
28
29
|
|
|
29
30
|
Date commands are in two forms: period shifting commands and partial
|
|
30
31
|
datetime shifting commands. The first type is more familiar: they
|
|
@@ -54,6 +55,11 @@ They are represented by either a Weekday object or a PartialDate
|
|
|
54
55
|
object with a count. A count of 0 means setting instead of shifting.
|
|
55
56
|
Only integer counts are acceptable.
|
|
56
57
|
|
|
58
|
+
A trailing "/" on a partial date command sets all fields after the
|
|
59
|
+
last specified field to zero. For example, `12::/` sets the hour to
|
|
60
|
+
12 and the minute, second and microsecond to 0, whereas `12::` would
|
|
61
|
+
leave those unchanged.
|
|
62
|
+
|
|
57
63
|
It is an error to set to an invalid date (e.g., --31 applied on
|
|
58
64
|
2019-06-25 is an error). The datetime parts which are specified must
|
|
59
65
|
be consecutive (it is an error to specify 12::05). It is also an
|
|
@@ -71,6 +77,9 @@ will cause the date to moved backwards until the date is valid. E.g.,
|
|
|
71
77
|
if you shift by -6- with count 1 (next June) from 2019-05-31, you get
|
|
72
78
|
2019-06-30. With count 2 you get 2020-06-30.
|
|
73
79
|
|
|
80
|
+
All these functionalities are available in the constructors too. Read
|
|
81
|
+
their docstring to find how to use them.
|
|
82
|
+
|
|
74
83
|
This library is grown out of frustration that it is tedious to have a
|
|
75
84
|
shell script or program to get a datetime like "the next 6pm from now"
|
|
76
85
|
or "the next 3rd of any month from two days ago". With this module
|
|
@@ -46,6 +46,11 @@ They are represented by either a Weekday object or a PartialDate
|
|
|
46
46
|
object with a count. A count of 0 means setting instead of shifting.
|
|
47
47
|
Only integer counts are acceptable.
|
|
48
48
|
|
|
49
|
+
A trailing "/" on a partial date command sets all fields after the
|
|
50
|
+
last specified field to zero. For example, `12::/` sets the hour to
|
|
51
|
+
12 and the minute, second and microsecond to 0, whereas `12::` would
|
|
52
|
+
leave those unchanged.
|
|
53
|
+
|
|
49
54
|
It is an error to set to an invalid date (e.g., --31 applied on
|
|
50
55
|
2019-06-25 is an error). The datetime parts which are specified must
|
|
51
56
|
be consecutive (it is an error to specify 12::05). It is also an
|
|
@@ -79,6 +84,7 @@ is observed. Contributions are welcome.
|
|
|
79
84
|
|
|
80
85
|
"""
|
|
81
86
|
|
|
87
|
+
import contextlib
|
|
82
88
|
import datetime
|
|
83
89
|
import re
|
|
84
90
|
import typing
|
|
@@ -86,9 +92,7 @@ import typing
|
|
|
86
92
|
import dateutil.relativedelta as dr
|
|
87
93
|
|
|
88
94
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
__version__ = '0.2'
|
|
95
|
+
__version__ = '0.3'
|
|
92
96
|
|
|
93
97
|
class ParseError(ValueError):
|
|
94
98
|
"""Represent an error in parsing."""
|
|
@@ -111,8 +115,11 @@ class Period:
|
|
|
111
115
|
|
|
112
116
|
"""
|
|
113
117
|
def __init__(self, count: float, period: str):
|
|
114
|
-
|
|
115
|
-
'hour', 'minute', 'second')
|
|
118
|
+
if period not in ('year', 'month', 'week', 'day',
|
|
119
|
+
'hour', 'minute', 'second'):
|
|
120
|
+
raise ValueError(f'Invalid period: {period}')
|
|
121
|
+
if period in ('year', 'month') and count != int(count):
|
|
122
|
+
raise ValueError(f'Invalid count {count} for period {period}')
|
|
116
123
|
self._count = count
|
|
117
124
|
self._period = period
|
|
118
125
|
|
|
@@ -145,11 +152,12 @@ class Period:
|
|
|
145
152
|
raise ParseError('Cannot parse string %s' % cmdstr)
|
|
146
153
|
gdt = match.groupdict()
|
|
147
154
|
cnt: float
|
|
155
|
+
with contextlib.suppress(ValueError):
|
|
156
|
+
return cls(int(gdt['count']), gdt['period'])
|
|
148
157
|
try:
|
|
149
|
-
|
|
150
|
-
except
|
|
151
|
-
|
|
152
|
-
return cls(cnt, gdt['period'])
|
|
158
|
+
return cls(float(gdt['count']), gdt['period'])
|
|
159
|
+
except ValueError as err:
|
|
160
|
+
raise ParseError('Cannot parse string %s' % cmdstr)
|
|
153
161
|
|
|
154
162
|
|
|
155
163
|
_WEEKDAY_CLS = [dr.SU, dr.MO, dr.TU, dr.WE, dr.TH, dr.FR, dr.SA]
|
|
@@ -178,7 +186,8 @@ class Weekday:
|
|
|
178
186
|
"""
|
|
179
187
|
def __init__(self, count: int, day: int):
|
|
180
188
|
self._count = count
|
|
181
|
-
|
|
189
|
+
if day not in range(7):
|
|
190
|
+
raise ValueError('Invalid weekday: %d' % day)
|
|
182
191
|
self._day = day
|
|
183
192
|
self._drcls = _WEEKDAY_CLS[day]
|
|
184
193
|
|
|
@@ -196,7 +205,7 @@ class Weekday:
|
|
|
196
205
|
|
|
197
206
|
PARSE_RE = re.compile(r'''
|
|
198
207
|
^
|
|
199
|
-
(?P<count> [+-] (?: [0-9]+
|
|
208
|
+
(?P<count> [+-] (?: [0-9]+) )?
|
|
200
209
|
(?P<weekday> sun|mon|tue|wed|thu|fri|sat)
|
|
201
210
|
$
|
|
202
211
|
''', re.X)
|
|
@@ -242,14 +251,15 @@ class PartialDate:
|
|
|
242
251
|
|
|
243
252
|
Args:
|
|
244
253
|
|
|
245
|
-
count
|
|
246
|
-
year
|
|
247
|
-
month
|
|
248
|
-
day
|
|
249
|
-
hour
|
|
250
|
-
minute
|
|
251
|
-
second
|
|
252
|
-
microsecond
|
|
254
|
+
count: The number of periods to shift
|
|
255
|
+
year: The year number
|
|
256
|
+
month: The month number (1 to 12)
|
|
257
|
+
day: The day number (1 to 31)
|
|
258
|
+
hour: The hour number (0 to 23)
|
|
259
|
+
minute: The minute number (0 to 59)
|
|
260
|
+
second: The second number (0 to smaller than 60)
|
|
261
|
+
microsecond: The microsecond number (0 to 999999)
|
|
262
|
+
zero: Whether to zero out the fields after the last specified one
|
|
253
263
|
|
|
254
264
|
"""
|
|
255
265
|
|
|
@@ -259,19 +269,26 @@ class PartialDate:
|
|
|
259
269
|
self, count: int = 0, year: typing.Optional[int] = None,
|
|
260
270
|
month: typing.Optional[int] = None, day: typing.Optional[int] = None,
|
|
261
271
|
hour: typing.Optional[int] = None, minute: typing.Optional[int] = None,
|
|
262
|
-
second: typing.Optional[int] = None,
|
|
263
|
-
microsecond: typing.Optional[int] = None
|
|
272
|
+
second: typing.Optional[typing.Union[int, float]] = None,
|
|
273
|
+
microsecond: typing.Optional[int] = None,
|
|
274
|
+
zero: bool = False
|
|
264
275
|
):
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
sig = ''.join([("0" if v is None else "1") for v in vals])
|
|
270
|
-
assert not self._INVALID_SIG_RE.search(sig), \
|
|
271
|
-
'Non-consecutive components'
|
|
276
|
+
if count and year:
|
|
277
|
+
raise ValueError('Absolute date with non-zero count')
|
|
278
|
+
if isinstance(second, float) and microsecond is not None:
|
|
279
|
+
raise ValueError('Doubly specified microsecond')
|
|
272
280
|
if isinstance(second, float):
|
|
273
281
|
second, orig_second = int(second), second
|
|
274
282
|
microsecond = int((orig_second - second) * 1000000 + 0.5)
|
|
283
|
+
vals = [year, month, day, hour, minute, second, microsecond]
|
|
284
|
+
sig = ''.join([("0" if v is None else "1") for v in vals])
|
|
285
|
+
if self._INVALID_SIG_RE.search(sig):
|
|
286
|
+
raise ValueError('Non-consecutive components')
|
|
287
|
+
if zero:
|
|
288
|
+
lastset = sig.rfind('1')
|
|
289
|
+
for i in range(lastset + 1, 7):
|
|
290
|
+
vals[i] = 0
|
|
291
|
+
year, month, day, hour, minute, second, microsecond = vals
|
|
275
292
|
self._count = count
|
|
276
293
|
self._year = year
|
|
277
294
|
self._month = month
|
|
@@ -412,13 +429,17 @@ class PartialDate:
|
|
|
412
429
|
empty string. If all date parts are not specified the "--T"
|
|
413
430
|
may be omitted. If all the time parts are not specified the
|
|
414
431
|
"T::." may be omitted. If the microsecond part is not
|
|
415
|
-
specified the "." part may be omitted.
|
|
432
|
+
specified the "." part may be omitted. A trailing "/" causes
|
|
433
|
+
all fields after the last specified field to be set to 0.
|
|
416
434
|
|
|
417
435
|
Args:
|
|
418
436
|
|
|
419
437
|
cmdstr (str): The command string
|
|
420
438
|
|
|
421
439
|
"""
|
|
440
|
+
zero = cmdstr.endswith('/')
|
|
441
|
+
if zero:
|
|
442
|
+
cmdstr = cmdstr[:-1]
|
|
422
443
|
match = cls.PARSE_RE1.match(cmdstr.lower())
|
|
423
444
|
if not match:
|
|
424
445
|
match = cls.PARSE_RE2.match(cmdstr)
|
|
@@ -443,7 +464,8 @@ class PartialDate:
|
|
|
443
464
|
_matchval('hour'),
|
|
444
465
|
_matchval('minute'),
|
|
445
466
|
_matchval('second'),
|
|
446
|
-
microsecond
|
|
467
|
+
microsecond,
|
|
468
|
+
zero)
|
|
447
469
|
|
|
448
470
|
|
|
449
471
|
def parse(cmdstr: str) -> typing.Union[Period, Weekday, PartialDate]:
|
|
@@ -1,21 +1,25 @@
|
|
|
1
1
|
from __future__ import print_function
|
|
2
2
|
|
|
3
|
-
import sys
|
|
4
3
|
import datetime
|
|
4
|
+
import sys
|
|
5
|
+
import time
|
|
5
6
|
|
|
6
7
|
import datec
|
|
7
8
|
|
|
8
9
|
|
|
9
10
|
def main() -> None:
|
|
10
11
|
dt = datetime.datetime.now()
|
|
12
|
+
wait = False
|
|
11
13
|
for cmd in sys.argv[1:]:
|
|
12
14
|
if cmd == '-h':
|
|
13
15
|
print('''Date command
|
|
14
16
|
|
|
15
|
-
Usage: datec [<command>] ...
|
|
17
|
+
Usage: datec [-w] [<command>] ...
|
|
16
18
|
|
|
17
|
-
Datec starts from the current time and apply commands.
|
|
18
|
-
datetime is printed in ISO YYYY-MM-DDTHH:MM:SS.ffffff format.
|
|
19
|
+
Datec starts from the current time and apply commands. Normally the
|
|
20
|
+
ending datetime is printed in ISO YYYY-MM-DDTHH:MM:SS.ffffff format.
|
|
21
|
+
But if -w is given, no output is printed; instead the program sleeps
|
|
22
|
+
until the ending datetime.
|
|
19
23
|
|
|
20
24
|
<command> may be of the following forms:
|
|
21
25
|
|
|
@@ -28,8 +32,16 @@ datetime is printed in ISO YYYY-MM-DDTHH:MM:SS.ffffff format.
|
|
|
28
32
|
|
|
29
33
|
''', file=sys.stderr)
|
|
30
34
|
sys.exit(0)
|
|
35
|
+
if cmd == '-w':
|
|
36
|
+
wait = True
|
|
37
|
+
continue
|
|
31
38
|
dt = dt + datec.parse(cmd.lower())
|
|
32
|
-
|
|
39
|
+
if wait:
|
|
40
|
+
delta = (dt - datetime.datetime.now()).total_seconds()
|
|
41
|
+
if delta > 0:
|
|
42
|
+
time.sleep(delta)
|
|
43
|
+
else:
|
|
44
|
+
print(dt.isoformat())
|
|
33
45
|
|
|
34
46
|
|
|
35
47
|
if __name__ == '__main__':
|
datec-0.3/datec/py.typed
ADDED
|
File without changes
|
|
@@ -13,6 +13,8 @@ class DatecTest(unittest.TestCase):
|
|
|
13
13
|
datetime.datetime(2019, 5, 15, 0, 0, 1, 500000))
|
|
14
14
|
with self.assertRaises(datec.ParseError):
|
|
15
15
|
datec.Period.parse('+1mon')
|
|
16
|
+
with self.assertRaises(datec.ParseError):
|
|
17
|
+
datec.Period.parse('+.day')
|
|
16
18
|
|
|
17
19
|
def test_weekday(self):
|
|
18
20
|
dt = datetime.datetime(2019, 5, 15)
|
|
@@ -41,6 +43,8 @@ class DatecTest(unittest.TestCase):
|
|
|
41
43
|
datetime.datetime(2019, 5, 15, 8, 17, 16))
|
|
42
44
|
self.assertEqual(dt + datec.PartialDate(2, minute=7, second=16),
|
|
43
45
|
datetime.datetime(2019, 5, 15, 10, 7, 16))
|
|
46
|
+
self.assertEqual(dt + datec.PartialDate(2, minute=7, second=16.5),
|
|
47
|
+
datetime.datetime(2019, 5, 15, 10, 7, 16, 500000))
|
|
44
48
|
self.assertEqual(dt + datec.PartialDate.parse(':7:16'),
|
|
45
49
|
datetime.datetime(2019, 5, 15, 8, 7, 16))
|
|
46
50
|
self.assertEqual(dt + datec.PartialDate.parse('-1x:7:16'),
|
|
@@ -80,3 +84,35 @@ class DatecTest(unittest.TestCase):
|
|
|
80
84
|
datetime.datetime(2019, 3, 30))
|
|
81
85
|
with self.assertRaises(ValueError):
|
|
82
86
|
dt + datec.PartialDate(1, month=2, day=30)
|
|
87
|
+
|
|
88
|
+
def test_partialdate_zero(self):
|
|
89
|
+
dt = datetime.datetime(2019, 5, 15, 8, 15)
|
|
90
|
+
self.assertEqual(dt + datec.PartialDate(0, hour=12, zero=True),
|
|
91
|
+
datetime.datetime(2019, 5, 15, 12, 0, 0))
|
|
92
|
+
self.assertEqual(dt + datec.PartialDate(0, minute=30, zero=True),
|
|
93
|
+
datetime.datetime(2019, 5, 15, 8, 30, 0))
|
|
94
|
+
self.assertEqual(dt + datec.parse('12::/'),
|
|
95
|
+
datetime.datetime(2019, 5, 15, 12, 0, 0))
|
|
96
|
+
self.assertEqual(dt + datec.parse('12:30:/'),
|
|
97
|
+
datetime.datetime(2019, 5, 15, 12, 30, 0))
|
|
98
|
+
self.assertEqual(dt + datec.parse('2020-03-15/'),
|
|
99
|
+
datetime.datetime(2020, 3, 15, 0, 0, 0))
|
|
100
|
+
# without zero, minute/second are unchanged
|
|
101
|
+
self.assertEqual(dt + datec.PartialDate(0, hour=12),
|
|
102
|
+
datetime.datetime(2019, 5, 15, 12, 15))
|
|
103
|
+
self.assertEqual(dt + datec.parse('12::'),
|
|
104
|
+
datetime.datetime(2019, 5, 15, 12, 15))
|
|
105
|
+
|
|
106
|
+
def test_validation(self):
|
|
107
|
+
with self.assertRaises(ValueError):
|
|
108
|
+
datec.Period(1, 'invalid')
|
|
109
|
+
with self.assertRaises(ValueError):
|
|
110
|
+
datec.Period(1.5, 'month')
|
|
111
|
+
with self.assertRaises(ValueError):
|
|
112
|
+
datec.Weekday(1, 7)
|
|
113
|
+
with self.assertRaises(ValueError):
|
|
114
|
+
datec.PartialDate(1, year=2020)
|
|
115
|
+
with self.assertRaises(ValueError):
|
|
116
|
+
datec.PartialDate(second=1.5, microsecond=500)
|
|
117
|
+
with self.assertRaises(ValueError):
|
|
118
|
+
datec.PartialDate(hour=12, second=30)
|
|
File without changes
|
{datec-0.2 → datec-0.3}/LICENSE
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|