├── .coveragerc ├── .gitignore ├── .travis.yml ├── CHANGES.rst ├── LICENSE ├── README.rst ├── setup.cfg ├── setup.py ├── sqlalchemy_utc ├── __init__.py ├── now.py ├── py.typed ├── sqltypes.py ├── timezone.py └── version.py ├── tests ├── __init__.py ├── conftest.py ├── test_now.py └── test_sqltypes.py └── tox.ini /.coveragerc: -------------------------------------------------------------------------------- 1 | [run] 2 | source = sqlalchemy_utc/ 3 | omit = 4 | tests/* 5 | setup.py 6 | 7 | [report] 8 | exclude_lines = 9 | def __repr__ 10 | def __str__ 11 | if __name__ = .__main__.: 12 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | 2 | # Created by https://www.gitignore.io/api/osx,pydev,python,pycharm,visualstudiocode 3 | 4 | ### OSX ### 5 | *.DS_Store 6 | .AppleDouble 7 | .LSOverride 8 | 9 | # Icon must end with two \r 10 | Icon 11 | 12 | # Thumbnails 13 | ._* 14 | 15 | # Files that might appear in the root of a volume 16 | .DocumentRevisions-V100 17 | .fseventsd 18 | .Spotlight-V100 19 | .TemporaryItems 20 | .Trashes 21 | .VolumeIcon.icns 22 | .com.apple.timemachine.donotpresent 23 | 24 | # Directories potentially created on remote AFP share 25 | .AppleDB 26 | .AppleDesktop 27 | Network Trash Folder 28 | Temporary Items 29 | .apdisk 30 | 31 | ### PyCharm ### 32 | # Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm 33 | # Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 34 | 35 | # User-specific stuff: 36 | .idea/**/workspace.xml 37 | .idea/**/tasks.xml 38 | .idea/dictionaries 39 | 40 | # Sensitive or high-churn files: 41 | .idea/**/dataSources/ 42 | .idea/**/dataSources.ids 43 | .idea/**/dataSources.xml 44 | .idea/**/dataSources.local.xml 45 | .idea/**/sqlDataSources.xml 46 | .idea/**/dynamic.xml 47 | .idea/**/uiDesigner.xml 48 | 49 | # Gradle: 50 | .idea/**/gradle.xml 51 | .idea/**/libraries 52 | 53 | # CMake 54 | cmake-build-debug/ 55 | 56 | # Mongo Explorer plugin: 57 | .idea/**/mongoSettings.xml 58 | 59 | ## File-based project format: 60 | *.iws 61 | 62 | ## Plugin-specific files: 63 | 64 | # IntelliJ 65 | /out/ 66 | 67 | # mpeltonen/sbt-idea plugin 68 | .idea_modules/ 69 | 70 | # JIRA plugin 71 | atlassian-ide-plugin.xml 72 | 73 | # Cursive Clojure plugin 74 | .idea/replstate.xml 75 | 76 | # Ruby plugin and RubyMine 77 | /.rakeTasks 78 | 79 | # Crashlytics plugin (for Android Studio and IntelliJ) 80 | com_crashlytics_export_strings.xml 81 | crashlytics.properties 82 | crashlytics-build.properties 83 | fabric.properties 84 | 85 | ### PyCharm Patch ### 86 | # Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721 87 | 88 | # *.iml 89 | # modules.xml 90 | # .idea/misc.xml 91 | # *.ipr 92 | 93 | # Sonarlint plugin 94 | .idea/sonarlint 95 | 96 | ### pydev ### 97 | .pydevproject 98 | 99 | ### Python ### 100 | # Byte-compiled / optimized / DLL files 101 | __pycache__/ 102 | *.py[cod] 103 | *$py.class 104 | 105 | # C extensions 106 | *.so 107 | 108 | # Distribution / packaging 109 | .Python 110 | build/ 111 | develop-eggs/ 112 | dist/ 113 | downloads/ 114 | eggs/ 115 | .eggs/ 116 | lib/ 117 | lib64/ 118 | parts/ 119 | sdist/ 120 | var/ 121 | wheels/ 122 | *.egg-info/ 123 | .installed.cfg 124 | *.egg 125 | 126 | # PyInstaller 127 | # Usually these files are written by a python script from a template 128 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 129 | *.manifest 130 | *.spec 131 | 132 | # Installer logs 133 | pip-log.txt 134 | pip-delete-this-directory.txt 135 | 136 | # Unit test / coverage reports 137 | htmlcov/ 138 | .tox/ 139 | .coverage 140 | .coverage.* 141 | .cache 142 | nosetests.xml 143 | coverage.xml 144 | *.cover 145 | .hypothesis/ 146 | 147 | # Translations 148 | *.mo 149 | *.pot 150 | 151 | # Django stuff: 152 | *.log 153 | local_settings.py 154 | 155 | # Flask stuff: 156 | instance/ 157 | .webassets-cache 158 | 159 | # Scrapy stuff: 160 | .scrapy 161 | 162 | # Sphinx documentation 163 | docs/_build/ 164 | 165 | # PyBuilder 166 | target/ 167 | 168 | # Jupyter Notebook 169 | .ipynb_checkpoints 170 | 171 | # pyenv 172 | .python-version 173 | 174 | # celery beat schedule file 175 | celerybeat-schedule.* 176 | 177 | # SageMath parsed files 178 | *.sage.py 179 | 180 | # Environments 181 | .env 182 | .venv 183 | env/ 184 | venv/ 185 | ENV/ 186 | env.bak/ 187 | venv.bak/ 188 | 189 | # Spyder project settings 190 | .spyderproject 191 | .spyproject 192 | 193 | # Rope project settings 194 | .ropeproject 195 | 196 | # mkdocs documentation 197 | /site 198 | 199 | # mypy 200 | .mypy_cache/ 201 | 202 | ### VisualStudioCode ### 203 | .vscode/* 204 | !.vscode/settings.json 205 | !.vscode/tasks.json 206 | !.vscode/launch.json 207 | !.vscode/extensions.json 208 | .history 209 | 210 | 211 | # End of https://www.gitignore.io/api/osx,pydev,python,pycharm,visualstudiocode 212 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: python 2 | python: 3 | # Supported versions 4 | - pypy3 5 | - 3.6 6 | - 3.7 7 | - 3.8 8 | - 3.9 9 | - pypy 10 | # Unsupported from Python team 11 | - 2.7 12 | # Completely unsupported 13 | - 3.4 14 | - 3.5 15 | # See: 16 | # https://www.python.org/downloads/ 17 | # https://www.pypy.org/download_advanced.html 18 | 19 | env: 20 | # Supported versions 21 | - SQLALCHEMY_VER=">=1.3.0,<1.4.0" PIP_OPTS="" 22 | - SQLALCHEMY_VER=">=1.4.0,<1.5.0" PIP_OPTS="" 23 | # Unsupported versions 24 | - SQLALCHEMY_VER=">=1.0.0,<1.1.0" PIP_OPTS="" 25 | - SQLALCHEMY_VER=">=1.1.0,<1.2.0" PIP_OPTS="" 26 | - SQLALCHEMY_VER=">=1.2.0,<1.3.0" PIP_OPTS="" 27 | # See: https://www.sqlalchemy.org/download.html 28 | 29 | services: 30 | - postgresql 31 | - mysql 32 | 33 | jobs: 34 | exclude: 35 | # No package of SQLAlchemy v1.4 for these Python versions 36 | - python: 3.4 37 | env: SQLALCHEMY_VER=">=1.4.0,<1.5.0" PIP_OPTS="" 38 | - python: 3.5 39 | env: SQLALCHEMY_VER=">=1.4.0,<1.5.0" PIP_OPTS="" 40 | 41 | before_install: 42 | - export PY=`python -c 'import sys; print("pypy" if hasattr(sys,"pypy_version_info") else "%d.%d" % sys.version_info[:2])'` 43 | - export PY_VER=`python -c 'import sys; print("%d.%d" % sys.version_info[:2])'` 44 | - echo "PY='$PY'" 45 | - echo "PY_VER='$PY_VER'" 46 | - if [[ "$PY" = "pypy" ]]; then 47 | export PG_URL="postgresql+psycopg2cffi:///utc_test"; 48 | export MYSQL_URL="mysql+pymysql://root@localhost/utc_test"; 49 | else 50 | export PG_URL="postgresql+psycopg2:///utc_test"; 51 | export MYSQL_URL="mysql+mysqlconnector://root@localhost/utc_test"; 52 | fi 53 | - export TEST_DATABASE_URLS="$PG_URL $MYSQL_URL" 54 | 55 | install: 56 | - if [[ "$PIP_OPTS" != "" ]]; then 57 | pip install $PIP_OPTS "SQLAlchemy $SQLALCHEMY_VER"; 58 | else 59 | pip install "SQLAlchemy $SQLALCHEMY_VER"; 60 | fi 61 | - pip install -e . 62 | - pip install pytest codecov flake8 flake8-import-order 63 | # pytest-cov 2.10.0 requires a pytest >= 4.6 which is not available in the Python 2.7 environment. 64 | # Use an older version for it. 65 | - if [[ "$PY_VER" = "2.7" && "$PY" != "pypy" ]]; then 66 | pip install 'pytest-cov < 2.10.0'; 67 | else 68 | pip install pytest-cov; 69 | fi 70 | # PyMySQL dropped support for Python 2.7 in version 1.0.0: 71 | # https://github.com/PyMySQL/PyMySQL/blob/master/CHANGELOG.md#v100 72 | # mysql-coonector-python did the same since version 8.0.24: 73 | # https://dev.mysql.com/doc/connector-python/en/connector-python-versions.html 74 | - if [[ "$PY" = "pypy" ]]; then 75 | pip install psycopg2cffi 'pymysql < 1.0.0'; 76 | elif [[ "$PY_VER" = "2.7" ]]; then 77 | pip install psycopg2 'mysql-connector-python < 8.0.24'; 78 | else 79 | pip install psycopg2 mysql-connector-python; 80 | fi 81 | 82 | before_script: 83 | - createdb -E utf8 -T postgres utc_test 84 | - mysql -e 'CREATE DATABASE utc_test;' 85 | 86 | script: 87 | - pytest --cov=. --durations=10 tests 88 | - flake8 . 89 | 90 | after_success: 91 | - codecov 92 | -------------------------------------------------------------------------------- /CHANGES.rst: -------------------------------------------------------------------------------- 1 | Changelog 2 | ========= 3 | 4 | 0.14.0 5 | ------ 6 | 7 | Released on September 24, 2021. 8 | 9 | - Add cache_ok flag on ``UtcDateTime`` to supress Pandas warnings. 10 | [`#14`_ by derekderie] 11 | 12 | .. _#14: https://github.com/spoqa/sqlalchemy-utc/pull/14 13 | 14 | 15 | 0.13.0 16 | ------ 17 | 18 | Released on September 24, 2021. 19 | 20 | - Add milliseconds to SQLite datetimes. [`#12`_ by Giovanni Santini] 21 | - Add support for newer python versions. (3.7, 3.8, 3.9) 22 | [`#12`_ by Giovanni Santini] 23 | 24 | .. _#12: https://github.com/spoqa/sqlalchemy-utc/pull/12 25 | 26 | 27 | 0.12.0 28 | ------ 29 | 30 | Released on May 7, 2021. 31 | 32 | - Add `py.typed` file to the package to be compatible with PEP-561. 33 | [`#10`_ by Dima Boger] 34 | 35 | .. _#10: https://github.com/spoqa/sqlalchemy-utc/pull/10 36 | 37 | 38 | 0.11.0 39 | ------ 40 | 41 | Released on November 13, 2020. 42 | 43 | - Ensured always returning the datetime with UTC timezone. 44 | [`#8`_ by Eduard Christian Dumitrescu] 45 | 46 | .. _#8: https://github.com/spoqa/sqlalchemy-utc/pull/8 47 | 48 | 49 | 0.10.0 50 | ------ 51 | 52 | Released on January 25, 2018. 53 | 54 | - Dropped support of older Python versions: 2.6, 3.2, and 3.3. 55 | [`#2`_ by George Leslie-Waksman] 56 | - Added ``sqlalchemy_utc.utcnow()`` function as an alternative to 57 | ``sqlalchemy.sql.functions.now()`` for generating ``UtcDateTime`` values 58 | on the database server. [`#4`_ by George Leslie-Waksman] 59 | 60 | .. _#2: https://github.com/spoqa/sqlalchemy-utc/pull/2 61 | .. _#4: https://github.com/spoqa/sqlalchemy-utc/pull/4 62 | 63 | 64 | 0.9.0 65 | ----- 66 | 67 | First version. Released on June 22, 2016. 68 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (C) 2015-2016 by Hong Minhee 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy 4 | of this software and associated documentation files (the "Software"), to deal 5 | in the Software without restriction, including without limitation the rights 6 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 7 | copies of the Software, and to permit persons to whom the Software is 8 | furnished to do so, subject to the following conditions: 9 | 10 | The above copyright notice and this permission notice shall be included in 11 | all copies or substantial portions of the Software. 12 | 13 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 14 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 15 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 16 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 17 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 18 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 19 | THE SOFTWARE. 20 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | SQLAlchemy-Utc 2 | ============== 3 | 4 | .. image:: https://badge.fury.io/py/SQLAlchemy-Utc.svg? 5 | :target: https://pypi.python.org/pypi/SQLAlchemy-Utc 6 | .. image:: https://travis-ci.com/spoqa/sqlalchemy-utc.svg?branch=master 7 | :target: https://travis-ci.com/spoqa/sqlalchemy-utc 8 | .. image:: https://codecov.io/github/spoqa/sqlalchemy-utc/coverage.svg?branch=master 9 | :target: https://codecov.io/github/spoqa/sqlalchemy-utc?branch=master 10 | 11 | This package provides a drop-in replacement of SQLAlchemy's built-in `DateTime`_ 12 | type with ``timezone=True`` option enabled. Although SQLAlchemy's built-in 13 | ``DateTime`` type provides ``timezone=True`` option, since some vendors like 14 | SQLite and MySQL don't provide ``timestamptz`` data type, the option doesn't 15 | make any effect on these vendors. 16 | 17 | ``UtcDateTime`` type is equivalent to the built-in ``DateTime`` with 18 | ``timezone=True`` option enabled on vendors that support ``timestamptz`` 19 | e.g. PostgreSQL, but on SQLite or MySQL, it shifts all ``datetime.datetime`` 20 | values to UTC offset before store them, and returns always aware 21 | ``datetime.datetime`` values through result sets. 22 | 23 | Long story short, ``UtcDateTime`` does: 24 | 25 | - take only aware ``datetime.datetime``, 26 | - return only aware ``datetime.datetime``, 27 | - never take or return naive ``datetime.datetime``, 28 | - ensure timestamps in database always to be encoded in UTC, and 29 | - work as you'd expect. 30 | 31 | A SQLAlchemy helper function, ``utcnow()``, is provided as an alternative 32 | to ``func.now()`` for generating ``UtcDateTime`` values on the server. For 33 | example: ``Column('time', UtcDateTime(), default=utcnow())``. 34 | 35 | Written by `Hong Minhee`_ at Spoqa_, and distributed under MIT license. 36 | 37 | .. _DateTime: http://docs.sqlalchemy.org/en/latest/core/type_basics.html#sqlalchemy.types.DateTime 38 | .. _Hong Minhee: https://hongminhee.org/ 39 | .. _Spoqa: http://www.spoqa.com/ 40 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [bdist_wheel] 2 | universal = 1 3 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | import ast 2 | import os.path 3 | 4 | from setuptools import find_packages, setup 5 | 6 | 7 | def readme(): 8 | try: 9 | with open('README.rst') as f: 10 | readme = f.read() 11 | except IOError: 12 | pass 13 | try: 14 | with open('CHANGES.rst') as f: 15 | readme += '\n\n' + f.read() 16 | except IOError: 17 | pass 18 | return readme 19 | 20 | 21 | def get_version(): 22 | module_path = os.path.join( 23 | os.path.dirname(__file__), 24 | 'sqlalchemy_utc', 25 | 'version.py') 26 | module_file = open(module_path) 27 | try: 28 | module_code = module_file.read() 29 | finally: 30 | module_file.close() 31 | tree = ast.parse(module_code, module_path) 32 | for node in ast.iter_child_nodes(tree): 33 | if not isinstance(node, ast.Assign) or len(node.targets) != 1: 34 | continue 35 | target, = node.targets 36 | if isinstance(target, ast.Name) and target.id == '__version__': 37 | return node.value.s 38 | 39 | 40 | install_requires = ['setuptools', 'SQLAlchemy >= 0.9.0'] 41 | 42 | 43 | setup( 44 | name='SQLAlchemy-Utc', 45 | description='SQLAlchemy type to store aware datetime values', 46 | long_description=readme(), 47 | version=get_version(), 48 | url='https://github.com/spoqa/sqlalchemy-utc', 49 | packages=find_packages(exclude=('tests*',)), 50 | package_data={'sqlalchemy_utc': ['py.typed']}, 51 | author='Hong Minhee', 52 | author_email='hongminhee' '@' 'member.fsf.org', 53 | license='MIT License', 54 | install_requires=install_requires, 55 | classifiers=[ 56 | 'Development Status :: 4 - Beta', 57 | 'Intended Audience :: Developers', 58 | 'License :: OSI Approved :: MIT License', 59 | 'Operating System :: OS Independent', 60 | 'Programming Language :: Python :: 2.7', 61 | 'Programming Language :: Python :: 3.4', 62 | 'Programming Language :: Python :: 3.5', 63 | 'Programming Language :: Python :: 3.6', 64 | 'Programming Language :: Python :: 3.7', 65 | 'Programming Language :: Python :: 3.8', 66 | 'Programming Language :: Python :: 3.9', 67 | 'Programming Language :: Python :: Implementation :: CPython', 68 | 'Programming Language :: Python :: Implementation :: PyPy', 69 | 'Programming Language :: Python :: Implementation :: Stackless', 70 | 'Programming Language :: SQL', 71 | 'Topic :: Database :: Front-Ends', 72 | 'Topic :: Software Development', 73 | ] 74 | ) 75 | -------------------------------------------------------------------------------- /sqlalchemy_utc/__init__.py: -------------------------------------------------------------------------------- 1 | from .now import utcnow 2 | from .sqltypes import UtcDateTime 3 | from .timezone import utc 4 | from .version import __version__ 5 | 6 | __all__ = [ 7 | '__version__', 8 | 'utc', 9 | 'UtcDateTime', 10 | 'utcnow', 11 | ] 12 | -------------------------------------------------------------------------------- /sqlalchemy_utc/now.py: -------------------------------------------------------------------------------- 1 | 2 | from sqlalchemy.ext.compiler import compiles 3 | from sqlalchemy.sql.functions import FunctionElement 4 | 5 | from .sqltypes import UtcDateTime 6 | 7 | 8 | class utcnow(FunctionElement): 9 | """UTCNOW() expression for multiple dialects.""" 10 | 11 | inherit_cache = True 12 | type = UtcDateTime() 13 | 14 | 15 | @compiles(utcnow) 16 | def default_sql_utcnow(element, compiler, **kw): 17 | """Assume, by default, time zones work correctly. 18 | 19 | Note: 20 | This is a valid assumption for PostgreSQL and Oracle. 21 | """ 22 | return 'CURRENT_TIMESTAMP' 23 | 24 | 25 | @compiles(utcnow, 'mysql') 26 | def mysql_sql_utcnow(element, compiler, **kw): 27 | """MySQL returns now as localtime, so we convert to UTC. 28 | 29 | Warning: 30 | MySQL does not support the use of functions for sqlalchemy 31 | `server_default=` values. The utcnow function must be used as 32 | `default=` when interacting with a MySQL server. 33 | """ 34 | return "CONVERT_TZ(CURRENT_TIMESTAMP, @@session.time_zone, '+00:00')" 35 | 36 | 37 | @compiles(utcnow, 'sqlite') 38 | def sqlite_sql_utcnow(element, compiler, **kw): 39 | """SQLite DATETIME('NOW') returns a correct `datetime.datetime` but does not 40 | add milliseconds to it. 41 | 42 | Directly call STRFTIME with the final %f modifier in order to get those. 43 | """ 44 | return r"(STRFTIME('%Y-%m-%d %H:%M:%f', 'NOW'))" 45 | 46 | 47 | @compiles(utcnow, 'mssql') 48 | def mssql_sql_utcnow(element, compiler, **kw): 49 | """MS SQL provides a function for the UTC datetime.""" 50 | return 'GETUTCDATE()' 51 | -------------------------------------------------------------------------------- /sqlalchemy_utc/py.typed: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoqa/sqlalchemy-utc/c97363951b37f879db4c4a1a70b11bfa416f91b2/sqlalchemy_utc/py.typed -------------------------------------------------------------------------------- /sqlalchemy_utc/sqltypes.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from sqlalchemy.types import DateTime, TypeDecorator 4 | 5 | from .timezone import utc 6 | 7 | 8 | class UtcDateTime(TypeDecorator): 9 | """Almost equivalent to :class:`~sqlalchemy.types.DateTime` with 10 | ``timezone=True`` option, but it differs from that by: 11 | 12 | - Never silently take naive :class:`~datetime.datetime`, instead it 13 | always raise :exc:`ValueError` unless time zone aware value. 14 | - :class:`~datetime.datetime` value's :attr:`~datetime.datetime.tzinfo` 15 | is always converted to UTC. 16 | - Unlike SQLAlchemy's built-in :class:`~sqlalchemy.types.DateTime`, 17 | it never return naive :class:`~datetime.datetime`, but time zone 18 | aware value, even with SQLite or MySQL. 19 | 20 | """ 21 | 22 | impl = DateTime(timezone=True) 23 | cache_ok = True 24 | 25 | def process_bind_param(self, value, dialect): 26 | if value is not None: 27 | if not isinstance(value, datetime.datetime): 28 | raise TypeError('expected datetime.datetime, not ' + 29 | repr(value)) 30 | elif value.tzinfo is None: 31 | raise ValueError('naive datetime is disallowed') 32 | return value.astimezone(utc) 33 | 34 | def process_result_value(self, value, dialect): 35 | if value is not None: 36 | if value.tzinfo is None: 37 | value = value.replace(tzinfo=utc) 38 | else: 39 | value = value.astimezone(utc) 40 | return value 41 | -------------------------------------------------------------------------------- /sqlalchemy_utc/timezone.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | 4 | class Utc(datetime.tzinfo): 5 | 6 | __slots__ = () 7 | 8 | zero = datetime.timedelta(0) 9 | 10 | def utcoffset(self, _): 11 | return self.zero 12 | 13 | def dst(self, _): 14 | return self.zero 15 | 16 | def tzname(self, _): 17 | return 'UTC' 18 | 19 | 20 | try: 21 | utc = datetime.timezone.utc 22 | except AttributeError: 23 | utc = Utc() 24 | -------------------------------------------------------------------------------- /sqlalchemy_utc/version.py: -------------------------------------------------------------------------------- 1 | __version__ = '0.14.0' 2 | -------------------------------------------------------------------------------- /tests/__init__.py: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/spoqa/sqlalchemy-utc/c97363951b37f879db4c4a1a70b11bfa416f91b2/tests/__init__.py -------------------------------------------------------------------------------- /tests/conftest.py: -------------------------------------------------------------------------------- 1 | import os 2 | 3 | from pytest import fixture 4 | 5 | from sqlalchemy.engine import create_engine 6 | from sqlalchemy.pool import NullPool 7 | 8 | 9 | try: 10 | database_urls = os.environ['TEST_DATABASE_URLS'].split() 11 | except KeyError: 12 | database_urls = [] 13 | 14 | 15 | @fixture(scope='function', params=['sqlite://'] + database_urls) 16 | def fx_engine(request): 17 | url = request.param 18 | engine = create_engine(url, poolclass=NullPool) 19 | request.addfinalizer(engine.dispose) 20 | return engine 21 | -------------------------------------------------------------------------------- /tests/test_now.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | from pytest import yield_fixture 4 | 5 | from sqlalchemy import Column, MetaData, Table, select 6 | 7 | from sqlalchemy_utc import UtcDateTime, utc, utcnow 8 | 9 | 10 | TABLE = Table( 11 | 'test_table', MetaData(), 12 | Column('time', UtcDateTime, default=utcnow())) 13 | 14 | 15 | @yield_fixture 16 | def fx_connection(fx_engine): 17 | connection = fx_engine.connect() 18 | try: 19 | transaction = connection.begin() 20 | try: 21 | TABLE.create(connection) 22 | yield connection 23 | finally: 24 | transaction.rollback() 25 | finally: 26 | connection.close() 27 | 28 | 29 | def test_utcnow_timezone(fx_connection): 30 | fx_connection.execute(TABLE.insert(), [{}]) 31 | rows = fx_connection.execute(select([TABLE])).fetchall() 32 | server_now = rows[0].time 33 | local_now = datetime.datetime.now(utc) 34 | assert server_now.tzinfo is not None 35 | assert abs(server_now - local_now) < datetime.timedelta(seconds=30) 36 | -------------------------------------------------------------------------------- /tests/test_sqltypes.py: -------------------------------------------------------------------------------- 1 | import datetime 2 | 3 | try: 4 | from psycopg2ct.compat import register 5 | except ImportError: 6 | pass 7 | else: 8 | register() 9 | from pytest import mark, raises, yield_fixture 10 | 11 | from sqlalchemy.exc import StatementError 12 | from sqlalchemy.ext.declarative import declarative_base 13 | from sqlalchemy.orm import sessionmaker 14 | from sqlalchemy.schema import Column 15 | 16 | from sqlalchemy_utc import UtcDateTime, utc 17 | 18 | 19 | Base = declarative_base() 20 | Session = sessionmaker() 21 | 22 | 23 | @yield_fixture 24 | def fx_connection(fx_engine): 25 | connection = fx_engine.connect() 26 | try: 27 | transaction = connection.begin() 28 | try: 29 | metadata = Base.metadata 30 | metadata.create_all(bind=connection) 31 | yield connection 32 | finally: 33 | transaction.rollback() 34 | finally: 35 | connection.close() 36 | 37 | 38 | @yield_fixture 39 | def fx_session(fx_connection): 40 | session = Session(bind=fx_connection) 41 | try: 42 | yield session 43 | finally: 44 | session.close() 45 | 46 | 47 | class UtcDateTimeTable(Base): 48 | 49 | time = Column(UtcDateTime, primary_key=True) 50 | 51 | __tablename__ = 'tb_utc_datetime' 52 | 53 | 54 | class FixedOffset(datetime.tzinfo): 55 | 56 | zero = datetime.timedelta(0) 57 | 58 | def __init__(self, offset, name): 59 | self.offset = offset 60 | self.name = name 61 | 62 | def utcoffset(self, _): 63 | return self.offset 64 | 65 | def tzname(self, _): 66 | return self.name 67 | 68 | def dst(self, _): 69 | return self.zero 70 | 71 | 72 | @mark.parametrize('tzinfo', [ 73 | utc, 74 | FixedOffset(datetime.timedelta(hours=9), 'KST'), 75 | ]) 76 | def test_utc_datetime(fx_session, tzinfo): 77 | aware_time = datetime.datetime.now(tzinfo).replace(microsecond=0) 78 | e = UtcDateTimeTable(time=aware_time) 79 | fx_session.add(e) 80 | fx_session.flush() 81 | saved_time, = fx_session.query(UtcDateTimeTable.time).one() 82 | assert saved_time == aware_time 83 | if fx_session.bind.dialect.name in ('sqlite', 'mysql'): 84 | zero = datetime.timedelta(0) 85 | assert saved_time.tzinfo.utcoffset(aware_time) == zero 86 | assert saved_time.tzinfo.dst(aware_time) in (None, zero) 87 | 88 | 89 | def test_utc_datetime_naive(fx_session): 90 | with raises((ValueError, StatementError)): 91 | a = UtcDateTimeTable(time=datetime.datetime.now()) 92 | fx_session.add(a) 93 | fx_session.flush() 94 | 95 | 96 | def test_utc_datetime_type(fx_session): 97 | with raises((TypeError, StatementError)): 98 | a = UtcDateTimeTable(time=str(datetime.datetime.now())) 99 | fx_session.add(a) 100 | fx_session.flush() 101 | -------------------------------------------------------------------------------- /tox.ini: -------------------------------------------------------------------------------- 1 | [tox] 2 | envlist = pypy, py27, pypy3, py34, py35, py36, py37, py38, py39 3 | minversion = 1.6.0 4 | 5 | [testenv] 6 | deps = 7 | pytest 8 | flake8 9 | flake8-import-order 10 | commands = 11 | pytest tests 12 | flake8 13 | 14 | [flake8] 15 | max-complexity = 10 16 | exclude = 17 | .git, 18 | .tox, 19 | __pycache__ 20 | --------------------------------------------------------------------------------