sera-2 1.1.0__py3-none-any.whl → 1.2.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.
@@ -1,21 +1,162 @@
1
1
  from __future__ import annotations
2
2
 
3
+ from operator import is_
3
4
  from typing import Sequence
4
5
 
5
- from codegen.models import DeferredVar, PredefinedFn, Program, expr, stmt
6
- from sera.misc import assert_isinstance, filter_duplication
7
- from sera.models import DataProperty, ObjectProperty, Package, Schema
6
+ from codegen.models import AST, DeferredVar, PredefinedFn, Program, expr, stmt
7
+ from sera.misc import (
8
+ assert_isinstance,
9
+ assert_not_null,
10
+ filter_duplication,
11
+ to_snake_case,
12
+ )
13
+ from sera.models import (
14
+ Cardinality,
15
+ Class,
16
+ DataProperty,
17
+ ObjectProperty,
18
+ Package,
19
+ Schema,
20
+ )
8
21
 
9
22
 
10
23
  def make_python_data_model(schema: Schema, target_pkg: Package):
11
24
  """Generate public classes for the API from the schema."""
12
- for cls in schema.topological_sort():
13
- program = Program()
25
+ app = target_pkg.app
26
+
27
+ def from_db_type_conversion(
28
+ record: expr.ExprIdent, prop: DataProperty | ObjectProperty
29
+ ):
30
+ value = PredefinedFn.attr_getter(record, expr.ExprIdent(prop.name))
31
+ if isinstance(prop, ObjectProperty) and prop.target.db is not None:
32
+ if prop.cardinality.is_star_to_many():
33
+ return PredefinedFn.map_list(
34
+ value,
35
+ lambda item: expr.ExprFuncCall(
36
+ PredefinedFn.attr_getter(
37
+ expr.ExprIdent(prop.target.name), expr.ExprIdent("from_db")
38
+ ),
39
+ [PredefinedFn.attr_getter(item, expr.ExprIdent(prop.name))],
40
+ ),
41
+ )
42
+ else:
43
+ return expr.ExprFuncCall(
44
+ PredefinedFn.attr_getter(
45
+ expr.ExprIdent(prop.target.name), expr.ExprIdent("from_db")
46
+ ),
47
+ [value],
48
+ )
49
+
50
+ return value
51
+
52
+ def to_db_type_conversion(
53
+ program: Program,
54
+ slf: expr.ExprIdent,
55
+ cls: Class,
56
+ prop: DataProperty | ObjectProperty,
57
+ ):
58
+ value = PredefinedFn.attr_getter(slf, expr.ExprIdent(prop.name))
59
+ if (
60
+ isinstance(prop, ObjectProperty)
61
+ and prop.target.db is not None
62
+ and prop.cardinality == Cardinality.MANY_TO_MANY
63
+ ):
64
+ # we have to use the associated object
65
+ AssociationTable = f"{cls.name}{prop.target.name}"
66
+ program.import_(
67
+ app.models.db.path
68
+ + f".{to_snake_case(AssociationTable)}.{AssociationTable}",
69
+ True,
70
+ )
71
+ return PredefinedFn.map_list(
72
+ value,
73
+ lambda item: expr.ExprFuncCall(
74
+ expr.ExprIdent(AssociationTable),
75
+ [PredefinedFn.keyword_assignment(f"{prop.name}_id", item)],
76
+ ),
77
+ )
78
+ return value
79
+
80
+ def make_create(program: Program, cls: Class):
14
81
  program.import_("__future__.annotations", True)
15
82
  program.import_("msgspec", False)
