sera-2 1.13.1__py3-none-any.whl → 1.14.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/exports/schema.py CHANGED
@@ -1,128 +1,154 @@
1
1
  from pathlib import Path
2
2
  from typing import Annotated
3
3
 
4
+ import orjson
4
5
  import typer
5
6
 
6
- from sera.models import Cardinality, Class, DataProperty, Schema, parse_schema
7
- from sera.models._datatype import DataType
8
-
9
-
10
- def get_prisma_field_type(datatype: DataType) -> str:
11
- pytype = datatype.get_python_type().type
12
- if pytype == "str":
13
- return "String"
14
- if pytype == "int":
15
- return "Int"
16
- if pytype == "float":
17
- return "Float"
18
- if pytype == "bool":
19
- return "Boolean"
20
- if pytype == "bytes":
21
- return "Bytes"
22
- if pytype == "dict":
23
- return "Json"
24
- if pytype == "datetime":
25
- return "DateTime"
26
- if pytype == "list[str]":
27
- return "String[]"
28
- if pytype == "list[int]":
29
- return "Int[]"
30
- if pytype == "list[float]":
31
- return "Float[]"
32
- if pytype == "list[bool]":
33
- return "Boolean[]"
34
- if pytype == "list[bytes]":
35
- return "Bytes[]"
36
- if pytype == "list[dict]":
37
- return "Json[]"
38
- if pytype == "list[datetime]":
39
- return "DateTime[]"
40
-
41
- raise ValueError(f"Unsupported data type for Prisma: {pytype}")
42
-
43
-
44
- def to_prisma_model(schema: Schema, cls: Class, lines: list[str]):
45
- """Convert a Sera Class to a Prisma model string representation."""
46
- lines.append(f"model {cls.name} {{")
47
-
48
- if cls.db is None:
49
- # This class has no database mapping, we must generate a default key for it
50
- lines.append(
51
- f" {'id'.ljust(30)} {'Int'.ljust(10)} @id @default(autoincrement())"
52
- )
53
- # lines.append(f" @@unique([%s])" % ", ".join(cls.properties.keys()))
54
-
55
- for prop in cls.properties.values():
56
- propattrs = ""
57
- if isinstance(prop, DataProperty):
58
- proptype = get_prisma_field_type(prop.datatype)
59
- if prop.is_optional:
60
- proptype = f"{proptype}?"
61
- if prop.db is not None and prop.db.is_primary_key:
62
- propattrs += "@id "
63
-
64
- lines.append(f" {prop.name.ljust(30)} {proptype.ljust(10)} {propattrs}")
65
- continue
66
-
67
- if prop.cardinality == Cardinality.MANY_TO_MANY:
68
- # For many-to-many relationships, we need to handle the join table
69
- lines.append(
70
- f" {prop.name.ljust(30)} {(prop.target.name + '[]').ljust(10)}"
71
- )
72
- else:
73
- lines.append(
74
- f" {(prop.name + '_').ljust(30)} {prop.target.name.ljust(10)} @relation(fields: [{prop.name}], references: [id])"
75
- )
76
- lines.append(f" {prop.name.ljust(30)} {'Int'.ljust(10)} @unique")
77
-
78
- lines.append("")
79
- for upstream_cls, reverse_upstream_prop in schema.get_upstream_classes(cls):
80
- if (
81
- reverse_upstream_prop.cardinality == Cardinality.MANY_TO_ONE
82
- or reverse_upstream_prop.cardinality == Cardinality.MANY_TO_MANY
83
- ):
84
-
85
- proptype = f"{upstream_cls.name}[]"
86
- else:
87
- proptype = upstream_cls.name + "?"
88
- lines.append(f" {upstream_cls.name.lower().ljust(30)} {proptype.ljust(10)}")
89
-
90
- lines.append("}\n")
91
-
92
-
93
- def export_prisma_schema(schema: Schema, outfile: Path):
94
- """Export Prisma schema file"""
95
- lines = []
96
-
97
- # Datasource
98
- lines.append("datasource db {")
99
- lines.append(
100
- ' provider = "postgresql"'
101
- ) # Defaulting to postgresql as per user context
102
- lines.append(' url = env("DATABASE_URL")')
103
- lines.append("}\n")
104
-
105
- # Generator
106
- lines.append("generator client {")
107
- lines.append(' provider = "prisma-client-py"')
108
- lines.append(" recursive_type_depth = 5")
109
- lines.append("}\n")
110
-
111
- # Enums
112
- if schema.enums:
113
- for enum_name, enum_def in schema.enums.items():
114
- lines.append(f"enum {enum_name} {{")
115
- # Assuming enum_def.values is a list of strings based on previous errors
116
- for val_str in enum_def.values:
117
- lines.append(f" {val_str}")
118
- lines.append("}\\n")
119
-
120
- # Models
7
+ from sera.misc import assert_not_null, to_snake_case
8
+ from sera.models import Cardinality, DataProperty, Schema, parse_schema
9
+
10
+
11
+ def export_tbls(schema: Schema, outfile: Path):
12
+ out = {
13
+ "name": schema.name,
14
+ "tables": [],
15
+ "relations": [],
16
+ }
17
+
18
+ DUMMY_IDPROP = "dkey"
19
+
121
20
  for cls in schema.topological_sort():
122
- to_prisma_model(schema, cls, lines)
21
+ table = {
22
+ "name": cls.name,
23
+ "type": "BASE TABLE",
24
+ "columns": [],
25
+ "constraints": [],
26
+ }
27
+
28
+ if cls.db is None:
29
+ # This class has no database mapping, we must generate a default key for it
30
+ table["columns"].append(
31
+ {
32
+ "name": DUMMY_IDPROP,
33
+ "type": "UNSET",
34
+ "nullable": False,
35
+ }
36
+ )
123
37
 
