Instructions to use dl1wjd2/Logistics_AX with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- ultralytics
How to use dl1wjd2/Logistics_AX with ultralytics:
from huggingface_hub import hf_hub_download from ultralytics import YOLO # pick the weights file from this repo's "Files and versions" tab weights = hf_hub_download("dl1wjd2/Logistics_AX", "<weights>.pt") model = YOLO(weights) source = 'http://images.cocodataset.org/val2017/000000039769.jpg' model.predict(source=source, save=True) - Notebooks
- Google Colab
- Kaggle
Upload folder using huggingface_hub (part 4)
Browse filesThis view is limited to 50 files because it contains too many changes. See raw diff
- .gitattributes +11 -0
- hug/lib/python3.12/site-packages/dateutil/parser/__init__.py +61 -0
- hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/__init__.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/_parser.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/isoparser.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/parser/_parser.py +1613 -0
- hug/lib/python3.12/site-packages/dateutil/parser/isoparser.py +416 -0
- hug/lib/python3.12/site-packages/dateutil/relativedelta.py +596 -0
- hug/lib/python3.12/site-packages/dateutil/rrule.py +1764 -0
- hug/lib/python3.12/site-packages/dateutil/tz/__init__.py +60 -0
- hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/__init__.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_common.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_factories.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzfile.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzpath.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/tz.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/win.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/tz/_common.py +419 -0
- hug/lib/python3.12/site-packages/dateutil/tz/_factories.py +80 -0
- hug/lib/python3.12/site-packages/dateutil/tz/_tzfile.py +1428 -0
- hug/lib/python3.12/site-packages/dateutil/tz/_tzpath.py +104 -0
- hug/lib/python3.12/site-packages/dateutil/tz/tz.py +1472 -0
- hug/lib/python3.12/site-packages/dateutil/tz/win.py +370 -0
- hug/lib/python3.12/site-packages/dateutil/tzwin.py +2 -0
- hug/lib/python3.12/site-packages/dateutil/utils.py +71 -0
- hug/lib/python3.12/site-packages/dateutil/zoneinfo/__init__.py +233 -0
- hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/__init__.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/rebuild.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/dateutil/zoneinfo/rebuild.py +42 -0
- hug/lib/python3.12/site-packages/distutils-precedence.pth +3 -0
- hug/lib/python3.12/site-packages/example.py +170 -0
- hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/INSTALLER +1 -0
- hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/METADATA +38 -0
- hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/RECORD +52 -0
- hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/WHEEL +4 -0
- hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/licenses/LICENSE +21 -0
- hug/lib/python3.12/site-packages/filelock/__init__.py +174 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/__init__.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_api.cpython-312.pyc +3 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_async.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_async_read_write.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_descriptor.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_error.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_identity.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_lease.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_marker.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_read_write.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_soft.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_soft_protocol.cpython-312.pyc +0 -0
- hug/lib/python3.12/site-packages/filelock/__pycache__/_strict.cpython-312.pyc +0 -0
.gitattributes
CHANGED
|
@@ -71,3 +71,14 @@ hug/lib/python3.12/site-packages/cuda/bindings/nvrtc.cpython-312-x86_64-linux-gn
|
|
| 71 |
hug/lib/python3.12/site-packages/cuda/bindings/nvvm.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 72 |
hug/lib/python3.12/site-packages/cuda/bindings/runtime.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 73 |
hug/lib/python3.12/site-packages/cv2/cv2.abi3.so filter=lfs diff=lfs merge=lfs -text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 71 |
hug/lib/python3.12/site-packages/cuda/bindings/nvvm.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 72 |
hug/lib/python3.12/site-packages/cuda/bindings/runtime.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 73 |
hug/lib/python3.12/site-packages/cv2/cv2.abi3.so filter=lfs diff=lfs merge=lfs -text
|
| 74 |
+
hug/lib/python3.12/site-packages/filelock/__pycache__/_api.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 75 |
+
hug/lib/python3.12/site-packages/fontTools/__pycache__/agl.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 76 |
+
hug/lib/python3.12/site-packages/fontTools/cffLib/__pycache__/__init__.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 77 |
+
hug/lib/python3.12/site-packages/fontTools/cu2qu/cu2qu.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 78 |
+
hug/lib/python3.12/site-packages/fontTools/designspaceLib/__pycache__/__init__.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 79 |
+
hug/lib/python3.12/site-packages/fontTools/feaLib/__pycache__/ast.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 80 |
+
hug/lib/python3.12/site-packages/fontTools/feaLib/__pycache__/parser.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 81 |
+
hug/lib/python3.12/site-packages/fontTools/feaLib/lexer.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 82 |
+
hug/lib/python3.12/site-packages/fontTools/misc/bezierTools.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
| 83 |
+
hug/lib/python3.12/site-packages/fontTools/otlLib/__pycache__/builder.cpython-312.pyc filter=lfs diff=lfs merge=lfs -text
|
| 84 |
+
hug/lib/python3.12/site-packages/fontTools/pens/momentsPen.cpython-312-x86_64-linux-gnu.so filter=lfs diff=lfs merge=lfs -text
|
hug/lib/python3.12/site-packages/dateutil/parser/__init__.py
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
from ._parser import parse, parser, parserinfo, ParserError
|
| 3 |
+
from ._parser import DEFAULTPARSER, DEFAULTTZPARSER
|
| 4 |
+
from ._parser import UnknownTimezoneWarning
|
| 5 |
+
|
| 6 |
+
from ._parser import __doc__
|
| 7 |
+
|
| 8 |
+
from .isoparser import isoparser, isoparse
|
| 9 |
+
|
| 10 |
+
__all__ = ['parse', 'parser', 'parserinfo',
|
| 11 |
+
'isoparse', 'isoparser',
|
| 12 |
+
'ParserError',
|
| 13 |
+
'UnknownTimezoneWarning']
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
###
|
| 17 |
+
# Deprecate portions of the private interface so that downstream code that
|
| 18 |
+
# is improperly relying on it is given *some* notice.
|
| 19 |
+
|
| 20 |
+
|
| 21 |
+
def __deprecated_private_func(f):
|
| 22 |
+
from functools import wraps
|
| 23 |
+
import warnings
|
| 24 |
+
|
| 25 |
+
msg = ('{name} is a private function and may break without warning, '
|
| 26 |
+
'it will be moved and or renamed in future versions.')
|
| 27 |
+
msg = msg.format(name=f.__name__)
|
| 28 |
+
|
| 29 |
+
@wraps(f)
|
| 30 |
+
def deprecated_func(*args, **kwargs):
|
| 31 |
+
warnings.warn(msg, DeprecationWarning)
|
| 32 |
+
return f(*args, **kwargs)
|
| 33 |
+
|
| 34 |
+
return deprecated_func
|
| 35 |
+
|
| 36 |
+
def __deprecate_private_class(c):
|
| 37 |
+
import warnings
|
| 38 |
+
|
| 39 |
+
msg = ('{name} is a private class and may break without warning, '
|
| 40 |
+
'it will be moved and or renamed in future versions.')
|
| 41 |
+
msg = msg.format(name=c.__name__)
|
| 42 |
+
|
| 43 |
+
class private_class(c):
|
| 44 |
+
__doc__ = c.__doc__
|
| 45 |
+
|
| 46 |
+
def __init__(self, *args, **kwargs):
|
| 47 |
+
warnings.warn(msg, DeprecationWarning)
|
| 48 |
+
super(private_class, self).__init__(*args, **kwargs)
|
| 49 |
+
|
| 50 |
+
private_class.__name__ = c.__name__
|
| 51 |
+
|
| 52 |
+
return private_class
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
from ._parser import _timelex, _resultbase
|
| 56 |
+
from ._parser import _tzparser, _parsetz
|
| 57 |
+
|
| 58 |
+
_timelex = __deprecate_private_class(_timelex)
|
| 59 |
+
_tzparser = __deprecate_private_class(_tzparser)
|
| 60 |
+
_resultbase = __deprecate_private_class(_resultbase)
|
| 61 |
+
_parsetz = __deprecated_private_func(_parsetz)
|
hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (2.65 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/_parser.cpython-312.pyc
ADDED
|
Binary file (61.8 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/parser/__pycache__/isoparser.cpython-312.pyc
ADDED
|
Binary file (15.6 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/parser/_parser.py
ADDED
|
@@ -0,0 +1,1613 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
This module offers a generic date/time string parser which is able to parse
|
| 4 |
+
most known formats to represent a date and/or time.
|
| 5 |
+
|
| 6 |
+
This module attempts to be forgiving with regards to unlikely input formats,
|
| 7 |
+
returning a datetime object even for dates which are ambiguous. If an element
|
| 8 |
+
of a date/time stamp is omitted, the following rules are applied:
|
| 9 |
+
|
| 10 |
+
- If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour
|
| 11 |
+
on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is
|
| 12 |
+
specified.
|
| 13 |
+
- If a time zone is omitted, a timezone-naive datetime is returned.
|
| 14 |
+
|
| 15 |
+
If any other elements are missing, they are taken from the
|
| 16 |
+
:class:`datetime.datetime` object passed to the parameter ``default``. If this
|
| 17 |
+
results in a day number exceeding the valid number of days per month, the
|
| 18 |
+
value falls back to the end of the month.
|
| 19 |
+
|
| 20 |
+
Additional resources about date/time string formats can be found below:
|
| 21 |
+
|
| 22 |
+
- `A summary of the international standard date and time notation
|
| 23 |
+
<https://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_
|
| 24 |
+
- `W3C Date and Time Formats <https://www.w3.org/TR/NOTE-datetime>`_
|
| 25 |
+
- `Time Formats (Planetary Rings Node) <https://pds-rings.seti.org:443/tools/time_formats.html>`_
|
| 26 |
+
- `CPAN ParseDate module
|
| 27 |
+
<https://metacpan.org/pod/release/MUIR/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_
|
| 28 |
+
- `Java SimpleDateFormat Class
|
| 29 |
+
<https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_
|
| 30 |
+
"""
|
| 31 |
+
from __future__ import unicode_literals
|
| 32 |
+
|
| 33 |
+
import datetime
|
| 34 |
+
import re
|
| 35 |
+
import string
|
| 36 |
+
import time
|
| 37 |
+
import warnings
|
| 38 |
+
|
| 39 |
+
from calendar import monthrange
|
| 40 |
+
from io import StringIO
|
| 41 |
+
|
| 42 |
+
import six
|
| 43 |
+
from six import integer_types, text_type
|
| 44 |
+
|
| 45 |
+
from decimal import Decimal
|
| 46 |
+
|
| 47 |
+
from warnings import warn
|
| 48 |
+
|
| 49 |
+
from .. import relativedelta
|
| 50 |
+
from .. import tz
|
| 51 |
+
|
| 52 |
+
__all__ = ["parse", "parserinfo", "ParserError"]
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
# TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth
|
| 56 |
+
# making public and/or figuring out if there is something we can
|
| 57 |
+
# take off their plate.
|
| 58 |
+
class _timelex(object):
|
| 59 |
+
# Fractional seconds are sometimes split by a comma
|
| 60 |
+
_split_decimal = re.compile("([.,])")
|
| 61 |
+
|
| 62 |
+
def __init__(self, instream):
|
| 63 |
+
if isinstance(instream, (bytes, bytearray)):
|
| 64 |
+
instream = instream.decode()
|
| 65 |
+
|
| 66 |
+
if isinstance(instream, text_type):
|
| 67 |
+
instream = StringIO(instream)
|
| 68 |
+
elif getattr(instream, 'read', None) is None:
|
| 69 |
+
raise TypeError('Parser must be a string or character stream, not '
|
| 70 |
+
'{itype}'.format(itype=instream.__class__.__name__))
|
| 71 |
+
|
| 72 |
+
self.instream = instream
|
| 73 |
+
self.charstack = []
|
| 74 |
+
self.tokenstack = []
|
| 75 |
+
self.eof = False
|
| 76 |
+
|
| 77 |
+
def get_token(self):
|
| 78 |
+
"""
|
| 79 |
+
This function breaks the time string into lexical units (tokens), which
|
| 80 |
+
can be parsed by the parser. Lexical units are demarcated by changes in
|
| 81 |
+
the character set, so any continuous string of letters is considered
|
| 82 |
+
one unit, any continuous string of numbers is considered one unit.
|
| 83 |
+
|
| 84 |
+
The main complication arises from the fact that dots ('.') can be used
|
| 85 |
+
both as separators (e.g. "Sep.20.2009") or decimal points (e.g.
|
| 86 |
+
"4:30:21.447"). As such, it is necessary to read the full context of
|
| 87 |
+
any dot-separated strings before breaking it into tokens; as such, this
|
| 88 |
+
function maintains a "token stack", for when the ambiguous context
|
| 89 |
+
demands that multiple tokens be parsed at once.
|
| 90 |
+
"""
|
| 91 |
+
if self.tokenstack:
|
| 92 |
+
return self.tokenstack.pop(0)
|
| 93 |
+
|
| 94 |
+
seenletters = False
|
| 95 |
+
token = None
|
| 96 |
+
state = None
|
| 97 |
+
|
| 98 |
+
while not self.eof:
|
| 99 |
+
# We only realize that we've reached the end of a token when we
|
| 100 |
+
# find a character that's not part of the current token - since
|
| 101 |
+
# that character may be part of the next token, it's stored in the
|
| 102 |
+
# charstack.
|
| 103 |
+
if self.charstack:
|
| 104 |
+
nextchar = self.charstack.pop(0)
|
| 105 |
+
else:
|
| 106 |
+
nextchar = self.instream.read(1)
|
| 107 |
+
while nextchar == '\x00':
|
| 108 |
+
nextchar = self.instream.read(1)
|
| 109 |
+
|
| 110 |
+
if not nextchar:
|
| 111 |
+
self.eof = True
|
| 112 |
+
break
|
| 113 |
+
elif not state:
|
| 114 |
+
# First character of the token - determines if we're starting
|
| 115 |
+
# to parse a word, a number or something else.
|
| 116 |
+
token = nextchar
|
| 117 |
+
if self.isword(nextchar):
|
| 118 |
+
state = 'a'
|
| 119 |
+
elif self.isnum(nextchar):
|
| 120 |
+
state = '0'
|
| 121 |
+
elif self.isspace(nextchar):
|
| 122 |
+
token = ' '
|
| 123 |
+
break # emit token
|
| 124 |
+
else:
|
| 125 |
+
break # emit token
|
| 126 |
+
elif state == 'a':
|
| 127 |
+
# If we've already started reading a word, we keep reading
|
| 128 |
+
# letters until we find something that's not part of a word.
|
| 129 |
+
seenletters = True
|
| 130 |
+
if self.isword(nextchar):
|
| 131 |
+
token += nextchar
|
| 132 |
+
elif nextchar == '.':
|
| 133 |
+
token += nextchar
|
| 134 |
+
state = 'a.'
|
| 135 |
+
else:
|
| 136 |
+
self.charstack.append(nextchar)
|
| 137 |
+
break # emit token
|
| 138 |
+
elif state == '0':
|
| 139 |
+
# If we've already started reading a number, we keep reading
|
| 140 |
+
# numbers until we find something that doesn't fit.
|
| 141 |
+
if self.isnum(nextchar):
|
| 142 |
+
token += nextchar
|
| 143 |
+
elif nextchar == '.' or (nextchar == ',' and len(token) >= 2):
|
| 144 |
+
token += nextchar
|
| 145 |
+
state = '0.'
|
| 146 |
+
else:
|
| 147 |
+
self.charstack.append(nextchar)
|
| 148 |
+
break # emit token
|
| 149 |
+
elif state == 'a.':
|
| 150 |
+
# If we've seen some letters and a dot separator, continue
|
| 151 |
+
# parsing, and the tokens will be broken up later.
|
| 152 |
+
seenletters = True
|
| 153 |
+
if nextchar == '.' or self.isword(nextchar):
|
| 154 |
+
token += nextchar
|
| 155 |
+
elif self.isnum(nextchar) and token[-1] == '.':
|
| 156 |
+
token += nextchar
|
| 157 |
+
state = '0.'
|
| 158 |
+
else:
|
| 159 |
+
self.charstack.append(nextchar)
|
| 160 |
+
break # emit token
|
| 161 |
+
elif state == '0.':
|
| 162 |
+
# If we've seen at least one dot separator, keep going, we'll
|
| 163 |
+
# break up the tokens later.
|
| 164 |
+
if nextchar == '.' or self.isnum(nextchar):
|
| 165 |
+
token += nextchar
|
| 166 |
+
elif self.isword(nextchar) and token[-1] == '.':
|
| 167 |
+
token += nextchar
|
| 168 |
+
state = 'a.'
|
| 169 |
+
else:
|
| 170 |
+
self.charstack.append(nextchar)
|
| 171 |
+
break # emit token
|
| 172 |
+
|
| 173 |
+
if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or
|
| 174 |
+
token[-1] in '.,')):
|
| 175 |
+
l = self._split_decimal.split(token)
|
| 176 |
+
token = l[0]
|
| 177 |
+
for tok in l[1:]:
|
| 178 |
+
if tok:
|
| 179 |
+
self.tokenstack.append(tok)
|
| 180 |
+
|
| 181 |
+
if state == '0.' and token.count('.') == 0:
|
| 182 |
+
token = token.replace(',', '.')
|
| 183 |
+
|
| 184 |
+
return token
|
| 185 |
+
|
| 186 |
+
def __iter__(self):
|
| 187 |
+
return self
|
| 188 |
+
|
| 189 |
+
def __next__(self):
|
| 190 |
+
token = self.get_token()
|
| 191 |
+
if token is None:
|
| 192 |
+
raise StopIteration
|
| 193 |
+
|
| 194 |
+
return token
|
| 195 |
+
|
| 196 |
+
def next(self):
|
| 197 |
+
return self.__next__() # Python 2.x support
|
| 198 |
+
|
| 199 |
+
@classmethod
|
| 200 |
+
def split(cls, s):
|
| 201 |
+
return list(cls(s))
|
| 202 |
+
|
| 203 |
+
@classmethod
|
| 204 |
+
def isword(cls, nextchar):
|
| 205 |
+
""" Whether or not the next character is part of a word """
|
| 206 |
+
return nextchar.isalpha()
|
| 207 |
+
|
| 208 |
+
@classmethod
|
| 209 |
+
def isnum(cls, nextchar):
|
| 210 |
+
""" Whether the next character is part of a number """
|
| 211 |
+
return nextchar.isdigit()
|
| 212 |
+
|
| 213 |
+
@classmethod
|
| 214 |
+
def isspace(cls, nextchar):
|
| 215 |
+
""" Whether the next character is whitespace """
|
| 216 |
+
return nextchar.isspace()
|
| 217 |
+
|
| 218 |
+
|
| 219 |
+
class _resultbase(object):
|
| 220 |
+
|
| 221 |
+
def __init__(self):
|
| 222 |
+
for attr in self.__slots__:
|
| 223 |
+
setattr(self, attr, None)
|
| 224 |
+
|
| 225 |
+
def _repr(self, classname):
|
| 226 |
+
l = []
|
| 227 |
+
for attr in self.__slots__:
|
| 228 |
+
value = getattr(self, attr)
|
| 229 |
+
if value is not None:
|
| 230 |
+
l.append("%s=%s" % (attr, repr(value)))
|
| 231 |
+
return "%s(%s)" % (classname, ", ".join(l))
|
| 232 |
+
|
| 233 |
+
def __len__(self):
|
| 234 |
+
return (sum(getattr(self, attr) is not None
|
| 235 |
+
for attr in self.__slots__))
|
| 236 |
+
|
| 237 |
+
def __repr__(self):
|
| 238 |
+
return self._repr(self.__class__.__name__)
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
class parserinfo(object):
|
| 242 |
+
"""
|
| 243 |
+
Class which handles what inputs are accepted. Subclass this to customize
|
| 244 |
+
the language and acceptable values for each parameter.
|
| 245 |
+
|
| 246 |
+
:param dayfirst:
|
| 247 |
+
Whether to interpret the first value in an ambiguous 3-integer date
|
| 248 |
+
(e.g. 01/05/09) as the day (``True``) or month (``False``). If
|
| 249 |
+
``yearfirst`` is set to ``True``, this distinguishes between YDM
|
| 250 |
+
and YMD. Default is ``False``.
|
| 251 |
+
|
| 252 |
+
:param yearfirst:
|
| 253 |
+
Whether to interpret the first value in an ambiguous 3-integer date
|
| 254 |
+
(e.g. 01/05/09) as the year. If ``True``, the first number is taken
|
| 255 |
+
to be the year, otherwise the last number is taken to be the year.
|
| 256 |
+
Default is ``False``.
|
| 257 |
+
"""
|
| 258 |
+
|
| 259 |
+
# m from a.m/p.m, t from ISO T separator
|
| 260 |
+
JUMP = [" ", ".", ",", ";", "-", "/", "'",
|
| 261 |
+
"at", "on", "and", "ad", "m", "t", "of",
|
| 262 |
+
"st", "nd", "rd", "th"]
|
| 263 |
+
|
| 264 |
+
WEEKDAYS = [("Mon", "Monday"),
|
| 265 |
+
("Tue", "Tuesday"), # TODO: "Tues"
|
| 266 |
+
("Wed", "Wednesday"),
|
| 267 |
+
("Thu", "Thursday"), # TODO: "Thurs"
|
| 268 |
+
("Fri", "Friday"),
|
| 269 |
+
("Sat", "Saturday"),
|
| 270 |
+
("Sun", "Sunday")]
|
| 271 |
+
MONTHS = [("Jan", "January"),
|
| 272 |
+
("Feb", "February"), # TODO: "Febr"
|
| 273 |
+
("Mar", "March"),
|
| 274 |
+
("Apr", "April"),
|
| 275 |
+
("May", "May"),
|
| 276 |
+
("Jun", "June"),
|
| 277 |
+
("Jul", "July"),
|
| 278 |
+
("Aug", "August"),
|
| 279 |
+
("Sep", "Sept", "September"),
|
| 280 |
+
("Oct", "October"),
|
| 281 |
+
("Nov", "November"),
|
| 282 |
+
("Dec", "December")]
|
| 283 |
+
HMS = [("h", "hour", "hours"),
|
| 284 |
+
("m", "minute", "minutes"),
|
| 285 |
+
("s", "second", "seconds")]
|
| 286 |
+
AMPM = [("am", "a"),
|
| 287 |
+
("pm", "p")]
|
| 288 |
+
UTCZONE = ["UTC", "GMT", "Z", "z"]
|
| 289 |
+
PERTAIN = ["of"]
|
| 290 |
+
TZOFFSET = {}
|
| 291 |
+
# TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate",
|
| 292 |
+
# "Anno Domini", "Year of Our Lord"]
|
| 293 |
+
|
| 294 |
+
def __init__(self, dayfirst=False, yearfirst=False):
|
| 295 |
+
self._jump = self._convert(self.JUMP)
|
| 296 |
+
self._weekdays = self._convert(self.WEEKDAYS)
|
| 297 |
+
self._months = self._convert(self.MONTHS)
|
| 298 |
+
self._hms = self._convert(self.HMS)
|
| 299 |
+
self._ampm = self._convert(self.AMPM)
|
| 300 |
+
self._utczone = self._convert(self.UTCZONE)
|
| 301 |
+
self._pertain = self._convert(self.PERTAIN)
|
| 302 |
+
|
| 303 |
+
self.dayfirst = dayfirst
|
| 304 |
+
self.yearfirst = yearfirst
|
| 305 |
+
|
| 306 |
+
self._year = time.localtime().tm_year
|
| 307 |
+
self._century = self._year // 100 * 100
|
| 308 |
+
|
| 309 |
+
def _convert(self, lst):
|
| 310 |
+
dct = {}
|
| 311 |
+
for i, v in enumerate(lst):
|
| 312 |
+
if isinstance(v, tuple):
|
| 313 |
+
for v in v:
|
| 314 |
+
dct[v.lower()] = i
|
| 315 |
+
else:
|
| 316 |
+
dct[v.lower()] = i
|
| 317 |
+
return dct
|
| 318 |
+
|
| 319 |
+
def jump(self, name):
|
| 320 |
+
return name.lower() in self._jump
|
| 321 |
+
|
| 322 |
+
def weekday(self, name):
|
| 323 |
+
try:
|
| 324 |
+
return self._weekdays[name.lower()]
|
| 325 |
+
except KeyError:
|
| 326 |
+
pass
|
| 327 |
+
return None
|
| 328 |
+
|
| 329 |
+
def month(self, name):
|
| 330 |
+
try:
|
| 331 |
+
return self._months[name.lower()] + 1
|
| 332 |
+
except KeyError:
|
| 333 |
+
pass
|
| 334 |
+
return None
|
| 335 |
+
|
| 336 |
+
def hms(self, name):
|
| 337 |
+
try:
|
| 338 |
+
return self._hms[name.lower()]
|
| 339 |
+
except KeyError:
|
| 340 |
+
return None
|
| 341 |
+
|
| 342 |
+
def ampm(self, name):
|
| 343 |
+
try:
|
| 344 |
+
return self._ampm[name.lower()]
|
| 345 |
+
except KeyError:
|
| 346 |
+
return None
|
| 347 |
+
|
| 348 |
+
def pertain(self, name):
|
| 349 |
+
return name.lower() in self._pertain
|
| 350 |
+
|
| 351 |
+
def utczone(self, name):
|
| 352 |
+
return name.lower() in self._utczone
|
| 353 |
+
|
| 354 |
+
def tzoffset(self, name):
|
| 355 |
+
if name in self._utczone:
|
| 356 |
+
return 0
|
| 357 |
+
|
| 358 |
+
return self.TZOFFSET.get(name)
|
| 359 |
+
|
| 360 |
+
def convertyear(self, year, century_specified=False):
|
| 361 |
+
"""
|
| 362 |
+
Converts two-digit years to year within [-50, 49]
|
| 363 |
+
range of self._year (current local time)
|
| 364 |
+
"""
|
| 365 |
+
|
| 366 |
+
# Function contract is that the year is always positive
|
| 367 |
+
assert year >= 0
|
| 368 |
+
|
| 369 |
+
if year < 100 and not century_specified:
|
| 370 |
+
# assume current century to start
|
| 371 |
+
year += self._century
|
| 372 |
+
|
| 373 |
+
if year >= self._year + 50: # if too far in future
|
| 374 |
+
year -= 100
|
| 375 |
+
elif year < self._year - 50: # if too far in past
|
| 376 |
+
year += 100
|
| 377 |
+
|
| 378 |
+
return year
|
| 379 |
+
|
| 380 |
+
def validate(self, res):
|
| 381 |
+
# move to info
|
| 382 |
+
if res.year is not None:
|
| 383 |
+
res.year = self.convertyear(res.year, res.century_specified)
|
| 384 |
+
|
| 385 |
+
if ((res.tzoffset == 0 and not res.tzname) or
|
| 386 |
+
(res.tzname == 'Z' or res.tzname == 'z')):
|
| 387 |
+
res.tzname = "UTC"
|
| 388 |
+
res.tzoffset = 0
|
| 389 |
+
elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname):
|
| 390 |
+
res.tzoffset = 0
|
| 391 |
+
return True
|
| 392 |
+
|
| 393 |
+
|
| 394 |
+
class _ymd(list):
|
| 395 |
+
def __init__(self, *args, **kwargs):
|
| 396 |
+
super(self.__class__, self).__init__(*args, **kwargs)
|
| 397 |
+
self.century_specified = False
|
| 398 |
+
self.dstridx = None
|
| 399 |
+
self.mstridx = None
|
| 400 |
+
self.ystridx = None
|
| 401 |
+
|
| 402 |
+
@property
|
| 403 |
+
def has_year(self):
|
| 404 |
+
return self.ystridx is not None
|
| 405 |
+
|
| 406 |
+
@property
|
| 407 |
+
def has_month(self):
|
| 408 |
+
return self.mstridx is not None
|
| 409 |
+
|
| 410 |
+
@property
|
| 411 |
+
def has_day(self):
|
| 412 |
+
return self.dstridx is not None
|
| 413 |
+
|
| 414 |
+
def could_be_day(self, value):
|
| 415 |
+
if self.has_day:
|
| 416 |
+
return False
|
| 417 |
+
elif not self.has_month:
|
| 418 |
+
return 1 <= value <= 31
|
| 419 |
+
elif not self.has_year:
|
| 420 |
+
# Be permissive, assume leap year
|
| 421 |
+
month = self[self.mstridx]
|
| 422 |
+
return 1 <= value <= monthrange(2000, month)[1]
|
| 423 |
+
else:
|
| 424 |
+
month = self[self.mstridx]
|
| 425 |
+
year = self[self.ystridx]
|
| 426 |
+
return 1 <= value <= monthrange(year, month)[1]
|
| 427 |
+
|
| 428 |
+
def append(self, val, label=None):
|
| 429 |
+
if hasattr(val, '__len__'):
|
| 430 |
+
if val.isdigit() and len(val) > 2:
|
| 431 |
+
self.century_specified = True
|
| 432 |
+
if label not in [None, 'Y']: # pragma: no cover
|
| 433 |
+
raise ValueError(label)
|
| 434 |
+
label = 'Y'
|
| 435 |
+
elif val > 100:
|
| 436 |
+
self.century_specified = True
|
| 437 |
+
if label not in [None, 'Y']: # pragma: no cover
|
| 438 |
+
raise ValueError(label)
|
| 439 |
+
label = 'Y'
|
| 440 |
+
|
| 441 |
+
super(self.__class__, self).append(int(val))
|
| 442 |
+
|
| 443 |
+
if label == 'M':
|
| 444 |
+
if self.has_month:
|
| 445 |
+
raise ValueError('Month is already set')
|
| 446 |
+
self.mstridx = len(self) - 1
|
| 447 |
+
elif label == 'D':
|
| 448 |
+
if self.has_day:
|
| 449 |
+
raise ValueError('Day is already set')
|
| 450 |
+
self.dstridx = len(self) - 1
|
| 451 |
+
elif label == 'Y':
|
| 452 |
+
if self.has_year:
|
| 453 |
+
raise ValueError('Year is already set')
|
| 454 |
+
self.ystridx = len(self) - 1
|
| 455 |
+
|
| 456 |
+
def _resolve_from_stridxs(self, strids):
|
| 457 |
+
"""
|
| 458 |
+
Try to resolve the identities of year/month/day elements using
|
| 459 |
+
ystridx, mstridx, and dstridx, if enough of these are specified.
|
| 460 |
+
"""
|
| 461 |
+
if len(self) == 3 and len(strids) == 2:
|
| 462 |
+
# we can back out the remaining stridx value
|
| 463 |
+
missing = [x for x in range(3) if x not in strids.values()]
|
| 464 |
+
key = [x for x in ['y', 'm', 'd'] if x not in strids]
|
| 465 |
+
assert len(missing) == len(key) == 1
|
| 466 |
+
key = key[0]
|
| 467 |
+
val = missing[0]
|
| 468 |
+
strids[key] = val
|
| 469 |
+
|
| 470 |
+
assert len(self) == len(strids) # otherwise this should not be called
|
| 471 |
+
out = {key: self[strids[key]] for key in strids}
|
| 472 |
+
return (out.get('y'), out.get('m'), out.get('d'))
|
| 473 |
+
|
| 474 |
+
def resolve_ymd(self, yearfirst, dayfirst):
|
| 475 |
+
len_ymd = len(self)
|
| 476 |
+
year, month, day = (None, None, None)
|
| 477 |
+
|
| 478 |
+
strids = (('y', self.ystridx),
|
| 479 |
+
('m', self.mstridx),
|
| 480 |
+
('d', self.dstridx))
|
| 481 |
+
|
| 482 |
+
strids = {key: val for key, val in strids if val is not None}
|
| 483 |
+
if (len(self) == len(strids) > 0 or
|
| 484 |
+
(len(self) == 3 and len(strids) == 2)):
|
| 485 |
+
return self._resolve_from_stridxs(strids)
|
| 486 |
+
|
| 487 |
+
mstridx = self.mstridx
|
| 488 |
+
|
| 489 |
+
if len_ymd > 3:
|
| 490 |
+
raise ValueError("More than three YMD values")
|
| 491 |
+
elif len_ymd == 1 or (mstridx is not None and len_ymd == 2):
|
| 492 |
+
# One member, or two members with a month string
|
| 493 |
+
if mstridx is not None:
|
| 494 |
+
month = self[mstridx]
|
| 495 |
+
# since mstridx is 0 or 1, self[mstridx-1] always
|
| 496 |
+
# looks up the other element
|
| 497 |
+
other = self[mstridx - 1]
|
| 498 |
+
else:
|
| 499 |
+
other = self[0]
|
| 500 |
+
|
| 501 |
+
if len_ymd > 1 or mstridx is None:
|
| 502 |
+
if other > 31:
|
| 503 |
+
year = other
|
| 504 |
+
else:
|
| 505 |
+
day = other
|
| 506 |
+
|
| 507 |
+
elif len_ymd == 2:
|
| 508 |
+
# Two members with numbers
|
| 509 |
+
if self[0] > 31:
|
| 510 |
+
# 99-01
|
| 511 |
+
year, month = self
|
| 512 |
+
elif self[1] > 31:
|
| 513 |
+
# 01-99
|
| 514 |
+
month, year = self
|
| 515 |
+
elif dayfirst and self[1] <= 12:
|
| 516 |
+
# 13-01
|
| 517 |
+
day, month = self
|
| 518 |
+
else:
|
| 519 |
+
# 01-13
|
| 520 |
+
month, day = self
|
| 521 |
+
|
| 522 |
+
elif len_ymd == 3:
|
| 523 |
+
# Three members
|
| 524 |
+
if mstridx == 0:
|
| 525 |
+
if self[1] > 31:
|
| 526 |
+
# Apr-2003-25
|
| 527 |
+
month, year, day = self
|
| 528 |
+
else:
|
| 529 |
+
month, day, year = self
|
| 530 |
+
elif mstridx == 1:
|
| 531 |
+
if self[0] > 31 or (yearfirst and self[2] <= 31):
|
| 532 |
+
# 99-Jan-01
|
| 533 |
+
year, month, day = self
|
| 534 |
+
else:
|
| 535 |
+
# 01-Jan-01
|
| 536 |
+
# Give precedence to day-first, since
|
| 537 |
+
# two-digit years is usually hand-written.
|
| 538 |
+
day, month, year = self
|
| 539 |
+
|
| 540 |
+
elif mstridx == 2:
|
| 541 |
+
# WTF!?
|
| 542 |
+
if self[1] > 31:
|
| 543 |
+
# 01-99-Jan
|
| 544 |
+
day, year, month = self
|
| 545 |
+
else:
|
| 546 |
+
# 99-01-Jan
|
| 547 |
+
year, day, month = self
|
| 548 |
+
|
| 549 |
+
else:
|
| 550 |
+
if (self[0] > 31 or
|
| 551 |
+
self.ystridx == 0 or
|
| 552 |
+
(yearfirst and self[1] <= 12 and self[2] <= 31)):
|
| 553 |
+
# 99-01-01
|
| 554 |
+
if dayfirst and self[2] <= 12:
|
| 555 |
+
year, day, month = self
|
| 556 |
+
else:
|
| 557 |
+
year, month, day = self
|
| 558 |
+
elif self[0] > 12 or (dayfirst and self[1] <= 12):
|
| 559 |
+
# 13-01-01
|
| 560 |
+
day, month, year = self
|
| 561 |
+
else:
|
| 562 |
+
# 01-13-01
|
| 563 |
+
month, day, year = self
|
| 564 |
+
|
| 565 |
+
return year, month, day
|
| 566 |
+
|
| 567 |
+
|
| 568 |
+
class parser(object):
|
| 569 |
+
def __init__(self, info=None):
|
| 570 |
+
self.info = info or parserinfo()
|
| 571 |
+
|
| 572 |
+
def parse(self, timestr, default=None,
|
| 573 |
+
ignoretz=False, tzinfos=None, **kwargs):
|
| 574 |
+
"""
|
| 575 |
+
Parse the date/time string into a :class:`datetime.datetime` object.
|
| 576 |
+
|
| 577 |
+
:param timestr:
|
| 578 |
+
Any date/time string using the supported formats.
|
| 579 |
+
|
| 580 |
+
:param default:
|
| 581 |
+
The default datetime object, if this is a datetime object and not
|
| 582 |
+
``None``, elements specified in ``timestr`` replace elements in the
|
| 583 |
+
default object.
|
| 584 |
+
|
| 585 |
+
:param ignoretz:
|
| 586 |
+
If set ``True``, time zones in parsed strings are ignored and a
|
| 587 |
+
naive :class:`datetime.datetime` object is returned.
|
| 588 |
+
|
| 589 |
+
:param tzinfos:
|
| 590 |
+
Additional time zone names / aliases which may be present in the
|
| 591 |
+
string. This argument maps time zone names (and optionally offsets
|
| 592 |
+
from those time zones) to time zones. This parameter can be a
|
| 593 |
+
dictionary with timezone aliases mapping time zone names to time
|
| 594 |
+
zones or a function taking two parameters (``tzname`` and
|
| 595 |
+
``tzoffset``) and returning a time zone.
|
| 596 |
+
|
| 597 |
+
The timezones to which the names are mapped can be an integer
|
| 598 |
+
offset from UTC in seconds or a :class:`tzinfo` object.
|
| 599 |
+
|
| 600 |
+
.. doctest::
|
| 601 |
+
:options: +NORMALIZE_WHITESPACE
|
| 602 |
+
|
| 603 |
+
>>> from dateutil.parser import parse
|
| 604 |
+
>>> from dateutil.tz import gettz
|
| 605 |
+
>>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
|
| 606 |
+
>>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
|
| 607 |
+
datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
|
| 608 |
+
>>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
|
| 609 |
+
datetime.datetime(2012, 1, 19, 17, 21,
|
| 610 |
+
tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
|
| 611 |
+
|
| 612 |
+
This parameter is ignored if ``ignoretz`` is set.
|
| 613 |
+
|
| 614 |
+
:param \\*\\*kwargs:
|
| 615 |
+
Keyword arguments as passed to ``_parse()``.
|
| 616 |
+
|
| 617 |
+
:return:
|
| 618 |
+
Returns a :class:`datetime.datetime` object or, if the
|
| 619 |
+
``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
|
| 620 |
+
first element being a :class:`datetime.datetime` object, the second
|
| 621 |
+
a tuple containing the fuzzy tokens.
|
| 622 |
+
|
| 623 |
+
:raises ParserError:
|
| 624 |
+
Raised for invalid or unknown string format, if the provided
|
| 625 |
+
:class:`tzinfo` is not in a valid format, or if an invalid date
|
| 626 |
+
would be created.
|
| 627 |
+
|
| 628 |
+
:raises TypeError:
|
| 629 |
+
Raised for non-string or character stream input.
|
| 630 |
+
|
| 631 |
+
:raises OverflowError:
|
| 632 |
+
Raised if the parsed date exceeds the largest valid C integer on
|
| 633 |
+
your system.
|
| 634 |
+
"""
|
| 635 |
+
|
| 636 |
+
if default is None:
|
| 637 |
+
default = datetime.datetime.now().replace(hour=0, minute=0,
|
| 638 |
+
second=0, microsecond=0)
|
| 639 |
+
|
| 640 |
+
res, skipped_tokens = self._parse(timestr, **kwargs)
|
| 641 |
+
|
| 642 |
+
if res is None:
|
| 643 |
+
raise ParserError("Unknown string format: %s", timestr)
|
| 644 |
+
|
| 645 |
+
if len(res) == 0:
|
| 646 |
+
raise ParserError("String does not contain a date: %s", timestr)
|
| 647 |
+
|
| 648 |
+
try:
|
| 649 |
+
ret = self._build_naive(res, default)
|
| 650 |
+
except ValueError as e:
|
| 651 |
+
six.raise_from(ParserError(str(e) + ": %s", timestr), e)
|
| 652 |
+
|
| 653 |
+
if not ignoretz:
|
| 654 |
+
ret = self._build_tzaware(ret, res, tzinfos)
|
| 655 |
+
|
| 656 |
+
if kwargs.get('fuzzy_with_tokens', False):
|
| 657 |
+
return ret, skipped_tokens
|
| 658 |
+
else:
|
| 659 |
+
return ret
|
| 660 |
+
|
| 661 |
+
class _result(_resultbase):
|
| 662 |
+
__slots__ = ["year", "month", "day", "weekday",
|
| 663 |
+
"hour", "minute", "second", "microsecond",
|
| 664 |
+
"tzname", "tzoffset", "ampm","any_unused_tokens"]
|
| 665 |
+
|
| 666 |
+
def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False,
|
| 667 |
+
fuzzy_with_tokens=False):
|
| 668 |
+
"""
|
| 669 |
+
Private method which performs the heavy lifting of parsing, called from
|
| 670 |
+
``parse()``, which passes on its ``kwargs`` to this function.
|
| 671 |
+
|
| 672 |
+
:param timestr:
|
| 673 |
+
The string to parse.
|
| 674 |
+
|
| 675 |
+
:param dayfirst:
|
| 676 |
+
Whether to interpret the first value in an ambiguous 3-integer date
|
| 677 |
+
(e.g. 01/05/09) as the day (``True``) or month (``False``). If
|
| 678 |
+
``yearfirst`` is set to ``True``, this distinguishes between YDM
|
| 679 |
+
and YMD. If set to ``None``, this value is retrieved from the
|
| 680 |
+
current :class:`parserinfo` object (which itself defaults to
|
| 681 |
+
``False``).
|
| 682 |
+
|
| 683 |
+
:param yearfirst:
|
| 684 |
+
Whether to interpret the first value in an ambiguous 3-integer date
|
| 685 |
+
(e.g. 01/05/09) as the year. If ``True``, the first number is taken
|
| 686 |
+
to be the year, otherwise the last number is taken to be the year.
|
| 687 |
+
If this is set to ``None``, the value is retrieved from the current
|
| 688 |
+
:class:`parserinfo` object (which itself defaults to ``False``).
|
| 689 |
+
|
| 690 |
+
:param fuzzy:
|
| 691 |
+
Whether to allow fuzzy parsing, allowing for string like "Today is
|
| 692 |
+
January 1, 2047 at 8:21:00AM".
|
| 693 |
+
|
| 694 |
+
:param fuzzy_with_tokens:
|
| 695 |
+
If ``True``, ``fuzzy`` is automatically set to True, and the parser
|
| 696 |
+
will return a tuple where the first element is the parsed
|
| 697 |
+
:class:`datetime.datetime` datetimestamp and the second element is
|
| 698 |
+
a tuple containing the portions of the string which were ignored:
|
| 699 |
+
|
| 700 |
+
.. doctest::
|
| 701 |
+
|
| 702 |
+
>>> from dateutil.parser import parse
|
| 703 |
+
>>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
|
| 704 |
+
(datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
|
| 705 |
+
|
| 706 |
+
"""
|
| 707 |
+
if fuzzy_with_tokens:
|
| 708 |
+
fuzzy = True
|
| 709 |
+
|
| 710 |
+
info = self.info
|
| 711 |
+
|
| 712 |
+
if dayfirst is None:
|
| 713 |
+
dayfirst = info.dayfirst
|
| 714 |
+
|
| 715 |
+
if yearfirst is None:
|
| 716 |
+
yearfirst = info.yearfirst
|
| 717 |
+
|
| 718 |
+
res = self._result()
|
| 719 |
+
l = _timelex.split(timestr) # Splits the timestr into tokens
|
| 720 |
+
|
| 721 |
+
skipped_idxs = []
|
| 722 |
+
|
| 723 |
+
# year/month/day list
|
| 724 |
+
ymd = _ymd()
|
| 725 |
+
|
| 726 |
+
len_l = len(l)
|
| 727 |
+
i = 0
|
| 728 |
+
try:
|
| 729 |
+
while i < len_l:
|
| 730 |
+
|
| 731 |
+
# Check if it's a number
|
| 732 |
+
value_repr = l[i]
|
| 733 |
+
try:
|
| 734 |
+
value = float(value_repr)
|
| 735 |
+
except ValueError:
|
| 736 |
+
value = None
|
| 737 |
+
|
| 738 |
+
if value is not None:
|
| 739 |
+
# Numeric token
|
| 740 |
+
i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy)
|
| 741 |
+
|
| 742 |
+
# Check weekday
|
| 743 |
+
elif info.weekday(l[i]) is not None:
|
| 744 |
+
value = info.weekday(l[i])
|
| 745 |
+
res.weekday = value
|
| 746 |
+
|
| 747 |
+
# Check month name
|
| 748 |
+
elif info.month(l[i]) is not None:
|
| 749 |
+
value = info.month(l[i])
|
| 750 |
+
ymd.append(value, 'M')
|
| 751 |
+
|
| 752 |
+
if i + 1 < len_l:
|
| 753 |
+
if l[i + 1] in ('-', '/'):
|
| 754 |
+
# Jan-01[-99]
|
| 755 |
+
sep = l[i + 1]
|
| 756 |
+
ymd.append(l[i + 2])
|
| 757 |
+
|
| 758 |
+
if i + 3 < len_l and l[i + 3] == sep:
|
| 759 |
+
# Jan-01-99
|
| 760 |
+
ymd.append(l[i + 4])
|
| 761 |
+
i += 2
|
| 762 |
+
|
| 763 |
+
i += 2
|
| 764 |
+
|
| 765 |
+
elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and
|
| 766 |
+
info.pertain(l[i + 2])):
|
| 767 |
+
# Jan of 01
|
| 768 |
+
# In this case, 01 is clearly year
|
| 769 |
+
if l[i + 4].isdigit():
|
| 770 |
+
# Convert it here to become unambiguous
|
| 771 |
+
value = int(l[i + 4])
|
| 772 |
+
year = str(info.convertyear(value))
|
| 773 |
+
ymd.append(year, 'Y')
|
| 774 |
+
else:
|
| 775 |
+
# Wrong guess
|
| 776 |
+
pass
|
| 777 |
+
# TODO: not hit in tests
|
| 778 |
+
i += 4
|
| 779 |
+
|
| 780 |
+
# Check am/pm
|
| 781 |
+
elif info.ampm(l[i]) is not None:
|
| 782 |
+
value = info.ampm(l[i])
|
| 783 |
+
val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy)
|
| 784 |
+
|
| 785 |
+
if val_is_ampm:
|
| 786 |
+
res.hour = self._adjust_ampm(res.hour, value)
|
| 787 |
+
res.ampm = value
|
| 788 |
+
|
| 789 |
+
elif fuzzy:
|
| 790 |
+
skipped_idxs.append(i)
|
| 791 |
+
|
| 792 |
+
# Check for a timezone name
|
| 793 |
+
elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]):
|
| 794 |
+
res.tzname = l[i]
|
| 795 |
+
res.tzoffset = info.tzoffset(res.tzname)
|
| 796 |
+
|
| 797 |
+
# Check for something like GMT+3, or BRST+3. Notice
|
| 798 |
+
# that it doesn't mean "I am 3 hours after GMT", but
|
| 799 |
+
# "my time +3 is GMT". If found, we reverse the
|
| 800 |
+
# logic so that timezone parsing code will get it
|
| 801 |
+
# right.
|
| 802 |
+
if i + 1 < len_l and l[i + 1] in ('+', '-'):
|
| 803 |
+
l[i + 1] = ('+', '-')[l[i + 1] == '+']
|
| 804 |
+
res.tzoffset = None
|
| 805 |
+
if info.utczone(res.tzname):
|
| 806 |
+
# With something like GMT+3, the timezone
|
| 807 |
+
# is *not* GMT.
|
| 808 |
+
res.tzname = None
|
| 809 |
+
|
| 810 |
+
# Check for a numbered timezone
|
| 811 |
+
elif res.hour is not None and l[i] in ('+', '-'):
|
| 812 |
+
signal = (-1, 1)[l[i] == '+']
|
| 813 |
+
len_li = len(l[i + 1])
|
| 814 |
+
|
| 815 |
+
# TODO: check that l[i + 1] is integer?
|
| 816 |
+
if len_li == 4:
|
| 817 |
+
# -0300
|
| 818 |
+
hour_offset = int(l[i + 1][:2])
|
| 819 |
+
min_offset = int(l[i + 1][2:])
|
| 820 |
+
elif i + 2 < len_l and l[i + 2] == ':':
|
| 821 |
+
# -03:00
|
| 822 |
+
hour_offset = int(l[i + 1])
|
| 823 |
+
min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like?
|
| 824 |
+
i += 2
|
| 825 |
+
elif len_li <= 2:
|
| 826 |
+
# -[0]3
|
| 827 |
+
hour_offset = int(l[i + 1][:2])
|
| 828 |
+
min_offset = 0
|
| 829 |
+
else:
|
| 830 |
+
raise ValueError(timestr)
|
| 831 |
+
|
| 832 |
+
res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60)
|
| 833 |
+
|
| 834 |
+
# Look for a timezone name between parenthesis
|
| 835 |
+
if (i + 5 < len_l and
|
| 836 |
+
info.jump(l[i + 2]) and l[i + 3] == '(' and
|
| 837 |
+
l[i + 5] == ')' and
|
| 838 |
+
3 <= len(l[i + 4]) and
|
| 839 |
+
self._could_be_tzname(res.hour, res.tzname,
|
| 840 |
+
None, l[i + 4])):
|
| 841 |
+
# -0300 (BRST)
|
| 842 |
+
res.tzname = l[i + 4]
|
| 843 |
+
i += 4
|
| 844 |
+
|
| 845 |
+
i += 1
|
| 846 |
+
|
| 847 |
+
# Check jumps
|
| 848 |
+
elif not (info.jump(l[i]) or fuzzy):
|
| 849 |
+
raise ValueError(timestr)
|
| 850 |
+
|
| 851 |
+
else:
|
| 852 |
+
skipped_idxs.append(i)
|
| 853 |
+
i += 1
|
| 854 |
+
|
| 855 |
+
# Process year/month/day
|
| 856 |
+
year, month, day = ymd.resolve_ymd(yearfirst, dayfirst)
|
| 857 |
+
|
| 858 |
+
res.century_specified = ymd.century_specified
|
| 859 |
+
res.year = year
|
| 860 |
+
res.month = month
|
| 861 |
+
res.day = day
|
| 862 |
+
|
| 863 |
+
except (IndexError, ValueError):
|
| 864 |
+
return None, None
|
| 865 |
+
|
| 866 |
+
if not info.validate(res):
|
| 867 |
+
return None, None
|
| 868 |
+
|
| 869 |
+
if fuzzy_with_tokens:
|
| 870 |
+
skipped_tokens = self._recombine_skipped(l, skipped_idxs)
|
| 871 |
+
return res, tuple(skipped_tokens)
|
| 872 |
+
else:
|
| 873 |
+
return res, None
|
| 874 |
+
|
| 875 |
+
def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy):
|
| 876 |
+
# Token is a number
|
| 877 |
+
value_repr = tokens[idx]
|
| 878 |
+
try:
|
| 879 |
+
value = self._to_decimal(value_repr)
|
| 880 |
+
except Exception as e:
|
| 881 |
+
six.raise_from(ValueError('Unknown numeric token'), e)
|
| 882 |
+
|
| 883 |
+
len_li = len(value_repr)
|
| 884 |
+
|
| 885 |
+
len_l = len(tokens)
|
| 886 |
+
|
| 887 |
+
if (len(ymd) == 3 and len_li in (2, 4) and
|
| 888 |
+
res.hour is None and
|
| 889 |
+
(idx + 1 >= len_l or
|
| 890 |
+
(tokens[idx + 1] != ':' and
|
| 891 |
+
info.hms(tokens[idx + 1]) is None))):
|
| 892 |
+
# 19990101T23[59]
|
| 893 |
+
s = tokens[idx]
|
| 894 |
+
res.hour = int(s[:2])
|
| 895 |
+
|
| 896 |
+
if len_li == 4:
|
| 897 |
+
res.minute = int(s[2:])
|
| 898 |
+
|
| 899 |
+
elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6):
|
| 900 |
+
# YYMMDD or HHMMSS[.ss]
|
| 901 |
+
s = tokens[idx]
|
| 902 |
+
|
| 903 |
+
if not ymd and '.' not in tokens[idx]:
|
| 904 |
+
ymd.append(s[:2])
|
| 905 |
+
ymd.append(s[2:4])
|
| 906 |
+
ymd.append(s[4:])
|
| 907 |
+
else:
|
| 908 |
+
# 19990101T235959[.59]
|
| 909 |
+
|
| 910 |
+
# TODO: Check if res attributes already set.
|
| 911 |
+
res.hour = int(s[:2])
|
| 912 |
+
res.minute = int(s[2:4])
|
| 913 |
+
res.second, res.microsecond = self._parsems(s[4:])
|
| 914 |
+
|
| 915 |
+
elif len_li in (8, 12, 14):
|
| 916 |
+
# YYYYMMDD
|
| 917 |
+
s = tokens[idx]
|
| 918 |
+
ymd.append(s[:4], 'Y')
|
| 919 |
+
ymd.append(s[4:6])
|
| 920 |
+
ymd.append(s[6:8])
|
| 921 |
+
|
| 922 |
+
if len_li > 8:
|
| 923 |
+
res.hour = int(s[8:10])
|
| 924 |
+
res.minute = int(s[10:12])
|
| 925 |
+
|
| 926 |
+
if len_li > 12:
|
| 927 |
+
res.second = int(s[12:])
|
| 928 |
+
|
| 929 |
+
elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None:
|
| 930 |
+
# HH[ ]h or MM[ ]m or SS[.ss][ ]s
|
| 931 |
+
hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True)
|
| 932 |
+
(idx, hms) = self._parse_hms(idx, tokens, info, hms_idx)
|
| 933 |
+
if hms is not None:
|
| 934 |
+
# TODO: checking that hour/minute/second are not
|
| 935 |
+
# already set?
|
| 936 |
+
self._assign_hms(res, value_repr, hms)
|
| 937 |
+
|
| 938 |
+
elif idx + 2 < len_l and tokens[idx + 1] == ':':
|
| 939 |
+
# HH:MM[:SS[.ss]]
|
| 940 |
+
res.hour = int(value)
|
| 941 |
+
value = self._to_decimal(tokens[idx + 2]) # TODO: try/except for this?
|
| 942 |
+
(res.minute, res.second) = self._parse_min_sec(value)
|
| 943 |
+
|
| 944 |
+
if idx + 4 < len_l and tokens[idx + 3] == ':':
|
| 945 |
+
res.second, res.microsecond = self._parsems(tokens[idx + 4])
|
| 946 |
+
|
| 947 |
+
idx += 2
|
| 948 |
+
|
| 949 |
+
idx += 2
|
| 950 |
+
|
| 951 |
+
elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'):
|
| 952 |
+
sep = tokens[idx + 1]
|
| 953 |
+
ymd.append(value_repr)
|
| 954 |
+
|
| 955 |
+
if idx + 2 < len_l and not info.jump(tokens[idx + 2]):
|
| 956 |
+
if tokens[idx + 2].isdigit():
|
| 957 |
+
# 01-01[-01]
|
| 958 |
+
ymd.append(tokens[idx + 2])
|
| 959 |
+
else:
|
| 960 |
+
# 01-Jan[-01]
|
| 961 |
+
value = info.month(tokens[idx + 2])
|
| 962 |
+
|
| 963 |
+
if value is not None:
|
| 964 |
+
ymd.append(value, 'M')
|
| 965 |
+
else:
|
| 966 |
+
raise ValueError()
|
| 967 |
+
|
| 968 |
+
if idx + 3 < len_l and tokens[idx + 3] == sep:
|
| 969 |
+
# We have three members
|
| 970 |
+
value = info.month(tokens[idx + 4])
|
| 971 |
+
|
| 972 |
+
if value is not None:
|
| 973 |
+
ymd.append(value, 'M')
|
| 974 |
+
else:
|
| 975 |
+
ymd.append(tokens[idx + 4])
|
| 976 |
+
idx += 2
|
| 977 |
+
|
| 978 |
+
idx += 1
|
| 979 |
+
idx += 1
|
| 980 |
+
|
| 981 |
+
elif idx + 1 >= len_l or info.jump(tokens[idx + 1]):
|
| 982 |
+
if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None:
|
| 983 |
+
# 12 am
|
| 984 |
+
hour = int(value)
|
| 985 |
+
res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2]))
|
| 986 |
+
idx += 1
|
| 987 |
+
else:
|
| 988 |
+
# Year, month or day
|
| 989 |
+
ymd.append(value)
|
| 990 |
+
idx += 1
|
| 991 |
+
|
| 992 |
+
elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24):
|
| 993 |
+
# 12am
|
| 994 |
+
hour = int(value)
|
| 995 |
+
res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1]))
|
| 996 |
+
idx += 1
|
| 997 |
+
|
| 998 |
+
elif ymd.could_be_day(value):
|
| 999 |
+
ymd.append(value)
|
| 1000 |
+
|
| 1001 |
+
elif not fuzzy:
|
| 1002 |
+
raise ValueError()
|
| 1003 |
+
|
| 1004 |
+
return idx
|
| 1005 |
+
|
| 1006 |
+
def _find_hms_idx(self, idx, tokens, info, allow_jump):
|
| 1007 |
+
len_l = len(tokens)
|
| 1008 |
+
|
| 1009 |
+
if idx+1 < len_l and info.hms(tokens[idx+1]) is not None:
|
| 1010 |
+
# There is an "h", "m", or "s" label following this token. We take
|
| 1011 |
+
# assign the upcoming label to the current token.
|
| 1012 |
+
# e.g. the "12" in 12h"
|
| 1013 |
+
hms_idx = idx + 1
|
| 1014 |
+
|
| 1015 |
+
elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and
|
| 1016 |
+
info.hms(tokens[idx+2]) is not None):
|
| 1017 |
+
# There is a space and then an "h", "m", or "s" label.
|
| 1018 |
+
# e.g. the "12" in "12 h"
|
| 1019 |
+
hms_idx = idx + 2
|
| 1020 |
+
|
| 1021 |
+
elif idx > 0 and info.hms(tokens[idx-1]) is not None:
|
| 1022 |
+
# There is a "h", "m", or "s" preceding this token. Since neither
|
| 1023 |
+
# of the previous cases was hit, there is no label following this
|
| 1024 |
+
# token, so we use the previous label.
|
| 1025 |
+
# e.g. the "04" in "12h04"
|
| 1026 |
+
hms_idx = idx-1
|
| 1027 |
+
|
| 1028 |
+
elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and
|
| 1029 |
+
info.hms(tokens[idx-2]) is not None):
|
| 1030 |
+
# If we are looking at the final token, we allow for a
|
| 1031 |
+
# backward-looking check to skip over a space.
|
| 1032 |
+
# TODO: Are we sure this is the right condition here?
|
| 1033 |
+
hms_idx = idx - 2
|
| 1034 |
+
|
| 1035 |
+
else:
|
| 1036 |
+
hms_idx = None
|
| 1037 |
+
|
| 1038 |
+
return hms_idx
|
| 1039 |
+
|
| 1040 |
+
def _assign_hms(self, res, value_repr, hms):
|
| 1041 |
+
# See GH issue #427, fixing float rounding
|
| 1042 |
+
value = self._to_decimal(value_repr)
|
| 1043 |
+
|
| 1044 |
+
if hms == 0:
|
| 1045 |
+
# Hour
|
| 1046 |
+
res.hour = int(value)
|
| 1047 |
+
if value % 1:
|
| 1048 |
+
res.minute = int(60*(value % 1))
|
| 1049 |
+
|
| 1050 |
+
elif hms == 1:
|
| 1051 |
+
(res.minute, res.second) = self._parse_min_sec(value)
|
| 1052 |
+
|
| 1053 |
+
elif hms == 2:
|
| 1054 |
+
(res.second, res.microsecond) = self._parsems(value_repr)
|
| 1055 |
+
|
| 1056 |
+
def _could_be_tzname(self, hour, tzname, tzoffset, token):
|
| 1057 |
+
return (hour is not None and
|
| 1058 |
+
tzname is None and
|
| 1059 |
+
tzoffset is None and
|
| 1060 |
+
len(token) <= 5 and
|
| 1061 |
+
(all(x in string.ascii_uppercase for x in token)
|
| 1062 |
+
or token in self.info.UTCZONE))
|
| 1063 |
+
|
| 1064 |
+
def _ampm_valid(self, hour, ampm, fuzzy):
|
| 1065 |
+
"""
|
| 1066 |
+
For fuzzy parsing, 'a' or 'am' (both valid English words)
|
| 1067 |
+
may erroneously trigger the AM/PM flag. Deal with that
|
| 1068 |
+
here.
|
| 1069 |
+
"""
|
| 1070 |
+
val_is_ampm = True
|
| 1071 |
+
|
| 1072 |
+
# If there's already an AM/PM flag, this one isn't one.
|
| 1073 |
+
if fuzzy and ampm is not None:
|
| 1074 |
+
val_is_ampm = False
|
| 1075 |
+
|
| 1076 |
+
# If AM/PM is found and hour is not, raise a ValueError
|
| 1077 |
+
if hour is None:
|
| 1078 |
+
if fuzzy:
|
| 1079 |
+
val_is_ampm = False
|
| 1080 |
+
else:
|
| 1081 |
+
raise ValueError('No hour specified with AM or PM flag.')
|
| 1082 |
+
elif not 0 <= hour <= 12:
|
| 1083 |
+
# If AM/PM is found, it's a 12 hour clock, so raise
|
| 1084 |
+
# an error for invalid range
|
| 1085 |
+
if fuzzy:
|
| 1086 |
+
val_is_ampm = False
|
| 1087 |
+
else:
|
| 1088 |
+
raise ValueError('Invalid hour specified for 12-hour clock.')
|
| 1089 |
+
|
| 1090 |
+
return val_is_ampm
|
| 1091 |
+
|
| 1092 |
+
def _adjust_ampm(self, hour, ampm):
|
| 1093 |
+
if hour < 12 and ampm == 1:
|
| 1094 |
+
hour += 12
|
| 1095 |
+
elif hour == 12 and ampm == 0:
|
| 1096 |
+
hour = 0
|
| 1097 |
+
return hour
|
| 1098 |
+
|
| 1099 |
+
def _parse_min_sec(self, value):
|
| 1100 |
+
# TODO: Every usage of this function sets res.second to the return
|
| 1101 |
+
# value. Are there any cases where second will be returned as None and
|
| 1102 |
+
# we *don't* want to set res.second = None?
|
| 1103 |
+
minute = int(value)
|
| 1104 |
+
second = None
|
| 1105 |
+
|
| 1106 |
+
sec_remainder = value % 1
|
| 1107 |
+
if sec_remainder:
|
| 1108 |
+
second = int(60 * sec_remainder)
|
| 1109 |
+
return (minute, second)
|
| 1110 |
+
|
| 1111 |
+
def _parse_hms(self, idx, tokens, info, hms_idx):
|
| 1112 |
+
# TODO: Is this going to admit a lot of false-positives for when we
|
| 1113 |
+
# just happen to have digits and "h", "m" or "s" characters in non-date
|
| 1114 |
+
# text? I guess hex hashes won't have that problem, but there's plenty
|
| 1115 |
+
# of random junk out there.
|
| 1116 |
+
if hms_idx is None:
|
| 1117 |
+
hms = None
|
| 1118 |
+
new_idx = idx
|
| 1119 |
+
elif hms_idx > idx:
|
| 1120 |
+
hms = info.hms(tokens[hms_idx])
|
| 1121 |
+
new_idx = hms_idx
|
| 1122 |
+
else:
|
| 1123 |
+
# Looking backwards, increment one.
|
| 1124 |
+
hms = info.hms(tokens[hms_idx]) + 1
|
| 1125 |
+
new_idx = idx
|
| 1126 |
+
|
| 1127 |
+
return (new_idx, hms)
|
| 1128 |
+
|
| 1129 |
+
# ------------------------------------------------------------------
|
| 1130 |
+
# Handling for individual tokens. These are kept as methods instead
|
| 1131 |
+
# of functions for the sake of customizability via subclassing.
|
| 1132 |
+
|
| 1133 |
+
def _parsems(self, value):
|
| 1134 |
+
"""Parse a I[.F] seconds value into (seconds, microseconds)."""
|
| 1135 |
+
if "." not in value:
|
| 1136 |
+
return int(value), 0
|
| 1137 |
+
else:
|
| 1138 |
+
i, f = value.split(".")
|
| 1139 |
+
return int(i), int(f.ljust(6, "0")[:6])
|
| 1140 |
+
|
| 1141 |
+
def _to_decimal(self, val):
|
| 1142 |
+
try:
|
| 1143 |
+
decimal_value = Decimal(val)
|
| 1144 |
+
# See GH 662, edge case, infinite value should not be converted
|
| 1145 |
+
# via `_to_decimal`
|
| 1146 |
+
if not decimal_value.is_finite():
|
| 1147 |
+
raise ValueError("Converted decimal value is infinite or NaN")
|
| 1148 |
+
except Exception as e:
|
| 1149 |
+
msg = "Could not convert %s to decimal" % val
|
| 1150 |
+
six.raise_from(ValueError(msg), e)
|
| 1151 |
+
else:
|
| 1152 |
+
return decimal_value
|
| 1153 |
+
|
| 1154 |
+
# ------------------------------------------------------------------
|
| 1155 |
+
# Post-Parsing construction of datetime output. These are kept as
|
| 1156 |
+
# methods instead of functions for the sake of customizability via
|
| 1157 |
+
# subclassing.
|
| 1158 |
+
|
| 1159 |
+
def _build_tzinfo(self, tzinfos, tzname, tzoffset):
|
| 1160 |
+
if callable(tzinfos):
|
| 1161 |
+
tzdata = tzinfos(tzname, tzoffset)
|
| 1162 |
+
else:
|
| 1163 |
+
tzdata = tzinfos.get(tzname)
|
| 1164 |
+
# handle case where tzinfo is paased an options that returns None
|
| 1165 |
+
# eg tzinfos = {'BRST' : None}
|
| 1166 |
+
if isinstance(tzdata, datetime.tzinfo) or tzdata is None:
|
| 1167 |
+
tzinfo = tzdata
|
| 1168 |
+
elif isinstance(tzdata, text_type):
|
| 1169 |
+
tzinfo = tz.tzstr(tzdata)
|
| 1170 |
+
elif isinstance(tzdata, integer_types):
|
| 1171 |
+
tzinfo = tz.tzoffset(tzname, tzdata)
|
| 1172 |
+
else:
|
| 1173 |
+
raise TypeError("Offset must be tzinfo subclass, tz string, "
|
| 1174 |
+
"or int offset.")
|
| 1175 |
+
return tzinfo
|
| 1176 |
+
|
| 1177 |
+
def _build_tzaware(self, naive, res, tzinfos):
|
| 1178 |
+
if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)):
|
| 1179 |
+
tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset)
|
| 1180 |
+
aware = naive.replace(tzinfo=tzinfo)
|
| 1181 |
+
aware = self._assign_tzname(aware, res.tzname)
|
| 1182 |
+
|
| 1183 |
+
elif res.tzname and res.tzname in time.tzname:
|
| 1184 |
+
aware = naive.replace(tzinfo=tz.tzlocal())
|
| 1185 |
+
|
| 1186 |
+
# Handle ambiguous local datetime
|
| 1187 |
+
aware = self._assign_tzname(aware, res.tzname)
|
| 1188 |
+
|
| 1189 |
+
# This is mostly relevant for winter GMT zones parsed in the UK
|
| 1190 |
+
if (aware.tzname() != res.tzname and
|
| 1191 |
+
res.tzname in self.info.UTCZONE):
|
| 1192 |
+
aware = aware.replace(tzinfo=tz.UTC)
|
| 1193 |
+
|
| 1194 |
+
elif res.tzoffset == 0:
|
| 1195 |
+
aware = naive.replace(tzinfo=tz.UTC)
|
| 1196 |
+
|
| 1197 |
+
elif res.tzoffset:
|
| 1198 |
+
aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset))
|
| 1199 |
+
|
| 1200 |
+
elif not res.tzname and not res.tzoffset:
|
| 1201 |
+
# i.e. no timezone information was found.
|
| 1202 |
+
aware = naive
|
| 1203 |
+
|
| 1204 |
+
elif res.tzname:
|
| 1205 |
+
# tz-like string was parsed but we don't know what to do
|
| 1206 |
+
# with it
|
| 1207 |
+
warnings.warn("tzname {tzname} identified but not understood. "
|
| 1208 |
+
"Pass `tzinfos` argument in order to correctly "
|
| 1209 |
+
"return a timezone-aware datetime. In a future "
|
| 1210 |
+
"version, this will raise an "
|
| 1211 |
+
"exception.".format(tzname=res.tzname),
|
| 1212 |
+
category=UnknownTimezoneWarning)
|
| 1213 |
+
aware = naive
|
| 1214 |
+
|
| 1215 |
+
return aware
|
| 1216 |
+
|
| 1217 |
+
def _build_naive(self, res, default):
|
| 1218 |
+
repl = {}
|
| 1219 |
+
for attr in ("year", "month", "day", "hour",
|
| 1220 |
+
"minute", "second", "microsecond"):
|
| 1221 |
+
value = getattr(res, attr)
|
| 1222 |
+
if value is not None:
|
| 1223 |
+
repl[attr] = value
|
| 1224 |
+
|
| 1225 |
+
if 'day' not in repl:
|
| 1226 |
+
# If the default day exceeds the last day of the month, fall back
|
| 1227 |
+
# to the end of the month.
|
| 1228 |
+
cyear = default.year if res.year is None else res.year
|
| 1229 |
+
cmonth = default.month if res.month is None else res.month
|
| 1230 |
+
cday = default.day if res.day is None else res.day
|
| 1231 |
+
|
| 1232 |
+
if cday > monthrange(cyear, cmonth)[1]:
|
| 1233 |
+
repl['day'] = monthrange(cyear, cmonth)[1]
|
| 1234 |
+
|
| 1235 |
+
naive = default.replace(**repl)
|
| 1236 |
+
|
| 1237 |
+
if res.weekday is not None and not res.day:
|
| 1238 |
+
naive = naive + relativedelta.relativedelta(weekday=res.weekday)
|
| 1239 |
+
|
| 1240 |
+
return naive
|
| 1241 |
+
|
| 1242 |
+
def _assign_tzname(self, dt, tzname):
|
| 1243 |
+
if dt.tzname() != tzname:
|
| 1244 |
+
new_dt = tz.enfold(dt, fold=1)
|
| 1245 |
+
if new_dt.tzname() == tzname:
|
| 1246 |
+
return new_dt
|
| 1247 |
+
|
| 1248 |
+
return dt
|
| 1249 |
+
|
| 1250 |
+
def _recombine_skipped(self, tokens, skipped_idxs):
|
| 1251 |
+
"""
|
| 1252 |
+
>>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"]
|
| 1253 |
+
>>> skipped_idxs = [0, 1, 2, 5]
|
| 1254 |
+
>>> _recombine_skipped(tokens, skipped_idxs)
|
| 1255 |
+
["foo bar", "baz"]
|
| 1256 |
+
"""
|
| 1257 |
+
skipped_tokens = []
|
| 1258 |
+
for i, idx in enumerate(sorted(skipped_idxs)):
|
| 1259 |
+
if i > 0 and idx - 1 == skipped_idxs[i - 1]:
|
| 1260 |
+
skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx]
|
| 1261 |
+
else:
|
| 1262 |
+
skipped_tokens.append(tokens[idx])
|
| 1263 |
+
|
| 1264 |
+
return skipped_tokens
|
| 1265 |
+
|
| 1266 |
+
|
| 1267 |
+
DEFAULTPARSER = parser()
|
| 1268 |
+
|
| 1269 |
+
|
| 1270 |
+
def parse(timestr, parserinfo=None, **kwargs):
|
| 1271 |
+
"""
|
| 1272 |
+
|
| 1273 |
+
Parse a string in one of the supported formats, using the
|
| 1274 |
+
``parserinfo`` parameters.
|
| 1275 |
+
|
| 1276 |
+
:param timestr:
|
| 1277 |
+
A string containing a date/time stamp.
|
| 1278 |
+
|
| 1279 |
+
:param parserinfo:
|
| 1280 |
+
A :class:`parserinfo` object containing parameters for the parser.
|
| 1281 |
+
If ``None``, the default arguments to the :class:`parserinfo`
|
| 1282 |
+
constructor are used.
|
| 1283 |
+
|
| 1284 |
+
The ``**kwargs`` parameter takes the following keyword arguments:
|
| 1285 |
+
|
| 1286 |
+
:param default:
|
| 1287 |
+
The default datetime object, if this is a datetime object and not
|
| 1288 |
+
``None``, elements specified in ``timestr`` replace elements in the
|
| 1289 |
+
default object.
|
| 1290 |
+
|
| 1291 |
+
:param ignoretz:
|
| 1292 |
+
If set ``True``, time zones in parsed strings are ignored and a naive
|
| 1293 |
+
:class:`datetime` object is returned.
|
| 1294 |
+
|
| 1295 |
+
:param tzinfos:
|
| 1296 |
+
Additional time zone names / aliases which may be present in the
|
| 1297 |
+
string. This argument maps time zone names (and optionally offsets
|
| 1298 |
+
from those time zones) to time zones. This parameter can be a
|
| 1299 |
+
dictionary with timezone aliases mapping time zone names to time
|
| 1300 |
+
zones or a function taking two parameters (``tzname`` and
|
| 1301 |
+
``tzoffset``) and returning a time zone.
|
| 1302 |
+
|
| 1303 |
+
The timezones to which the names are mapped can be an integer
|
| 1304 |
+
offset from UTC in seconds or a :class:`tzinfo` object.
|
| 1305 |
+
|
| 1306 |
+
.. doctest::
|
| 1307 |
+
:options: +NORMALIZE_WHITESPACE
|
| 1308 |
+
|
| 1309 |
+
>>> from dateutil.parser import parse
|
| 1310 |
+
>>> from dateutil.tz import gettz
|
| 1311 |
+
>>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")}
|
| 1312 |
+
>>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos)
|
| 1313 |
+
datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200))
|
| 1314 |
+
>>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos)
|
| 1315 |
+
datetime.datetime(2012, 1, 19, 17, 21,
|
| 1316 |
+
tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago'))
|
| 1317 |
+
|
| 1318 |
+
This parameter is ignored if ``ignoretz`` is set.
|
| 1319 |
+
|
| 1320 |
+
:param dayfirst:
|
| 1321 |
+
Whether to interpret the first value in an ambiguous 3-integer date
|
| 1322 |
+
(e.g. 01/05/09) as the day (``True``) or month (``False``). If
|
| 1323 |
+
``yearfirst`` is set to ``True``, this distinguishes between YDM and
|
| 1324 |
+
YMD. If set to ``None``, this value is retrieved from the current
|
| 1325 |
+
:class:`parserinfo` object (which itself defaults to ``False``).
|
| 1326 |
+
|
| 1327 |
+
:param yearfirst:
|
| 1328 |
+
Whether to interpret the first value in an ambiguous 3-integer date
|
| 1329 |
+
(e.g. 01/05/09) as the year. If ``True``, the first number is taken to
|
| 1330 |
+
be the year, otherwise the last number is taken to be the year. If
|
| 1331 |
+
this is set to ``None``, the value is retrieved from the current
|
| 1332 |
+
:class:`parserinfo` object (which itself defaults to ``False``).
|
| 1333 |
+
|
| 1334 |
+
:param fuzzy:
|
| 1335 |
+
Whether to allow fuzzy parsing, allowing for string like "Today is
|
| 1336 |
+
January 1, 2047 at 8:21:00AM".
|
| 1337 |
+
|
| 1338 |
+
:param fuzzy_with_tokens:
|
| 1339 |
+
If ``True``, ``fuzzy`` is automatically set to True, and the parser
|
| 1340 |
+
will return a tuple where the first element is the parsed
|
| 1341 |
+
:class:`datetime.datetime` datetimestamp and the second element is
|
| 1342 |
+
a tuple containing the portions of the string which were ignored:
|
| 1343 |
+
|
| 1344 |
+
.. doctest::
|
| 1345 |
+
|
| 1346 |
+
>>> from dateutil.parser import parse
|
| 1347 |
+
>>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True)
|
| 1348 |
+
(datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at '))
|
| 1349 |
+
|
| 1350 |
+
:return:
|
| 1351 |
+
Returns a :class:`datetime.datetime` object or, if the
|
| 1352 |
+
``fuzzy_with_tokens`` option is ``True``, returns a tuple, the
|
| 1353 |
+
first element being a :class:`datetime.datetime` object, the second
|
| 1354 |
+
a tuple containing the fuzzy tokens.
|
| 1355 |
+
|
| 1356 |
+
:raises ParserError:
|
| 1357 |
+
Raised for invalid or unknown string formats, if the provided
|
| 1358 |
+
:class:`tzinfo` is not in a valid format, or if an invalid date would
|
| 1359 |
+
be created.
|
| 1360 |
+
|
| 1361 |
+
:raises OverflowError:
|
| 1362 |
+
Raised if the parsed date exceeds the largest valid C integer on
|
| 1363 |
+
your system.
|
| 1364 |
+
"""
|
| 1365 |
+
if parserinfo:
|
| 1366 |
+
return parser(parserinfo).parse(timestr, **kwargs)
|
| 1367 |
+
else:
|
| 1368 |
+
return DEFAULTPARSER.parse(timestr, **kwargs)
|
| 1369 |
+
|
| 1370 |
+
|
| 1371 |
+
class _tzparser(object):
|
| 1372 |
+
|
| 1373 |
+
class _result(_resultbase):
|
| 1374 |
+
|
| 1375 |
+
__slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset",
|
| 1376 |
+
"start", "end"]
|
| 1377 |
+
|
| 1378 |
+
class _attr(_resultbase):
|
| 1379 |
+
__slots__ = ["month", "week", "weekday",
|
| 1380 |
+
"yday", "jyday", "day", "time"]
|
| 1381 |
+
|
| 1382 |
+
def __repr__(self):
|
| 1383 |
+
return self._repr("")
|
| 1384 |
+
|
| 1385 |
+
def __init__(self):
|
| 1386 |
+
_resultbase.__init__(self)
|
| 1387 |
+
self.start = self._attr()
|
| 1388 |
+
self.end = self._attr()
|
| 1389 |
+
|
| 1390 |
+
def parse(self, tzstr):
|
| 1391 |
+
res = self._result()
|
| 1392 |
+
l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x]
|
| 1393 |
+
used_idxs = list()
|
| 1394 |
+
try:
|
| 1395 |
+
|
| 1396 |
+
len_l = len(l)
|
| 1397 |
+
|
| 1398 |
+
i = 0
|
| 1399 |
+
while i < len_l:
|
| 1400 |
+
# BRST+3[BRDT[+2]]
|
| 1401 |
+
j = i
|
| 1402 |
+
while j < len_l and not [x for x in l[j]
|
| 1403 |
+
if x in "0123456789:,-+"]:
|
| 1404 |
+
j += 1
|
| 1405 |
+
if j != i:
|
| 1406 |
+
if not res.stdabbr:
|
| 1407 |
+
offattr = "stdoffset"
|
| 1408 |
+
res.stdabbr = "".join(l[i:j])
|
| 1409 |
+
else:
|
| 1410 |
+
offattr = "dstoffset"
|
| 1411 |
+
res.dstabbr = "".join(l[i:j])
|
| 1412 |
+
|
| 1413 |
+
for ii in range(j):
|
| 1414 |
+
used_idxs.append(ii)
|
| 1415 |
+
i = j
|
| 1416 |
+
if (i < len_l and (l[i] in ('+', '-') or l[i][0] in
|
| 1417 |
+
"0123456789")):
|
| 1418 |
+
if l[i] in ('+', '-'):
|
| 1419 |
+
# Yes, that's right. See the TZ variable
|
| 1420 |
+
# documentation.
|
| 1421 |
+
signal = (1, -1)[l[i] == '+']
|
| 1422 |
+
used_idxs.append(i)
|
| 1423 |
+
i += 1
|
| 1424 |
+
else:
|
| 1425 |
+
signal = -1
|
| 1426 |
+
len_li = len(l[i])
|
| 1427 |
+
if len_li == 4:
|
| 1428 |
+
# -0300
|
| 1429 |
+
setattr(res, offattr, (int(l[i][:2]) * 3600 +
|
| 1430 |
+
int(l[i][2:]) * 60) * signal)
|
| 1431 |
+
elif i + 1 < len_l and l[i + 1] == ':':
|
| 1432 |
+
# -03:00
|
| 1433 |
+
setattr(res, offattr,
|
| 1434 |
+
(int(l[i]) * 3600 +
|
| 1435 |
+
int(l[i + 2]) * 60) * signal)
|
| 1436 |
+
used_idxs.append(i)
|
| 1437 |
+
i += 2
|
| 1438 |
+
elif len_li <= 2:
|
| 1439 |
+
# -[0]3
|
| 1440 |
+
setattr(res, offattr,
|
| 1441 |
+
int(l[i][:2]) * 3600 * signal)
|
| 1442 |
+
else:
|
| 1443 |
+
return None
|
| 1444 |
+
used_idxs.append(i)
|
| 1445 |
+
i += 1
|
| 1446 |
+
if res.dstabbr:
|
| 1447 |
+
break
|
| 1448 |
+
else:
|
| 1449 |
+
break
|
| 1450 |
+
|
| 1451 |
+
|
| 1452 |
+
if i < len_l:
|
| 1453 |
+
for j in range(i, len_l):
|
| 1454 |
+
if l[j] == ';':
|
| 1455 |
+
l[j] = ','
|
| 1456 |
+
|
| 1457 |
+
assert l[i] == ','
|
| 1458 |
+
|
| 1459 |
+
i += 1
|
| 1460 |
+
|
| 1461 |
+
if i >= len_l:
|
| 1462 |
+
pass
|
| 1463 |
+
elif (8 <= l.count(',') <= 9 and
|
| 1464 |
+
not [y for x in l[i:] if x != ','
|
| 1465 |
+
for y in x if y not in "0123456789+-"]):
|
| 1466 |
+
# GMT0BST,3,0,30,3600,10,0,26,7200[,3600]
|
| 1467 |
+
for x in (res.start, res.end):
|
| 1468 |
+
x.month = int(l[i])
|
| 1469 |
+
used_idxs.append(i)
|
| 1470 |
+
i += 2
|
| 1471 |
+
if l[i] == '-':
|
| 1472 |
+
value = int(l[i + 1]) * -1
|
| 1473 |
+
used_idxs.append(i)
|
| 1474 |
+
i += 1
|
| 1475 |
+
else:
|
| 1476 |
+
value = int(l[i])
|
| 1477 |
+
used_idxs.append(i)
|
| 1478 |
+
i += 2
|
| 1479 |
+
if value:
|
| 1480 |
+
x.week = value
|
| 1481 |
+
x.weekday = (int(l[i]) - 1) % 7
|
| 1482 |
+
else:
|
| 1483 |
+
x.day = int(l[i])
|
| 1484 |
+
used_idxs.append(i)
|
| 1485 |
+
i += 2
|
| 1486 |
+
x.time = int(l[i])
|
| 1487 |
+
used_idxs.append(i)
|
| 1488 |
+
i += 2
|
| 1489 |
+
if i < len_l:
|
| 1490 |
+
if l[i] in ('-', '+'):
|
| 1491 |
+
signal = (-1, 1)[l[i] == "+"]
|
| 1492 |
+
used_idxs.append(i)
|
| 1493 |
+
i += 1
|
| 1494 |
+
else:
|
| 1495 |
+
signal = 1
|
| 1496 |
+
used_idxs.append(i)
|
| 1497 |
+
res.dstoffset = (res.stdoffset + int(l[i]) * signal)
|
| 1498 |
+
|
| 1499 |
+
# This was a made-up format that is not in normal use
|
| 1500 |
+
warn(('Parsed time zone "%s"' % tzstr) +
|
| 1501 |
+
'is in a non-standard dateutil-specific format, which ' +
|
| 1502 |
+
'is now deprecated; support for parsing this format ' +
|
| 1503 |
+
'will be removed in future versions. It is recommended ' +
|
| 1504 |
+
'that you switch to a standard format like the GNU ' +
|
| 1505 |
+
'TZ variable format.', tz.DeprecatedTzFormatWarning)
|
| 1506 |
+
elif (l.count(',') == 2 and l[i:].count('/') <= 2 and
|
| 1507 |
+
not [y for x in l[i:] if x not in (',', '/', 'J', 'M',
|
| 1508 |
+
'.', '-', ':')
|
| 1509 |
+
for y in x if y not in "0123456789"]):
|
| 1510 |
+
for x in (res.start, res.end):
|
| 1511 |
+
if l[i] == 'J':
|
| 1512 |
+
# non-leap year day (1 based)
|
| 1513 |
+
used_idxs.append(i)
|
| 1514 |
+
i += 1
|
| 1515 |
+
x.jyday = int(l[i])
|
| 1516 |
+
elif l[i] == 'M':
|
| 1517 |
+
# month[-.]week[-.]weekday
|
| 1518 |
+
used_idxs.append(i)
|
| 1519 |
+
i += 1
|
| 1520 |
+
x.month = int(l[i])
|
| 1521 |
+
used_idxs.append(i)
|
| 1522 |
+
i += 1
|
| 1523 |
+
assert l[i] in ('-', '.')
|
| 1524 |
+
used_idxs.append(i)
|
| 1525 |
+
i += 1
|
| 1526 |
+
x.week = int(l[i])
|
| 1527 |
+
if x.week == 5:
|
| 1528 |
+
x.week = -1
|
| 1529 |
+
used_idxs.append(i)
|
| 1530 |
+
i += 1
|
| 1531 |
+
assert l[i] in ('-', '.')
|
| 1532 |
+
used_idxs.append(i)
|
| 1533 |
+
i += 1
|
| 1534 |
+
x.weekday = (int(l[i]) - 1) % 7
|
| 1535 |
+
else:
|
| 1536 |
+
# year day (zero based)
|
| 1537 |
+
x.yday = int(l[i]) + 1
|
| 1538 |
+
|
| 1539 |
+
used_idxs.append(i)
|
| 1540 |
+
i += 1
|
| 1541 |
+
|
| 1542 |
+
if i < len_l and l[i] == '/':
|
| 1543 |
+
used_idxs.append(i)
|
| 1544 |
+
i += 1
|
| 1545 |
+
# start time
|
| 1546 |
+
len_li = len(l[i])
|
| 1547 |
+
if len_li == 4:
|
| 1548 |
+
# -0300
|
| 1549 |
+
x.time = (int(l[i][:2]) * 3600 +
|
| 1550 |
+
int(l[i][2:]) * 60)
|
| 1551 |
+
elif i + 1 < len_l and l[i + 1] == ':':
|
| 1552 |
+
# -03:00
|
| 1553 |
+
x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60
|
| 1554 |
+
used_idxs.append(i)
|
| 1555 |
+
i += 2
|
| 1556 |
+
if i + 1 < len_l and l[i + 1] == ':':
|
| 1557 |
+
used_idxs.append(i)
|
| 1558 |
+
i += 2
|
| 1559 |
+
x.time += int(l[i])
|
| 1560 |
+
elif len_li <= 2:
|
| 1561 |
+
# -[0]3
|
| 1562 |
+
x.time = (int(l[i][:2]) * 3600)
|
| 1563 |
+
else:
|
| 1564 |
+
return None
|
| 1565 |
+
used_idxs.append(i)
|
| 1566 |
+
i += 1
|
| 1567 |
+
|
| 1568 |
+
assert i == len_l or l[i] == ','
|
| 1569 |
+
|
| 1570 |
+
i += 1
|
| 1571 |
+
|
| 1572 |
+
assert i >= len_l
|
| 1573 |
+
|
| 1574 |
+
except (IndexError, ValueError, AssertionError):
|
| 1575 |
+
return None
|
| 1576 |
+
|
| 1577 |
+
unused_idxs = set(range(len_l)).difference(used_idxs)
|
| 1578 |
+
res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"})
|
| 1579 |
+
return res
|
| 1580 |
+
|
| 1581 |
+
|
| 1582 |
+
DEFAULTTZPARSER = _tzparser()
|
| 1583 |
+
|
| 1584 |
+
|
| 1585 |
+
def _parsetz(tzstr):
|
| 1586 |
+
return DEFAULTTZPARSER.parse(tzstr)
|
| 1587 |
+
|
| 1588 |
+
|
| 1589 |
+
class ParserError(ValueError):
|
| 1590 |
+
"""Exception subclass used for any failure to parse a datetime string.
|
| 1591 |
+
|
| 1592 |
+
This is a subclass of :py:exc:`ValueError`, and should be raised any time
|
| 1593 |
+
earlier versions of ``dateutil`` would have raised ``ValueError``.
|
| 1594 |
+
|
| 1595 |
+
.. versionadded:: 2.8.1
|
| 1596 |
+
"""
|
| 1597 |
+
def __str__(self):
|
| 1598 |
+
try:
|
| 1599 |
+
return self.args[0] % self.args[1:]
|
| 1600 |
+
except (TypeError, IndexError):
|
| 1601 |
+
return super(ParserError, self).__str__()
|
| 1602 |
+
|
| 1603 |
+
def __repr__(self):
|
| 1604 |
+
args = ", ".join("'%s'" % arg for arg in self.args)
|
| 1605 |
+
return "%s(%s)" % (self.__class__.__name__, args)
|
| 1606 |
+
|
| 1607 |
+
|
| 1608 |
+
class UnknownTimezoneWarning(RuntimeWarning):
|
| 1609 |
+
"""Raised when the parser finds a timezone it cannot parse into a tzinfo.
|
| 1610 |
+
|
| 1611 |
+
.. versionadded:: 2.7.0
|
| 1612 |
+
"""
|
| 1613 |
+
# vim:ts=4:sw=4:et
|
hug/lib/python3.12/site-packages/dateutil/parser/isoparser.py
ADDED
|
@@ -0,0 +1,416 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
This module offers a parser for ISO-8601 strings
|
| 4 |
+
|
| 5 |
+
It is intended to support all valid date, time and datetime formats per the
|
| 6 |
+
ISO-8601 specification.
|
| 7 |
+
|
| 8 |
+
..versionadded:: 2.7.0
|
| 9 |
+
"""
|
| 10 |
+
from datetime import datetime, timedelta, time, date
|
| 11 |
+
import calendar
|
| 12 |
+
from dateutil import tz
|
| 13 |
+
|
| 14 |
+
from functools import wraps
|
| 15 |
+
|
| 16 |
+
import re
|
| 17 |
+
import six
|
| 18 |
+
|
| 19 |
+
__all__ = ["isoparse", "isoparser"]
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def _takes_ascii(f):
|
| 23 |
+
@wraps(f)
|
| 24 |
+
def func(self, str_in, *args, **kwargs):
|
| 25 |
+
# If it's a stream, read the whole thing
|
| 26 |
+
str_in = getattr(str_in, 'read', lambda: str_in)()
|
| 27 |
+
|
| 28 |
+
# If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII
|
| 29 |
+
if isinstance(str_in, six.text_type):
|
| 30 |
+
# ASCII is the same in UTF-8
|
| 31 |
+
try:
|
| 32 |
+
str_in = str_in.encode('ascii')
|
| 33 |
+
except UnicodeEncodeError as e:
|
| 34 |
+
msg = 'ISO-8601 strings should contain only ASCII characters'
|
| 35 |
+
six.raise_from(ValueError(msg), e)
|
| 36 |
+
|
| 37 |
+
return f(self, str_in, *args, **kwargs)
|
| 38 |
+
|
| 39 |
+
return func
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
class isoparser(object):
|
| 43 |
+
def __init__(self, sep=None):
|
| 44 |
+
"""
|
| 45 |
+
:param sep:
|
| 46 |
+
A single character that separates date and time portions. If
|
| 47 |
+
``None``, the parser will accept any single character.
|
| 48 |
+
For strict ISO-8601 adherence, pass ``'T'``.
|
| 49 |
+
"""
|
| 50 |
+
if sep is not None:
|
| 51 |
+
if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'):
|
| 52 |
+
raise ValueError('Separator must be a single, non-numeric ' +
|
| 53 |
+
'ASCII character')
|
| 54 |
+
|
| 55 |
+
sep = sep.encode('ascii')
|
| 56 |
+
|
| 57 |
+
self._sep = sep
|
| 58 |
+
|
| 59 |
+
@_takes_ascii
|
| 60 |
+
def isoparse(self, dt_str):
|
| 61 |
+
"""
|
| 62 |
+
Parse an ISO-8601 datetime string into a :class:`datetime.datetime`.
|
| 63 |
+
|
| 64 |
+
An ISO-8601 datetime string consists of a date portion, followed
|
| 65 |
+
optionally by a time portion - the date and time portions are separated
|
| 66 |
+
by a single character separator, which is ``T`` in the official
|
| 67 |
+
standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be
|
| 68 |
+
combined with a time portion.
|
| 69 |
+
|
| 70 |
+
Supported date formats are:
|
| 71 |
+
|
| 72 |
+
Common:
|
| 73 |
+
|
| 74 |
+
- ``YYYY``
|
| 75 |
+
- ``YYYY-MM``
|
| 76 |
+
- ``YYYY-MM-DD`` or ``YYYYMMDD``
|
| 77 |
+
|
| 78 |
+
Uncommon:
|
| 79 |
+
|
| 80 |
+
- ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0)
|
| 81 |
+
- ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day
|
| 82 |
+
|
| 83 |
+
The ISO week and day numbering follows the same logic as
|
| 84 |
+
:func:`datetime.date.isocalendar`.
|
| 85 |
+
|
| 86 |
+
Supported time formats are:
|
| 87 |
+
|
| 88 |
+
- ``hh``
|
| 89 |
+
- ``hh:mm`` or ``hhmm``
|
| 90 |
+
- ``hh:mm:ss`` or ``hhmmss``
|
| 91 |
+
- ``hh:mm:ss.ssssss`` (Up to 6 sub-second digits)
|
| 92 |
+
|
| 93 |
+
Midnight is a special case for `hh`, as the standard supports both
|
| 94 |
+
00:00 and 24:00 as a representation. The decimal separator can be
|
| 95 |
+
either a dot or a comma.
|
| 96 |
+
|
| 97 |
+
|
| 98 |
+
.. caution::
|
| 99 |
+
|
| 100 |
+
Support for fractional components other than seconds is part of the
|
| 101 |
+
ISO-8601 standard, but is not currently implemented in this parser.
|
| 102 |
+
|
| 103 |
+
Supported time zone offset formats are:
|
| 104 |
+
|
| 105 |
+
- `Z` (UTC)
|
| 106 |
+
- `±HH:MM`
|
| 107 |
+
- `±HHMM`
|
| 108 |
+
- `±HH`
|
| 109 |
+
|
| 110 |
+
Offsets will be represented as :class:`dateutil.tz.tzoffset` objects,
|
| 111 |
+
with the exception of UTC, which will be represented as
|
| 112 |
+
:class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such
|
| 113 |
+
as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`.
|
| 114 |
+
|
| 115 |
+
:param dt_str:
|
| 116 |
+
A string or stream containing only an ISO-8601 datetime string
|
| 117 |
+
|
| 118 |
+
:return:
|
| 119 |
+
Returns a :class:`datetime.datetime` representing the string.
|
| 120 |
+
Unspecified components default to their lowest value.
|
| 121 |
+
|
| 122 |
+
.. warning::
|
| 123 |
+
|
| 124 |
+
As of version 2.7.0, the strictness of the parser should not be
|
| 125 |
+
considered a stable part of the contract. Any valid ISO-8601 string
|
| 126 |
+
that parses correctly with the default settings will continue to
|
| 127 |
+
parse correctly in future versions, but invalid strings that
|
| 128 |
+
currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not
|
| 129 |
+
guaranteed to continue failing in future versions if they encode
|
| 130 |
+
a valid date.
|
| 131 |
+
|
| 132 |
+
.. versionadded:: 2.7.0
|
| 133 |
+
"""
|
| 134 |
+
components, pos = self._parse_isodate(dt_str)
|
| 135 |
+
|
| 136 |
+
if len(dt_str) > pos:
|
| 137 |
+
if self._sep is None or dt_str[pos:pos + 1] == self._sep:
|
| 138 |
+
components += self._parse_isotime(dt_str[pos + 1:])
|
| 139 |
+
else:
|
| 140 |
+
raise ValueError('String contains unknown ISO components')
|
| 141 |
+
|
| 142 |
+
if len(components) > 3 and components[3] == 24:
|
| 143 |
+
components[3] = 0
|
| 144 |
+
return datetime(*components) + timedelta(days=1)
|
| 145 |
+
|
| 146 |
+
return datetime(*components)
|
| 147 |
+
|
| 148 |
+
@_takes_ascii
|
| 149 |
+
def parse_isodate(self, datestr):
|
| 150 |
+
"""
|
| 151 |
+
Parse the date portion of an ISO string.
|
| 152 |
+
|
| 153 |
+
:param datestr:
|
| 154 |
+
The string portion of an ISO string, without a separator
|
| 155 |
+
|
| 156 |
+
:return:
|
| 157 |
+
Returns a :class:`datetime.date` object
|
| 158 |
+
"""
|
| 159 |
+
components, pos = self._parse_isodate(datestr)
|
| 160 |
+
if pos < len(datestr):
|
| 161 |
+
raise ValueError('String contains unknown ISO ' +
|
| 162 |
+
'components: {!r}'.format(datestr.decode('ascii')))
|
| 163 |
+
return date(*components)
|
| 164 |
+
|
| 165 |
+
@_takes_ascii
|
| 166 |
+
def parse_isotime(self, timestr):
|
| 167 |
+
"""
|
| 168 |
+
Parse the time portion of an ISO string.
|
| 169 |
+
|
| 170 |
+
:param timestr:
|
| 171 |
+
The time portion of an ISO string, without a separator
|
| 172 |
+
|
| 173 |
+
:return:
|
| 174 |
+
Returns a :class:`datetime.time` object
|
| 175 |
+
"""
|
| 176 |
+
components = self._parse_isotime(timestr)
|
| 177 |
+
if components[0] == 24:
|
| 178 |
+
components[0] = 0
|
| 179 |
+
return time(*components)
|
| 180 |
+
|
| 181 |
+
@_takes_ascii
|
| 182 |
+
def parse_tzstr(self, tzstr, zero_as_utc=True):
|
| 183 |
+
"""
|
| 184 |
+
Parse a valid ISO time zone string.
|
| 185 |
+
|
| 186 |
+
See :func:`isoparser.isoparse` for details on supported formats.
|
| 187 |
+
|
| 188 |
+
:param tzstr:
|
| 189 |
+
A string representing an ISO time zone offset
|
| 190 |
+
|
| 191 |
+
:param zero_as_utc:
|
| 192 |
+
Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones
|
| 193 |
+
|
| 194 |
+
:return:
|
| 195 |
+
Returns :class:`dateutil.tz.tzoffset` for offsets and
|
| 196 |
+
:class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is
|
| 197 |
+
specified) offsets equivalent to UTC.
|
| 198 |
+
"""
|
| 199 |
+
return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc)
|
| 200 |
+
|
| 201 |
+
# Constants
|
| 202 |
+
_DATE_SEP = b'-'
|
| 203 |
+
_TIME_SEP = b':'
|
| 204 |
+
_FRACTION_REGEX = re.compile(b'[\\.,]([0-9]+)')
|
| 205 |
+
|
| 206 |
+
def _parse_isodate(self, dt_str):
|
| 207 |
+
try:
|
| 208 |
+
return self._parse_isodate_common(dt_str)
|
| 209 |
+
except ValueError:
|
| 210 |
+
return self._parse_isodate_uncommon(dt_str)
|
| 211 |
+
|
| 212 |
+
def _parse_isodate_common(self, dt_str):
|
| 213 |
+
len_str = len(dt_str)
|
| 214 |
+
components = [1, 1, 1]
|
| 215 |
+
|
| 216 |
+
if len_str < 4:
|
| 217 |
+
raise ValueError('ISO string too short')
|
| 218 |
+
|
| 219 |
+
# Year
|
| 220 |
+
components[0] = int(dt_str[0:4])
|
| 221 |
+
pos = 4
|
| 222 |
+
if pos >= len_str:
|
| 223 |
+
return components, pos
|
| 224 |
+
|
| 225 |
+
has_sep = dt_str[pos:pos + 1] == self._DATE_SEP
|
| 226 |
+
if has_sep:
|
| 227 |
+
pos += 1
|
| 228 |
+
|
| 229 |
+
# Month
|
| 230 |
+
if len_str - pos < 2:
|
| 231 |
+
raise ValueError('Invalid common month')
|
| 232 |
+
|
| 233 |
+
components[1] = int(dt_str[pos:pos + 2])
|
| 234 |
+
pos += 2
|
| 235 |
+
|
| 236 |
+
if pos >= len_str:
|
| 237 |
+
if has_sep:
|
| 238 |
+
return components, pos
|
| 239 |
+
else:
|
| 240 |
+
raise ValueError('Invalid ISO format')
|
| 241 |
+
|
| 242 |
+
if has_sep:
|
| 243 |
+
if dt_str[pos:pos + 1] != self._DATE_SEP:
|
| 244 |
+
raise ValueError('Invalid separator in ISO string')
|
| 245 |
+
pos += 1
|
| 246 |
+
|
| 247 |
+
# Day
|
| 248 |
+
if len_str - pos < 2:
|
| 249 |
+
raise ValueError('Invalid common day')
|
| 250 |
+
components[2] = int(dt_str[pos:pos + 2])
|
| 251 |
+
return components, pos + 2
|
| 252 |
+
|
| 253 |
+
def _parse_isodate_uncommon(self, dt_str):
|
| 254 |
+
if len(dt_str) < 4:
|
| 255 |
+
raise ValueError('ISO string too short')
|
| 256 |
+
|
| 257 |
+
# All ISO formats start with the year
|
| 258 |
+
year = int(dt_str[0:4])
|
| 259 |
+
|
| 260 |
+
has_sep = dt_str[4:5] == self._DATE_SEP
|
| 261 |
+
|
| 262 |
+
pos = 4 + has_sep # Skip '-' if it's there
|
| 263 |
+
if dt_str[pos:pos + 1] == b'W':
|
| 264 |
+
# YYYY-?Www-?D?
|
| 265 |
+
pos += 1
|
| 266 |
+
weekno = int(dt_str[pos:pos + 2])
|
| 267 |
+
pos += 2
|
| 268 |
+
|
| 269 |
+
dayno = 1
|
| 270 |
+
if len(dt_str) > pos:
|
| 271 |
+
if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep:
|
| 272 |
+
raise ValueError('Inconsistent use of dash separator')
|
| 273 |
+
|
| 274 |
+
pos += has_sep
|
| 275 |
+
|
| 276 |
+
dayno = int(dt_str[pos:pos + 1])
|
| 277 |
+
pos += 1
|
| 278 |
+
|
| 279 |
+
base_date = self._calculate_weekdate(year, weekno, dayno)
|
| 280 |
+
else:
|
| 281 |
+
# YYYYDDD or YYYY-DDD
|
| 282 |
+
if len(dt_str) - pos < 3:
|
| 283 |
+
raise ValueError('Invalid ordinal day')
|
| 284 |
+
|
| 285 |
+
ordinal_day = int(dt_str[pos:pos + 3])
|
| 286 |
+
pos += 3
|
| 287 |
+
|
| 288 |
+
if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)):
|
| 289 |
+
raise ValueError('Invalid ordinal day' +
|
| 290 |
+
' {} for year {}'.format(ordinal_day, year))
|
| 291 |
+
|
| 292 |
+
base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1)
|
| 293 |
+
|
| 294 |
+
components = [base_date.year, base_date.month, base_date.day]
|
| 295 |
+
return components, pos
|
| 296 |
+
|
| 297 |
+
def _calculate_weekdate(self, year, week, day):
|
| 298 |
+
"""
|
| 299 |
+
Calculate the day of corresponding to the ISO year-week-day calendar.
|
| 300 |
+
|
| 301 |
+
This function is effectively the inverse of
|
| 302 |
+
:func:`datetime.date.isocalendar`.
|
| 303 |
+
|
| 304 |
+
:param year:
|
| 305 |
+
The year in the ISO calendar
|
| 306 |
+
|
| 307 |
+
:param week:
|
| 308 |
+
The week in the ISO calendar - range is [1, 53]
|
| 309 |
+
|
| 310 |
+
:param day:
|
| 311 |
+
The day in the ISO calendar - range is [1 (MON), 7 (SUN)]
|
| 312 |
+
|
| 313 |
+
:return:
|
| 314 |
+
Returns a :class:`datetime.date`
|
| 315 |
+
"""
|
| 316 |
+
if not 0 < week < 54:
|
| 317 |
+
raise ValueError('Invalid week: {}'.format(week))
|
| 318 |
+
|
| 319 |
+
if not 0 < day < 8: # Range is 1-7
|
| 320 |
+
raise ValueError('Invalid weekday: {}'.format(day))
|
| 321 |
+
|
| 322 |
+
# Get week 1 for the specific year:
|
| 323 |
+
jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it
|
| 324 |
+
week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1)
|
| 325 |
+
|
| 326 |
+
# Now add the specific number of weeks and days to get what we want
|
| 327 |
+
week_offset = (week - 1) * 7 + (day - 1)
|
| 328 |
+
return week_1 + timedelta(days=week_offset)
|
| 329 |
+
|
| 330 |
+
def _parse_isotime(self, timestr):
|
| 331 |
+
len_str = len(timestr)
|
| 332 |
+
components = [0, 0, 0, 0, None]
|
| 333 |
+
pos = 0
|
| 334 |
+
comp = -1
|
| 335 |
+
|
| 336 |
+
if len_str < 2:
|
| 337 |
+
raise ValueError('ISO time too short')
|
| 338 |
+
|
| 339 |
+
has_sep = False
|
| 340 |
+
|
| 341 |
+
while pos < len_str and comp < 5:
|
| 342 |
+
comp += 1
|
| 343 |
+
|
| 344 |
+
if timestr[pos:pos + 1] in b'-+Zz':
|
| 345 |
+
# Detect time zone boundary
|
| 346 |
+
components[-1] = self._parse_tzstr(timestr[pos:])
|
| 347 |
+
pos = len_str
|
| 348 |
+
break
|
| 349 |
+
|
| 350 |
+
if comp == 1 and timestr[pos:pos+1] == self._TIME_SEP:
|
| 351 |
+
has_sep = True
|
| 352 |
+
pos += 1
|
| 353 |
+
elif comp == 2 and has_sep:
|
| 354 |
+
if timestr[pos:pos+1] != self._TIME_SEP:
|
| 355 |
+
raise ValueError('Inconsistent use of colon separator')
|
| 356 |
+
pos += 1
|
| 357 |
+
|
| 358 |
+
if comp < 3:
|
| 359 |
+
# Hour, minute, second
|
| 360 |
+
components[comp] = int(timestr[pos:pos + 2])
|
| 361 |
+
pos += 2
|
| 362 |
+
|
| 363 |
+
if comp == 3:
|
| 364 |
+
# Fraction of a second
|
| 365 |
+
frac = self._FRACTION_REGEX.match(timestr[pos:])
|
| 366 |
+
if not frac:
|
| 367 |
+
continue
|
| 368 |
+
|
| 369 |
+
us_str = frac.group(1)[:6] # Truncate to microseconds
|
| 370 |
+
components[comp] = int(us_str) * 10**(6 - len(us_str))
|
| 371 |
+
pos += len(frac.group())
|
| 372 |
+
|
| 373 |
+
if pos < len_str:
|
| 374 |
+
raise ValueError('Unused components in ISO string')
|
| 375 |
+
|
| 376 |
+
if components[0] == 24:
|
| 377 |
+
# Standard supports 00:00 and 24:00 as representations of midnight
|
| 378 |
+
if any(component != 0 for component in components[1:4]):
|
| 379 |
+
raise ValueError('Hour may only be 24 at 24:00:00.000')
|
| 380 |
+
|
| 381 |
+
return components
|
| 382 |
+
|
| 383 |
+
def _parse_tzstr(self, tzstr, zero_as_utc=True):
|
| 384 |
+
if tzstr == b'Z' or tzstr == b'z':
|
| 385 |
+
return tz.UTC
|
| 386 |
+
|
| 387 |
+
if len(tzstr) not in {3, 5, 6}:
|
| 388 |
+
raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters')
|
| 389 |
+
|
| 390 |
+
if tzstr[0:1] == b'-':
|
| 391 |
+
mult = -1
|
| 392 |
+
elif tzstr[0:1] == b'+':
|
| 393 |
+
mult = 1
|
| 394 |
+
else:
|
| 395 |
+
raise ValueError('Time zone offset requires sign')
|
| 396 |
+
|
| 397 |
+
hours = int(tzstr[1:3])
|
| 398 |
+
if len(tzstr) == 3:
|
| 399 |
+
minutes = 0
|
| 400 |
+
else:
|
| 401 |
+
minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):])
|
| 402 |
+
|
| 403 |
+
if zero_as_utc and hours == 0 and minutes == 0:
|
| 404 |
+
return tz.UTC
|
| 405 |
+
else:
|
| 406 |
+
if minutes > 59:
|
| 407 |
+
raise ValueError('Invalid minutes in time zone offset')
|
| 408 |
+
|
| 409 |
+
if hours > 23:
|
| 410 |
+
raise ValueError('Invalid hours in time zone offset')
|
| 411 |
+
|
| 412 |
+
return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60)
|
| 413 |
+
|
| 414 |
+
|
| 415 |
+
DEFAULT_ISOPARSER = isoparser()
|
| 416 |
+
isoparse = DEFAULT_ISOPARSER.isoparse
|
hug/lib/python3.12/site-packages/dateutil/relativedelta.py
ADDED
|
@@ -0,0 +1,596 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import datetime
|
| 3 |
+
import calendar
|
| 4 |
+
|
| 5 |
+
import operator
|
| 6 |
+
from math import copysign
|
| 7 |
+
|
| 8 |
+
from six import integer_types
|
| 9 |
+
from warnings import warn
|
| 10 |
+
|
| 11 |
+
from ._common import weekday
|
| 12 |
+
|
| 13 |
+
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
| 14 |
+
|
| 15 |
+
__all__ = ["relativedelta", "MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
class relativedelta(object):
|
| 19 |
+
"""
|
| 20 |
+
The relativedelta type is designed to be applied to an existing datetime and
|
| 21 |
+
can replace specific components of that datetime, or represents an interval
|
| 22 |
+
of time.
|
| 23 |
+
|
| 24 |
+
It is based on the specification of the excellent work done by M.-A. Lemburg
|
| 25 |
+
in his
|
| 26 |
+
`mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension.
|
| 27 |
+
However, notice that this type does *NOT* implement the same algorithm as
|
| 28 |
+
his work. Do *NOT* expect it to behave like mx.DateTime's counterpart.
|
| 29 |
+
|
| 30 |
+
There are two different ways to build a relativedelta instance. The
|
| 31 |
+
first one is passing it two date/datetime classes::
|
| 32 |
+
|
| 33 |
+
relativedelta(datetime1, datetime2)
|
| 34 |
+
|
| 35 |
+
The second one is passing it any number of the following keyword arguments::
|
| 36 |
+
|
| 37 |
+
relativedelta(arg1=x,arg2=y,arg3=z...)
|
| 38 |
+
|
| 39 |
+
year, month, day, hour, minute, second, microsecond:
|
| 40 |
+
Absolute information (argument is singular); adding or subtracting a
|
| 41 |
+
relativedelta with absolute information does not perform an arithmetic
|
| 42 |
+
operation, but rather REPLACES the corresponding value in the
|
| 43 |
+
original datetime with the value(s) in relativedelta.
|
| 44 |
+
|
| 45 |
+
years, months, weeks, days, hours, minutes, seconds, microseconds:
|
| 46 |
+
Relative information, may be negative (argument is plural); adding
|
| 47 |
+
or subtracting a relativedelta with relative information performs
|
| 48 |
+
the corresponding arithmetic operation on the original datetime value
|
| 49 |
+
with the information in the relativedelta.
|
| 50 |
+
|
| 51 |
+
weekday:
|
| 52 |
+
One of the weekday instances (MO, TU, etc) available in the
|
| 53 |
+
relativedelta module. These instances may receive a parameter N,
|
| 54 |
+
specifying the Nth weekday, which could be positive or negative
|
| 55 |
+
(like MO(+1) or MO(-2)). Not specifying it is the same as specifying
|
| 56 |
+
+1. You can also use an integer, where 0=MO. This argument is always
|
| 57 |
+
relative e.g. if the calculated date is already Monday, using MO(1)
|
| 58 |
+
or MO(-1) won't change the day. To effectively make it absolute, use
|
| 59 |
+
it in combination with the day argument (e.g. day=1, MO(1) for first
|
| 60 |
+
Monday of the month).
|
| 61 |
+
|
| 62 |
+
leapdays:
|
| 63 |
+
Will add given days to the date found, if year is a leap
|
| 64 |
+
year, and the date found is post 28 of february.
|
| 65 |
+
|
| 66 |
+
yearday, nlyearday:
|
| 67 |
+
Set the yearday or the non-leap year day (jump leap days).
|
| 68 |
+
These are converted to day/month/leapdays information.
|
| 69 |
+
|
| 70 |
+
There are relative and absolute forms of the keyword
|
| 71 |
+
arguments. The plural is relative, and the singular is
|
| 72 |
+
absolute. For each argument in the order below, the absolute form
|
| 73 |
+
is applied first (by setting each attribute to that value) and
|
| 74 |
+
then the relative form (by adding the value to the attribute).
|
| 75 |
+
|
| 76 |
+
The order of attributes considered when this relativedelta is
|
| 77 |
+
added to a datetime is:
|
| 78 |
+
|
| 79 |
+
1. Year
|
| 80 |
+
2. Month
|
| 81 |
+
3. Day
|
| 82 |
+
4. Hours
|
| 83 |
+
5. Minutes
|
| 84 |
+
6. Seconds
|
| 85 |
+
7. Microseconds
|
| 86 |
+
|
| 87 |
+
Finally, weekday is applied, using the rule described above.
|
| 88 |
+
|
| 89 |
+
For example
|
| 90 |
+
|
| 91 |
+
>>> from datetime import datetime
|
| 92 |
+
>>> from dateutil.relativedelta import relativedelta, MO
|
| 93 |
+
>>> dt = datetime(2018, 4, 9, 13, 37, 0)
|
| 94 |
+
>>> delta = relativedelta(hours=25, day=1, weekday=MO(1))
|
| 95 |
+
>>> dt + delta
|
| 96 |
+
datetime.datetime(2018, 4, 2, 14, 37)
|
| 97 |
+
|
| 98 |
+
First, the day is set to 1 (the first of the month), then 25 hours
|
| 99 |
+
are added, to get to the 2nd day and 14th hour, finally the
|
| 100 |
+
weekday is applied, but since the 2nd is already a Monday there is
|
| 101 |
+
no effect.
|
| 102 |
+
|
| 103 |
+
"""
|
| 104 |
+
|
| 105 |
+
def __init__(self, dt1=None, dt2=None,
|
| 106 |
+
years=0, months=0, days=0, leapdays=0, weeks=0,
|
| 107 |
+
hours=0, minutes=0, seconds=0, microseconds=0,
|
| 108 |
+
year=None, month=None, day=None, weekday=None,
|
| 109 |
+
yearday=None, nlyearday=None,
|
| 110 |
+
hour=None, minute=None, second=None, microsecond=None):
|
| 111 |
+
|
| 112 |
+
if dt1 and dt2:
|
| 113 |
+
# datetime is a subclass of date. So both must be date
|
| 114 |
+
if not (isinstance(dt1, datetime.date) and
|
| 115 |
+
isinstance(dt2, datetime.date)):
|
| 116 |
+
raise TypeError("relativedelta only diffs datetime/date")
|
| 117 |
+
|
| 118 |
+
# We allow two dates, or two datetimes, so we coerce them to be
|
| 119 |
+
# of the same type
|
| 120 |
+
if (isinstance(dt1, datetime.datetime) !=
|
| 121 |
+
isinstance(dt2, datetime.datetime)):
|
| 122 |
+
if not isinstance(dt1, datetime.datetime):
|
| 123 |
+
dt1 = datetime.datetime.fromordinal(dt1.toordinal())
|
| 124 |
+
elif not isinstance(dt2, datetime.datetime):
|
| 125 |
+
dt2 = datetime.datetime.fromordinal(dt2.toordinal())
|
| 126 |
+
|
| 127 |
+
self.years = 0
|
| 128 |
+
self.months = 0
|
| 129 |
+
self.days = 0
|
| 130 |
+
self.leapdays = 0
|
| 131 |
+
self.hours = 0
|
| 132 |
+
self.minutes = 0
|
| 133 |
+
self.seconds = 0
|
| 134 |
+
self.microseconds = 0
|
| 135 |
+
self.year = None
|
| 136 |
+
self.month = None
|
| 137 |
+
self.day = None
|
| 138 |
+
self.weekday = None
|
| 139 |
+
self.hour = None
|
| 140 |
+
self.minute = None
|
| 141 |
+
self.second = None
|
| 142 |
+
self.microsecond = None
|
| 143 |
+
self._has_time = 0
|
| 144 |
+
|
| 145 |
+
# Get year / month delta between the two
|
| 146 |
+
months = (dt1.year - dt2.year) * 12 + (dt1.month - dt2.month)
|
| 147 |
+
self._set_months(months)
|
| 148 |
+
|
| 149 |
+
# Remove the year/month delta so the timedelta is just well-defined
|
| 150 |
+
# time units (seconds, days and microseconds)
|
| 151 |
+
dtm = self.__radd__(dt2)
|
| 152 |
+
|
| 153 |
+
# If we've overshot our target, make an adjustment
|
| 154 |
+
if dt1 < dt2:
|
| 155 |
+
compare = operator.gt
|
| 156 |
+
increment = 1
|
| 157 |
+
else:
|
| 158 |
+
compare = operator.lt
|
| 159 |
+
increment = -1
|
| 160 |
+
|
| 161 |
+
while compare(dt1, dtm):
|
| 162 |
+
months += increment
|
| 163 |
+
self._set_months(months)
|
| 164 |
+
dtm = self.__radd__(dt2)
|
| 165 |
+
|
| 166 |
+
# Get the timedelta between the "months-adjusted" date and dt1
|
| 167 |
+
delta = dt1 - dtm
|
| 168 |
+
self.seconds = delta.seconds + delta.days * 86400
|
| 169 |
+
self.microseconds = delta.microseconds
|
| 170 |
+
else:
|
| 171 |
+
# Check for non-integer values in integer-only quantities
|
| 172 |
+
if any(x is not None and x != int(x) for x in (years, months)):
|
| 173 |
+
raise ValueError("Non-integer years and months are "
|
| 174 |
+
"ambiguous and not currently supported.")
|
| 175 |
+
|
| 176 |
+
# Relative information
|
| 177 |
+
self.years = int(years)
|
| 178 |
+
self.months = int(months)
|
| 179 |
+
self.days = days + weeks * 7
|
| 180 |
+
self.leapdays = leapdays
|
| 181 |
+
self.hours = hours
|
| 182 |
+
self.minutes = minutes
|
| 183 |
+
self.seconds = seconds
|
| 184 |
+
self.microseconds = microseconds
|
| 185 |
+
|
| 186 |
+
# Absolute information
|
| 187 |
+
self.year = year
|
| 188 |
+
self.month = month
|
| 189 |
+
self.day = day
|
| 190 |
+
self.hour = hour
|
| 191 |
+
self.minute = minute
|
| 192 |
+
self.second = second
|
| 193 |
+
self.microsecond = microsecond
|
| 194 |
+
|
| 195 |
+
if any(x is not None and int(x) != x
|
| 196 |
+
for x in (year, month, day, hour,
|
| 197 |
+
minute, second, microsecond)):
|
| 198 |
+
# For now we'll deprecate floats - later it'll be an error.
|
| 199 |
+
warn("Non-integer value passed as absolute information. " +
|
| 200 |
+
"This is not a well-defined condition and will raise " +
|
| 201 |
+
"errors in future versions.", DeprecationWarning)
|
| 202 |
+
|
| 203 |
+
if isinstance(weekday, integer_types):
|
| 204 |
+
self.weekday = weekdays[weekday]
|
| 205 |
+
else:
|
| 206 |
+
self.weekday = weekday
|
| 207 |
+
|
| 208 |
+
yday = 0
|
| 209 |
+
if nlyearday:
|
| 210 |
+
yday = nlyearday
|
| 211 |
+
elif yearday:
|
| 212 |
+
yday = yearday
|
| 213 |
+
if yearday > 59:
|
| 214 |
+
self.leapdays = -1
|
| 215 |
+
if yday:
|
| 216 |
+
ydayidx = [31, 59, 90, 120, 151, 181, 212,
|
| 217 |
+
243, 273, 304, 334, 366]
|
| 218 |
+
for idx, ydays in enumerate(ydayidx):
|
| 219 |
+
if yday <= ydays:
|
| 220 |
+
self.month = idx+1
|
| 221 |
+
if idx == 0:
|
| 222 |
+
self.day = yday
|
| 223 |
+
else:
|
| 224 |
+
self.day = yday-ydayidx[idx-1]
|
| 225 |
+
break
|
| 226 |
+
else:
|
| 227 |
+
raise ValueError("invalid year day (%d)" % yday)
|
| 228 |
+
|
| 229 |
+
self._fix()
|
| 230 |
+
|
| 231 |
+
def _fix(self):
|
| 232 |
+
if abs(self.microseconds) > 999999:
|
| 233 |
+
s = _sign(self.microseconds)
|
| 234 |
+
div, mod = divmod(self.microseconds * s, 1000000)
|
| 235 |
+
self.microseconds = mod * s
|
| 236 |
+
self.seconds += div * s
|
| 237 |
+
if abs(self.seconds) > 59:
|
| 238 |
+
s = _sign(self.seconds)
|
| 239 |
+
div, mod = divmod(self.seconds * s, 60)
|
| 240 |
+
self.seconds = mod * s
|
| 241 |
+
self.minutes += div * s
|
| 242 |
+
if abs(self.minutes) > 59:
|
| 243 |
+
s = _sign(self.minutes)
|
| 244 |
+
div, mod = divmod(self.minutes * s, 60)
|
| 245 |
+
self.minutes = mod * s
|
| 246 |
+
self.hours += div * s
|
| 247 |
+
if abs(self.hours) > 23:
|
| 248 |
+
s = _sign(self.hours)
|
| 249 |
+
div, mod = divmod(self.hours * s, 24)
|
| 250 |
+
self.hours = mod * s
|
| 251 |
+
self.days += div * s
|
| 252 |
+
if abs(self.months) > 11:
|
| 253 |
+
s = _sign(self.months)
|
| 254 |
+
div, mod = divmod(self.months * s, 12)
|
| 255 |
+
self.months = mod * s
|
| 256 |
+
self.years += div * s
|
| 257 |
+
if (self.hours or self.minutes or self.seconds or self.microseconds
|
| 258 |
+
or self.hour is not None or self.minute is not None or
|
| 259 |
+
self.second is not None or self.microsecond is not None):
|
| 260 |
+
self._has_time = 1
|
| 261 |
+
else:
|
| 262 |
+
self._has_time = 0
|
| 263 |
+
|
| 264 |
+
@property
|
| 265 |
+
def weeks(self):
|
| 266 |
+
return int(self.days / 7.0)
|
| 267 |
+
|
| 268 |
+
@weeks.setter
|
| 269 |
+
def weeks(self, value):
|
| 270 |
+
self.days = self.days - (self.weeks * 7) + value * 7
|
| 271 |
+
|
| 272 |
+
def _set_months(self, months):
|
| 273 |
+
self.months = months
|
| 274 |
+
if abs(self.months) > 11:
|
| 275 |
+
s = _sign(self.months)
|
| 276 |
+
div, mod = divmod(self.months * s, 12)
|
| 277 |
+
self.months = mod * s
|
| 278 |
+
self.years = div * s
|
| 279 |
+
else:
|
| 280 |
+
self.years = 0
|
| 281 |
+
|
| 282 |
+
def normalized(self):
|
| 283 |
+
"""
|
| 284 |
+
Return a version of this object represented entirely using integer
|
| 285 |
+
values for the relative attributes.
|
| 286 |
+
|
| 287 |
+
>>> relativedelta(days=1.5, hours=2).normalized()
|
| 288 |
+
relativedelta(days=+1, hours=+14)
|
| 289 |
+
|
| 290 |
+
:return:
|
| 291 |
+
Returns a :class:`dateutil.relativedelta.relativedelta` object.
|
| 292 |
+
"""
|
| 293 |
+
# Cascade remainders down (rounding each to roughly nearest microsecond)
|
| 294 |
+
days = int(self.days)
|
| 295 |
+
|
| 296 |
+
hours_f = round(self.hours + 24 * (self.days - days), 11)
|
| 297 |
+
hours = int(hours_f)
|
| 298 |
+
|
| 299 |
+
minutes_f = round(self.minutes + 60 * (hours_f - hours), 10)
|
| 300 |
+
minutes = int(minutes_f)
|
| 301 |
+
|
| 302 |
+
seconds_f = round(self.seconds + 60 * (minutes_f - minutes), 8)
|
| 303 |
+
seconds = int(seconds_f)
|
| 304 |
+
|
| 305 |
+
microseconds = round(self.microseconds + 1e6 * (seconds_f - seconds))
|
| 306 |
+
|
| 307 |
+
# Constructor carries overflow back up with call to _fix()
|
| 308 |
+
return self.__class__(years=self.years, months=self.months,
|
| 309 |
+
days=days, hours=hours, minutes=minutes,
|
| 310 |
+
seconds=seconds, microseconds=microseconds,
|
| 311 |
+
leapdays=self.leapdays, year=self.year,
|
| 312 |
+
month=self.month, day=self.day,
|
| 313 |
+
weekday=self.weekday, hour=self.hour,
|
| 314 |
+
minute=self.minute, second=self.second,
|
| 315 |
+
microsecond=self.microsecond)
|
| 316 |
+
|
| 317 |
+
def __add__(self, other):
|
| 318 |
+
if isinstance(other, relativedelta):
|
| 319 |
+
return self.__class__(years=other.years + self.years,
|
| 320 |
+
months=other.months + self.months,
|
| 321 |
+
days=other.days + self.days,
|
| 322 |
+
hours=other.hours + self.hours,
|
| 323 |
+
minutes=other.minutes + self.minutes,
|
| 324 |
+
seconds=other.seconds + self.seconds,
|
| 325 |
+
microseconds=(other.microseconds +
|
| 326 |
+
self.microseconds),
|
| 327 |
+
leapdays=other.leapdays or self.leapdays,
|
| 328 |
+
year=(other.year if other.year is not None
|
| 329 |
+
else self.year),
|
| 330 |
+
month=(other.month if other.month is not None
|
| 331 |
+
else self.month),
|
| 332 |
+
day=(other.day if other.day is not None
|
| 333 |
+
else self.day),
|
| 334 |
+
weekday=(other.weekday if other.weekday is not None
|
| 335 |
+
else self.weekday),
|
| 336 |
+
hour=(other.hour if other.hour is not None
|
| 337 |
+
else self.hour),
|
| 338 |
+
minute=(other.minute if other.minute is not None
|
| 339 |
+
else self.minute),
|
| 340 |
+
second=(other.second if other.second is not None
|
| 341 |
+
else self.second),
|
| 342 |
+
microsecond=(other.microsecond if other.microsecond
|
| 343 |
+
is not None else
|
| 344 |
+
self.microsecond))
|
| 345 |
+
if isinstance(other, datetime.timedelta):
|
| 346 |
+
return self.__class__(years=self.years,
|
| 347 |
+
months=self.months,
|
| 348 |
+
days=self.days + other.days,
|
| 349 |
+
hours=self.hours,
|
| 350 |
+
minutes=self.minutes,
|
| 351 |
+
seconds=self.seconds + other.seconds,
|
| 352 |
+
microseconds=self.microseconds + other.microseconds,
|
| 353 |
+
leapdays=self.leapdays,
|
| 354 |
+
year=self.year,
|
| 355 |
+
month=self.month,
|
| 356 |
+
day=self.day,
|
| 357 |
+
weekday=self.weekday,
|
| 358 |
+
hour=self.hour,
|
| 359 |
+
minute=self.minute,
|
| 360 |
+
second=self.second,
|
| 361 |
+
microsecond=self.microsecond)
|
| 362 |
+
if not isinstance(other, datetime.date):
|
| 363 |
+
return NotImplemented
|
| 364 |
+
elif self._has_time and not isinstance(other, datetime.datetime):
|
| 365 |
+
other = datetime.datetime.fromordinal(other.toordinal())
|
| 366 |
+
year = (self.year or other.year)+self.years
|
| 367 |
+
month = self.month or other.month
|
| 368 |
+
if self.months:
|
| 369 |
+
assert 1 <= abs(self.months) <= 12
|
| 370 |
+
month += self.months
|
| 371 |
+
if month > 12:
|
| 372 |
+
year += 1
|
| 373 |
+
month -= 12
|
| 374 |
+
elif month < 1:
|
| 375 |
+
year -= 1
|
| 376 |
+
month += 12
|
| 377 |
+
day = min(calendar.monthrange(year, month)[1],
|
| 378 |
+
self.day or other.day)
|
| 379 |
+
repl = {"year": year, "month": month, "day": day}
|
| 380 |
+
for attr in ["hour", "minute", "second", "microsecond"]:
|
| 381 |
+
value = getattr(self, attr)
|
| 382 |
+
if value is not None:
|
| 383 |
+
repl[attr] = value
|
| 384 |
+
days = self.days
|
| 385 |
+
if self.leapdays and month > 2 and calendar.isleap(year):
|
| 386 |
+
days += self.leapdays
|
| 387 |
+
ret = (other.replace(**repl)
|
| 388 |
+
+ datetime.timedelta(days=days,
|
| 389 |
+
hours=self.hours,
|
| 390 |
+
minutes=self.minutes,
|
| 391 |
+
seconds=self.seconds,
|
| 392 |
+
microseconds=self.microseconds))
|
| 393 |
+
if self.weekday:
|
| 394 |
+
weekday, nth = self.weekday.weekday, self.weekday.n or 1
|
| 395 |
+
jumpdays = (abs(nth) - 1) * 7
|
| 396 |
+
if nth > 0:
|
| 397 |
+
jumpdays += (7 - ret.weekday() + weekday) % 7
|
| 398 |
+
else:
|
| 399 |
+
jumpdays += (ret.weekday() - weekday) % 7
|
| 400 |
+
jumpdays *= -1
|
| 401 |
+
ret += datetime.timedelta(days=jumpdays)
|
| 402 |
+
return ret
|
| 403 |
+
|
| 404 |
+
def __radd__(self, other):
|
| 405 |
+
return self.__add__(other)
|
| 406 |
+
|
| 407 |
+
def __rsub__(self, other):
|
| 408 |
+
return self.__neg__().__radd__(other)
|
| 409 |
+
|
| 410 |
+
def __sub__(self, other):
|
| 411 |
+
if not isinstance(other, relativedelta):
|
| 412 |
+
return NotImplemented # In case the other object defines __rsub__
|
| 413 |
+
return self.__class__(years=self.years - other.years,
|
| 414 |
+
months=self.months - other.months,
|
| 415 |
+
days=self.days - other.days,
|
| 416 |
+
hours=self.hours - other.hours,
|
| 417 |
+
minutes=self.minutes - other.minutes,
|
| 418 |
+
seconds=self.seconds - other.seconds,
|
| 419 |
+
microseconds=self.microseconds - other.microseconds,
|
| 420 |
+
leapdays=self.leapdays or other.leapdays,
|
| 421 |
+
year=(self.year if self.year is not None
|
| 422 |
+
else other.year),
|
| 423 |
+
month=(self.month if self.month is not None else
|
| 424 |
+
other.month),
|
| 425 |
+
day=(self.day if self.day is not None else
|
| 426 |
+
other.day),
|
| 427 |
+
weekday=(self.weekday if self.weekday is not None else
|
| 428 |
+
other.weekday),
|
| 429 |
+
hour=(self.hour if self.hour is not None else
|
| 430 |
+
other.hour),
|
| 431 |
+
minute=(self.minute if self.minute is not None else
|
| 432 |
+
other.minute),
|
| 433 |
+
second=(self.second if self.second is not None else
|
| 434 |
+
other.second),
|
| 435 |
+
microsecond=(self.microsecond if self.microsecond
|
| 436 |
+
is not None else
|
| 437 |
+
other.microsecond))
|
| 438 |
+
|
| 439 |
+
def __abs__(self):
|
| 440 |
+
return self.__class__(years=abs(self.years),
|
| 441 |
+
months=abs(self.months),
|
| 442 |
+
days=abs(self.days),
|
| 443 |
+
hours=abs(self.hours),
|
| 444 |
+
minutes=abs(self.minutes),
|
| 445 |
+
seconds=abs(self.seconds),
|
| 446 |
+
microseconds=abs(self.microseconds),
|
| 447 |
+
leapdays=self.leapdays,
|
| 448 |
+
year=self.year,
|
| 449 |
+
month=self.month,
|
| 450 |
+
day=self.day,
|
| 451 |
+
weekday=self.weekday,
|
| 452 |
+
hour=self.hour,
|
| 453 |
+
minute=self.minute,
|
| 454 |
+
second=self.second,
|
| 455 |
+
microsecond=self.microsecond)
|
| 456 |
+
|
| 457 |
+
def __neg__(self):
|
| 458 |
+
return self.__class__(years=-self.years,
|
| 459 |
+
months=-self.months,
|
| 460 |
+
days=-self.days,
|
| 461 |
+
hours=-self.hours,
|
| 462 |
+
minutes=-self.minutes,
|
| 463 |
+
seconds=-self.seconds,
|
| 464 |
+
microseconds=-self.microseconds,
|
| 465 |
+
leapdays=self.leapdays,
|
| 466 |
+
year=self.year,
|
| 467 |
+
month=self.month,
|
| 468 |
+
day=self.day,
|
| 469 |
+
weekday=self.weekday,
|
| 470 |
+
hour=self.hour,
|
| 471 |
+
minute=self.minute,
|
| 472 |
+
second=self.second,
|
| 473 |
+
microsecond=self.microsecond)
|
| 474 |
+
|
| 475 |
+
def __bool__(self):
|
| 476 |
+
return not (not self.years and
|
| 477 |
+
not self.months and
|
| 478 |
+
not self.days and
|
| 479 |
+
not self.hours and
|
| 480 |
+
not self.minutes and
|
| 481 |
+
not self.seconds and
|
| 482 |
+
not self.microseconds and
|
| 483 |
+
not self.leapdays and
|
| 484 |
+
self.year is None and
|
| 485 |
+
self.month is None and
|
| 486 |
+
self.day is None and
|
| 487 |
+
self.weekday is None and
|
| 488 |
+
self.hour is None and
|
| 489 |
+
self.minute is None and
|
| 490 |
+
self.second is None and
|
| 491 |
+
self.microsecond is None)
|
| 492 |
+
# Compatibility with Python 2.x
|
| 493 |
+
__nonzero__ = __bool__
|
| 494 |
+
|
| 495 |
+
def __mul__(self, other):
|
| 496 |
+
try:
|
| 497 |
+
f = float(other)
|
| 498 |
+
except TypeError:
|
| 499 |
+
return NotImplemented
|
| 500 |
+
|
| 501 |
+
return self.__class__(years=int(self.years * f),
|
| 502 |
+
months=int(self.months * f),
|
| 503 |
+
days=int(self.days * f),
|
| 504 |
+
hours=int(self.hours * f),
|
| 505 |
+
minutes=int(self.minutes * f),
|
| 506 |
+
seconds=int(self.seconds * f),
|
| 507 |
+
microseconds=int(self.microseconds * f),
|
| 508 |
+
leapdays=self.leapdays,
|
| 509 |
+
year=self.year,
|
| 510 |
+
month=self.month,
|
| 511 |
+
day=self.day,
|
| 512 |
+
weekday=self.weekday,
|
| 513 |
+
hour=self.hour,
|
| 514 |
+
minute=self.minute,
|
| 515 |
+
second=self.second,
|
| 516 |
+
microsecond=self.microsecond)
|
| 517 |
+
|
| 518 |
+
__rmul__ = __mul__
|
| 519 |
+
|
| 520 |
+
def __eq__(self, other):
|
| 521 |
+
if not isinstance(other, relativedelta):
|
| 522 |
+
return NotImplemented
|
| 523 |
+
if self.weekday or other.weekday:
|
| 524 |
+
if not self.weekday or not other.weekday:
|
| 525 |
+
return False
|
| 526 |
+
if self.weekday.weekday != other.weekday.weekday:
|
| 527 |
+
return False
|
| 528 |
+
n1, n2 = self.weekday.n, other.weekday.n
|
| 529 |
+
if n1 != n2 and not ((not n1 or n1 == 1) and (not n2 or n2 == 1)):
|
| 530 |
+
return False
|
| 531 |
+
return (self.years == other.years and
|
| 532 |
+
self.months == other.months and
|
| 533 |
+
self.days == other.days and
|
| 534 |
+
self.hours == other.hours and
|
| 535 |
+
self.minutes == other.minutes and
|
| 536 |
+
self.seconds == other.seconds and
|
| 537 |
+
self.microseconds == other.microseconds and
|
| 538 |
+
self.leapdays == other.leapdays and
|
| 539 |
+
self.year == other.year and
|
| 540 |
+
self.month == other.month and
|
| 541 |
+
self.day == other.day and
|
| 542 |
+
self.hour == other.hour and
|
| 543 |
+
self.minute == other.minute and
|
| 544 |
+
self.second == other.second and
|
| 545 |
+
self.microsecond == other.microsecond)
|
| 546 |
+
|
| 547 |
+
def __hash__(self):
|
| 548 |
+
return hash((
|
| 549 |
+
self.weekday,
|
| 550 |
+
self.years,
|
| 551 |
+
self.months,
|
| 552 |
+
self.days,
|
| 553 |
+
self.hours,
|
| 554 |
+
self.minutes,
|
| 555 |
+
self.seconds,
|
| 556 |
+
self.microseconds,
|
| 557 |
+
self.leapdays,
|
| 558 |
+
self.year,
|
| 559 |
+
self.month,
|
| 560 |
+
self.day,
|
| 561 |
+
self.hour,
|
| 562 |
+
self.minute,
|
| 563 |
+
self.second,
|
| 564 |
+
self.microsecond,
|
| 565 |
+
))
|
| 566 |
+
|
| 567 |
+
def __div__(self, other):
|
| 568 |
+
try:
|
| 569 |
+
reciprocal = 1 / float(other)
|
| 570 |
+
except TypeError:
|
| 571 |
+
return NotImplemented
|
| 572 |
+
|
| 573 |
+
return self.__mul__(reciprocal)
|
| 574 |
+
|
| 575 |
+
__truediv__ = __div__
|
| 576 |
+
|
| 577 |
+
def __repr__(self):
|
| 578 |
+
l = []
|
| 579 |
+
for attr in ["years", "months", "days", "leapdays",
|
| 580 |
+
"hours", "minutes", "seconds", "microseconds"]:
|
| 581 |
+
value = getattr(self, attr)
|
| 582 |
+
if value:
|
| 583 |
+
l.append("{attr}={value:+g}".format(attr=attr, value=value))
|
| 584 |
+
for attr in ["year", "month", "day", "weekday",
|
| 585 |
+
"hour", "minute", "second", "microsecond"]:
|
| 586 |
+
value = getattr(self, attr)
|
| 587 |
+
if value is not None:
|
| 588 |
+
l.append("{attr}={value}".format(attr=attr, value=repr(value)))
|
| 589 |
+
return "{classname}({attrs})".format(classname=self.__class__.__name__,
|
| 590 |
+
attrs=", ".join(l))
|
| 591 |
+
|
| 592 |
+
|
| 593 |
+
def _sign(x):
|
| 594 |
+
return int(copysign(1, x))
|
| 595 |
+
|
| 596 |
+
# vim:ts=4:sw=4:et
|
hug/lib/python3.12/site-packages/dateutil/rrule.py
ADDED
|
@@ -0,0 +1,1764 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
The rrule module offers a small, complete, and very fast, implementation of
|
| 4 |
+
the recurrence rules documented in the
|
| 5 |
+
`iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_,
|
| 6 |
+
including support for caching of results.
|
| 7 |
+
"""
|
| 8 |
+
import calendar
|
| 9 |
+
import datetime
|
| 10 |
+
import heapq
|
| 11 |
+
import itertools
|
| 12 |
+
import re
|
| 13 |
+
import sys
|
| 14 |
+
from functools import wraps
|
| 15 |
+
# For warning about deprecation of until and count
|
| 16 |
+
from warnings import warn
|
| 17 |
+
|
| 18 |
+
from six import advance_iterator, integer_types
|
| 19 |
+
|
| 20 |
+
from six.moves import _thread, range
|
| 21 |
+
|
| 22 |
+
from ._common import weekday as weekdaybase
|
| 23 |
+
|
| 24 |
+
try:
|
| 25 |
+
from math import gcd
|
| 26 |
+
except ImportError:
|
| 27 |
+
from fractions import gcd
|
| 28 |
+
|
| 29 |
+
__all__ = ["rrule", "rruleset", "rrulestr",
|
| 30 |
+
"YEARLY", "MONTHLY", "WEEKLY", "DAILY",
|
| 31 |
+
"HOURLY", "MINUTELY", "SECONDLY",
|
| 32 |
+
"MO", "TU", "WE", "TH", "FR", "SA", "SU"]
|
| 33 |
+
|
| 34 |
+
# Every mask is 7 days longer to handle cross-year weekly periods.
|
| 35 |
+
M366MASK = tuple([1]*31+[2]*29+[3]*31+[4]*30+[5]*31+[6]*30 +
|
| 36 |
+
[7]*31+[8]*31+[9]*30+[10]*31+[11]*30+[12]*31+[1]*7)
|
| 37 |
+
M365MASK = list(M366MASK)
|
| 38 |
+
M29, M30, M31 = list(range(1, 30)), list(range(1, 31)), list(range(1, 32))
|
| 39 |
+
MDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
|
| 40 |
+
MDAY365MASK = list(MDAY366MASK)
|
| 41 |
+
M29, M30, M31 = list(range(-29, 0)), list(range(-30, 0)), list(range(-31, 0))
|
| 42 |
+
NMDAY366MASK = tuple(M31+M29+M31+M30+M31+M30+M31+M31+M30+M31+M30+M31+M31[:7])
|
| 43 |
+
NMDAY365MASK = list(NMDAY366MASK)
|
| 44 |
+
M366RANGE = (0, 31, 60, 91, 121, 152, 182, 213, 244, 274, 305, 335, 366)
|
| 45 |
+
M365RANGE = (0, 31, 59, 90, 120, 151, 181, 212, 243, 273, 304, 334, 365)
|
| 46 |
+
WDAYMASK = [0, 1, 2, 3, 4, 5, 6]*55
|
| 47 |
+
del M29, M30, M31, M365MASK[59], MDAY365MASK[59], NMDAY365MASK[31]
|
| 48 |
+
MDAY365MASK = tuple(MDAY365MASK)
|
| 49 |
+
M365MASK = tuple(M365MASK)
|
| 50 |
+
|
| 51 |
+
FREQNAMES = ['YEARLY', 'MONTHLY', 'WEEKLY', 'DAILY', 'HOURLY', 'MINUTELY', 'SECONDLY']
|
| 52 |
+
|
| 53 |
+
(YEARLY,
|
| 54 |
+
MONTHLY,
|
| 55 |
+
WEEKLY,
|
| 56 |
+
DAILY,
|
| 57 |
+
HOURLY,
|
| 58 |
+
MINUTELY,
|
| 59 |
+
SECONDLY) = list(range(7))
|
| 60 |
+
|
| 61 |
+
# Imported on demand.
|
| 62 |
+
easter = None
|
| 63 |
+
parser = None
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
class weekday(weekdaybase):
|
| 67 |
+
"""
|
| 68 |
+
This version of weekday does not allow n = 0.
|
| 69 |
+
"""
|
| 70 |
+
def __init__(self, wkday, n=None):
|
| 71 |
+
if n == 0:
|
| 72 |
+
raise ValueError("Can't create weekday with n==0")
|
| 73 |
+
|
| 74 |
+
super(weekday, self).__init__(wkday, n)
|
| 75 |
+
|
| 76 |
+
|
| 77 |
+
MO, TU, WE, TH, FR, SA, SU = weekdays = tuple(weekday(x) for x in range(7))
|
| 78 |
+
|
| 79 |
+
|
| 80 |
+
def _invalidates_cache(f):
|
| 81 |
+
"""
|
| 82 |
+
Decorator for rruleset methods which may invalidate the
|
| 83 |
+
cached length.
|
| 84 |
+
"""
|
| 85 |
+
@wraps(f)
|
| 86 |
+
def inner_func(self, *args, **kwargs):
|
| 87 |
+
rv = f(self, *args, **kwargs)
|
| 88 |
+
self._invalidate_cache()
|
| 89 |
+
return rv
|
| 90 |
+
|
| 91 |
+
return inner_func
|
| 92 |
+
|
| 93 |
+
|
| 94 |
+
class rrulebase(object):
|
| 95 |
+
def __init__(self, cache=False):
|
| 96 |
+
if cache:
|
| 97 |
+
self._cache = []
|
| 98 |
+
self._cache_lock = _thread.allocate_lock()
|
| 99 |
+
self._invalidate_cache()
|
| 100 |
+
else:
|
| 101 |
+
self._cache = None
|
| 102 |
+
self._cache_complete = False
|
| 103 |
+
self._len = None
|
| 104 |
+
|
| 105 |
+
def __reduce_ex__(self, protocol):
|
| 106 |
+
rv = super(rrulebase, self).__reduce_ex__(protocol)
|
| 107 |
+
state, slots = rv[2], None
|
| 108 |
+
if isinstance(state, tuple):
|
| 109 |
+
state, slots = state
|
| 110 |
+
if self._cache is not None:
|
| 111 |
+
state = dict(state)
|
| 112 |
+
state.pop("_cache_lock", None)
|
| 113 |
+
state.pop("_cache_gen", None)
|
| 114 |
+
state["_cache"] = []
|
| 115 |
+
if slots is not None:
|
| 116 |
+
state = (state, slots)
|
| 117 |
+
return rv[:2] + (state,) + rv[3:]
|
| 118 |
+
|
| 119 |
+
def __setstate__(self, state):
|
| 120 |
+
if isinstance(state, tuple):
|
| 121 |
+
state, slots = state
|
| 122 |
+
for name, value in slots.items():
|
| 123 |
+
setattr(self, name, value)
|
| 124 |
+
self.__dict__.update(state)
|
| 125 |
+
if self._cache is not None:
|
| 126 |
+
self._cache_lock = _thread.allocate_lock()
|
| 127 |
+
self._invalidate_cache()
|
| 128 |
+
|
| 129 |
+
def __iter__(self):
|
| 130 |
+
if self._cache_complete:
|
| 131 |
+
return iter(self._cache)
|
| 132 |
+
elif self._cache is None:
|
| 133 |
+
return self._iter()
|
| 134 |
+
else:
|
| 135 |
+
return self._iter_cached()
|
| 136 |
+
|
| 137 |
+
def _invalidate_cache(self):
|
| 138 |
+
if self._cache is not None:
|
| 139 |
+
self._cache = []
|
| 140 |
+
self._cache_complete = False
|
| 141 |
+
self._cache_gen = self._iter()
|
| 142 |
+
|
| 143 |
+
if self._cache_lock.locked():
|
| 144 |
+
self._cache_lock.release()
|
| 145 |
+
|
| 146 |
+
self._len = None
|
| 147 |
+
|
| 148 |
+
def _iter_cached(self):
|
| 149 |
+
i = 0
|
| 150 |
+
gen = self._cache_gen
|
| 151 |
+
cache = self._cache
|
| 152 |
+
acquire = self._cache_lock.acquire
|
| 153 |
+
release = self._cache_lock.release
|
| 154 |
+
while gen:
|
| 155 |
+
if i == len(cache):
|
| 156 |
+
acquire()
|
| 157 |
+
if self._cache_complete:
|
| 158 |
+
break
|
| 159 |
+
try:
|
| 160 |
+
for j in range(10):
|
| 161 |
+
cache.append(advance_iterator(gen))
|
| 162 |
+
except StopIteration:
|
| 163 |
+
self._cache_gen = gen = None
|
| 164 |
+
self._cache_complete = True
|
| 165 |
+
break
|
| 166 |
+
release()
|
| 167 |
+
yield cache[i]
|
| 168 |
+
i += 1
|
| 169 |
+
while i < self._len:
|
| 170 |
+
yield cache[i]
|
| 171 |
+
i += 1
|
| 172 |
+
|
| 173 |
+
def __getitem__(self, item):
|
| 174 |
+
if self._cache_complete:
|
| 175 |
+
return self._cache[item]
|
| 176 |
+
elif isinstance(item, slice):
|
| 177 |
+
if item.step and item.step < 0:
|
| 178 |
+
return list(iter(self))[item]
|
| 179 |
+
else:
|
| 180 |
+
return list(itertools.islice(self,
|
| 181 |
+
item.start or 0,
|
| 182 |
+
item.stop or sys.maxsize,
|
| 183 |
+
item.step or 1))
|
| 184 |
+
elif item >= 0:
|
| 185 |
+
gen = iter(self)
|
| 186 |
+
try:
|
| 187 |
+
for i in range(item+1):
|
| 188 |
+
res = advance_iterator(gen)
|
| 189 |
+
except StopIteration:
|
| 190 |
+
raise IndexError
|
| 191 |
+
return res
|
| 192 |
+
else:
|
| 193 |
+
return list(iter(self))[item]
|
| 194 |
+
|
| 195 |
+
def __contains__(self, item):
|
| 196 |
+
if self._cache_complete:
|
| 197 |
+
return item in self._cache
|
| 198 |
+
else:
|
| 199 |
+
for i in self:
|
| 200 |
+
if i == item:
|
| 201 |
+
return True
|
| 202 |
+
elif i > item:
|
| 203 |
+
return False
|
| 204 |
+
return False
|
| 205 |
+
|
| 206 |
+
# __len__() introduces a large performance penalty.
|
| 207 |
+
def count(self):
|
| 208 |
+
""" Returns the number of recurrences in this set. It will have go
|
| 209 |
+
through the whole recurrence, if this hasn't been done before. """
|
| 210 |
+
if self._len is None:
|
| 211 |
+
for x in self:
|
| 212 |
+
pass
|
| 213 |
+
return self._len
|
| 214 |
+
|
| 215 |
+
def before(self, dt, inc=False):
|
| 216 |
+
""" Returns the last recurrence before the given datetime instance. The
|
| 217 |
+
inc keyword defines what happens if dt is an occurrence. With
|
| 218 |
+
inc=True, if dt itself is an occurrence, it will be returned. """
|
| 219 |
+
if self._cache_complete:
|
| 220 |
+
gen = self._cache
|
| 221 |
+
else:
|
| 222 |
+
gen = self
|
| 223 |
+
last = None
|
| 224 |
+
if inc:
|
| 225 |
+
for i in gen:
|
| 226 |
+
if i > dt:
|
| 227 |
+
break
|
| 228 |
+
last = i
|
| 229 |
+
else:
|
| 230 |
+
for i in gen:
|
| 231 |
+
if i >= dt:
|
| 232 |
+
break
|
| 233 |
+
last = i
|
| 234 |
+
return last
|
| 235 |
+
|
| 236 |
+
def after(self, dt, inc=False):
|
| 237 |
+
""" Returns the first recurrence after the given datetime instance. The
|
| 238 |
+
inc keyword defines what happens if dt is an occurrence. With
|
| 239 |
+
inc=True, if dt itself is an occurrence, it will be returned. """
|
| 240 |
+
if self._cache_complete:
|
| 241 |
+
gen = self._cache
|
| 242 |
+
else:
|
| 243 |
+
gen = self
|
| 244 |
+
if inc:
|
| 245 |
+
for i in gen:
|
| 246 |
+
if i >= dt:
|
| 247 |
+
return i
|
| 248 |
+
else:
|
| 249 |
+
for i in gen:
|
| 250 |
+
if i > dt:
|
| 251 |
+
return i
|
| 252 |
+
return None
|
| 253 |
+
|
| 254 |
+
def xafter(self, dt, count=None, inc=False):
|
| 255 |
+
"""
|
| 256 |
+
Generator which yields up to `count` recurrences after the given
|
| 257 |
+
datetime instance, equivalent to `after`.
|
| 258 |
+
|
| 259 |
+
:param dt:
|
| 260 |
+
The datetime at which to start generating recurrences.
|
| 261 |
+
|
| 262 |
+
:param count:
|
| 263 |
+
The maximum number of recurrences to generate. If `None` (default),
|
| 264 |
+
dates are generated until the recurrence rule is exhausted.
|
| 265 |
+
|
| 266 |
+
:param inc:
|
| 267 |
+
If `dt` is an instance of the rule and `inc` is `True`, it is
|
| 268 |
+
included in the output.
|
| 269 |
+
|
| 270 |
+
:yields: Yields a sequence of `datetime` objects.
|
| 271 |
+
"""
|
| 272 |
+
|
| 273 |
+
if self._cache_complete:
|
| 274 |
+
gen = self._cache
|
| 275 |
+
else:
|
| 276 |
+
gen = self
|
| 277 |
+
|
| 278 |
+
# Select the comparison function
|
| 279 |
+
if inc:
|
| 280 |
+
comp = lambda dc, dtc: dc >= dtc
|
| 281 |
+
else:
|
| 282 |
+
comp = lambda dc, dtc: dc > dtc
|
| 283 |
+
|
| 284 |
+
# Generate dates
|
| 285 |
+
n = 0
|
| 286 |
+
for d in gen:
|
| 287 |
+
if comp(d, dt):
|
| 288 |
+
if count is not None:
|
| 289 |
+
n += 1
|
| 290 |
+
if n > count:
|
| 291 |
+
break
|
| 292 |
+
|
| 293 |
+
yield d
|
| 294 |
+
|
| 295 |
+
def between(self, after, before, inc=False, count=1):
|
| 296 |
+
""" Returns all the occurrences of the rrule between after and before.
|
| 297 |
+
The inc keyword defines what happens if after and/or before are
|
| 298 |
+
themselves occurrences. With inc=True, they will be included in the
|
| 299 |
+
list, if they are found in the recurrence set. """
|
| 300 |
+
if self._cache_complete:
|
| 301 |
+
gen = self._cache
|
| 302 |
+
else:
|
| 303 |
+
gen = self
|
| 304 |
+
started = False
|
| 305 |
+
l = []
|
| 306 |
+
if inc:
|
| 307 |
+
for i in gen:
|
| 308 |
+
if i > before:
|
| 309 |
+
break
|
| 310 |
+
elif not started:
|
| 311 |
+
if i >= after:
|
| 312 |
+
started = True
|
| 313 |
+
l.append(i)
|
| 314 |
+
else:
|
| 315 |
+
l.append(i)
|
| 316 |
+
else:
|
| 317 |
+
for i in gen:
|
| 318 |
+
if i >= before:
|
| 319 |
+
break
|
| 320 |
+
elif not started:
|
| 321 |
+
if i > after:
|
| 322 |
+
started = True
|
| 323 |
+
l.append(i)
|
| 324 |
+
else:
|
| 325 |
+
l.append(i)
|
| 326 |
+
return l
|
| 327 |
+
|
| 328 |
+
|
| 329 |
+
class rrule(rrulebase):
|
| 330 |
+
"""
|
| 331 |
+
That's the base of the rrule operation. It accepts all the keywords
|
| 332 |
+
defined in the RFC as its constructor parameters (except byday,
|
| 333 |
+
which was renamed to byweekday) and more. The constructor prototype is::
|
| 334 |
+
|
| 335 |
+
rrule(freq)
|
| 336 |
+
|
| 337 |
+
Where freq must be one of YEARLY, MONTHLY, WEEKLY, DAILY, HOURLY, MINUTELY,
|
| 338 |
+
or SECONDLY.
|
| 339 |
+
|
| 340 |
+
.. note::
|
| 341 |
+
Per RFC section 3.3.10, recurrence instances falling on invalid dates
|
| 342 |
+
and times are ignored rather than coerced:
|
| 343 |
+
|
| 344 |
+
Recurrence rules may generate recurrence instances with an invalid
|
| 345 |
+
date (e.g., February 30) or nonexistent local time (e.g., 1:30 AM
|
| 346 |
+
on a day where the local time is moved forward by an hour at 1:00
|
| 347 |
+
AM). Such recurrence instances MUST be ignored and MUST NOT be
|
| 348 |
+
counted as part of the recurrence set.
|
| 349 |
+
|
| 350 |
+
This can lead to possibly surprising behavior when, for example, the
|
| 351 |
+
start date occurs at the end of the month:
|
| 352 |
+
|
| 353 |
+
>>> from dateutil.rrule import rrule, MONTHLY
|
| 354 |
+
>>> from datetime import datetime
|
| 355 |
+
>>> start_date = datetime(2014, 12, 31)
|
| 356 |
+
>>> list(rrule(freq=MONTHLY, count=4, dtstart=start_date))
|
| 357 |
+
... # doctest: +NORMALIZE_WHITESPACE
|
| 358 |
+
[datetime.datetime(2014, 12, 31, 0, 0),
|
| 359 |
+
datetime.datetime(2015, 1, 31, 0, 0),
|
| 360 |
+
datetime.datetime(2015, 3, 31, 0, 0),
|
| 361 |
+
datetime.datetime(2015, 5, 31, 0, 0)]
|
| 362 |
+
|
| 363 |
+
Additionally, it supports the following keyword arguments:
|
| 364 |
+
|
| 365 |
+
:param dtstart:
|
| 366 |
+
The recurrence start. Besides being the base for the recurrence,
|
| 367 |
+
missing parameters in the final recurrence instances will also be
|
| 368 |
+
extracted from this date. If not given, datetime.now() will be used
|
| 369 |
+
instead.
|
| 370 |
+
:param interval:
|
| 371 |
+
The interval between each freq iteration. For example, when using
|
| 372 |
+
YEARLY, an interval of 2 means once every two years, but with HOURLY,
|
| 373 |
+
it means once every two hours. The default interval is 1.
|
| 374 |
+
:param wkst:
|
| 375 |
+
The week start day. Must be one of the MO, TU, WE constants, or an
|
| 376 |
+
integer, specifying the first day of the week. This will affect
|
| 377 |
+
recurrences based on weekly periods. The default week start is got
|
| 378 |
+
from calendar.firstweekday(), and may be modified by
|
| 379 |
+
calendar.setfirstweekday().
|
| 380 |
+
:param count:
|
| 381 |
+
If given, this determines how many occurrences will be generated.
|
| 382 |
+
|
| 383 |
+
.. note::
|
| 384 |
+
As of version 2.5.0, the use of the keyword ``until`` in conjunction
|
| 385 |
+
with ``count`` is deprecated, to make sure ``dateutil`` is fully
|
| 386 |
+
compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
|
| 387 |
+
html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
|
| 388 |
+
**must not** occur in the same call to ``rrule``.
|
| 389 |
+
:param until:
|
| 390 |
+
If given, this must be a datetime instance specifying the upper-bound
|
| 391 |
+
limit of the recurrence. The last recurrence in the rule is the greatest
|
| 392 |
+
datetime that is less than or equal to the value specified in the
|
| 393 |
+
``until`` parameter.
|
| 394 |
+
|
| 395 |
+
.. note::
|
| 396 |
+
As of version 2.5.0, the use of the keyword ``until`` in conjunction
|
| 397 |
+
with ``count`` is deprecated, to make sure ``dateutil`` is fully
|
| 398 |
+
compliant with `RFC-5545 Sec. 3.3.10 <https://tools.ietf.org/
|
| 399 |
+
html/rfc5545#section-3.3.10>`_. Therefore, ``until`` and ``count``
|
| 400 |
+
**must not** occur in the same call to ``rrule``.
|
| 401 |
+
:param bysetpos:
|
| 402 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 403 |
+
positive or negative. Each given integer will specify an occurrence
|
| 404 |
+
number, corresponding to the nth occurrence of the rule inside the
|
| 405 |
+
frequency period. For example, a bysetpos of -1 if combined with a
|
| 406 |
+
MONTHLY frequency, and a byweekday of (MO, TU, WE, TH, FR), will
|
| 407 |
+
result in the last work day of every month.
|
| 408 |
+
:param bymonth:
|
| 409 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 410 |
+
meaning the months to apply the recurrence to.
|
| 411 |
+
:param bymonthday:
|
| 412 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 413 |
+
meaning the month days to apply the recurrence to.
|
| 414 |
+
:param byyearday:
|
| 415 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 416 |
+
meaning the year days to apply the recurrence to.
|
| 417 |
+
:param byeaster:
|
| 418 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 419 |
+
positive or negative. Each integer will define an offset from the
|
| 420 |
+
Easter Sunday. Passing the offset 0 to byeaster will yield the Easter
|
| 421 |
+
Sunday itself. This is an extension to the RFC specification.
|
| 422 |
+
:param byweekno:
|
| 423 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 424 |
+
meaning the week numbers to apply the recurrence to. Week numbers
|
| 425 |
+
have the meaning described in ISO8601, that is, the first week of
|
| 426 |
+
the year is that containing at least four days of the new year.
|
| 427 |
+
:param byweekday:
|
| 428 |
+
If given, it must be either an integer (0 == MO), a sequence of
|
| 429 |
+
integers, one of the weekday constants (MO, TU, etc), or a sequence
|
| 430 |
+
of these constants. When given, these variables will define the
|
| 431 |
+
weekdays where the recurrence will be applied. It's also possible to
|
| 432 |
+
use an argument n for the weekday instances, which will mean the nth
|
| 433 |
+
occurrence of this weekday in the period. For example, with MONTHLY,
|
| 434 |
+
or with YEARLY and BYMONTH, using FR(+1) in byweekday will specify the
|
| 435 |
+
first friday of the month where the recurrence happens. Notice that in
|
| 436 |
+
the RFC documentation, this is specified as BYDAY, but was renamed to
|
| 437 |
+
avoid the ambiguity of that keyword.
|
| 438 |
+
:param byhour:
|
| 439 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 440 |
+
meaning the hours to apply the recurrence to.
|
| 441 |
+
:param byminute:
|
| 442 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 443 |
+
meaning the minutes to apply the recurrence to.
|
| 444 |
+
:param bysecond:
|
| 445 |
+
If given, it must be either an integer, or a sequence of integers,
|
| 446 |
+
meaning the seconds to apply the recurrence to.
|
| 447 |
+
:param cache:
|
| 448 |
+
If given, it must be a boolean value specifying to enable or disable
|
| 449 |
+
caching of results. If you will use the same rrule instance multiple
|
| 450 |
+
times, enabling caching will improve the performance considerably.
|
| 451 |
+
"""
|
| 452 |
+
def __init__(self, freq, dtstart=None,
|
| 453 |
+
interval=1, wkst=None, count=None, until=None, bysetpos=None,
|
| 454 |
+
bymonth=None, bymonthday=None, byyearday=None, byeaster=None,
|
| 455 |
+
byweekno=None, byweekday=None,
|
| 456 |
+
byhour=None, byminute=None, bysecond=None,
|
| 457 |
+
cache=False):
|
| 458 |
+
super(rrule, self).__init__(cache)
|
| 459 |
+
global easter
|
| 460 |
+
if not dtstart:
|
| 461 |
+
if until and until.tzinfo:
|
| 462 |
+
dtstart = datetime.datetime.now(tz=until.tzinfo).replace(microsecond=0)
|
| 463 |
+
else:
|
| 464 |
+
dtstart = datetime.datetime.now().replace(microsecond=0)
|
| 465 |
+
elif not isinstance(dtstart, datetime.datetime):
|
| 466 |
+
dtstart = datetime.datetime.fromordinal(dtstart.toordinal())
|
| 467 |
+
else:
|
| 468 |
+
dtstart = dtstart.replace(microsecond=0)
|
| 469 |
+
self._dtstart = dtstart
|
| 470 |
+
self._tzinfo = dtstart.tzinfo
|
| 471 |
+
self._freq = freq
|
| 472 |
+
self._interval = interval
|
| 473 |
+
self._count = count
|
| 474 |
+
|
| 475 |
+
# Cache the original byxxx rules, if they are provided, as the _byxxx
|
| 476 |
+
# attributes do not necessarily map to the inputs, and this can be
|
| 477 |
+
# a problem in generating the strings. Only store things if they've
|
| 478 |
+
# been supplied (the string retrieval will just use .get())
|
| 479 |
+
self._original_rule = {}
|
| 480 |
+
|
| 481 |
+
if until and not isinstance(until, datetime.datetime):
|
| 482 |
+
until = datetime.datetime.fromordinal(until.toordinal())
|
| 483 |
+
self._until = until
|
| 484 |
+
|
| 485 |
+
if self._dtstart and self._until:
|
| 486 |
+
if (self._dtstart.tzinfo is not None) != (self._until.tzinfo is not None):
|
| 487 |
+
# According to RFC5545 Section 3.3.10:
|
| 488 |
+
# https://tools.ietf.org/html/rfc5545#section-3.3.10
|
| 489 |
+
#
|
| 490 |
+
# > If the "DTSTART" property is specified as a date with UTC
|
| 491 |
+
# > time or a date with local time and time zone reference,
|
| 492 |
+
# > then the UNTIL rule part MUST be specified as a date with
|
| 493 |
+
# > UTC time.
|
| 494 |
+
raise ValueError(
|
| 495 |
+
'RRULE UNTIL values must be specified in UTC when DTSTART '
|
| 496 |
+
'is timezone-aware'
|
| 497 |
+
)
|
| 498 |
+
|
| 499 |
+
if count is not None and until:
|
| 500 |
+
warn("Using both 'count' and 'until' is inconsistent with RFC 5545"
|
| 501 |
+
" and has been deprecated in dateutil. Future versions will "
|
| 502 |
+
"raise an error.", DeprecationWarning)
|
| 503 |
+
|
| 504 |
+
if wkst is None:
|
| 505 |
+
self._wkst = calendar.firstweekday()
|
| 506 |
+
elif isinstance(wkst, integer_types):
|
| 507 |
+
self._wkst = wkst
|
| 508 |
+
else:
|
| 509 |
+
self._wkst = wkst.weekday
|
| 510 |
+
|
| 511 |
+
if bysetpos is None:
|
| 512 |
+
self._bysetpos = None
|
| 513 |
+
elif isinstance(bysetpos, integer_types):
|
| 514 |
+
if bysetpos == 0 or not (-366 <= bysetpos <= 366):
|
| 515 |
+
raise ValueError("bysetpos must be between 1 and 366, "
|
| 516 |
+
"or between -366 and -1")
|
| 517 |
+
self._bysetpos = (bysetpos,)
|
| 518 |
+
else:
|
| 519 |
+
self._bysetpos = tuple(bysetpos)
|
| 520 |
+
for pos in self._bysetpos:
|
| 521 |
+
if pos == 0 or not (-366 <= pos <= 366):
|
| 522 |
+
raise ValueError("bysetpos must be between 1 and 366, "
|
| 523 |
+
"or between -366 and -1")
|
| 524 |
+
|
| 525 |
+
if self._bysetpos:
|
| 526 |
+
self._original_rule['bysetpos'] = self._bysetpos
|
| 527 |
+
|
| 528 |
+
if (byweekno is None and byyearday is None and bymonthday is None and
|
| 529 |
+
byweekday is None and byeaster is None):
|
| 530 |
+
if freq == YEARLY:
|
| 531 |
+
if bymonth is None:
|
| 532 |
+
bymonth = dtstart.month
|
| 533 |
+
self._original_rule['bymonth'] = None
|
| 534 |
+
bymonthday = dtstart.day
|
| 535 |
+
self._original_rule['bymonthday'] = None
|
| 536 |
+
elif freq == MONTHLY:
|
| 537 |
+
bymonthday = dtstart.day
|
| 538 |
+
self._original_rule['bymonthday'] = None
|
| 539 |
+
elif freq == WEEKLY:
|
| 540 |
+
byweekday = dtstart.weekday()
|
| 541 |
+
self._original_rule['byweekday'] = None
|
| 542 |
+
|
| 543 |
+
# bymonth
|
| 544 |
+
if bymonth is None:
|
| 545 |
+
self._bymonth = None
|
| 546 |
+
else:
|
| 547 |
+
if isinstance(bymonth, integer_types):
|
| 548 |
+
bymonth = (bymonth,)
|
| 549 |
+
|
| 550 |
+
self._bymonth = tuple(sorted(set(bymonth)))
|
| 551 |
+
|
| 552 |
+
if 'bymonth' not in self._original_rule:
|
| 553 |
+
self._original_rule['bymonth'] = self._bymonth
|
| 554 |
+
|
| 555 |
+
# byyearday
|
| 556 |
+
if byyearday is None:
|
| 557 |
+
self._byyearday = None
|
| 558 |
+
else:
|
| 559 |
+
if isinstance(byyearday, integer_types):
|
| 560 |
+
byyearday = (byyearday,)
|
| 561 |
+
|
| 562 |
+
self._byyearday = tuple(sorted(set(byyearday)))
|
| 563 |
+
self._original_rule['byyearday'] = self._byyearday
|
| 564 |
+
|
| 565 |
+
# byeaster
|
| 566 |
+
if byeaster is not None:
|
| 567 |
+
if not easter:
|
| 568 |
+
from dateutil import easter
|
| 569 |
+
if isinstance(byeaster, integer_types):
|
| 570 |
+
self._byeaster = (byeaster,)
|
| 571 |
+
else:
|
| 572 |
+
self._byeaster = tuple(sorted(byeaster))
|
| 573 |
+
|
| 574 |
+
self._original_rule['byeaster'] = self._byeaster
|
| 575 |
+
else:
|
| 576 |
+
self._byeaster = None
|
| 577 |
+
|
| 578 |
+
# bymonthday
|
| 579 |
+
if bymonthday is None:
|
| 580 |
+
self._bymonthday = ()
|
| 581 |
+
self._bynmonthday = ()
|
| 582 |
+
else:
|
| 583 |
+
if isinstance(bymonthday, integer_types):
|
| 584 |
+
bymonthday = (bymonthday,)
|
| 585 |
+
|
| 586 |
+
bymonthday = set(bymonthday) # Ensure it's unique
|
| 587 |
+
|
| 588 |
+
self._bymonthday = tuple(sorted(x for x in bymonthday if x > 0))
|
| 589 |
+
self._bynmonthday = tuple(sorted(x for x in bymonthday if x < 0))
|
| 590 |
+
|
| 591 |
+
# Storing positive numbers first, then negative numbers
|
| 592 |
+
if 'bymonthday' not in self._original_rule:
|
| 593 |
+
self._original_rule['bymonthday'] = tuple(
|
| 594 |
+
itertools.chain(self._bymonthday, self._bynmonthday))
|
| 595 |
+
|
| 596 |
+
# byweekno
|
| 597 |
+
if byweekno is None:
|
| 598 |
+
self._byweekno = None
|
| 599 |
+
else:
|
| 600 |
+
if isinstance(byweekno, integer_types):
|
| 601 |
+
byweekno = (byweekno,)
|
| 602 |
+
|
| 603 |
+
self._byweekno = tuple(sorted(set(byweekno)))
|
| 604 |
+
|
| 605 |
+
self._original_rule['byweekno'] = self._byweekno
|
| 606 |
+
|
| 607 |
+
# byweekday / bynweekday
|
| 608 |
+
if byweekday is None:
|
| 609 |
+
self._byweekday = None
|
| 610 |
+
self._bynweekday = None
|
| 611 |
+
else:
|
| 612 |
+
# If it's one of the valid non-sequence types, convert to a
|
| 613 |
+
# single-element sequence before the iterator that builds the
|
| 614 |
+
# byweekday set.
|
| 615 |
+
if isinstance(byweekday, integer_types) or hasattr(byweekday, "n"):
|
| 616 |
+
byweekday = (byweekday,)
|
| 617 |
+
|
| 618 |
+
self._byweekday = set()
|
| 619 |
+
self._bynweekday = set()
|
| 620 |
+
for wday in byweekday:
|
| 621 |
+
if isinstance(wday, integer_types):
|
| 622 |
+
self._byweekday.add(wday)
|
| 623 |
+
elif not wday.n or freq > MONTHLY:
|
| 624 |
+
self._byweekday.add(wday.weekday)
|
| 625 |
+
else:
|
| 626 |
+
self._bynweekday.add((wday.weekday, wday.n))
|
| 627 |
+
|
| 628 |
+
if not self._byweekday:
|
| 629 |
+
self._byweekday = None
|
| 630 |
+
elif not self._bynweekday:
|
| 631 |
+
self._bynweekday = None
|
| 632 |
+
|
| 633 |
+
if self._byweekday is not None:
|
| 634 |
+
self._byweekday = tuple(sorted(self._byweekday))
|
| 635 |
+
orig_byweekday = [weekday(x) for x in self._byweekday]
|
| 636 |
+
else:
|
| 637 |
+
orig_byweekday = ()
|
| 638 |
+
|
| 639 |
+
if self._bynweekday is not None:
|
| 640 |
+
self._bynweekday = tuple(sorted(self._bynweekday))
|
| 641 |
+
orig_bynweekday = [weekday(*x) for x in self._bynweekday]
|
| 642 |
+
else:
|
| 643 |
+
orig_bynweekday = ()
|
| 644 |
+
|
| 645 |
+
if 'byweekday' not in self._original_rule:
|
| 646 |
+
self._original_rule['byweekday'] = tuple(itertools.chain(
|
| 647 |
+
orig_byweekday, orig_bynweekday))
|
| 648 |
+
|
| 649 |
+
# byhour
|
| 650 |
+
if byhour is None:
|
| 651 |
+
if freq < HOURLY:
|
| 652 |
+
self._byhour = {dtstart.hour}
|
| 653 |
+
else:
|
| 654 |
+
self._byhour = None
|
| 655 |
+
else:
|
| 656 |
+
if isinstance(byhour, integer_types):
|
| 657 |
+
byhour = (byhour,)
|
| 658 |
+
|
| 659 |
+
if freq == HOURLY:
|
| 660 |
+
self._byhour = self.__construct_byset(start=dtstart.hour,
|
| 661 |
+
byxxx=byhour,
|
| 662 |
+
base=24)
|
| 663 |
+
else:
|
| 664 |
+
self._byhour = set(byhour)
|
| 665 |
+
|
| 666 |
+
self._byhour = tuple(sorted(self._byhour))
|
| 667 |
+
self._original_rule['byhour'] = self._byhour
|
| 668 |
+
|
| 669 |
+
# byminute
|
| 670 |
+
if byminute is None:
|
| 671 |
+
if freq < MINUTELY:
|
| 672 |
+
self._byminute = {dtstart.minute}
|
| 673 |
+
else:
|
| 674 |
+
self._byminute = None
|
| 675 |
+
else:
|
| 676 |
+
if isinstance(byminute, integer_types):
|
| 677 |
+
byminute = (byminute,)
|
| 678 |
+
|
| 679 |
+
if freq == MINUTELY:
|
| 680 |
+
self._byminute = self.__construct_byset(start=dtstart.minute,
|
| 681 |
+
byxxx=byminute,
|
| 682 |
+
base=60)
|
| 683 |
+
else:
|
| 684 |
+
self._byminute = set(byminute)
|
| 685 |
+
|
| 686 |
+
self._byminute = tuple(sorted(self._byminute))
|
| 687 |
+
self._original_rule['byminute'] = self._byminute
|
| 688 |
+
|
| 689 |
+
# bysecond
|
| 690 |
+
if bysecond is None:
|
| 691 |
+
if freq < SECONDLY:
|
| 692 |
+
self._bysecond = ((dtstart.second,))
|
| 693 |
+
else:
|
| 694 |
+
self._bysecond = None
|
| 695 |
+
else:
|
| 696 |
+
if isinstance(bysecond, integer_types):
|
| 697 |
+
bysecond = (bysecond,)
|
| 698 |
+
|
| 699 |
+
self._bysecond = set(bysecond)
|
| 700 |
+
|
| 701 |
+
if freq == SECONDLY:
|
| 702 |
+
self._bysecond = self.__construct_byset(start=dtstart.second,
|
| 703 |
+
byxxx=bysecond,
|
| 704 |
+
base=60)
|
| 705 |
+
else:
|
| 706 |
+
self._bysecond = set(bysecond)
|
| 707 |
+
|
| 708 |
+
self._bysecond = tuple(sorted(self._bysecond))
|
| 709 |
+
self._original_rule['bysecond'] = self._bysecond
|
| 710 |
+
|
| 711 |
+
if self._freq >= HOURLY:
|
| 712 |
+
self._timeset = None
|
| 713 |
+
else:
|
| 714 |
+
self._timeset = []
|
| 715 |
+
for hour in self._byhour:
|
| 716 |
+
for minute in self._byminute:
|
| 717 |
+
for second in self._bysecond:
|
| 718 |
+
self._timeset.append(
|
| 719 |
+
datetime.time(hour, minute, second,
|
| 720 |
+
tzinfo=self._tzinfo))
|
| 721 |
+
self._timeset.sort()
|
| 722 |
+
self._timeset = tuple(self._timeset)
|
| 723 |
+
|
| 724 |
+
def __str__(self):
|
| 725 |
+
"""
|
| 726 |
+
Output a string that would generate this RRULE if passed to rrulestr.
|
| 727 |
+
This is mostly compatible with RFC5545, except for the
|
| 728 |
+
dateutil-specific extension BYEASTER.
|
| 729 |
+
"""
|
| 730 |
+
|
| 731 |
+
output = []
|
| 732 |
+
h, m, s = [None] * 3
|
| 733 |
+
if self._dtstart:
|
| 734 |
+
output.append(self._dtstart.strftime('DTSTART:%Y%m%dT%H%M%S'))
|
| 735 |
+
h, m, s = self._dtstart.timetuple()[3:6]
|
| 736 |
+
|
| 737 |
+
parts = ['FREQ=' + FREQNAMES[self._freq]]
|
| 738 |
+
if self._interval != 1:
|
| 739 |
+
parts.append('INTERVAL=' + str(self._interval))
|
| 740 |
+
|
| 741 |
+
if self._wkst:
|
| 742 |
+
parts.append('WKST=' + repr(weekday(self._wkst))[0:2])
|
| 743 |
+
|
| 744 |
+
if self._count is not None:
|
| 745 |
+
parts.append('COUNT=' + str(self._count))
|
| 746 |
+
|
| 747 |
+
if self._until:
|
| 748 |
+
parts.append(self._until.strftime('UNTIL=%Y%m%dT%H%M%S'))
|
| 749 |
+
|
| 750 |
+
if self._original_rule.get('byweekday') is not None:
|
| 751 |
+
# The str() method on weekday objects doesn't generate
|
| 752 |
+
# RFC5545-compliant strings, so we should modify that.
|
| 753 |
+
original_rule = dict(self._original_rule)
|
| 754 |
+
wday_strings = []
|
| 755 |
+
for wday in original_rule['byweekday']:
|
| 756 |
+
if wday.n:
|
| 757 |
+
wday_strings.append('{n:+d}{wday}'.format(
|
| 758 |
+
n=wday.n,
|
| 759 |
+
wday=repr(wday)[0:2]))
|
| 760 |
+
else:
|
| 761 |
+
wday_strings.append(repr(wday))
|
| 762 |
+
|
| 763 |
+
original_rule['byweekday'] = wday_strings
|
| 764 |
+
else:
|
| 765 |
+
original_rule = self._original_rule
|
| 766 |
+
|
| 767 |
+
partfmt = '{name}={vals}'
|
| 768 |
+
for name, key in [('BYSETPOS', 'bysetpos'),
|
| 769 |
+
('BYMONTH', 'bymonth'),
|
| 770 |
+
('BYMONTHDAY', 'bymonthday'),
|
| 771 |
+
('BYYEARDAY', 'byyearday'),
|
| 772 |
+
('BYWEEKNO', 'byweekno'),
|
| 773 |
+
('BYDAY', 'byweekday'),
|
| 774 |
+
('BYHOUR', 'byhour'),
|
| 775 |
+
('BYMINUTE', 'byminute'),
|
| 776 |
+
('BYSECOND', 'bysecond'),
|
| 777 |
+
('BYEASTER', 'byeaster')]:
|
| 778 |
+
value = original_rule.get(key)
|
| 779 |
+
if value:
|
| 780 |
+
parts.append(partfmt.format(name=name, vals=(','.join(str(v)
|
| 781 |
+
for v in value))))
|
| 782 |
+
|
| 783 |
+
output.append('RRULE:' + ';'.join(parts))
|
| 784 |
+
return '\n'.join(output)
|
| 785 |
+
|
| 786 |
+
def replace(self, **kwargs):
|
| 787 |
+
"""Return new rrule with same attributes except for those attributes given new
|
| 788 |
+
values by whichever keyword arguments are specified."""
|
| 789 |
+
new_kwargs = {"interval": self._interval,
|
| 790 |
+
"count": self._count,
|
| 791 |
+
"dtstart": self._dtstart,
|
| 792 |
+
"freq": self._freq,
|
| 793 |
+
"until": self._until,
|
| 794 |
+
"wkst": self._wkst,
|
| 795 |
+
"cache": False if self._cache is None else True }
|
| 796 |
+
new_kwargs.update(self._original_rule)
|
| 797 |
+
new_kwargs.update(kwargs)
|
| 798 |
+
return rrule(**new_kwargs)
|
| 799 |
+
|
| 800 |
+
def _iter(self):
|
| 801 |
+
year, month, day, hour, minute, second, weekday, yearday, _ = \
|
| 802 |
+
self._dtstart.timetuple()
|
| 803 |
+
|
| 804 |
+
# Some local variables to speed things up a bit
|
| 805 |
+
freq = self._freq
|
| 806 |
+
interval = self._interval
|
| 807 |
+
wkst = self._wkst
|
| 808 |
+
until = self._until
|
| 809 |
+
bymonth = self._bymonth
|
| 810 |
+
byweekno = self._byweekno
|
| 811 |
+
byyearday = self._byyearday
|
| 812 |
+
byweekday = self._byweekday
|
| 813 |
+
byeaster = self._byeaster
|
| 814 |
+
bymonthday = self._bymonthday
|
| 815 |
+
bynmonthday = self._bynmonthday
|
| 816 |
+
bysetpos = self._bysetpos
|
| 817 |
+
byhour = self._byhour
|
| 818 |
+
byminute = self._byminute
|
| 819 |
+
bysecond = self._bysecond
|
| 820 |
+
|
| 821 |
+
ii = _iterinfo(self)
|
| 822 |
+
ii.rebuild(year, month)
|
| 823 |
+
|
| 824 |
+
getdayset = {YEARLY: ii.ydayset,
|
| 825 |
+
MONTHLY: ii.mdayset,
|
| 826 |
+
WEEKLY: ii.wdayset,
|
| 827 |
+
DAILY: ii.ddayset,
|
| 828 |
+
HOURLY: ii.ddayset,
|
| 829 |
+
MINUTELY: ii.ddayset,
|
| 830 |
+
SECONDLY: ii.ddayset}[freq]
|
| 831 |
+
|
| 832 |
+
if freq < HOURLY:
|
| 833 |
+
timeset = self._timeset
|
| 834 |
+
else:
|
| 835 |
+
gettimeset = {HOURLY: ii.htimeset,
|
| 836 |
+
MINUTELY: ii.mtimeset,
|
| 837 |
+
SECONDLY: ii.stimeset}[freq]
|
| 838 |
+
if ((freq >= HOURLY and
|
| 839 |
+
self._byhour and hour not in self._byhour) or
|
| 840 |
+
(freq >= MINUTELY and
|
| 841 |
+
self._byminute and minute not in self._byminute) or
|
| 842 |
+
(freq >= SECONDLY and
|
| 843 |
+
self._bysecond and second not in self._bysecond)):
|
| 844 |
+
timeset = ()
|
| 845 |
+
else:
|
| 846 |
+
timeset = gettimeset(hour, minute, second)
|
| 847 |
+
|
| 848 |
+
total = 0
|
| 849 |
+
count = self._count
|
| 850 |
+
while True:
|
| 851 |
+
# Get dayset with the right frequency
|
| 852 |
+
dayset, start, end = getdayset(year, month, day)
|
| 853 |
+
|
| 854 |
+
# Do the "hard" work ;-)
|
| 855 |
+
filtered = False
|
| 856 |
+
for i in dayset[start:end]:
|
| 857 |
+
if ((bymonth and ii.mmask[i] not in bymonth) or
|
| 858 |
+
(byweekno and not ii.wnomask[i]) or
|
| 859 |
+
(byweekday and ii.wdaymask[i] not in byweekday) or
|
| 860 |
+
(ii.nwdaymask and not ii.nwdaymask[i]) or
|
| 861 |
+
(byeaster and not ii.eastermask[i]) or
|
| 862 |
+
((bymonthday or bynmonthday) and
|
| 863 |
+
ii.mdaymask[i] not in bymonthday and
|
| 864 |
+
ii.nmdaymask[i] not in bynmonthday) or
|
| 865 |
+
(byyearday and
|
| 866 |
+
((i < ii.yearlen and i+1 not in byyearday and
|
| 867 |
+
-ii.yearlen+i not in byyearday) or
|
| 868 |
+
(i >= ii.yearlen and i+1-ii.yearlen not in byyearday and
|
| 869 |
+
-ii.nextyearlen+i-ii.yearlen not in byyearday)))):
|
| 870 |
+
dayset[i] = None
|
| 871 |
+
filtered = True
|
| 872 |
+
|
| 873 |
+
# Output results
|
| 874 |
+
if bysetpos and timeset:
|
| 875 |
+
poslist = []
|
| 876 |
+
for pos in bysetpos:
|
| 877 |
+
if pos < 0:
|
| 878 |
+
daypos, timepos = divmod(pos, len(timeset))
|
| 879 |
+
else:
|
| 880 |
+
daypos, timepos = divmod(pos-1, len(timeset))
|
| 881 |
+
try:
|
| 882 |
+
i = [x for x in dayset[start:end]
|
| 883 |
+
if x is not None][daypos]
|
| 884 |
+
time = timeset[timepos]
|
| 885 |
+
except IndexError:
|
| 886 |
+
pass
|
| 887 |
+
else:
|
| 888 |
+
date = datetime.date.fromordinal(ii.yearordinal+i)
|
| 889 |
+
res = datetime.datetime.combine(date, time)
|
| 890 |
+
if res not in poslist:
|
| 891 |
+
poslist.append(res)
|
| 892 |
+
poslist.sort()
|
| 893 |
+
for res in poslist:
|
| 894 |
+
if until and res > until:
|
| 895 |
+
self._len = total
|
| 896 |
+
return
|
| 897 |
+
elif res >= self._dtstart:
|
| 898 |
+
if count is not None:
|
| 899 |
+
count -= 1
|
| 900 |
+
if count < 0:
|
| 901 |
+
self._len = total
|
| 902 |
+
return
|
| 903 |
+
total += 1
|
| 904 |
+
yield res
|
| 905 |
+
else:
|
| 906 |
+
for i in dayset[start:end]:
|
| 907 |
+
if i is not None:
|
| 908 |
+
date = datetime.date.fromordinal(ii.yearordinal + i)
|
| 909 |
+
for time in timeset:
|
| 910 |
+
res = datetime.datetime.combine(date, time)
|
| 911 |
+
if until and res > until:
|
| 912 |
+
self._len = total
|
| 913 |
+
return
|
| 914 |
+
elif res >= self._dtstart:
|
| 915 |
+
if count is not None:
|
| 916 |
+
count -= 1
|
| 917 |
+
if count < 0:
|
| 918 |
+
self._len = total
|
| 919 |
+
return
|
| 920 |
+
|
| 921 |
+
total += 1
|
| 922 |
+
yield res
|
| 923 |
+
|
| 924 |
+
# Handle frequency and interval
|
| 925 |
+
fixday = False
|
| 926 |
+
if freq == YEARLY:
|
| 927 |
+
year += interval
|
| 928 |
+
if year > datetime.MAXYEAR:
|
| 929 |
+
self._len = total
|
| 930 |
+
return
|
| 931 |
+
ii.rebuild(year, month)
|
| 932 |
+
elif freq == MONTHLY:
|
| 933 |
+
month += interval
|
| 934 |
+
if month > 12:
|
| 935 |
+
div, mod = divmod(month, 12)
|
| 936 |
+
month = mod
|
| 937 |
+
year += div
|
| 938 |
+
if month == 0:
|
| 939 |
+
month = 12
|
| 940 |
+
year -= 1
|
| 941 |
+
if year > datetime.MAXYEAR:
|
| 942 |
+
self._len = total
|
| 943 |
+
return
|
| 944 |
+
ii.rebuild(year, month)
|
| 945 |
+
elif freq == WEEKLY:
|
| 946 |
+
if wkst > weekday:
|
| 947 |
+
day += -(weekday+1+(6-wkst))+self._interval*7
|
| 948 |
+
else:
|
| 949 |
+
day += -(weekday-wkst)+self._interval*7
|
| 950 |
+
weekday = wkst
|
| 951 |
+
fixday = True
|
| 952 |
+
elif freq == DAILY:
|
| 953 |
+
day += interval
|
| 954 |
+
fixday = True
|
| 955 |
+
elif freq == HOURLY:
|
| 956 |
+
if filtered:
|
| 957 |
+
# Jump to one iteration before next day
|
| 958 |
+
hour += ((23-hour)//interval)*interval
|
| 959 |
+
|
| 960 |
+
if byhour:
|
| 961 |
+
ndays, hour = self.__mod_distance(value=hour,
|
| 962 |
+
byxxx=self._byhour,
|
| 963 |
+
base=24)
|
| 964 |
+
else:
|
| 965 |
+
ndays, hour = divmod(hour+interval, 24)
|
| 966 |
+
|
| 967 |
+
if ndays:
|
| 968 |
+
day += ndays
|
| 969 |
+
fixday = True
|
| 970 |
+
|
| 971 |
+
timeset = gettimeset(hour, minute, second)
|
| 972 |
+
elif freq == MINUTELY:
|
| 973 |
+
if filtered:
|
| 974 |
+
# Jump to one iteration before next day
|
| 975 |
+
minute += ((1439-(hour*60+minute))//interval)*interval
|
| 976 |
+
|
| 977 |
+
valid = False
|
| 978 |
+
rep_rate = (24*60)
|
| 979 |
+
for j in range(rep_rate // gcd(interval, rep_rate)):
|
| 980 |
+
if byminute:
|
| 981 |
+
nhours, minute = \
|
| 982 |
+
self.__mod_distance(value=minute,
|
| 983 |
+
byxxx=self._byminute,
|
| 984 |
+
base=60)
|
| 985 |
+
else:
|
| 986 |
+
nhours, minute = divmod(minute+interval, 60)
|
| 987 |
+
|
| 988 |
+
div, hour = divmod(hour+nhours, 24)
|
| 989 |
+
if div:
|
| 990 |
+
day += div
|
| 991 |
+
fixday = True
|
| 992 |
+
filtered = False
|
| 993 |
+
|
| 994 |
+
if not byhour or hour in byhour:
|
| 995 |
+
valid = True
|
| 996 |
+
break
|
| 997 |
+
|
| 998 |
+
if not valid:
|
| 999 |
+
raise ValueError('Invalid combination of interval and ' +
|
| 1000 |
+
'byhour resulting in empty rule.')
|
| 1001 |
+
|
| 1002 |
+
timeset = gettimeset(hour, minute, second)
|
| 1003 |
+
elif freq == SECONDLY:
|
| 1004 |
+
if filtered:
|
| 1005 |
+
# Jump to one iteration before next day
|
| 1006 |
+
second += (((86399 - (hour * 3600 + minute * 60 + second))
|
| 1007 |
+
// interval) * interval)
|
| 1008 |
+
|
| 1009 |
+
rep_rate = (24 * 3600)
|
| 1010 |
+
valid = False
|
| 1011 |
+
for j in range(0, rep_rate // gcd(interval, rep_rate)):
|
| 1012 |
+
if bysecond:
|
| 1013 |
+
nminutes, second = \
|
| 1014 |
+
self.__mod_distance(value=second,
|
| 1015 |
+
byxxx=self._bysecond,
|
| 1016 |
+
base=60)
|
| 1017 |
+
else:
|
| 1018 |
+
nminutes, second = divmod(second+interval, 60)
|
| 1019 |
+
|
| 1020 |
+
div, minute = divmod(minute+nminutes, 60)
|
| 1021 |
+
if div:
|
| 1022 |
+
hour += div
|
| 1023 |
+
div, hour = divmod(hour, 24)
|
| 1024 |
+
if div:
|
| 1025 |
+
day += div
|
| 1026 |
+
fixday = True
|
| 1027 |
+
|
| 1028 |
+
if ((not byhour or hour in byhour) and
|
| 1029 |
+
(not byminute or minute in byminute) and
|
| 1030 |
+
(not bysecond or second in bysecond)):
|
| 1031 |
+
valid = True
|
| 1032 |
+
break
|
| 1033 |
+
|
| 1034 |
+
if not valid:
|
| 1035 |
+
raise ValueError('Invalid combination of interval, ' +
|
| 1036 |
+
'byhour and byminute resulting in empty' +
|
| 1037 |
+
' rule.')
|
| 1038 |
+
|
| 1039 |
+
timeset = gettimeset(hour, minute, second)
|
| 1040 |
+
|
| 1041 |
+
if fixday and day > 28:
|
| 1042 |
+
daysinmonth = calendar.monthrange(year, month)[1]
|
| 1043 |
+
if day > daysinmonth:
|
| 1044 |
+
while day > daysinmonth:
|
| 1045 |
+
day -= daysinmonth
|
| 1046 |
+
month += 1
|
| 1047 |
+
if month == 13:
|
| 1048 |
+
month = 1
|
| 1049 |
+
year += 1
|
| 1050 |
+
if year > datetime.MAXYEAR:
|
| 1051 |
+
self._len = total
|
| 1052 |
+
return
|
| 1053 |
+
daysinmonth = calendar.monthrange(year, month)[1]
|
| 1054 |
+
ii.rebuild(year, month)
|
| 1055 |
+
|
| 1056 |
+
def __construct_byset(self, start, byxxx, base):
|
| 1057 |
+
"""
|
| 1058 |
+
If a `BYXXX` sequence is passed to the constructor at the same level as
|
| 1059 |
+
`FREQ` (e.g. `FREQ=HOURLY,BYHOUR={2,4,7},INTERVAL=3`), there are some
|
| 1060 |
+
specifications which cannot be reached given some starting conditions.
|
| 1061 |
+
|
| 1062 |
+
This occurs whenever the interval is not coprime with the base of a
|
| 1063 |
+
given unit and the difference between the starting position and the
|
| 1064 |
+
ending position is not coprime with the greatest common denominator
|
| 1065 |
+
between the interval and the base. For example, with a FREQ of hourly
|
| 1066 |
+
starting at 17:00 and an interval of 4, the only valid values for
|
| 1067 |
+
BYHOUR would be {21, 1, 5, 9, 13, 17}, because 4 and 24 are not
|
| 1068 |
+
coprime.
|
| 1069 |
+
|
| 1070 |
+
:param start:
|
| 1071 |
+
Specifies the starting position.
|
| 1072 |
+
:param byxxx:
|
| 1073 |
+
An iterable containing the list of allowed values.
|
| 1074 |
+
:param base:
|
| 1075 |
+
The largest allowable value for the specified frequency (e.g.
|
| 1076 |
+
24 hours, 60 minutes).
|
| 1077 |
+
|
| 1078 |
+
This does not preserve the type of the iterable, returning a set, since
|
| 1079 |
+
the values should be unique and the order is irrelevant, this will
|
| 1080 |
+
speed up later lookups.
|
| 1081 |
+
|
| 1082 |
+
In the event of an empty set, raises a :exception:`ValueError`, as this
|
| 1083 |
+
results in an empty rrule.
|
| 1084 |
+
"""
|
| 1085 |
+
|
| 1086 |
+
cset = set()
|
| 1087 |
+
|
| 1088 |
+
# Support a single byxxx value.
|
| 1089 |
+
if isinstance(byxxx, integer_types):
|
| 1090 |
+
byxxx = (byxxx, )
|
| 1091 |
+
|
| 1092 |
+
for num in byxxx:
|
| 1093 |
+
i_gcd = gcd(self._interval, base)
|
| 1094 |
+
# Use divmod rather than % because we need to wrap negative nums.
|
| 1095 |
+
if i_gcd == 1 or divmod(num - start, i_gcd)[1] == 0:
|
| 1096 |
+
cset.add(num)
|
| 1097 |
+
|
| 1098 |
+
if len(cset) == 0:
|
| 1099 |
+
raise ValueError("Invalid rrule byxxx generates an empty set.")
|
| 1100 |
+
|
| 1101 |
+
return cset
|
| 1102 |
+
|
| 1103 |
+
def __mod_distance(self, value, byxxx, base):
|
| 1104 |
+
"""
|
| 1105 |
+
Calculates the next value in a sequence where the `FREQ` parameter is
|
| 1106 |
+
specified along with a `BYXXX` parameter at the same "level"
|
| 1107 |
+
(e.g. `HOURLY` specified with `BYHOUR`).
|
| 1108 |
+
|
| 1109 |
+
:param value:
|
| 1110 |
+
The old value of the component.
|
| 1111 |
+
:param byxxx:
|
| 1112 |
+
The `BYXXX` set, which should have been generated by
|
| 1113 |
+
`rrule._construct_byset`, or something else which checks that a
|
| 1114 |
+
valid rule is present.
|
| 1115 |
+
:param base:
|
| 1116 |
+
The largest allowable value for the specified frequency (e.g.
|
| 1117 |
+
24 hours, 60 minutes).
|
| 1118 |
+
|
| 1119 |
+
If a valid value is not found after `base` iterations (the maximum
|
| 1120 |
+
number before the sequence would start to repeat), this raises a
|
| 1121 |
+
:exception:`ValueError`, as no valid values were found.
|
| 1122 |
+
|
| 1123 |
+
This returns a tuple of `divmod(n*interval, base)`, where `n` is the
|
| 1124 |
+
smallest number of `interval` repetitions until the next specified
|
| 1125 |
+
value in `byxxx` is found.
|
| 1126 |
+
"""
|
| 1127 |
+
accumulator = 0
|
| 1128 |
+
for ii in range(1, base + 1):
|
| 1129 |
+
# Using divmod() over % to account for negative intervals
|
| 1130 |
+
div, value = divmod(value + self._interval, base)
|
| 1131 |
+
accumulator += div
|
| 1132 |
+
if value in byxxx:
|
| 1133 |
+
return (accumulator, value)
|
| 1134 |
+
|
| 1135 |
+
|
| 1136 |
+
class _iterinfo(object):
|
| 1137 |
+
__slots__ = ["rrule", "lastyear", "lastmonth",
|
| 1138 |
+
"yearlen", "nextyearlen", "yearordinal", "yearweekday",
|
| 1139 |
+
"mmask", "mrange", "mdaymask", "nmdaymask",
|
| 1140 |
+
"wdaymask", "wnomask", "nwdaymask", "eastermask"]
|
| 1141 |
+
|
| 1142 |
+
def __init__(self, rrule):
|
| 1143 |
+
for attr in self.__slots__:
|
| 1144 |
+
setattr(self, attr, None)
|
| 1145 |
+
self.rrule = rrule
|
| 1146 |
+
|
| 1147 |
+
def rebuild(self, year, month):
|
| 1148 |
+
# Every mask is 7 days longer to handle cross-year weekly periods.
|
| 1149 |
+
rr = self.rrule
|
| 1150 |
+
if year != self.lastyear:
|
| 1151 |
+
self.yearlen = 365 + calendar.isleap(year)
|
| 1152 |
+
self.nextyearlen = 365 + calendar.isleap(year + 1)
|
| 1153 |
+
firstyday = datetime.date(year, 1, 1)
|
| 1154 |
+
self.yearordinal = firstyday.toordinal()
|
| 1155 |
+
self.yearweekday = firstyday.weekday()
|
| 1156 |
+
|
| 1157 |
+
wday = datetime.date(year, 1, 1).weekday()
|
| 1158 |
+
if self.yearlen == 365:
|
| 1159 |
+
self.mmask = M365MASK
|
| 1160 |
+
self.mdaymask = MDAY365MASK
|
| 1161 |
+
self.nmdaymask = NMDAY365MASK
|
| 1162 |
+
self.wdaymask = WDAYMASK[wday:]
|
| 1163 |
+
self.mrange = M365RANGE
|
| 1164 |
+
else:
|
| 1165 |
+
self.mmask = M366MASK
|
| 1166 |
+
self.mdaymask = MDAY366MASK
|
| 1167 |
+
self.nmdaymask = NMDAY366MASK
|
| 1168 |
+
self.wdaymask = WDAYMASK[wday:]
|
| 1169 |
+
self.mrange = M366RANGE
|
| 1170 |
+
|
| 1171 |
+
if not rr._byweekno:
|
| 1172 |
+
self.wnomask = None
|
| 1173 |
+
else:
|
| 1174 |
+
self.wnomask = [0]*(self.yearlen+7)
|
| 1175 |
+
# no1wkst = firstwkst = self.wdaymask.index(rr._wkst)
|
| 1176 |
+
no1wkst = firstwkst = (7-self.yearweekday+rr._wkst) % 7
|
| 1177 |
+
if no1wkst >= 4:
|
| 1178 |
+
no1wkst = 0
|
| 1179 |
+
# Number of days in the year, plus the days we got
|
| 1180 |
+
# from last year.
|
| 1181 |
+
wyearlen = self.yearlen+(self.yearweekday-rr._wkst) % 7
|
| 1182 |
+
else:
|
| 1183 |
+
# Number of days in the year, minus the days we
|
| 1184 |
+
# left in last year.
|
| 1185 |
+
wyearlen = self.yearlen-no1wkst
|
| 1186 |
+
div, mod = divmod(wyearlen, 7)
|
| 1187 |
+
numweeks = div+mod//4
|
| 1188 |
+
for n in rr._byweekno:
|
| 1189 |
+
if n < 0:
|
| 1190 |
+
n += numweeks+1
|
| 1191 |
+
if not (0 < n <= numweeks):
|
| 1192 |
+
continue
|
| 1193 |
+
if n > 1:
|
| 1194 |
+
i = no1wkst+(n-1)*7
|
| 1195 |
+
if no1wkst != firstwkst:
|
| 1196 |
+
i -= 7-firstwkst
|
| 1197 |
+
else:
|
| 1198 |
+
i = no1wkst
|
| 1199 |
+
for j in range(7):
|
| 1200 |
+
self.wnomask[i] = 1
|
| 1201 |
+
i += 1
|
| 1202 |
+
if self.wdaymask[i] == rr._wkst:
|
| 1203 |
+
break
|
| 1204 |
+
if 1 in rr._byweekno:
|
| 1205 |
+
# Check week number 1 of next year as well
|
| 1206 |
+
# TODO: Check -numweeks for next year.
|
| 1207 |
+
i = no1wkst+numweeks*7
|
| 1208 |
+
if no1wkst != firstwkst:
|
| 1209 |
+
i -= 7-firstwkst
|
| 1210 |
+
if i < self.yearlen:
|
| 1211 |
+
# If week starts in next year, we
|
| 1212 |
+
# don't care about it.
|
| 1213 |
+
for j in range(7):
|
| 1214 |
+
self.wnomask[i] = 1
|
| 1215 |
+
i += 1
|
| 1216 |
+
if self.wdaymask[i] == rr._wkst:
|
| 1217 |
+
break
|
| 1218 |
+
if no1wkst:
|
| 1219 |
+
# Check last week number of last year as
|
| 1220 |
+
# well. If no1wkst is 0, either the year
|
| 1221 |
+
# started on week start, or week number 1
|
| 1222 |
+
# got days from last year, so there are no
|
| 1223 |
+
# days from last year's last week number in
|
| 1224 |
+
# this year.
|
| 1225 |
+
if -1 not in rr._byweekno:
|
| 1226 |
+
lyearweekday = datetime.date(year-1, 1, 1).weekday()
|
| 1227 |
+
lno1wkst = (7-lyearweekday+rr._wkst) % 7
|
| 1228 |
+
lyearlen = 365+calendar.isleap(year-1)
|
| 1229 |
+
if lno1wkst >= 4:
|
| 1230 |
+
lno1wkst = 0
|
| 1231 |
+
lnumweeks = 52+(lyearlen +
|
| 1232 |
+
(lyearweekday-rr._wkst) % 7) % 7//4
|
| 1233 |
+
else:
|
| 1234 |
+
lnumweeks = 52+(self.yearlen-no1wkst) % 7//4
|
| 1235 |
+
else:
|
| 1236 |
+
lnumweeks = -1
|
| 1237 |
+
if lnumweeks in rr._byweekno:
|
| 1238 |
+
for i in range(no1wkst):
|
| 1239 |
+
self.wnomask[i] = 1
|
| 1240 |
+
|
| 1241 |
+
if (rr._bynweekday and (month != self.lastmonth or
|
| 1242 |
+
year != self.lastyear)):
|
| 1243 |
+
ranges = []
|
| 1244 |
+
if rr._freq == YEARLY:
|
| 1245 |
+
if rr._bymonth:
|
| 1246 |
+
for month in rr._bymonth:
|
| 1247 |
+
ranges.append(self.mrange[month-1:month+1])
|
| 1248 |
+
else:
|
| 1249 |
+
ranges = [(0, self.yearlen)]
|
| 1250 |
+
elif rr._freq == MONTHLY:
|
| 1251 |
+
ranges = [self.mrange[month-1:month+1]]
|
| 1252 |
+
if ranges:
|
| 1253 |
+
# Weekly frequency won't get here, so we may not
|
| 1254 |
+
# care about cross-year weekly periods.
|
| 1255 |
+
self.nwdaymask = [0]*self.yearlen
|
| 1256 |
+
for first, last in ranges:
|
| 1257 |
+
last -= 1
|
| 1258 |
+
for wday, n in rr._bynweekday:
|
| 1259 |
+
if n < 0:
|
| 1260 |
+
i = last+(n+1)*7
|
| 1261 |
+
i -= (self.wdaymask[i]-wday) % 7
|
| 1262 |
+
else:
|
| 1263 |
+
i = first+(n-1)*7
|
| 1264 |
+
i += (7-self.wdaymask[i]+wday) % 7
|
| 1265 |
+
if first <= i <= last:
|
| 1266 |
+
self.nwdaymask[i] = 1
|
| 1267 |
+
|
| 1268 |
+
if rr._byeaster:
|
| 1269 |
+
self.eastermask = [0]*(self.yearlen+7)
|
| 1270 |
+
eyday = easter.easter(year).toordinal()-self.yearordinal
|
| 1271 |
+
for offset in rr._byeaster:
|
| 1272 |
+
self.eastermask[eyday+offset] = 1
|
| 1273 |
+
|
| 1274 |
+
self.lastyear = year
|
| 1275 |
+
self.lastmonth = month
|
| 1276 |
+
|
| 1277 |
+
def ydayset(self, year, month, day):
|
| 1278 |
+
return list(range(self.yearlen)), 0, self.yearlen
|
| 1279 |
+
|
| 1280 |
+
def mdayset(self, year, month, day):
|
| 1281 |
+
dset = [None]*self.yearlen
|
| 1282 |
+
start, end = self.mrange[month-1:month+1]
|
| 1283 |
+
for i in range(start, end):
|
| 1284 |
+
dset[i] = i
|
| 1285 |
+
return dset, start, end
|
| 1286 |
+
|
| 1287 |
+
def wdayset(self, year, month, day):
|
| 1288 |
+
# We need to handle cross-year weeks here.
|
| 1289 |
+
dset = [None]*(self.yearlen+7)
|
| 1290 |
+
i = datetime.date(year, month, day).toordinal()-self.yearordinal
|
| 1291 |
+
start = i
|
| 1292 |
+
for j in range(7):
|
| 1293 |
+
dset[i] = i
|
| 1294 |
+
i += 1
|
| 1295 |
+
# if (not (0 <= i < self.yearlen) or
|
| 1296 |
+
# self.wdaymask[i] == self.rrule._wkst):
|
| 1297 |
+
# This will cross the year boundary, if necessary.
|
| 1298 |
+
if self.wdaymask[i] == self.rrule._wkst:
|
| 1299 |
+
break
|
| 1300 |
+
return dset, start, i
|
| 1301 |
+
|
| 1302 |
+
def ddayset(self, year, month, day):
|
| 1303 |
+
dset = [None] * self.yearlen
|
| 1304 |
+
i = datetime.date(year, month, day).toordinal() - self.yearordinal
|
| 1305 |
+
dset[i] = i
|
| 1306 |
+
return dset, i, i + 1
|
| 1307 |
+
|
| 1308 |
+
def htimeset(self, hour, minute, second):
|
| 1309 |
+
tset = []
|
| 1310 |
+
rr = self.rrule
|
| 1311 |
+
for minute in rr._byminute:
|
| 1312 |
+
for second in rr._bysecond:
|
| 1313 |
+
tset.append(datetime.time(hour, minute, second,
|
| 1314 |
+
tzinfo=rr._tzinfo))
|
| 1315 |
+
tset.sort()
|
| 1316 |
+
return tset
|
| 1317 |
+
|
| 1318 |
+
def mtimeset(self, hour, minute, second):
|
| 1319 |
+
tset = []
|
| 1320 |
+
rr = self.rrule
|
| 1321 |
+
for second in rr._bysecond:
|
| 1322 |
+
tset.append(datetime.time(hour, minute, second, tzinfo=rr._tzinfo))
|
| 1323 |
+
tset.sort()
|
| 1324 |
+
return tset
|
| 1325 |
+
|
| 1326 |
+
def stimeset(self, hour, minute, second):
|
| 1327 |
+
return (datetime.time(hour, minute, second,
|
| 1328 |
+
tzinfo=self.rrule._tzinfo),)
|
| 1329 |
+
|
| 1330 |
+
|
| 1331 |
+
class rruleset(rrulebase):
|
| 1332 |
+
""" The rruleset type allows more complex recurrence setups, mixing
|
| 1333 |
+
multiple rules, dates, exclusion rules, and exclusion dates. The type
|
| 1334 |
+
constructor takes the following keyword arguments:
|
| 1335 |
+
|
| 1336 |
+
:param cache: If True, caching of results will be enabled, improving
|
| 1337 |
+
performance of multiple queries considerably. """
|
| 1338 |
+
|
| 1339 |
+
class _genitem(object):
|
| 1340 |
+
def __init__(self, genlist, gen):
|
| 1341 |
+
try:
|
| 1342 |
+
self.dt = advance_iterator(gen)
|
| 1343 |
+
genlist.append(self)
|
| 1344 |
+
except StopIteration:
|
| 1345 |
+
pass
|
| 1346 |
+
self.genlist = genlist
|
| 1347 |
+
self.gen = gen
|
| 1348 |
+
|
| 1349 |
+
def __next__(self):
|
| 1350 |
+
try:
|
| 1351 |
+
self.dt = advance_iterator(self.gen)
|
| 1352 |
+
except StopIteration:
|
| 1353 |
+
if self.genlist[0] is self:
|
| 1354 |
+
heapq.heappop(self.genlist)
|
| 1355 |
+
else:
|
| 1356 |
+
self.genlist.remove(self)
|
| 1357 |
+
heapq.heapify(self.genlist)
|
| 1358 |
+
|
| 1359 |
+
next = __next__
|
| 1360 |
+
|
| 1361 |
+
def __lt__(self, other):
|
| 1362 |
+
return self.dt < other.dt
|
| 1363 |
+
|
| 1364 |
+
def __gt__(self, other):
|
| 1365 |
+
return self.dt > other.dt
|
| 1366 |
+
|
| 1367 |
+
def __eq__(self, other):
|
| 1368 |
+
return self.dt == other.dt
|
| 1369 |
+
|
| 1370 |
+
def __ne__(self, other):
|
| 1371 |
+
return self.dt != other.dt
|
| 1372 |
+
|
| 1373 |
+
def __init__(self, cache=False):
|
| 1374 |
+
super(rruleset, self).__init__(cache)
|
| 1375 |
+
self._rrule = []
|
| 1376 |
+
self._rdate = []
|
| 1377 |
+
self._exrule = []
|
| 1378 |
+
self._exdate = []
|
| 1379 |
+
|
| 1380 |
+
@_invalidates_cache
|
| 1381 |
+
def rrule(self, rrule):
|
| 1382 |
+
""" Include the given :py:class:`rrule` instance in the recurrence set
|
| 1383 |
+
generation. """
|
| 1384 |
+
self._rrule.append(rrule)
|
| 1385 |
+
|
| 1386 |
+
@_invalidates_cache
|
| 1387 |
+
def rdate(self, rdate):
|
| 1388 |
+
""" Include the given :py:class:`datetime` instance in the recurrence
|
| 1389 |
+
set generation. """
|
| 1390 |
+
self._rdate.append(rdate)
|
| 1391 |
+
|
| 1392 |
+
@_invalidates_cache
|
| 1393 |
+
def exrule(self, exrule):
|
| 1394 |
+
""" Include the given rrule instance in the recurrence set exclusion
|
| 1395 |
+
list. Dates which are part of the given recurrence rules will not
|
| 1396 |
+
be generated, even if some inclusive rrule or rdate matches them.
|
| 1397 |
+
"""
|
| 1398 |
+
self._exrule.append(exrule)
|
| 1399 |
+
|
| 1400 |
+
@_invalidates_cache
|
| 1401 |
+
def exdate(self, exdate):
|
| 1402 |
+
""" Include the given datetime instance in the recurrence set
|
| 1403 |
+
exclusion list. Dates included that way will not be generated,
|
| 1404 |
+
even if some inclusive rrule or rdate matches them. """
|
| 1405 |
+
self._exdate.append(exdate)
|
| 1406 |
+
|
| 1407 |
+
def _iter(self):
|
| 1408 |
+
rlist = []
|
| 1409 |
+
self._rdate.sort()
|
| 1410 |
+
self._genitem(rlist, iter(self._rdate))
|
| 1411 |
+
for gen in [iter(x) for x in self._rrule]:
|
| 1412 |
+
self._genitem(rlist, gen)
|
| 1413 |
+
exlist = []
|
| 1414 |
+
self._exdate.sort()
|
| 1415 |
+
self._genitem(exlist, iter(self._exdate))
|
| 1416 |
+
for gen in [iter(x) for x in self._exrule]:
|
| 1417 |
+
self._genitem(exlist, gen)
|
| 1418 |
+
lastdt = None
|
| 1419 |
+
total = 0
|
| 1420 |
+
heapq.heapify(rlist)
|
| 1421 |
+
heapq.heapify(exlist)
|
| 1422 |
+
while rlist:
|
| 1423 |
+
ritem = rlist[0]
|
| 1424 |
+
if not lastdt or lastdt != ritem.dt:
|
| 1425 |
+
while exlist and exlist[0] < ritem:
|
| 1426 |
+
exitem = exlist[0]
|
| 1427 |
+
advance_iterator(exitem)
|
| 1428 |
+
if exlist and exlist[0] is exitem:
|
| 1429 |
+
heapq.heapreplace(exlist, exitem)
|
| 1430 |
+
if not exlist or ritem != exlist[0]:
|
| 1431 |
+
total += 1
|
| 1432 |
+
yield ritem.dt
|
| 1433 |
+
lastdt = ritem.dt
|
| 1434 |
+
advance_iterator(ritem)
|
| 1435 |
+
if rlist and rlist[0] is ritem:
|
| 1436 |
+
heapq.heapreplace(rlist, ritem)
|
| 1437 |
+
self._len = total
|
| 1438 |
+
|
| 1439 |
+
|
| 1440 |
+
|
| 1441 |
+
|
| 1442 |
+
class _rrulestr(object):
|
| 1443 |
+
""" Parses a string representation of a recurrence rule or set of
|
| 1444 |
+
recurrence rules.
|
| 1445 |
+
|
| 1446 |
+
:param s:
|
| 1447 |
+
Required, a string defining one or more recurrence rules.
|
| 1448 |
+
|
| 1449 |
+
:param dtstart:
|
| 1450 |
+
If given, used as the default recurrence start if not specified in the
|
| 1451 |
+
rule string.
|
| 1452 |
+
|
| 1453 |
+
:param cache:
|
| 1454 |
+
If set ``True`` caching of results will be enabled, improving
|
| 1455 |
+
performance of multiple queries considerably.
|
| 1456 |
+
|
| 1457 |
+
:param unfold:
|
| 1458 |
+
If set ``True`` indicates that a rule string is split over more
|
| 1459 |
+
than one line and should be joined before processing.
|
| 1460 |
+
|
| 1461 |
+
:param forceset:
|
| 1462 |
+
If set ``True`` forces a :class:`dateutil.rrule.rruleset` to
|
| 1463 |
+
be returned.
|
| 1464 |
+
|
| 1465 |
+
:param compatible:
|
| 1466 |
+
If set ``True`` forces ``unfold`` and ``forceset`` to be ``True``.
|
| 1467 |
+
|
| 1468 |
+
:param ignoretz:
|
| 1469 |
+
If set ``True``, time zones in parsed strings are ignored and a naive
|
| 1470 |
+
:class:`datetime.datetime` object is returned.
|
| 1471 |
+
|
| 1472 |
+
:param tzids:
|
| 1473 |
+
If given, a callable or mapping used to retrieve a
|
| 1474 |
+
:class:`datetime.tzinfo` from a string representation.
|
| 1475 |
+
Defaults to :func:`dateutil.tz.gettz`.
|
| 1476 |
+
|
| 1477 |
+
:param tzinfos:
|
| 1478 |
+
Additional time zone names / aliases which may be present in a string
|
| 1479 |
+
representation. See :func:`dateutil.parser.parse` for more
|
| 1480 |
+
information.
|
| 1481 |
+
|
| 1482 |
+
:return:
|
| 1483 |
+
Returns a :class:`dateutil.rrule.rruleset` or
|
| 1484 |
+
:class:`dateutil.rrule.rrule`
|
| 1485 |
+
"""
|
| 1486 |
+
|
| 1487 |
+
_freq_map = {"YEARLY": YEARLY,
|
| 1488 |
+
"MONTHLY": MONTHLY,
|
| 1489 |
+
"WEEKLY": WEEKLY,
|
| 1490 |
+
"DAILY": DAILY,
|
| 1491 |
+
"HOURLY": HOURLY,
|
| 1492 |
+
"MINUTELY": MINUTELY,
|
| 1493 |
+
"SECONDLY": SECONDLY}
|
| 1494 |
+
|
| 1495 |
+
_weekday_map = {"MO": 0, "TU": 1, "WE": 2, "TH": 3,
|
| 1496 |
+
"FR": 4, "SA": 5, "SU": 6}
|
| 1497 |
+
|
| 1498 |
+
def _handle_int(self, rrkwargs, name, value, **kwargs):
|
| 1499 |
+
rrkwargs[name.lower()] = int(value)
|
| 1500 |
+
|
| 1501 |
+
def _handle_int_list(self, rrkwargs, name, value, **kwargs):
|
| 1502 |
+
rrkwargs[name.lower()] = [int(x) for x in value.split(',')]
|
| 1503 |
+
|
| 1504 |
+
_handle_INTERVAL = _handle_int
|
| 1505 |
+
_handle_COUNT = _handle_int
|
| 1506 |
+
_handle_BYSETPOS = _handle_int_list
|
| 1507 |
+
_handle_BYMONTH = _handle_int_list
|
| 1508 |
+
_handle_BYMONTHDAY = _handle_int_list
|
| 1509 |
+
_handle_BYYEARDAY = _handle_int_list
|
| 1510 |
+
_handle_BYEASTER = _handle_int_list
|
| 1511 |
+
_handle_BYWEEKNO = _handle_int_list
|
| 1512 |
+
_handle_BYHOUR = _handle_int_list
|
| 1513 |
+
_handle_BYMINUTE = _handle_int_list
|
| 1514 |
+
_handle_BYSECOND = _handle_int_list
|
| 1515 |
+
|
| 1516 |
+
def _handle_FREQ(self, rrkwargs, name, value, **kwargs):
|
| 1517 |
+
rrkwargs["freq"] = self._freq_map[value]
|
| 1518 |
+
|
| 1519 |
+
def _handle_UNTIL(self, rrkwargs, name, value, **kwargs):
|
| 1520 |
+
global parser
|
| 1521 |
+
if not parser:
|
| 1522 |
+
from dateutil import parser
|
| 1523 |
+
try:
|
| 1524 |
+
rrkwargs["until"] = parser.parse(value,
|
| 1525 |
+
ignoretz=kwargs.get("ignoretz"),
|
| 1526 |
+
tzinfos=kwargs.get("tzinfos"))
|
| 1527 |
+
except ValueError:
|
| 1528 |
+
raise ValueError("invalid until date")
|
| 1529 |
+
|
| 1530 |
+
def _handle_WKST(self, rrkwargs, name, value, **kwargs):
|
| 1531 |
+
rrkwargs["wkst"] = self._weekday_map[value]
|
| 1532 |
+
|
| 1533 |
+
def _handle_BYWEEKDAY(self, rrkwargs, name, value, **kwargs):
|
| 1534 |
+
"""
|
| 1535 |
+
Two ways to specify this: +1MO or MO(+1)
|
| 1536 |
+
"""
|
| 1537 |
+
l = []
|
| 1538 |
+
for wday in value.split(','):
|
| 1539 |
+
if '(' in wday:
|
| 1540 |
+
# If it's of the form TH(+1), etc.
|
| 1541 |
+
splt = wday.split('(')
|
| 1542 |
+
w = splt[0]
|
| 1543 |
+
n = int(splt[1][:-1])
|
| 1544 |
+
elif len(wday):
|
| 1545 |
+
# If it's of the form +1MO
|
| 1546 |
+
for i in range(len(wday)):
|
| 1547 |
+
if wday[i] not in '+-0123456789':
|
| 1548 |
+
break
|
| 1549 |
+
n = wday[:i] or None
|
| 1550 |
+
w = wday[i:]
|
| 1551 |
+
if n:
|
| 1552 |
+
n = int(n)
|
| 1553 |
+
else:
|
| 1554 |
+
raise ValueError("Invalid (empty) BYDAY specification.")
|
| 1555 |
+
|
| 1556 |
+
l.append(weekdays[self._weekday_map[w]](n))
|
| 1557 |
+
rrkwargs["byweekday"] = l
|
| 1558 |
+
|
| 1559 |
+
_handle_BYDAY = _handle_BYWEEKDAY
|
| 1560 |
+
|
| 1561 |
+
def _parse_rfc_rrule(self, line,
|
| 1562 |
+
dtstart=None,
|
| 1563 |
+
cache=False,
|
| 1564 |
+
ignoretz=False,
|
| 1565 |
+
tzinfos=None):
|
| 1566 |
+
if line.find(':') != -1:
|
| 1567 |
+
name, value = line.split(':')
|
| 1568 |
+
if name != "RRULE":
|
| 1569 |
+
raise ValueError("unknown parameter name")
|
| 1570 |
+
else:
|
| 1571 |
+
value = line
|
| 1572 |
+
rrkwargs = {}
|
| 1573 |
+
for pair in value.split(';'):
|
| 1574 |
+
name, value = pair.split('=')
|
| 1575 |
+
name = name.upper()
|
| 1576 |
+
value = value.upper()
|
| 1577 |
+
try:
|
| 1578 |
+
getattr(self, "_handle_"+name)(rrkwargs, name, value,
|
| 1579 |
+
ignoretz=ignoretz,
|
| 1580 |
+
tzinfos=tzinfos)
|
| 1581 |
+
except AttributeError:
|
| 1582 |
+
raise ValueError("unknown parameter '%s'" % name)
|
| 1583 |
+
except (KeyError, ValueError):
|
| 1584 |
+
raise ValueError("invalid '%s': %s" % (name, value))
|
| 1585 |
+
return rrule(dtstart=dtstart, cache=cache, **rrkwargs)
|
| 1586 |
+
|
| 1587 |
+
def _parse_date_value(self, date_value, parms, rule_tzids,
|
| 1588 |
+
ignoretz, tzids, tzinfos):
|
| 1589 |
+
global parser
|
| 1590 |
+
if not parser:
|
| 1591 |
+
from dateutil import parser
|
| 1592 |
+
|
| 1593 |
+
datevals = []
|
| 1594 |
+
value_found = False
|
| 1595 |
+
TZID = None
|
| 1596 |
+
|
| 1597 |
+
for parm in parms:
|
| 1598 |
+
if parm.startswith("TZID="):
|
| 1599 |
+
try:
|
| 1600 |
+
tzkey = rule_tzids[parm.split('TZID=')[-1]]
|
| 1601 |
+
except KeyError:
|
| 1602 |
+
continue
|
| 1603 |
+
if tzids is None:
|
| 1604 |
+
from . import tz
|
| 1605 |
+
tzlookup = tz.gettz
|
| 1606 |
+
elif callable(tzids):
|
| 1607 |
+
tzlookup = tzids
|
| 1608 |
+
else:
|
| 1609 |
+
tzlookup = getattr(tzids, 'get', None)
|
| 1610 |
+
if tzlookup is None:
|
| 1611 |
+
msg = ('tzids must be a callable, mapping, or None, '
|
| 1612 |
+
'not %s' % tzids)
|
| 1613 |
+
raise ValueError(msg)
|
| 1614 |
+
|
| 1615 |
+
TZID = tzlookup(tzkey)
|
| 1616 |
+
continue
|
| 1617 |
+
|
| 1618 |
+
# RFC 5445 3.8.2.4: The VALUE parameter is optional, but may be found
|
| 1619 |
+
# only once.
|
| 1620 |
+
if parm not in {"VALUE=DATE-TIME", "VALUE=DATE"}:
|
| 1621 |
+
raise ValueError("unsupported parm: " + parm)
|
| 1622 |
+
else:
|
| 1623 |
+
if value_found:
|
| 1624 |
+
msg = ("Duplicate value parameter found in: " + parm)
|
| 1625 |
+
raise ValueError(msg)
|
| 1626 |
+
value_found = True
|
| 1627 |
+
|
| 1628 |
+
for datestr in date_value.split(','):
|
| 1629 |
+
date = parser.parse(datestr, ignoretz=ignoretz, tzinfos=tzinfos)
|
| 1630 |
+
if TZID is not None:
|
| 1631 |
+
if date.tzinfo is None:
|
| 1632 |
+
date = date.replace(tzinfo=TZID)
|
| 1633 |
+
else:
|
| 1634 |
+
raise ValueError('DTSTART/EXDATE specifies multiple timezone')
|
| 1635 |
+
datevals.append(date)
|
| 1636 |
+
|
| 1637 |
+
return datevals
|
| 1638 |
+
|
| 1639 |
+
def _parse_rfc(self, s,
|
| 1640 |
+
dtstart=None,
|
| 1641 |
+
cache=False,
|
| 1642 |
+
unfold=False,
|
| 1643 |
+
forceset=False,
|
| 1644 |
+
compatible=False,
|
| 1645 |
+
ignoretz=False,
|
| 1646 |
+
tzids=None,
|
| 1647 |
+
tzinfos=None):
|
| 1648 |
+
global parser
|
| 1649 |
+
if compatible:
|
| 1650 |
+
forceset = True
|
| 1651 |
+
unfold = True
|
| 1652 |
+
|
| 1653 |
+
TZID_NAMES = dict(map(
|
| 1654 |
+
lambda x: (x.upper(), x),
|
| 1655 |
+
re.findall('TZID=(?P<name>[^:]+):', s)
|
| 1656 |
+
))
|
| 1657 |
+
s = s.upper()
|
| 1658 |
+
if not s.strip():
|
| 1659 |
+
raise ValueError("empty string")
|
| 1660 |
+
if unfold:
|
| 1661 |
+
lines = s.splitlines()
|
| 1662 |
+
i = 0
|
| 1663 |
+
while i < len(lines):
|
| 1664 |
+
line = lines[i].rstrip()
|
| 1665 |
+
if not line:
|
| 1666 |
+
del lines[i]
|
| 1667 |
+
elif i > 0 and line[0] == " ":
|
| 1668 |
+
lines[i-1] += line[1:]
|
| 1669 |
+
del lines[i]
|
| 1670 |
+
else:
|
| 1671 |
+
i += 1
|
| 1672 |
+
else:
|
| 1673 |
+
lines = s.split()
|
| 1674 |
+
if (not forceset and len(lines) == 1 and (s.find(':') == -1 or
|
| 1675 |
+
s.startswith('RRULE:'))):
|
| 1676 |
+
return self._parse_rfc_rrule(lines[0], cache=cache,
|
| 1677 |
+
dtstart=dtstart, ignoretz=ignoretz,
|
| 1678 |
+
tzinfos=tzinfos)
|
| 1679 |
+
else:
|
| 1680 |
+
rrulevals = []
|
| 1681 |
+
rdatevals = []
|
| 1682 |
+
exrulevals = []
|
| 1683 |
+
exdatevals = []
|
| 1684 |
+
for line in lines:
|
| 1685 |
+
if not line:
|
| 1686 |
+
continue
|
| 1687 |
+
if line.find(':') == -1:
|
| 1688 |
+
name = "RRULE"
|
| 1689 |
+
value = line
|
| 1690 |
+
else:
|
| 1691 |
+
name, value = line.split(':', 1)
|
| 1692 |
+
parms = name.split(';')
|
| 1693 |
+
if not parms:
|
| 1694 |
+
raise ValueError("empty property name")
|
| 1695 |
+
name = parms[0]
|
| 1696 |
+
parms = parms[1:]
|
| 1697 |
+
if name == "RRULE":
|
| 1698 |
+
for parm in parms:
|
| 1699 |
+
if parm.startswith("X-"):
|
| 1700 |
+
# Experimental random vendor features; safe to ignore
|
| 1701 |
+
continue
|
| 1702 |
+
raise ValueError("unsupported RRULE parm: " + parm)
|
| 1703 |
+
rrulevals.append(value)
|
| 1704 |
+
elif name == "RDATE":
|
| 1705 |
+
for parm in parms:
|
| 1706 |
+
if parm != "VALUE=DATE-TIME":
|
| 1707 |
+
raise ValueError("unsupported RDATE parm: "+parm)
|
| 1708 |
+
rdatevals.append(value)
|
| 1709 |
+
elif name == "EXRULE":
|
| 1710 |
+
for parm in parms:
|
| 1711 |
+
raise ValueError("unsupported EXRULE parm: "+parm)
|
| 1712 |
+
exrulevals.append(value)
|
| 1713 |
+
elif name == "EXDATE":
|
| 1714 |
+
exdatevals.extend(
|
| 1715 |
+
self._parse_date_value(value, parms,
|
| 1716 |
+
TZID_NAMES, ignoretz,
|
| 1717 |
+
tzids, tzinfos)
|
| 1718 |
+
)
|
| 1719 |
+
elif name == "DTSTART":
|
| 1720 |
+
dtvals = self._parse_date_value(value, parms, TZID_NAMES,
|
| 1721 |
+
ignoretz, tzids, tzinfos)
|
| 1722 |
+
if len(dtvals) != 1:
|
| 1723 |
+
raise ValueError("Multiple DTSTART values specified:" +
|
| 1724 |
+
value)
|
| 1725 |
+
dtstart = dtvals[0]
|
| 1726 |
+
else:
|
| 1727 |
+
raise ValueError("unsupported property: "+name)
|
| 1728 |
+
if (forceset or len(rrulevals) > 1 or rdatevals
|
| 1729 |
+
or exrulevals or exdatevals):
|
| 1730 |
+
if not parser and (rdatevals or exdatevals):
|
| 1731 |
+
from dateutil import parser
|
| 1732 |
+
rset = rruleset(cache=cache)
|
| 1733 |
+
for value in rrulevals:
|
| 1734 |
+
rset.rrule(self._parse_rfc_rrule(value, dtstart=dtstart,
|
| 1735 |
+
ignoretz=ignoretz,
|
| 1736 |
+
tzinfos=tzinfos))
|
| 1737 |
+
for value in rdatevals:
|
| 1738 |
+
for datestr in value.split(','):
|
| 1739 |
+
rset.rdate(parser.parse(datestr,
|
| 1740 |
+
ignoretz=ignoretz,
|
| 1741 |
+
tzinfos=tzinfos))
|
| 1742 |
+
for value in exrulevals:
|
| 1743 |
+
rset.exrule(self._parse_rfc_rrule(value, dtstart=dtstart,
|
| 1744 |
+
ignoretz=ignoretz,
|
| 1745 |
+
tzinfos=tzinfos))
|
| 1746 |
+
for value in exdatevals:
|
| 1747 |
+
rset.exdate(value)
|
| 1748 |
+
if compatible and dtstart:
|
| 1749 |
+
rset.rdate(dtstart)
|
| 1750 |
+
return rset
|
| 1751 |
+
else:
|
| 1752 |
+
return self._parse_rfc_rrule(rrulevals[0],
|
| 1753 |
+
dtstart=dtstart,
|
| 1754 |
+
cache=cache,
|
| 1755 |
+
ignoretz=ignoretz,
|
| 1756 |
+
tzinfos=tzinfos)
|
| 1757 |
+
|
| 1758 |
+
def __call__(self, s, **kwargs):
|
| 1759 |
+
return self._parse_rfc(s, **kwargs)
|
| 1760 |
+
|
| 1761 |
+
|
| 1762 |
+
rrulestr = _rrulestr()
|
| 1763 |
+
|
| 1764 |
+
# vim:ts=4:sw=4:et
|
hug/lib/python3.12/site-packages/dateutil/tz/__init__.py
ADDED
|
@@ -0,0 +1,60 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import sys
|
| 3 |
+
|
| 4 |
+
from . import _tzpath
|
| 5 |
+
from ._tzpath import reset_tzpath
|
| 6 |
+
from .tz import *
|
| 7 |
+
from .tz import __doc__
|
| 8 |
+
|
| 9 |
+
__all__ = [
|
| 10 |
+
"tzutc",
|
| 11 |
+
"tzoffset",
|
| 12 |
+
"tzlocal",
|
| 13 |
+
"tzfile",
|
| 14 |
+
"tzrange",
|
| 15 |
+
"tzstr",
|
| 16 |
+
"tzical",
|
| 17 |
+
"tzwin",
|
| 18 |
+
"tzwinlocal",
|
| 19 |
+
"gettz",
|
| 20 |
+
"enfold",
|
| 21 |
+
"datetime_ambiguous",
|
| 22 |
+
"datetime_exists",
|
| 23 |
+
"resolve_imaginary",
|
| 24 |
+
"available_iana_timezones",
|
| 25 |
+
"UTC",
|
| 26 |
+
"TZPATH",
|
| 27 |
+
"reset_tzpath",
|
| 28 |
+
"DeprecatedTzFormatWarning",
|
| 29 |
+
"DeprecatedTzKeyWarning",
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
if sys.version_info < (3, 7):
|
| 33 |
+
# Module-level __getattr__ was added in Python 3.7, so instead of lazily
|
| 34 |
+
# populating TZPATH on every access, we will register a callback with
|
| 35 |
+
# reset_tzpath to update the top-level tuple.
|
| 36 |
+
TZPATH = _tzpath.TZPATH
|
| 37 |
+
|
| 38 |
+
def _tzpath_callback(new_tzpath):
|
| 39 |
+
global TZPATH
|
| 40 |
+
TZPATH = new_tzpath
|
| 41 |
+
|
| 42 |
+
_tzpath.TZPATH_CALLBACKS.append(_tzpath_callback)
|
| 43 |
+
|
| 44 |
+
else:
|
| 45 |
+
|
| 46 |
+
def __getattr__(name):
|
| 47 |
+
if name == "TZPATH":
|
| 48 |
+
return _tzpath.TZPATH
|
| 49 |
+
else:
|
| 50 |
+
raise AttributeError(
|
| 51 |
+
"Module %s has no attribute %s" % (repr(__name__), repr(name))
|
| 52 |
+
)
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
def __dir__():
|
| 56 |
+
return sorted(list(globals()) + ["TZPATH"])
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
class DeprecatedTzFormatWarning(Warning):
|
| 60 |
+
"""Warning raised when time zones are parsed from deprecated formats."""
|
hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (1.75 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_common.cpython-312.pyc
ADDED
|
Binary file (14.2 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_factories.cpython-312.pyc
ADDED
|
Binary file (4.59 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzfile.cpython-312.pyc
ADDED
|
Binary file (47.6 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/_tzpath.cpython-312.pyc
ADDED
|
Binary file (3.97 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/tz.cpython-312.pyc
ADDED
|
Binary file (54.3 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/tz/__pycache__/win.cpython-312.pyc
ADDED
|
Binary file (17.3 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/tz/_common.py
ADDED
|
@@ -0,0 +1,419 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from six import PY2
|
| 2 |
+
|
| 3 |
+
from functools import wraps
|
| 4 |
+
|
| 5 |
+
from datetime import datetime, timedelta, tzinfo
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
ZERO = timedelta(0)
|
| 9 |
+
|
| 10 |
+
__all__ = ['tzname_in_python2', 'enfold']
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def tzname_in_python2(namefunc):
|
| 14 |
+
"""Change unicode output into bytestrings in Python 2
|
| 15 |
+
|
| 16 |
+
tzname() API changed in Python 3. It used to return bytes, but was changed
|
| 17 |
+
to unicode strings
|
| 18 |
+
"""
|
| 19 |
+
if PY2:
|
| 20 |
+
@wraps(namefunc)
|
| 21 |
+
def adjust_encoding(*args, **kwargs):
|
| 22 |
+
name = namefunc(*args, **kwargs)
|
| 23 |
+
if name is not None:
|
| 24 |
+
name = name.encode()
|
| 25 |
+
|
| 26 |
+
return name
|
| 27 |
+
|
| 28 |
+
return adjust_encoding
|
| 29 |
+
else:
|
| 30 |
+
return namefunc
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
# The following is adapted from Alexander Belopolsky's tz library
|
| 34 |
+
# https://github.com/abalkin/tz
|
| 35 |
+
if hasattr(datetime, 'fold'):
|
| 36 |
+
# This is the pre-python 3.6 fold situation
|
| 37 |
+
def enfold(dt, fold=1):
|
| 38 |
+
"""
|
| 39 |
+
Provides a unified interface for assigning the ``fold`` attribute to
|
| 40 |
+
datetimes both before and after the implementation of PEP-495.
|
| 41 |
+
|
| 42 |
+
:param fold:
|
| 43 |
+
The value for the ``fold`` attribute in the returned datetime. This
|
| 44 |
+
should be either 0 or 1.
|
| 45 |
+
|
| 46 |
+
:return:
|
| 47 |
+
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
| 48 |
+
``fold`` for all versions of Python. In versions prior to
|
| 49 |
+
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
| 50 |
+
subclass of :py:class:`datetime.datetime` with the ``fold``
|
| 51 |
+
attribute added, if ``fold`` is 1.
|
| 52 |
+
|
| 53 |
+
.. versionadded:: 2.6.0
|
| 54 |
+
"""
|
| 55 |
+
return dt.replace(fold=fold)
|
| 56 |
+
|
| 57 |
+
else:
|
| 58 |
+
class _DatetimeWithFold(datetime):
|
| 59 |
+
"""
|
| 60 |
+
This is a class designed to provide a PEP 495-compliant interface for
|
| 61 |
+
Python versions before 3.6. It is used only for dates in a fold, so
|
| 62 |
+
the ``fold`` attribute is fixed at ``1``.
|
| 63 |
+
|
| 64 |
+
.. versionadded:: 2.6.0
|
| 65 |
+
"""
|
| 66 |
+
__slots__ = ()
|
| 67 |
+
|
| 68 |
+
def replace(self, *args, **kwargs):
|
| 69 |
+
"""
|
| 70 |
+
Return a datetime with the same attributes, except for those
|
| 71 |
+
attributes given new values by whichever keyword arguments are
|
| 72 |
+
specified. Note that tzinfo=None can be specified to create a naive
|
| 73 |
+
datetime from an aware datetime with no conversion of date and time
|
| 74 |
+
data.
|
| 75 |
+
|
| 76 |
+
This is reimplemented in ``_DatetimeWithFold`` because pypy3 will
|
| 77 |
+
return a ``datetime.datetime`` even if ``fold`` is unchanged.
|
| 78 |
+
"""
|
| 79 |
+
argnames = (
|
| 80 |
+
'year', 'month', 'day', 'hour', 'minute', 'second',
|
| 81 |
+
'microsecond', 'tzinfo'
|
| 82 |
+
)
|
| 83 |
+
|
| 84 |
+
for arg, argname in zip(args, argnames):
|
| 85 |
+
if argname in kwargs:
|
| 86 |
+
raise TypeError('Duplicate argument: {}'.format(argname))
|
| 87 |
+
|
| 88 |
+
kwargs[argname] = arg
|
| 89 |
+
|
| 90 |
+
for argname in argnames:
|
| 91 |
+
if argname not in kwargs:
|
| 92 |
+
kwargs[argname] = getattr(self, argname)
|
| 93 |
+
|
| 94 |
+
dt_class = self.__class__ if kwargs.get('fold', 1) else datetime
|
| 95 |
+
|
| 96 |
+
return dt_class(**kwargs)
|
| 97 |
+
|
| 98 |
+
@property
|
| 99 |
+
def fold(self):
|
| 100 |
+
return 1
|
| 101 |
+
|
| 102 |
+
def enfold(dt, fold=1):
|
| 103 |
+
"""
|
| 104 |
+
Provides a unified interface for assigning the ``fold`` attribute to
|
| 105 |
+
datetimes both before and after the implementation of PEP-495.
|
| 106 |
+
|
| 107 |
+
:param fold:
|
| 108 |
+
The value for the ``fold`` attribute in the returned datetime. This
|
| 109 |
+
should be either 0 or 1.
|
| 110 |
+
|
| 111 |
+
:return:
|
| 112 |
+
Returns an object for which ``getattr(dt, 'fold', 0)`` returns
|
| 113 |
+
``fold`` for all versions of Python. In versions prior to
|
| 114 |
+
Python 3.6, this is a ``_DatetimeWithFold`` object, which is a
|
| 115 |
+
subclass of :py:class:`datetime.datetime` with the ``fold``
|
| 116 |
+
attribute added, if ``fold`` is 1.
|
| 117 |
+
|
| 118 |
+
.. versionadded:: 2.6.0
|
| 119 |
+
"""
|
| 120 |
+
if getattr(dt, 'fold', 0) == fold:
|
| 121 |
+
return dt
|
| 122 |
+
|
| 123 |
+
args = dt.timetuple()[:6]
|
| 124 |
+
args += (dt.microsecond, dt.tzinfo)
|
| 125 |
+
|
| 126 |
+
if fold:
|
| 127 |
+
return _DatetimeWithFold(*args)
|
| 128 |
+
else:
|
| 129 |
+
return datetime(*args)
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
def _validate_fromutc_inputs(f):
|
| 133 |
+
"""
|
| 134 |
+
The CPython version of ``fromutc`` checks that the input is a ``datetime``
|
| 135 |
+
object and that ``self`` is attached as its ``tzinfo``.
|
| 136 |
+
"""
|
| 137 |
+
@wraps(f)
|
| 138 |
+
def fromutc(self, dt):
|
| 139 |
+
if not isinstance(dt, datetime):
|
| 140 |
+
raise TypeError("fromutc() requires a datetime argument")
|
| 141 |
+
if dt.tzinfo is not self:
|
| 142 |
+
raise ValueError("dt.tzinfo is not self")
|
| 143 |
+
|
| 144 |
+
return f(self, dt)
|
| 145 |
+
|
| 146 |
+
return fromutc
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
class _tzinfo(tzinfo):
|
| 150 |
+
"""
|
| 151 |
+
Base class for all ``dateutil`` ``tzinfo`` objects.
|
| 152 |
+
"""
|
| 153 |
+
|
| 154 |
+
def is_ambiguous(self, dt):
|
| 155 |
+
"""
|
| 156 |
+
Whether or not the "wall time" of a given datetime is ambiguous in this
|
| 157 |
+
zone.
|
| 158 |
+
|
| 159 |
+
:param dt:
|
| 160 |
+
A :py:class:`datetime.datetime`, naive or time zone aware.
|
| 161 |
+
|
| 162 |
+
|
| 163 |
+
:return:
|
| 164 |
+
Returns ``True`` if ambiguous, ``False`` otherwise.
|
| 165 |
+
|
| 166 |
+
.. versionadded:: 2.6.0
|
| 167 |
+
"""
|
| 168 |
+
|
| 169 |
+
dt = dt.replace(tzinfo=self)
|
| 170 |
+
|
| 171 |
+
wall_0 = enfold(dt, fold=0)
|
| 172 |
+
wall_1 = enfold(dt, fold=1)
|
| 173 |
+
|
| 174 |
+
same_offset = wall_0.utcoffset() == wall_1.utcoffset()
|
| 175 |
+
same_dt = wall_0.replace(tzinfo=None) == wall_1.replace(tzinfo=None)
|
| 176 |
+
|
| 177 |
+
return same_dt and not same_offset
|
| 178 |
+
|
| 179 |
+
def _fold_status(self, dt_utc, dt_wall):
|
| 180 |
+
"""
|
| 181 |
+
Determine the fold status of a "wall" datetime, given a representation
|
| 182 |
+
of the same datetime as a (naive) UTC datetime. This is calculated based
|
| 183 |
+
on the assumption that ``dt.utcoffset() - dt.dst()`` is constant for all
|
| 184 |
+
datetimes, and that this offset is the actual number of hours separating
|
| 185 |
+
``dt_utc`` and ``dt_wall``.
|
| 186 |
+
|
| 187 |
+
:param dt_utc:
|
| 188 |
+
Representation of the datetime as UTC
|
| 189 |
+
|
| 190 |
+
:param dt_wall:
|
| 191 |
+
Representation of the datetime as "wall time". This parameter must
|
| 192 |
+
either have a `fold` attribute or have a fold-naive
|
| 193 |
+
:class:`datetime.tzinfo` attached, otherwise the calculation may
|
| 194 |
+
fail.
|
| 195 |
+
"""
|
| 196 |
+
if self.is_ambiguous(dt_wall):
|
| 197 |
+
delta_wall = dt_wall - dt_utc
|
| 198 |
+
_fold = int(delta_wall == (dt_utc.utcoffset() - dt_utc.dst()))
|
| 199 |
+
else:
|
| 200 |
+
_fold = 0
|
| 201 |
+
|
| 202 |
+
return _fold
|
| 203 |
+
|
| 204 |
+
def _fold(self, dt):
|
| 205 |
+
return getattr(dt, 'fold', 0)
|
| 206 |
+
|
| 207 |
+
def _fromutc(self, dt):
|
| 208 |
+
"""
|
| 209 |
+
Given a timezone-aware datetime in a given timezone, calculates a
|
| 210 |
+
timezone-aware datetime in a new timezone.
|
| 211 |
+
|
| 212 |
+
Since this is the one time that we *know* we have an unambiguous
|
| 213 |
+
datetime object, we take this opportunity to determine whether the
|
| 214 |
+
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
| 215 |
+
occurrence, chronologically, of the ambiguous datetime).
|
| 216 |
+
|
| 217 |
+
:param dt:
|
| 218 |
+
A timezone-aware :class:`datetime.datetime` object.
|
| 219 |
+
"""
|
| 220 |
+
|
| 221 |
+
# Re-implement the algorithm from Python's datetime.py
|
| 222 |
+
dtoff = dt.utcoffset()
|
| 223 |
+
if dtoff is None:
|
| 224 |
+
raise ValueError("fromutc() requires a non-None utcoffset() "
|
| 225 |
+
"result")
|
| 226 |
+
|
| 227 |
+
# The original datetime.py code assumes that `dst()` defaults to
|
| 228 |
+
# zero during ambiguous times. PEP 495 inverts this presumption, so
|
| 229 |
+
# for pre-PEP 495 versions of python, we need to tweak the algorithm.
|
| 230 |
+
dtdst = dt.dst()
|
| 231 |
+
if dtdst is None:
|
| 232 |
+
raise ValueError("fromutc() requires a non-None dst() result")
|
| 233 |
+
delta = dtoff - dtdst
|
| 234 |
+
|
| 235 |
+
dt += delta
|
| 236 |
+
# Set fold=1 so we can default to being in the fold for
|
| 237 |
+
# ambiguous dates.
|
| 238 |
+
dtdst = enfold(dt, fold=1).dst()
|
| 239 |
+
if dtdst is None:
|
| 240 |
+
raise ValueError("fromutc(): dt.dst gave inconsistent "
|
| 241 |
+
"results; cannot convert")
|
| 242 |
+
return dt + dtdst
|
| 243 |
+
|
| 244 |
+
@_validate_fromutc_inputs
|
| 245 |
+
def fromutc(self, dt):
|
| 246 |
+
"""
|
| 247 |
+
Given a timezone-aware datetime in a given timezone, calculates a
|
| 248 |
+
timezone-aware datetime in a new timezone.
|
| 249 |
+
|
| 250 |
+
Since this is the one time that we *know* we have an unambiguous
|
| 251 |
+
datetime object, we take this opportunity to determine whether the
|
| 252 |
+
datetime is ambiguous and in a "fold" state (e.g. if it's the first
|
| 253 |
+
occurrence, chronologically, of the ambiguous datetime).
|
| 254 |
+
|
| 255 |
+
:param dt:
|
| 256 |
+
A timezone-aware :class:`datetime.datetime` object.
|
| 257 |
+
"""
|
| 258 |
+
dt_wall = self._fromutc(dt)
|
| 259 |
+
|
| 260 |
+
# Calculate the fold status given the two datetimes.
|
| 261 |
+
_fold = self._fold_status(dt, dt_wall)
|
| 262 |
+
|
| 263 |
+
# Set the default fold value for ambiguous dates
|
| 264 |
+
return enfold(dt_wall, fold=_fold)
|
| 265 |
+
|
| 266 |
+
|
| 267 |
+
class tzrangebase(_tzinfo):
|
| 268 |
+
"""
|
| 269 |
+
This is an abstract base class for time zones represented by an annual
|
| 270 |
+
transition into and out of DST. Child classes should implement the following
|
| 271 |
+
methods:
|
| 272 |
+
|
| 273 |
+
* ``__init__(self, *args, **kwargs)``
|
| 274 |
+
* ``transitions(self, year)`` - this is expected to return a tuple of
|
| 275 |
+
datetimes representing the DST on and off transitions in standard
|
| 276 |
+
time.
|
| 277 |
+
|
| 278 |
+
A fully initialized ``tzrangebase`` subclass should also provide the
|
| 279 |
+
following attributes:
|
| 280 |
+
* ``hasdst``: Boolean whether or not the zone uses DST.
|
| 281 |
+
* ``_dst_offset`` / ``_std_offset``: :class:`datetime.timedelta` objects
|
| 282 |
+
representing the respective UTC offsets.
|
| 283 |
+
* ``_dst_abbr`` / ``_std_abbr``: Strings representing the timezone short
|
| 284 |
+
abbreviations in DST and STD, respectively.
|
| 285 |
+
* ``_hasdst``: Whether or not the zone has DST.
|
| 286 |
+
|
| 287 |
+
.. versionadded:: 2.6.0
|
| 288 |
+
"""
|
| 289 |
+
def __init__(self):
|
| 290 |
+
raise NotImplementedError('tzrangebase is an abstract base class')
|
| 291 |
+
|
| 292 |
+
def utcoffset(self, dt):
|
| 293 |
+
isdst = self._isdst(dt)
|
| 294 |
+
|
| 295 |
+
if isdst is None:
|
| 296 |
+
return None
|
| 297 |
+
elif isdst:
|
| 298 |
+
return self._dst_offset
|
| 299 |
+
else:
|
| 300 |
+
return self._std_offset
|
| 301 |
+
|
| 302 |
+
def dst(self, dt):
|
| 303 |
+
isdst = self._isdst(dt)
|
| 304 |
+
|
| 305 |
+
if isdst is None:
|
| 306 |
+
return None
|
| 307 |
+
elif isdst:
|
| 308 |
+
return self._dst_base_offset
|
| 309 |
+
else:
|
| 310 |
+
return ZERO
|
| 311 |
+
|
| 312 |
+
@tzname_in_python2
|
| 313 |
+
def tzname(self, dt):
|
| 314 |
+
if self._isdst(dt):
|
| 315 |
+
return self._dst_abbr
|
| 316 |
+
else:
|
| 317 |
+
return self._std_abbr
|
| 318 |
+
|
| 319 |
+
def fromutc(self, dt):
|
| 320 |
+
""" Given a datetime in UTC, return local time """
|
| 321 |
+
if not isinstance(dt, datetime):
|
| 322 |
+
raise TypeError("fromutc() requires a datetime argument")
|
| 323 |
+
|
| 324 |
+
if dt.tzinfo is not self:
|
| 325 |
+
raise ValueError("dt.tzinfo is not self")
|
| 326 |
+
|
| 327 |
+
# Get transitions - if there are none, fixed offset
|
| 328 |
+
transitions = self.transitions(dt.year)
|
| 329 |
+
if transitions is None:
|
| 330 |
+
return dt + self.utcoffset(dt)
|
| 331 |
+
|
| 332 |
+
# Get the transition times in UTC
|
| 333 |
+
dston, dstoff = transitions
|
| 334 |
+
|
| 335 |
+
dston -= self._std_offset
|
| 336 |
+
dstoff -= self._std_offset
|
| 337 |
+
|
| 338 |
+
utc_transitions = (dston, dstoff)
|
| 339 |
+
dt_utc = dt.replace(tzinfo=None)
|
| 340 |
+
|
| 341 |
+
isdst = self._naive_isdst(dt_utc, utc_transitions)
|
| 342 |
+
|
| 343 |
+
if isdst:
|
| 344 |
+
dt_wall = dt + self._dst_offset
|
| 345 |
+
else:
|
| 346 |
+
dt_wall = dt + self._std_offset
|
| 347 |
+
|
| 348 |
+
_fold = int(not isdst and self.is_ambiguous(dt_wall))
|
| 349 |
+
|
| 350 |
+
return enfold(dt_wall, fold=_fold)
|
| 351 |
+
|
| 352 |
+
def is_ambiguous(self, dt):
|
| 353 |
+
"""
|
| 354 |
+
Whether or not the "wall time" of a given datetime is ambiguous in this
|
| 355 |
+
zone.
|
| 356 |
+
|
| 357 |
+
:param dt:
|
| 358 |
+
A :py:class:`datetime.datetime`, naive or time zone aware.
|
| 359 |
+
|
| 360 |
+
|
| 361 |
+
:return:
|
| 362 |
+
Returns ``True`` if ambiguous, ``False`` otherwise.
|
| 363 |
+
|
| 364 |
+
.. versionadded:: 2.6.0
|
| 365 |
+
"""
|
| 366 |
+
if not self.hasdst:
|
| 367 |
+
return False
|
| 368 |
+
|
| 369 |
+
start, end = self.transitions(dt.year)
|
| 370 |
+
|
| 371 |
+
dt = dt.replace(tzinfo=None)
|
| 372 |
+
return (end <= dt < end + self._dst_base_offset)
|
| 373 |
+
|
| 374 |
+
def _isdst(self, dt):
|
| 375 |
+
if not self.hasdst:
|
| 376 |
+
return False
|
| 377 |
+
elif dt is None:
|
| 378 |
+
return None
|
| 379 |
+
|
| 380 |
+
transitions = self.transitions(dt.year)
|
| 381 |
+
|
| 382 |
+
if transitions is None:
|
| 383 |
+
return False
|
| 384 |
+
|
| 385 |
+
dt = dt.replace(tzinfo=None)
|
| 386 |
+
|
| 387 |
+
isdst = self._naive_isdst(dt, transitions)
|
| 388 |
+
|
| 389 |
+
# Handle ambiguous dates
|
| 390 |
+
if not isdst and self.is_ambiguous(dt):
|
| 391 |
+
return not self._fold(dt)
|
| 392 |
+
else:
|
| 393 |
+
return isdst
|
| 394 |
+
|
| 395 |
+
def _naive_isdst(self, dt, transitions):
|
| 396 |
+
dston, dstoff = transitions
|
| 397 |
+
|
| 398 |
+
dt = dt.replace(tzinfo=None)
|
| 399 |
+
|
| 400 |
+
if dston < dstoff:
|
| 401 |
+
isdst = dston <= dt < dstoff
|
| 402 |
+
else:
|
| 403 |
+
isdst = not dstoff <= dt < dston
|
| 404 |
+
|
| 405 |
+
return isdst
|
| 406 |
+
|
| 407 |
+
@property
|
| 408 |
+
def _dst_base_offset(self):
|
| 409 |
+
return self._dst_offset - self._std_offset
|
| 410 |
+
|
| 411 |
+
__hash__ = None
|
| 412 |
+
|
| 413 |
+
def __ne__(self, other):
|
| 414 |
+
return not (self == other)
|
| 415 |
+
|
| 416 |
+
def __repr__(self):
|
| 417 |
+
return "%s(...)" % self.__class__.__name__
|
| 418 |
+
|
| 419 |
+
__reduce__ = object.__reduce__
|
hug/lib/python3.12/site-packages/dateutil/tz/_factories.py
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import timedelta
|
| 2 |
+
import weakref
|
| 3 |
+
from collections import OrderedDict
|
| 4 |
+
|
| 5 |
+
from six.moves import _thread
|
| 6 |
+
|
| 7 |
+
|
| 8 |
+
class _TzSingleton(type):
|
| 9 |
+
def __init__(cls, *args, **kwargs):
|
| 10 |
+
cls.__instance = None
|
| 11 |
+
super(_TzSingleton, cls).__init__(*args, **kwargs)
|
| 12 |
+
|
| 13 |
+
def __call__(cls):
|
| 14 |
+
if cls.__instance is None:
|
| 15 |
+
cls.__instance = super(_TzSingleton, cls).__call__()
|
| 16 |
+
return cls.__instance
|
| 17 |
+
|
| 18 |
+
|
| 19 |
+
class _TzFactory(type):
|
| 20 |
+
def instance(cls, *args, **kwargs):
|
| 21 |
+
"""Alternate constructor that returns a fresh instance"""
|
| 22 |
+
return type.__call__(cls, *args, **kwargs)
|
| 23 |
+
|
| 24 |
+
|
| 25 |
+
class _TzOffsetFactory(_TzFactory):
|
| 26 |
+
def __init__(cls, *args, **kwargs):
|
| 27 |
+
cls.__instances = weakref.WeakValueDictionary()
|
| 28 |
+
cls.__strong_cache = OrderedDict()
|
| 29 |
+
cls.__strong_cache_size = 8
|
| 30 |
+
|
| 31 |
+
cls._cache_lock = _thread.allocate_lock()
|
| 32 |
+
|
| 33 |
+
def __call__(cls, name, offset):
|
| 34 |
+
if isinstance(offset, timedelta):
|
| 35 |
+
key = (name, offset.total_seconds())
|
| 36 |
+
else:
|
| 37 |
+
key = (name, offset)
|
| 38 |
+
|
| 39 |
+
instance = cls.__instances.get(key, None)
|
| 40 |
+
if instance is None:
|
| 41 |
+
instance = cls.__instances.setdefault(key,
|
| 42 |
+
cls.instance(name, offset))
|
| 43 |
+
|
| 44 |
+
# This lock may not be necessary in Python 3. See GH issue #901
|
| 45 |
+
with cls._cache_lock:
|
| 46 |
+
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
| 47 |
+
|
| 48 |
+
# Remove an item if the strong cache is overpopulated
|
| 49 |
+
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
| 50 |
+
cls.__strong_cache.popitem(last=False)
|
| 51 |
+
|
| 52 |
+
return instance
|
| 53 |
+
|
| 54 |
+
|
| 55 |
+
class _TzStrFactory(_TzFactory):
|
| 56 |
+
def __init__(cls, *args, **kwargs):
|
| 57 |
+
cls.__instances = weakref.WeakValueDictionary()
|
| 58 |
+
cls.__strong_cache = OrderedDict()
|
| 59 |
+
cls.__strong_cache_size = 8
|
| 60 |
+
|
| 61 |
+
cls.__cache_lock = _thread.allocate_lock()
|
| 62 |
+
|
| 63 |
+
def __call__(cls, s, posix_offset=False):
|
| 64 |
+
key = (s, posix_offset)
|
| 65 |
+
instance = cls.__instances.get(key, None)
|
| 66 |
+
|
| 67 |
+
if instance is None:
|
| 68 |
+
instance = cls.__instances.setdefault(key,
|
| 69 |
+
cls.instance(s, posix_offset))
|
| 70 |
+
|
| 71 |
+
# This lock may not be necessary in Python 3. See GH issue #901
|
| 72 |
+
with cls.__cache_lock:
|
| 73 |
+
cls.__strong_cache[key] = cls.__strong_cache.pop(key, instance)
|
| 74 |
+
|
| 75 |
+
# Remove an item if the strong cache is overpopulated
|
| 76 |
+
if len(cls.__strong_cache) > cls.__strong_cache_size:
|
| 77 |
+
cls.__strong_cache.popitem(last=False)
|
| 78 |
+
|
| 79 |
+
return instance
|
| 80 |
+
|
hug/lib/python3.12/site-packages/dateutil/tz/_tzfile.py
ADDED
|
@@ -0,0 +1,1428 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
import bisect
|
| 3 |
+
import calendar
|
| 4 |
+
import functools
|
| 5 |
+
import io
|
| 6 |
+
import os
|
| 7 |
+
import re
|
| 8 |
+
import stat
|
| 9 |
+
import struct
|
| 10 |
+
import sys
|
| 11 |
+
from datetime import MAXYEAR, MINYEAR, datetime, timedelta
|
| 12 |
+
|
| 13 |
+
import six
|
| 14 |
+
|
| 15 |
+
from ._common import _tzinfo, enfold, tzname_in_python2
|
| 16 |
+
|
| 17 |
+
EPOCH = datetime(1970, 1, 1)
|
| 18 |
+
EPOCHORDINAL = EPOCH.toordinal()
|
| 19 |
+
|
| 20 |
+
# TODO: Move this to _common.py
|
| 21 |
+
try:
|
| 22 |
+
# Python 3.7 feature
|
| 23 |
+
from contextlib import nullcontext as _nullcontext
|
| 24 |
+
except ImportError:
|
| 25 |
+
import contextlib
|
| 26 |
+
|
| 27 |
+
@contextlib.contextmanager
|
| 28 |
+
def _nullcontext(context):
|
| 29 |
+
yield context
|
| 30 |
+
|
| 31 |
+
|
| 32 |
+
# It is relatively expensive to construct new timedelta objects, and in most
|
| 33 |
+
# cases we're looking at the same deltas, like integer numbers of hours, etc.
|
| 34 |
+
# To improve speed and memory use, we'll keep a dictionary with references
|
| 35 |
+
# to the ones we've already used so far.
|
| 36 |
+
#
|
| 37 |
+
# Loading every time zone in the 2020a version of the time zone database
|
| 38 |
+
# requires 447 timedeltas, which requires approximately the amount of space
|
| 39 |
+
# that ZoneInfo("America/New_York") with 236 transitions takes up, so we will
|
| 40 |
+
# set the cache size to 512 so that in the common case we always get cache
|
| 41 |
+
# hits, but specifically crafted ZoneInfo objects don't leak arbitrary amounts
|
| 42 |
+
# of memory.
|
| 43 |
+
_TIMEDELTA_CACHE_SIZE = 512
|
| 44 |
+
if six.PY2:
|
| 45 |
+
|
| 46 |
+
def _cache_load_timedelta(f):
|
| 47 |
+
from collections import OrderedDict
|
| 48 |
+
|
| 49 |
+
from six.moves import _thread
|
| 50 |
+
|
| 51 |
+
CACHE_LOCK = _thread.allocate_lock()
|
| 52 |
+
TIMEDELTA_CACHE = OrderedDict()
|
| 53 |
+
|
| 54 |
+
@functools.wraps(f)
|
| 55 |
+
def cached_load_timedelta(seconds):
|
| 56 |
+
# OrderedDict is implemented in Python on Python 2, so every
|
| 57 |
+
# access has to hold the lock to keep its linked list intact.
|
| 58 |
+
with CACHE_LOCK:
|
| 59 |
+
rv = TIMEDELTA_CACHE.pop(seconds, None)
|
| 60 |
+
if rv is None:
|
| 61 |
+
rv = f(seconds)
|
| 62 |
+
|
| 63 |
+
TIMEDELTA_CACHE[seconds] = rv
|
| 64 |
+
|
| 65 |
+
if len(TIMEDELTA_CACHE) > _TIMEDELTA_CACHE_SIZE:
|
| 66 |
+
TIMEDELTA_CACHE.popitem(last=False)
|
| 67 |
+
|
| 68 |
+
return rv
|
| 69 |
+
|
| 70 |
+
return cached_load_timedelta
|
| 71 |
+
|
| 72 |
+
else:
|
| 73 |
+
_cache_load_timedelta = functools.lru_cache(maxsize=512)
|
| 74 |
+
|
| 75 |
+
|
| 76 |
+
@_cache_load_timedelta
|
| 77 |
+
def _load_timedelta(seconds):
|
| 78 |
+
return timedelta(seconds=seconds)
|
| 79 |
+
|
| 80 |
+
|
| 81 |
+
def _unpickle(cls, key, filename, data):
|
| 82 |
+
obj = cls.__new__(cls)
|
| 83 |
+
obj._key = key
|
| 84 |
+
obj._filename = filename
|
| 85 |
+
|
| 86 |
+
obj._load_from_data(data)
|
| 87 |
+
|
| 88 |
+
return obj
|
| 89 |
+
|
| 90 |
+
|
| 91 |
+
class tzfile(_tzinfo):
|
| 92 |
+
"""
|
| 93 |
+
This is a ``tzinfo`` subclass that allows one to use the ``tzfile(5)``
|
| 94 |
+
format timezone files to extract current and historical zone information.
|
| 95 |
+
|
| 96 |
+
:param fileobj:
|
| 97 |
+
This can be an opened file stream or a file name that the time zone
|
| 98 |
+
information can be read from.
|
| 99 |
+
|
| 100 |
+
:param filename:
|
| 101 |
+
This is an optional parameter specifying the source of the time zone
|
| 102 |
+
information in the event that ``fileobj`` is a file object. If omitted
|
| 103 |
+
and ``fileobj`` is a file stream, this parameter will be set either to
|
| 104 |
+
``fileobj``'s ``name`` attribute or to ``repr(fileobj)``.
|
| 105 |
+
|
| 106 |
+
This class is intended to be mostly equivalent to the standard library
|
| 107 |
+
:py:class:`zoneinfo.ZoneInfo` class. The primary differences are:
|
| 108 |
+
|
| 109 |
+
* All caching in ``tzfile`` is handled by :func:`tz.gettz` level (you
|
| 110 |
+
should generally not attempt to construct a ``tzfile`` directly).
|
| 111 |
+
* ``tzfile`` objects are pickled by value rather than by reference to
|
| 112 |
+
the IANA key.
|
| 113 |
+
* ``tzfile`` has equality-by-value semantics, whereas
|
| 114 |
+
:py:class:`~zoneinfo.ZoneInfo` has equality-by-identity semantics.
|
| 115 |
+
|
| 116 |
+
See `Sources for Time Zone and Daylight Saving Time Data
|
| 117 |
+
<https://data.iana.org/time-zones/tz-link.html>`_ for more information.
|
| 118 |
+
Time zone files can be compiled from the `IANA Time Zone database files
|
| 119 |
+
<https://www.iana.org/time-zones>`_ with the `zic time zone compiler
|
| 120 |
+
<https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_
|
| 121 |
+
|
| 122 |
+
.. note::
|
| 123 |
+
|
| 124 |
+
Only construct a ``tzfile`` directly if you have a specific timezone
|
| 125 |
+
file on disk that you want to read into a Python ``tzinfo`` object.
|
| 126 |
+
If you want to get a ``tzfile`` representing a specific IANA zone,
|
| 127 |
+
(e.g. ``'America/New_York'``), you should call
|
| 128 |
+
:func:`dateutil.tz.gettz` with the zone identifier.
|
| 129 |
+
|
| 130 |
+
.. versionchanged:: 3.0.0
|
| 131 |
+
``tzfile`` is now a partial backport of :py:class:`zoneinfo.ZoneInfo`,
|
| 132 |
+
which brings with it support for Version 2 and 3 ``tzfile(5)`` binaries.
|
| 133 |
+
|
| 134 |
+
**Examples:**
|
| 135 |
+
|
| 136 |
+
Using the US Eastern time zone as an example, we can see that a ``tzfile``
|
| 137 |
+
provides time zone information for the standard Daylight Saving offsets:
|
| 138 |
+
|
| 139 |
+
.. testsetup:: tzfile
|
| 140 |
+
|
| 141 |
+
from dateutil.tz import gettz
|
| 142 |
+
from datetime import datetime
|
| 143 |
+
|
| 144 |
+
.. doctest:: tzfile
|
| 145 |
+
|
| 146 |
+
>>> NYC = gettz('America/New_York')
|
| 147 |
+
>>> NYC
|
| 148 |
+
tzfile('/usr/share/zoneinfo/America/New_York')
|
| 149 |
+
|
| 150 |
+
>>> print(datetime(2016, 1, 3, tzinfo=NYC)) # EST
|
| 151 |
+
2016-01-03 00:00:00-05:00
|
| 152 |
+
|
| 153 |
+
>>> print(datetime(2016, 7, 7, tzinfo=NYC)) # EDT
|
| 154 |
+
2016-07-07 00:00:00-04:00
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
The ``tzfile`` structure contains a fully history of the time zone,
|
| 158 |
+
so historical dates will also have the right offsets. For example, before
|
| 159 |
+
the adoption of standard time on November 18, 1883, New York used local
|
| 160 |
+
mean time:
|
| 161 |
+
|
| 162 |
+
.. doctest:: tzfile
|
| 163 |
+
|
| 164 |
+
>>> print(datetime(1883, 11, 18, tzinfo=NYC)) # LMT
|
| 165 |
+
1883-11-18 00:00:00-04:56:02
|
| 166 |
+
|
| 167 |
+
Python versions before 3.6 do not support sub-minute offsets, so there the
|
| 168 |
+
offset is rounded to ``-04:56``.
|
| 169 |
+
|
| 170 |
+
And during World War II, New York was on "Eastern War Time", which was a
|
| 171 |
+
state of permanent daylight saving time:
|
| 172 |
+
|
| 173 |
+
.. doctest:: tzfile
|
| 174 |
+
|
| 175 |
+
>>> print(datetime(1944, 2, 7, tzinfo=NYC)) # EWT
|
| 176 |
+
1944-02-07 00:00:00-04:00
|
| 177 |
+
|
| 178 |
+
"""
|
| 179 |
+
|
| 180 |
+
def __init__(self, fileobj, filename=None, key=None):
|
| 181 |
+
super(tzfile, self).__init__()
|
| 182 |
+
|
| 183 |
+
file_opened_here = False
|
| 184 |
+
if isinstance(fileobj, six.string_types):
|
| 185 |
+
self._filename = fileobj
|
| 186 |
+
fileobj = open(fileobj, "rb")
|
| 187 |
+
file_opened_here = True
|
| 188 |
+
elif filename is not None:
|
| 189 |
+
self._filename = filename
|
| 190 |
+
elif hasattr(fileobj, "name"):
|
| 191 |
+
self._filename = fileobj.name
|
| 192 |
+
elif key is not None:
|
| 193 |
+
self._filename = key
|
| 194 |
+
else:
|
| 195 |
+
self._filename = repr(fileobj)
|
| 196 |
+
|
| 197 |
+
if fileobj is not None:
|
| 198 |
+
if not file_opened_here:
|
| 199 |
+
fileobj = _nullcontext(fileobj)
|
| 200 |
+
|
| 201 |
+
with fileobj as file_stream:
|
| 202 |
+
self._load_file(file_stream)
|
| 203 |
+
|
| 204 |
+
self._key = key
|
| 205 |
+
|
| 206 |
+
@property
|
| 207 |
+
def key(self):
|
| 208 |
+
"""
|
| 209 |
+
Read-only attribute returning the ``key`` constructor argument.
|
| 210 |
+
|
| 211 |
+
For zones generated from :func:`gettz`, this will be the IANA key
|
| 212 |
+
passed to ``gettz`` — no normalization is done, so although in most
|
| 213 |
+
distributions of the time zone database "US/Eastern" is a link to
|
| 214 |
+
"America/New_York", ``tz.gettz("US/Eastern").key`` should always return
|
| 215 |
+
``"US/Eastern"``.
|
| 216 |
+
|
| 217 |
+
For zones constructed via another mechanism without specifying ``key``,
|
| 218 |
+
this returns :obj:`None`.
|
| 219 |
+
"""
|
| 220 |
+
return self._key
|
| 221 |
+
|
| 222 |
+
def is_ambiguous(self, dt):
|
| 223 |
+
"""
|
| 224 |
+
Whether or not the "wall time" of a given datetime is ambiguous in this
|
| 225 |
+
zone.
|
| 226 |
+
|
| 227 |
+
:param dt:
|
| 228 |
+
A :py:class:`datetime.datetime`, naive or time zone aware.
|
| 229 |
+
|
| 230 |
+
|
| 231 |
+
:return:
|
| 232 |
+
Returns ``True`` if ambiguous, ``False`` otherwise.
|
| 233 |
+
|
| 234 |
+
.. versionadded:: 2.6.0
|
| 235 |
+
"""
|
| 236 |
+
return self._is_ambiguous_or_imaginary(dt) == 1
|
| 237 |
+
|
| 238 |
+
def _is_ambiguous_or_imaginary(self, dt):
|
| 239 |
+
if dt is None or self._fixed_offset:
|
| 240 |
+
return 0
|
| 241 |
+
|
| 242 |
+
timestamp = self._get_local_timestamp(dt)
|
| 243 |
+
tti_0 = self._find_trans(dt, fold=0, timestamp=timestamp)
|
| 244 |
+
tti_1 = self._find_trans(dt, fold=1, timestamp=timestamp)
|
| 245 |
+
|
| 246 |
+
# Returns 1 if ambiguous, -1 if imaginary and 0 otherwise.
|
| 247 |
+
return int(tti_0.utcoff > tti_1.utcoff) - int(
|
| 248 |
+
tti_1.utcoff > tti_0.utcoff
|
| 249 |
+
)
|
| 250 |
+
|
| 251 |
+
def utcoffset(self, dt):
|
| 252 |
+
return self._find_trans(dt).utcoff
|
| 253 |
+
|
| 254 |
+
def dst(self, dt):
|
| 255 |
+
return self._find_trans(dt).dstoff
|
| 256 |
+
|
| 257 |
+
@tzname_in_python2
|
| 258 |
+
def tzname(self, dt):
|
| 259 |
+
return self._find_trans(dt).tzname
|
| 260 |
+
|
| 261 |
+
def fromutc(self, dt):
|
| 262 |
+
"""
|
| 263 |
+
The ``tzfile`` implementation of :py:func:`datetime.tzinfo.fromutc`.
|
| 264 |
+
|
| 265 |
+
:param dt:
|
| 266 |
+
A :py:class:`datetime.datetime` object.
|
| 267 |
+
|
| 268 |
+
:raises TypeError:
|
| 269 |
+
Raised if ``dt`` is not a :py:class:`datetime.datetime` object.
|
| 270 |
+
|
| 271 |
+
:raises ValueError:
|
| 272 |
+
Raised if this is called with a ``dt`` which does not have this
|
| 273 |
+
``tzinfo`` attached.
|
| 274 |
+
|
| 275 |
+
:return:
|
| 276 |
+
Returns a :py:class:`datetime.datetime` object representing the
|
| 277 |
+
wall time in ``self``'s time zone.
|
| 278 |
+
"""
|
| 279 |
+
|
| 280 |
+
if not isinstance(dt, datetime):
|
| 281 |
+
raise TypeError("fromutc() requires a datetime argument")
|
| 282 |
+
if dt.tzinfo is not self:
|
| 283 |
+
raise ValueError("dt.tzinfo is not self")
|
| 284 |
+
|
| 285 |
+
timestamp = self._get_local_timestamp(dt)
|
| 286 |
+
num_trans = len(self._trans_utc)
|
| 287 |
+
|
| 288 |
+
if num_trans >= 1 and timestamp < self._trans_utc[0]:
|
| 289 |
+
tti = self._tti_before
|
| 290 |
+
fold = 0
|
| 291 |
+
elif (
|
| 292 |
+
num_trans == 0 or timestamp > self._trans_utc[-1]
|
| 293 |
+
) and not isinstance(self._tz_after, _ttinfo):
|
| 294 |
+
tti, fold = self._tz_after.get_trans_info_fromutc(
|
| 295 |
+
timestamp, dt.year
|
| 296 |
+
)
|
| 297 |
+
|
| 298 |
+
# The TZ string only knows about its own transitions, so check
|
| 299 |
+
# whether we are in a fold created by the last explicit one.
|
| 300 |
+
if num_trans and not fold:
|
| 301 |
+
if num_trans > 1:
|
| 302 |
+
tti_prev = self._ttinfos[-2]
|
| 303 |
+
else:
|
| 304 |
+
tti_prev = self._tti_before
|
| 305 |
+
|
| 306 |
+
shift = tti_prev.utcoff - tti.utcoff
|
| 307 |
+
fold = shift.total_seconds() > timestamp - self._trans_utc[-1]
|
| 308 |
+
elif num_trans == 0:
|
| 309 |
+
tti = self._tz_after
|
| 310 |
+
fold = 0
|
| 311 |
+
else:
|
| 312 |
+
idx = bisect.bisect_right(self._trans_utc, timestamp)
|
| 313 |
+
|
| 314 |
+
if num_trans > 1 and timestamp >= self._trans_utc[1]:
|
| 315 |
+
tti_prev, tti = self._ttinfos[idx - 2 : idx]
|
| 316 |
+
elif timestamp > self._trans_utc[-1]:
|
| 317 |
+
# Only reachable with exactly one transition, so the
|
| 318 |
+
# offset before it is _tti_before, not _ttinfos[-1].
|
| 319 |
+
tti_prev = self._tti_before
|
| 320 |
+
tti = self._tz_after
|
| 321 |
+
else:
|
| 322 |
+
tti_prev = self._tti_before
|
| 323 |
+
tti = self._ttinfos[0]
|
| 324 |
+
|
| 325 |
+
# Detect fold
|
| 326 |
+
shift = tti_prev.utcoff - tti.utcoff
|
| 327 |
+
fold = shift.total_seconds() > timestamp - self._trans_utc[idx - 1]
|
| 328 |
+
dt += tti.utcoff
|
| 329 |
+
if fold:
|
| 330 |
+
return enfold(dt, fold=1)
|
| 331 |
+
else:
|
| 332 |
+
return dt
|
| 333 |
+
|
| 334 |
+
def _find_trans(self, dt, fold=None, timestamp=None):
|
| 335 |
+
if dt is None:
|
| 336 |
+
if self._fixed_offset:
|
| 337 |
+
return self._tz_after
|
| 338 |
+
else:
|
| 339 |
+
return _NO_TTINFO
|
| 340 |
+
|
| 341 |
+
if timestamp is None:
|
| 342 |
+
ts = self._get_local_timestamp(dt)
|
| 343 |
+
else:
|
| 344 |
+
ts = timestamp
|
| 345 |
+
|
| 346 |
+
if fold is None:
|
| 347 |
+
fold = getattr(dt, "fold", 0)
|
| 348 |
+
|
| 349 |
+
lt = self._trans_local[fold]
|
| 350 |
+
|
| 351 |
+
num_trans = len(lt)
|
| 352 |
+
|
| 353 |
+
if num_trans and ts < lt[0]:
|
| 354 |
+
return self._tti_before
|
| 355 |
+
elif not num_trans or ts > lt[-1]:
|
| 356 |
+
if isinstance(self._tz_after, _TZStr):
|
| 357 |
+
tti = self._tz_after.get_trans_info(ts, dt.year, fold)
|
| 358 |
+
|
| 359 |
+
# The TZ string only describes times after the last explicit
|
| 360 |
+
# transition. If its answer for this local time is a different
|
| 361 |
+
# offset from the last transition's, and corresponds to a time
|
| 362 |
+
# before that transition (e.g. when the TZ string's own DST
|
| 363 |
+
# period for that year ends after the transition), the offset
|
| 364 |
+
# from the last explicit transition applies instead.
|
| 365 |
+
last_tti = self._ttinfos[-1] if num_trans else None
|
| 366 |
+
if (
|
| 367 |
+
last_tti is not None
|
| 368 |
+
and tti.utcoff != last_tti.utcoff
|
| 369 |
+
and ts - tti.utcoff.total_seconds() < self._trans_utc[-1]
|
| 370 |
+
):
|
| 371 |
+
return last_tti
|
| 372 |
+
|
| 373 |
+
return tti
|
| 374 |
+
else:
|
| 375 |
+
return self._tz_after
|
| 376 |
+
else:
|
| 377 |
+
# idx is the transition that occurs after this timestamp, so we
|
| 378 |
+
# subtract off 1 to get the current ttinfo
|
| 379 |
+
idx = bisect.bisect_right(lt, ts) - 1
|
| 380 |
+
assert idx >= 0
|
| 381 |
+
return self._ttinfos[idx]
|
| 382 |
+
|
| 383 |
+
def _get_local_timestamp(self, dt):
|
| 384 |
+
return (
|
| 385 |
+
(dt.toordinal() - EPOCHORDINAL) * 86400
|
| 386 |
+
+ dt.hour * 3600
|
| 387 |
+
+ dt.minute * 60
|
| 388 |
+
+ dt.second
|
| 389 |
+
)
|
| 390 |
+
|
| 391 |
+
# The attributes set by _unpickle and _load_from_data
|
| 392 |
+
_UNPICKLE_ATTRS = frozenset(
|
| 393 |
+
[
|
| 394 |
+
"_filename",
|
| 395 |
+
"_fixed_offset",
|
| 396 |
+
"_isdst",
|
| 397 |
+
"_key",
|
| 398 |
+
"_trans_idx",
|
| 399 |
+
"_trans_local",
|
| 400 |
+
"_trans_utc",
|
| 401 |
+
"_tti_before",
|
| 402 |
+
"_ttinfo_list",
|
| 403 |
+
"_ttinfos",
|
| 404 |
+
"_tz_after",
|
| 405 |
+
"_tz_str",
|
| 406 |
+
"_utcoff_raw",
|
| 407 |
+
]
|
| 408 |
+
)
|
| 409 |
+
|
| 410 |
+
def __reduce__(self):
|
| 411 |
+
return self.__reduce_ex__(None)
|
| 412 |
+
|
| 413 |
+
def __reduce_ex__(self, protocol):
|
| 414 |
+
utcoff = [
|
| 415 |
+
int(ttinfo.utcoff.total_seconds()) for ttinfo in self._ttinfo_list
|
| 416 |
+
]
|
| 417 |
+
utcoff = getattr(self, "_utcoff_raw", utcoff)
|
| 418 |
+
abbr = [ttinfo.tzname for ttinfo in self._ttinfo_list]
|
| 419 |
+
|
| 420 |
+
# dst() is inferred per transition, so the transitions don't map back
|
| 421 |
+
# to the local time types, and these are kept as they were in the file
|
| 422 |
+
trans_idx = self._trans_idx
|
| 423 |
+
isdst = self._isdst
|
| 424 |
+
|
| 425 |
+
trans_utc = self._trans_utc
|
| 426 |
+
tz_str = self._tz_str
|
| 427 |
+
|
| 428 |
+
data = (trans_idx, trans_utc, utcoff, isdst, abbr, tz_str)
|
| 429 |
+
args = (self.__class__, self._key, self._filename, data)
|
| 430 |
+
|
| 431 |
+
# Preserve any other instance state, such as attributes added by a
|
| 432 |
+
# subclass, but not the attributes that _unpickle rebuilds from data
|
| 433 |
+
state = {
|
| 434 |
+
k: v for k, v in vars(self).items() if k not in self._UNPICKLE_ATTRS
|
| 435 |
+
}
|
| 436 |
+
if state:
|
| 437 |
+
return (_unpickle, args, state)
|
| 438 |
+
|
| 439 |
+
return (_unpickle, args)
|
| 440 |
+
|
| 441 |
+
def _load_file(self, fobj):
|
| 442 |
+
# Reading the file requires seeking past the parts we don't use and
|
| 443 |
+
# reading the footer a line at a time, so read anything that doesn't
|
| 444 |
+
# support that (e.g. a pipe) into memory first; TZif files are small.
|
| 445 |
+
if not _is_seekable(fobj) or not hasattr(fobj, "readline"):
|
| 446 |
+
fobj = io.BytesIO(fobj.read())
|
| 447 |
+
|
| 448 |
+
# Retrieve all the data as it exists in the zoneinfo file
|
| 449 |
+
data = load_data(fobj)
|
| 450 |
+
self._load_from_data(data)
|
| 451 |
+
|
| 452 |
+
def _load_from_data(self, data):
|
| 453 |
+
trans_idx, trans_utc, utcoff, isdst, abbr, tz_str = data
|
| 454 |
+
|
| 455 |
+
# Infer the DST offsets (needed for .dst()) from the data, for each
|
| 456 |
+
# transition and for each local time type
|
| 457 |
+
trans_dstoff = self._utcoff_to_dstoff(
|
| 458 |
+
trans_idx, trans_utc, utcoff, isdst
|
| 459 |
+
)
|
| 460 |
+
dstoff = [3600 if dst else 0 for dst in isdst]
|
| 461 |
+
for idx, trans_off in zip(trans_idx, trans_dstoff):
|
| 462 |
+
dstoff[idx] = trans_off
|
| 463 |
+
|
| 464 |
+
if sys.version_info < (3, 6):
|
| 465 |
+
# For python pre-3.6, round to full-minutes if that's not the case.
|
| 466 |
+
# Python's datetime doesn't accept sub-minute timezones. Check
|
| 467 |
+
# http://python.org/sf/1447945 or https://bugs.python.org/issue5288
|
| 468 |
+
# for some information.
|
| 469 |
+
utcoff_adjusted = [60 * ((s + 30) // 60) for s in utcoff]
|
| 470 |
+
dstoff = [60 * ((s + 30) // 60) for s in dstoff]
|
| 471 |
+
trans_dstoff = [60 * ((s + 30) // 60) for s in trans_dstoff]
|
| 472 |
+
self._utcoff_raw = utcoff
|
| 473 |
+
else:
|
| 474 |
+
utcoff_adjusted = utcoff
|
| 475 |
+
|
| 476 |
+
# Convert all the transition times (UTC) into "seconds since 1970-01-01 local time"
|
| 477 |
+
trans_local = self._ts_to_local(trans_idx, trans_utc, utcoff_adjusted)
|
| 478 |
+
|
| 479 |
+
# Construct `_ttinfo` objects for each transition in the file
|
| 480 |
+
_ttinfo_list = [
|
| 481 |
+
_ttinfo(
|
| 482 |
+
_load_timedelta(utcoffset), _load_timedelta(dstoffset), tzname
|
| 483 |
+
)
|
| 484 |
+
for utcoffset, dstoffset, tzname in zip(
|
| 485 |
+
utcoff_adjusted, dstoff, abbr
|
| 486 |
+
)
|
| 487 |
+
]
|
| 488 |
+
|
| 489 |
+
self._ttinfo_list = _ttinfo_list
|
| 490 |
+
|
| 491 |
+
self._trans_idx = trans_idx
|
| 492 |
+
self._isdst = isdst
|
| 493 |
+
self._trans_utc = trans_utc
|
| 494 |
+
self._trans_local = trans_local
|
| 495 |
+
|
| 496 |
+
# Transitions to the same local time type usually have the same DST
|
| 497 |
+
# offset, so share the _ttinfo objects between them where possible.
|
| 498 |
+
ttinfos = {}
|
| 499 |
+
self._ttinfos = []
|
| 500 |
+
for idx, trans_off in zip(trans_idx, trans_dstoff):
|
| 501 |
+
tti = _ttinfo_list[idx]
|
| 502 |
+
if tti.dstoff.total_seconds() != trans_off:
|
| 503 |
+
key = (idx, trans_off)
|
| 504 |
+
if key not in ttinfos:
|
| 505 |
+
ttinfos[key] = _ttinfo(
|
| 506 |
+
tti.utcoff, _load_timedelta(trans_off), tti.tzname
|
| 507 |
+
)
|
| 508 |
+
tti = ttinfos[key]
|
| 509 |
+
self._ttinfos.append(tti)
|
| 510 |
+
|
| 511 |
+
# Find the first non-DST transition
|
| 512 |
+
for i in range(len(isdst)):
|
| 513 |
+
if not isdst[i]:
|
| 514 |
+
self._tti_before = _ttinfo_list[i]
|
| 515 |
+
break
|
| 516 |
+
else:
|
| 517 |
+
if self._ttinfos:
|
| 518 |
+
self._tti_before = self._ttinfos[0]
|
| 519 |
+
else:
|
| 520 |
+
self._tti_before = None
|
| 521 |
+
|
| 522 |
+
# Set the "fallback" time zone
|
| 523 |
+
self._tz_str = tz_str
|
| 524 |
+
if tz_str is not None and tz_str != b"":
|
| 525 |
+
self._tz_after = _parse_tz_str(tz_str.decode())
|
| 526 |
+
else:
|
| 527 |
+
if not self._ttinfos and not _ttinfo_list:
|
| 528 |
+
raise ValueError("No time zone information found.")
|
| 529 |
+
|
| 530 |
+
if self._ttinfos:
|
| 531 |
+
self._tz_after = self._ttinfos[-1]
|
| 532 |
+
else:
|
| 533 |
+
# With no transitions and no TZ string, RFC 9636 Section 3.2
|
| 534 |
+
# says that local time is specified by time type 0
|
| 535 |
+
self._tz_after = _ttinfo_list[0]
|
| 536 |
+
|
| 537 |
+
# Determine if this is a "fixed offset" zone, meaning that the output
|
| 538 |
+
# of the utcoffset, dst and tzname functions does not depend on the
|
| 539 |
+
# specific datetime passed.
|
| 540 |
+
#
|
| 541 |
+
# We make three simplifying assumptions here:
|
| 542 |
+
#
|
| 543 |
+
# 1. If _tz_after is not a _ttinfo, it has transitions that might
|
| 544 |
+
# actually occur (it is possible to construct TZ strings that
|
| 545 |
+
# specify STD and DST but no transitions ever occur, such as
|
| 546 |
+
# AAA0BBB,0/0,J365/25).
|
| 547 |
+
# 2. If _ttinfo_list contains more than one _ttinfo object, the objects
|
| 548 |
+
# represent different offsets.
|
| 549 |
+
# 3. _ttinfo_list contains no unused _ttinfos (in which case an
|
| 550 |
+
# otherwise fixed-offset zone with extra _ttinfos defined may
|
| 551 |
+
# appear to *not* be a fixed offset zone).
|
| 552 |
+
#
|
| 553 |
+
# Violations to these assumptions would be fairly exotic, and exotic
|
| 554 |
+
# zones should almost certainly not be used with datetime.time (the
|
| 555 |
+
# only thing that would be affected by this).
|
| 556 |
+
if len(_ttinfo_list) > 1 or not isinstance(self._tz_after, _ttinfo):
|
| 557 |
+
self._fixed_offset = False
|
| 558 |
+
elif not _ttinfo_list:
|
| 559 |
+
self._fixed_offset = True
|
| 560 |
+
else:
|
| 561 |
+
self._fixed_offset = _ttinfo_list[0] == self._tz_after
|
| 562 |
+
|
| 563 |
+
@staticmethod
|
| 564 |
+
def _utcoff_to_dstoff(trans_idx, trans_utc, utcoffsets, isdsts):
|
| 565 |
+
"""Infer the DST offset (the value of dst()) for each transition.
|
| 566 |
+
|
| 567 |
+
TZif files give the total UTC offset of each local time type and
|
| 568 |
+
whether it is DST, but not how much of the offset is DST, so this has
|
| 569 |
+
to be worked out from the standard time on either side of each DST
|
| 570 |
+
period. That is done for each transition rather than for each local
|
| 571 |
+
time type, because zic reuses a local time type for periods with
|
| 572 |
+
different standard offsets (e.g. UTC+0 DST in the Azores was double
|
| 573 |
+
summer time in the 1940s and summer time later).
|
| 574 |
+
"""
|
| 575 |
+
n = len(trans_idx)
|
| 576 |
+
|
| 577 |
+
# The DST offset implied by the last standard time before and the
|
| 578 |
+
# first standard time after each transition. Before the first
|
| 579 |
+
# transition, it's the first standard time type, as for _tti_before.
|
| 580 |
+
first_std_utcoff = next(
|
| 581 |
+
(utcoff for utcoff, dst in zip(utcoffsets, isdsts) if not dst),
|
| 582 |
+
None,
|
| 583 |
+
)
|
| 584 |
+
before = [None] * n
|
| 585 |
+
after = [None] * n
|
| 586 |
+
for indices, implied, std_utcoff in (
|
| 587 |
+
(range(n), before, first_std_utcoff),
|
| 588 |
+
(range(n - 1, -1, -1), after, None),
|
| 589 |
+
):
|
| 590 |
+
for i in indices:
|
| 591 |
+
idx = trans_idx[i]
|
| 592 |
+
if not isdsts[idx]:
|
| 593 |
+
std_utcoff = utcoffsets[idx]
|
| 594 |
+
elif std_utcoff is not None:
|
| 595 |
+
implied[i] = utcoffsets[idx] - std_utcoff
|
| 596 |
+
|
| 597 |
+
# DST offsets where the standard time is the same on both sides
|
| 598 |
+
clear_cut = [
|
| 599 |
+
(trans_utc[i], before[i])
|
| 600 |
+
for i in range(n)
|
| 601 |
+
if before[i] and before[i] == after[i]
|
| 602 |
+
]
|
| 603 |
+
|
| 604 |
+
dstoffs = [0] * n
|
| 605 |
+
for i in range(n):
|
| 606 |
+
if not isdsts[trans_idx[i]]:
|
| 607 |
+
continue
|
| 608 |
+
|
| 609 |
+
candidates = {c for c in (before[i], after[i]) if c}
|
| 610 |
+
if len(candidates) > 1:
|
| 611 |
+
# The standard offset changed during the DST period. A change
|
| 612 |
+
# from local mean time can give something like 0:34:39, which
|
| 613 |
+
# is not a plausible amount of DST.
|
| 614 |
+
plausible = {c for c in candidates if c % 300 == 0}
|
| 615 |
+
candidates = plausible or candidates
|
| 616 |
+
|
| 617 |
+
if len(candidates) > 1:
|
| 618 |
+
# Prefer the amount of DST the zone used around the same
|
| 619 |
+
# time, e.g. Tehran used 1:00 and Rarotonga 0:30, though the
|
| 620 |
+
# TZif data for Tehran in 1977 and for Rarotonga in 1978 look
|
| 621 |
+
# the same. This has to be local in time, because some zones
|
| 622 |
+
# used different amounts in different eras (e.g. Montevideo
|
| 623 |
+
# used 0:30 in the 1920s and 1:00 later).
|
| 624 |
+
nearby = {
|
| 625 |
+
dstoff
|
| 626 |
+
for ts, dstoff in clear_cut
|
| 627 |
+
if abs(ts - trans_utc[i]) <= _DST_CONSENSUS_WINDOW
|
| 628 |
+
}
|
| 629 |
+
consensus = candidates & nearby
|
| 630 |
+
if len(consensus) == 1:
|
| 631 |
+
candidates = consensus
|
| 632 |
+
|
| 633 |
+
if candidates:
|
| 634 |
+
# Otherwise, prefer positive DST, then the smaller amount. In
|
| 635 |
+
# the 1942 occupation zones, e.g. Europe/Kyiv, the standard
|
| 636 |
+
# time before gives -1:00 and the one after +1:00.
|
| 637 |
+
dstoffs[i] = min(candidates, key=lambda c: (c < 0, abs(c)))
|
| 638 |
+
else:
|
| 639 |
+
# Without any standard time to compare to, one hour is a much
|
| 640 |
+
# better guess than 0
|
| 641 |
+
dstoffs[i] = 3600
|
| 642 |
+
|
| 643 |
+
return dstoffs
|
| 644 |
+
|
| 645 |
+
@staticmethod
|
| 646 |
+
def _ts_to_local(trans_idx, trans_list_utc, utcoffsets):
|
| 647 |
+
"""Generate number of seconds since 1970 *in the local time*.
|
| 648 |
+
|
| 649 |
+
This is necessary to easily find the transition times in local time"""
|
| 650 |
+
if not trans_list_utc:
|
| 651 |
+
return [[], []]
|
| 652 |
+
|
| 653 |
+
# Start with the timestamps and modify in-place
|
| 654 |
+
trans_list_wall = [list(trans_list_utc), list(trans_list_utc)]
|
| 655 |
+
|
| 656 |
+
if len(utcoffsets) > 1:
|
| 657 |
+
offset_0 = utcoffsets[0]
|
| 658 |
+
offset_1 = utcoffsets[trans_idx[0]]
|
| 659 |
+
if offset_1 > offset_0:
|
| 660 |
+
offset_1, offset_0 = offset_0, offset_1
|
| 661 |
+
else:
|
| 662 |
+
offset_0 = offset_1 = utcoffsets[0]
|
| 663 |
+
|
| 664 |
+
trans_list_wall[0][0] += offset_0
|
| 665 |
+
trans_list_wall[1][0] += offset_1
|
| 666 |
+
|
| 667 |
+
for i in range(1, len(trans_idx)):
|
| 668 |
+
offset_0 = utcoffsets[trans_idx[i - 1]]
|
| 669 |
+
offset_1 = utcoffsets[trans_idx[i]]
|
| 670 |
+
|
| 671 |
+
if offset_1 > offset_0:
|
| 672 |
+
offset_1, offset_0 = offset_0, offset_1
|
| 673 |
+
|
| 674 |
+
trans_list_wall[0][i] += offset_0
|
| 675 |
+
trans_list_wall[1][i] += offset_1
|
| 676 |
+
|
| 677 |
+
return trans_list_wall
|
| 678 |
+
|
| 679 |
+
def __eq__(self, other):
|
| 680 |
+
if not isinstance(other, tzfile):
|
| 681 |
+
return NotImplemented
|
| 682 |
+
|
| 683 |
+
return (
|
| 684 |
+
self._trans_utc == other._trans_utc
|
| 685 |
+
and self._trans_local == other._trans_local
|
| 686 |
+
and self._ttinfos == other._ttinfos
|
| 687 |
+
and self._tti_before == other._tti_before
|
| 688 |
+
and self._fallback_key() == other._fallback_key()
|
| 689 |
+
)
|
| 690 |
+
|
| 691 |
+
def _fallback_key(self):
|
| 692 |
+
# The behavior after the last transition is determined either by the
|
| 693 |
+
# TZ string or, in its absence, by a single _ttinfo; _TZStr objects
|
| 694 |
+
# do not define equality, so compare the string they came from.
|
| 695 |
+
if isinstance(self._tz_after, _ttinfo):
|
| 696 |
+
return self._tz_after
|
| 697 |
+
return self._tz_str
|
| 698 |
+
|
| 699 |
+
__hash__ = None
|
| 700 |
+
|
| 701 |
+
def __ne__(self, other):
|
| 702 |
+
return not (self == other)
|
| 703 |
+
|
| 704 |
+
def __repr__(self):
|
| 705 |
+
return "%s(%s)" % (self.__class__.__name__, repr(self._filename))
|
| 706 |
+
|
| 707 |
+
|
| 708 |
+
# Before Python 3.16, pipes on Windows claim to be seekable, but seeking on
|
| 709 |
+
# them silently does nothing. See https://github.com/python/cpython/issues/86768
|
| 710 |
+
_SEEKABLE_PIPES_BUG = sys.platform == "win32" and sys.version_info < (3, 16)
|
| 711 |
+
|
| 712 |
+
|
| 713 |
+
def _is_seekable(fobj):
|
| 714 |
+
if not hasattr(fobj, "seek"):
|
| 715 |
+
return False
|
| 716 |
+
|
| 717 |
+
seekable = getattr(fobj, "seekable", None)
|
| 718 |
+
# Python 2 file objects can seek but have no seekable() method
|
| 719 |
+
if seekable is not None and not seekable():
|
| 720 |
+
return False
|
| 721 |
+
|
| 722 |
+
if not _SEEKABLE_PIPES_BUG:
|
| 723 |
+
return True
|
| 724 |
+
|
| 725 |
+
# For anything backed by a file descriptor, only trust regular files.
|
| 726 |
+
try:
|
| 727 |
+
fd = fobj.fileno()
|
| 728 |
+
except (AttributeError, OSError, ValueError):
|
| 729 |
+
# io.UnsupportedOperation (e.g. from BytesIO) is both of the latter
|
| 730 |
+
return True
|
| 731 |
+
|
| 732 |
+
try:
|
| 733 |
+
return stat.S_ISREG(os.fstat(fd).st_mode)
|
| 734 |
+
except OSError:
|
| 735 |
+
return True
|
| 736 |
+
|
| 737 |
+
|
| 738 |
+
def load_data(fobj):
|
| 739 |
+
try:
|
| 740 |
+
return _load_data(fobj)
|
| 741 |
+
except struct.error as e:
|
| 742 |
+
# struct.unpack fails when a read comes up short
|
| 743 |
+
six.raise_from(
|
| 744 |
+
ValueError("Invalid TZif file: unexpected end of file"), e
|
| 745 |
+
)
|
| 746 |
+
|
| 747 |
+
|
| 748 |
+
def _load_data(fobj):
|
| 749 |
+
header = _TZifHeader.from_file(fobj)
|
| 750 |
+
|
| 751 |
+
if header.version == 1:
|
| 752 |
+
time_size = 4
|
| 753 |
+
time_type = "l"
|
| 754 |
+
else:
|
| 755 |
+
# Version 2+ has 64-bit integer transition times
|
| 756 |
+
time_size = 8
|
| 757 |
+
time_type = "q"
|
| 758 |
+
|
| 759 |
+
# Version 2+ also starts with a Version 1 header and data, which
|
| 760 |
+
# we need to skip now
|
| 761 |
+
skip_bytes = (
|
| 762 |
+
header.timecnt * 5 # Transition times and types
|
| 763 |
+
+ header.typecnt * 6 # Local time type records
|
| 764 |
+
+ header.charcnt # Time zone designations
|
| 765 |
+
+ header.leapcnt * 8 # Leap second records
|
| 766 |
+
+ header.isstdcnt # Standard/wall indicators
|
| 767 |
+
+ header.isutcnt # UT/local indicators
|
| 768 |
+
)
|
| 769 |
+
|
| 770 |
+
fobj.seek(skip_bytes, 1)
|
| 771 |
+
|
| 772 |
+
# Now we need to read the second header, which is not the same
|
| 773 |
+
# as the first
|
| 774 |
+
header = _TZifHeader.from_file(fobj)
|
| 775 |
+
|
| 776 |
+
typecnt = header.typecnt
|
| 777 |
+
timecnt = header.timecnt
|
| 778 |
+
charcnt = header.charcnt
|
| 779 |
+
|
| 780 |
+
# The data portion starts with timecnt transitions and indices
|
| 781 |
+
if timecnt:
|
| 782 |
+
trans_list_utc = struct.unpack(
|
| 783 |
+
">%s%s" % (timecnt, time_type), fobj.read(timecnt * time_size)
|
| 784 |
+
)
|
| 785 |
+
trans_idx = struct.unpack(">%sB" % timecnt, fobj.read(timecnt))
|
| 786 |
+
|
| 787 |
+
if max(trans_idx) >= typecnt:
|
| 788 |
+
raise ValueError(
|
| 789 |
+
"Invalid transition index found while reading TZif: %s"
|
| 790 |
+
% max(trans_idx)
|
| 791 |
+
)
|
| 792 |
+
else:
|
| 793 |
+
trans_list_utc = ()
|
| 794 |
+
trans_idx = ()
|
| 795 |
+
|
| 796 |
+
# Read the ttinfo struct, (utoff, isdst, abbrind)
|
| 797 |
+
if typecnt:
|
| 798 |
+
utcoff, isdst, abbrind = zip(
|
| 799 |
+
*(struct.unpack(">lbb", fobj.read(6)) for i in range(typecnt))
|
| 800 |
+
)
|
| 801 |
+
else:
|
| 802 |
+
utcoff = ()
|
| 803 |
+
isdst = ()
|
| 804 |
+
abbrind = ()
|
| 805 |
+
|
| 806 |
+
# Now read the abbreviations. They are null-terminated strings, indexed
|
| 807 |
+
# not by position in the array but by position in the unsplit
|
| 808 |
+
# abbreviation string. I suppose this makes more sense in C, which uses
|
| 809 |
+
# null to terminate the strings, but it's inconvenient here...
|
| 810 |
+
abbr_vals = {}
|
| 811 |
+
abbr_chars = fobj.read(charcnt)
|
| 812 |
+
|
| 813 |
+
def get_abbr(idx):
|
| 814 |
+
# Gets a string starting at idx and running until the next \x00
|
| 815 |
+
#
|
| 816 |
+
# We cannot pre-populate abbr_vals by splitting on \x00 because there
|
| 817 |
+
# are some zones that use subsets of longer abbreviations, like so:
|
| 818 |
+
#
|
| 819 |
+
# LMT\x00AHST\x00HDT\x00
|
| 820 |
+
#
|
| 821 |
+
# Where the idx to abbr mapping should be:
|
| 822 |
+
#
|
| 823 |
+
# {0: "LMT", 4: "AHST", 5: "HST", 9: "HDT"}
|
| 824 |
+
if idx not in abbr_vals:
|
| 825 |
+
span_end = abbr_chars.find(b"\x00", idx)
|
| 826 |
+
abbr_vals[idx] = abbr_chars[idx:span_end].decode()
|
| 827 |
+
|
| 828 |
+
return abbr_vals[idx]
|
| 829 |
+
|
| 830 |
+
abbr = tuple(get_abbr(idx) for idx in abbrind)
|
| 831 |
+
|
| 832 |
+
# The remainder of the file consists of leap seconds (currently unused) and
|
| 833 |
+
# the standard/wall and ut/local indicators, which are metadata we don't need.
|
| 834 |
+
# In version 2 files, we need to skip the unnecessary data to get at the TZ string,
|
| 835 |
+
# and in version 1 files we'll just hopefully consume the rest of the file.
|
| 836 |
+
|
| 837 |
+
# Each leap second record has size (time_size + 4)
|
| 838 |
+
skip_bytes = (
|
| 839 |
+
header.isutcnt + header.isstdcnt + header.leapcnt * (time_size + 4)
|
| 840 |
+
)
|
| 841 |
+
fobj.seek(skip_bytes, 1)
|
| 842 |
+
if header.version >= 2:
|
| 843 |
+
c = fobj.read(1) # Should be \n
|
| 844 |
+
if c != b"\n":
|
| 845 |
+
raise ValueError("Invalid TZif file: expected newline, got %r" % c)
|
| 846 |
+
|
| 847 |
+
line = fobj.readline()
|
| 848 |
+
if not line.endswith(b"\n"):
|
| 849 |
+
raise ValueError("Invalid TZif file: unexpected end of file")
|
| 850 |
+
tz_str = line.rstrip(b"\n")
|
| 851 |
+
else:
|
| 852 |
+
tz_str = None
|
| 853 |
+
|
| 854 |
+
return trans_idx, trans_list_utc, utcoff, isdst, abbr, tz_str
|
| 855 |
+
|
| 856 |
+
|
| 857 |
+
# How close in time (in seconds) the DST periods that _utcoff_to_dstoff uses
|
| 858 |
+
# to choose between two possible DST offsets have to be
|
| 859 |
+
_DST_CONSENSUS_WINDOW = 10 * 365 * 86400
|
| 860 |
+
|
| 861 |
+
|
| 862 |
+
class _TZifHeader(object):
|
| 863 |
+
__slots__ = [
|
| 864 |
+
"version",
|
| 865 |
+
"isutcnt",
|
| 866 |
+
"isstdcnt",
|
| 867 |
+
"leapcnt",
|
| 868 |
+
"timecnt",
|
| 869 |
+
"typecnt",
|
| 870 |
+
"charcnt",
|
| 871 |
+
]
|
| 872 |
+
|
| 873 |
+
def __init__(self, *args):
|
| 874 |
+
assert len(self.__slots__) == len(args)
|
| 875 |
+
for attr, val in zip(self.__slots__, args):
|
| 876 |
+
setattr(self, attr, val)
|
| 877 |
+
|
| 878 |
+
@classmethod
|
| 879 |
+
def from_file(cls, stream):
|
| 880 |
+
# The header starts with a 4-byte "magic" value
|
| 881 |
+
if stream.read(4) != b"TZif":
|
| 882 |
+
raise ValueError("Invalid TZif file: magic not found")
|
| 883 |
+
|
| 884 |
+
_version = stream.read(1)
|
| 885 |
+
if _version == b"\x00":
|
| 886 |
+
version = 1
|
| 887 |
+
else:
|
| 888 |
+
version = int(_version)
|
| 889 |
+
stream.read(15)
|
| 890 |
+
|
| 891 |
+
args = (version,)
|
| 892 |
+
|
| 893 |
+
# Slots are defined in the order that the bytes are arranged
|
| 894 |
+
args = args + struct.unpack(">6l", stream.read(24))
|
| 895 |
+
|
| 896 |
+
return cls(*args)
|
| 897 |
+
|
| 898 |
+
|
| 899 |
+
class _ttinfo(object):
|
| 900 |
+
__slots__ = ["utcoff", "dstoff", "tzname"]
|
| 901 |
+
|
| 902 |
+
def __init__(self, utcoff, dstoff, tzname):
|
| 903 |
+
self.utcoff = utcoff
|
| 904 |
+
self.dstoff = dstoff
|
| 905 |
+
self.tzname = tzname
|
| 906 |
+
|
| 907 |
+
def __eq__(self, other):
|
| 908 |
+
if not isinstance(other, _ttinfo):
|
| 909 |
+
return NotImplemented
|
| 910 |
+
|
| 911 |
+
return (
|
| 912 |
+
self.utcoff == other.utcoff
|
| 913 |
+
and self.dstoff == other.dstoff
|
| 914 |
+
and self.tzname == other.tzname
|
| 915 |
+
)
|
| 916 |
+
|
| 917 |
+
def __ne__(self, other):
|
| 918 |
+
# Python 2 does not derive __ne__ from __eq__
|
| 919 |
+
eq = self.__eq__(other)
|
| 920 |
+
if eq is NotImplemented:
|
| 921 |
+
return eq
|
| 922 |
+
return not eq
|
| 923 |
+
|
| 924 |
+
def __repr__(self): # pragma: nocover
|
| 925 |
+
return "%s(%s, %s, %s)" % (
|
| 926 |
+
self.__class__.__name__,
|
| 927 |
+
self.utcoff,
|
| 928 |
+
self.dstoff,
|
| 929 |
+
self.tzname,
|
| 930 |
+
)
|
| 931 |
+
|
| 932 |
+
|
| 933 |
+
_NO_TTINFO = _ttinfo(None, None, None)
|
| 934 |
+
|
| 935 |
+
|
| 936 |
+
def _unpack(t):
|
| 937 |
+
# Python 2-specific backport for x, *y = t
|
| 938 |
+
return t[0], t[1:]
|
| 939 |
+
|
| 940 |
+
|
| 941 |
+
# re.ASCII does not exist on Python 2, where str patterns are ASCII-only
|
| 942 |
+
# anyway (and unicode patterns are too unless re.UNICODE is passed).
|
| 943 |
+
_RE_ASCII = getattr(re, "ASCII", 0)
|
| 944 |
+
|
| 945 |
+
|
| 946 |
+
def _fullmatch(pattern, string, flags=0):
|
| 947 |
+
# Python 2-specific backport of re.fullmatch
|
| 948 |
+
return re.match(r"(?:%s)\Z" % pattern, string, flags)
|
| 949 |
+
|
| 950 |
+
|
| 951 |
+
def _round_offset(seconds):
|
| 952 |
+
# Python < 3.6 does not support sub-minute offsets, see the comment in
|
| 953 |
+
# tzfile._load_from_data.
|
| 954 |
+
if sys.version_info < (3, 6):
|
| 955 |
+
return 60 * ((seconds + 30) // 60)
|
| 956 |
+
return seconds
|
| 957 |
+
|
| 958 |
+
|
| 959 |
+
def _parse_tz_str(tz_str):
|
| 960 |
+
# The tz string has the format:
|
| 961 |
+
#
|
| 962 |
+
# std[offset[dst[offset],start[/time],end[/time]]]
|
| 963 |
+
#
|
| 964 |
+
# std and dst must be 3 or more characters long and must not contain
|
| 965 |
+
# a leading colon, embedded digits, commas, nor a plus or minus signs;
|
| 966 |
+
# The spaces between "std" and "offset" are only for display and are
|
| 967 |
+
# not actually present in the string.
|
| 968 |
+
#
|
| 969 |
+
# The format of the offset is ``[+|-]hh[:mm[:ss]]``
|
| 970 |
+
|
| 971 |
+
# TODO: When we drop Python 2, this should be offset_str, *start_end_str = ...
|
| 972 |
+
offset_str, start_end_str = _unpack(tz_str.split(",", 1))
|
| 973 |
+
|
| 974 |
+
parser_re = (
|
| 975 |
+
r"(?P<std>[a-zA-Z]+|<[a-zA-Z0-9+-]+>)"
|
| 976 |
+
r"(?:"
|
| 977 |
+
r"(?P<stdoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)"
|
| 978 |
+
r"(?:"
|
| 979 |
+
r"(?P<dst>[a-zA-Z]+|<[a-zA-Z0-9+-]+>)"
|
| 980 |
+
r"(?P<dstoff>[+-]?\d{1,3}(?::\d{2}(?::\d{2})?)?)?"
|
| 981 |
+
r")?" # dst
|
| 982 |
+
r")?" # stdoff
|
| 983 |
+
)
|
| 984 |
+
|
| 985 |
+
m = _fullmatch(parser_re, offset_str, _RE_ASCII)
|
| 986 |
+
|
| 987 |
+
if m is None:
|
| 988 |
+
raise ValueError("%s is not a valid TZ string" % tz_str)
|
| 989 |
+
|
| 990 |
+
std_abbr = m.group("std")
|
| 991 |
+
dst_abbr = m.group("dst")
|
| 992 |
+
dst_offset = None
|
| 993 |
+
|
| 994 |
+
std_abbr = std_abbr.strip("<>")
|
| 995 |
+
|
| 996 |
+
if dst_abbr:
|
| 997 |
+
dst_abbr = dst_abbr.strip("<>")
|
| 998 |
+
|
| 999 |
+
std_offset = m.group("stdoff")
|
| 1000 |
+
if std_offset:
|
| 1001 |
+
try:
|
| 1002 |
+
std_offset = _round_offset(_parse_tz_delta(std_offset))
|
| 1003 |
+
except ValueError as e:
|
| 1004 |
+
raise ValueError("Invalid STD offset in %s" % tz_str)
|
| 1005 |
+
else:
|
| 1006 |
+
# The STD offset is required
|
| 1007 |
+
raise ValueError("Invalid STD offset in %s" % tz_str)
|
| 1008 |
+
|
| 1009 |
+
if dst_abbr is not None:
|
| 1010 |
+
dst_offset = m.group("dstoff")
|
| 1011 |
+
if dst_offset:
|
| 1012 |
+
try:
|
| 1013 |
+
dst_offset = _round_offset(_parse_tz_delta(dst_offset))
|
| 1014 |
+
except ValueError as e:
|
| 1015 |
+
raise ValueError("Invalid DST offset in %s" % tz_str)
|
| 1016 |
+
else:
|
| 1017 |
+
dst_offset = std_offset + 3600
|
| 1018 |
+
|
| 1019 |
+
if not start_end_str:
|
| 1020 |
+
raise ValueError("Missing transition rules: %s" % tz_str)
|
| 1021 |
+
|
| 1022 |
+
start_end_strs = start_end_str[0].split(",", 1)
|
| 1023 |
+
try:
|
| 1024 |
+
start, end = (_parse_dst_start_end(x) for x in start_end_strs)
|
| 1025 |
+
except ValueError as e:
|
| 1026 |
+
raise ValueError("Invalid TZ string: %s" % tz_str)
|
| 1027 |
+
|
| 1028 |
+
return _TZStr(std_abbr, std_offset, dst_abbr, dst_offset, start, end)
|
| 1029 |
+
elif start_end_str:
|
| 1030 |
+
raise ValueError("Transition rule present without DST: %s" % tz_str)
|
| 1031 |
+
else:
|
| 1032 |
+
# This is a static ttinfo, don't return _TZStr
|
| 1033 |
+
return _ttinfo(
|
| 1034 |
+
_load_timedelta(std_offset), _load_timedelta(0), std_abbr
|
| 1035 |
+
)
|
| 1036 |
+
|
| 1037 |
+
|
| 1038 |
+
def _parse_dst_start_end(dststr):
|
| 1039 |
+
date, time = _unpack(dststr.split("/", 1))
|
| 1040 |
+
date_type = date[:1]
|
| 1041 |
+
if date_type == "M":
|
| 1042 |
+
n_is_julian = False
|
| 1043 |
+
m = _fullmatch(r"M(\d{1,2})\.(\d)\.(\d)", date, _RE_ASCII)
|
| 1044 |
+
if m is None:
|
| 1045 |
+
raise ValueError("Invalid dst start/end date: %s" % dststr)
|
| 1046 |
+
date_offset = tuple(map(int, m.groups()))
|
| 1047 |
+
offset = _CalendarOffset(*date_offset)
|
| 1048 |
+
else:
|
| 1049 |
+
if date_type == "J":
|
| 1050 |
+
n_is_julian = True
|
| 1051 |
+
date = date[1:]
|
| 1052 |
+
else:
|
| 1053 |
+
n_is_julian = False
|
| 1054 |
+
|
| 1055 |
+
if _fullmatch(r"\d{1,3}", date, _RE_ASCII) is None:
|
| 1056 |
+
raise ValueError("Invalid dst start/end date: %s" % dststr)
|
| 1057 |
+
doy = int(date)
|
| 1058 |
+
offset = _DayOffset(doy, n_is_julian)
|
| 1059 |
+
|
| 1060 |
+
if time:
|
| 1061 |
+
offset.hour, offset.minute, offset.second = _parse_transition_time(
|
| 1062 |
+
time[0]
|
| 1063 |
+
)
|
| 1064 |
+
|
| 1065 |
+
return offset
|
| 1066 |
+
|
| 1067 |
+
|
| 1068 |
+
def _parse_transition_time(time_str):
|
| 1069 |
+
match = _fullmatch(
|
| 1070 |
+
r"(?P<sign>[+-])?(?P<h>\d{1,3})(:(?P<m>\d{2})(:(?P<s>\d{2}))?)?",
|
| 1071 |
+
time_str,
|
| 1072 |
+
_RE_ASCII,
|
| 1073 |
+
)
|
| 1074 |
+
if match is None:
|
| 1075 |
+
raise ValueError("Invalid time: %s" % time_str)
|
| 1076 |
+
|
| 1077 |
+
h, m, s = (int(v or 0) for v in map(match.group, ("h", "m", "s")))
|
| 1078 |
+
|
| 1079 |
+
if h > 167:
|
| 1080 |
+
raise ValueError("Hour must be in [0, 167]: %s" % time_str)
|
| 1081 |
+
|
| 1082 |
+
if match.group("sign") == "-":
|
| 1083 |
+
h, m, s = -h, -m, -s
|
| 1084 |
+
|
| 1085 |
+
return h, m, s
|
| 1086 |
+
|
| 1087 |
+
|
| 1088 |
+
def _parse_tz_delta(tz_delta):
|
| 1089 |
+
match = _fullmatch(
|
| 1090 |
+
r"(?P<sign>[+-])?(?P<h>\d{1,3})(:(?P<m>\d{2})(:(?P<s>\d{2}))?)?",
|
| 1091 |
+
tz_delta,
|
| 1092 |
+
_RE_ASCII,
|
| 1093 |
+
)
|
| 1094 |
+
# Anything passed to this function should already have hit an equivalent
|
| 1095 |
+
# regular expression to find the section to parse.
|
| 1096 |
+
assert match is not None, tz_delta
|
| 1097 |
+
|
| 1098 |
+
h, m, s = (int(v or 0) for v in map(match.group, ("h", "m", "s")))
|
| 1099 |
+
|
| 1100 |
+
total = h * 3600 + m * 60 + s
|
| 1101 |
+
|
| 1102 |
+
if h > 24:
|
| 1103 |
+
raise ValueError("Offset hours must be in [0, 24]: %s" % tz_delta)
|
| 1104 |
+
|
| 1105 |
+
# Yes, +5 maps to an offset of -5h
|
| 1106 |
+
if match.group("sign") != "-":
|
| 1107 |
+
total *= -1
|
| 1108 |
+
|
| 1109 |
+
return total
|
| 1110 |
+
|
| 1111 |
+
|
| 1112 |
+
class _TZStr(object):
|
| 1113 |
+
__slots__ = (
|
| 1114 |
+
"std",
|
| 1115 |
+
"dst",
|
| 1116 |
+
"start",
|
| 1117 |
+
"end",
|
| 1118 |
+
"get_trans_info",
|
| 1119 |
+
"get_trans_info_fromutc",
|
| 1120 |
+
"dst_diff",
|
| 1121 |
+
)
|
| 1122 |
+
|
| 1123 |
+
def __init__(
|
| 1124 |
+
self, std_abbr, std_offset, dst_abbr, dst_offset, start=None, end=None
|
| 1125 |
+
):
|
| 1126 |
+
self.dst_diff = dst_offset - std_offset
|
| 1127 |
+
std_offset = _load_timedelta(std_offset)
|
| 1128 |
+
self.std = _ttinfo(
|
| 1129 |
+
utcoff=std_offset, dstoff=_load_timedelta(0), tzname=std_abbr
|
| 1130 |
+
)
|
| 1131 |
+
|
| 1132 |
+
self.start = start
|
| 1133 |
+
self.end = end
|
| 1134 |
+
|
| 1135 |
+
dst_offset = _load_timedelta(dst_offset)
|
| 1136 |
+
delta = _load_timedelta(self.dst_diff)
|
| 1137 |
+
self.dst = _ttinfo(utcoff=dst_offset, dstoff=delta, tzname=dst_abbr)
|
| 1138 |
+
|
| 1139 |
+
# These are assertions because the constructor should only be called
|
| 1140 |
+
# by functions that would fail before passing start or end
|
| 1141 |
+
assert start is not None, "No transition start specified"
|
| 1142 |
+
assert end is not None, "No transition end specified"
|
| 1143 |
+
|
| 1144 |
+
self.get_trans_info = self._get_trans_info
|
| 1145 |
+
self.get_trans_info_fromutc = self._get_trans_info_fromutc
|
| 1146 |
+
|
| 1147 |
+
def transitions(self, year):
|
| 1148 |
+
start = self.start.year_to_epoch(year)
|
| 1149 |
+
end = self.end.year_to_epoch(year)
|
| 1150 |
+
return start, end
|
| 1151 |
+
|
| 1152 |
+
def _near_new_year(self, ts, year):
|
| 1153 |
+
"""Whether ts is close enough to the new year that the rules for the
|
| 1154 |
+
previous or next year might matter."""
|
| 1155 |
+
year_start = _post_epoch_days_before_year(year) * 86400
|
| 1156 |
+
year_end = _post_epoch_days_before_year(year + 1) * 86400
|
| 1157 |
+
return (
|
| 1158 |
+
ts < year_start + _NEW_YEAR_MARGIN
|
| 1159 |
+
or ts >= year_end - _NEW_YEAR_MARGIN
|
| 1160 |
+
)
|
| 1161 |
+
|
| 1162 |
+
def _get_trans_info(self, ts, year, fold):
|
| 1163 |
+
"""Get the information about the current transition - tti"""
|
| 1164 |
+
if self._near_new_year(ts, year):
|
| 1165 |
+
return self._get_trans_info_near_new_year(ts, fold)
|
| 1166 |
+
|
| 1167 |
+
start, end = self.transitions(year)
|
| 1168 |
+
|
| 1169 |
+
# With fold = 0, the period (denominated in local time) with the
|
| 1170 |
+
# smaller offset starts at the end of the gap and ends at the end of
|
| 1171 |
+
# the fold; with fold = 1, it runs from the start of the gap to the
|
| 1172 |
+
# beginning of the fold.
|
| 1173 |
+
#
|
| 1174 |
+
# So in order to determine the DST boundaries we need to know both
|
| 1175 |
+
# the fold and whether DST is positive or negative (rare), and it
|
| 1176 |
+
# turns out that this boils down to fold XOR is_positive.
|
| 1177 |
+
if fold == (self.dst_diff >= 0):
|
| 1178 |
+
end -= self.dst_diff
|
| 1179 |
+
else:
|
| 1180 |
+
start += self.dst_diff
|
| 1181 |
+
|
| 1182 |
+
if start < end:
|
| 1183 |
+
isdst = start <= ts < end
|
| 1184 |
+
else:
|
| 1185 |
+
isdst = not (end <= ts < start)
|
| 1186 |
+
|
| 1187 |
+
return self.dst if isdst else self.std
|
| 1188 |
+
|
| 1189 |
+
def _get_trans_info_near_new_year(self, ts, fold):
|
| 1190 |
+
# Answer the question in terms of UTC, where the transitions from the
|
| 1191 |
+
# adjacent years are taken into account: a local time is valid for
|
| 1192 |
+
# an offset if the rules give that offset at the corresponding UTC
|
| 1193 |
+
# time.
|
| 1194 |
+
valid = []
|
| 1195 |
+
for tti in (self.std, self.dst):
|
| 1196 |
+
utc_ts = ts - tti.utcoff.total_seconds()
|
| 1197 |
+
try:
|
| 1198 |
+
utc_year = (EPOCH + timedelta(seconds=utc_ts)).year
|
| 1199 |
+
except OverflowError:
|
| 1200 |
+
# Before year 1 or after year 9999
|
| 1201 |
+
utc_year = MINYEAR if utc_ts < 0 else MAXYEAR
|
| 1202 |
+
actual, _ = self._get_trans_info_fromutc_near_new_year(
|
| 1203 |
+
utc_ts, utc_year
|
| 1204 |
+
)
|
| 1205 |
+
if actual is tti:
|
| 1206 |
+
valid.append(tti)
|
| 1207 |
+
|
| 1208 |
+
if len(valid) == 1:
|
| 1209 |
+
return valid[0]
|
| 1210 |
+
|
| 1211 |
+
smaller, larger = sorted((self.std, self.dst), key=lambda t: t.utcoff)
|
| 1212 |
+
if valid:
|
| 1213 |
+
# Ambiguous: fold=0 is the first occurrence, with the larger offset
|
| 1214 |
+
return larger if fold == 0 else smaller
|
| 1215 |
+
else:
|
| 1216 |
+
# Imaginary: fold=0 uses the offset from before the transition
|
| 1217 |
+
return smaller if fold == 0 else larger
|
| 1218 |
+
|
| 1219 |
+
def _get_trans_info_fromutc_near_new_year(self, ts, year):
|
| 1220 |
+
# The rules are in terms of the local year, which near the new year
|
| 1221 |
+
# is not always the UTC year, and a rule can put a transition up to a
|
| 1222 |
+
# week into the previous or next year (e.g. "J365/25"), so look at the
|
| 1223 |
+
# transitions for the years on either side as well and use the last
|
| 1224 |
+
# one before ts.
|
| 1225 |
+
std_utcoff = self.std.utcoff.total_seconds()
|
| 1226 |
+
dst_utcoff = self.dst.utcoff.total_seconds()
|
| 1227 |
+
|
| 1228 |
+
transitions = []
|
| 1229 |
+
for y in (year - 1, year, year + 1):
|
| 1230 |
+
start, end = self.transitions(y)
|
| 1231 |
+
transitions.append((start - std_utcoff, True))
|
| 1232 |
+
transitions.append((end - dst_utcoff, False))
|
| 1233 |
+
|
| 1234 |
+
# When one DST period ends at the same instant as the next one starts,
|
| 1235 |
+
# as with "DST all year" rules like "0/0,J365/25", sorting puts the
|
| 1236 |
+
# end first, so these stay in DST without a transition in between.
|
| 1237 |
+
transitions.sort()
|
| 1238 |
+
|
| 1239 |
+
isdst = not transitions[0][1]
|
| 1240 |
+
last_change = None
|
| 1241 |
+
i = 0
|
| 1242 |
+
while i < len(transitions) and transitions[i][0] <= ts:
|
| 1243 |
+
trans_ts = transitions[i][0]
|
| 1244 |
+
new_isdst = isdst
|
| 1245 |
+
while i < len(transitions) and transitions[i][0] == trans_ts:
|
| 1246 |
+
new_isdst = transitions[i][1]
|
| 1247 |
+
i += 1
|
| 1248 |
+
|
| 1249 |
+
if new_isdst != isdst:
|
| 1250 |
+
isdst = new_isdst
|
| 1251 |
+
last_change = trans_ts
|
| 1252 |
+
|
| 1253 |
+
if isdst:
|
| 1254 |
+
tti, tti_prev = self.dst, self.std
|
| 1255 |
+
else:
|
| 1256 |
+
tti, tti_prev = self.std, self.dst
|
| 1257 |
+
|
| 1258 |
+
# A transition to a smaller offset creates a fold just after it.
|
| 1259 |
+
fold = False
|
| 1260 |
+
if last_change is not None:
|
| 1261 |
+
shift = (tti_prev.utcoff - tti.utcoff).total_seconds()
|
| 1262 |
+
fold = shift > ts - last_change
|
| 1263 |
+
|
| 1264 |
+
return (tti, fold)
|
| 1265 |
+
|
| 1266 |
+
def _get_trans_info_fromutc(self, ts, year):
|
| 1267 |
+
if self._near_new_year(ts, year):
|
| 1268 |
+
return self._get_trans_info_fromutc_near_new_year(ts, year)
|
| 1269 |
+
|
| 1270 |
+
start, end = self.transitions(year)
|
| 1271 |
+
start -= self.std.utcoff.total_seconds()
|
| 1272 |
+
end -= self.dst.utcoff.total_seconds()
|
| 1273 |
+
|
| 1274 |
+
if start < end:
|
| 1275 |
+
isdst = start <= ts < end
|
| 1276 |
+
else:
|
| 1277 |
+
isdst = not (end <= ts < start)
|
| 1278 |
+
|
| 1279 |
+
# For positive DST, the ambiguous period is one dst_diff after the end
|
| 1280 |
+
# of DST; for negative DST, the ambiguous period is one dst_diff before
|
| 1281 |
+
# the start of DST.
|
| 1282 |
+
if self.dst_diff > 0:
|
| 1283 |
+
ambig_start = end
|
| 1284 |
+
ambig_end = end + self.dst_diff
|
| 1285 |
+
else:
|
| 1286 |
+
ambig_start = start
|
| 1287 |
+
ambig_end = start - self.dst_diff
|
| 1288 |
+
|
| 1289 |
+
fold = ambig_start <= ts < ambig_end
|
| 1290 |
+
|
| 1291 |
+
return (self.dst if isdst else self.std, fold)
|
| 1292 |
+
|
| 1293 |
+
|
| 1294 |
+
# A rule can put a transition up to 167 hours (just under a week) away from
|
| 1295 |
+
# the day it names, and local time is at most a day or so away from UTC, so
|
| 1296 |
+
# only times within this distance of the new year can be affected by the
|
| 1297 |
+
# rules for the previous or next year.
|
| 1298 |
+
_NEW_YEAR_MARGIN = 10 * 86400
|
| 1299 |
+
|
| 1300 |
+
|
| 1301 |
+
def _post_epoch_days_before_year(year):
|
| 1302 |
+
"""Get the number of days between 1970-01-01 and YEAR-01-01"""
|
| 1303 |
+
y = year - 1
|
| 1304 |
+
return y * 365 + y // 4 - y // 100 + y // 400 + 1 - EPOCHORDINAL
|
| 1305 |
+
|
| 1306 |
+
|
| 1307 |
+
class _DayOffset(object):
|
| 1308 |
+
__slots__ = ["d", "julian", "hour", "minute", "second"]
|
| 1309 |
+
|
| 1310 |
+
def __init__(self, d, julian, hour=2, minute=0, second=0):
|
| 1311 |
+
min_day = 0 + julian # convert bool to int
|
| 1312 |
+
if not min_day <= d <= 365:
|
| 1313 |
+
raise ValueError("d must be in [%s, 365], not: %s" % (min_day, d))
|
| 1314 |
+
|
| 1315 |
+
self.d = d
|
| 1316 |
+
self.julian = julian
|
| 1317 |
+
self.hour = hour
|
| 1318 |
+
self.minute = minute
|
| 1319 |
+
self.second = second
|
| 1320 |
+
|
| 1321 |
+
def year_to_epoch(self, year):
|
| 1322 |
+
days_before_year = _post_epoch_days_before_year(year)
|
| 1323 |
+
|
| 1324 |
+
# "n" counts days from 0 and includes February 29; "Jn" counts days
|
| 1325 |
+
# from 1 and never counts February 29, so in leap years, J60 (March 1)
|
| 1326 |
+
# and every day after it is one day later than its number suggests.
|
| 1327 |
+
d = self.d
|
| 1328 |
+
if self.julian:
|
| 1329 |
+
d -= 1
|
| 1330 |
+
if d >= 59 and calendar.isleap(year):
|
| 1331 |
+
d += 1
|
| 1332 |
+
|
| 1333 |
+
epoch = (days_before_year + d) * 86400
|
| 1334 |
+
epoch += self.hour * 3600 + self.minute * 60 + self.second
|
| 1335 |
+
|
| 1336 |
+
return epoch
|
| 1337 |
+
|
| 1338 |
+
|
| 1339 |
+
class _CalendarOffset(object):
|
| 1340 |
+
__slots__ = ["m", "w", "d", "hour", "minute", "second"]
|
| 1341 |
+
|
| 1342 |
+
_DAYS_BEFORE_MONTH = (
|
| 1343 |
+
-1,
|
| 1344 |
+
0,
|
| 1345 |
+
31,
|
| 1346 |
+
59,
|
| 1347 |
+
90,
|
| 1348 |
+
120,
|
| 1349 |
+
151,
|
| 1350 |
+
181,
|
| 1351 |
+
212,
|
| 1352 |
+
243,
|
| 1353 |
+
273,
|
| 1354 |
+
304,
|
| 1355 |
+
334,
|
| 1356 |
+
)
|
| 1357 |
+
|
| 1358 |
+
def __init__(self, m, w, d, hour=2, minute=0, second=0):
|
| 1359 |
+
if not 1 <= m <= 12:
|
| 1360 |
+
raise ValueError("m must be in [1, 12]")
|
| 1361 |
+
|
| 1362 |
+
if not 1 <= w <= 5:
|
| 1363 |
+
raise ValueError("w must be in [1, 5]")
|
| 1364 |
+
|
| 1365 |
+
if not 0 <= d <= 6:
|
| 1366 |
+
raise ValueError("d must be in [0, 6]")
|
| 1367 |
+
|
| 1368 |
+
self.m = m
|
| 1369 |
+
self.w = w
|
| 1370 |
+
self.d = d
|
| 1371 |
+
self.hour = hour
|
| 1372 |
+
self.minute = minute
|
| 1373 |
+
self.second = second
|
| 1374 |
+
|
| 1375 |
+
@classmethod
|
| 1376 |
+
def _ymd2ord(cls, year, month, day):
|
| 1377 |
+
return (
|
| 1378 |
+
_post_epoch_days_before_year(year)
|
| 1379 |
+
+ cls._DAYS_BEFORE_MONTH[month]
|
| 1380 |
+
+ (month > 2 and calendar.isleap(year))
|
| 1381 |
+
+ day
|
| 1382 |
+
- 1
|
| 1383 |
+
)
|
| 1384 |
+
|
| 1385 |
+
# TODO: These are not actually epoch dates as they are expressed in local time
|
| 1386 |
+
def year_to_epoch(self, year):
|
| 1387 |
+
"""Calculates the datetime of the occurrence from the year"""
|
| 1388 |
+
# We know year and month, we need to convert w, d into day of month
|
| 1389 |
+
#
|
| 1390 |
+
# Week 1 is the first week in which day `d` (where 0 = Sunday) appears.
|
| 1391 |
+
# Week 5 represents the last occurrence of day `d`, so we need to know
|
| 1392 |
+
# the range of the month.
|
| 1393 |
+
#
|
| 1394 |
+
# This doesn't use calendar.monthrange because the rules for years 0
|
| 1395 |
+
# and 10000 can affect times that datetime can represent, and
|
| 1396 |
+
# calendar.monthrange constructs a datetime.date. Instead, get the
|
| 1397 |
+
# weekday (0 = Monday) from the ordinal: 1970-01-01 was a Thursday.
|
| 1398 |
+
first_ordinal = self._ymd2ord(year, self.m, 1)
|
| 1399 |
+
first_day = (first_ordinal + 3) % 7
|
| 1400 |
+
if self.m == 12:
|
| 1401 |
+
next_ordinal = self._ymd2ord(year + 1, 1, 1)
|
| 1402 |
+
else:
|
| 1403 |
+
next_ordinal = self._ymd2ord(year, self.m + 1, 1)
|
| 1404 |
+
days_in_month = next_ordinal - first_ordinal
|
| 1405 |
+
|
| 1406 |
+
# This equation seems magical, so I'll break it down:
|
| 1407 |
+
# 1. calendar says 0 = Monday, POSIX says 0 = Sunday
|
| 1408 |
+
# so we need first_day + 1 to get 1 = Monday -> 7 = Sunday,
|
| 1409 |
+
# which is still equivalent because this math is mod 7
|
| 1410 |
+
# 2. Get first day - desired day mod 7: -1 % 7 = 6, so we don't need
|
| 1411 |
+
# to do anything to adjust negative numbers.
|
| 1412 |
+
# 3. Add 1 because month days are a 1-based index.
|
| 1413 |
+
month_day = (self.d - (first_day + 1)) % 7 + 1
|
| 1414 |
+
|
| 1415 |
+
# Now use a 0-based index version of `w` to calculate the w-th
|
| 1416 |
+
# occurrence of `d`
|
| 1417 |
+
month_day += (self.w - 1) * 7
|
| 1418 |
+
|
| 1419 |
+
# month_day will only be > days_in_month if w was 5, and `w` means
|
| 1420 |
+
# "last occurrence of `d`", so now we just check if we over-shot the
|
| 1421 |
+
# end of the month and if so knock off 1 week.
|
| 1422 |
+
if month_day > days_in_month:
|
| 1423 |
+
month_day -= 7
|
| 1424 |
+
|
| 1425 |
+
ordinal = self._ymd2ord(year, self.m, month_day)
|
| 1426 |
+
epoch = ordinal * 86400
|
| 1427 |
+
epoch += self.hour * 3600 + self.minute * 60 + self.second
|
| 1428 |
+
return epoch
|
hug/lib/python3.12/site-packages/dateutil/tz/_tzpath.py
ADDED
|
@@ -0,0 +1,104 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
try:
|
| 2 |
+
import zoneinfo
|
| 3 |
+
|
| 4 |
+
reset_tzpath = zoneinfo.reset_tzpath
|
| 5 |
+
InvalidTZPathWarning = zoneinfo.InvalidTZPathWarning
|
| 6 |
+
|
| 7 |
+
def __getattr__(name):
|
| 8 |
+
if name == "TZPATH":
|
| 9 |
+
return zoneinfo.TZPATH
|
| 10 |
+
else:
|
| 11 |
+
raise AttributeError(
|
| 12 |
+
"Module %s has no attribute %s" % (repr(__name__), repr(name))
|
| 13 |
+
)
|
| 14 |
+
|
| 15 |
+
except ImportError:
|
| 16 |
+
import os
|
| 17 |
+
import sys
|
| 18 |
+
import warnings
|
| 19 |
+
|
| 20 |
+
import six
|
| 21 |
+
|
| 22 |
+
class InvalidTZPathWarning(RuntimeWarning):
|
| 23 |
+
"""Warning raised if an invalid path is specified in PYTHONTZPATH."""
|
| 24 |
+
|
| 25 |
+
def _parse_python_tzpath(env_var, stacklevel):
|
| 26 |
+
if not env_var:
|
| 27 |
+
return ()
|
| 28 |
+
|
| 29 |
+
raw_tzpath = env_var.split(os.pathsep)
|
| 30 |
+
new_tzpath = tuple(filter(os.path.isabs, raw_tzpath))
|
| 31 |
+
|
| 32 |
+
# If anything has been filtered out, we will warn about it
|
| 33 |
+
if len(new_tzpath) != len(raw_tzpath):
|
| 34 |
+
msg = _get_invalid_paths_message(raw_tzpath)
|
| 35 |
+
|
| 36 |
+
warnings.warn(
|
| 37 |
+
"Invalid paths specified in PYTHONTZPATH environment variable. "
|
| 38 |
+
+ msg,
|
| 39 |
+
InvalidTZPathWarning,
|
| 40 |
+
stacklevel=stacklevel,
|
| 41 |
+
)
|
| 42 |
+
|
| 43 |
+
return new_tzpath
|
| 44 |
+
|
| 45 |
+
def _get_invalid_paths_message(tzpaths):
|
| 46 |
+
invalid_paths = (path for path in tzpaths if not os.path.isabs(path))
|
| 47 |
+
|
| 48 |
+
prefix = "\n "
|
| 49 |
+
indented_str = prefix + prefix.join(invalid_paths)
|
| 50 |
+
|
| 51 |
+
return (
|
| 52 |
+
"Paths should be absolute but found the following relative paths:"
|
| 53 |
+
+ indented_str
|
| 54 |
+
)
|
| 55 |
+
|
| 56 |
+
def _reset_tzpath(to=None, stacklevel=4):
|
| 57 |
+
global TZPATH
|
| 58 |
+
tzpaths = to
|
| 59 |
+
if tzpaths is not None:
|
| 60 |
+
if isinstance(tzpaths, six.string_types + (bytes,)):
|
| 61 |
+
raise TypeError(
|
| 62 |
+
"tzpaths must be a sequence, not %s: %s"
|
| 63 |
+
% (
|
| 64 |
+
type(tzpaths),
|
| 65 |
+
repr(tzpaths),
|
| 66 |
+
)
|
| 67 |
+
)
|
| 68 |
+
|
| 69 |
+
# Path-like objects not supported by os.path operations before 3.6
|
| 70 |
+
if sys.version_info < (3, 6):
|
| 71 |
+
tzpaths = tuple(map(str, tzpaths))
|
| 72 |
+
|
| 73 |
+
if not all(map(os.path.isabs, tzpaths)):
|
| 74 |
+
raise ValueError(_get_invalid_paths_message(tzpaths))
|
| 75 |
+
base_tzpath = tzpaths
|
| 76 |
+
else:
|
| 77 |
+
env_var = os.environ.get("PYTHONTZPATH", None)
|
| 78 |
+
if env_var is not None:
|
| 79 |
+
base_tzpath = _parse_python_tzpath(env_var, stacklevel)
|
| 80 |
+
elif sys.platform != "win32":
|
| 81 |
+
base_tzpath = (
|
| 82 |
+
"/usr/share/zoneinfo",
|
| 83 |
+
"/usr/lib/zoneinfo",
|
| 84 |
+
"/usr/share/lib/zoneinfo",
|
| 85 |
+
"/etc/zoneinfo",
|
| 86 |
+
)
|
| 87 |
+
else:
|
| 88 |
+
base_tzpath = ()
|
| 89 |
+
|
| 90 |
+
TZPATH = tuple(base_tzpath)
|
| 91 |
+
if TZPATH_CALLBACKS:
|
| 92 |
+
for callback in TZPATH_CALLBACKS:
|
| 93 |
+
callback(TZPATH)
|
| 94 |
+
|
| 95 |
+
def reset_tzpath(to=None):
|
| 96 |
+
# As in CPython, the warning about PYTHONTZPATH is raised from a
|
| 97 |
+
# helper that is used both at import time and by this function, so
|
| 98 |
+
# that each can pass the stacklevel that points at its caller.
|
| 99 |
+
_reset_tzpath(to)
|
| 100 |
+
|
| 101 |
+
TZPATH = ()
|
| 102 |
+
TZPATH_CALLBACKS = []
|
| 103 |
+
|
| 104 |
+
_reset_tzpath(stacklevel=5)
|
hug/lib/python3.12/site-packages/dateutil/tz/tz.py
ADDED
|
@@ -0,0 +1,1472 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
This module offers timezone implementations subclassing the abstract
|
| 4 |
+
:py:class:`datetime.tzinfo` type. There are classes to handle tzfile format
|
| 5 |
+
files (usually are in :file:`/etc/localtime`, :file:`/usr/share/zoneinfo`,
|
| 6 |
+
etc), TZ environment string (in all known formats), given ranges (with help
|
| 7 |
+
from relative deltas), local machine timezone, fixed offset timezone, and UTC
|
| 8 |
+
timezone.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import bisect
|
| 12 |
+
import datetime
|
| 13 |
+
import os
|
| 14 |
+
import struct
|
| 15 |
+
import sys
|
| 16 |
+
import time
|
| 17 |
+
import warnings
|
| 18 |
+
import weakref
|
| 19 |
+
from collections import OrderedDict
|
| 20 |
+
|
| 21 |
+
import six
|
| 22 |
+
from six import string_types
|
| 23 |
+
from six.moves import _thread
|
| 24 |
+
|
| 25 |
+
from .. import _tzdata_impl
|
| 26 |
+
from . import _tzpath
|
| 27 |
+
from ._common import (
|
| 28 |
+
_tzinfo,
|
| 29 |
+
_validate_fromutc_inputs,
|
| 30 |
+
enfold,
|
| 31 |
+
tzname_in_python2,
|
| 32 |
+
tzrangebase,
|
| 33 |
+
)
|
| 34 |
+
from ._factories import _TzOffsetFactory, _TzSingleton, _TzStrFactory
|
| 35 |
+
from ._tzfile import tzfile
|
| 36 |
+
|
| 37 |
+
try:
|
| 38 |
+
from .win import tzwin, tzwinlocal
|
| 39 |
+
except ImportError:
|
| 40 |
+
tzwin = tzwinlocal = None
|
| 41 |
+
|
| 42 |
+
# For warning about rounding tzinfo
|
| 43 |
+
from warnings import warn
|
| 44 |
+
|
| 45 |
+
ZERO = datetime.timedelta(0)
|
| 46 |
+
EPOCH = datetime.datetime(1970, 1, 1, 0, 0)
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@six.add_metaclass(_TzSingleton)
|
| 50 |
+
class tzutc(datetime.tzinfo):
|
| 51 |
+
"""
|
| 52 |
+
This is a tzinfo object that represents the UTC time zone.
|
| 53 |
+
|
| 54 |
+
**Examples:**
|
| 55 |
+
|
| 56 |
+
.. doctest::
|
| 57 |
+
|
| 58 |
+
>>> from datetime import *
|
| 59 |
+
>>> from dateutil.tz import *
|
| 60 |
+
|
| 61 |
+
>>> datetime.now()
|
| 62 |
+
datetime.datetime(2003, 9, 27, 9, 40, 1, 521290)
|
| 63 |
+
|
| 64 |
+
>>> datetime.now(tzutc())
|
| 65 |
+
datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc())
|
| 66 |
+
|
| 67 |
+
>>> datetime.now(tzutc()).tzname()
|
| 68 |
+
'UTC'
|
| 69 |
+
|
| 70 |
+
.. versionchanged:: 2.7.0
|
| 71 |
+
``tzutc()`` is now a singleton, so the result of ``tzutc()`` will
|
| 72 |
+
always return the same object.
|
| 73 |
+
|
| 74 |
+
.. doctest::
|
| 75 |
+
|
| 76 |
+
>>> from dateutil.tz import tzutc, UTC
|
| 77 |
+
>>> tzutc() is tzutc()
|
| 78 |
+
True
|
| 79 |
+
>>> tzutc() is UTC
|
| 80 |
+
True
|
| 81 |
+
"""
|
| 82 |
+
def utcoffset(self, dt):
|
| 83 |
+
return ZERO
|
| 84 |
+
|
| 85 |
+
def dst(self, dt):
|
| 86 |
+
return ZERO
|
| 87 |
+
|
| 88 |
+
@tzname_in_python2
|
| 89 |
+
def tzname(self, dt):
|
| 90 |
+
return "UTC"
|
| 91 |
+
|
| 92 |
+
def is_ambiguous(self, dt):
|
| 93 |
+
"""
|
| 94 |
+
Whether or not the "wall time" of a given datetime is ambiguous in this
|
| 95 |
+
zone.
|
| 96 |
+
|
| 97 |
+
:param dt:
|
| 98 |
+
A :py:class:`datetime.datetime`, naive or time zone aware.
|
| 99 |
+
|
| 100 |
+
|
| 101 |
+
:return:
|
| 102 |
+
Returns ``True`` if ambiguous, ``False`` otherwise.
|
| 103 |
+
|
| 104 |
+
.. versionadded:: 2.6.0
|
| 105 |
+
"""
|
| 106 |
+
return False
|
| 107 |
+
|
| 108 |
+
@_validate_fromutc_inputs
|
| 109 |
+
def fromutc(self, dt):
|
| 110 |
+
"""
|
| 111 |
+
Fast track version of fromutc() returns the original ``dt`` object for
|
| 112 |
+
any valid :py:class:`datetime.datetime` object.
|
| 113 |
+
"""
|
| 114 |
+
return dt
|
| 115 |
+
|
| 116 |
+
def __eq__(self, other):
|
| 117 |
+
if not isinstance(other, (tzutc, tzoffset)):
|
| 118 |
+
return NotImplemented
|
| 119 |
+
|
| 120 |
+
return (isinstance(other, tzutc) or
|
| 121 |
+
(isinstance(other, tzoffset) and other._offset == ZERO))
|
| 122 |
+
|
| 123 |
+
__hash__ = None
|
| 124 |
+
|
| 125 |
+
def __ne__(self, other):
|
| 126 |
+
return not (self == other)
|
| 127 |
+
|
| 128 |
+
def __repr__(self):
|
| 129 |
+
return "%s()" % self.__class__.__name__
|
| 130 |
+
|
| 131 |
+
__reduce__ = object.__reduce__
|
| 132 |
+
|
| 133 |
+
|
| 134 |
+
#: Convenience constant providing a :class:`tzutc()` instance
|
| 135 |
+
#:
|
| 136 |
+
#: .. versionadded:: 2.7.0
|
| 137 |
+
UTC = tzutc()
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
@six.add_metaclass(_TzOffsetFactory)
|
| 141 |
+
class tzoffset(datetime.tzinfo):
|
| 142 |
+
"""
|
| 143 |
+
A simple class for representing a fixed offset from UTC.
|
| 144 |
+
|
| 145 |
+
:param name:
|
| 146 |
+
The timezone name, to be returned when ``tzname()`` is called.
|
| 147 |
+
:param offset:
|
| 148 |
+
The time zone offset in seconds, or (since version 2.6.0, represented
|
| 149 |
+
as a :py:class:`datetime.timedelta` object).
|
| 150 |
+
"""
|
| 151 |
+
def __init__(self, name, offset):
|
| 152 |
+
self._name = name
|
| 153 |
+
|
| 154 |
+
try:
|
| 155 |
+
# Allow a timedelta
|
| 156 |
+
offset = offset.total_seconds()
|
| 157 |
+
except (TypeError, AttributeError):
|
| 158 |
+
pass
|
| 159 |
+
|
| 160 |
+
self._offset = datetime.timedelta(seconds=_get_supported_offset(offset))
|
| 161 |
+
|
| 162 |
+
def utcoffset(self, dt):
|
| 163 |
+
return self._offset
|
| 164 |
+
|
| 165 |
+
def dst(self, dt):
|
| 166 |
+
return ZERO
|
| 167 |
+
|
| 168 |
+
@tzname_in_python2
|
| 169 |
+
def tzname(self, dt):
|
| 170 |
+
return self._name
|
| 171 |
+
|
| 172 |
+
@_validate_fromutc_inputs
|
| 173 |
+
def fromutc(self, dt):
|
| 174 |
+
return dt + self._offset
|
| 175 |
+
|
| 176 |
+
def is_ambiguous(self, dt):
|
| 177 |
+
"""
|
| 178 |
+
Whether or not the "wall time" of a given datetime is ambiguous in this
|
| 179 |
+
zone.
|
| 180 |
+
|
| 181 |
+
:param dt:
|
| 182 |
+
A :py:class:`datetime.datetime`, naive or time zone aware.
|
| 183 |
+
:return:
|
| 184 |
+
Returns ``True`` if ambiguous, ``False`` otherwise.
|
| 185 |
+
|
| 186 |
+
.. versionadded:: 2.6.0
|
| 187 |
+
"""
|
| 188 |
+
return False
|
| 189 |
+
|
| 190 |
+
def __eq__(self, other):
|
| 191 |
+
if not isinstance(other, tzoffset):
|
| 192 |
+
return NotImplemented
|
| 193 |
+
|
| 194 |
+
return self._offset == other._offset
|
| 195 |
+
|
| 196 |
+
__hash__ = None
|
| 197 |
+
|
| 198 |
+
def __ne__(self, other):
|
| 199 |
+
return not (self == other)
|
| 200 |
+
|
| 201 |
+
def __repr__(self):
|
| 202 |
+
return "%s(%s, %s)" % (self.__class__.__name__,
|
| 203 |
+
repr(self._name),
|
| 204 |
+
int(self._offset.total_seconds()))
|
| 205 |
+
|
| 206 |
+
__reduce__ = object.__reduce__
|
| 207 |
+
|
| 208 |
+
|
| 209 |
+
class tzlocal(_tzinfo):
|
| 210 |
+
"""
|
| 211 |
+
A :class:`tzinfo` subclass built around the ``time`` timezone functions.
|
| 212 |
+
"""
|
| 213 |
+
def __init__(self):
|
| 214 |
+
super(tzlocal, self).__init__()
|
| 215 |
+
|
| 216 |
+
self._std_offset = datetime.timedelta(seconds=-time.timezone)
|
| 217 |
+
if time.daylight:
|
| 218 |
+
self._dst_offset = datetime.timedelta(seconds=-time.altzone)
|
| 219 |
+
else:
|
| 220 |
+
self._dst_offset = self._std_offset
|
| 221 |
+
|
| 222 |
+
self._dst_saved = self._dst_offset - self._std_offset
|
| 223 |
+
self._hasdst = bool(self._dst_saved)
|
| 224 |
+
self._tznames = tuple(time.tzname)
|
| 225 |
+
|
| 226 |
+
def utcoffset(self, dt):
|
| 227 |
+
if dt is None and self._hasdst:
|
| 228 |
+
return None
|
| 229 |
+
|
| 230 |
+
if self._isdst(dt):
|
| 231 |
+
return self._dst_offset
|
| 232 |
+
else:
|
| 233 |
+
return self._std_offset
|
| 234 |
+
|
| 235 |
+
def dst(self, dt):
|
| 236 |
+
if dt is None and self._hasdst:
|
| 237 |
+
return None
|
| 238 |
+
|
| 239 |
+
if self._isdst(dt):
|
| 240 |
+
return self._dst_offset - self._std_offset
|
| 241 |
+
else:
|
| 242 |
+
return ZERO
|
| 243 |
+
|
| 244 |
+
@tzname_in_python2
|
| 245 |
+
def tzname(self, dt):
|
| 246 |
+
return self._tznames[self._isdst(dt)]
|
| 247 |
+
|
| 248 |
+
def is_ambiguous(self, dt):
|
| 249 |
+
"""
|
| 250 |
+
Whether or not the "wall time" of a given datetime is ambiguous in this
|
| 251 |
+
zone.
|
| 252 |
+
|
| 253 |
+
:param dt:
|
| 254 |
+
A :py:class:`datetime.datetime`, naive or time zone aware.
|
| 255 |
+
|
| 256 |
+
|
| 257 |
+
:return:
|
| 258 |
+
Returns ``True`` if ambiguous, ``False`` otherwise.
|
| 259 |
+
|
| 260 |
+
.. versionadded:: 2.6.0
|
| 261 |
+
"""
|
| 262 |
+
naive_dst = self._naive_is_dst(dt)
|
| 263 |
+
return (not naive_dst and
|
| 264 |
+
(naive_dst != self._naive_is_dst(dt - self._dst_saved)))
|
| 265 |
+
|
| 266 |
+
def _naive_is_dst(self, dt):
|
| 267 |
+
timestamp = _datetime_to_timestamp(dt)
|
| 268 |
+
return time.localtime(timestamp + time.timezone).tm_isdst
|
| 269 |
+
|
| 270 |
+
def _isdst(self, dt, fold_naive=True):
|
| 271 |
+
# We can't use mktime here. It is unstable when deciding if
|
| 272 |
+
# the hour near to a change is DST or not.
|
| 273 |
+
#
|
| 274 |
+
# timestamp = time.mktime((dt.year, dt.month, dt.day, dt.hour,
|
| 275 |
+
# dt.minute, dt.second, dt.weekday(), 0, -1))
|
| 276 |
+
# return time.localtime(timestamp).tm_isdst
|
| 277 |
+
#
|
| 278 |
+
# The code above yields the following result:
|
| 279 |
+
#
|
| 280 |
+
# >>> import tz, datetime
|
| 281 |
+
# >>> t = tz.tzlocal()
|
| 282 |
+
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
| 283 |
+
# 'BRDT'
|
| 284 |
+
# >>> datetime.datetime(2003,2,16,0,tzinfo=t).tzname()
|
| 285 |
+
# 'BRST'
|
| 286 |
+
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
| 287 |
+
# 'BRST'
|
| 288 |
+
# >>> datetime.datetime(2003,2,15,22,tzinfo=t).tzname()
|
| 289 |
+
# 'BRDT'
|
| 290 |
+
# >>> datetime.datetime(2003,2,15,23,tzinfo=t).tzname()
|
| 291 |
+
# 'BRDT'
|
| 292 |
+
#
|
| 293 |
+
# Here is a more stable implementation:
|
| 294 |
+
#
|
| 295 |
+
if not self._hasdst:
|
| 296 |
+
return False
|
| 297 |
+
|
| 298 |
+
# Check for ambiguous times:
|
| 299 |
+
dstval = self._naive_is_dst(dt)
|
| 300 |
+
fold = getattr(dt, 'fold', None)
|
| 301 |
+
|
| 302 |
+
if self.is_ambiguous(dt):
|
| 303 |
+
if fold is not None:
|
| 304 |
+
return not self._fold(dt)
|
| 305 |
+
else:
|
| 306 |
+
return True
|
| 307 |
+
|
| 308 |
+
return dstval
|
| 309 |
+
|
| 310 |
+
def __eq__(self, other):
|
| 311 |
+
if isinstance(other, tzlocal):
|
| 312 |
+
return (self._std_offset == other._std_offset and
|
| 313 |
+
self._dst_offset == other._dst_offset)
|
| 314 |
+
elif isinstance(other, tzutc):
|
| 315 |
+
return (not self._hasdst and
|
| 316 |
+
self._tznames[0] in {'UTC', 'GMT'} and
|
| 317 |
+
self._std_offset == ZERO)
|
| 318 |
+
elif isinstance(other, tzoffset):
|
| 319 |
+
return (not self._hasdst and
|
| 320 |
+
self._tznames[0] == other._name and
|
| 321 |
+
self._std_offset == other._offset)
|
| 322 |
+
else:
|
| 323 |
+
return NotImplemented
|
| 324 |
+
|
| 325 |
+
__hash__ = None
|
| 326 |
+
|
| 327 |
+
def __ne__(self, other):
|
| 328 |
+
return not (self == other)
|
| 329 |
+
|
| 330 |
+
def __repr__(self):
|
| 331 |
+
return "%s()" % self.__class__.__name__
|
| 332 |
+
|
| 333 |
+
__reduce__ = object.__reduce__
|
| 334 |
+
|
| 335 |
+
|
| 336 |
+
class tzrange(tzrangebase):
|
| 337 |
+
"""
|
| 338 |
+
The ``tzrange`` object is a time zone specified by a set of offsets and
|
| 339 |
+
abbreviations, equivalent to the way the ``TZ`` variable can be specified
|
| 340 |
+
in POSIX-like systems, but using Python delta objects to specify DST
|
| 341 |
+
start, end and offsets.
|
| 342 |
+
|
| 343 |
+
:param stdabbr:
|
| 344 |
+
The abbreviation for standard time (e.g. ``'EST'``).
|
| 345 |
+
|
| 346 |
+
:param stdoffset:
|
| 347 |
+
An integer or :class:`datetime.timedelta` object or equivalent
|
| 348 |
+
specifying the base offset from UTC.
|
| 349 |
+
|
| 350 |
+
If unspecified, +00:00 is used.
|
| 351 |
+
|
| 352 |
+
:param dstabbr:
|
| 353 |
+
The abbreviation for DST / "Summer" time (e.g. ``'EDT'``).
|
| 354 |
+
|
| 355 |
+
If specified, with no other DST information, DST is assumed to occur
|
| 356 |
+
and the default behavior or ``dstoffset``, ``start`` and ``end`` is
|
| 357 |
+
used. If unspecified and no other DST information is specified, it
|
| 358 |
+
is assumed that this zone has no DST.
|
| 359 |
+
|
| 360 |
+
If this is unspecified and other DST information is *is* specified,
|
| 361 |
+
DST occurs in the zone but the time zone abbreviation is left
|
| 362 |
+
unchanged.
|
| 363 |
+
|
| 364 |
+
:param dstoffset:
|
| 365 |
+
A an integer or :class:`datetime.timedelta` object or equivalent
|
| 366 |
+
specifying the UTC offset during DST. If unspecified and any other DST
|
| 367 |
+
information is specified, it is assumed to be the STD offset +1 hour.
|
| 368 |
+
|
| 369 |
+
:param start:
|
| 370 |
+
A :class:`relativedelta.relativedelta` object or equivalent specifying
|
| 371 |
+
the time and time of year that daylight savings time starts. To
|
| 372 |
+
specify, for example, that DST starts at 2AM on the 2nd Sunday in
|
| 373 |
+
March, pass:
|
| 374 |
+
|
| 375 |
+
``relativedelta(hours=2, month=3, day=1, weekday=SU(+2))``
|
| 376 |
+
|
| 377 |
+
If unspecified and any other DST information is specified, the default
|
| 378 |
+
value is 2 AM on the first Sunday in April.
|
| 379 |
+
|
| 380 |
+
:param end:
|
| 381 |
+
A :class:`relativedelta.relativedelta` object or equivalent
|
| 382 |
+
representing the time and time of year that daylight savings time
|
| 383 |
+
ends, with the same specification method as in ``start``. One note is
|
| 384 |
+
that this should point to the first time in the *standard* zone, so if
|
| 385 |
+
a transition occurs at 2AM in the DST zone and the clocks are set back
|
| 386 |
+
1 hour to 1AM, set the ``hours`` parameter to +1.
|
| 387 |
+
|
| 388 |
+
|
| 389 |
+
**Examples:**
|
| 390 |
+
|
| 391 |
+
.. testsetup:: tzrange
|
| 392 |
+
|
| 393 |
+
from dateutil.tz import tzrange, tzstr
|
| 394 |
+
|
| 395 |
+
.. doctest:: tzrange
|
| 396 |
+
|
| 397 |
+
>>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT")
|
| 398 |
+
True
|
| 399 |
+
|
| 400 |
+
>>> from dateutil.relativedelta import *
|
| 401 |
+
>>> range1 = tzrange("EST", -18000, "EDT")
|
| 402 |
+
>>> range2 = tzrange("EST", -18000, "EDT", -14400,
|
| 403 |
+
... relativedelta(hours=+2, month=4, day=1,
|
| 404 |
+
... weekday=SU(+1)),
|
| 405 |
+
... relativedelta(hours=+1, month=10, day=31,
|
| 406 |
+
... weekday=SU(-1)))
|
| 407 |
+
>>> tzstr('EST5EDT') == range1 == range2
|
| 408 |
+
True
|
| 409 |
+
|
| 410 |
+
"""
|
| 411 |
+
def __init__(self, stdabbr, stdoffset=None,
|
| 412 |
+
dstabbr=None, dstoffset=None,
|
| 413 |
+
start=None, end=None):
|
| 414 |
+
|
| 415 |
+
global relativedelta
|
| 416 |
+
from dateutil import relativedelta
|
| 417 |
+
|
| 418 |
+
self._std_abbr = stdabbr
|
| 419 |
+
self._dst_abbr = dstabbr
|
| 420 |
+
|
| 421 |
+
try:
|
| 422 |
+
stdoffset = stdoffset.total_seconds()
|
| 423 |
+
except (TypeError, AttributeError):
|
| 424 |
+
pass
|
| 425 |
+
|
| 426 |
+
try:
|
| 427 |
+
dstoffset = dstoffset.total_seconds()
|
| 428 |
+
except (TypeError, AttributeError):
|
| 429 |
+
pass
|
| 430 |
+
|
| 431 |
+
if stdoffset is not None:
|
| 432 |
+
self._std_offset = datetime.timedelta(seconds=stdoffset)
|
| 433 |
+
else:
|
| 434 |
+
self._std_offset = ZERO
|
| 435 |
+
|
| 436 |
+
if dstoffset is not None:
|
| 437 |
+
self._dst_offset = datetime.timedelta(seconds=dstoffset)
|
| 438 |
+
elif dstabbr and stdoffset is not None:
|
| 439 |
+
self._dst_offset = self._std_offset + datetime.timedelta(hours=+1)
|
| 440 |
+
else:
|
| 441 |
+
self._dst_offset = ZERO
|
| 442 |
+
|
| 443 |
+
if dstabbr and start is None:
|
| 444 |
+
self._start_delta = relativedelta.relativedelta(
|
| 445 |
+
hours=+2, month=4, day=1, weekday=relativedelta.SU(+1))
|
| 446 |
+
else:
|
| 447 |
+
self._start_delta = start
|
| 448 |
+
|
| 449 |
+
if dstabbr and end is None:
|
| 450 |
+
self._end_delta = relativedelta.relativedelta(
|
| 451 |
+
hours=+1, month=10, day=31, weekday=relativedelta.SU(-1))
|
| 452 |
+
else:
|
| 453 |
+
self._end_delta = end
|
| 454 |
+
|
| 455 |
+
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
| 456 |
+
self.hasdst = bool(self._start_delta)
|
| 457 |
+
|
| 458 |
+
def transitions(self, year):
|
| 459 |
+
"""
|
| 460 |
+
For a given year, get the DST on and off transition times, expressed
|
| 461 |
+
always on the standard time side. For zones with no transitions, this
|
| 462 |
+
function returns ``None``.
|
| 463 |
+
|
| 464 |
+
:param year:
|
| 465 |
+
The year whose transitions you would like to query.
|
| 466 |
+
|
| 467 |
+
:return:
|
| 468 |
+
Returns a :class:`tuple` of :class:`datetime.datetime` objects,
|
| 469 |
+
``(dston, dstoff)`` for zones with an annual DST transition, or
|
| 470 |
+
``None`` for fixed offset zones.
|
| 471 |
+
"""
|
| 472 |
+
if not self.hasdst:
|
| 473 |
+
return None
|
| 474 |
+
|
| 475 |
+
base_year = datetime.datetime(year, 1, 1)
|
| 476 |
+
|
| 477 |
+
start = base_year + self._start_delta
|
| 478 |
+
end = base_year + self._end_delta
|
| 479 |
+
|
| 480 |
+
return (start, end)
|
| 481 |
+
|
| 482 |
+
def __eq__(self, other):
|
| 483 |
+
if not isinstance(other, tzrange):
|
| 484 |
+
return NotImplemented
|
| 485 |
+
|
| 486 |
+
return (self._std_abbr == other._std_abbr and
|
| 487 |
+
self._dst_abbr == other._dst_abbr and
|
| 488 |
+
self._std_offset == other._std_offset and
|
| 489 |
+
self._dst_offset == other._dst_offset and
|
| 490 |
+
self._start_delta == other._start_delta and
|
| 491 |
+
self._end_delta == other._end_delta)
|
| 492 |
+
|
| 493 |
+
@property
|
| 494 |
+
def _dst_base_offset(self):
|
| 495 |
+
return self._dst_base_offset_
|
| 496 |
+
|
| 497 |
+
|
| 498 |
+
@six.add_metaclass(_TzStrFactory)
|
| 499 |
+
class tzstr(tzrange):
|
| 500 |
+
"""
|
| 501 |
+
``tzstr`` objects are time zone objects specified by a time-zone string as
|
| 502 |
+
it would be passed to a ``TZ`` variable on POSIX-style systems (see
|
| 503 |
+
the `GNU C Library: TZ Variable`_ for more details).
|
| 504 |
+
|
| 505 |
+
There is one notable exception, which is that POSIX-style time zones use an
|
| 506 |
+
inverted offset format, so normally ``GMT+3`` would be parsed as an offset
|
| 507 |
+
3 hours *behind* GMT. The ``tzstr`` time zone object will parse this as an
|
| 508 |
+
offset 3 hours *ahead* of GMT. If you would like to maintain the POSIX
|
| 509 |
+
behavior, pass a ``True`` value to ``posix_offset``.
|
| 510 |
+
|
| 511 |
+
The :class:`tzrange` object provides the same functionality, but is
|
| 512 |
+
specified using :class:`relativedelta.relativedelta` objects. rather than
|
| 513 |
+
strings.
|
| 514 |
+
|
| 515 |
+
:param s:
|
| 516 |
+
A time zone string in ``TZ`` variable format. This can be a
|
| 517 |
+
:class:`bytes` (2.x: :class:`str`), :class:`str` (2.x:
|
| 518 |
+
:class:`unicode`) or a stream emitting unicode characters
|
| 519 |
+
(e.g. :class:`StringIO`).
|
| 520 |
+
|
| 521 |
+
:param posix_offset:
|
| 522 |
+
Optional. If set to ``True``, interpret strings such as ``GMT+3`` or
|
| 523 |
+
``UTC+3`` as being 3 hours *behind* UTC rather than ahead, per the
|
| 524 |
+
POSIX standard.
|
| 525 |
+
|
| 526 |
+
.. caution::
|
| 527 |
+
|
| 528 |
+
Prior to version 2.7.0, this function also supported time zones
|
| 529 |
+
in the format:
|
| 530 |
+
|
| 531 |
+
* ``EST5EDT,4,0,6,7200,10,0,26,7200,3600``
|
| 532 |
+
* ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600``
|
| 533 |
+
|
| 534 |
+
This format is non-standard and has been deprecated; this function
|
| 535 |
+
will raise a :class:`DeprecatedTZFormatWarning` until
|
| 536 |
+
support is removed in a future version.
|
| 537 |
+
|
| 538 |
+
.. _`GNU C Library: TZ Variable`:
|
| 539 |
+
https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
|
| 540 |
+
"""
|
| 541 |
+
def __init__(self, s, posix_offset=False):
|
| 542 |
+
global parser
|
| 543 |
+
from dateutil.parser import _parser as parser
|
| 544 |
+
|
| 545 |
+
self._s = s
|
| 546 |
+
|
| 547 |
+
res = parser._parsetz(s)
|
| 548 |
+
if res is None or res.any_unused_tokens:
|
| 549 |
+
raise ValueError("unknown string format")
|
| 550 |
+
|
| 551 |
+
# Here we break the compatibility with the TZ variable handling.
|
| 552 |
+
# GMT-3 actually *means* the timezone -3.
|
| 553 |
+
if res.stdabbr in ("GMT", "UTC") and not posix_offset:
|
| 554 |
+
res.stdoffset *= -1
|
| 555 |
+
|
| 556 |
+
# We must initialize it first, since _delta() needs
|
| 557 |
+
# _std_offset and _dst_offset set. Use False in start/end
|
| 558 |
+
# to avoid building it two times.
|
| 559 |
+
tzrange.__init__(self, res.stdabbr, res.stdoffset,
|
| 560 |
+
res.dstabbr, res.dstoffset,
|
| 561 |
+
start=False, end=False)
|
| 562 |
+
|
| 563 |
+
if not res.dstabbr:
|
| 564 |
+
self._start_delta = None
|
| 565 |
+
self._end_delta = None
|
| 566 |
+
else:
|
| 567 |
+
self._start_delta = self._delta(res.start)
|
| 568 |
+
if self._start_delta:
|
| 569 |
+
self._end_delta = self._delta(res.end, isend=1)
|
| 570 |
+
|
| 571 |
+
self.hasdst = bool(self._start_delta)
|
| 572 |
+
|
| 573 |
+
def _delta(self, x, isend=0):
|
| 574 |
+
from dateutil import relativedelta
|
| 575 |
+
kwargs = {}
|
| 576 |
+
if x.month is not None:
|
| 577 |
+
kwargs["month"] = x.month
|
| 578 |
+
if x.weekday is not None:
|
| 579 |
+
kwargs["weekday"] = relativedelta.weekday(x.weekday, x.week)
|
| 580 |
+
if x.week > 0:
|
| 581 |
+
kwargs["day"] = 1
|
| 582 |
+
else:
|
| 583 |
+
kwargs["day"] = 31
|
| 584 |
+
elif x.day:
|
| 585 |
+
kwargs["day"] = x.day
|
| 586 |
+
elif x.yday is not None:
|
| 587 |
+
kwargs["yearday"] = x.yday
|
| 588 |
+
elif x.jyday is not None:
|
| 589 |
+
kwargs["nlyearday"] = x.jyday
|
| 590 |
+
if not kwargs:
|
| 591 |
+
# Default is to start on first sunday of april, and end
|
| 592 |
+
# on last sunday of october.
|
| 593 |
+
if not isend:
|
| 594 |
+
kwargs["month"] = 4
|
| 595 |
+
kwargs["day"] = 1
|
| 596 |
+
kwargs["weekday"] = relativedelta.SU(+1)
|
| 597 |
+
else:
|
| 598 |
+
kwargs["month"] = 10
|
| 599 |
+
kwargs["day"] = 31
|
| 600 |
+
kwargs["weekday"] = relativedelta.SU(-1)
|
| 601 |
+
if x.time is not None:
|
| 602 |
+
kwargs["seconds"] = x.time
|
| 603 |
+
else:
|
| 604 |
+
# Default is 2AM.
|
| 605 |
+
kwargs["seconds"] = 7200
|
| 606 |
+
if isend:
|
| 607 |
+
# Convert to standard time, to follow the documented way
|
| 608 |
+
# of working with the extra hour. See the documentation
|
| 609 |
+
# of the tzinfo class.
|
| 610 |
+
delta = self._dst_offset - self._std_offset
|
| 611 |
+
kwargs["seconds"] -= delta.seconds + delta.days * 86400
|
| 612 |
+
return relativedelta.relativedelta(**kwargs)
|
| 613 |
+
|
| 614 |
+
def __repr__(self):
|
| 615 |
+
return "%s(%s)" % (self.__class__.__name__, repr(self._s))
|
| 616 |
+
|
| 617 |
+
|
| 618 |
+
class _tzicalvtzcomp(object):
|
| 619 |
+
def __init__(self, tzoffsetfrom, tzoffsetto, isdst,
|
| 620 |
+
tzname=None, rrule=None):
|
| 621 |
+
self.tzoffsetfrom = datetime.timedelta(seconds=tzoffsetfrom)
|
| 622 |
+
self.tzoffsetto = datetime.timedelta(seconds=tzoffsetto)
|
| 623 |
+
self.tzoffsetdiff = self.tzoffsetto - self.tzoffsetfrom
|
| 624 |
+
self.isdst = isdst
|
| 625 |
+
self.tzname = tzname
|
| 626 |
+
self.rrule = rrule
|
| 627 |
+
|
| 628 |
+
|
| 629 |
+
class _tzicalvtz(_tzinfo):
|
| 630 |
+
def __init__(self, tzid, comps=[]):
|
| 631 |
+
super(_tzicalvtz, self).__init__()
|
| 632 |
+
|
| 633 |
+
self._tzid = tzid
|
| 634 |
+
self._comps = comps
|
| 635 |
+
self._cachedate = []
|
| 636 |
+
self._cachecomp = []
|
| 637 |
+
self._cache_lock = _thread.allocate_lock()
|
| 638 |
+
|
| 639 |
+
def _find_comp(self, dt):
|
| 640 |
+
if len(self._comps) == 1:
|
| 641 |
+
return self._comps[0]
|
| 642 |
+
|
| 643 |
+
dt = dt.replace(tzinfo=None)
|
| 644 |
+
|
| 645 |
+
try:
|
| 646 |
+
with self._cache_lock:
|
| 647 |
+
return self._cachecomp[self._cachedate.index(
|
| 648 |
+
(dt, self._fold(dt)))]
|
| 649 |
+
except ValueError:
|
| 650 |
+
pass
|
| 651 |
+
|
| 652 |
+
lastcompdt = None
|
| 653 |
+
lastcomp = None
|
| 654 |
+
|
| 655 |
+
for comp in self._comps:
|
| 656 |
+
compdt = self._find_compdt(comp, dt)
|
| 657 |
+
|
| 658 |
+
if compdt and (not lastcompdt or lastcompdt < compdt):
|
| 659 |
+
lastcompdt = compdt
|
| 660 |
+
lastcomp = comp
|
| 661 |
+
|
| 662 |
+
if not lastcomp:
|
| 663 |
+
# RFC says nothing about what to do when a given
|
| 664 |
+
# time is before the first onset date. We'll look for the
|
| 665 |
+
# first standard component, or the first component, if
|
| 666 |
+
# none is found.
|
| 667 |
+
for comp in self._comps:
|
| 668 |
+
if not comp.isdst:
|
| 669 |
+
lastcomp = comp
|
| 670 |
+
break
|
| 671 |
+
else:
|
| 672 |
+
lastcomp = comp[0]
|
| 673 |
+
|
| 674 |
+
with self._cache_lock:
|
| 675 |
+
self._cachedate.insert(0, (dt, self._fold(dt)))
|
| 676 |
+
self._cachecomp.insert(0, lastcomp)
|
| 677 |
+
|
| 678 |
+
if len(self._cachedate) > 10:
|
| 679 |
+
self._cachedate.pop()
|
| 680 |
+
self._cachecomp.pop()
|
| 681 |
+
|
| 682 |
+
return lastcomp
|
| 683 |
+
|
| 684 |
+
def _find_compdt(self, comp, dt):
|
| 685 |
+
if comp.tzoffsetdiff < ZERO and self._fold(dt):
|
| 686 |
+
dt -= comp.tzoffsetdiff
|
| 687 |
+
|
| 688 |
+
compdt = comp.rrule.before(dt, inc=True)
|
| 689 |
+
|
| 690 |
+
return compdt
|
| 691 |
+
|
| 692 |
+
def utcoffset(self, dt):
|
| 693 |
+
if dt is None:
|
| 694 |
+
return None
|
| 695 |
+
|
| 696 |
+
return self._find_comp(dt).tzoffsetto
|
| 697 |
+
|
| 698 |
+
def dst(self, dt):
|
| 699 |
+
comp = self._find_comp(dt)
|
| 700 |
+
if comp.isdst:
|
| 701 |
+
return comp.tzoffsetdiff
|
| 702 |
+
else:
|
| 703 |
+
return ZERO
|
| 704 |
+
|
| 705 |
+
@tzname_in_python2
|
| 706 |
+
def tzname(self, dt):
|
| 707 |
+
return self._find_comp(dt).tzname
|
| 708 |
+
|
| 709 |
+
def __repr__(self):
|
| 710 |
+
return "<tzicalvtz %s>" % repr(self._tzid)
|
| 711 |
+
|
| 712 |
+
__reduce__ = object.__reduce__
|
| 713 |
+
|
| 714 |
+
|
| 715 |
+
class tzical(object):
|
| 716 |
+
"""
|
| 717 |
+
This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure
|
| 718 |
+
as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects.
|
| 719 |
+
|
| 720 |
+
:param `fileobj`:
|
| 721 |
+
A file or stream in iCalendar format, which should be UTF-8 encoded
|
| 722 |
+
with CRLF endings.
|
| 723 |
+
|
| 724 |
+
.. _`RFC 5545`: https://tools.ietf.org/html/rfc5545
|
| 725 |
+
"""
|
| 726 |
+
def __init__(self, fileobj):
|
| 727 |
+
global rrule
|
| 728 |
+
from dateutil import rrule
|
| 729 |
+
|
| 730 |
+
if isinstance(fileobj, string_types):
|
| 731 |
+
self._s = fileobj
|
| 732 |
+
# ical should be encoded in UTF-8 with CRLF
|
| 733 |
+
fileobj = open(fileobj, 'r')
|
| 734 |
+
else:
|
| 735 |
+
self._s = getattr(fileobj, 'name', repr(fileobj))
|
| 736 |
+
fileobj = _nullcontext(fileobj)
|
| 737 |
+
|
| 738 |
+
self._vtz = {}
|
| 739 |
+
|
| 740 |
+
with fileobj as fobj:
|
| 741 |
+
self._parse_rfc(fobj.read())
|
| 742 |
+
|
| 743 |
+
def keys(self):
|
| 744 |
+
"""
|
| 745 |
+
Retrieves the available time zones as a list.
|
| 746 |
+
"""
|
| 747 |
+
return list(self._vtz.keys())
|
| 748 |
+
|
| 749 |
+
def get(self, tzid=None):
|
| 750 |
+
"""
|
| 751 |
+
Retrieve a :py:class:`datetime.tzinfo` object by its ``tzid``.
|
| 752 |
+
|
| 753 |
+
:param tzid:
|
| 754 |
+
If there is exactly one time zone available, omitting ``tzid``
|
| 755 |
+
or passing :py:const:`None` value returns it. Otherwise a valid
|
| 756 |
+
key (which can be retrieved from :func:`keys`) is required.
|
| 757 |
+
|
| 758 |
+
:raises ValueError:
|
| 759 |
+
Raised if ``tzid`` is not specified but there are either more
|
| 760 |
+
or fewer than 1 zone defined.
|
| 761 |
+
|
| 762 |
+
:returns:
|
| 763 |
+
Returns either a :py:class:`datetime.tzinfo` object representing
|
| 764 |
+
the relevant time zone or :py:const:`None` if the ``tzid`` was
|
| 765 |
+
not found.
|
| 766 |
+
"""
|
| 767 |
+
if tzid is None:
|
| 768 |
+
if len(self._vtz) == 0:
|
| 769 |
+
raise ValueError("no timezones defined")
|
| 770 |
+
elif len(self._vtz) > 1:
|
| 771 |
+
raise ValueError("more than one timezone available")
|
| 772 |
+
tzid = next(iter(self._vtz))
|
| 773 |
+
|
| 774 |
+
return self._vtz.get(tzid)
|
| 775 |
+
|
| 776 |
+
def _parse_offset(self, s):
|
| 777 |
+
s = s.strip()
|
| 778 |
+
if not s:
|
| 779 |
+
raise ValueError("empty offset")
|
| 780 |
+
if s[0] in ('+', '-'):
|
| 781 |
+
signal = (-1, +1)[s[0] == '+']
|
| 782 |
+
s = s[1:]
|
| 783 |
+
else:
|
| 784 |
+
signal = +1
|
| 785 |
+
if len(s) == 4:
|
| 786 |
+
return (int(s[:2]) * 3600 + int(s[2:]) * 60) * signal
|
| 787 |
+
elif len(s) == 6:
|
| 788 |
+
return (int(s[:2]) * 3600 + int(s[2:4]) * 60 + int(s[4:])) * signal
|
| 789 |
+
else:
|
| 790 |
+
raise ValueError("invalid offset: " + s)
|
| 791 |
+
|
| 792 |
+
def _parse_rfc(self, s):
|
| 793 |
+
lines = s.splitlines()
|
| 794 |
+
if not lines:
|
| 795 |
+
raise ValueError("empty string")
|
| 796 |
+
|
| 797 |
+
# Unfold
|
| 798 |
+
i = 0
|
| 799 |
+
while i < len(lines):
|
| 800 |
+
line = lines[i].rstrip()
|
| 801 |
+
if not line:
|
| 802 |
+
del lines[i]
|
| 803 |
+
elif i > 0 and line[0] == " ":
|
| 804 |
+
lines[i-1] += line[1:]
|
| 805 |
+
del lines[i]
|
| 806 |
+
else:
|
| 807 |
+
i += 1
|
| 808 |
+
|
| 809 |
+
tzid = None
|
| 810 |
+
comps = []
|
| 811 |
+
invtz = False
|
| 812 |
+
comptype = None
|
| 813 |
+
for line in lines:
|
| 814 |
+
if not line:
|
| 815 |
+
continue
|
| 816 |
+
name, value = line.split(':', 1)
|
| 817 |
+
parms = name.split(';')
|
| 818 |
+
if not parms:
|
| 819 |
+
raise ValueError("empty property name")
|
| 820 |
+
name = parms[0].upper()
|
| 821 |
+
parms = parms[1:]
|
| 822 |
+
if invtz:
|
| 823 |
+
if name == "BEGIN":
|
| 824 |
+
if value in ("STANDARD", "DAYLIGHT"):
|
| 825 |
+
# Process component
|
| 826 |
+
pass
|
| 827 |
+
else:
|
| 828 |
+
raise ValueError("unknown component: "+value)
|
| 829 |
+
comptype = value
|
| 830 |
+
founddtstart = False
|
| 831 |
+
tzoffsetfrom = None
|
| 832 |
+
tzoffsetto = None
|
| 833 |
+
rrulelines = []
|
| 834 |
+
tzname = None
|
| 835 |
+
elif name == "END":
|
| 836 |
+
if value == "VTIMEZONE":
|
| 837 |
+
if comptype:
|
| 838 |
+
raise ValueError("component not closed: "+comptype)
|
| 839 |
+
if not tzid:
|
| 840 |
+
raise ValueError("mandatory TZID not found")
|
| 841 |
+
if not comps:
|
| 842 |
+
raise ValueError(
|
| 843 |
+
"at least one component is needed")
|
| 844 |
+
# Process vtimezone
|
| 845 |
+
self._vtz[tzid] = _tzicalvtz(tzid, comps)
|
| 846 |
+
invtz = False
|
| 847 |
+
elif value == comptype:
|
| 848 |
+
if not founddtstart:
|
| 849 |
+
raise ValueError("mandatory DTSTART not found")
|
| 850 |
+
if tzoffsetfrom is None:
|
| 851 |
+
raise ValueError(
|
| 852 |
+
"mandatory TZOFFSETFROM not found")
|
| 853 |
+
if tzoffsetto is None:
|
| 854 |
+
raise ValueError(
|
| 855 |
+
"mandatory TZOFFSETFROM not found")
|
| 856 |
+
# Process component
|
| 857 |
+
rr = None
|
| 858 |
+
if rrulelines:
|
| 859 |
+
rr = rrule.rrulestr("\n".join(rrulelines),
|
| 860 |
+
compatible=True,
|
| 861 |
+
ignoretz=True,
|
| 862 |
+
cache=True)
|
| 863 |
+
comp = _tzicalvtzcomp(tzoffsetfrom, tzoffsetto,
|
| 864 |
+
(comptype == "DAYLIGHT"),
|
| 865 |
+
tzname, rr)
|
| 866 |
+
comps.append(comp)
|
| 867 |
+
comptype = None
|
| 868 |
+
else:
|
| 869 |
+
raise ValueError("invalid component end: "+value)
|
| 870 |
+
elif comptype:
|
| 871 |
+
if name == "DTSTART":
|
| 872 |
+
# DTSTART in VTIMEZONE takes a subset of valid RRULE
|
| 873 |
+
# values under RFC 5545.
|
| 874 |
+
for parm in parms:
|
| 875 |
+
if parm != 'VALUE=DATE-TIME':
|
| 876 |
+
msg = ('Unsupported DTSTART param in ' +
|
| 877 |
+
'VTIMEZONE: ' + parm)
|
| 878 |
+
raise ValueError(msg)
|
| 879 |
+
rrulelines.append(line)
|
| 880 |
+
founddtstart = True
|
| 881 |
+
elif name in ("RRULE", "RDATE", "EXRULE", "EXDATE"):
|
| 882 |
+
rrulelines.append(line)
|
| 883 |
+
elif name == "TZOFFSETFROM":
|
| 884 |
+
if parms:
|
| 885 |
+
raise ValueError(
|
| 886 |
+
"unsupported %s parm: %s " % (name, parms[0]))
|
| 887 |
+
tzoffsetfrom = self._parse_offset(value)
|
| 888 |
+
elif name == "TZOFFSETTO":
|
| 889 |
+
if parms:
|
| 890 |
+
raise ValueError(
|
| 891 |
+
"unsupported TZOFFSETTO parm: "+parms[0])
|
| 892 |
+
tzoffsetto = self._parse_offset(value)
|
| 893 |
+
elif name == "TZNAME":
|
| 894 |
+
if parms:
|
| 895 |
+
raise ValueError(
|
| 896 |
+
"unsupported TZNAME parm: "+parms[0])
|
| 897 |
+
tzname = value
|
| 898 |
+
elif name == "COMMENT":
|
| 899 |
+
pass
|
| 900 |
+
else:
|
| 901 |
+
raise ValueError("unsupported property: "+name)
|
| 902 |
+
else:
|
| 903 |
+
if name == "TZID":
|
| 904 |
+
if parms:
|
| 905 |
+
raise ValueError(
|
| 906 |
+
"unsupported TZID parm: "+parms[0])
|
| 907 |
+
tzid = value
|
| 908 |
+
elif name in ("TZURL", "LAST-MODIFIED", "COMMENT"):
|
| 909 |
+
pass
|
| 910 |
+
else:
|
| 911 |
+
raise ValueError("unsupported property: "+name)
|
| 912 |
+
elif name == "BEGIN" and value == "VTIMEZONE":
|
| 913 |
+
tzid = None
|
| 914 |
+
comps = []
|
| 915 |
+
invtz = True
|
| 916 |
+
|
| 917 |
+
def __repr__(self):
|
| 918 |
+
return "%s(%s)" % (self.__class__.__name__, repr(self._s))
|
| 919 |
+
|
| 920 |
+
|
| 921 |
+
TZFILES = ["/etc/localtime", "localtime"]
|
| 922 |
+
|
| 923 |
+
def __get_gettz():
|
| 924 |
+
tzlocal_classes = (tzlocal,)
|
| 925 |
+
if tzwinlocal is not None:
|
| 926 |
+
tzlocal_classes += (tzwinlocal,)
|
| 927 |
+
|
| 928 |
+
class GettzFunc(object):
|
| 929 |
+
"""
|
| 930 |
+
Retrieve a time zone object from a string representation
|
| 931 |
+
|
| 932 |
+
This function is intended to retrieve the :py:class:`tzinfo` subclass
|
| 933 |
+
that best represents the time zone that would be used if a POSIX
|
| 934 |
+
`TZ variable`_ were set to the same value.
|
| 935 |
+
|
| 936 |
+
If no argument or an empty string is passed to ``gettz``, local time
|
| 937 |
+
is returned:
|
| 938 |
+
|
| 939 |
+
.. code-block:: python3
|
| 940 |
+
|
| 941 |
+
>>> gettz()
|
| 942 |
+
tzfile('/etc/localtime')
|
| 943 |
+
|
| 944 |
+
This function is also the preferred way to map IANA tz database keys
|
| 945 |
+
to :class:`tzfile` objects. It searches for these keys in the
|
| 946 |
+
following order:
|
| 947 |
+
|
| 948 |
+
1. On the search path defined in :data:`TZPATH`.
|
| 949 |
+
2. In the `tzdata <https://pypi.org/project/tzdata/>`_ Python package.
|
| 950 |
+
3. Using the system-specific mechanisms (e.g. the Windows registry).
|
| 951 |
+
|
| 952 |
+
.. code-block:: python3
|
| 953 |
+
|
| 954 |
+
>>> gettz('Pacific/Kiritimati')
|
| 955 |
+
tzfile('/usr/share/zoneinfo/Pacific/Kiritimati')
|
| 956 |
+
|
| 957 |
+
On Windows, the standard is extended to include the Windows-specific
|
| 958 |
+
zone names provided by the operating system:
|
| 959 |
+
|
| 960 |
+
.. code-block:: python3
|
| 961 |
+
|
| 962 |
+
>>> gettz('Egypt Standard Time')
|
| 963 |
+
tzwin('Egypt Standard Time')
|
| 964 |
+
|
| 965 |
+
Passing a GNU ``TZ`` style string time zone specification returns a
|
| 966 |
+
:class:`tzstr` object:
|
| 967 |
+
|
| 968 |
+
.. code-block:: python3
|
| 969 |
+
|
| 970 |
+
>>> gettz('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
|
| 971 |
+
tzstr('AEST-10AEDT-11,M10.1.0/2,M4.1.0/3')
|
| 972 |
+
|
| 973 |
+
:param name:
|
| 974 |
+
A time zone name (IANA, or, on Windows, Windows keys), location of
|
| 975 |
+
a ``tzfile(5)`` zoneinfo file or ``TZ`` variable style time zone
|
| 976 |
+
specifier. An empty string, no argument or ``None`` is interpreted
|
| 977 |
+
as local time.
|
| 978 |
+
|
| 979 |
+
:return:
|
| 980 |
+
Returns an instance of one of ``dateutil``'s :py:class:`tzinfo`
|
| 981 |
+
subclasses.
|
| 982 |
+
|
| 983 |
+
.. versionchanged:: 2.7.0
|
| 984 |
+
|
| 985 |
+
After version 2.7.0, any two calls to ``gettz`` using the same
|
| 986 |
+
input strings will return the same object:
|
| 987 |
+
|
| 988 |
+
.. code-block:: python3
|
| 989 |
+
|
| 990 |
+
>>> tz.gettz('America/Chicago') is tz.gettz('America/Chicago')
|
| 991 |
+
True
|
| 992 |
+
|
| 993 |
+
In addition to improving performance, this ensures that
|
| 994 |
+
`"same zone" semantics`_ are used for datetimes in the same zone.
|
| 995 |
+
|
| 996 |
+
|
| 997 |
+
.. _`TZ variable`:
|
| 998 |
+
https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html
|
| 999 |
+
|
| 1000 |
+
.. _`"same zone" semantics`:
|
| 1001 |
+
https://blog.ganssle.io/articles/2018/02/aware-datetime-arithmetic.html
|
| 1002 |
+
"""
|
| 1003 |
+
def __init__(self):
|
| 1004 |
+
|
| 1005 |
+
self.__instances = weakref.WeakValueDictionary()
|
| 1006 |
+
self.__strong_cache_size = 8
|
| 1007 |
+
self.__strong_cache = OrderedDict()
|
| 1008 |
+
self._cache_lock = _thread.allocate_lock()
|
| 1009 |
+
|
| 1010 |
+
def __call__(self, name=None):
|
| 1011 |
+
with self._cache_lock:
|
| 1012 |
+
rv = self.__instances.get(name, None)
|
| 1013 |
+
|
| 1014 |
+
if rv is None:
|
| 1015 |
+
# TODO: This holds the cache lock while nocache() reads
|
| 1016 |
+
# the file, so a slow lookup (e.g. walking TZPATH on a
|
| 1017 |
+
# network file system) blocks every other gettz call,
|
| 1018 |
+
# including cache hits for unrelated keys.
|
| 1019 |
+
rv = self.nocache(name=name)
|
| 1020 |
+
if not (name is None
|
| 1021 |
+
or isinstance(rv, tzlocal_classes)
|
| 1022 |
+
or rv is None):
|
| 1023 |
+
# tzlocal is slightly more complicated than the other
|
| 1024 |
+
# time zone providers because it depends on environment
|
| 1025 |
+
# at construction time, so don't cache that.
|
| 1026 |
+
#
|
| 1027 |
+
# We also cannot store weak references to None, so we
|
| 1028 |
+
# will also not store that.
|
| 1029 |
+
self.__instances[name] = rv
|
| 1030 |
+
else:
|
| 1031 |
+
# No need for strong caching, return immediately
|
| 1032 |
+
return rv
|
| 1033 |
+
|
| 1034 |
+
self.__strong_cache[name] = self.__strong_cache.pop(name, rv)
|
| 1035 |
+
|
| 1036 |
+
if len(self.__strong_cache) > self.__strong_cache_size:
|
| 1037 |
+
self.__strong_cache.popitem(last=False)
|
| 1038 |
+
|
| 1039 |
+
return rv
|
| 1040 |
+
|
| 1041 |
+
def set_cache_size(self, size):
|
| 1042 |
+
with self._cache_lock:
|
| 1043 |
+
self.__strong_cache_size = size
|
| 1044 |
+
while len(self.__strong_cache) > size:
|
| 1045 |
+
self.__strong_cache.popitem(last=False)
|
| 1046 |
+
|
| 1047 |
+
def cache_clear(self):
|
| 1048 |
+
with self._cache_lock:
|
| 1049 |
+
self.__instances = weakref.WeakValueDictionary()
|
| 1050 |
+
self.__strong_cache.clear()
|
| 1051 |
+
|
| 1052 |
+
@staticmethod
|
| 1053 |
+
def nocache(name=None):
|
| 1054 |
+
"""A non-cached version of gettz"""
|
| 1055 |
+
tz = None
|
| 1056 |
+
if not name:
|
| 1057 |
+
try:
|
| 1058 |
+
name = os.environ["TZ"]
|
| 1059 |
+
except KeyError:
|
| 1060 |
+
pass
|
| 1061 |
+
if name is None or name in ("", ":"):
|
| 1062 |
+
for filepath in TZFILES:
|
| 1063 |
+
if not os.path.isabs(filepath):
|
| 1064 |
+
filename = filepath
|
| 1065 |
+
for path in _tzpath.TZPATH:
|
| 1066 |
+
filepath = os.path.join(path, filename)
|
| 1067 |
+
if _isfile(filepath):
|
| 1068 |
+
break
|
| 1069 |
+
else:
|
| 1070 |
+
continue
|
| 1071 |
+
if _isfile(filepath):
|
| 1072 |
+
try:
|
| 1073 |
+
tz = tzfile(filepath)
|
| 1074 |
+
break
|
| 1075 |
+
except (IOError, OSError, ValueError):
|
| 1076 |
+
pass
|
| 1077 |
+
else:
|
| 1078 |
+
tz = tzlocal()
|
| 1079 |
+
else:
|
| 1080 |
+
try:
|
| 1081 |
+
if name.startswith(":"):
|
| 1082 |
+
name = name[1:]
|
| 1083 |
+
except TypeError as e:
|
| 1084 |
+
if isinstance(name, bytes):
|
| 1085 |
+
new_msg = "gettz argument should be str, not bytes"
|
| 1086 |
+
six.raise_from(TypeError(new_msg), e)
|
| 1087 |
+
else:
|
| 1088 |
+
raise
|
| 1089 |
+
|
| 1090 |
+
# A NUL byte can never be part of a key or a path, and
|
| 1091 |
+
# depending on the platform and Python version it raises
|
| 1092 |
+
# ValueError from os.path, winreg or open(), so reject it
|
| 1093 |
+
# up front rather than catching it in every branch.
|
| 1094 |
+
if "\x00" in name:
|
| 1095 |
+
return None
|
| 1096 |
+
|
| 1097 |
+
if os.path.isabs(name):
|
| 1098 |
+
if _isfile(name):
|
| 1099 |
+
tz = tzfile(name)
|
| 1100 |
+
else:
|
| 1101 |
+
tz = None
|
| 1102 |
+
else:
|
| 1103 |
+
if _escapes_tzpath(name):
|
| 1104 |
+
_warn_tzpath_escape(name)
|
| 1105 |
+
|
| 1106 |
+
for path in _tzpath.TZPATH:
|
| 1107 |
+
filepath = os.path.join(path, name)
|
| 1108 |
+
if not _isfile(filepath):
|
| 1109 |
+
filepath = filepath.replace(' ', '_')
|
| 1110 |
+
if not _isfile(filepath):
|
| 1111 |
+
continue
|
| 1112 |
+
try:
|
| 1113 |
+
tz = tzfile(filepath, key=name)
|
| 1114 |
+
break
|
| 1115 |
+
except (IOError, OSError, ValueError):
|
| 1116 |
+
# TODO: A file that exists but can't be read or
|
| 1117 |
+
# isn't a valid TZif file is silently skipped,
|
| 1118 |
+
# and the key is looked up in the next TZPATH
|
| 1119 |
+
# entry and then in tzdata. Consider warning
|
| 1120 |
+
# here, since this usually means the system time
|
| 1121 |
+
# zone data is broken.
|
| 1122 |
+
pass
|
| 1123 |
+
else:
|
| 1124 |
+
tz = None
|
| 1125 |
+
# As with the search path lookup above, also try
|
| 1126 |
+
# the name with spaces replaced by underscores.
|
| 1127 |
+
candidates = [name]
|
| 1128 |
+
if " " in name:
|
| 1129 |
+
candidates.append(name.replace(" ", "_"))
|
| 1130 |
+
|
| 1131 |
+
for candidate in candidates:
|
| 1132 |
+
try:
|
| 1133 |
+
with _tzdata_impl._load_tzdata(candidate) as f:
|
| 1134 |
+
return tzfile(f, key=name)
|
| 1135 |
+
except ValueError:
|
| 1136 |
+
# TZFileNotFound is a ValueError, and so is
|
| 1137 |
+
# the error raised for files in tzdata that
|
| 1138 |
+
# are not TZif files (e.g. "zone.tab"),
|
| 1139 |
+
# which are skipped just as on TZPATH.
|
| 1140 |
+
pass
|
| 1141 |
+
|
| 1142 |
+
if tzwin is not None:
|
| 1143 |
+
try:
|
| 1144 |
+
tz = tzwin(name)
|
| 1145 |
+
except (WindowsError, UnicodeEncodeError):
|
| 1146 |
+
# UnicodeEncodeError is for Python 2.7 compat
|
| 1147 |
+
tz = None
|
| 1148 |
+
|
| 1149 |
+
if not tz:
|
| 1150 |
+
for c in name:
|
| 1151 |
+
# name is not a tzstr unless it has at least
|
| 1152 |
+
# one offset. For short values of "name", an
|
| 1153 |
+
# explicit for loop seems to be the fastest way
|
| 1154 |
+
# To determine if a string contains a digit
|
| 1155 |
+
if c in "0123456789":
|
| 1156 |
+
try:
|
| 1157 |
+
tz = tzstr(name)
|
| 1158 |
+
except ValueError:
|
| 1159 |
+
pass
|
| 1160 |
+
break
|
| 1161 |
+
else:
|
| 1162 |
+
if name in ("GMT", "UTC"):
|
| 1163 |
+
tz = UTC
|
| 1164 |
+
elif name in time.tzname:
|
| 1165 |
+
tz = tzlocal()
|
| 1166 |
+
return tz
|
| 1167 |
+
|
| 1168 |
+
return GettzFunc()
|
| 1169 |
+
|
| 1170 |
+
|
| 1171 |
+
gettz = __get_gettz()
|
| 1172 |
+
del __get_gettz
|
| 1173 |
+
|
| 1174 |
+
|
| 1175 |
+
class DeprecatedTzKeyWarning(FutureWarning):
|
| 1176 |
+
"""Warning raised when :func:`gettz` is given a key outside of TZPATH.
|
| 1177 |
+
|
| 1178 |
+
Relative keys like ``"../../etc/localtime"`` resolve to files outside of
|
| 1179 |
+
every directory on :data:`TZPATH`. Support for these is deprecated, and a
|
| 1180 |
+
warnings filter turns this warning into an error by default. To keep the
|
| 1181 |
+
old behavior for now, filter it out, e.g.::
|
| 1182 |
+
|
| 1183 |
+
warnings.filterwarnings(
|
| 1184 |
+
"ignore", category=dateutil.tz.DeprecatedTzKeyWarning
|
| 1185 |
+
)
|
| 1186 |
+
|
| 1187 |
+
Absolute paths to TZif files are not affected.
|
| 1188 |
+
"""
|
| 1189 |
+
|
| 1190 |
+
|
| 1191 |
+
def _escapes_tzpath(name):
|
| 1192 |
+
# A relative key stays inside every TZPATH entry unless, once normalized
|
| 1193 |
+
# (which also turns os.altsep into os.sep), it starts by going up a
|
| 1194 |
+
# directory.
|
| 1195 |
+
return os.path.normpath(name).split(os.sep, 1)[0] == os.pardir
|
| 1196 |
+
|
| 1197 |
+
|
| 1198 |
+
def _warn_tzpath_escape(name):
|
| 1199 |
+
# As in dateutil.zoneinfo.rebuild, the filter is appended at call time,
|
| 1200 |
+
# so any filter the user has installed for this category wins.
|
| 1201 |
+
warnings.filterwarnings(
|
| 1202 |
+
"error", category=DeprecatedTzKeyWarning, append=True
|
| 1203 |
+
)
|
| 1204 |
+
warnings.warn(
|
| 1205 |
+
"gettz(%r) refers to a file outside of TZPATH; support for keys "
|
| 1206 |
+
"like this is deprecated and will be removed in a future version. "
|
| 1207 |
+
"Use an absolute path instead." % (name,),
|
| 1208 |
+
DeprecatedTzKeyWarning,
|
| 1209 |
+
stacklevel=4,
|
| 1210 |
+
)
|
| 1211 |
+
|
| 1212 |
+
|
| 1213 |
+
def available_iana_timezones():
|
| 1214 |
+
"""Returns a set containing all available IANA time zones keys.
|
| 1215 |
+
|
| 1216 |
+
These are suitable for use with :func:`gettz`. This is essentially a direct
|
| 1217 |
+
port of :py:func:`zoneinfo.available_timezones`.
|
| 1218 |
+
|
| 1219 |
+
The search includes time zones found on :data:`TZPATH` as well as any
|
| 1220 |
+
time zones provided by the `tzdata <https://pypi.org/project/tzdata/>`_
|
| 1221 |
+
package if it is installed.
|
| 1222 |
+
|
| 1223 |
+
:return:
|
| 1224 |
+
Returns a mutable set containing strings of time zones. Each call will
|
| 1225 |
+
return a fresh set.
|
| 1226 |
+
|
| 1227 |
+
.. caution::
|
| 1228 |
+
|
| 1229 |
+
This may attempt to open a large number of files, since the best way to
|
| 1230 |
+
determine if a given file on the time zone search path is to open it
|
| 1231 |
+
and check for the "magic string" at the beginning.
|
| 1232 |
+
|
| 1233 |
+
.. note::
|
| 1234 |
+
|
| 1235 |
+
These values are not designed to be exposed to end-users; for
|
| 1236 |
+
user-facing elements, applications should use something like CLDR (the
|
| 1237 |
+
Unicode Common Locale Data Repository) to get more user-friendly
|
| 1238 |
+
strings.
|
| 1239 |
+
"""
|
| 1240 |
+
# Start with loading from the tzdata package if it exists: this has a
|
| 1241 |
+
# pre-assembled list of zones that only requires opening one file.
|
| 1242 |
+
try:
|
| 1243 |
+
valid_zones = set(_tzdata_impl._load_tzdata_keys())
|
| 1244 |
+
except ImportError:
|
| 1245 |
+
valid_zones = set()
|
| 1246 |
+
|
| 1247 |
+
def valid_key(fpath):
|
| 1248 |
+
try:
|
| 1249 |
+
with open(fpath, "rb") as f:
|
| 1250 |
+
return f.read(4) == b"TZif"
|
| 1251 |
+
except Exception: # pragma: nocover
|
| 1252 |
+
return False
|
| 1253 |
+
|
| 1254 |
+
for tz_root in _tzpath.TZPATH:
|
| 1255 |
+
if not os.path.exists(tz_root):
|
| 1256 |
+
continue
|
| 1257 |
+
|
| 1258 |
+
for root, dirnames, files in os.walk(tz_root):
|
| 1259 |
+
if root == tz_root:
|
| 1260 |
+
# right/ and posix/ are special directories and shouldn't be
|
| 1261 |
+
# included in the output of available zones
|
| 1262 |
+
if "right" in dirnames:
|
| 1263 |
+
dirnames.remove("right")
|
| 1264 |
+
if "posix" in dirnames:
|
| 1265 |
+
dirnames.remove("posix")
|
| 1266 |
+
|
| 1267 |
+
for file in files:
|
| 1268 |
+
fpath = os.path.join(root, file)
|
| 1269 |
+
|
| 1270 |
+
key = os.path.relpath(fpath, start=tz_root)
|
| 1271 |
+
if os.sep != "/": # pragma: nocover
|
| 1272 |
+
key = key.replace(os.sep, "/")
|
| 1273 |
+
|
| 1274 |
+
if not key or key in valid_zones:
|
| 1275 |
+
continue
|
| 1276 |
+
|
| 1277 |
+
if valid_key(fpath):
|
| 1278 |
+
valid_zones.add(key)
|
| 1279 |
+
|
| 1280 |
+
if "posixrules" in valid_zones:
|
| 1281 |
+
# posixrules is a special symlink-only time zone where it exists, it
|
| 1282 |
+
# should not be included in the output
|
| 1283 |
+
valid_zones.remove("posixrules")
|
| 1284 |
+
if "localtime" in valid_zones:
|
| 1285 |
+
# localtime is a special symlink-only time zone where it exists, it
|
| 1286 |
+
# should not be included in the output
|
| 1287 |
+
valid_zones.remove("localtime")
|
| 1288 |
+
|
| 1289 |
+
return valid_zones
|
| 1290 |
+
|
| 1291 |
+
|
| 1292 |
+
def datetime_exists(dt, tz=None):
|
| 1293 |
+
"""
|
| 1294 |
+
Given a datetime and a time zone, determine whether or not a given datetime
|
| 1295 |
+
would fall in a gap.
|
| 1296 |
+
|
| 1297 |
+
:param dt:
|
| 1298 |
+
A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
|
| 1299 |
+
is provided.)
|
| 1300 |
+
|
| 1301 |
+
:param tz:
|
| 1302 |
+
A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
|
| 1303 |
+
``None`` or not provided, the datetime's own time zone will be used.
|
| 1304 |
+
|
| 1305 |
+
:return:
|
| 1306 |
+
Returns a boolean value whether or not the "wall time" exists in
|
| 1307 |
+
``tz``.
|
| 1308 |
+
|
| 1309 |
+
.. versionadded:: 2.7.0
|
| 1310 |
+
"""
|
| 1311 |
+
if tz is None:
|
| 1312 |
+
if dt.tzinfo is None:
|
| 1313 |
+
raise ValueError('Datetime is naive and no time zone provided.')
|
| 1314 |
+
tz = dt.tzinfo
|
| 1315 |
+
|
| 1316 |
+
dt = dt.replace(tzinfo=None)
|
| 1317 |
+
|
| 1318 |
+
# This is essentially a test of whether or not the datetime can survive
|
| 1319 |
+
# a round trip to UTC.
|
| 1320 |
+
dt_rt = dt.replace(tzinfo=tz).astimezone(UTC).astimezone(tz)
|
| 1321 |
+
dt_rt = dt_rt.replace(tzinfo=None)
|
| 1322 |
+
|
| 1323 |
+
return dt == dt_rt
|
| 1324 |
+
|
| 1325 |
+
|
| 1326 |
+
def datetime_ambiguous(dt, tz=None):
|
| 1327 |
+
"""
|
| 1328 |
+
Given a datetime and a time zone, determine whether or not a given datetime
|
| 1329 |
+
is ambiguous (i.e if there are two times differentiated only by their DST
|
| 1330 |
+
status).
|
| 1331 |
+
|
| 1332 |
+
:param dt:
|
| 1333 |
+
A :class:`datetime.datetime` (whose time zone will be ignored if ``tz``
|
| 1334 |
+
is provided.)
|
| 1335 |
+
|
| 1336 |
+
:param tz:
|
| 1337 |
+
A :class:`datetime.tzinfo` with support for the ``fold`` attribute. If
|
| 1338 |
+
``None`` or not provided, the datetime's own time zone will be used.
|
| 1339 |
+
|
| 1340 |
+
:return:
|
| 1341 |
+
Returns a boolean value whether or not the "wall time" is ambiguous in
|
| 1342 |
+
``tz``.
|
| 1343 |
+
|
| 1344 |
+
.. versionadded:: 2.6.0
|
| 1345 |
+
"""
|
| 1346 |
+
if tz is None:
|
| 1347 |
+
if dt.tzinfo is None:
|
| 1348 |
+
raise ValueError('Datetime is naive and no time zone provided.')
|
| 1349 |
+
|
| 1350 |
+
tz = dt.tzinfo
|
| 1351 |
+
|
| 1352 |
+
# If a time zone defines its own "is_ambiguous" function, we'll use that.
|
| 1353 |
+
is_ambiguous_fn = getattr(tz, 'is_ambiguous', None)
|
| 1354 |
+
if is_ambiguous_fn is not None:
|
| 1355 |
+
try:
|
| 1356 |
+
return tz.is_ambiguous(dt)
|
| 1357 |
+
except Exception:
|
| 1358 |
+
pass
|
| 1359 |
+
|
| 1360 |
+
# If it doesn't come out and tell us it's ambiguous, we'll just check if
|
| 1361 |
+
# the fold attribute has any effect on this particular date and time.
|
| 1362 |
+
dt = dt.replace(tzinfo=tz)
|
| 1363 |
+
wall_0 = enfold(dt, fold=0)
|
| 1364 |
+
wall_1 = enfold(dt, fold=1)
|
| 1365 |
+
|
| 1366 |
+
same_offset = wall_0.utcoffset() == wall_1.utcoffset()
|
| 1367 |
+
same_dst = wall_0.dst() == wall_1.dst()
|
| 1368 |
+
|
| 1369 |
+
return not (same_offset and same_dst)
|
| 1370 |
+
|
| 1371 |
+
|
| 1372 |
+
def resolve_imaginary(dt):
|
| 1373 |
+
"""
|
| 1374 |
+
Given a datetime that may be imaginary, return an existing datetime.
|
| 1375 |
+
|
| 1376 |
+
This function assumes that an imaginary datetime represents what the
|
| 1377 |
+
wall time would be in a zone had the offset transition not occurred, so
|
| 1378 |
+
it will always fall forward by the transition's change in offset.
|
| 1379 |
+
|
| 1380 |
+
.. doctest::
|
| 1381 |
+
|
| 1382 |
+
>>> from dateutil import tz
|
| 1383 |
+
>>> from datetime import datetime
|
| 1384 |
+
>>> NYC = tz.gettz('America/New_York')
|
| 1385 |
+
>>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC)))
|
| 1386 |
+
2017-03-12 03:30:00-04:00
|
| 1387 |
+
|
| 1388 |
+
>>> KIR = tz.gettz('Pacific/Kiritimati')
|
| 1389 |
+
>>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR)))
|
| 1390 |
+
1995-01-02 12:30:00+14:00
|
| 1391 |
+
|
| 1392 |
+
As a note, :func:`datetime.astimezone` is guaranteed to produce a valid,
|
| 1393 |
+
existing datetime, so a round-trip to and from UTC is sufficient to get
|
| 1394 |
+
an extant datetime, however, this generally "falls back" to an earlier time
|
| 1395 |
+
rather than falling forward to the STD side (though no guarantees are made
|
| 1396 |
+
about this behavior).
|
| 1397 |
+
|
| 1398 |
+
:param dt:
|
| 1399 |
+
A :class:`datetime.datetime` which may or may not exist.
|
| 1400 |
+
|
| 1401 |
+
:return:
|
| 1402 |
+
Returns an existing :class:`datetime.datetime`. If ``dt`` was not
|
| 1403 |
+
imaginary, the datetime returned is guaranteed to be the same object
|
| 1404 |
+
passed to the function.
|
| 1405 |
+
|
| 1406 |
+
.. versionadded:: 2.7.0
|
| 1407 |
+
"""
|
| 1408 |
+
if dt.tzinfo is not None and not datetime_exists(dt):
|
| 1409 |
+
|
| 1410 |
+
curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset()
|
| 1411 |
+
old_offset = (dt - datetime.timedelta(hours=24)).utcoffset()
|
| 1412 |
+
|
| 1413 |
+
dt += curr_offset - old_offset
|
| 1414 |
+
|
| 1415 |
+
return dt
|
| 1416 |
+
|
| 1417 |
+
|
| 1418 |
+
def _datetime_to_timestamp(dt):
|
| 1419 |
+
"""
|
| 1420 |
+
Convert a :class:`datetime.datetime` object to an epoch timestamp in
|
| 1421 |
+
seconds since January 1, 1970, ignoring the time zone.
|
| 1422 |
+
"""
|
| 1423 |
+
return (dt.replace(tzinfo=None) - EPOCH).total_seconds()
|
| 1424 |
+
|
| 1425 |
+
|
| 1426 |
+
if sys.version_info >= (3, 6):
|
| 1427 |
+
def _get_supported_offset(second_offset):
|
| 1428 |
+
return second_offset
|
| 1429 |
+
else:
|
| 1430 |
+
def _get_supported_offset(second_offset):
|
| 1431 |
+
# For python pre-3.6, round to full-minutes if that's not the case.
|
| 1432 |
+
# Python's datetime doesn't accept sub-minute timezones. Check
|
| 1433 |
+
# http://python.org/sf/1447945 or https://bugs.python.org/issue5288
|
| 1434 |
+
# for some information.
|
| 1435 |
+
old_offset = second_offset
|
| 1436 |
+
calculated_offset = 60 * ((second_offset + 30) // 60)
|
| 1437 |
+
return calculated_offset
|
| 1438 |
+
|
| 1439 |
+
|
| 1440 |
+
if sys.version_info < (3, 8):
|
| 1441 |
+
|
| 1442 |
+
def _isfile(path):
|
| 1443 |
+
# bpo-33721: In Python 3.8 non-UTF8 paths return False rather than
|
| 1444 |
+
# raising an error. See https://bugs.python.org/issue33721
|
| 1445 |
+
try:
|
| 1446 |
+
return os.path.isfile(path)
|
| 1447 |
+
except ValueError:
|
| 1448 |
+
return False
|
| 1449 |
+
|
| 1450 |
+
else:
|
| 1451 |
+
_isfile = os.path.isfile
|
| 1452 |
+
|
| 1453 |
+
|
| 1454 |
+
try:
|
| 1455 |
+
# Python 3.7 feature
|
| 1456 |
+
from contextlib import nullcontext as _nullcontext
|
| 1457 |
+
except ImportError:
|
| 1458 |
+
class _nullcontext(object):
|
| 1459 |
+
"""
|
| 1460 |
+
Class for wrapping contexts so that they are passed through in a
|
| 1461 |
+
with statement.
|
| 1462 |
+
"""
|
| 1463 |
+
def __init__(self, context):
|
| 1464 |
+
self.context = context
|
| 1465 |
+
|
| 1466 |
+
def __enter__(self):
|
| 1467 |
+
return self.context
|
| 1468 |
+
|
| 1469 |
+
def __exit__(*args, **kwargs):
|
| 1470 |
+
pass
|
| 1471 |
+
|
| 1472 |
+
# vim:ts=4:sw=4:et
|
hug/lib/python3.12/site-packages/dateutil/tz/win.py
ADDED
|
@@ -0,0 +1,370 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
This module provides an interface to the native time zone data on Windows,
|
| 4 |
+
including :py:class:`datetime.tzinfo` implementations.
|
| 5 |
+
|
| 6 |
+
Attempting to import this module on a non-Windows platform will raise an
|
| 7 |
+
:py:obj:`ImportError`.
|
| 8 |
+
"""
|
| 9 |
+
# This code was originally contributed by Jeffrey Harris.
|
| 10 |
+
import datetime
|
| 11 |
+
import struct
|
| 12 |
+
|
| 13 |
+
from six.moves import winreg
|
| 14 |
+
from six import text_type
|
| 15 |
+
|
| 16 |
+
try:
|
| 17 |
+
import ctypes
|
| 18 |
+
from ctypes import wintypes
|
| 19 |
+
except ValueError:
|
| 20 |
+
# ValueError is raised on non-Windows systems for some horrible reason.
|
| 21 |
+
raise ImportError("Running tzwin on non-Windows system")
|
| 22 |
+
|
| 23 |
+
from ._common import tzrangebase
|
| 24 |
+
|
| 25 |
+
__all__ = ["tzwin", "tzwinlocal", "tzres"]
|
| 26 |
+
|
| 27 |
+
ONEWEEK = datetime.timedelta(7)
|
| 28 |
+
|
| 29 |
+
TZKEYNAMENT = r"SOFTWARE\Microsoft\Windows NT\CurrentVersion\Time Zones"
|
| 30 |
+
TZKEYNAME9X = r"SOFTWARE\Microsoft\Windows\CurrentVersion\Time Zones"
|
| 31 |
+
TZLOCALKEYNAME = r"SYSTEM\CurrentControlSet\Control\TimeZoneInformation"
|
| 32 |
+
|
| 33 |
+
|
| 34 |
+
def _settzkeyname():
|
| 35 |
+
handle = winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE)
|
| 36 |
+
try:
|
| 37 |
+
winreg.OpenKey(handle, TZKEYNAMENT).Close()
|
| 38 |
+
TZKEYNAME = TZKEYNAMENT
|
| 39 |
+
except WindowsError:
|
| 40 |
+
TZKEYNAME = TZKEYNAME9X
|
| 41 |
+
handle.Close()
|
| 42 |
+
return TZKEYNAME
|
| 43 |
+
|
| 44 |
+
|
| 45 |
+
TZKEYNAME = _settzkeyname()
|
| 46 |
+
|
| 47 |
+
|
| 48 |
+
class tzres(object):
|
| 49 |
+
"""
|
| 50 |
+
Class for accessing ``tzres.dll``, which contains timezone name related
|
| 51 |
+
resources.
|
| 52 |
+
|
| 53 |
+
.. versionadded:: 2.5.0
|
| 54 |
+
"""
|
| 55 |
+
p_wchar = ctypes.POINTER(wintypes.WCHAR) # Pointer to a wide char
|
| 56 |
+
|
| 57 |
+
def __init__(self, tzres_loc='tzres.dll'):
|
| 58 |
+
# Load the user32 DLL so we can load strings from tzres
|
| 59 |
+
user32 = ctypes.WinDLL('user32')
|
| 60 |
+
|
| 61 |
+
# Specify the LoadStringW function
|
| 62 |
+
user32.LoadStringW.argtypes = (wintypes.HINSTANCE,
|
| 63 |
+
wintypes.UINT,
|
| 64 |
+
wintypes.LPWSTR,
|
| 65 |
+
ctypes.c_int)
|
| 66 |
+
|
| 67 |
+
self.LoadStringW = user32.LoadStringW
|
| 68 |
+
self._tzres = ctypes.WinDLL(tzres_loc)
|
| 69 |
+
self.tzres_loc = tzres_loc
|
| 70 |
+
|
| 71 |
+
def load_name(self, offset):
|
| 72 |
+
"""
|
| 73 |
+
Load a timezone name from a DLL offset (integer).
|
| 74 |
+
|
| 75 |
+
>>> from dateutil.tzwin import tzres
|
| 76 |
+
>>> tzr = tzres()
|
| 77 |
+
>>> print(tzr.load_name(112))
|
| 78 |
+
'Eastern Standard Time'
|
| 79 |
+
|
| 80 |
+
:param offset:
|
| 81 |
+
A positive integer value referring to a string from the tzres dll.
|
| 82 |
+
|
| 83 |
+
.. note::
|
| 84 |
+
|
| 85 |
+
Offsets found in the registry are generally of the form
|
| 86 |
+
``@tzres.dll,-114``. The offset in this case is 114, not -114.
|
| 87 |
+
|
| 88 |
+
"""
|
| 89 |
+
resource = self.p_wchar()
|
| 90 |
+
lpBuffer = ctypes.cast(ctypes.byref(resource), wintypes.LPWSTR)
|
| 91 |
+
nchar = self.LoadStringW(self._tzres._handle, offset, lpBuffer, 0)
|
| 92 |
+
return resource[:nchar]
|
| 93 |
+
|
| 94 |
+
def name_from_string(self, tzname_str):
|
| 95 |
+
"""
|
| 96 |
+
Parse strings as returned from the Windows registry into the time zone
|
| 97 |
+
name as defined in the registry.
|
| 98 |
+
|
| 99 |
+
>>> from dateutil.tzwin import tzres
|
| 100 |
+
>>> tzr = tzres()
|
| 101 |
+
>>> print(tzr.name_from_string('@tzres.dll,-251'))
|
| 102 |
+
'Dateline Daylight Time'
|
| 103 |
+
>>> print(tzr.name_from_string('Eastern Standard Time'))
|
| 104 |
+
'Eastern Standard Time'
|
| 105 |
+
|
| 106 |
+
:param tzname_str:
|
| 107 |
+
A timezone name string as returned from a Windows registry key.
|
| 108 |
+
|
| 109 |
+
:return:
|
| 110 |
+
Returns the localized timezone string from tzres.dll if the string
|
| 111 |
+
is of the form `@tzres.dll,-offset`, else returns the input string.
|
| 112 |
+
"""
|
| 113 |
+
if not tzname_str.startswith('@'):
|
| 114 |
+
return tzname_str
|
| 115 |
+
|
| 116 |
+
name_splt = tzname_str.split(',-')
|
| 117 |
+
try:
|
| 118 |
+
offset = int(name_splt[1])
|
| 119 |
+
except:
|
| 120 |
+
raise ValueError("Malformed timezone string.")
|
| 121 |
+
|
| 122 |
+
return self.load_name(offset)
|
| 123 |
+
|
| 124 |
+
|
| 125 |
+
class tzwinbase(tzrangebase):
|
| 126 |
+
"""tzinfo class based on win32's timezones available in the registry."""
|
| 127 |
+
def __init__(self):
|
| 128 |
+
raise NotImplementedError('tzwinbase is an abstract base class')
|
| 129 |
+
|
| 130 |
+
def __eq__(self, other):
|
| 131 |
+
# Compare on all relevant dimensions, including name.
|
| 132 |
+
if not isinstance(other, tzwinbase):
|
| 133 |
+
return NotImplemented
|
| 134 |
+
|
| 135 |
+
return (self._std_offset == other._std_offset and
|
| 136 |
+
self._dst_offset == other._dst_offset and
|
| 137 |
+
self._stddayofweek == other._stddayofweek and
|
| 138 |
+
self._dstdayofweek == other._dstdayofweek and
|
| 139 |
+
self._stdweeknumber == other._stdweeknumber and
|
| 140 |
+
self._dstweeknumber == other._dstweeknumber and
|
| 141 |
+
self._stdhour == other._stdhour and
|
| 142 |
+
self._dsthour == other._dsthour and
|
| 143 |
+
self._stdminute == other._stdminute and
|
| 144 |
+
self._dstminute == other._dstminute and
|
| 145 |
+
self._std_abbr == other._std_abbr and
|
| 146 |
+
self._dst_abbr == other._dst_abbr)
|
| 147 |
+
|
| 148 |
+
@staticmethod
|
| 149 |
+
def list():
|
| 150 |
+
"""Return a list of all time zones known to the system."""
|
| 151 |
+
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
| 152 |
+
with winreg.OpenKey(handle, TZKEYNAME) as tzkey:
|
| 153 |
+
result = [winreg.EnumKey(tzkey, i)
|
| 154 |
+
for i in range(winreg.QueryInfoKey(tzkey)[0])]
|
| 155 |
+
return result
|
| 156 |
+
|
| 157 |
+
def display(self):
|
| 158 |
+
"""
|
| 159 |
+
Return the display name of the time zone.
|
| 160 |
+
"""
|
| 161 |
+
return self._display
|
| 162 |
+
|
| 163 |
+
def transitions(self, year):
|
| 164 |
+
"""
|
| 165 |
+
For a given year, get the DST on and off transition times, expressed
|
| 166 |
+
always on the standard time side. For zones with no transitions, this
|
| 167 |
+
function returns ``None``.
|
| 168 |
+
|
| 169 |
+
:param year:
|
| 170 |
+
The year whose transitions you would like to query.
|
| 171 |
+
|
| 172 |
+
:return:
|
| 173 |
+
Returns a :class:`tuple` of :class:`datetime.datetime` objects,
|
| 174 |
+
``(dston, dstoff)`` for zones with an annual DST transition, or
|
| 175 |
+
``None`` for fixed offset zones.
|
| 176 |
+
"""
|
| 177 |
+
|
| 178 |
+
if not self.hasdst:
|
| 179 |
+
return None
|
| 180 |
+
|
| 181 |
+
dston = picknthweekday(year, self._dstmonth, self._dstdayofweek,
|
| 182 |
+
self._dsthour, self._dstminute,
|
| 183 |
+
self._dstweeknumber)
|
| 184 |
+
|
| 185 |
+
dstoff = picknthweekday(year, self._stdmonth, self._stddayofweek,
|
| 186 |
+
self._stdhour, self._stdminute,
|
| 187 |
+
self._stdweeknumber)
|
| 188 |
+
|
| 189 |
+
# Ambiguous dates default to the STD side
|
| 190 |
+
dstoff -= self._dst_base_offset
|
| 191 |
+
|
| 192 |
+
return dston, dstoff
|
| 193 |
+
|
| 194 |
+
def _get_hasdst(self):
|
| 195 |
+
return self._dstmonth != 0
|
| 196 |
+
|
| 197 |
+
@property
|
| 198 |
+
def _dst_base_offset(self):
|
| 199 |
+
return self._dst_base_offset_
|
| 200 |
+
|
| 201 |
+
|
| 202 |
+
class tzwin(tzwinbase):
|
| 203 |
+
"""
|
| 204 |
+
Time zone object created from the zone info in the Windows registry
|
| 205 |
+
|
| 206 |
+
These are similar to :py:class:`dateutil.tz.tzrange` objects in that
|
| 207 |
+
the time zone data is provided in the format of a single offset rule
|
| 208 |
+
for either 0 or 2 time zone transitions per year.
|
| 209 |
+
|
| 210 |
+
:param: name
|
| 211 |
+
The name of a Windows time zone key, e.g. "Eastern Standard Time".
|
| 212 |
+
The full list of keys can be retrieved with :func:`tzwin.list`.
|
| 213 |
+
"""
|
| 214 |
+
|
| 215 |
+
def __init__(self, name):
|
| 216 |
+
self._name = name
|
| 217 |
+
|
| 218 |
+
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
| 219 |
+
tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name)
|
| 220 |
+
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
| 221 |
+
keydict = valuestodict(tzkey)
|
| 222 |
+
|
| 223 |
+
self._std_abbr = keydict["Std"]
|
| 224 |
+
self._dst_abbr = keydict["Dlt"]
|
| 225 |
+
|
| 226 |
+
self._display = keydict["Display"]
|
| 227 |
+
|
| 228 |
+
# See http://ww_winreg.jsiinc.com/SUBA/tip0300/rh0398.htm
|
| 229 |
+
tup = struct.unpack("=3l16h", keydict["TZI"])
|
| 230 |
+
stdoffset = -tup[0]-tup[1] # Bias + StandardBias * -1
|
| 231 |
+
dstoffset = stdoffset-tup[2] # + DaylightBias * -1
|
| 232 |
+
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
| 233 |
+
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
| 234 |
+
|
| 235 |
+
# for the meaning see the win32 TIME_ZONE_INFORMATION structure docs
|
| 236 |
+
# http://msdn.microsoft.com/en-us/library/windows/desktop/ms725481(v=vs.85).aspx
|
| 237 |
+
(self._stdmonth,
|
| 238 |
+
self._stddayofweek, # Sunday = 0
|
| 239 |
+
self._stdweeknumber, # Last = 5
|
| 240 |
+
self._stdhour,
|
| 241 |
+
self._stdminute) = tup[4:9]
|
| 242 |
+
|
| 243 |
+
(self._dstmonth,
|
| 244 |
+
self._dstdayofweek, # Sunday = 0
|
| 245 |
+
self._dstweeknumber, # Last = 5
|
| 246 |
+
self._dsthour,
|
| 247 |
+
self._dstminute) = tup[12:17]
|
| 248 |
+
|
| 249 |
+
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
| 250 |
+
self.hasdst = self._get_hasdst()
|
| 251 |
+
|
| 252 |
+
def __repr__(self):
|
| 253 |
+
return "tzwin(%s)" % repr(self._name)
|
| 254 |
+
|
| 255 |
+
def __reduce__(self):
|
| 256 |
+
return (self.__class__, (self._name,))
|
| 257 |
+
|
| 258 |
+
|
| 259 |
+
class tzwinlocal(tzwinbase):
|
| 260 |
+
"""
|
| 261 |
+
Class representing the local time zone information in the Windows registry
|
| 262 |
+
|
| 263 |
+
While :class:`dateutil.tz.tzlocal` makes system calls (via the :mod:`time`
|
| 264 |
+
module) to retrieve time zone information, ``tzwinlocal`` retrieves the
|
| 265 |
+
rules directly from the Windows registry and creates an object like
|
| 266 |
+
:class:`dateutil.tz.tzwin`.
|
| 267 |
+
|
| 268 |
+
Because Windows does not have an equivalent of :func:`time.tzset`, on
|
| 269 |
+
Windows, :class:`dateutil.tz.tzlocal` instances will always reflect the
|
| 270 |
+
time zone settings *at the time that the process was started*, meaning
|
| 271 |
+
changes to the machine's time zone settings during the run of a program
|
| 272 |
+
on Windows will **not** be reflected by :class:`dateutil.tz.tzlocal`.
|
| 273 |
+
Because ``tzwinlocal`` reads the registry directly, it is unaffected by
|
| 274 |
+
this issue.
|
| 275 |
+
"""
|
| 276 |
+
def __init__(self):
|
| 277 |
+
with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle:
|
| 278 |
+
with winreg.OpenKey(handle, TZLOCALKEYNAME) as tzlocalkey:
|
| 279 |
+
keydict = valuestodict(tzlocalkey)
|
| 280 |
+
|
| 281 |
+
self._std_abbr = keydict["StandardName"]
|
| 282 |
+
self._dst_abbr = keydict["DaylightName"]
|
| 283 |
+
|
| 284 |
+
try:
|
| 285 |
+
tzkeyname = text_type('{kn}\\{sn}').format(kn=TZKEYNAME,
|
| 286 |
+
sn=self._std_abbr)
|
| 287 |
+
with winreg.OpenKey(handle, tzkeyname) as tzkey:
|
| 288 |
+
_keydict = valuestodict(tzkey)
|
| 289 |
+
self._display = _keydict["Display"]
|
| 290 |
+
except OSError:
|
| 291 |
+
self._display = None
|
| 292 |
+
|
| 293 |
+
stdoffset = -keydict["Bias"]-keydict["StandardBias"]
|
| 294 |
+
dstoffset = stdoffset-keydict["DaylightBias"]
|
| 295 |
+
|
| 296 |
+
self._std_offset = datetime.timedelta(minutes=stdoffset)
|
| 297 |
+
self._dst_offset = datetime.timedelta(minutes=dstoffset)
|
| 298 |
+
|
| 299 |
+
# For reasons unclear, in this particular key, the day of week has been
|
| 300 |
+
# moved to the END of the SYSTEMTIME structure.
|
| 301 |
+
tup = struct.unpack("=8h", keydict["StandardStart"])
|
| 302 |
+
|
| 303 |
+
(self._stdmonth,
|
| 304 |
+
self._stdweeknumber, # Last = 5
|
| 305 |
+
self._stdhour,
|
| 306 |
+
self._stdminute) = tup[1:5]
|
| 307 |
+
|
| 308 |
+
self._stddayofweek = tup[7]
|
| 309 |
+
|
| 310 |
+
tup = struct.unpack("=8h", keydict["DaylightStart"])
|
| 311 |
+
|
| 312 |
+
(self._dstmonth,
|
| 313 |
+
self._dstweeknumber, # Last = 5
|
| 314 |
+
self._dsthour,
|
| 315 |
+
self._dstminute) = tup[1:5]
|
| 316 |
+
|
| 317 |
+
self._dstdayofweek = tup[7]
|
| 318 |
+
|
| 319 |
+
self._dst_base_offset_ = self._dst_offset - self._std_offset
|
| 320 |
+
self.hasdst = self._get_hasdst()
|
| 321 |
+
|
| 322 |
+
def __repr__(self):
|
| 323 |
+
return "tzwinlocal()"
|
| 324 |
+
|
| 325 |
+
def __str__(self):
|
| 326 |
+
# str will return the standard name, not the daylight name.
|
| 327 |
+
return "tzwinlocal(%s)" % repr(self._std_abbr)
|
| 328 |
+
|
| 329 |
+
def __reduce__(self):
|
| 330 |
+
return (self.__class__, ())
|
| 331 |
+
|
| 332 |
+
|
| 333 |
+
def picknthweekday(year, month, dayofweek, hour, minute, whichweek):
|
| 334 |
+
""" dayofweek == 0 means Sunday, whichweek 5 means last instance """
|
| 335 |
+
first = datetime.datetime(year, month, 1, hour, minute)
|
| 336 |
+
|
| 337 |
+
# This will work if dayofweek is ISO weekday (1-7) or Microsoft-style (0-6),
|
| 338 |
+
# Because 7 % 7 = 0
|
| 339 |
+
weekdayone = first.replace(day=((dayofweek - first.isoweekday()) % 7) + 1)
|
| 340 |
+
wd = weekdayone + ((whichweek - 1) * ONEWEEK)
|
| 341 |
+
if (wd.month != month):
|
| 342 |
+
wd -= ONEWEEK
|
| 343 |
+
|
| 344 |
+
return wd
|
| 345 |
+
|
| 346 |
+
|
| 347 |
+
def valuestodict(key):
|
| 348 |
+
"""Convert a registry key's values to a dictionary."""
|
| 349 |
+
dout = {}
|
| 350 |
+
size = winreg.QueryInfoKey(key)[1]
|
| 351 |
+
tz_res = None
|
| 352 |
+
|
| 353 |
+
for i in range(size):
|
| 354 |
+
key_name, value, dtype = winreg.EnumValue(key, i)
|
| 355 |
+
if dtype == winreg.REG_DWORD or dtype == winreg.REG_DWORD_LITTLE_ENDIAN:
|
| 356 |
+
# If it's a DWORD (32-bit integer), it's stored as unsigned - convert
|
| 357 |
+
# that to a proper signed integer
|
| 358 |
+
if value & (1 << 31):
|
| 359 |
+
value = value - (1 << 32)
|
| 360 |
+
elif dtype == winreg.REG_SZ:
|
| 361 |
+
# If it's a reference to the tzres DLL, load the actual string
|
| 362 |
+
if value.startswith('@tzres'):
|
| 363 |
+
tz_res = tz_res or tzres()
|
| 364 |
+
value = tz_res.name_from_string(value)
|
| 365 |
+
|
| 366 |
+
value = value.rstrip('\x00') # Remove trailing nulls
|
| 367 |
+
|
| 368 |
+
dout[key_name] = value
|
| 369 |
+
|
| 370 |
+
return dout
|
hug/lib/python3.12/site-packages/dateutil/tzwin.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# tzwin has moved to dateutil.tz.win
|
| 2 |
+
from .tz.win import *
|
hug/lib/python3.12/site-packages/dateutil/utils.py
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
This module offers general convenience and utility functions for dealing with
|
| 4 |
+
datetimes.
|
| 5 |
+
|
| 6 |
+
.. versionadded:: 2.7.0
|
| 7 |
+
"""
|
| 8 |
+
from __future__ import unicode_literals
|
| 9 |
+
|
| 10 |
+
from datetime import datetime, time
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def today(tzinfo=None):
|
| 14 |
+
"""
|
| 15 |
+
Returns a :py:class:`datetime` representing the current day at midnight
|
| 16 |
+
|
| 17 |
+
:param tzinfo:
|
| 18 |
+
The time zone to attach (also used to determine the current day).
|
| 19 |
+
|
| 20 |
+
:return:
|
| 21 |
+
A :py:class:`datetime.datetime` object representing the current day
|
| 22 |
+
at midnight.
|
| 23 |
+
"""
|
| 24 |
+
|
| 25 |
+
dt = datetime.now(tzinfo)
|
| 26 |
+
return datetime.combine(dt.date(), time(0, tzinfo=tzinfo))
|
| 27 |
+
|
| 28 |
+
|
| 29 |
+
def default_tzinfo(dt, tzinfo):
|
| 30 |
+
"""
|
| 31 |
+
Sets the ``tzinfo`` parameter on naive datetimes only
|
| 32 |
+
|
| 33 |
+
This is useful for example when you are provided a datetime that may have
|
| 34 |
+
either an implicit or explicit time zone, such as when parsing a time zone
|
| 35 |
+
string.
|
| 36 |
+
|
| 37 |
+
.. doctest::
|
| 38 |
+
|
| 39 |
+
>>> from dateutil.tz import tzoffset
|
| 40 |
+
>>> from dateutil.parser import parse
|
| 41 |
+
>>> from dateutil.utils import default_tzinfo
|
| 42 |
+
>>> dflt_tz = tzoffset("EST", -18000)
|
| 43 |
+
>>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz))
|
| 44 |
+
2014-01-01 12:30:00+00:00
|
| 45 |
+
>>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz))
|
| 46 |
+
2014-01-01 12:30:00-05:00
|
| 47 |
+
|
| 48 |
+
:param dt:
|
| 49 |
+
The datetime on which to replace the time zone
|
| 50 |
+
|
| 51 |
+
:param tzinfo:
|
| 52 |
+
The :py:class:`datetime.tzinfo` subclass instance to assign to
|
| 53 |
+
``dt`` if (and only if) it is naive.
|
| 54 |
+
|
| 55 |
+
:return:
|
| 56 |
+
Returns an aware :py:class:`datetime.datetime`.
|
| 57 |
+
"""
|
| 58 |
+
if dt.tzinfo is not None:
|
| 59 |
+
return dt
|
| 60 |
+
else:
|
| 61 |
+
return dt.replace(tzinfo=tzinfo)
|
| 62 |
+
|
| 63 |
+
|
| 64 |
+
def within_delta(dt1, dt2, delta):
|
| 65 |
+
"""
|
| 66 |
+
Useful for comparing two datetimes that may have a negligible difference
|
| 67 |
+
to be considered equal.
|
| 68 |
+
"""
|
| 69 |
+
delta = abs(delta)
|
| 70 |
+
difference = dt1 - dt2
|
| 71 |
+
return -delta <= difference <= delta
|
hug/lib/python3.12/site-packages/dateutil/zoneinfo/__init__.py
ADDED
|
@@ -0,0 +1,233 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# -*- coding: utf-8 -*-
|
| 2 |
+
"""
|
| 3 |
+
The ``dateutil.zoneinfo`` module was originally designed to work with a tarball
|
| 4 |
+
shipped with ``dateutil``. Since ``dateutil`` now uses data from the same
|
| 5 |
+
sources as :py:mod:`zoneinfo`, this module is no longer necessary.
|
| 6 |
+
|
| 7 |
+
.. caution::
|
| 8 |
+
This module is deprecated, and its functionality has been dramatically
|
| 9 |
+
curtailed. It is recommended that you adopt the standard library module
|
| 10 |
+
:py:mod:`zoneinfo` or :func:`dateutil.tz.gettz` instead.
|
| 11 |
+
|
| 12 |
+
"""
|
| 13 |
+
|
| 14 |
+
import json
|
| 15 |
+
import sys
|
| 16 |
+
import warnings
|
| 17 |
+
from tarfile import TarFile
|
| 18 |
+
|
| 19 |
+
import six
|
| 20 |
+
|
| 21 |
+
from dateutil import _tzdata_impl
|
| 22 |
+
from dateutil.tz import tzfile as _tzfile
|
| 23 |
+
|
| 24 |
+
__all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"]
|
| 25 |
+
|
| 26 |
+
ZONEFILENAME = None
|
| 27 |
+
METADATA_FN = 'METADATA'
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
def _importer_stacklevel():
|
| 31 |
+
"""The ``stacklevel`` that attributes a warning to the importing code.
|
| 32 |
+
|
| 33 |
+
This module can be imported through ``dateutil.__getattr__`` (e.g. by
|
| 34 |
+
``from dateutil import zoneinfo``), which calls
|
| 35 |
+
``importlib.import_module``; the warnings machinery skips the import
|
| 36 |
+
system's own frames, but not those two, so skip them here.
|
| 37 |
+
"""
|
| 38 |
+
level = 1
|
| 39 |
+
frame = sys._getframe(1) # The body of this module
|
| 40 |
+
while frame.f_back is not None:
|
| 41 |
+
frame = frame.f_back
|
| 42 |
+
filename = frame.f_code.co_filename
|
| 43 |
+
if "importlib" in filename and "_bootstrap" in filename:
|
| 44 |
+
# Since Python 3.5, the warnings machinery skips these frames
|
| 45 |
+
# without counting them; before that they count like any other.
|
| 46 |
+
if sys.version_info < (3, 5):
|
| 47 |
+
level += 1
|
| 48 |
+
continue
|
| 49 |
+
|
| 50 |
+
level += 1
|
| 51 |
+
if frame.f_globals.get("__name__") not in ("importlib", "dateutil"):
|
| 52 |
+
return level
|
| 53 |
+
|
| 54 |
+
return 2
|
| 55 |
+
|
| 56 |
+
|
| 57 |
+
warnings.warn(
|
| 58 |
+
"The `dateutil.zoneinfo` module has been replaced with a wrapper around "
|
| 59 |
+
"the tzdata package, and its use is deprecated, to be removed in a future "
|
| 60 |
+
"version. Use the standard library module `zoneinfo` or `dateutil.tz` "
|
| 61 |
+
"instead.",
|
| 62 |
+
DeprecationWarning,
|
| 63 |
+
stacklevel=_importer_stacklevel(),
|
| 64 |
+
)
|
| 65 |
+
|
| 66 |
+
|
| 67 |
+
tzfile = _tzfile
|
| 68 |
+
|
| 69 |
+
class ZoneInfoFile(object):
|
| 70 |
+
def __init__(self, zonefile_stream=None):
|
| 71 |
+
if zonefile_stream is not None:
|
| 72 |
+
self._load_legacy_zonefile_stream(zonefile_stream)
|
| 73 |
+
else:
|
| 74 |
+
self._eager_load_tzdata()
|
| 75 |
+
|
| 76 |
+
def _eager_load_tzdata(self):
|
| 77 |
+
try:
|
| 78 |
+
import tzdata
|
| 79 |
+
except ImportError as e:
|
| 80 |
+
six.raise_from(
|
| 81 |
+
ImportError(
|
| 82 |
+
"The tzdata module is required to use ZoneInfoFile; either add "
|
| 83 |
+
"a dependency on tzdata or migrate away from dateutil.zoneinfo."
|
| 84 |
+
),
|
| 85 |
+
e,
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
zone_dict = {}
|
| 89 |
+
|
| 90 |
+
for key in _tzdata_impl._load_tzdata_keys():
|
| 91 |
+
with _tzdata_impl._load_tzdata(key) as f:
|
| 92 |
+
zone_dict[key] = tzfile(f, key=key)
|
| 93 |
+
|
| 94 |
+
self.zones = zone_dict
|
| 95 |
+
self.metadata = {
|
| 96 |
+
"metadata_version": "3.0",
|
| 97 |
+
"tzversion": tzdata.IANA_VERSION,
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
def _load_legacy_zonefile_stream(self, zonefile_stream):
|
| 101 |
+
with TarFile.open(fileobj=zonefile_stream) as tf:
|
| 102 |
+
self.zones = {
|
| 103 |
+
zf.name: tzfile(tf.extractfile(zf), filename=zf.name)
|
| 104 |
+
for zf in tf.getmembers()
|
| 105 |
+
if zf.isfile() and zf.name != METADATA_FN
|
| 106 |
+
}
|
| 107 |
+
# deal with links: They'll point to their parent object. Less
|
| 108 |
+
# waste of memory
|
| 109 |
+
links = {
|
| 110 |
+
zl.name: self.zones[zl.linkname]
|
| 111 |
+
for zl in tf.getmembers()
|
| 112 |
+
if zl.islnk() or zl.issym()
|
| 113 |
+
}
|
| 114 |
+
self.zones.update(links)
|
| 115 |
+
try:
|
| 116 |
+
metadata_json = tf.extractfile(tf.getmember(METADATA_FN))
|
| 117 |
+
metadata_str = metadata_json.read().decode("UTF-8")
|
| 118 |
+
self.metadata = json.loads(metadata_str)
|
| 119 |
+
except KeyError:
|
| 120 |
+
# no metadata in tar file
|
| 121 |
+
self.metadata = None
|
| 122 |
+
|
| 123 |
+
def get(self, name, default=None):
|
| 124 |
+
"""
|
| 125 |
+
Wrapper for :func:`ZoneInfoFile.zones.get`. This is a convenience method
|
| 126 |
+
for retrieving zones from the zone dictionary.
|
| 127 |
+
|
| 128 |
+
:param name:
|
| 129 |
+
The name of the zone to retrieve. (Generally IANA zone names)
|
| 130 |
+
|
| 131 |
+
:param default:
|
| 132 |
+
The value to return in the event of a missing key.
|
| 133 |
+
|
| 134 |
+
.. versionadded:: 2.6.0
|
| 135 |
+
|
| 136 |
+
"""
|
| 137 |
+
return self.zones.get(name, default)
|
| 138 |
+
|
| 139 |
+
|
| 140 |
+
# The current API has gettz as a module function, although in fact it taps into
|
| 141 |
+
# a stateful class. So as a workaround for now, without changing the API, we
|
| 142 |
+
# will create a new "global" class instance the first time a user requests a
|
| 143 |
+
# timezone. Ugly, but adheres to the api.
|
| 144 |
+
#
|
| 145 |
+
# TODO: Remove after deprecation period.
|
| 146 |
+
_CLASS_ZONE_INSTANCE = []
|
| 147 |
+
|
| 148 |
+
|
| 149 |
+
def get_zonefile_instance(new_instance=False):
|
| 150 |
+
"""
|
| 151 |
+
This is a convenience function which provides a :class:`ZoneInfoFile`
|
| 152 |
+
instance using the data provided by the ``dateutil`` package. By default, it
|
| 153 |
+
caches a single instance of the ZoneInfoFile object and returns that.
|
| 154 |
+
|
| 155 |
+
:param new_instance:
|
| 156 |
+
If ``True``, a new instance of :class:`ZoneInfoFile` is instantiated and
|
| 157 |
+
used as the cached instance for the next call. Otherwise, new instances
|
| 158 |
+
are created only as necessary.
|
| 159 |
+
|
| 160 |
+
:return:
|
| 161 |
+
Returns a :class:`ZoneInfoFile` object.
|
| 162 |
+
|
| 163 |
+
.. versionadded:: 2.6
|
| 164 |
+
"""
|
| 165 |
+
if new_instance:
|
| 166 |
+
zif = None
|
| 167 |
+
else:
|
| 168 |
+
zif = getattr(get_zonefile_instance, '_cached_instance', None)
|
| 169 |
+
|
| 170 |
+
if zif is None:
|
| 171 |
+
zif = ZoneInfoFile()
|
| 172 |
+
|
| 173 |
+
get_zonefile_instance._cached_instance = zif
|
| 174 |
+
|
| 175 |
+
return zif
|
| 176 |
+
|
| 177 |
+
|
| 178 |
+
def gettz(name):
|
| 179 |
+
"""
|
| 180 |
+
This retrieves a time zone from the ``tzdata`` package.
|
| 181 |
+
|
| 182 |
+
:param name:
|
| 183 |
+
An IANA-style time zone name, as found in the zoneinfo file.
|
| 184 |
+
|
| 185 |
+
:return:
|
| 186 |
+
Returns a :class:`dateutil.tz.tzfile` time zone object.
|
| 187 |
+
|
| 188 |
+
.. warning::
|
| 189 |
+
It is generally inadvisable to use this function, and it is only
|
| 190 |
+
provided for API compatibility with earlier versions. This is *not*
|
| 191 |
+
equivalent to ``dateutil.tz.gettz()``, which selects an appropriate
|
| 192 |
+
time zone based on the inputs, favoring system zoneinfo. This is ONLY
|
| 193 |
+
for accessing the dateutil-specific zoneinfo (which may be out of
|
| 194 |
+
date compared to the system zoneinfo).
|
| 195 |
+
|
| 196 |
+
.. deprecated:: 2.6
|
| 197 |
+
If you need to use a specific zoneinfofile over the system zoneinfo,
|
| 198 |
+
instantiate a :class:`dateutil.zoneinfo.ZoneInfoFile` object and call
|
| 199 |
+
:func:`dateutil.zoneinfo.ZoneInfoFile.get(name)` instead.
|
| 200 |
+
|
| 201 |
+
Use :func:`get_zonefile_instance` to retrieve an instance of the
|
| 202 |
+
dateutil-provided zoneinfo.
|
| 203 |
+
"""
|
| 204 |
+
warnings.warn("zoneinfo.gettz() will be removed in future versions, "
|
| 205 |
+
"to use the dateutil-provided zoneinfo files, instantiate a "
|
| 206 |
+
"ZoneInfoFile object and use ZoneInfoFile.zones.get() "
|
| 207 |
+
"instead. See the documentation for details.",
|
| 208 |
+
DeprecationWarning)
|
| 209 |
+
|
| 210 |
+
if len(_CLASS_ZONE_INSTANCE) == 0:
|
| 211 |
+
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile())
|
| 212 |
+
return _CLASS_ZONE_INSTANCE[0].zones.get(name)
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def gettz_db_metadata():
|
| 216 |
+
"""Get the zonefile metadata
|
| 217 |
+
|
| 218 |
+
:returns:
|
| 219 |
+
A dictionary with the database metadata
|
| 220 |
+
|
| 221 |
+
.. deprecated:: 2.6
|
| 222 |
+
See deprecation warning in :func:`zoneinfo.gettz`. To get metadata,
|
| 223 |
+
query the attribute ``zoneinfo.ZoneInfoFile.metadata``.
|
| 224 |
+
"""
|
| 225 |
+
warnings.warn("zoneinfo.gettz_db_metadata() will be removed in future "
|
| 226 |
+
"versions, to use the dateutil-provided zoneinfo files, "
|
| 227 |
+
"ZoneInfoFile object and query the 'metadata' attribute "
|
| 228 |
+
"instead. See the documentation for details.",
|
| 229 |
+
DeprecationWarning)
|
| 230 |
+
|
| 231 |
+
if len(_CLASS_ZONE_INSTANCE) == 0:
|
| 232 |
+
_CLASS_ZONE_INSTANCE.append(ZoneInfoFile())
|
| 233 |
+
return _CLASS_ZONE_INSTANCE[0].metadata
|
hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (9.5 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/zoneinfo/__pycache__/rebuild.cpython-312.pyc
ADDED
|
Binary file (1.8 kB). View file
|
|
|
hug/lib/python3.12/site-packages/dateutil/zoneinfo/rebuild.py
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import warnings
|
| 2 |
+
|
| 3 |
+
__all__ = ["rebuild", "RebuildDeprecationWarning"]
|
| 4 |
+
|
| 5 |
+
|
| 6 |
+
class RebuildDeprecationWarning(FutureWarning):
|
| 7 |
+
"""Warning raised by :func:`rebuild`, which no longer does anything.
|
| 8 |
+
|
| 9 |
+
A warnings filter turns this into an error by default. To keep calling
|
| 10 |
+
:func:`rebuild` as a no-op, filter it out, e.g.::
|
| 11 |
+
|
| 12 |
+
warnings.filterwarnings(
|
| 13 |
+
"ignore", category=dateutil.zoneinfo.rebuild.RebuildDeprecationWarning
|
| 14 |
+
)
|
| 15 |
+
"""
|
| 16 |
+
|
| 17 |
+
|
| 18 |
+
def rebuild(filename, tag=None, format="gz", zonegroups=[], metadata=None):
|
| 19 |
+
"""Formerly rebuilt the zoneinfo tarball bundled with ``dateutil``.
|
| 20 |
+
|
| 21 |
+
``dateutil`` no longer ships a zoneinfo tarball; it reads time zone data
|
| 22 |
+
from the system and from the `tzdata <https://pypi.org/project/tzdata/>`_
|
| 23 |
+
package instead. This function now does nothing except raise
|
| 24 |
+
:class:`RebuildDeprecationWarning`, which is an error unless a warnings
|
| 25 |
+
filter says otherwise.
|
| 26 |
+
|
| 27 |
+
.. deprecated:: 3.0.0
|
| 28 |
+
"""
|
| 29 |
+
# The filter is added at call time rather than at import time, so that it
|
| 30 |
+
# is still in place if the module was first imported inside a
|
| 31 |
+
# warnings.catch_warnings() block. It is appended, so any filter the user
|
| 32 |
+
# has installed for this category takes precedence.
|
| 33 |
+
warnings.filterwarnings(
|
| 34 |
+
"error", category=RebuildDeprecationWarning, append=True
|
| 35 |
+
)
|
| 36 |
+
warnings.warn(
|
| 37 |
+
"dateutil.zoneinfo.rebuild() no longer does anything, because dateutil "
|
| 38 |
+
"no longer ships a zoneinfo tarball. Use the tzdata package or the "
|
| 39 |
+
"system time zone data instead.",
|
| 40 |
+
RebuildDeprecationWarning,
|
| 41 |
+
stacklevel=2,
|
| 42 |
+
)
|
hug/lib/python3.12/site-packages/distutils-precedence.pth
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2638ce9e2500e572a5e0de7faed6661eb569d1b696fcba07b0dd223da5f5d224
|
| 3 |
+
size 151
|
hug/lib/python3.12/site-packages/example.py
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
#################################################################################
|
| 2 |
+
# Copyright (c) 2020, NVIDIA Corporation. All rights reserved. #
|
| 3 |
+
# #
|
| 4 |
+
# Redistribution and use in source and binary forms, with or without #
|
| 5 |
+
# modification, are permitted provided that the following conditions are met: #
|
| 6 |
+
# #
|
| 7 |
+
# * Redistributions of source code must retain the above copyright notice, #
|
| 8 |
+
# this list of conditions and the following disclaimer. #
|
| 9 |
+
# * Redistributions in binary form must reproduce the above copyright #
|
| 10 |
+
# notice, this list of conditions and the following disclaimer in the #
|
| 11 |
+
# documentation and/or other materials provided with the distribution. #
|
| 12 |
+
# * Neither the name of the NVIDIA Corporation nor the names of its #
|
| 13 |
+
# contributors may be used to endorse or promote products derived from #
|
| 14 |
+
# this software without specific prior written permission. #
|
| 15 |
+
# #
|
| 16 |
+
# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" #
|
| 17 |
+
# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE #
|
| 18 |
+
# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE #
|
| 19 |
+
# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE #
|
| 20 |
+
# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR #
|
| 21 |
+
# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF #
|
| 22 |
+
# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS #
|
| 23 |
+
# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN #
|
| 24 |
+
# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) #
|
| 25 |
+
# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF #
|
| 26 |
+
# THE POSSIBILITY OF SUCH DAMAGE. #
|
| 27 |
+
#################################################################################
|
| 28 |
+
|
| 29 |
+
#
|
| 30 |
+
# Sample script to demonstrate the usage of NVML API python bindings
|
| 31 |
+
#
|
| 32 |
+
|
| 33 |
+
# To Run:
|
| 34 |
+
# $ python ./example.py
|
| 35 |
+
|
| 36 |
+
from pynvml import *
|
| 37 |
+
|
| 38 |
+
#
|
| 39 |
+
# Helper function
|
| 40 |
+
#
|
| 41 |
+
def StrVirt(mode):
|
| 42 |
+
if mode == NVML_GPU_VIRTUALIZATION_MODE_NONE:
|
| 43 |
+
return "None";
|
| 44 |
+
elif mode == NVML_GPU_VIRTUALIZATION_MODE_PASSTHROUGH:
|
| 45 |
+
return "Pass-Through";
|
| 46 |
+
elif mode == NVML_GPU_VIRTUALIZATION_MODE_VGPU:
|
| 47 |
+
return "VGPU";
|
| 48 |
+
elif mode == NVML_GPU_VIRTUALIZATION_MODE_HOST_VGPU:
|
| 49 |
+
return "Host VGPU";
|
| 50 |
+
elif mode == NVML_GPU_VIRTUALIZATION_MODE_HOST_VSGA:
|
| 51 |
+
return "Host VSGA";
|
| 52 |
+
else:
|
| 53 |
+
return "Unknown";
|
| 54 |
+
|
| 55 |
+
#
|
| 56 |
+
# Converts errors into string messages
|
| 57 |
+
#
|
| 58 |
+
def handleError(err):
|
| 59 |
+
if (err.value == NVML_ERROR_NOT_SUPPORTED):
|
| 60 |
+
return "N/A"
|
| 61 |
+
else:
|
| 62 |
+
return err.__str__()
|
| 63 |
+
|
| 64 |
+
#######
|
| 65 |
+
def deviceQuery():
|
| 66 |
+
|
| 67 |
+
strResult = ''
|
| 68 |
+
try:
|
| 69 |
+
#
|
| 70 |
+
# Initialize NVML
|
| 71 |
+
#
|
| 72 |
+
nvmlInit()
|
| 73 |
+
|
| 74 |
+
strResult += ' <driver_version>' + str(nvmlSystemGetDriverVersion()) + '</driver_version>\n'
|
| 75 |
+
|
| 76 |
+
deviceCount = nvmlDeviceGetCount()
|
| 77 |
+
strResult += ' <attached_gpus>' + str(deviceCount) + '</attached_gpus>\n'
|
| 78 |
+
|
| 79 |
+
for i in range(0, deviceCount):
|
| 80 |
+
handle = nvmlDeviceGetHandleByIndex(i)
|
| 81 |
+
|
| 82 |
+
pciInfo = nvmlDeviceGetPciInfo(handle)
|
| 83 |
+
|
| 84 |
+
strResult += ' <gpu id="%s">\n' % pciInfo.busId
|
| 85 |
+
|
| 86 |
+
strResult += ' <product_name>' + nvmlDeviceGetName(handle) + '</product_name>\n'
|
| 87 |
+
|
| 88 |
+
brandNames = {NVML_BRAND_UNKNOWN : "Unknown",
|
| 89 |
+
NVML_BRAND_QUADRO : "Quadro",
|
| 90 |
+
NVML_BRAND_TESLA : "Tesla",
|
| 91 |
+
NVML_BRAND_NVS : "NVS",
|
| 92 |
+
NVML_BRAND_GRID : "Grid",
|
| 93 |
+
NVML_BRAND_TITAN : "Titan",
|
| 94 |
+
NVML_BRAND_GEFORCE : "GeForce",
|
| 95 |
+
NVML_BRAND_NVIDIA_VAPPS : "NVIDIA Virtual Applications",
|
| 96 |
+
NVML_BRAND_NVIDIA_VPC : "NVIDIA Virtual PC",
|
| 97 |
+
NVML_BRAND_NVIDIA_VCS : "NVIDIA vGPU for Compute",
|
| 98 |
+
NVML_BRAND_NVIDIA_VWS : "NVIDIA RTX Virtual Workstation",
|
| 99 |
+
NVML_BRAND_NVIDIA_CLOUD_GAMING : "NVIDIA Cloud Gaming",
|
| 100 |
+
NVML_BRAND_NVIDIA_VGAMEDEV : "NVIDIA RTX Virtual Game Dev",
|
| 101 |
+
NVML_BRAND_QUADRO_RTX : "Quadro RTX",
|
| 102 |
+
NVML_BRAND_NVIDIA_RTX : "NVIDIA RTX",
|
| 103 |
+
NVML_BRAND_NVIDIA : "NVIDIA",
|
| 104 |
+
NVML_BRAND_GEFORCE_RTX : "GeForce RTX",
|
| 105 |
+
NVML_BRAND_TITAN_RTX : "TITAN RTX",
|
| 106 |
+
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
try:
|
| 110 |
+
# If nvmlDeviceGetBrand() succeeds it is guaranteed to be in the dictionary
|
| 111 |
+
brandName = brandNames[nvmlDeviceGetBrand(handle)]
|
| 112 |
+
except NVMLError as err:
|
| 113 |
+
brandName = handleError(err)
|
| 114 |
+
|
| 115 |
+
strResult += ' <product_brand>' + brandName + '</product_brand>\n'
|
| 116 |
+
|
| 117 |
+
try:
|
| 118 |
+
serial = nvmlDeviceGetSerial(handle)
|
| 119 |
+
except NVMLError as err:
|
| 120 |
+
serial = handleError(err)
|
| 121 |
+
|
| 122 |
+
strResult += ' <serial>' + serial + '</serial>\n'
|
| 123 |
+
|
| 124 |
+
try:
|
| 125 |
+
uuid = nvmlDeviceGetUUID(handle)
|
| 126 |
+
except NVMLError as err:
|
| 127 |
+
uuid = handleError(err)
|
| 128 |
+
|
| 129 |
+
strResult += ' <uuid>' + uuid + '</uuid>\n'
|
| 130 |
+
|
| 131 |
+
strResult += ' <gpu_virtualization_mode>\n'
|
| 132 |
+
try:
|
| 133 |
+
mode = StrVirt(nvmlDeviceGetVirtualizationMode(handle))
|
| 134 |
+
except NVMLError as err:
|
| 135 |
+
mode = handleError(err)
|
| 136 |
+
strResult += ' <virtualization_mode>' + mode + '</virtualization_mode>\n'
|
| 137 |
+
strResult += ' </gpu_virtualization_mode>\n'
|
| 138 |
+
|
| 139 |
+
try:
|
| 140 |
+
gridLicensableFeatures = nvmlDeviceGetGridLicensableFeatures(handle)
|
| 141 |
+
if gridLicensableFeatures.isGridLicenseSupported == 1:
|
| 142 |
+
strResult += ' <vgpu_software_licensed_product>\n'
|
| 143 |
+
for i in range(gridLicensableFeatures.licensableFeaturesCount):
|
| 144 |
+
if gridLicensableFeatures.gridLicensableFeatures[i].featureState == 0:
|
| 145 |
+
if nvmlDeviceGetVirtualizationMode(handle) == NVML_GPU_VIRTUALIZATION_MODE_PASSTHROUGH:
|
| 146 |
+
strResult += ' <licensed_product_name>' + 'NVIDIA Virtual Applications' + '</licensed_product_name>\n'
|
| 147 |
+
strResult += ' <license_status>' + 'Licensed' + '</license_status>\n'
|
| 148 |
+
else:
|
| 149 |
+
strResult += ' <licensed_product_name>' + gridLicensableFeatures.gridLicensableFeatures[i].productName + '</licensed_product_name>\n'
|
| 150 |
+
strResult += ' <license_status>' + 'Unlicensed' + '</license_status>\n'
|
| 151 |
+
else:
|
| 152 |
+
strResult += ' <licensed_product_name>' + gridLicensableFeatures.gridLicensableFeatures[i].productName + '</licensed_product_name>\n'
|
| 153 |
+
strResult += ' <license_status>' + 'Licensed' + '</license_status>\n'
|
| 154 |
+
strResult += ' </vgpu_software_licensed_product>\n'
|
| 155 |
+
except NVMLError as err:
|
| 156 |
+
gridLicensableFeatures = handleError(err)
|
| 157 |
+
|
| 158 |
+
strResult += ' </gpu>\n'
|
| 159 |
+
|
| 160 |
+
except NVMLError as err:
|
| 161 |
+
strResult += 'example.py: ' + err.__str__() + '\n'
|
| 162 |
+
|
| 163 |
+
nvmlShutdown()
|
| 164 |
+
|
| 165 |
+
return strResult
|
| 166 |
+
|
| 167 |
+
# If this is not exectued when module is imported
|
| 168 |
+
if __name__ == "__main__":
|
| 169 |
+
print(deviceQuery())
|
| 170 |
+
|
hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/INSTALLER
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
pip
|
hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/METADATA
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Metadata-Version: 2.5
|
| 2 |
+
Name: filelock
|
| 3 |
+
Version: 4.2.4
|
| 4 |
+
Summary: A platform independent file lock.
|
| 5 |
+
Project-URL: Documentation, https://py-filelock.readthedocs.io
|
| 6 |
+
Project-URL: Homepage, https://github.com/tox-dev/py-filelock
|
| 7 |
+
Project-URL: Source, https://github.com/tox-dev/py-filelock
|
| 8 |
+
Project-URL: Tracker, https://github.com/tox-dev/py-filelock/issues
|
| 9 |
+
Maintainer-email: Bernát Gábor <gaborjbernat@gmail.com>
|
| 10 |
+
License-Expression: MIT
|
| 11 |
+
License-File: LICENSE
|
| 12 |
+
Keywords: application,cache,directory,log,user
|
| 13 |
+
Classifier: Development Status :: 5 - Production/Stable
|
| 14 |
+
Classifier: Intended Audience :: Developers
|
| 15 |
+
Classifier: License :: OSI Approved :: MIT License
|
| 16 |
+
Classifier: Operating System :: OS Independent
|
| 17 |
+
Classifier: Programming Language :: Python
|
| 18 |
+
Classifier: Programming Language :: Python :: 3 :: Only
|
| 19 |
+
Classifier: Programming Language :: Python :: 3.11
|
| 20 |
+
Classifier: Programming Language :: Python :: 3.12
|
| 21 |
+
Classifier: Programming Language :: Python :: 3.13
|
| 22 |
+
Classifier: Programming Language :: Python :: 3.14
|
| 23 |
+
Classifier: Programming Language :: Python :: 3.15
|
| 24 |
+
Classifier: Topic :: Internet
|
| 25 |
+
Classifier: Topic :: Software Development :: Libraries
|
| 26 |
+
Classifier: Topic :: System
|
| 27 |
+
Requires-Python: >=3.11
|
| 28 |
+
Description-Content-Type: text/markdown
|
| 29 |
+
|
| 30 |
+
# filelock
|
| 31 |
+
|
| 32 |
+
[](https://pypi.org/project/filelock/)
|
| 33 |
+
[](https://pypi.org/project/filelock/)
|
| 34 |
+
[](https://py-filelock.readthedocs.io/en/latest/?badge=latest)
|
| 35 |
+
[](https://pepy.tech/project/filelock)
|
| 36 |
+
[](https://github.com/tox-dev/py-filelock/actions/workflows/check.yaml)
|
| 37 |
+
|
| 38 |
+
For more information checkout the [official documentation](https://py-filelock.readthedocs.io/en/latest/index.html).
|
hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/RECORD
ADDED
|
@@ -0,0 +1,52 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
filelock-4.2.4.dist-info/INSTALLER,sha256=zuuue4knoyJ-UwPPXg8fezS7VCrXJQrAP7zeNuwvFQg,4
|
| 2 |
+
filelock-4.2.4.dist-info/METADATA,sha256=2ZwPcqerhMkktzQBiiilABUF_g-WKAxEYrhkMT7d21A,1976
|
| 3 |
+
filelock-4.2.4.dist-info/RECORD,,
|
| 4 |
+
filelock-4.2.4.dist-info/WHEEL,sha256=W3fkpkm7-wf9vBI5Z-7s0eWkeM-spu78I8Neb98DeEg,87
|
| 5 |
+
filelock-4.2.4.dist-info/licenses/LICENSE,sha256=YIyJ1QYK6ZIa3M8yNmlbxlSplG4SMj72wCHfoE4pTUg,1088
|
| 6 |
+
filelock/__init__.py,sha256=LaXgCbdErp-ugbr3o5bzpYJMBwi1hg0_hO4_vrwP1lk,5693
|
| 7 |
+
filelock/__pycache__/__init__.cpython-312.pyc,,
|
| 8 |
+
filelock/__pycache__/_api.cpython-312.pyc,,
|
| 9 |
+
filelock/__pycache__/_async.cpython-312.pyc,,
|
| 10 |
+
filelock/__pycache__/_async_read_write.cpython-312.pyc,,
|
| 11 |
+
filelock/__pycache__/_descriptor.cpython-312.pyc,,
|
| 12 |
+
filelock/__pycache__/_error.cpython-312.pyc,,
|
| 13 |
+
filelock/__pycache__/_identity.cpython-312.pyc,,
|
| 14 |
+
filelock/__pycache__/_lease.cpython-312.pyc,,
|
| 15 |
+
filelock/__pycache__/_marker.cpython-312.pyc,,
|
| 16 |
+
filelock/__pycache__/_read_write.cpython-312.pyc,,
|
| 17 |
+
filelock/__pycache__/_soft.cpython-312.pyc,,
|
| 18 |
+
filelock/__pycache__/_soft_protocol.cpython-312.pyc,,
|
| 19 |
+
filelock/__pycache__/_strict.cpython-312.pyc,,
|
| 20 |
+
filelock/__pycache__/_unix.cpython-312.pyc,,
|
| 21 |
+
filelock/__pycache__/_util.cpython-312.pyc,,
|
| 22 |
+
filelock/__pycache__/_windows.cpython-312.pyc,,
|
| 23 |
+
filelock/__pycache__/asyncio.cpython-312.pyc,,
|
| 24 |
+
filelock/__pycache__/version.cpython-312.pyc,,
|
| 25 |
+
filelock/_api.py,sha256=OUugdO27gcc_gdfVOJYo0C6Bv6lvXL8Fv84vT7le378,106170
|
| 26 |
+
filelock/_async.py,sha256=jSqb-YUze8cjKaNrlJ2qlMdfN9s1DZbgRMnozp4kDZs,20487
|
| 27 |
+
filelock/_async_read_write.py,sha256=Ts6N_p5-UQLDoYHJKE67NfFV6ZpIcpX0cuu3S55dXzQ,17947
|
| 28 |
+
filelock/_descriptor.py,sha256=Z81x98R7RbN5aHgaRzu48i7c0Ktsheze6Yg5uvWrU0Y,3277
|
| 29 |
+
filelock/_error.py,sha256=sZNfFV3wacnrGCrnOipucPbnHN78NPt1IDnlg1nON1k,3235
|
| 30 |
+
filelock/_identity.py,sha256=s2Kk4SQI0C5en9lHsPkWOVvGzuyY_wgZl8JRSOn63_8,17183
|
| 31 |
+
filelock/_lease.py,sha256=eK0xXpuccVNM8o1BXSQByBE4TaIVYQSbSvfbbS6dReY,19173
|
| 32 |
+
filelock/_marker.py,sha256=dVjalDpcpwqIRF7VUvFK2tkVZ4OCthjmmIinKy9jhFE,7896
|
| 33 |
+
filelock/_read_write.py,sha256=5e0gtCHfLArSNKYH0jGdBXuD60m7stQV8KhuYgUvqQU,68071
|
| 34 |
+
filelock/_soft.py,sha256=fXLlCQKHzDHaXCuwU8bXZw6qgmF1LEKRWVifxnRhRyk,33064
|
| 35 |
+
filelock/_soft_protocol.py,sha256=D64pnXMlM7ysKpdx0fNXhJJgiMcS9T2Qwsu2GpPs2i0,218
|
| 36 |
+
filelock/_soft_rw/__init__.py,sha256=JAMF4NA010_Jq-nEgHHZgWHZ7Of9wI3PKHg24mDdX50,452
|
| 37 |
+
filelock/_soft_rw/__pycache__/__init__.cpython-312.pyc,,
|
| 38 |
+
filelock/_soft_rw/__pycache__/_async.cpython-312.pyc,,
|
| 39 |
+
filelock/_soft_rw/__pycache__/_protocol.cpython-312.pyc,,
|
| 40 |
+
filelock/_soft_rw/__pycache__/_storage.cpython-312.pyc,,
|
| 41 |
+
filelock/_soft_rw/__pycache__/_sync.cpython-312.pyc,,
|
| 42 |
+
filelock/_soft_rw/_async.py,sha256=v3q2adfqb9vQRwqhlctYdKZYlltJ4XutZcJ7IXuDy4M,16939
|
| 43 |
+
filelock/_soft_rw/_protocol.py,sha256=O-ENv_XstRpIIDh4GnFoPNfkIg-xsDc8tVpsfBhrklA,30996
|
| 44 |
+
filelock/_soft_rw/_storage.py,sha256=SbhhE8HQp7RcPo7BPnUNSEi8di2b6M7JwXHBh4EkuSc,13691
|
| 45 |
+
filelock/_soft_rw/_sync.py,sha256=qVgup1GUig8fDSWLkl16z8_9UXojAcPJ_nZWl5BzIaI,37638
|
| 46 |
+
filelock/_strict.py,sha256=iaEnG4QmSESjubZIhz0aq4Fq5cWmBy3STeim-8F6AZs,58570
|
| 47 |
+
filelock/_unix.py,sha256=BLotMhNLFKTrW6zQ_qoI9qZGo3YqYfhGO_qP5W_p6Lw,10985
|
| 48 |
+
filelock/_util.py,sha256=XW3K2JNfJapekS7YgUYBGiOFYcSf8A1cBqnKfU6jxzY,19280
|
| 49 |
+
filelock/_windows.py,sha256=cDFvJN8dJtdDYjHYPt_kplvJCklD_yI7_UiaJkV-V80,38880
|
| 50 |
+
filelock/asyncio.py,sha256=x3-lsFBKzEnPQR_2bQDoSD9ErdmV_3lw5rPyRq7AZc4,42865
|
| 51 |
+
filelock/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
| 52 |
+
filelock/version.py,sha256=v35Uwrw-IApU2AA-2B77yCvOvNQ1Vh7gXvxfaJ9Vd14,520
|
hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/WHEEL
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Wheel-Version: 1.0
|
| 2 |
+
Generator: hatchling 1.32.4
|
| 3 |
+
Root-Is-Purelib: true
|
| 4 |
+
Tag: py3-none-any
|
hug/lib/python3.12/site-packages/filelock-4.2.4.dist-info/licenses/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
MIT License
|
| 2 |
+
|
| 3 |
+
Copyright (c) 2025 Bernát Gábor and contributors
|
| 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.
|
hug/lib/python3.12/site-packages/filelock/__init__.py
ADDED
|
@@ -0,0 +1,174 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
A platform independent file lock that supports the with-statement.
|
| 3 |
+
|
| 4 |
+
.. autodata:: filelock.__version__
|
| 5 |
+
:no-value:
|
| 6 |
+
|
| 7 |
+
.. autodata:: filelock.OwnerMode
|
| 8 |
+
|
| 9 |
+
.. autodata:: filelock.CompromiseReason
|
| 10 |
+
|
| 11 |
+
"""
|
| 12 |
+
|
| 13 |
+
from __future__ import annotations
|
| 14 |
+
|
| 15 |
+
__lazy_modules__ = ["filelock._descriptor", "filelock._error", "importlib"]
|
| 16 |
+
|
| 17 |
+
import sys
|
| 18 |
+
import warnings
|
| 19 |
+
from importlib import import_module
|
| 20 |
+
from typing import TYPE_CHECKING, Final
|
| 21 |
+
|
| 22 |
+
from filelock._api import AcquireReturnProxy, BaseFileLock, CloseErrorPolicy, ContextErrorPolicy, LockOptions
|
| 23 |
+
from filelock._descriptor import lock_descriptor, unlock_descriptor
|
| 24 |
+
from filelock._error import LeaseSettingsMismatch, SoftFileLockLifetimeWarning, SoftFileLockProtocolError, Timeout
|
| 25 |
+
from filelock._unix import UnixFileLock
|
| 26 |
+
from filelock._unix import has_fcntl as _has_fcntl
|
| 27 |
+
from filelock._windows import WindowsFileLock
|
| 28 |
+
|
| 29 |
+
# Keeps ``filelock.version`` the string 4.0.12 exposed rather than the submodule.
|
| 30 |
+
from filelock.version import version
|
| 31 |
+
|
| 32 |
+
if TYPE_CHECKING:
|
| 33 |
+
from filelock import asyncio
|
| 34 |
+
from filelock._async_read_write import AsyncAcquireReadWriteReturnProxy, AsyncReadWriteLock
|
| 35 |
+
from filelock._lease import CompromiseReason, LeaseCompromise, SoftFileLease
|
| 36 |
+
from filelock._marker import MarkerSoftFileLock, OwnerMode, OwnerRecord
|
| 37 |
+
from filelock._read_write import ReadWriteLock
|
| 38 |
+
from filelock._soft import SoftFileLock
|
| 39 |
+
from filelock._soft_rw import AsyncAcquireSoftReadWriteReturnProxy, AsyncSoftReadWriteLock, SoftReadWriteLock
|
| 40 |
+
from filelock._strict import StrictSoftFileClaim, StrictSoftFileClaimState, StrictSoftFileLock
|
| 41 |
+
from filelock.asyncio import (
|
| 42 |
+
AsyncAcquireReturnProxy,
|
| 43 |
+
AsyncSoftFileLease,
|
| 44 |
+
AsyncSoftFileLock,
|
| 45 |
+
AsyncStrictSoftFileLock,
|
| 46 |
+
AsyncUnixFileLock,
|
| 47 |
+
AsyncWindowsFileLock,
|
| 48 |
+
BaseAsyncFileLock,
|
| 49 |
+
)
|
| 50 |
+
|
| 51 |
+
#: version of the project as a string
|
| 52 |
+
__version__: Final[str] = version
|
| 53 |
+
|
| 54 |
+
#: ``True`` when :func:`fcntl.flock` is available. Off Windows, :data:`FileLock` then resolves to
|
| 55 |
+
#: :class:`UnixFileLock`, otherwise to :class:`SoftFileLock`. Always ``False`` on Windows.
|
| 56 |
+
#:
|
| 57 |
+
#: :meta hide-value:
|
| 58 |
+
has_fcntl: Final[bool] = _has_fcntl
|
| 59 |
+
|
| 60 |
+
|
| 61 |
+
if sys.platform == "win32": # pragma: win32 cover
|
| 62 |
+
_FileLock: type[BaseFileLock] = WindowsFileLock
|
| 63 |
+
else: # pragma: win32 no cover # ruff:ignore[collapsible-else-if] # the else carries the win32 no-cover pragma
|
| 64 |
+
if has_fcntl:
|
| 65 |
+
_FileLock: type[BaseFileLock] = UnixFileLock
|
| 66 |
+
else:
|
| 67 |
+
from filelock._soft import SoftFileLock
|
| 68 |
+
|
| 69 |
+
_FileLock = SoftFileLock
|
| 70 |
+
warnings.warn("only soft file lock is available", stacklevel=2)
|
| 71 |
+
|
| 72 |
+
if TYPE_CHECKING:
|
| 73 |
+
FileLock = SoftFileLock
|
| 74 |
+
AsyncFileLock = AsyncSoftFileLock
|
| 75 |
+
else:
|
| 76 |
+
#: Alias for the lock, which should be used for the current platform.
|
| 77 |
+
FileLock = _FileLock
|
| 78 |
+
|
| 79 |
+
# Importing asyncio and sqlite3 costs more than the rest of the package, so names that need them load on first access.
|
| 80 |
+
_LAZY_ATTRIBUTES: Final[dict[str, str]] = {
|
| 81 |
+
"AsyncAcquireReadWriteReturnProxy": "._async_read_write",
|
| 82 |
+
"AsyncAcquireReturnProxy": ".asyncio",
|
| 83 |
+
"AsyncAcquireSoftReadWriteReturnProxy": "._soft_rw",
|
| 84 |
+
"AsyncFileLock": ".asyncio",
|
| 85 |
+
"AsyncReadWriteLock": "._async_read_write",
|
| 86 |
+
"AsyncSoftFileLease": ".asyncio",
|
| 87 |
+
"AsyncSoftFileLock": ".asyncio",
|
| 88 |
+
"AsyncSoftReadWriteLock": "._soft_rw",
|
| 89 |
+
"AsyncStrictSoftFileLock": ".asyncio",
|
| 90 |
+
"AsyncUnixFileLock": ".asyncio",
|
| 91 |
+
"AsyncWindowsFileLock": ".asyncio",
|
| 92 |
+
"BaseAsyncFileLock": ".asyncio",
|
| 93 |
+
"CompromiseReason": "._lease",
|
| 94 |
+
"LeaseCompromise": "._lease",
|
| 95 |
+
"MarkerSoftFileLock": "._marker",
|
| 96 |
+
"OwnerMode": "._marker",
|
| 97 |
+
"OwnerRecord": "._marker",
|
| 98 |
+
"ReadWriteLock": "._read_write",
|
| 99 |
+
"SoftFileLease": "._lease",
|
| 100 |
+
"SoftFileLock": "._soft",
|
| 101 |
+
"SoftReadWriteLock": "._soft_rw",
|
| 102 |
+
"StrictSoftFileClaim": "._strict",
|
| 103 |
+
"StrictSoftFileClaimState": "._strict",
|
| 104 |
+
"StrictSoftFileLock": "._strict",
|
| 105 |
+
"asyncio": ".asyncio",
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
|
| 109 |
+
def __getattr__(name: str) -> object:
|
| 110 |
+
if (module_name := _LAZY_ATTRIBUTES.get(name)) is None:
|
| 111 |
+
msg = f"module {__name__!r} has no attribute {name!r}"
|
| 112 |
+
raise AttributeError(msg)
|
| 113 |
+
try:
|
| 114 |
+
module = import_module(module_name, __name__)
|
| 115 |
+
except ImportError: # pragma: lacks sqlite3
|
| 116 |
+
if module_name not in {"._read_write", "._async_read_write"}:
|
| 117 |
+
raise
|
| 118 |
+
value = None
|
| 119 |
+
else:
|
| 120 |
+
if name == "asyncio":
|
| 121 |
+
value = module
|
| 122 |
+
else:
|
| 123 |
+
value = getattr(module, f"Async{_FileLock.__name__}" if name == "AsyncFileLock" else name)
|
| 124 |
+
globals()[name] = value
|
| 125 |
+
return value
|
| 126 |
+
|
| 127 |
+
|
| 128 |
+
def __dir__() -> list[str]:
|
| 129 |
+
return sorted({*globals(), *_LAZY_ATTRIBUTES})
|
| 130 |
+
|
| 131 |
+
|
| 132 |
+
__all__ = [
|
| 133 |
+
"AcquireReturnProxy",
|
| 134 |
+
"AsyncAcquireReadWriteReturnProxy",
|
| 135 |
+
"AsyncAcquireReturnProxy",
|
| 136 |
+
"AsyncAcquireSoftReadWriteReturnProxy",
|
| 137 |
+
"AsyncFileLock",
|
| 138 |
+
"AsyncReadWriteLock",
|
| 139 |
+
"AsyncSoftFileLease",
|
| 140 |
+
"AsyncSoftFileLock",
|
| 141 |
+
"AsyncSoftReadWriteLock",
|
| 142 |
+
"AsyncStrictSoftFileLock",
|
| 143 |
+
"AsyncUnixFileLock",
|
| 144 |
+
"AsyncWindowsFileLock",
|
| 145 |
+
"BaseAsyncFileLock",
|
| 146 |
+
"BaseFileLock",
|
| 147 |
+
"CloseErrorPolicy",
|
| 148 |
+
"CompromiseReason",
|
| 149 |
+
"ContextErrorPolicy",
|
| 150 |
+
"FileLock",
|
| 151 |
+
"LeaseCompromise",
|
| 152 |
+
"LeaseSettingsMismatch",
|
| 153 |
+
"LockOptions",
|
| 154 |
+
"MarkerSoftFileLock",
|
| 155 |
+
"OwnerMode",
|
| 156 |
+
"OwnerRecord",
|
| 157 |
+
"ReadWriteLock",
|
| 158 |
+
"SoftFileLease",
|
| 159 |
+
"SoftFileLock",
|
| 160 |
+
"SoftFileLockLifetimeWarning",
|
| 161 |
+
"SoftFileLockProtocolError",
|
| 162 |
+
"SoftReadWriteLock",
|
| 163 |
+
"StrictSoftFileClaim",
|
| 164 |
+
"StrictSoftFileClaimState",
|
| 165 |
+
"StrictSoftFileLock",
|
| 166 |
+
"Timeout",
|
| 167 |
+
"UnixFileLock",
|
| 168 |
+
"WindowsFileLock",
|
| 169 |
+
"__version__",
|
| 170 |
+
"asyncio",
|
| 171 |
+
"has_fcntl",
|
| 172 |
+
"lock_descriptor",
|
| 173 |
+
"unlock_descriptor",
|
| 174 |
+
]
|
hug/lib/python3.12/site-packages/filelock/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (4.65 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_api.cpython-312.pyc
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:228cffaa0359c96ef77da374611ffc1fc2167ce70ec382681b1242a5ef040863
|
| 3 |
+
size 111999
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_async.cpython-312.pyc
ADDED
|
Binary file (25.4 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_async_read_write.cpython-312.pyc
ADDED
|
Binary file (21.6 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_descriptor.cpython-312.pyc
ADDED
|
Binary file (3.66 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_error.cpython-312.pyc
ADDED
|
Binary file (5.02 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_identity.cpython-312.pyc
ADDED
|
Binary file (17.6 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_lease.cpython-312.pyc
ADDED
|
Binary file (20.1 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_marker.cpython-312.pyc
ADDED
|
Binary file (10.2 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_read_write.cpython-312.pyc
ADDED
|
Binary file (78.6 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_soft.cpython-312.pyc
ADDED
|
Binary file (34.5 kB). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_soft_protocol.cpython-312.pyc
ADDED
|
Binary file (447 Bytes). View file
|
|
|
hug/lib/python3.12/site-packages/filelock/__pycache__/_strict.cpython-312.pyc
ADDED
|
Binary file (65.5 kB). View file
|
|
|