sera-2 1.24.1__py3-none-any.whl → 1.25.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/libs/base_service.py CHANGED
@@ -10,8 +10,8 @@ from sqlalchemy.orm import contains_eager, load_only
10
10
 
11
11
  from sera.libs.base_orm import BaseORM
12
12
  from sera.libs.search_helper import Query, QueryOp
13
- from sera.misc import assert_not_null, to_snake_case
14
- from sera.models import Cardinality, Class, DataProperty, ObjectProperty
13
+ from sera.misc import assert_isinstance, assert_not_null, to_snake_case
14
+ from sera.models import Cardinality, Class, DataProperty, IndexType, ObjectProperty
15
15
 
16
16
  R = TypeVar("R", bound=BaseORM)
17
17
  ID = TypeVar("ID") # ID of a class
@@ -20,6 +20,7 @@ SqlResult = TypeVar("SqlResult", bound=Result)
20
20
 
21
21
  class QueryResult(NamedTuple, Generic[R]):
22
22
  records: Sequence[R]
23
+ extra_columns: Mapping[str, Sequence]
23
24
  total: Optional[int]
24
25
 
25
26
 
@@ -36,6 +37,7 @@ class BaseAsyncService(Generic[ID, R]):
36
37
  self._cls_id_prop = getattr(self.orm_cls, self.id_prop.name)
37
38
  self.is_id_auto_increment = assert_not_null(self.id_prop.db).is_auto_increment
38
39
 
