sera-2 1.2.0__py3-none-any.whl → 1.4.2__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/misc/__init__.py CHANGED
@@ -3,6 +3,8 @@ from sera.misc._utils import (
3
3
  assert_isinstance,
4
4
  assert_not_null,
5
5
  filter_duplication,
6
+ to_camel_case,
7
+ to_pascal_case,
6
8
  to_snake_case,
7
9
  )
8
10
 
@@ -13,4 +15,6 @@ __all__ = [
13
15
  "assert_isinstance",
14
16
  "filter_duplication",
15
17
  "assert_not_null",
18
+ "to_snake_case",
19
+ "to_camel_case",
16
20
  ]
sera/misc/_utils.py CHANGED
@@ -13,6 +13,18 @@ def to_snake_case(camelcase: str) -> str:
13
13
  return snake.lower()
14
14
 
15
15
 
16
+ def to_camel_case(snake: str) -> str:
17
+ """Convert snake_case to camelCase."""
18
+ components = snake.split("_")
19
+ return components[0] + "".join(x.title() for x in components[1:])
20
+
21
+
22
+ def to_pascal_case(snake: str) -> str:
23
+ """Convert snake_case to PascalCase."""
24
+ components = snake.split("_")
25
+ return "".join(x.title() for x in components)
26
+
27
+
16
28
  def assert_isinstance(x: Any, cls: type[T]) -> T:
17
29
  if not isinstance(x, cls):
18
30
  raise Exception(f"{type(x)} doesn't match with {type(cls)}")
sera/models/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from sera.models._class import Class
2
2
  from sera.models._collection import DataCollection
3
- from sera.models._datatype import DataType
3
+ from sera.models._datatype import DataType, PyTypeWithDep, TsTypeWithDep
4
4
  from sera.models._module import App, Language, Module, Package
5
5
  from sera.models._multi_lingual_string import MultiLingualString
6
6
  from sera.models._parse import parse_schema
@@ -22,4 +22,6 @@ __all__ = [
22
22
  "Module",
23
23
  "App",
24
24
  "Language",
25
+ "PyTypeWithDep",
26
+ "TsTypeWithDep",
25
27
  ]
sera/models/_class.py CHANGED
@@ -8,12 +8,20 @@ from sera.models._multi_lingual_string import MultiLingualString
8
8
  from sera.models._property import DataProperty, ObjectProperty
9
9
 
10
10
 
11
+ @dataclass(kw_only=True)
12
+ class Index:
13
+ name: str
14
+ columns: list[str]
15
+ unique: bool = False
16
+
17
+
11
18
  @dataclass(kw_only=True)
12
19
  class ClassDBMapInfo:
13
20
  """Represent database information for a class."""
14
21
 
15
22
  # name of a corresponding table in the database for this class
16
23
  table_name: str
24
+ indices: list[Index] = field(default_factory=list)
17
25
 
18
26
 
19
27
  @dataclass(kw_only=True)
@@ -43,16 +51,25 @@ class Class:
43
51
  Get the ID property of this class.
44
52
  The ID property is the one tagged with is_primary_key
