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.

Files changed (25) hide show
  1. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/PKG-INFO +2 -1
  2. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/__init__.py +3 -1
  3. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/authentication.py +33 -17
  4. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/list_utils.py +16 -0
  5. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/pandas_utils.py +10 -0
  6. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/pyproject.toml +2 -1
  7. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/LICENSE.md +0 -0
  8. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/README.md +0 -0
  9. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/app_activity.py +0 -0
  10. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/app_rights.py +0 -0
  11. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/app_user.py +0 -0
  12. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/auth_configuration.py +0 -0
  13. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/backup.py +0 -0
  14. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/check_dependencies.py +0 -0
  15. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/custom_equal.py +0 -0
  16. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/db_connection.py +0 -0
  17. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/db_filters.py +0 -0
  18. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/db_insertion.py +0 -0
  19. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/db_retrieval.py +0 -0
  20. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/enum_utils.py +0 -0
  21. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/logger.py +0 -0
  22. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/permissions.py +0 -0
  23. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/pydantic_utils.py +0 -0
  24. {ecodev_core-0.0.8 → ecodev_core-0.0.10}/ecodev_core/read_write.py +0 -0
  25. {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.8
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: Dict):
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) -> Union[Dict, HTTPException]:
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.update({'exp': expire})
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.8"
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