ecodev-core 0.0.8__tar.gz → 0.0.10__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.8 → ecodev_core-0.0.10}/PKG-INFO +2 -1
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/__init__.py +3 -1
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/authentication.py +33 -17
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/list_utils.py +16 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/pandas_utils.py +10 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/pyproject.toml +2 -1
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/LICENSE.md +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/README.md +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/app_activity.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/app_rights.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/app_user.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/auth_configuration.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/backup.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/check_dependencies.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/custom_equal.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/db_connection.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/db_filters.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/db_insertion.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/db_retrieval.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/enum_utils.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/logger.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/permissions.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/pydantic_utils.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/read_write.py +0 -0
- {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/safe_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.1
|
|
2
2
|
Name: ecodev-core
|
|
3
|
-
Version: 0.0.
|
|
3
|
+
Version: 0.0.10
|
|
4
4
|
Summary: Low level sqlmodel/fastapi/pydantic building blocks
|
|
5
5
|
License: MIT
|
|
6
6
|
Author: Thomas Epelbaum
|
|
@@ -36,6 +36,7 @@ Classifier: Typing :: Typed
|
|
|
36
36
|
Requires-Dist: fastapi (>=0,<1)
|
|
37
37
|
Requires-Dist: httpx (>=0,<1)
|
|
38
38
|
Requires-Dist: numpy (>=1,<2)
|
|
39
|
+
Requires-Dist: openpyxl (>=3,<4)
|
|
39
40
|
Requires-Dist: pandas (>=2,<3)
|
|
40
41
|
Requires-Dist: passlib[bcyrypt] (>=1,<2)
|
|
41
42
|
Requires-Dist: psycopg2-binary (>=2,<3)
|
|
@@ -42,11 +42,13 @@ from ecodev_core.db_retrieval import ServerSideField
|
|
|
42
42
|
from ecodev_core.enum_utils import enum_converter
|
|
43
43
|
from ecodev_core.list_utils import first_or_default
|
|
44
44
|
from ecodev_core.list_utils import first_transformed_or_default
|
|
45
|
+
from ecodev_core.list_utils import group_by
|
|
45
46
|
from ecodev_core.list_utils import group_by_value
|
|
46
47
|
from ecodev_core.list_utils import lselect
|
|
47
48
|
from ecodev_core.list_utils import lselectfirst
|
|
48
49
|
from ecodev_core.logger import log_critical
|
|
49
50
|
from ecodev_core.logger import logger_get
|
|
51
|
+
from ecodev_core.pandas_utils import get_excelfile
|
|
50
52
|
from ecodev_core.pandas_utils import jsonify_series
|
|
51
53
|
from ecodev_core.pandas_utils import pd_equals
|
|
52
54
|
from ecodev_core.permissions import Permission
|
|
@@ -76,4 +78,4 @@ __all__ = [
|
|
|
76
78
|
'enum_converter', 'ServerSideFilter', 'get_rows', 'count_rows', 'ServerSideField', 'get_raw_df',
|
|
77
79
|
'generic_insertion', 'custom_equal', 'is_authorized_user', 'get_method', 'AppActivity',
|
|
78
80
|
'fastapi_monitor', 'dash_monitor', 'is_monitoring_user', 'get_recent_activities', 'select_user',
|
|
79
|
-
'get_access_token', 'safe_get_user', 'backup']
|
|
81
|
+
'get_access_token', 'safe_get_user', 'backup', 'group_by', 'get_excelfile']
|
|
@@ -39,6 +39,7 @@ CONTEXT = CryptContext(schemes=['bcrypt'], deprecated='auto')
|
|
|
39
39
|
MONITORING = 'monitoring'
|
|
40
40
|
MONITORING_ERROR = 'Could not validate credentials. You need to be the monitoring user to call this'
|
|
41
41
|
INVALID_USER = 'Invalid User'
|
|
42
|
+
INVALID_TFA = 'Invalid TFA code'
|
|
42
43
|
ADMIN_ERROR = 'Could not validate credentials. You need admin rights to call this'
|
|
43
44
|
INVALID_CREDENTIALS = 'Invalid Credentials'
|
|
44
45
|
log = logger_get(__name__)
|
|
@@ -93,7 +94,7 @@ class JwtAuth(AuthenticationBackend):
|
|
|
93
94
|
return True if token else False
|
|
94
95
|
|
|
95
96
|
@staticmethod
|
|
96
|
-
def authorized(form:
|
|
97
|
+
def authorized(form: Any):
|
|
97
98
|
"""
|
|
98
99
|
Check that the user information contained in the form corresponds to an admin user
|
|
99
100
|
"""
|
|
@@ -122,9 +123,19 @@ class JwtAuth(AuthenticationBackend):
|
|
|
122
123
|
|
|
123
124
|
def attempt_to_log(user: str,
|
|
124
125
|
password: str,
|
|
125
|
-
session: Session
|
|
126
|
+
session: Session,
|
|
127
|
+
tfa_value: Optional[str] = None
|
|
128
|
+
) -> Union[Dict, HTTPException]:
|
|
126
129
|
"""
|
|
127
|
-
Factorized security logic. Ensure that the user is a legit one with a valid password
|
|
130
|
+
Factorized security logic. Ensure that the user is a legit one with a valid password.
|
|
131
|
+
If so, generate a token (with or without encoded tfa_value depending on whether this argument is
|
|
132
|
+
passed as an argument). If not, returns an HTTP exception with an intelligible error message.
|
|
133
|
+
|
|
134
|
+
Attributes are:
|
|
135
|
+
user: the user as expected to be found in the AppUser db
|
|
136
|
+
password: the plain password, to be compared with the hashed one in the AppUser db
|
|
137
|
+
session: db connection
|
|
138
|
+
tfa_value: if filled, add it encoded to the generated token
|
|
128
139
|
"""
|
|
129
140
|
selector = select(AppUser).where(col(AppUser.user) == user)
|
|
130
141
|
if not (db_user := session.exec(selector).first()):
|
|
@@ -134,54 +145,54 @@ def attempt_to_log(user: str,
|
|
|
134
145
|
log.warning('invalid user')
|
|
135
146
|
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail=INVALID_CREDENTIALS)
|
|
136
147
|
|
|
137
|
-
return {'access_token': _create_access_token(data={'user_id': db_user.id}),
|
|
148
|
+
return {'access_token': _create_access_token(data={'user_id': db_user.id, 'tfa': tfa_value}),
|
|
138
149
|
'token_type': 'bearer'}
|
|
139
150
|
|
|
140
151
|
|
|
141
|
-
def is_authorized_user(token: str = Depends(SCHEME)) -> bool:
|
|
152
|
+
def is_authorized_user(token: str = Depends(SCHEME), tfa_value: Optional[str] = None) -> bool:
|
|
142
153
|
"""
|
|
143
154
|
Check if the passed token corresponds to an authorized user
|
|
144
155
|
"""
|
|
145
156
|
try:
|
|
146
|
-
return get_current_user(token) is not None
|
|
157
|
+
return get_current_user(token, tfa_value) is not None
|
|
147
158
|
except Exception:
|
|
148
159
|
return False
|
|
149
160
|
|
|
150
161
|
|
|
151
|
-
def safe_get_user(token: Dict) -> Union[AppUser, None]:
|
|
162
|
+
def safe_get_user(token: Dict, tfa: bool = False) -> Union[AppUser, None]:
|
|
152
163
|
"""
|
|
153
164
|
Safe method returning a user if one found given the passed token
|
|
154
165
|
"""
|
|
155
166
|
try:
|
|
156
|
-
return get_user(get_access_token(token))
|
|
167
|
+
return get_user(get_access_token(token), token['tfa'] if tfa else None)
|
|
157
168
|
except (HTTPException, AttributeError):
|
|
158
169
|
return None
|
|
159
170
|
|
|
160
171
|
|
|
161
|
-
def get_user(token: str = Depends(SCHEME)) -> AppUser:
|
|
172
|
+
def get_user(token: str = Depends(SCHEME), tfa_value: Optional[str] = None) -> AppUser:
|
|
162
173
|
"""
|
|
163
174
|
Retrieves (if it exists) the db user corresponding to the passed token
|
|
164
175
|
"""
|
|
165
|
-
if user := get_current_user(token):
|
|
176
|
+
if user := get_current_user(token, tfa_value):
|
|
166
177
|
return user
|
|
167
178
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=INVALID_CREDENTIALS,
|
|
168
179
|
headers={'WWW-Authenticate': 'Bearer'})
|
|
169
180
|
|
|
170
181
|
|
|
171
|
-
def get_current_user(token: str) -> Union[AppUser, None]:
|
|
182
|
+
def get_current_user(token: str, tfa_value: Optional[str] = None) -> Union[AppUser, None]:
|
|
172
183
|
"""
|
|
173
184
|
Retrieves (if it exists) a valid (meaning who has valid credentials) user from the db
|
|
174
185
|
"""
|
|
175
|
-
token = _verify_access_token(token)
|
|
186
|
+
token = _verify_access_token(token, tfa_value)
|
|
176
187
|
with Session(engine) as session:
|
|
177
188
|
return session.exec(select(AppUser).where(col(AppUser.id) == token.id)).first()
|
|
178
189
|
|
|
179
190
|
|
|
180
|
-
def is_admin_user(token: str = Depends(SCHEME)) -> AppUser:
|
|
191
|
+
def is_admin_user(token: str = Depends(SCHEME), tfa_value: Optional[str] = None) -> AppUser:
|
|
181
192
|
"""
|
|
182
193
|
Retrieves (if it exists) the admin (meaning who has valid credentials) user from the db
|
|
183
194
|
"""
|
|
184
|
-
if (user := get_current_user(token)) and user.permission == Permission.ADMIN:
|
|
195
|
+
if (user := get_current_user(token, tfa_value)) and user.permission == Permission.ADMIN:
|
|
185
196
|
return user
|
|
186
197
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=ADMIN_ERROR,
|
|
187
198
|
headers={'WWW-Authenticate': 'Bearer'})
|
|
@@ -197,22 +208,27 @@ def is_monitoring_user(token: str = Depends(SCHEME)) -> AppUser:
|
|
|
197
208
|
detail=MONITORING_ERROR, headers={'WWW-Authenticate': 'Bearer'})
|
|
198
209
|
|
|
199
210
|
|
|
200
|
-
def _create_access_token(data: Dict) -> str:
|
|
211
|
+
def _create_access_token(data: Dict, tfa_value: Optional[str] = None) -> str:
|
|
201
212
|
"""
|
|
202
213
|
Create an access token out of the passed data. Only called if credentials are valid
|
|
203
214
|
"""
|
|
204
215
|
to_encode = data.copy()
|
|
205
216
|
expire = datetime.now(timezone.utc) + timedelta(minutes=AUTH.access_token_expire_minutes)
|
|
206
|
-
to_encode
|
|
217
|
+
to_encode['exp'] = expire
|
|
218
|
+
if tfa_value:
|
|
219
|
+
to_encode['tfa'] = _hash_password(tfa_value)
|
|
207
220
|
return jwt.encode(to_encode, AUTH.secret_key, algorithm=AUTH.algorithm)
|
|
208
221
|
|
|
209
222
|
|
|
210
|
-
def _verify_access_token(token: str) -> TokenData:
|
|
223
|
+
def _verify_access_token(token: str, tfa_value: Optional[str] = None) -> TokenData:
|
|
211
224
|
"""
|
|
212
225
|
Retrieves the token data associated to the passed token if it contains valid credential info.
|
|
213
226
|
"""
|
|
214
227
|
try:
|
|
215
228
|
payload = jwt.decode(token, AUTH.secret_key, algorithms=[AUTH.algorithm])
|
|
229
|
+
if tfa_value and not _check_password(tfa_value, payload.get('tfa')):
|
|
230
|
+
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=INVALID_TFA,
|
|
231
|
+
headers={'WWW-Authenticate': 'Bearer'})
|
|
216
232
|
if (user_id := payload.get('user_id')) is None:
|
|
217
233
|
raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail=INVALID_USER,
|
|
218
234
|
headers={'WWW-Authenticate': 'Bearer'})
|
|
@@ -2,11 +2,14 @@
|
|
|
2
2
|
Module implementing helper methods working on lists
|
|
3
3
|
"""
|
|
4
4
|
from collections import defaultdict
|
|
5
|
+
from itertools import groupby
|
|
5
6
|
from typing import Any
|
|
6
7
|
from typing import Callable
|
|
7
8
|
from typing import Dict
|
|
9
|
+
from typing import Iterator
|
|
8
10
|
from typing import List
|
|
9
11
|
from typing import Optional
|
|
12
|
+
from typing import Tuple
|
|
10
13
|
from typing import Union
|
|
11
14
|
|
|
12
15
|
|
|
@@ -42,6 +45,19 @@ def first_or_default(sequence: Union[List[Any], None],
|
|
|
42
45
|
return next((elt for elt in sequence if condition(elt)), default)
|
|
43
46
|
|
|
44
47
|
|
|
48
|
+
def group_by(sequence: List[Any], key: Union[Callable, None]) -> Iterator[Tuple[Any, List[Any]]]:
|
|
49
|
+
"""
|
|
50
|
+
Extension of itertools groupby method.
|
|
51
|
+
|
|
52
|
+
Reasons of existence:
|
|
53
|
+
- do the sorting before the grouping to avoid the usual mistake of forgetting the sorting
|
|
54
|
+
- convert the group Iterator to a list. More convenient that the default groupby behaviour
|
|
55
|
+
in all cases where you need to iterate more than once on the group
|
|
56
|
+
"""
|
|
57
|
+
for key, group in groupby(sorted(sequence, key=key), key=key):
|
|
58
|
+
yield key, list(group)
|
|
59
|
+
|
|
60
|
+
|
|
45
61
|
def lselect(sequence: List[Any], condition: Union[Callable, None] = None) -> List[Any]:
|
|
46
62
|
"""
|
|
47
63
|
Filter the passed sequence according to the passed condition
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
Module implementing some utilitary methods on pandas types
|
|
3
3
|
"""
|
|
4
4
|
import tempfile
|
|
5
|
+
from base64 import b64decode
|
|
5
6
|
from pathlib import Path
|
|
6
7
|
from typing import Dict
|
|
7
8
|
|
|
@@ -28,3 +29,12 @@ def jsonify_series(row: pd.Series) -> Dict:
|
|
|
28
29
|
"""
|
|
29
30
|
return {key: None if isinstance(value, float) and np.isnan(value) else value for key, value in
|
|
30
31
|
row.to_dict().items()}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def get_excelfile(contents: str) -> pd.ExcelFile:
|
|
35
|
+
"""
|
|
36
|
+
Function which converts user xlsx file upload into a pd.ExcelFile
|
|
37
|
+
"""
|
|
38
|
+
content_type, content_string = contents.split(',')
|
|
39
|
+
xl = b64decode(content_string)
|
|
40
|
+
return pd.ExcelFile(xl)
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[tool.poetry]
|
|
2
2
|
name = "ecodev-core"
|
|
3
|
-
version = "0.0.
|
|
3
|
+
version = "0.0.10"
|
|
4
4
|
description = "Low level sqlmodel/fastapi/pydantic building blocks"
|
|
5
5
|
authors = ["Thomas Epelbaum <tomepel@gmail.com>",
|
|
6
6
|
"Olivier Gabriel <olivier.gabriel.geom@gmail.com>",
|
|
@@ -53,6 +53,7 @@ sqladmin = "0.15.2"
|
|
|
53
53
|
httpx = "~0"
|
|
54
54
|
pydantic-settings = "~2"
|
|
55
55
|
psycopg2-binary = "~2"
|
|
56
|
+
openpyxl = "~3"
|
|
56
57
|
|
|
57
58
|
|
|
58
59
|
[build-system]
|
|
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
|