ecodev-core 0.0.43__py3-none-any.whl → 0.0.45__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.

Potentially problematic release.


This version of ecodev-core might be problematic. Click here for more details.

ecodev_core/__init__.py CHANGED
@@ -40,6 +40,12 @@ from ecodev_core.db_insertion import get_raw_df
40
40
  from ecodev_core.db_retrieval import count_rows
41
41
  from ecodev_core.db_retrieval import get_rows
42
42
  from ecodev_core.db_retrieval import ServerSideField
43
+ from ecodev_core.db_upsertion import field
44
+ from ecodev_core.db_upsertion import sfield
45
+ from ecodev_core.db_upsertion import upsert_data
46
+ from ecodev_core.db_upsertion import upsert_deletor
47
+ from ecodev_core.db_upsertion import upsert_df_data
48
+ from ecodev_core.db_upsertion import upsert_selector
43
49
  from ecodev_core.deployment import Deployment
44
50
  from ecodev_core.email_sender import send_email
45
51
  from ecodev_core.enum_utils import enum_converter
@@ -78,7 +84,10 @@ from ecodev_core.safe_utils import SafeTestCase
78
84
  from ecodev_core.safe_utils import SimpleReturn
79
85
  from ecodev_core.safe_utils import stringify
80
86
  from ecodev_core.settings import Settings
