datec 0.2__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.
datec/__init__.py ADDED
@@ -0,0 +1,465 @@
1
+ """Date command: A command-based date computation engine
2
+
3
+ datec allows you to use "date commands" to modify datetime's by adding
4
+ to them, like this:
5
+
6
+ datetime.datetime.now() + datec.Period(2, 'week')
7
+
8
+ A date command can be parsed from strings using the parse() function,
9
+ which create a command from a string representation. This forms the
10
+ basis of the datec command, which is a command-line program to output
11
+ datetime after applying date commands. In general the date
12
+ representation is NxYYYY-mm-ddTHH:MM:SS.ffffff, where unspecified
13
+ parts are omitted leaving the symbols intact, like "2x-2-29T3::." (see
14
+ the following for the meaning). If the fractional part is not
15
+ specified the "." may be omitted, if all time parts are not specified
16
+ the "T::." can be omitted, if all date parts are not specified the
17
+ "--T" can be omitted, and if Nx may be omitted in some cases for
18
+ setting a partial datetime or weekday. There are a couple other more
19
+ formats like +3week and -2wed for shifting by period and weekday.
20
+
21
+ Date commands are in two forms: period shifting commands and partial
22
+ datetime shifting commands. The first type is more familiar: they
23
+ look like
24
+
25
+ * +2week (shift the datetime forward by 2 week)
26
+ * -1month (shift the datetime backward by 1 month)
27
+
28
+ Period is one of year, month, week, day, hour, minute and second,
29
+ represented by an object of the Period class. Fractional numbers are
30
+ acceptable except for year and month. If shifting a period leads to
31
+ an invalid date (e.g., shift backward 1 month from 2019-07-31), it
32
+ moves backwards the closest valid date (here, 2019-06-30). In general
33
+ the parts finer than the shifted part is unaffected (e.g., shifting 1
34
+ month from 2019-07-31 02:00 gives you 2019-06-30 02:00).
35
+
36
+ Partial datetime shifting is less familiar. It looks like:
37
+
38
+ * 12:: (set the hour number to 12)
39
+ * +2x12:: (move forward to the second hour 12)
40
+ * +4x--31 (move forward to the fourth occurrence of day 31 of a month)
41
+ * -3x-02-29 (move backward to the third occurrence of February 29)
42
+ * wed (set to the Wednesday of the same week, week starts on Sunday)
43
+ * -3wed (move to the third Wednesday before the current datetime)
44
+
45
+ They are represented by either a Weekday object or a PartialDate
46
+ object with a count. A count of 0 means setting instead of shifting.
47
+ Only integer counts are acceptable.
48
+
49
+ It is an error to set to an invalid date (e.g., --31 applied on
50
+ 2019-06-25 is an error). The datetime parts which are specified must
51
+ be consecutive (it is an error to specify 12::05). It is also an
52
+ error to shift for occurrence of a partial date with year specified
53
+ (e.g., "+2x2019--").
54
+
55
+ On the other hand, shifting to an invalid date with day number
56
+ specified will shift more until a specified date is valid. For
57
+ example, if you add -2-29 with count 1 to 2019-01-01, you end up with
58
+ 2020-02-29, because 2019-02-29 is not a valid date. If the count is 2
59
+ you get 2024-02-29 instead.
60
+
61
+ Shifting to an invalid date by a partial date with just a month number
62
+ will cause the date to moved backwards until the date is valid. E.g.,
63
+ if you shift by -6- with count 1 (next June) from 2019-05-31, you get
64
+ 2019-06-30. With count 2 you get 2020-06-30.
65
+
66
+ This library is grown out of frustration that it is tedious to have a
67
+ shell script or program to get a datetime like "the next 6pm from now"
68
+ or "the next 3rd of any month from two days ago". With this module
69
+ they can be specified like "+1x18:00:00.0" and "-2day +1x--3"
70
+ respectively. In the expected use cases, counts are small numbers.
71
+ So the library is not always efficient (at times we just loop "count"
72
+ times to step forward or backward). Whenever it is simple to do so,
73
+ the implementation just forward to relativedelta, in which case they
74
+ are more efficient.
75
+
76
+ At present the program does not handle timezone and daylight saving.
77
+ This is bacause the author lives at a place where no daylight saving
78
+ is observed. Contributions are welcome.
79
+
80
+ """
81
+
82
+ import datetime
83
+ import re
84
+ import typing
85
+
86
+ import dateutil.relativedelta as dr
87
+
88
+
89
+ __metaclass__ = type
90
+
91
+ __version__ = '0.2'
92
+
93
+ class ParseError(ValueError):
94
+ """Represent an error in parsing."""
95
+
96
+
97
+ class Period:
98
+ """Represent a command that shift a number of period
99
+
100
+ A period may be a year, month, week, day, hour, minute or second,
101
+ which is the string to be used in the period argument. If you
102
+ shift by month/year and it ends up into an invalid date, the
103
+ result is "truncated" back to the previous valid day. Shifting a
104
+ non-integer number of periods is supported except for months and
105
+ years.
106
+
107
+ Args:
108
+
109
+ count (float): The number of periods to shift
110
+ period (str): The period
111
+
112
+ """
113
+ def __init__(self, count: float, period: str):
114
+ assert period in ('year', 'month', 'week', 'day',
115
+ 'hour', 'minute', 'second')
116
+ self._count = count
117
+ self._period = period
118
+
119
+ def __radd__(self, dt: datetime.datetime) -> datetime.datetime:
120
+ return dt + dr.relativedelta(
121
+ **{self._period + 's': self._count}) # type: ignore
122
+
123
+ PARSE_RE = re.compile(r'''
124
+ ^
125
+ (?P<count> [+-] (?: [0-9]+ | [0-9]*\.[0-9]*) )
126
+ (?P<period> year|month|week|day|hour|minute|second)
127
+ $
128
+ ''', re.X)
129
+
130
+ @classmethod
131
+ def parse(cls, cmdstr: str) -> 'Period':
132
+ """Parse a command string to a Period object
133
+
134
+ The command string should be of the form "<N><period>", where
135
+ <N> is an explicitly signed number, and <period> is a period
136
+ string (case insensitive).
137
+
138
+ Args:
139
+
140
+ cmdstr (str): The command string
141
+
142
+ """
143
+ match = cls.PARSE_RE.match(cmdstr.lower())
144
+ if not match:
145
+ raise ParseError('Cannot parse string %s' % cmdstr)
146
+ gdt = match.groupdict()
147
+ cnt: float
148
+ try:
149
+ cnt = int(gdt['count'])
150
+ except Exception:
151
+ cnt = float(gdt['count'])
152
+ return cls(cnt, gdt['period'])
153
+
154
+
155
+ _WEEKDAY_CLS = [dr.SU, dr.MO, dr.TU, dr.WE, dr.TH, dr.FR, dr.SA]
156
+ SUN, MON, TUE, WED, THU, FRI, SAT = range(7)
157
+ _WEEKDAY_NUM = {'sun': 0, 'mon': 1, 'tue': 2, 'wed': 3,
158
+ 'thu': 4, 'fri': 5, 'sat': 6}
159
+
160
+
161
+ class Weekday:
162
+ """Represent a command that set or shift by weekday
163
+
164
+ A weekday is a number from 0 to 6, representing Sunday, Monday,
165
+ ..., Friday (the constants SUN, MON, etc. are provided for
166
+ readability of constant weekdays). If you set a weekday, by using
167
+ a zero count, it moves to the weekday of the current week (week
168
+ always starts on Sunday). A non-zero (integer) count would
169
+ instead shift forward or backward by that number of occurrences of
170
+ that weekday. If the original date is already that weekday it is
171
+ not counted as one of those occurrences.
172
+
173
+ Args:
174
+
175
+ count (int): The number of periods to shift
176
+ day (int): The weekday
177
+
178
+ """
179
+ def __init__(self, count: int, day: int):
180
+ self._count = count
181
+ assert day in range(7)
182
+ self._day = day
183
+ self._drcls = _WEEKDAY_CLS[day]
184
+
185
+ def __radd__(self, dt: datetime.datetime) -> datetime.datetime:
186
+ if self._count > 0:
187
+ return dt + dr.relativedelta(
188
+ days=1, weekday=self._drcls(self._count))
189
+ if self._count < 0:
190
+ return dt + dr.relativedelta(
191
+ days=-1, weekday=self._drcls(self._count))
192
+ dow = (dt.weekday() + 1) % 7
193
+ if self._day < dow:
194
+ return dt + dr.relativedelta(weekday=self._drcls(-1))
195
+ return dt + dr.relativedelta(weekday=self._drcls(1))
196
+
197
+ PARSE_RE = re.compile(r'''
198
+ ^
199
+ (?P<count> [+-] (?: [0-9]+ | [0-9]*\.[0-9]*) )?
200
+ (?P<weekday> sun|mon|tue|wed|thu|fri|sat)
201
+ $
202
+ ''', re.X)
203
+
204
+ @classmethod
205
+ def parse(cls, cmdstr: str) -> 'Weekday':
206
+ """Parse a command string to a Weekday object
207
+
208
+ The command string should be of the form "<N><weekday>", where
209
+ <N> is an explicitly signed number or empty string
210
+ (representing 0), and <weekday> is a weekday 3-letter string
211
+ like sun, mon, etc (case insensitive).
212
+
213
+ Args:
214
+
215
+ cmdstr (str): The command string
216
+
217
+ """
218
+ match = cls.PARSE_RE.match(cmdstr.lower())
219
+ if not match:
220
+ raise ParseError('Cannot parse string %s' % cmdstr)
221
+ gdt = match.groupdict()
222
+ cnt = int(gdt['count']) if gdt['count'] else 0
223
+ return cls(cnt, _WEEKDAY_NUM[gdt['weekday']])
224
+
225
+
226
+ class PartialDate:
227
+ """Represent a command that set or shift by partial date
228
+
229
+ A partial date command specifies a count and the values of some of
230
+ year, month, day, hour, minute, second and microsecond. The
231
+ specified value must be contiguous among the parts above.
232
+
233
+ Using a count of 0 sets the specified fields. It raises an error
234
+ if the result is an invalid date.
235
+
236
+ Using a positive or negative count shift the date forward or
237
+ backward, and in this case the year must not be specified. It
238
+ only counts valid dates. E.g., you can shift forward by a certain
239
+ number of Feb 29. The exception is when setting the month only.
240
+ In that case, if the result is an invalid date, the date is
241
+ "truncated" to the last valid date.
242
+
243
+ Args:
244
+
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)
253
+
254
+ """
255
+
256
+ _INVALID_SIG_RE = re.compile('10+1')
257
+
258
+ def __init__(
259
+ self, count: int = 0, year: typing.Optional[int] = None,
260
+ month: typing.Optional[int] = None, day: typing.Optional[int] = None,
261
+ hour: typing.Optional[int] = None, minute: typing.Optional[int] = None,
262
+ second: typing.Optional[int] = None,
263
+ microsecond: typing.Optional[int] = None
264
+ ):
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'
272
+ if isinstance(second, float):
273
+ second, orig_second = int(second), second
274
+ microsecond = int((orig_second - second) * 1000000 + 0.5)
275
+ self._count = count
276
+ self._year = year
277
+ self._month = month
278
+ self._day = day
279
+ self._hour = hour
280
+ self._minute = minute
281
+ self._second = second
282
+ self._microsecond = microsecond
283
+ self._firstset = sig.find('1')
284
+
285
+ _FIRSTSET_MOD = [
286
+ '', 'years', 'months', 'days', 'hours', 'minutes', 'seconds'
287
+ ]
288
+
289
+ def __radd__(self, dt: datetime.datetime) -> datetime.datetime:
290
+ if self._firstset == -1:
291
+ return dt
292
+ if not(self._count):
293
+ return self._rset(dt)
294
+ # modify day or finer, or day specified and is not vulnerable
295
+ # to variable month length
296
+ if self._firstset > 2 or \
297
+ (self._day is not None and self._day <= 28):
298
+ return self._simpleshift(dt)
299
+ if self._day is None:
300
+ return self._monthshift(dt)
301
+ return self._dayshift(dt)
302
+
303
+ def _rset(self, dt: datetime.datetime) -> datetime.datetime:
304
+ updater = {'year': self._year,
305
+ 'month': self._month,
306
+ 'day': self._day,
307
+ 'hour': self._hour,
308
+ 'minute': self._minute,
309
+ 'second': self._second,
310
+ 'microsecond': self._microsecond}
311
+ updater = {k: v for k, v in updater.items() if v is not None}
312
+ return dt.replace(**updater) # type: ignore
313
+
314
+ def _simpleshift(self, dt: datetime.datetime) -> datetime.datetime:
315
+ remain = self._count
316
+ ret = self._rset(dt)
317
+ if self._count < 0:
318
+ if ret < dt:
319
+ remain += 1
320
+ else:
321
+ if ret > dt:
322
+ remain -= 1
323
+ mod_field = self._FIRSTSET_MOD[self._firstset]
324
+ return ret + dr.relativedelta(**{mod_field: remain}) # type: ignore
325
+
326
+ def _dayshift(self, dt: datetime.datetime) -> datetime.datetime:
327
+ # Day specified
328
+ if self._firstset == 2: # modify month
329
+ shift = dr.relativedelta(months=1 if self._count > 0 else -1)
330
+ limit = 2 # Must be able to find a 31st day in 2 months
331
+ else:
332
+ shift = dr.relativedelta(years=1 if self._count > 0 else -1)
333
+ limit = 8 # Must be able to find a Feb 29 in 8 years
334
+ count = abs(self._count)
335
+ # Find first date
336
+ curr = dt
337
+ for _ in range(limit):
338
+ try:
339
+ ret = self._rset(curr)
340
+ except ValueError:
341
+ curr += shift
342
+ continue
343
+ if (self._count > 0) == (ret > dt):
344
+ count -= 1
345
+ break
346
+ else:
347
+ raise ValueError('Failed day shifting: invalid date?')
348
+ # Find count occurrences
349
+ while True:
350
+ if count == 0:
351
+ return ret
352
+ ret += shift
353
+ try:
354
+ ret = self._rset(ret)
355
+ except ValueError:
356
+ continue
357
+ count -= 1
358
+
359
+ def _monthshift(self, dt: datetime.datetime) -> datetime.datetime:
360
+ # Only month specified, shift by month rather than by year
361
+ assert self._month is not None
362
+ if self._count > 0:
363
+ num_months = self._month - dt.month
364
+ sign = 1
365
+ else:
366
+ num_months = dt.month - self._month
367
+ sign = -1
368
+ if num_months <= 0:
369
+ num_months += 12
370
+ num_months += (abs(self._count) - 1) * 12
371
+ return dt + dr.relativedelta(months=sign * num_months)
372
+
373
+ PARSE_RE1 = re.compile(r'''
374
+ ^
375
+ (?: (?P<count> [+-] (?: [0-9]+ | [0-9]*\.[0-9]*) ) x)?
376
+ (?P<year> [0-9]*)
377
+ -
378
+ (?P<month> [0-9]*)
379
+ -
380
+ (?P<day> [0-9]*)
381
+ $
382
+ ''', re.X)
383
+
384
+ PARSE_RE2 = re.compile(r'''
385
+ ^
386
+ (?: (?P<count> [+-] (?: [0-9]+ | [0-9]*\.[0-9]*) ) x)?
387
+ (?:
388
+ (?P<year> [0-9]*)
389
+ -
390
+ (?P<month> [0-9]*)
391
+ -
392
+ (?P<day> [0-9]*)
393
+ t
394
+ )?
395
+ (?P<hour> [0-9]*)
396
+ :
397
+ (?P<minute> [0-9]*)
398
+ :
399
+ (?P<second> [0-9]*)
400
+ (?:\. (?P<microsecond> [0-9]*) )?
401
+ $
402
+ ''', re.X)
403
+
404
+ @classmethod
405
+ def parse(cls, cmdstr: str) -> 'PartialDate':
406
+ """Parse a command string to a PartialDate object
407
+
408
+ The command string should be of the form
409
+ "<N>x<year>-<month>-<day>T<hour>:<minute>:<second>.<micro>",
410
+ where <N> is an explicitly signed number or empty string
411
+ (representing 0). To skip the specification of a part use
412
+ empty string. If all date parts are not specified the "--T"
413
+ may be omitted. If all the time parts are not specified the
414
+ "T::." may be omitted. If the microsecond part is not
415
+ specified the "." part may be omitted.
416
+
417
+ Args:
418
+
419
+ cmdstr (str): The command string
420
+
421
+ """
422
+ match = cls.PARSE_RE1.match(cmdstr.lower())
423
+ if not match:
424
+ match = cls.PARSE_RE2.match(cmdstr)
425
+ if not match:
426
+ raise ParseError('Cannot parse string %s' % cmdstr)
427
+ gdt = match.groupdict()
428
+
429
+ def _matchval(key: str) -> typing.Optional[int]:
430
+ val = gdt.get(key)
431
+ if not val:
432
+ return None
433
+ return int(val)
434
+
435
+ microsecond = None
436
+ msval = gdt.get('microsecond')
437
+ if msval:
438
+ microsecond = int(msval.ljust(6, '0')[:6])
439
+ return cls(_matchval('count') or 0,
440
+ _matchval('year'),
441
+ _matchval('month'),
442
+ _matchval('day'),
443
+ _matchval('hour'),
444
+ _matchval('minute'),
445
+ _matchval('second'),
446
+ microsecond)
447
+
448
+
449
+ def parse(cmdstr: str) -> typing.Union[Period, Weekday, PartialDate]:
450
+ """Attempt to parse one of the possible date command
451
+
452
+ Args:
453
+
454
+ cmdstr (str): The command string
455
+
456
+ """
457
+ try:
458
+ return Period.parse(cmdstr)
459
+ except ParseError:
460
+ pass
461
+ try:
462
+ return Weekday.parse(cmdstr)
463
+ except ParseError:
464
+ pass
465
+ return PartialDate.parse(cmdstr)
datec/__main__.py ADDED
@@ -0,0 +1,36 @@
1
+ from __future__ import print_function
2
+
3
+ import sys
4
+ import datetime
5
+
6
+ import datec
7
+
8
+
9
+ def main() -> None:
10
+ dt = datetime.datetime.now()
11
+ for cmd in sys.argv[1:]:
12
+ if cmd == '-h':
13
+ print('''Date command
14
+
15
+ Usage: datec [<command>] ...
16
+
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
+
20
+ <command> may be of the following forms:
21
+
22
+ * +2week: shift two weeks ahead
23
+ * mon: set to to the Monday of the current week
24
+ * +2mon: shift two Mondays ahead
25
+ * -3-7T14:: set month to 3, day to 7, and hour to 14
26
+ * +2x--31: move forward by 2 month day 31 (months without a 31st
27
+ day does not count.
28
+
29
+ ''', file=sys.stderr)
30
+ sys.exit(0)
31
+ dt = dt + datec.parse(cmd.lower())
32
+ print(dt.isoformat())
33
+
34
+
35
+ if __name__ == '__main__':
36
+ main()
@@ -0,0 +1,110 @@
1
+ Metadata-Version: 2.4
2
+ Name: datec
3
+ Version: 0.2
4
+ Summary: Date Command
5
+ Project-URL: Homepage, https://github.com/isaacto/datec
6
+ Project-URL: Repository, https://github.com/isaacto/datec.git
7
+ Project-URL: Issues, https://github.com/isaacto/datec/issues
8
+ Author-email: Isaac To <isaac.to@gmail.com>
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Topic :: Software Development :: Libraries
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: python-dateutil
23
+ Description-Content-Type: text/markdown
24
+
25
+ # Date command: A command-based date computation engine
26
+
27
+ ## Installation
28
+
29
+ You can install the package simply by
30
+
31
+ pip install datec
32
+
33
+ ## Usage
34
+
35
+ datec allows you to use "date commands" to modify datetime's by adding
36
+ to them, like this:
37
+
38
+ datetime.datetime.now() + datec.Period(2, 'week')
39
+
40
+ A date command can be parsed from strings using the parse() function,
41
+ which create a command from a string representation. This forms the
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.
52
+
53
+ Date commands are in two forms: period shifting commands and partial
54
+ datetime shifting commands. The first type is more familiar: they
55
+ look like
56
+
57
+ * +2week (shift the datetime forward by 2 week)
58
+ * -1month (shift the datetime backward by 1 month)
59
+
60
+ Period is one of year, month, week, day, hour, minute and second,
61
+ represented by an object of the Period class. Fractional numbers are
62
+ acceptable except for year and month. If shifting a period leads to
63
+ an invalid date (e.g., shift backward 1 month from 2019-07-31), it
64
+ moves backwards the closest valid date (here, 2019-06-30). In general
65
+ the parts finer than the shifted part is unaffected (e.g., shifting 1
66
+ month from 2019-07-31 02:00 gives you 2019-06-30 02:00).
67
+
68
+ Partial datetime shifting is less familiar. It looks like:
69
+
70
+ * 12:: (set the hour number to 12)
71
+ * +2x12:: (move forward to the second hour 12)
72
+ * +4x--31 (move forward to the fourth occurrence of day 31 of a month)
73
+ * -3x-02-29 (move backward to the third occurrence of February 29)
74
+ * wed (set to the Wednesday of the same week, week starts on Sunday)
75
+ * -3wed (move to the third Wednesday before the current datetime)
76
+
77
+ They are represented by either a Weekday object or a PartialDate
78
+ object with a count. A count of 0 means setting instead of shifting.
79
+ Only integer counts are acceptable.
80
+
81
+ It is an error to set to an invalid date (e.g., --31 applied on
82
+ 2019-06-25 is an error). The datetime parts which are specified must
83
+ be consecutive (it is an error to specify 12::05). It is also an
84
+ error to shift for occurrence of a partial date with year specified
85
+ (e.g., "+2x2019--").
86
+
87
+ On the other hand, shifting to an invalid date with day number
88
+ specified will shift more until a specified date is valid. For
89
+ example, if you add -2-29 with count 1 to 2019-01-01, you end up with
90
+ 2020-02-29, because 2019-02-29 is not a valid date. If the count is 2
91
+ you get 2024-02-29 instead.
92
+
93
+ Shifting to an invalid date by a partial date with just a month number
94
+ will cause the date to moved backwards until the date is valid. E.g.,
95
+ if you shift by -6- with count 1 (next June) from 2019-05-31, you get
96
+ 2019-06-30. With count 2 you get 2020-06-30.
97
+
98
+ This library is grown out of frustration that it is tedious to have a
99
+ shell script or program to get a datetime like "the next 6pm from now"
100
+ or "the next 3rd of any month from two days ago". With this module
101
+ they can be specified like "+1x18:00:00.0" and "-2day +1x--3"
102
+ respectively. In the expected use cases, counts are small numbers.
103
+ So the library is not always efficient (at times we just loop "count"
104
+ times to step forward or backward). Whenever it is simple to do so,
105
+ the implementation just forward to relativedelta, in which case they
106
+ are more efficient.
107
+
108
+ At present the program does not handle timezone and daylight saving.
109
+ This is bacause the author lives at a place where no daylight saving
110
+ is observed. Contributions are welcome.
@@ -0,0 +1,7 @@
1
+ datec/__init__.py,sha256=Uc9-7TJIm3FTeUdE9Z8zrhK3Nt5Fp1mVBTs6fALJEMw,16510
2
+ datec/__main__.py,sha256=tVFpFXkXb_BWoigg1iDMb7J3DS_DxbZFA7bfFtLGjhA,846
3
+ datec-0.2.dist-info/METADATA,sha256=-abLLdsNDTQ52oLUyMsjOMtFC-p4lC5O3xvggr8oAW0,4945
4
+ datec-0.2.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
5
+ datec-0.2.dist-info/entry_points.txt,sha256=X3JE4JFOFKiDVJ93KVOzE9y63rMPwtRnF3mzO8nGb_o,46
6
+ datec-0.2.dist-info/licenses/LICENSE,sha256=lar4g9VcHbwsiTsRFqX2M5p5Y08mTLLJ4cSLTmbb1cc,1064
7
+ datec-0.2.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.28.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,2 @@
1
+ [console_scripts]
2
+ datec = datec:__main__.main
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2019 isaacto
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.