pax_global_header00006660000000000000000000000064141232527420014514gustar00rootroot0000000000000052 comment=435c9d881e8230f3f7c9938f301782d63d3fed78 spoqa-sqlalchemy-utc-435c9d8/000077500000000000000000000000001412325274200161355ustar00rootroot00000000000000spoqa-sqlalchemy-utc-435c9d8/.coveragerc000066400000000000000000000002301412325274200202510ustar00rootroot00000000000000[run] source = sqlalchemy_utc/ omit = tests/* setup.py [report] exclude_lines = def __repr__ def __str__ if __name__ = .__main__.: spoqa-sqlalchemy-utc-435c9d8/.gitignore000066400000000000000000000061341412325274200201310ustar00rootroot00000000000000 # Created by https://www.gitignore.io/api/osx,pydev,python,pycharm,visualstudiocode ### OSX ### *.DS_Store .AppleDouble .LSOverride # Icon must end with two \r Icon # Thumbnails ._* # Files that might appear in the root of a volume .DocumentRevisions-V100 .fseventsd .Spotlight-V100 .TemporaryItems .Trashes .VolumeIcon.icns .com.apple.timemachine.donotpresent # Directories potentially created on remote AFP share .AppleDB .AppleDesktop Network Trash Folder Temporary Items .apdisk ### PyCharm ### # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 # User-specific stuff: .idea/**/workspace.xml .idea/**/tasks.xml .idea/dictionaries # Sensitive or high-churn files: .idea/**/dataSources/ .idea/**/dataSources.ids .idea/**/dataSources.xml .idea/**/dataSources.local.xml .idea/**/sqlDataSources.xml .idea/**/dynamic.xml .idea/**/uiDesigner.xml # Gradle: .idea/**/gradle.xml .idea/**/libraries # CMake cmake-build-debug/ # Mongo Explorer plugin: .idea/**/mongoSettings.xml ## File-based project format: *.iws ## Plugin-specific files: # IntelliJ /out/ # mpeltonen/sbt-idea plugin .idea_modules/ # JIRA plugin atlassian-ide-plugin.xml # Cursive Clojure plugin .idea/replstate.xml # Ruby plugin and RubyMine /.rakeTasks # Crashlytics plugin (for Android Studio and IntelliJ) com_crashlytics_export_strings.xml crashlytics.properties crashlytics-build.properties fabric.properties ### PyCharm Patch ### # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 # *.iml # modules.xml # .idea/misc.xml # *.ipr # Sonarlint plugin .idea/sonarlint ### pydev ### .pydevproject ### Python ### # Byte-compiled / optimized / DLL files __pycache__/ *.py[cod] *$py.class # C extensions *.so # Distribution / packaging .Python build/ develop-eggs/ dist/ downloads/ eggs/ .eggs/ lib/ lib64/ parts/ sdist/ var/ wheels/ *.egg-info/ .installed.cfg *.egg # PyInstaller # Usually these files are written by a python script from a template # before PyInstaller builds the exe, so as to inject date/other infos into it. *.manifest *.spec # Installer logs pip-log.txt pip-delete-this-directory.txt # Unit test / coverage reports htmlcov/ .tox/ .coverage .coverage.* .cache nosetests.xml coverage.xml *.cover .hypothesis/ # Translations *.mo *.pot # Django stuff: *.log local_settings.py # Flask stuff: instance/ .webassets-cache # Scrapy stuff: .scrapy # Sphinx documentation docs/_build/ # PyBuilder target/ # Jupyter Notebook .ipynb_checkpoints # pyenv .python-version # celery beat schedule file celerybeat-schedule.* # SageMath parsed files *.sage.py # Environments .env .venv env/ venv/ ENV/ env.bak/ venv.bak/ # Spyder project settings .spyderproject .spyproject # Rope project settings .ropeproject # mkdocs documentation /site # mypy .mypy_cache/ ### VisualStudioCode ### .vscode/* !.vscode/settings.json !.vscode/tasks.json !.vscode/launch.json !.vscode/extensions.json .history # End of https://www.gitignore.io/api/osx,pydev,python,pycharm,visualstudiocode spoqa-sqlalchemy-utc-435c9d8/.travis.yml000066400000000000000000000050701412325274200202500ustar00rootroot00000000000000language: python python: # Supported versions - pypy3 - 3.6 - 3.7 - 3.8 - 3.9 - pypy # Unsupported from Python team - 2.7 # Completely unsupported - 3.4 - 3.5 # See: # https://www.python.org/downloads/ # https://www.pypy.org/download_advanced.html env: # Supported versions - SQLALCHEMY_VER=">=1.3.0,<1.4.0" PIP_OPTS="" - SQLALCHEMY_VER=">=1.4.0,<1.5.0" PIP_OPTS="" # Unsupported versions - SQLALCHEMY_VER=">=1.0.0,<1.1.0" PIP_OPTS="" - SQLALCHEMY_VER=">=1.1.0,<1.2.0" PIP_OPTS="" - SQLALCHEMY_VER=">=1.2.0,<1.3.0" PIP_OPTS="" # See: https://www.sqlalchemy.org/download.html services: - postgresql - mysql jobs: exclude: # No package of SQLAlchemy v1.4 for these Python versions - python: 3.4 env: SQLALCHEMY_VER=">=1.4.0,<1.5.0" PIP_OPTS="" - python: 3.5 env: SQLALCHEMY_VER=">=1.4.0,<1.5.0" PIP_OPTS="" before_install: - export PY=`python -c 'import sys; print("pypy" if hasattr(sys,"pypy_version_info") else "%d.%d" % sys.version_info[:2])'` - export PY_VER=`python -c 'import sys; print("%d.%d" % sys.version_info[:2])'` - echo "PY='$PY'" - echo "PY_VER='$PY_VER'" - if [[ "$PY" = "pypy" ]]; then export PG_URL="postgresql+psycopg2cffi:///utc_test"; export MYSQL_URL="mysql+pymysql://root@localhost/utc_test"; else export PG_URL="postgresql+psycopg2:///utc_test"; export MYSQL_URL="mysql+mysqlconnector://root@localhost/utc_test"; fi - export TEST_DATABASE_URLS="$PG_URL $MYSQL_URL" install: - if [[ "$PIP_OPTS" != "" ]]; then pip install $PIP_OPTS "SQLAlchemy $SQLALCHEMY_VER"; else pip install "SQLAlchemy $SQLALCHEMY_VER"; fi - pip install -e . - pip install pytest codecov flake8 flake8-import-order # pytest-cov 2.10.0 requires a pytest >= 4.6 which is not available in the Python 2.7 environment. # Use an older version for it. - if [[ "$PY_VER" = "2.7" && "$PY" != "pypy" ]]; then pip install 'pytest-cov < 2.10.0'; else pip install pytest-cov; fi # PyMySQL dropped support for Python 2.7 in version 1.0.0: # https://github.com/PyMySQL/PyMySQL/blob/master/CHANGELOG.md#v100 # mysql-coonector-python did the same since version 8.0.24: # https://dev.mysql.com/doc/connector-python/en/connector-python-versions.html - if [[ "$PY" = "pypy" ]]; then pip install psycopg2cffi 'pymysql < 1.0.0'; elif [[ "$PY_VER" = "2.7" ]]; then pip install psycopg2 'mysql-connector-python < 8.0.24'; else pip install psycopg2 mysql-connector-python; fi before_script: - createdb -E utf8 -T postgres utc_test - mysql -e 'CREATE DATABASE utc_test;' script: - pytest --cov=. --durations=10 tests - flake8 . after_success: - codecov spoqa-sqlalchemy-utc-435c9d8/CHANGES.rst000066400000000000000000000025701412325274200177430ustar00rootroot00000000000000Changelog ========= 0.14.0 ------ Released on September 24, 2021. - Add cache_ok flag on ``UtcDateTime`` to supress Pandas warnings. [`#14`_ by derekderie] .. _#14: https://github.com/spoqa/sqlalchemy-utc/pull/14 0.13.0 ------ Released on September 24, 2021. - Add milliseconds to SQLite datetimes. [`#12`_ by Giovanni Santini] - Add support for newer python versions. (3.7, 3.8, 3.9) [`#12`_ by Giovanni Santini] .. _#12: https://github.com/spoqa/sqlalchemy-utc/pull/12 0.12.0 ------ Released on May 7, 2021. - Add `py.typed` file to the package to be compatible with PEP-561. [`#10`_ by Dima Boger] .. _#10: https://github.com/spoqa/sqlalchemy-utc/pull/10 0.11.0 ------ Released on November 13, 2020. - Ensured always returning the datetime with UTC timezone. [`#8`_ by Eduard Christian Dumitrescu] .. _#8: https://github.com/spoqa/sqlalchemy-utc/pull/8 0.10.0 ------ Released on January 25, 2018. - Dropped support of older Python versions: 2.6, 3.2, and 3.3. [`#2`_ by George Leslie-Waksman] - Added ``sqlalchemy_utc.utcnow()`` function as an alternative to ``sqlalchemy.sql.functions.now()`` for generating ``UtcDateTime`` values on the database server. [`#4`_ by George Leslie-Waksman] .. _#2: https://github.com/spoqa/sqlalchemy-utc/pull/2 .. _#4: https://github.com/spoqa/sqlalchemy-utc/pull/4 0.9.0 ----- First version. Released on June 22, 2016. spoqa-sqlalchemy-utc-435c9d8/LICENSE000066400000000000000000000021001412325274200171330ustar00rootroot00000000000000Copyright (C) 2015-2016 by Hong Minhee Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. spoqa-sqlalchemy-utc-435c9d8/README.rst000066400000000000000000000034531412325274200176310ustar00rootroot00000000000000SQLAlchemy-Utc ============== .. image:: https://badge.fury.io/py/SQLAlchemy-Utc.svg? :target: https://pypi.python.org/pypi/SQLAlchemy-Utc .. image:: https://travis-ci.com/spoqa/sqlalchemy-utc.svg?branch=master :target: https://travis-ci.com/spoqa/sqlalchemy-utc .. image:: https://codecov.io/github/spoqa/sqlalchemy-utc/coverage.svg?branch=master :target: https://codecov.io/github/spoqa/sqlalchemy-utc?branch=master This package provides a drop-in replacement of SQLAlchemy's built-in `DateTime`_ type with ``timezone=True`` option enabled. Although SQLAlchemy's built-in ``DateTime`` type provides ``timezone=True`` option, since some vendors like SQLite and MySQL don't provide ``timestamptz`` data type, the option doesn't make any effect on these vendors. ``UtcDateTime`` type is equivalent to the built-in ``DateTime`` with ``timezone=True`` option enabled on vendors that support ``timestamptz`` e.g. PostgreSQL, but on SQLite or MySQL, it shifts all ``datetime.datetime`` values to UTC offset before store them, and returns always aware ``datetime.datetime`` values through result sets. Long story short, ``UtcDateTime`` does: - take only aware ``datetime.datetime``, - return only aware ``datetime.datetime``, - never take or return naive ``datetime.datetime``, - ensure timestamps in database always to be encoded in UTC, and - work as you'd expect. A SQLAlchemy helper function, ``utcnow()``, is provided as an alternative to ``func.now()`` for generating ``UtcDateTime`` values on the server. For example: ``Column('time', UtcDateTime(), default=utcnow())``. Written by `Hong Minhee`_ at Spoqa_, and distributed under MIT license. .. _DateTime: http://docs.sqlalchemy.org/en/latest/core/type_basics.html#sqlalchemy.types.DateTime .. _Hong Minhee: https://hongminhee.org/ .. _Spoqa: http://www.spoqa.com/ spoqa-sqlalchemy-utc-435c9d8/setup.cfg000066400000000000000000000000341412325274200177530ustar00rootroot00000000000000[bdist_wheel] universal = 1 spoqa-sqlalchemy-utc-435c9d8/setup.py000066400000000000000000000044141412325274200176520ustar00rootroot00000000000000import ast import os.path from setuptools import find_packages, setup def readme(): try: with open('README.rst') as f: readme = f.read() except IOError: pass try: with open('CHANGES.rst') as f: readme += '\n\n' + f.read() except IOError: pass return readme def get_version(): module_path = os.path.join( os.path.dirname(__file__), 'sqlalchemy_utc', 'version.py') module_file = open(module_path) try: module_code = module_file.read() finally: module_file.close() tree = ast.parse(module_code, module_path) for node in ast.iter_child_nodes(tree): if not isinstance(node, ast.Assign) or len(node.targets) != 1: continue target, = node.targets if isinstance(target, ast.Name) and target.id == '__version__': return node.value.s install_requires = ['setuptools', 'SQLAlchemy >= 0.9.0'] setup( name='SQLAlchemy-Utc', description='SQLAlchemy type to store aware datetime values', long_description=readme(), version=get_version(), url='https://github.com/spoqa/sqlalchemy-utc', packages=find_packages(exclude=('tests*',)), package_data={'sqlalchemy_utc': ['py.typed']}, author='Hong Minhee', author_email='hongminhee' '@' 'member.fsf.org', license='MIT License', install_requires=install_requires, classifiers=[ 'Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: OS Independent', 'Programming Language :: Python :: 2.7', 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', 'Programming Language :: Python :: 3.8', 'Programming Language :: Python :: 3.9', 'Programming Language :: Python :: Implementation :: CPython', 'Programming Language :: Python :: Implementation :: PyPy', 'Programming Language :: Python :: Implementation :: Stackless', 'Programming Language :: SQL', 'Topic :: Database :: Front-Ends', 'Topic :: Software Development', ] ) spoqa-sqlalchemy-utc-435c9d8/sqlalchemy_utc/000077500000000000000000000000001412325274200211525ustar00rootroot00000000000000spoqa-sqlalchemy-utc-435c9d8/sqlalchemy_utc/__init__.py000066400000000000000000000003031412325274200232570ustar00rootroot00000000000000from .now import utcnow from .sqltypes import UtcDateTime from .timezone import utc from .version import __version__ __all__ = [ '__version__', 'utc', 'UtcDateTime', 'utcnow', ] spoqa-sqlalchemy-utc-435c9d8/sqlalchemy_utc/now.py000066400000000000000000000025621412325274200223340ustar00rootroot00000000000000 from sqlalchemy.ext.compiler import compiles from sqlalchemy.sql.functions import FunctionElement from .sqltypes import UtcDateTime class utcnow(FunctionElement): """UTCNOW() expression for multiple dialects.""" type = UtcDateTime() @compiles(utcnow) def default_sql_utcnow(element, compiler, **kw): """Assume, by default, time zones work correctly. Note: This is a valid assumption for PostgreSQL and Oracle. """ return 'CURRENT_TIMESTAMP' @compiles(utcnow, 'mysql') def mysql_sql_utcnow(element, compiler, **kw): """MySQL returns now as localtime, so we convert to UTC. Warning: MySQL does not support the use of functions for sqlalchemy `server_default=` values. The utcnow function must be used as `default=` when interacting with a MySQL server. """ return "CONVERT_TZ(CURRENT_TIMESTAMP, @@session.time_zone, '+00:00')" @compiles(utcnow, 'sqlite') def sqlite_sql_utcnow(element, compiler, **kw): """SQLite DATETIME('NOW') returns a correct `datetime.datetime` but does not add milliseconds to it. Directly call STRFTIME with the final %f modifier in order to get those. """ return r"(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))" @compiles(utcnow, 'mssql') def mssql_sql_utcnow(element, compiler, **kw): """MS SQL provides a function for the UTC datetime.""" return 'GETUTCDATE()' spoqa-sqlalchemy-utc-435c9d8/sqlalchemy_utc/py.typed000066400000000000000000000000001412325274200226370ustar00rootroot00000000000000spoqa-sqlalchemy-utc-435c9d8/sqlalchemy_utc/sqltypes.py000066400000000000000000000026111412325274200234100ustar00rootroot00000000000000import datetime from sqlalchemy.types import DateTime, TypeDecorator from .timezone import utc class UtcDateTime(TypeDecorator): """Almost equivalent to :class:`~sqlalchemy.types.DateTime` with ``timezone=True`` option, but it differs from that by: - Never silently take naive :class:`~datetime.datetime`, instead it always raise :exc:`ValueError` unless time zone aware value. - :class:`~datetime.datetime` value's :attr:`~datetime.datetime.tzinfo` is always converted to UTC. - Unlike SQLAlchemy's built-in :class:`~sqlalchemy.types.DateTime`, it never return naive :class:`~datetime.datetime`, but time zone aware value, even with SQLite or MySQL. """ impl = DateTime(timezone=True) cache_ok = True def process_bind_param(self, value, dialect): if value is not None: if not isinstance(value, datetime.datetime): raise TypeError('expected datetime.datetime, not ' + repr(value)) elif value.tzinfo is None: raise ValueError('naive datetime is disallowed') return value.astimezone(utc) def process_result_value(self, value, dialect): if value is not None: if value.tzinfo is None: value = value.replace(tzinfo=utc) else: value = value.astimezone(utc) return value spoqa-sqlalchemy-utc-435c9d8/sqlalchemy_utc/timezone.py000066400000000000000000000005071412325274200233600ustar00rootroot00000000000000import datetime class Utc(datetime.tzinfo): __slots__ = () zero = datetime.timedelta(0) def utcoffset(self, _): return self.zero def dst(self, _): return self.zero def tzname(self, _): return 'UTC' try: utc = datetime.timezone.utc except AttributeError: utc = Utc() spoqa-sqlalchemy-utc-435c9d8/sqlalchemy_utc/version.py000066400000000000000000000000271412325274200232100ustar00rootroot00000000000000__version__ = '0.14.0' spoqa-sqlalchemy-utc-435c9d8/tests/000077500000000000000000000000001412325274200172775ustar00rootroot00000000000000spoqa-sqlalchemy-utc-435c9d8/tests/__init__.py000066400000000000000000000000001412325274200213760ustar00rootroot00000000000000spoqa-sqlalchemy-utc-435c9d8/tests/conftest.py000066400000000000000000000007061412325274200215010ustar00rootroot00000000000000import os from pytest import fixture from sqlalchemy.engine import create_engine from sqlalchemy.pool import NullPool try: database_urls = os.environ['TEST_DATABASE_URLS'].split() except KeyError: database_urls = [] @fixture(scope='function', params=['sqlite://'] + database_urls) def fx_engine(request): url = request.param engine = create_engine(url, poolclass=NullPool) request.addfinalizer(engine.dispose) return engine spoqa-sqlalchemy-utc-435c9d8/tests/test_now.py000066400000000000000000000016041412325274200215140ustar00rootroot00000000000000import datetime from pytest import yield_fixture from sqlalchemy import Column, MetaData, Table, select from sqlalchemy_utc import UtcDateTime, utc, utcnow TABLE = Table( 'test_table', MetaData(), Column('time', UtcDateTime, default=utcnow())) @yield_fixture def fx_connection(fx_engine): connection = fx_engine.connect() try: transaction = connection.begin() try: TABLE.create(connection) yield connection finally: transaction.rollback() finally: connection.close() def test_utcnow_timezone(fx_connection): fx_connection.execute(TABLE.insert(), [{}]) rows = fx_connection.execute(select([TABLE])).fetchall() server_now = rows[0].time local_now = datetime.datetime.now(utc) assert server_now.tzinfo is not None assert abs(server_now - local_now) < datetime.timedelta(seconds=30) spoqa-sqlalchemy-utc-435c9d8/tests/test_sqltypes.py000066400000000000000000000045621412325274200226030ustar00rootroot00000000000000import datetime try: from psycopg2ct.compat import register except ImportError: pass else: register() from pytest import mark, raises, yield_fixture from sqlalchemy.exc import StatementError from sqlalchemy.ext.declarative import declarative_base from sqlalchemy.orm import sessionmaker from sqlalchemy.schema import Column from sqlalchemy_utc import UtcDateTime, utc Base = declarative_base() Session = sessionmaker() @yield_fixture def fx_connection(fx_engine): connection = fx_engine.connect() try: transaction = connection.begin() try: metadata = Base.metadata metadata.create_all(bind=connection) yield connection finally: transaction.rollback() finally: connection.close() @yield_fixture def fx_session(fx_connection): session = Session(bind=fx_connection) try: yield session finally: session.close() class UtcDateTimeTable(Base): time = Column(UtcDateTime, primary_key=True) __tablename__ = 'tb_utc_datetime' class FixedOffset(datetime.tzinfo): zero = datetime.timedelta(0) def __init__(self, offset, name): self.offset = offset self.name = name def utcoffset(self, _): return self.offset def tzname(self, _): return self.name def dst(self, _): return self.zero @mark.parametrize('tzinfo', [ utc, FixedOffset(datetime.timedelta(hours=9), 'KST'), ]) def test_utc_datetime(fx_session, tzinfo): aware_time = datetime.datetime.now(tzinfo).replace(microsecond=0) e = UtcDateTimeTable(time=aware_time) fx_session.add(e) fx_session.flush() saved_time, = fx_session.query(UtcDateTimeTable.time).one() assert saved_time == aware_time if fx_session.bind.dialect.name in ('sqlite', 'mysql'): zero = datetime.timedelta(0) assert saved_time.tzinfo.utcoffset(aware_time) == zero assert saved_time.tzinfo.dst(aware_time) in (None, zero) def test_utc_datetime_naive(fx_session): with raises((ValueError, StatementError)): a = UtcDateTimeTable(time=datetime.datetime.now()) fx_session.add(a) fx_session.flush() def test_utc_datetime_type(fx_session): with raises((TypeError, StatementError)): a = UtcDateTimeTable(time=str(datetime.datetime.now())) fx_session.add(a) fx_session.flush() spoqa-sqlalchemy-utc-435c9d8/tox.ini000066400000000000000000000004141412325274200174470ustar00rootroot00000000000000[tox] envlist = pypy, py27, pypy3, py34, py35, py36, py37, py38, py39 minversion = 1.6.0 [testenv] deps = pytest flake8 flake8-import-order commands = pytest tests flake8 [flake8] max-complexity = 10 exclude = .git, .tox, __pycache__