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