sera-2 1.12.3__py3-none-any.whl → 1.12.6__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/libs/base_orm.py CHANGED
@@ -4,10 +4,12 @@ from typing import Optional
4
4
 
5
5
  import orjson
6
6
  from msgspec.json import decode, encode
7
- from sera.typing import UNSET
8
7
  from sqlalchemy import LargeBinary, TypeDecorator
9
8
  from sqlalchemy import create_engine as sqlalchemy_create_engine
10
9
  from sqlalchemy import update
10
+ from sqlalchemy.ext.asyncio import create_async_engine as sqlalchemy_create_async_engine
11
+
12
+ from sera.typing import UNSET
11
13
 
12
14
 
13
15
  class BaseORM:
@@ -104,11 +106,27 @@ class DictDataclassType(TypeDecorator):
104
106
  def create_engine(
105
107
  dbconn: str,
106
108
  connect_args: Optional[dict] = None,
107
- debug: bool = False,
109
+ echo: bool = False,
110
+ ):
111
+ if dbconn.startswith("sqlite"):
112
+ connect_args = {"check_same_thread": False}
113
+ else:
114
+ connect_args = {}
115
+ engine = sqlalchemy_create_engine(dbconn, connect_args=connect_args, echo=echo)
116
+ return engine
117
+
118
+
119
+ def create_async_engine(
120
+ dbconn: str,
121
+ connect_args: Optional[dict] = None,
122
+ echo: bool = False,
108
123
  ):
109
124
  if dbconn.startswith("sqlite"):
110
125
  connect_args = {"check_same_thread": False}
111
126
  else:
112
127
  connect_args = {}
113
- engine = sqlalchemy_create_engine(dbconn, connect_args=connect_args, echo=debug)
128
+
129
+ engine = sqlalchemy_create_async_engine(
130
+ dbconn, connect_args=connect_args, echo=echo
131
+ )
114
132
  return engine
sera/libs/base_service.py CHANGED
@@ -4,8 +4,11 @@ from enum import Enum
4
4
  from math import dist
5
5
  from typing import Annotated, Any, Generic, NamedTuple, Optional, Sequence, TypeVar
6
6
 
7
- from sqlalchemy import Result, Select, exists, func, select
8
- from sqlalchemy.orm import Session, load_only
7
+ from litestar.exceptions import HTTPException
8
+ from sqlalchemy import Result, Select, delete, exists, func, select
9
+ from sqlalchemy.exc import IntegrityError
10
+ from sqlalchemy.ext.asyncio import AsyncSession
11
+ from sqlalchemy.orm import load_only
9
12
 
10
13
  from sera.libs.base_orm import BaseORM
11
14
  from sera.misc import assert_not_null
@@ -41,7 +44,7 @@ class QueryResult(NamedTuple, Generic[R]):
41
44
  total: int
42
45
 
43
46
 
44
- class BaseService(Generic[ID, R]):
47
+ class BaseAsyncService(Generic[ID, R]):
45
48
 
46
49
  instance = None
47
50
 
@@ -51,7 +54,7 @@ class BaseService(Generic[ID, R]):
51
54
  self.id_prop = assert_not_null(cls.get_id_property())
52
55
 
53
56
  self._cls_id_prop = getattr(self.orm_cls, self.id_prop.name)
54
- self.is_id_auto_increment = self.id_prop.db.is_auto_increment
57
+ self.is_id_auto_increment = assert_not_null(self.id_prop.db).is_auto_increment
55
58
 
56
59
  @classmethod
57
60
  def get_instance(cls):
@@ -59,10 +62,10 @@ class BaseService(Generic[ID, R]):
59
62
  if cls.instance is None:
60
63
  # assume that the subclass overrides the __init__ method
61
64
  # so that we don't need to pass the class and orm_cls
62
- cls.instance = cls()
65
+ cls.instance = cls() # type: ignore[call-arg]
63
66
  return cls.instance
64
67
 
