From 0635916a2f7ebffcc7a11f6407a0b3b6391be056 Mon Sep 17 00:00:00 2001 From: Scott Gasch Date: Fri, 21 Oct 2022 12:01:45 -0700 Subject: [PATCH] Lift easter() function from dateutils and remove that dependency from this project. Original copyright and attribution in the code and the root NOTICE file. --- NOTICE | 9 ++ examples/cron/cron.py | 9 +- pyproject.template | 1 - pyproject.toml | 1 - src/pyutils/datetimez/dateparse_utils.py | 15 +-- src/pyutils/datetimez/datetime_utils.py | 140 +++++++++++++++++++++++ tests/datetimez/dateparse_utils_test.py | 8 ++ 7 files changed, 165 insertions(+), 18 deletions(-) diff --git a/NOTICE b/NOTICE index 46c18d1..aa64f30 100644 --- a/NOTICE +++ b/NOTICE @@ -64,6 +64,15 @@ source of all forked code. + Minor cleanup and style tweaks, + Added type hints. + 4. The Easter calculation (function easter() in dateutilz.datetime_utils.py) + was copied directly with from the dateutil (pip install python-dateutil) + project in order to remove that dependency from this project. + + Dateutil is an Apache 2.0 licensed open source project. The original + copyright for that code is reproduced above the stolen function. + + Scott didn't do anything to this logic other than minor formatting changes. + Thank you to everyone who makes their code available for reuse by others and contributes to the open source ecosystem. Scott is especially grateful to the authors of the projects above. Thank you. diff --git a/examples/cron/cron.py b/examples/cron/cron.py index 958aa61..7acc419 100755 --- a/examples/cron/cron.py +++ b/examples/cron/cron.py @@ -157,15 +157,16 @@ def main() -> int: record = config.config['lockfile_audit_record'] cmd = ' '.join(config.config['command']) if record: + start = lf.locktime with open(record, 'a') as wf: - print( - f'{lockfile_path}, ACQUIRE, {lf.locktime}, {cmd}', file=wf - ) + print(f'{lockfile_path}, ACQUIRE, {start}, {cmd}', file=wf) retval = run_command(timeout, timestamp_file) if record: + end = datetime.datetime.now().timestamp() + duration = datetime_utils.describe_duration_briefly(end - start) with open(record, 'a') as wf: print( - f'{lockfile_path}, RELEASE, {datetime.datetime.now().timestamp()}, {cmd}', + f'{lockfile_path}, RELEASE({duration}), {end}, {cmd}', file=wf, ) return retval diff --git a/pyproject.template b/pyproject.template index 3eb42f9..15fa3d9 100644 --- a/pyproject.template +++ b/pyproject.template @@ -20,7 +20,6 @@ dependencies = [ "holidays", "kazoo", "overrides", - "python-dateutil", "pytz", ] diff --git a/pyproject.toml b/pyproject.toml index fd62aed..e5c69b5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -20,7 +20,6 @@ dependencies = [ "holidays", "kazoo", "overrides", - "python-dateutil", "pytz", ] diff --git a/src/pyutils/datetimez/dateparse_utils.py b/src/pyutils/datetimez/dateparse_utils.py index 47cca3c..48250f5 100755 --- a/src/pyutils/datetimez/dateparse_utils.py +++ b/src/pyutils/datetimez/dateparse_utils.py @@ -100,8 +100,6 @@ import sys from typing import Any, Callable, Dict, Optional import antlr4 # type: ignore -import dateutil.easter -import dateutil.tz import holidays # type: ignore import pytz @@ -117,6 +115,7 @@ from pyutils.datetimez.datetime_utils import ( TimeUnit, date_to_datetime, datetime_to_date, + easter, n_timeunits_from_base, ) from pyutils.security import acl @@ -457,7 +456,7 @@ class DateParser(dateparse_utilsListener): # Holiday names if name == 'easte': - return dateutil.easter.easter(year=year) + return easter(year=year) elif name == 'hallo': return datetime.date(year=year, month=10, day=31) @@ -537,14 +536,6 @@ class DateParser(dateparse_utilsListener): except Exception: pass - # Try dateutil - try: - tz2 = dateutil.tz.gettz(txt) - if tz2 is not None: - return tz2 - except Exception: - pass - # Try constructing an offset in seconds try: txt_sign = txt[0] @@ -553,7 +544,7 @@ class DateParser(dateparse_utilsListener): hour = int(txt[1:3]) minute = int(txt[-2:]) offset = sign * (hour * 60 * 60) + sign * (minute * 60) - tzoffset = dateutil.tz.tzoffset(txt, offset) + tzoffset = datetime.timezone(datetime.timedelta(seconds=offset)) return tzoffset except Exception: pass diff --git a/src/pyutils/datetimez/datetime_utils.py b/src/pyutils/datetimez/datetime_utils.py index 5ddf4b6..2f428cc 100644 --- a/src/pyutils/datetimez/datetime_utils.py +++ b/src/pyutils/datetimez/datetime_utils.py @@ -1260,6 +1260,146 @@ def describe_timedelta_briefly( ) # Note: drops milliseconds +# The code to compute easter on a given year was copied from dateutil (pip +# install python-dateutil) and dumped in here to avoid a dependency. Dateutil +# is an Apache 2.0 LICENSE open source project: + +# Copyright 2017- Paul Ganssle +# Copyright 2017- dateutil contributors (see AUTHORS file) + +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at + +# http://www.apache.org/licenses/LICENSE-2.0 + +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The above license applies to all contributions after 2017-12-01, as well as +# all contributions that have been re-licensed (see AUTHORS file for the list of +# contributors who have re-licensed their code). +# -------------------------------------------------------------------------------- +# dateutil - Extensions to the standard Python datetime module. + +# Copyright (c) 2003-2011 - Gustavo Niemeyer +# Copyright (c) 2012-2014 - Tomi Pieviläinen +# Copyright (c) 2014-2016 - Yaron de Leeuw +# Copyright (c) 2015- - Paul Ganssle +# Copyright (c) 2015- - dateutil contributors (see AUTHORS file) + +# All rights reserved. + +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: + +# * Redistributions of source code must retain the above copyright notice, +# this list of conditions and the following disclaimer. +# * Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# * Neither the name of the copyright holder nor the names of its +# contributors may be used to endorse or promote products derived from +# this software without specific prior written permission. + +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR +# CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, +# EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, +# PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR +# PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF +# LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING +# NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS +# SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + +# The above BSD License Applies to all code, even that also covered by Apache 2.0. + +EASTER_JULIAN = 1 +EASTER_ORTHODOX = 2 +EASTER_WESTERN = 3 + + +def easter(year, method=EASTER_WESTERN): + """ + This method was ported from the work done by GM Arts, + on top of the algorithm by Claus Tondering, which was + based in part on the algorithm of Ouding (1940), as + quoted in "Explanatory Supplement to the Astronomical + Almanac", P. Kenneth Seidelmann, editor. + + This algorithm implements three different Easter + calculation methods: + + 1. Original calculation in Julian calendar, valid in + dates after 326 AD + 2. Original method, with date converted to Gregorian + calendar, valid in years 1583 to 4099 + 3. Revised method, in Gregorian calendar, valid in + years 1583 to 4099 as well + + These methods are represented by the constants: + + * ``EASTER_JULIAN = 1`` + * ``EASTER_ORTHODOX = 2`` + * ``EASTER_WESTERN = 3`` + + The default method is method 3. + + More about the algorithm may be found at: + + `GM Arts: Easter Algorithms `_ + + and + + `The Calendar FAQ: Easter `_ + + """ + + if not (1 <= method <= 3): + raise ValueError("invalid method") + + # g - Golden year - 1 + # c - Century + # h - (23 - Epact) mod 30 + # i - Number of days from March 21 to Paschal Full Moon + # j - Weekday for PFM (0=Sunday, etc) + # p - Number of days from March 21 to Sunday on or before PFM + # (-6 to 28 methods 1 & 3, to 56 for method 2) + # e - Extra days to add for method 2 (converting Julian + # date to Gregorian date) + + y = year + g = y % 19 + e = 0 + if method < 3: + # Old method + i = (19 * g + 15) % 30 + j = (y + y // 4 + i) % 7 + if method == 2: + # Extra dates to convert Julian to Gregorian date + e = 10 + if y > 1600: + e = e + y // 100 - 16 - (y // 100 - 16) // 4 + else: + # New method + c = y // 100 + h = (c - c // 4 - (8 * c + 13) // 25 + 19 * g + 15) % 30 + i = h - (h // 28) * (1 - (h // 28) * (29 // (h + 1)) * ((21 - g) // 11)) + j = (y + y // 4 + i + 2 - c + c // 4) % 7 + + # p can be from -6 to 56 corresponding to dates 22 March to 23 May + # (later dates apply to method 2, although 23 May never actually occurs) + p = i - j + e + d = 1 + (p + 27 + (p + 6) // 40) % 31 + m = 3 + (p + 26) // 30 + return datetime.date(int(y), int(m), int(d)) + + if __name__ == '__main__': import doctest diff --git a/tests/datetimez/dateparse_utils_test.py b/tests/datetimez/dateparse_utils_test.py index 93c7b96..370b00e 100755 --- a/tests/datetimez/dateparse_utils_test.py +++ b/tests/datetimez/dateparse_utils_test.py @@ -19,6 +19,14 @@ parsable_expressions = [ ('tomorrow', datetime.datetime(2021, 7, 3)), ('yesterday', datetime.datetime(2021, 7, 1)), ('21:30', datetime.datetime(2021, 7, 2, 21, 30, 0, 0)), + ( + '21:30 EST', + datetime.datetime(2021, 7, 2, 21, 30, 0, 0, tzinfo=pytz.timezone('EST')), + ), + ( + '21:30 -0500', + datetime.datetime(2021, 7, 2, 21, 30, 0, 0, tzinfo=pytz.timezone('EST')), + ), ('12:01am', datetime.datetime(2021, 7, 2, 0, 1, 0, 0)), ('12:02p', datetime.datetime(2021, 7, 2, 12, 2, 0, 0)), ('0:03', datetime.datetime(2021, 7, 2, 0, 3, 0, 0)), -- 2.46.0