ecodev-core 0.0.1__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 +79 -0
- ecodev_core/app_activity.py +108 -0
- ecodev_core/app_rights.py +24 -0
- ecodev_core/app_user.py +92 -0
- ecodev_core/auth_configuration.py +22 -0
- ecodev_core/authentication.py +226 -0
- ecodev_core/check_dependencies.py +179 -0
- ecodev_core/custom_equal.py +27 -0
- ecodev_core/db_connection.py +68 -0
- ecodev_core/db_filters.py +142 -0
- ecodev_core/db_insertion.py +108 -0
- ecodev_core/db_retrieval.py +194 -0
- ecodev_core/enum_utils.py +21 -0
- ecodev_core/list_utils.py +65 -0
- ecodev_core/logger.py +106 -0
- ecodev_core/pandas_utils.py +30 -0
- ecodev_core/permissions.py +15 -0
- ecodev_core/pydantic_utils.py +52 -0
- ecodev_core/read_write.py +40 -0
- ecodev_core/safe_utils.py +197 -0
- ecodev_core-0.0.1.dist-info/LICENSE.md +11 -0
- ecodev_core-0.0.1.dist-info/METADATA +72 -0
- ecodev_core-0.0.1.dist-info/RECORD +24 -0
- ecodev_core-0.0.1.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Low level methods to retrieve data from db in a paginated way
|
|
3
|
+
"""
|
|
4
|
+
from math import ceil
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing import Callable
|
|
7
|
+
from typing import Dict
|
|
8
|
+
from typing import List
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from typing import Tuple
|
|
11
|
+
from typing import Union
|
|
12
|
+
|
|
13
|
+
import pandas as pd
|
|
14
|
+
from sqlmodel import col
|
|
15
|
+
from sqlmodel import or_
|
|
16
|
+
from sqlmodel import select
|
|
17
|
+
from sqlmodel import Session
|
|
18
|
+
from sqlmodel.sql.expression import Select
|
|
19
|
+
from sqlmodel.sql.expression import SelectOfScalar
|
|
20
|
+
|
|
21
|
+
from ecodev_core.db_connection import engine
|
|
22
|
+
from ecodev_core.db_filters import SERVER_SIDE_FILTERS
|
|
23
|
+
from ecodev_core.db_filters import ServerSideFilter
|
|
24
|
+
from ecodev_core.list_utils import first_or_default
|
|
25
|
+
from ecodev_core.pydantic_utils import Frozen
|
|
26
|
+
|
|
27
|
+
SelectOfScalar.inherit_cache = True # type: ignore
|
|
28
|
+
Select.inherit_cache = True # type: ignore
|
|
29
|
+
OPERATORS = ['>=', '<=', '!=', '=', '<', '>', 'contains ']
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class ServerSideField(Frozen):
|
|
33
|
+
"""
|
|
34
|
+
Simple class used for sever side data retrieval
|
|
35
|
+
|
|
36
|
+
Attributes are:
|
|
37
|
+
- col_name: the name as it will appear on the frontend interface
|
|
38
|
+
- field_name: the SQLModel attribute name associated with this field
|
|
39
|
+
- field: the SQLModel attribute associated with this field
|
|
40
|
+
- filter: the filtering mechanism to use for this field
|
|
41
|
+
"""
|
|
42
|
+
col_name: str
|
|
43
|
+
field_name: str
|
|
44
|
+
field: Any
|
|
45
|
+
filter: ServerSideFilter
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def count_rows(fields: List[ServerSideField],
|
|
49
|
+
model: Any,
|
|
50
|
+
limit: Union[int, None] = None,
|
|
51
|
+
filter_str: str = '',
|
|
52
|
+
search_str: str = '',
|
|
53
|
+
search_cols: Optional[List] = None) -> int:
|
|
54
|
+
"""
|
|
55
|
+
Count the total number of rows in the db model, with statically defined field_filters fed with
|
|
56
|
+
dynamically set frontend filters. Divide this total number by limit to account for pagination.
|
|
57
|
+
"""
|
|
58
|
+
with Session(engine) as session:
|
|
59
|
+
count = _get_full_query(fields, model, filter_str, session, True,
|
|
60
|
+
search_str, search_cols).count()
|
|
61
|
+
|
|
62
|
+
return ceil(count / limit) if limit else count
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_rows(fields: List[ServerSideField],
|
|
66
|
+
model: Any,
|
|
67
|
+
limit: Union[int, None] = None,
|
|
68
|
+
offset: Union[int, None] = None,
|
|
69
|
+
filter_str: str = '',
|
|
70
|
+
search_str: str = '',
|
|
71
|
+
search_cols: Optional[List] = None,
|
|
72
|
+
fields_order: Optional[Callable] = None
|
|
73
|
+
) -> pd.DataFrame:
|
|
74
|
+
"""
|
|
75
|
+
Select relevant row lines from model db. Select the whole db if no limit or offset is provided.
|
|
76
|
+
Convert the rows to a dataframe in order to show the result in a dash data_table.
|
|
77
|
+
|
|
78
|
+
NB:
|
|
79
|
+
* 'fields_order' specify how to order the result rows
|
|
80
|
+
* 'limit' and 'offset' correspond to the pagination of the results.
|
|
81
|
+
* 'search_str' corresponds to the search string from the search input.
|
|
82
|
+
"""
|
|
83
|
+
with Session(engine) as session:
|
|
84
|
+
rows = _paginate_db_lines(fields, model, session, limit, offset, filter_str,
|
|
85
|
+
search_str, search_cols, fields_order)
|
|
86
|
+
if len(raw_df := pd.DataFrame.from_records([row.dict() for row in rows])) > 0:
|
|
87
|
+
return raw_df.rename(columns={field.field_name: field.col_name for field in fields}
|
|
88
|
+
)[[field.col_name for field in fields]]
|
|
89
|
+
return pd.DataFrame(columns=[field.col_name for field in fields])
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def _paginate_db_lines(fields: List[ServerSideField],
|
|
93
|
+
model: Any,
|
|
94
|
+
session: Session,
|
|
95
|
+
limit: Union[int, None],
|
|
96
|
+
offset: Union[int, None],
|
|
97
|
+
filter_str: str,
|
|
98
|
+
search_str: str = '',
|
|
99
|
+
search_cols: Optional[List] = None,
|
|
100
|
+
fields_order: Optional[Callable] = None,
|
|
101
|
+
) -> List:
|
|
102
|
+
"""
|
|
103
|
+
Select relevant row lines from model db. Select the whole db if no limit or offset is provided.
|
|
104
|
+
"""
|
|
105
|
+
if fields_order is None:
|
|
106
|
+
fields_order = _get_default_field_order(fields)
|
|
107
|
+
|
|
108
|
+
query = fields_order(_get_full_query(fields, model, filter_str, session, count=False,
|
|
109
|
+
search_str=search_str, search_cols=search_cols))
|
|
110
|
+
if limit is not None and offset is not None:
|
|
111
|
+
return list(session.exec(query.offset(offset * limit).limit(limit)))
|
|
112
|
+
return list(session.exec(query).all())
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def _get_full_query(fields: List[ServerSideField],
|
|
116
|
+
model: Any,
|
|
117
|
+
filter_str: str,
|
|
118
|
+
session: Session,
|
|
119
|
+
count: bool = False,
|
|
120
|
+
search_str: str = '',
|
|
121
|
+
search_cols: Optional[List] = None
|
|
122
|
+
) -> SelectOfScalar:
|
|
123
|
+
"""
|
|
124
|
+
Forge a complete select query given both search and filter strings
|
|
125
|
+
|
|
126
|
+
NB:
|
|
127
|
+
* This relies on the passed statically defined field_filters corresponding to the model.
|
|
128
|
+
* The field_filters are used jointly with the dynamically set frontend filters.
|
|
129
|
+
|
|
130
|
+
"""
|
|
131
|
+
filter_query = _get_filter_query(fields, model, _get_frontend_filters(filter_str), session,
|
|
132
|
+
count)
|
|
133
|
+
|
|
134
|
+
if not search_str or not search_cols:
|
|
135
|
+
return filter_query
|
|
136
|
+
|
|
137
|
+
return filter_query.where(or_(col(field).ilike(f'%{search_str.strip()}%')
|
|
138
|
+
for field in search_cols))
|
|
139
|
+
|
|
140
|
+
|
|
141
|
+
def _get_frontend_filters(raw_filters: str) -> Dict[str, Tuple[str, str]]:
|
|
142
|
+
"""
|
|
143
|
+
Forge a dictionary of field keys, (operator, value) values in order to filter a db model.
|
|
144
|
+
"""
|
|
145
|
+
split_filters = raw_filters.split(' && ')
|
|
146
|
+
return {elt[elt.find('{') + 1: elt.rfind('}')]: _forge_filter(elt) for elt in split_filters}
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
def _forge_filter(elt: str) -> Tuple[str, str]:
|
|
150
|
+
"""
|
|
151
|
+
Forge the operator and value associated to the passed element. Do so by scanning the ordered
|
|
152
|
+
sequence of OPERATORS and returning the first matching (value is on the right of it).
|
|
153
|
+
"""
|
|
154
|
+
return next(((key, elt.split(key)[-1]) for key in OPERATORS if key in elt), ('', ''))
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def _get_filter_query(fields: List[ServerSideField],
|
|
158
|
+
model: Any,
|
|
159
|
+
frontend_filters: Dict[str, Tuple[str, str]],
|
|
160
|
+
session: Session,
|
|
161
|
+
count: bool = False
|
|
162
|
+
) -> SelectOfScalar:
|
|
163
|
+
"""
|
|
164
|
+
Filter a model given backend static field_filters called with dynamically set frontend_filters.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
* either the query fetching the filtered rows (count = False)
|
|
168
|
+
* or the filter row count.
|
|
169
|
+
"""
|
|
170
|
+
query = session.query(model) if count else select(model)
|
|
171
|
+
if not frontend_filters or not all(frontend_filters.keys()):
|
|
172
|
+
return query
|
|
173
|
+
|
|
174
|
+
for key, (operator, value) in frontend_filters.items():
|
|
175
|
+
if field := first_or_default(fields, lambda x: x.col_name == key):
|
|
176
|
+
query = SERVER_SIDE_FILTERS[field.filter](query=query, operator=operator,
|
|
177
|
+
value=value, field=field.field)
|
|
178
|
+
|
|
179
|
+
return query
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
def _get_default_field_order(fields: List[ServerSideField]) -> Callable:
|
|
183
|
+
"""
|
|
184
|
+
Recover default field order from list of fields
|
|
185
|
+
"""
|
|
186
|
+
def fields_order(query):
|
|
187
|
+
"""
|
|
188
|
+
Default field ordering
|
|
189
|
+
|
|
190
|
+
Take the initial query as input and specify the order to use.
|
|
191
|
+
"""
|
|
192
|
+
return query.order_by(*[field.field for field in fields])
|
|
193
|
+
|
|
194
|
+
return fields_order
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing helper methods working on lists
|
|
3
|
+
"""
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from typing import Type
|
|
6
|
+
from typing import Union
|
|
7
|
+
|
|
8
|
+
from ecodev_core.safe_utils import stringify
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def enum_converter(field: Union[str, float],
|
|
12
|
+
enum_type: Type,
|
|
13
|
+
default: Union[Enum, None] = None
|
|
14
|
+
) -> Union[Enum, None]:
|
|
15
|
+
"""
|
|
16
|
+
Convert possibly None field to an enum_type if possible, return default otherwise
|
|
17
|
+
"""
|
|
18
|
+
try:
|
|
19
|
+
return enum_type(stringify(field))
|
|
20
|
+
except ValueError:
|
|
21
|
+
return default
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing helper methods working on lists
|
|
3
|
+
"""
|
|
4
|
+
from collections import defaultdict
|
|
5
|
+
from typing import Any
|
|
6
|
+
from typing import Callable
|
|
7
|
+
from typing import Dict
|
|
8
|
+
from typing import List
|
|
9
|
+
from typing import Optional
|
|
10
|
+
from typing import Union
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def group_by_value(list_to_group: List[Any]) -> Dict[Any, List[int]]:
|
|
14
|
+
"""
|
|
15
|
+
Given a list, group together all equal values by storing them in a dictionary.
|
|
16
|
+
The keys are the unique list values (think about overriding the class equals if you pass
|
|
17
|
+
to this method your custom classes) and the values are list of ints, corresponding to the
|
|
18
|
+
position of the current key in the original list.
|
|
19
|
+
|
|
20
|
+
See https://towardsdatascience.com/explaining-the-settingwithcopywarning-in-pandas-ebc19d799d25
|
|
21
|
+
for why not to use df['base_year'][values] for instance
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
indices: Dict[Any, List[int]] = defaultdict(list)
|
|
25
|
+
for i in range(len(list_to_group)):
|
|
26
|
+
indices[list_to_group[i]].append(i)
|
|
27
|
+
return indices
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def first_or_default(sequence: Union[List[Any], None],
|
|
31
|
+
condition: Union[Callable, None] = None,
|
|
32
|
+
default: Optional[Any] = None
|
|
33
|
+
) -> Union[Any, None]:
|
|
34
|
+
"""
|
|
35
|
+
Returns the first element of a sequence, or default value if the sequence contains no elements.
|
|
36
|
+
"""
|
|
37
|
+
if not sequence:
|
|
38
|
+
return default
|
|
39
|
+
|
|
40
|
+
if condition is None:
|
|
41
|
+
return next(iter(sequence), default)
|
|
42
|
+
return next((elt for elt in sequence if condition(elt)), default)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def lselect(sequence: List[Any], condition: Union[Callable, None] = None) -> List[Any]:
|
|
46
|
+
"""
|
|
47
|
+
Filter the passed sequence according to the passed condition
|
|
48
|
+
"""
|
|
49
|
+
return list(filter(condition, sequence))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def lselectfirst(sequence: List[Any], condition: Union[Callable, None] = None) -> Union[Any, None]:
|
|
53
|
+
"""
|
|
54
|
+
Select the filtered element of the passed sequence according to the passed condition
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
return filtered_list[0] if (filtered_list := list(filter(condition, sequence))) else None
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def first_transformed_or_default(sequence: List[Any], transformation: Callable) -> Union[Any, None]:
|
|
61
|
+
"""
|
|
62
|
+
Returns the first non-trivial transformed element of a sequence,
|
|
63
|
+
or default value if no non-trivial transformed elements are found.
|
|
64
|
+
"""
|
|
65
|
+
return next((fx for elt in sequence if (fx := transformation(elt)) is not None), None)
|
ecodev_core/logger.py
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Helpers for pretty logging
|
|
3
|
+
"""
|
|
4
|
+
import logging
|
|
5
|
+
import sys
|
|
6
|
+
import traceback
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def log_critical(message: str, logger):
|
|
10
|
+
"""
|
|
11
|
+
Traceback enabled for unintended serious errors
|
|
12
|
+
"""
|
|
13
|
+
logger.error(message)
|
|
14
|
+
logger.error(traceback.format_exc())
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def logger_get(name, level=logging.DEBUG):
|
|
18
|
+
"""
|
|
19
|
+
Main method called by all other modules to log
|
|
20
|
+
"""
|
|
21
|
+
logger = logging.getLogger(name)
|
|
22
|
+
config_log(logger, level, MyFormatter())
|
|
23
|
+
return logger
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class MyFormatter(logging.Formatter):
|
|
27
|
+
"""
|
|
28
|
+
Formatter to print %(filename)s:%(funcName)s:%(lineno)d on 24 characters
|
|
29
|
+
|
|
30
|
+
Typical format :
|
|
31
|
+
2016-10-26 14:20:21,379 | DEBUG | logger:log_me:57 : This is a log
|
|
32
|
+
"""
|
|
33
|
+
message_width = 110
|
|
34
|
+
cpath_width = 32
|
|
35
|
+
date_fmt = '%Y-%m-%d %H:%M:%S'
|
|
36
|
+
|
|
37
|
+
pink = '\x1b[35m'
|
|
38
|
+
green = '\x1b[32m'
|
|
39
|
+
yellow = '\x1b[33m'
|
|
40
|
+
red = '\x1b[31m'
|
|
41
|
+
bold_red = '\x1b[31;1m'
|
|
42
|
+
reset = '\x1b[0m'
|
|
43
|
+
|
|
44
|
+
FORMATS = {
|
|
45
|
+
logging.DEBUG: pink,
|
|
46
|
+
logging.INFO: green,
|
|
47
|
+
logging.WARNING: yellow,
|
|
48
|
+
logging.ERROR: red,
|
|
49
|
+
logging.CRITICAL: bold_red,
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
def format(self, record):
|
|
53
|
+
"""
|
|
54
|
+
Format logs
|
|
55
|
+
"""
|
|
56
|
+
initial_record = f'{record.module}:{record.funcName}:{ record.lineno}'
|
|
57
|
+
cpath = initial_record[-self.cpath_width:].ljust(self.cpath_width)
|
|
58
|
+
time = self.formatTime(record, self.date_fmt)
|
|
59
|
+
prefix = f'{time} | {record.levelname} | {record.process} | {cpath}'
|
|
60
|
+
|
|
61
|
+
# fixing max length
|
|
62
|
+
limited_lines = []
|
|
63
|
+
for line in record.getMessage().split(str('\n')):
|
|
64
|
+
while len(line) > self.message_width:
|
|
65
|
+
if (last_space_position := line[:self.message_width - 1].rfind(' ')) > 0:
|
|
66
|
+
splitting_position = last_space_position
|
|
67
|
+
else:
|
|
68
|
+
splitting_position = self.message_width
|
|
69
|
+
limited_lines.append(line[:splitting_position])
|
|
70
|
+
line = line[splitting_position:]
|
|
71
|
+
|
|
72
|
+
# don't forget end of line
|
|
73
|
+
limited_lines.append(line)
|
|
74
|
+
|
|
75
|
+
# formatting final message
|
|
76
|
+
final_message = ''.join(f'{prefix} | {line}\n' for line in limited_lines).rstrip()
|
|
77
|
+
|
|
78
|
+
return f'{self.FORMATS[record.levelno]}{final_message}{self.reset}'
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def config_log(logger, level, formatter):
|
|
82
|
+
""" Configures the logging.
|
|
83
|
+
|
|
84
|
+
This function defines the root logger. It needs to be called only once.
|
|
85
|
+
Then, all modules should log like this:
|
|
86
|
+
'''
|
|
87
|
+
from logger.logger import get as logger_get
|
|
88
|
+
log = logger_get(__name__)
|
|
89
|
+
'''
|
|
90
|
+
If the function is called more than once, duplicate handlers are ignored
|
|
91
|
+
to avoid duplicate logging.
|
|
92
|
+
|
|
93
|
+
Args:
|
|
94
|
+
logger: logging object
|
|
95
|
+
level: Logging level
|
|
96
|
+
formatter: Logging format
|
|
97
|
+
|
|
98
|
+
"""
|
|
99
|
+
# Get the root logger (because no name is specified in getLogger())
|
|
100
|
+
logger.setLevel(level)
|
|
101
|
+
logger.propagate = False
|
|
102
|
+
|
|
103
|
+
console_handler = logging.StreamHandler(stream=sys.stdout)
|
|
104
|
+
if all(handler.stream.name != console_handler.stream.name for handler in logger.handlers):
|
|
105
|
+
console_handler.setFormatter(formatter)
|
|
106
|
+
logger.addHandler(console_handler)
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing some utilitary methods on pandas types
|
|
3
|
+
"""
|
|
4
|
+
import tempfile
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Dict
|
|
7
|
+
|
|
8
|
+
import numpy as np
|
|
9
|
+
import pandas as pd
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def pd_equals(prediction: pd.DataFrame, gt_path=Path):
|
|
13
|
+
"""
|
|
14
|
+
Since some Nones are serialized as Nans by pandas (heavy type inference),
|
|
15
|
+
we store the prediction at a temporary location in order to reload it on the fly and compare it
|
|
16
|
+
to a pre-store ground truth, in order that both gt and prediction benefited from the same
|
|
17
|
+
type inferences.
|
|
18
|
+
"""
|
|
19
|
+
with tempfile.TemporaryDirectory() as folder:
|
|
20
|
+
prediction.to_csv(Path(folder) / 'tmp.csv', index=False)
|
|
21
|
+
reloaded_prediction = pd.read_csv(Path(folder) / 'tmp.csv')
|
|
22
|
+
pd.testing.assert_frame_equal(reloaded_prediction, pd.read_csv(gt_path))
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def jsonify_series(row: pd.Series) -> Dict:
|
|
26
|
+
"""
|
|
27
|
+
Convert a serie into a json compliant dictionary (replacing np.nans by Nones)
|
|
28
|
+
"""
|
|
29
|
+
return {key: None if isinstance(value, float) and np.isnan(value) else value for key, value in
|
|
30
|
+
row.to_dict().items()}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing all permission levels an application user can have
|
|
3
|
+
"""
|
|
4
|
+
from enum import Enum
|
|
5
|
+
from enum import unique
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@unique
|
|
9
|
+
class Permission(str, Enum):
|
|
10
|
+
"""
|
|
11
|
+
Enum listing all permission levels an application user can have
|
|
12
|
+
"""
|
|
13
|
+
ADMIN = 'Admin'
|
|
14
|
+
Consultant = 'Consultant'
|
|
15
|
+
Client = 'Client'
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Simple Pydantic wrapper classes around BaseModel to accommodate for orm and frozen cases
|
|
3
|
+
"""
|
|
4
|
+
from pydantic import BaseModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class Basic(BaseModel):
|
|
8
|
+
"""
|
|
9
|
+
Basic pydantic configuration
|
|
10
|
+
"""
|
|
11
|
+
|
|
12
|
+
class Config:
|
|
13
|
+
"""
|
|
14
|
+
Allow mutation in inheriting classes
|
|
15
|
+
"""
|
|
16
|
+
allow_mutation = True
|
|
17
|
+
arbitrary_types_allowed = True
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
class Frozen(BaseModel):
|
|
21
|
+
"""
|
|
22
|
+
Frozen pydantic configuration
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
class Config:
|
|
26
|
+
"""
|
|
27
|
+
Forbid mutation in order to freeze the inheriting classes
|
|
28
|
+
"""
|
|
29
|
+
allow_mutation = False
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class CustomFrozen(Frozen):
|
|
33
|
+
"""
|
|
34
|
+
Frozen pydantic configuration for custom types
|
|
35
|
+
"""
|
|
36
|
+
class Config:
|
|
37
|
+
"""
|
|
38
|
+
Allow arbitrary custom types
|
|
39
|
+
"""
|
|
40
|
+
arbitrary_types_allowed = True
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class OrmFrozen(CustomFrozen):
|
|
44
|
+
"""
|
|
45
|
+
Frozen pydantic configuration for orm like object
|
|
46
|
+
"""
|
|
47
|
+
|
|
48
|
+
class Config:
|
|
49
|
+
"""
|
|
50
|
+
Allow to create object from orm one
|
|
51
|
+
"""
|
|
52
|
+
orm_mode = True
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module regrouping low level reading and writing helper methods
|
|
3
|
+
"""
|
|
4
|
+
import json
|
|
5
|
+
import os
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import Dict
|
|
8
|
+
from typing import List
|
|
9
|
+
from typing import Union
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
def write_json_file(json_data: Union[Dict, List], file_path: Path):
|
|
13
|
+
"""
|
|
14
|
+
Write json_data at file_path location
|
|
15
|
+
"""
|
|
16
|
+
os.umask(0)
|
|
17
|
+
with open(file_path, 'w', encoding='utf-8') as f:
|
|
18
|
+
f.write(json.dumps(json_data, indent=4))
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def load_json_file(file_path: Path):
|
|
22
|
+
"""
|
|
23
|
+
Load a json file at file_path location
|
|
24
|
+
"""
|
|
25
|
+
with open(file_path, 'r', encoding='utf-8') as f:
|
|
26
|
+
loaded_json = json.load(f)
|
|
27
|
+
|
|
28
|
+
return loaded_json
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def make_dir(directory: Path):
|
|
32
|
+
"""
|
|
33
|
+
Helper that create the directory "directory" if it doesn't exist yet
|
|
34
|
+
"""
|
|
35
|
+
try:
|
|
36
|
+
os.umask(0)
|
|
37
|
+
os.makedirs(directory)
|
|
38
|
+
except OSError as error:
|
|
39
|
+
if not directory.is_dir():
|
|
40
|
+
raise OSError(f'directory={directory!r} should exist but does not.: {error}') from error
|