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.
- dclassql-0.1.0/PKG-INFO +164 -0
- dclassql-0.1.0/README.md +154 -0
- dclassql-0.1.0/pyproject.toml +25 -0
- dclassql-0.1.0/src/dclassql/__init__.py +12 -0
- dclassql-0.1.0/src/dclassql/cli.py +123 -0
- dclassql-0.1.0/src/dclassql/codegen.py +508 -0
- dclassql-0.1.0/src/dclassql/db_pool.py +76 -0
- dclassql-0.1.0/src/dclassql/model_inspector.py +383 -0
- dclassql-0.1.0/src/dclassql/push/__init__.py +60 -0
- dclassql-0.1.0/src/dclassql/push/base.py +417 -0
- dclassql-0.1.0/src/dclassql/push/sqlite.py +195 -0
- dclassql-0.1.0/src/dclassql/runtime/backends.py +669 -0
- dclassql-0.1.0/src/dclassql/runtime/datasource.py +39 -0
- dclassql-0.1.0/src/dclassql/table_spec.py +142 -0
- dclassql-0.1.0/src/dclassql/unwarp.py +12 -0
dclassql-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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
|
dclassql-0.1.0/README.md
ADDED
|
@@ -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
|
+

|
|
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,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())
|