sera-2 1.1.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.
sera/models/_module.py ADDED
@@ -0,0 +1,140 @@
1
+ from __future__ import annotations
2
+
3
+ import datetime
4
+ from dataclasses import dataclass
5
+ from enum import Enum
6
+ from pathlib import Path
7
+ from typing import Sequence
8
+
9
+ import black
10
+ import black.mode
11
+ from codegen.models import Program
12
+ from loguru import logger
13
+
14
+
15
+ class Language(str, Enum):
16
+ Python = "python"
17
+ Typescript = "typescript"
18
+
19
+
20
+ @dataclass
21
+ class Package:
22
+ """Represent an target generated module in the application"""
23
+
24
+ app: App
25
+ path: str
26
+ dir: Path
27
+ language: Language
28
+
29
+ def ensure_exists(self):
30
+ """Ensure the module exists"""
31
+ self.dir.mkdir(parents=True, exist_ok=True)
32
+ if self.language == Language.Python:
33
+ if not (self.dir / "__init__.py").exists():
34
+ (self.dir / "__init__.py").touch()
35
+
36
+ def pkg(self, name: str) -> Package:
37
+ """Create a package in this package"""
38
+ return Package(self.app, f"{self.path}.{name}", self.dir / name, self.language)
39
+
40
+ def module(self, name: str) -> Module:
41
+ """Create a module in this package"""
42
+ return Module(self, name, self.language)
43
+
44
+
45
+ @dataclass
46
+ class Module:
47
+ """Represent a module in a package"""
48
+
49
+ package: Package
50
+ name: str
51
+ language: Language
52
+
53
+ @property
54
+ def path(self) -> str:
55
+ return f"{self.package.path}.{self.name}"
56
+
57
+ def write(self, program: Program):
58
+ """Write the module to disk"""
59
+ self.package.ensure_exists()
60
+ if self.language == Language.Python:
61
+ try:
62
+ code = black.format_str(
63
+ program.root.to_python(),
64
+ mode=black.Mode(
65
+ target_versions={black.mode.TargetVersion.PY312},
66
+ line_length=120,
67
+ ),
68
+ )
69
+ except:
70
+ logger.error("Error writing module {}", self.path)
71
+ print(">>> Program")
72
+ print(program.root.to_python())
73
+ print("<<<")
74
+ raise
75
+ else:
76
+ assert self.language == Language.Typescript
77
+ raise NotImplementedError()
78
+
79
+ copyright_statement = f"# Generated automatically at {datetime.datetime.now().strftime('%Y-%m-%d %H:%M:%S')}. All rights reserved.\n\n"
80
+ (self.package.dir / f"{self.name}.py").write_text(copyright_statement + code)
81
+
82
+ def exists(self) -> bool:
83
+ """Check if the module exists"""
84
+ return (self.package.dir / f"{self.name}.py").exists()
85
+
86
+
87
+ @dataclass
88
+ class AppModels(Package):
89
+ data: Package
90
+ db: Package
91
+
92
+ @staticmethod
93
+ def from_pkg(pkg: Package) -> AppModels:
94
+ """Create an AppModels from a package"""
95
+ return AppModels(
96
+ pkg.app,
97
+ path=pkg.path,
98
+ dir=pkg.dir,
99
+ language=pkg.language,
100
+ data=pkg.pkg("data"),
101
+ db=pkg.pkg("db"),
102
+ )
103
+
104
+
105
+ @dataclass
106
+ class App:
107
+ """Represent the generated application"""
108
+
109
+ # top-level package of the application
110
+ root: Package
111
+
112
+ # application configuration
113
+ config: Module
114
+
115
+ # models of the application
116
+ models: AppModels
117
+
118
+ # services of the application, which encode the business logic
119
+ services: Package
120
+
121
+ # API of the application
122
+ api: Package
123
+
124
+ schema_files: Sequence[Path]
125
+
126
+ language: Language
127
+
128
+ def __init__(
129
+ self, name: str, dir: Path, schema_files: Sequence[Path], language: Language
130
+ ):
131
+ self.root = Package(self, name, dir, language)
132
+
133
+ self.config = self.root.module("config")
134
+ self.models = AppModels.from_pkg(self.root.pkg("models"))
135
+ self.services = self.root.pkg("services")
136
+ self.api = self.root.pkg("api")
137
+
138
+ self.schema_files = schema_files
139
+
140
+ self.language = language
@@ -0,0 +1,38 @@
1
+ from __future__ import annotations
2
+
3
+
4
+ class MultiLingualString(str):
5
+ lang: str
6
+ lang2value: dict[str, str]
7
+
8
+ def __new__(cls, lang2value: dict[str, str], lang):
9
+ object = str.__new__(cls, lang2value[lang])
10
+ object.lang = lang
11
+ object.lang2value = lang2value
12
+ return object
13
+
14
+ def as_lang(self, lang: str) -> str:
15
+ return self.lang2value[lang]
16
+
17
+ def as_lang_default(self, lang: str, default: str) -> str:
18
+ return self.lang2value.get(lang, default)
19
+
20
+ def has_lang(self, lang: str) -> bool:
21
+ return lang in self.lang2value
22
+
23
+ @staticmethod
24
+ def en(label: str):
25
+ return MultiLingualString(lang2value={"en": label}, lang="en")
26
+
27
+ @staticmethod
28
+ def from_dict(obj: dict):
29
+ return MultiLingualString(obj["lang2value"], obj["lang"])
30
+
31
+ def to_dict(self):
32
+ return {"lang2value": self.lang2value, "lang": self.lang}
33
+
34
+ def to_tuple(self):
35
+ return self.lang2value, self.lang
36
+
37
+ def __getnewargs__(self) -> tuple[dict[str, str], str]: # type: ignore
38
+ return self.lang2value, self.lang
sera/models/_parse.py ADDED
@@ -0,0 +1,153 @@
1
+ from __future__ import annotations
2
+
3
+ from pathlib import Path
4
+ from typing import Sequence
5
+
6
+ import serde.yaml
7
+ from sera.models._class import Class, ClassDBMapInfo
8
+ from sera.models._datatype import DataType
9
+ from sera.models._multi_lingual_string import MultiLingualString
10
+ from sera.models._property import (
11
+ Cardinality,
12
+ DataPropDBInfo,
13
+ DataProperty,
14
+ ForeignKeyOnDelete,
15
+ ForeignKeyOnUpdate,
16
+ ObjectPropDBInfo,
17
+ ObjectProperty,
18
+ )
19
+ from sera.models._schema import Schema
20
+
21
+
22
+ def parse_schema(files: Sequence[Path | str]) -> Schema:
23
+ schema = Schema(classes={})
24
+
25
+ # parse all classes
26
+ raw_defs = {}
27
+ for file in files:
28
+ for k, v in serde.yaml.deser(file).items():
29
+ cdef = _parse_class_without_prop(schema, k, v)
30
+ assert k not in schema.classes
31
+ schema.classes[k] = cdef
32
+ raw_defs[k] = v
33
+
34
+ # now parse properties of the classes
35
+ for clsname, v in raw_defs.items():
36
+ cdef = schema.classes[clsname]
37
+
38
+ for propname, prop in (v["props"] or {}).items():
39
+ assert propname not in cdef.properties
40
+ cdef.properties[propname] = _parse_property(schema, propname, prop)
41
+
42
+ return schema
43
+
44
+
45
+ def _parse_class_without_prop(schema: Schema, clsname: str, cls: dict) -> Class:
46
+ db = None
47
+ if "db" in cls:
48
+ db = ClassDBMapInfo(table_name=cls["db"]["table_name"])
49
+ return Class(
50
+ name=clsname,
51
+ label=_parse_multi_lingual_string(cls["label"]),
52
+ description=_parse_multi_lingual_string(cls["desc"]),
53
+ properties={},
54
+ db=db,
55
+ )
56
+
57
+
58
+ def _parse_property(
59
+ schema: Schema, prop_name: str, prop: dict
60
+ ) -> DataProperty | ObjectProperty:
61
+ if isinstance(prop, str):
62
+ datatype = prop
63
+ if datatype in schema.classes:
64
+ return ObjectProperty(
65
+ name=prop_name,
66
+ label=_parse_multi_lingual_string(prop_name),
67
+ description=_parse_multi_lingual_string(""),
68
+ target=schema.classes[datatype],
69
+ cardinality=Cardinality.ONE_TO_ONE,
70
+ is_private=False,
71
+ )
72
+ else:
73
+ return DataProperty(
74
+ name=prop_name,
75
+ label=_parse_multi_lingual_string(prop_name),
76
+ description=_parse_multi_lingual_string(""),
77
+ datatype=_parse_datatype(datatype),
78
+ is_private=False,
79
+ )
80
+
81
+ db = prop.get("db", {})
82
+
83
+ assert isinstance(prop, dict), prop
84
+ if "datatype" in prop:
85
+ return DataProperty(
86
+ name=prop_name,
87
+ label=_parse_multi_lingual_string(prop.get("label", prop_name)),
88
+ description=_parse_multi_lingual_string(prop.get("desc", "")),
89
+ datatype=_parse_datatype(prop["datatype"]),
90
+ is_private=prop.get("is_private", False),
91
+ db=(
92
+ DataPropDBInfo(
93
+ is_primary_key=db.get("is_primary_key", False),
94
+ is_auto_increment=db.get("is_auto_increment", False),
95
+ is_unique=db.get("is_unique", False),
96
+ )
97
+ if "db" in prop
98
+ else None
99
+ ),
100
+ )
101
+
102
+ assert "target" in prop, prop
103
+ return ObjectProperty(
104
+ name=prop_name,
105
+ label=_parse_multi_lingual_string(prop.get("label", prop_name)),
106
+ description=_parse_multi_lingual_string(prop.get("desc", "")),
107
+ target=schema.classes[prop["target"]],
108
+ cardinality=Cardinality(prop.get("cardinality", "1:1")),
109
+ is_optional=prop.get("is_optional", False),
110
+ is_private=prop.get("is_private", False),
111
+ db=(
112
+ ObjectPropDBInfo(
113
+ is_embedded=db.get("is_embedded", None),
114
+ on_delete=ForeignKeyOnDelete(db.get("on_delete", "restrict")),
115
+ on_update=ForeignKeyOnUpdate(db.get("on_update", "restrict")),
116
+ )
117
+ if "db" in prop
118
+ else None
119
+ ),
120
+ )
121
+
122
+
123
+ def _parse_multi_lingual_string(o: dict | str) -> MultiLingualString:
124
+ if isinstance(o, str):
125
+ return MultiLingualString.en(o)
126
+ assert isinstance(o, dict), o
127
+ assert "en" in o
128
+ return MultiLingualString(lang2value=o, lang="en")
129
+
130
+
131
+ def _parse_datatype(datatype: str) -> DataType:
132
+ if datatype.endswith("[]"):
133
+ datatype = datatype[:-2]
134
+ is_list = True
135
+ else:
136
+ is_list = False
137
+
138
+ if datatype == "string":
139
+ return DataType("str", is_list=is_list, parent=None)
140
+ if datatype == "integer":
141
+ return DataType("int", is_list=is_list, parent=None)
142
+ if datatype == "datetime":
143
+ return DataType("datetime", is_list=is_list, parent=None)
144
+ if datatype == "bool":
145
+ return DataType("bool", is_list=is_list, parent=None)
146
+ if datatype == "float":
147
+ return DataType("float", is_list=is_list, parent=None)
148
+ if datatype == "bytes":
149
+ return DataType("bytes", is_list=is_list, parent=None)
150
+ if datatype == "dict":
151
+ return DataType("dict", is_list=is_list, parent=None)
152
+
153
+ raise NotImplementedError(datatype)
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass, field
4
+ from enum import Enum
5
+ from typing import TYPE_CHECKING, Literal, Optional
6
+
7
+ from sera.models._datatype import DataType
8
+ from sera.models._multi_lingual_string import MultiLingualString
9
+
10
+ if TYPE_CHECKING:
11
+ from sera.models._class import Class
12
+
13
+
14
+ class ForeignKeyOnDelete(str, Enum):
15
+ CASCADE = "cascade"
16
+ SET_NULL = "set null"
17
+ RESTRICT = "restrict"
18
+
19
+ def to_sqlalchemy(self) -> str:
20
+ if self == ForeignKeyOnDelete.CASCADE:
21
+ return "CASCADE"
22
+ elif self == ForeignKeyOnDelete.SET_NULL:
23
+ return "SET NULL"
24
+ elif self == ForeignKeyOnDelete.RESTRICT:
25
+ return "RESTRICT"
26
+ raise NotImplementedError(self)
27
+
28
+
29
+ class ForeignKeyOnUpdate(str, Enum):
30
+ CASCADE = "cascade"
31
+ DELETE = "delete"
32
+ RESTRICT = "restrict"
33
+
34
+ def to_sqlalchemy(self) -> str:
35
+ if self == ForeignKeyOnUpdate.CASCADE:
36
+ return "CASCADE"
37
+ elif self == ForeignKeyOnUpdate.DELETE:
38
+ return "DELETE"
39
+ elif self == ForeignKeyOnUpdate.RESTRICT:
40
+ return "RESTRICT"
41
+ raise NotImplementedError(self)
42
+
43
+
44
+ class Cardinality(str, Enum):
45
+ ONE_TO_ONE = "1:1"
46
+ ONE_TO_MANY = "1:N"
47
+ MANY_TO_ONE = "N:1"
48
+ MANY_TO_MANY = "N:N"
49
+
50
+ def is_star_to_many(self) -> bool:
51
+ return self in [
52
+ Cardinality.ONE_TO_MANY,
53
+ Cardinality.MANY_TO_MANY,
54
+ ]
55
+
56
+
57
+ @dataclass(kw_only=True)
58
+ class Property:
59
+ """Represent a property of a class."""
60
+
61
+ # name of the property in the application layer
62
+ name: str = field(
63
+ metadata={
64
+ "description": "Name of the property in the application layer, so it must be a valid Python identifier"
65
+ }
66
+ )
67
+ # human-readable name of the property
68
+ label: MultiLingualString
69
+ # human-readable description of the property
70
+ description: MultiLingualString
71
+ # whether this property is private and cannot be accessed by the end users
72
+ # default it is false
73
+ is_private: bool = field(default=False)
74
+
75
+
76
+ @dataclass(kw_only=True)
77
+ class DataPropDBInfo:
78
+ """Represent database information for a data property."""
79
+
80
+ # whether this property is a primary key or not
81
+ is_primary_key: bool = False
82
+ # if this property is an integer primary key, whether it is auto-incremented or not
83
+ is_auto_increment: bool = False
84
+ # whether this property contains unique values
85
+ is_unique: bool = False
86
+
87
+
88
+ @dataclass(kw_only=True)
89
+ class DataProperty(Property):
90
+ # data type of the property
91
+ datatype: DataType
92
+ # other database properties of this property
93
+ db: Optional[DataPropDBInfo] = None
94
+
95
+
96
+ @dataclass(kw_only=True)
97
+ class ObjectPropDBInfo:
98
+ """Represent database information for an object property."""
99
+
100
+ # if the target class is not stored in the database, whether to store this property as a composite class
101
+ # (see SQLAlchemy composite) or embedded (JSON). Note that it doesn't make sense to embed in composite mode
102
+ # if the cardinality is not 1:1
103
+ is_embedded: Optional[Literal["composite", "json"]] = None
104
+
105
+ # if the target class is stored in the database, control the cascade behavior
106
+ on_delete: ForeignKeyOnDelete = ForeignKeyOnDelete.RESTRICT
107
+ on_update: ForeignKeyOnUpdate = ForeignKeyOnUpdate.RESTRICT
108
+
109
+
110
+ @dataclass(kw_only=True)
111
+ class ObjectProperty(Property):
112
+ # the target class of the property
113
+ target: Class
114
+ # the cardinality of the property -- is it one-to-one, many-to-one, etc.
115
+ # if the cardinality is many-to-many, a new joint class is going to be generated automatically
116
+ # to store the relationship -- users can overwrite this generated class by define the one with the same
117
+ # name
118
+ cardinality: Cardinality
119
+ # whether this property is optional or not
120
+ is_optional: bool = False
121
+ # whether this property is stored as a mapping dic[str, Target] or not
122
+ # only valid for *-to-many relationships
123
+ is_map: bool = False
124
+ db: Optional[ObjectPropDBInfo] = None
sera/models/_schema.py ADDED
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+
3
+ from dataclasses import dataclass
4
+ from graphlib import TopologicalSorter
5
+
6
+ from sera.models._class import Class
7
+ from sera.models._property import ObjectProperty
8
+
9
+
10
+ @dataclass
11
+ class Schema:
12
+ classes: dict[str, Class]
13
+
14
+ def topological_sort(self) -> list[Class]:
15
+ """
16
+ Sort classes in topological order using graphlib.TopologicalSorter.
17
+ """
18
+ # Build the dependency graph
19
+ graph = {}
20
+ for cls_name, cls in self.classes.items():
21
+ dependencies = set()
22
+ for prop in cls.properties.values():
23
+ if isinstance(prop, ObjectProperty) and prop.target.name != cls_name:
24
+ dependencies.add(prop.target.name)
25
+ graph[cls_name] = dependencies
26
+
27
+ # Create topological sorter and get sorted class names
28
+ sorter = TopologicalSorter(graph)
29
+ sorted_names = list(sorter.static_order())
30
+
31
+ # Convert sorted names back to Class objects
32
+ return [self.classes[name] for name in sorted_names]
sera/namespace.py ADDED
@@ -0,0 +1,5 @@
1
+ from __future__ import annotations
2
+
3
+ from sera.misc import SingleNS
4
+
5
+ APP_NS = SingleNS("app", "https://purl.org/sera/1.0/")
sera/typing.py ADDED
@@ -0,0 +1,11 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Annotated, TypeVar
4
+
5
+
6
+ class doc(str):
7
+ """A docstring for a type. Typically used in Annotated"""
8
+
9
+
10
+ T = TypeVar("T")
11
+ FieldName = Annotated[str, doc("field name of a class")]
@@ -0,0 +1,20 @@
1
+ Metadata-Version: 2.1
2
+ Name: sera-2
3
+ Version: 1.1.0
4
+ Summary:
5
+ Author: Binh Vu
6
+ Author-email: bvu687@gmail.com
7
+ Requires-Python: >=3.12,<4.0
8
+ Classifier: Programming Language :: Python :: 3
9
+ Classifier: Programming Language :: Python :: 3.12
10
+ Classifier: Programming Language :: Python :: 3.13
11
+ Requires-Dist: black (>=25.0.1,<26.0.0)
12
+ Requires-Dist: codegen-2 (>=2.1.4,<3.0.0)
13
+ Requires-Dist: litestar (>=2.15.1,<3.0.0)
14
+ Requires-Dist: msgspec (>=0.19.0,<0.20.0)
15
+ Description-Content-Type: text/markdown
16
+
17
+ # Overview
18
+
19
+ This library enables rapid application development by leveraging a graph-based architecture.
20
+
@@ -0,0 +1,29 @@
1
+ sera/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
+ sera/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
3
+ sera/libs/api_helper.py,sha256=hUEy0INHM18lxTQ348tgbXNceOHcjiAnqmuL_8CRpLQ,2509
4
+ sera/libs/base_orm.py,sha256=IxsHjNPjF60YbLyTsMTOhQbjQX-L30UZtvE_vJDgZfo,2950
5
+ sera/libs/base_service.py,sha256=nVHODZjdTqC8rrDKxYUWrK95APrtdMSCYzflePNxXns,2379
6
+ sera/make/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ sera/make/__main__.py,sha256=TLHA3e9QVoE0Nic6HSQDNiAe1MrTTw5IdYlNSNxjN5Q,912
8
+ sera/make/make_app.py,sha256=e0DQpx6ilB76lxg2BYF6wjL7RFRQzvmOXq2322xm2SQ,4398
9
+ sera/make/make_python_api.py,sha256=ikZfJrJ4B606td4os1QgX-7G5QJypmR-3qgLvGOIRh0,8765
10
+ sera/make/make_python_model.py,sha256=1zC_NaFkyLYpZ3ytT-ZV6d8xmxtQHUpEFsqd0PkJ6x4,12031
11
+ sera/make/make_python_services.py,sha256=RsinYZdfkrTlTn9CT50VgqGs9w6IZawsJx-KEmqfnEY,2062
12
+ sera/make/make_typescript_model.py,sha256=U4S_2y3zgLZVfMenHRaJFBW8yqh2mUBuI291LGQVOJ8,35
13
+ sera/misc/__init__.py,sha256=hjW2Lf1itdqlDjf1X8hwSZZfmE0XKDFlYtMMC1AXkT4,300
14
+ sera/misc/_rdf.py,sha256=G0ekWLUSw-eiKUOaPSGkELbZld19S9d36gWwiswjrrs,1671
15
+ sera/misc/_utils.py,sha256=MY85CZruRLYI2bf44d-a8g4lJibznD2MnAfd9VY1UdQ,1094
16
+ sera/models/__init__.py,sha256=3ooJQulUc7oyUoMM89KT5f6nsoYagCjYEyC0AmyYyzY,661
17
+ sera/models/_class.py,sha256=vFCuzG0vfAHYBjk1HQMFszT7zUAqGex-VAVJ2CUPIaE,1824
18
+ sera/models/_collection.py,sha256=u3k4t7Z38FiZj9CstPArnf9KsIGr05zbwUn2ZrbQVJE,1078
19
+ sera/models/_datatype.py,sha256=JkD3aRQ11DWicS2xaiNPwhBzJNJPwSkktBunx3EVr1s,1714
20
+ sera/models/_module.py,sha256=-XW4bJ3v4QZPYPRMnbUW2zp0F5aJVH8uve_MrnwYz7w,3736
21
+ sera/models/_multi_lingual_string.py,sha256=cVoxge1SW68dVmOjDZJU93tUq2ccJse3kSPqxrNkmgc,1091
22
+ sera/models/_parse.py,sha256=l106WMAFTLjKio5L3rBK3B0NtwjnXh-sLF4Ofk2uFHE,5072
23
+ sera/models/_property.py,sha256=lrI6V2zEblUVuEy9lahbLPLFisFRX6zrkO8wFWzU35k,4018
24
+ sera/models/_schema.py,sha256=1F_Ict1NLvDcQDUZwcvnRS5MtsOTv584y3ChymUeUYA,1046
25
+ sera/namespace.py,sha256=5NJ0A7weZwblqkncpgY2Vfcat04mNtigNcVrqu7TGOc,123
26
+ sera/typing.py,sha256=CsazgVptDN-far3yISogTtA2KBJlFFOn0lK_WyZFDv8,230
27
+ sera_2-1.1.0.dist-info/METADATA,sha256=zmJ_UdY6SLvr7d6pkcQL-oCo9DYB8gxpiM3CikxzySU,599
28
+ sera_2-1.1.0.dist-info/WHEEL,sha256=Nq82e9rUAnEjt98J6MlVmMCZb-t9cYE2Ir1kpBmnWfs,88
29
+ sera_2-1.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.1
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any