dclassql 0.1.2__tar.gz → 0.1.3__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.
Files changed (34) hide show
  1. {dclassql-0.1.2 → dclassql-0.1.3}/PKG-INFO +1 -1
  2. {dclassql-0.1.2 → dclassql-0.1.3}/pyproject.toml +1 -1
  3. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/__init__.py +2 -0
  4. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/client.py +22 -13
  5. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/codegen.py +2 -0
  6. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/runtime/backends/base.py +15 -20
  7. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/runtime/backends/protocols.py +11 -0
  8. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/runtime/backends/sqlite.py +24 -10
  9. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/templates/client_module.py.jinja +0 -2
  10. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/templates/partials/imports.jinja +1 -0
  11. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/templates/partials/model_section.jinja +4 -1
  12. dclassql-0.1.2/src/dclassql/templates/partials/datasource_config.jinja +0 -11
  13. {dclassql-0.1.2 → dclassql-0.1.3}/README.md +0 -0
  14. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/.gitignore +0 -0
  15. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/cli.py +0 -0
  16. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/db_pool.py +0 -0
  17. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/generated_models/__init__.py +0 -0
  18. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/generated_models/test_models.py +0 -0
  19. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/model_inspector.py +0 -0
  20. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/push/__init__.py +0 -0
  21. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/push/base.py +0 -0
  22. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/push/sqlite.py +0 -0
  23. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/runtime/backends/__init__.py +0 -0
  24. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/runtime/backends/lazy.py +0 -0
  25. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/runtime/backends/metadata.py +0 -0
  26. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/runtime/datasource.py +0 -0
  27. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/runtime/sqlite_adapters.py +0 -0
  28. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/table_spec.py +0 -0
  29. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/templates/__init__.py +0 -0
  30. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/templates/partials/client_class.jinja +0 -0
  31. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/templates/partials/exports.jinja +0 -0
  32. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/templates/partials/macros.jinja +0 -0
  33. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/typing.py +0 -0
  34. {dclassql-0.1.2 → dclassql-0.1.3}/src/dclassql/unwarp.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: dclassql
3
- Version: 0.1.2
3
+ Version: 0.1.3
4
4
  Summary: A type-safe ORM generator for Python, creating fully type-hinted database clients from plain dataclass definitions.
5
5
  Keywords: orm,codegen,sqlite,dataclass,typed
6
6
  Author: myuanz
@@ -1,6 +1,6 @@
1
1
  [project]
2
2
  name = "dclassql"
3
- version = "0.1.2"
3
+ version = "0.1.3"
4
4
  description = "A type-safe ORM generator for Python, creating fully type-hinted database clients from plain dataclass definitions."
5
5
  readme = "README.md"