81
-
87
+ from ecodev_core.version import db_to_value
88
+ from ecodev_core.version import get_row_versions
89
+ from ecodev_core.version import get_versions
90
+ from ecodev_core.version import Version
82
91
  __all__ = [
83
92
  'AUTH', 'Token', 'get_app_services', 'attempt_to_log', 'get_current_user', 'is_admin_user',
84
93
  'write_json_file', 'load_json_file', 'make_dir', 'check_dependencies', 'compute_dependencies',
@@ -92,4 +101,6 @@ __all__ = [
92
101
  'fastapi_monitor', 'dash_monitor', 'is_monitoring_user', 'get_recent_activities', 'select_user',
93
102
  'get_access_token', 'safe_get_user', 'backup', 'group_by', 'get_excelfile', 'upsert_new_user',
94
103
  'datify', 'safe_drop_columns', 'get_value', 'is_null', 'send_email', 'first_func_or_default',
95
- 'sort_by_keys', 'sort_by_values', 'Settings', 'load_yaml_file', 'Deployment']
104
+ 'sort_by_keys', 'sort_by_values', 'Settings', 'load_yaml_file', 'Deployment', 'Version',
105
+ 'sfield', 'field', 'upsert_df_data', 'upsert_deletor', 'get_row_versions', 'get_versions',
106
+ 'db_to_value', 'upsert_data', 'upsert_selector']
@@ -0,0 +1,111 @@
1
+ """
2
+ Module handling CRUD and version operations
3
+ """
4
+ from functools import partial
5
+ from typing import Union
6
+
7
+ import pandas as pd
8
+ import progressbar
9
+ from sqlmodel import and_
10
+ from sqlmodel import Field
11
+ from sqlmodel import inspect
12
+ from sqlmodel import select
13
+ from sqlmodel import Session
14
+ from sqlmodel import SQLModel
15
+ from sqlmodel import update
16
+ from sqlmodel.main import SQLModelMetaclass
17
+ from sqlmodel.sql.expression import SelectOfScalar
18
+
19
+ from ecodev_core.version import get_row_versions
20
+ from ecodev_core.version import Version
21
+
22
+ BATCH_SIZE = 5000
23
+ FILTER_ON = 'filter_on'
24
+ INFO = 'info'
25
+
26
+
27
+ def sfield(**kwargs):
28
+ """
29
+ Field constructor for columns not to be versioned. Those are the columns on which to select.
30
+ They morally are a sort of unique identifier of a row (like id but more business meaningful)
31
+ """
32
+ return Field(**kwargs, sa_column_kwargs={INFO: {FILTER_ON: True}})
33
+
34
+
35
+ def field(**kwargs):
36
+ """
37
+ Field constructor for columns to be versioned.
38
+ """
39
+ return Field(**kwargs, sa_column_kwargs={INFO: {FILTER_ON: False}})
40
+
41
+
42
+ def upsert_selector(values: SQLModel, db_schema: SQLModelMetaclass) -> SelectOfScalar:
43
+ """
44
+ Return the query allowing to select on column not to be versioned values.
45
+ """
46
+ conditions = [getattr(db_schema, x.name) == getattr(values, x.name)
47
+ for x in inspect(db_schema).c if x.info.get(FILTER_ON) is True]
48
+ return select(db_schema).where(and_(*conditions))
49
+
50
+
51
+ def upsert_updator(values: SQLModel,
52
+ row_id: int,
53
+ session: Session,
54
+ db_schema: SQLModelMetaclass
55
+ ) -> None:
56
+ """
57
+ Update the passed row_id from db_schema db with passed new_values.
58
+ Only update columns to be versioned.
59
+
60
+ At the same time, store previous (column, row_id) versions for all columns that changed values.
61
+ """
62
+ to_update = {col.name: getattr(values, col.name)
63
+ for col in inspect(db_schema).c if col.info.get(FILTER_ON) is False}
64
+ db = session.exec(select(db_schema).where(db_schema.id == row_id)).first().model_dump()
65
+ col_types = {x: y.annotation for x, y in db_schema.__fields__.items()}
66
+ table = db_schema.__tablename__
67
+
68
+ for col, val in {k: v for k, v in db.items() if k in to_update and v != to_update[k]}.items():
69
+ session.add(Version.from_table_row(table, col, row_id, col_types[col], val))
70
+
71
+ return update(db_schema).where(db_schema.id == row_id).values(**to_update)
72
+
73
+
74
+ def upsert_deletor(values: SQLModel, session: Session):
75
+ """
76
+ Delete row in db corresponding to the passed values, selecting on columns not to be versioned.
77
+ """
78
+ db_schema = values.__class__
79
+ if in_db := session.exec(upsert_selector(values, db_schema=db_schema)).first():
80
+ for version in get_row_versions(db_schema.__tablename__, in_db.id, session):
81
+ session.delete(version)
82
+ session.delete(in_db)
83
+ session.commit()
84
+
85
+
86
+ def upsert_df_data(df: Union[pd.DataFrame], db_schema: SQLModelMetaclass, session: Session) -> None:
87
+ """
88
+ Upsert the passed df into db_schema db.
89
+ """
90
+ upsert_data([x.to_dict() for _, x in df.iterrows()], session, raw_db_schema=db_schema)
91
+
92
+
93
+ def upsert_data(data: list[dict | SQLModelMetaclass],
94
+ session: Session,
95
+ raw_db_schema: SQLModelMetaclass | None = None) -> None:
96
+ """
97
+ Upsert the passed list of dicts (corresponding to db_schema) into db_schema db.
98
+ """
99
+ db_schema = raw_db_schema or data[0].__class__
100
+ selector = partial(upsert_selector, db_schema=db_schema)
101
+ updator = partial(upsert_updator, db_schema=db_schema)
102
+ batches = [data[i:i + BATCH_SIZE] for i in range(0, len(data), BATCH_SIZE)]
103
+
104
+ for batch in progressbar.progressbar(batches, redirect_stdout=False):
105
+ for row in batch:
106
+ new_object = db_schema(**row) if isinstance(row, dict) else row
107
+ if in_db := session.exec(selector(new_object)).first():
108
+ session.exec(updator(new_object, in_db.id, session))
109
+ else:
110
+ session.add(new_object)
111
+ session.commit()
ecodev_core/version.py ADDED
@@ -0,0 +1,144 @@
1
+ """
2
+ Module implementing the Version table
3
+ """
4
+ from datetime import datetime
5
+ from enum import Enum
6
+ from enum import EnumType
7
+ from enum import unique
8
+ from typing import Optional
9
+
10
+ from sqlmodel import desc
11
+ from sqlmodel import Field
12
+ from sqlmodel import Index
13
+ from sqlmodel import select
14
+ from sqlmodel import Session
15
+ from sqlmodel import SQLModel
16
+
17
+
18
+ @unique
19
+ class ColType(str, Enum):
20
+ """
21
+ Enum listing all col types allowed to be stored in version
22
+ """
23
+ STR = 'str'
24
+ BOOL = 'bool'
25
+ INT = 'int'
26
+ FLOAT = 'float'
27
+ DATE = 'datetime'
28
+ ENUM = 'enum'
29
+
30
+
31
+ COL_TYPES = str | int | bool | float | datetime | Enum | None
32
+
33
+
34
+ class Version(SQLModel, table=True): # type: ignore
35
+ """
36
+ Table handling versioning.
37
+
38
+ Attributes are:
39
+ - table: the table to version
40
+ - column: the column of table to version
41
+ - row_id: the the row id of the column of table to version
42
+ - col_type: the column type
43
+ - value: the value to store (previous row/column/table version)
44
+ """
45
+ __tablename__ = 'version'
46
+ id: Optional[int] = Field(default=None, primary_key=True)
47
+ created_at: datetime = Field(default_factory=datetime.utcnow)
48
+ table: str = Field(index=True)
49
+ column: str = Field(index=True)
50
+ row_id: int = Field(index=True)
51
+ col_type: ColType
52
+ value: str | None = Field(index=True)
53
+
54
+ __table_args__ = (
55
+ Index(
56
+ 'version_filter_id',
57
+ 'table',
58
+ 'row_id',
59
+ 'column'
60
+ ),
61
+ )
62
+
63
+ @classmethod
64
+ def from_table_row(cls, table: str,
65
+ column: str,
66
+ row_id: int,
67
+ raw_type: type | EnumType,
68
+ raw_val: COL_TYPES
69
+ ) -> 'Version':
70
+ """
71
+ Create a new Version out of the passed information
72
+ """
73
+ col_type = _col_type_to_db(raw_type)
74
+ value = _value_to_db(raw_val, col_type)
75
+ return cls(table=table, column=column, row_id=row_id, col_type=col_type, value=value)
76
+
77
+
78
+ def get_versions(table: str, column: str, row_id: int, session: Session) -> list[Version]:
79
+ """
80
+ Retrieve all previous versions of a (table, column, row_id) triplet
81
+ """
82
+ query = select(Version).where(Version.table == table, Version.row_id == row_id,
83
+ Version.column == column).order_by(desc(Version.created_at))
84
+ return session.exec(query).all()
85
+
86
+
87
+ def get_row_versions(table: str, row_id: int, session: Session) -> list[Version]:
88
+ """
89
+ Retrieve all previous versions of a (table, row_id) couples. Hence all columns previous versions
90
+ """
91
+ query = select(Version).where(Version.table == table, Version.row_id == row_id
92
+ ).order_by(desc(Version.created_at))
93
+ return session.exec(query).all()
94
+
95
+
96
+ def _col_type_to_db(col_type: type | EnumType) -> ColType:
97
+ """
98
+ Forge ColType out of passed col_type
99
+ """
100
+ if col_type == int:
101
+ return ColType.INT
102
+ if col_type == bool:
103
+ return ColType.BOOL
104
+ if col_type == float:
105
+ return ColType.FLOAT
106
+ if col_type == str:
107
+ return ColType.STR
108
+ if col_type == datetime:
109
+ return ColType.DATE
110
+ return ColType.ENUM
111
+
112
+
113
+ def _value_to_db(value: COL_TYPES, col_type: ColType) -> str | None:
114
+ """
115
+ Convert a value to version to it's str version
116
+ """
117
+ if value is None:
118
+ return None
119
+
120
+ match col_type:
121
+ case ColType.BOOL | ColType.STR | ColType.INT | ColType.FLOAT:
122
+ return str(value)
123
+ case ColType.DATE:
124
+ return value.strftime('%Y-%m-%d %H:%M:%S.%f') # type: ignore[union-attr]
125
+ case ColType.ENUM:
126
+ return value.name # type: ignore[union-attr]
127
+ case _:
128
+ return None
129
+
130
+
131
+ def db_to_value(db_value: str | None, col_type: type | EnumType) -> COL_TYPES:
132
+ """
133
+ Convert back a str version value stored to a real value (types hancled listed in ColType)
134
+ NB: assumption that if the type is not known, this is an enum type.
135
+ """
136
+ if db_value is None:
137
+ return None
138
+ if col_type in [int, str, float]:
139
+ return col_type(db_value)
140
+ if col_type == bool:
141
+ return True if db_value == 'True' else False
142
+ if col_type == datetime:
143
+ return datetime.strptime(db_value, '%Y-%m-%d %H:%M:%S.%f')
144
+ return col_type[db_value] # type: ignore[index]
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: ecodev-core
3
- Version: 0.0.43
3
+ Version: 0.0.45
4
4
  Summary: Low level sqlmodel/fastapi/pydantic building blocks