124
- with outfile.open("w", encoding="utf-8") as f:
125
- f.write("\n".join(lines))
38
+ for prop in cls.properties.values():
39
+ column = {
40
+ "name": prop.name,
41
+ "nullable": not prop.is_optional,
42
+ }
43
+
44
+ if isinstance(prop, DataProperty):
45
+ column["type"] = prop.datatype.get_python_type().type
46
+ if prop.db is not None and prop.db.is_primary_key:
47
+ table["constraints"].append(
48
+ {
49
+ "name": f"{cls.name}_pkey",
50
+ "type": "PRIMARY KEY",
51
+ "def": f"PRIMARY KEY ({prop.name})",
52
+ "table": cls.name,
53
+ "referenced_table": cls.name,
54
+ "columns": [prop.name],
55
+ }
56
+ )
57
+ else:
58
+ if prop.cardinality == Cardinality.MANY_TO_MANY:
59
+ # For many-to-many relationships, we need to create a join table
60
+ jointable = {
61
+ "name": f"{cls.name}{prop.target.name}",
62
+ "type": "JOIN TABLE",
63
+ "columns": [
64
+ {
65
+ "name": f"{to_snake_case(cls.name)}_id",
66
+ "type": assert_not_null(cls.get_id_property())
67
+ .datatype.get_python_type()
68
+ .type,
69
+ "nullable": False,
70
+ },
71
+ {
72
+ "name": f"{to_snake_case(prop.target.name)}_id",
73
+ "type": assert_not_null(prop.target.get_id_property())
74
+ .datatype.get_python_type()
75
+ .type,
76
+ "nullable": False,
77
+ },
78
+ ],
79
+ }
80
+ out["tables"].append(jointable)
81
+ out["relations"].extend(
82
+ [
83
+ {
84
+ "table": f"{cls.name}{prop.target.name}",
85
+ "columns": [f"{to_snake_case(cls.name)}_id"],
86
+ "cardinality": "zero_or_more",
87
+ "parent_table": cls.name,
88
+ "parent_columns": [
89
+ assert_not_null(cls.get_id_property()).name
90
+ ],
91
+ "parent_cardinality": "zero_or_one",
92
+ "def": "", # LiamERD does not use `def` so we can leave it empty for now
93
+ },
94
+ {
95
+ "table": f"{cls.name}{prop.target.name}",
96
+ "columns": [f"{to_snake_case(prop.target.name)}_id"],
97
+ "cardinality": "zero_or_more",
98
+ "parent_table": prop.target.name,
99
+ "parent_columns": [
100
+ assert_not_null(prop.target.get_id_property()).name
101
+ ],
102
+ "parent_cardinality": "zero_or_one",
103
+ "def": "",
104
+ },
105
+ ]
106
+ )
107
+ # we actually want to skip adding this N-to-N column
108
+ continue
109
+
110
+ if prop.target.db is not None:
111
+ idprop = assert_not_null(prop.target.get_id_property())
112
+ idprop_name = idprop.name
113
+ column["type"] = idprop.datatype.get_python_type().type
114
+ else:
115
+ column["type"] = prop.target.name
116
+ idprop_name = (
117
+ DUMMY_IDPROP # a dummy property name for visualization purposes
118
+ )
119
+ assert idprop_name not in prop.target.properties
120
+
121
+ # somehow LiamERD only support zero_or_more or zero_or_one (exactly one does not work)
122
+ if prop.cardinality == Cardinality.ONE_TO_ONE:
123
+ cardinality = "zero_or_one"
124
+ parent_cardinality = "zero_or_one"
125
+ elif prop.cardinality == Cardinality.ONE_TO_MANY:
126
+ cardinality = "zero_or_one"
127
+ parent_cardinality = "zero_or_more"
128
+ elif prop.cardinality == Cardinality.MANY_TO_ONE:
129
+ cardinality = "zero_or_more"
130
+ parent_cardinality = "zero_or_one"
131
+ elif prop.cardinality == Cardinality.MANY_TO_MANY:
132
+ raise Exception("Unreachable")
133
+
134
+ out["relations"].append(
135
+ {
136
+ "table": cls.name,
137
+ "columns": [prop.name],
138
+ "cardinality": cardinality,
139
+ "parent_table": prop.target.name,
140
+ "parent_columns": [idprop_name],
141
+ "parent_cardinality": parent_cardinality,
142
+ "def": f"FOREIGN KEY ({prop.name}) REFERENCES {prop.target.name}({idprop_name})",
143
+ }
144
+ )
145
+
146
+ table["columns"].append(column)
147
+
148
+ out["tables"].append(table)
149
+
150
+ outfile.parent.mkdir(parents=True, exist_ok=True)
151
+ outfile.write_bytes(orjson.dumps(out, option=orjson.OPT_INDENT_2))
126
152
 
127
153
 
128
154
  app = typer.Typer(pretty_exceptions_short=True, pretty_exceptions_enable=False)
@@ -139,7 +165,7 @@ def cli(
139
165
  outfile: Annotated[
140
166
  Path,
141
167
  typer.Option(
142
- "-o", "--output", help="Output file for the Prisma schema", writable=True
168
+ "-o", "--output", help="Output file for the tbls schema", writable=True
143
169
  ),
144
170
  ],
145
171
  ):
@@ -147,7 +173,7 @@ def cli(
147
173
  "sera",
148
174
  schema_files,
149
175
  )
150
- export_prisma_schema(
176
+ export_tbls(
151
177
  schema,
152
178
  outfile,
153
179
  )
@@ -7,8 +7,7 @@ from loguru import logger
7
7
 
8
8
  from sera.misc import assert_not_null, to_snake_case
9
9
  from sera.models import App, DataCollection, Module, Package, SystemControlledMode
10
-
11
- GLOBAL_IDENTS = {"AsyncSession": "sqlalchemy.ext.asyncio.AsyncSession"}
10
+ from sera.typing import GLOBAL_IDENTS
12
11
 
13
12
 
14
13
  def make_python_api(app: App, collections: Sequence[DataCollection]):
@@ -544,19 +543,20 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
544
543
  )
545
544
  program.import_(
546
545
  app.models.data.path
547
- + f".{collection.get_pymodule_name()}.Upsert{collection.name}",
546
+ + f".{collection.get_pymodule_name()}.Create{collection.name}",
548
547
  True,
549
548
  )
550
549
 
551
550
  # assuming the collection has only one class
552
551
  cls = collection.cls
