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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: datec
3
- Version: 0.2
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. In general the date
44
- representation is NxYYYY-mm-ddTHH:MM:SS.ffffff, where unspecified
45
- parts are omitted leaving the symbols intact, like "2x-2-29T3::." (see
46
- the following for the meaning). If the fractional part is not
47
- specified the "." may be omitted, if all time parts are not specified
48
- the "T::." can be omitted, if all date parts are not specified the
49
- "--T" can be omitted, and if Nx may be omitted in some cases for
50
- setting a partial datetime or weekday. There are a couple other more
51
- formats like +3week and -2wed for shifting by period and weekday.
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. In general the date
20
- representation is NxYYYY-mm-ddTHH:MM:SS.ffffff, where unspecified
21
- parts are omitted leaving the symbols intact, like "2x-2-29T3::." (see
22
- the following for the meaning). If the fractional part is not
23
- specified the "." may be omitted, if all time parts are not specified
24
- the "T::." can be omitted, if all date parts are not specified the
25
- "--T" can be omitted, and if Nx may be omitted in some cases for
26
- setting a partial datetime or weekday. There are a couple other more
27
- formats like +3week and -2wed for shifting by period and weekday.
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
- __metaclass__ = type
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
- assert period in ('year', 'month', 'week', 'day',
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
- cnt = int(gdt['count'])
150
- except Exception:
151
- cnt = float(gdt['count'])
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
- assert day in range(7)
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]+ | [0-9]*\.[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 (int): The number of periods to shift
246
- year (int): The year number
247
- month (int): The month number (1 to 12)
248
- day (int): The day number (1 to 31)
249
- hour (int): The hour number (0 to 23)
250
- minute (int): The minute number (0 to 59)
251
- second (int or float): The second number (0 to smaller than 60)
252
- microsecond (int): The microsecond number (0 to 999999)
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
- assert not count or not year, 'Absolute date with non-zero count'
266
- assert not isinstance(second, float) or \
267
- microsecond is None, 'Doubly specified microsecond'
268
- vals = [year, month, day, hour, minute, second, microsecond]
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. The ending
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
- print(dt.isoformat())
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__':
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
File without changes
File without changes
File without changes