5
5
  License: MIT
6
6
  Author: Thomas Epelbaum
@@ -1,4 +1,4 @@
1
- ecodev_core/__init__.py,sha256=gIm_-wiXh8YyM-7xSQ07uNFNlPyAngmw45Sd062CT-k,5152
1
+ ecodev_core/__init__.py,sha256=cIICQkHHfjmPIpLnpGlJ6GzGDvFT51bDeI8c1Kg3MH0,5781
2
2
  ecodev_core/app_activity.py,sha256=KBtI-35LBLPDppFB7xjxWthXQrY3Z_aGDnC-HrW8Ea0,4641
3
3
  ecodev_core/app_rights.py,sha256=RZPdDtydFqc_nFj96huKAc56BS0qS6ScKv4Kghqd6lc,726
4
4
  ecodev_core/app_user.py,sha256=r1bqA4H08x53XmxmjwyGKl_PFjYQazzBbVErdkztqeE,2947
@@ -11,6 +11,7 @@ ecodev_core/db_connection.py,sha256=bc5MujZ57f204wTsuNVdn1JdP-zBzkDJxHmdxBDTiNs,
11
11
  ecodev_core/db_filters.py,sha256=T_5JVF27UEu7sC6NOm7-W3_Y0GLfbWQO_EeTXcD2cv8,5041