45
53
  """
46
- assert self.db is not None, "This class is not stored in the database"
47
54
  for prop in self.properties.values():
48
55
  if (
49
56
  isinstance(prop, DataProperty)
50
57
  and prop.db is not None
51
58
  and prop.db.is_primary_key
52
59
  ):
60
+ assert (
61
+ self.db is not None
62
+ ), "This class is not stored in the database and thus, cannot have a primary key"
53
63
  return prop
64
+ assert (
65
+ self.db is None
66
+ ), "This class is stored in the database and thus, must have a primary key"
54
67
  return None
55
68
 
56
69
  def get_pymodule_name(self) -> str:
57
70
  """Get the python module name of this class as if there is a python module created to store this class only."""
58
71
  return to_snake_case(self.name)
72
+
73
+ def get_tsmodule_name(self) -> str:
74
+ """Get the typescript module name of this class as if there is a typescript module created to store this class only."""
75
+ return self.name[0].lower() + self.name[1:]
@@ -3,6 +3,7 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
 
5
5
  from sera.models._class import Class
6
+ from sera.models._property import DataProperty
6
7
 
7
8
 
8
9
  @dataclass
@@ -24,8 +25,15 @@ class DataCollection:
24
25
  """Get the fields of this collection that can be used in a queries."""
25
26
  field_names = set()
26
27
  for prop in self.cls.properties.values():
27
- if prop.db is None:
28
- # This property is not stored in the database, so we skip it
28
+ if prop.db is None or prop.data.is_private:
29
+ # This property is not stored in the database or it's private, so we skip it
30
+ continue
31
+ if (
32
+ isinstance(prop, DataProperty)
33
+ and prop.db is not None
34
+ and not prop.db.is_indexed
35
+ ):
36
+ # This property is not indexed, so we skip it
29
37
  continue
30
38
  field_names.add(prop.name)
31
39
  return field_names
sera/models/_datatype.py CHANGED
@@ -1,10 +1,12 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  import datetime
4
- from dataclasses import dataclass
4
+ from dataclasses import dataclass, field
5
5
  from enum import Enum
6
6
  from typing import Literal
7
7
 
8
+ from codegen.models import expr
9
+
8
10
  PyDataType = Literal["str", "int", "datetime", "float", "bool", "bytes", "dict"]
9
11
  TypescriptDataType = Literal["string", "number", "boolean"]
10
12
  SQLAlchemyDataType = Literal[
@@ -20,73 +22,176 @@ SQLAlchemyDataType = Literal[
20
22
 
21
23
 
22
24
  @dataclass
23
- class TypeWithDep:
25
+ class PyTypeWithDep:
24
26
  type: str
25
27
  dep: str | None = None
26
28
 
27
-
28
- @dataclass
29
- class PyTypeWithDep(TypeWithDep):
30
-
31
29
  def get_python_type(self) -> type:
32
30
  """Get the Python type from the type string for typing annotation in Python."""
33
- if self.type == "str":
34
- return str
35
- elif self.type == "int":
36
- return int
37
- elif self.type == "float":
38
- return float
39
- elif self.type == "bool":
40
- return bool
41
- elif self.type == "bytes":
42
- return bytes
43
- elif self.type == "dict":
44
- return dict
45
- elif self.type == "datetime":
46
- return datetime.datetime
47
- else:
31
+ type = {
32
+ "str": str,
33
+ "int": int,
34
+ "float": float,
35
+ "bool": bool,
36
+ "bytes": bytes,
37
+ "dict": dict,
38
+ "datetime": datetime.datetime,
39
+ "list[str]": list[str],
40
+ "list[int]": list[int],
41
+ "list[float]": list[float],
42
+ "list[bool]": list[bool],
43
+ "list[bytes]": list[bytes],
44
+ "list[dict]": list[dict],
45
+ "list[datetime]": list[datetime.datetime],
46
+ }.get(self.type, None)
47
+ if type is None:
48
48
  raise ValueError(f"Unknown type: {self.type}")
49
+ return type
50
+
51
+ def as_list_type(self) -> PyTypeWithDep:
52
+ """Convert the type to a list type."""
53
+ return PyTypeWithDep(type=f"list[{self.type}]", dep=self.dep)
54
+
55
+
56
+ @dataclass
57
+ class TsTypeWithDep:
58
+ type: str
59
+ dep: str | None = None
60
+
61
+ def get_default(self) -> expr.ExprConstant:
62
+ if self.type.endswith("[]"):
63
+ return expr.ExprConstant([])
64
+ if self.type == "string":
65
+ return expr.ExprConstant("")
66
+ if self.type == "number":
67
+ return expr.ExprConstant(0)
68
+ if self.type == "boolean":
69
+ return expr.ExprConstant(False)
70
+ if self.type == "string | undefined":
71
+ return expr.ExprConstant("undefined")
72
+ raise ValueError(f"Unknown type: {self.type}")
73
+
74
+ def as_list_type(self) -> TsTypeWithDep:
75
+ return TsTypeWithDep(type=f"{self.type}[]", dep=self.dep)
76
+
77
+
78
+ @dataclass
79
+ class SQLTypeWithDep:
80
+ type: str
81
+ mapped_pytype: str
82
+ deps: list[str] = field(default_factory=list)
83
+
84
+ def as_list_type(self) -> SQLTypeWithDep:
85
+ """Convert the type to a list type."""
86
+ return SQLTypeWithDep(
87
+ type=f"ARRAY({self.type})",
88
+ deps=self.deps + ["sqlalchemy.ARRAY"],
89
+ mapped_pytype=f"list[{self.mapped_pytype}]",
90
+ )
49
91
 
50
92
 
51
93
  @dataclass
52
94
  class DataType:
53
- pytype: PyDataType
54
- sqltype: SQLAlchemyDataType
55
- tstype: TypescriptDataType
95
+ pytype: PyTypeWithDep
96
+ sqltype: SQLTypeWithDep
97
+ tstype: TsTypeWithDep
56
98
 
57
99
  is_list: bool = False
58
100
 
59
- def get_python_type(self) -> TypeWithDep:
60
- if self.pytype in ["str", "int", "float", "bool", "bytes", "dict"]:
61
- return TypeWithDep(type=self.pytype)
62
- if self.pytype == "datetime":
63
- return TypeWithDep(type="datetime", dep="datetime.datetime")
64
- raise NotImplementedError(self.pytype)
101
+ def get_python_type(self) -> PyTypeWithDep:
102
+ pytype = self.pytype
103
+ if self.is_list:
104
+ return pytype.as_list_type()
105
+ return pytype
65
106
 
66
- def get_sqlalchemy_type(self) -> TypeWithDep:
67
- if self.pytype in ["str", "int", "float", "bool", "bytes"]:
68
- return TypeWithDep(type=self.pytype)
69
- if self.pytype == "dict":
70
- return TypeWithDep(type="JSON")
71
- if self.pytype == "datetime":
72
- return TypeWithDep(type="datetime", dep="datetime.datetime")
73
- raise NotImplementedError(self.pytype)
107
+ def get_sqlalchemy_type(self) -> SQLTypeWithDep:
108
+ sqltype = self.sqltype
109
+ if self.is_list:
110
+ return sqltype.as_list_type()
111
+ return sqltype
112
+
113
+ def get_typescript_type(self) -> TsTypeWithDep:
114
+ tstype = self.tstype
115
+ if self.is_list:
116
+ return tstype.as_list_type()
117
+ return tstype
74
118
 
75
119
 
76
120
  predefined_datatypes = {
77
- "string": DataType(pytype="str", sqltype="String", tstype="string", is_list=False),
121
+ "string": DataType(
122
+ pytype=PyTypeWithDep(type="str"),
123
+ sqltype=SQLTypeWithDep(
124
+ type="String", mapped_pytype="str", deps=["sqlalchemy.String"]
125
+ ),
126
+ tstype=TsTypeWithDep(type="string"),
127
+ is_list=False,
128
+ ),
129
+ "optional[string]": DataType(
130
+ pytype=PyTypeWithDep(type="Optional[str]", dep="typing.Optional"),
131
+ sqltype=SQLTypeWithDep(
132
+ type="String",
133
+ mapped_pytype="Optional[str]",
134
+ deps=["sqlalchemy.String", "typing.Optional"],
135
+ ),
136
+ tstype=TsTypeWithDep(type="string | undefined"),
137
+ is_list=False,
138
+ ),
78
139
  "integer": DataType(
79
- pytype="int", sqltype="Integer", tstype="number", is_list=False
140
+ pytype=PyTypeWithDep(type="int"),
141
+ sqltype=SQLTypeWithDep(
142
+ type="Integer", mapped_pytype="int", deps=["sqlalchemy.Integer"]
143
+ ),
144
+ tstype=TsTypeWithDep(type="number"),
145
+ is_list=False,
80
146
  ),
81
147
  "datetime": DataType(
82
- pytype="datetime", sqltype="DateTime", tstype="string", is_list=False
148
+ pytype=PyTypeWithDep(type="datetime", dep="datetime.datetime"),
149
+ sqltype=SQLTypeWithDep(
150
+ type="DateTime", mapped_pytype="datetime", deps=["sqlalchemy.DateTime"]
151
+ ),
152
+ tstype=TsTypeWithDep(type="string"),
153
+ is_list=False,
154
+ ),
155
+ "float": DataType(
156
+ pytype=PyTypeWithDep(type="float"),
157
+ sqltype=SQLTypeWithDep(
158
+ type="Float", mapped_pytype="float", deps=["sqlalchemy.Float"]
159
+ ),
160
+ tstype=TsTypeWithDep(type="number"),
161
+ is_list=False,
83
162
  ),
84
- "float": DataType(pytype="float", sqltype="Float", tstype="number", is_list=False),
85
163
  "boolean": DataType(
86
- pytype="bool", sqltype="Boolean", tstype="boolean", is_list=False
164
+ pytype=PyTypeWithDep(type="bool"),
165
+ sqltype=SQLTypeWithDep(
166
+ type="Boolean", mapped_pytype="bool", deps=["sqlalchemy.Boolean"]
167
+ ),
168
+ tstype=TsTypeWithDep(type="boolean"),
169
+ is_list=False,
87
170
  ),
88
171
  "bytes": DataType(
89
- pytype="bytes", sqltype="LargeBinary", tstype="string", is_list=False
172
+ pytype=PyTypeWithDep(type="bytes"),
173
+ sqltype=SQLTypeWithDep(
174
+ type="LargeBinary", mapped_pytype="bytes", deps=["sqlalchemy.LargeBinary"]
175
+ ),
176
+ tstype=TsTypeWithDep(type="string"),
177
+ is_list=False,
178
+ ),
179
+ "dict": DataType(
180
+ pytype=PyTypeWithDep(type="dict"),
181
+ sqltype=SQLTypeWithDep(
182
+ type="JSON", mapped_pytype="dict", deps=["sqlalchemy.JSON"]
183
+ ),
184
+ tstype=TsTypeWithDep(type="string"),
185
+ is_list=False,
186
+ ),
187
+ }
188
+
189
+ predefined_py_datatypes = {"bytes": PyTypeWithDep(type="bytes")}
190
+ predefined_sql_datatypes = {
191
+ "bit": SQLTypeWithDep(
192
+ type="BIT", mapped_pytype="bytes", deps=["sqlalchemy.dialects.postgresql.BIT"]
90
193
  ),
91
- "dict": DataType(pytype="dict", sqltype="JSON", tstype="string", is_list=False),
194
+ }
195
+ predefined_ts_datatypes = {
196
+ "string": TsTypeWithDep(type="string"),
92
197
  }
sera/models/_module.py CHANGED
@@ -1,6 +1,6 @@
1
1
  from __future__ import annotations
2
2
 
3
- import datetime
3
+ import subprocess
4
4
  from dataclasses import dataclass
5
5
  from enum import Enum
6
6
  from pathlib import Path
@@ -75,22 +75,53 @@ class Module:
75
75
  raise
76
76
  else:
77
77
  assert self.language == Language.Typescript
78
- raise NotImplementedError()
78
+ try:
79
+ code = program.root.to_typescript()
80
+ except:
81
+ logger.error("Error writing module {}", self.path)
82
+ print(">>> Program")
83
+ print(program.root.to_typescript())
84
+ print("<<<")
85
+ raise
86
+
87
+ if self.language == Language.Python:
88
+ outfile = self.package.dir / f"{self.name}.py"
89
+ copyright_statement = f"# Generated by SERA. All rights reserved.\n\n"
90
+ else:
91
+ assert self.language == Language.Typescript
92
+ outfile = self.package.dir / f"{self.name}.ts"
93
+ copyright_statement = f"/// Generated by SERA. All rights reserved.\n\n"
79
94
 
80
- outfile = self.package.dir / f"{self.name}.py"
81
95
  if outfile.exists():
82
- if outfile.read_text().startswith("# sera:skip"):
96
+ outfile_content = outfile.read_text()
97
+ if outfile_content.startswith("# sera:skip") or outfile_content.startswith(
98
+ "/// sera:skip"
99
+ ):
83
100
  logger.info(
84
101
  "`{}` already exists and is in manual edit mode. Skip updating it.",
85
102
  outfile,
86
103
  )
87
104
  return
88
- copyright_statement = f"# Generated by SERA \n\n"
105
+
89
106
  outfile.write_text(copyright_statement + code)
90
107
 
108
+ if self.language == Language.Typescript:
109
+ # invoke prettier to format the code
110
+ try:
111
+ subprocess.check_output(
112
+ ["npx", "prettier", "--write", str(outfile.absolute())],
113
+ cwd=self.package.app.root.dir,
114
+ )
115
+ except Exception as e:
116
+ logger.error("Error formatting Typescript file: {}", e)
117
+ raise
118
+
91
119
  def exists(self) -> bool:
92
120
  """Check if the module exists"""
93
- return (self.package.dir / f"{self.name}.py").exists()
121
+ if self.language == Language.Python:
122
+ return (self.package.dir / f"{self.name}.py").exists()
123
+ else:
124
+ return (self.package.dir / f"{self.name}.ts").exists()
94
125
 
95
126
 
96
127
  @dataclass
sera/models/_parse.py CHANGED
@@ -1,12 +1,19 @@
1
1
  from __future__ import annotations
2
2
 
3
+ import re
3
4
  from copy import deepcopy
4
5
  from pathlib import Path
5
6
  from typing import Sequence
6
7
 
7
8
  import serde.yaml
8
- from sera.models._class import Class, ClassDBMapInfo
9
- from sera.models._datatype import DataType, predefined_datatypes
9
+ from sera.models._class import Class, ClassDBMapInfo, Index
10
+ from sera.models._datatype import (
11
+ DataType,
12
+ predefined_datatypes,
13
+ predefined_py_datatypes,
14
+ predefined_sql_datatypes,
15
+ predefined_ts_datatypes,
16
+ )
10
17
  from sera.models._multi_lingual_string import MultiLingualString
11
18
  from sera.models._property import (
12
19
  Cardinality,
@@ -16,6 +23,7 @@ from sera.models._property import (
16
23
  ForeignKeyOnUpdate,
17
24
  ObjectPropDBInfo,
18
25
  ObjectProperty,
26
+ PropDataAttrs,
19
27
  )
20
28
  from sera.models._schema import Schema
21
29
 
@@ -46,7 +54,16 @@ def parse_schema(files: Sequence[Path | str]) -> Schema:
46
54
  def _parse_class_without_prop(schema: Schema, clsname: str, cls: dict) -> Class:
47
55
  db = None
48
56
  if "db" in cls:
49
- db = ClassDBMapInfo(table_name=cls["db"]["table_name"])
57
+ indices = []
58
+ for idx in cls["db"].get("indices", []):
59
+ index = Index(
60
+ name=idx.get("name", "_".join(idx["columns"]) + "_index"),
61
+ columns=idx["columns"],
62
+ unique=idx.get("unique", False),
63
+ )
64
+ indices.append(index)
65
+ db = ClassDBMapInfo(table_name=cls["db"]["table_name"], indices=indices)
66
+
50
67
  return Class(
51
68
  name=clsname,
52
69
  label=_parse_multi_lingual_string(cls["label"]),
@@ -68,7 +85,6 @@ def _parse_property(
68
85
  description=_parse_multi_lingual_string(""),
69
86
  target=schema.classes[datatype],
70
87
  cardinality=Cardinality.ONE_TO_ONE,
71
- is_private=False,
72
88
  )
73
89
  else:
74
90
  return DataProperty(
@@ -76,10 +92,14 @@ def _parse_property(
76
92
  label=_parse_multi_lingual_string(prop_name),
77
93
  description=_parse_multi_lingual_string(""),
78
94
  datatype=_parse_datatype(datatype),
79
- is_private=False,
80
95
  )
81
96
 
82
97
  db = prop.get("db", {})
98
+ _data = prop.get("data", {})
99
+ data_attrs = PropDataAttrs(
100
+ is_private=_data.get("is_private", False),
101
+ datatype=_parse_datatype(_data["datatype"]) if "datatype" in _data else None,
102
+ )
83
103
 
84
104
  assert isinstance(prop, dict), prop
85
105
  if "datatype" in prop:
@@ -88,12 +108,16 @@ def _parse_property(
88
108
  label=_parse_multi_lingual_string(prop.get("label", prop_name)),
89
109
  description=_parse_multi_lingual_string(prop.get("desc", "")),
90
110
  datatype=_parse_datatype(prop["datatype"]),
91
- is_private=prop.get("is_private", False),
111
+ data=data_attrs,
92
112
  db=(
93
113
  DataPropDBInfo(
94
114
  is_primary_key=db.get("is_primary_key", False),
95
115
  is_auto_increment=db.get("is_auto_increment", False),
96
116
  is_unique=db.get("is_unique", False),
117
+ is_indexed=db.get("is_indexed", False)
118
+ or db.get("is_unique", False)
119
+ or db.get("is_primary_key", False),
120
+ is_nullable=db.get("is_nullable", False),
97
121
  )
98
122
  if "db" in prop
99
123
  else None
@@ -108,7 +132,7 @@ def _parse_property(
108
132
  target=schema.classes[prop["target"]],
109
133
  cardinality=Cardinality(prop.get("cardinality", "1:1")),
110
134
  is_optional=prop.get("is_optional", False),
111
- is_private=prop.get("is_private", False),
135
+ data=data_attrs,
112
136
  db=(
113
137
  ObjectPropDBInfo(
114
138
  is_embedded=db.get("is_embedded", None),
@@ -139,16 +163,43 @@ def _parse_multi_lingual_string(o: dict | str) -> MultiLingualString:
139
163
  return MultiLingualString(lang2value=o, lang="en")
140
164
 
141
165
 
142
- def _parse_datatype(datatype: str) -> DataType:
143
- if datatype.endswith("[]"):
144
- datatype = datatype[:-2]
145
- is_list = True
146
- else:
147
- is_list = False
166
+ def _parse_datatype(datatype: dict | str) -> DataType:
167
+ if isinstance(datatype, str):
168
+ if datatype.endswith("[]"):
169
+ datatype = datatype[:-2]
170
+ is_list = True
171
+ else:
172
+ is_list = False
173
+
174
+ if datatype not in predefined_datatypes:
175
+ raise NotImplementedError(datatype)
176
+
177
+ dt = deepcopy(predefined_datatypes[datatype])
178
+ dt.is_list = is_list
179
+ return dt
180
+ if isinstance(datatype, dict):
181
+ is_list = datatype.get("is_list", False)
182
+
183
+ # Parse SQL type and argument if present
184
+ m = re.match(r"^([a-zA-Z0-9_]+)(\([^)]+\))?$", datatype["sqltype"])
185
+ if m is not None:
186
+ sql_type_name = m.group(1)
187
+ sql_type_arg = m.group(2)
188
+ # Use the extracted type to get the predefined SQL type
189
+ if sql_type_name not in predefined_sql_datatypes:
190
+ raise NotImplementedError(sql_type_name)
191
+ sql_type = predefined_sql_datatypes[sql_type_name]
192
+ if sql_type_arg is not None:
193
+ # process the argument
194
+ sql_type.type = sql_type.type + sql_type_arg
195
+ else:
196
+ raise ValueError(f"Invalid SQL type format: {datatype['sqltype']}")
148
197
 
149
- if datatype not in predefined_datatypes:
150
- raise NotImplementedError(datatype)
198
+ return DataType(
199
+ pytype=predefined_py_datatypes[datatype["pytype"]],
200
+ sqltype=sql_type,
201
+ tstype=predefined_ts_datatypes[datatype["tstype"]],
202
+ is_list=is_list,
203
+ )
151
204
 
152
- dt = deepcopy(predefined_datatypes[datatype])
153
- dt.is_list = is_list
154
- return dt
205
+ raise NotImplementedError(datatype)
sera/models/_property.py CHANGED
@@ -54,6 +54,19 @@ class Cardinality(str, Enum):
54
54
  ]
55
55
 
56
56
 
57
+ @dataclass(kw_only=True)
58
+ class PropDataAttrs:
59
+ """Storing other attributes for generating data model (upsert & public) -- this is different from a db model"""
60
+
61
+ # whether this property is private and cannot be accessed by the end users
62
+ # meaning the public data model will not include this property
63
+ # default it is false
64
+ is_private: bool = False
65
+
66
+ # whether this data model has a different data type than the one from the database
67
+ datatype: Optional[DataType] = None
68
+
69
+
57
70
  @dataclass(kw_only=True)
58
71
  class Property:
59
72
  """Represent a property of a class."""
@@ -68,9 +81,8 @@ class Property:
68
81
  label: MultiLingualString
69
82
  # human-readable description of the property
70
83
  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)
84
+ # other attributes for generating data model such as upsert and return.
85
+ data: PropDataAttrs = field(default_factory=PropDataAttrs)
74
86
 
75
87
 
76
88
  @dataclass(kw_only=True)
@@ -83,6 +95,10 @@ class DataPropDBInfo:
83
95
  is_auto_increment: bool = False
84
96
  # whether this property contains unique values
85
97
  is_unique: bool = False
98
+ # whether this property is indexed or not
99
+ is_indexed: bool = False
100
+ # whether this property is nullable or not
101
+ is_nullable: bool = False
86
102
 
87
103
 
88
104
  @dataclass(kw_only=True)
@@ -92,6 +108,14 @@ class DataProperty(Property):
92
108
  # other database properties of this property
93
109
  db: Optional[DataPropDBInfo] = None
94
110
 
111
+ def get_data_model_datatype(self) -> DataType:
112
+ if self.data.datatype is not None:
113
+ return self.data.datatype
114
+ return self.datatype
115
+
116
+ def is_diff_data_model_datatype(self):
117
+ return self.data.datatype is not None
118
+
95
119
 
96
120
  @dataclass(kw_only=True)
97
121
  class ObjectPropDBInfo:
sera/typing.py CHANGED
@@ -9,3 +9,6 @@ class doc(str):
9
9
 
10
10
  T = TypeVar("T")
11
11
  FieldName = Annotated[str, doc("field name of a class")]
12
+ ObjectPath = Annotated[
13
+ str, doc("path of an object (e.g., can be function, class, etc.)")
14
+ ]
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.1
1
+ Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.2.0
3
+ Version: 1.4.2
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -8,10 +8,10 @@ Requires-Python: >=3.12,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Programming Language :: Python :: 3.13
11
- Requires-Dist: black (>=25.0.1,<26.0.0)
12
11
  Requires-Dist: codegen-2 (>=2.1.4,<3.0.0)
13
12
  Requires-Dist: litestar (>=2.15.1,<3.0.0)
14
13
  Requires-Dist: msgspec (>=0.19.0,<0.20.0)
14
+ Project-URL: Repository, https://github.com/binh-vu/sera
15
15
  Description-Content-Type: text/markdown
16
16
 
17
17
  # Overview