65
- def get(
68
+ async def get(
66
69
  self,
67
70
  query: Query,
68
71
  limit: int,
@@ -71,7 +74,7 @@ class BaseService(Generic[ID, R]):
71
74
  sorted_by: list[str],
72
75
  group_by: list[str],
73
76
  fields: list[str],
74
- session: Session,
77
+ session: AsyncSession,
75
78
  ) -> QueryResult[R]:
76
79
  """Retrieving records matched a query.
77
80
 
@@ -103,35 +106,37 @@ class BaseService(Generic[ID, R]):
103
106
 
104
107
  cq = select(func.count()).select_from(q.subquery())
105
108
  rq = q.limit(limit).offset(offset)
106
- records = self._process_result(session.execute(rq)).scalars().all()
107
- total = session.execute(cq).scalar_one()
109
+ records = self._process_result(await session.execute(rq)).scalars().all()
110
+ total = (await session.execute(cq)).scalar_one()
108
111
  return QueryResult(records, total)
109
112
 
110
- def get_by_id(self, id: ID, session: Session) -> Optional[R]:
113
+ async def get_by_id(self, id: ID, session: AsyncSession) -> Optional[R]:
111
114
  """Retrieving a record by ID."""
112
115
  q = self._select().where(self._cls_id_prop == id)
113
- result = self._process_result(session.execute(q)).scalar_one_or_none()
116
+ result = self._process_result(await session.execute(q)).scalar_one_or_none()
114
117
  return result
115
118
 
116
- def has_id(self, id: ID, session: Session) -> bool:
119
+ async def has_id(self, id: ID, session: AsyncSession) -> bool:
117
120
  """Check whether we have a record with the given ID."""
118
- q = exists().where(self._cls_id_prop == id)
119
- result = session.query(q).scalar()
121
+ q = exists().where(self._cls_id_prop == id).select()
122
+ result = (await session.execute(q)).scalar()
120
123
  return bool(result)
121
124
 
122
- def create(self, record: R, session: Session) -> R:
125
+ async def create(self, record: R, session: AsyncSession) -> R:
123
126
  """Create a new record."""
124
127
  if self.is_id_auto_increment:
125
128
  setattr(record, self.id_prop.name, None)
126
129
 
127
- session.add(record)
128
- session.commit()
130
+ try:
131
+ session.add(record)
132
+ await session.flush()
133
+ except IntegrityError:
134
+ raise HTTPException(detail="Invalid request", status_code=409)
129
135
  return record
130
136
 
131
- def update(self, record: R, session: Session) -> R:
137
+ async def update(self, record: R, session: AsyncSession) -> R:
132
138
  """Update an existing record."""
133
- session.execute(record.get_update_query())
134
- session.commit()
139
+ await session.execute(record.get_update_query())
135
140
  return record
136
141
 
137
142
  def _select(self) -> Select:
@@ -141,3 +146,7 @@ class BaseService(Generic[ID, R]):
141
146
  def _process_result(self, result: SqlResult) -> SqlResult:
142
147
  """Process the result of a query."""
143
148
  return result
149
+
150
+ async def truncate(self, session: AsyncSession) -> None:
151
+ """Truncate the table."""
152
+ await session.execute(delete(self.orm_cls))
@@ -1,16 +1,13 @@
1
1
  from __future__ import annotations
2
2
 
3
3
  from datetime import datetime, timezone
4
- from typing import Callable, Generator, Generic, Optional, Sequence, Type
4
+ from typing import Awaitable, Callable, Generic, Sequence
5
5
 
6
6
  from litestar import Request
7
7
  from litestar.connection import ASGIConnection
8
8
  from litestar.exceptions import NotAuthorizedException
9
9
  from litestar.middleware import AbstractAuthenticationMiddleware, AuthenticationResult
10
10
  from litestar.types import ASGIApp, Method, Scopes
11
- from litestar.types.composite_types import Dependencies
12
- from sqlalchemy import select
13
- from sqlalchemy.orm import Session
14
11
 
15
12
  from sera.typing import T
16
13
 
@@ -26,7 +23,7 @@ class AuthMiddleware(AbstractAuthenticationMiddleware, Generic[T]):
26
23
  def __init__(
27
24
  self,
28
25
  app: ASGIApp,
29
- user_handler: Callable[[str], T],
26
+ user_handler: Callable[[str], Awaitable[T]],
30
27
  exclude: str | list[str] | None = None,
31
28
  exclude_from_auth_key: str = "exclude_from_auth",
32
29
  exclude_http_methods: Sequence[Method] | None = None,
@@ -59,7 +56,7 @@ class AuthMiddleware(AbstractAuthenticationMiddleware, Generic[T]):
59
56
  detail="Credentials expired",
60
57
  )
61
58
 
62
- user = self.user_handler(userid)
59
+ user = await self.user_handler(userid)
63
60
  if user is None:
64
61
  raise NotAuthorizedException(
65
62
  detail="User not found",
sera/make/__main__.py CHANGED
@@ -49,4 +49,5 @@ def cli(
49
49
  make_app(app_dir, schema_files, api_collections, language, referenced_schema)
50
50
 
51
51
 
52
- app()
52
+ if __name__ == "__main__":
53
+ app()
@@ -2,11 +2,13 @@ from __future__ import annotations
2
2
 
3
3
  from typing import Sequence
4
4
 
5
- from codegen.models import DeferredVar, PredefinedFn, Program, expr, stmt
5
+ from codegen.models import DeferredVar, ImportHelper, PredefinedFn, Program, expr, stmt
6
6
  from loguru import logger
7
7
 
8
8
  from sera.misc import assert_not_null, to_snake_case
9
- from sera.models import App, DataCollection, Module, Package
9
+ from sera.models import App, DataCollection, Module, Package, SystemControlledMode
10
+
11
+ GLOBAL_IDENTS = {"AsyncSession": "sqlalchemy.ext.asyncio.AsyncSession"}
10
12
 
11
13
 
12
14
  def make_python_api(app: App, collections: Sequence[DataCollection]):
@@ -112,12 +114,13 @@ def make_python_get_api(
112
114
  app = target_pkg.app
113
115
 
114
116
  program = Program()
117
+ import_helper = ImportHelper(program, GLOBAL_IDENTS)
118
+
115
119
  program.import_("__future__.annotations", True)
116
120
  program.import_("typing.Annotated", True)
117
121
  program.import_("litestar.get", True)
118
122
  program.import_("litestar.Request", True)
119
123
  program.import_("litestar.params.Parameter", True)
120
- program.import_("sqlalchemy.orm.Session", True)
121
124
  program.import_(app.config.path + ".API_DEBUG", True)
122
125
  program.import_(
123
126
  app.services.path
@@ -191,7 +194,7 @@ def make_python_get_api(
191
194
  ),
192
195
  DeferredVar.simple(
193
196
  "session",
194
- expr.ExprIdent("Session"),
197
+ import_helper.use("AsyncSession"),
195
198
  ),
196
199
  ],
197
200
  return_type=expr.ExprIdent(f"dict"),
@@ -226,35 +229,37 @@ def make_python_get_api(
226
229
  ),
227
230
  lambda ast102: ast102.assign(
228
231
  DeferredVar.simple("result"),
229
- expr.ExprFuncCall(
230
- PredefinedFn.attr_getter(
231
- expr.ExprIdent("service"),
232
- expr.ExprIdent("get"),
233
- ),
234
- [
235
- expr.ExprIdent("query"),
236
- PredefinedFn.keyword_assignment(
237
- "limit", expr.ExprIdent("limit")
238
- ),
239
- PredefinedFn.keyword_assignment(
240
- "offset", expr.ExprIdent("offset")
241
- ),
242
- PredefinedFn.keyword_assignment(
243
- "unique", expr.ExprIdent("unique")
244
- ),
245
- PredefinedFn.keyword_assignment(
246
- "sorted_by", expr.ExprIdent("sorted_by")
247
- ),
248
- PredefinedFn.keyword_assignment(
249
- "group_by", expr.ExprIdent("group_by")
250
- ),
251
- PredefinedFn.keyword_assignment(
252
- "fields", expr.ExprIdent("fields")
253
- ),
254
- PredefinedFn.keyword_assignment(
255
- "session", expr.ExprIdent("session")
232
+ expr.ExprAwait(
233
+ expr.ExprFuncCall(
234
+ PredefinedFn.attr_getter(
235
+ expr.ExprIdent("service"),
236
+ expr.ExprIdent("get"),
256
237
  ),
257
- ],
238
+ [
239
+ expr.ExprIdent("query"),
240
+ PredefinedFn.keyword_assignment(
241
+ "limit", expr.ExprIdent("limit")
242
+ ),
243
+ PredefinedFn.keyword_assignment(
244
+ "offset", expr.ExprIdent("offset")
245
+ ),
246
+ PredefinedFn.keyword_assignment(
247
+ "unique", expr.ExprIdent("unique")
248
+ ),
249
+ PredefinedFn.keyword_assignment(
250
+ "sorted_by", expr.ExprIdent("sorted_by")
251
+ ),
252
+ PredefinedFn.keyword_assignment(
253
+ "group_by", expr.ExprIdent("group_by")
254
+ ),
255
+ PredefinedFn.keyword_assignment(
256
+ "fields", expr.ExprIdent("fields")
257
+ ),
258
+ PredefinedFn.keyword_assignment(
259
+ "session", expr.ExprIdent("session")
260
+ ),
261
+ ],
262
+ )
258
263
  ),
259
264
  ),
260
265
  lambda ast103: ast103.return_(
@@ -300,14 +305,13 @@ def make_python_get_by_id_api(
300
305
  """Make an endpoint for querying resource by id"""
301
306
  app = target_pkg.app
302
307
 
303
- ServiceNameDep = to_snake_case(f"{collection.name}ServiceDependency")
304
-
305
308
  program = Program()
309
+ import_helper = ImportHelper(program, GLOBAL_IDENTS)
310
+
306
311
  program.import_("__future__.annotations", True)
307
312
  program.import_("litestar.get", True)
308
313
  program.import_("litestar.status_codes", True)
309
314
  program.import_("litestar.exceptions.HTTPException", True)
310
- program.import_("sqlalchemy.orm.Session", True)
311
315
  program.import_(
312
316
  app.services.path
313
317
  + f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
@@ -342,7 +346,7 @@ def make_python_get_by_id_api(
342
346
  ),
343
347
  DeferredVar.simple(
344
348
  "session",
345
- expr.ExprIdent("Session"),
349
+ import_helper.use("AsyncSession"),
346
350
  ),
347
351
  ],
348
352
  return_type=expr.ExprIdent("dict"),
@@ -361,12 +365,14 @@ def make_python_get_by_id_api(
361
365
  ),
362
366
  lambda ast11: ast11.assign(
363
367
  DeferredVar.simple("record"),
364
- expr.ExprFuncCall(
365
- expr.ExprIdent("service.get_by_id"),
366
- [
367
- expr.ExprIdent("id"),
368
- expr.ExprIdent("session"),
369
- ],
368
+ expr.ExprAwait(
369
+ expr.ExprFuncCall(
370
+ expr.ExprIdent("service.get_by_id"),
371
+ [
372
+ expr.ExprIdent("id"),
373
+ expr.ExprIdent("session"),
374
+ ],
375
+ )
370
376
  ),
371
377
  ),
372
378
  lambda ast12: ast12.if_(PredefinedFn.is_null(expr.ExprIdent("record")))(
@@ -423,14 +429,13 @@ def make_python_has_api(
423
429
  """Make an endpoint for querying resource by id"""
424
430
  app = target_pkg.app
425
431
 
426
- ServiceNameDep = to_snake_case(f"{collection.name}ServiceDependency")
427
-
428
432
  program = Program()
433
+ import_helper = ImportHelper(program, GLOBAL_IDENTS)
434
+
429
435
  program.import_("__future__.annotations", True)
430
436
  program.import_("litestar.head", True)
431
437
  program.import_("litestar.status_codes", True)
432
438
  program.import_("litestar.exceptions.HTTPException", True)
433
- program.import_("sqlalchemy.orm.Session", True)
434
439
  program.import_(
435
440
  app.services.path
436
441
  + f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
@@ -465,13 +470,15 @@ def make_python_has_api(
465
470
  ),
466
471
  DeferredVar.simple(
467
472
  "session",
468
- expr.ExprIdent("Session"),
473
+ import_helper.use("AsyncSession"),
469
474
  ),
470
475
  ],
471
476
  return_type=expr.ExprConstant(None),
472
477
  is_async=True,
473
478
  )(
474
- stmt.SingleExprStatement(expr.ExprConstant("Retrieving record by id")),
479
+ stmt.SingleExprStatement(
480
+ expr.ExprConstant("Checking if record exists by id")
481
+ ),
475
482
  lambda ast100: ast100.assign(
476
483
  DeferredVar.simple("service"),
477
484
  expr.ExprFuncCall(
@@ -484,12 +491,14 @@ def make_python_has_api(
484
491
  ),
485
492
  lambda ast11: ast11.assign(
486
493
  DeferredVar.simple("record_exist"),
487
- expr.ExprFuncCall(
488
- expr.ExprIdent("service.has_id"),
489
- [
490
- expr.ExprIdent("id"),
491
- expr.ExprIdent("session"),
492
- ],
494
+ expr.ExprAwait(
495
+ expr.ExprFuncCall(
496
+ expr.ExprIdent("service.has_id"),
497
+ [
498
+ expr.ExprIdent("id"),
499
+ expr.ExprIdent("session"),
500
+ ],
501
+ )
493
502
  ),
494
503
  ),
495
504
  lambda ast12: ast12.if_(expr.ExprNegation(expr.ExprIdent("record_exist")))(
@@ -524,9 +533,10 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
524
533
  app = target_pkg.app
525
534
 
526
535
  program = Program()
536
+ import_helper = ImportHelper(program, GLOBAL_IDENTS)
537
+
527
538
  program.import_("__future__.annotations", True)
528
539
  program.import_("litestar.post", True)
529
- program.import_("sqlalchemy.orm.Session", True)
530
540
  program.import_(
531
541
  app.services.path
532
542
  + f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
@@ -540,12 +550,13 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
540
550
 
541
551
  # assuming the collection has only one class
542
552
  cls = collection.cls
543
- has_system_controlled_prop = any(
544
- prop.data.is_system_controlled for prop in cls.properties.values()
553
+ has_restricted_system_controlled_prop = any(
554
+ prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
555
+ for prop in cls.properties.values()
545
556
  )
546
557
  idprop = assert_not_null(cls.get_id_property())
547
558
 
548
- if has_system_controlled_prop:
559
+ if has_restricted_system_controlled_prop:
549
560
  program.import_("sera.libs.api_helper.SingleAutoUSCP", True)
550
561
 
551
562
  func_name = "create"
@@ -568,7 +579,7 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
568
579
  ),
569
580
  )
570
581
  ]
571
- if has_system_controlled_prop
582
+ if has_restricted_system_controlled_prop
572
583
  else []
573
584
  ),
574
585
  )
@@ -582,7 +593,7 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
582
593
  ),
583
594
  DeferredVar.simple(
584
595
  "session",
585
- expr.ExprIdent("Session"),
596
+ import_helper.use("AsyncSession"),
586
597
  ),
587
598
  ],
588
599
  return_type=expr.ExprIdent(idprop.datatype.get_python_type().type),
@@ -601,13 +612,17 @@ def make_python_create_api(collection: DataCollection, target_pkg: Package):
601
612
  ),
602
613
  lambda ast13: ast13.return_(
603
614
  PredefinedFn.attr_getter(
604
- expr.ExprMethodCall(
605
- expr.ExprIdent("service"),
606
- "create",
607
- [
608
- expr.ExprMethodCall(expr.ExprIdent("data"), "to_db", []),
609
- expr.ExprIdent("session"),
610
- ],
615
+ expr.ExprAwait(
616
+ expr.ExprMethodCall(
617
+ expr.ExprIdent("service"),
618
+ "create",
619
+ [
620
+ expr.ExprMethodCall(
621
+ expr.ExprIdent("data"), "to_db", []
622
+ ),
623
+ expr.ExprIdent("session"),
624
+ ],
625
+ )
611
626
  ),
612
627
  expr.ExprIdent(idprop.name),
613
628
  )
@@ -626,9 +641,10 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
626
641
  app = target_pkg.app
627
642
 
628
643
  program = Program()
644
+ import_helper = ImportHelper(program, GLOBAL_IDENTS)
645
+
629
646
  program.import_("__future__.annotations", True)
630
647
  program.import_("litestar.put", True)
631
- program.import_("sqlalchemy.orm.Session", True)
632
648
  program.import_(
633
649
  app.services.path
634
650
  + f".{collection.get_pymodule_name()}.{collection.get_service_name()}",
@@ -645,10 +661,11 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
645
661
  id_prop = assert_not_null(cls.get_id_property())
646
662
  id_type = id_prop.datatype.get_python_type().type
647
663
 
648
- has_system_controlled_prop = any(
649
- prop.data.is_system_controlled for prop in cls.properties.values()
664
+ has_restricted_system_controlled_prop = any(
665
+ prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
666
+ for prop in cls.properties.values()
650
667
  )
651
- if has_system_controlled_prop:
668
+ if has_restricted_system_controlled_prop:
652
669
  program.import_("sera.libs.api_helper.SingleAutoUSCP", True)
653
670
 
654
671
  func_name = "update"
@@ -671,7 +688,7 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
671
688
  ),
672
689
  )
673
690
  ]
674
- if has_system_controlled_prop
691
+ if has_restricted_system_controlled_prop
675
692
  else []
676
693
  ),
677
694
  )
@@ -689,7 +706,7 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
689
706
  ),
690
707
  DeferredVar.simple(
691
708
  "session",
692
- expr.ExprIdent("Session"),
709
+ import_helper.use("AsyncSession"),
693
710
  ),
694
711
  ],
695
712
  return_type=expr.ExprIdent(id_prop.datatype.get_python_type().type),
@@ -715,13 +732,17 @@ def make_python_update_api(collection: DataCollection, target_pkg: Package):
715
732
  ),
716
733
  lambda ast13: ast13.return_(
717
734
  PredefinedFn.attr_getter(
718
- expr.ExprMethodCall(
719
- expr.ExprIdent("service"),
720
- "update",
721
- [
722
- expr.ExprMethodCall(expr.ExprIdent("data"), "to_db", []),
723
- expr.ExprIdent("session"),
724
- ],
735
+ expr.ExprAwait(
736
+ expr.ExprMethodCall(
737
+ expr.ExprIdent("service"),
738
+ "update",
739
+ [
740
+ expr.ExprMethodCall(
741
+ expr.ExprIdent("data"), "to_db", []
742
+ ),
743
+ expr.ExprIdent("session"),
744
+ ],
745
+ )
725
746
  ),
726
747
  expr.ExprIdent(id_prop.name),
727
748
  )
@@ -1,9 +1,16 @@
1
1
  from __future__ import annotations
2
2
 
3
- from operator import is_
4
3
  from typing import Callable, Optional, Sequence
5
4
 
6
- from codegen.models import AST, DeferredVar, PredefinedFn, Program, expr, stmt
5
+ from codegen.models import (
6
+ AST,
7
+ DeferredVar,
8
+ ImportHelper,
9
+ PredefinedFn,
10
+ Program,
11
+ expr,
12
+ stmt,
13
+ )
7
14
 
8
15
  from sera.misc import (
9
16
  assert_isinstance,
@@ -20,6 +27,7 @@ from sera.models import (
20
27
  PyTypeWithDep,
21
28
  Schema,
22
29
  )
30
+ from sera.models._property import SystemControlledMode
23
31
  from sera.typing import ObjectPath
24
32
 
25
33
 
@@ -211,10 +219,19 @@ def make_python_data_model(
211
219
  alias=f"{cls.name}DB",
212
220
  )
213
221
 
214
- has_system_controlled = any(
215
- prop.data.is_system_controlled for prop in cls.properties.values()
222
+ ident_manager = ImportHelper(
223
+ program,
224
+ {
225
+ "UNSET": "sera.typing.UNSET",
226
+ },
227
+ )
228
+
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
232
+ for prop in cls.properties.values()
216
233
  )
217
- if has_system_controlled:
234
+ if has_restricted_system_controlled:
218
235
  program.import_("typing.TypedDict", True)
219
236
  program.root(
220
237
  stmt.LineBreak(),
@@ -236,6 +253,7 @@ def make_python_data_model(
236
253
  )
237
254
  for prop in cls.properties.values()
238
255
  if prop.data.is_system_controlled
256
+ == SystemControlledMode.RESTRICTED
239
257
  ],
240
258
  ),
241
259
  )
@@ -246,11 +264,10 @@ def make_python_data_model(
246
264
  [expr.ExprIdent("msgspec.Struct"), expr.ExprIdent("kw_only=True")],
247
265
  )
248
266
  for prop in cls.properties.values():
249
- # this is a create object, so users can create private field
250
- # hence, we do not check for prop.is_private -- however, this field can be omitted
251
- # during update, so we need to mark it as optional
252
- # if prop.data.is_private:
253
- # continue
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:
270
+ continue
254
271
 
255
272
  if isinstance(prop, DataProperty):
256
273
  pytype = prop.get_data_model_datatype().get_python_type()
@@ -272,14 +289,20 @@ def make_python_data_model(
272
289
  else:
273
290
  raise NotImplementedError(prop.data.constraints)
274
291
 
275
- if prop.data.is_private:
292
+ if (
293
+ prop.data.is_private
294
+ or prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
295
+ ):
276
296
  program.import_("typing.Union", True)
277
297
  program.import_("sera.typing.UnsetType", True)
278
298
  program.import_("sera.typing.UNSET", True)
279
299
  pytype_type = f"Union[{pytype_type}, UnsetType]"
280
300
 
281
301
  prop_default_value = None
282
- if prop.data.is_private:
302
+ if (
303
+ prop.data.is_private
304
+ or prop.data.is_system_controlled == SystemControlledMode.RESTRICTED
305
+ ):
283
306
  prop_default_value = expr.ExprIdent("UNSET")
284
307
  elif prop.default_value is not None:
285
308
  prop_default_value = expr.ExprConstant(prop.default_value)
@@ -321,25 +344,28 @@ def make_python_data_model(
321
344
  elif prop.is_optional:
322
345
  pytype = pytype.as_optional_type()
323
346
 
347
+ 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
+
324
356
  for dep in pytype.deps:
325
357
  program.import_(dep, True)
326
358
 
327
- cls_ast(stmt.DefClassVarStatement(prop.name, pytype.type))
328
-
329
- # has_to_db = True
330
- # if any(prop for prop in cls.properties.values() if isinstance(prop, ObjectProperty) and prop.cardinality == Cardinality.MANY_TO_MANY):
331
- # # if the class has many-to-many relationship, we need to
359
+ cls_ast(
360
+ stmt.DefClassVarStatement(
361
+ prop.name, pytype_type, prop_default_value
362
+ )
363
+ )
332
364
 
333
- if has_system_controlled:
365
+ if has_restricted_system_controlled:
334
366
  program.import_("typing.Optional", True)
367
+ program.import_("sera.typing.is_set", True)
335
368
  cls_ast(
336
- stmt.LineBreak(),
337
- stmt.Comment(
338
- "_verified is a special marker to indicate whether the data is updated by the system."
339
- ),
340
- stmt.DefClassVarStatement(
341
- "_verified", "bool", expr.ExprConstant(False)
342
- ),
343
369
  stmt.LineBreak(),
344
370
  lambda ast: ast.func(
345
371
  "__post_init__",
@@ -347,12 +373,17 @@ def make_python_data_model(
347
373
  DeferredVar.simple("self"),
348
374
  ],
349
375
  )(
350
- stmt.AssignStatement(
351
- PredefinedFn.attr_getter(
352
- expr.ExprIdent("self"), expr.ExprIdent("_verified")
353
- ),
354
- expr.ExprConstant(False),
355
- )
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
+ ]
356
387
  ),
357
388
  stmt.LineBreak(),
358
389
  lambda ast: ast.func(
@@ -381,14 +412,9 @@ def make_python_data_model(
381
412
  )
382
413
  for prop in cls.properties.values()
383
414
  if prop.data.is_system_controlled
415
+ == SystemControlledMode.RESTRICTED
384
416
  ]
385
417
  ),
386
- stmt.AssignStatement(
387
- PredefinedFn.attr_getter(
388
- expr.ExprIdent("self"), expr.ExprIdent("_verified")
389
- ),
390
- expr.ExprConstant(True),
391
- ),
392
418
  ),
393
419
  )
394
420
 
@@ -405,14 +431,27 @@ def make_python_data_model(
405
431
  )(
406
432
  (
407
433
  stmt.AssertionStatement(
408
- PredefinedFn.attr_getter(
409
- expr.ExprIdent("self"), expr.ExprIdent("_verified")
434
+ expr.ExprLogicalAnd(
435
+ [
436
+ expr.ExprFuncCall(
437
+ expr.ExprIdent("is_set"),
438
+ [
439
+ PredefinedFn.attr_getter(
440
+ expr.ExprIdent("self"),
441
+ expr.ExprIdent(prop.name),
442
+ )
443
+ ],
444
+ )
445
+ for prop in cls.properties.values()
446
+ if prop.data.is_system_controlled
447
+ == SystemControlledMode.RESTRICTED
448
+ ]
410
449
  ),
411
450
  expr.ExprConstant(
412
451
  "The model data must be verified before converting to db model"
413
452
  ),
414
453
  )
415
- if has_system_controlled
454
+ if has_restricted_system_controlled
416
455
  else None
417
456
  ),
418
457
  lambda ast10: ast10.return_(
@@ -421,8 +460,13 @@ def make_python_data_model(
421
460
  f"{cls.name}DB" if cls.db is not None else cls.name
422
461
  ),
423
462
  [
424
- to_db_type_conversion(
425
- program, expr.ExprIdent("self"), cls, prop
463
+ (
464
+ to_db_type_conversion(
465
+ program, expr.ExprIdent("self"), cls, prop
466
+ )
467
+ if prop.data.is_system_controlled
468
+ != SystemControlledMode.AUTO
469
+ else ident_manager.use("UNSET")
426
470
  )
427
471
  for prop in cls.properties.values()
428
472
  ],
@@ -623,8 +667,11 @@ def make_python_relational_model(
623
667
  program.import_("__future__.annotations", True)
624
668
  program.import_("sera.libs.base_orm.BaseORM", True)
625
669
  program.import_("sera.libs.base_orm.create_engine", True)
670
+ program.import_("sera.libs.base_orm.create_async_engine", True)
626
671
  program.import_("sqlalchemy.orm.DeclarativeBase", True)
627
672
  program.import_("sqlalchemy.orm.Session", True)
673
+ program.import_("sqlalchemy.ext.asyncio.AsyncSession", True)
674
+ program.import_("sqlalchemy.text", True)
628
675
 
629
676
  # assume configuration for the app at the top level
630
677
  program.import_(f"{app.config.path}.DB_CONNECTION", True)
@@ -673,14 +720,22 @@ def make_python_relational_model(
673
720
 
674
721
  program.root.linebreak()
675
722
  program.root.assign(
676
- DeferredVar("engine", force_name="engine"),
723
+ DeferredVar.simple("engine"),
677
724
  expr.ExprFuncCall(
678
725
  expr.ExprIdent("create_engine"),
679
726
  [
680
727
  expr.ExprIdent("DB_CONNECTION"),
681
- PredefinedFn.keyword_assignment(
682
- "debug", expr.ExprIdent("DB_DEBUG")
683
- ),
728
+ PredefinedFn.keyword_assignment("echo", expr.ExprIdent("DB_DEBUG")),
729
+ ],
730
+ ),
731
+ )
732
+ program.root.assign(
733
+ DeferredVar.simple("async_engine"),
734
+ expr.ExprFuncCall(
735
+ expr.ExprIdent("create_async_engine"),
736
+ [
737
+ expr.ExprIdent("DB_CONNECTION"),
738
+ PredefinedFn.keyword_assignment("echo", expr.ExprIdent("DB_DEBUG")),
684
739
  ],
685
740
  ),
686
741
  )
@@ -691,17 +746,96 @@ def make_python_relational_model(
691
746
  )
692
747
 
693
748
  program.root.linebreak()
694
- program.root.func("async_get_session", [], is_async=True)(
695
- lambda ast00: ast00.python_stmt("with Session(engine) as session:")(
696
- lambda ast01: ast01.python_stmt("yield session")
749
+ program.root.func("get_async_session", [], is_async=True)(
750
+ lambda ast: ast.python_stmt(
751
+ "async with AsyncSession(async_engine, expire_on_commit=False) as session:"
752
+ )(
753
+ lambda ast_l1: ast_l1.try_()(stmt.PythonStatement("yield session")),
754
+ lambda ast_l1: ast_l1.catch()(
755
+ stmt.SingleExprStatement(
756
+ expr.ExprAwait(
757
+ expr.ExprFuncCall(
758
+ PredefinedFn.attr_getter(
759
+ expr.ExprIdent("session"),
760
+ expr.ExprIdent("rollback"),
761
+ ),
762
+ [],
763
+ )
764
+ )
765
+ ),
766
+ stmt.PythonStatement("raise"),
767
+ ),
768
+ lambda ast_l1: ast_l1.else_()(
769
+ stmt.SingleExprStatement(
770
+ expr.ExprAwait(
771
+ expr.ExprFuncCall(
772
+ PredefinedFn.attr_getter(
773
+ expr.ExprIdent("session"), expr.ExprIdent("execute")
774
+ ),
775
+ [
776
+ expr.ExprFuncCall(
777
+ expr.ExprIdent("text"),
778
+ [expr.ExprConstant("RESET ROLE;")],
779
+ )
780
+ ],
781
+ )
782
+ )
783
+ ),
784
+ stmt.SingleExprStatement(
785
+ expr.ExprAwait(
786
+ expr.ExprFuncCall(
787
+ PredefinedFn.attr_getter(
788
+ expr.ExprIdent("session"), expr.ExprIdent("commit")
789
+ ),
790
+ [],
791
+ )
792
+ )
793
+ ),
794
+ ),
697
795
  )
698
796
  )
699
797
 
700
798
  program.root.linebreak()
701
799
  program.root.python_stmt("@contextmanager")
702
800
  program.root.func("get_session", [])(
703
- lambda ast00: ast00.python_stmt("with Session(engine) as session:")(
704
- lambda ast01: ast01.python_stmt("yield session")
801
+ lambda ast: ast.python_stmt(
802
+ "with Session(engine, expire_on_commit=False) as session:"
803
+ )(
804
+ lambda ast_l1: ast_l1.try_()(stmt.PythonStatement("yield session")),
805
+ lambda ast_l1: ast_l1.catch()(
806
+ stmt.SingleExprStatement(
807
+ expr.ExprFuncCall(
808
+ PredefinedFn.attr_getter(
809
+ expr.ExprIdent("session"), expr.ExprIdent("rollback")
810
+ ),
811
+ [],
812
+ )
813
+ ),
814
+ stmt.PythonStatement("raise"),
815
+ ),
816
+ lambda ast_l1: ast_l1.else_()(
817
+ stmt.SingleExprStatement(
818
+ expr.ExprFuncCall(
819
+ PredefinedFn.attr_getter(
820
+ expr.ExprIdent("session"), expr.ExprIdent("execute")
821
+ ),
822
+ [
823
+ expr.ExprFuncCall(
824
+ expr.ExprIdent("text"),
825
+ [expr.ExprConstant("RESET ROLE;")],
826
+ )
827
+ ],
828
+ )
829
+ ),
830
+ stmt.SingleExprStatement(
831
+ expr.ExprFuncCall(
832
+ PredefinedFn.attr_getter(
833
+ expr.ExprIdent("session"), expr.ExprIdent("commit")
834
+ ),
835
+ [],
836
+ )
837
+ ),
838
+ ),
705
839
  )
706
840
  )
707
841
 
@@ -4,6 +4,7 @@ from typing import Sequence
4
4
 
5
5
  from codegen.models import DeferredVar, Program, expr, stmt
6
6
  from loguru import logger
7
+
7
8
  from sera.misc import assert_not_null
8
9
  from sera.models import App, DataCollection, Package
9
10
 
@@ -35,13 +36,13 @@ def make_python_service(collection: DataCollection, target_pkg: Package):
35
36
  True,
36
37
  )
37
38
  program.import_(app.config.path + f".schema", True)
38
- program.import_("sera.libs.base_service.BaseService", True)
39
+ program.import_("sera.libs.base_service.BaseAsyncService", True)
39
40
 
40
41
  program.root(
41
42
  stmt.LineBreak(),
42
43
  lambda ast00: ast00.class_(
43
44
  collection.get_service_name(),
44
- [expr.ExprIdent(f"BaseService[{id_type}, {cls.name}]")],
45
+ [expr.ExprIdent(f"BaseAsyncService[{id_type}, {cls.name}]")],
45
46
  )(
46
47
  lambda ast01: ast01.func(
47
48
  "__init__",
sera/models/__init__.py CHANGED
@@ -5,7 +5,13 @@ from sera.models._enum import Enum
5
5
  from sera.models._module import App, Module, Package
6
6
  from sera.models._multi_lingual_string import MultiLingualString
7
7
  from sera.models._parse import parse_schema
8
- from sera.models._property import Cardinality, DataProperty, ObjectProperty, Property
8
+ from sera.models._property import (
9
+ Cardinality,
10
+ DataProperty,
11
+ ObjectProperty,
12
+ Property,
13
+ SystemControlledMode,
14
+ )
9
15
  from sera.models._schema import Schema
10
16
 
11
17
  __all__ = [
@@ -25,4 +31,5 @@ __all__ = [
25
31
  "PyTypeWithDep",
26
32
  "TsTypeWithDep",
27
33
  "Enum",
34
+ "SystemControlledMode",
28
35
  ]
@@ -3,7 +3,15 @@ from __future__ import annotations
3
3
  from dataclasses import dataclass
4
4
  from typing import Literal
5
5
 
6
- ConstraintName = Literal["phone_number", "email", "not_empty", "username", "password"]
6
+ ConstraintName = Literal[
7
+ "phone_number",
8
+ "email",
9
+ "not_empty",
10
+ "username",
11
+ "password",
12
+ "whole_number",
13
+ "positive_number",
14
+ ]
7
15
 
8
16
 
9
17
  @dataclass
@@ -25,6 +33,10 @@ class Constraint:
25
33
  )
26
34
  elif self.name == "password":
27
35
  return "msgspec.Meta(min_length=8, max_length=40)"
36
+ elif self.name == "whole_number":
37
+ return "msgspec.Meta(ge=0)"
38
+ elif self.name == "positive_number":
39
+ return "msgspec.Meta(gt=0)"
28
40
 
29
41
  raise NotImplementedError()
30
42
 
@@ -43,4 +55,6 @@ predefined_constraints: dict[ConstraintName, Constraint] = {
43
55
  "not_empty": Constraint("not_empty", ()),
44
56
  "username": Constraint("username", ()),
45
57
  "password": Constraint("password", ()),
58
+ "whole_number": Constraint("whole_number", ()),
59
+ "positive_number": Constraint("positive_number", ()),
46
60
  }
sera/models/_datatype.py CHANGED
@@ -164,6 +164,16 @@ predefined_datatypes = {
164
164
  tstype=TsTypeWithDep(type="number"),
165
165
  is_list=False,
166
166
  ),
167
+ "date": DataType(
168
+ pytype=PyTypeWithDep(type="date", deps=["datetime.date"]),
169
+ sqltype=SQLTypeWithDep(
170
+ type="Date",
171
+ mapped_pytype="date",
172
+ deps=["sqlalchemy.Date", "datetime.date"],
173
+ ),
174
+ tstype=TsTypeWithDep(type="string"),
175
+ is_list=False,
176
+ ),
167
177
  "datetime": DataType(
168
178
  pytype=PyTypeWithDep(type="datetime", deps=["datetime.datetime"]),
169
179
  sqltype=SQLTypeWithDep(
sera/models/_parse.py CHANGED
@@ -31,6 +31,7 @@ from sera.models._property import (
31
31
  ObjectPropDBInfo,
32
32
  ObjectProperty,
33
33
  PropDataAttrs,
34
+ SystemControlledMode,
34
35
  )
35
36
  from sera.models._schema import Schema
36
37
 
@@ -76,7 +77,7 @@ def _parse_class_without_prop(schema: Schema, clsname: str, cls: dict) -> Class:
76
77
  return Class(
77
78
  name=clsname,
78
79
  label=_parse_multi_lingual_string(cls["label"]),
79
- description=_parse_multi_lingual_string(cls["desc"]),
80
+ description=_parse_multi_lingual_string(cls.get("desc", "")),
80
81
  properties={},
81
82
  db=db,
82
83
  )
@@ -131,7 +132,9 @@ def _parse_property(
131
132
  constraints=[
132
133
  _parse_constraint(constraint) for constraint in _data.get("constraints", [])
133
134
  ],
134
- is_system_controlled=prop.get("is_system_controlled", False),
135
+ is_system_controlled=SystemControlledMode(
136
+ _data.get("is_system_controlled", SystemControlledMode.NO.value)
137
+ ),
135
138
  )
136
139
 
137
140
  assert isinstance(prop, dict), prop
@@ -222,12 +225,14 @@ def _parse_datatype(schema: Schema, datatype: dict | str) -> DataType:
222
225
  # the correct package yet.
223
226
  pytype=PyTypeWithDep(
224
227
  type=enum.name,
225
- dep=f"{schema.name}.models.enums.{enum.get_pymodule_name()}.{enum.name}",
228
+ deps=[
229
+ f"{schema.name}.models.enums.{enum.get_pymodule_name()}.{enum.name}"
230
+ ],
226
231
  ),
227
232
  sqltype=SQLTypeWithDep(
228
233
  type="String", mapped_pytype="str", deps=["sqlalchemy.String"]
229
234
  ),
230
- tstype=TsTypeWithDep(type=enum.name, dep="@/models/enums"),
235
+ tstype=TsTypeWithDep(type=enum.name, deps=["@/models/enums"]),
231
236
  is_list=is_list,
232
237
  )
233
238
 
sera/models/_property.py CHANGED
@@ -56,6 +56,19 @@ class Cardinality(str, Enum):
56
56
  ]
57
57
 
58
58
 
59
+ class SystemControlledMode(str, Enum):
60
+ """Indicates if this property is controlled by the system.
61
+
62
+ There are two modes:
63
+ 1. The system automatically sets the value, and users cannot modify it.
64
+ 2. Users with special roles are allowed to set the value and other users cannot modify it
65
+ """
66
+
67
+ AUTO = "auto"
68
+ RESTRICTED = "restricted"
69
+ NO = "no"
70
+
71
+
59
72
  @dataclass(kw_only=True)
60
73
  class PropDataAttrs:
61
74
  """Storing other attributes for generating data model (upsert & public) -- this is different from a db model"""
@@ -72,7 +85,7 @@ class PropDataAttrs:
72
85
  constraints: list[Constraint] = field(default_factory=list)
73
86
 
74
87
  # whether this property is controlled by the system or not
75
- is_system_controlled: bool = False
88
+ is_system_controlled: SystemControlledMode = SystemControlledMode.NO
76
89
 
77
90
 
78
91
  @dataclass(kw_only=True)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sera-2
3
- Version: 1.12.3
3
+ Version: 1.12.6
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -9,13 +9,13 @@ 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.10.0,<3.0.0)
12
+ Requires-Dist: codegen-2 (>=2.11.0,<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)
16
16
  Requires-Dist: msgspec (>=0.19.0,<0.20.0)
17
17
  Requires-Dist: serde2 (>=1.9.0,<2.0.0)
18
- Requires-Dist: sqlalchemy (>=2.0.40,<3.0.0)
18
+ Requires-Dist: sqlalchemy[asyncio] (>=2.0.41,<3.0.0)
19
19
  Requires-Dist: typer (>=0.12.3,<0.13.0)
20
20
  Project-URL: Repository, https://github.com/binh-vu/sera
21
21
  Description-Content-Type: text/markdown
@@ -2,36 +2,36 @@ sera/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
2
2
  sera/constants.py,sha256=mzAaMyIx8TJK0-RYYJ5I24C4s0Uvj26OLMJmBo0pxHI,123
3
3
  sera/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
4
  sera/libs/api_helper.py,sha256=47y1kcwk3Xd2ZEMnUj_0OwCuUmgwOs5kYrE95BDVUn4,5411
5
- sera/libs/base_orm.py,sha256=dyh0OT2sbHku5qPJXvRzYAHRTSXvvbQaS-Qwg65Bw04,2918
6
- sera/libs/base_service.py,sha256=8uKMaIicLc8LbN5vyaK9dGWoCLf_wuaaOw9ooSiqSWM,4614
5
+ sera/libs/base_orm.py,sha256=5hOH_diUeaABm3cpE2-9u50VRqG1QW2osPQnvVHIhIA,3365
6
+ sera/libs/base_service.py,sha256=AX1WoTHte6Z_birkkfagkNE6BrCLTlTjQE4jEsKEaAY,5152
7
7
  sera/libs/dag/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
8
8
  sera/libs/dag/_dag.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
9
9
  sera/libs/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
10
- sera/libs/middlewares/auth.py,sha256=aH2fJshQpJT_dw3lUr67doZ8zdf90XyakdY35qZ1iZs,2443
10
+ sera/libs/middlewares/auth.py,sha256=r6aix1ZBwxMd1Jv5hMCTB8a_gFOJQ6egvxIrf3DWEOs,2323
11
11
  sera/libs/middlewares/uscp.py,sha256=H5umW8iEQSCdb_MJ5Im49kxg1E7TpxSg1p2_2A5zI1U,2600
12
12
  sera/make/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
- sera/make/__main__.py,sha256=G5O7s6135-708honwqMFn2yPTs06WbGQTHpupID0eZ4,1417
13
+ sera/make/__main__.py,sha256=bt-gDF8E026OWc2zqr9_a3paMOiDkFd3ybWn8ltL2g0,1448
14
14
  sera/make/make_app.py,sha256=n9NtW73O3s_5Q31VHIRmnd-jEIcpDO7ksAsOdovde2s,5999
15
- sera/make/make_python_api.py,sha256=CM0NTcSUMgRagscvpZwrbI_PR9d4CsgZ2xUVM0rCVVg,26031
16
- sera/make/make_python_model.py,sha256=ldaDnuwudkq81w2hwKZIuRqFnqhxOAaIT_VhYandXG4,46128
17
- sera/make/make_python_services.py,sha256=RsinYZdfkrTlTn9CT50VgqGs9w6IZawsJx-KEmqfnEY,2062
15
+ sera/make/make_python_api.py,sha256=sf-J5Pt1LTyM_H-SgXSAvKjEMDrRA6WnDpgDPbJG360,26896
16
+ sera/make/make_python_model.py,sha256=XWoNmfVNfdzF3lwEaItPZjo27ibqL9yjHA3a0vrHsnA,51708
17
+ sera/make/make_python_services.py,sha256=0ZpWLwQ7Nwfn8BXAikAB4JRpNknpSJyJgY5b1cjtxV4,2073
18
18
  sera/make/make_typescript_model.py,sha256=ugDdSTw_1ayHLuL--92RQ8hf_D-dpJtnvmUZNxcwcDs,63687
19
19
  sera/misc/__init__.py,sha256=Dh4uDq0D4N53h3zhvmwfa5a0TPVRSUvLzb0hkFuPirk,411
20
20
  sera/misc/_formatter.py,sha256=aCGYL08l8f3aLODHxSocxBBwkRYEo3K1QzCDEn3suj0,1685
21
21
  sera/misc/_utils.py,sha256=V5g4oLGHOhUCR75Kkcn1w01pAvGvaepK-T8Z3pIgHjI,1450
22
- sera/models/__init__.py,sha256=vJC5Kzo_N7wd16ocNPy1VvAZDGNiWeiAhWJ4ihATKvA,780
22
+ sera/models/__init__.py,sha256=VcC7HvqXuYrkgXwzs2vOH6LJPpzFBkeDvYVNrd3P-6E,855
23
23
  sera/models/_class.py,sha256=Wf0e8x6-szG9TzoFkAlqj7_dG0SCICMBw_333n3paxk,2514
24
24
  sera/models/_collection.py,sha256=ZnQEriKC4X88Zz48Kn1AVZKH-1_l8OgWa-zf2kcQOOE,1414
25
- sera/models/_constraints.py,sha256=L6QwKL3hRJ5DvvIB4JNgLoyvTKbE9FrpYRzezM4CweE,1484
26
- sera/models/_datatype.py,sha256=RDn0lDl_b0O_PwRqobcD1O903owEAEoMzRf4dyRcgBk,6819
25
+ sera/models/_constraints.py,sha256=PsSOX94b9-73wZTcUif9KTzV9uTfoF0WN87g4GZXQmU,1827
26
+ sera/models/_datatype.py,sha256=y5kfim0G3gLhnGjiokFBr8leU1Y6as_Vw7oK-caOo68,7140
27
27
  sera/models/_default.py,sha256=ABggW6qdPR4ZDqIPJdJ0GCGQ-7kfsfZmQ_DchgZEa-I,137
28
28
  sera/models/_enum.py,sha256=sy0q7E646F-APsqrVQ52r1fAQ_DCAeaNq5YM5QN3zIk,2070
29
29
  sera/models/_module.py,sha256=8QRSCubZmdDP9rL58rGAS6X5VCrkc1ZHvuMu1I1KrWk,5043
30
30
  sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
31
- sera/models/_parse.py,sha256=uw6fvvh1ucGqE2jFTCCr-e6_qMfZfSVpaPolNxmrHww,9897
32
- sera/models/_property.py,sha256=SJSm5fZJimd2rQuL4UH_aZuNyp9v7x64xMbEVbtYx8Q,5633
31
+ sera/models/_parse.py,sha256=q_YZ7PrHWIN85_WW-fPP7-2gLXlGWM2-EIdbYXuG7Xg,10052
32
+ sera/models/_property.py,sha256=4y9F58D6DoX25-6aWPBRiE72nCPQy0KWlGNDTZXSV-8,6038
33
33
  sera/models/_schema.py,sha256=r-Gqg9Lb_wR3UrbNvfXXgt_qs5bts0t2Ve7aquuF_OI,1155
34
34
  sera/typing.py,sha256=Q4QMfbtfrCjC9tFfsZPhsAnbNX4lm4NHQ9lmjNXYdV0,772
35
- sera_2-1.12.3.dist-info/METADATA,sha256=5KV9Z9cwjWA1UZKqo9iCrj0XRwL54xS7JmRrGAc5S7s,858
36
- sera_2-1.12.3.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
37
- sera_2-1.12.3.dist-info/RECORD,,
35
+ sera_2-1.12.6.dist-info/METADATA,sha256=MlOqmC3MJASvXLuS8k0KO5UE8325j9Ff53RArvIn5Wo,867
36
+ sera_2-1.12.6.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
37
+ sera_2-1.12.6.dist-info/RECORD,,