16
- # program.import_("dataclasses.dataclass", True)
83
+ program.import_(
84
+ app.models.db.path + f".{cls.get_pymodule_name()}.{cls.name}",
85
+ True,
86
+ alias=f"{cls.name}DB",
87
+ )
88
+ cls_ast = program.root.class_(
89
+ "Upsert" + cls.name, [expr.ExprIdent("msgspec.Struct")]
90
+ )
91
+ for prop in cls.properties.values():
92
+ # this is a create object, so users can create private field
93
+ # hence, we do not check for prop.is_private
94
+ # if prop.is_private:
95
+ # continue
96
+
97
+ if isinstance(prop, DataProperty):
98
+ pytype = prop.datatype.get_python_type()
99
+ if pytype.dep is not None:
100
+ program.import_(pytype.dep, True)
101
+ cls_ast(stmt.DefClassVarStatement(prop.name, pytype.type))
102
+ elif isinstance(prop, ObjectProperty):
103
+ if prop.target.db is not None:
104
+ # if the target class is in the database, we expect the user to pass the foreign key for it.
105
+ pytype = (
106
+ assert_not_null(prop.target.get_id_property())
107
+ .datatype.get_python_type()
108
+ .type
109
+ )
110
+ else:
111
+ program.import_(
112
+ f"{target_pkg.module(prop.target.get_pymodule_name()).path}.{prop.target.name}",
113
+ is_import_attr=True,
114
+ )
115
+ pytype = prop.target.name
116
+
117
+ if prop.cardinality.is_star_to_many():
118
+ pytype = f"list[{pytype}]"
119
+ cls_ast(stmt.DefClassVarStatement(prop.name, pytype))
120
+
121
+ # has_to_db = True
122
+ # if any(prop for prop in cls.properties.values() if isinstance(prop, ObjectProperty) and prop.cardinality == Cardinality.MANY_TO_MANY):
123
+ # # if the class has many-to-many relationship, we need to
124
+ cls_ast(
125
+ stmt.LineBreak(),
126
+ lambda ast00: ast00.func(
127
+ "to_db",
128
+ [
129
+ DeferredVar.simple("self"),
130
+ ],
131
+ return_type=expr.ExprIdent(f"{cls.name}DB"),
132
+ )(
133
+ lambda ast10: ast10.return_(
134
+ expr.ExprFuncCall(
135
+ expr.ExprIdent(f"{cls.name}DB"),
136
+ [
137
+ to_db_type_conversion(
138
+ program, expr.ExprIdent("self"), cls, prop
139
+ )
140
+ for prop in cls.properties.values()
141
+ ],
142
+ )
143
+ )
144
+ ),
145
+ )
146
+
147
+ def make_update(program: Program, cls: Class):
148
+ if not cls.is_public:
149
+ # skip classes that are not public
150
+ return
151
+
152
+ program.import_("__future__.annotations", True)
153
+ program.import_("msgspec", False)
154
+ program.import_(
155
+ app.models.db.path + f".{cls.get_pymodule_name()}.{cls.name}",
156
+ True,
157
+ alias=f"{cls.name}DB",
158
+ )
17
159
 
18
- # program.root(stmt.PythonStatement("@dataclass"))
19
160
  cls_ast = program.root.class_(cls.name, [expr.ExprIdent("msgspec.Struct")])
20
161
  for prop in cls.properties.values():
21
162
  if prop.is_private:
@@ -32,9 +173,42 @@ def make_python_data_model(schema: Schema, target_pkg: Package):
32
173
  f"{target_pkg.module(prop.target.get_pymodule_name()).path}.{prop.target.name}",
33
174
  is_import_attr=True,
34
175
  )
35
- pytype = prop.target.name
176
+ if prop.cardinality.is_star_to_many():
177
+ pytype = f"list[{prop.target.name}]"
178
+ else:
179
+ pytype = prop.target.name
36
180
  cls_ast(stmt.DefClassVarStatement(prop.name, pytype))
37
181
 
