Skip to content

Commit ab44f90

Browse files
Release/sql types (#140)
* chore: Update makefile for tests-coverage command with database support * chore: Update pyproject.toml packages formatting * chore: Update VSCode settings for Python linting and formatting * feat: Add database types for various data types * feat: Add PySQLXJsonEnconder class for JSON encoding * chore: Refactor import statements in pysqlx_engine * Refactor import statements in pysqlx_engine * remove benchmark tests * chore: Update PySQLXEngineSync test_query.py with type casting and tuple parameter --------- Co-authored-by: carlos.rian <carlos.rian@quartile.com>
1 parent e6bd88a commit ab44f90

File tree

22 files changed

+757
-200570
lines changed

22 files changed

+757
-200570
lines changed

.vscode/settings.json

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,18 @@
11
{
2-
"python.testing.pytestArgs": [
3-
"tests"
4-
],
52
"python.testing.unittestEnabled": false,
63
"python.testing.pytestEnabled": true,
7-
"python.linting.mypyEnabled": false,
4+
"files.watcherExclude": {
5+
"**/__pycache__/**": true,
6+
},
7+
"[python]": {
8+
"editor.rulers": [
9+
120
10+
],
11+
"editor.formatOnSave": true,
12+
"editor.codeActionsOnSave": {
13+
"source.organizeImports": "explicit"
14+
},
15+
"editor.defaultFormatter": "ms-python.black-formatter",
16+
"editor.bracketPairColorization.enabled": true
17+
},
818
}

makefile

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@ upt-dev-deps:
2525

2626

2727
#--ignore=tests/benchmark
28-
tests-coverage:
28+
tests-coverage-with-database:
2929
make tests-databases && sleep 10 && \
3030
poetry run pytest tests -v -x \
3131
--doctest-modules \
@@ -36,6 +36,16 @@ tests-coverage:
3636
--cov-report=html:tests/results/html \
3737
--junitxml=tests/results/xml/test-results.xml
3838

39+
tests-coverage:
40+
poetry run pytest tests -v -x \
41+
--doctest-modules \
42+
--ignore=tests/benchmark \
43+
--cov=pysqlx_engine \
44+
--cov=tests \
45+
--durations=0 \
46+
--cov-report=html:tests/results/html \
47+
--junitxml=tests/results/xml/test-results.xml
48+
3949
requirements:
4050
poetry export -f requirements.txt --output requirements.txt --without-hashes
4151

poetry.lock

Lines changed: 178 additions & 192 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

pyproject.toml

Lines changed: 28 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ homepage = "https://carlos-rian.github.io/pysqlx-engine"
1111
documentation = "https://carlos-rian.github.io/pysqlx-engine"
1212
keywords = ["async", "database", "sql", "engine", "fastapi"]
1313

14-
packages = [
15-
{ include = "pysqlx_engine"},
16-
]
14+
packages = [{ include = "pysqlx_engine" }]
1715

1816
classifiers = [
1917
"Framework :: AnyIO",
@@ -49,9 +47,7 @@ classifiers = [
4947

5048
[tool.poetry.dependencies]
5149
python = "^3.7"
52-
pydantic = [
53-
{version = ">=1 <3", python = ">=3.7"}
54-
]
50+
pydantic = [{ version = ">=1 <3", python = ">=3.7" }]
5551
pysqlx-core = "^0.1"
5652
typing-extensions = ">=4.5,<5"
5753
Pygments = "^2.15.1"
@@ -60,7 +56,7 @@ Pygments = "^2.15.1"
6056
black = "^23"
6157
isort = [
6258
{ version = "^5.11", python = "<3.8" },
63-
{ version = "^5.12", python = ">=3.8" }
59+
{ version = "^5.12", python = ">=3.8" },
6460
]
6561
pytest = ">=7.*"
6662
pytest-asyncio = ">=0.*"
@@ -69,6 +65,22 @@ pytest-dotenv = "^0"
6965
pytest-xdist = "^3"
7066
toml = "^0.10.2"
7167
httpx = "^0.24"
68+
ruff = "^0.3.4"
69+
70+
[tool.ruff]
71+
line-length = 120
72+
73+
[tool.ruff.format]
74+
quote-style = "double"
75+
indent-style = "tab"
76+
docstring-code-format = true
77+
docstring-code-line-length = 120
78+
79+
[tool.ruff.lint]
80+
ignore = ["E402", "F403", "F405"]
81+
82+
[tool.ruff.lint.isort]
83+
case-sensitive = true
7284

7385

7486
[build-system]
@@ -83,10 +95,12 @@ filterwarnings = 'error'
8395
timeout = 30
8496
xfail_strict = true
8597
# min, max, mean, stddev, median, iqr, outliers, ops, rounds, iterations
86-
addopts = [
87-
'--benchmark-columns', 'min,mean,stddev,outliers,rounds,iterations',
88-
'--benchmark-group-by', 'group',
89-
'--benchmark-warmup', 'on',
90-
'--benchmark-disable', # this is enable by `make benchmark` when you actually want to run benchmarks
91-
]
92-
98+
#addopts = [
99+
# '--benchmark-columns',
100+
# 'min,mean,stddev,outliers,rounds,iterations',
101+
# '--benchmark-group-by',
102+
# 'group',
103+
# '--benchmark-warmup',
104+
# 'on',
105+
# '--benchmark-disable', # this is enable by `make benchmark` when you actually want to run benchmarks
106+
#]

pysqlx_engine/_core/abc.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
from abc import ABC, abstractmethod
2+
3+
from typing_extensions import TypeVar
4+
5+
from .const import PROVIDER
6+
7+
T = TypeVar("T")
8+
9+
10+
class AbstractDatabaseType(ABC):
11+
@abstractmethod
12+
def __init__(self, value: T): ...
13+
14+
@abstractmethod
15+
def convert(self, provider: PROVIDER, field: str = "") -> T: ...

pysqlx_engine/_core/aconn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
ParserSQL,
1111
) # import necessary using _core to not subscribe default parser
1212
from .const import ISOLATION_LEVEL
13-
from .until import check_isolation_level, check_sql_and_parameters, pysqlx_get_error
13+
from .util import check_isolation_level, check_sql_and_parameters, pysqlx_get_error
1414

1515

1616
class PySQLXEngine:

pysqlx_engine/_core/conn.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
from .._core.parser import MyModel # import necessary using _core to not subscribe default parser
44
from .const import ISOLATION_LEVEL
5-
from .until import force_sync
5+
from .util import force_sync
66
from .aconn import PySQLXEngine as _PySQLXEngine
77

88

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import json
2+
from datetime import date, datetime, time
3+
from decimal import Decimal
4+
from uuid import UUID
5+
6+
from .abc import AbstractDatabaseType
7+
from .errors import ParameterInvalidJsonValueError
8+
9+
10+
class PySQLXJsonEnconder(json.JSONEncoder):
11+
def default(self, obj):
12+
if isinstance(obj, bytes):
13+
return obj.hex()
14+
15+
elif isinstance(obj, (UUID, time, date, datetime, Decimal)):
16+
return str(obj)
17+
try:
18+
return super().default(obj)
19+
except TypeError as err:
20+
raise ParameterInvalidJsonValueError(
21+
typ_from=type(obj),
22+
typ_to="json",
23+
details=str(err),
24+
)

pysqlx_engine/_core/param.py

Lines changed: 19 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -6,14 +6,12 @@
66
from typing import Any, Callable, Dict, List, Tuple, Type, Union
77
from uuid import UUID
88

9+
from pysqlx_engine._core.json_econder import PySQLXJsonEnconder
10+
911
from .const import PROVIDER
10-
from .errors import (
11-
ParameterInvalidJsonValueError,
12-
ParameterInvalidProviderError,
13-
ParameterInvalidValueError,
14-
)
12+
from .errors import ParameterInvalidProviderError, ParameterInvalidValueError
1513

16-
value_type = Union[
14+
SupportedValueType = Union[
1715
bool,
1816
str,
1917
int,
@@ -44,6 +42,12 @@ def try_str(provider: PROVIDER, value: str, _f: str = "") -> str:
4442
return f"'{value}'"
4543

4644

45+
@lru_cache(maxsize=None)
46+
def try_nstr(provider: PROVIDER, value: str, _f: str = "") -> str:
47+
value = value.replace("'", "''")
48+
return f"'{value}'" if provider != "sqlserver" else f"N'{value}'"
49+
50+
4751
@lru_cache(maxsize=None)
4852
def try_int(_p: PROVIDER, value: int, _f: str = "") -> int:
4953
return value
@@ -54,6 +58,11 @@ def try_json(provider: PROVIDER, value: Union[Dict[str, Any], List[Dict[str, Any
5458
return f"'{data}'"
5559

5660

61+
def try_njson(provider: PROVIDER, value: Union[Dict[str, Any], List[Dict[str, Any]]], _f: str = "") -> str:
62+
data = json.dumps(value, ensure_ascii=False, cls=PySQLXJsonEnconder).replace("'", "''")
63+
return f"'{data}'" if provider != "sqlserver" else f"N'{data}'"
64+
65+
5766
@lru_cache(maxsize=None)
5867
def try_uuid(_a: PROVIDER, value: UUID, _f: str = "") -> str:
5968
return f"'{value}'"
@@ -172,22 +181,10 @@ def try_tuple(provider: PROVIDER, values: Tuple[Any], field: str = "") -> str:
172181
return "'{}'"
173182

174183

175-
class PySQLXJsonEnconder(json.JSONEncoder):
176-
def default(self, obj):
177-
if isinstance(obj, bytes):
178-
return obj.hex()
179-
180-
elif isinstance(obj, (UUID, time, date, datetime, Decimal)):
181-
return str(obj)
182-
183-
try:
184-
return super().default(obj)
185-
except TypeError as err:
186-
raise ParameterInvalidJsonValueError(
187-
typ_from=type(obj),
188-
typ_to="json",
189-
details=str(err),
190-
)
184+
@lru_cache(maxsize=None)
185+
def try_ntuple(provider: PROVIDER, values: Tuple[Any], field: str = "") -> str:
186+
_v = try_tuple(provider, values, field)
187+
return _v if provider != "sqlserver" else f"N{_v}"
191188

192189

193190
def get_method(typ: Type) -> Callable:
@@ -208,27 +205,3 @@ def get_method(typ: Type) -> Callable:
208205
}
209206

210207
return METHODS.get(typ)
211-
212-
213-
def convert(provider: PROVIDER, value: value_type, field: str = "") -> Union[str, int, float]:
214-
if value is None:
215-
return "NULL"
216-
217-
elif isinstance(value, Enum):
218-
return try_enum(provider, value, field)
219-
220-
elif isinstance(value, tuple) and len(value) > 0 and isinstance(value[0], Enum):
221-
return try_tuple_enum(provider, value, field)
222-
223-
typ_ = type(value)
224-
method = get_method(typ=typ_)
225-
if method is None:
226-
raise ParameterInvalidValueError(
227-
field=field,
228-
provider=provider,
229-
typ_from=typ_,
230-
typ_to="str|int|float|etc",
231-
details="invalid type, the value is not a allowed type.",
232-
)
233-
234-
return method(provider, value, field)
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
from enum import Enum
2+
from typing import Union
3+
4+
from .abc import AbstractDatabaseType
5+
from .const import PROVIDER
6+
from .errors import ParameterInvalidValueError
7+
from .param import SupportedValueType, get_method, try_enum, try_tuple_enum
8+
9+
10+
def convert(provider: PROVIDER, value: SupportedValueType, field: str = "") -> Union[str, int, float]:
11+
if value is None:
12+
return "NULL"
13+
14+
elif isinstance(value, AbstractDatabaseType):
15+
return value.convert(provider=provider, field=field)
16+
17+
elif isinstance(value, Enum):
18+
return try_enum(provider, value, field)
19+
20+
elif isinstance(value, tuple) and len(value) > 0 and isinstance(value[0], Enum):
21+
return try_tuple_enum(provider, value, field)
22+
23+
typ_ = type(value)
24+
method = get_method(typ=typ_)
25+
if method is None:
26+
raise ParameterInvalidValueError(
27+
field=field,
28+
provider=provider,
29+
typ_from=typ_,
30+
typ_to="str|int|float|etc",
31+
details="invalid type, the value is not a allowed type.",
32+
)
33+
34+
return method(provider, value, field)

0 commit comments

Comments
 (0)