553
- has_restricted_system_controlled_prop = any(
554
- prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
552
+ is_on_create_update_props = any(
553
+ prop.data.system_controlled is not None
554
+ and prop.data.system_controlled.is_on_create_value_updated()
555
555
  for prop in cls.properties.values()
556
556
  )
557
557
  idprop = assert_not_null(cls.get_id_property())
558
558
 
559
- if has_restricted_system_controlled_prop:
559
+ if is_on_create_update_props:
560
560
  program.import_("sera.libs.api_helper.SingleAutoUSCP", True)
561
561
 
562
562
  func_name = "create"
@@ -575,11 +575,11 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
575
575
  "dto",
576
576
  PredefinedFn.item_getter(
577
577
  expr.ExprIdent("SingleAutoUSCP"),
578
- expr.ExprIdent(f"Upsert{cls.name}"),
578
+ expr.ExprIdent(f"Create{cls.name}"),
579
579
  ),
580
580
  )
581
581
  ]
582
- if has_restricted_system_controlled_prop
582
+ if is_on_create_update_props
583
583
  else []
584
584
  ),
585
585
  )
@@ -589,7 +589,7 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
589
589
  [
590
590
  DeferredVar.simple(
591
591
  "data",
592
- expr.ExprIdent(f"Upsert{cls.name}"),
592
+ expr.ExprIdent(f"Create{cls.name}"),
593
593
  ),
594
594
  DeferredVar.simple(
595
595
  "session",
@@ -652,7 +652,7 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
652
652
  )
653
653
  program.import_(
654
654
  app.models.data.path
655
- + f".{collection.get_pymodule_name()}.Upsert{collection.name}",
655
+ + f".{collection.get_pymodule_name()}.Update{collection.name}",
656
656
  True,
657
657
  )
658
658
 
@@ -661,11 +661,12 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
661
661
  id_prop = assert_not_null(cls.get_id_property())
662
662
  id_type = id_prop.datatype.get_python_type().type
663
663
 
664
- has_restricted_system_controlled_prop = any(
665
- prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
664
+ is_on_update_update_props = any(
665
+ prop.data.system_controlled is not None
666
+ and prop.data.system_controlled.is_on_update_value_updated()
666
667
  for prop in cls.properties.values()
667
668
  )
668
- if has_restricted_system_controlled_prop:
669
+ if is_on_update_update_props:
669
670
  program.import_("sera.libs.api_helper.SingleAutoUSCP", True)
670
671
 
671
672
  func_name = "update"
@@ -684,11 +685,11 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
684
685
  "dto",
685
686
  PredefinedFn.item_getter(
686
687
  expr.ExprIdent("SingleAutoUSCP"),
687
- expr.ExprIdent(f"Upsert{cls.name}"),
688
+ expr.ExprIdent(f"Update{cls.name}"),
688
689
  ),
689
690
  )
690
691
  ]
691
- if has_restricted_system_controlled_prop
692
+ if is_on_update_update_props
692
693
  else []
693
694
  ),
694
695
  )
@@ -702,7 +703,7 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
702
703
  ),
703
704
  DeferredVar.simple(
704
705
  "data",
705
- expr.ExprIdent(f"Upsert{cls.name}"),
706
+ expr.ExprIdent(f"Update{cls.name}"),
706
707
  ),