40
+ # mapping from property name to ORM class for object properties
39
41
  self.prop2orm: dict[str, type] = {
40
42
  prop.name: orm_classes[prop.target.name]
41
43
  for prop in cls.properties.values()
@@ -103,15 +105,15 @@ class BaseAsyncService(Generic[ID, R]):
103
105
  # A -> B is either 1:1 or N:1, we will store the foreign key is in A
104
106
  # .join(B, A.<foreign_key> == B.id)
105
107
  self.join_clauses[prop.name] = [
106
- {
107
- "class": target_tbl,
108
- "condition": getattr(
109
- target_tbl,
110
- assert_not_null(target_cls.get_id_property()).name,
111
- )
112
- == getattr(self.orm_cls, source_fk),
113
- "contains_eager": getattr(self.orm_cls, source_relprop),
114
- },
108
+ # {
109
+ # "class": target_tbl,
110
+ # "condition": getattr(
111
+ # target_tbl,
112
+ # assert_not_null(target_cls.get_id_property()).name,
113
+ # )
114
+ # == getattr(self.orm_cls, source_fk),
115
+ # "contains_eager": getattr(self.orm_cls, source_relprop),
116
+ # },
115
117
  ]
116
118
 
117
119
  @classmethod
@@ -135,6 +137,7 @@ class BaseAsyncService(Generic[ID, R]):
135
137
  session: The database session
136
138
  """
137
139
  q = self._select()
140
+ extra_cols = []
138
141
 
139
142
  if len(query.fields) > 0:
140
143
  q = q.options(
@@ -195,12 +198,60 @@ class BaseAsyncService(Generic[ID, R]):
195
198
  q = q.where(~getattr(self.orm_cls, clause.field).in_(clause.value))
196
199
  else:
197
200
  assert clause.op == QueryOp.fuzzy
198
- # Assuming fuzzy search is implemented as a full-text search
199
- q = q.where(
200
- func.to_tsvector(getattr(self.orm_cls, clause.field)).match(
201
- clause.value
202
- )
201
+ clause_prop = self.cls.properties[clause.field]
202
+ assert (
203
+ isinstance(clause_prop, DataProperty) and clause_prop.db is not None
203
204
  )
205
+ clause_orm_field = getattr(self.orm_cls, clause.field)
206
+ extra_cols.append(f"{clause.field}_score")
207
+
208
+ if clause_prop.db.index_type == IndexType.POSTGRES_FTS_SEVI:
209
+ # fuzzy search is implemented using Postgres Full-Text Search
210
+ # sevi is a custom text search configuration that we defined in `configs/postgres-fts.sql`
211
+ q = q.where(
212
+ func.to_tsvector("sevi", clause_orm_field).bool_op("@@")(
213
+ func.plainto_tsquery("sevi", clause.value)
214
+ )
215
+ )
216
+ # TODO: figure out which rank function is better
217
+ # https://www.postgresql.org/docs/current/textsearch-controls.html#TEXTSEARCH-RANKING
218
+ q = q.order_by(
219
+ func.ts_rank_cd(
220
+ func.to_tsvector("sevi", clause_orm_field),
221
+ func.plainto_tsquery("sevi", clause.value),
222
+ ).desc()
223
+ )
224
+ q = q.add_columns(
225
+ func.ts_rank_cd(
226
+ func.to_tsvector("sevi", clause_orm_field),
227
+ func.plainto_tsquery("sevi", clause.value),
228
+ ).label(f"{clause.field}_score")
229
+ )
230
+ elif clause_prop.db.index_type == IndexType.POSTGRES_TRIGRAM:
231
+ # fuzzy search is implemented using Postgres trigram index
232
+ # using a custom function f_unaccent to ignore accents -- see `configs/postgres-fts.sql`
233
+ q = q.where(
234
+ func.f_unaccent(clause_orm_field).bool_op("%>")(
235
+ func.f_unaccent(clause.value)
236
+ )
237
+ )
238
+ q = q.order_by(
239
+ func.f_unaccent(clause_orm_field).op("<->>")(
240
+ func.f_unaccent(clause.value)
241
+ )
242
+ )
243
+ q = q.add_columns(
244
+ (
245
+ 1
246
+ - func.f_unaccent(clause_orm_field).op("<->>")(
247
+ func.f_unaccent(clause.value)
248
+ )
249
+ ).label(f"{clause.field}_score")
250
+ )
251
+ else:
252
+ raise NotImplementedError(
253
+ f"Fuzzy search is not implemented for index type {clause_prop.db.index_type}"
254
+ )
204
255
 
205
256
  for join_condition in query.join_conditions:
206
257
  for join_clause in self.join_clauses[join_condition.prop]:
@@ -211,16 +262,29 @@ class BaseAsyncService(Generic[ID, R]):
211
262
  full=join_condition.join_type == "full",
212
263
  ).options(contains_eager(join_clause["contains_eager"]))
213
264
 
214
- print(">>>", join_clause)
215
-
216
- cq = select(func.count()).select_from(q.subquery())
265
+ # Create count query without order_by clause to improve performance
266
+ cq = select(func.count()).select_from(q.order_by(None).subquery())
217
267
  rq = q.limit(query.limit).offset(query.offset)
218
- records = self._process_result(await session.execute(rq)).scalars().all()
268
+
269
+ if len(extra_cols) == 0:
270
+ records = self._process_result(await session.execute(rq)).scalars().all()
271
+ extra_columns = {}
272
+ else:
273
+ records = []
274
+ raw_extra_columns = [[] for col in extra_cols]
275
+ for row in self._process_result(await session.execute(rq)):
276
+ records.append(row[0])
277
+ for i in range(len(extra_cols)):
278
+ raw_extra_columns[i].append(row[i + 1])
279
+ extra_columns = {
280
+ col: raw_extra_columns[i] for i, col in enumerate(extra_cols)
281
+ }
282
+
219
283
  if query.return_total:
220
284
  total = (await session.execute(cq)).scalar_one()
221
285
  else:
222
286
  total = None
223
- return QueryResult(records, total)
287
+ return QueryResult(records, extra_columns, total)
224
288
 
225
289
  async def get_by_id(self, id: ID, session: AsyncSession) -> Optional[R]:
226
290
  """Retrieving a record by ID."""
@@ -253,9 +253,9 @@ class Query(msgspec.Struct):
253
253
  else:
254
254
  if join_clause.join_type != "inner":
255
255
  output[target_name].extend(
256
- deser_func((val := getattr(record, source_relprop)))
256
+ deser_func(val)
257
257
  for record in result.records
258
- if val is not None
258
+ if (val := getattr(record, source_relprop)) is not None
259
259
  )
260
260
  else:
261
261
  output[target_name].extend(
@@ -265,6 +265,8 @@ class Query(msgspec.Struct):
265
265
 
266
266
  deser_func = dataschema[cls.name].from_db
267
267
  output[cls.name] = [deser_func(record) for record in result.records]
268
+ # include extra columns such as fuzzy search scores
269
+ output.update(result.extra_columns)
268
270
 
269
271
  return output
270
272
 
@@ -22,6 +22,7 @@ from sera.models import (
22
22
  Cardinality,
23
23
  Class,
24
24
  DataProperty,
25
+ IndexType,
25
26
  ObjectProperty,
26
27
  Package,
27
28
  PyTypeWithDep,
@@ -1207,14 +1208,79 @@ def make_python_relational_model(
1207
1208
  )
1208
1209
 
1209
1210
  index_stmts = []
1210
- if len(cls.db.indices) > 0:
1211
+
1212
+ if len(cls.db.indices) > 0 or any(
1213
+ isinstance(prop, DataProperty)
1214
+ and prop.db is not None
1215
+ and prop.db.is_indexed
1216
+ and (
1217
+ prop.db.index_type == IndexType.POSTGRES_FTS_SEVI
1218
+ or prop.db.index_type == IndexType.POSTGRES_TRIGRAM
1219
+ )
1220
+ for prop in cls.properties.values()
1221
+ ):
1211
1222
  program.import_("sqlalchemy.Index", True)
1223
+
1224
+ fts_index = []
1225
+ for prop in cls.properties.values():
1226
+ if (
1227
+ not isinstance(prop, DataProperty)
1228
+ or prop.db is None
1229
+ or not prop.db.is_indexed
1230
+ ):
1231
+ continue
1232
+ if prop.db.index_type == IndexType.POSTGRES_FTS_SEVI:
1233
+ fts_index.append(
1234
+ expr.ExprFuncCall(
1235
+ expr.ExprIdent("Index"),
1236
+ [
1237
+ expr.ExprConstant(
1238
+ f"ix_{cls.db.table_name}_{get_property_name(prop)}_gin"
1239
+ ),
1240
+ expr.ExprFuncCall(
1241
+ ident_manager.use("text"),
1242
+ [
1243
+ expr.ExprConstant(
1244
+ f"to_tsvector('sevi', {get_property_name(prop)})"
1245
+ )
1246
+ ],
1247
+ ),
1248
+ PredefinedFn.keyword_assignment(
1249
+ "postgresql_using", expr.ExprConstant("gin")
1250
+ ),
1251
+ ],
1252
+ )
1253
+ )
1254
+ if prop.db.index_type == IndexType.POSTGRES_TRIGRAM:
1255
+ fts_index.append(
1256
+ expr.ExprFuncCall(
1257
+ expr.ExprIdent("Index"),
1258
+ [
1259
+ expr.ExprConstant(
1260
+ f"ix_{cls.db.table_name}_{get_property_name(prop)}_gist"
1261
+ ),
1262
+ expr.ExprFuncCall(
1263
+ expr.ExprIdent("text"),
1264
+ [
1265
+ expr.ExprConstant(
1266
+ f"f_unaccent({get_property_name(prop)}) gist_trgm_ops(siglen=256)"
1267
+ )
1268
+ ],
1269
+ ),
1270
+ PredefinedFn.keyword_assignment(
1271
+ "postgresql_using", expr.ExprConstant("gist")
1272
+ ),
1273
+ ],
1274
+ )
1275
+ )
1276
+
1212
1277
  index_stmts.append(
1213
1278
  stmt.DefClassVarStatement(
1214
1279
  "_table_args__",
1215
1280
  None,
1216
1281
  PredefinedFn.tuple(
1217
- [
1282
+ fts_index
1283
+ + [
1218
1284
  expr.ExprFuncCall(
1219
1285
  expr.ExprIdent("Index"),
1220
1286
  [expr.ExprConstant(index.name)]
@@ -1317,7 +1383,8 @@ def make_python_relational_model(
1317
1383
  "unique", expr.ExprConstant(True)
1318
1384
  )
1319
1385
  )
1320
- elif prop.db.is_indexed:
1386
+ elif prop.db.is_indexed and prop.db.index_type == IndexType.DEFAULT:
1387
+ # only add index=True for default index type
1321
1388
  propvalargs.append(
1322
1389
  PredefinedFn.keyword_assignment(
1323
1390
  "index", expr.ExprConstant(True)
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
+ IndexType,
12
+ ObjectProperty,
13
+ Property,
14
+ )
9
15
  from sera.models._schema import Schema
10
16
 
11
17
  __all__ = [
@@ -14,6 +20,7 @@ __all__ = [
14
20
  "Property",
15
21
  "DataProperty",
16
22
  "ObjectProperty",
23
+ "IndexType",
17
24
  "Class",
18
25
  "Cardinality",
19
26
  "DataType",
sera/models/_class.py CHANGED
@@ -5,7 +5,7 @@ from typing import Optional
5
5
 
6
6
  from sera.misc import to_kebab_case, to_snake_case
7
7
  from sera.models._multi_lingual_string import MultiLingualString
8
- from sera.models._property import DataProperty, ObjectProperty
8
+ from sera.models._property import DataProperty, IndexType, ObjectProperty
9
9
 
10
10
 
11
11
  @dataclass(kw_only=True)
@@ -13,6 +13,7 @@ class Index:
13
13
  name: str
14
14
  columns: list[str]
15
15
  unique: bool = False
16
+ index_type: IndexType = IndexType.DEFAULT
16
17
 
17
18
 
18
19
  @dataclass(kw_only=True)
sera/models/_parse.py CHANGED
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import re
4
4
  from copy import deepcopy
5
+ from operator import index
5
6
  from pathlib import Path
6
7
  from typing import Sequence
7
8
 
@@ -30,6 +31,7 @@ from sera.models._property import (
30
31
  ForeignKeyOnDelete,
31
32
  ForeignKeyOnUpdate,
32
33
  GetSCPropValueFunc,
34
+ IndexType,
33
35
  ObjectPropDBInfo,
34
36
  ObjectProperty,
35
37
  PropDataAttrs,
@@ -150,7 +152,7 @@ def _parse_property(
150
152
 
151
153
  assert isinstance(prop, dict), prop
152
154
  if "datatype" in prop:
153
- return DataProperty(
155
+ return_prop = DataProperty(
154
156
  name=prop_name,
155
157
  label=_parse_multi_lingual_string(prop.get("label", prop_name)),
156
158
  description=_parse_multi_lingual_string(prop.get("desc", "")),
@@ -164,6 +166,9 @@ def _parse_property(
164
166
  is_indexed=db.get("is_indexed", False)
165
167
  or db.get("is_unique", False)
166
168
  or db.get("is_primary_key", False),
169
+ index_type=(
170
+ IndexType(db["index_type"]) if "index_type" in db else None
171
+ ),
167
172
  foreign_key=schema.classes.get(db.get("foreign_key")),
168
173
  )
169
174
  if "db" in prop
@@ -173,6 +178,10 @@ def _parse_property(
173
178
  default_value=_parse_default_value(prop.get("default_value", None)),
174
179
  default_factory=_parse_default_factory(prop.get("default_factory", None)),
175
180
  )
181
+ if return_prop.db is not None and return_prop.db.is_indexed:
182
+ if return_prop.db.index_type is None:
183
+ return_prop.db.index_type = IndexType.DEFAULT
184
+ return return_prop
176
185
 
177
186
  assert "target" in prop, prop
178
187
  return ObjectProperty(
sera/models/_property.py CHANGED
@@ -16,6 +16,12 @@ if TYPE_CHECKING:
16
16
  from sera.models._class import Class
17
17
 
18
18
 
19
+ class IndexType(str, Enum):
20
+ DEFAULT = "default"
21
+ POSTGRES_FTS_SEVI = "postgres_fts_sevi"
22
+ POSTGRES_TRIGRAM = "postgres_trigram"
23
+
24
+
19
25
  class ForeignKeyOnDelete(str, Enum):
20
26
  CASCADE = "cascade"
21
27
  SET_NULL = "set null"
@@ -150,6 +156,8 @@ class DataPropDBInfo:
150
156
  is_unique: bool = False
151
157
  # whether this property is indexed or not
152
158
  is_indexed: bool = False
159
+ # type of the index if it is indexed --- if is_indexed is True, this must be not None
160
+ index_type: Optional[IndexType] = None
153
161
  # this is used in conjunction with is_primary_key = True for the case of
154
162
  # extending a table with frequently updated properties. The value for the `foreign_key`
155
163
  # will be a target class. The cardinality is one-to-one, on target class deletion,
sera/typing.py CHANGED
@@ -39,4 +39,5 @@ GLOBAL_IDENTS = {
39
39
  "UNSET": "sera.typing.UNSET",
40
40
  "ForeignKey": "sqlalchemy.ForeignKey",
41
41
  "Optional": "typing.Optional",
42
+ "text": "sqlalchemy.text",
42
43
  }
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.3
1
+ Metadata-Version: 2.4
2
2
  Name: sera-2
3
- Version: 1.24.1
3
+ Version: 1.25.0
4
4
  Summary:
5
5
  Author: Binh Vu
6
6
  Author-email: bvu687@gmail.com
@@ -8,8 +8,9 @@ Requires-Python: >=3.12,<4.0
8
8
  Classifier: Programming Language :: Python :: 3
9
9
  Classifier: Programming Language :: Python :: 3.12
10
10
  Classifier: Programming Language :: Python :: 3.13
11
+ Classifier: Programming Language :: Python :: 3.14
11
12
  Requires-Dist: black (==25.1.0)
12
- Requires-Dist: codegen-2 (>=2.14.0,<3.0.0)
13
+ Requires-Dist: codegen-2 (>=2.15.1,<3.0.0)
13
14
  Requires-Dist: graph-wrapper (>=1.7.3,<2.0.0)
14
15
  Requires-Dist: isort (==6.0.1)
15
16
  Requires-Dist: litestar (>=2.15.1,<3.0.0)
@@ -7,7 +7,7 @@ sera/libs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
7
  sera/libs/api_helper.py,sha256=SkdgZkoYlX6HCyD_D6EiBvE0dWH0QW40xh4opIy737M,3237
8
8
  sera/libs/api_test_helper.py,sha256=SVGhl9UXPyCOw3IRK2znV6_c5tl1lTYYsb9qXJnJHoE,1344
9
9
  sera/libs/base_orm.py,sha256=5hOH_diUeaABm3cpE2-9u50VRqG1QW2osPQnvVHIhIA,3365
10
- sera/libs/base_service.py,sha256=wJ1x0xrxjczRU6Y43EjQb7RlkZ0tAY58tFayDXWBzLs,10773
10
+ sera/libs/base_service.py,sha256=I3mPufl0A0HKKkmYbs_9X0651bkedks4dRQgNnSa7ns,14114
11
11
  sera/libs/directed_computing_graph/__init__.py,sha256=xiF5_I1y9HtQ-cyq02iwkRYgEZvxBB8YIvysCHCLBco,1290
12
12
  sera/libs/directed_computing_graph/_dcg.py,sha256=nQf9MhnTkFU2-dxhv_PFThz9L61Nn8cvYIOoMq0OVb8,15352
13
13
  sera/libs/directed_computing_graph/_edge.py,sha256=iBq6cpLWWyuD99QWTHVEh8naWUJrR4WJJuq5iuCrwHo,1026
@@ -19,12 +19,12 @@ sera/libs/directed_computing_graph/_type_conversion.py,sha256=_XGvDidOJVmHS4gqdP
19
19
  sera/libs/middlewares/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
20
20
  sera/libs/middlewares/auth.py,sha256=r6aix1ZBwxMd1Jv5hMCTB8a_gFOJQ6egvxIrf3DWEOs,2323
21
21
  sera/libs/middlewares/uscp.py,sha256=DRy99nmS3qS5HLjRMIGP0oNUtQIci_a4hL5xQh-lXNY,2322
22
- sera/libs/search_helper.py,sha256=G7wig3K-mT0xWtn1lkG_YffZq8SzlBkot3H42HUz1sw,13675
22
+ sera/libs/search_helper.py,sha256=3ADhO6s6iIL3TWuH2zicAm_NmJfIm7wPY0cRQjESx2Y,13779
23
23
  sera/make/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
24
24
  sera/make/__main__.py,sha256=HRfOR53p351h6KblVvYm3DLhDIfEtk6R0kjl78_S_S8,1453
25
25
  sera/make/make_app.py,sha256=dTzpJRPGoCojCdJr1TAzwbaquctwwitrGPxkRm9skpo,6033
26
26
  sera/make/make_python_api.py,sha256=sfaYnX6Vwj0xWPpvpWbBhq8GyNHgNkvwlyZr7NDCO0k,24770
27
- sera/make/make_python_model.py,sha256=YHtZtHWN9X6s8IFiG-5w8sZSbsVWhU_sHtqVrkZkfgA,69686
27
+ sera/make/make_python_model.py,sha256=FYqtzfVMfJxRPuEVhFuYCG5-DzVOS2aW_5RUtgiQY7Y,72685
28
28
  sera/make/make_python_services.py,sha256=SvdU--QF23FN_d-Ao07CeXIK9d5eL_-mdXYwXx69dRE,2102
29
29
  sera/make/make_typescript_model.py,sha256=Ax6xVrZ-x3xaKCsv6X4NKLdaMsAOau_GDrZK1TJ8NL8,57543
30
30
  sera/make/ts_frontend/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -34,8 +34,8 @@ sera/make/ts_frontend/misc.py,sha256=R96Tm4Sz05YPVk-gJ8vGW_GbMoUM7sLStar1LVnEn-I
34
34
  sera/misc/__init__.py,sha256=ee7OODdWPKP8pCDxsfg1EOdxxKFMJCoNMljgV0NCxBw,767
35
35
  sera/misc/_formatter.py,sha256=aCGYL08l8f3aLODHxSocxBBwkRYEo3K1QzCDEn3suj0,1685
36
36
  sera/misc/_utils.py,sha256=_zWGjihxTEo3N1gzdxxB5CPCBBHxsviKWhQSCEonzVQ,13420
37
- sera/models/__init__.py,sha256=vJC5Kzo_N7wd16ocNPy1VvAZDGNiWeiAhWJ4ihATKvA,780
38
- sera/models/_class.py,sha256=_jLbewobv0Q6KLNd_jH-Z8vnDfwkRBbhdJbRDtKyE6g,2865
37
+ sera/models/__init__.py,sha256=ZCnwzgUXhmHEcx5a7nYLopsBXJmSnfSkjt4kUH_vPnM,833
38
+ sera/models/_class.py,sha256=495nP6j4pZIX1j0ZElUHNxe9doUX7vzSIwVrp_lAOHE,2922
39
39
  sera/models/_collection.py,sha256=72ZBdVa3UE8UyghJ8V-BPimO0f68zFRJvXdzfNaKDMo,2874
40
40
  sera/models/_constraints.py,sha256=SwUkvV8sESfuO3En6keA_r8GxKiarXYxMb5biml63lU,2021
41
41
  sera/models/_datatype.py,sha256=OlCnQlEpB4w9doz0hPhaZk5d4sF664CdGznSBvx0X9g,10293
@@ -43,10 +43,10 @@ sera/models/_default.py,sha256=ABggW6qdPR4ZDqIPJdJ0GCGQ-7kfsfZmQ_DchgZEa-I,137
43
43
  sera/models/_enum.py,sha256=FRtEfto2httDB308W8OAuHh2LSazV3v16DHixlz1IZA,2088
44
44
  sera/models/_module.py,sha256=r9_3nEbLaZKO-jvbZuFYy9DkM6E9jY8hBsBhQBULpVU,5428
45
45
  sera/models/_multi_lingual_string.py,sha256=JETN6k00VH4wrA4w5vAHMEJV8fp3SY9bJebskFTjQLA,1186
46
- sera/models/_parse.py,sha256=GL1a5HAlH4S389AyioLjz_9QFpH2rDTMOfXYXFC3fB4,12806
47
- sera/models/_property.py,sha256=9yMDxrmbyuF6-29lQjiq163Xzwbk75TlmGBpu0NLpkI,7485
46
+ sera/models/_parse.py,sha256=nihBEDni-jKwecSBagd6bupDOuCzGviuoD9u6-rT64Q,13203
47
+ sera/models/_property.py,sha256=Qo23KZl5OQNbuceygicgC3_Yv5ve1KxiFiMoWq6Ljj0,7758
48
48
  sera/models/_schema.py,sha256=VxJEiqgVvbXgcSUK4UW6JnRcggk4nsooVSE6MyXmfNY,1636
49
- sera/typing.py,sha256=m4rir-fB6Cgcm7_ZSXXcNdla2LJgq96WXxtTTrDaJno,1058
50
- sera_2-1.24.1.dist-info/METADATA,sha256=1DHbHN_a6VlNhVu8xrWJNig7IZCE2WzGvsRLGPnstGM,936
51
- sera_2-1.24.1.dist-info/WHEEL,sha256=b4K_helf-jlQoXBBETfwnf4B04YC67LOev0jo4fX5m8,88
52
- sera_2-1.24.1.dist-info/RECORD,,
49
+ sera/typing.py,sha256=7S6Ah-Yfcl8a0xAJ4lxEcguq3rYVM7SBeOiuxyJhC04,1089
50
+ sera_2-1.25.0.dist-info/METADATA,sha256=7NpIOo2Jk7y1K6hK81XLFOJ0oJiP765h1M5Jch841V0,987
51
+ sera_2-1.25.0.dist-info/WHEEL,sha256=M5asmiAlL6HEcOq52Yi5mmk9KmTVjY2RDPtO4p9DMrc,88
52
+ sera_2-1.25.0.dist-info/RECORD,,
@@ -1,4 +1,4 @@
1
1
  Wheel-Version: 1.0
2
- Generator: poetry-core 2.1.3
2
+ Generator: poetry-core 2.2.0
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any