python-dateutil
Advanced tools
| # Set the default behavior, in case people don't have core.autocrlf set. | ||
| * text=auto | ||
| # Specify what's text and should be normalized | ||
| *.py text | ||
| *.in text | ||
| *.rst text | ||
| *.cfg text | ||
| *.ini text | ||
| *.yml text | ||
| *.json text | ||
| *.bat text | ||
| *.sh text | ||
| RELEASING text | ||
| # NEWS and Windows batch files should be crlf | ||
| NEWS eol=crlf | ||
| *.bat eol=crlf |
+20
| # Byte-compiled / optimized / DLL files | ||
| __pycache__/ | ||
| *.py[cod] | ||
| build/ | ||
| dist/ | ||
| *.egg-info/ | ||
| .tox/ | ||
| # Autogenerated version information | ||
| dateutil/_version.py | ||
| # Sphinx documentation | ||
| docs/_build/ | ||
| # Timezone information | ||
| tzdata*.tar.gz | ||
| .idea | ||
| .cache |
+33
| sudo: false | ||
| language: python | ||
| cache: pip | ||
| python: | ||
| - "2.7" | ||
| - "3.3" | ||
| - "3.4" | ||
| - "3.5" | ||
| - "3.6" | ||
| - "nightly" | ||
| - "pypy-5.4" | ||
| - "pypy3" | ||
| env: | ||
| TOXENV=py | ||
| matrix: | ||
| fast_finish: true | ||
| include: | ||
| - python: 3.6 | ||
| env: TOXENV=docs | ||
| allow_failures: | ||
| - python: "nightly" | ||
| install: | ||
| - pip install -U six && pip install -U tox | ||
| - ./ci_tools/retry.sh python updatezinfo.py | ||
| script: | ||
| - tox | ||
| after_success: | ||
| - if [[ $TOXENV == "py" ]]; then tox -e coverage,codecov; fi | ||
+45
| build: false | ||
| environment: | ||
| matrix: | ||
| - PYTHON_VERSION: 27 | ||
| - PYTHON_VERSION: 33 | ||
| - PYTHON_VERSION: 34 | ||
| - PYTHON_VERSION: 35 | ||
| - PYTHON_VERSION: 36 | ||
| platform: | ||
| - x64 | ||
| - x86 | ||
| matrix: | ||
| fast_finish: true | ||
| exclude: | ||
| - platform: x86 | ||
| PYTHON_VERSION: 33 | ||
| - platform: x86 | ||
| PYTHON_VERSION: 34 | ||
| - platform: x86 | ||
| PYTHON_VERSION: 35 | ||
| install: | ||
| # set env variables | ||
| - set TOXENV=py%PYTHON_VERSION% | ||
| - if %PLATFORM% == "X64" (set PYTHON_PATH=C:\Python%PYTHON_VERSION%-x64) ELSE (set PYTHON_PATH=C:\Python%PYTHON_VERSION%) | ||
| # Add PostgreSQL (zic), Python and scripts directory to current path | ||
| - set PATH=%PYTHON_PATH%;c:\Program Files\PostgreSQL\9.3\bin\;%PATH% | ||
| - set PYTHON=%PYTHON_PATH%/python.exe | ||
| - "%PYTHON% -c \"import sys; print(sys.executable, sys.version)\"" | ||
| # This frequently fails with network errors, so we'll retry it up to 5 times | ||
| # with a 1 minute rate limit. | ||
| - "%PYTHON% -m pip install six" | ||
| - "ci_tools/retry.bat %PYTHON% updatezinfo.py" | ||
| # This environment variable tells the test suite it's OK to mess with the time zone. | ||
| - set DATEUTIL_MAY_CHANGE_TZ=1 | ||
| - C:\Python36\python -m pip install -U tox | ||
| test_script: | ||
| - C:\Python36\scripts\tox | ||
| after_test: | ||
| - C:\Python36\scripts\tox -e coverage,codecov |
+84
| This is a (possibly incomplete) list of all the contributors to python-dateutil, | ||
| initially generated from the git author metadata. The details of their specific | ||
| contributions can be found in the git history. | ||
| Prior to 2017-12-01, the library was licensed solely under the BSD 3-clause | ||
| license, all contributions on or after 2017-12-01 are dual-licensed between | ||
| Apache 2.0 and BSD 3-clause. In the list below, anyone whose name is marked with | ||
| **R** has agreed to re-license their previously submitted code under Apache 2.0. | ||
| Anyone whose name is marked with a **D** has only made contributions since the | ||
| switch, and thus all their contributions are dual-licensed. | ||
| ## Contributors (alphabetical order) | ||
| - Adam Chainz <adam@MASKED> | ||
| - Adrien Cossa <cossa@MASKED> | ||
| - Alec Nikolas Reiter <alecreiter@MASKED> | ||
| - Alec Reiter <areiter@MASKED> | ||
| - Alex Chamberlain (gh: @alexchamberlain) **D** | ||
| - Alex Verdyan <verdyan@MASKED> | ||
| - Alex Willmer <alex@MASKED> (gh: @moreati) | ||
| - Alexander Brugh <alexander.brugh@MASKED> (gh: @abrugh) | ||
| - Andrew Murray <radarhere@MASKED> | ||
| - Bernat Gabor <bgabor8@bloomberg.net> (gh: @gaborbernat) **D** | ||
| - Brandon W Maister <quodlibetor@MASKED> | ||
| - Brock Mendel <jbrockmendel@MASKED> (gh: @jbrockmendel) **R** | ||
| - Carlos <carlosxl@MASKED> | ||
| - Christopher Corley <cscorley@MASKED> | ||
| - Claudio Canepa <ccanepacc@MASKED> | ||
| - Daniel Lepage <dplepage@MASKED> | ||
| - David Lehrian <david@MASKED> | ||
| - Dominik Kozaczko <dominik@MASKED> | ||
| - Elvis Pranskevichus <el@MASKED> | ||
| - Florian Rathgeber (gh: @kynan) | ||
| - Gabriel Poesia <gabriel.poesia@MASKED> | ||
| - Gustavo Niemeyer <gustavo@niemeyer.net> (gh: @niemeyer) | ||
| - Holger Joukl <holger.joukl@MASKED> (gh: @hjoukl) | ||
| - Igor <mrigor83@MASKED> | ||
| - Ionuț Ciocîrlan <jdxlark@MASKED> | ||
| - Jake Chorley (gh: @jakec-github) **D** | ||
| - Jan Studený <jendas1@MASKED> | ||
| - Jitesh <jitesh@MASKED> | ||
| - Jon Dufresne <jon.dufresne@MASKED> (gh: @jdufresne) **R** | ||
| - Jonas Neubert <jonas@MASKED> | ||
| - Kirit Thadaka <kirit.thadaka@gmail.com> (gh: @kirit93) **D** | ||
| - Kubilay Kocak <koobs@MASKED> | ||
| - Laszlo Kiss Kollar <kiss.kollar.laszlo@MASKED> (gh: @lkollar) **D** | ||
| - Mario Corchero <mcorcherojim@MASKED> (gh: @mariocj89) **R** | ||
| - Mateusz Dziedzic (gh: @m-dz) **D** | ||
| - Matthew Schinckel <matt@MASKED> | ||
| - Max Shenfield <shenfieldmax@MASKED> | ||
| - Maxime Lorant <maxime.lorant@MASKED> | ||
| - Michael Aquilina <michaelaquilina@MASKED> (gh: @MichaelAquilina) | ||
| - Michael J. Schultz <mjschultz@MASKED> | ||
| - Mike Gilbert <floppym@MASKED> | ||
| - Nick Smith <nick.smith@MASKED> | ||
| - Paul Ganssle <paul@ganssle.io> (gh: @pganssle) **R** | ||
| - Pascal van Kooten <kootenpv@MASKED> | ||
| - Pavel Ponomarev <comrad.awsum@MASKED> | ||
| - Peter Bieringer <pb@MASKED> | ||
| - Quentin Pradet <quentin@MASKED> | ||
| - Roy Williams <rwilliams@MASKED> | ||
| - Savraj <savraj@MASKED> | ||
| - Sergey Vishnikin <armicron@MASKED> | ||
| - Stefan Bonchev **D** | ||
| - Thierry Bastian <thierryb@MASKED> | ||
| - Thomas A Caswell <tcaswell@MASKED> (gh: @tacaswell) | ||
| - Thomas Achtemichuk <tom@MASKED> | ||
| - Thomas Kluyver <takowl@MASKED> (gh: @takluyver) | ||
| - Tomasz Kluczkowski (gh: @Tomasz-Kluczkowski) **D** | ||
| - Tomi Pieviläinen <tomi.pievilainen@iki.fi> | ||
| - Unrud <Unrud@MASKED> (gh: @unrud) | ||
| - Xavier Lapointe <lapointe.xavier@MASKED> (gh: @lapointexavier) **D** | ||
| - X O <xo@MASKED> | ||
| - Yaron de Leeuw <me@jarondl.net> (gh: @jarondl) | ||
| - Yoney <alper_yoney@hotmail.com> **D** | ||
| - Zbigniew Jędrzejewski-Szmek <zbyszek@MASKED> | ||
| - bachmann <bachmann.matt@MASKED> | ||
| - bjv <brandon.vanvaerenbergh@MASKED> (@bjamesvERT) | ||
| - gl <gl@MASKED> | ||
| - labrys <labrys@MASKED> (gh: @labrys) | ||
| - ms-boom <ms-boom@MASKED> | ||
| - ryanss <ryanssdev@MASKED> (gh: @ryanss) | ||
| Unless someone has deliberately given permission to publish their e-mail, I have masked the domain names. If you are not on this list and believe you should be, or you *are* on this list and your information is inaccurate, please e-mail the current maintainer or the mailing list (dateutil@python.org) with your name, e-mail (if desired) and github (if desired / applicable), as you would like them displayed. Additionally, please indicate if you are willing to dual license your old contributions under Apache 2.0. |
| @echo off | ||
| REM This script takes a command and retries it a few times if it fails, with a | ||
| REM timeout between each retry. | ||
| setlocal EnableDelayedExpansion | ||
| REM Loop at most n_retries times, waiting sleep_time times between | ||
| set sleep_time=60 | ||
| set n_retries=5 | ||
| for /l %%x in (1, 1, %n_retries%) do ( | ||
| call %* | ||
| if not ERRORLEVEL 1 EXIT /B 0 | ||
| timeout /t %sleep_time% /nobreak > nul | ||
| ) | ||
| REM If it failed all n_retries times, we can give up at last. | ||
| EXIT /B 1 |
| #!/usr/bin/env bash | ||
| sleep_time=60 | ||
| n_retries=5 | ||
| for i in `seq 1 $n_retries`; do | ||
| "$@" && exit 0 | ||
| sleep $sleep_time | ||
| done | ||
| exit 1 |
| coverage: | ||
| status: | ||
| patch: false | ||
| changes: false | ||
| project: | ||
| default: | ||
| target: '80' | ||
| comment: false |
| # Contributing | ||
| This document outlines the ways to contribute to `python-dateutil`. This is a fairly small, low-traffic project, so most of the contribution norms (coding style, acceptance criteria) have been developed ad hoc and this document will not be exhaustive. If you are interested in contributing code or documentation, please take a moment to at least review the license section to understand how your code will be licensed. | ||
| ## Types of contribution | ||
| ### Bug reports | ||
| Bug reports are an important type of contribution - it's important to get feedback about how the library is failing, and there's no better way to do that than to hear about real-life failure cases. A good bug report will include: | ||
| 1. A minimal, reproducible example - a small, self-contained script that can reproduce the behavior is the best way to get your bug fixed. For more information and tips on how to structure these, read [StackOverflow's guide to creating a minimal, complete, verified example](https://stackoverflow.com/help/mcve). | ||
| 2. The platform and versions of everything involved, at a minimum please include operating system, `python` version and `dateutil` version. Instructions on getting your versions: | ||
| - `dateutil`: `python -c 'import dateutil; print(dateutil.__version__)'` | ||
| - `Python`: `python --version` | ||
| 3. A description of the problem - what *is* happening and what *should* happen. | ||
| While pull requests fixing bugs are accepted, they are *not* required - the bug report in itself is a great contribution. | ||
| ### Feature requests | ||
| If you would like to see a new feature in `dateutil`, it is probably best to start an issue for discussion rather than taking the time to implement a feature which may or may not be appropriate for `dateutil`'s API. For minor features (ones where you don't have to put a lot of effort into the PR), a pull request is fine but still not necessary. | ||
| ### Pull requests | ||
| If you would like to fix something in `dateutil` - improvements to documentation, bug fixes, feature implementations, fixes to the build system, etc - pull requests are welcome! Where possible, try to keep your coding to [PEP 8 style](https://www.python.org/dev/peps/pep-0008/), with the minor modification that the existing `dateutil` class naming style does not use the CapWords convention, or where the existing style does not follow PEP 8. | ||
| The most important thing to include in your pull request are *tests* - please write one or more tests to cover the behavior you intend your patch to improve. Ideally, tests would use only the public interface - try to get 100% difference coverage using only supported behavior of the API. | ||
| ## License | ||
| Starting December 1, 2017, all contributions will be assumed to be released under a dual license - the [Apache 2.0 License](https://www.apache.org/licenses/LICENSE-2.0) and the [3-Clause BSD License](https://opensource.org/licenses/BSD-3-Clause) unless otherwise specified in the pull request. | ||
| All contributions before December 1, 2017 except those explicitly relicensed, are only under the 3-clause BSD license. | ||
| ## Building and releasing | ||
| When you get the source, it does not contain the internal zoneinfo | ||
| database. To get (and update) the database, run the updatezinfo.py script. Make sure | ||
| that the zic command is in your path, and that you have network connectivity | ||
| to get the latest timezone information from IANA, or from [our mirror of the | ||
| IANA database](https://dateutil.github.io/tzdata/). | ||
| ## Development Setup | ||
| Install the the dependencies for running the test suite using `pip` or `conda`. | ||
| ### pip | ||
| Run the following commands to create a [virtual environment](https://virtualenv.pypa.io) with all dependencies installed: | ||
| python -m virtualenv .venv # Create virtual environment in .venv directory | ||
| . .venv/bin/activate # Activate the virtual environment | ||
| pip install -r requirements.txt # Install the dependencies | ||
| ### conda | ||
| Run the following commands to create a [conda environment](https://conda.io) with all dependencies installed: | ||
| conda create -n dateutil # Create a conda environment | ||
| # conda create -n dateutil python=3.6 # or specify a version | ||
| source activate dateutil # Activate the conda environment | ||
| pip install -r requirements.txt # Install the dependencies | ||
| ## Testing | ||
| dateutil has a comprehensive test suite, which can be run simply by running | ||
| `python -m pytest` in the project root. Note that if you don't have the internal | ||
| zoneinfo database, some tests will fail. Apart from that, all tests should pass. | ||
| To easily test dateutil against all supported Python versions, you can use | ||
| [tox](https://tox.readthedocs.io/en/latest/). | ||
| All GitHub pull requests are automatically tested using travis and appveyor. |
| # -*- coding: utf-8 -*- | ||
| from ._parser import parse, parser, parserinfo | ||
| from ._parser import DEFAULTPARSER, DEFAULTTZPARSER | ||
| from ._parser import UnknownTimezoneWarning | ||
| from ._parser import __doc__ | ||
| from .isoparser import isoparser, isoparse | ||
| __all__ = ['parse', 'parser', 'parserinfo', | ||
| 'isoparse', 'isoparser', | ||
| 'UnknownTimezoneWarning'] | ||
| ### | ||
| # Deprecate portions of the private interface so that downstream code that | ||
| # is improperly relying on it is given *some* notice. | ||
| def __deprecated_private_func(f): | ||
| from functools import wraps | ||
| import warnings | ||
| msg = ('{name} is a private function and may break without warning, ' | ||
| 'it will be moved and or renamed in future versions.') | ||
| msg = msg.format(name=f.__name__) | ||
| @wraps(f) | ||
| def deprecated_func(*args, **kwargs): | ||
| warnings.warn(msg, DeprecationWarning) | ||
| return f(*args, **kwargs) | ||
| return deprecated_func | ||
| def __deprecate_private_class(c): | ||
| import warnings | ||
| msg = ('{name} is a private class and may break without warning, ' | ||
| 'it will be moved and or renamed in future versions.') | ||
| msg = msg.format(name=c.__name__) | ||
| class private_class(c): | ||
| __doc__ = c.__doc__ | ||
| def __init__(self, *args, **kwargs): | ||
| warnings.warn(msg, DeprecationWarning) | ||
| super(private_class, self).__init__(*args, **kwargs) | ||
| private_class.__name__ = c.__name__ | ||
| return private_class | ||
| from ._parser import _timelex, _resultbase | ||
| from ._parser import _tzparser, _parsetz | ||
| _timelex = __deprecate_private_class(_timelex) | ||
| _tzparser = __deprecate_private_class(_tzparser) | ||
| _resultbase = __deprecate_private_class(_resultbase) | ||
| _parsetz = __deprecated_private_func(_parsetz) |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module offers a generic date/time string parser which is able to parse | ||
| most known formats to represent a date and/or time. | ||
| This module attempts to be forgiving with regards to unlikely input formats, | ||
| returning a datetime object even for dates which are ambiguous. If an element | ||
| of a date/time stamp is omitted, the following rules are applied: | ||
| - If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour | ||
| on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is | ||
| specified. | ||
| - If a time zone is omitted, a timezone-naive datetime is returned. | ||
| If any other elements are missing, they are taken from the | ||
| :class:`datetime.datetime` object passed to the parameter ``default``. If this | ||
| results in a day number exceeding the valid number of days per month, the | ||
| value falls back to the end of the month. | ||
| Additional resources about date/time string formats can be found below: | ||
| - `A summary of the international standard date and time notation | ||
| <http://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_ | ||
| - `W3C Date and Time Formats <http://www.w3.org/TR/NOTE-datetime>`_ | ||
| - `Time Formats (Planetary Rings Node) <https://pds-rings.seti.org:443/tools/time_formats.html>`_ | ||
| - `CPAN ParseDate module | ||
| <http://search.cpan.org/~muir/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_ | ||
| - `Java SimpleDateFormat Class | ||
| <https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_ | ||
| """ | ||
| from __future__ import unicode_literals | ||
| import datetime | ||
| import re | ||
| import string | ||
| import time | ||
| import warnings | ||
| from calendar import monthrange | ||
| from io import StringIO | ||
| import six | ||
| from six import binary_type, integer_types, text_type | ||
| from decimal import Decimal | ||
| from warnings import warn | ||
| from .. import relativedelta | ||
| from .. import tz | ||
| __all__ = ["parse", "parserinfo"] | ||
| # TODO: pandas.core.tools.datetimes imports this explicitly. Might be worth | ||
| # making public and/or figuring out if there is something we can | ||
| # take off their plate. | ||
| class _timelex(object): | ||
| # Fractional seconds are sometimes split by a comma | ||
| _split_decimal = re.compile("([.,])") | ||
| def __init__(self, instream): | ||
| if six.PY2: | ||
| # In Python 2, we can't duck type properly because unicode has | ||
| # a 'decode' function, and we'd be double-decoding | ||
| if isinstance(instream, (binary_type, bytearray)): | ||
| instream = instream.decode() | ||
| else: | ||
| if getattr(instream, 'decode', None) is not None: | ||
| instream = instream.decode() | ||
| if isinstance(instream, text_type): | ||
| instream = StringIO(instream) | ||
| elif getattr(instream, 'read', None) is None: | ||
| raise TypeError('Parser must be a string or character stream, not ' | ||
| '{itype}'.format(itype=instream.__class__.__name__)) | ||
| self.instream = instream | ||
| self.charstack = [] | ||
| self.tokenstack = [] | ||
| self.eof = False | ||
| def get_token(self): | ||
| """ | ||
| This function breaks the time string into lexical units (tokens), which | ||
| can be parsed by the parser. Lexical units are demarcated by changes in | ||
| the character set, so any continuous string of letters is considered | ||
| one unit, any continuous string of numbers is considered one unit. | ||
| The main complication arises from the fact that dots ('.') can be used | ||
| both as separators (e.g. "Sep.20.2009") or decimal points (e.g. | ||
| "4:30:21.447"). As such, it is necessary to read the full context of | ||
| any dot-separated strings before breaking it into tokens; as such, this | ||
| function maintains a "token stack", for when the ambiguous context | ||
| demands that multiple tokens be parsed at once. | ||
| """ | ||
| if self.tokenstack: | ||
| return self.tokenstack.pop(0) | ||
| seenletters = False | ||
| token = None | ||
| state = None | ||
| while not self.eof: | ||
| # We only realize that we've reached the end of a token when we | ||
| # find a character that's not part of the current token - since | ||
| # that character may be part of the next token, it's stored in the | ||
| # charstack. | ||
| if self.charstack: | ||
| nextchar = self.charstack.pop(0) | ||
| else: | ||
| nextchar = self.instream.read(1) | ||
| while nextchar == '\x00': | ||
| nextchar = self.instream.read(1) | ||
| if not nextchar: | ||
| self.eof = True | ||
| break | ||
| elif not state: | ||
| # First character of the token - determines if we're starting | ||
| # to parse a word, a number or something else. | ||
| token = nextchar | ||
| if self.isword(nextchar): | ||
| state = 'a' | ||
| elif self.isnum(nextchar): | ||
| state = '0' | ||
| elif self.isspace(nextchar): | ||
| token = ' ' | ||
| break # emit token | ||
| else: | ||
| break # emit token | ||
| elif state == 'a': | ||
| # If we've already started reading a word, we keep reading | ||
| # letters until we find something that's not part of a word. | ||
| seenletters = True | ||
| if self.isword(nextchar): | ||
| token += nextchar | ||
| elif nextchar == '.': | ||
| token += nextchar | ||
| state = 'a.' | ||
| else: | ||
| self.charstack.append(nextchar) | ||
| break # emit token | ||
| elif state == '0': | ||
| # If we've already started reading a number, we keep reading | ||
| # numbers until we find something that doesn't fit. | ||
| if self.isnum(nextchar): | ||
| token += nextchar | ||
| elif nextchar == '.' or (nextchar == ',' and len(token) >= 2): | ||
| token += nextchar | ||
| state = '0.' | ||
| else: | ||
| self.charstack.append(nextchar) | ||
| break # emit token | ||
| elif state == 'a.': | ||
| # If we've seen some letters and a dot separator, continue | ||
| # parsing, and the tokens will be broken up later. | ||
| seenletters = True | ||
| if nextchar == '.' or self.isword(nextchar): | ||
| token += nextchar | ||
| elif self.isnum(nextchar) and token[-1] == '.': | ||
| token += nextchar | ||
| state = '0.' | ||
| else: | ||
| self.charstack.append(nextchar) | ||
| break # emit token | ||
| elif state == '0.': | ||
| # If we've seen at least one dot separator, keep going, we'll | ||
| # break up the tokens later. | ||
| if nextchar == '.' or self.isnum(nextchar): | ||
| token += nextchar | ||
| elif self.isword(nextchar) and token[-1] == '.': | ||
| token += nextchar | ||
| state = 'a.' | ||
| else: | ||
| self.charstack.append(nextchar) | ||
| break # emit token | ||
| if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or | ||
| token[-1] in '.,')): | ||
| l = self._split_decimal.split(token) | ||
| token = l[0] | ||
| for tok in l[1:]: | ||
| if tok: | ||
| self.tokenstack.append(tok) | ||
| if state == '0.' and token.count('.') == 0: | ||
| token = token.replace(',', '.') | ||
| return token | ||
| def __iter__(self): | ||
| return self | ||
| def __next__(self): | ||
| token = self.get_token() | ||
| if token is None: | ||
| raise StopIteration | ||
| return token | ||
| def next(self): | ||
| return self.__next__() # Python 2.x support | ||
| @classmethod | ||
| def split(cls, s): | ||
| return list(cls(s)) | ||
| @classmethod | ||
| def isword(cls, nextchar): | ||
| """ Whether or not the next character is part of a word """ | ||
| return nextchar.isalpha() | ||
| @classmethod | ||
| def isnum(cls, nextchar): | ||
| """ Whether the next character is part of a number """ | ||
| return nextchar.isdigit() | ||
| @classmethod | ||
| def isspace(cls, nextchar): | ||
| """ Whether the next character is whitespace """ | ||
| return nextchar.isspace() | ||
| class _resultbase(object): | ||
| def __init__(self): | ||
| for attr in self.__slots__: | ||
| setattr(self, attr, None) | ||
| def _repr(self, classname): | ||
| l = [] | ||
| for attr in self.__slots__: | ||
| value = getattr(self, attr) | ||
| if value is not None: | ||
| l.append("%s=%s" % (attr, repr(value))) | ||
| return "%s(%s)" % (classname, ", ".join(l)) | ||
| def __len__(self): | ||
| return (sum(getattr(self, attr) is not None | ||
| for attr in self.__slots__)) | ||
| def __repr__(self): | ||
| return self._repr(self.__class__.__name__) | ||
| class parserinfo(object): | ||
| """ | ||
| Class which handles what inputs are accepted. Subclass this to customize | ||
| the language and acceptable values for each parameter. | ||
| :param dayfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the day (``True``) or month (``False``). If | ||
| ``yearfirst`` is set to ``True``, this distinguishes between YDM | ||
| and YMD. Default is ``False``. | ||
| :param yearfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the year. If ``True``, the first number is taken | ||
| to be the year, otherwise the last number is taken to be the year. | ||
| Default is ``False``. | ||
| """ | ||
| # m from a.m/p.m, t from ISO T separator | ||
| JUMP = [" ", ".", ",", ";", "-", "/", "'", | ||
| "at", "on", "and", "ad", "m", "t", "of", | ||
| "st", "nd", "rd", "th"] | ||
| WEEKDAYS = [("Mon", "Monday"), | ||
| ("Tue", "Tuesday"), # TODO: "Tues" | ||
| ("Wed", "Wednesday"), | ||
| ("Thu", "Thursday"), # TODO: "Thurs" | ||
| ("Fri", "Friday"), | ||
| ("Sat", "Saturday"), | ||
| ("Sun", "Sunday")] | ||
| MONTHS = [("Jan", "January"), | ||
| ("Feb", "February"), # TODO: "Febr" | ||
| ("Mar", "March"), | ||
| ("Apr", "April"), | ||
| ("May", "May"), | ||
| ("Jun", "June"), | ||
| ("Jul", "July"), | ||
| ("Aug", "August"), | ||
| ("Sep", "Sept", "September"), | ||
| ("Oct", "October"), | ||
| ("Nov", "November"), | ||
| ("Dec", "December")] | ||
| HMS = [("h", "hour", "hours"), | ||
| ("m", "minute", "minutes"), | ||
| ("s", "second", "seconds")] | ||
| AMPM = [("am", "a"), | ||
| ("pm", "p")] | ||
| UTCZONE = ["UTC", "GMT", "Z"] | ||
| PERTAIN = ["of"] | ||
| TZOFFSET = {} | ||
| # TODO: ERA = ["AD", "BC", "CE", "BCE", "Stardate", | ||
| # "Anno Domini", "Year of Our Lord"] | ||
| def __init__(self, dayfirst=False, yearfirst=False): | ||
| self._jump = self._convert(self.JUMP) | ||
| self._weekdays = self._convert(self.WEEKDAYS) | ||
| self._months = self._convert(self.MONTHS) | ||
| self._hms = self._convert(self.HMS) | ||
| self._ampm = self._convert(self.AMPM) | ||
| self._utczone = self._convert(self.UTCZONE) | ||
| self._pertain = self._convert(self.PERTAIN) | ||
| self.dayfirst = dayfirst | ||
| self.yearfirst = yearfirst | ||
| self._year = time.localtime().tm_year | ||
| self._century = self._year // 100 * 100 | ||
| def _convert(self, lst): | ||
| dct = {} | ||
| for i, v in enumerate(lst): | ||
| if isinstance(v, tuple): | ||
| for v in v: | ||
| dct[v.lower()] = i | ||
| else: | ||
| dct[v.lower()] = i | ||
| return dct | ||
| def jump(self, name): | ||
| return name.lower() in self._jump | ||
| def weekday(self, name): | ||
| try: | ||
| return self._weekdays[name.lower()] | ||
| except KeyError: | ||
| pass | ||
| return None | ||
| def month(self, name): | ||
| try: | ||
| return self._months[name.lower()] + 1 | ||
| except KeyError: | ||
| pass | ||
| return None | ||
| def hms(self, name): | ||
| try: | ||
| return self._hms[name.lower()] | ||
| except KeyError: | ||
| return None | ||
| def ampm(self, name): | ||
| try: | ||
| return self._ampm[name.lower()] | ||
| except KeyError: | ||
| return None | ||
| def pertain(self, name): | ||
| return name.lower() in self._pertain | ||
| def utczone(self, name): | ||
| return name.lower() in self._utczone | ||
| def tzoffset(self, name): | ||
| if name in self._utczone: | ||
| return 0 | ||
| return self.TZOFFSET.get(name) | ||
| def convertyear(self, year, century_specified=False): | ||
| if year < 100 and not century_specified: | ||
| year += self._century | ||
| if abs(year - self._year) >= 50: | ||
| if year < self._year: | ||
| year += 100 | ||
| else: | ||
| year -= 100 | ||
| return year | ||
| def validate(self, res): | ||
| # move to info | ||
| if res.year is not None: | ||
| res.year = self.convertyear(res.year, res.century_specified) | ||
| if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': | ||
| res.tzname = "UTC" | ||
| res.tzoffset = 0 | ||
| elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): | ||
| res.tzoffset = 0 | ||
| return True | ||
| class _ymd(list): | ||
| def __init__(self, *args, **kwargs): | ||
| super(self.__class__, self).__init__(*args, **kwargs) | ||
| self.century_specified = False | ||
| self.dstridx = None | ||
| self.mstridx = None | ||
| self.ystridx = None | ||
| @property | ||
| def has_year(self): | ||
| return self.ystridx is not None | ||
| @property | ||
| def has_month(self): | ||
| return self.mstridx is not None | ||
| @property | ||
| def has_day(self): | ||
| return self.dstridx is not None | ||
| def could_be_day(self, value): | ||
| if self.has_day: | ||
| return False | ||
| elif not self.has_month: | ||
| return 1 <= value <= 31 | ||
| elif not self.has_year: | ||
| # Be permissive, assume leapyear | ||
| month = self[self.mstridx] | ||
| return 1 <= value <= monthrange(2000, month)[1] | ||
| else: | ||
| month = self[self.mstridx] | ||
| year = self[self.ystridx] | ||
| return 1 <= value <= monthrange(year, month)[1] | ||
| def append(self, val, label=None): | ||
| if hasattr(val, '__len__'): | ||
| if val.isdigit() and len(val) > 2: | ||
| self.century_specified = True | ||
| if label not in [None, 'Y']: # pragma: no cover | ||
| raise ValueError(label) | ||
| label = 'Y' | ||
| elif val > 100: | ||
| self.century_specified = True | ||
| if label not in [None, 'Y']: # pragma: no cover | ||
| raise ValueError(label) | ||
| label = 'Y' | ||
| super(self.__class__, self).append(int(val)) | ||
| if label == 'M': | ||
| if self.has_month: | ||
| raise ValueError('Month is already set') | ||
| self.mstridx = len(self) - 1 | ||
| elif label == 'D': | ||
| if self.has_day: | ||
| raise ValueError('Day is already set') | ||
| self.dstridx = len(self) - 1 | ||
| elif label == 'Y': | ||
| if self.has_year: | ||
| raise ValueError('Year is already set') | ||
| self.ystridx = len(self) - 1 | ||
| def resolve_ymd(self, yearfirst, dayfirst): | ||
| len_ymd = len(self) | ||
| year, month, day = (None, None, None) | ||
| mstridx = self.mstridx | ||
| if len_ymd > 3: | ||
| raise ValueError("More than three YMD values") | ||
| elif len_ymd == 1 or (mstridx is not None and len_ymd == 2): | ||
| # One member, or two members with a month string | ||
| if mstridx is not None: | ||
| month = self[mstridx] | ||
| del self[mstridx] | ||
| if len_ymd > 1 or mstridx is None: | ||
| if self[0] > 31: | ||
| year = self[0] | ||
| else: | ||
| day = self[0] | ||
| elif len_ymd == 2: | ||
| # Two members with numbers | ||
| if self[0] > 31: | ||
| # 99-01 | ||
| year, month = self | ||
| elif self[1] > 31: | ||
| # 01-99 | ||
| month, year = self | ||
| elif dayfirst and self[1] <= 12: | ||
| # 13-01 | ||
| day, month = self | ||
| else: | ||
| # 01-13 | ||
| month, day = self | ||
| elif len_ymd == 3: | ||
| # Three members | ||
| if mstridx == 0: | ||
| if self[1] > 31: | ||
| # Apr-2003-25 | ||
| month, year, day = self | ||
| else: | ||
| month, day, year = self | ||
| elif mstridx == 1: | ||
| if self[0] > 31 or (yearfirst and self[2] <= 31): | ||
| # 99-Jan-01 | ||
| year, month, day = self | ||
| else: | ||
| # 01-Jan-01 | ||
| # Give precendence to day-first, since | ||
| # two-digit years is usually hand-written. | ||
| day, month, year = self | ||
| elif mstridx == 2: | ||
| # WTF!? | ||
| if self[1] > 31: | ||
| # 01-99-Jan | ||
| day, year, month = self | ||
| else: | ||
| # 99-01-Jan | ||
| year, day, month = self | ||
| else: | ||
| if (self[0] > 31 or | ||
| self.ystridx == 0 or | ||
| (yearfirst and self[1] <= 12 and self[2] <= 31)): | ||
| # 99-01-01 | ||
| if dayfirst and self[2] <= 12: | ||
| year, day, month = self | ||
| else: | ||
| year, month, day = self | ||
| elif self[0] > 12 or (dayfirst and self[1] <= 12): | ||
| # 13-01-01 | ||
| day, month, year = self | ||
| else: | ||
| # 01-13-01 | ||
| month, day, year = self | ||
| return year, month, day | ||
| class parser(object): | ||
| def __init__(self, info=None): | ||
| self.info = info or parserinfo() | ||
| def parse(self, timestr, default=None, | ||
| ignoretz=False, tzinfos=None, **kwargs): | ||
| """ | ||
| Parse the date/time string into a :class:`datetime.datetime` object. | ||
| :param timestr: | ||
| Any date/time string using the supported formats. | ||
| :param default: | ||
| The default datetime object, if this is a datetime object and not | ||
| ``None``, elements specified in ``timestr`` replace elements in the | ||
| default object. | ||
| :param ignoretz: | ||
| If set ``True``, time zones in parsed strings are ignored and a | ||
| naive :class:`datetime.datetime` object is returned. | ||
| :param tzinfos: | ||
| Additional time zone names / aliases which may be present in the | ||
| string. This argument maps time zone names (and optionally offsets | ||
| from those time zones) to time zones. This parameter can be a | ||
| dictionary with timezone aliases mapping time zone names to time | ||
| zones or a function taking two parameters (``tzname`` and | ||
| ``tzoffset``) and returning a time zone. | ||
| The timezones to which the names are mapped can be an integer | ||
| offset from UTC in seconds or a :class:`tzinfo` object. | ||
| .. doctest:: | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> from dateutil.parser import parse | ||
| >>> from dateutil.tz import gettz | ||
| >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} | ||
| >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) | ||
| datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) | ||
| >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) | ||
| datetime.datetime(2012, 1, 19, 17, 21, | ||
| tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) | ||
| This parameter is ignored if ``ignoretz`` is set. | ||
| :param \\*\\*kwargs: | ||
| Keyword arguments as passed to ``_parse()``. | ||
| :return: | ||
| Returns a :class:`datetime.datetime` object or, if the | ||
| ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the | ||
| first element being a :class:`datetime.datetime` object, the second | ||
| a tuple containing the fuzzy tokens. | ||
| :raises ValueError: | ||
| Raised for invalid or unknown string format, if the provided | ||
| :class:`tzinfo` is not in a valid format, or if an invalid date | ||
| would be created. | ||
| :raises TypeError: | ||
| Raised for non-string or character stream input. | ||
| :raises OverflowError: | ||
| Raised if the parsed date exceeds the largest valid C integer on | ||
| your system. | ||
| """ | ||
| if default is None: | ||
| default = datetime.datetime.now().replace(hour=0, minute=0, | ||
| second=0, microsecond=0) | ||
| res, skipped_tokens = self._parse(timestr, **kwargs) | ||
| if res is None: | ||
| raise ValueError("Unknown string format:", timestr) | ||
| if len(res) == 0: | ||
| raise ValueError("String does not contain a date:", timestr) | ||
| ret = self._build_naive(res, default) | ||
| if not ignoretz: | ||
| ret = self._build_tzaware(ret, res, tzinfos) | ||
| if kwargs.get('fuzzy_with_tokens', False): | ||
| return ret, skipped_tokens | ||
| else: | ||
| return ret | ||
| class _result(_resultbase): | ||
| __slots__ = ["year", "month", "day", "weekday", | ||
| "hour", "minute", "second", "microsecond", | ||
| "tzname", "tzoffset", "ampm","any_unused_tokens"] | ||
| def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, | ||
| fuzzy_with_tokens=False): | ||
| """ | ||
| Private method which performs the heavy lifting of parsing, called from | ||
| ``parse()``, which passes on its ``kwargs`` to this function. | ||
| :param timestr: | ||
| The string to parse. | ||
| :param dayfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the day (``True``) or month (``False``). If | ||
| ``yearfirst`` is set to ``True``, this distinguishes between YDM | ||
| and YMD. If set to ``None``, this value is retrieved from the | ||
| current :class:`parserinfo` object (which itself defaults to | ||
| ``False``). | ||
| :param yearfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the year. If ``True``, the first number is taken | ||
| to be the year, otherwise the last number is taken to be the year. | ||
| If this is set to ``None``, the value is retrieved from the current | ||
| :class:`parserinfo` object (which itself defaults to ``False``). | ||
| :param fuzzy: | ||
| Whether to allow fuzzy parsing, allowing for string like "Today is | ||
| January 1, 2047 at 8:21:00AM". | ||
| :param fuzzy_with_tokens: | ||
| If ``True``, ``fuzzy`` is automatically set to True, and the parser | ||
| will return a tuple where the first element is the parsed | ||
| :class:`datetime.datetime` datetimestamp and the second element is | ||
| a tuple containing the portions of the string which were ignored: | ||
| .. doctest:: | ||
| >>> from dateutil.parser import parse | ||
| >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) | ||
| (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) | ||
| """ | ||
| if fuzzy_with_tokens: | ||
| fuzzy = True | ||
| info = self.info | ||
| if dayfirst is None: | ||
| dayfirst = info.dayfirst | ||
| if yearfirst is None: | ||
| yearfirst = info.yearfirst | ||
| res = self._result() | ||
| l = _timelex.split(timestr) # Splits the timestr into tokens | ||
| skipped_idxs = [] | ||
| # year/month/day list | ||
| ymd = _ymd() | ||
| len_l = len(l) | ||
| i = 0 | ||
| try: | ||
| while i < len_l: | ||
| # Check if it's a number | ||
| value_repr = l[i] | ||
| try: | ||
| value = float(value_repr) | ||
| except ValueError: | ||
| value = None | ||
| if value is not None: | ||
| # Numeric token | ||
| i = self._parse_numeric_token(l, i, info, ymd, res, fuzzy) | ||
| # Check weekday | ||
| elif info.weekday(l[i]) is not None: | ||
| value = info.weekday(l[i]) | ||
| res.weekday = value | ||
| # Check month name | ||
| elif info.month(l[i]) is not None: | ||
| value = info.month(l[i]) | ||
| ymd.append(value, 'M') | ||
| if i + 1 < len_l: | ||
| if l[i + 1] in ('-', '/'): | ||
| # Jan-01[-99] | ||
| sep = l[i + 1] | ||
| ymd.append(l[i + 2]) | ||
| if i + 3 < len_l and l[i + 3] == sep: | ||
| # Jan-01-99 | ||
| ymd.append(l[i + 4]) | ||
| i += 2 | ||
| i += 2 | ||
| elif (i + 4 < len_l and l[i + 1] == l[i + 3] == ' ' and | ||
| info.pertain(l[i + 2])): | ||
| # Jan of 01 | ||
| # In this case, 01 is clearly year | ||
| if l[i + 4].isdigit(): | ||
| # Convert it here to become unambiguous | ||
| value = int(l[i + 4]) | ||
| year = str(info.convertyear(value)) | ||
| ymd.append(year, 'Y') | ||
| else: | ||
| # Wrong guess | ||
| pass | ||
| # TODO: not hit in tests | ||
| i += 4 | ||
| # Check am/pm | ||
| elif info.ampm(l[i]) is not None: | ||
| value = info.ampm(l[i]) | ||
| val_is_ampm = self._ampm_valid(res.hour, res.ampm, fuzzy) | ||
| if val_is_ampm: | ||
| res.hour = self._adjust_ampm(res.hour, value) | ||
| res.ampm = value | ||
| elif fuzzy: | ||
| skipped_idxs.append(i) | ||
| # Check for a timezone name | ||
| elif self._could_be_tzname(res.hour, res.tzname, res.tzoffset, l[i]): | ||
| res.tzname = l[i] | ||
| res.tzoffset = info.tzoffset(res.tzname) | ||
| # Check for something like GMT+3, or BRST+3. Notice | ||
| # that it doesn't mean "I am 3 hours after GMT", but | ||
| # "my time +3 is GMT". If found, we reverse the | ||
| # logic so that timezone parsing code will get it | ||
| # right. | ||
| if i + 1 < len_l and l[i + 1] in ('+', '-'): | ||
| l[i + 1] = ('+', '-')[l[i + 1] == '+'] | ||
| res.tzoffset = None | ||
| if info.utczone(res.tzname): | ||
| # With something like GMT+3, the timezone | ||
| # is *not* GMT. | ||
| res.tzname = None | ||
| # Check for a numbered timezone | ||
| elif res.hour is not None and l[i] in ('+', '-'): | ||
| signal = (-1, 1)[l[i] == '+'] | ||
| len_li = len(l[i + 1]) | ||
| # TODO: check that l[i + 1] is integer? | ||
| if len_li == 4: | ||
| # -0300 | ||
| hour_offset = int(l[i + 1][:2]) | ||
| min_offset = int(l[i + 1][2:]) | ||
| elif i + 2 < len_l and l[i + 2] == ':': | ||
| # -03:00 | ||
| hour_offset = int(l[i + 1]) | ||
| min_offset = int(l[i + 3]) # TODO: Check that l[i+3] is minute-like? | ||
| i += 2 | ||
| elif len_li <= 2: | ||
| # -[0]3 | ||
| hour_offset = int(l[i + 1][:2]) | ||
| min_offset = 0 | ||
| else: | ||
| raise ValueError(timestr) | ||
| res.tzoffset = signal * (hour_offset * 3600 + min_offset * 60) | ||
| # Look for a timezone name between parenthesis | ||
| if (i + 5 < len_l and | ||
| info.jump(l[i + 2]) and l[i + 3] == '(' and | ||
| l[i + 5] == ')' and | ||
| 3 <= len(l[i + 4]) and | ||
| self._could_be_tzname(res.hour, res.tzname, | ||
| None, l[i + 4])): | ||
| # -0300 (BRST) | ||
| res.tzname = l[i + 4] | ||
| i += 4 | ||
| i += 1 | ||
| # Check jumps | ||
| elif not (info.jump(l[i]) or fuzzy): | ||
| raise ValueError(timestr) | ||
| else: | ||
| skipped_idxs.append(i) | ||
| i += 1 | ||
| # Process year/month/day | ||
| year, month, day = ymd.resolve_ymd(yearfirst, dayfirst) | ||
| res.century_specified = ymd.century_specified | ||
| res.year = year | ||
| res.month = month | ||
| res.day = day | ||
| except (IndexError, ValueError): | ||
| return None, None | ||
| if not info.validate(res): | ||
| return None, None | ||
| if fuzzy_with_tokens: | ||
| skipped_tokens = self._recombine_skipped(l, skipped_idxs) | ||
| return res, tuple(skipped_tokens) | ||
| else: | ||
| return res, None | ||
| def _parse_numeric_token(self, tokens, idx, info, ymd, res, fuzzy): | ||
| # Token is a number | ||
| value_repr = tokens[idx] | ||
| value = Decimal(value_repr) | ||
| len_li = len(value_repr) | ||
| len_l = len(tokens) | ||
| if (len(ymd) == 3 and len_li in (2, 4) and | ||
| res.hour is None and | ||
| (idx + 1 >= len_l or | ||
| (tokens[idx + 1] != ':' and | ||
| info.hms(tokens[idx + 1]) is None))): | ||
| # 19990101T23[59] | ||
| s = tokens[idx] | ||
| res.hour = int(s[:2]) | ||
| if len_li == 4: | ||
| res.minute = int(s[2:]) | ||
| elif len_li == 6 or (len_li > 6 and tokens[idx].find('.') == 6): | ||
| # YYMMDD or HHMMSS[.ss] | ||
| s = tokens[idx] | ||
| if not ymd and '.' not in tokens[idx]: | ||
| ymd.append(s[:2]) | ||
| ymd.append(s[2:4]) | ||
| ymd.append(s[4:]) | ||
| else: | ||
| # 19990101T235959[.59] | ||
| # TODO: Check if res attributes already set. | ||
| res.hour = int(s[:2]) | ||
| res.minute = int(s[2:4]) | ||
| res.second, res.microsecond = self._parsems(s[4:]) | ||
| elif len_li in (8, 12, 14): | ||
| # YYYYMMDD | ||
| s = tokens[idx] | ||
| ymd.append(s[:4], 'Y') | ||
| ymd.append(s[4:6]) | ||
| ymd.append(s[6:8]) | ||
| if len_li > 8: | ||
| res.hour = int(s[8:10]) | ||
| res.minute = int(s[10:12]) | ||
| if len_li > 12: | ||
| res.second = int(s[12:]) | ||
| elif self._find_hms_idx(idx, tokens, info, allow_jump=True) is not None: | ||
| # HH[ ]h or MM[ ]m or SS[.ss][ ]s | ||
| hms_idx = self._find_hms_idx(idx, tokens, info, allow_jump=True) | ||
| (idx, hms) = self._parse_hms(idx, tokens, info, hms_idx) | ||
| if hms is not None: | ||
| # TODO: checking that hour/minute/second are not | ||
| # already set? | ||
| self._assign_hms(res, value_repr, hms) | ||
| elif idx + 2 < len_l and tokens[idx + 1] == ':': | ||
| # HH:MM[:SS[.ss]] | ||
| res.hour = int(value) | ||
| value = Decimal(tokens[idx + 2]) # TODO: try/except for this? | ||
| (res.minute, res.second) = self._parse_min_sec(value) | ||
| if idx + 4 < len_l and tokens[idx + 3] == ':': | ||
| res.second, res.microsecond = self._parsems(tokens[idx + 4]) | ||
| idx += 2 | ||
| idx += 2 | ||
| elif idx + 1 < len_l and tokens[idx + 1] in ('-', '/', '.'): | ||
| sep = tokens[idx + 1] | ||
| ymd.append(value_repr) | ||
| if idx + 2 < len_l and not info.jump(tokens[idx + 2]): | ||
| if tokens[idx + 2].isdigit(): | ||
| # 01-01[-01] | ||
| ymd.append(tokens[idx + 2]) | ||
| else: | ||
| # 01-Jan[-01] | ||
| value = info.month(tokens[idx + 2]) | ||
| if value is not None: | ||
| ymd.append(value, 'M') | ||
| else: | ||
| raise ValueError() | ||
| if idx + 3 < len_l and tokens[idx + 3] == sep: | ||
| # We have three members | ||
| value = info.month(tokens[idx + 4]) | ||
| if value is not None: | ||
| ymd.append(value, 'M') | ||
| else: | ||
| ymd.append(tokens[idx + 4]) | ||
| idx += 2 | ||
| idx += 1 | ||
| idx += 1 | ||
| elif idx + 1 >= len_l or info.jump(tokens[idx + 1]): | ||
| if idx + 2 < len_l and info.ampm(tokens[idx + 2]) is not None: | ||
| # 12 am | ||
| hour = int(value) | ||
| res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 2])) | ||
| idx += 1 | ||
| else: | ||
| # Year, month or day | ||
| ymd.append(value) | ||
| idx += 1 | ||
| elif info.ampm(tokens[idx + 1]) is not None and (0 <= value < 24): | ||
| # 12am | ||
| hour = int(value) | ||
| res.hour = self._adjust_ampm(hour, info.ampm(tokens[idx + 1])) | ||
| idx += 1 | ||
| elif ymd.could_be_day(value): | ||
| ymd.append(value) | ||
| elif not fuzzy: | ||
| raise ValueError() | ||
| return idx | ||
| def _find_hms_idx(self, idx, tokens, info, allow_jump): | ||
| len_l = len(tokens) | ||
| if idx+1 < len_l and info.hms(tokens[idx+1]) is not None: | ||
| # There is an "h", "m", or "s" label following this token. We take | ||
| # assign the upcoming label to the current token. | ||
| # e.g. the "12" in 12h" | ||
| hms_idx = idx + 1 | ||
| elif (allow_jump and idx+2 < len_l and tokens[idx+1] == ' ' and | ||
| info.hms(tokens[idx+2]) is not None): | ||
| # There is a space and then an "h", "m", or "s" label. | ||
| # e.g. the "12" in "12 h" | ||
| hms_idx = idx + 2 | ||
| elif idx > 0 and info.hms(tokens[idx-1]) is not None: | ||
| # There is a "h", "m", or "s" preceeding this token. Since neither | ||
| # of the previous cases was hit, there is no label following this | ||
| # token, so we use the previous label. | ||
| # e.g. the "04" in "12h04" | ||
| hms_idx = idx-1 | ||
| elif (1 < idx == len_l-1 and tokens[idx-1] == ' ' and | ||
| info.hms(tokens[idx-2]) is not None): | ||
| # If we are looking at the final token, we allow for a | ||
| # backward-looking check to skip over a space. | ||
| # TODO: Are we sure this is the right condition here? | ||
| hms_idx = idx - 2 | ||
| else: | ||
| hms_idx = None | ||
| return hms_idx | ||
| def _assign_hms(self, res, value_repr, hms): | ||
| # See GH issue #427, fixing float rounding | ||
| value = Decimal(value_repr) | ||
| if hms == 0: | ||
| # Hour | ||
| res.hour = int(value) | ||
| if value % 1: | ||
| res.minute = int(60*(value % 1)) | ||
| elif hms == 1: | ||
| (res.minute, res.second) = self._parse_min_sec(value) | ||
| elif hms == 2: | ||
| (res.second, res.microsecond) = self._parsems(value_repr) | ||
| def _could_be_tzname(self, hour, tzname, tzoffset, token): | ||
| return (hour is not None and | ||
| tzname is None and | ||
| tzoffset is None and | ||
| len(token) <= 5 and | ||
| all(x in string.ascii_uppercase for x in token)) | ||
| def _ampm_valid(self, hour, ampm, fuzzy): | ||
| """ | ||
| For fuzzy parsing, 'a' or 'am' (both valid English words) | ||
| may erroneously trigger the AM/PM flag. Deal with that | ||
| here. | ||
| """ | ||
| val_is_ampm = True | ||
| # If there's already an AM/PM flag, this one isn't one. | ||
| if fuzzy and ampm is not None: | ||
| val_is_ampm = False | ||
| # If AM/PM is found and hour is not, raise a ValueError | ||
| if hour is None: | ||
| if fuzzy: | ||
| val_is_ampm = False | ||
| else: | ||
| raise ValueError('No hour specified with AM or PM flag.') | ||
| elif not 0 <= hour <= 12: | ||
| # If AM/PM is found, it's a 12 hour clock, so raise | ||
| # an error for invalid range | ||
| if fuzzy: | ||
| val_is_ampm = False | ||
| else: | ||
| raise ValueError('Invalid hour specified for 12-hour clock.') | ||
| return val_is_ampm | ||
| def _adjust_ampm(self, hour, ampm): | ||
| if hour < 12 and ampm == 1: | ||
| hour += 12 | ||
| elif hour == 12 and ampm == 0: | ||
| hour = 0 | ||
| return hour | ||
| def _parse_min_sec(self, value): | ||
| # TODO: Every usage of this function sets res.second to the return | ||
| # value. Are there any cases where second will be returned as None and | ||
| # we *dont* want to set res.second = None? | ||
| minute = int(value) | ||
| second = None | ||
| sec_remainder = value % 1 | ||
| if sec_remainder: | ||
| second = int(60 * sec_remainder) | ||
| return (minute, second) | ||
| def _parsems(self, value): | ||
| """Parse a I[.F] seconds value into (seconds, microseconds).""" | ||
| if "." not in value: | ||
| return int(value), 0 | ||
| else: | ||
| i, f = value.split(".") | ||
| return int(i), int(f.ljust(6, "0")[:6]) | ||
| def _parse_hms(self, idx, tokens, info, hms_idx): | ||
| # TODO: Is this going to admit a lot of false-positives for when we | ||
| # just happen to have digits and "h", "m" or "s" characters in non-date | ||
| # text? I guess hex hashes won't have that problem, but there's plenty | ||
| # of random junk out there. | ||
| if hms_idx is None: | ||
| hms = None | ||
| new_idx = idx | ||
| elif hms_idx > idx: | ||
| hms = info.hms(tokens[hms_idx]) | ||
| new_idx = hms_idx | ||
| else: | ||
| # Looking backwards, increment one. | ||
| hms = info.hms(tokens[hms_idx]) + 1 | ||
| new_idx = idx | ||
| return (new_idx, hms) | ||
| def _recombine_skipped(self, tokens, skipped_idxs): | ||
| """ | ||
| >>> tokens = ["foo", " ", "bar", " ", "19June2000", "baz"] | ||
| >>> skipped_idxs = [0, 1, 2, 5] | ||
| >>> _recombine_skipped(tokens, skipped_idxs) | ||
| ["foo bar", "baz"] | ||
| """ | ||
| skipped_tokens = [] | ||
| for i, idx in enumerate(sorted(skipped_idxs)): | ||
| if i > 0 and idx - 1 == skipped_idxs[i - 1]: | ||
| skipped_tokens[-1] = skipped_tokens[-1] + tokens[idx] | ||
| else: | ||
| skipped_tokens.append(tokens[idx]) | ||
| return skipped_tokens | ||
| def _build_tzinfo(self, tzinfos, tzname, tzoffset): | ||
| if callable(tzinfos): | ||
| tzdata = tzinfos(tzname, tzoffset) | ||
| else: | ||
| tzdata = tzinfos.get(tzname) | ||
| if isinstance(tzdata, datetime.tzinfo): | ||
| tzinfo = tzdata | ||
| elif isinstance(tzdata, text_type): | ||
| tzinfo = tz.tzstr(tzdata) | ||
| elif isinstance(tzdata, integer_types): | ||
| tzinfo = tz.tzoffset(tzname, tzdata) | ||
| else: | ||
| raise ValueError("Offset must be tzinfo subclass, " | ||
| "tz string, or int offset.") | ||
| return tzinfo | ||
| def _build_tzaware(self, naive, res, tzinfos): | ||
| if (callable(tzinfos) or (tzinfos and res.tzname in tzinfos)): | ||
| tzinfo = self._build_tzinfo(tzinfos, res.tzname, res.tzoffset) | ||
| aware = naive.replace(tzinfo=tzinfo) | ||
| aware = self._assign_tzname(aware, res.tzname) | ||
| elif res.tzname and res.tzname in time.tzname: | ||
| aware = naive.replace(tzinfo=tz.tzlocal()) | ||
| # Handle ambiguous local datetime | ||
| aware = self._assign_tzname(aware, res.tzname) | ||
| # This is mostly relevant for winter GMT zones parsed in the UK | ||
| if (aware.tzname() != res.tzname and | ||
| res.tzname in self.info.UTCZONE): | ||
| aware = aware.replace(tzinfo=tz.tzutc()) | ||
| elif res.tzoffset == 0: | ||
| aware = naive.replace(tzinfo=tz.tzutc()) | ||
| elif res.tzoffset: | ||
| aware = naive.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) | ||
| elif not res.tzname and not res.tzoffset: | ||
| # i.e. no timezone information was found. | ||
| aware = naive | ||
| elif res.tzname: | ||
| # tz-like string was parsed but we don't know what to do | ||
| # with it | ||
| warnings.warn("tzname {tzname} identified but not understood. " | ||
| "Pass `tzinfos` argument in order to correctly " | ||
| "return a timezone-aware datetime. In a future " | ||
| "version, this raise an " | ||
| "exception.".format(tzname=res.tzname), | ||
| category=UnknownTimezoneWarning) | ||
| aware = naive | ||
| return aware | ||
| def _build_naive(self, res, default): | ||
| repl = {} | ||
| for attr in ("year", "month", "day", "hour", | ||
| "minute", "second", "microsecond"): | ||
| value = getattr(res, attr) | ||
| if value is not None: | ||
| repl[attr] = value | ||
| if 'day' not in repl: | ||
| # If the default day exceeds the last day of the month, fall back | ||
| # to the end of the month. | ||
| cyear = default.year if res.year is None else res.year | ||
| cmonth = default.month if res.month is None else res.month | ||
| cday = default.day if res.day is None else res.day | ||
| if cday > monthrange(cyear, cmonth)[1]: | ||
| repl['day'] = monthrange(cyear, cmonth)[1] | ||
| naive = default.replace(**repl) | ||
| if res.weekday is not None and not res.day: | ||
| naive = naive + relativedelta.relativedelta(weekday=res.weekday) | ||
| return naive | ||
| def _assign_tzname(self, dt, tzname): | ||
| if dt.tzname() != tzname: | ||
| new_dt = tz.enfold(dt, fold=1) | ||
| if new_dt.tzname() == tzname: | ||
| return new_dt | ||
| return dt | ||
| DEFAULTPARSER = parser() | ||
| def parse(timestr, parserinfo=None, **kwargs): | ||
| """ | ||
| Parse a string in one of the supported formats, using the | ||
| ``parserinfo`` parameters. | ||
| :param timestr: | ||
| A string containing a date/time stamp. | ||
| :param parserinfo: | ||
| A :class:`parserinfo` object containing parameters for the parser. | ||
| If ``None``, the default arguments to the :class:`parserinfo` | ||
| constructor are used. | ||
| The ``**kwargs`` parameter takes the following keyword arguments: | ||
| :param default: | ||
| The default datetime object, if this is a datetime object and not | ||
| ``None``, elements specified in ``timestr`` replace elements in the | ||
| default object. | ||
| :param ignoretz: | ||
| If set ``True``, time zones in parsed strings are ignored and a naive | ||
| :class:`datetime` object is returned. | ||
| :param tzinfos: | ||
| Additional time zone names / aliases which may be present in the | ||
| string. This argument maps time zone names (and optionally offsets | ||
| from those time zones) to time zones. This parameter can be a | ||
| dictionary with timezone aliases mapping time zone names to time | ||
| zones or a function taking two parameters (``tzname`` and | ||
| ``tzoffset``) and returning a time zone. | ||
| The timezones to which the names are mapped can be an integer | ||
| offset from UTC in seconds or a :class:`tzinfo` object. | ||
| .. doctest:: | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> from dateutil.parser import parse | ||
| >>> from dateutil.tz import gettz | ||
| >>> tzinfos = {"BRST": -7200, "CST": gettz("America/Chicago")} | ||
| >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) | ||
| datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -7200)) | ||
| >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) | ||
| datetime.datetime(2012, 1, 19, 17, 21, | ||
| tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) | ||
| This parameter is ignored if ``ignoretz`` is set. | ||
| :param dayfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the day (``True``) or month (``False``). If | ||
| ``yearfirst`` is set to ``True``, this distinguishes between YDM and | ||
| YMD. If set to ``None``, this value is retrieved from the current | ||
| :class:`parserinfo` object (which itself defaults to ``False``). | ||
| :param yearfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the year. If ``True``, the first number is taken to | ||
| be the year, otherwise the last number is taken to be the year. If | ||
| this is set to ``None``, the value is retrieved from the current | ||
| :class:`parserinfo` object (which itself defaults to ``False``). | ||
| :param fuzzy: | ||
| Whether to allow fuzzy parsing, allowing for string like "Today is | ||
| January 1, 2047 at 8:21:00AM". | ||
| :param fuzzy_with_tokens: | ||
| If ``True``, ``fuzzy`` is automatically set to True, and the parser | ||
| will return a tuple where the first element is the parsed | ||
| :class:`datetime.datetime` datetimestamp and the second element is | ||
| a tuple containing the portions of the string which were ignored: | ||
| .. doctest:: | ||
| >>> from dateutil.parser import parse | ||
| >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) | ||
| (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) | ||
| :return: | ||
| Returns a :class:`datetime.datetime` object or, if the | ||
| ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the | ||
| first element being a :class:`datetime.datetime` object, the second | ||
| a tuple containing the fuzzy tokens. | ||
| :raises ValueError: | ||
| Raised for invalid or unknown string format, if the provided | ||
| :class:`tzinfo` is not in a valid format, or if an invalid date | ||
| would be created. | ||
| :raises OverflowError: | ||
| Raised if the parsed date exceeds the largest valid C integer on | ||
| your system. | ||
| """ | ||
| if parserinfo: | ||
| return parser(parserinfo).parse(timestr, **kwargs) | ||
| else: | ||
| return DEFAULTPARSER.parse(timestr, **kwargs) | ||
| class _tzparser(object): | ||
| class _result(_resultbase): | ||
| __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", | ||
| "start", "end"] | ||
| class _attr(_resultbase): | ||
| __slots__ = ["month", "week", "weekday", | ||
| "yday", "jyday", "day", "time"] | ||
| def __repr__(self): | ||
| return self._repr("") | ||
| def __init__(self): | ||
| _resultbase.__init__(self) | ||
| self.start = self._attr() | ||
| self.end = self._attr() | ||
| def parse(self, tzstr): | ||
| res = self._result() | ||
| l = [x for x in re.split(r'([,:.]|[a-zA-Z]+|[0-9]+)',tzstr) if x] | ||
| used_idxs = list() | ||
| try: | ||
| len_l = len(l) | ||
| i = 0 | ||
| while i < len_l: | ||
| # BRST+3[BRDT[+2]] | ||
| j = i | ||
| while j < len_l and not [x for x in l[j] | ||
| if x in "0123456789:,-+"]: | ||
| j += 1 | ||
| if j != i: | ||
| if not res.stdabbr: | ||
| offattr = "stdoffset" | ||
| res.stdabbr = "".join(l[i:j]) | ||
| else: | ||
| offattr = "dstoffset" | ||
| res.dstabbr = "".join(l[i:j]) | ||
| for ii in range(j): | ||
| used_idxs.append(ii) | ||
| i = j | ||
| if (i < len_l and (l[i] in ('+', '-') or l[i][0] in | ||
| "0123456789")): | ||
| if l[i] in ('+', '-'): | ||
| # Yes, that's right. See the TZ variable | ||
| # documentation. | ||
| signal = (1, -1)[l[i] == '+'] | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| else: | ||
| signal = -1 | ||
| len_li = len(l[i]) | ||
| if len_li == 4: | ||
| # -0300 | ||
| setattr(res, offattr, (int(l[i][:2]) * 3600 + | ||
| int(l[i][2:]) * 60) * signal) | ||
| elif i + 1 < len_l and l[i + 1] == ':': | ||
| # -03:00 | ||
| setattr(res, offattr, | ||
| (int(l[i]) * 3600 + | ||
| int(l[i + 2]) * 60) * signal) | ||
| used_idxs.append(i) | ||
| i += 2 | ||
| elif len_li <= 2: | ||
| # -[0]3 | ||
| setattr(res, offattr, | ||
| int(l[i][:2]) * 3600 * signal) | ||
| else: | ||
| return None | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| if res.dstabbr: | ||
| break | ||
| else: | ||
| break | ||
| if i < len_l: | ||
| for j in range(i, len_l): | ||
| if l[j] == ';': | ||
| l[j] = ',' | ||
| assert l[i] == ',' | ||
| i += 1 | ||
| if i >= len_l: | ||
| pass | ||
| elif (8 <= l.count(',') <= 9 and | ||
| not [y for x in l[i:] if x != ',' | ||
| for y in x if y not in "0123456789+-"]): | ||
| # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] | ||
| for x in (res.start, res.end): | ||
| x.month = int(l[i]) | ||
| used_idxs.append(i) | ||
| i += 2 | ||
| if l[i] == '-': | ||
| value = int(l[i + 1]) * -1 | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| else: | ||
| value = int(l[i]) | ||
| used_idxs.append(i) | ||
| i += 2 | ||
| if value: | ||
| x.week = value | ||
| x.weekday = (int(l[i]) - 1) % 7 | ||
| else: | ||
| x.day = int(l[i]) | ||
| used_idxs.append(i) | ||
| i += 2 | ||
| x.time = int(l[i]) | ||
| used_idxs.append(i) | ||
| i += 2 | ||
| if i < len_l: | ||
| if l[i] in ('-', '+'): | ||
| signal = (-1, 1)[l[i] == "+"] | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| else: | ||
| signal = 1 | ||
| used_idxs.append(i) | ||
| res.dstoffset = (res.stdoffset + int(l[i]) * signal) | ||
| # This was a made-up format that is not in normal use | ||
| warn(('Parsed time zone "%s"' % tzstr) + | ||
| 'is in a non-standard dateutil-specific format, which ' + | ||
| 'is now deprecated; support for parsing this format ' + | ||
| 'will be removed in future versions. It is recommended ' + | ||
| 'that you switch to a standard format like the GNU ' + | ||
| 'TZ variable format.', tz.DeprecatedTzFormatWarning) | ||
| elif (l.count(',') == 2 and l[i:].count('/') <= 2 and | ||
| not [y for x in l[i:] if x not in (',', '/', 'J', 'M', | ||
| '.', '-', ':') | ||
| for y in x if y not in "0123456789"]): | ||
| for x in (res.start, res.end): | ||
| if l[i] == 'J': | ||
| # non-leap year day (1 based) | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| x.jyday = int(l[i]) | ||
| elif l[i] == 'M': | ||
| # month[-.]week[-.]weekday | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| x.month = int(l[i]) | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| assert l[i] in ('-', '.') | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| x.week = int(l[i]) | ||
| if x.week == 5: | ||
| x.week = -1 | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| assert l[i] in ('-', '.') | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| x.weekday = (int(l[i]) - 1) % 7 | ||
| else: | ||
| # year day (zero based) | ||
| x.yday = int(l[i]) + 1 | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| if i < len_l and l[i] == '/': | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| # start time | ||
| len_li = len(l[i]) | ||
| if len_li == 4: | ||
| # -0300 | ||
| x.time = (int(l[i][:2]) * 3600 + | ||
| int(l[i][2:]) * 60) | ||
| elif i + 1 < len_l and l[i + 1] == ':': | ||
| # -03:00 | ||
| x.time = int(l[i]) * 3600 + int(l[i + 2]) * 60 | ||
| used_idxs.append(i) | ||
| i += 2 | ||
| if i + 1 < len_l and l[i + 1] == ':': | ||
| used_idxs.append(i) | ||
| i += 2 | ||
| x.time += int(l[i]) | ||
| elif len_li <= 2: | ||
| # -[0]3 | ||
| x.time = (int(l[i][:2]) * 3600) | ||
| else: | ||
| return None | ||
| used_idxs.append(i) | ||
| i += 1 | ||
| assert i == len_l or l[i] == ',' | ||
| i += 1 | ||
| assert i >= len_l | ||
| except (IndexError, ValueError, AssertionError): | ||
| return None | ||
| unused_idxs = set(range(len_l)).difference(used_idxs) | ||
| res.any_unused_tokens = not {l[n] for n in unused_idxs}.issubset({",",":"}) | ||
| return res | ||
| DEFAULTTZPARSER = _tzparser() | ||
| def _parsetz(tzstr): | ||
| return DEFAULTTZPARSER.parse(tzstr) | ||
| class UnknownTimezoneWarning(RuntimeWarning): | ||
| """Raised when the parser finds a timezone it cannot parse into a tzinfo""" | ||
| # vim:ts=4:sw=4:et |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module offers a parser for ISO-8601 strings | ||
| It is intended to support all valid date, time and datetime formats per the | ||
| ISO-8601 specification. | ||
| """ | ||
| from datetime import datetime, timedelta, time, date | ||
| import calendar | ||
| from dateutil import tz | ||
| from functools import wraps | ||
| import re | ||
| import six | ||
| __all__ = ["isoparse", "isoparser"] | ||
| def _takes_ascii(f): | ||
| @wraps(f) | ||
| def func(self, str_in, *args, **kwargs): | ||
| # If it's a stream, read the whole thing | ||
| str_in = getattr(str_in, 'read', lambda: str_in)() | ||
| # If it's unicode, turn it into bytes, since ISO-8601 only covers ASCII | ||
| if isinstance(str_in, six.text_type): | ||
| # ASCII is the same in UTF-8 | ||
| try: | ||
| str_in = str_in.encode('ascii') | ||
| except UnicodeEncodeError as e: | ||
| msg = 'ISO-8601 strings should contain only ASCII characters' | ||
| six.raise_from(ValueError(msg), e) | ||
| return f(self, str_in, *args, **kwargs) | ||
| return func | ||
| class isoparser(object): | ||
| def __init__(self, sep=None): | ||
| """ | ||
| :param sep: | ||
| A single character that separates date and time portions. If | ||
| ``None``, the parser will accept any single character. | ||
| For strict ISO-8601 adherence, pass ``'T'``. | ||
| """ | ||
| if sep is not None: | ||
| if (len(sep) != 1 or ord(sep) >= 128 or sep in '0123456789'): | ||
| raise ValueError('Separator must be a single, non-numeric ' + | ||
| 'ASCII character') | ||
| sep = sep.encode('ascii') | ||
| self._sep = sep | ||
| @_takes_ascii | ||
| def isoparse(self, dt_str): | ||
| """ | ||
| Parse an ISO-8601 datetime string into a :class:`datetime.datetime`. | ||
| An ISO-8601 datetime string consists of a date portion, followed | ||
| optionally by a time portion - the date and time portions are separated | ||
| by a single character separator, which is ``T`` in the official | ||
| standard. Incomplete date formats (such as ``YYYY-MM``) may *not* be | ||
| combined with a time portion. | ||
| Supported date formats are: | ||
| Common: | ||
| - ``YYYY`` | ||
| - ``YYYY-MM`` or ``YYYYMM`` | ||
| - ``YYYY-MM-DD`` or ``YYYYMMDD`` | ||
| Uncommon: | ||
| - ``YYYY-Www`` or ``YYYYWww`` - ISO week (day defaults to 0) | ||
| - ``YYYY-Www-D`` or ``YYYYWwwD`` - ISO week and day | ||
| The ISO week and day numbering follows the same logic as | ||
| :func:`datetime.date.isocalendar`. | ||
| Supported time formats are: | ||
| - ``hh`` | ||
| - ``hh:mm`` or ``hhmm`` | ||
| - ``hh:mm:ss`` or ``hhmmss`` | ||
| - ``hh:mm:ss.sss`` or ``hh:mm:ss.ssssss`` (3-6 sub-second digits) | ||
| Midnight is a special case for `hh`, as the standard supports both | ||
| 00:00 and 24:00 as a representation. | ||
| .. caution:: | ||
| Support for fractional components other than seconds is part of the | ||
| ISO-8601 standard, but is not currently implemented in this parser. | ||
| Supported time zone offset formats are: | ||
| - `Z` (UTC) | ||
| - `±HH:MM` | ||
| - `±HHMM` | ||
| - `±HH` | ||
| Offsets will be represented as :class:`dateutil.tz.tzoffset` objects, | ||
| with the exception of UTC, which will be represented as | ||
| :class:`dateutil.tz.tzutc`. Time zone offsets equivalent to UTC (such | ||
| as `+00:00`) will also be represented as :class:`dateutil.tz.tzutc`. | ||
| :param dt_str: | ||
| A string or stream containing only an ISO-8601 datetime string | ||
| :return: | ||
| Returns a :class:`datetime.datetime` representing the string. | ||
| Unspecified components default to their lowest value. | ||
| .. warning:: | ||
| As of version 2.7.0, the strictness of the parser should not be | ||
| considered a stable part of the contract. Any valid ISO-8601 string | ||
| that parses correctly with the default settings will continue to | ||
| parse correctly in future versions, but invalid strings that | ||
| currently fail (e.g. ``2017-01-01T00:00+00:00:00``) are not | ||
| guaranteed to continue failing in future versions if they encode | ||
| a valid date. | ||
| """ | ||
| components, pos = self._parse_isodate(dt_str) | ||
| if len(dt_str) > pos: | ||
| if self._sep is None or dt_str[pos:pos + 1] == self._sep: | ||
| components += self._parse_isotime(dt_str[pos + 1:]) | ||
| else: | ||
| raise ValueError('String contains unknown ISO components') | ||
| return datetime(*components) | ||
| @_takes_ascii | ||
| def parse_isodate(self, datestr): | ||
| """ | ||
| Parse the date portion of an ISO string. | ||
| :param datestr: | ||
| The string portion of an ISO string, without a separator | ||
| :return: | ||
| Returns a :class:`datetime.date` object | ||
| """ | ||
| components, pos = self._parse_isodate(datestr) | ||
| if pos < len(datestr): | ||
| raise ValueError('String contains unknown ISO ' + | ||
| 'components: {}'.format(datestr)) | ||
| return date(*components) | ||
| @_takes_ascii | ||
| def parse_isotime(self, timestr): | ||
| """ | ||
| Parse the time portion of an ISO string. | ||
| :param timestr: | ||
| The time portion of an ISO string, without a separator | ||
| :return: | ||
| Returns a :class:`datetime.time` object | ||
| """ | ||
| return time(*self._parse_isotime(timestr)) | ||
| @_takes_ascii | ||
| def parse_tzstr(self, tzstr, zero_as_utc=True): | ||
| """ | ||
| Parse a valid ISO time zone string. | ||
| See :func:`isoparser.isoparse` for details on supported formats. | ||
| :param tzstr: | ||
| A string representing an ISO time zone offset | ||
| :param zero_as_utc: | ||
| Whether to return :class:`dateutil.tz.tzutc` for zero-offset zones | ||
| :return: | ||
| Returns :class:`dateutil.tz.tzoffset` for offsets and | ||
| :class:`dateutil.tz.tzutc` for ``Z`` and (if ``zero_as_utc`` is | ||
| specified) offsets equivalent to UTC. | ||
| """ | ||
| return self._parse_tzstr(tzstr, zero_as_utc=zero_as_utc) | ||
| # Constants | ||
| _MICROSECOND_END_REGEX = re.compile(b'[-+Z]+') | ||
| _DATE_SEP = b'-' | ||
| _TIME_SEP = b':' | ||
| _MICRO_SEP = b'.' | ||
| def _parse_isodate(self, dt_str): | ||
| try: | ||
| return self._parse_isodate_common(dt_str) | ||
| except ValueError: | ||
| return self._parse_isodate_uncommon(dt_str) | ||
| def _parse_isodate_common(self, dt_str): | ||
| len_str = len(dt_str) | ||
| components = [1, 1, 1] | ||
| if len_str < 4: | ||
| raise ValueError('ISO string too short') | ||
| # Year | ||
| components[0] = int(dt_str[0:4]) | ||
| pos = 4 | ||
| if pos >= len_str: | ||
| return components, pos | ||
| has_sep = dt_str[pos:pos + 1] == self._DATE_SEP | ||
| if has_sep: | ||
| pos += 1 | ||
| # Month | ||
| if len_str - pos < 2: | ||
| raise ValueError('Invalid common month') | ||
| components[1] = int(dt_str[pos:pos + 2]) | ||
| pos += 2 | ||
| if pos >= len_str: | ||
| if has_sep: | ||
| return components, pos | ||
| else: | ||
| raise ValueError('Invalid ISO format') | ||
| if has_sep: | ||
| if dt_str[pos:pos + 1] != self._DATE_SEP: | ||
| raise ValueError('Invalid separator in ISO string') | ||
| pos += 1 | ||
| # Day | ||
| if len_str - pos < 2: | ||
| raise ValueError('Invalid common day') | ||
| components[2] = int(dt_str[pos:pos + 2]) | ||
| return components, pos + 2 | ||
| def _parse_isodate_uncommon(self, dt_str): | ||
| if len(dt_str) < 4: | ||
| raise ValueError('ISO string too short') | ||
| # All ISO formats start with the year | ||
| year = int(dt_str[0:4]) | ||
| has_sep = dt_str[4:5] == self._DATE_SEP | ||
| pos = 4 + has_sep # Skip '-' if it's there | ||
| if dt_str[pos:pos + 1] == b'W': | ||
| # YYYY-?Www-?D? | ||
| pos += 1 | ||
| weekno = int(dt_str[pos:pos + 2]) | ||
| pos += 2 | ||
| dayno = 1 | ||
| if len(dt_str) > pos: | ||
| if (dt_str[pos:pos + 1] == self._DATE_SEP) != has_sep: | ||
| raise ValueError('Inconsistent use of dash separator') | ||
| pos += has_sep | ||
| dayno = int(dt_str[pos:pos + 1]) | ||
| pos += 1 | ||
| base_date = self._calculate_weekdate(year, weekno, dayno) | ||
| else: | ||
| # YYYYDDD or YYYY-DDD | ||
| if len(dt_str) - pos < 3: | ||
| raise ValueError('Invalid ordinal day') | ||
| ordinal_day = int(dt_str[pos:pos + 3]) | ||
| pos += 3 | ||
| if ordinal_day < 1 or ordinal_day > (365 + calendar.isleap(year)): | ||
| raise ValueError('Invalid ordinal day' + | ||
| ' {} for year {}'.format(ordinal_day, year)) | ||
| base_date = date(year, 1, 1) + timedelta(days=ordinal_day - 1) | ||
| components = [base_date.year, base_date.month, base_date.day] | ||
| return components, pos | ||
| def _calculate_weekdate(self, year, week, day): | ||
| """ | ||
| Calculate the day of corresponding to the ISO year-week-day calendar. | ||
| This function is effectively the inverse of | ||
| :func:`datetime.date.isocalendar`. | ||
| :param year: | ||
| The year in the ISO calendar | ||
| :param week: | ||
| The week in the ISO calendar - range is [1, 53] | ||
| :param day: | ||
| The day in the ISO calendar - range is [1 (MON), 7 (SUN)] | ||
| :return: | ||
| Returns a :class:`datetime.date` | ||
| """ | ||
| if not 0 < week < 54: | ||
| raise ValueError('Invalid week: {}'.format(week)) | ||
| if not 0 < day < 8: # Range is 1-7 | ||
| raise ValueError('Invalid weekday: {}'.format(day)) | ||
| # Get week 1 for the specific year: | ||
| jan_4 = date(year, 1, 4) # Week 1 always has January 4th in it | ||
| week_1 = jan_4 - timedelta(days=jan_4.isocalendar()[2] - 1) | ||
| # Now add the specific number of weeks and days to get what we want | ||
| week_offset = (week - 1) * 7 + (day - 1) | ||
| return week_1 + timedelta(days=week_offset) | ||
| def _parse_isotime(self, timestr): | ||
| len_str = len(timestr) | ||
| components = [0, 0, 0, 0, None] | ||
| pos = 0 | ||
| comp = -1 | ||
| if len(timestr) < 2: | ||
| raise ValueError('ISO time too short') | ||
| has_sep = len_str >= 3 and timestr[2:3] == self._TIME_SEP | ||
| while pos < len_str and comp < 5: | ||
| comp += 1 | ||
| if timestr[pos:pos + 1] in b'-+Z': | ||
| # Detect time zone boundary | ||
| components[-1] = self._parse_tzstr(timestr[pos:]) | ||
| pos = len_str | ||
| break | ||
| if comp < 3: | ||
| # Hour, minute, second | ||
| components[comp] = int(timestr[pos:pos + 2]) | ||
| pos += 2 | ||
| if (has_sep and pos < len_str and | ||
| timestr[pos:pos + 1] == self._TIME_SEP): | ||
| pos += 1 | ||
| if comp == 3: | ||
| # Microsecond | ||
| if timestr[pos:pos + 1] != self._MICRO_SEP: | ||
| continue | ||
| pos += 1 | ||
| us_str = self._MICROSECOND_END_REGEX.split(timestr[pos:pos + 6], | ||
| 1)[0] | ||
| components[comp] = int(us_str) * 10**(6 - len(us_str)) | ||
| pos += len(us_str) | ||
| if pos < len_str: | ||
| raise ValueError('Unused components in ISO string') | ||
| if components[0] == 24: | ||
| # Standard supports 00:00 and 24:00 as representations of midnight | ||
| if any(component != 0 for component in components[1:4]): | ||
| raise ValueError('Hour may only be 24 at 24:00:00.000') | ||
| components[0] = 0 | ||
| return components | ||
| def _parse_tzstr(self, tzstr, zero_as_utc=True): | ||
| if tzstr == b'Z': | ||
| return tz.tzutc() | ||
| if len(tzstr) not in {3, 5, 6}: | ||
| raise ValueError('Time zone offset must be 1, 3, 5 or 6 characters') | ||
| if tzstr[0:1] == b'-': | ||
| mult = -1 | ||
| elif tzstr[0:1] == b'+': | ||
| mult = 1 | ||
| else: | ||
| raise ValueError('Time zone offset requires sign') | ||
| hours = int(tzstr[1:3]) | ||
| if len(tzstr) == 3: | ||
| minutes = 0 | ||
| else: | ||
| minutes = int(tzstr[(4 if tzstr[3:4] == self._TIME_SEP else 3):]) | ||
| if zero_as_utc and hours == 0 and minutes == 0: | ||
| return tz.tzutc() | ||
| else: | ||
| if minutes > 59: | ||
| raise ValueError('Invalid minutes in time zone offset') | ||
| if hours > 23: | ||
| raise ValueError('Invalid hours in time zone offset') | ||
| return tz.tzoffset(None, mult * (hours * 60 + minutes) * 60) | ||
| DEFAULT_ISOPARSER = isoparser() | ||
| isoparse = DEFAULT_ISOPARSER.isoparse |
| """Test for the "import *" functionality. | ||
| As imort * can be only done at module level, it has been added in a separate file | ||
| """ | ||
| import unittest | ||
| prev_locals = list(locals()) | ||
| from dateutil import * | ||
| new_locals = {name:value for name,value in locals().items() | ||
| if name not in prev_locals} | ||
| new_locals.pop('prev_locals') | ||
| class ImportStarTest(unittest.TestCase): | ||
| """ Test that `from dateutil import *` adds the modules in __all__ locally""" | ||
| def testImportedModules(self): | ||
| import dateutil.easter | ||
| import dateutil.parser | ||
| import dateutil.relativedelta | ||
| import dateutil.rrule | ||
| import dateutil.tz | ||
| import dateutil.utils | ||
| import dateutil.zoneinfo | ||
| self.assertEquals(dateutil.easter, new_locals.pop("easter")) | ||
| self.assertEquals(dateutil.parser, new_locals.pop("parser")) | ||
| self.assertEquals(dateutil.relativedelta, new_locals.pop("relativedelta")) | ||
| self.assertEquals(dateutil.rrule, new_locals.pop("rrule")) | ||
| self.assertEquals(dateutil.tz, new_locals.pop("tz")) | ||
| self.assertEquals(dateutil.utils, new_locals.pop("utils")) | ||
| self.assertEquals(dateutil.zoneinfo, new_locals.pop("zoneinfo")) | ||
| self.assertFalse(new_locals) |
| # -*- coding: utf-8 -*- | ||
| """ | ||
| Tests for implementation details, not necessarily part of the user-facing | ||
| API. | ||
| The motivating case for these tests is #483, where we want to smoke-test | ||
| code that may be difficult to reach through the standard API calls. | ||
| """ | ||
| import unittest | ||
| import sys | ||
| import pytest | ||
| from dateutil.parser._parser import _ymd | ||
| from dateutil import tz | ||
| IS_PY32 = sys.version_info[0:2] == (3, 2) | ||
| class TestYMD(unittest.TestCase): | ||
| # @pytest.mark.smoke | ||
| def test_could_be_day(self): | ||
| ymd = _ymd('foo bar 124 baz') | ||
| ymd.append(2, 'M') | ||
| assert ymd.has_month | ||
| assert not ymd.has_year | ||
| assert ymd.could_be_day(4) | ||
| assert not ymd.could_be_day(-6) | ||
| assert not ymd.could_be_day(32) | ||
| # Assumes leapyear | ||
| assert ymd.could_be_day(29) | ||
| ymd.append(1999) | ||
| assert ymd.has_year | ||
| assert not ymd.could_be_day(29) | ||
| ymd.append(16, 'D') | ||
| assert ymd.has_day | ||
| assert not ymd.could_be_day(1) | ||
| ymd = _ymd('foo bar 124 baz') | ||
| ymd.append(1999) | ||
| assert ymd.could_be_day(31) | ||
| ### | ||
| # Test that private interfaces in _parser are deprecated properly | ||
| @pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') | ||
| def test_parser_private_warns(): | ||
| from dateutil.parser import _timelex, _tzparser | ||
| from dateutil.parser import _parsetz | ||
| with pytest.warns(DeprecationWarning): | ||
| _tzparser() | ||
| with pytest.warns(DeprecationWarning): | ||
| _timelex('2014-03-03') | ||
| with pytest.warns(DeprecationWarning): | ||
| _parsetz('+05:00') | ||
| @pytest.mark.skipif(IS_PY32, reason='pytest.warns not supported on Python 3.2') | ||
| def test_parser_parser_private_not_warns(): | ||
| from dateutil.parser._parser import _timelex, _tzparser | ||
| from dateutil.parser._parser import _parsetz | ||
| with pytest.warns(None) as recorder: | ||
| _tzparser() | ||
| assert len(recorder) == 0 | ||
| with pytest.warns(None) as recorder: | ||
| _timelex('2014-03-03') | ||
| assert len(recorder) == 0 | ||
| with pytest.warns(None) as recorder: | ||
| _parsetz('+05:00') | ||
| assert len(recorder) == 0 | ||
| @pytest.mark.tzstr | ||
| def test_tzstr_internal_timedeltas(): | ||
| with pytest.warns(tz.DeprecatedTzFormatWarning): | ||
| tz1 = tz.tzstr("EST5EDT,5,4,0,7200,11,-3,0,7200") | ||
| with pytest.warns(tz.DeprecatedTzFormatWarning): | ||
| tz2 = tz.tzstr("EST5EDT,4,1,0,7200,10,-1,0,7200") | ||
| assert tz1._start_delta != tz2._start_delta | ||
| assert tz1._end_delta != tz2._end_delta |
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from datetime import datetime, timedelta, date, time | ||
| import itertools as it | ||
| from dateutil.tz import tz | ||
| from dateutil.parser import isoparser, isoparse | ||
| import pytest | ||
| import six | ||
| UTC = tz.tzutc() | ||
| def _generate_tzoffsets(limited): | ||
| def _mkoffset(hmtuple, fmt): | ||
| h, m = hmtuple | ||
| m_td = (-1 if h < 0 else 1) * m | ||
| tzo = tz.tzoffset(None, timedelta(hours=h, minutes=m_td)) | ||
| return tzo, fmt.format(h, m) | ||
| out = [] | ||
| if not limited: | ||
| # The subset that's just hours | ||
| hm_out_h = [(h, 0) for h in (-23, -5, 0, 5, 23)] | ||
| out.extend([_mkoffset(hm, '{:+03d}') for hm in hm_out_h]) | ||
| # Ones that have hours and minutes | ||
| hm_out = [] + hm_out_h | ||
| hm_out += [(-12, 15), (11, 30), (10, 2), (5, 15), (-5, 30)] | ||
| else: | ||
| hm_out = [(-5, -0)] | ||
| fmts = ['{:+03d}:{:02d}', '{:+03d}{:02d}'] | ||
| out += [_mkoffset(hm, fmt) for hm in hm_out for fmt in fmts] | ||
| # Also add in UTC and naive | ||
| out.append((tz.tzutc(), 'Z')) | ||
| out.append((None, '')) | ||
| return out | ||
| FULL_TZOFFSETS = _generate_tzoffsets(False) | ||
| FULL_TZOFFSETS_AWARE = [x for x in FULL_TZOFFSETS if x[1]] | ||
| TZOFFSETS = _generate_tzoffsets(True) | ||
| DATES = [datetime(1996, 1, 1), datetime(2017, 1, 1)] | ||
| @pytest.mark.parametrize('dt', tuple(DATES)) | ||
| def test_year_only(dt): | ||
| dtstr = dt.strftime('%Y') | ||
| assert isoparse(dtstr) == dt | ||
| DATES += [datetime(2000, 2, 1), datetime(2017, 4, 1)] | ||
| @pytest.mark.parametrize('dt', tuple(DATES)) | ||
| def test_year_month(dt): | ||
| fmt = '%Y-%m' | ||
| dtstr = dt.strftime(fmt) | ||
| assert isoparse(dtstr) == dt | ||
| DATES += [datetime(2016, 2, 29), datetime(2018, 3, 15)] | ||
| YMD_FMTS = ('%Y%m%d', '%Y-%m-%d') | ||
| @pytest.mark.parametrize('dt', tuple(DATES)) | ||
| @pytest.mark.parametrize('fmt', YMD_FMTS) | ||
| def test_year_month_day(dt, fmt): | ||
| dtstr = dt.strftime(fmt) | ||
| assert isoparse(dtstr) == dt | ||
| def _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, | ||
| microsecond_precision=None): | ||
| tzi, offset_str = tzoffset | ||
| fmt = date_fmt + 'T' + time_fmt | ||
| dt = dt.replace(tzinfo=tzi) | ||
| dtstr = dt.strftime(fmt) | ||
| if microsecond_precision is not None: | ||
| if not fmt.endswith('%f'): | ||
| raise ValueError('Time format has no microseconds!') | ||
| if microsecond_precision != 6: | ||
| dtstr = dtstr[:-(6 - microsecond_precision)] | ||
| elif microsecond_precision > 6: | ||
| raise ValueError('Precision must be 1-6') | ||
| dtstr += offset_str | ||
| assert isoparse(dtstr) == dt | ||
| DATETIMES = [datetime(1998, 4, 16, 12), | ||
| datetime(2019, 11, 18, 23), | ||
| datetime(2014, 12, 16, 4)] | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| def test_ymd_h(dt, date_fmt, tzoffset): | ||
| _isoparse_date_and_time(dt, date_fmt, '%H', tzoffset) | ||
| DATETIMES = [datetime(2012, 1, 6, 9, 37)] | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', ('%H%M', '%H:%M')) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| def test_ymd_hm(dt, date_fmt, time_fmt, tzoffset): | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| DATETIMES = [datetime(2003, 9, 2, 22, 14, 2), | ||
| datetime(2003, 8, 8, 14, 9, 14), | ||
| datetime(2003, 4, 7, 6, 14, 59)] | ||
| HMS_FMTS = ('%H%M%S', '%H:%M:%S') | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', HMS_FMTS) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| def test_ymd_hms(dt, date_fmt, time_fmt, tzoffset): | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| DATETIMES = [datetime(2017, 11, 27, 6, 14, 30, 123456)] | ||
| @pytest.mark.parametrize('dt', tuple(DATETIMES)) | ||
| @pytest.mark.parametrize('date_fmt', YMD_FMTS) | ||
| @pytest.mark.parametrize('time_fmt', (x + '.%f' for x in HMS_FMTS)) | ||
| @pytest.mark.parametrize('tzoffset', TZOFFSETS) | ||
| @pytest.mark.parametrize('precision', list(range(3, 7))) | ||
| def test_ymd_hms_micro(dt, date_fmt, time_fmt, tzoffset, precision): | ||
| # Truncate the microseconds to the desired precision for the representation | ||
| dt = dt.replace(microsecond=int(round(dt.microsecond, precision-6))) | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset, precision) | ||
| @pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) | ||
| def test_full_tzoffsets(tzoffset): | ||
| dt = datetime(2017, 11, 27, 6, 14, 30, 123456) | ||
| date_fmt = '%Y-%m-%d' | ||
| time_fmt = '%H:%M:%S.%f' | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| @pytest.mark.parametrize('dt_str', [ | ||
| '2014-04-11T00', | ||
| '2014-04-11T24', | ||
| '2014-04-11T00:00', | ||
| '2014-04-11T24:00', | ||
| '2014-04-11T00:00:00', | ||
| '2014-04-11T24:00:00', | ||
| '2014-04-11T00:00:00.000', | ||
| '2014-04-11T24:00:00.000', | ||
| '2014-04-11T00:00:00.000000', | ||
| '2014-04-11T24:00:00.000000'] | ||
| ) | ||
| def test_datetime_midnight(dt_str): | ||
| assert isoparse(dt_str) == datetime(2014, 4, 11, 0, 0, 0, 0) | ||
| @pytest.mark.parametrize('datestr', [ | ||
| '2014-01-01', | ||
| '20140101', | ||
| ]) | ||
| @pytest.mark.parametrize('sep', [' ', 'a', 'T', '_', '-']) | ||
| def test_isoparse_sep_none(datestr, sep): | ||
| isostr = datestr + sep + '14:33:09' | ||
| assert isoparse(isostr) == datetime(2014, 1, 1, 14, 33, 9) | ||
| ## | ||
| # Uncommon date formats | ||
| TIME_ARGS = ('time_args', | ||
| ((None, time(0), None), ) + tuple(('%H:%M:%S.%f', _t, _tz) | ||
| for _t, _tz in it.product([time(0), time(9, 30), time(14, 47)], | ||
| TZOFFSETS))) | ||
| @pytest.mark.parametrize('isocal,dt_expected',[ | ||
| ((2017, 10), datetime(2017, 3, 6)), | ||
| ((2020, 1), datetime(2019, 12, 30)), # ISO year != Cal year | ||
| ((2004, 53), datetime(2004, 12, 27)), # Only half the week is in 2014 | ||
| ]) | ||
| def test_isoweek(isocal, dt_expected): | ||
| # TODO: Figure out how to parametrize this on formats, too | ||
| for fmt in ('{:04d}-W{:02d}', '{:04d}W{:02d}'): | ||
| dtstr = fmt.format(*isocal) | ||
| assert isoparse(dtstr) == dt_expected | ||
| @pytest.mark.parametrize('isocal,dt_expected',[ | ||
| ((2016, 13, 7), datetime(2016, 4, 3)), | ||
| ((2004, 53, 7), datetime(2005, 1, 2)), # ISO year != Cal year | ||
| ((2009, 1, 2), datetime(2008, 12, 30)), # ISO year < Cal year | ||
| ((2009, 53, 6), datetime(2010, 1, 2)) # ISO year > Cal year | ||
| ]) | ||
| def test_isoweek_day(isocal, dt_expected): | ||
| # TODO: Figure out how to parametrize this on formats, too | ||
| for fmt in ('{:04d}-W{:02d}-{:d}', '{:04d}W{:02d}{:d}'): | ||
| dtstr = fmt.format(*isocal) | ||
| assert isoparse(dtstr) == dt_expected | ||
| @pytest.mark.parametrize('isoord,dt_expected', [ | ||
| ((2004, 1), datetime(2004, 1, 1)), | ||
| ((2016, 60), datetime(2016, 2, 29)), | ||
| ((2017, 60), datetime(2017, 3, 1)), | ||
| ((2016, 366), datetime(2016, 12, 31)), | ||
| ((2017, 365), datetime(2017, 12, 31)) | ||
| ]) | ||
| def test_iso_ordinal(isoord, dt_expected): | ||
| for fmt in ('{:04d}-{:03d}', '{:04d}{:03d}'): | ||
| dtstr = fmt.format(*isoord) | ||
| assert isoparse(dtstr) == dt_expected | ||
| ### | ||
| # Acceptance of bytes | ||
| @pytest.mark.parametrize('isostr,dt', [ | ||
| (b'2014', datetime(2014, 1, 1)), | ||
| (b'20140204', datetime(2014, 2, 4)), | ||
| (b'2014-02-04', datetime(2014, 2, 4)), | ||
| (b'2014-02-04T12', datetime(2014, 2, 4, 12)), | ||
| (b'2014-02-04T12:30', datetime(2014, 2, 4, 12, 30)), | ||
| (b'2014-02-04T12:30:15', datetime(2014, 2, 4, 12, 30, 15)), | ||
| (b'2014-02-04T12:30:15.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), | ||
| (b'20140204T123015.224', datetime(2014, 2, 4, 12, 30, 15, 224000)), | ||
| (b'2014-02-04T12:30:15.224Z', datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| tz.tzutc())), | ||
| (b'2014-02-04T12:30:15.224+05:00', | ||
| datetime(2014, 2, 4, 12, 30, 15, 224000, | ||
| tzinfo=tz.tzoffset(None, timedelta(hours=5))))]) | ||
| def test_bytes(isostr, dt): | ||
| assert isoparse(isostr) == dt | ||
| ### | ||
| # Invalid ISO strings | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('201', ValueError), # ISO string too short | ||
| ('2012-0425', ValueError), # Inconsistent date separators | ||
| ('201204-25', ValueError), # Inconsistent date separators | ||
| ('20120425T0120:00', ValueError), # Inconsistent time separators | ||
| ('20120425T012500-334', ValueError), # Wrong microsecond separator | ||
| ('2001-1', ValueError), # YYYY-M not valid | ||
| ('2012-04-9', ValueError), # YYYY-MM-D not valid | ||
| ('201204', ValueError), # YYYYMM not valid | ||
| ('20120411T03:30+', ValueError), # Time zone too short | ||
| ('20120411T03:30+1234567', ValueError), # Time zone too long | ||
| ('20120411T03:30-25:40', ValueError), # Time zone invalid | ||
| ('2012-1a', ValueError), # Invalid month | ||
| ('20120411T03:30+00:60', ValueError), # Time zone invalid minutes | ||
| ('20120411T03:30+00:61', ValueError), # Time zone invalid minutes | ||
| ('20120411T033030.123456012:00', # No sign in time zone | ||
| ValueError), | ||
| ('2012-W00', ValueError), # Invalid ISO week | ||
| ('2012-W55', ValueError), # Invalid ISO week | ||
| ('2012-W01-0', ValueError), # Invalid ISO week day | ||
| ('2012-W01-8', ValueError), # Invalid ISO week day | ||
| ('2013-000', ValueError), # Invalid ordinal day | ||
| ('2013-366', ValueError), # Invalid ordinal day | ||
| ('2013366', ValueError), # Invalid ordinal day | ||
| ('2014-03-12Т12:30:14', ValueError), # Cyrillic T | ||
| ('2014-04-21T24:00:01', ValueError), # Invalid use of 24 for midnight | ||
| ('2014_W01-1', ValueError), # Invalid separator | ||
| ('2014W01-1', ValueError), # Inconsistent use of dashes | ||
| ('2014-W011', ValueError), # Inconsistent use of dashes | ||
| ]) | ||
| def test_iso_raises(isostr, exception): | ||
| with pytest.raises(exception): | ||
| isoparse(isostr) | ||
| @pytest.mark.parametrize('sep_act,valid_sep', [ | ||
| ('C', 'T'), | ||
| ('T', 'C') | ||
| ]) | ||
| def test_iso_raises_sep(sep_act, valid_sep): | ||
| isostr = '2012-04-25' + sep_act + '01:25:00' | ||
| @pytest.mark.xfail() | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('20120425T01:2000', ValueError), # Inconsistent time separators | ||
| ]) | ||
| def test_iso_raises_failing(isostr, exception): | ||
| # These are test cases where the current implementation is too lenient | ||
| # and need to be fixed | ||
| with pytest.raises(exception): | ||
| isoparse(isostr) | ||
| ### | ||
| # Test ISOParser constructor | ||
| @pytest.mark.parametrize('sep', [' ', '9', '🍛']) | ||
| def test_isoparser_invalid_sep(sep): | ||
| with pytest.raises(ValueError): | ||
| isoparser(sep=sep) | ||
| # This only fails on Python 3 | ||
| @pytest.mark.xfail(six.PY3, reason="Fails on Python 3 only") | ||
| def test_isoparser_byte_sep(): | ||
| dt = datetime(2017, 12, 6, 12, 30, 45) | ||
| dt_str = dt.isoformat(sep=str('T')) | ||
| dt_rt = isoparser(sep=b'T').isoparse(dt_str) | ||
| assert dt == dt_rt | ||
| ### | ||
| # Test parse_tzstr | ||
| @pytest.mark.parametrize('tzoffset', FULL_TZOFFSETS) | ||
| def test_parse_tzstr(tzoffset): | ||
| dt = datetime(2017, 11, 27, 6, 14, 30, 123456) | ||
| date_fmt = '%Y-%m-%d' | ||
| time_fmt = '%H:%M:%S.%f' | ||
| _isoparse_date_and_time(dt, date_fmt, time_fmt, tzoffset) | ||
| @pytest.mark.parametrize('tzstr', [ | ||
| '-00:00', '+00:00', '+00', '-00', '+0000', '-0000' | ||
| ]) | ||
| @pytest.mark.parametrize('zero_as_utc', [True, False]) | ||
| def test_parse_tzstr_zero_as_utc(tzstr, zero_as_utc): | ||
| tzi = isoparser().parse_tzstr(tzstr, zero_as_utc=zero_as_utc) | ||
| assert tzi == tz.tzutc() | ||
| assert (type(tzi) == tz.tzutc) == zero_as_utc | ||
| @pytest.mark.parametrize('tzstr,exception', [ | ||
| ('00:00', ValueError), # No sign | ||
| ('05:00', ValueError), # No sign | ||
| ('_00:00', ValueError), # Invalid sign | ||
| ('+25:00', ValueError), # Offset too large | ||
| ('00:0000', ValueError), # String too long | ||
| ]) | ||
| def test_parse_tzstr_fails(tzstr, exception): | ||
| with pytest.raises(exception): | ||
| isoparser().parse_tzstr(tzstr) | ||
| ### | ||
| # Test parse_isodate | ||
| def __make_date_examples(): | ||
| dates_no_day = [ | ||
| date(1999, 12, 1), | ||
| date(2016, 2, 1) | ||
| ] | ||
| if six.PY3: | ||
| # strftime does not support dates before 1900 in Python 2 | ||
| dates_no_day.append(date(1000, 11, 1)) | ||
| # Only one supported format for dates with no day | ||
| o = zip(dates_no_day, it.repeat('%Y-%m')) | ||
| dates_w_day = [ | ||
| date(1969, 12, 31), | ||
| date(1900, 1, 1), | ||
| date(2016, 2, 29), | ||
| date(2017, 11, 14) | ||
| ] | ||
| dates_w_day_fmts = ('%Y%m%d', '%Y-%m-%d') | ||
| o = it.chain(o, it.product(dates_w_day, dates_w_day_fmts)) | ||
| return list(o) | ||
| @pytest.mark.parametrize('d,dt_fmt', __make_date_examples()) | ||
| @pytest.mark.parametrize('as_bytes', [True, False]) | ||
| def test_parse_isodate(d, dt_fmt, as_bytes): | ||
| d_str = d.strftime(dt_fmt) | ||
| if isinstance(d_str, six.text_type) and as_bytes: | ||
| d_str = d_str.encode('ascii') | ||
| elif isinstance(d_str, six.binary_type) and not as_bytes: | ||
| d_str = d_str.decode('ascii') | ||
| iparser = isoparser() | ||
| assert iparser.parse_isodate(d_str) == d | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('243', ValueError), # ISO string too short | ||
| ('2014-0423', ValueError), # Inconsistent date separators | ||
| ('201404-23', ValueError), # Inconsistent date separators | ||
| ('2014日03月14', ValueError), # Not ASCII | ||
| ('2013-02-29', ValueError), # Not a leap year | ||
| ('2014/12/03', ValueError), # Wrong separators | ||
| ('2014-04-19T', ValueError), # Unknown components | ||
| ]) | ||
| def test_isodate_raises(isostr, exception): | ||
| with pytest.raises(exception): | ||
| isoparser().parse_isodate(isostr) | ||
| ### | ||
| # Test parse_isotime | ||
| def __make_time_examples(): | ||
| outputs = [] | ||
| # HH | ||
| time_h = [time(0), time(8), time(22)] | ||
| time_h_fmts = ['%H'] | ||
| outputs.append(it.product(time_h, time_h_fmts)) | ||
| # HHMM / HH:MM | ||
| time_hm = [time(0, 0), time(0, 30), time(8, 47), time(16, 1)] | ||
| time_hm_fmts = ['%H%M', '%H:%M'] | ||
| outputs.append(it.product(time_hm, time_hm_fmts)) | ||
| # HHMMSS / HH:MM:SS | ||
| time_hms = [time(0, 0, 0), time(0, 15, 30), | ||
| time(8, 2, 16), time(12, 0), time(16, 2), time(20, 45)] | ||
| time_hms_fmts = ['%H%M%S', '%H:%M:%S'] | ||
| outputs.append(it.product(time_hms, time_hms_fmts)) | ||
| # HHMMSS.ffffff / HH:MM:SS.ffffff | ||
| time_hmsu = [time(0, 0, 0, 0), time(4, 15, 3, 247993), | ||
| time(14, 21, 59, 948730), | ||
| time(23, 59, 59, 999999)] | ||
| time_hmsu_fmts = ['%H%M%S.%f', '%H:%M:%S.%f'] | ||
| outputs.append(it.product(time_hmsu, time_hmsu_fmts)) | ||
| outputs = list(map(list, outputs)) | ||
| # Time zones | ||
| ex_naive = list(it.chain.from_iterable(x[0:2] for x in outputs)) | ||
| o = it.product(ex_naive, TZOFFSETS) # ((time, fmt), (tzinfo, offsetstr)) | ||
| o = ((t.replace(tzinfo=tzi), fmt + off_str) | ||
| for (t, fmt), (tzi, off_str) in o) | ||
| outputs.append(o) | ||
| return list(it.chain.from_iterable(outputs)) | ||
| @pytest.mark.parametrize('time_val,time_fmt', __make_time_examples()) | ||
| @pytest.mark.parametrize('as_bytes', [True, False]) | ||
| def test_isotime(time_val, time_fmt, as_bytes): | ||
| tstr = time_val.strftime(time_fmt) | ||
| if isinstance(time_val, six.text_type) and as_bytes: | ||
| tstr = tstr.encode('ascii') | ||
| elif isinstance(time_val, six.binary_type) and not as_bytes: | ||
| tstr = tstr.decode('ascii') | ||
| iparser = isoparser() | ||
| assert iparser.parse_isotime(tstr) == time_val | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('3', ValueError), # ISO string too short | ||
| ('14時30分15秒', ValueError), # Not ASCII | ||
| ('14_30_15', ValueError), # Invalid separators | ||
| ('1430:15', ValueError), # Inconsistent separator use | ||
| ('14:30:15.3684000309', ValueError), # Too much us precision | ||
| ('25', ValueError), # Invalid hours | ||
| ('25:15', ValueError), # Invalid hours | ||
| ('14:60', ValueError), # Invalid minutes | ||
| ('14:59:61', ValueError), # Invalid seconds | ||
| ('14:30:15.3446830500', ValueError), # No sign in time zone | ||
| ('14:30:15+', ValueError), # Time zone too short | ||
| ('14:30:15+1234567', ValueError), # Time zone invalid | ||
| ('14:59:59+25:00', ValueError), # Invalid tz hours | ||
| ('14:59:59+12:62', ValueError), # Invalid tz minutes | ||
| ('14:59:30_344583', ValueError), # Invalid microsecond separator | ||
| ]) | ||
| def test_isotime_raises(isostr, exception): | ||
| iparser = isoparser() | ||
| with pytest.raises(exception): | ||
| iparser.parse_isotime(isostr) | ||
| @pytest.mark.xfail() | ||
| @pytest.mark.parametrize('isostr,exception', [ | ||
| ('14:3015', ValueError), # Inconsistent separator use | ||
| ('201202', ValueError) # Invalid ISO format | ||
| ]) | ||
| def test_isotime_raises_xfail(isostr, exception): | ||
| iparser = isoparser() | ||
| with pytest.raises(exception): | ||
| iparser.parse_isotime(isostr) |
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from datetime import timedelta, datetime | ||
| import unittest | ||
| from dateutil import tz | ||
| from dateutil import utils | ||
| from dateutil.utils import within_delta | ||
| from freezegun import freeze_time | ||
| UTC = tz.tzutc() | ||
| NYC = tz.gettz("America/New_York") | ||
| class UtilsTest(unittest.TestCase): | ||
| @freeze_time(datetime(2014, 12, 15, 1, 21, 33, 4003)) | ||
| def testToday(self): | ||
| self.assertEqual(utils.today(), datetime(2014, 12, 15, 0, 0, 0)) | ||
| @freeze_time(datetime(2014, 12, 15, 12), tz_offset=5) | ||
| def testTodayTzInfo(self): | ||
| self.assertEqual(utils.today(NYC), | ||
| datetime(2014, 12, 15, 0, 0, 0, tzinfo=NYC)) | ||
| @freeze_time(datetime(2014, 12, 15, 23), tz_offset=5) | ||
| def testTodayTzInfoDifferentDay(self): | ||
| self.assertEqual(utils.today(UTC), | ||
| datetime(2014, 12, 16, 0, 0, 0, tzinfo=UTC)) | ||
| def testDefaultTZInfoNaive(self): | ||
| dt = datetime(2014, 9, 14, 9, 30) | ||
| self.assertIs(utils.default_tzinfo(dt, NYC).tzinfo, | ||
| NYC) | ||
| def testDefaultTZInfoAware(self): | ||
| dt = datetime(2014, 9, 14, 9, 30, tzinfo=UTC) | ||
| self.assertIs(utils.default_tzinfo(dt, NYC).tzinfo, | ||
| UTC) | ||
| def testWithinDelta(self): | ||
| d1 = datetime(2016, 1, 1, 12, 14, 1, 9) | ||
| d2 = d1.replace(microsecond=15) | ||
| self.assertTrue(within_delta(d1, d2, timedelta(seconds=1))) | ||
| self.assertFalse(within_delta(d1, d2, timedelta(microseconds=1))) | ||
| def testWithinDeltaWithNegativeDelta(self): | ||
| d1 = datetime(2016, 1, 1) | ||
| d2 = datetime(2015, 12, 31) | ||
| self.assertTrue(within_delta(d2, d1, timedelta(days=-1))) |
| from datetime import timedelta | ||
| class _TzSingleton(type): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instance = None | ||
| super(_TzSingleton, cls).__init__(*args, **kwargs) | ||
| def __call__(cls): | ||
| if cls.__instance is None: | ||
| cls.__instance = super(_TzSingleton, cls).__call__() | ||
| return cls.__instance | ||
| class _TzFactory(type): | ||
| def instance(cls, *args, **kwargs): | ||
| """Alternate constructor that returns a fresh instance""" | ||
| return type.__call__(cls, *args, **kwargs) | ||
| class _TzOffsetFactory(_TzFactory): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instances = {} | ||
| def __call__(cls, name, offset): | ||
| if isinstance(offset, timedelta): | ||
| key = (name, offset.total_seconds()) | ||
| else: | ||
| key = (name, offset) | ||
| instance = cls.__instances.get(key, None) | ||
| if instance is None: | ||
| instance = cls.__instances.setdefault(key, | ||
| cls.instance(name, offset)) | ||
| return instance | ||
| class _TzStrFactory(_TzFactory): | ||
| def __init__(cls, *args, **kwargs): | ||
| cls.__instances = {} | ||
| def __call__(cls, s, posix_offset=False): | ||
| key = (s, posix_offset) | ||
| instance = cls.__instances.get(key, None) | ||
| if instance is None: | ||
| instance = cls.__instances.setdefault(key, | ||
| cls.instance(s, posix_offset)) | ||
| return instance | ||
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from datetime import datetime, time | ||
| def today(tzinfo=None): | ||
| """ | ||
| Returns a :py:class:`datetime` representing the current day at midnight | ||
| :param tzinfo: | ||
| The time zone to attach (also used to determine the current day). | ||
| :return: | ||
| A :py:class:`datetime.datetime` object representing the current day | ||
| at midnight. | ||
| """ | ||
| dt = datetime.now(tzinfo) | ||
| return datetime.combine(dt.date(), time(0, tzinfo=tzinfo)) | ||
| def default_tzinfo(dt, tzinfo): | ||
| """ | ||
| Sets the the ``tzinfo`` parameter on naive datetimes only | ||
| This is useful for example when you are provided a datetime that may have | ||
| either an implicit or explicit time zone, such as when parsing a time zone | ||
| string. | ||
| .. doctest:: | ||
| >>> from dateutil.tz import tzoffset | ||
| >>> from dateutil.parser import parse | ||
| >>> from dateutil.utils import default_tzinfo | ||
| >>> dflt_tz = tzoffset("EST", -18000) | ||
| >>> print(default_tzinfo(parse('2014-01-01 12:30 UTC'), dflt_tz)) | ||
| 2014-01-01 12:30:00+00:00 | ||
| >>> print(default_tzinfo(parse('2014-01-01 12:30'), dflt_tz)) | ||
| 2014-01-01 12:30:00-05:00 | ||
| :param dt: | ||
| The datetime on which to replace the time zone | ||
| :param tzinfo: | ||
| The :py:class:`datetime.tzinfo` subclass instance to assign to | ||
| ``dt`` if (and only if) it is naive. | ||
| :return: | ||
| Returns an aware :py:class:`datetime.datetime`. | ||
| """ | ||
| if dt.tzinfo is not None: | ||
| return dt | ||
| else: | ||
| return dt.replace(tzinfo=tzinfo) | ||
| def within_delta(dt1, dt2, delta): | ||
| """ | ||
| Useful for comparing two datetimes that may a negilible difference | ||
| to be considered equal. | ||
| """ | ||
| delta = abs(delta) | ||
| difference = dt1 - dt2 | ||
| return -delta <= difference <= delta |
+266
| #!/usr/bin/env python3 | ||
| # -*- coding: utf-8 -*- | ||
| # | ||
| # dateutil documentation build configuration file, created by | ||
| # sphinx-quickstart on Thu Nov 20 23:18:41 2014. | ||
| # | ||
| # This file is execfile()d with the current directory set to its | ||
| # containing dir. | ||
| # | ||
| # Note that not all possible configuration values are present in this | ||
| # autogenerated file. | ||
| # | ||
| # All configuration values have a default; values that are commented out | ||
| # serve to show the default. | ||
| import sys | ||
| import os | ||
| # If extensions (or modules to document with autodoc) are in another directory, | ||
| # add these directories to sys.path here. If the directory is relative to the | ||
| # documentation root, use os.path.abspath to make it absolute, like shown here. | ||
| #sys.path.insert(0, os.path.abspath('.')) | ||
| sys.path.insert(0, os.path.abspath('../')) | ||
| # -- General configuration ------------------------------------------------ | ||
| # If your documentation needs a minimal Sphinx version, state it here. | ||
| #needs_sphinx = '1.0' | ||
| # Add any Sphinx extension module names here, as strings. They can be | ||
| # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom | ||
| # ones. | ||
| extensions = [ | ||
| 'sphinx.ext.autodoc', | ||
| 'sphinx.ext.doctest', | ||
| 'sphinx.ext.viewcode', | ||
| ] | ||
| # Add any paths that contain templates here, relative to this directory. | ||
| templates_path = ['_templates'] | ||
| # The suffix of source filenames. | ||
| source_suffix = '.rst' | ||
| # The encoding of source files. | ||
| #source_encoding = 'utf-8-sig' | ||
| # The master toctree document. | ||
| master_doc = 'index' | ||
| # General information about the project. | ||
| project = 'dateutil' | ||
| copyright = '2016, dateutil' | ||
| # The version info for the project you're documenting, acts as replacement for | ||
| # |version| and |release|, also used in various other places throughout the | ||
| # built documents. | ||
| # | ||
| # The short X.Y version. | ||
| from dateutil import __version__ as VERSION # Explicitly use a relative path | ||
| version = VERSION | ||
| # The full version, including alpha/beta/rc tags. | ||
| release = VERSION | ||
| # The language for content autogenerated by Sphinx. Refer to documentation | ||
| # for a list of supported languages. | ||
| #language = None | ||
| # There are two options for replacing |today|: either, you set today to some | ||
| # non-false value, then it is used: | ||
| #today = '' | ||
| # Else, today_fmt is used as the format for a strftime call. | ||
| #today_fmt = '%B %d, %Y' | ||
| # List of patterns, relative to source directory, that match files and | ||
| # directories to ignore when looking for source files. | ||
| exclude_patterns = ['_build'] | ||
| # The reST default role (used for this markup: `text`) to use for all | ||
| # documents. | ||
| #default_role = None | ||
| # If true, '()' will be appended to :func: etc. cross-reference text. | ||
| #add_function_parentheses = True | ||
| # If true, the current module name will be prepended to all description | ||
| # unit titles (such as .. function::). | ||
| #add_module_names = True | ||
| # If true, sectionauthor and moduleauthor directives will be shown in the | ||
| # output. They are ignored by default. | ||
| #show_authors = False | ||
| # The name of the Pygments (syntax highlighting) style to use. | ||
| pygments_style = 'sphinx' | ||
| # A list of ignored prefixes for module index sorting. | ||
| #modindex_common_prefix = [] | ||
| # If true, keep warnings as "system message" paragraphs in the built documents. | ||
| #keep_warnings = False | ||
| # -- Options for HTML output ---------------------------------------------- | ||
| # The theme to use for HTML and HTML Help pages. See the documentation for | ||
| # a list of builtin themes. | ||
| html_theme = 'default' | ||
| # Theme options are theme-specific and customize the look and feel of a theme | ||
| # further. For a list of options available for each theme, see the | ||
| # documentation. | ||
| #html_theme_options = {} | ||
| # Add any paths that contain custom themes here, relative to this directory. | ||
| #html_theme_path = [] | ||
| # The name for this set of Sphinx documents. If None, it defaults to | ||
| # "<project> v<release> documentation". | ||
| #html_title = None | ||
| # A shorter title for the navigation bar. Default is the same as html_title. | ||
| #html_short_title = None | ||
| # The name of an image file (relative to this directory) to place at the top | ||
| # of the sidebar. | ||
| #html_logo = None | ||
| # The name of an image file (within the static path) to use as favicon of the | ||
| # docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 | ||
| # pixels large. | ||
| #html_favicon = None | ||
| # Add any paths that contain custom static files (such as style sheets) here, | ||
| # relative to this directory. They are copied after the builtin static files, | ||
| # so a file named "default.css" will overwrite the builtin "default.css". | ||
| html_static_path = [] | ||
| # Add any extra paths that contain custom files (such as robots.txt or | ||
| # .htaccess) here, relative to this directory. These files are copied | ||
| # directly to the root of the documentation. | ||
| #html_extra_path = [] | ||
| # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, | ||
| # using the given strftime format. | ||
| #html_last_updated_fmt = '%b %d, %Y' | ||
| # If true, SmartyPants will be used to convert quotes and dashes to | ||
| # typographically correct entities. | ||
| #html_use_smartypants = True | ||
| # Custom sidebar templates, maps document names to template names. | ||
| #html_sidebars = {} | ||
| # Additional templates that should be rendered to pages, maps page names to | ||
| # template names. | ||
| #html_additional_pages = {} | ||
| # If false, no module index is generated. | ||
| #html_domain_indices = True | ||
| # If false, no index is generated. | ||
| #html_use_index = True | ||
| # If true, the index is split into individual pages for each letter. | ||
| #html_split_index = False | ||
| # If true, links to the reST sources are added to the pages. | ||
| #html_show_sourcelink = True | ||
| # If true, "Created using Sphinx" is shown in the HTML footer. Default is True. | ||
| #html_show_sphinx = True | ||
| # If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. | ||
| #html_show_copyright = True | ||
| # If true, an OpenSearch description file will be output, and all pages will | ||
| # contain a <link> tag referring to it. The value of this option must be the | ||
| # base URL from which the finished HTML is served. | ||
| #html_use_opensearch = '' | ||
| # This is the file name suffix for HTML files (e.g. ".xhtml"). | ||
| #html_file_suffix = None | ||
| # Output file base name for HTML help builder. | ||
| htmlhelp_basename = 'dateutildoc' | ||
| # -- Options for LaTeX output --------------------------------------------- | ||
| latex_elements = { | ||
| # The paper size ('letterpaper' or 'a4paper'). | ||
| #'papersize': 'letterpaper', | ||
| # The font size ('10pt', '11pt' or '12pt'). | ||
| #'pointsize': '10pt', | ||
| # Additional stuff for the LaTeX preamble. | ||
| #'preamble': '', | ||
| } | ||
| # Grouping the document tree into LaTeX files. List of tuples | ||
| # (source start file, target name, title, | ||
| # author, documentclass [howto, manual, or own class]). | ||
| latex_documents = [ | ||
| ('index', 'dateutil.tex', 'dateutil Documentation', | ||
| 'dateutil', 'manual'), | ||
| ] | ||
| # The name of an image file (relative to this directory) to place at the top of | ||
| # the title page. | ||
| #latex_logo = None | ||
| # For "manual" documents, if this is true, then toplevel headings are parts, | ||
| # not chapters. | ||
| #latex_use_parts = False | ||
| # If true, show page references after internal links. | ||
| #latex_show_pagerefs = False | ||
| # If true, show URL addresses after external links. | ||
| #latex_show_urls = False | ||
| # Documents to append as an appendix to all manuals. | ||
| #latex_appendices = [] | ||
| # If false, no module index is generated. | ||
| #latex_domain_indices = True | ||
| # -- Options for manual page output --------------------------------------- | ||
| # One entry per manual page. List of tuples | ||
| # (source start file, name, description, authors, manual section). | ||
| man_pages = [ | ||
| ('index', 'dateutil', 'dateutil Documentation', | ||
| ['dateutil'], 1) | ||
| ] | ||
| # If true, show URL addresses after external links. | ||
| #man_show_urls = False | ||
| # -- Options for Texinfo output ------------------------------------------- | ||
| # Grouping the document tree into Texinfo files. List of tuples | ||
| # (source start file, target name, title, author, | ||
| # dir menu entry, description, category) | ||
| texinfo_documents = [ | ||
| ('index', 'dateutil', 'dateutil Documentation', | ||
| 'dateutil', 'dateutil', 'One line description of project.', | ||
| 'Miscellaneous'), | ||
| ] | ||
| # Documents to append as an appendix to all manuals. | ||
| #texinfo_appendices = [] | ||
| # If false, no module index is generated. | ||
| #texinfo_domain_indices = True | ||
| # How to display URL addresses: 'footnote', 'no', or 'inline'. | ||
| #texinfo_show_urls = 'footnote' | ||
| # If true, do not generate a @detailmenu in the "Top" node's menu. | ||
| #texinfo_no_detailmenu = False |
| ====== | ||
| easter | ||
| ====== | ||
| .. automodule:: dateutil.easter | ||
| :members: | ||
| :undoc-members: |
+1469
| dateutil examples | ||
| ================= | ||
| .. contents:: | ||
| relativedelta examples | ||
| ---------------------- | ||
| .. testsetup:: relativedelta | ||
| from datetime import *; from dateutil.relativedelta import * | ||
| import calendar | ||
| NOW = datetime(2003, 9, 17, 20, 54, 47, 282310) | ||
| TODAY = date(2003, 9, 17) | ||
| Let's begin our trip:: | ||
| >>> from datetime import *; from dateutil.relativedelta import * | ||
| >>> import calendar | ||
| Store some values:: | ||
| >>> NOW = datetime.now() | ||
| >>> TODAY = date.today() | ||
| >>> NOW | ||
| datetime.datetime(2003, 9, 17, 20, 54, 47, 282310) | ||
| >>> TODAY | ||
| datetime.date(2003, 9, 17) | ||
| Next month | ||
| .. doctest:: relativedelta | ||
| >>> NOW+relativedelta(months=+1) | ||
| datetime.datetime(2003, 10, 17, 20, 54, 47, 282310) | ||
| Next month, plus one week. | ||
| .. doctest:: relativedelta | ||
| >>> NOW+relativedelta(months=+1, weeks=+1) | ||
| datetime.datetime(2003, 10, 24, 20, 54, 47, 282310) | ||
| Next month, plus one week, at 10am. | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(months=+1, weeks=+1, hour=10) | ||
| datetime.datetime(2003, 10, 24, 10, 0) | ||
| Here is another example using an absolute relativedelta. Notice the use of | ||
| year and month (both singular) which causes the values to be *replaced* in the | ||
| original datetime rather than performing an arithmetic operation on them. | ||
| .. doctest:: relativedelta | ||
| >>> NOW+relativedelta(year=1, month=1) | ||
| datetime.datetime(1, 1, 17, 20, 54, 47, 282310) | ||
| Let's try the other way around. Notice that the | ||
| hour setting we get in the relativedelta is relative, | ||
| since it's a difference, and the weeks parameter | ||
| has gone. | ||
| .. doctest:: relativedelta | ||
| >>> relativedelta(datetime(2003, 10, 24, 10, 0), TODAY) | ||
| relativedelta(months=+1, days=+7, hours=+10) | ||
| One month before one year. | ||
| .. doctest:: relativedelta | ||
| >>> NOW+relativedelta(years=+1, months=-1) | ||
| datetime.datetime(2004, 8, 17, 20, 54, 47, 282310) | ||
| How does it handle months with different numbers of days? | ||
| Notice that adding one month will never cross the month | ||
| boundary. | ||
| .. doctest:: relativedelta | ||
| >>> date(2003,1,27)+relativedelta(months=+1) | ||
| datetime.date(2003, 2, 27) | ||
| >>> date(2003,1,31)+relativedelta(months=+1) | ||
| datetime.date(2003, 2, 28) | ||
| >>> date(2003,1,31)+relativedelta(months=+2) | ||
| datetime.date(2003, 3, 31) | ||
| The logic for years is the same, even on leap years. | ||
| .. doctest:: relativedelta | ||
| >>> date(2000,2,28)+relativedelta(years=+1) | ||
| datetime.date(2001, 2, 28) | ||
| >>> date(2000,2,29)+relativedelta(years=+1) | ||
| datetime.date(2001, 2, 28) | ||
| >>> date(1999,2,28)+relativedelta(years=+1) | ||
| datetime.date(2000, 2, 28) | ||
| >>> date(1999,3,1)+relativedelta(years=+1) | ||
| datetime.date(2000, 3, 1) | ||
| >>> date(2001,2,28)+relativedelta(years=-1) | ||
| datetime.date(2000, 2, 28) | ||
| >>> date(2001,3,1)+relativedelta(years=-1) | ||
| datetime.date(2000, 3, 1) | ||
| Next friday | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(weekday=FR) | ||
| datetime.date(2003, 9, 19) | ||
| >>> TODAY+relativedelta(weekday=calendar.FRIDAY) | ||
| datetime.date(2003, 9, 19) | ||
| Last friday in this month. | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(day=31, weekday=FR(-1)) | ||
| datetime.date(2003, 9, 26) | ||
| Next wednesday (it's today!). | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(weekday=WE(+1)) | ||
| datetime.date(2003, 9, 17) | ||
| Next wednesday, but not today. | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(days=+1, weekday=WE(+1)) | ||
| datetime.date(2003, 9, 24) | ||
| Following | ||
| [http://www.cl.cam.ac.uk/~mgk25/iso-time.html ISO year week number notation] | ||
| find the first day of the 15th week of 1997. | ||
| .. doctest:: relativedelta | ||
| >>> datetime(1997,1,1)+relativedelta(day=4, weekday=MO(-1), weeks=+14) | ||
| datetime.datetime(1997, 4, 7, 0, 0) | ||
| How long ago has the millennium changed? | ||
| .. doctest:: relativedelta | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> relativedelta(NOW, date(2001,1,1)) | ||
| relativedelta(years=+2, months=+8, days=+16, | ||
| hours=+20, minutes=+54, seconds=+47, microseconds=+282310) | ||
| How old is John? | ||
| .. doctest:: relativedelta | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> johnbirthday = datetime(1978, 4, 5, 12, 0) | ||
| >>> relativedelta(NOW, johnbirthday) | ||
| relativedelta(years=+25, months=+5, days=+12, | ||
| hours=+8, minutes=+54, seconds=+47, microseconds=+282310) | ||
| It works with dates too. | ||
| .. doctest:: relativedelta | ||
| >>> relativedelta(TODAY, johnbirthday) | ||
| relativedelta(years=+25, months=+5, days=+11, hours=+12) | ||
| Obtain today's date using the yearday: | ||
| .. doctest:: relativedelta | ||
| >>> date(2003, 1, 1)+relativedelta(yearday=260) | ||
| datetime.date(2003, 9, 17) | ||
| We can use today's date, since yearday should be absolute | ||
| in the given year: | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(yearday=260) | ||
| datetime.date(2003, 9, 17) | ||
| Last year it should be in the same day: | ||
| .. doctest:: relativedelta | ||
| >>> date(2002, 1, 1)+relativedelta(yearday=260) | ||
| datetime.date(2002, 9, 17) | ||
| But not in a leap year: | ||
| .. doctest:: relativedelta | ||
| >>> date(2000, 1, 1)+relativedelta(yearday=260) | ||
| datetime.date(2000, 9, 16) | ||
| We can use the non-leap year day to ignore this: | ||
| .. doctest:: relativedelta | ||
| >>> date(2000, 1, 1)+relativedelta(nlyearday=260) | ||
| datetime.date(2000, 9, 17) | ||
| rrule examples | ||
| -------------- | ||
| These examples were converted from the RFC. | ||
| Prepare the environment. | ||
| .. testsetup:: rrule | ||
| from dateutil.rrule import * | ||
| from dateutil.parser import * | ||
| from datetime import * | ||
| import pprint | ||
| import sys | ||
| sys.displayhook = pprint.pprint | ||
| .. doctest:: rrule | ||
| >>> from dateutil.rrule import * | ||
| >>> from dateutil.parser import * | ||
| >>> from datetime import * | ||
| >>> import pprint | ||
| >>> import sys | ||
| >>> sys.displayhook = pprint.pprint | ||
| Daily, for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(DAILY, count=10, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 3, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 5, 9, 0), | ||
| datetime.datetime(1997, 9, 6, 9, 0), | ||
| datetime.datetime(1997, 9, 7, 9, 0), | ||
| datetime.datetime(1997, 9, 8, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 10, 9, 0), | ||
| datetime.datetime(1997, 9, 11, 9, 0)] | ||
| Daily until December 24, 1997 | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||
| >>> list(rrule(DAILY, | ||
| ... dtstart=parse("19970902T090000"), | ||
| ... until=parse("19971224T000000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 3, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| ... | ||
| datetime.datetime(1997, 12, 21, 9, 0), | ||
| datetime.datetime(1997, 12, 22, 9, 0), | ||
| datetime.datetime(1997, 12, 23, 9, 0)] | ||
| Every other day, 5 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(DAILY, interval=2, count=5, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 6, 9, 0), | ||
| datetime.datetime(1997, 9, 8, 9, 0), | ||
| datetime.datetime(1997, 9, 10, 9, 0)] | ||
| Every 10 days, 5 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(DAILY, interval=10, count=5, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 12, 9, 0), | ||
| datetime.datetime(1997, 9, 22, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 12, 9, 0)] | ||
| Everyday in January, for 3 years. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||
| >>> list(rrule(YEARLY, bymonth=1, byweekday=range(7), | ||
| ... dtstart=parse("19980101T090000"), | ||
| ... until=parse("20000131T090000"))) | ||
| [datetime.datetime(1998, 1, 1, 9, 0), | ||
| datetime.datetime(1998, 1, 2, 9, 0), | ||
| ... | ||
| datetime.datetime(1998, 1, 30, 9, 0), | ||
| datetime.datetime(1998, 1, 31, 9, 0), | ||
| datetime.datetime(1999, 1, 1, 9, 0), | ||
| datetime.datetime(1999, 1, 2, 9, 0), | ||
| ... | ||
| datetime.datetime(1999, 1, 30, 9, 0), | ||
| datetime.datetime(1999, 1, 31, 9, 0), | ||
| datetime.datetime(2000, 1, 1, 9, 0), | ||
| datetime.datetime(2000, 1, 2, 9, 0), | ||
| ... | ||
| datetime.datetime(2000, 1, 30, 9, 0), | ||
| datetime.datetime(2000, 1, 31, 9, 0)] | ||
| Same thing, in another way. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||
| >>> list(rrule(DAILY, bymonth=1, | ||
| ... dtstart=parse("19980101T090000"), | ||
| ... until=parse("20000131T090000"))) | ||
| [datetime.datetime(1998, 1, 1, 9, 0), | ||
| ... | ||
| datetime.datetime(2000, 1, 31, 9, 0)] | ||
| Weekly for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=10, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 23, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 7, 9, 0), | ||
| datetime.datetime(1997, 10, 14, 9, 0), | ||
| datetime.datetime(1997, 10, 21, 9, 0), | ||
| datetime.datetime(1997, 10, 28, 9, 0), | ||
| datetime.datetime(1997, 11, 4, 9, 0)] | ||
| Every other week, 6 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, interval=2, count=6, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 14, 9, 0), | ||
| datetime.datetime(1997, 10, 28, 9, 0), | ||
| datetime.datetime(1997, 11, 11, 9, 0)] | ||
| Weekly on Tuesday and Thursday for 5 weeks. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=10, wkst=SU, byweekday=(TU,TH), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 11, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 18, 9, 0), | ||
| datetime.datetime(1997, 9, 23, 9, 0), | ||
| datetime.datetime(1997, 9, 25, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0)] | ||
| Every other week on Tuesday and Thursday, for 8 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, interval=2, count=8, | ||
| ... wkst=SU, byweekday=(TU,TH), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 18, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 14, 9, 0), | ||
| datetime.datetime(1997, 10, 16, 9, 0)] | ||
| Monthly on the 1st Friday for ten occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=10, byweekday=FR(1), | ||
| ... dtstart=parse("19970905T090000"))) | ||
| [datetime.datetime(1997, 9, 5, 9, 0), | ||
| datetime.datetime(1997, 10, 3, 9, 0), | ||
| datetime.datetime(1997, 11, 7, 9, 0), | ||
| datetime.datetime(1997, 12, 5, 9, 0), | ||
| datetime.datetime(1998, 1, 2, 9, 0), | ||
| datetime.datetime(1998, 2, 6, 9, 0), | ||
| datetime.datetime(1998, 3, 6, 9, 0), | ||
| datetime.datetime(1998, 4, 3, 9, 0), | ||
| datetime.datetime(1998, 5, 1, 9, 0), | ||
| datetime.datetime(1998, 6, 5, 9, 0)] | ||
| Every other month on the 1st and last Sunday of the month for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, interval=2, count=10, | ||
| ... byweekday=(SU(1), SU(-1)), | ||
| ... dtstart=parse("19970907T090000"))) | ||
| [datetime.datetime(1997, 9, 7, 9, 0), | ||
| datetime.datetime(1997, 9, 28, 9, 0), | ||
| datetime.datetime(1997, 11, 2, 9, 0), | ||
| datetime.datetime(1997, 11, 30, 9, 0), | ||
| datetime.datetime(1998, 1, 4, 9, 0), | ||
| datetime.datetime(1998, 1, 25, 9, 0), | ||
| datetime.datetime(1998, 3, 1, 9, 0), | ||
| datetime.datetime(1998, 3, 29, 9, 0), | ||
| datetime.datetime(1998, 5, 3, 9, 0), | ||
| datetime.datetime(1998, 5, 31, 9, 0)] | ||
| Monthly on the second to last Monday of the month for 6 months. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=6, byweekday=MO(-2), | ||
| ... dtstart=parse("19970922T090000"))) | ||
| [datetime.datetime(1997, 9, 22, 9, 0), | ||
| datetime.datetime(1997, 10, 20, 9, 0), | ||
| datetime.datetime(1997, 11, 17, 9, 0), | ||
| datetime.datetime(1997, 12, 22, 9, 0), | ||
| datetime.datetime(1998, 1, 19, 9, 0), | ||
| datetime.datetime(1998, 2, 16, 9, 0)] | ||
| Monthly on the third to the last day of the month, for 6 months. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=6, bymonthday=-3, | ||
| ... dtstart=parse("19970928T090000"))) | ||
| [datetime.datetime(1997, 9, 28, 9, 0), | ||
| datetime.datetime(1997, 10, 29, 9, 0), | ||
| datetime.datetime(1997, 11, 28, 9, 0), | ||
| datetime.datetime(1997, 12, 29, 9, 0), | ||
| datetime.datetime(1998, 1, 29, 9, 0), | ||
| datetime.datetime(1998, 2, 26, 9, 0)] | ||
| Monthly on the 2nd and 15th of the month for 5 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=5, bymonthday=(2,15), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 15, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 15, 9, 0), | ||
| datetime.datetime(1997, 11, 2, 9, 0)] | ||
| Monthly on the first and last day of the month for 3 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=5, bymonthday=(-1,1,), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 1, 9, 0), | ||
| datetime.datetime(1997, 10, 31, 9, 0), | ||
| datetime.datetime(1997, 11, 1, 9, 0), | ||
| datetime.datetime(1997, 11, 30, 9, 0)] | ||
| Every 18 months on the 10th thru 15th of the month for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, interval=18, count=10, | ||
| ... bymonthday=range(10,16), | ||
| ... dtstart=parse("19970910T090000"))) | ||
| [datetime.datetime(1997, 9, 10, 9, 0), | ||
| datetime.datetime(1997, 9, 11, 9, 0), | ||
| datetime.datetime(1997, 9, 12, 9, 0), | ||
| datetime.datetime(1997, 9, 13, 9, 0), | ||
| datetime.datetime(1997, 9, 14, 9, 0), | ||
| datetime.datetime(1997, 9, 15, 9, 0), | ||
| datetime.datetime(1999, 3, 10, 9, 0), | ||
| datetime.datetime(1999, 3, 11, 9, 0), | ||
| datetime.datetime(1999, 3, 12, 9, 0), | ||
| datetime.datetime(1999, 3, 13, 9, 0)] | ||
| Every Tuesday, every other month, 6 occurences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, interval=2, count=6, byweekday=TU, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 23, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 11, 4, 9, 0)] | ||
| Yearly in June and July for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=4, bymonth=(6,7), | ||
| ... dtstart=parse("19970610T090000"))) | ||
| [datetime.datetime(1997, 6, 10, 9, 0), | ||
| datetime.datetime(1997, 7, 10, 9, 0), | ||
| datetime.datetime(1998, 6, 10, 9, 0), | ||
| datetime.datetime(1998, 7, 10, 9, 0)] | ||
| Every 3rd year on the 1st, 100th and 200th day for 4 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=4, interval=3, byyearday=(1,100,200), | ||
| ... dtstart=parse("19970101T090000"))) | ||
| [datetime.datetime(1997, 1, 1, 9, 0), | ||
| datetime.datetime(1997, 4, 10, 9, 0), | ||
| datetime.datetime(1997, 7, 19, 9, 0), | ||
| datetime.datetime(2000, 1, 1, 9, 0)] | ||
| Every 20th Monday of the year, 3 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=3, byweekday=MO(20), | ||
| ... dtstart=parse("19970519T090000"))) | ||
| [datetime.datetime(1997, 5, 19, 9, 0), | ||
| datetime.datetime(1998, 5, 18, 9, 0), | ||
| datetime.datetime(1999, 5, 17, 9, 0)] | ||
| Monday of week number 20 (where the default start of the week is Monday), | ||
| 3 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=3, byweekno=20, byweekday=MO, | ||
| ... dtstart=parse("19970512T090000"))) | ||
| [datetime.datetime(1997, 5, 12, 9, 0), | ||
| datetime.datetime(1998, 5, 11, 9, 0), | ||
| datetime.datetime(1999, 5, 17, 9, 0)] | ||
| The week number 1 may be in the last year. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=3, byweekno=1, byweekday=MO, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 12, 29, 9, 0), | ||
| datetime.datetime(1999, 1, 4, 9, 0), | ||
| datetime.datetime(2000, 1, 3, 9, 0)] | ||
| And the week numbers greater than 51 may be in the next year. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=3, byweekno=52, byweekday=SU, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 12, 28, 9, 0), | ||
| datetime.datetime(1998, 12, 27, 9, 0), | ||
| datetime.datetime(2000, 1, 2, 9, 0)] | ||
| Only some years have week number 53: | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=3, byweekno=53, byweekday=MO, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1998, 12, 28, 9, 0), | ||
| datetime.datetime(2004, 12, 27, 9, 0), | ||
| datetime.datetime(2009, 12, 28, 9, 0)] | ||
| Every Friday the 13th, 4 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=4, byweekday=FR, bymonthday=13, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1998, 2, 13, 9, 0), | ||
| datetime.datetime(1998, 3, 13, 9, 0), | ||
| datetime.datetime(1998, 11, 13, 9, 0), | ||
| datetime.datetime(1999, 8, 13, 9, 0)] | ||
| Every four years, the first Tuesday after a Monday in November, | ||
| 3 occurrences (U.S. Presidential Election day): | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, interval=4, count=3, bymonth=11, | ||
| ... byweekday=TU, bymonthday=(2,3,4,5,6,7,8), | ||
| ... dtstart=parse("19961105T090000"))) | ||
| [datetime.datetime(1996, 11, 5, 9, 0), | ||
| datetime.datetime(2000, 11, 7, 9, 0), | ||
| datetime.datetime(2004, 11, 2, 9, 0)] | ||
| The 3rd instance into the month of one of Tuesday, Wednesday or | ||
| Thursday, for the next 3 months: | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=3, byweekday=(TU,WE,TH), | ||
| ... bysetpos=3, dtstart=parse("19970904T090000"))) | ||
| [datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 10, 7, 9, 0), | ||
| datetime.datetime(1997, 11, 6, 9, 0)] | ||
| The 2nd to last weekday of the month, 3 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=3, byweekday=(MO,TU,WE,TH,FR), | ||
| ... bysetpos=-2, dtstart=parse("19970929T090000"))) | ||
| [datetime.datetime(1997, 9, 29, 9, 0), | ||
| datetime.datetime(1997, 10, 30, 9, 0), | ||
| datetime.datetime(1997, 11, 27, 9, 0)] | ||
| Every 3 hours from 9:00 AM to 5:00 PM on a specific day. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(HOURLY, interval=3, | ||
| ... dtstart=parse("19970902T090000"), | ||
| ... until=parse("19970902T170000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 2, 12, 0), | ||
| datetime.datetime(1997, 9, 2, 15, 0)] | ||
| Every 15 minutes for 6 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MINUTELY, interval=15, count=6, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 2, 9, 15), | ||
| datetime.datetime(1997, 9, 2, 9, 30), | ||
| datetime.datetime(1997, 9, 2, 9, 45), | ||
| datetime.datetime(1997, 9, 2, 10, 0), | ||
| datetime.datetime(1997, 9, 2, 10, 15)] | ||
| Every hour and a half for 4 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MINUTELY, interval=90, count=4, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 2, 10, 30), | ||
| datetime.datetime(1997, 9, 2, 12, 0), | ||
| datetime.datetime(1997, 9, 2, 13, 30)] | ||
| Every 20 minutes from 9:00 AM to 4:40 PM for two days. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||
| >>> list(rrule(MINUTELY, interval=20, count=48, | ||
| ... byhour=range(9,17), byminute=(0,20,40), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 2, 9, 20), | ||
| ... | ||
| datetime.datetime(1997, 9, 2, 16, 20), | ||
| datetime.datetime(1997, 9, 2, 16, 40), | ||
| datetime.datetime(1997, 9, 3, 9, 0), | ||
| datetime.datetime(1997, 9, 3, 9, 20), | ||
| ... | ||
| datetime.datetime(1997, 9, 3, 16, 20), | ||
| datetime.datetime(1997, 9, 3, 16, 40)] | ||
| An example where the days generated makes a difference because of `wkst`. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, interval=2, count=4, | ||
| ... byweekday=(TU,SU), wkst=MO, | ||
| ... dtstart=parse("19970805T090000"))) | ||
| [datetime.datetime(1997, 8, 5, 9, 0), | ||
| datetime.datetime(1997, 8, 10, 9, 0), | ||
| datetime.datetime(1997, 8, 19, 9, 0), | ||
| datetime.datetime(1997, 8, 24, 9, 0)] | ||
| >>> list(rrule(WEEKLY, interval=2, count=4, | ||
| ... byweekday=(TU,SU), wkst=SU, | ||
| ... dtstart=parse("19970805T090000"))) | ||
| [datetime.datetime(1997, 8, 5, 9, 0), | ||
| datetime.datetime(1997, 8, 17, 9, 0), | ||
| datetime.datetime(1997, 8, 19, 9, 0), | ||
| datetime.datetime(1997, 8, 31, 9, 0)] | ||
| rruleset examples | ||
| ----------------- | ||
| Daily, for 7 days, jumping Saturday and Sunday occurrences. | ||
| .. testsetup:: rruleset | ||
| import datetime | ||
| from dateutil.parser import parse | ||
| from dateutil.rrule import rrule, rruleset | ||
| from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY | ||
| from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU | ||
| import pprint | ||
| import sys | ||
| sys.displayhook = pprint.pprint | ||
| .. doctest:: rruleset | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> set = rruleset() | ||
| >>> set.rrule(rrule(DAILY, count=7, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| >>> set.exrule(rrule(YEARLY, byweekday=(SA,SU), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| >>> list(set) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 3, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 5, 9, 0), | ||
| datetime.datetime(1997, 9, 8, 9, 0)] | ||
| Weekly, for 4 weeks, plus one time on day 7, and not on day 16. | ||
| .. doctest:: rruleset | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> set = rruleset() | ||
| >>> set.rrule(rrule(WEEKLY, count=4, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| >>> set.rdate(datetime.datetime(1997, 9, 7, 9, 0)) | ||
| >>> set.exdate(datetime.datetime(1997, 9, 16, 9, 0)) | ||
| >>> list(set) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 7, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 23, 9, 0)] | ||
| rrulestr() examples | ||
| ------------------- | ||
| Every 10 days, 5 occurrences. | ||
| .. testsetup:: rrulestr | ||
| from dateutil.parser import parse | ||
| from dateutil.rrule import rruleset, rrulestr | ||
| import pprint | ||
| import sys | ||
| sys.displayhook = pprint.pprint | ||
| .. doctest:: rrulestr | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrulestr(""" | ||
| ... DTSTART:19970902T090000 | ||
| ... RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 | ||
| ... """)) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 12, 9, 0), | ||
| datetime.datetime(1997, 9, 22, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 12, 9, 0)] | ||
| Same thing, but passing only the `RRULE` value. | ||
| .. doctest:: rrulestr | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5", | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 12, 9, 0), | ||
| datetime.datetime(1997, 9, 22, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 12, 9, 0)] | ||
| Notice that when using a single rule, it returns an | ||
| `rrule` instance, unless `forceset` was used. | ||
| .. doctest:: rrulestr | ||
| :options: +ELLIPSIS | ||
| >>> rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5") | ||
| <dateutil.rrule.rrule object at 0x...> | ||
| >>> rrulestr(""" | ||
| ... DTSTART:19970902T090000 | ||
| ... RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 | ||
| ... """) | ||
| <dateutil.rrule.rrule object at 0x...> | ||
| >>> rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5", forceset=True) | ||
| <dateutil.rrule.rruleset object at 0x...> | ||
| But when an `rruleset` is needed, it is automatically used. | ||
| .. doctest:: rrulestr | ||
| :options: +ELLIPSIS | ||
| >>> rrulestr(""" | ||
| ... DTSTART:19970902T090000 | ||
| ... RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 | ||
| ... RRULE:FREQ=DAILY;INTERVAL=5;COUNT=3 | ||
| ... """) | ||
| <dateutil.rrule.rruleset object at 0x...> | ||
| parse examples | ||
| -------------- | ||
| The following code will prepare the environment: | ||
| .. doctest:: tz | ||
| >>> from dateutil.parser import * | ||
| >>> from dateutil.tz import * | ||
| >>> from datetime import * | ||
| >>> TZOFFSETS = {"BRST": -10800} | ||
| >>> BRSTTZ = tzoffset("BRST", -10800) | ||
| >>> DEFAULT = datetime(2003, 9, 25) | ||
| Some simple examples based on the `date` command, using the | ||
| `ZOFFSET` dictionary to provide the BRST timezone offset. | ||
| .. doctest:: tz | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> parse("Thu Sep 25 10:36:28 BRST 2003", tzinfos=TZOFFSETS) | ||
| datetime.datetime(2003, 9, 25, 10, 36, 28, | ||
| tzinfo=tzoffset('BRST', -10800)) | ||
| >>> parse("2003 10:36:28 BRST 25 Sep Thu", tzinfos=TZOFFSETS) | ||
| datetime.datetime(2003, 9, 25, 10, 36, 28, | ||
| tzinfo=tzoffset('BRST', -10800)) | ||
| Notice that since BRST is my local timezone, parsing it without | ||
| further timezone settings will yield a `tzlocal` timezone. | ||
| .. doctest:: tz | ||
| >>> parse("Thu Sep 25 10:36:28 BRST 2003") | ||
| datetime.datetime(2003, 9, 25, 10, 36, 28, tzinfo=tzlocal()) | ||
| We can also ask to ignore the timezone explicitly: | ||
| .. doctest:: tz | ||
| >>> parse("Thu Sep 25 10:36:28 BRST 2003", ignoretz=True) | ||
| datetime.datetime(2003, 9, 25, 10, 36, 28) | ||
| That's the same as processing a string without timezone: | ||
| .. doctest:: tz | ||
| >>> parse("Thu Sep 25 10:36:28 2003") | ||
| datetime.datetime(2003, 9, 25, 10, 36, 28) | ||
| Without the year, but passing our `DEFAULT` datetime to return | ||
| the same year, no mattering what year we currently are in: | ||
| .. doctest:: tz | ||
| >>> parse("Thu Sep 25 10:36:28", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 10, 36, 28) | ||
| Strip it further: | ||
| .. doctest:: tz | ||
| >>> parse("Thu Sep 10:36:28", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 10, 36, 28) | ||
| >>> parse("Thu 10:36:28", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 10, 36, 28) | ||
| >>> parse("Thu 10:36", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 10, 36) | ||
| >>> parse("10:36", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 10, 36) | ||
| Strip in a different way: | ||
| .. doctest:: tz | ||
| >>> parse("Thu Sep 25 2003") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("Sep 25 2003") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("Sep 2003", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("Sep", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("2003", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| Another format, based on `date -R` (RFC822): | ||
| .. doctest:: tz | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> parse("Thu, 25 Sep 2003 10:49:41 -0300") | ||
| datetime.datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=tzoffset(None, -10800)) | ||
| ISO format: | ||
| .. doctest:: tz | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> parse("2003-09-25T10:49:41.5-03:00") | ||
| datetime.datetime(2003, 9, 25, 10, 49, 41, 500000, | ||
| tzinfo=tzoffset(None, -10800)) | ||
| Some variations: | ||
| .. doctest:: tz | ||
| >>> parse("2003-09-25T10:49:41") | ||
| datetime.datetime(2003, 9, 25, 10, 49, 41) | ||
| >>> parse("2003-09-25T10:49") | ||
| datetime.datetime(2003, 9, 25, 10, 49) | ||
| >>> parse("2003-09-25T10") | ||
| datetime.datetime(2003, 9, 25, 10, 0) | ||
| >>> parse("2003-09-25") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| ISO format, without separators: | ||
| .. doctest:: tz | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> parse("20030925T104941.5-0300") | ||
| datetime.datetime(2003, 9, 25, 10, 49, 41, 500000, | ||
| tzinfo=tzoffset(None, -10800)) | ||
| >>> parse("20030925T104941-0300") | ||
| datetime.datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=tzoffset(None, -10800)) | ||
| >>> parse("20030925T104941") | ||
| datetime.datetime(2003, 9, 25, 10, 49, 41) | ||
| >>> parse("20030925T1049") | ||
| datetime.datetime(2003, 9, 25, 10, 49) | ||
| >>> parse("20030925T10") | ||
| datetime.datetime(2003, 9, 25, 10, 0) | ||
| >>> parse("20030925") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| Everything together. | ||
| .. doctest:: tz | ||
| >>> parse("199709020900") | ||
| datetime.datetime(1997, 9, 2, 9, 0) | ||
| >>> parse("19970902090059") | ||
| datetime.datetime(1997, 9, 2, 9, 0, 59) | ||
| Different date orderings: | ||
| .. doctest:: tz | ||
| >>> parse("2003-09-25") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("2003-Sep-25") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("25-Sep-2003") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("Sep-25-2003") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("09-25-2003") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("25-09-2003") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| Check some ambiguous dates: | ||
| .. doctest:: tz | ||
| >>> parse("10-09-2003") | ||
| datetime.datetime(2003, 10, 9, 0, 0) | ||
| >>> parse("10-09-2003", dayfirst=True) | ||
| datetime.datetime(2003, 9, 10, 0, 0) | ||
| >>> parse("10-09-03") | ||
| datetime.datetime(2003, 10, 9, 0, 0) | ||
| >>> parse("10-09-03", yearfirst=True) | ||
| datetime.datetime(2010, 9, 3, 0, 0) | ||
| Other date separators are allowed: | ||
| .. doctest:: tz | ||
| >>> parse("2003.Sep.25") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("2003/09/25") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| Even with spaces: | ||
| .. doctest:: tz | ||
| >>> parse("2003 Sep 25") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("2003 09 25") | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| Hours with letters work: | ||
| .. doctest:: tz | ||
| >>> parse("10h36m28.5s", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 10, 36, 28, 500000) | ||
| >>> parse("01s02h03m", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 2, 3, 1) | ||
| >>> parse("01h02m03", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 1, 2, 3) | ||
| >>> parse("01h02", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 1, 2) | ||
| >>> parse("01h02s", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 1, 0, 2) | ||
| With AM/PM: | ||
| .. doctest:: tz | ||
| >>> parse("10h am", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 10, 0) | ||
| >>> parse("10pm", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 22, 0) | ||
| >>> parse("12:00am", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| >>> parse("12pm", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 12, 0) | ||
| Some special treating for ''pertain'' relations: | ||
| .. doctest:: tz | ||
| >>> parse("Sep 03", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 3, 0, 0) | ||
| >>> parse("Sep of 03", default=DEFAULT) | ||
| datetime.datetime(2003, 9, 25, 0, 0) | ||
| Fuzzy parsing: | ||
| .. doctest:: tz | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> s = "Today is 25 of September of 2003, exactly " \ | ||
| ... "at 10:49:41 with timezone -03:00." | ||
| >>> parse(s, fuzzy=True) | ||
| datetime.datetime(2003, 9, 25, 10, 49, 41, | ||
| tzinfo=tzoffset(None, -10800)) | ||
| Other random formats: | ||
| .. doctest:: tz | ||
| >>> parse("Wed, July 10, '96") | ||
| datetime.datetime(1996, 7, 10, 0, 0) | ||
| >>> parse("1996.07.10 AD at 15:08:56 PDT", ignoretz=True) | ||
| datetime.datetime(1996, 7, 10, 15, 8, 56) | ||
| >>> parse("Tuesday, April 12, 1952 AD 3:30:42pm PST", ignoretz=True) | ||
| datetime.datetime(1952, 4, 12, 15, 30, 42) | ||
| >>> parse("November 5, 1994, 8:15:30 am EST", ignoretz=True) | ||
| datetime.datetime(1994, 11, 5, 8, 15, 30) | ||
| >>> parse("3rd of May 2001") | ||
| datetime.datetime(2001, 5, 3, 0, 0) | ||
| >>> parse("5:50 A.M. on June 13, 1990") | ||
| datetime.datetime(1990, 6, 13, 5, 50) | ||
| tzutc examples | ||
| -------------- | ||
| .. doctest:: tzutc | ||
| >>> from datetime import * | ||
| >>> from dateutil.tz import * | ||
| >>> datetime.now() | ||
| datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) | ||
| >>> datetime.now(tzutc()) | ||
| datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) | ||
| >>> datetime.now(tzutc()).tzname() | ||
| 'UTC' | ||
| tzoffset examples | ||
| ----------------- | ||
| .. doctest:: tzoffset | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> from datetime import * | ||
| >>> from dateutil.tz import * | ||
| >>> datetime.now(tzoffset("BRST", -10800)) | ||
| datetime.datetime(2003, 9, 27, 9, 52, 43, 624904, | ||
| tzinfo=tzinfo=tzoffset('BRST', -10800)) | ||
| >>> datetime.now(tzoffset("BRST", -10800)).tzname() | ||
| 'BRST' | ||
| >>> datetime.now(tzoffset("BRST", -10800)).astimezone(tzutc()) | ||
| datetime.datetime(2003, 9, 27, 12, 53, 11, 446419, | ||
| tzinfo=tzutc()) | ||
| tzlocal examples | ||
| ---------------- | ||
| .. doctest:: tzlocal | ||
| >>> from datetime import * | ||
| >>> from dateutil.tz import * | ||
| >>> datetime.now(tzlocal()) | ||
| datetime.datetime(2003, 9, 27, 10, 1, 43, 673605, | ||
| tzinfo=tzlocal()) | ||
| >>> datetime.now(tzlocal()).tzname() | ||
| 'BRST' | ||
| >>> datetime.now(tzlocal()).astimezone(tzoffset(None, 0)) | ||
| datetime.datetime(2003, 9, 27, 13, 3, 0, 11493, | ||
| tzinfo=tzoffset(None, 0)) | ||
| tzstr examples | ||
| -------------- | ||
| Here are examples of the recognized formats: | ||
| * `EST5EDT` | ||
| * `EST5EDT4,M4.1.0/02:00:00,M10-5-0/02:00` | ||
| * `EST5EDT4,95/02:00:00,298/02:00` | ||
| * `EST5EDT4,J96/02:00:00,J299/02:00` | ||
| Notice that if daylight information is not present, but a | ||
| daylight abbreviation was provided, `tzstr` will follow the | ||
| convention of using the first sunday of April to start daylight | ||
| saving, and the last sunday of October to end it. If start or | ||
| end time is not present, 2AM will be used, and if the daylight | ||
| offset is not present, the standard offset plus one hour will | ||
| be used. This convention is the same as used in the GNU libc. | ||
| This also means that some of the above examples are exactly | ||
| equivalent, and all of these examples are equivalent | ||
| in the year of 2003. | ||
| Here is the example mentioned in the | ||
| [https://docs.python.org/3/library/time.html time module documentation]. | ||
| .. testsetup:: tzstr | ||
| import os | ||
| import time | ||
| from datetime import datetime | ||
| from dateutil.tz import tzstr | ||
| .. doctest:: tzstr | ||
| >>> os.environ['TZ'] = 'EST+05EDT,M4.1.0,M10.5.0' | ||
| >>> time.tzset() | ||
| >>> time.strftime('%X %x %Z') | ||
| '02:07:36 05/08/03 EDT' | ||
| >>> os.environ['TZ'] = 'AEST-10AEDT-11,M10.5.0,M3.5.0' | ||
| >>> time.tzset() | ||
| >>> time.strftime('%X %x %Z') | ||
| '16:08:12 05/08/03 AEST' | ||
| And here is an example showing the same information using `tzstr`, | ||
| without touching system settings. | ||
| .. doctest:: tzstr | ||
| >>> tz1 = tzstr('EST+05EDT,M4.1.0,M10.5.0') | ||
| >>> tz2 = tzstr('AEST-10AEDT-11,M10.5.0,M3.5.0') | ||
| >>> dt = datetime(2003, 5, 8, 2, 7, 36, tzinfo=tz1) | ||
| >>> dt.strftime('%X %x %Z') | ||
| '02:07:36 05/08/03 EDT' | ||
| >>> dt.astimezone(tz2).strftime('%X %x %Z') | ||
| '16:07:36 05/08/03 AEST' | ||
| Are these really equivalent? | ||
| .. doctest:: tzstr | ||
| >>> tzstr('EST5EDT') == tzstr('EST5EDT,M4.1.0,M10.5.0') | ||
| True | ||
| Check the daylight limit. | ||
| .. doctest:: tzstr | ||
| >>> tz = tzstr('EST+05EDT,M4.1.0,M10.5.0') | ||
| >>> datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname() | ||
| 'EST' | ||
| >>> datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname() | ||
| 'EDT' | ||
| >>> datetime(2003, 10, 26, 0, 59, tzinfo=tz).tzname() | ||
| 'EDT' | ||
| >>> datetime(2003, 10, 26, 2, 00, tzinfo=tz).tzname() | ||
| 'EST' | ||
| tzrange examples | ||
| ---------------- | ||
| .. testsetup:: tzrange | ||
| from dateutil.tz import tzrange, tzstr | ||
| .. doctest:: tzrange | ||
| >>> tzstr('EST5EDT') == tzrange("EST", -18000, "EDT") | ||
| True | ||
| >>> from dateutil.relativedelta import * | ||
| >>> range1 = tzrange("EST", -18000, "EDT") | ||
| >>> range2 = tzrange("EST", -18000, "EDT", -14400, | ||
| ... relativedelta(hours=+2, month=4, day=1, | ||
| ... weekday=SU(+1)), | ||
| ... relativedelta(hours=+1, month=10, day=31, | ||
| ... weekday=SU(-1))) | ||
| >>> tzstr('EST5EDT') == range1 == range2 | ||
| True | ||
| Notice a minor detail in the last example: while the DST should end | ||
| at 2AM, the delta will catch 1AM. That's because the daylight saving | ||
| time should end at 2AM standard time (the difference between STD and | ||
| DST is 1h in the given example) instead of the DST time. That's how | ||
| the `tzinfo` subtypes should deal with the extra hour that happens | ||
| when going back to the standard time. Check | ||
| [https://docs.python.org/3/library/datetime.html#datetime.tzinfo tzinfo documentation] | ||
| for more information. | ||
| tzfile examples | ||
| --------------- | ||
| .. testsetup:: tzfile | ||
| from datetime import datetime | ||
| from dateutil.tz import tzfile, tzutc | ||
| .. doctest:: tzfile | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> tz = tzfile("/etc/localtime") | ||
| >>> datetime.now(tz) | ||
| datetime.datetime(2003, 9, 27, 12, 3, 48, 392138, | ||
| tzinfo=tzfile('/etc/localtime')) | ||
| >>> datetime.now(tz).astimezone(tzutc()) | ||
| datetime.datetime(2003, 9, 27, 15, 3, 53, 70863, | ||
| tzinfo=tzutc()) | ||
| >>> datetime.now(tz).tzname() | ||
| 'BRST' | ||
| >>> datetime(2003, 1, 1, tzinfo=tz).tzname() | ||
| 'BRDT' | ||
| Check the daylight limit. | ||
| .. doctest:: tzfile | ||
| >>> tz = tzfile('/usr/share/zoneinfo/EST5EDT') | ||
| >>> datetime(2003, 4, 6, 1, 59, tzinfo=tz).tzname() | ||
| 'EST' | ||
| >>> datetime(2003, 4, 6, 2, 00, tzinfo=tz).tzname() | ||
| 'EDT' | ||
| >>> datetime(2003, 10, 26, 0, 59, tzinfo=tz).tzname() | ||
| 'EDT' | ||
| >>> datetime(2003, 10, 26, 1, 00, tzinfo=tz).tzname() | ||
| 'EST' | ||
| tzical examples | ||
| --------------- | ||
| Here is a sample file extracted from the RFC. This file defines | ||
| the `EST5EDT` timezone, and will be used in the following example. | ||
| .. include:: samples/EST5EDT.ics | ||
| :literal: | ||
| And here is an example exploring a `tzical` type: | ||
| .. doctest:: tzfile | ||
| >>> from dateutil.tz import *; from datetime import * | ||
| >>> tz = tzical('samples/EST5EDT.ics') | ||
| >>> tz.keys() | ||
| ['US-Eastern'] | ||
| >>> est = tz.get('US-Eastern') | ||
| >>> est | ||
| <tzicalvtz 'US-Eastern'> | ||
| >>> datetime.now(est) | ||
| datetime.datetime(2003, 10, 6, 19, 44, 18, 667987, | ||
| tzinfo=<tzicalvtz 'US-Eastern'>) | ||
| >>> est == tz.get() | ||
| True | ||
| Let's check the daylight ranges, as usual: | ||
| .. doctest:: tzfile | ||
| >>> datetime(2003, 4, 6, 1, 59, tzinfo=est).tzname() | ||
| 'EST' | ||
| >>> datetime(2003, 4, 6, 2, 00, tzinfo=est).tzname() | ||
| 'EDT' | ||
| >>> datetime(2003, 10, 26, 0, 59, tzinfo=est).tzname() | ||
| 'EDT' | ||
| >>> datetime(2003, 10, 26, 1, 00, tzinfo=est).tzname() | ||
| 'EST' | ||
| tzwin examples | ||
| -------------- | ||
| .. doctest:: tzwin | ||
| >>> tz = tzwin("E. South America Standard Time") | ||
| tzwinlocal examples | ||
| ------------------- | ||
| .. doctest:: tzwinlocal | ||
| >>> tz = tzwinlocal() | ||
| # vim:ts=4:sw=4:et |
| .. dateutil documentation master file, created by | ||
| sphinx-quickstart on Thu Nov 20 23:18:41 2014. | ||
| You can adapt this file completely to your liking, but it should at least | ||
| contain the root `toctree` directive. | ||
| .. include:: ../README.rst | ||
| Documentation | ||
| ============= | ||
| Contents: | ||
| .. toctree:: | ||
| :maxdepth: 2 | ||
| self | ||
| easter | ||
| parser | ||
| relativedelta | ||
| rrule | ||
| tz | ||
| utils | ||
| zoneinfo | ||
| examples | ||
| Indices and tables | ||
| ================== | ||
| * :ref:`genindex` | ||
| * :ref:`modindex` | ||
| * :ref:`search` |
+242
| @ECHO OFF | ||
| REM Command file for Sphinx documentation | ||
| if "%SPHINXBUILD%" == "" ( | ||
| set SPHINXBUILD=sphinx-build | ||
| ) | ||
| set BUILDDIR=_build | ||
| set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% . | ||
| set I18NSPHINXOPTS=%SPHINXOPTS% . | ||
| if NOT "%PAPER%" == "" ( | ||
| set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% | ||
| set I18NSPHINXOPTS=-D latex_paper_size=%PAPER% %I18NSPHINXOPTS% | ||
| ) | ||
| if "%1" == "" goto help | ||
| if "%1" == "help" ( | ||
| :help | ||
| echo.Please use `make ^<target^>` where ^<target^> is one of | ||
| echo. html to make standalone HTML files | ||
| echo. dirhtml to make HTML files named index.html in directories | ||
| echo. singlehtml to make a single large HTML file | ||
| echo. pickle to make pickle files | ||
| echo. json to make JSON files | ||
| echo. htmlhelp to make HTML files and a HTML help project | ||
| echo. qthelp to make HTML files and a qthelp project | ||
| echo. devhelp to make HTML files and a Devhelp project | ||
| echo. epub to make an epub | ||
| echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter | ||
| echo. text to make text files | ||
| echo. man to make manual pages | ||
| echo. texinfo to make Texinfo files | ||
| echo. gettext to make PO message catalogs | ||
| echo. changes to make an overview over all changed/added/deprecated items | ||
| echo. xml to make Docutils-native XML files | ||
| echo. pseudoxml to make pseudoxml-XML files for display purposes | ||
| echo. linkcheck to check all external links for integrity | ||
| echo. doctest to run all doctests embedded in the documentation if enabled | ||
| goto end | ||
| ) | ||
| if "%1" == "clean" ( | ||
| for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i | ||
| del /q /s %BUILDDIR%\* | ||
| goto end | ||
| ) | ||
| %SPHINXBUILD% 2> nul | ||
| if errorlevel 9009 ( | ||
| echo. | ||
| echo.The 'sphinx-build' command was not found. Make sure you have Sphinx | ||
| echo.installed, then set the SPHINXBUILD environment variable to point | ||
| echo.to the full path of the 'sphinx-build' executable. Alternatively you | ||
| echo.may add the Sphinx directory to PATH. | ||
| echo. | ||
| echo.If you don't have Sphinx installed, grab it from | ||
| echo.http://sphinx-doc.org/ | ||
| exit /b 1 | ||
| ) | ||
| if "%1" == "html" ( | ||
| %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The HTML pages are in %BUILDDIR%/html. | ||
| goto end | ||
| ) | ||
| if "%1" == "dirhtml" ( | ||
| %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. | ||
| goto end | ||
| ) | ||
| if "%1" == "singlehtml" ( | ||
| %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. | ||
| goto end | ||
| ) | ||
| if "%1" == "pickle" ( | ||
| %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished; now you can process the pickle files. | ||
| goto end | ||
| ) | ||
| if "%1" == "json" ( | ||
| %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished; now you can process the JSON files. | ||
| goto end | ||
| ) | ||
| if "%1" == "htmlhelp" ( | ||
| %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished; now you can run HTML Help Workshop with the ^ | ||
| .hhp project file in %BUILDDIR%/htmlhelp. | ||
| goto end | ||
| ) | ||
| if "%1" == "qthelp" ( | ||
| %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished; now you can run "qcollectiongenerator" with the ^ | ||
| .qhcp project file in %BUILDDIR%/qthelp, like this: | ||
| echo.^> qcollectiongenerator %BUILDDIR%\qthelp\dateutil.qhcp | ||
| echo.To view the help file: | ||
| echo.^> assistant -collectionFile %BUILDDIR%\qthelp\dateutil.ghc | ||
| goto end | ||
| ) | ||
| if "%1" == "devhelp" ( | ||
| %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. | ||
| goto end | ||
| ) | ||
| if "%1" == "epub" ( | ||
| %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The epub file is in %BUILDDIR%/epub. | ||
| goto end | ||
| ) | ||
| if "%1" == "latex" ( | ||
| %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. | ||
| goto end | ||
| ) | ||
| if "%1" == "latexpdf" ( | ||
| %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex | ||
| cd %BUILDDIR%/latex | ||
| make all-pdf | ||
| cd %BUILDDIR%/.. | ||
| echo. | ||
| echo.Build finished; the PDF files are in %BUILDDIR%/latex. | ||
| goto end | ||
| ) | ||
| if "%1" == "latexpdfja" ( | ||
| %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex | ||
| cd %BUILDDIR%/latex | ||
| make all-pdf-ja | ||
| cd %BUILDDIR%/.. | ||
| echo. | ||
| echo.Build finished; the PDF files are in %BUILDDIR%/latex. | ||
| goto end | ||
| ) | ||
| if "%1" == "text" ( | ||
| %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The text files are in %BUILDDIR%/text. | ||
| goto end | ||
| ) | ||
| if "%1" == "man" ( | ||
| %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The manual pages are in %BUILDDIR%/man. | ||
| goto end | ||
| ) | ||
| if "%1" == "texinfo" ( | ||
| %SPHINXBUILD% -b texinfo %ALLSPHINXOPTS% %BUILDDIR%/texinfo | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The Texinfo files are in %BUILDDIR%/texinfo. | ||
| goto end | ||
| ) | ||
| if "%1" == "gettext" ( | ||
| %SPHINXBUILD% -b gettext %I18NSPHINXOPTS% %BUILDDIR%/locale | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The message catalogs are in %BUILDDIR%/locale. | ||
| goto end | ||
| ) | ||
| if "%1" == "changes" ( | ||
| %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.The overview file is in %BUILDDIR%/changes. | ||
| goto end | ||
| ) | ||
| if "%1" == "linkcheck" ( | ||
| %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Link check complete; look for any errors in the above output ^ | ||
| or in %BUILDDIR%/linkcheck/output.txt. | ||
| goto end | ||
| ) | ||
| if "%1" == "doctest" ( | ||
| %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Testing of doctests in the sources finished, look at the ^ | ||
| results in %BUILDDIR%/doctest/output.txt. | ||
| goto end | ||
| ) | ||
| if "%1" == "xml" ( | ||
| %SPHINXBUILD% -b xml %ALLSPHINXOPTS% %BUILDDIR%/xml | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The XML files are in %BUILDDIR%/xml. | ||
| goto end | ||
| ) | ||
| if "%1" == "pseudoxml" ( | ||
| %SPHINXBUILD% -b pseudoxml %ALLSPHINXOPTS% %BUILDDIR%/pseudoxml | ||
| if errorlevel 1 exit /b 1 | ||
| echo. | ||
| echo.Build finished. The pseudo-XML files are in %BUILDDIR%/pseudoxml. | ||
| goto end | ||
| ) | ||
| :end |
+177
| # Makefile for Sphinx documentation | ||
| # | ||
| # You can set these variables from the command line. | ||
| SPHINXOPTS = | ||
| SPHINXBUILD = sphinx-build | ||
| PAPER = | ||
| BUILDDIR = _build | ||
| # User-friendly check for sphinx-build | ||
| ifeq ($(shell which $(SPHINXBUILD) >/dev/null 2>&1; echo $$?), 1) | ||
| $(error The '$(SPHINXBUILD)' command was not found. Make sure you have Sphinx installed, then set the SPHINXBUILD environment variable to point to the full path of the '$(SPHINXBUILD)' executable. Alternatively you can add the directory with the executable to your PATH. If you don't have Sphinx installed, grab it from http://sphinx-doc.org/) | ||
| endif | ||
| # Internal variables. | ||
| PAPEROPT_a4 = -D latex_paper_size=a4 | ||
| PAPEROPT_letter = -D latex_paper_size=letter | ||
| ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | ||
| # the i18n builder cannot share the environment and doctrees with the others | ||
| I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . | ||
| .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest gettext | ||
| help: | ||
| @echo "Please use \`make <target>' where <target> is one of" | ||
| @echo " html to make standalone HTML files" | ||
| @echo " dirhtml to make HTML files named index.html in directories" | ||
| @echo " singlehtml to make a single large HTML file" | ||
| @echo " pickle to make pickle files" | ||
| @echo " json to make JSON files" | ||
| @echo " htmlhelp to make HTML files and a HTML help project" | ||
| @echo " qthelp to make HTML files and a qthelp project" | ||
| @echo " devhelp to make HTML files and a Devhelp project" | ||
| @echo " epub to make an epub" | ||
| @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" | ||
| @echo " latexpdf to make LaTeX files and run them through pdflatex" | ||
| @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx" | ||
| @echo " text to make text files" | ||
| @echo " man to make manual pages" | ||
| @echo " texinfo to make Texinfo files" | ||
| @echo " info to make Texinfo files and run them through makeinfo" | ||
| @echo " gettext to make PO message catalogs" | ||
| @echo " changes to make an overview of all changed/added/deprecated items" | ||
| @echo " xml to make Docutils-native XML files" | ||
| @echo " pseudoxml to make pseudoxml-XML files for display purposes" | ||
| @echo " linkcheck to check all external links for integrity" | ||
| @echo " doctest to run all doctests embedded in the documentation (if enabled)" | ||
| clean: | ||
| rm -rf $(BUILDDIR)/* | ||
| html: | ||
| $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html | ||
| @echo | ||
| @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." | ||
| dirhtml: | ||
| $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml | ||
| @echo | ||
| @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." | ||
| singlehtml: | ||
| $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml | ||
| @echo | ||
| @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." | ||
| pickle: | ||
| $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle | ||
| @echo | ||
| @echo "Build finished; now you can process the pickle files." | ||
| json: | ||
| $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json | ||
| @echo | ||
| @echo "Build finished; now you can process the JSON files." | ||
| htmlhelp: | ||
| $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp | ||
| @echo | ||
| @echo "Build finished; now you can run HTML Help Workshop with the" \ | ||
| ".hhp project file in $(BUILDDIR)/htmlhelp." | ||
| qthelp: | ||
| $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp | ||
| @echo | ||
| @echo "Build finished; now you can run "qcollectiongenerator" with the" \ | ||
| ".qhcp project file in $(BUILDDIR)/qthelp, like this:" | ||
| @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/dateutil.qhcp" | ||
| @echo "To view the help file:" | ||
| @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/dateutil.qhc" | ||
| devhelp: | ||
| $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp | ||
| @echo | ||
| @echo "Build finished." | ||
| @echo "To view the help file:" | ||
| @echo "# mkdir -p $$HOME/.local/share/devhelp/dateutil" | ||
| @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/dateutil" | ||
| @echo "# devhelp" | ||
| epub: | ||
| $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub | ||
| @echo | ||
| @echo "Build finished. The epub file is in $(BUILDDIR)/epub." | ||
| latex: | ||
| $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | ||
| @echo | ||
| @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." | ||
| @echo "Run \`make' in that directory to run these through (pdf)latex" \ | ||
| "(use \`make latexpdf' here to do that automatically)." | ||
| latexpdf: | ||
| $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | ||
| @echo "Running LaTeX files through pdflatex..." | ||
| $(MAKE) -C $(BUILDDIR)/latex all-pdf | ||
| @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | ||
| latexpdfja: | ||
| $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex | ||
| @echo "Running LaTeX files through platex and dvipdfmx..." | ||
| $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja | ||
| @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." | ||
| text: | ||
| $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text | ||
| @echo | ||
| @echo "Build finished. The text files are in $(BUILDDIR)/text." | ||
| man: | ||
| $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man | ||
| @echo | ||
| @echo "Build finished. The manual pages are in $(BUILDDIR)/man." | ||
| texinfo: | ||
| $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | ||
| @echo | ||
| @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo." | ||
| @echo "Run \`make' in that directory to run these through makeinfo" \ | ||
| "(use \`make info' here to do that automatically)." | ||
| info: | ||
| $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo | ||
| @echo "Running Texinfo files through makeinfo..." | ||
| make -C $(BUILDDIR)/texinfo info | ||
| @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo." | ||
| gettext: | ||
| $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale | ||
| @echo | ||
| @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale." | ||
| changes: | ||
| $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes | ||
| @echo | ||
| @echo "The overview file is in $(BUILDDIR)/changes." | ||
| linkcheck: | ||
| $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck | ||
| @echo | ||
| @echo "Link check complete; look for any errors in the above output " \ | ||
| "or in $(BUILDDIR)/linkcheck/output.txt." | ||
| doctest: | ||
| $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest | ||
| @echo "Testing of doctests in the sources finished, look at the " \ | ||
| "results in $(BUILDDIR)/doctest/output.txt." | ||
| xml: | ||
| $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml | ||
| @echo | ||
| @echo "Build finished. The XML files are in $(BUILDDIR)/xml." | ||
| pseudoxml: | ||
| $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml | ||
| @echo | ||
| @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml." |
| ====== | ||
| parser | ||
| ====== | ||
| .. automodule:: dateutil.parser | ||
| .. automethod:: dateutil.parser.parse | ||
| .. autoclass:: dateutil.parser.parserinfo | ||
| :members: | ||
| :undoc-members: | ||
| .. automethod:: dateutil.parser.isoparse |
| ============= | ||
| relativedelta | ||
| ============= | ||
| .. automodule:: dateutil.relativedelta | ||
| :members: | ||
| :undoc-members: | ||
| .. testsetup:: relativedelta | ||
| Examples | ||
| -------- | ||
| >>> from datetime import *; from dateutil.relativedelta import * | ||
| >>> import calendar | ||
| >>> NOW = datetime(2003, 9, 17, 20, 54, 47, 282310) | ||
| >>> TODAY = date(2003, 9, 17) | ||
| Let's begin our trip:: | ||
| >>> from datetime import *; from dateutil.relativedelta import * | ||
| >>> import calendar | ||
| Store some values:: | ||
| >>> NOW = datetime.now() | ||
| >>> TODAY = date.today() | ||
| >>> NOW | ||
| datetime.datetime(2003, 9, 17, 20, 54, 47, 282310) | ||
| >>> TODAY | ||
| datetime.date(2003, 9, 17) | ||
| Next month | ||
| .. doctest:: relativedelta | ||
| >>> NOW+relativedelta(months=+1) | ||
| datetime.datetime(2003, 10, 17, 20, 54, 47, 282310) | ||
| Next month, plus one week. | ||
| .. doctest:: relativedelta | ||
| >>> NOW+relativedelta(months=+1, weeks=+1) | ||
| datetime.datetime(2003, 10, 24, 20, 54, 47, 282310) | ||
| Next month, plus one week, at 10am. | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(months=+1, weeks=+1, hour=10) | ||
| datetime.datetime(2003, 10, 24, 10, 0) | ||
| Here is another example using an absolute relativedelta. Notice the use of | ||
| year and month (both singular) which causes the values to be *replaced* in the | ||
| original datetime rather than performing an arithmetic operation on them. | ||
| .. doctest:: relativedelta | ||
| >>> NOW+relativedelta(year=1, month=1) | ||
| datetime.datetime(1, 1, 17, 20, 54, 47, 282310) | ||
| Let's try the other way around. Notice that the | ||
| hour setting we get in the relativedelta is relative, | ||
| since it's a difference, and the weeks parameter | ||
| has gone. | ||
| .. doctest:: relativedelta | ||
| >>> relativedelta(datetime(2003, 10, 24, 10, 0), TODAY) | ||
| relativedelta(months=+1, days=+7, hours=+10) | ||
| One month before one year. | ||
| .. doctest:: relativedelta | ||
| >>> NOW+relativedelta(years=+1, months=-1) | ||
| datetime.datetime(2004, 8, 17, 20, 54, 47, 282310) | ||
| How does it handle months with different numbers of days? | ||
| Notice that adding one month will never cross the month | ||
| boundary. | ||
| .. doctest:: relativedelta | ||
| >>> date(2003,1,27)+relativedelta(months=+1) | ||
| datetime.date(2003, 2, 27) | ||
| >>> date(2003,1,31)+relativedelta(months=+1) | ||
| datetime.date(2003, 2, 28) | ||
| >>> date(2003,1,31)+relativedelta(months=+2) | ||
| datetime.date(2003, 3, 31) | ||
| The logic for years is the same, even on leap years. | ||
| .. doctest:: relativedelta | ||
| >>> date(2000,2,28)+relativedelta(years=+1) | ||
| datetime.date(2001, 2, 28) | ||
| >>> date(2000,2,29)+relativedelta(years=+1) | ||
| datetime.date(2001, 2, 28) | ||
| >>> date(1999,2,28)+relativedelta(years=+1) | ||
| datetime.date(2000, 2, 28) | ||
| >>> date(1999,3,1)+relativedelta(years=+1) | ||
| datetime.date(2000, 3, 1) | ||
| >>> date(2001,2,28)+relativedelta(years=-1) | ||
| datetime.date(2000, 2, 28) | ||
| >>> date(2001,3,1)+relativedelta(years=-1) | ||
| datetime.date(2000, 3, 1) | ||
| Next friday | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(weekday=FR) | ||
| datetime.date(2003, 9, 19) | ||
| >>> TODAY+relativedelta(weekday=calendar.FRIDAY) | ||
| datetime.date(2003, 9, 19) | ||
| Last friday in this month. | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(day=31, weekday=FR(-1)) | ||
| datetime.date(2003, 9, 26) | ||
| Next wednesday (it's today!). | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(weekday=WE(+1)) | ||
| datetime.date(2003, 9, 17) | ||
| Next wednesday, but not today. | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(days=+1, weekday=WE(+1)) | ||
| datetime.date(2003, 9, 24) | ||
| Following | ||
| [http://www.cl.cam.ac.uk/~mgk25/iso-time.html ISO year week number notation] | ||
| find the first day of the 15th week of 1997. | ||
| .. doctest:: relativedelta | ||
| >>> datetime(1997,1,1)+relativedelta(day=4, weekday=MO(-1), weeks=+14) | ||
| datetime.datetime(1997, 4, 7, 0, 0) | ||
| How long ago has the millennium changed? | ||
| .. doctest:: relativedelta | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> relativedelta(NOW, date(2001,1,1)) | ||
| relativedelta(years=+2, months=+8, days=+16, | ||
| hours=+20, minutes=+54, seconds=+47, microseconds=+282310) | ||
| How old is John? | ||
| .. doctest:: relativedelta | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> johnbirthday = datetime(1978, 4, 5, 12, 0) | ||
| >>> relativedelta(NOW, johnbirthday) | ||
| relativedelta(years=+25, months=+5, days=+12, | ||
| hours=+8, minutes=+54, seconds=+47, microseconds=+282310) | ||
| It works with dates too. | ||
| .. doctest:: relativedelta | ||
| >>> relativedelta(TODAY, johnbirthday) | ||
| relativedelta(years=+25, months=+5, days=+11, hours=+12) | ||
| Obtain today's date using the yearday: | ||
| .. doctest:: relativedelta | ||
| >>> date(2003, 1, 1)+relativedelta(yearday=260) | ||
| datetime.date(2003, 9, 17) | ||
| We can use today's date, since yearday should be absolute | ||
| in the given year: | ||
| .. doctest:: relativedelta | ||
| >>> TODAY+relativedelta(yearday=260) | ||
| datetime.date(2003, 9, 17) | ||
| Last year it should be in the same day: | ||
| .. doctest:: relativedelta | ||
| >>> date(2002, 1, 1)+relativedelta(yearday=260) | ||
| datetime.date(2002, 9, 17) | ||
| But not in a leap year: | ||
| .. doctest:: relativedelta | ||
| >>> date(2000, 1, 1)+relativedelta(yearday=260) | ||
| datetime.date(2000, 9, 16) | ||
| We can use the non-leap year day to ignore this: | ||
| .. doctest:: relativedelta | ||
| >>> date(2000, 1, 1)+relativedelta(nlyearday=260) | ||
| datetime.date(2000, 9, 17) |
+670
| ===== | ||
| rrule | ||
| ===== | ||
| .. automodule:: dateutil.rrule | ||
| :members: | ||
| :undoc-members: | ||
| rrule examples | ||
| -------------- | ||
| These examples were converted from the RFC. | ||
| Prepare the environment. | ||
| .. testsetup:: rrule | ||
| from dateutil.rrule import * | ||
| from dateutil.parser import * | ||
| from datetime import * | ||
| import pprint | ||
| import sys | ||
| sys.displayhook = pprint.pprint | ||
| .. doctest:: rrule | ||
| >>> from dateutil.rrule import * | ||
| >>> from dateutil.parser import * | ||
| >>> from datetime import * | ||
| >>> import pprint | ||
| >>> import sys | ||
| >>> sys.displayhook = pprint.pprint | ||
| Daily, for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(DAILY, count=10, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 3, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 5, 9, 0), | ||
| datetime.datetime(1997, 9, 6, 9, 0), | ||
| datetime.datetime(1997, 9, 7, 9, 0), | ||
| datetime.datetime(1997, 9, 8, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 10, 9, 0), | ||
| datetime.datetime(1997, 9, 11, 9, 0)] | ||
| Daily until December 24, 1997 | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||
| >>> list(rrule(DAILY, | ||
| ... dtstart=parse("19970902T090000"), | ||
| ... until=parse("19971224T000000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 3, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| ... | ||
| datetime.datetime(1997, 12, 21, 9, 0), | ||
| datetime.datetime(1997, 12, 22, 9, 0), | ||
| datetime.datetime(1997, 12, 23, 9, 0)] | ||
| Every other day, 5 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(DAILY, interval=2, count=5, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 6, 9, 0), | ||
| datetime.datetime(1997, 9, 8, 9, 0), | ||
| datetime.datetime(1997, 9, 10, 9, 0)] | ||
| Every 10 days, 5 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(DAILY, interval=10, count=5, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 12, 9, 0), | ||
| datetime.datetime(1997, 9, 22, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 12, 9, 0)] | ||
| Everyday in January, for 3 years. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||
| >>> list(rrule(YEARLY, bymonth=1, byweekday=range(7), | ||
| ... dtstart=parse("19980101T090000"), | ||
| ... until=parse("20000131T090000"))) | ||
| [datetime.datetime(1998, 1, 1, 9, 0), | ||
| datetime.datetime(1998, 1, 2, 9, 0), | ||
| ... | ||
| datetime.datetime(1998, 1, 30, 9, 0), | ||
| datetime.datetime(1998, 1, 31, 9, 0), | ||
| datetime.datetime(1999, 1, 1, 9, 0), | ||
| datetime.datetime(1999, 1, 2, 9, 0), | ||
| ... | ||
| datetime.datetime(1999, 1, 30, 9, 0), | ||
| datetime.datetime(1999, 1, 31, 9, 0), | ||
| datetime.datetime(2000, 1, 1, 9, 0), | ||
| datetime.datetime(2000, 1, 2, 9, 0), | ||
| ... | ||
| datetime.datetime(2000, 1, 30, 9, 0), | ||
| datetime.datetime(2000, 1, 31, 9, 0)] | ||
| Same thing, in another way. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||
| >>> list(rrule(DAILY, bymonth=1, | ||
| ... dtstart=parse("19980101T090000"), | ||
| ... until=parse("20000131T090000"))) | ||
| [datetime.datetime(1998, 1, 1, 9, 0), | ||
| ... | ||
| datetime.datetime(2000, 1, 31, 9, 0)] | ||
| Weekly for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=10, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 23, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 7, 9, 0), | ||
| datetime.datetime(1997, 10, 14, 9, 0), | ||
| datetime.datetime(1997, 10, 21, 9, 0), | ||
| datetime.datetime(1997, 10, 28, 9, 0), | ||
| datetime.datetime(1997, 11, 4, 9, 0)] | ||
| Every other week, 6 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, interval=2, count=6, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 14, 9, 0), | ||
| datetime.datetime(1997, 10, 28, 9, 0), | ||
| datetime.datetime(1997, 11, 11, 9, 0)] | ||
| Weekly on Tuesday and Thursday for 5 weeks. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=10, wkst=SU, byweekday=(TU,TH), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 11, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 18, 9, 0), | ||
| datetime.datetime(1997, 9, 23, 9, 0), | ||
| datetime.datetime(1997, 9, 25, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0)] | ||
| Every other week on Tuesday and Thursday, for 8 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, interval=2, count=8, | ||
| ... wkst=SU, byweekday=(TU,TH), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 18, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 14, 9, 0), | ||
| datetime.datetime(1997, 10, 16, 9, 0)] | ||
| Monthly on the 1st Friday for ten occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=10, byweekday=FR(1), | ||
| ... dtstart=parse("19970905T090000"))) | ||
| [datetime.datetime(1997, 9, 5, 9, 0), | ||
| datetime.datetime(1997, 10, 3, 9, 0), | ||
| datetime.datetime(1997, 11, 7, 9, 0), | ||
| datetime.datetime(1997, 12, 5, 9, 0), | ||
| datetime.datetime(1998, 1, 2, 9, 0), | ||
| datetime.datetime(1998, 2, 6, 9, 0), | ||
| datetime.datetime(1998, 3, 6, 9, 0), | ||
| datetime.datetime(1998, 4, 3, 9, 0), | ||
| datetime.datetime(1998, 5, 1, 9, 0), | ||
| datetime.datetime(1998, 6, 5, 9, 0)] | ||
| Every other month on the 1st and last Sunday of the month for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, interval=2, count=10, | ||
| ... byweekday=(SU(1), SU(-1)), | ||
| ... dtstart=parse("19970907T090000"))) | ||
| [datetime.datetime(1997, 9, 7, 9, 0), | ||
| datetime.datetime(1997, 9, 28, 9, 0), | ||
| datetime.datetime(1997, 11, 2, 9, 0), | ||
| datetime.datetime(1997, 11, 30, 9, 0), | ||
| datetime.datetime(1998, 1, 4, 9, 0), | ||
| datetime.datetime(1998, 1, 25, 9, 0), | ||
| datetime.datetime(1998, 3, 1, 9, 0), | ||
| datetime.datetime(1998, 3, 29, 9, 0), | ||
| datetime.datetime(1998, 5, 3, 9, 0), | ||
| datetime.datetime(1998, 5, 31, 9, 0)] | ||
| Monthly on the second to last Monday of the month for 6 months. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=6, byweekday=MO(-2), | ||
| ... dtstart=parse("19970922T090000"))) | ||
| [datetime.datetime(1997, 9, 22, 9, 0), | ||
| datetime.datetime(1997, 10, 20, 9, 0), | ||
| datetime.datetime(1997, 11, 17, 9, 0), | ||
| datetime.datetime(1997, 12, 22, 9, 0), | ||
| datetime.datetime(1998, 1, 19, 9, 0), | ||
| datetime.datetime(1998, 2, 16, 9, 0)] | ||
| Monthly on the third to the last day of the month, for 6 months. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=6, bymonthday=-3, | ||
| ... dtstart=parse("19970928T090000"))) | ||
| [datetime.datetime(1997, 9, 28, 9, 0), | ||
| datetime.datetime(1997, 10, 29, 9, 0), | ||
| datetime.datetime(1997, 11, 28, 9, 0), | ||
| datetime.datetime(1997, 12, 29, 9, 0), | ||
| datetime.datetime(1998, 1, 29, 9, 0), | ||
| datetime.datetime(1998, 2, 26, 9, 0)] | ||
| Monthly on the 2nd and 15th of the month for 5 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=5, bymonthday=(2,15), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 15, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 15, 9, 0), | ||
| datetime.datetime(1997, 11, 2, 9, 0)] | ||
| Monthly on the first and last day of the month for 3 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=5, bymonthday=(-1,1,), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 10, 1, 9, 0), | ||
| datetime.datetime(1997, 10, 31, 9, 0), | ||
| datetime.datetime(1997, 11, 1, 9, 0), | ||
| datetime.datetime(1997, 11, 30, 9, 0)] | ||
| Every 18 months on the 10th thru 15th of the month for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, interval=18, count=10, | ||
| ... bymonthday=range(10,16), | ||
| ... dtstart=parse("19970910T090000"))) | ||
| [datetime.datetime(1997, 9, 10, 9, 0), | ||
| datetime.datetime(1997, 9, 11, 9, 0), | ||
| datetime.datetime(1997, 9, 12, 9, 0), | ||
| datetime.datetime(1997, 9, 13, 9, 0), | ||
| datetime.datetime(1997, 9, 14, 9, 0), | ||
| datetime.datetime(1997, 9, 15, 9, 0), | ||
| datetime.datetime(1999, 3, 10, 9, 0), | ||
| datetime.datetime(1999, 3, 11, 9, 0), | ||
| datetime.datetime(1999, 3, 12, 9, 0), | ||
| datetime.datetime(1999, 3, 13, 9, 0)] | ||
| Every Tuesday, every other month, 6 occurences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, interval=2, count=6, byweekday=TU, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 16, 9, 0), | ||
| datetime.datetime(1997, 9, 23, 9, 0), | ||
| datetime.datetime(1997, 9, 30, 9, 0), | ||
| datetime.datetime(1997, 11, 4, 9, 0)] | ||
| Yearly in June and July for 10 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=4, bymonth=(6,7), | ||
| ... dtstart=parse("19970610T090000"))) | ||
| [datetime.datetime(1997, 6, 10, 9, 0), | ||
| datetime.datetime(1997, 7, 10, 9, 0), | ||
| datetime.datetime(1998, 6, 10, 9, 0), | ||
| datetime.datetime(1998, 7, 10, 9, 0)] | ||
| Every 3rd year on the 1st, 100th and 200th day for 4 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=4, interval=3, byyearday=(1,100,200), | ||
| ... dtstart=parse("19970101T090000"))) | ||
| [datetime.datetime(1997, 1, 1, 9, 0), | ||
| datetime.datetime(1997, 4, 10, 9, 0), | ||
| datetime.datetime(1997, 7, 19, 9, 0), | ||
| datetime.datetime(2000, 1, 1, 9, 0)] | ||
| Every 20th Monday of the year, 3 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=3, byweekday=MO(20), | ||
| ... dtstart=parse("19970519T090000"))) | ||
| [datetime.datetime(1997, 5, 19, 9, 0), | ||
| datetime.datetime(1998, 5, 18, 9, 0), | ||
| datetime.datetime(1999, 5, 17, 9, 0)] | ||
| Monday of week number 20 (where the default start of the week is Monday), | ||
| 3 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=3, byweekno=20, byweekday=MO, | ||
| ... dtstart=parse("19970512T090000"))) | ||
| [datetime.datetime(1997, 5, 12, 9, 0), | ||
| datetime.datetime(1998, 5, 11, 9, 0), | ||
| datetime.datetime(1999, 5, 17, 9, 0)] | ||
| The week number 1 may be in the last year. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=3, byweekno=1, byweekday=MO, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 12, 29, 9, 0), | ||
| datetime.datetime(1999, 1, 4, 9, 0), | ||
| datetime.datetime(2000, 1, 3, 9, 0)] | ||
| And the week numbers greater than 51 may be in the next year. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=3, byweekno=52, byweekday=SU, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 12, 28, 9, 0), | ||
| datetime.datetime(1998, 12, 27, 9, 0), | ||
| datetime.datetime(2000, 1, 2, 9, 0)] | ||
| Only some years have week number 53: | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, count=3, byweekno=53, byweekday=MO, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1998, 12, 28, 9, 0), | ||
| datetime.datetime(2004, 12, 27, 9, 0), | ||
| datetime.datetime(2009, 12, 28, 9, 0)] | ||
| Every Friday the 13th, 4 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, count=4, byweekday=FR, bymonthday=13, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1998, 2, 13, 9, 0), | ||
| datetime.datetime(1998, 3, 13, 9, 0), | ||
| datetime.datetime(1998, 11, 13, 9, 0), | ||
| datetime.datetime(1999, 8, 13, 9, 0)] | ||
| Every four years, the first Tuesday after a Monday in November, | ||
| 3 occurrences (U.S. Presidential Election day): | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(YEARLY, interval=4, count=3, bymonth=11, | ||
| ... byweekday=TU, bymonthday=(2,3,4,5,6,7,8), | ||
| ... dtstart=parse("19961105T090000"))) | ||
| [datetime.datetime(1996, 11, 5, 9, 0), | ||
| datetime.datetime(2000, 11, 7, 9, 0), | ||
| datetime.datetime(2004, 11, 2, 9, 0)] | ||
| The 3rd instance into the month of one of Tuesday, Wednesday or | ||
| Thursday, for the next 3 months: | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=3, byweekday=(TU,WE,TH), | ||
| ... bysetpos=3, dtstart=parse("19970904T090000"))) | ||
| [datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 10, 7, 9, 0), | ||
| datetime.datetime(1997, 11, 6, 9, 0)] | ||
| The 2nd to last weekday of the month, 3 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MONTHLY, count=3, byweekday=(MO,TU,WE,TH,FR), | ||
| ... bysetpos=-2, dtstart=parse("19970929T090000"))) | ||
| [datetime.datetime(1997, 9, 29, 9, 0), | ||
| datetime.datetime(1997, 10, 30, 9, 0), | ||
| datetime.datetime(1997, 11, 27, 9, 0)] | ||
| Every 3 hours from 9:00 AM to 5:00 PM on a specific day. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(HOURLY, interval=3, | ||
| ... dtstart=parse("19970902T090000"), | ||
| ... until=parse("19970902T170000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 2, 12, 0), | ||
| datetime.datetime(1997, 9, 2, 15, 0)] | ||
| Every 15 minutes for 6 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MINUTELY, interval=15, count=6, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 2, 9, 15), | ||
| datetime.datetime(1997, 9, 2, 9, 30), | ||
| datetime.datetime(1997, 9, 2, 9, 45), | ||
| datetime.datetime(1997, 9, 2, 10, 0), | ||
| datetime.datetime(1997, 9, 2, 10, 15)] | ||
| Every hour and a half for 4 occurrences. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(MINUTELY, interval=90, count=4, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 2, 10, 30), | ||
| datetime.datetime(1997, 9, 2, 12, 0), | ||
| datetime.datetime(1997, 9, 2, 13, 30)] | ||
| Every 20 minutes from 9:00 AM to 4:40 PM for two days. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE, +ELLIPSIS | ||
| >>> list(rrule(MINUTELY, interval=20, count=48, | ||
| ... byhour=range(9,17), byminute=(0,20,40), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 2, 9, 20), | ||
| ... | ||
| datetime.datetime(1997, 9, 2, 16, 20), | ||
| datetime.datetime(1997, 9, 2, 16, 40), | ||
| datetime.datetime(1997, 9, 3, 9, 0), | ||
| datetime.datetime(1997, 9, 3, 9, 20), | ||
| ... | ||
| datetime.datetime(1997, 9, 3, 16, 20), | ||
| datetime.datetime(1997, 9, 3, 16, 40)] | ||
| An example where the days generated makes a difference because of `wkst`. | ||
| .. doctest:: rrule | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrule(WEEKLY, interval=2, count=4, | ||
| ... byweekday=(TU,SU), wkst=MO, | ||
| ... dtstart=parse("19970805T090000"))) | ||
| [datetime.datetime(1997, 8, 5, 9, 0), | ||
| datetime.datetime(1997, 8, 10, 9, 0), | ||
| datetime.datetime(1997, 8, 19, 9, 0), | ||
| datetime.datetime(1997, 8, 24, 9, 0)] | ||
| >>> list(rrule(WEEKLY, interval=2, count=4, | ||
| ... byweekday=(TU,SU), wkst=SU, | ||
| ... dtstart=parse("19970805T090000"))) | ||
| [datetime.datetime(1997, 8, 5, 9, 0), | ||
| datetime.datetime(1997, 8, 17, 9, 0), | ||
| datetime.datetime(1997, 8, 19, 9, 0), | ||
| datetime.datetime(1997, 8, 31, 9, 0)] | ||
| rruleset examples | ||
| ----------------- | ||
| Daily, for 7 days, jumping Saturday and Sunday occurrences. | ||
| .. testsetup:: rruleset | ||
| import datetime | ||
| from dateutil.parser import parse | ||
| from dateutil.rrule import rrule, rruleset | ||
| from dateutil.rrule import YEARLY, MONTHLY, WEEKLY, DAILY | ||
| from dateutil.rrule import MO, TU, WE, TH, FR, SA, SU | ||
| import pprint | ||
| import sys | ||
| sys.displayhook = pprint.pprint | ||
| .. doctest:: rruleset | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> set = rruleset() | ||
| >>> set.rrule(rrule(DAILY, count=7, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| >>> set.exrule(rrule(YEARLY, byweekday=(SA,SU), | ||
| ... dtstart=parse("19970902T090000"))) | ||
| >>> list(set) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 3, 9, 0), | ||
| datetime.datetime(1997, 9, 4, 9, 0), | ||
| datetime.datetime(1997, 9, 5, 9, 0), | ||
| datetime.datetime(1997, 9, 8, 9, 0)] | ||
| Weekly, for 4 weeks, plus one time on day 7, and not on day 16. | ||
| .. doctest:: rruleset | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> set = rruleset() | ||
| >>> set.rrule(rrule(WEEKLY, count=4, | ||
| ... dtstart=parse("19970902T090000"))) | ||
| >>> set.rdate(datetime.datetime(1997, 9, 7, 9, 0)) | ||
| >>> set.exdate(datetime.datetime(1997, 9, 16, 9, 0)) | ||
| >>> list(set) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 7, 9, 0), | ||
| datetime.datetime(1997, 9, 9, 9, 0), | ||
| datetime.datetime(1997, 9, 23, 9, 0)] | ||
| rrulestr() examples | ||
| ------------------- | ||
| Every 10 days, 5 occurrences. | ||
| .. testsetup:: rrulestr | ||
| from dateutil.parser import parse | ||
| from dateutil.rrule import rruleset, rrulestr | ||
| import pprint | ||
| import sys | ||
| sys.displayhook = pprint.pprint | ||
| .. doctest:: rrulestr | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrulestr(""" | ||
| ... DTSTART:19970902T090000 | ||
| ... RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 | ||
| ... """)) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 12, 9, 0), | ||
| datetime.datetime(1997, 9, 22, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 12, 9, 0)] | ||
| Same thing, but passing only the `RRULE` value. | ||
| .. doctest:: rrulestr | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> list(rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5", | ||
| ... dtstart=parse("19970902T090000"))) | ||
| [datetime.datetime(1997, 9, 2, 9, 0), | ||
| datetime.datetime(1997, 9, 12, 9, 0), | ||
| datetime.datetime(1997, 9, 22, 9, 0), | ||
| datetime.datetime(1997, 10, 2, 9, 0), | ||
| datetime.datetime(1997, 10, 12, 9, 0)] | ||
| Notice that when using a single rule, it returns an | ||
| `rrule` instance, unless `forceset` was used. | ||
| .. doctest:: rrulestr | ||
| :options: +ELLIPSIS | ||
| >>> rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5") | ||
| <dateutil.rrule.rrule object at 0x...> | ||
| >>> rrulestr(""" | ||
| ... DTSTART:19970902T090000 | ||
| ... RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 | ||
| ... """) | ||
| <dateutil.rrule.rrule object at 0x...> | ||
| >>> rrulestr("FREQ=DAILY;INTERVAL=10;COUNT=5", forceset=True) | ||
| <dateutil.rrule.rruleset object at 0x...> | ||
| But when an `rruleset` is needed, it is automatically used. | ||
| .. doctest:: rrulestr | ||
| :options: +ELLIPSIS | ||
| >>> rrulestr(""" | ||
| ... DTSTART:19970902T090000 | ||
| ... RRULE:FREQ=DAILY;INTERVAL=10;COUNT=5 | ||
| ... RRULE:FREQ=DAILY;INTERVAL=5;COUNT=3 | ||
| ... """) | ||
| <dateutil.rrule.rruleset object at 0x...> |
Sorry, the diff of this file is not supported yet
| == | ||
| tz | ||
| == | ||
| .. automodule:: dateutil.tz | ||
| :members: | ||
| :undoc-members: |
| ===== | ||
| utils | ||
| ===== | ||
| .. automodule:: dateutil.utils | ||
| :members: | ||
| :undoc-members: |
| ======== | ||
| zoneinfo | ||
| ======== | ||
| .. automodule:: dateutil.zoneinfo | ||
| :members: | ||
| :undoc-members: | ||
| .. automodule:: dateutil.zoneinfo.rebuild | ||
| :members: rebuild | ||
| zonefile_metadata | ||
| ----------------- | ||
| The zonefile metadata defines the version and exact location of | ||
| the timezone database to download. It is used in the ``updatezinfo.py`` | ||
| script. A json encoded file is included in the source-code, and | ||
| within each tar file we produce. The json file is attached here: | ||
| .. literalinclude:: ../zonefile_metadata.json | ||
| :language: json |
+79
| """ | ||
| Release script | ||
| """ | ||
| import glob | ||
| import os | ||
| import shutil | ||
| import subprocess | ||
| import sys | ||
| import click | ||
| @click.group() | ||
| def cli(): | ||
| pass | ||
| @cli.command() | ||
| def build(): | ||
| DIST_PATH = 'dist' | ||
| if os.path.exists(DIST_PATH) and os.listdir(DIST_PATH): | ||
| if click.confirm('{} is not empty - delete contents?'.format(DIST_PATH)): | ||
| shutil.rmtree(DIST_PATH) | ||
| os.makedirs(DIST_PATH) | ||
| else: | ||
| click.echo('Aborting') | ||
| sys.exit(1) | ||
| subprocess.check_call(['python', 'setup.py', 'bdist_wheel']) | ||
| subprocess.check_call(['python', 'setup.py', 'sdist', | ||
| '--formats=gztar']) | ||
| @cli.command() | ||
| def sign(): | ||
| # Sign all the distribution files | ||
| for fpath in glob.glob('dist/*'): | ||
| subprocess.check_call(['gpg', '--armor', '--output', fpath + '.asc', | ||
| '--detach-sig', fpath]) | ||
| # Verify the distribution files | ||
| for fpath in glob.glob('dist/*'): | ||
| if fpath.endswith('.asc'): | ||
| continue | ||
| subprocess.check_call(['gpg', '--verify', fpath + '.asc', fpath]) | ||
| @cli.command() | ||
| @click.option('--passfile', default=None) | ||
| @click.option('--release/--no-release', default=False) | ||
| def upload(passfile, release): | ||
| if release: | ||
| repository='pypi' | ||
| else: | ||
| repository='pypitest' | ||
| env = os.environ.copy() | ||
| if passfile is not None: | ||
| gpg_call = subprocess.run(['gpg', '-d', passfile], | ||
| stdout=subprocess.PIPE, | ||
| stderr=subprocess.PIPE) | ||
| username, password = gpg_call.stdout.decode('utf-8').split('\n') | ||
| env['TWINE_USERNAME'] = username | ||
| env['TWINE_PASSWORD'] = password | ||
| dist_files = glob.glob('dist/*') | ||
| for dist_file in dist_files: | ||
| if dist_file.endswith('.asc'): | ||
| continue | ||
| if dist_file + '.asc' not in dist_files: | ||
| raise ValueError('Missing signature file for: {}'.format(dist_file)) | ||
| args = ['twine', 'upload', '-r', repository] + dist_files | ||
| p = subprocess.Popen(args, env=env) | ||
| p.wait() | ||
| if __name__ == "__main__": | ||
| cli() |
Sorry, the diff of this file is not supported yet
| six | ||
| pytest >= 3.0; python_version != '3.3' | ||
| pytest < 3.3; python_version == '3.3' | ||
| pytest-cov >= 2.0.0 | ||
| freezegun | ||
| coverage | ||
| mock ; python_version < '3.0' |
+59
| [tox] | ||
| envlist = py27, | ||
| py33, | ||
| py34, | ||
| py35, | ||
| py36, | ||
| py37, | ||
| pypy, | ||
| pypy3, | ||
| coverage, | ||
| docs | ||
| minversion = 2.9.0 | ||
| skip_missing_interpreters = true | ||
| [testenv] | ||
| description = run the unit tests with pytest under {basepython} | ||
| setenv = COVERAGE_FILE={toxworkdir}/.coverage.{envname} | ||
| passenv = DATEUTIL_MAY_CHANGE_TZ TOXENV CI TRAVIS TRAVIS_* APPVEYOR APPVEYOR_* CODECOV_* | ||
| commands = python -m pytest -m "not xfail" {posargs: "{toxinidir}/dateutil/test" --cov-config="{toxinidir}/tox.ini" --cov=dateutil} | ||
| python -m pytest -m "xfail" {posargs: "{toxinidir}/dateutil/test"} | ||
| deps = -rrequirements-dev.txt | ||
| [testenv:coverage] | ||
| description = combine coverage data and create reports | ||
| deps = coverage | ||
| skip_install = True | ||
| changedir = {toxworkdir} | ||
| setenv = COVERAGE_FILE=.coverage | ||
| commands = coverage erase | ||
| coverage combine | ||
| coverage report --rcfile={toxinidir}/tox.ini | ||
| coverage xml | ||
| [testenv:codecov] | ||
| description = [only run on CI]: upload coverage data to codecov (depends on coverage running first) | ||
| deps = codecov | ||
| skip_install = True | ||
| commands = codecov --file {toxworkdir}/coverage.xml | ||
| [testenv:dev] | ||
| description = DEV environment | ||
| usedevelop = True | ||
| commands = python -m pip list --format=columns | ||
| python -c 'import sys; print(sys.executable)' | ||
| [coverage:run] | ||
| source = dateutil | ||
| [coverage:report] | ||
| skip_covered = True | ||
| show_missing = True | ||
| [testenv:docs] | ||
| description = invoke sphinx-build to build the HTML docs, check that URIs are valid | ||
| basepython = python3.6 | ||
| deps = sphinx >= 1.6.3, < 2 | ||
| {[testenv]deps} | ||
| commands = sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" {posargs:-W --color -bhtml} | ||
| sphinx-build -d "{toxworkdir}/docs_doctree" docs "{toxworkdir}/docs_out" {posargs:-W --color -blinkcheck} |
| # -*- coding: utf-8 -*- | ||
| from ._version import VERSION as __version__ | ||
| try: | ||
| from ._version import version as __version__ | ||
| except ImportError: | ||
| __version__ = 'unknown' | ||
| __all__ = ['easter', 'parser', 'relativedelta', 'rrule', 'tz', | ||
| 'utils', 'zoneinfo'] |
+10
-1
@@ -27,4 +27,11 @@ """ | ||
| __hash__ = None | ||
| def __hash__(self): | ||
| return hash(( | ||
| self.weekday, | ||
| self.n, | ||
| )) | ||
| def __ne__(self, other): | ||
| return not (self == other) | ||
| def __repr__(self): | ||
@@ -36,1 +43,3 @@ s = ("MO", "TU", "WE", "TH", "FR", "SA", "SU")[self.weekday] | ||
| return "%s(%+d)" % (s, self.n) | ||
| # vim:ts=4:sw=4:et |
+4
-10
@@ -1,10 +0,4 @@ | ||
| """ | ||
| Contains information about the dateutil version. | ||
| """ | ||
| VERSION_MAJOR = 2 | ||
| VERSION_MINOR = 6 | ||
| VERSION_PATCH = 1 | ||
| VERSION_TUPLE = (VERSION_MAJOR, VERSION_MINOR, VERSION_PATCH) | ||
| VERSION = '.'.join(map(str, VERSION_TUPLE)) | ||
| # coding: utf-8 | ||
| # file generated by setuptools_scm | ||
| # don't change, don't track in version control | ||
| version = '2.7.0' |
@@ -44,7 +44,7 @@ # -*- coding: utf-8 -*- | ||
| http://users.chariot.net.au/~gmarts/eastalg.htm | ||
| `GM Arts: Easter Algorithms <http://www.gmarts.org/index.php?go=415>`_ | ||
| and | ||
| http://www.tondering.dk/claus/calendar.html | ||
| `The Calendar FAQ: Easter <https://www.tondering.dk/claus/cal/easter.php>`_ | ||
@@ -51,0 +51,0 @@ """ |
@@ -22,3 +22,3 @@ # -*- coding: utf-8 -*- | ||
| work done by M.-A. Lemburg in his | ||
| `mx.DateTime <http://www.egenix.com/files/python/mxDateTime.html>`_ extension. | ||
| `mx.DateTime <https://www.egenix.com/products/python/mxBase/mxDateTime/>`_ extension. | ||
| However, notice that this type does *NOT* implement the same algorithm as | ||
@@ -38,3 +38,3 @@ his work. Do *NOT* expect it to behave like mx.DateTime's counterpart. | ||
| Absolute information (argument is singular); adding or subtracting a | ||
| relativedelta with absolute information does not perform an aritmetic | ||
| relativedelta with absolute information does not perform an arithmetic | ||
| operation, but rather REPLACES the corresponding value in the | ||
@@ -100,7 +100,2 @@ original datetime with the value(s) in relativedelta. | ||
| # Check for non-integer values in integer-only quantities | ||
| if any(x is not None and x != int(x) for x in (years, months)): | ||
| raise ValueError("Non-integer years and months are " | ||
| "ambiguous and not currently supported.") | ||
| if dt1 and dt2: | ||
@@ -165,5 +160,10 @@ # datetime is a subclass of date. So both must be date | ||
| else: | ||
| # Check for non-integer values in integer-only quantities | ||
| if any(x is not None and x != int(x) for x in (years, months)): | ||
| raise ValueError("Non-integer years and months are " | ||
| "ambiguous and not currently supported.") | ||
| # Relative information | ||
| self.years = years | ||
| self.months = months | ||
| self.years = int(years) | ||
| self.months = int(months) | ||
| self.days = days + weeks * 7 | ||
@@ -256,3 +256,3 @@ self.leapdays = leapdays | ||
| def weeks(self): | ||
| return self.days // 7 | ||
| return int(self.days / 7.0) | ||
@@ -430,2 +430,20 @@ @weeks.setter | ||
| def __abs__(self): | ||
| return self.__class__(years=abs(self.years), | ||
| months=abs(self.months), | ||
| days=abs(self.days), | ||
| hours=abs(self.hours), | ||
| minutes=abs(self.minutes), | ||
| seconds=abs(self.seconds), | ||
| microseconds=abs(self.microseconds), | ||
| leapdays=self.leapdays, | ||
| year=self.year, | ||
| month=self.month, | ||
| day=self.day, | ||
| weekday=self.weekday, | ||
| hour=self.hour, | ||
| minute=self.minute, | ||
| second=self.second, | ||
| microsecond=self.microsecond) | ||
| def __neg__(self): | ||
@@ -521,3 +539,21 @@ return self.__class__(years=-self.years, | ||
| __hash__ = None | ||
| def __hash__(self): | ||
| return hash(( | ||
| self.weekday, | ||
| self.years, | ||
| self.months, | ||
| self.days, | ||
| self.hours, | ||
| self.minutes, | ||
| self.seconds, | ||
| self.microseconds, | ||
| self.leapdays, | ||
| self.year, | ||
| self.month, | ||
| self.day, | ||
| self.hour, | ||
| self.minute, | ||
| self.second, | ||
| self.microsecond, | ||
| )) | ||
@@ -524,0 +560,0 @@ def __ne__(self, other): |
+56
-12
@@ -5,3 +5,3 @@ # -*- coding: utf-8 -*- | ||
| the recurrence rules documented in the | ||
| `iCalendar RFC <http://www.ietf.org/rfc/rfc2445.txt>`_, | ||
| `iCalendar RFC <https://tools.ietf.org/html/rfc5545>`_, | ||
| including support for caching of results. | ||
@@ -12,2 +12,3 @@ """ | ||
| import calendar | ||
| import re | ||
| import sys | ||
@@ -364,3 +365,3 @@ | ||
| As of version 2.5.0, the use of the ``until`` keyword together | ||
| with the ``count`` keyword is deprecated per RFC-2445 Sec. 4.3.10. | ||
| with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. | ||
| :param until: | ||
@@ -374,3 +375,3 @@ If given, this must be a datetime instance, that will specify the | ||
| As of version 2.5.0, the use of the ``until`` keyword together | ||
| with the ``count`` keyword is deprecated per RFC-2445 Sec. 4.3.10. | ||
| with the ``count`` keyword is deprecated per RFC-5545 Sec. 3.3.10. | ||
| :param bysetpos: | ||
@@ -454,3 +455,3 @@ If given, it must be either an integer, or a sequence of integers, | ||
| if count is not None and until: | ||
| warn("Using both 'count' and 'until' is inconsistent with RFC 2445" | ||
| warn("Using both 'count' and 'until' is inconsistent with RFC 5545" | ||
| " and has been deprecated in dateutil. Future versions will " | ||
@@ -592,3 +593,3 @@ "raise an error.", DeprecationWarning) | ||
| else: | ||
| orig_byweekday = tuple() | ||
| orig_byweekday = () | ||
@@ -599,3 +600,3 @@ if self._bynweekday is not None: | ||
| else: | ||
| orig_bynweekday = tuple() | ||
| orig_bynweekday = () | ||
@@ -609,3 +610,3 @@ if 'byweekday' not in self._original_rule: | ||
| if freq < HOURLY: | ||
| self._byhour = set((dtstart.hour,)) | ||
| self._byhour = {dtstart.hour} | ||
| else: | ||
@@ -630,3 +631,3 @@ self._byhour = None | ||
| if freq < MINUTELY: | ||
| self._byminute = set((dtstart.minute,)) | ||
| self._byminute = {dtstart.minute} | ||
| else: | ||
@@ -686,3 +687,3 @@ self._byminute = None | ||
| Output a string that would generate this RRULE if passed to rrulestr. | ||
| This is mostly compatible with RFC2445, except for the | ||
| This is mostly compatible with RFC5545, except for the | ||
| dateutil-specific extension BYEASTER. | ||
@@ -712,3 +713,3 @@ """ | ||
| # The str() method on weekday objects doesn't generate | ||
| # RFC2445-compliant strings, so we should modify that. | ||
| # RFC5545-compliant strings, so we should modify that. | ||
| original_rule = dict(self._original_rule) | ||
@@ -744,3 +745,3 @@ wday_strings = [] | ||
| output.append(';'.join(parts)) | ||
| output.append('RRULE:' + ';'.join(parts)) | ||
| return '\n'.join(output) | ||
@@ -1511,2 +1512,3 @@ | ||
| ignoretz=False, | ||
| tzids=None, | ||
| tzinfos=None): | ||
@@ -1517,2 +1519,7 @@ global parser | ||
| unfold = True | ||
| TZID_NAMES = dict(map( | ||
| lambda x: (x.upper(), x), | ||
| re.findall('TZID=(?P<name>[^:]+):', s) | ||
| )) | ||
| s = s.upper() | ||
@@ -1577,4 +1584,36 @@ if not s.strip(): | ||
| elif name == "DTSTART": | ||
| # RFC 5445 3.8.2.4: The VALUE parameter is optional, but | ||
| # may be found only once. | ||
| value_found = False | ||
| TZID = None | ||
| valid_values = {"VALUE=DATE-TIME", "VALUE=DATE"} | ||
| for parm in parms: | ||
| raise ValueError("unsupported DTSTART parm: "+parm) | ||
| if parm.startswith("TZID="): | ||
| try: | ||
| tzkey = TZID_NAMES[parm.split('TZID=')[-1]] | ||
| except KeyError: | ||
| continue | ||
| if tzids is None: | ||
| from . import tz | ||
| tzlookup = tz.gettz | ||
| elif callable(tzids): | ||
| tzlookup = tzids | ||
| else: | ||
| tzlookup = getattr(tzids, 'get', None) | ||
| if tzlookup is None: | ||
| msg = ('tzids must be a callable, ' + | ||
| 'mapping, or None, ' + | ||
| 'not %s' % tzids) | ||
| raise ValueError(msg) | ||
| TZID = tzlookup(tzkey) | ||
| continue | ||
| if parm not in valid_values: | ||
| raise ValueError("unsupported DTSTART parm: "+parm) | ||
| else: | ||
| if value_found: | ||
| msg = ("Duplicate value parameter found in " + | ||
| "DTSTART: " + parm) | ||
| raise ValueError(msg) | ||
| value_found = True | ||
| if not parser: | ||
@@ -1584,2 +1623,7 @@ from dateutil import parser | ||
| tzinfos=tzinfos) | ||
| if TZID is not None: | ||
| if dtstart.tzinfo is None: | ||
| dtstart = dtstart.replace(tzinfo=TZID) | ||
| else: | ||
| raise ValueError('DTSTART specifies multiple timezones') | ||
| else: | ||
@@ -1586,0 +1630,0 @@ raise ValueError("unsupported property: "+name) |
| from __future__ import unicode_literals | ||
| try: | ||
| import unittest2 as unittest | ||
| except ImportError: | ||
| import unittest | ||
| import os | ||
| import datetime | ||
| import time | ||
@@ -84,3 +78,3 @@ import subprocess | ||
| def assertPicklable(self, obj, asfile=False, | ||
| def assertPicklable(self, obj, singleton=False, asfile=False, | ||
| dump_kwargs=None, load_kwargs=None): | ||
@@ -97,3 +91,4 @@ """ | ||
| nobj = get_nobj(obj, dump_kwargs, load_kwargs) | ||
| self.assertIsNot(obj, nobj) | ||
| if not singleton: | ||
| self.assertIsNot(obj, nobj) | ||
| self.assertEqual(obj, nobj) | ||
@@ -215,14 +210,2 @@ | ||
| ### | ||
| # Compatibility functions | ||
| def _total_seconds(td): | ||
| # Python 2.6 doesn't have a total_seconds() method on timedelta objects | ||
| return ((td.seconds + td.days * 86400) * 1000000 + | ||
| td.microseconds) // 1000000 | ||
| total_seconds = getattr(datetime.timedelta, 'total_seconds', _total_seconds) | ||
| ### | ||
| # Utility classes | ||
@@ -229,0 +212,0 @@ class NotAValueClass(object): |
@@ -5,8 +5,4 @@ from dateutil.easter import easter | ||
| from datetime import date | ||
| import unittest | ||
| try: | ||
| import unittest2 as unittest | ||
| except ImportError: | ||
| import unittest | ||
| # List of easters between 1990 and 2050 | ||
@@ -13,0 +9,0 @@ western_easter_dates = [ |
| import sys | ||
| import unittest | ||
| try: | ||
| import unittest2 as unittest | ||
| except ImportError: | ||
| import unittest | ||
| class ImportVersionTest(unittest.TestCase): | ||
@@ -121,5 +117,10 @@ """ Test that dateutil.__version__ can be imported""" | ||
| from dateutil.tz import tzwinlocal | ||
| from dateutil.tz import UTC | ||
| from dateutil.tz import datetime_ambiguous | ||
| from dateutil.tz import datetime_exists | ||
| from dateutil.tz import resolve_imaginary | ||
| tz_all = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", | ||
| "tzstr", "tzical", "gettz"] | ||
| "tzstr", "tzical", "gettz", "datetime_ambiguous", | ||
| "datetime_exists", "resolve_imaginary", "UTC"] | ||
@@ -132,3 +133,2 @@ tz_all += ["tzwin", "tzwinlocal"] if sys.platform.startswith("win") else [] | ||
| @unittest.skipUnless(sys.platform.startswith('win'), "Requires Windows") | ||
@@ -135,0 +135,0 @@ class ImportTZWinTest(unittest.TestCase): |
+273
-77
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from ._common import unittest | ||
| import itertools | ||
| from datetime import datetime, timedelta | ||
| import unittest | ||
| import sys | ||
| from dateutil import tz | ||
| from dateutil.tz import tzoffset | ||
| from dateutil.parser import parse, parserinfo | ||
| from dateutil.parser import UnknownTimezoneWarning | ||
| from ._common import TZEnvContext | ||
| from six import assertRaisesRegex, PY3 | ||
| from six.moves import StringIO | ||
| import pytest | ||
| # Platform info | ||
| IS_WIN = sys.platform.startswith('win') | ||
| try: | ||
| datetime.now().strftime('%-d') | ||
| PLATFORM_HAS_DASH_D = True | ||
| except ValueError: | ||
| PLATFORM_HAS_DASH_D = False | ||
| class TestFormat(unittest.TestCase): | ||
| def test_ybd(self): | ||
| # If we have a 4-digit year, a non-numeric month (abbreviated or not), | ||
| # and a day (1 or 2 digits), then there is no ambiguity as to which | ||
| # token is a year/month/day. This holds regardless of what order the | ||
| # terms are in and for each of the separators below. | ||
| seps = ['-', ' ', '/', '.'] | ||
| year_tokens = ['%Y'] | ||
| month_tokens = ['%b', '%B'] | ||
| day_tokens = ['%d'] | ||
| if PLATFORM_HAS_DASH_D: | ||
| day_tokens.append('%-d') | ||
| prods = itertools.product(year_tokens, month_tokens, day_tokens) | ||
| perms = [y for x in prods for y in itertools.permutations(x)] | ||
| unambig_fmts = [sep.join(perm) for sep in seps for perm in perms] | ||
| actual = datetime(2003, 9, 25) | ||
| for fmt in unambig_fmts: | ||
| dstr = actual.strftime(fmt) | ||
| res = parse(dstr) | ||
| self.assertEqual(res, actual) | ||
| class ParserTest(unittest.TestCase): | ||
@@ -61,2 +106,10 @@ | ||
| def testParseBytes(self): | ||
| self.assertEqual(parse(b'2014 January 19'), datetime(2014, 1, 19)) | ||
| def testParseBytearray(self): | ||
| # GH #417 | ||
| self.assertEqual(parse(bytearray(b'2014 January 19')), | ||
| datetime(2014, 1, 19)) | ||
| def testParserParseStr(self): | ||
@@ -159,6 +212,2 @@ from dateutil.parser import parser | ||
| def testDateCommandFormatStrip9(self): | ||
| self.assertEqual(parse("Sep 25 2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateCommandFormatStrip10(self): | ||
@@ -249,18 +298,2 @@ self.assertEqual(parse("Sep 2003", default=self.default), | ||
| def testDateWithDash2(self): | ||
| self.assertEqual(parse("2003-Sep-25"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDash3(self): | ||
| self.assertEqual(parse("25-Sep-2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDash4(self): | ||
| self.assertEqual(parse("25-Sep-2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDash5(self): | ||
| self.assertEqual(parse("Sep-25-2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDash6(self): | ||
@@ -294,18 +327,2 @@ self.assertEqual(parse("09-25-2003"), | ||
| def testDateWithDot2(self): | ||
| self.assertEqual(parse("2003.Sep.25"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDot3(self): | ||
| self.assertEqual(parse("25.Sep.2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDot4(self): | ||
| self.assertEqual(parse("25.Sep.2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDot5(self): | ||
| self.assertEqual(parse("Sep.25.2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithDot6(self): | ||
@@ -339,18 +356,2 @@ self.assertEqual(parse("09.25.2003"), | ||
| def testDateWithSlash2(self): | ||
| self.assertEqual(parse("2003/Sep/25"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSlash3(self): | ||
| self.assertEqual(parse("25/Sep/2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSlash4(self): | ||
| self.assertEqual(parse("25/Sep/2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSlash5(self): | ||
| self.assertEqual(parse("Sep/25/2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSlash6(self): | ||
@@ -384,18 +385,2 @@ self.assertEqual(parse("09/25/2003"), | ||
| def testDateWithSpace2(self): | ||
| self.assertEqual(parse("2003 Sep 25"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSpace3(self): | ||
| self.assertEqual(parse("25 Sep 2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSpace4(self): | ||
| self.assertEqual(parse("25 Sep 2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSpace5(self): | ||
| self.assertEqual(parse("Sep 25 2003"), | ||
| datetime(2003, 9, 25)) | ||
| def testDateWithSpace6(self): | ||
@@ -433,6 +418,2 @@ self.assertEqual(parse("09 25 2003"), | ||
| def testStrangelyOrderedDate2(self): | ||
| self.assertEqual(parse("2003 25 Sep"), | ||
| datetime(2003, 9, 25)) | ||
| def testStrangelyOrderedDate3(self): | ||
@@ -604,5 +585,6 @@ self.assertEqual(parse("25 03 Sep"), | ||
| s1 = "Jan 29, 1945 14:45 AM I going to see you there?" | ||
| with pytest.warns(UnknownTimezoneWarning): | ||
| res = parse(s1, fuzzy=True) | ||
| self.assertEqual(res, datetime(1945, 1, 29, 14, 45)) | ||
| self.assertEqual(parse(s1, fuzzy=True), datetime(1945, 1, 29, 14, 45)) | ||
| def testExtraSpace(self): | ||
@@ -714,5 +696,7 @@ self.assertEqual(parse(" July 4 , 1976 12:01:02 am "), | ||
| def testRandomFormat26(self): | ||
| self.assertEqual(parse("5:50 A.M. on June 13, 1990"), | ||
| datetime(1990, 6, 13, 5, 50)) | ||
| with pytest.warns(UnknownTimezoneWarning): | ||
| res = parse("5:50 A.M. on June 13, 1990") | ||
| self.assertEqual(res, datetime(1990, 6, 13, 5, 50)) | ||
| def testRandomFormat27(self): | ||
@@ -907,1 +891,213 @@ self.assertEqual(parse("3rd of May 2001"), datetime(2001, 5, 3)) | ||
| datetime(2015, 9, 25)) | ||
| def test_mstridx(self): | ||
| # See GH408 | ||
| dtstr = '2015-15-May' | ||
| self.assertEqual(parse(dtstr), | ||
| datetime(2015, 5, 15)) | ||
| def test_idx_check(self): | ||
| dtstr = '2017-07-17 06:15:' | ||
| # Pre-PR, the trailing colon will cause an IndexError at 824-825 | ||
| # when checking `i < len_l` and then accessing `l[i+1]` | ||
| res = parse(dtstr, fuzzy=True) | ||
| self.assertEqual(res, datetime(2017, 7, 17, 6, 15)) | ||
| def test_dBY(self): | ||
| # See GH360 | ||
| dtstr = '13NOV2017' | ||
| res = parse(dtstr) | ||
| self.assertEqual(res, datetime(2017, 11, 13)) | ||
| def test_hmBY(self): | ||
| # See GH#483 | ||
| dtstr = '02:17NOV2017' | ||
| res = parse(dtstr, default=self.default) | ||
| self.assertEqual(res, datetime(2017, 11, self.default.day, 2, 17)) | ||
| def test_validate_hour(self): | ||
| # See GH353 | ||
| invalid = "201A-01-01T23:58:39.239769+03:00" | ||
| with self.assertRaises(ValueError): | ||
| parse(invalid) | ||
| def test_era_trailing_year(self): | ||
| dstr = 'AD2001' | ||
| res = parse(dstr) | ||
| assert res.year == 2001, res | ||
| def test_pre_12_year_same_month(self): | ||
| # See GH PR #293 | ||
| dtstr = '0003-03-04' | ||
| assert parse(dtstr) == datetime(3, 3, 4) | ||
| class TestParseUnimplementedCases(object): | ||
| @pytest.mark.xfail | ||
| def test_somewhat_ambiguous_string(self): | ||
| # Ref: github issue #487 | ||
| # The parser is choosing the wrong part for hour | ||
| # causing datetime to raise an exception. | ||
| dtstr = '1237 PM BRST Mon Oct 30 2017' | ||
| res = parse(dtstr, tzinfo=self.tzinfos) | ||
| assert res == datetime(2017, 10, 30, 12, 37, tzinfo=self.tzinfos) | ||
| @pytest.mark.xfail | ||
| def test_YmdH_M_S(self): | ||
| # found in nasdaq's ftp data | ||
| dstr = '1991041310:19:24' | ||
| expected = datetime(1991, 4, 13, 10, 19, 24) | ||
| res = parse(dstr) | ||
| assert res == expected, (res, expected) | ||
| @pytest.mark.xfail | ||
| def test_first_century(self): | ||
| dstr = '0031 Nov 03' | ||
| expected = datetime(31, 11, 3) | ||
| res = parse(dstr) | ||
| assert res == expected, res | ||
| @pytest.mark.xfail | ||
| def test_era_trailing_year_with_dots(self): | ||
| dstr = 'A.D.2001' | ||
| res = parse(dstr) | ||
| assert res.year == 2001, res | ||
| @pytest.mark.xfail | ||
| def test_ad_nospace(self): | ||
| expected = datetime(6, 5, 19) | ||
| for dstr in [' 6AD May 19', ' 06AD May 19', | ||
| ' 006AD May 19', ' 0006AD May 19']: | ||
| res = parse(dstr) | ||
| assert res == expected, (dstr, res) | ||
| @pytest.mark.xfail | ||
| def test_four_letter_day(self): | ||
| dstr = 'Frid Dec 30, 2016' | ||
| expected = datetime(2016, 12, 30) | ||
| res = parse(dstr) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_non_date_number(self): | ||
| dstr = '1,700' | ||
| with pytest.raises(ValueError): | ||
| parse(dstr) | ||
| @pytest.mark.xfail | ||
| def test_on_era(self): | ||
| # This could be classified as an "eras" test, but the relevant part | ||
| # about this is the ` on ` | ||
| dstr = '2:15 PM on January 2nd 1973 A.D.' | ||
| expected = datetime(1973, 1, 2, 14, 15) | ||
| res = parse(dstr) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year(self): | ||
| # This was found in the wild at insidertrading.org | ||
| dstr = "2011 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" | ||
| res = parse(dstr, fuzzy_with_tokens=True) | ||
| expected = datetime(2012, 11, 7) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year_tokens(self): | ||
| # This was found in the wild at insidertrading.org | ||
| # Unlike in the case above, identifying the first "2012" as the year | ||
| # would not be a problem, but infering that the latter 2012 is hhmm | ||
| # is a problem. | ||
| dstr = "2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d NOVEMBER 7, 2012" | ||
| expected = datetime(2012, 11, 7) | ||
| (res, tokens) = parse(dstr, fuzzy_with_tokens=True) | ||
| assert res == expected | ||
| assert tokens == ("2012 MARTIN CHILDREN'S IRREVOCABLE TRUST u/a/d ",) | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year2(self): | ||
| # This was found in the wild at insidertrading.org | ||
| dstr = ("Berylson Amy Smith 1998 Grantor Retained Annuity Trust " | ||
| "u/d/t November 2, 1998 f/b/o Jennifer L Berylson") | ||
| res = parse(dstr, fuzzy_with_tokens=True) | ||
| expected = datetime(1998, 11, 2) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_extraneous_year3(self): | ||
| # This was found in the wild at insidertrading.org | ||
| dstr = "SMITH R & WEISS D 94 CHILD TR FBO M W SMITH UDT 12/1/1994" | ||
| res = parse(dstr, fuzzy_with_tokens=True) | ||
| expected = datetime(1994, 12, 1) | ||
| assert res == expected | ||
| @pytest.mark.xfail | ||
| def test_unambiguous_YYYYMM(self): | ||
| # 171206 can be parsed as YYMMDD. However, 201712 cannot be parsed | ||
| # as instance of YYMMDD and parser could fallback to YYYYMM format. | ||
| dstr = "201712" | ||
| res = parse(dstr) | ||
| expected = datetime(2017, 12, 1) | ||
| assert res == expected | ||
| @pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var') | ||
| def test_parse_unambiguous_nonexistent_local(): | ||
| # When dates are specified "EST" even when they should be "EDT" in the | ||
| # local time zone, we should still assign the local time zone | ||
| with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): | ||
| dt_exp = datetime(2011, 8, 1, 12, 30, tzinfo=tz.tzlocal()) | ||
| dt = parse('2011-08-01T12:30 EST') | ||
| assert dt.tzname() == 'EDT' | ||
| assert dt == dt_exp | ||
| @pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var') | ||
| def test_tzlocal_in_gmt(): | ||
| # GH #318 | ||
| with TZEnvContext('GMT0BST,M3.5.0,M10.5.0'): | ||
| # This is an imaginary datetime in tz.tzlocal() but should still | ||
| # parse using the GMT-as-alias-for-UTC rule | ||
| dt = parse('2004-05-01T12:00 GMT') | ||
| dt_exp = datetime(2004, 5, 1, 12, tzinfo=tz.tzutc()) | ||
| assert dt == dt_exp | ||
| @pytest.mark.skipif(IS_WIN, reason='Windows does not use TZ var') | ||
| def test_tzlocal_parse_fold(): | ||
| # One manifestion of GH #318 | ||
| with TZEnvContext('EST+5EDT,M3.2.0/2,M11.1.0/2'): | ||
| dt_exp = datetime(2011, 11, 6, 1, 30, tzinfo=tz.tzlocal()) | ||
| dt_exp = tz.enfold(dt_exp, fold=1) | ||
| dt = parse('2011-11-06T01:30 EST') | ||
| # Because this is ambiguous, kuntil `tz.tzlocal() is tz.tzlocal()` | ||
| # we'll just check the attributes we care about rather than | ||
| # dt == dt_exp | ||
| assert dt.tzname() == dt_exp.tzname() | ||
| assert dt.replace(tzinfo=None) == dt_exp.replace(tzinfo=None) | ||
| assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') | ||
| assert dt.astimezone(tz.tzutc()) == dt_exp.astimezone(tz.tzutc()) | ||
| def test_parse_tzinfos_fold(): | ||
| NYC = tz.gettz('America/New_York') | ||
| tzinfos = {'EST': NYC, 'EDT': NYC} | ||
| dt_exp = tz.enfold(datetime(2011, 11, 6, 1, 30, tzinfo=NYC), fold=1) | ||
| dt = parse('2011-11-06T01:30 EST', tzinfos=tzinfos) | ||
| assert dt == dt_exp | ||
| assert dt.tzinfo is dt_exp.tzinfo | ||
| assert getattr(dt, 'fold') == getattr(dt_exp, 'fold') | ||
| assert dt.astimezone(tz.tzutc()) == dt_exp.astimezone(tz.tzutc()) | ||
| @pytest.mark.parametrize('dtstr,dt', [ | ||
| ('5.6h', datetime(2003, 9, 25, 5, 36)), | ||
| ('5.6m', datetime(2003, 9, 25, 0, 5, 36)), | ||
| # '5.6s' never had a rounding problem, test added for completeness | ||
| ('5.6s', datetime(2003, 9, 25, 0, 0, 5, 600000)) | ||
| ]) | ||
| def test_rounding_floatlike_strings(dtstr, dt): | ||
| assert parse(dtstr, default=datetime(2003, 9, 25)) == dt |
| # -*- coding: utf-8 -*- | ||
| from __future__ import unicode_literals | ||
| from ._common import unittest, WarningTestMixin, NotAValue | ||
| from ._common import WarningTestMixin, NotAValue | ||
| import calendar | ||
| from datetime import datetime, date, timedelta | ||
| import unittest | ||
@@ -205,2 +206,27 @@ from dateutil.relativedelta import relativedelta, MO, TU, WE, FR, SU | ||
| def testAdditionFloatValue(self): | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(days=float(1)), | ||
| datetime(2000, 1, 2)) | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(months=float(1)), | ||
| datetime(2000, 2, 1)) | ||
| self.assertEqual(datetime(2000, 1, 1) + relativedelta(years=float(1)), | ||
| datetime(2001, 1, 1)) | ||
| def testAdditionFloatFractionals(self): | ||
| self.assertEqual(datetime(2000, 1, 1, 0) + | ||
| relativedelta(days=float(0.5)), | ||
| datetime(2000, 1, 1, 12)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0) + | ||
| relativedelta(hours=float(0.5)), | ||
| datetime(2000, 1, 1, 0, 30)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0, 0) + | ||
| relativedelta(minutes=float(0.5)), | ||
| datetime(2000, 1, 1, 0, 0, 30)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + | ||
| relativedelta(seconds=float(0.5)), | ||
| datetime(2000, 1, 1, 0, 0, 0, 500000)) | ||
| self.assertEqual(datetime(2000, 1, 1, 0, 0, 0, 0) + | ||
| relativedelta(microseconds=float(500000.25)), | ||
| datetime(2000, 1, 1, 0, 0, 0, 500000)) | ||
| def testSubtraction(self): | ||
@@ -248,2 +274,16 @@ self.assertEqual(relativedelta(days=10) - | ||
| def testAbsoluteValueNegative(self): | ||
| rd_base = relativedelta(years=-1, months=-5, days=-2, hours=-3, | ||
| minutes=-5, seconds=-2, microseconds=-12) | ||
| rd_expected = relativedelta(years=1, months=5, days=2, hours=3, | ||
| minutes=5, seconds=2, microseconds=12) | ||
| self.assertEqual(abs(rd_base), rd_expected) | ||
| def testAbsoluteValuePositive(self): | ||
| rd_base = relativedelta(years=1, months=5, days=2, hours=3, | ||
| minutes=5, seconds=2, microseconds=12) | ||
| rd_expected = rd_base | ||
| self.assertEqual(abs(rd_base), rd_expected) | ||
| def testComparison(self): | ||
@@ -580,1 +620,62 @@ d1 = relativedelta(years=1, months=1, days=1, leapdays=0, hours=1, | ||
| self.assertEqual(expected, rd + td) | ||
| def testHashable(self): | ||
| try: | ||
| {relativedelta(minute=1): 'test'} | ||
| except: | ||
| self.fail("relativedelta() failed to hash!") | ||
| class RelativeDeltaWeeksPropertyGetterTest(unittest.TestCase): | ||
| """Test the weeks property getter""" | ||
| def test_one_day(self): | ||
| rd = relativedelta(days=1) | ||
| self.assertEqual(rd.days, 1) | ||
| self.assertEqual(rd.weeks, 0) | ||
| def test_minus_one_day(self): | ||
| rd = relativedelta(days=-1) | ||
| self.assertEqual(rd.days, -1) | ||
| self.assertEqual(rd.weeks, 0) | ||
| def test_height_days(self): | ||
| rd = relativedelta(days=8) | ||
| self.assertEqual(rd.days, 8) | ||
| self.assertEqual(rd.weeks, 1) | ||
| def test_minus_height_days(self): | ||
| rd = relativedelta(days=-8) | ||
| self.assertEqual(rd.days, -8) | ||
| self.assertEqual(rd.weeks, -1) | ||
| class RelativeDeltaWeeksPropertySetterTest(unittest.TestCase): | ||
| """Test the weeks setter which makes a "smart" update of the days attribute""" | ||
| def test_one_day_set_one_week(self): | ||
| rd = relativedelta(days=1) | ||
| rd.weeks = 1 # add 7 days | ||
| self.assertEqual(rd.days, 8) | ||
| self.assertEqual(rd.weeks, 1) | ||
| def test_minus_one_day_set_one_week(self): | ||
| rd = relativedelta(days=-1) | ||
| rd.weeks = 1 # add 7 days | ||
| self.assertEqual(rd.days, 6) | ||
| self.assertEqual(rd.weeks, 0) | ||
| def test_height_days_set_minus_one_week(self): | ||
| rd = relativedelta(days=8) | ||
| rd.weeks = -1 # change from 1 week, 1 day to -1 week, 1 day | ||
| self.assertEqual(rd.days, -6) | ||
| self.assertEqual(rd.weeks, 0) | ||
| def test_minus_height_days_set_minus_one_week(self): | ||
| rd = relativedelta(days=-8) | ||
| rd.weeks = -1 # does not change anything | ||
| self.assertEqual(rd.days, -8) | ||
| self.assertEqual(rd.weeks, -1) | ||
| # vim:ts=4:sw=4:et |
| from .tz import * | ||
| #: Convenience constant providing a :class:`tzutc()` instance | ||
| #: | ||
| #: .. versionadded:: 2.7.0 | ||
| UTC = tzutc() | ||
| __all__ = ["tzutc", "tzoffset", "tzlocal", "tzfile", "tzrange", | ||
| "tzstr", "tzical", "tzwin", "tzwinlocal", "gettz", | ||
| "enfold", "datetime_ambiguous", "datetime_exists"] | ||
| "enfold", "datetime_ambiguous", "datetime_exists", | ||
| "resolve_imaginary", "UTC", "DeprecatedTzFormatWarning"] | ||
| class DeprecatedTzFormatWarning(Warning): | ||
| """Warning raised when time zones are parsed from deprecated formats.""" |
@@ -64,2 +64,32 @@ from six import PY3 | ||
| def replace(self, *args, **kwargs): | ||
| """ | ||
| Return a datetime with the same attributes, except for those | ||
| attributes given new values by whichever keyword arguments are | ||
| specified. Note that tzinfo=None can be specified to create a naive | ||
| datetime from an aware datetime with no conversion of date and time | ||
| data. | ||
| This is reimplemented in ``_DatetimeWithFold`` because pypy3 will | ||
| return a ``datetime.datetime`` even if ``fold`` is unchanged. | ||
| """ | ||
| argnames = ( | ||
| 'year', 'month', 'day', 'hour', 'minute', 'second', | ||
| 'microsecond', 'tzinfo' | ||
| ) | ||
| for arg, argname in zip(args, argnames): | ||
| if argname in kwargs: | ||
| raise TypeError('Duplicate argument: {}'.format(argname)) | ||
| kwargs[argname] = arg | ||
| for argname in argnames: | ||
| if argname not in kwargs: | ||
| kwargs[argname] = getattr(self, argname) | ||
| dt_class = self.__class__ if kwargs.get('fold', 1) else datetime | ||
| return dt_class(**kwargs) | ||
| @property | ||
@@ -387,10 +417,1 @@ def fold(self): | ||
| __reduce__ = object.__reduce__ | ||
| def _total_seconds(td): | ||
| # Python 2.6 doesn't have a total_seconds() method on timedelta objects | ||
| return ((td.seconds + td.days * 86400) * 1000000 + | ||
| td.microseconds) // 1000000 | ||
| _total_seconds = getattr(timedelta, 'total_seconds', _total_seconds) |
+239
-94
@@ -17,7 +17,11 @@ # -*- coding: utf-8 -*- | ||
| import six | ||
| from six import string_types | ||
| from ._common import tzname_in_python2, _tzinfo, _total_seconds | ||
| from six.moves import _thread | ||
| from ._common import tzname_in_python2, _tzinfo | ||
| from ._common import tzrangebase, enfold | ||
| from ._common import _validate_fromutc_inputs | ||
| from ._factories import _TzSingleton, _TzOffsetFactory | ||
| from ._factories import _TzStrFactory | ||
| try: | ||
@@ -33,5 +37,34 @@ from .win import tzwin, tzwinlocal | ||
| @six.add_metaclass(_TzSingleton) | ||
| class tzutc(datetime.tzinfo): | ||
| """ | ||
| This is a tzinfo object that represents the UTC time zone. | ||
| **Examples:** | ||
| .. doctest:: | ||
| >>> from datetime import * | ||
| >>> from dateutil.tz import * | ||
| >>> datetime.now() | ||
| datetime.datetime(2003, 9, 27, 9, 40, 1, 521290) | ||
| >>> datetime.now(tzutc()) | ||
| datetime.datetime(2003, 9, 27, 12, 40, 12, 156379, tzinfo=tzutc()) | ||
| >>> datetime.now(tzutc()).tzname() | ||
| 'UTC' | ||
| .. versionchanged:: 2.7.0 | ||
| ``tzutc()`` is now a singleton, so the result of ``tzutc()`` will | ||
| always return the same object. | ||
| .. doctest:: | ||
| >>> from dateutil.tz import tzutc, UTC | ||
| >>> tzutc() is tzutc() | ||
| True | ||
| >>> tzutc() is UTC | ||
| True | ||
| """ | ||
@@ -90,2 +123,3 @@ def utcoffset(self, dt): | ||
| @six.add_metaclass(_TzOffsetFactory) | ||
| class tzoffset(datetime.tzinfo): | ||
@@ -97,6 +131,5 @@ """ | ||
| The timezone name, to be returned when ``tzname()`` is called. | ||
| :param offset: | ||
| The time zone offset in seconds, or (since version 2.6.0, represented | ||
| as a :py:class:`datetime.timedelta` object. | ||
| as a :py:class:`datetime.timedelta` object). | ||
| """ | ||
@@ -108,3 +141,3 @@ def __init__(self, name, offset): | ||
| # Allow a timedelta | ||
| offset = _total_seconds(offset) | ||
| offset = offset.total_seconds() | ||
| except (TypeError, AttributeError): | ||
@@ -135,4 +168,2 @@ pass | ||
| A :py:class:`datetime.datetime`, naive or time zone aware. | ||
| :return: | ||
@@ -159,3 +190,3 @@ Returns ``True`` if ambiguous, ``False`` otherwise. | ||
| repr(self._name), | ||
| int(_total_seconds(self._offset))) | ||
| int(self._offset.total_seconds())) | ||
@@ -180,2 +211,3 @@ __reduce__ = object.__reduce__ | ||
| self._hasdst = bool(self._dst_saved) | ||
| self._tznames = tuple(time.tzname) | ||
@@ -202,3 +234,3 @@ def utcoffset(self, dt): | ||
| def tzname(self, dt): | ||
| return time.tzname[self._isdst(dt)] | ||
| return self._tznames[self._isdst(dt)] | ||
@@ -268,8 +300,16 @@ def is_ambiguous(self, dt): | ||
| def __eq__(self, other): | ||
| if not isinstance(other, tzlocal): | ||
| if isinstance(other, tzlocal): | ||
| return (self._std_offset == other._std_offset and | ||
| self._dst_offset == other._dst_offset) | ||
| elif isinstance(other, tzutc): | ||
| return (not self._hasdst and | ||
| self._tznames[0] in {'UTC', 'GMT'} and | ||
| self._std_offset == ZERO) | ||
| elif isinstance(other, tzoffset): | ||
| return (not self._hasdst and | ||
| self._tznames[0] == other._name and | ||
| self._std_offset == other._offset) | ||
| else: | ||
| return NotImplemented | ||
| return (self._std_offset == other._std_offset and | ||
| self._dst_offset == other._dst_offset) | ||
| __hash__ = None | ||
@@ -360,4 +400,4 @@ | ||
| See `Sources for Time Zone and Daylight Saving Time Data | ||
| <http://www.twinsun.com/tz/tz-link.htm>`_ for more information. Time zone | ||
| files can be compiled from the `IANA Time Zone database files | ||
| <https://data.iana.org/time-zones/tz-link.html>`_ for more information. Time | ||
| zone files can be compiled from the `IANA Time Zone database files | ||
| <https://www.iana.org/time-zones>`_ with the `zic time zone compiler | ||
@@ -630,3 +670,3 @@ <https://www.freebsd.org/cgi/man.cgi?query=zic&sektion=8>`_ | ||
| # We want to know when the previous transition was, so subtract off 1 | ||
| return idx - 1 | ||
| return idx - 1 | ||
@@ -865,3 +905,3 @@ def _get_ttinfo(self, idx): | ||
| try: | ||
| stdoffset = _total_seconds(stdoffset) | ||
| stdoffset = stdoffset.total_seconds() | ||
| except (TypeError, AttributeError): | ||
@@ -871,3 +911,3 @@ pass | ||
| try: | ||
| dstoffset = _total_seconds(dstoffset) | ||
| dstoffset = dstoffset.total_seconds() | ||
| except (TypeError, AttributeError): | ||
@@ -943,2 +983,3 @@ pass | ||
| @six.add_metaclass(_TzStrFactory) | ||
| class tzstr(tzrange): | ||
@@ -970,2 +1011,14 @@ """ | ||
| .. caution:: | ||
| Prior to version 2.7.0, this function also supported time zones | ||
| in the format: | ||
| * ``EST5EDT,4,0,6,7200,10,0,26,7200,3600`` | ||
| * ``EST5EDT,4,1,0,7200,10,-1,0,7200,3600`` | ||
| This format is non-standard and has been deprecated; this function | ||
| will raise a :class:`DeprecatedTZFormatWarning` until | ||
| support is removed in a future version. | ||
| .. _`GNU C Library: TZ Variable`: | ||
@@ -976,3 +1029,3 @@ https://www.gnu.org/software/libc/manual/html_node/TZ-Variable.html | ||
| global parser | ||
| from dateutil import parser | ||
| from dateutil.parser import _parser as parser | ||
@@ -982,3 +1035,3 @@ self._s = s | ||
| res = parser._parsetz(s) | ||
| if res is None: | ||
| if res is None or res.any_unused_tokens: | ||
| raise ValueError("unknown string format") | ||
@@ -1072,2 +1125,3 @@ | ||
| self._cachecomp = [] | ||
| self._cache_lock = _thread.allocate_lock() | ||
@@ -1081,3 +1135,5 @@ def _find_comp(self, dt): | ||
| try: | ||
| return self._cachecomp[self._cachedate.index((dt, self._fold(dt)))] | ||
| with self._cache_lock: | ||
| return self._cachecomp[self._cachedate.index( | ||
| (dt, self._fold(dt)))] | ||
| except ValueError: | ||
@@ -1108,8 +1164,9 @@ pass | ||
| self._cachedate.insert(0, (dt, self._fold(dt))) | ||
| self._cachecomp.insert(0, lastcomp) | ||
| with self._cache_lock: | ||
| self._cachedate.insert(0, (dt, self._fold(dt))) | ||
| self._cachecomp.insert(0, lastcomp) | ||
| if len(self._cachedate) > 10: | ||
| self._cachedate.pop() | ||
| self._cachecomp.pop() | ||
| if len(self._cachedate) > 10: | ||
| self._cachedate.pop() | ||
| self._cachecomp.pop() | ||
@@ -1152,3 +1209,3 @@ return lastcomp | ||
| This object is designed to parse an iCalendar-style ``VTIMEZONE`` structure | ||
| as set out in `RFC 2445`_ Section 4.6.5 into one or more `tzinfo` objects. | ||
| as set out in `RFC 5545`_ Section 4.6.5 into one or more `tzinfo` objects. | ||
@@ -1159,3 +1216,3 @@ :param `fileobj`: | ||
| .. _`RFC 2445`: https://www.ietf.org/rfc/rfc2445.txt | ||
| .. _`RFC 5545`: https://tools.ietf.org/html/rfc5545 | ||
| """ | ||
@@ -1308,2 +1365,9 @@ def __init__(self, fileobj): | ||
| if name == "DTSTART": | ||
| # DTSTART in VTIMEZONE takes a subset of valid RRULE | ||
| # values under RFC 5545. | ||
| for parm in parms: | ||
| if parm != 'VALUE=DATE-TIME': | ||
| msg = ('Unsupported DTSTART param in ' + | ||
| 'VTIMEZONE: ' + parm) | ||
| raise ValueError(msg) | ||
| rrulelines.append(line) | ||
@@ -1361,77 +1425,111 @@ founddtstart = True | ||
| def __get_gettz(): | ||
| tzlocal_classes = (tzlocal,) | ||
| if tzwinlocal is not None: | ||
| tzlocal_classes += (tzwinlocal,) | ||
| def gettz(name=None): | ||
| tz = None | ||
| if not name: | ||
| try: | ||
| name = os.environ["TZ"] | ||
| except KeyError: | ||
| pass | ||
| if name is None or name == ":": | ||
| for filepath in TZFILES: | ||
| if not os.path.isabs(filepath): | ||
| filename = filepath | ||
| for path in TZPATHS: | ||
| filepath = os.path.join(path, filename) | ||
| class GettzFunc(object): | ||
| def __init__(self): | ||
| self.__instances = {} | ||
| self._cache_lock = _thread.allocate_lock() | ||
| def __call__(self, name=None): | ||
| with self._cache_lock: | ||
| rv = self.__instances.get(name, None) | ||
| if rv is None: | ||
| rv = self.nocache(name=name) | ||
| if not (name is None or isinstance(rv, tzlocal_classes)): | ||
| # tzlocal is slightly more complicated than the other | ||
| # time zone providers because it depends on environment | ||
| # at construction time, so don't cache that. | ||
| self.__instances[name] = rv | ||
| return rv | ||
| def cache_clear(self): | ||
| with self._cache_lock: | ||
| self.__instances = {} | ||
| @staticmethod | ||
| def nocache(name=None): | ||
| """A non-cached version of gettz""" | ||
| tz = None | ||
| if not name: | ||
| try: | ||
| name = os.environ["TZ"] | ||
| except KeyError: | ||
| pass | ||
| if name is None or name == ":": | ||
| for filepath in TZFILES: | ||
| if not os.path.isabs(filepath): | ||
| filename = filepath | ||
| for path in TZPATHS: | ||
| filepath = os.path.join(path, filename) | ||
| if os.path.isfile(filepath): | ||
| break | ||
| else: | ||
| continue | ||
| if os.path.isfile(filepath): | ||
| break | ||
| try: | ||
| tz = tzfile(filepath) | ||
| break | ||
| except (IOError, OSError, ValueError): | ||
| pass | ||
| else: | ||
| continue | ||
| if os.path.isfile(filepath): | ||
| try: | ||
| tz = tzfile(filepath) | ||
| break | ||
| except (IOError, OSError, ValueError): | ||
| pass | ||
| else: | ||
| tz = tzlocal() | ||
| else: | ||
| if name.startswith(":"): | ||
| name = name[:-1] | ||
| if os.path.isabs(name): | ||
| if os.path.isfile(name): | ||
| tz = tzfile(name) | ||
| tz = tzlocal() | ||
| else: | ||
| tz = None | ||
| else: | ||
| for path in TZPATHS: | ||
| filepath = os.path.join(path, name) | ||
| if not os.path.isfile(filepath): | ||
| filepath = filepath.replace(' ', '_') | ||
| if not os.path.isfile(filepath): | ||
| continue | ||
| try: | ||
| tz = tzfile(filepath) | ||
| break | ||
| except (IOError, OSError, ValueError): | ||
| pass | ||
| else: | ||
| tz = None | ||
| if tzwin is not None: | ||
| try: | ||
| tz = tzwin(name) | ||
| except WindowsError: | ||
| if name.startswith(":"): | ||
| name = name[1:] | ||
| if os.path.isabs(name): | ||
| if os.path.isfile(name): | ||
| tz = tzfile(name) | ||
| else: | ||
| tz = None | ||
| if not tz: | ||
| from dateutil.zoneinfo import get_zonefile_instance | ||
| tz = get_zonefile_instance().get(name) | ||
| if not tz: | ||
| for c in name: | ||
| # name must have at least one offset to be a tzstr | ||
| if c in "0123456789": | ||
| try: | ||
| tz = tzstr(name) | ||
| except ValueError: | ||
| pass | ||
| else: | ||
| for path in TZPATHS: | ||
| filepath = os.path.join(path, name) | ||
| if not os.path.isfile(filepath): | ||
| filepath = filepath.replace(' ', '_') | ||
| if not os.path.isfile(filepath): | ||
| continue | ||
| try: | ||
| tz = tzfile(filepath) | ||
| break | ||
| except (IOError, OSError, ValueError): | ||
| pass | ||
| else: | ||
| if name in ("GMT", "UTC"): | ||
| tz = tzutc() | ||
| elif name in time.tzname: | ||
| tz = tzlocal() | ||
| return tz | ||
| tz = None | ||
| if tzwin is not None: | ||
| try: | ||
| tz = tzwin(name) | ||
| except WindowsError: | ||
| tz = None | ||
| if not tz: | ||
| from dateutil.zoneinfo import get_zonefile_instance | ||
| tz = get_zonefile_instance().get(name) | ||
| if not tz: | ||
| for c in name: | ||
| # name must have at least one offset to be a tzstr | ||
| if c in "0123456789": | ||
| try: | ||
| tz = tzstr(name) | ||
| except ValueError: | ||
| pass | ||
| break | ||
| else: | ||
| if name in ("GMT", "UTC"): | ||
| tz = tzutc() | ||
| elif name in time.tzname: | ||
| tz = tzlocal() | ||
| return tz | ||
| return GettzFunc() | ||
| gettz = __get_gettz() | ||
| del __get_gettz | ||
| def datetime_exists(dt, tz=None): | ||
@@ -1452,2 +1550,4 @@ """ | ||
| Returns a boolean value whether or not the "wall time" exists in ``tz``. | ||
| ..versionadded:: 2.7.0 | ||
| """ | ||
@@ -1515,2 +1615,47 @@ if tz is None: | ||
| def resolve_imaginary(dt): | ||
| """ | ||
| Given a datetime that may be imaginary, return an existing datetime. | ||
| This function assumes that an imaginary datetime represents what the | ||
| wall time would be in a zone had the offset transition not occurred, so | ||
| it will always fall forward by the transition's change in offset. | ||
| ..doctest:: | ||
| >>> from dateutil import tz | ||
| >>> from datetime import datetime | ||
| >>> NYC = tz.gettz('America/New_York') | ||
| >>> print(tz.resolve_imaginary(datetime(2017, 3, 12, 2, 30, tzinfo=NYC))) | ||
| 2017-03-12 03:30:00-04:00 | ||
| >>> KIR = tz.gettz('Pacific/Kiritimati') | ||
| >>> print(tz.resolve_imaginary(datetime(1995, 1, 1, 12, 30, tzinfo=KIR))) | ||
| 1995-01-02 12:30:00+14:00 | ||
| As a note, :func:`datetime.astimezone` is guaranteed to produce a valid, | ||
| existing datetime, so a round-trip to and from UTC is sufficient to get | ||
| an extant datetime, however, this generally "falls back" to an earlier time | ||
| rather than falling forward to the STD side (though no guarantees are made | ||
| about this behavior). | ||
| :param dt: | ||
| A :class:`datetime.datetime` which may or may not exist. | ||
| :return: | ||
| Returns an existing :class:`datetime.datetime`. If ``dt`` was not | ||
| imaginary, the datetime returned is guaranteed to be the same object | ||
| passed to the function. | ||
| ..versionadded:: 2.7.0 | ||
| """ | ||
| if dt.tzinfo is not None and not datetime_exists(dt): | ||
| curr_offset = (dt + datetime.timedelta(hours=24)).utcoffset() | ||
| old_offset = (dt - datetime.timedelta(hours=24)).utcoffset() | ||
| dt += curr_offset - old_offset | ||
| return dt | ||
| def _datetime_to_timestamp(dt): | ||
@@ -1521,3 +1666,3 @@ """ | ||
| """ | ||
| return _total_seconds((dt.replace(tzinfo=None) - EPOCH)) | ||
| return (dt.replace(tzinfo=None) - EPOCH).total_seconds() | ||
@@ -1524,0 +1669,0 @@ |
@@ -195,3 +195,2 @@ # This code was originally contributed by Jeffrey Harris. | ||
| # multiple contexts only possible in 2.7 and 3.1, we still support 2.6 | ||
| with winreg.ConnectRegistry(None, winreg.HKEY_LOCAL_MACHINE) as handle: | ||
@@ -198,0 +197,0 @@ tzkeyname = text_type("{kn}\\{name}").format(kn=TZKEYNAME, name=name) |
@@ -8,7 +8,6 @@ # -*- coding: utf-8 -*- | ||
| from io import BytesIO | ||
| from contextlib import closing | ||
| from dateutil.tz import tzfile | ||
| from dateutil.tz import tzfile as _tzfile | ||
| __all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata", "rebuild"] | ||
| __all__ = ["get_zonefile_instance", "gettz", "gettz_db_metadata"] | ||
@@ -18,11 +17,4 @@ ZONEFILENAME = "dateutil-zoneinfo.tar.gz" | ||
| # python2.6 compatability. Note that TarFile.__exit__ != TarFile.close, but | ||
| # it's close enough for python2.6 | ||
| tar_open = TarFile.open | ||
| if not hasattr(TarFile, '__exit__'): | ||
| def tar_open(*args, **kwargs): | ||
| return closing(TarFile.open(*args, **kwargs)) | ||
| class tzfile(tzfile): | ||
| class tzfile(_tzfile): | ||
| def __reduce__(self): | ||
@@ -43,19 +35,11 @@ return (gettz, (self._filename,)) | ||
| if zonefile_stream is not None: | ||
| with tar_open(fileobj=zonefile_stream, mode='r') as tf: | ||
| # dict comprehension does not work on python2.6 | ||
| # TODO: get back to the nicer syntax when we ditch python2.6 | ||
| # self.zones = {zf.name: tzfile(tf.extractfile(zf), | ||
| # filename = zf.name) | ||
| # for zf in tf.getmembers() if zf.isfile()} | ||
| self.zones = dict((zf.name, tzfile(tf.extractfile(zf), | ||
| filename=zf.name)) | ||
| for zf in tf.getmembers() | ||
| if zf.isfile() and zf.name != METADATA_FN) | ||
| with TarFile.open(fileobj=zonefile_stream) as tf: | ||
| self.zones = {zf.name: tzfile(tf.extractfile(zf), filename=zf.name) | ||
| for zf in tf.getmembers() | ||
| if zf.isfile() and zf.name != METADATA_FN} | ||
| # deal with links: They'll point to their parent object. Less | ||
| # waste of memory | ||
| # links = {zl.name: self.zones[zl.linkname] | ||
| # for zl in tf.getmembers() if zl.islnk() or zl.issym()} | ||
| links = dict((zl.name, self.zones[zl.linkname]) | ||
| for zl in tf.getmembers() if | ||
| zl.islnk() or zl.issym()) | ||
| links = {zl.name: self.zones[zl.linkname] | ||
| for zl in tf.getmembers() if | ||
| zl.islnk() or zl.issym()} | ||
| self.zones.update(links) | ||
@@ -70,3 +54,3 @@ try: | ||
| else: | ||
| self.zones = dict() | ||
| self.zones = {} | ||
| self.metadata = None | ||
@@ -97,3 +81,3 @@ | ||
| # TODO: Remove after deprecation period. | ||
| _CLASS_ZONE_INSTANCE = list() | ||
| _CLASS_ZONE_INSTANCE = [] | ||
@@ -100,0 +84,0 @@ |
@@ -7,4 +7,5 @@ import logging | ||
| from subprocess import check_call | ||
| from tarfile import TarFile | ||
| from dateutil.zoneinfo import tar_open, METADATA_FN, ZONEFILENAME | ||
| from dateutil.zoneinfo import METADATA_FN, ZONEFILENAME | ||
@@ -15,3 +16,3 @@ | ||
| filename is the timezone tarball from ftp.iana.org/tz. | ||
| filename is the timezone tarball from ``ftp.iana.org/tz``. | ||
@@ -23,3 +24,3 @@ """ | ||
| try: | ||
| with tar_open(filename) as tf: | ||
| with TarFile.open(filename) as tf: | ||
| for name in zonegroups: | ||
@@ -37,3 +38,3 @@ tf.extract(name, tmpdir) | ||
| target = os.path.join(moduledir, ZONEFILENAME) | ||
| with tar_open(target, "w:%s" % format) as tf: | ||
| with TarFile.open(target, "w:%s" % format) as tf: | ||
| for entry in os.listdir(zonedir): | ||
@@ -40,0 +41,0 @@ entrypath = os.path.join(zonedir, entry) |
+22
-0
@@ -0,1 +1,20 @@ | ||
| Copyright 2017- Paul Ganssle <paul@ganssle.io> | ||
| Copyright 2017- dateutil contributors (see AUTHORS file) | ||
| Licensed under the Apache License, Version 2.0 (the "License"); | ||
| you may not use this file except in compliance with the License. | ||
| You may obtain a copy of the License at | ||
| http://www.apache.org/licenses/LICENSE-2.0 | ||
| Unless required by applicable law or agreed to in writing, software | ||
| distributed under the License is distributed on an "AS IS" BASIS, | ||
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| See the License for the specific language governing permissions and | ||
| limitations under the License. | ||
| The above license applies to all contributions after 2017-12-01, as well as | ||
| all contributions that have been re-licensed (see AUTHORS file for the list of | ||
| contributors who have re-licensed their code). | ||
| -------------------------------------------------------------------------------- | ||
| dateutil - Extensions to the standard Python datetime module. | ||
@@ -7,2 +26,3 @@ | ||
| Copyright (c) 2015- - Paul Ganssle <paul@ganssle.io> | ||
| Copyright (c) 2015- - dateutil contributors (see AUTHORS file) | ||
@@ -34,1 +54,3 @@ All rights reserved. | ||
| SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | ||
| The above BSD License Applies to all code, even that also covered by Apache 2.0. |
+149
-1
@@ -0,1 +1,149 @@ | ||
| Version 2.7.0 | ||
| ---------- | ||
| - Dropped support for Python 2.6 (gh pr #362 by @jdufresne) | ||
| - Dropped support for Python 3.2 (gh pr #626) | ||
| - Updated zoneinfo file to 2018c (gh pr #616) | ||
| - Changed licensing scheme so all new contributions are dual licensed under | ||
| Apache 2.0 and BSD. (gh pr #542, issue #496) | ||
| - Added __all__ variable to the root package. Reported by @tebriel | ||
| (gh issue #406), fixed by @mariocj89 (gh pr #494) | ||
| - Added python_requires to setup.py so that pip will distribute the right | ||
| version of dateutil. Fixed by @jakec-github (gh issue #537, pr #552) | ||
| - Added the utils submodule, for miscellaneous utilities. | ||
| - Added within_delta function to utils - added by @justanr (gh issue #432, | ||
| gh pr #437) | ||
| - Added today function to utils (gh pr #474) | ||
| - Added default_tzinfo function to utils (gh pr #475), solving an issue | ||
| reported by @nealmcb (gh issue #94) | ||
| - Added dedicated ISO 8601 parsing function isoparse (gh issue #424). | ||
| Initial implementation by @pganssle in gh pr #489 and #622, with a | ||
| pre-release fix by @kirit93 (gh issue #546, gh pr #573). | ||
| - Moved parser module into parser/_parser.py and officially deprecated the use | ||
| of several private functions and classes from that module. (gh pr #501, #515) | ||
| - Tweaked parser error message to include rejected string format, added by | ||
| @pbiering (gh pr #300) | ||
| - Add support for parsing bytesarray, reported by @uckelman (gh issue #417) and | ||
| fixed by @uckelman and @pganssle (gh pr #514) | ||
| - Started raising a warning when the parser finds a timezone string that it | ||
| cannot construct a tzinfo instance for (rather than succeeding with no | ||
| indication of an error). Reported and fixed by @jbrockmendel (gh pr #540) | ||
| - Dropped the use of assert in the parser. Fixed by @jbrockmendel (gh pr #502) | ||
| - Fixed to assertion logic in parser to support dates like '2015-15-May', | ||
| reported and fixed by @jbrockmendel (gh pr #409) | ||
| - Fixed IndexError in parser on dates with trailing colons, reported and fixed | ||
| by @jbrockmendel (gh pr #420) | ||
| - Fixed bug where hours were not validated, leading to improper parse. Reported | ||
| by @heappro (gh pr #353), fixed by @jbrockmendel (gh pr #482) | ||
| - Fixed problem parsing strings in %b-%Y-%d format. Reported and fixed by | ||
| @jbrockmendel (gh pr #481) | ||
| - Fixed problem parsing strings in the %d%B%y format. Reported by @asishm | ||
| (gh issue #360), fixed by @jbrockmendel (gh pr #483) | ||
| - Fixed problem parsing certain unambiguous strings when year <99 (gh pr #510). | ||
| Reported by @alexwlchan (gh issue #293). | ||
| - Fixed issue with parsing an unambiguous string representation of an ambiguous | ||
| datetime such that if possible the correct value for fold is set. Fixes | ||
| issue reported by @JordonPhillips and @pganssle (gh issue #318, #320, | ||
| gh pr #517) | ||
| - Fixed issue with improper rounding of fractional components. Reported by | ||
| @dddmello (gh issue #427), fixed by @m-dz (gh pr #570) | ||
| - Performance improvement to parser from removing certain min() calls. Reported | ||
| and fixed by @jbrockmendel (gh pr #589) | ||
| - Significantly refactored parser code by @jbrockmendel (gh prs #419, #436, | ||
| #490, #498, #539) and @pganssle (gh prs #435, #468) | ||
| - Implementated of __hash__ for relativedelta and weekday, reported and fixed | ||
| by @mrigor (gh pr #389) | ||
| - Implemented __abs__ for relativedelta. Reported by @binnisb and @pferreir | ||
| (gh issue #350, pr #472) | ||
| - Fixed relativedelta.weeks property getter and setter to work for both | ||
| negative and positive values. Reported and fixed by @souliane (gh issue #459, | ||
| pr #460) | ||
| - Fixed issue where passing whole number floats to the months or years | ||
| arguments of the relativedelta constructor would lead to errors during | ||
| addition. Reported by @arouanet (gh pr #411), fixed by @lkollar (gh pr #553) | ||
| - Added a pre-built tz.UTC object representing UTC (gh pr #497) | ||
| - Added a cache to tz.gettz so that by default it will return the same object | ||
| for identical inputs. This will change the semantics of certain operations | ||
| between datetimes constructed with tzinfo=tz.gettz(...). (gh pr #628) | ||
| - Changed the behavior of tz.tzutc to return a singleton (gh pr #497, #504) | ||
| - Changed the behavior of tz.tzoffset to return the same object when passed the | ||
| same inputs, with a corresponding performance improvement (gh pr #504) | ||
| - Changed the behavior of tz.tzstr to return the same object when passed the | ||
| same inputs. (gh pr #628) | ||
| - Added .instance alternate constructors for tz.tzoffset and tz.tzstr, to | ||
| allow the construction of a new instance if desired. (gh pr #628) | ||
| - Added the tz.gettz.nocache function to allow explicit retrieval of a new | ||
| instance of the relevant tzinfo. (gh pr #628) | ||
| - Expand definition of tz.tzlocal equality so that the local zone is allow | ||
| equality with tzoffset and tzutc. (gh pr #598) | ||
| - Deprecated the idiosyncratic tzstr format mentioned in several examples but | ||
| evidently designed exclusively for dateutil, and very likely not used by | ||
| any current users. (gh issue #595, gh pr #606) | ||
| - Added the tz.resolve_imaginary function, which generates a real date from | ||
| an imaginary one, if necessary. Implemented by @Cheukting (gh issue #339, | ||
| gh pr #607) | ||
| - Fixed issue where the tz.tzstr constructor would erroneously succeed if | ||
| passed an invalid value for tzstr. Fixed by @pablogsal (gh issue #259, | ||
| gh pr #581) | ||
| - Fixed issue with tz.gettz for TZ variables that start with a colon. Reported | ||
| and fixed by @lapointexavier (gh pr #601) | ||
| - Added a lock to tz.tzical's cache. Reported and fixed by @Unrud (gh pr #430) | ||
| - Fixed an issue with fold support on certain Python 3 implementations that | ||
| used the pre-3.6 pure Python implementation of datetime.replace, most | ||
| notably pypy3 (gh pr #446). | ||
| - Added support for VALUE=DATE-TIME for DTSTART in rrulestr. Reported by @potuz | ||
| (gh issue #401) and fixed by @Unrud (gh pr #429) | ||
| - Started enforcing that within VTIMEZONE, the VALUE parameter can only be | ||
| omitted or DATE-TIME, per RFC 5545. Reported by @Unrud (gh pr #439) | ||
| - Added support for TZID parameter for DTSTART in rrulestr. Reported and | ||
| fixed by @ryanpetrello (gh issue #614, gh pr #624) | ||
| - Added 'RRULE:' prefix to rrule strings generated by rrule.__str__, in | ||
| compliance with the RFC. Reported by @AndrewPashkin (gh issue #86), fixed by | ||
| @jarondl and @mlorant (gh pr #450) | ||
| - Switched to setuptools_scm for version management, automatically calculating | ||
| a version number from the git metadata. Reported by @jreback (gh issue #511), | ||
| implemented by @Sulley38 (gh pr #564) | ||
| - Switched setup.py to use find_packages, and started testing against pip | ||
| installed versions of dateutil in CI. Fixed issue with parser import | ||
| discovered by @jreback in pandas-dev/pandas#18141. (gh issue #507, pr #509) | ||
| - Switched test suite to using pytest (gh pr #495) | ||
| - Switched CI over to use tox. Fixed by @gaborbernat (gh pr #549) | ||
| - Added a test-only dependency on freezegun. (gh pr #474) | ||
| - Reduced number of CI builds on Appveyor. Fixed by @kirit93 (gh issue #529, | ||
| gh pr #579) | ||
| - Made xfails strict by default, so that an xpass is a failure. (gh pr #567) | ||
| - Added a documentation generation stage to tox and CI. (gh pr #568) | ||
| - Added an explicit warning when running python setup.py explaining how to run | ||
| the test suites with pytest. Fixed by @lkollar. (gh issue #544, gh pr #548) | ||
| - Added requirements-dev.txt for test dependency management (gh pr #499, #516) | ||
| - Fixed code coverage metrics to account for Windows builds (gh pr #526) | ||
| - Fixed code coverage metrics to NOT count xfails. Fixed by @gaborbernat | ||
| (gh issue #519, gh pr #563) | ||
| - Style improvement to zoneinfo.tzfile that was confusing to static type | ||
| checkers. Reported and fixed by @quodlibetor (gh pr #485) | ||
| - Several unused imports were removed by @jdufresne. (gh pr #486) | ||
| - Switched isinstance(*, collections.Callable) to callable, which is available | ||
| on all supported Python versions. Implemented by @jdufresne (gh pr #612) | ||
| - Added CONTRIBUTING.md (gh pr #533) | ||
| - Added AUTHORS.md (gh pr #542) | ||
| - Corrected setup.py metadata to reflect author vs. maintainer, (gh issue #477, | ||
| gh pr #538) | ||
| - Corrected README to reflect that tests are now run in pytest. Reported and | ||
| fixed by @m-dz (gh issue #556, gh pr #557) | ||
| - Updated all references to RFC 2445 (iCalendar) to point to RFC 5545. Fixed | ||
| by @mariocj89 (gh issue #543, gh pr #555) | ||
| - Corrected parse documentation to reflect proper integer offset units, | ||
| reported and fixed by @abrugh (gh pr #458) | ||
| - Fixed dangling parenthesis in tzoffset documentation (gh pr #461) | ||
| - Started including the license file in wheels. Reported and fixed by | ||
| @jdufresne (gh pr #476) | ||
| - Indendation fixes to parser docstring by @jbrockmendel (gh pr #492) | ||
| - Moved many examples from the "examples" documentation into their appropriate | ||
| module documentation pages. Fixed by @Tomasz-Kluczkowski and @jakec-github | ||
| (gh pr #558, #561) | ||
| - Fixed documentation so that the parser.isoparse documentation displays. | ||
| Fixed by @alexchamberlain (gh issue #545, gh pr #560) | ||
| - Refactored build and release sections and added setup instructions to | ||
| CONTRIBUTING. Reported and fixed by @kynan (gh pr #562) | ||
| - Cleaned up various dead links in the documentation. (gh pr #602, #608, #618) | ||
| Version 2.6.1 | ||
@@ -238,3 +386,3 @@ ------------- | ||
| fixing an issue with wildcard imports of dateutil.zoneinfo. (gh pr #66). | ||
| - (PyPi only) Fix an issue with source distributions not including the test | ||
| - (PyPI only) Fix an issue with source distributions not including the test | ||
| suite. | ||
@@ -241,0 +389,0 @@ |
+6
-5
@@ -1,4 +0,4 @@ | ||
| Metadata-Version: 1.1 | ||
| Metadata-Version: 1.2 | ||
| Name: python-dateutil | ||
| Version: 2.6.1 | ||
| Version: 2.7.0 | ||
| Summary: Extensions to the standard Python datetime module | ||
@@ -8,3 +8,4 @@ Home-page: https://dateutil.readthedocs.io | ||
| Author-email: dateutil@python.org | ||
| License: Simplified BSD | ||
| License: Apache 2.0 | ||
| Description-Content-Type: UNKNOWN | ||
| Description: | ||
@@ -20,6 +21,4 @@ The dateutil module provides powerful extensions to the | ||
| Classifier: Programming Language :: Python :: 2 | ||
| Classifier: Programming Language :: Python :: 2.6 | ||
| Classifier: Programming Language :: Python :: 2.7 | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3.2 | ||
| Classifier: Programming Language :: Python :: 3.3 | ||
@@ -29,3 +28,5 @@ Classifier: Programming Language :: Python :: 3.4 | ||
| Classifier: Programming Language :: Python :: 3.6 | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Topic :: Software Development :: Libraries | ||
| Requires: six | ||
| Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.* |
@@ -1,4 +0,4 @@ | ||
| Metadata-Version: 1.1 | ||
| Metadata-Version: 1.2 | ||
| Name: python-dateutil | ||
| Version: 2.6.1 | ||
| Version: 2.7.0 | ||
| Summary: Extensions to the standard Python datetime module | ||
@@ -8,3 +8,4 @@ Home-page: https://dateutil.readthedocs.io | ||
| Author-email: dateutil@python.org | ||
| License: Simplified BSD | ||
| License: Apache 2.0 | ||
| Description-Content-Type: UNKNOWN | ||
| Description: | ||
@@ -20,6 +21,4 @@ The dateutil module provides powerful extensions to the | ||
| Classifier: Programming Language :: Python :: 2 | ||
| Classifier: Programming Language :: Python :: 2.6 | ||
| Classifier: Programming Language :: Python :: 2.7 | ||
| Classifier: Programming Language :: Python :: 3 | ||
| Classifier: Programming Language :: Python :: 3.2 | ||
| Classifier: Programming Language :: Python :: 3.3 | ||
@@ -29,3 +28,5 @@ Classifier: Programming Language :: Python :: 3.4 | ||
| Classifier: Programming Language :: Python :: 3.6 | ||
| Classifier: Programming Language :: Python :: 3.7 | ||
| Classifier: Topic :: Software Development :: Libraries | ||
| Requires: six | ||
| Requires-Python: >=2.7, !=3.0.*, !=3.1.*, !=3.2.* |
@@ -1,1 +0,1 @@ | ||
| six >=1.5 | ||
| six>=1.5 |
@@ -0,1 +1,6 @@ | ||
| .gitattributes | ||
| .gitignore | ||
| .travis.yml | ||
| AUTHORS.md | ||
| CONTRIBUTING.md | ||
| LICENSE | ||
@@ -5,6 +10,14 @@ MANIFEST.in | ||
| README.rst | ||
| RELEASING | ||
| appveyor.yml | ||
| codecov.yml | ||
| release.py | ||
| requirements-dev.txt | ||
| setup.cfg | ||
| setup.py | ||
| tox.ini | ||
| updatezinfo.py | ||
| zonefile_metadata.json | ||
| ci_tools/retry.bat | ||
| ci_tools/retry.sh | ||
| dateutil/__init__.py | ||
@@ -14,10 +27,16 @@ dateutil/_common.py | ||
| dateutil/easter.py | ||
| dateutil/parser.py | ||
| dateutil/relativedelta.py | ||
| dateutil/rrule.py | ||
| dateutil/tzwin.py | ||
| dateutil/utils.py | ||
| dateutil/parser/__init__.py | ||
| dateutil/parser/_parser.py | ||
| dateutil/parser/isoparser.py | ||
| dateutil/test/__init__.py | ||
| dateutil/test/_common.py | ||
| dateutil/test/test_easter.py | ||
| dateutil/test/test_import_star.py | ||
| dateutil/test/test_imports.py | ||
| dateutil/test/test_internals.py | ||
| dateutil/test/test_isoparser.py | ||
| dateutil/test/test_parser.py | ||
@@ -27,4 +46,6 @@ dateutil/test/test_relativedelta.py | ||
| dateutil/test/test_tz.py | ||
| dateutil/test/test_utils.py | ||
| dateutil/tz/__init__.py | ||
| dateutil/tz/_common.py | ||
| dateutil/tz/_factories.py | ||
| dateutil/tz/tz.py | ||
@@ -35,2 +56,15 @@ dateutil/tz/win.py | ||
| dateutil/zoneinfo/rebuild.py | ||
| docs/Makefile | ||
| docs/conf.py | ||
| docs/easter.rst | ||
| docs/examples.rst | ||
| docs/index.rst | ||
| docs/make.bat | ||
| docs/parser.rst | ||
| docs/relativedelta.rst | ||
| docs/rrule.rst | ||
| docs/tz.rst | ||
| docs/utils.rst | ||
| docs/zoneinfo.rst | ||
| docs/samples/EST5EDT.ics | ||
| python_dateutil.egg-info/PKG-INFO | ||
@@ -37,0 +71,0 @@ python_dateutil.egg-info/SOURCES.txt |
+24
-26
| dateutil - powerful extensions to datetime | ||
| ========================================== | ||
| .. image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square | ||
| :target: https://pypi.python.org/pypi/python-dateutil/ | ||
| :alt: pypi version | ||
| .. image:: https://img.shields.io/travis/dateutil/dateutil/master.svg?style=flat-square | ||
@@ -16,11 +20,7 @@ :target: https://travis-ci.org/dateutil/dateutil | ||
| .. image:: https://img.shields.io/pypi/dd/python-dateutil.svg?style=flat-square | ||
| :target: https://pypi.python.org/pypi/python-dateutil/ | ||
| :alt: pypi downloads per day | ||
| .. image:: https://badges.gitter.im/dateutil/dateutil.svg | ||
| :alt: Join the chat at https://gitter.im/dateutil/dateutil | ||
| :target: https://gitter.im/dateutil/dateutil | ||
| .. image:: https://img.shields.io/pypi/v/python-dateutil.svg?style=flat-square | ||
| :target: https://pypi.python.org/pypi/python-dateutil/ | ||
| :alt: pypi version | ||
| The `dateutil` module provides powerful extensions to | ||
@@ -36,3 +36,3 @@ the standard `datetime` module, available in Python. | ||
| The documentation is hosted at: | ||
| https://dateutil.readthedocs.io/ | ||
| https://dateutil.readthedocs.io/en/stable/ | ||
@@ -98,3 +98,8 @@ Code | ||
| Contributing | ||
| ============ | ||
| We welcome many types of contributions - bug reports, pull requests (code, infrastructure or documentation fixes). For more information about how to contribute to the project, see the ``CONTRIBUTING.md`` file in the repository. | ||
| Author | ||
@@ -112,13 +117,6 @@ ====== | ||
| Our mailing list is available at `dateutil@python.org <https://mail.python.org/mailman/listinfo/dateutil>`_. As it is hosted by the PSF, it is subject to the `PSF code of | ||
| conduct <https://www.python.org/psf/codeofconduct/>`_. | ||
| Starting with version 2.4.1, all source and binary distributions will be signed | ||
| by a PGP key that has, at the very least, been signed by the key which made the | ||
| previous release. A table of release signing keys can be found below: | ||
| Building and releasing | ||
| ====================== | ||
| When you get the source, it does not contain the internal zoneinfo | ||
| database. To get (and update) the database, run the updatezinfo.py script. Make sure | ||
| that the zic command is in your path, and that you have network connectivity | ||
| to get the latest timezone information from IANA, or from `our mirror of the | ||
| IANA database <https://dateutil.github.io/tzdata/>`_. | ||
| Starting with version 2.4.1, all source and binary distributions will be signed | ||
@@ -134,15 +132,15 @@ by a PGP key that has, at the very least, been signed by the key which made the | ||
| Testing | ||
| Contact | ||
| ======= | ||
| dateutil has a comprehensive test suite, which can be run simply by running | ||
| `python setup.py test [-q]` in the project root. Note that if you don't have the internal | ||
| zoneinfo database, some tests will fail. Apart from that, all tests should pass. | ||
| Our mailing list is available at `dateutil@python.org <https://mail.python.org/mailman/listinfo/dateutil>`_. As it is hosted by the PSF, it is subject to the `PSF code of | ||
| conduct <https://www.python.org/psf/codeofconduct/>`_. | ||
| To easily test dateutil against all supported Python versions, you can use | ||
| `tox <https://tox.readthedocs.io/en/latest/>`_. | ||
| License | ||
| ======= | ||
| All github pull requests are automatically tested using travis and appveyor. | ||
| All contributions after December 1, 2017 released under dual license - either `Apache 2.0 License <https://www.apache.org/licenses/LICENSE-2.0>`_ or the `BSD 3-Clause License <https://opensource.org/licenses/BSD-3-Clause>`_. Contributions before December 1, 2017 - except those those explicitly relicensed - are released only under the BSD 3-Clause License. | ||
| .. _6B49 ACBA DCF6 BD1C A206 67AB CD54 FCE3 D964 BEFB: | ||
| https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB | ||
| https://pgp.mit.edu/pks/lookup?op=vindex&search=0xCD54FCE3D964BEFB |
+6
-0
| [bdist_wheel] | ||
| universal = 1 | ||
| [metadata] | ||
| license_file = LICENSE | ||
| [tool:pytest] | ||
| xfail_strict = true | ||
| [egg_info] | ||
@@ -5,0 +11,0 @@ tag_build = |
+34
-12
| #!/usr/bin/python | ||
| from os.path import isfile | ||
| import codecs | ||
| import os | ||
| import re | ||
| from setuptools import setup | ||
| import setuptools | ||
| from setuptools import setup, find_packages | ||
| from setuptools.command.test import test as TestCommand | ||
| from dateutil._version import VERSION | ||
| from distutils.version import LooseVersion | ||
| import warnings | ||
@@ -14,9 +15,26 @@ if isfile("MANIFEST"): | ||
| PACKAGES = find_packages(where='.', exclude=['dateutil.test']) | ||
| if LooseVersion(setuptools.__version__) <= LooseVersion("24.3"): | ||
| warnings.warn("python_requires requires setuptools version > 24.3", | ||
| UserWarning) | ||
| class Unsupported(TestCommand): | ||
| def run(self): | ||
| print("Running 'test' with setup.py is not supported. " | ||
| "Use 'pytest' or 'tox' to run the tests.") | ||
| setup(name="python-dateutil", | ||
| version=VERSION, | ||
| use_scm_version={ | ||
| 'write_to': 'dateutil/_version.py', | ||
| }, | ||
| description="Extensions to the standard Python datetime module", | ||
| author="Paul Ganssle", | ||
| author_email="dateutil@python.org", | ||
| author="Gustavo Niemeyer", | ||
| author_email="gustavo@niemeyer.net", | ||
| maintainer="Paul Ganssle", | ||
| maintainer_email="dateutil@python.org", | ||
| url="https://dateutil.readthedocs.io", | ||
| license="Simplified BSD", | ||
| license="Apache 2.0", | ||
| long_description=""" | ||
@@ -26,6 +44,8 @@ The dateutil module provides powerful extensions to the | ||
| """, | ||
| packages=["dateutil", "dateutil.zoneinfo", "dateutil.tz"], | ||
| packages=PACKAGES, | ||
| python_requires=">=2.7, !=3.0.*, !=3.1.*, !=3.2.*", | ||
| package_data={"dateutil.zoneinfo": ["dateutil-zoneinfo.tar.gz"]}, | ||
| zip_safe=True, | ||
| requires=["six"], | ||
| setup_requires=['setuptools_scm'], | ||
| install_requires=["six >=1.5"], # XXX fix when packaging is sane again | ||
@@ -38,6 +58,4 @@ classifiers=[ | ||
| 'Programming Language :: Python :: 2', | ||
| 'Programming Language :: Python :: 2.6', | ||
| 'Programming Language :: Python :: 2.7', | ||
| 'Programming Language :: Python :: 3', | ||
| 'Programming Language :: Python :: 3.2', | ||
| 'Programming Language :: Python :: 3.3', | ||
@@ -47,5 +65,9 @@ 'Programming Language :: Python :: 3.4', | ||
| 'Programming Language :: Python :: 3.6', | ||
| 'Programming Language :: Python :: 3.7', | ||
| 'Topic :: Software Development :: Libraries', | ||
| ], | ||
| test_suite="dateutil.test" | ||
| test_suite="dateutil.test", | ||
| cmdclass={ | ||
| "test": Unsupported | ||
| } | ||
| ) |
@@ -7,5 +7,5 @@ { | ||
| ], | ||
| "tzdata_file": "tzdata2017b.tar.gz", | ||
| "tzdata_file_sha512": "3e090dba1f52e4c63b4930b28f4bf38b56aabd6728f23094cb5801d10f4e464f17231f17b75b8866714bf98199c166ea840de0787b75b2274aa419a4e14bbc4d", | ||
| "tzversion": "2017b", | ||
| "tzdata_file": "tzdata2018c.tar.gz", | ||
| "tzdata_file_sha512": "0575c87c9ffcde7b7f62b0df928d8a0e8d93f832b5ef7227b3d8431686f851f1fd5372c7e768f2979830352433b31df7f3979a00c28ea3973a3a0987ebbfb835", | ||
| "tzversion": "2018c", | ||
| "zonegroups": [ | ||
@@ -12,0 +12,0 @@ "africa", |
-1374
| # -*- coding: utf-8 -*- | ||
| """ | ||
| This module offers a generic date/time string parser which is able to parse | ||
| most known formats to represent a date and/or time. | ||
| This module attempts to be forgiving with regards to unlikely input formats, | ||
| returning a datetime object even for dates which are ambiguous. If an element | ||
| of a date/time stamp is omitted, the following rules are applied: | ||
| - If AM or PM is left unspecified, a 24-hour clock is assumed, however, an hour | ||
| on a 12-hour clock (``0 <= hour <= 12``) *must* be specified if AM or PM is | ||
| specified. | ||
| - If a time zone is omitted, a timezone-naive datetime is returned. | ||
| If any other elements are missing, they are taken from the | ||
| :class:`datetime.datetime` object passed to the parameter ``default``. If this | ||
| results in a day number exceeding the valid number of days per month, the | ||
| value falls back to the end of the month. | ||
| Additional resources about date/time string formats can be found below: | ||
| - `A summary of the international standard date and time notation | ||
| <http://www.cl.cam.ac.uk/~mgk25/iso-time.html>`_ | ||
| - `W3C Date and Time Formats <http://www.w3.org/TR/NOTE-datetime>`_ | ||
| - `Time Formats (Planetary Rings Node) <http://pds-rings.seti.org/tools/time_formats.html>`_ | ||
| - `CPAN ParseDate module | ||
| <http://search.cpan.org/~muir/Time-modules-2013.0912/lib/Time/ParseDate.pm>`_ | ||
| - `Java SimpleDateFormat Class | ||
| <https://docs.oracle.com/javase/6/docs/api/java/text/SimpleDateFormat.html>`_ | ||
| """ | ||
| from __future__ import unicode_literals | ||
| import datetime | ||
| import string | ||
| import time | ||
| import collections | ||
| import re | ||
| from io import StringIO | ||
| from calendar import monthrange | ||
| from six import text_type, binary_type, integer_types | ||
| from . import relativedelta | ||
| from . import tz | ||
| __all__ = ["parse", "parserinfo"] | ||
| class _timelex(object): | ||
| # Fractional seconds are sometimes split by a comma | ||
| _split_decimal = re.compile("([.,])") | ||
| def __init__(self, instream): | ||
| if isinstance(instream, binary_type): | ||
| instream = instream.decode() | ||
| if isinstance(instream, text_type): | ||
| instream = StringIO(instream) | ||
| if getattr(instream, 'read', None) is None: | ||
| raise TypeError('Parser must be a string or character stream, not ' | ||
| '{itype}'.format(itype=instream.__class__.__name__)) | ||
| self.instream = instream | ||
| self.charstack = [] | ||
| self.tokenstack = [] | ||
| self.eof = False | ||
| def get_token(self): | ||
| """ | ||
| This function breaks the time string into lexical units (tokens), which | ||
| can be parsed by the parser. Lexical units are demarcated by changes in | ||
| the character set, so any continuous string of letters is considered | ||
| one unit, any continuous string of numbers is considered one unit. | ||
| The main complication arises from the fact that dots ('.') can be used | ||
| both as separators (e.g. "Sep.20.2009") or decimal points (e.g. | ||
| "4:30:21.447"). As such, it is necessary to read the full context of | ||
| any dot-separated strings before breaking it into tokens; as such, this | ||
| function maintains a "token stack", for when the ambiguous context | ||
| demands that multiple tokens be parsed at once. | ||
| """ | ||
| if self.tokenstack: | ||
| return self.tokenstack.pop(0) | ||
| seenletters = False | ||
| token = None | ||
| state = None | ||
| while not self.eof: | ||
| # We only realize that we've reached the end of a token when we | ||
| # find a character that's not part of the current token - since | ||
| # that character may be part of the next token, it's stored in the | ||
| # charstack. | ||
| if self.charstack: | ||
| nextchar = self.charstack.pop(0) | ||
| else: | ||
| nextchar = self.instream.read(1) | ||
| while nextchar == '\x00': | ||
| nextchar = self.instream.read(1) | ||
| if not nextchar: | ||
| self.eof = True | ||
| break | ||
| elif not state: | ||
| # First character of the token - determines if we're starting | ||
| # to parse a word, a number or something else. | ||
| token = nextchar | ||
| if self.isword(nextchar): | ||
| state = 'a' | ||
| elif self.isnum(nextchar): | ||
| state = '0' | ||
| elif self.isspace(nextchar): | ||
| token = ' ' | ||
| break # emit token | ||
| else: | ||
| break # emit token | ||
| elif state == 'a': | ||
| # If we've already started reading a word, we keep reading | ||
| # letters until we find something that's not part of a word. | ||
| seenletters = True | ||
| if self.isword(nextchar): | ||
| token += nextchar | ||
| elif nextchar == '.': | ||
| token += nextchar | ||
| state = 'a.' | ||
| else: | ||
| self.charstack.append(nextchar) | ||
| break # emit token | ||
| elif state == '0': | ||
| # If we've already started reading a number, we keep reading | ||
| # numbers until we find something that doesn't fit. | ||
| if self.isnum(nextchar): | ||
| token += nextchar | ||
| elif nextchar == '.' or (nextchar == ',' and len(token) >= 2): | ||
| token += nextchar | ||
| state = '0.' | ||
| else: | ||
| self.charstack.append(nextchar) | ||
| break # emit token | ||
| elif state == 'a.': | ||
| # If we've seen some letters and a dot separator, continue | ||
| # parsing, and the tokens will be broken up later. | ||
| seenletters = True | ||
| if nextchar == '.' or self.isword(nextchar): | ||
| token += nextchar | ||
| elif self.isnum(nextchar) and token[-1] == '.': | ||
| token += nextchar | ||
| state = '0.' | ||
| else: | ||
| self.charstack.append(nextchar) | ||
| break # emit token | ||
| elif state == '0.': | ||
| # If we've seen at least one dot separator, keep going, we'll | ||
| # break up the tokens later. | ||
| if nextchar == '.' or self.isnum(nextchar): | ||
| token += nextchar | ||
| elif self.isword(nextchar) and token[-1] == '.': | ||
| token += nextchar | ||
| state = 'a.' | ||
| else: | ||
| self.charstack.append(nextchar) | ||
| break # emit token | ||
| if (state in ('a.', '0.') and (seenletters or token.count('.') > 1 or | ||
| token[-1] in '.,')): | ||
| l = self._split_decimal.split(token) | ||
| token = l[0] | ||
| for tok in l[1:]: | ||
| if tok: | ||
| self.tokenstack.append(tok) | ||
| if state == '0.' and token.count('.') == 0: | ||
| token = token.replace(',', '.') | ||
| return token | ||
| def __iter__(self): | ||
| return self | ||
| def __next__(self): | ||
| token = self.get_token() | ||
| if token is None: | ||
| raise StopIteration | ||
| return token | ||
| def next(self): | ||
| return self.__next__() # Python 2.x support | ||
| @classmethod | ||
| def split(cls, s): | ||
| return list(cls(s)) | ||
| @classmethod | ||
| def isword(cls, nextchar): | ||
| """ Whether or not the next character is part of a word """ | ||
| return nextchar.isalpha() | ||
| @classmethod | ||
| def isnum(cls, nextchar): | ||
| """ Whether the next character is part of a number """ | ||
| return nextchar.isdigit() | ||
| @classmethod | ||
| def isspace(cls, nextchar): | ||
| """ Whether the next character is whitespace """ | ||
| return nextchar.isspace() | ||
| class _resultbase(object): | ||
| def __init__(self): | ||
| for attr in self.__slots__: | ||
| setattr(self, attr, None) | ||
| def _repr(self, classname): | ||
| l = [] | ||
| for attr in self.__slots__: | ||
| value = getattr(self, attr) | ||
| if value is not None: | ||
| l.append("%s=%s" % (attr, repr(value))) | ||
| return "%s(%s)" % (classname, ", ".join(l)) | ||
| def __len__(self): | ||
| return (sum(getattr(self, attr) is not None | ||
| for attr in self.__slots__)) | ||
| def __repr__(self): | ||
| return self._repr(self.__class__.__name__) | ||
| class parserinfo(object): | ||
| """ | ||
| Class which handles what inputs are accepted. Subclass this to customize | ||
| the language and acceptable values for each parameter. | ||
| :param dayfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the day (``True``) or month (``False``). If | ||
| ``yearfirst`` is set to ``True``, this distinguishes between YDM | ||
| and YMD. Default is ``False``. | ||
| :param yearfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the year. If ``True``, the first number is taken | ||
| to be the year, otherwise the last number is taken to be the year. | ||
| Default is ``False``. | ||
| """ | ||
| # m from a.m/p.m, t from ISO T separator | ||
| JUMP = [" ", ".", ",", ";", "-", "/", "'", | ||
| "at", "on", "and", "ad", "m", "t", "of", | ||
| "st", "nd", "rd", "th"] | ||
| WEEKDAYS = [("Mon", "Monday"), | ||
| ("Tue", "Tuesday"), | ||
| ("Wed", "Wednesday"), | ||
| ("Thu", "Thursday"), | ||
| ("Fri", "Friday"), | ||
| ("Sat", "Saturday"), | ||
| ("Sun", "Sunday")] | ||
| MONTHS = [("Jan", "January"), | ||
| ("Feb", "February"), | ||
| ("Mar", "March"), | ||
| ("Apr", "April"), | ||
| ("May", "May"), | ||
| ("Jun", "June"), | ||
| ("Jul", "July"), | ||
| ("Aug", "August"), | ||
| ("Sep", "Sept", "September"), | ||
| ("Oct", "October"), | ||
| ("Nov", "November"), | ||
| ("Dec", "December")] | ||
| HMS = [("h", "hour", "hours"), | ||
| ("m", "minute", "minutes"), | ||
| ("s", "second", "seconds")] | ||
| AMPM = [("am", "a"), | ||
| ("pm", "p")] | ||
| UTCZONE = ["UTC", "GMT", "Z"] | ||
| PERTAIN = ["of"] | ||
| TZOFFSET = {} | ||
| def __init__(self, dayfirst=False, yearfirst=False): | ||
| self._jump = self._convert(self.JUMP) | ||
| self._weekdays = self._convert(self.WEEKDAYS) | ||
| self._months = self._convert(self.MONTHS) | ||
| self._hms = self._convert(self.HMS) | ||
| self._ampm = self._convert(self.AMPM) | ||
| self._utczone = self._convert(self.UTCZONE) | ||
| self._pertain = self._convert(self.PERTAIN) | ||
| self.dayfirst = dayfirst | ||
| self.yearfirst = yearfirst | ||
| self._year = time.localtime().tm_year | ||
| self._century = self._year // 100 * 100 | ||
| def _convert(self, lst): | ||
| dct = {} | ||
| for i, v in enumerate(lst): | ||
| if isinstance(v, tuple): | ||
| for v in v: | ||
| dct[v.lower()] = i | ||
| else: | ||
| dct[v.lower()] = i | ||
| return dct | ||
| def jump(self, name): | ||
| return name.lower() in self._jump | ||
| def weekday(self, name): | ||
| if len(name) >= min(len(n) for n in self._weekdays.keys()): | ||
| try: | ||
| return self._weekdays[name.lower()] | ||
| except KeyError: | ||
| pass | ||
| return None | ||
| def month(self, name): | ||
| if len(name) >= min(len(n) for n in self._months.keys()): | ||
| try: | ||
| return self._months[name.lower()] + 1 | ||
| except KeyError: | ||
| pass | ||
| return None | ||
| def hms(self, name): | ||
| try: | ||
| return self._hms[name.lower()] | ||
| except KeyError: | ||
| return None | ||
| def ampm(self, name): | ||
| try: | ||
| return self._ampm[name.lower()] | ||
| except KeyError: | ||
| return None | ||
| def pertain(self, name): | ||
| return name.lower() in self._pertain | ||
| def utczone(self, name): | ||
| return name.lower() in self._utczone | ||
| def tzoffset(self, name): | ||
| if name in self._utczone: | ||
| return 0 | ||
| return self.TZOFFSET.get(name) | ||
| def convertyear(self, year, century_specified=False): | ||
| if year < 100 and not century_specified: | ||
| year += self._century | ||
| if abs(year - self._year) >= 50: | ||
| if year < self._year: | ||
| year += 100 | ||
| else: | ||
| year -= 100 | ||
| return year | ||
| def validate(self, res): | ||
| # move to info | ||
| if res.year is not None: | ||
| res.year = self.convertyear(res.year, res.century_specified) | ||
| if res.tzoffset == 0 and not res.tzname or res.tzname == 'Z': | ||
| res.tzname = "UTC" | ||
| res.tzoffset = 0 | ||
| elif res.tzoffset != 0 and res.tzname and self.utczone(res.tzname): | ||
| res.tzoffset = 0 | ||
| return True | ||
| class _ymd(list): | ||
| def __init__(self, tzstr, *args, **kwargs): | ||
| super(self.__class__, self).__init__(*args, **kwargs) | ||
| self.century_specified = False | ||
| self.tzstr = tzstr | ||
| @staticmethod | ||
| def token_could_be_year(token, year): | ||
| try: | ||
| return int(token) == year | ||
| except ValueError: | ||
| return False | ||
| @staticmethod | ||
| def find_potential_year_tokens(year, tokens): | ||
| return [token for token in tokens if _ymd.token_could_be_year(token, year)] | ||
| def find_probable_year_index(self, tokens): | ||
| """ | ||
| attempt to deduce if a pre 100 year was lost | ||
| due to padded zeros being taken off | ||
| """ | ||
| for index, token in enumerate(self): | ||
| potential_year_tokens = _ymd.find_potential_year_tokens(token, tokens) | ||
| if len(potential_year_tokens) == 1 and len(potential_year_tokens[0]) > 2: | ||
| return index | ||
| def append(self, val): | ||
| if hasattr(val, '__len__'): | ||
| if val.isdigit() and len(val) > 2: | ||
| self.century_specified = True | ||
| elif val > 100: | ||
| self.century_specified = True | ||
| super(self.__class__, self).append(int(val)) | ||
| def resolve_ymd(self, mstridx, yearfirst, dayfirst): | ||
| len_ymd = len(self) | ||
| year, month, day = (None, None, None) | ||
| if len_ymd > 3: | ||
| raise ValueError("More than three YMD values") | ||
| elif len_ymd == 1 or (mstridx != -1 and len_ymd == 2): | ||
| # One member, or two members with a month string | ||
| if mstridx != -1: | ||
| month = self[mstridx] | ||
| del self[mstridx] | ||
| if len_ymd > 1 or mstridx == -1: | ||
| if self[0] > 31: | ||
| year = self[0] | ||
| else: | ||
| day = self[0] | ||
| elif len_ymd == 2: | ||
| # Two members with numbers | ||
| if self[0] > 31: | ||
| # 99-01 | ||
| year, month = self | ||
| elif self[1] > 31: | ||
| # 01-99 | ||
| month, year = self | ||
| elif dayfirst and self[1] <= 12: | ||
| # 13-01 | ||
| day, month = self | ||
| else: | ||
| # 01-13 | ||
| month, day = self | ||
| elif len_ymd == 3: | ||
| # Three members | ||
| if mstridx == 0: | ||
| month, day, year = self | ||
| elif mstridx == 1: | ||
| if self[0] > 31 or (yearfirst and self[2] <= 31): | ||
| # 99-Jan-01 | ||
| year, month, day = self | ||
| else: | ||
| # 01-Jan-01 | ||
| # Give precendence to day-first, since | ||
| # two-digit years is usually hand-written. | ||
| day, month, year = self | ||
| elif mstridx == 2: | ||
| # WTF!? | ||
| if self[1] > 31: | ||
| # 01-99-Jan | ||
| day, year, month = self | ||
| else: | ||
| # 99-01-Jan | ||
| year, day, month = self | ||
| else: | ||
| if self[0] > 31 or \ | ||
| self.find_probable_year_index(_timelex.split(self.tzstr)) == 0 or \ | ||
| (yearfirst and self[1] <= 12 and self[2] <= 31): | ||
| # 99-01-01 | ||
| if dayfirst and self[2] <= 12: | ||
| year, day, month = self | ||
| else: | ||
| year, month, day = self | ||
| elif self[0] > 12 or (dayfirst and self[1] <= 12): | ||
| # 13-01-01 | ||
| day, month, year = self | ||
| else: | ||
| # 01-13-01 | ||
| month, day, year = self | ||
| return year, month, day | ||
| class parser(object): | ||
| def __init__(self, info=None): | ||
| self.info = info or parserinfo() | ||
| def parse(self, timestr, default=None, ignoretz=False, tzinfos=None, **kwargs): | ||
| """ | ||
| Parse the date/time string into a :class:`datetime.datetime` object. | ||
| :param timestr: | ||
| Any date/time string using the supported formats. | ||
| :param default: | ||
| The default datetime object, if this is a datetime object and not | ||
| ``None``, elements specified in ``timestr`` replace elements in the | ||
| default object. | ||
| :param ignoretz: | ||
| If set ``True``, time zones in parsed strings are ignored and a | ||
| naive :class:`datetime.datetime` object is returned. | ||
| :param tzinfos: | ||
| Additional time zone names / aliases which may be present in the | ||
| string. This argument maps time zone names (and optionally offsets | ||
| from those time zones) to time zones. This parameter can be a | ||
| dictionary with timezone aliases mapping time zone names to time | ||
| zones or a function taking two parameters (``tzname`` and | ||
| ``tzoffset``) and returning a time zone. | ||
| The timezones to which the names are mapped can be an integer | ||
| offset from UTC in minutes or a :class:`tzinfo` object. | ||
| .. doctest:: | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> from dateutil.parser import parse | ||
| >>> from dateutil.tz import gettz | ||
| >>> tzinfos = {"BRST": -10800, "CST": gettz("America/Chicago")} | ||
| >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) | ||
| datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -10800)) | ||
| >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) | ||
| datetime.datetime(2012, 1, 19, 17, 21, | ||
| tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) | ||
| This parameter is ignored if ``ignoretz`` is set. | ||
| :param **kwargs: | ||
| Keyword arguments as passed to ``_parse()``. | ||
| :return: | ||
| Returns a :class:`datetime.datetime` object or, if the | ||
| ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the | ||
| first element being a :class:`datetime.datetime` object, the second | ||
| a tuple containing the fuzzy tokens. | ||
| :raises ValueError: | ||
| Raised for invalid or unknown string format, if the provided | ||
| :class:`tzinfo` is not in a valid format, or if an invalid date | ||
| would be created. | ||
| :raises TypeError: | ||
| Raised for non-string or character stream input. | ||
| :raises OverflowError: | ||
| Raised if the parsed date exceeds the largest valid C integer on | ||
| your system. | ||
| """ | ||
| if default is None: | ||
| default = datetime.datetime.now().replace(hour=0, minute=0, | ||
| second=0, microsecond=0) | ||
| res, skipped_tokens = self._parse(timestr, **kwargs) | ||
| if res is None: | ||
| raise ValueError("Unknown string format") | ||
| if len(res) == 0: | ||
| raise ValueError("String does not contain a date.") | ||
| repl = {} | ||
| for attr in ("year", "month", "day", "hour", | ||
| "minute", "second", "microsecond"): | ||
| value = getattr(res, attr) | ||
| if value is not None: | ||
| repl[attr] = value | ||
| if 'day' not in repl: | ||
| # If the default day exceeds the last day of the month, fall back to | ||
| # the end of the month. | ||
| cyear = default.year if res.year is None else res.year | ||
| cmonth = default.month if res.month is None else res.month | ||
| cday = default.day if res.day is None else res.day | ||
| if cday > monthrange(cyear, cmonth)[1]: | ||
| repl['day'] = monthrange(cyear, cmonth)[1] | ||
| ret = default.replace(**repl) | ||
| if res.weekday is not None and not res.day: | ||
| ret = ret+relativedelta.relativedelta(weekday=res.weekday) | ||
| if not ignoretz: | ||
| if (isinstance(tzinfos, collections.Callable) or | ||
| tzinfos and res.tzname in tzinfos): | ||
| if isinstance(tzinfos, collections.Callable): | ||
| tzdata = tzinfos(res.tzname, res.tzoffset) | ||
| else: | ||
| tzdata = tzinfos.get(res.tzname) | ||
| if isinstance(tzdata, datetime.tzinfo): | ||
| tzinfo = tzdata | ||
| elif isinstance(tzdata, text_type): | ||
| tzinfo = tz.tzstr(tzdata) | ||
| elif isinstance(tzdata, integer_types): | ||
| tzinfo = tz.tzoffset(res.tzname, tzdata) | ||
| else: | ||
| raise ValueError("Offset must be tzinfo subclass, " | ||
| "tz string, or int offset.") | ||
| ret = ret.replace(tzinfo=tzinfo) | ||
| elif res.tzname and res.tzname in time.tzname: | ||
| ret = ret.replace(tzinfo=tz.tzlocal()) | ||
| elif res.tzoffset == 0: | ||
| ret = ret.replace(tzinfo=tz.tzutc()) | ||
| elif res.tzoffset: | ||
| ret = ret.replace(tzinfo=tz.tzoffset(res.tzname, res.tzoffset)) | ||
| if kwargs.get('fuzzy_with_tokens', False): | ||
| return ret, skipped_tokens | ||
| else: | ||
| return ret | ||
| class _result(_resultbase): | ||
| __slots__ = ["year", "month", "day", "weekday", | ||
| "hour", "minute", "second", "microsecond", | ||
| "tzname", "tzoffset", "ampm"] | ||
| def _parse(self, timestr, dayfirst=None, yearfirst=None, fuzzy=False, | ||
| fuzzy_with_tokens=False): | ||
| """ | ||
| Private method which performs the heavy lifting of parsing, called from | ||
| ``parse()``, which passes on its ``kwargs`` to this function. | ||
| :param timestr: | ||
| The string to parse. | ||
| :param dayfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the day (``True``) or month (``False``). If | ||
| ``yearfirst`` is set to ``True``, this distinguishes between YDM | ||
| and YMD. If set to ``None``, this value is retrieved from the | ||
| current :class:`parserinfo` object (which itself defaults to | ||
| ``False``). | ||
| :param yearfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the year. If ``True``, the first number is taken | ||
| to be the year, otherwise the last number is taken to be the year. | ||
| If this is set to ``None``, the value is retrieved from the current | ||
| :class:`parserinfo` object (which itself defaults to ``False``). | ||
| :param fuzzy: | ||
| Whether to allow fuzzy parsing, allowing for string like "Today is | ||
| January 1, 2047 at 8:21:00AM". | ||
| :param fuzzy_with_tokens: | ||
| If ``True``, ``fuzzy`` is automatically set to True, and the parser | ||
| will return a tuple where the first element is the parsed | ||
| :class:`datetime.datetime` datetimestamp and the second element is | ||
| a tuple containing the portions of the string which were ignored: | ||
| .. doctest:: | ||
| >>> from dateutil.parser import parse | ||
| >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) | ||
| (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) | ||
| """ | ||
| if fuzzy_with_tokens: | ||
| fuzzy = True | ||
| info = self.info | ||
| if dayfirst is None: | ||
| dayfirst = info.dayfirst | ||
| if yearfirst is None: | ||
| yearfirst = info.yearfirst | ||
| res = self._result() | ||
| l = _timelex.split(timestr) # Splits the timestr into tokens | ||
| # keep up with the last token skipped so we can recombine | ||
| # consecutively skipped tokens (-2 for when i begins at 0). | ||
| last_skipped_token_i = -2 | ||
| skipped_tokens = list() | ||
| try: | ||
| # year/month/day list | ||
| ymd = _ymd(timestr) | ||
| # Index of the month string in ymd | ||
| mstridx = -1 | ||
| len_l = len(l) | ||
| i = 0 | ||
| while i < len_l: | ||
| # Check if it's a number | ||
| try: | ||
| value_repr = l[i] | ||
| value = float(value_repr) | ||
| except ValueError: | ||
| value = None | ||
| if value is not None: | ||
| # Token is a number | ||
| len_li = len(l[i]) | ||
| i += 1 | ||
| if (len(ymd) == 3 and len_li in (2, 4) | ||
| and res.hour is None and (i >= len_l or (l[i] != ':' and | ||
| info.hms(l[i]) is None))): | ||
| # 19990101T23[59] | ||
| s = l[i-1] | ||
| res.hour = int(s[:2]) | ||
| if len_li == 4: | ||
| res.minute = int(s[2:]) | ||
| elif len_li == 6 or (len_li > 6 and l[i-1].find('.') == 6): | ||
| # YYMMDD or HHMMSS[.ss] | ||
| s = l[i-1] | ||
| if not ymd and l[i-1].find('.') == -1: | ||
| #ymd.append(info.convertyear(int(s[:2]))) | ||
| ymd.append(s[:2]) | ||
| ymd.append(s[2:4]) | ||
| ymd.append(s[4:]) | ||
| else: | ||
| # 19990101T235959[.59] | ||
| res.hour = int(s[:2]) | ||
| res.minute = int(s[2:4]) | ||
| res.second, res.microsecond = _parsems(s[4:]) | ||
| elif len_li in (8, 12, 14): | ||
| # YYYYMMDD | ||
| s = l[i-1] | ||
| ymd.append(s[:4]) | ||
| ymd.append(s[4:6]) | ||
| ymd.append(s[6:8]) | ||
| if len_li > 8: | ||
| res.hour = int(s[8:10]) | ||
| res.minute = int(s[10:12]) | ||
| if len_li > 12: | ||
| res.second = int(s[12:]) | ||
| elif ((i < len_l and info.hms(l[i]) is not None) or | ||
| (i+1 < len_l and l[i] == ' ' and | ||
| info.hms(l[i+1]) is not None)): | ||
| # HH[ ]h or MM[ ]m or SS[.ss][ ]s | ||
| if l[i] == ' ': | ||
| i += 1 | ||
| idx = info.hms(l[i]) | ||
| while True: | ||
| if idx == 0: | ||
| res.hour = int(value) | ||
| if value % 1: | ||
| res.minute = int(60*(value % 1)) | ||
| elif idx == 1: | ||
| res.minute = int(value) | ||
| if value % 1: | ||
| res.second = int(60*(value % 1)) | ||
| elif idx == 2: | ||
| res.second, res.microsecond = \ | ||
| _parsems(value_repr) | ||
| i += 1 | ||
| if i >= len_l or idx == 2: | ||
| break | ||
| # 12h00 | ||
| try: | ||
| value_repr = l[i] | ||
| value = float(value_repr) | ||
| except ValueError: | ||
| break | ||
| else: | ||
| i += 1 | ||
| idx += 1 | ||
| if i < len_l: | ||
| newidx = info.hms(l[i]) | ||
| if newidx is not None: | ||
| idx = newidx | ||
| elif (i == len_l and l[i-2] == ' ' and | ||
| info.hms(l[i-3]) is not None): | ||
| # X h MM or X m SS | ||
| idx = info.hms(l[i-3]) | ||
| if idx == 0: # h | ||
| res.minute = int(value) | ||
| sec_remainder = value % 1 | ||
| if sec_remainder: | ||
| res.second = int(60 * sec_remainder) | ||
| elif idx == 1: # m | ||
| res.second, res.microsecond = \ | ||
| _parsems(value_repr) | ||
| # We don't need to advance the tokens here because the | ||
| # i == len_l call indicates that we're looking at all | ||
| # the tokens already. | ||
| elif i+1 < len_l and l[i] == ':': | ||
| # HH:MM[:SS[.ss]] | ||
| res.hour = int(value) | ||
| i += 1 | ||
| value = float(l[i]) | ||
| res.minute = int(value) | ||
| if value % 1: | ||
| res.second = int(60*(value % 1)) | ||
| i += 1 | ||
| if i < len_l and l[i] == ':': | ||
| res.second, res.microsecond = _parsems(l[i+1]) | ||
| i += 2 | ||
| elif i < len_l and l[i] in ('-', '/', '.'): | ||
| sep = l[i] | ||
| ymd.append(value_repr) | ||
| i += 1 | ||
| if i < len_l and not info.jump(l[i]): | ||
| try: | ||
| # 01-01[-01] | ||
| ymd.append(l[i]) | ||
| except ValueError: | ||
| # 01-Jan[-01] | ||
| value = info.month(l[i]) | ||
| if value is not None: | ||
| ymd.append(value) | ||
| assert mstridx == -1 | ||
| mstridx = len(ymd)-1 | ||
| else: | ||
| return None, None | ||
| i += 1 | ||
| if i < len_l and l[i] == sep: | ||
| # We have three members | ||
| i += 1 | ||
| value = info.month(l[i]) | ||
| if value is not None: | ||
| ymd.append(value) | ||
| mstridx = len(ymd)-1 | ||
| assert mstridx == -1 | ||
| else: | ||
| ymd.append(l[i]) | ||
| i += 1 | ||
| elif i >= len_l or info.jump(l[i]): | ||
| if i+1 < len_l and info.ampm(l[i+1]) is not None: | ||
| # 12 am | ||
| res.hour = int(value) | ||
| if res.hour < 12 and info.ampm(l[i+1]) == 1: | ||
| res.hour += 12 | ||
| elif res.hour == 12 and info.ampm(l[i+1]) == 0: | ||
| res.hour = 0 | ||
| i += 1 | ||
| else: | ||
| # Year, month or day | ||
| ymd.append(value) | ||
| i += 1 | ||
| elif info.ampm(l[i]) is not None: | ||
| # 12am | ||
| res.hour = int(value) | ||
| if res.hour < 12 and info.ampm(l[i]) == 1: | ||
| res.hour += 12 | ||
| elif res.hour == 12 and info.ampm(l[i]) == 0: | ||
| res.hour = 0 | ||
| i += 1 | ||
| elif not fuzzy: | ||
| return None, None | ||
| else: | ||
| i += 1 | ||
| continue | ||
| # Check weekday | ||
| value = info.weekday(l[i]) | ||
| if value is not None: | ||
| res.weekday = value | ||
| i += 1 | ||
| continue | ||
| # Check month name | ||
| value = info.month(l[i]) | ||
| if value is not None: | ||
| ymd.append(value) | ||
| assert mstridx == -1 | ||
| mstridx = len(ymd)-1 | ||
| i += 1 | ||
| if i < len_l: | ||
| if l[i] in ('-', '/'): | ||
| # Jan-01[-99] | ||
| sep = l[i] | ||
| i += 1 | ||
| ymd.append(l[i]) | ||
| i += 1 | ||
| if i < len_l and l[i] == sep: | ||
| # Jan-01-99 | ||
| i += 1 | ||
| ymd.append(l[i]) | ||
| i += 1 | ||
| elif (i+3 < len_l and l[i] == l[i+2] == ' ' | ||
| and info.pertain(l[i+1])): | ||
| # Jan of 01 | ||
| # In this case, 01 is clearly year | ||
| try: | ||
| value = int(l[i+3]) | ||
| except ValueError: | ||
| # Wrong guess | ||
| pass | ||
| else: | ||
| # Convert it here to become unambiguous | ||
| ymd.append(str(info.convertyear(value))) | ||
| i += 4 | ||
| continue | ||
| # Check am/pm | ||
| value = info.ampm(l[i]) | ||
| if value is not None: | ||
| # For fuzzy parsing, 'a' or 'am' (both valid English words) | ||
| # may erroneously trigger the AM/PM flag. Deal with that | ||
| # here. | ||
| val_is_ampm = True | ||
| # If there's already an AM/PM flag, this one isn't one. | ||
| if fuzzy and res.ampm is not None: | ||
| val_is_ampm = False | ||
| # If AM/PM is found and hour is not, raise a ValueError | ||
| if res.hour is None: | ||
| if fuzzy: | ||
| val_is_ampm = False | ||
| else: | ||
| raise ValueError('No hour specified with ' + | ||
| 'AM or PM flag.') | ||
| elif not 0 <= res.hour <= 12: | ||
| # If AM/PM is found, it's a 12 hour clock, so raise | ||
| # an error for invalid range | ||
| if fuzzy: | ||
| val_is_ampm = False | ||
| else: | ||
| raise ValueError('Invalid hour specified for ' + | ||
| '12-hour clock.') | ||
| if val_is_ampm: | ||
| if value == 1 and res.hour < 12: | ||
| res.hour += 12 | ||
| elif value == 0 and res.hour == 12: | ||
| res.hour = 0 | ||
| res.ampm = value | ||
| elif fuzzy: | ||
| last_skipped_token_i = self._skip_token(skipped_tokens, | ||
| last_skipped_token_i, i, l) | ||
| i += 1 | ||
| continue | ||
| # Check for a timezone name | ||
| if (res.hour is not None and len(l[i]) <= 5 and | ||
| res.tzname is None and res.tzoffset is None and | ||
| not [x for x in l[i] if x not in | ||
| string.ascii_uppercase]): | ||
| res.tzname = l[i] | ||
| res.tzoffset = info.tzoffset(res.tzname) | ||
| i += 1 | ||
| # Check for something like GMT+3, or BRST+3. Notice | ||
| # that it doesn't mean "I am 3 hours after GMT", but | ||
| # "my time +3 is GMT". If found, we reverse the | ||
| # logic so that timezone parsing code will get it | ||
| # right. | ||
| if i < len_l and l[i] in ('+', '-'): | ||
| l[i] = ('+', '-')[l[i] == '+'] | ||
| res.tzoffset = None | ||
| if info.utczone(res.tzname): | ||
| # With something like GMT+3, the timezone | ||
| # is *not* GMT. | ||
| res.tzname = None | ||
| continue | ||
| # Check for a numbered timezone | ||
| if res.hour is not None and l[i] in ('+', '-'): | ||
| signal = (-1, 1)[l[i] == '+'] | ||
| i += 1 | ||
| len_li = len(l[i]) | ||
| if len_li == 4: | ||
| # -0300 | ||
| res.tzoffset = int(l[i][:2])*3600+int(l[i][2:])*60 | ||
| elif i+1 < len_l and l[i+1] == ':': | ||
| # -03:00 | ||
| res.tzoffset = int(l[i])*3600+int(l[i+2])*60 | ||
| i += 2 | ||
| elif len_li <= 2: | ||
| # -[0]3 | ||
| res.tzoffset = int(l[i][:2])*3600 | ||
| else: | ||
| return None, None | ||
| i += 1 | ||
| res.tzoffset *= signal | ||
| # Look for a timezone name between parenthesis | ||
| if (i+3 < len_l and | ||
| info.jump(l[i]) and l[i+1] == '(' and l[i+3] == ')' and | ||
| 3 <= len(l[i+2]) <= 5 and | ||
| not [x for x in l[i+2] | ||
| if x not in string.ascii_uppercase]): | ||
| # -0300 (BRST) | ||
| res.tzname = l[i+2] | ||
| i += 4 | ||
| continue | ||
| # Check jumps | ||
| if not (info.jump(l[i]) or fuzzy): | ||
| return None, None | ||
| last_skipped_token_i = self._skip_token(skipped_tokens, | ||
| last_skipped_token_i, i, l) | ||
| i += 1 | ||
| # Process year/month/day | ||
| year, month, day = ymd.resolve_ymd(mstridx, yearfirst, dayfirst) | ||
| if year is not None: | ||
| res.year = year | ||
| res.century_specified = ymd.century_specified | ||
| if month is not None: | ||
| res.month = month | ||
| if day is not None: | ||
| res.day = day | ||
| except (IndexError, ValueError, AssertionError): | ||
| return None, None | ||
| if not info.validate(res): | ||
| return None, None | ||
| if fuzzy_with_tokens: | ||
| return res, tuple(skipped_tokens) | ||
| else: | ||
| return res, None | ||
| @staticmethod | ||
| def _skip_token(skipped_tokens, last_skipped_token_i, i, l): | ||
| if last_skipped_token_i == i - 1: | ||
| # recombine the tokens | ||
| skipped_tokens[-1] += l[i] | ||
| else: | ||
| # just append | ||
| skipped_tokens.append(l[i]) | ||
| last_skipped_token_i = i | ||
| return last_skipped_token_i | ||
| DEFAULTPARSER = parser() | ||
| def parse(timestr, parserinfo=None, **kwargs): | ||
| """ | ||
| Parse a string in one of the supported formats, using the | ||
| ``parserinfo`` parameters. | ||
| :param timestr: | ||
| A string containing a date/time stamp. | ||
| :param parserinfo: | ||
| A :class:`parserinfo` object containing parameters for the parser. | ||
| If ``None``, the default arguments to the :class:`parserinfo` | ||
| constructor are used. | ||
| The ``**kwargs`` parameter takes the following keyword arguments: | ||
| :param default: | ||
| The default datetime object, if this is a datetime object and not | ||
| ``None``, elements specified in ``timestr`` replace elements in the | ||
| default object. | ||
| :param ignoretz: | ||
| If set ``True``, time zones in parsed strings are ignored and a naive | ||
| :class:`datetime` object is returned. | ||
| :param tzinfos: | ||
| Additional time zone names / aliases which may be present in the | ||
| string. This argument maps time zone names (and optionally offsets | ||
| from those time zones) to time zones. This parameter can be a | ||
| dictionary with timezone aliases mapping time zone names to time | ||
| zones or a function taking two parameters (``tzname`` and | ||
| ``tzoffset``) and returning a time zone. | ||
| The timezones to which the names are mapped can be an integer | ||
| offset from UTC in minutes or a :class:`tzinfo` object. | ||
| .. doctest:: | ||
| :options: +NORMALIZE_WHITESPACE | ||
| >>> from dateutil.parser import parse | ||
| >>> from dateutil.tz import gettz | ||
| >>> tzinfos = {"BRST": -10800, "CST": gettz("America/Chicago")} | ||
| >>> parse("2012-01-19 17:21:00 BRST", tzinfos=tzinfos) | ||
| datetime.datetime(2012, 1, 19, 17, 21, tzinfo=tzoffset(u'BRST', -10800)) | ||
| >>> parse("2012-01-19 17:21:00 CST", tzinfos=tzinfos) | ||
| datetime.datetime(2012, 1, 19, 17, 21, | ||
| tzinfo=tzfile('/usr/share/zoneinfo/America/Chicago')) | ||
| This parameter is ignored if ``ignoretz`` is set. | ||
| :param dayfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the day (``True``) or month (``False``). If | ||
| ``yearfirst`` is set to ``True``, this distinguishes between YDM and | ||
| YMD. If set to ``None``, this value is retrieved from the current | ||
| :class:`parserinfo` object (which itself defaults to ``False``). | ||
| :param yearfirst: | ||
| Whether to interpret the first value in an ambiguous 3-integer date | ||
| (e.g. 01/05/09) as the year. If ``True``, the first number is taken to | ||
| be the year, otherwise the last number is taken to be the year. If | ||
| this is set to ``None``, the value is retrieved from the current | ||
| :class:`parserinfo` object (which itself defaults to ``False``). | ||
| :param fuzzy: | ||
| Whether to allow fuzzy parsing, allowing for string like "Today is | ||
| January 1, 2047 at 8:21:00AM". | ||
| :param fuzzy_with_tokens: | ||
| If ``True``, ``fuzzy`` is automatically set to True, and the parser | ||
| will return a tuple where the first element is the parsed | ||
| :class:`datetime.datetime` datetimestamp and the second element is | ||
| a tuple containing the portions of the string which were ignored: | ||
| .. doctest:: | ||
| >>> from dateutil.parser import parse | ||
| >>> parse("Today is January 1, 2047 at 8:21:00AM", fuzzy_with_tokens=True) | ||
| (datetime.datetime(2047, 1, 1, 8, 21), (u'Today is ', u' ', u'at ')) | ||
| :return: | ||
| Returns a :class:`datetime.datetime` object or, if the | ||
| ``fuzzy_with_tokens`` option is ``True``, returns a tuple, the | ||
| first element being a :class:`datetime.datetime` object, the second | ||
| a tuple containing the fuzzy tokens. | ||
| :raises ValueError: | ||
| Raised for invalid or unknown string format, if the provided | ||
| :class:`tzinfo` is not in a valid format, or if an invalid date | ||
| would be created. | ||
| :raises OverflowError: | ||
| Raised if the parsed date exceeds the largest valid C integer on | ||
| your system. | ||
| """ | ||
| if parserinfo: | ||
| return parser(parserinfo).parse(timestr, **kwargs) | ||
| else: | ||
| return DEFAULTPARSER.parse(timestr, **kwargs) | ||
| class _tzparser(object): | ||
| class _result(_resultbase): | ||
| __slots__ = ["stdabbr", "stdoffset", "dstabbr", "dstoffset", | ||
| "start", "end"] | ||
| class _attr(_resultbase): | ||
| __slots__ = ["month", "week", "weekday", | ||
| "yday", "jyday", "day", "time"] | ||
| def __repr__(self): | ||
| return self._repr("") | ||
| def __init__(self): | ||
| _resultbase.__init__(self) | ||
| self.start = self._attr() | ||
| self.end = self._attr() | ||
| def parse(self, tzstr): | ||
| res = self._result() | ||
| l = _timelex.split(tzstr) | ||
| try: | ||
| len_l = len(l) | ||
| i = 0 | ||
| while i < len_l: | ||
| # BRST+3[BRDT[+2]] | ||
| j = i | ||
| while j < len_l and not [x for x in l[j] | ||
| if x in "0123456789:,-+"]: | ||
| j += 1 | ||
| if j != i: | ||
| if not res.stdabbr: | ||
| offattr = "stdoffset" | ||
| res.stdabbr = "".join(l[i:j]) | ||
| else: | ||
| offattr = "dstoffset" | ||
| res.dstabbr = "".join(l[i:j]) | ||
| i = j | ||
| if (i < len_l and (l[i] in ('+', '-') or l[i][0] in | ||
| "0123456789")): | ||
| if l[i] in ('+', '-'): | ||
| # Yes, that's right. See the TZ variable | ||
| # documentation. | ||
| signal = (1, -1)[l[i] == '+'] | ||
| i += 1 | ||
| else: | ||
| signal = -1 | ||
| len_li = len(l[i]) | ||
| if len_li == 4: | ||
| # -0300 | ||
| setattr(res, offattr, (int(l[i][:2])*3600 + | ||
| int(l[i][2:])*60)*signal) | ||
| elif i+1 < len_l and l[i+1] == ':': | ||
| # -03:00 | ||
| setattr(res, offattr, | ||
| (int(l[i])*3600+int(l[i+2])*60)*signal) | ||
| i += 2 | ||
| elif len_li <= 2: | ||
| # -[0]3 | ||
| setattr(res, offattr, | ||
| int(l[i][:2])*3600*signal) | ||
| else: | ||
| return None | ||
| i += 1 | ||
| if res.dstabbr: | ||
| break | ||
| else: | ||
| break | ||
| if i < len_l: | ||
| for j in range(i, len_l): | ||
| if l[j] == ';': | ||
| l[j] = ',' | ||
| assert l[i] == ',' | ||
| i += 1 | ||
| if i >= len_l: | ||
| pass | ||
| elif (8 <= l.count(',') <= 9 and | ||
| not [y for x in l[i:] if x != ',' | ||
| for y in x if y not in "0123456789"]): | ||
| # GMT0BST,3,0,30,3600,10,0,26,7200[,3600] | ||
| for x in (res.start, res.end): | ||
| x.month = int(l[i]) | ||
| i += 2 | ||
| if l[i] == '-': | ||
| value = int(l[i+1])*-1 | ||
| i += 1 | ||
| else: | ||
| value = int(l[i]) | ||
| i += 2 | ||
| if value: | ||
| x.week = value | ||
| x.weekday = (int(l[i])-1) % 7 | ||
| else: | ||
| x.day = int(l[i]) | ||
| i += 2 | ||
| x.time = int(l[i]) | ||
| i += 2 | ||
| if i < len_l: | ||
| if l[i] in ('-', '+'): | ||
| signal = (-1, 1)[l[i] == "+"] | ||
| i += 1 | ||
| else: | ||
| signal = 1 | ||
| res.dstoffset = (res.stdoffset+int(l[i]))*signal | ||
| elif (l.count(',') == 2 and l[i:].count('/') <= 2 and | ||
| not [y for x in l[i:] if x not in (',', '/', 'J', 'M', | ||
| '.', '-', ':') | ||
| for y in x if y not in "0123456789"]): | ||
| for x in (res.start, res.end): | ||
| if l[i] == 'J': | ||
| # non-leap year day (1 based) | ||
| i += 1 | ||
| x.jyday = int(l[i]) | ||
| elif l[i] == 'M': | ||
| # month[-.]week[-.]weekday | ||
| i += 1 | ||
| x.month = int(l[i]) | ||
| i += 1 | ||
| assert l[i] in ('-', '.') | ||
| i += 1 | ||
| x.week = int(l[i]) | ||
| if x.week == 5: | ||
| x.week = -1 | ||
| i += 1 | ||
| assert l[i] in ('-', '.') | ||
| i += 1 | ||
| x.weekday = (int(l[i])-1) % 7 | ||
| else: | ||
| # year day (zero based) | ||
| x.yday = int(l[i])+1 | ||
| i += 1 | ||
| if i < len_l and l[i] == '/': | ||
| i += 1 | ||
| # start time | ||
| len_li = len(l[i]) | ||
| if len_li == 4: | ||
| # -0300 | ||
| x.time = (int(l[i][:2])*3600+int(l[i][2:])*60) | ||
| elif i+1 < len_l and l[i+1] == ':': | ||
| # -03:00 | ||
| x.time = int(l[i])*3600+int(l[i+2])*60 | ||
| i += 2 | ||
| if i+1 < len_l and l[i+1] == ':': | ||
| i += 2 | ||
| x.time += int(l[i]) | ||
| elif len_li <= 2: | ||
| # -[0]3 | ||
| x.time = (int(l[i][:2])*3600) | ||
| else: | ||
| return None | ||
| i += 1 | ||
| assert i == len_l or l[i] == ',' | ||
| i += 1 | ||
| assert i >= len_l | ||
| except (IndexError, ValueError, AssertionError): | ||
| return None | ||
| return res | ||
| DEFAULTTZPARSER = _tzparser() | ||
| def _parsetz(tzstr): | ||
| return DEFAULTTZPARSER.parse(tzstr) | ||
| def _parsems(value): | ||
| """Parse a I[.F] seconds value into (seconds, microseconds).""" | ||
| if "." not in value: | ||
| return int(value), 0 | ||
| else: | ||
| i, f = value.split(".") | ||
| return int(i), int(f.ljust(6, "0")[:6]) | ||
| # vim:ts=4:sw=4:et |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Alert delta unavailable
Currently unable to show alert delta for PyPI packages.
962754
26.81%72
89.47%14706
17.65%