707
708
  DeferredVar.simple(
708
709
  "session",
@@ -1,6 +1,8 @@
1
1
  from __future__ import annotations
2
2
 
3
- from typing import Callable, Optional, Sequence
3
+ import sys
4
+ from ast import Not
5
+ from typing import Callable, Literal, Optional, Sequence
4
6
 
5
7
  from codegen.models import (
6
8
  AST,
@@ -28,7 +30,7 @@ from sera.models import (
28
30
  Schema,
29
31
  )
30
32
  from sera.models._property import SystemControlledMode
31
- from sera.typing import ObjectPath
33
+ from sera.typing import GLOBAL_IDENTS, ObjectPath
32
34
 
33
35
 
34
36
  def make_python_enums(
@@ -144,6 +146,7 @@ def make_python_data_model(
144
146
  program: Program,
145
147
  slf: expr.ExprIdent,
146
148
  cls: Class,
149
+ mode: Literal["create", "update"],
147
150
  prop: DataProperty | ObjectProperty,
148
151
  ):
149
152
  value = PredefinedFn.attr_getter(slf, expr.ExprIdent(prop.name))
@@ -194,7 +197,7 @@ def make_python_data_model(
194
197
  prop.datatype.get_python_type().type,
195
198
  )(value)
196
199
 
197
- if prop.data.is_private:
200
+ if mode == "update" and prop.data.is_private:
198
201
  # if the property is private and it's UNSET, we cannot transform it to the database type
199
202
  # and has to use the UNSET value (the update query will ignore this field)
200
203
  program.import_("sera.typing.UNSET", True)
@@ -208,7 +211,133 @@ def make_python_data_model(
208
211
  value = converted_value
209
212
  return value
210
213
 
211
- def make_upsert(program: Program, cls: Class):
214
+ def make_uscp_func(
215
+ cls: Class,
216
+ mode: Literal["create", "update"],
217
+ ast: AST,
218
+ ident_manager: ImportHelper,
219
+ ):
220
+ func = ast.func(
221
+ "update_system_controlled_props",
222
+ [
223
+ DeferredVar.simple("self"),
224
+ DeferredVar.simple(
225
+ "conn",
226
+ ident_manager.use("ASGIConnection"),
227
+ ),
228
+ ],
229
+ )
230
+
231
+ assign_user = False
232
+
233
+ for prop in cls.properties.values():
234
+ if prop.data.system_controlled is None:
235
+ continue
236
+
237
+ update_func = None
238
+ if mode == "create":
239
+ if prop.data.system_controlled.on_create_bypass is not None:
240
+ # by-pass the update function are handled later
241
+ continue
242
+
243
+ if prop.data.system_controlled.is_on_create_value_updated():
244
+ update_func = (
245
+ prop.data.system_controlled.get_on_create_update_func()
246
+ )
247
+ else:
248
+ if prop.data.system_controlled.on_update_bypass is not None:
249
+ # by-pass the update function are handled later
250
+ continue
251
+ if prop.data.system_controlled.is_on_update_value_updated():
252
+ update_func = (
253
+ prop.data.system_controlled.get_on_update_update_func()
254
+ )
255
+
256
+ if update_func is None:
257
+ continue
258
+
259
+ if update_func.func == "getattr":
260
+ if update_func.args[0] == "user":
261
+ if len(update_func.args) != 2:
262
+ raise NotImplementedError(
263
+ f"Unsupported update function: {update_func.func} with args {update_func.args}"
264
+ )
265
+ if not assign_user:
266
+ func(
267
+ stmt.AssignStatement(
268
+ expr.ExprIdent("user"),
269
+ PredefinedFn.item_getter(
270
+ PredefinedFn.attr_getter(
271
+ expr.ExprIdent("conn"),
272
+ expr.ExprIdent("scope"),
273
+ ),
274
+ expr.ExprConstant("user"),
275
+ ),
276
+ )
277
+ )
278
+ assign_user = True
279
+ epr = PredefinedFn.attr_getter(
280
+ expr.ExprIdent("user"), expr.ExprIdent(update_func.args[1])
281
+ )
282
+ elif update_func.args[0] == "self":
283
+ epr = PredefinedFn.attr_getter(
284
+ expr.ExprIdent("self"), expr.ExprIdent(update_func.args[1])
285
+ )
286
+ else:
287
+ raise NotImplementedError(
288
+ f"Unsupported update function: {update_func.func} with args {update_func.args}"
289
+ )
290
+ else:
291
+ raise NotImplementedError(update_func.func)
292
+
293
+ smt = stmt.AssignStatement(
294
+ PredefinedFn.attr_getter(
295
+ expr.ExprIdent("self"), expr.ExprIdent(prop.name)
296
+ ),
297
+ epr,
298
+ )
299
+ func(smt)
300
+
301
+ # handle the by-pass properties here
302
+ for prop in cls.properties.values():
303
+ if prop.data.system_controlled is None:
304
+ continue
305
+
306
+ update_func = None
307
+ if mode == "create":
308
+ if prop.data.system_controlled.on_create_bypass is None:
309
+ # non by-pass the update function are handled earlier
310
+ continue
311
+
312
+ if prop.data.system_controlled.is_on_create_value_updated():
313
+ update_func = (
314
+ prop.data.system_controlled.get_on_create_update_func()
315
+ )
316
+ else:
317
+ if prop.data.system_controlled.on_update_bypass is None:
318
+ # non by-pass the update function are handled earlier
319
+ continue
320
+
321
+ if prop.data.system_controlled.is_on_update_value_updated():
322
+ update_func = (
323
+ prop.data.system_controlled.get_on_update_update_func()
324
+ )
325
+
326
+ if update_func is None:
327
+ continue
328
+
329
+ raise NotImplementedError("We haven't handled the by-pass properties yet.")
330
+
331
+ func(
332
+ stmt.AssignStatement(
333
+ PredefinedFn.attr_getter(
334
+ expr.ExprIdent("self"), expr.ExprIdent("_is_scp_updated")
335
+ ),
336
+ expr.ExprConstant(True),
337
+ )
338
+ )
339
+
340
+ def make_create(program: Program, cls: Class):
212
341
  program.import_("__future__.annotations", True)
213
342
  program.import_("msgspec", False)
214
343
  if cls.db is not None:
@@ -221,52 +350,214 @@ def make_python_data_model(
221
350
 
222
351
  ident_manager = ImportHelper(
223
352
  program,
224
- {
225
- "UNSET": "sera.typing.UNSET",
226
- },
353
+ GLOBAL_IDENTS,
227
354
  )
228
355
 
229
- # property that normal users cannot set, but super users can
230
- has_restricted_system_controlled = any(
231
- prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
356
+ is_on_create_value_updated = any(
357
+ prop.data.system_controlled is not None
358
+ and prop.data.system_controlled.is_on_create_value_updated()
232
359
  for prop in cls.properties.values()
233
360
  )
234
- if has_restricted_system_controlled:
235
- program.import_("typing.TypedDict", True)
236
- program.root(
361
+ program.root.linebreak()
362
+ cls_ast = program.root.class_(
363
+ "Create" + cls.name,
364
+ [expr.ExprIdent("msgspec.Struct"), expr.ExprIdent("kw_only=True")],
365
+ )
366
+ for prop in cls.properties.values():
367
+ # Skip fields that are system-controlled (e.g., cached or derived fields)
368
+ # and cannot be updated based on information parsed from the request.
369
+ if (
370
+ prop.data.system_controlled is not None
371
+ and prop.data.system_controlled.is_on_create_ignored()
372
+ ):
373
+ continue
374
+
375
+ if isinstance(prop, DataProperty):
376
+ pytype = prop.get_data_model_datatype().get_python_type()
377
+ if prop.is_optional:
378
+ pytype = pytype.as_optional_type()
379
+
380
+ for dep in pytype.deps:
381
+ program.import_(dep, True)
382
+
383
+ pytype_type = pytype.type
384
+ if len(prop.data.constraints) > 0:
385
+ # if the property has constraints, we need to figure out
386
+ program.import_("typing.Annotated", True)
387
+ if len(prop.data.constraints) == 1:
388
+ pytype_type = f"Annotated[%s, %s]" % (
389
+ pytype_type,
390
+ prop.data.constraints[0].get_msgspec_constraint(),
391
+ )
392
+ else:
393
+ raise NotImplementedError(prop.data.constraints)
394
+
395
+ # private property are available for creating, but not for updating.
396
+ # so we do not need to skip it.
397
+ # if prop.data.is_private:
398
+ # program.import_("typing.Union", True)
399
+ # program.import_("sera.typing.UnsetType", True)
400
+ # program.import_("sera.typing.UNSET", True)
401
+ # pytype_type = f"Union[{pytype_type}, UnsetType]"
402
+
403
+ prop_default_value = None
404
+ # if prop.data.is_private:
405
+ # prop_default_value = expr.ExprIdent("UNSET")
406
+ if prop.default_value is not None:
407
+ prop_default_value = expr.ExprConstant(prop.default_value)
408
+ elif prop.default_factory is not None:
409
+ program.import_(prop.default_factory.pyfunc, True)
410
+ prop_default_value = expr.ExprFuncCall(
411
+ expr.ExprIdent("msgspec.field"),
412
+ [
413
+ PredefinedFn.keyword_assignment(
414
+ "default_factory",
415
+ expr.ExprIdent(prop.default_factory.pyfunc),
416
+ )
417
+ ],
418
+ )
419
+
420
+ cls_ast(
421
+ stmt.DefClassVarStatement(
422
+ prop.name, pytype_type, prop_default_value
423
+ )
424
+ )
425
+ elif isinstance(prop, ObjectProperty):
426
+ if prop.target.db is not None:
427
+ # if the target class is in the database, we expect the user to pass the foreign key for it.
428
+ pytype = (
429
+ assert_not_null(prop.target.get_id_property())
430
+ .get_data_model_datatype()
431
+ .get_python_type()
432
+ )
433
+ else:
434
+ pytype = PyTypeWithDep(
435
+ f"Create{prop.target.name}",
436
+ [
437
+ f"{target_pkg.module(prop.target.get_pymodule_name()).path}.Create{prop.target.name}"
438
+ ],
439
+ )
440
+
441
+ if prop.cardinality.is_star_to_many():
442
+ pytype = pytype.as_list_type()
443
+ elif prop.is_optional:
444
+ pytype = pytype.as_optional_type()
445
+
446
+ pytype_type = pytype.type
447
+
448
+ for dep in pytype.deps:
449
+ program.import_(dep, True)
450
+
451
+ cls_ast(stmt.DefClassVarStatement(prop.name, pytype_type))
452
+
453
+ if is_on_create_value_updated:
454
+ program.import_("typing.Optional", True)
455
+ program.import_("sera.typing.is_set", True)
456
+ cls_ast(
457
+ stmt.Comment(
458
+ "A marker to indicate that the system-controlled properties are updated"
459
+ ),
460
+ stmt.DefClassVarStatement(
461
+ "_is_scp_updated", "bool", expr.ExprConstant(False)
462
+ ),
237
463
  stmt.LineBreak(),
238
- lambda ast: ast.class_(
239
- "SystemControlledProps",
240
- [expr.ExprIdent("TypedDict")],
464
+ lambda ast: ast.func(
465
+ "__post_init__",
466
+ [
467
+ DeferredVar.simple("self"),
468
+ ],
241
469
  )(
242
- *[
243
- stmt.DefClassVarStatement(
244
- prop.name,
470
+ stmt.AssignStatement(
471
+ PredefinedFn.attr_getter(
472
+ expr.ExprIdent("self"), expr.ExprIdent("_is_scp_updated")
473
+ ),
474
+ expr.ExprConstant(False),
475
+ ),
476
+ ),
477
+ stmt.LineBreak(),
478
+ lambda ast: make_uscp_func(cls, "create", ast, ident_manager),
479
+ )
480
+
481
+ cls_ast(
482
+ stmt.LineBreak(),
483
+ lambda ast00: ast00.func(
484
+ "to_db",
485
+ [
486
+ DeferredVar.simple("self"),
487
+ ],
488
+ return_type=expr.ExprIdent(
489
+ f"{cls.name}DB" if cls.db is not None else cls.name
490
+ ),
491
+ )(
492
+ (
493
+ stmt.AssertionStatement(
494
+ PredefinedFn.attr_getter(
495
+ expr.ExprIdent("self"),
496
+ expr.ExprIdent("_is_scp_updated"),
497
+ ),
498
+ expr.ExprConstant(
499
+ "The model data must be verified before converting to db model"
500
+ ),
501
+ )
502
+ if is_on_create_value_updated
503
+ else None
504
+ ),
505
+ lambda ast10: ast10.return_(
506
+ expr.ExprFuncCall(
507
+ expr.ExprIdent(
508
+ f"{cls.name}DB" if cls.db is not None else cls.name
509
+ ),
510
+ [
245
511
  (
246
- prop.get_data_model_datatype().get_python_type().type
247
- if isinstance(prop, DataProperty)
248
- else assert_not_null(prop.target.get_id_property())
249
- .get_data_model_datatype()
250
- .get_python_type()
251
- .type
252
- ),
253
- )
254
- for prop in cls.properties.values()
255
- if prop.data.is_system_controlled
256
- == SystemControlledMode.RESTRICTED
257
- ],
512
+ ident_manager.use("UNSET")
513
+ if prop.data.system_controlled is not None
514
+ and prop.data.system_controlled.is_on_create_ignored()
515
+ else to_db_type_conversion(
516
+ program, expr.ExprIdent("self"), cls, "create", prop
517
+ )
518
+ )
519
+ for prop in cls.properties.values()
520
+ ],
521
+ )
258
522
  ),
523
+ ),
524
+ )
525
+
526
+ def make_update(program: Program, cls: Class):
527
+ program.import_("__future__.annotations", True)
528
+ program.import_("msgspec", False)
529
+ if cls.db is not None:
530
+ # if the class is stored in the database, we need to import the database module
531
+ program.import_(
532
+ app.models.db.path + f".{cls.get_pymodule_name()}.{cls.name}",
533
+ True,
534
+ alias=f"{cls.name}DB",
259
535
  )
260
536
 
537
+ ident_manager = ImportHelper(
538
+ program,
539
+ GLOBAL_IDENTS,
540
+ )
541
+
542
+ # property that normal users cannot set, but super users can
543
+ is_on_update_value_updated = any(
544
+ prop.data.system_controlled is not None
545
+ and prop.data.system_controlled.is_on_update_value_updated()
546
+ for prop in cls.properties.values()
547
+ )
548
+
261
549
  program.root.linebreak()
262
550
  cls_ast = program.root.class_(
263
- "Upsert" + cls.name,
551
+ "Update" + cls.name,
264
552
  [expr.ExprIdent("msgspec.Struct"), expr.ExprIdent("kw_only=True")],
265
553
  )
266
554
  for prop in cls.properties.values():
267
- # a field that is fully controlled by the system (e.g., cached or derived fields)
268
- # aren't allowed to be set by the users, so we skip them
269
- if prop.data.is_system_controlled == SystemControlledMode.AUTO:
555
+ # Skip fields that are system-controlled (e.g., cached or derived fields)
556
+ # and cannot be updated based on information parsed from the request.
557
+ if (
558
+ prop.data.system_controlled is not None
559
+ and prop.data.system_controlled.is_on_update_ignored()
560
+ ):
270
561
  continue
271
562
 
272
563
  if isinstance(prop, DataProperty):
@@ -289,20 +580,14 @@ def make_python_data_model(
289
580
  else:
290
581
  raise NotImplementedError(prop.data.constraints)
291
582
 
292
- if (
293
- prop.data.is_private
294
- or prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
295
- ):
583
+ if prop.data.is_private:
296
584
  program.import_("typing.Union", True)
297
585
  program.import_("sera.typing.UnsetType", True)
298
586
  program.import_("sera.typing.UNSET", True)
299
587
  pytype_type = f"Union[{pytype_type}, UnsetType]"
300
588
 
301
589
  prop_default_value = None
302
- if (
303
- prop.data.is_private
304
- or prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
305
- ):
590
+ if prop.data.is_private:
306
591
  prop_default_value = expr.ExprIdent("UNSET")
307
592
  elif prop.default_value is not None:
308
593
  prop_default_value = expr.ExprConstant(prop.default_value)
@@ -333,9 +618,9 @@ def make_python_data_model(
333
618
  )
334
619
  else:
335
620
  pytype = PyTypeWithDep(
336
- f"Upsert{prop.target.name}",
621
+ f"Update{prop.target.name}",
337
622
  [
338
- f"{target_pkg.module(prop.target.get_pymodule_name()).path}.Upsert{prop.target.name}"
623
+ f"{target_pkg.module(prop.target.get_pymodule_name()).path}.Update{prop.target.name}"
339
624
  ],
340
625
  )
341
626
 
@@ -345,77 +630,38 @@ def make_python_data_model(
345
630
  pytype = pytype.as_optional_type()
346
631
 
347
632
  pytype_type = pytype.type
348
- prop_default_value = None
349
- if prop.data.is_system_controlled == SystemControlledMode.RESTRICTED:
350
- program.import_("typing.Union", True)
351
- program.import_("sera.typing.UnsetType", True)
352
- program.import_("sera.typing.UNSET", True)
353
- pytype_type = f"Union[{pytype_type}, UnsetType]"
354
- prop_default_value = expr.ExprIdent("UNSET")
355
633
 
356
634
  for dep in pytype.deps:
357
635
  program.import_(dep, True)
358
636
 
359
- cls_ast(
360
- stmt.DefClassVarStatement(
361
- prop.name, pytype_type, prop_default_value
362
- )
363
- )
637
+ cls_ast(stmt.DefClassVarStatement(prop.name, pytype_type))
364
638
 
365
- if has_restricted_system_controlled:
639
+ if is_on_update_value_updated:
366
640
  program.import_("typing.Optional", True)
367
641
  program.import_("sera.typing.is_set", True)
368
642
  cls_ast(
369
- stmt.LineBreak(),
370
- lambda ast: ast.func(
371
- "__post_init__",
372
- [
373
- DeferredVar.simple("self"),
374
- ],
375
- )(
376
- *[
377
- stmt.AssignStatement(
378
- PredefinedFn.attr_getter(
379
- expr.ExprIdent("self"), expr.ExprIdent(prop.name)
380
- ),
381
- expr.ExprIdent("UNSET"),
382
- )
383
- for prop in cls.properties.values()
384
- if prop.data.is_system_controlled
385
- == SystemControlledMode.RESTRICTED
386
- ]
643
+ stmt.Comment(
644
+ "A marker to indicate that the system-controlled properties are updated"
645
+ ),
646
+ stmt.DefClassVarStatement(
647
+ "_is_scp_updated", "bool", expr.ExprConstant(False)
387
648
  ),
388
649
  stmt.LineBreak(),
389
650
  lambda ast: ast.func(
390
- "update_system_controlled_props",
651
+ "__post_init__",
391
652
  [
392
653
  DeferredVar.simple("self"),
393
- DeferredVar.simple(
394
- "data",
395
- expr.ExprIdent("Optional[SystemControlledProps]"),
396
- ),
397
654
  ],
398
655
  )(
399
- lambda ast00: ast00.if_(
400
- expr.ExprNegation(
401
- expr.ExprIs(expr.ExprIdent("data"), expr.ExprConstant(None))
656
+ stmt.AssignStatement(
657
+ PredefinedFn.attr_getter(
658
+ expr.ExprIdent("self"), expr.ExprIdent("_is_scp_updated")
402
659
  ),
403
- )(
404
- *[
405
- stmt.AssignStatement(
406
- PredefinedFn.attr_getter(
407
- expr.ExprIdent("self"), expr.ExprIdent(prop.name)
408
- ),
409
- PredefinedFn.item_getter(
410
- expr.ExprIdent("data"), expr.ExprConstant(prop.name)
411
- ),
412
- )
413
- for prop in cls.properties.values()
414
- if prop.data.is_system_controlled
415
- == SystemControlledMode.RESTRICTED
416
- ]
660
+ expr.ExprConstant(False),
417
661
  ),
418
662
  ),
663
+ stmt.LineBreak(),
664
+ lambda ast: make_uscp_func(cls, "update", ast, ident_manager),
419
665
  )
420
666
 
421
667
  cls_ast(
@@ -443,15 +689,15 @@ def make_python_data_model(
443
689
  ],
444
690
  )
445
691
  for prop in cls.properties.values()
446
- if prop.data.is_system_controlled
447
- == SystemControlledMode.RESTRICTED
692
+ if prop.data.system_controlled is not None
693
+ and prop.data.system_controlled.is_on_update_value_updated()
448
694
  ]
449
695
  ),
450
696
  expr.ExprConstant(
451
697
  "The model data must be verified before converting to db model"
452
698
  ),
453
699
  )
454
- if has_restricted_system_controlled
700
+ if is_on_update_value_updated
455
701
  else None
456
702
  ),
457
703
  lambda ast10: ast10.return_(
@@ -461,12 +707,12 @@ def make_python_data_model(
461
707
  ),
462
708
  [
463
709
  (
464
- to_db_type_conversion(
465
- program, expr.ExprIdent("self"), cls, prop
710
+ ident_manager.use("UNSET")
711
+ if prop.data.system_controlled is not None
712
+ and prop.data.system_controlled.is_on_update_ignored()
713
+ else to_db_type_conversion(
714
+ program, expr.ExprIdent("self"), cls, "update", prop
466
715
  )
467
- if prop.data.is_system_controlled
468
- != SystemControlledMode.AUTO
469
- else ident_manager.use("UNSET")
470
716
  )
471
717
  for prop in cls.properties.values()
472
718
  ],
@@ -617,7 +863,7 @@ def make_python_data_model(
617
863
  PredefinedFn.tuple(
618
864
  [
619
865
  to_db_type_conversion(
620
- program, expr.ExprIdent("self"), cls, prop
866
+ program, expr.ExprIdent("self"), cls, "create", prop
621
867
  )
622
868
  for prop in cls.properties.values()
623
869
  ]
@@ -631,7 +877,9 @@ def make_python_data_model(
631
877
  continue
632
878
 
633
879
  program = Program()
634
- make_upsert(program, cls)
880
+ make_create(program, cls)
881
+ program.root.linebreak()
882
+ make_update(program, cls)
635
883
  program.root.linebreak()
636
884
  make_normal(program, cls)
637
885
  target_pkg.module(cls.get_pymodule_name()).write(program)
sera/models/_parse.py CHANGED
@@ -5,6 +5,7 @@ from copy import deepcopy
5
5
  from pathlib import Path
6
6
  from typing import Sequence
7
7
 
8
+ import orjson
8
9
  import serde.yaml
9
10
 
10
11
  from sera.models._class import Class, ClassDBMapInfo, Index
@@ -28,9 +29,11 @@ from sera.models._property import (
28
29
  DataProperty,
29
30
  ForeignKeyOnDelete,
30
31
  ForeignKeyOnUpdate,
32
+ GetSCPropValueFunc,
31
33
  ObjectPropDBInfo,
32
34
  ObjectProperty,
33
35
  PropDataAttrs,
36
+ SystemControlledAttrs,
34
37
  SystemControlledMode,
35
38
  )
36
39
  from sera.models._schema import Schema
@@ -135,6 +138,9 @@ def _parse_property(
135
138
  is_system_controlled=SystemControlledMode(
136
139
  _data.get("is_system_controlled", SystemControlledMode.NO.value)
137
140
  ),
141
+ system_controlled=_parse_system_controlled_attrs(
142
+ _data.get("system_controlled")
143
+ ),
138
144
  )
139
145
 
140
146
  assert isinstance(prop, dict), prop
@@ -286,3 +292,59 @@ def _parse_default_factory(default_factory: dict | None) -> DefaultFactory | Non
286
292
  return DefaultFactory(
287
293
  pyfunc=default_factory["pyfunc"], tsfunc=default_factory["tsfunc"]
288
294
  )
295
+
296
+
297
+ def _parse_system_controlled_attrs(
298
+ attrs: dict | None,
299
+ ) -> SystemControlledAttrs | None:
300
+ if attrs is None:
301
+ return None
302
+ if not isinstance(attrs, dict):
303
+ raise NotImplementedError(attrs)
304
+
305
+ if "on_upsert" in attrs:
306
+ attrs = attrs.copy()
307
+ attrs.update(
308
+ {
309
+ "on_create": attrs["on_upsert"],
310
+ "on_create_bypass": attrs.get("on_upsert_bypass"),
311
+ "on_update": attrs["on_upsert"],
312
+ "on_update_bypass": attrs.get("on_upsert_bypass"),
313
+ }
314
+ )
315
+
316
+ if "on_create" not in attrs or "on_update" not in attrs:
317
+ raise ValueError(
318
+ "System controlled attributes must have 'on_create', 'on_update', or 'on_upsert' must be defined."
319
+ )
320
+
321
+ keys = {}
322
+ for key in ["on_create", "on_update"]:
323
+ if attrs[key] == "ignored":
324
+ keys[key] = "ignored"
325
+ elif attrs[key].find(":") != -1:
326
+ func, args = attrs[key].split(":")
327
+ assert func == "getattr", f"Unsupported function: {func}"
328
+ args = orjson.loads(args)
329
+ keys[key] = GetSCPropValueFunc(
330
+ func=func,
331
+ args=args,
332
+ )
333
+ else:
334
+ raise ValueError(
335
+ f"System controlled attribute '{key}' must be 'ignored' or a function call in the format '<funcname>:<args>'."
336
+ )
337
+
338
+ if attrs[key + "_bypass"] is not None:
339
+ if not isinstance(attrs[key + "_bypass"], str):
340
+ raise ValueError(
341
+ f"System controlled attribute '{key}_bypass' must be a string."
342
+ )
343
+ keys[key + "_bypass"] = attrs[key + "_bypass"]
344
+
345
+ return SystemControlledAttrs(
346
+ on_create=keys["on_create"],
347
+ on_create_bypass=keys.get("on_create_bypass"),
348
+ on_update=keys["on_update"],
349
+ on_update_bypass=keys.get("on_update_bypass"),
350
+ )
sera/models/_property.py CHANGED
@@ -69,6 +69,43 @@ class SystemControlledMode(str, Enum):
69
69
  NO = "no"
70
70
 
71
71
 
72
+ @dataclass(kw_only=True)
73
+ class GetSCPropValueFunc:
74
+
75
+ func: Literal["getattr"]
76
+ args: tuple[str, ...]
77
+
78
+
79
+ @dataclass(kw_only=True)
80
+ class SystemControlledAttrs:
81
+ """Attributes for a system-controlled property."""
82
+
83
+ on_create_bypass: Optional[str]
84
+ on_create: Literal["ignored"] | GetSCPropValueFunc
85
+ on_update_bypass: Optional[str]
86
+ on_update: Literal["ignored"] | GetSCPropValueFunc
87
+
88
+ def is_on_create_value_updated(self) -> bool:
89
+ return isinstance(self.on_create, GetSCPropValueFunc)
90
+
91
+ def get_on_create_update_func(self) -> GetSCPropValueFunc:
92
+ assert isinstance(self.on_create, GetSCPropValueFunc)
93
+ return self.on_create
94
+
95
+ def is_on_create_ignored(self) -> bool:
96
+ return self.on_create == "ignored"
97
+
98
+ def is_on_update_ignored(self) -> bool:
99
+ return self.on_update == "ignored"
100
+
101
+ def is_on_update_value_updated(self) -> bool:
102
+ return isinstance(self.on_update, GetSCPropValueFunc)
103
+
104
+ def get_on_update_update_func(self) -> GetSCPropValueFunc:
105
+ assert isinstance(self.on_update, GetSCPropValueFunc)
106
+ return self.on_update
107
+
108
+
72
109
  @dataclass(kw_only=True)
73
110
  class PropDataAttrs:
74
111
  """Storing other attributes for generating data model (upsert & public) -- this is different from a db model"""
@@ -87,6 +124,9 @@ class PropDataAttrs:
87
124
  # whether this property is controlled by the system or not
88
125
  is_system_controlled: SystemControlledMode = SystemControlledMode.NO
89
126
 
127
+ # if this property is controlled by the system, the attributes for the system-controlled property
128
+ system_controlled: Optional[SystemControlledAttrs] = None
129
+
90
130
 
91
131
  @dataclass(kw_only=True)
92
132
  class Property:
sera/typing.py CHANGED
@@ -30,3 +30,11 @@ UNSET: Any = msgspec.UNSET
30
30
  def is_set(value: Union[T, UnsetType]) -> TypeGuard[T]:
31
31
  """Typeguard to check if a value is set (not UNSET)"""
32
32
  return value is not UNSET
33
+
34
+
35
+ # Global identifiers for codegen
36
+ GLOBAL_IDENTS = {
37
+ "AsyncSession": "sqlalchemy.ext.asyncio.AsyncSession",
38
+ "ASGIConnection": "litestar.connection.ASGIConnection",
39
+ "UNSET": "sera.typing.UNSET",
40
+ }
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.13.1
3
+ Version: 1.14.0
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -9,7 +9,7 @@ Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Programming Language :: Python :: 3.13
11
11
  Requires-Dist: black (>=25.1.0,<26.0.0)
12
- Requires-Dist: codegen-2 (>=2.11.0,<3.0.0)
12
+ Requires-Dist: codegen-2 (>=2.11.1,<3.0.0)
13
13
  Requires-Dist: isort (>=6.0.1,<7.0.0)
14
14
  Requires-Dist: litestar (>=2.15.1,<3.0.0)
15
15
  Requires-Dist: loguru (>=0.7.0,<0.8.0)
@@ -1,7 +1,7 @@
1
1
  sera/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sera/constants.py,sha256=mzAaMyIx8TJK0-RYYJ5I24C4s0Uvj26OLMJmBo0pxHI,123
3
3
  sera/exports/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
- sera/exports/schema.py,sha256=3F5Kx3r0zOmiVpDf9vnzGhtdcuF-vfgMhBbp4Ko4fgw,4740
4
+ sera/exports/schema.py,sha256=EAFKwULu8Nmb3IUBMCyt7M6YM4TGPo8yRRRu_KxBTxs,7045
5
5
  sera/exports/test.py,sha256=jK1EJmLGiy7eREpnY_68IIVRH43uH8S_u5Z7STPbXOM,2002
6
6
  sera/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  sera/libs/api_helper.py,sha256=47y1kcwk3Xd2ZEMnUj_0OwCuUmgwOs5kYrE95BDVUn4,5411
@@ -16,8 +16,8 @@ sera/libs/middlewares/uscp.py,sha256=H5umW8iEQSCdb_MJ5Im49kxg1E7TpxSg1p2_2A5zI1U
16
16
  sera/make/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
17
17
  sera/make/__main__.py,sha256=bt-gDF8E026OWc2zqr9_a3paMOiDkFd3ybWn8ltL2g0,1448
18
18
  sera/make/make_app.py,sha256=n9NtW73O3s_5Q31VHIRmnd-jEIcpDO7ksAsOdovde2s,5999
19
- sera/make/make_python_api.py,sha256=sf-J5Pt1LTyM_H-SgXSAvKjEMDrRA6WnDpgDPbJG360,26896
20
- sera/make/make_python_model.py,sha256=XWoNmfVNfdzF3lwEaItPZjo27ibqL9yjHA3a0vrHsnA,51708
19
+ sera/make/make_python_api.py,sha256=9Pr5IxhC5iFkNq8DEh8Wfqa01AGm9YUxf9qdLmMdf_Y,26875
20
+ sera/make/make_python_model.py,sha256=Q9lirILQS_J00FfRBE8JZvMZTsfkZ6iziHknwxpU-fo,61420
21
21
  sera/make/make_python_services.py,sha256=0ZpWLwQ7Nwfn8BXAikAB4JRpNknpSJyJgY5b1cjtxV4,2073
22
22
  sera/make/make_typescript_model.py,sha256=ugDdSTw_1ayHLuL--92RQ8hf_D-dpJtnvmUZNxcwcDs,63687
23
23
  sera/misc/__init__.py,sha256=Dh4uDq0D4N53h3zhvmwfa5a0TPVRSUvLzb0hkFuPirk,411
@@ -32,10 +32,10 @@ sera/models/_default.py,sha256=ABggW6qdPR4ZDqIPJdJ0GCGQ-7kfsfZmQ_DchgZEa-I,137
32
32
  sera/models/_enum.py,sha256=sy0q7E646F-APsqrVQ52r1fAQ_DCAeaNq5YM5QN3zIk,2070
33
33
  sera/models/_module.py,sha256=8QRSCubZmdDP9rL58rGAS6X5VCrkc1ZHvuMu1I1KrWk,5043
34
34
  sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
35
- sera/models/_parse.py,sha256=q_YZ7PrHWIN85_WW-fPP7-2gLXlGWM2-EIdbYXuG7Xg,10052
36
- sera/models/_property.py,sha256=4y9F58D6DoX25-6aWPBRiE72nCPQy0KWlGNDTZXSV-8,6038
35
+ sera/models/_parse.py,sha256=IhDBmtdwilI_SOV-Rj_DGiNUHTH_sE1s7-OhoGG0hK0,12154
36
+ sera/models/_property.py,sha256=yITA3B5lsFTMLMwXmIjPzEun_F_Z1NoRx9lroQEWXHM,7320
37
37
  sera/models/_schema.py,sha256=VxJEiqgVvbXgcSUK4UW6JnRcggk4nsooVSE6MyXmfNY,1636
38
- sera/typing.py,sha256=Q4QMfbtfrCjC9tFfsZPhsAnbNX4lm4NHQ9lmjNXYdV0,772
39
- sera_2-1.13.1.dist-info/METADATA,sha256=xM2la_NEr7esZtrLlTbwARiHlqp43Y0VVqQR4zuFEmo,867
40
- sera_2-1.13.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
41
- sera_2-1.13.1.dist-info/RECORD,,
38
+ sera/typing.py,sha256=Fl4-UzLJu1GdLLk_g87fA7nT9wQGelNnGzag6dg_0gs,980
39
+ sera_2-1.14.0.dist-info/METADATA,sha256=rcYbdo7hCFXIijgTD_qnQ8RAAEDTYpbZHvDHrEmicOU,867
40
+ sera_2-1.14.0.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
41
+ sera_2-1.14.0.dist-info/RECORD,,