diff --git a/.travis.yml b/.travis.yml index 12947e8..cc1355c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,4 @@ language: python -sudo: required -dist: xenial group: travis_latest git: @@ -8,22 +6,25 @@ git: quiet: true python: +- 3.5 - 3.6 -- 3.7 +- pypy3 os: - linux -install: pip install -e .[tests] +install: +- pip install -e .[tests] +- if [[ $TRAVIS_PYTHON_VERSION == 3.6* && $TRAVIS_OS_NAME == linux ]]; then pip install -e .[cov]; fi script: - pytest -rsv -- flake8 -- mypy . --ignore-missing-imports +- if [[ $TRAVIS_PYTHON_VERSION == 3.6* && $TRAVIS_OS_NAME == linux ]]; then flake8; fi +- if [[ $TRAVIS_PYTHON_VERSION == 3.6* && $TRAVIS_OS_NAME == linux ]]; then mypy . --ignore-missing-imports; fi after_success: -- if [[ $TRAVIS_PYTHON_VERSION == 3.6* ]]; then - pytest --cov; +- if [[ $TRAVIS_PYTHON_VERSION == 3.6* && $TRAVIS_OS_NAME == linux ]]; then + pytest --cov --cov-config=setup.cfg; coveralls; fi diff --git a/MANIFEST.in b/MANIFEST.in index 1aba38f..065cc87 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1 +1,2 @@ include LICENSE +recursive-include tests *.py diff --git a/README.md b/README.md index a697169..4022df1 100644 --- a/README.md +++ b/README.md @@ -10,8 +10,7 @@ # Science Dates & Times Date & time conversions used in the sciences. -The assumption is that datetimes are timezone-naive, as this will be required soon in Numpy *et al* for -`numpy.datetime64`. +The assumption is that datetimes are timezone-naive, as this is required in Numpy *et al* for `numpy.datetime64`. ## Install diff --git a/date2doy.jl b/date2doy.jl index 1c7a6d1..52fb17c 100755 --- a/date2doy.jl +++ b/date2doy.jl @@ -2,7 +2,7 @@ #= command-line utility to convert date to day of year =# -using Base.Dates +using Dates using ArgParse diff --git a/randomdate.jl b/randomdate.jl index 12e6cee..c4ef162 100755 --- a/randomdate.jl +++ b/randomdate.jl @@ -4,21 +4,21 @@ generate a random date (month and day) in a year. Michael Hirsch, Ph.D. =# -using Base.Dates +using Dates using ArgParse function randomdate(year) - t = DateTime(year) - t += Day(rand(0:daysinyear(t)-1)) + t = DateTime(year) + t += Day(rand(0:daysinyear(t)-1)) end s = ArgParseSettings() @add_arg_table s begin - "year" - help = "year in which to generate a random date" - arg_type = Int - required = true + "year" + help = "year in which to generate a random date" + arg_type = Int + required = true end s = parse_args(s) diff --git a/sciencedates/__init__.py b/sciencedates/__init__.py index 29f5b1e..c2aa0d5 100644 --- a/sciencedates/__init__.py +++ b/sciencedates/__init__.py @@ -1,11 +1,15 @@ -from __future__ import division import datetime -from pytz import UTC import numpy as np from dateutil.parser import parse import calendar import random -from typing import Union, Tuple, Any, List +from typing import Union, Tuple, List + +from .findnearest import find_nearest # noqa: F401 +try: + from .tz import forceutc # noqa: F401 +except ImportError: + pass def datetime2yeardoy(time: Union[str, datetime.datetime]) -> Tuple[int, float]: @@ -17,7 +21,7 @@ def datetime2yeardoy(time: Union[str, datetime.datetime]) -> Tuple[int, float]: yd: yyyyddd four digit year, 3 digit day of year (INTEGER) utsec: seconds from midnight utc """ - T: np.ndarray = np.atleast_1d(time) + T = np.atleast_1d(time) utsec = np.empty_like(T, float) yd = np.empty_like(T, int) @@ -116,7 +120,7 @@ def datetime2gtd(time: Union[str, datetime.datetime, np.datetime64], elif isinstance(t, (datetime.datetime, datetime.date)): pass else: - raise TypeError(f'unknown time datatype {type(t)}') + raise TypeError('unknown time datatype {}'.format(type(t))) # %% Day of year doy[i] = int(t.strftime('%j')) # %% seconds since utc midnight @@ -145,36 +149,6 @@ def datetime2utsec(t: Union[str, datetime.date, datetime.datetime, np.datetime64 datetime.datetime.min.time())) -def forceutc(t: Union[str, datetime.datetime, datetime.date, np.datetime64]) -> Union[datetime.datetime, datetime.date]: - """ - Add UTC to datetime-naive and convert to UTC for datetime aware - - input: python datetime (naive, utc, non-utc) or Numpy datetime64 #FIXME add Pandas and AstroPy time classes - output: utc datetime - """ - # need to passthrough None for simpler external logic. -# %% polymorph to datetime - if isinstance(t, str): - t = parse(t) - elif isinstance(t, np.datetime64): - t = t.astype(datetime.datetime) - elif isinstance(t, datetime.datetime): - pass - elif isinstance(t, datetime.date): - return t - elif isinstance(t, (np.ndarray, list, tuple)): - return np.asarray([forceutc(T) for T in t]) - else: - raise TypeError('datetime only input') -# %% enforce UTC on datetime - if t.tzinfo is None: # datetime-naive - t = t.replace(tzinfo=UTC) - else: # datetime-aware - t = t.astimezone(UTC) # changes timezone, preserving absolute time. E.g. noon EST = 5PM UTC - - return t - - def yeardec2datetime(atime: float) -> datetime.datetime: """ Convert atime (a float) to DT.datetime @@ -223,7 +197,7 @@ def datetime2yeardec(time: Union[str, datetime.datetime, datetime.date]) -> floa elif isinstance(time, (tuple, list, np.ndarray)): return np.asarray([datetime2yeardec(t) for t in time]) else: - raise TypeError(f'unknown input type {type(time)}') + raise TypeError('unknown input type {}'.format(type(time))) year = t.year @@ -233,46 +207,6 @@ def datetime2yeardec(time: Union[str, datetime.datetime, datetime.date]) -> floa return year + ((t - boy).total_seconds() / ((eoy - boy).total_seconds())) -# %% -def find_nearest(x, x0) -> Tuple[int, Any]: - """ - This find_nearest function does NOT assume sorted input - - inputs: - x: array (float, int, datetime, h5py.Dataset) within which to search for x0 - x0: singleton or array of values to search for in x - - outputs: - idx: index of flattened x nearest to x0 (i.e. works with higher than 1-D arrays also) - xidx: x[idx] - - Observe how bisect.bisect() gives the incorrect result! - - idea based on: - http://stackoverflow.com/questions/2566412/find-nearest-value-in-numpy-array - - """ - x = np.asanyarray(x) # for indexing upon return - x0 = np.atleast_1d(x0) -# %% - if x.size == 0 or x0.size == 0: - raise ValueError('empty input(s)') - - if x0.ndim not in (0, 1): - raise ValueError('2-D x0 not handled yet') -# %% - ind = np.empty_like(x0, dtype=int) - - # NOTE: not trapping IndexError (all-nan) becaues returning None can surprise with slice indexing - for i, xi in enumerate(x0): - if xi is not None and (isinstance(xi, (datetime.datetime, datetime.date, np.datetime64)) or np.isfinite(xi)): - ind[i] = np.nanargmin(abs(x-xi)) - else: - raise ValueError('x0 must NOT be None or NaN to avoid surprising None return value') - - return ind.squeeze()[()], x[ind].squeeze()[()] # [()] to pop scalar from 0d array while being OK with ndim>0 - - def randomdate(year: int) -> datetime.date: """ gives random date in year""" if calendar.isleap(year): diff --git a/sciencedates/findnearest.py b/sciencedates/findnearest.py new file mode 100644 index 0000000..64cfa75 --- /dev/null +++ b/sciencedates/findnearest.py @@ -0,0 +1,42 @@ +from typing import Tuple, Any +import numpy as np +import datetime + + +def find_nearest(x, x0) -> Tuple[int, Any]: + """ + This find_nearest function does NOT assume sorted input + + inputs: + x: array (float, int, datetime, h5py.Dataset) within which to search for x0 + x0: singleton or array of values to search for in x + + outputs: + idx: index of flattened x nearest to x0 (i.e. works with higher than 1-D arrays also) + xidx: x[idx] + + Observe how bisect.bisect() gives the incorrect result! + + idea based on: + http://stackoverflow.com/questions/2566412/find-nearest-value-in-numpy-array + + """ + x = np.asanyarray(x) # for indexing upon return + x0 = np.atleast_1d(x0) +# %% + if x.size == 0 or x0.size == 0: + raise ValueError('empty input(s)') + + if x0.ndim not in (0, 1): + raise ValueError('2-D x0 not handled yet') +# %% + ind = np.empty_like(x0, dtype=int) + + # NOTE: not trapping IndexError (all-nan) becaues returning None can surprise with slice indexing + for i, xi in enumerate(x0): + if xi is not None and (isinstance(xi, (datetime.datetime, datetime.date, np.datetime64)) or np.isfinite(xi)): + ind[i] = np.nanargmin(abs(x-xi)) + else: + raise ValueError('x0 must NOT be None or NaN to avoid surprising None return value') + + return ind.squeeze()[()], x[ind].squeeze()[()] # [()] to pop scalar from 0d array while being OK with ndim>0 diff --git a/sciencedates/tz.py b/sciencedates/tz.py new file mode 100644 index 0000000..d45fb50 --- /dev/null +++ b/sciencedates/tz.py @@ -0,0 +1,35 @@ +from typing import Union +import numpy as np +import datetime +from pytz import UTC +from dateutil.parser import parse + + +def forceutc(t: Union[str, datetime.datetime, datetime.date, np.datetime64]) -> Union[datetime.datetime, datetime.date]: + """ + Add UTC to datetime-naive and convert to UTC for datetime aware + + input: python datetime (naive, utc, non-utc) or Numpy datetime64 #FIXME add Pandas and AstroPy time classes + output: utc datetime + """ + # need to passthrough None for simpler external logic. +# %% polymorph to datetime + if isinstance(t, str): + t = parse(t) + elif isinstance(t, np.datetime64): + t = t.astype(datetime.datetime) + elif isinstance(t, datetime.datetime): + pass + elif isinstance(t, datetime.date): + return t + elif isinstance(t, (np.ndarray, list, tuple)): + return np.asarray([forceutc(T) for T in t]) + else: + raise TypeError('datetime only input') +# %% enforce UTC on datetime + if t.tzinfo is None: # datetime-naive + t = t.replace(tzinfo=UTC) + else: # datetime-aware + t = t.astimezone(UTC) # changes timezone, preserving absolute time. E.g. noon EST = 5PM UTC + + return t diff --git a/setup.cfg b/setup.cfg index 774f0ff..a754917 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,7 +1,8 @@ [metadata] name = sciencedates -version = 1.4.3 +version = 1.4.4 author = Michael Hirsch, Ph.D. +author_email = scivision@users.noreply.github.com url = https://github.com/scivision/sciencedates description = Date conversions used in the sciences. keywords = @@ -11,17 +12,19 @@ classifiers = Development Status :: 5 - Production/Stable Environment :: Console Intended Audience :: Science/Research - License :: OSI Approved :: MIT License Operating System :: OS Independent + Programming Language :: Python :: 3.5 Programming Language :: Python :: 3.6 Programming Language :: Python :: 3.7 + Programming Language :: Python :: Implementation :: CPython + Programming Language :: Python :: Implementation :: PyPy Topic :: Utilities license_file = LICENSE long_description = file: README.md long_description_content_type = text/markdown [options] -python_requires = >= 3.6 +python_requires = >= 3.5 setup_requires = setuptools >= 38.6 pip >= 10 @@ -30,19 +33,21 @@ include_package_data = True packages = find: install_requires = numpy - pytz python-dateutil [options.extras_require] tests = pytest +cov = pytest-cov coveralls flake8 mypy -plot = - xarray +plot = + xarray matplotlib +timezone = + pytz [options.entry_points] console_scripts = @@ -59,7 +64,6 @@ omit = /home/travis/virtualenv/* */site-packages/* */bin/* - */build/* [coverage:report] exclude_lines = diff --git a/tests/test_conv.py b/tests/test_conv.py index 18254ba..9df7469 100755 --- a/tests/test_conv.py +++ b/tests/test_conv.py @@ -2,10 +2,10 @@ import datetime from dateutil.parser import parse import numpy as np -from pytz import timezone from numpy.testing import assert_allclose, assert_equal import pytest import sciencedates as sd +import sys # T = [datetime.datetime(2013, 7, 2, 12, 0, 0)] T.append(T[0].date()) @@ -13,6 +13,7 @@ T.append(str(T[0])) Tdt = (T[0],)*3 +OLDPY = sys.version_info < (3, 6) def test_yearint(): @@ -62,8 +63,11 @@ def test_yeardec(): assert_equal(sd.yeardec2datetime(sd.datetime2yeardec(Tdt)), T[0]) +@pytest.mark.xfail(OLDPY, reason='py36+') def test_utc(): - estdt = T[0].astimezone(timezone('EST')) + pytz = pytest.importorskip('pytz') + + estdt = T[0].astimezone(pytz.timezone('EST')) utcdt = sd.forceutc(estdt) assert utcdt == estdt @@ -73,5 +77,8 @@ def test_utc(): assert sd.forceutc(d) == d +if __name__ == '__main__': + pytest.main([__file__]) + if __name__ == '__main__': pytest.main() diff --git a/tests/test_msis.py b/tests/test_msis.py index 6bcdf90..ec62e4f 100644 --- a/tests/test_msis.py +++ b/tests/test_msis.py @@ -82,4 +82,4 @@ def test_glon(): if __name__ == '__main__': - pytest.main() + pytest.main([__file__]) diff --git a/tests/test_time.py b/tests/test_time.py index 22b8042..072fd34 100644 --- a/tests/test_time.py +++ b/tests/test_time.py @@ -23,4 +23,4 @@ def test_randomdate(): if __name__ == '__main__': - pytest.main() + pytest.main([__file__])