ecodev-core 0.0.42__tar.gz → 0.0.44__tar.gz
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-0.0.42 → ecodev_core-0.0.44}/PKG-INFO +1 -1
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/__init__.py +13 -2
- ecodev_core-0.0.44/ecodev_core/db_upsertion.py +108 -0
- ecodev_core-0.0.44/ecodev_core/version.py +144 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/pyproject.toml +1 -1
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/LICENSE.md +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/README.md +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/app_activity.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/app_rights.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/app_user.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/auth_configuration.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/authentication.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/backup.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/check_dependencies.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/custom_equal.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/db_connection.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/db_filters.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/db_insertion.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/db_retrieval.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/deployment.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/email_sender.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/enum_utils.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/es_connection.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/list_utils.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/logger.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/pandas_utils.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/permissions.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/pydantic_utils.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/read_write.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/safe_utils.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/settings.py +0 -0
- {ecodev_core-0.0.42 → ecodev_core-0.0.44}/ecodev_core/sqlmodel_utils.py +0 -0
|
@@ -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_dict_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_data', 'upsert_deletor', 'get_row_versions', 'get_versions',
|
|
106
|
+
'db_to_value', 'upsert_dict_data', 'upsert_selector']
|
|
@@ -0,0 +1,108 @@
|
|
|
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_data(df: Union[pd.DataFrame], db_schema: SQLModelMetaclass, session: Session) -> None:
|
|
87
|
+
"""
|
|
88
|
+
Upsert the passed df into db_schema db.
|
|
89
|
+
"""
|
|
90
|
+
upsert_dict_data([x.to_dict() for _, x in df.iterrows()], db_schema, session)
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def upsert_dict_data(data: list[dict], db_schema: SQLModelMetaclass, session: Session) -> None:
|
|
94
|
+
"""
|
|
95
|
+
Upsert the passed list of dicts (corresponding to db_schema) into db_schema db.
|
|
96
|
+
"""
|
|
97
|
+
selector = partial(upsert_selector, db_schema=db_schema)
|
|
98
|
+
updator = partial(upsert_updator, db_schema=db_schema)
|
|
99
|
+
batches = [data[i:i + BATCH_SIZE] for i in range(0, len(data), BATCH_SIZE)]
|
|
100
|
+
|
|
101
|
+
for batch in progressbar.progressbar(batches, redirect_stdout=False):
|
|
102
|
+
for row in batch:
|
|
103
|
+
new_object = db_schema(**row)
|
|
104
|
+
if in_db := session.exec(selector(new_object)).first():
|
|
105
|
+
session.exec(updator(new_object, in_db.id, session))
|
|
106
|
+
else:
|
|
107
|
+
session.add(new_object)
|
|
108
|
+
session.commit()
|
|
@@ -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]
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|