ddsql 0.0.4__tar.gz → 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,22 +1,14 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: ddsql
3
- Version: 0.0.4
3
+ Version: 0.1.0
4
4
  Summary: SQL Query Library
5
- License: MIT
6
- License-File: LICENSE
7
5
  Keywords: python,ddsql
8
6
  Author: davyddd
9
- Requires-Python: >=3.9,<3.15
10
- Classifier: License :: OSI Approved :: MIT License
11
- Classifier: Programming Language :: Python :: 3
12
- Classifier: Programming Language :: Python :: 3.9
13
- Classifier: Programming Language :: Python :: 3.10
14
- Classifier: Programming Language :: Python :: 3.11
15
- Classifier: Programming Language :: Python :: 3.12
16
- Classifier: Programming Language :: Python :: 3.13
17
- Classifier: Programming Language :: Python :: 3.14
18
- Requires-Dist: ddutils (<0.2.0)
19
- Requires-Dist: jinja2 (>=3.0.0,<4.0.0)
7
+ License-Expression: MIT
8
+ License-File: LICENSE
9
+ Requires-Dist: ddutils>=0.2.0,<0.3.0
10
+ Requires-Dist: jinja2>=3.0.0,<4.0.0
11
+ Requires-Python: >=3.11, <3.15
20
12
  Project-URL: Repository, https://github.com/davyddd/ddsql
21
13
  Description-Content-Type: text/markdown
22
14
 
@@ -38,6 +30,21 @@ Install the library using pip:
38
30
  pip install ddsql
39
31
  ```
40
32
 
33
+ ## Development
34
+
35
+ The project runs entirely in Docker. Requires Docker and [Fabric](https://www.fabfile.org/) on the host:
36
+
37
+ ```bash
38
+ fab build # build the dev image
39
+ fab tests # run pytest
40
+ fab linters # run ruff (with --fix), ty and complexipy
41
+ fab shell # IPython inside the container
42
+ fab bash # bash inside the container
43
+ ```
44
+
45
+ This project was generated from [dd-lib-stub](https://github.com/davyddd/dd-lib-stub);
46
+ run `copier update` to pull in template updates.
47
+
41
48
  ## Serializer
42
49
 
43
50
  The serializer converts Python types to their SQL representations.
@@ -177,7 +184,7 @@ which wraps raw database rows into the specified model (called internally by `Ad
177
184
 
178
185
  Required parameters:
179
186
  - **model** – a declarative class (e.g., dataclass) describing the output result structure;
180
- - **text** or **path** – the SQL template source.
187
+ - **text** or **path** – the SQL template source (inline string or path to a file).
181
188
 
182
189
  ### Inline Template (text)
183
190
 
@@ -193,25 +200,24 @@ query = Query(
193
200
 
194
201
  ### File Template (path)
195
202
 
196
- For file-based templates, set the `SQL_TEMPLATES_DIR` environment variable to the directory containing your SQL files:
197
-
198
- ```bash
199
- export SQL_TEMPLATES_DIR=/app/src/templates/sql/
200
- ```
201
-
202
- Then use a relative path:
203
+ Pass the path to the SQL file as `Path`. The file is checked at construction time, so a wrong path fails
204
+ on import rather than on the first query:
203
205
 
204
206
  ```python
207
+ from pathlib import Path
208
+
205
209
  from ddsql.query import Query
206
210
 
211
+ SQL_TEMPLATES_DIR = Path(__file__).parent / 'templates' / 'sql'
207
212
 
208
- # Loads template from /app/src/templates/sql/users/get_by_id.sql
209
213
  query = Query(
210
- model=User,
211
- path='users/get_by_id.sql'
214
+ model=User,
215
+ path=SQL_TEMPLATES_DIR / 'users' / 'get_by_id.sql',
212
216
  )
213
217
  ```
214
218
 
219
+ `{% include %}` and `{% import %}` inside the template resolve relative to the template's directory.
220
+
215
221
  ### Result
216
222
 
217
223
  The result of query execution is a `Result` object that wraps the data into the specified model:
@@ -273,4 +279,4 @@ async def get_users():
273
279
  .execute()
274
280
  )
275
281
  return result.get_list()
276
- ```
282
+ ```
@@ -16,6 +16,21 @@ Install the library using pip:
16
16
  pip install ddsql