12
12
  ecodev_core/db_insertion.py,sha256=RSCyAlUObbBlWJuMRX-YFY4VgtWqYLdwRqMWw--x95Y,3646
13
13
  ecodev_core/db_retrieval.py,sha256=IxyF3ZtKgACLiNFggK7boKggvMRKYDRD2IimxU4dap4,7345
14
+ ecodev_core/db_upsertion.py,sha256=HWx7yc1FAz6l6BQ_cPLUu5tIctO9F0WZj6zm1a06Fnw,4098
14
15
  ecodev_core/deployment.py,sha256=z8ACI00EtKknXOB8xyPwYIXTvPjIDOH9z9cBGEU0YrA,281
15
16
  ecodev_core/email_sender.py,sha256=XD7jAVXhGzvbiHqMhK9_aTEIS70Lw_CmPeAxRZGji-Y,1610
16
17
  ecodev_core/enum_utils.py,sha256=BkQ4YQ97tXBYmMcQiSIi0mbioD5CgVU79myg1BBAXuA,556
@@ -24,7 +25,8 @@ ecodev_core/read_write.py,sha256=YIGRERvFHU7vy-JIaCWAza4CPMysLRUHKJxN-ZgFmu0,120
24
25
  ecodev_core/safe_utils.py,sha256=Q8N15El1tSxZJJsy1i_1CCycuBN1_98QQoHmYJRcLIY,6904
25
26
  ecodev_core/settings.py,sha256=ARAPkXxggVUsYqSQIAgCK8C2DKSMPia1CekULn428bA,1562
26
27
  ecodev_core/sqlmodel_utils.py,sha256=t57H3QPtKRy4ujic1clMK_2L4p0yjGJLZbDjHPZ8M94,453
27
- ecodev_core-0.0.43.dist-info/LICENSE.md,sha256=8dqVJEbwXjPWjjRKjdLMym5k9Gi8hwtrHh84sti6KIs,1068
28
- ecodev_core-0.0.43.dist-info/METADATA,sha256=SgprQLCicMue6f4h59Bo4fvy2SxXAnoQ3ic7qUr0cEs,3509
29
- ecodev_core-0.0.43.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
30
- ecodev_core-0.0.43.dist-info/RECORD,,
28
+ ecodev_core/version.py,sha256=BUMmO_dxwyTPGIbnbOTFIED4JMmOmN8khQzP2ea-Obg,4377
29
+ ecodev_core-0.0.45.dist-info/LICENSE.md,sha256=8dqVJEbwXjPWjjRKjdLMym5k9Gi8hwtrHh84sti6KIs,1068
30
+ ecodev_core-0.0.45.dist-info/METADATA,sha256=SLR92pVQtSfoPqrU5Xp4hXcTEr4JjM-WCxsI-d9MvYg,3509
31
+ ecodev_core-0.0.45.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
32
+ ecodev_core-0.0.45.dist-info/RECORD,,