6
6
  authors = [
@@ -1,3 +1,4 @@
1
+ from .model_inspector import DataSourceConfig
1
2
  from .db_pool import BaseDBPool, save_local
2
3
  from .push import db_push
3
4
  from .runtime.backends.lazy import eager
@@ -24,4 +25,5 @@ __all__ = [
24
25
  'unwarp_or_raise',
25
26
  'BaseDBPool',
26
27
  'save_local',
28
+ 'DataSourceConfig',
27
29
  ]
@@ -1,10 +1,11 @@
1
1
  from __future__ import annotations
2
2
 
3
- from dataclasses import dataclass, field
3
+ from dataclasses import dataclass
4
4
  from types import MappingProxyType
5
5
  from typing import Any, Literal, Mapping, Sequence, NotRequired
6
6
  from typing_extensions import TypedDict
7
7
 
8
+ from dclassql import DataSourceConfig
8
9
  from dclassql.db_pool import BaseDBPool, save_local
9
10
  from dclassql.runtime.backends import BackendProtocol, ColumnSpec, ForeignKeySpec, RelationSpec
10
11
  from dclassql.runtime.datasource import open_sqlite_connection
@@ -12,18 +13,6 @@ from dclassql.runtime.datasource import open_sqlite_connection
12
13
  from datetime import datetime
13
14
  from dclassql.generated_models.test_models import Address, BirthDay, Book, User, UserBook
14
15
 
15
- @dataclass(slots=True)
16
- class DataSourceConfig:
17
- provider: str
18
- url: str | None
19
- name: str | None = None
20
-
21
- @property
22
- def key(self) -> str:
23
- return self.name or self.provider
24
-
25
-
26
-
27
16
  TAddressIncludeCol = Literal['user']
28
17
  TAddressSortableCol = Literal['id', 'location', 'user_id']
29
18
 
@@ -57,6 +46,7 @@ class AddressOrderByDict(TypedDict, total=False, closed=True):
57
46
  class AddressTable:
58
47
  model = Address
59
48
  insert_model = AddressInsert
49
+ table_name: str = 'Address'
60
50
  datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
61
51
  column_specs: tuple[ColumnSpec, ...] = (
62
52
  ColumnSpec(name='id', optional=False, auto_increment=True, has_default=False, has_default_factory=False),
@@ -84,6 +74,9 @@ class AddressTable:
84
74
  def __init__(self, backend: BackendProtocol) -> None:
85
75
  self._backend = backend
86
76
 
77
+ def __str__(self) -> str:
78
+ return self._backend.escape_identifier(self.table_name)
79
+
87
80
  def insert(self, data: AddressInsert | AddressInsertDict) -> Address:
88
81
  return self._backend.insert(self, data)
89
82
 
@@ -133,6 +126,7 @@ class BirthDayOrderByDict(TypedDict, total=False, closed=True):
133
126
  class BirthDayTable:
134
127
  model = BirthDay
135
128
  insert_model = BirthDayInsert
129
+ table_name: str = 'BirthDay'
136
130
  datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
137
131
  column_specs: tuple[ColumnSpec, ...] = (
138
132
  ColumnSpec(name='user_id', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
@@ -159,6 +153,9 @@ class BirthDayTable:
159
153
  def __init__(self, backend: BackendProtocol) -> None:
160
154
  self._backend = backend
161
155
 
156
+ def __str__(self) -> str:
157
+ return self._backend.escape_identifier(self.table_name)
158
+
162
159
  def insert(self, data: BirthDayInsert | BirthDayInsertDict) -> BirthDay:
163
160
  return self._backend.insert(self, data)
164
161
 
@@ -208,6 +205,7 @@ class BookOrderByDict(TypedDict, total=False, closed=True):
208
205
  class BookTable:
209
206
  model = Book
210
207
  insert_model = BookInsert
208
+ table_name: str = 'Book'
211
209
  datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
212
210
  column_specs: tuple[ColumnSpec, ...] = (
213
211
  ColumnSpec(name='id', optional=False, auto_increment=True, has_default=False, has_default_factory=False),
@@ -227,6 +225,9 @@ class BookTable:
227
225
  def __init__(self, backend: BackendProtocol) -> None:
228
226
  self._backend = backend
229
227
 
228
+ def __str__(self) -> str:
229
+ return self._backend.escape_identifier(self.table_name)
230
+
230
231
  def insert(self, data: BookInsert | BookInsertDict) -> Book:
231
232
  return self._backend.insert(self, data)
232
233
 
@@ -286,6 +287,7 @@ class UserOrderByDict(TypedDict, total=False, closed=True):
286
287
  class UserTable:
287
288
  model = User
288
289
  insert_model = UserInsert
290
+ table_name: str = 'User'
289
291
  datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
290
292
  column_specs: tuple[ColumnSpec, ...] = (
291
293
  ColumnSpec(name='id', optional=False, auto_increment=True, has_default=False, has_default_factory=False),
@@ -309,6 +311,9 @@ class UserTable:
309
311
  def __init__(self, backend: BackendProtocol) -> None:
310
312
  self._backend = backend
311
313
 
314
+ def __str__(self) -> str:
315
+ return self._backend.escape_identifier(self.table_name)
316
+
312
317
  def insert(self, data: UserInsert | UserInsertDict) -> User:
313
318
  return self._backend.insert(self, data)
314
319
 
@@ -363,6 +368,7 @@ class UserBookOrderByDict(TypedDict, total=False, closed=True):
363
368
  class UserBookTable:
364
369
  model = UserBook
365
370
  insert_model = UserBookInsert
371
+ table_name: str = 'UserBook'
366
372
  datasource = DataSourceConfig(provider='sqlite', url='sqlite:///test.db', name=None)
367
373
  column_specs: tuple[ColumnSpec, ...] = (
368
374
  ColumnSpec(name='user_id', optional=False, auto_increment=False, has_default=False, has_default_factory=False),
@@ -397,6 +403,9 @@ class UserBookTable:
397
403
  def __init__(self, backend: BackendProtocol) -> None:
398
404
  self._backend = backend
399
405
 
406
+ def __str__(self) -> str:
407
+ return self._backend.escape_identifier(self.table_name)
408
+
400
409
  def insert(self, data: UserBookInsert | UserBookInsertDict) -> UserBook:
401
410
  return self._backend.insert(self, data)
402
411
 
@@ -73,6 +73,7 @@ class RelationRender:
73
73
  class ModelRenderContext:
74
74
  name: str
75
75
  datasource_expr: str
76
+ table_name_literal: str
76
77
  insert_fields: tuple[InsertFieldSpec, ...]
77
78
  typed_dict_fields: tuple[TypedDictFieldSpec, ...]
78
79
  where_fields: tuple[WhereFieldSpec, ...]
@@ -253,6 +254,7 @@ def _build_model_context(
253
254
  return ModelRenderContext(
254
255
  name=name,
255
256
  datasource_expr=datasource_expr,
257
+ table_name_literal=repr(name),
256
258
  insert_fields=tuple(insert_fields),
257
259
  typed_dict_fields=tuple(typed_dict_fields),
258
260
  where_fields=tuple(where_fields),
@@ -49,17 +49,9 @@ class BackendBase(BackendProtocol, ABC):
49
49
  sql = self._render_query(insert_query)
50
50
  returning_columns = [spec.name for spec in table.column_specs]
51
51
  sql_with_returning = self._append_returning(sql, returning_columns)
52
- cursor = self._execute(sql_with_returning, params, auto_commit=False)
53
- connection = getattr(cursor, "connection", None)
54
- row: Any | None = None
55
- try:
56
- row = cursor.fetchone()
57
- if row is None:
58
- raise RuntimeError("Inserted row not returned by backend")
59
- if connection is not None:
60
- connection.commit()
61
- finally:
62
- cursor.close()
52
+
53
+ row = self.query_raw(sql_with_returning, params, auto_commit=True)[0]
54
+
63
55
  result = self._row_to_model(table, row, include_map={})
64
56
  self._invalidate_backrefs(table, result)
65
57
  return result
@@ -116,7 +108,7 @@ class BackendBase(BackendProtocol, ABC):
116
108
  select_query = select_query.limit(take)
117
109
 
118
110
  sql = self._render_query(select_query)
119
- rows = self._fetch_all(sql, params)
111
+ rows = self.query_raw(sql, params)
120
112
  include_map = include or {}
121
113
  return [self._row_to_model(table, row, include_map) for row in rows]
122
114
 
@@ -205,6 +197,17 @@ class BackendBase(BackendProtocol, ABC):
205
197
  raise RuntimeError("Inserted row could not be reloaded")
206
198
  return results[0]
207
199
 
200
+ def query_raw(self, sql: str, params: Sequence[object] | None = None, auto_commit: bool = False) -> Sequence[object]:
201
+ raise NotImplementedError
202
+
203
+ def execute_raw(self, sql: str, params: Sequence[object] | None = None, auto_commit: bool = True) -> int:
204
+ raise NotImplementedError
205
+
206
+ def escape_identifier(self, name: str) -> str:
207
+ if self.quote_char:
208
+ return format_quotes(name, self.quote_char)
209
+ raise ValueError("Backend does not support identifier quoting without a quote character set")
210
+
208
211
  def _invalidate_backrefs(
209
212
  self,
210
213
  table: TableProtocol[ModelT, InsertT, WhereT, IncludeT, OrderByT],
@@ -313,11 +316,3 @@ class BackendBase(BackendProtocol, ABC):
313
316
  else:
314
317
  column_sql = ", ".join(columns)
315
318
  return f"{trimmed} RETURNING {column_sql};"
316
-
317
- @abstractmethod
318
- def _fetch_all(self, sql: str, params: Sequence[Any]) -> Sequence[Any]:
319
- raise NotImplementedError
320
-
321
- @abstractmethod
322
- def _execute(self, sql: str, params: Sequence[Any], *, auto_commit: bool = True) -> Any:
323
- raise NotImplementedError
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  import sqlite3
4
4
  from typing import Callable, Literal, Mapping, Protocol, Sequence, runtime_checkable
5
5
 
6
+ from dclassql.model_inspector import DataSourceConfig
6
7
  from dclassql.typing import IncludeT, InsertT, ModelT, OrderByT, WhereT
7
8
 
8
9
  from .metadata import ColumnSpec, ForeignKeySpec, RelationSpec
@@ -21,9 +22,13 @@ class TableProtocol[
21
22
 
22
23
  model: type[ModelT]
23
24
  insert_model: type[InsertT]
25
+ table_name: str
26
+ datasource: DataSourceConfig
24
27
  column_specs: tuple[ColumnSpec, ...]
25
28
  column_specs_by_name: Mapping[str, ColumnSpec]
26
29
  primary_key: tuple[str, ...]
30
+ indexes: tuple[tuple[str, ...], ...]
31
+ unique_indexes: tuple[tuple[str, ...], ...]
27
32
  foreign_keys: tuple[ForeignKeySpec, ...]
28
33
  relations: tuple[RelationSpec[BackendProtocol], ...]
29
34
 
@@ -64,3 +69,9 @@ class BackendProtocol(Protocol):
64
69
  order_by: OrderByT | None = None,
65
70
  skip: int | None = None,
66
71
  ) -> ModelT | None: ...
72
+
73
+ def query_raw(self, sql: str, params: Sequence[object] | None = None, auto_commit: bool = False) -> Sequence[object]: ...
74
+
75
+ def execute_raw(self, sql: str, params: Sequence[object] | None = None, auto_commit: bool = True) -> int: ...
76
+
77
+ def escape_identifier(self, name: str) -> str: ...
@@ -83,16 +83,6 @@ class SQLiteBackend(BackendBase):
83
83
  results.append(instance)
84
84
  start = end
85
85
  return results
86
- def _fetch_all(self, sql: str, params: Sequence[Any]) -> list[sqlite3.Row]:
87
- cursor = self._execute(sql, params)
88
- return cursor.fetchall()
89
-
90
- def _execute(self, sql: str, params: Sequence[Any], *, auto_commit: bool = True) -> sqlite3.Cursor:
91
- connection = self._acquire_connection()
92
- cursor = connection.execute(sql, tuple(params))
93
- if auto_commit:
94
- connection.commit()
95
- return cursor
96
86
 
97
87
  def _acquire_connection(self) -> sqlite3.Connection:
98
88
  if self._factory is None:
@@ -126,3 +116,27 @@ class SQLiteBackend(BackendBase):
126
116
  connection.close()
127
117
  delattr(self._local, "connection")
128
118
  self._clear_identity_map()
119
+
120
+ def query_raw(self, sql: str, params: Sequence[object] | None = None, auto_commit: bool = False) -> Sequence[object]:
121
+ connection = self._acquire_connection()
122
+ cursor = connection.execute(sql, params or ())
123
+
124
+ try:
125
+ rows = cursor.fetchall()
126
+
127
+ if auto_commit:
128
+ connection.commit()
129
+ finally:
130
+ cursor.close()
131
+ return rows
132
+
133
+ def execute_raw(self, sql: str, params: Sequence[object] | None = None, auto_commit: bool = True) -> int:
134
+ connection = self._acquire_connection()
135
+ cursor = connection.execute(sql, params or ())
136
+ try:
137
+ affected = cursor.rowcount
138
+ if auto_commit:
139
+ connection.commit()
140
+ finally:
141
+ cursor.close()
142
+ return affected
@@ -1,8 +1,6 @@
1
1
  {% import 'partials/macros.jinja' as macros %}
2
2
  {% include 'partials/imports.jinja' %}
3
3
 
4
- {% include 'partials/datasource_config.jinja' %}
5
-
6
4
  {% for model in models %}
7
5
  {% include 'partials/model_section.jinja' %}
8
6
  {% endfor %}
@@ -5,6 +5,7 @@ from types import MappingProxyType
5
5
  from typing import Any, Literal, Mapping, Sequence, NotRequired
6
6
  from typing_extensions import TypedDict
7
7
 
8
+ from dclassql import DataSourceConfig
8
9
  from dclassql.db_pool import BaseDBPool, save_local
9
10
  from dclassql.runtime.backends import BackendProtocol, ColumnSpec, ForeignKeySpec, RelationSpec
10
11
  from dclassql.runtime.datasource import open_sqlite_connection
@@ -51,6 +51,7 @@ class {{ order_by_dict_class }}(TypedDict, total=False, closed=True):
51
51
  class {{ table_class }}:
52
52
  model = {{ name }}
53
53
  insert_model = {{ insert_class }}
54
+ table_name: str = {{ model.table_name_literal }}
54
55
  datasource = {{ model.datasource_expr }}
55
56
  {{ macros.column_specs(model.column_specs)|indent(4, True) }}
56
57
  column_specs_by_name: Mapping[str, ColumnSpec] = MappingProxyType({spec.name: spec for spec in column_specs})
@@ -63,6 +64,9 @@ class {{ table_class }}:
63
64
  def __init__(self, backend: {{ backend_signature }}) -> None:
64
65
  self._backend = backend
65
66
 
67
+ def __str__(self) -> str:
68
+ return self._backend.escape_identifier(self.table_name)
69
+
66
70
  def insert(self, data: {{ insert_class }} | {{ insert_dict_class }}) -> {{ name }}:
67
71
  return self._backend.insert(self, data)
68
72
 
@@ -82,4 +86,3 @@ class {{ table_class }}:
82
86
  where=where, include=include, order_by=order_by,
83
87
  skip=skip
84
88
  )
85
-
@@ -1,11 +0,0 @@
1
- @dataclass(slots=True)
2
- class DataSourceConfig:
3
- provider: str
4
- url: str | None
5
- name: str | None = None
6
-
7
- @property
8
- def key(self) -> str:
9
- return self.name or self.provider
10
-
11
-
File without changes
File without changes