17
17
  ```
18
18
 
19
+ ## Development
20
+
21
+ The project runs entirely in Docker. Requires Docker and [Fabric](https://www.fabfile.org/) on the host:
22
+
23
+ ```bash
24
+ fab build # build the dev image
25
+ fab tests # run pytest
26
+ fab linters # run ruff (with --fix), ty and complexipy
27
+ fab shell # IPython inside the container
28
+ fab bash # bash inside the container
29
+ ```
30
+
31
+ This project was generated from [dd-lib-stub](https://github.com/davyddd/dd-lib-stub);
32
+ run `copier update` to pull in template updates.
33
+
19
34
  ## Serializer
20
35
 
21
36
  The serializer converts Python types to their SQL representations.
@@ -155,7 +170,7 @@ which wraps raw database rows into the specified model (called internally by `Ad
155
170
 
156
171
  Required parameters:
157
172
  - **model** – a declarative class (e.g., dataclass) describing the output result structure;
158
- - **text** or **path** – the SQL template source.
173
+ - **text** or **path** – the SQL template source (inline string or path to a file).
159
174
 
160
175
  ### Inline Template (text)
161
176
 
@@ -171,25 +186,24 @@ query = Query(
171
186
 
172
187
  ### File Template (path)
173
188
 
174
- For file-based templates, set the `SQL_TEMPLATES_DIR` environment variable to the directory containing your SQL files:
175
-
176
- ```bash
177
- export SQL_TEMPLATES_DIR=/app/src/templates/sql/
178
- ```
179
-
180
- Then use a relative path:
189
+ Pass the path to the SQL file as `Path`. The file is checked at construction time, so a wrong path fails
190
+ on import rather than on the first query:
181
191
 
182
192
  ```python
193
+ from pathlib import Path
194
+
183
195
  from ddsql.query import Query
184
196
 
197
+ SQL_TEMPLATES_DIR = Path(__file__).parent / 'templates' / 'sql'
185
198
 
186
- # Loads template from /app/src/templates/sql/users/get_by_id.sql
187
199
  query = Query(
188
- model=User,
189
- path='users/get_by_id.sql'
200
+ model=User,
201
+ path=SQL_TEMPLATES_DIR / 'users' / 'get_by_id.sql',
190
202
  )
191
203
  ```
192
204
 
205
+ `{% include %}` and `{% import %}` inside the template resolve relative to the template's directory.
206
+
193
207
  ### Result
194
208
 
195
209
  The result of query execution is a `Result` object that wraps the data into the specified model:
@@ -1,13 +1,15 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC, abstractmethod
4
- from typing import TYPE_CHECKING, Any, Dict, Generic, Optional, Sequence, Type, TypeVar
4
+ from typing import TYPE_CHECKING, Any, Generic, TypeVar, overload
5
5
 
6
6
  from ddutils.annotation_helpers import is_subclass
7
7
 
8
8
  from ddsql.serializers import BaseSerializer
9
9
 
10
10
  if TYPE_CHECKING:
11
+ from collections.abc import Sequence
12
+
11
13
  from ddsql.query import Result
12
14
  from ddsql.sqlbase import SQLBase
13
15
 
@@ -15,7 +17,6 @@ if TYPE_CHECKING:
15
17
  class Adapter(ABC):
16
18
  serializer: BaseSerializer
17
19
 
18
- @classmethod
19
20
  def __init_subclass__(cls, **kwargs):
20
21
  serializer = getattr(cls, 'serializer', None)
21
22
  serializer_class = getattr(serializer, '__class__', None)
@@ -36,20 +37,28 @@ class Adapter(ABC):
36
37
  return self.sql.query.build_result(await self._execute())
37
38
 
38
39
  @abstractmethod
39
- async def _execute(self) -> Sequence[Dict[str, Any]]: # noqa: UP006
40
- ...
40
+ async def _execute(self) -> Sequence[dict[str, Any]]: ...
41
41
 
42
42
 
43
43
  AdapterT = TypeVar('AdapterT', bound=Adapter)
44
44
 
45
45
 
46
46
  class AdapterDescriptor(Generic[AdapterT]):
47
- adapter_class: Type[AdapterT] # noqa: UP006
47
+ adapter_class: type[AdapterT]
48
48
 
49
- def __init__(self, adapter_class: Type[AdapterT]): # noqa: UP006
49
+ def __init__(self, adapter_class: type[AdapterT]):
50
50
  self.adapter_class = adapter_class
51
51
 
52
- def __get__(self, sql: SQLBase, sql_class: Optional[Type[SQLBase]] = None) -> AdapterT: # noqa: UP006, UP007
52
+ @overload
53
+ def __get__(self, sql: None, sql_class: type[SQLBase]) -> AdapterDescriptor[AdapterT]: ...
54
+
55
+ @overload
56
+ def __get__(self, sql: SQLBase, sql_class: type[SQLBase] | None = None) -> AdapterT: ...
57
+
58
+ def __get__(self, sql: SQLBase | None, sql_class: type[SQLBase] | None = None) -> AdapterT | AdapterDescriptor[AdapterT]:
59
+ # accessed on the class, like `SQL.postgres`: return the descriptor itself, as `property` does
60
+ if sql is None:
61
+ return self
53
62
  return self.adapter_class(sql)
54
63
 
55
64
 
@@ -1,55 +1,51 @@
1
- import os
1
+ from collections.abc import Sequence
2
2
  from pathlib import Path
3
- from typing import Any, Dict, Generic, Optional, Sequence, Tuple, Type, TypeVar
3
+ from typing import Any, Generic, TypeVar
4
4
 
5
5
  from jinja2 import Environment, FileSystemLoader, Template
6
6
 
7
- SQL_TEMPLATES_DIR = os.getenv('SQL_TEMPLATES_DIR')
8
-
9
-
10
7
  DataT = TypeVar('DataT')
11
8
 
12
9
 
13
10
  class Result(Generic[DataT]):
14
11
  rows: Sequence[dict[str, Any]]
15
- model: Type[DataT]
12
+ model: type[DataT]
16
13
 
17
- def __init__(self, rows: Sequence[dict[str, Any]], model: Type[DataT]):
14
+ def __init__(self, rows: Sequence[dict[str, Any]], model: type[DataT]):
18
15
  self.rows = rows
19
16
  self.model = model
20
17
 
21
- def get(self) -> Optional[DataT]:
18
+ def get(self) -> DataT | None:
22
19
  if not self.rows:
23
20
  return None
24
21
  return self.model(**self.rows[0])
25
22
 
26
- def get_list(self) -> Tuple[DataT, ...]:
23
+ def get_list(self) -> tuple[DataT, ...]:
27
24
  return tuple(self.model(**row) for row in self.rows)
28
25
 
29
26
 
30
27
  class Query(Generic[DataT]):
31
- model: Type[DataT]
28
+ model: type[DataT]
32
29
  template: Template
33
30
 
34
- def __init__(self, model: Type[DataT], text: Optional[str] = None, path: Optional[str] = None):
31
+ def __init__(self, model: type[DataT], text: str | None = None, path: Path | None = None):
35
32
  self.model = model
36
33
 
37
34
  self.template = self.get_template(text, path)
38
35
 
39
36
  @staticmethod
40
- def get_template(text: Optional[str] = None, path: Optional[str] = None) -> Template:
37
+ def get_template(text: str | None = None, path: Path | None = None) -> Template:
41
38
  if text:
42
- file_system_loader = FileSystemLoader(Path('.'))
39
+ file_system_loader = None
43
40
  method = 'from_string'
44
41
  query = text
45
42
  elif path:
46
- if SQL_TEMPLATES_DIR is None:
47
- raise ValueError(
48
- 'SQL Templates dir is not defined. Make sure the SQL_TEMPLATES_DIR environment variable is set correctly.'
49
- )
50
- file_system_loader = FileSystemLoader(Path(SQL_TEMPLATES_DIR))
43
+ if not path.is_file():
44
+ raise FileNotFoundError(f'SQL template not found: {path.resolve()}')
45
+ # the loader is rooted at the template directory, so `{% include %}` resolves relative to the file
46
+ file_system_loader = FileSystemLoader(path.parent)
51
47
  method = 'get_template'
52
- query = path
48
+ query = path.name
53
49
  else:
54
50
  raise ValueError('One of `text` or `path` must be specified')
55
51
 
@@ -59,7 +55,7 @@ class Query(Generic[DataT]):
59
55
 
60
56
  return getattr(environment, method)(query)
61
57
 
62
- def update_globals(self, envs: Optional[Dict[str, Any]] = None):
58
+ def update_globals(self, envs: dict[str, Any] | None = None):
63
59
  if envs:
64
60
  self.template.environment.globals.update(envs)
65
61
 
@@ -68,7 +64,7 @@ class Query(Generic[DataT]):
68
64
  strings = sql.split('\n')
69
65
  return '\n'.join([string.strip() for string in strings])
70
66
 
71
- async def render_template(self, params: Dict[str, Any], template_functions: Optional[Dict[str, Any]] = None) -> str:
67
+ async def render_template(self, params: dict[str, Any], template_functions: dict[str, Any] | None = None) -> str:
72
68
  self.update_globals(template_functions)
73
69
 
74
70
  rendered_template = await self.template.render_async(**params)
@@ -1,13 +1,13 @@
1
1
  from collections.abc import Callable, Collection, Mapping
2
2
  from datetime import date, datetime
3
3
  from decimal import Decimal
4
- from typing import Any, Dict, Union
4
+ from typing import Any
5
5
  from uuid import UUID
6
6
 
7
7
 
8
8
  class BaseSerializer:
9
9
  @property
10
- def template_functions(self) -> Dict[str, Callable[[Any], str]]:
10
+ def template_functions(self) -> dict[str, Callable[[Any], str]]:
11
11
  return {'serialize_value': self.serialize_value}
12
12
 
13
13
  def serialize_value(self, value: Any) -> str:
@@ -41,7 +41,7 @@ class BaseSerializer:
41
41
  return f'{value}'.lower()
42
42
 
43
43
  @staticmethod
44
- def serialize_number(value: Union[int, float, Decimal]) -> str:
44
+ def serialize_number(value: int | float | Decimal) -> str:
45
45
  return f'{value}'
46
46
 
47
47
  @staticmethod
@@ -1,11 +1,10 @@
1
1
  from datetime import date, datetime
2
- from typing import Dict
3
2
  from uuid import UUID
4
3
 
5
4
  from ddsql.serializers import BaseSerializer
6
5
 
7
6
 
8
- def _build_escape_translation() -> Dict[int, str]:
7
+ def _build_escape_translation() -> dict[int, str]:
9
8
  # https://clickhouse.com/docs/en/sql-reference/syntax#string
10
9
  translation = {
11
10
  ord('\\'): '\\\\',
@@ -1,6 +1,5 @@
1
1
  import re
2
2
  from datetime import date, datetime
3
- from typing import Dict
4
3
  from uuid import UUID
5
4
 
6
5
  from ddsql.serializers import BaseSerializer
@@ -11,7 +10,7 @@ from ddsql.serializers import BaseSerializer
11
10
  _ESCAPE_SYNTAX_REQUIRED = re.compile(r'[\\\x00-\x1F\x7F]')
12
11
 
13
12
 
14
- def _build_escape_translation() -> Dict[int, str]:
13
+ def _build_escape_translation() -> dict[int, str]:
15
14
  # https://www.postgresql.org/docs/current/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS-ESCAPE
16
15
  translation = {
17
16
  ord('\\'): '\\\\',
@@ -1,12 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from abc import ABC
4
- from typing import TYPE_CHECKING, Any, Dict, TypeVar
4
+ from typing import TYPE_CHECKING, Any, TypeVar
5
5
 
6
- from ddutils.annotation_helpers import is_subclass
7
6
  from ddutils.class_helpers import classproperty
8
7
 
9
- from ddsql.adapter import Adapter
8
+ from ddsql.adapter import AdapterDescriptor
10
9
 
11
10
  if TYPE_CHECKING:
12
11
  from ddsql.query import Query
@@ -29,22 +28,17 @@ class SQLBase(ABC):
29
28
  """
30
29
 
31
30
  query: Query
32
- params: Dict[str, Any]
31
+ params: dict[str, Any]
33
32
 
34
- @classmethod
35
33
  def __init_subclass__(cls, **kwargs):
36
34
  if not cls.has_adapters:
37
35
  raise NotImplementedError('Subclasses must define at least one adapter')
38
36
 
39
37
  @classproperty
40
- def has_adapters(cls) -> bool:
41
- for field in cls.__annotations__:
42
- adapter = getattr(cls, field, None)
43
- adapter_class = getattr(adapter, '__class__', None)
44
- if adapter_class and is_subclass(adapter_class, Adapter):
45
- return True
46
-
47
- return False
38
+ def has_adapters(cls: type[SQLBase]) -> bool:
39
+ # Descriptors are looked up in the class dictionaries along the MRO, so inherited adapters count
40
+ # and no adapter gets instantiated just for the check
41
+ return any(isinstance(value, AdapterDescriptor) for klass in cls.__mro__ for value in vars(klass).values())
48
42
 
49
43
  def __init__(self, query: Query) -> None:
50
44
  self.query = query
@@ -0,0 +1,45 @@
1
+ [project]
2
+ name = "ddsql"
3
+ description = "SQL Query Library"
4
+ version = "0.1.0"
5
+ license = "MIT"
6
+ license-files = ["LICENSE"]
7
+ readme = "README.md"
8
+ requires-python = ">=3.11,<3.15"
9
+ keywords = [
10
+ "python",
11
+ "ddsql",
12
+ ]
13
+ dependencies = [
14
+ "ddutils>=0.2.0,<0.3.0",
15
+ "jinja2>=3.0.0,<4.0.0",
16
+ ]
17
+
18
+ [[project.authors]]
19
+ name = "davyddd"
20
+
21
+ [project.urls]
22
+ Repository = "https://github.com/davyddd/ddsql"
23
+
24
+ [dependency-groups]
25
+ dev = [
26
+ "ipdb==0.13.13",
27
+ "ipython==9.16.1",
28
+ ]
29
+ test = [
30
+ "pytest==9.0.3",
31
+ "pytest-cov==5.0.0",
32
+ "parameterized==0.9.0",
33
+ ]
34
+ linter = [
35
+ "ruff==0.15.10",
36
+ "ty==0.0.15",
37
+ "complexipy==8.0.1",
38
+ ]
39
+
40
+ [tool.uv.build-backend]
41
+ module-root = ""
42
+
43
+ [build-system]
44
+ requires = ["uv_build>=0.12.0,<0.13.0"]
45
+ build-backend = "uv_build"
@@ -0,0 +1,40 @@
1
+ [project]
2
+ name = "ddsql"
3
+ description = "SQL Query Library"
4
+ version = "0.1.0"
5
+ authors = [{ name = "davyddd" }]
6
+ license = "MIT"
7
+ license-files = ["LICENSE"]
8
+ readme = "README.md"
9
+ requires-python = ">=3.11,<3.15"
10
+ keywords = ["python", "ddsql"]
11
+ dependencies = [
12
+ "ddutils>=0.2.0,<0.3.0",
13
+ "jinja2>=3.0.0,<4.0.0",
14
+ ]
15
+
16
+ [project.urls]
17
+ Repository = "https://github.com/davyddd/ddsql"
18
+
19
+ [dependency-groups]
20
+ dev = [
21
+ "ipdb==0.13.13",
22
+ "ipython==9.16.1",
23
+ ]
24
+ test = [
25
+ "pytest==9.0.3",
26
+ "pytest-cov==5.0.0",
27
+ "parameterized==0.9.0",
28
+ ]
29
+ linter = [
30
+ "ruff==0.15.10",
31
+ "ty==0.0.15",
32
+ "complexipy==8.0.1",
33
+ ]
34
+
35
+ [tool.uv.build-backend]
36
+ module-root = ""
37
+
38
+ [build-system]
39
+ requires = ["uv_build>=0.12.0,<0.13.0"]
40
+ build-backend = "uv_build"
@@ -1,33 +0,0 @@
1
- [tool.poetry]
2
- name = "ddsql"
3
- description = "SQL Query Library"
4
- version = "0.0.4"
5
- authors = ["davyddd"]
6
- license = "MIT"
7
- repository = "https://github.com/davyddd/ddsql"
8
- readme = "README.md"
9
- keywords = ["python", "ddsql"]
10
- packages = [{include = "ddsql"}]
11
-
12
- [tool.poetry.dependencies]
13
- python = ">=3.9,<3.15"
14
- ddutils = "<0.2.0"
15
- jinja2 = ">=3.0.0,<4.0.0"
16
-
17
- [tool.poetry.group.dev.dependencies]
18
- ipdb = "0.13.9"
19
- ipython = "8.12.3"
20
-
21
- [tool.poetry.group.test.dependencies]
22
- pytest = "8.1.1"
23
- pytest-cov = "5.0.0"
24
- parameterized = "0.9.0"
25
- typing-extensions = "^4.12.2"
26
-
27
- [tool.poetry.group.linter.dependencies]
28
- mypy = "1.18.2"
29
- ruff = "0.14.4"
30
-
31
- [build-system]
32
- requires = ["poetry-core>=1.0.0"]
33
- build-backend = "poetry.core.masonry.api"
File without changes
File without changes
File without changes