ns-orm 0.0.0__py3-none-any.whl

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,53 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from importlib.machinery import SourceFileLoader
5
+ from importlib.util import module_from_spec, spec_from_loader
6
+ from pathlib import Path
7
+ from typing import Any, Optional
8
+
9
+ from ns_orm.migrations.migration import Migration
10
+
11
+ _MIGRATION_RE = re.compile(r"^\d{14}_.+\.py$")
12
+
13
+
14
+ class MigrationLoader:
15
+ def __init__(self, *, migrations_dir: Path):
16
+ self.migrations_dir = migrations_dir
17
+
18
+ def list_files(self) -> list[Path]:
19
+ if not self.migrations_dir.exists():
20
+ return []
21
+ files = [
22
+ p
23
+ for p in self.migrations_dir.iterdir()
24
+ if p.is_file() and _MIGRATION_RE.match(p.name)
25
+ ]
26
+ return sorted(files, key=lambda p: p.name)
27
+
28
+ def load_all(self) -> list[Migration]:
29
+ migrations: list[Migration] = []
30
+ for p in self.list_files():
31
+ migrations.append(self.load_file(p))
32
+ return migrations
33
+
34
+ def load_file(self, path: Path) -> Migration:
35
+ loader = SourceFileLoader(f"ns_orm_user_migration_{path.stem}", str(path))
36
+ spec = spec_from_loader(loader.name, loader)
37
+ if spec is None:
38
+ raise RuntimeError(f"Failed to load migration: {path}")
39
+ mod = module_from_spec(spec)
40
+ assert spec.loader is not None
41
+ spec.loader.exec_module(mod)
42
+ mig = getattr(mod, "migration", None)
43
+ if not isinstance(mig, Migration):
44
+ raise RuntimeError(
45
+ f"Migration file must define `migration: Migration`: {path}"
46
+ )
47
+ return mig
48
+
49
+ def latest_state(self) -> Optional[dict[str, Any]]:
50
+ migrations = self.load_all()
51
+ if not migrations:
52
+ return None
53
+ return migrations[-1].state
@@ -0,0 +1,14 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ns_orm.migrations.operations import Operation
7
+
8
+
9
+ @dataclass(frozen=True)
10
+ class Migration:
11
+ name: str
12
+ operations: list[Operation]
13
+ state: dict[str, Any]
14
+ dependencies: list[str] = ()
@@ -0,0 +1,93 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from typing import Any
5
+
6
+ from ns_orm.dialects import Dialect
7
+ from ns_orm.exceptions import QueryError
8
+
9
+
10
+ def _fielddef_from_value(value: Any) -> Any:
11
+ if isinstance(value, dict) and "__class__" in value:
12
+ cls_name = value.get("__class__", "")
13
+ kwargs = {k: v for k, v in value.items() if k != "__class__"}
14
+ from ns_orm import fields as f
15
+
16
+ cls = getattr(f, cls_name, None)
17
+ if cls is None:
18
+ return f.Int()
19
+ try:
20
+ return cls(**kwargs)
21
+ except Exception:
22
+ return cls()
23
+ return value
24
+
25
+
26
+ @dataclass(frozen=True)
27
+ class Operation:
28
+ def sql(self, dialect: Dialect) -> list[str]:
29
+ raise NotImplementedError
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class CreateTable(Operation):
34
+ table_name: str
35
+ columns: list[tuple[str, Any]]
36
+ pk_name: str
37
+ fks: list[tuple[str, str, str, str]]
38
+ unique_together: list[tuple[str, ...]] = ()
39
+
40
+ def sql(self, dialect: Dialect) -> list[str]:
41
+ cols = [(name, _fielddef_from_value(fdef)) for name, fdef in self.columns]
42
+ ddl = dialect.ddl_create_table(
43
+ table_name=self.table_name,
44
+ columns=cols,
45
+ pk_name=self.pk_name,
46
+ fks=self.fks,
47
+ uniques=[],
48
+ unique_together=self.unique_together,
49
+ )
50
+ return [ddl]
51
+
52
+
53
+ @dataclass(frozen=True)
54
+ class AddColumn(Operation):
55
+ table_name: str
56
+ column_name: str
57
+ column_def: Any
58
+
59
+ def sql(self, dialect: Dialect) -> list[str]:
60
+ col_def = _fielddef_from_value(self.column_def)
61
+ col = dialect.quote_ident(self.column_name)
62
+ ty = dialect.type_sql(col_def)
63
+ parts = [
64
+ f"ALTER TABLE {dialect.quote_ident(self.table_name)} ADD COLUMN {col} {ty}"
65
+ ]
66
+ nullable = getattr(col_def, "nullable", None)
67
+ if nullable is False:
68
+ parts[0] += " NOT NULL"
69
+ if getattr(col_def, "unique", False):
70
+ parts[0] += " UNIQUE"
71
+ return parts
72
+
73
+
74
+ @dataclass(frozen=True)
75
+ class DropColumn(Operation):
76
+ table_name: str
77
+ column_name: str
78
+
79
+ def sql(self, dialect: Dialect) -> list[str]:
80
+ if dialect.name == "sqlite":
81
+ raise QueryError("SQLite 不支持直接 DROP COLUMN,请手工重建表或扩展迁移器")
82
+ return [
83
+ f"ALTER TABLE {dialect.quote_ident(self.table_name)} "
84
+ f"DROP COLUMN {dialect.quote_ident(self.column_name)}"
85
+ ]
86
+
87
+
88
+ @dataclass(frozen=True)
89
+ class DropTable(Operation):
90
+ table_name: str
91
+
92
+ def sql(self, dialect: Dialect) -> list[str]:
93
+ return [f"DROP TABLE IF EXISTS {dialect.quote_ident(self.table_name)}"]
@@ -0,0 +1,42 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import asdict, is_dataclass
4
+ from typing import Any
5
+
6
+ from ns_orm.fields import ForeignKey, ManyToMany
7
+
8
+
9
+ def field_to_dict(field: Any) -> dict[str, Any]:
10
+ if is_dataclass(field):
11
+ data = asdict(field)
12
+ else:
13
+ data = dict(getattr(field, "__dict__", {}))
14
+ data["__class__"] = field.__class__.__name__
15
+ return data
16
+
17
+
18
+ def model_state(model: type[Any]) -> dict[str, Any]:
19
+ fields: dict[str, Any] = {}
20
+ fks: dict[str, Any] = {}
21
+ m2m: dict[str, Any] = {}
22
+
23
+ for name, fdef in model._meta.fields.items():
24
+ if isinstance(fdef, ManyToMany):
25
+ m2m[name] = field_to_dict(fdef)
26
+ continue
27
+ if isinstance(fdef, ForeignKey):
28
+ fks[name] = field_to_dict(fdef)
29
+ fields[name] = field_to_dict(fdef)
30
+
31
+ return {
32
+ "table_name": model.table_name(),
33
+ "schema": getattr(model._meta, "schema", None),
34
+ "pk_name": model.pk_name(),
35
+ "fields": fields,
36
+ "fks": fks,
37
+ "m2m": m2m,
38
+ }
39
+
40
+
41
+ def project_state(models: list[type[Any]]) -> dict[str, Any]:
42
+ return {m.__name__: model_state(m) for m in models}
@@ -0,0 +1,79 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from ns_orm.migrations.migration import Migration
7
+ from ns_orm.migrations.operations import (
8
+ AddColumn,
9
+ CreateTable,
10
+ DropColumn,
11
+ DropTable,
12
+ Operation,
13
+ )
14
+
15
+
16
+ class MigrationWriter:
17
+ def __init__(self, *, migrations_dir: Path):
18
+ self.migrations_dir = migrations_dir
19
+
20
+ def write(self, migration: Migration) -> Path:
21
+ self.migrations_dir.mkdir(parents=True, exist_ok=True)
22
+ path = self.migrations_dir / f"{migration.name}.py"
23
+ content = self._render(migration)
24
+ path.write_text(content, encoding="utf-8")
25
+ return path
26
+
27
+ def _render(self, migration: Migration) -> str:
28
+ ops_src = ",\n ".join(self._render_op(op) for op in migration.operations)
29
+ state_json = json.dumps(
30
+ migration.state, ensure_ascii=False, indent=2, sort_keys=True
31
+ )
32
+ return (
33
+ "from __future__ import annotations\n\n"
34
+ "from ns_orm.migrations.migration import Migration\n"
35
+ "from ns_orm.migrations.operations import (\n"
36
+ " AddColumn,\n"
37
+ " CreateTable,\n"
38
+ " DropColumn,\n"
39
+ " DropTable,\n"
40
+ ")\n\n"
41
+ f"migration = Migration(\n"
42
+ f" name={migration.name!r},\n"
43
+ f" dependencies={list(migration.dependencies)!r},\n"
44
+ f" operations=[\n"
45
+ f" {ops_src}\n"
46
+ f" ],\n"
47
+ f" state={state_json},\n"
48
+ f")\n"
49
+ )
50
+
51
+ def _render_op(self, op: Operation) -> str:
52
+ if isinstance(op, CreateTable):
53
+ return (
54
+ "CreateTable(\n"
55
+ f" table_name={op.table_name!r},\n"
56
+ f" columns={op.columns!r},\n"
57
+ f" pk_name={op.pk_name!r},\n"
58
+ f" fks={op.fks!r},\n"
59
+ f" unique_together={list(op.unique_together)!r},\n"
60
+ " )"
61
+ )
62
+ if isinstance(op, AddColumn):
63
+ return (
64
+ "AddColumn(\n"
65
+ f" table_name={op.table_name!r},\n"
66
+ f" column_name={op.column_name!r},\n"
67
+ f" column_def={op.column_def!r},\n"
68
+ " )"
69
+ )
70
+ if isinstance(op, DropColumn):
71
+ return (
72
+ "DropColumn(\n"
73
+ f" table_name={op.table_name!r},\n"
74
+ f" column_name={op.column_name!r},\n"
75
+ " )"
76
+ )
77
+ if isinstance(op, DropTable):
78
+ return f"DropTable(\n table_name={op.table_name!r},\n )"
79
+ raise TypeError(f"Unsupported operation: {op}")
ns_orm/model.py ADDED
@@ -0,0 +1,151 @@
1
+ from __future__ import annotations
2
+
3
+ import re
4
+ from typing import Any, ClassVar, TypeVar
5
+
6
+ from pydantic import BaseModel
7
+
8
+ from ns_orm.exceptions import ModelDefinitionError
9
+ from ns_orm.fields import FieldDef, ForeignKey, ManyToMany
10
+ from ns_orm.typing import ModelMetaInfo
11
+ from ns_orm.utils import (
12
+ get_class_namespace,
13
+ get_type_hints_with_extras,
14
+ is_classvar,
15
+ resolve_ref,
16
+ unwrap_annotated,
17
+ )
18
+
19
+ TModel = TypeVar("TModel", bound="Model")
20
+
21
+
22
+ def _default_table_name(cls_name: str) -> str:
23
+ s1 = re.sub("(.)([A-Z][a-z]+)", r"\1_\2", cls_name)
24
+ return re.sub("([a-z0-9])([A-Z])", r"\1_\2", s1).lower()
25
+
26
+
27
+ class Model(BaseModel):
28
+ class Config:
29
+ extra = "allow"
30
+ validate_assignment = True
31
+
32
+ _meta: ClassVar[ModelMetaInfo]
33
+ _registry: ClassVar[dict[str, type[Model]]] = {}
34
+ objects: ClassVar[Any]
35
+
36
+ @classmethod
37
+ def table_name(cls) -> str:
38
+ return cls._meta.table_name
39
+
40
+ @classmethod
41
+ def pk_name(cls) -> str:
42
+ return cls._meta.pk_name
43
+
44
+ @classmethod
45
+ def _resolve_model(cls, ref: str) -> type[Model]:
46
+ if ref in cls._registry:
47
+ return cls._registry[ref]
48
+ simple = ref.rsplit(".", 1)[-1]
49
+ if simple in cls._registry:
50
+ return cls._registry[simple]
51
+ raise ModelDefinitionError(f"Unresolved model reference: {ref}")
52
+
53
+ def pk_value(self) -> Any:
54
+ return getattr(self, self.pk_name())
55
+
56
+ def to_db_dict(self) -> dict[str, Any]:
57
+ data = self.dict()
58
+ cols = {k: v for k, v in data.items() if k in self._meta.fields}
59
+ return cols
60
+
61
+ def save(self, db: Any) -> Any:
62
+ from ns_orm.query import QuerySet
63
+
64
+ qs = QuerySet(self.__class__, db=db)
65
+ return qs.save_instance(self)
66
+
67
+ async def asave(self, db: Any) -> Any:
68
+ from ns_orm.query import AsyncQuerySet
69
+
70
+ qs = AsyncQuerySet(self.__class__, db=db)
71
+ return await qs.save_instance(self)
72
+
73
+ def delete(self, db: Any) -> int:
74
+ from ns_orm.query import QuerySet
75
+
76
+ qs = QuerySet(self.__class__, db=db)
77
+ return qs.filter(**{self.pk_name(): self.pk_value()}).delete()
78
+
79
+ async def adelete(self, db: Any) -> int:
80
+ from ns_orm.query import AsyncQuerySet
81
+
82
+ qs = AsyncQuerySet(self.__class__, db=db)
83
+ return await qs.filter(**{self.pk_name(): self.pk_value()}).delete()
84
+
85
+ def __init_subclass__(cls, **kwargs: Any) -> None:
86
+ super().__init_subclass__(**kwargs)
87
+ if cls.__name__ == "Model":
88
+ return
89
+
90
+ hints = get_type_hints_with_extras(cls)
91
+ namespace = get_class_namespace(cls)
92
+
93
+ fields: dict[str, FieldDef] = {}
94
+ relations: dict[str, tuple[str, ForeignKey]] = {}
95
+ m2m: dict[str, ManyToMany] = {}
96
+
97
+ for name, annotation in hints.items():
98
+ if is_classvar(annotation):
99
+ continue
100
+ base, meta = unwrap_annotated(annotation)
101
+ resolved_base = resolve_ref(base, namespace)
102
+ _ = resolved_base
103
+ for m in meta:
104
+ if isinstance(m, ForeignKey):
105
+ fields[name] = m
106
+ rel_name = name[:-3] if name.endswith("_id") else f"{name}_obj"
107
+ relations[rel_name] = (name, m)
108
+ elif isinstance(m, FieldDef):
109
+ fields[name] = m
110
+ elif isinstance(m, ManyToMany):
111
+ m2m[name] = m
112
+
113
+ if not fields:
114
+ raise ModelDefinitionError(
115
+ f"{cls.__name__} has no ORM fields. "
116
+ "Use typing.Annotated[..., FieldDef()]."
117
+ )
118
+
119
+ table_name = getattr(
120
+ getattr(cls, "Meta", object), "table_name", None
121
+ ) or _default_table_name(cls.__name__)
122
+ schema = getattr(getattr(cls, "Meta", object), "schema", None)
123
+ connect_name = getattr(getattr(cls, "Meta", object), "connect_name", None)
124
+
125
+ pk_candidates = [
126
+ n for n, f in fields.items() if getattr(f, "primary_key", False)
127
+ ]
128
+ if pk_candidates:
129
+ pk_name = pk_candidates[0]
130
+ elif "id" in fields:
131
+ pk_name = "id"
132
+ else:
133
+ raise ModelDefinitionError(f"{cls.__name__} missing primary key field")
134
+
135
+ cls._meta = ModelMetaInfo(
136
+ table_name=table_name,
137
+ schema=schema,
138
+ connect_name=connect_name,
139
+ fields=fields,
140
+ pk_name=pk_name,
141
+ table=None,
142
+ metadata=None,
143
+ relations=relations,
144
+ m2m=m2m,
145
+ )
146
+
147
+ cls._registry[cls.__name__] = cls
148
+ cls._registry[f"{cls.__module__}.{cls.__name__}"] = cls
149
+ from ns_orm.manager import Manager
150
+
151
+ cls.objects = Manager(model=cls)