Skip to content

Commit 857343f

Browse files
authored
support typehint (#57)
* support typehint * fix styles
1 parent 22fceb8 commit 857343f

File tree

6 files changed

+120
-52
lines changed

6 files changed

+120
-52
lines changed

docs/contributing.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,8 @@ $ python3 -m pip install ".[all]"
1818
$ git checkout -b new-branch
1919

2020
## 5. Run unittest (you should pass all test and coverage should be 100%)
21-
$ ./scripts/test.sh
21+
$ ./scripts/unittest.sh
22+
$ ./scripts/integration_test.sh
2223

2324
## 6. Format code
2425
$ ./scripts/format.sh

example.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ def example_rollback_with_custom_exception():
118118
class OriginalError(Exception):
119119
pass
120120

121-
with DataAPI(resource_arn=resource_arn, secret_arn=secret_arn, rollback_exception=rollback_exception=OriginalError) as data_api:
121+
with DataAPI(resource_arn=resource_arn, secret_arn=secret_arn, rollback_exception=OriginalError) as data_api:
122122
data_api.execute(Insert(Pets, {'name': 'dog'}))
123123
# some logic ...
124124

pydataapi/pydataapi.py

Lines changed: 40 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
from contextlib import AbstractContextManager
2+
from datetime import date, datetime, time
3+
from decimal import Decimal
24
from functools import wraps
35
from typing import (
46
Any,
@@ -47,6 +49,11 @@
4749
DOUBLE_VALUES: str = 'doubleValues'
4850
BLOB_VALUES: str = 'blobValues'
4951

52+
DECIMAL_TYPE_HINT: str = 'DECIMAL'
53+
TIMESTAMP_TYPE_HINT: str = 'TIMESTAMP'
54+
TIME_TYPE_HINT: str = 'TIME'
55+
DATE_TYPE_HINT: str = 'DATE'
56+
5057

5158
def generate_sql(query: Union[Query, Insert, Update, Delete, Select]) -> str:
5259
if hasattr(query, 'statement'):
@@ -138,33 +145,51 @@ def convert_array_value(value: Union[List, Tuple]) -> Dict[str, Any]:
138145
raise Exception(f'unsupported array type {type(value[0])}]: {value} ')
139146

140147

141-
def convert_value(value: Any) -> Dict[str, Any]:
148+
def create_sql_parameter(key: str, value: Any) -> Dict[str, Any]:
149+
converted_value: Dict[str, Any]
150+
type_hint: Optional[str] = None
151+
142152
if isinstance(value, bool):
143-
return {BOOLEAN_VALUE: value}
153+
converted_value = {BOOLEAN_VALUE: value}
144154
elif isinstance(value, str):
145-
return {STRING_VALUE: value}
155+
converted_value = {STRING_VALUE: value}
146156
elif isinstance(value, int):
147-
return {LONG_VALUE: value}
157+
converted_value = {LONG_VALUE: value}
148158
elif isinstance(value, float):
149-
return {DOUBLE_VALUE: value}
159+
converted_value = {DOUBLE_VALUE: value}
150160
elif isinstance(value, bytes):
151-
return {BLOB_VALUE: value}
161+
converted_value = {BLOB_VALUE: value}
152162
elif value is None:
153-
return {IS_NULL: True}
163+
converted_value = {IS_NULL: True}
154164
elif isinstance(value, (list, tuple)):
155-
if not value:
156-
return {IS_NULL: True}
157-
return convert_array_value(value)
158-
# TODO: support structValue
159-
return {STRING_VALUE: str(value)}
165+
if value:
166+
converted_value = convert_array_value(value)
167+
else:
168+
converted_value = {IS_NULL: True}
169+
elif isinstance(value, Decimal):
170+
converted_value = {STRING_VALUE: str(value)}
171+
type_hint = DECIMAL_TYPE_HINT
172+
elif isinstance(value, datetime):
173+
converted_value = {STRING_VALUE: value.strftime('%Y-%m-%d %H:%M:%S.%f')[:23]}
174+
type_hint = TIMESTAMP_TYPE_HINT
175+
elif isinstance(value, time):
176+
converted_value = {STRING_VALUE: value.strftime('%H:%M:%S.%f')[:12]}
177+
type_hint = TIME_TYPE_HINT
178+
elif isinstance(value, date):
179+
converted_value = {STRING_VALUE: value.strftime('%Y-%m-%d')}
180+
type_hint = DATE_TYPE_HINT
181+
else:
182+
# TODO: support structValue
183+
converted_value = {STRING_VALUE: str(value)}
184+
if type_hint:
185+
return {'name': key, 'value': converted_value, 'typeHint': type_hint}
186+
return {'name': key, 'value': converted_value}
160187

161188

162189
def create_sql_parameters(
163190
parameter: Dict[str, Any]
164191
) -> List[Dict[str, Union[str, Dict]]]:
165-
return [
166-
{'name': key, 'value': convert_value(value)} for key, value in parameter.items()
167-
]
192+
return [create_sql_parameter(key, value) for key, value in parameter.items()]
168193

169194

170195
def _get_value_from_row(row: Dict[str, Any]) -> Any:

setup.cfg

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ setup_requires =
2626
pytest-runner
2727
setuptools-scm
2828
install_requires =
29-
boto3 == 1.11.15
29+
boto3 == 1.12.7
3030
SQLAlchemy == 1.3.13
3131
pydantic == 1.4
3232
more-itertools == 8.0.2

tests/integration/test_mysql.py

Lines changed: 47 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,12 @@
11
import time
2+
from datetime import datetime
23
from typing import List
34

45
import boto3
56
import pytest
67
from pydataapi import DataAPI, Result, transaction
78
from pydataapi.pydataapi import Record
8-
from sqlalchemy import Column, Integer, String, create_engine
9+
from sqlalchemy import Column, DateTime, Integer, String, create_engine
910
from sqlalchemy.engine import Connection
1011
from sqlalchemy.ext.declarative import declarative_base
1112
from sqlalchemy.orm import Query, sessionmaker
@@ -18,6 +19,7 @@ class Pets(declarative_base()):
1819
__tablename__ = 'pets'
1920
id = Column(Integer, primary_key=True, autoincrement=True)
2021
name = Column(String(255, collation='utf8_unicode_ci'), default=None)
22+
seen_at = Column(DateTime, default=None)
2123

2224

2325
database: str = 'test'
@@ -56,7 +58,7 @@ def db_connection(module_scoped_container_getter) -> Connection:
5658
def create_table(db_connection) -> None:
5759
db_connection.execute('drop table if exists pets;')
5860
db_connection.execute(
59-
'create table pets (id int auto_increment not null primary key, name varchar(10));'
61+
'create table pets (id int auto_increment not null primary key, name varchar(10), seen_at TIMESTAMP null);'
6062
)
6163

6264

@@ -117,32 +119,33 @@ def test_with_statement(rds_data_client, db_connection):
117119
query = Query(Pets).filter(Pets.id == 1)
118120
result = data_api.execute(query)
119121

120-
assert list(result) == [Record([1, 'dog'], [])]
122+
assert list(result) == [Record([1, 'dog', None], [])]
121123

122124
result = data_api.execute('select * from pets')
123-
assert result.one().dict() == {'id': 1, 'name': 'dog'}
124-
125-
insert: Insert = Insert(Pets)
126-
data_api.batch_execute(
127-
insert,
128-
[
129-
{'id': 2, 'name': 'cat'},
130-
{'id': 3, 'name': 'snake'},
131-
{'id': 4, 'name': 'rabbit'},
132-
],
133-
)
134-
135-
result = data_api.execute('select * from pets')
136-
expected = [
137-
Record([1, 'dog'], ['id', 'name']),
138-
Record([2, 'cat'], ['id', 'name']),
139-
Record([3, 'snake'], ['id', 'name']),
140-
Record([4, 'rabbit'], ['id', 'name']),
141-
]
142-
assert list(result) == expected
143-
144-
for row, expected_row in zip(result, expected):
145-
assert row == expected_row
125+
assert result.one().dict() == {'id': 1, 'name': 'dog', 'seen_at': None}
126+
127+
# This is deprecated. SQL Alchemy object will be no longer supported
128+
# insert: Insert = Insert(Pets)
129+
# data_api.batch_execute(
130+
# insert,
131+
# [
132+
# {'id': 2, 'name': 'cat', 'seen_at': None},
133+
# {'id': 3, 'name': 'snake', 'seen_at': None},
134+
# {'id': 4, 'name': 'rabbit', 'seen_at': None},
135+
# ],
136+
# )
137+
#
138+
# result = data_api.execute('select * from pets')
139+
# expected = [
140+
# Record([1, 'dog', None], ['id', 'name', 'seen_at']),
141+
# Record([2, 'cat', None], ['id', 'name', 'seen_at']),
142+
# Record([3, 'snake', None], ['id', 'name', 'seen_at']),
143+
# Record([4, 'rabbit', None], ['id', 'name', 'seen_at']),
144+
# ]
145+
# assert list(result) == expected
146+
#
147+
# for row, expected_row in zip(result, expected):
148+
# assert row == expected_row
146149

147150

148151
def test_rollback(rds_data_client, db_connection):
@@ -199,10 +202,10 @@ class OtherError(Exception):
199202
except:
200203
pass
201204
result = list(get_connection().execute('select * from pets'))
202-
assert result == [(2, 'dog')]
205+
assert result == [(2, 'dog', None)]
203206

204207

205-
def test_dialect() -> None:
208+
def test_dialect(create_table) -> None:
206209
rds_data_client = boto3.client(
207210
'rds-data',
208211
endpoint_url='http://127.0.0.1:8080',
@@ -222,3 +225,19 @@ def test_dialect() -> None:
222225

223226
assert engine.has_table('foo') is False
224227
assert engine.has_table('pets') is True
228+
229+
Session = sessionmaker()
230+
Session.configure(bind=engine)
231+
session = Session()
232+
233+
dog = Pets(name="dog", seen_at=datetime(2020, 1, 2, 3, 4, 5, 6789))
234+
235+
session.add(dog)
236+
session.commit()
237+
238+
result = list(engine.execute('select * from pets'))
239+
assert result[0] == (
240+
1,
241+
'dog',
242+
'2020-01-02 03:04:05',
243+
) # TODO Update local-data-api to support typeHint

tests/pydataapi/test_pydataapi.py

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import datetime
2+
from decimal import Decimal
23
from typing import Any, Dict
34

45
import pytest
@@ -13,7 +14,7 @@
1314
UpdateResults,
1415
_get_value_from_row,
1516
convert_array_value,
16-
convert_value,
17+
create_sql_parameter,
1718
create_sql_parameters,
1819
generate_sql,
1920
transaction,
@@ -60,19 +61,41 @@ def mocked_client(mocker):
6061
([b'bytes', b'blob'], {'arrayValue': {'blobValues': [b'bytes', b'blob']}}),
6162
],
6263
)
63-
def test_convert_value(input_value: Any, expected: Dict[str, Any]) -> None:
64-
assert convert_value(input_value) == expected
64+
def test_create_sql_parameter(input_value: Any, expected: Dict[str, Any]) -> None:
65+
assert create_sql_parameter('', input_value)['value'] == expected
6566

6667

6768
def test_convert_value_other_types() -> None:
6869
class Dummy:
6970
def __str__(self):
7071
return 'Dummy'
7172

72-
assert convert_value(Dummy()) == {'stringValue': 'Dummy'}
73+
assert create_sql_parameter('', Dummy())['value'] == {'stringValue': 'Dummy'}
7374

74-
assert convert_value(datetime.datetime(2020, 1, 1)) == {
75-
'stringValue': '2020-01-01 00:00:00'
75+
assert create_sql_parameter('decimal', Decimal(123456789)) == {
76+
'name': 'decimal',
77+
'typeHint': 'DECIMAL',
78+
'value': {'stringValue': '123456789'},
79+
}
80+
81+
assert create_sql_parameter(
82+
'datetime', datetime.datetime(2020, 1, 2, 3, 4, 5, 678900)
83+
) == {
84+
'name': 'datetime',
85+
'typeHint': 'TIMESTAMP',
86+
'value': {'stringValue': '2020-01-02 03:04:05.678'},
87+
}
88+
89+
assert create_sql_parameter('date', datetime.date(2020, 1, 2)) == {
90+
'name': 'date',
91+
'typeHint': 'DATE',
92+
'value': {'stringValue': '2020-01-02'},
93+
}
94+
95+
assert create_sql_parameter('time', datetime.time(3, 4, 5, 678900)) == {
96+
'name': 'time',
97+
'typeHint': 'TIME',
98+
'value': {'stringValue': '03:04:05.678'},
7699
}
77100

78101

0 commit comments

Comments
 (0)