dclassql 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.
@@ -0,0 +1,164 @@
1
+ Metadata-Version: 2.3
2
+ Name: dclassql
3
+ Version: 0.1.0
4
+ Summary: Add your description here
5
+ Author: myuanz
6
+ Author-email: myuanz <provefars@gmail.com>
7
+ Requires-Dist: pypika>=0.48.9
8
+ Requires-Python: >=3.12
9
+ Description-Content-Type: text/markdown
10
+
11
+ # DataclassQL
12
+
13
+ DataclassQL 是一个基于 **平凡 dataclass 定义** 的 ORM 生成器, 可生成类型提示完整精巧的数据库客户端.
14
+
15
+ 模型文件保持干净、直观, 无需起手加一堆导入, 也没有 `mapped_column()`、`Annotation` 或额外的基类继承, 只需要 `dataclass`
16
+
17
+ ---
18
+
19
+ ## 设计目标
20
+
21
+ * **最小语法负担**: 模型定义就是合法平凡的 Python dataclass, Python 即 DSL
22
+ * **常用路径简洁**: 常用的定义只需要写少量的代码
23
+ * **静态类型安全**: 模型定义和生成代码全都类型安全. 本库作为 [prisma client python](https://prisma-client-py.readthedocs.io/en/stable/) 的精神继承者, 致力于完成如下体验:
24
+
25
+ ![](https://prisma-client-py.readthedocs.io/en/stable/showcase.gif)
26
+
27
+ ---
28
+
29
+ ## 示例
30
+
31
+ ```python
32
+ from dataclasses import dataclass
33
+ from datetime import datetime
34
+
35
+ @dataclass
36
+ class User:
37
+ id: int
38
+ name: str
39
+ email: str
40
+ last_login: datetime
41
+
42
+ def index(self):
43
+ yield self.name
44
+ yield self.last_login
45
+
46
+ def unique_index(self):
47
+ yield self.name, self.email
48
+ ```
49
+
50
+ 写出如下代码时:
51
+
52
+ ```python
53
+ from dclassql import client
54
+
55
+ client.user.insert({
56
+ "name": "Alice",
57
+ "email": "test@example.com",
58
+ })
59
+ ```
60
+
61
+ 将在类型空间得到报错:
62
+
63
+ ```
64
+ error: Argument of type "dict[str, str]" cannot be assigned to parameter "data" of type "UserInsertDict" in function "insert"
65
+ "last_login" is required in "UserInsertDict" (reportArgumentType)
66
+ ```
67
+
68
+
69
+ ## 安装
70
+
71
+ ```
72
+ uv add dclassql
73
+ ```
74
+
75
+ ## 当前状态
76
+
77
+ DataclassQL 仍在早期开发阶段, 已完成代码生成和 SQLite 支持, 后续将扩展更多数据库与查询功能.
78
+
79
+ ## 一份更长的例子
80
+
81
+ ```python
82
+ from dataclasses import dataclass
83
+ from datetime import datetime
84
+
85
+ __datasource__ = {
86
+ "provider": "sqlite",
87
+ "url": "sqlite:///test.db",
88
+ }
89
+
90
+
91
+ @dataclass
92
+ class Address:
93
+ id: int
94
+ location: str
95
+ user_id: int
96
+ user: 'User'
97
+
98
+ def foreign_key(self):
99
+ yield self.user.id == self.user_id, User.addresses
100
+
101
+
102
+ @dataclass
103
+ class BirthDay:
104
+ user_id: int
105
+ user: 'User'
106
+ date: datetime
107
+
108
+ def primary_key(self):
109
+ return self.user_id
110
+
111
+ def foreign_key(self):
112
+ yield self.user.id == self.user_id, User.birthday
113
+
114
+
115
+ @dataclass
116
+ class Book:
117
+ id: int
118
+ name: str
119
+ users: list['UserBook']
120
+
121
+ def index(self):
122
+ return self.name
123
+
124
+
125
+ @dataclass
126
+ class UserBook:
127
+ user_id: int
128
+ book_id: int
129
+ user: 'User'
130
+ book: Book
131
+ created_at: datetime
132
+
133
+ def primary_key(self):
134
+ return (self.user_id, self.book_id)
135
+
136
+ def index(self):
137
+ yield self.created_at
138
+
139
+ def foreign_key(self):
140
+ yield self.user.id == self.user_id, User.books
141
+ yield self.book.id == self.book_id, Book.users
142
+
143
+
144
+ @dataclass
145
+ class User:
146
+ id: int | None
147
+ name: str
148
+ email: str
149
+ last_login: datetime
150
+ birthday: BirthDay | None
151
+ addresses: list[Address]
152
+ books: list[UserBook]
153
+
154
+ def index(self):
155
+ yield self.name
156
+ yield self.name, self.email
157
+ yield self.last_login
158
+
159
+ def unique_index(self):
160
+ yield self.name, self.email
161
+
162
+ ```
163
+
164
+ 生成的代码请见: https://github.com/myuanz/dataclassql/blob/master/tests/results.py
@@ -0,0 +1,154 @@
1
+ # DataclassQL
2
+
3
+ DataclassQL 是一个基于 **平凡 dataclass 定义** 的 ORM 生成器, 可生成类型提示完整精巧的数据库客户端.
4
+
5
+ 模型文件保持干净、直观, 无需起手加一堆导入, 也没有 `mapped_column()`、`Annotation` 或额外的基类继承, 只需要 `dataclass`
6
+
7
+ ---
8
+
9
+ ## 设计目标
10
+
11
+ * **最小语法负担**: 模型定义就是合法平凡的 Python dataclass, Python 即 DSL
12
+ * **常用路径简洁**: 常用的定义只需要写少量的代码
13
+ * **静态类型安全**: 模型定义和生成代码全都类型安全. 本库作为 [prisma client python](https://prisma-client-py.readthedocs.io/en/stable/) 的精神继承者, 致力于完成如下体验:
14
+
15
+ ![](https://prisma-client-py.readthedocs.io/en/stable/showcase.gif)
16
+
17
+ ---
18
+
19
+ ## 示例
20
+
21
+ ```python
22
+ from dataclasses import dataclass
23
+ from datetime import datetime
24
+
25
+ @dataclass
26
+ class User:
27
+ id: int
28
+ name: str
29
+ email: str
30
+ last_login: datetime
31
+
32
+ def index(self):
33
+ yield self.name
34
+ yield self.last_login
35
+
36
+ def unique_index(self):
37
+ yield self.name, self.email
38
+ ```
39
+
40
+ 写出如下代码时:
41
+
42
+ ```python
43
+ from dclassql import client
44
+
45
+ client.user.insert({
46
+ "name": "Alice",
47
+ "email": "test@example.com",
48
+ })
49
+ ```
50
+
51
+ 将在类型空间得到报错:
52
+
53
+ ```
54
+ error: Argument of type "dict[str, str]" cannot be assigned to parameter "data" of type "UserInsertDict" in function "insert"
55
+ "last_login" is required in "UserInsertDict" (reportArgumentType)
56
+ ```
57
+
58
+
59
+ ## 安装
60
+
61
+ ```
62
+ uv add dclassql
63
+ ```
64
+
65
+ ## 当前状态
66
+
67
+ DataclassQL 仍在早期开发阶段, 已完成代码生成和 SQLite 支持, 后续将扩展更多数据库与查询功能.
68
+
69
+ ## 一份更长的例子
70
+
71
+ ```python
72
+ from dataclasses import dataclass
73
+ from datetime import datetime
74
+
75
+ __datasource__ = {
76
+ "provider": "sqlite",
77
+ "url": "sqlite:///test.db",
78
+ }
79
+
80
+
81
+ @dataclass
82
+ class Address:
83
+ id: int
84
+ location: str
85
+ user_id: int
86
+ user: 'User'
87
+
88
+ def foreign_key(self):
89
+ yield self.user.id == self.user_id, User.addresses
90
+
91
+
92
+ @dataclass
93
+ class BirthDay:
94
+ user_id: int
95
+ user: 'User'
96
+ date: datetime
97
+
98
+ def primary_key(self):
99
+ return self.user_id
100
+
101
+ def foreign_key(self):
102
+ yield self.user.id == self.user_id, User.birthday
103
+
104
+
105
+ @dataclass
106
+ class Book:
107
+ id: int
108
+ name: str
109
+ users: list['UserBook']
110
+
111
+ def index(self):
112
+ return self.name
113
+
114
+
115
+ @dataclass
116
+ class UserBook:
117
+ user_id: int
118
+ book_id: int
119
+ user: 'User'
120
+ book: Book
121
+ created_at: datetime
122
+
123
+ def primary_key(self):
124
+ return (self.user_id, self.book_id)
125
+
126
+ def index(self):
127
+ yield self.created_at
128
+
129
+ def foreign_key(self):
130
+ yield self.user.id == self.user_id, User.books
131
+ yield self.book.id == self.book_id, Book.users
132
+
133
+
134
+ @dataclass
135
+ class User:
136
+ id: int | None
137
+ name: str
138
+ email: str
139
+ last_login: datetime
140
+ birthday: BirthDay | None
141
+ addresses: list[Address]
142
+ books: list[UserBook]
143
+
144
+ def index(self):
145
+ yield self.name
146
+ yield self.name, self.email
147
+ yield self.last_login
148
+
149
+ def unique_index(self):
150
+ yield self.name, self.email
151
+
152
+ ```
153
+
154
+ 生成的代码请见: https://github.com/myuanz/dataclassql/blob/master/tests/results.py
@@ -0,0 +1,25 @@
1
+ [project]
2
+ name = "dclassql"
3
+ version = "0.1.0"
4
+ description = "Add your description here"
5
+ readme = "README.md"
6
+ authors = [
7
+ { name = "myuanz", email = "provefars@gmail.com" }
8
+ ]
9
+ requires-python = ">=3.12"
10
+ dependencies = [
11
+ "pypika>=0.48.9",
12
+ ]
13
+
14
+ [project.scripts]
15
+ "dql" = "dclassql.cli:main"
16
+
17
+ [build-system]
18
+ requires = ["uv_build>=0.8.22,<0.9.0"]
19
+ build-backend = "uv_build"
20
+
21
+ [dependency-groups]
22
+ dev = [
23
+ "pyright>=1.1.407",
24
+ "pytest>=8.4.2",
25
+ ]
@@ -0,0 +1,12 @@
1
+ from .db_pool import BaseDBPool, save_local
2
+ from .push import db_push
3
+ from .unwarp import unwarp, unwarp_or, unwarp_or_raise
4
+
5
+ __all__ = [
6
+ 'db_push',
7
+ 'unwarp',
8
+ 'unwarp_or',
9
+ 'unwarp_or_raise',
10
+ 'BaseDBPool',
11
+ 'save_local',
12
+ ]
@@ -0,0 +1,123 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import importlib.util
5
+ import sys
6
+ from pathlib import Path
7
+ from types import ModuleType
8
+ from typing import Any, Sequence
9
+
10
+ from .codegen import generate_client
11
+ from .push import db_push
12
+ from .runtime.datasource import open_sqlite_connection, resolve_sqlite_path
13
+
14
+
15
+ DEFAULT_MODEL_FILE = "model.py"
16
+
17
+
18
+ def load_module(module_path: Path) -> ModuleType:
19
+ module_path = module_path.resolve()
20
+ if not module_path.exists():
21
+ raise FileNotFoundError(f"Model file '{module_path}' does not exist")
22
+ module_name = module_path.stem
23
+ spec = importlib.util.spec_from_file_location(module_name, module_path)
24
+ if spec is None or spec.loader is None:
25
+ raise ImportError(f"Unable to load module from '{module_path}'")
26
+ module = importlib.util.module_from_spec(spec)
27
+ sys.modules[module_name] = module
28
+ sys.path.insert(0, str(module_path.parent))
29
+ try:
30
+ spec.loader.exec_module(module)
31
+ finally:
32
+ sys.path.pop(0)
33
+ return module
34
+
35
+
36
+ def collect_models(module: ModuleType) -> list[type[Any]]:
37
+ from dataclasses import is_dataclass
38
+
39
+ models: list[type[Any]] = []
40
+ for value in vars(module).values():
41
+ if isinstance(value, type) and is_dataclass(value) and value.__module__ == module.__name__:
42
+ models.append(value)
43
+ if not models:
44
+ raise ValueError("No dataclass models were found in the provided module")
45
+ return models
46
+
47
+
48
+ def push_database(models: Sequence[type[Any]]) -> None:
49
+ from .model_inspector import inspect_models
50
+
51
+ model_infos = inspect_models(models)
52
+ connections: dict[str, Any] = {}
53
+ opened: list[Any] = []
54
+ try:
55
+ for info in model_infos.values():
56
+ config = info.datasource
57
+ key = config.key
58
+ if key in connections:
59
+ continue
60
+ if config.provider != "sqlite":
61
+ raise ValueError(f"Unsupported provider '{config.provider}'")
62
+ connection = open_sqlite_connection(config.url)
63
+ connections[key] = connection
64
+ opened.append(connection)
65
+ db_push(models, connections)
66
+ finally:
67
+ for conn in opened:
68
+ try:
69
+ conn.close()
70
+ except Exception:
71
+ pass
72
+
73
+
74
+ def command_generate(module_path: Path) -> None:
75
+ module = load_module(module_path)
76
+ models = collect_models(module)
77
+ generated = generate_client(models)
78
+ sys.stdout.write(generated.code)
79
+
80
+
81
+ def command_push_db(module_path: Path) -> None:
82
+ module = load_module(module_path)
83
+ models = collect_models(module)
84
+ push_database(models)
85
+
86
+
87
+ def build_parser() -> argparse.ArgumentParser:
88
+ parser = argparse.ArgumentParser(prog="typed-db", description="Typed DB utilities.")
89
+ parser.add_argument(
90
+ "-m",
91
+ "--module",
92
+ type=Path,
93
+ default=Path(DEFAULT_MODEL_FILE),
94
+ help="Path to the model module file (default: model.py)",
95
+ )
96
+ subparsers = parser.add_subparsers(dest="command", required=True)
97
+
98
+ generate_parser = subparsers.add_parser("generate", help="Generate client code for given models")
99
+ generate_parser.set_defaults(handler=lambda args: command_generate(args.module))
100
+
101
+ push_parser = subparsers.add_parser("push-db", help="Apply schema and indexes to configured databases")
102
+ push_parser.set_defaults(handler=lambda args: command_push_db(args.module))
103
+
104
+ return parser
105
+
106
+
107
+ def main(argv: Sequence[str] | None = None) -> int:
108
+ parser = build_parser()
109
+ args = parser.parse_args(argv)
110
+ handler = getattr(args, "handler", None)
111
+ if handler is None:
112
+ parser.print_help()
113
+ return 1
114
+ try:
115
+ handler(args)
116
+ return 0
117
+ except Exception as exc: # pragma: no cover - CLI error reporting
118
+ print(f"Error: {exc}", file=sys.stderr)
119
+ return 1
120
+
121
+
122
+ if __name__ == "__main__": # pragma: no cover
123
+ raise SystemExit(main())