Lift easter() function from dateutils and remove that dependency from
authorScott Gasch <[email protected]>
Fri, 21 Oct 2022 19:01:45 +0000 (12:01 -0700)
committerScott Gasch <[email protected]>
Fri, 21 Oct 2022 19:01:45 +0000 (12:01 -0700)
this project.  Original copyright and attribution in the code and the
root NOTICE file.

NOTICE
examples/cron/cron.py
pyproject.template
pyproject.toml
src/pyutils/datetimez/dateparse_utils.py
src/pyutils/datetimez/datetime_utils.py
tests/datetimez/dateparse_utils_test.py

diff --git a/NOTICE b/NOTICE
index 46c18d1f7972f3b1c7e7a62a59d9948eb9ee4487..aa64f300909b35e4cee0dcb52cf8d765e17f2cf6 100644 (file)
--- 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.
index 958aa619309a4d9109939d09bf4e7d6b190fc986..7acc419cac84c81aef3a939f271e0c6f37cfeafb 100755 (executable)
@@ -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
index 3eb42f99247b9163f2b88cdca38984e065709c2d..15fa3d93b1e8271c3bd7c0ce036358d09594a1c4 100644 (file)
@@ -20,7 +20,6 @@ dependencies = [
     "holidays",
     "kazoo",
     "overrides",
-    "python-dateutil",
     "pytz",
 ]
 
index fd62aed2b9b1a7724614512ea3436968321b88c5..e5c69b56c79502c680852454f64ab80bec1bcb48 100644 (file)
@@ -20,7 +20,6 @@ dependencies = [
     "holidays",
     "kazoo",
     "overrides",
-    "python-dateutil",
     "pytz",
 ]
 
index 47cca3cdb81a5156ee32d5c34bb730c3f7c9af40..48250f5f6e7ae32e7418d4fc7011e776a6e9e146 100755 (executable)
@@ -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
index 5ddf4b6014f66b171bb88bf3421ab0623cdf1eba..2f428cce1fc58e40b3ec169c706919087b1686f7 100644 (file)
@@ -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 <[email protected]>
+# 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 <[email protected]>
+# Copyright (c) 2012-2014 - Tomi Pieviläinen <[email protected]>
+# Copyright (c) 2014-2016 - Yaron de Leeuw <[email protected]>
+# Copyright (c) 2015-     - Paul Ganssle <[email protected]>
+# 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 <http://www.gmarts.org/index.php?go=415>`_
+
+    and
+
+    `The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_
+
+    """
+
+    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
 
index 93c7b96e4c19af217fbafcf1ed5dbde13ec599c5..370b00e11d5499e2bc5c20d0755f730d5271ab26 100755 (executable)
@@ -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)),