182
+ cls_ast(
183
+ stmt.LineBreak(),
184
+ stmt.PythonDecoratorStatement(
185
+ expr.ExprFuncCall(expr.ExprIdent("classmethod"), [])
186
+ ),
187
+ lambda ast00: ast00.func(
188
+ "from_db",
189
+ [
190
+ DeferredVar.simple("cls"),
191
+ DeferredVar.simple("record", expr.ExprIdent(f"{cls.name}DB")),
192
+ ],
193
+ )(
194
+ lambda ast10: ast10.return_(
195
+ expr.ExprFuncCall(
196
+ expr.ExprIdent("cls"),
197
+ [
198
+ from_db_type_conversion(expr.ExprIdent("record"), prop)
199
+ for prop in cls.properties.values()
200
+ if not prop.is_private
201
+ ],
202
+ )
203
+ )
204
+ ),
205
+ )
206
+
207
+ for cls in schema.topological_sort():
208
+ program = Program()
209
+ make_create(program, cls)
210
+ program.root.linebreak()
211
+ make_update(program, cls)
38
212
  target_pkg.module(cls.get_pymodule_name()).write(program)
39
213
 
40
214
 
@@ -53,7 +227,7 @@ def make_python_relational_model(
53
227
  program.import_("__future__.annotations", True)
54
228
  program.import_("sera.libs.base_orm.BaseORM", True)
55
229
  program.import_("sera.libs.base_orm.create_engine", True)
56
- program.import_("contextlib.contextmanager", True)
230
+ program.import_("sqlalchemy.orm.DeclarativeBase", True)
57
231
  program.import_("sqlalchemy.orm.Session", True)
58
232
 
59
233
  # assume configuration for the app at the top level
@@ -91,7 +265,9 @@ def make_python_relational_model(
91
265
 
92
266
  type_map.append((expr.ExprIdent(type), expr.ExprIdent(maptype)))
93
267
 
94
- cls_ast = program.root.class_("Base", [expr.ExprIdent("BaseORM")])(
268
+ cls_ast = program.root.class_(
269
+ "Base", [expr.ExprIdent("DeclarativeBase"), expr.ExprIdent("BaseORM")]
270
+ )(
95
271
  stmt.DefClassVarStatement(
96
272
  "type_annotation_map", "dict", PredefinedFn.dict(type_map)
97
273
  ),
@@ -118,8 +294,7 @@ def make_python_relational_model(
118
294
  )
119
295
 
120
296
  program.root.linebreak()
121
- program.root.python_stmt("@contextmanager")
122
- program.root.func("get_session", [])(
297
+ program.root.func("get_session", [], is_async=True)(
123
298
  lambda ast00: ast00.python_stmt("with Session(engine) as session:")(
124
299
  lambda ast01: ast01.python_stmt("yield session")
125
300
  )
@@ -139,7 +314,6 @@ def make_python_relational_model(
139
314
  program.import_("sqlalchemy.orm.MappedAsDataclass", True)
140
315
  program.import_("sqlalchemy.orm.mapped_column", True)
141
316
  program.import_("sqlalchemy.orm.Mapped", True)
142
- program.import_("typing.ClassVar", True)
143
317
  program.import_(f"{target_pkg.path}.base.Base", True)
144
318
 
145
319
  cls_ast = program.root.class_(
@@ -148,7 +322,7 @@ def make_python_relational_model(
148
322
  cls_ast(
149
323
  stmt.DefClassVarStatement(
150
324
  "__tablename__",
151
- type="ClassVar[str]",
325
+ type=None,
152
326
  value=expr.ExprConstant(cls.db.table_name),
153
327
  ),
154
328
  stmt.LineBreak(),
@@ -189,94 +363,306 @@ def make_python_relational_model(
189
363
  propval = expr.ExprFuncCall(
190
364
  expr.ExprIdent("mapped_column"), propvalargs
191
365
  )
366
+ cls_ast(stmt.DefClassVarStatement(propname, proptype, propval))
192
367
  else:
193
368
  assert isinstance(prop, ObjectProperty)
194
- if prop.target.db is not None:
195
- # if the target class is in the database, we generate a foreign key for it.
196
- program.import_("sqlalchemy.ForeignKey", True)
197
-
198
- # we store this class in the database
199
- propname = f"{prop.name}_id"
200
- idprop = prop.target.get_id_property()
201
- assert idprop is not None
202
- idprop_pytype = idprop.datatype.get_sqlalchemy_type()
203
- if idprop_pytype.dep is not None:
204
- program.import_(idprop_pytype.dep, True)
205
-
206
- proptype = f"Mapped[{idprop_pytype.type}]"
207
-
208
- propvalargs: list[expr.Expr] = [
209
- expr.ExprConstant(f"{prop.target.db.table_name}.{idprop.name}")
210
- ]
211
- propvalargs.append(
369
+ make_python_relational_object_property(
370
+ program=program,
371
+ target_pkg=target_pkg,
372
+ cls_ast=cls_ast,
373
+ cls=cls,
374
+ prop=prop,
375
+ custom_types=custom_types,
376
+ )
377
+
378
+ target_pkg.module(cls.get_pymodule_name()).write(program)
379
+
380
+ # make a base class that implements the mapping for custom types
381
+ custom_types = filter_duplication(
382
+ custom_types, lambda p: (p.target.name, p.cardinality, p.is_optional, p.is_map)
383
+ )
384
+ make_base(custom_types)
385
+
386
+
387
+ def make_python_relational_object_property(
388
+ program: Program,
389
+ target_pkg: Package,
390
+ cls_ast: AST,
391
+ cls: Class,
392
+ prop: ObjectProperty,
393
+ custom_types: list[ObjectProperty],
394
+ ):
395
+ assert prop.db is not None
396
+ if prop.target.db is not None:
397
+ # if the target class is in the database, we generate a foreign key for it.
398
+ program.import_("sqlalchemy.orm.relationship", True)
399
+
400
+ if prop.cardinality == Cardinality.MANY_TO_MANY:
401
+ make_python_relational_object_property_many_to_many(
402
+ program, cls_ast, target_pkg, cls, prop
403
+ )
404
+ return
405
+
406
+ if prop.cardinality.is_star_to_many():
407
+ raise NotImplementedError()
408
+
409
+ # we store this class in the database
410
+ propname = f"{prop.name}_id"
411
+ idprop = prop.target.get_id_property()
412
+ assert idprop is not None
413
+ idprop_pytype = idprop.datatype.get_sqlalchemy_type()
414
+ if idprop_pytype.dep is not None:
415
+ program.import_(idprop_pytype.dep, True)
416
+
417
+ proptype = f"Mapped[{idprop_pytype.type}]"
418
+ propval = expr.ExprFuncCall(
419
+ expr.ExprIdent("mapped_column"),
420
+ [
421
+ expr.ExprFuncCall(
422
+ expr.ExprIdent("ForeignKey"),
423
+ [
424
+ expr.ExprConstant(f"{prop.target.db.table_name}.{idprop.name}"),
212
425
  PredefinedFn.keyword_assignment(
213
426
  "ondelete",
214
- expr.ExprConstant(prop.db.on_delete.to_sqlalchemy()),
215
- )
216
- )
217
- propvalargs.append(
427
+ expr.ExprConstant(prop.db.on_target_delete.to_sqlalchemy()),
428
+ ),
218
429
  PredefinedFn.keyword_assignment(
219
430
  "onupdate",
220
- expr.ExprConstant(prop.db.on_update.to_sqlalchemy()),
221
- )
222
- )
431
+ expr.ExprConstant(prop.db.on_target_update.to_sqlalchemy()),
432
+ ),
433
+ ],
434
+ ),
435
+ ],
436
+ )
223
437
 
224
- propval = expr.ExprFuncCall(
225
- expr.ExprIdent("mapped_column"),
226
- [
227
- expr.ExprFuncCall(
228
- expr.ExprIdent("ForeignKey"),
229
- propvalargs,
230
- ),
231
- ],
232
- )
233
- else:
234
- # if the target class is not in the database,
235
- program.import_(
236
- f"{target_pkg.module(prop.target.get_pymodule_name()).path}.{prop.target.name}",
237
- is_import_attr=True,
238
- )
239
- propname = prop.name
240
- proptype = f"Mapped[{prop.target.name}]"
241
-
242
- # we have two choices, one is to create a composite class, one is to create a custom field
243
- if prop.db.is_embedded == "composite":
244
- # for a class to be composite, it must have only data properties
245
- program.import_("sqlalchemy.orm.composite", True)
246
- propvalargs = [expr.ExprIdent(prop.target.name)]
247
- for p in prop.target.properties.values():
248
- propvalargs.append(
249
- expr.ExprFuncCall(
250
- expr.ExprIdent("mapped_column"),
251
- [
252
- expr.ExprIdent(f"{prop.name}_{p.name}"),
253
- expr.ExprIdent(
254
- assert_isinstance(p, DataProperty)
255
- .datatype.get_sqlalchemy_type()
256
- .type
257
- ),
258
- expr.PredefinedFn.keyword_assignment(
259
- "nullable",
260
- expr.ExprConstant(prop.is_optional),
261
- ),
262
- ],
263
- )
264
- )
265
- propval = expr.ExprFuncCall(
266
- expr.ExprIdent("composite"),
267
- propvalargs,
268
- )
269
- else:
270
- # we create a custom field, the custom field mapping need to be defined in the base
271
- propval = expr.ExprFuncCall(expr.ExprIdent("mapped_column"), [])
272
- custom_types.append(prop)
438
+ cls_ast(stmt.DefClassVarStatement(propname, proptype, propval))
439
+ return
273
440
 
274
- cls_ast(stmt.DefClassVarStatement(propname, proptype, propval))
441
+ # if the target class is not in the database,
442
+ program.import_(
443
+ f"{target_pkg.module(prop.target.get_pymodule_name()).path}.{prop.target.name}",
444
+ is_import_attr=True,
445
+ )
446
+ propname = prop.name
447
+ proptype = f"Mapped[{prop.target.name}]"
448
+
449
+ # we have two choices, one is to create a composite class, one is to create a custom field
450
+ if prop.db.is_embedded == "composite":
451
+ # for a class to be composite, it must have only data properties
452
+ program.import_("sqlalchemy.orm.composite", True)
453
+ propvalargs: list[expr.Expr] = [expr.ExprIdent(prop.target.name)]
454
+ for p in prop.target.properties.values():
455
+ propvalargs.append(
456
+ expr.ExprFuncCall(
457
+ expr.ExprIdent("mapped_column"),
458
+ [
459
+ expr.ExprIdent(f"{prop.name}_{p.name}"),
460
+ expr.ExprIdent(
461
+ assert_isinstance(p, DataProperty)
462
+ .datatype.get_sqlalchemy_type()
463
+ .type
464
+ ),
465
+ expr.PredefinedFn.keyword_assignment(
466
+ "nullable",
467
+ expr.ExprConstant(prop.is_optional),
468
+ ),
469
+ ],
470
+ )
471
+ )
472
+ propval = expr.ExprFuncCall(
473
+ expr.ExprIdent("composite"),
474
+ propvalargs,
475
+ )
476
+ else:
477
+ # we create a custom field, the custom field mapping need to be defined in the base
478
+ propval = expr.ExprFuncCall(expr.ExprIdent("mapped_column"), [])
479
+ custom_types.append(prop)
275
480
 
276
- target_pkg.module(cls.get_pymodule_name()).write(program)
481
+ cls_ast(stmt.DefClassVarStatement(propname, proptype, propval))
277
482
 
278
- # make a base class that implements the mapping for custom types
279
- custom_types = filter_duplication(
280
- custom_types, lambda p: (p.target.name, p.cardinality, p.is_optional, p.is_map)
483
+
484
+ def make_python_relational_object_property_many_to_many(
485
+ program: Program, ast: AST, target_pkg: Package, cls: Class, prop: ObjectProperty
486
+ ):
487
+ assert cls.db is not None
488
+ assert prop.db is not None and prop.target.db is not None
489
+ assert prop.cardinality == Cardinality.MANY_TO_MANY
490
+
491
+ # we create a new table to store the many-to-many relationship
492
+ new_table = f"{cls.name}{prop.target.name}"
493
+ clsdb = cls.db
494
+ propdb = prop.db
495
+ targetdb = prop.target.db
496
+
497
+ source_idprop = assert_not_null(cls.get_id_property())
498
+ source_id_type = source_idprop.datatype.get_python_type().type
499
+ target_idprop = assert_not_null(prop.target.get_id_property())
500
+ target_id_type = target_idprop.datatype.get_python_type().type
501
+
502
+ newprogram = Program()
503
+ newprogram.import_("__future__.annotations", True)
504
+ newprogram.import_("sqlalchemy.ForeignKey", True)
505
+ newprogram.import_("sqlalchemy.orm.mapped_column", True)
506
+ newprogram.import_("sqlalchemy.orm.Mapped", True)
507
+ newprogram.import_("sqlalchemy.orm.relationship", True)
508
+ newprogram.import_(f"{target_pkg.path}.base.Base", True)
509
+ newprogram.import_("typing.TYPE_CHECKING", True)
510
+ newprogram.import_area.if_(expr.ExprIdent("TYPE_CHECKING"))(
511
+ lambda ast00: ast00.import_(
512
+ target_pkg.path + f".{cls.get_pymodule_name()}.{cls.name}",
513
+ is_import_attr=True,
514
+ ),
515
+ lambda ast10: ast10.import_(
516
+ target_pkg.path + f".{prop.target.get_pymodule_name()}.{prop.target.name}",
517
+ is_import_attr=True,
518
+ ),
519
+ )
520
+
521
+ newprogram.root(
522
+ stmt.LineBreak(),
523
+ lambda ast00: ast00.class_(new_table, [expr.ExprIdent("Base")])(
524
+ stmt.DefClassVarStatement(
525
+ "__tablename__",
526
+ type=None,
527
+ value=expr.ExprConstant(f"{clsdb.table_name}_{targetdb.table_name}"),
528
+ ),
529
+ stmt.LineBreak(),
530
+ stmt.DefClassVarStatement(
531
+ "id",
532
+ "Mapped[int]",
533
+ expr.ExprFuncCall(
534
+ expr.ExprIdent("mapped_column"),
535
+ [
536
+ PredefinedFn.keyword_assignment(
537
+ "primary_key", expr.ExprConstant(True)
538
+ ),
539
+ PredefinedFn.keyword_assignment(
540
+ "autoincrement", expr.ExprConstant("auto")
541
+ ),
542
+ ],
543
+ ),
544
+ ),
545
+ stmt.DefClassVarStatement(
546
+ to_snake_case(cls.name),
547
+ f"Mapped[{cls.name}]",
548
+ expr.ExprFuncCall(
549
+ expr.ExprIdent("relationship"),
550
+ [
551
+ PredefinedFn.keyword_assignment(
552
+ "back_populates",
553
+ expr.ExprConstant(prop.name),
554
+ ),
555
+ PredefinedFn.keyword_assignment(
556
+ "lazy",
557
+ expr.ExprConstant("raise_on_sql"),
558
+ ),
559
+ ],
560
+ ),
561
+ ),
562
+ stmt.DefClassVarStatement(
563
+ to_snake_case(cls.name) + "_id",
564
+ f"Mapped[{source_id_type}]",
565
+ expr.ExprFuncCall(
566
+ expr.ExprIdent("mapped_column"),
567
+ [
568
+ expr.ExprFuncCall(
569
+ expr.ExprIdent("ForeignKey"),
570
+ [
571
+ expr.ExprConstant(
572
+ f"{clsdb.table_name}.{source_idprop.name}"
573
+ ),
574
+ PredefinedFn.keyword_assignment(
575
+ "ondelete",
576
+ expr.ExprConstant(
577
+ propdb.on_source_delete.to_sqlalchemy()
578
+ ),
579
+ ),
580
+ PredefinedFn.keyword_assignment(
581
+ "onupdate",
582
+ expr.ExprConstant(
583
+ propdb.on_source_update.to_sqlalchemy()
584
+ ),
585
+ ),
586
+ ],
587
+ ),
588
+ ],
589
+ ),
590
+ ),
591
+ stmt.DefClassVarStatement(
592
+ to_snake_case(prop.target.name),
593
+ f"Mapped[{prop.target.name}]",
594
+ expr.ExprFuncCall(
595
+ expr.ExprIdent("relationship"),
596
+ [
597
+ PredefinedFn.keyword_assignment(
598
+ "lazy",
599
+ expr.ExprConstant("raise_on_sql"),
600
+ ),
601
+ ],
602
+ ),
603
+ ),
604
+ stmt.DefClassVarStatement(
605
+ to_snake_case(prop.target.name) + "_id",
606
+ f"Mapped[{target_id_type}]",
607
+ expr.ExprFuncCall(
608
+ expr.ExprIdent("mapped_column"),
609
+ [
610
+ expr.ExprFuncCall(
611
+ expr.ExprIdent("ForeignKey"),
612
+ [
613
+ expr.ExprConstant(
614
+ f"{targetdb.table_name}.{target_idprop.name}"
615
+ ),
616
+ PredefinedFn.keyword_assignment(
617
+ "ondelete",
618
+ expr.ExprConstant(
619
+ propdb.on_target_delete.to_sqlalchemy()
620
+ ),
621
+ ),
622
+ PredefinedFn.keyword_assignment(
623
+ "onupdate",
624
+ expr.ExprConstant(
625
+ propdb.on_target_update.to_sqlalchemy()
626
+ ),
627
+ ),
628
+ ],
629
+ ),
630
+ ],
631
+ ),
632
+ ),
633
+ ),
634
+ )
635
+
636
+ new_table_module = target_pkg.module(to_snake_case(new_table))
637
+ new_table_module.write(newprogram)
638
+
639
+ # now we add the relationship to the source.
640
+ # we can configure it to be list, set, or dict depends on what we want.
641
+ program.import_(new_table_module.path + f".{new_table}", True)
642
+ # program.import_("typing.TYPE_CHECKING", True)
643
+ # program.import_area.if_(expr.ExprIdent("TYPE_CHECKING"))(
644
+ # lambda ast00: ast00.import_(
645
+ # target_pkg.path + f".{prop.target.get_pymodule_name()}.{prop.target.name}",
646
+ # is_import_attr=True,
647
+ # )
648
+ # )
649
+
650
+ ast(
651
+ stmt.DefClassVarStatement(
652
+ prop.name,
653
+ f"Mapped[list[{new_table}]]",
654
+ expr.ExprFuncCall(
655
+ expr.ExprIdent("relationship"),
656
+ [
657
+ PredefinedFn.keyword_assignment(
658
+ "back_populates",
659
+ expr.ExprConstant(to_snake_case(cls.name)),
660
+ ),
661
+ PredefinedFn.keyword_assignment(
662
+ "lazy",
663
+ expr.ExprConstant("raise_on_sql"),
664
+ ),
665
+ ],
666
+ ),
667
+ ),
281
668
  )
282
- make_base(custom_types)
sera/models/__init__.py CHANGED
@@ -1,7 +1,7 @@
1
1
  from sera.models._class import Class
2
2
  from sera.models._collection import DataCollection
3
3
  from sera.models._datatype import DataType
4
- from sera.models._module import App, Module, Package
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
7
7
  from sera.models._property import Cardinality, DataProperty, ObjectProperty, Property
@@ -21,4 +21,5 @@ __all__ = [
21
21
  "DataCollection",
22
22
  "Module",
23
23
  "App",
24
+ "Language",
24
25
  ]
sera/models/_class.py CHANGED
@@ -35,6 +35,8 @@ class Class:
35
35
 
36
36
  # whether to store this class in a table in the database
37
37
  db: Optional[ClassDBMapInfo]
38
+ # whether this class is public and we generate a data model for it.
39
+ is_public: bool = True
38
40
 
39
41
  def get_id_property(self) -> Optional[DataProperty]:
40
42
  """