ecodev-core 0.0.67__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.
- ecodev_core/__init__.py +129 -0
- ecodev_core/app_activity.py +126 -0
- ecodev_core/app_rights.py +24 -0
- ecodev_core/app_user.py +92 -0
- ecodev_core/auth_configuration.py +24 -0
- ecodev_core/authentication.py +316 -0
- ecodev_core/backup.py +105 -0
- ecodev_core/check_dependencies.py +179 -0
- ecodev_core/custom_equal.py +27 -0
- ecodev_core/db_connection.py +94 -0
- ecodev_core/db_filters.py +142 -0
- ecodev_core/db_i18n.py +211 -0
- ecodev_core/db_insertion.py +128 -0
- ecodev_core/db_retrieval.py +193 -0
- ecodev_core/db_upsertion.py +382 -0
- ecodev_core/deployment.py +16 -0
- ecodev_core/email_sender.py +60 -0
- ecodev_core/encryption.py +46 -0
- ecodev_core/enum_utils.py +21 -0
- ecodev_core/es_connection.py +79 -0
- ecodev_core/list_utils.py +134 -0
- ecodev_core/logger.py +122 -0
- ecodev_core/pandas_utils.py +69 -0
- ecodev_core/permissions.py +21 -0
- ecodev_core/pydantic_utils.py +33 -0
- ecodev_core/read_write.py +52 -0
- ecodev_core/rest_api_client.py +211 -0
- ecodev_core/rest_api_configuration.py +25 -0
- ecodev_core/safe_utils.py +241 -0
- ecodev_core/settings.py +51 -0
- ecodev_core/sqlmodel_utils.py +16 -0
- ecodev_core/token_banlist.py +18 -0
- ecodev_core/version.py +144 -0
- ecodev_core-0.0.67.dist-info/LICENSE.md +11 -0
- ecodev_core-0.0.67.dist-info/METADATA +87 -0
- ecodev_core-0.0.67.dist-info/RECORD +37 -0
- ecodev_core-0.0.67.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing a high level Client for calling REST API endpoints
|
|
3
|
+
"""
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from datetime import timezone
|
|
6
|
+
from typing import Optional
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
from ecodev_core import logger_get
|
|
10
|
+
from ecodev_core.authentication import ALGO
|
|
11
|
+
from ecodev_core.authentication import SECRET_KEY
|
|
12
|
+
from jose import jwt
|
|
13
|
+
from pydantic import BaseModel
|
|
14
|
+
from ecodev_core.rest_api_configuration import LOGIN_URL
|
|
15
|
+
from ecodev_core.rest_api_configuration import API_USER
|
|
16
|
+
from ecodev_core.rest_api_configuration import API_PASSWORD
|
|
17
|
+
|
|
18
|
+
log = logger_get(__name__)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class RestApiClient(BaseModel):
|
|
22
|
+
"""
|
|
23
|
+
Client for making calls to internal REST API endpoints.
|
|
24
|
+
|
|
25
|
+
Attributes:
|
|
26
|
+
timeout (int): HTTP connection timeout 30 (sec).
|
|
27
|
+
_token (dict): Last fetched authentication token.
|
|
28
|
+
|
|
29
|
+
NB:
|
|
30
|
+
- When using this class, tokens should be accessed using the property `token` and \
|
|
31
|
+
not `_token` to enforce token auto-refresh and avoid using expired auth. tokens.
|
|
32
|
+
"""
|
|
33
|
+
timeout: int = 30
|
|
34
|
+
_token: dict = {}
|
|
35
|
+
|
|
36
|
+
def _get_new_token(self) -> dict:
|
|
37
|
+
"""
|
|
38
|
+
Fetches the authentication token from login API.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
ConnectionRefusedError: If the request returned None
|
|
42
|
+
|
|
43
|
+
Returns:
|
|
44
|
+
dict: The authentication token response from login API.
|
|
45
|
+
"""
|
|
46
|
+
if (data := handle_response(requests.post(f'{LOGIN_URL}',
|
|
47
|
+
data={'username': API_USER,
|
|
48
|
+
'password': API_PASSWORD}))) is None:
|
|
49
|
+
raise ConnectionRefusedError('Failed to login')
|
|
50
|
+
return data
|
|
51
|
+
|
|
52
|
+
@property
|
|
53
|
+
def token(self) -> dict:
|
|
54
|
+
"""
|
|
55
|
+
Returns the authentication token with auto-refresh logic if the
|
|
56
|
+
token is expired or within 1 minute to expiration.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
token (dict): Dictionary containing Authentication token
|
|
60
|
+
"""
|
|
61
|
+
if self.get_exp() < datetime.now(timezone.utc).timestamp() + 60:
|
|
62
|
+
self._token = self._get_new_token()
|
|
63
|
+
return self._token
|
|
64
|
+
|
|
65
|
+
def get_exp(self) -> float:
|
|
66
|
+
"""
|
|
67
|
+
Fetch expiration time from existing token `_token`. Defaults to current time.
|
|
68
|
+
|
|
69
|
+
Returns:
|
|
70
|
+
float: Token expiration time.
|
|
71
|
+
"""
|
|
72
|
+
try:
|
|
73
|
+
payload = jwt.decode(self._token.get('access_token'), SECRET_KEY, algorithms=[ALGO])
|
|
74
|
+
return payload.get('exp', datetime.now(timezone.utc).timestamp())
|
|
75
|
+
except Exception:
|
|
76
|
+
log.warning('Failed to decode token, exp set to current timestamp')
|
|
77
|
+
return datetime.now(timezone.utc).timestamp()
|
|
78
|
+
|
|
79
|
+
def _get_header(self) -> dict:
|
|
80
|
+
"""
|
|
81
|
+
Returns the headers with authorization information for
|
|
82
|
+
HTTP requests to internal REST APIs.
|
|
83
|
+
|
|
84
|
+
Returns:
|
|
85
|
+
dict: HTTP request headers internal REST APIs.
|
|
86
|
+
"""
|
|
87
|
+
return {'accept': 'application/json',
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
'Authorization': f'Bearer {self.token.get("access_token")}'}
|
|
90
|
+
|
|
91
|
+
def get(self,
|
|
92
|
+
url: str,
|
|
93
|
+
params: Optional[dict] = None):
|
|
94
|
+
"""
|
|
95
|
+
Attributes:
|
|
96
|
+
url (str): Url of the HTTP request
|
|
97
|
+
params (Optional[dict] = None): Query parameters to add to the url. \
|
|
98
|
+
Defaults to None.
|
|
99
|
+
|
|
100
|
+
Returns:
|
|
101
|
+
response_data (Any): Response body
|
|
102
|
+
"""
|
|
103
|
+
return handle_response(requests.get(url=url, headers=self._get_header(),
|
|
104
|
+
timeout=self.timeout, params=params))
|
|
105
|
+
|
|
106
|
+
def post(self,
|
|
107
|
+
url: str,
|
|
108
|
+
data: Optional[dict] = None,
|
|
109
|
+
params: Optional[dict] = None):
|
|
110
|
+
"""
|
|
111
|
+
Attributes:
|
|
112
|
+
url (str): Url of the HTTP request
|
|
113
|
+
data (Optional[dict] = None): The body/payload of the request. Defaults to None.
|
|
114
|
+
params (Optional[dict] = None): Query parameters to add to the url. \
|
|
115
|
+
Defaults to None.
|
|
116
|
+
|
|
117
|
+
Returns:
|
|
118
|
+
response_data (Any): Response body
|
|
119
|
+
"""
|
|
120
|
+
return handle_response(requests.post(url=url, data=data, headers=self._get_header(),
|
|
121
|
+
timeout=self.timeout, params=params))
|
|
122
|
+
|
|
123
|
+
def put(self,
|
|
124
|
+
url: str,
|
|
125
|
+
data: dict,
|
|
126
|
+
params: Optional[dict] = None):
|
|
127
|
+
"""
|
|
128
|
+
Attributes:
|
|
129
|
+
url (str): Url of the HTTP request
|
|
130
|
+
data (Optional[dict] = None): The body/payload of the request. Defaults to None.
|
|
131
|
+
params (Optional[dict]): Requests parameters to add to the url. \
|
|
132
|
+
Defaults to None.
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
response_data (Any): Response body
|
|
136
|
+
"""
|
|
137
|
+
return handle_response(requests.put(url=url, data=data, headers=self._get_header(),
|
|
138
|
+
timeout=self.timeout, params=params))
|
|
139
|
+
|
|
140
|
+
def patch(self,
|
|
141
|
+
url: str,
|
|
142
|
+
data: dict,
|
|
143
|
+
params: Optional[dict] = None):
|
|
144
|
+
"""
|
|
145
|
+
Attributes:
|
|
146
|
+
url (str): Url of the HTTP request
|
|
147
|
+
data (Optional[dict] = None): The body/payload of the request. Defaults to None.
|
|
148
|
+
params (Optional[dict]): Query parameters to add to the url. \
|
|
149
|
+
Defaults to None.
|
|
150
|
+
|
|
151
|
+
Returns:
|
|
152
|
+
response_data (Any): Response body
|
|
153
|
+
"""
|
|
154
|
+
return handle_response(requests.patch(url=url, data=data, headers=self._get_header(),
|
|
155
|
+
timeout=self.timeout, params=params))
|
|
156
|
+
|
|
157
|
+
def delete(self,
|
|
158
|
+
url: str,
|
|
159
|
+
params: Optional[dict] = None):
|
|
160
|
+
"""
|
|
161
|
+
Attributes:
|
|
162
|
+
url (str): Url of the HTTP request
|
|
163
|
+
params (Optional[dict]): Query parameters to add to the url. \
|
|
164
|
+
Defaults to None.
|
|
165
|
+
|
|
166
|
+
Returns:
|
|
167
|
+
response_data (Any): Response body
|
|
168
|
+
"""
|
|
169
|
+
return handle_response(requests.delete(url=url, headers=self._get_header(),
|
|
170
|
+
timeout=self.timeout, params=params))
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def handle_response(response: requests.Response):
|
|
174
|
+
"""
|
|
175
|
+
Extracts the data from the http response object
|
|
176
|
+
|
|
177
|
+
Attributes:
|
|
178
|
+
response (requests.Response): HTTP response object to handle
|
|
179
|
+
|
|
180
|
+
Raises:
|
|
181
|
+
HTTPError: If the HTTP request returned an unsuccessful status code.
|
|
182
|
+
Exception: For any other error parsing the response.
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
data (Any): Extracted data from HTTP response object
|
|
186
|
+
"""
|
|
187
|
+
try:
|
|
188
|
+
response.raise_for_status()
|
|
189
|
+
response_body = response.json()
|
|
190
|
+
return response_body
|
|
191
|
+
except requests.HTTPError as http_exception:
|
|
192
|
+
log.error(f'Error {response.status_code} : {response.text}')
|
|
193
|
+
raise http_exception
|
|
194
|
+
except Exception as e:
|
|
195
|
+
log.error('Failed to parse response body')
|
|
196
|
+
raise e
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
REST_API_CLIENT: Optional[RestApiClient] = None
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def get_rest_api_client() -> RestApiClient:
|
|
203
|
+
"""
|
|
204
|
+
Initiate or return existing RestApiClient instance
|
|
205
|
+
"""
|
|
206
|
+
global REST_API_CLIENT
|
|
207
|
+
|
|
208
|
+
if REST_API_CLIENT is None:
|
|
209
|
+
REST_API_CLIENT = RestApiClient()
|
|
210
|
+
|
|
211
|
+
return REST_API_CLIENT
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing restapi authentication configuration.
|
|
3
|
+
"""
|
|
4
|
+
from pydantic_settings import BaseSettings
|
|
5
|
+
from pydantic_settings import SettingsConfigDict
|
|
6
|
+
|
|
7
|
+
from ecodev_core.settings import SETTINGS
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class RestApiConfiguration(BaseSettings):
|
|
11
|
+
"""
|
|
12
|
+
Simple authentication configuration class
|
|
13
|
+
"""
|
|
14
|
+
host: str = ''
|
|
15
|
+
user: str = ''
|
|
16
|
+
password: str = ''
|
|
17
|
+
model_config = SettingsConfigDict(env_file='.env')
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
API_AUTH = RestApiConfiguration()
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
LOGIN_URL = SETTINGS.api.host or API_AUTH.host + '/login'
|
|
24
|
+
API_USER = SETTINGS.api.user or API_AUTH.user
|
|
25
|
+
API_PASSWORD = SETTINGS.api.password or API_AUTH.password
|
|
@@ -0,0 +1,241 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Unittest wrapper ensuring safe setUp / tearDown of all tests.
|
|
3
|
+
This boilerplate code is not to be touched under any circumstances.
|
|
4
|
+
"""
|
|
5
|
+
import contextlib
|
|
6
|
+
import shutil
|
|
7
|
+
from datetime import datetime
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from typing import Any
|
|
10
|
+
from typing import Callable
|
|
11
|
+
from typing import List
|
|
12
|
+
from typing import Union
|
|
13
|
+
from unittest import TestCase
|
|
14
|
+
|
|
15
|
+
import numpy as np
|
|
16
|
+
import pandas as pd
|
|
17
|
+
from pydantic import Field
|
|
18
|
+
from sqlalchemy import Engine
|
|
19
|
+
from sqlmodel import create_engine
|
|
20
|
+
from sqlmodel import SQLModel
|
|
21
|
+
|
|
22
|
+
from ecodev_core.db_connection import exec_admin_queries
|
|
23
|
+
from ecodev_core.db_connection import TEST_DB
|
|
24
|
+
from ecodev_core.db_connection import TEST_DB_URL
|
|
25
|
+
from ecodev_core.logger import log_critical
|
|
26
|
+
from ecodev_core.logger import logger_get
|
|
27
|
+
from ecodev_core.pydantic_utils import Frozen
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
log = logger_get(__name__)
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class SafeTestCase(TestCase):
|
|
34
|
+
"""
|
|
35
|
+
SafeTestCase makes sure that setUp / tearDown methods are always run when they should be.
|
|
36
|
+
This boilerplate code is not to be touched under any circumstances.
|
|
37
|
+
"""
|
|
38
|
+
files_created: List[Path]
|
|
39
|
+
directories_created: List[Path]
|
|
40
|
+
test_engine: Engine
|
|
41
|
+
|
|
42
|
+
@classmethod
|
|
43
|
+
def setUpClass(cls) -> None:
|
|
44
|
+
"""
|
|
45
|
+
Class set up, prompt class name and set files and folders to be suppressed at tearDownClass
|
|
46
|
+
"""
|
|
47
|
+
log.info(f'Running test module: {cls.__module__.upper()}')
|
|
48
|
+
super().setUpClass()
|
|
49
|
+
cls.directories_created = []
|
|
50
|
+
cls.files_created = []
|
|
51
|
+
|
|
52
|
+
@classmethod
|
|
53
|
+
def tearDownClass(cls) -> None:
|
|
54
|
+
"""
|
|
55
|
+
Safely suppress all files and directories used for this class
|
|
56
|
+
"""
|
|
57
|
+
log.info(f'Done running test module: {cls.__module__.upper()}')
|
|
58
|
+
cls.safe_delete(cls.directories_created, cls.files_created)
|
|
59
|
+
|
|
60
|
+
def setUp(self) -> None:
|
|
61
|
+
"""
|
|
62
|
+
Test set up, prompt test name and set files and folders to be suppressed at tearDown
|
|
63
|
+
"""
|
|
64
|
+
super().setUp()
|
|
65
|
+
log.debug(f'Running test: {self._testMethodName.upper()}')
|
|
66
|
+
self.directories_created: List[Path] = []
|
|
67
|
+
self.files_created: List[Path] = []
|
|
68
|
+
exec_admin_queries([f'CREATE DATABASE {TEST_DB}'])
|
|
69
|
+
self.test_engine = create_engine(TEST_DB_URL, pool_pre_ping=True)
|
|
70
|
+
SQLModel.metadata.create_all(self.test_engine)
|
|
71
|
+
|
|
72
|
+
def tearDown(self) -> None:
|
|
73
|
+
"""
|
|
74
|
+
Safely suppress all files and directories used for this test
|
|
75
|
+
"""
|
|
76
|
+
log.debug(f'Done running test: {self._testMethodName.upper()}')
|
|
77
|
+
self.safe_delete(self.directories_created, self.files_created)
|
|
78
|
+
exec_admin_queries([
|
|
79
|
+
f"SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = '{TEST_DB}'",
|
|
80
|
+
f'DROP DATABASE IF EXISTS {TEST_DB}'
|
|
81
|
+
])
|
|
82
|
+
|
|
83
|
+
@classmethod
|
|
84
|
+
def safe_delete(cls, directories_created: List[Path], files_created: List[Path]) -> None:
|
|
85
|
+
"""
|
|
86
|
+
Safely suppress all passed files_created and directories_created
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
directories_created: directories used for the test making the call
|
|
90
|
+
files_created: files_created used for the test making the call
|
|
91
|
+
"""
|
|
92
|
+
for directory in directories_created:
|
|
93
|
+
with contextlib.suppress(FileNotFoundError):
|
|
94
|
+
shutil.rmtree(directory)
|
|
95
|
+
for file_path in files_created:
|
|
96
|
+
file_path.unlink(missing_ok=True)
|
|
97
|
+
|
|
98
|
+
def run(self, result=None):
|
|
99
|
+
"""
|
|
100
|
+
Wrapper around unittest run
|
|
101
|
+
"""
|
|
102
|
+
test_method = getattr(self, self._testMethodName)
|
|
103
|
+
wrapped_test = self._cleanup_wrapper(test_method, KeyboardInterrupt)
|
|
104
|
+
setattr(self, self._testMethodName, wrapped_test)
|
|
105
|
+
self.setUp = self._cleanup_wrapper(self.setUp, BaseException)
|
|
106
|
+
|
|
107
|
+
return super().run(result)
|
|
108
|
+
|
|
109
|
+
def _cleanup_wrapper(self, method, exception):
|
|
110
|
+
"""
|
|
111
|
+
Boilerplate code for clean setup and teardown
|
|
112
|
+
"""
|
|
113
|
+
|
|
114
|
+
def _wrapped(*args, **kwargs):
|
|
115
|
+
try:
|
|
116
|
+
return method(*args, **kwargs)
|
|
117
|
+
except exception:
|
|
118
|
+
self.tearDown()
|
|
119
|
+
self.doCleanups()
|
|
120
|
+
raise
|
|
121
|
+
|
|
122
|
+
return _wrapped
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
class SimpleReturn(Frozen):
|
|
126
|
+
"""
|
|
127
|
+
Simple output for routes not returning anything
|
|
128
|
+
"""
|
|
129
|
+
success: bool = Field(..., description=' True if the treatment went well.')
|
|
130
|
+
error: Union[str, None] = Field(..., description='the error that happened, if any.')
|
|
131
|
+
|
|
132
|
+
@classmethod
|
|
133
|
+
def route_success(cls) -> 'SimpleReturn':
|
|
134
|
+
"""
|
|
135
|
+
Format DropDocumentReturn if the document was successfully dropped
|
|
136
|
+
"""
|
|
137
|
+
return SimpleReturn(success=True, error=None)
|
|
138
|
+
|
|
139
|
+
@classmethod
|
|
140
|
+
def route_failure(cls, error: str) -> 'SimpleReturn':
|
|
141
|
+
"""
|
|
142
|
+
Format DropDocumentReturn if the document failed to be dropped
|
|
143
|
+
"""
|
|
144
|
+
return SimpleReturn(success=False, error=error)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
def safe_clt(func):
|
|
148
|
+
"""
|
|
149
|
+
Safe execution of typer commands
|
|
150
|
+
"""
|
|
151
|
+
def inner_function(*args, **kwargs):
|
|
152
|
+
try:
|
|
153
|
+
func(*args, **kwargs)
|
|
154
|
+
return SimpleReturn.route_success()
|
|
155
|
+
except Exception as error:
|
|
156
|
+
log_critical(f'something wrong happened: {error}', log)
|
|
157
|
+
return SimpleReturn.route_failure(str(error))
|
|
158
|
+
return inner_function
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def safe_method(func: Callable) -> Any | None:
|
|
162
|
+
"""
|
|
163
|
+
Safe execution of a method
|
|
164
|
+
"""
|
|
165
|
+
def inner_function(*args, **kwargs):
|
|
166
|
+
try:
|
|
167
|
+
return func(*args, **kwargs)
|
|
168
|
+
except Exception:
|
|
169
|
+
return None
|
|
170
|
+
return inner_function
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def stringify(x: Union[str, float], default: str | None = None) -> Union[str, None]:
|
|
174
|
+
"""
|
|
175
|
+
Safe conversion of a (str, np.nan) value into a (str,None) one
|
|
176
|
+
"""
|
|
177
|
+
return _transformify(x, str, default)
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def boolify(x: Union[Any], default: bool | None = None) -> Union[bool, None]:
|
|
181
|
+
"""
|
|
182
|
+
Safe conversion of a (str, np.nan) value into a (str,None) one
|
|
183
|
+
"""
|
|
184
|
+
return _transformify(x, _bool_check, default)
|
|
185
|
+
|
|
186
|
+
|
|
187
|
+
def intify(x: Union[str, float], default: int | None = None) -> Union[int, None]:
|
|
188
|
+
"""
|
|
189
|
+
Safe conversion of a (int, np.nan) value into a (int,None) one
|
|
190
|
+
"""
|
|
191
|
+
return _transformify(x, int, default)
|
|
192
|
+
|
|
193
|
+
|
|
194
|
+
def floatify(x: Union[str, float], default: float | None = None) -> Union[float, None]:
|
|
195
|
+
"""
|
|
196
|
+
Safe conversion of a (float, np.nan) value into a (float,None) one
|
|
197
|
+
"""
|
|
198
|
+
return _transformify(x, float, default)
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
def datify(date: datetime | str,
|
|
202
|
+
date_format: str,
|
|
203
|
+
default: datetime | None = None
|
|
204
|
+
) -> Union[datetime, None]:
|
|
205
|
+
"""
|
|
206
|
+
Safe conversion to a date format
|
|
207
|
+
"""
|
|
208
|
+
if pd.isnull(date):
|
|
209
|
+
return None
|
|
210
|
+
if isinstance(date, datetime):
|
|
211
|
+
return date
|
|
212
|
+
return _transformify(date, lambda x: datetime.strptime(x, date_format), default)
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
def _transformify(x: Union[Any, float],
|
|
216
|
+
transformation: Callable,
|
|
217
|
+
default: Any | None) -> Union[Any, None]:
|
|
218
|
+
"""
|
|
219
|
+
Safe conversion of a (Any, np.nan) value into a (Any,None) one thanks to transformation
|
|
220
|
+
"""
|
|
221
|
+
if x is None or (isinstance(x, float) and np.isnan(x)):
|
|
222
|
+
return default
|
|
223
|
+
|
|
224
|
+
try:
|
|
225
|
+
return transformed if (transformed := transformation(x)) is not None else default
|
|
226
|
+
|
|
227
|
+
except ValueError:
|
|
228
|
+
return default
|
|
229
|
+
|
|
230
|
+
|
|
231
|
+
def _bool_check(x: Union[Any, float]):
|
|
232
|
+
"""
|
|
233
|
+
Check if the passed element can be cast into a boolean, or the boolean value inferred.
|
|
234
|
+
"""
|
|
235
|
+
if isinstance(x, bool):
|
|
236
|
+
return x
|
|
237
|
+
|
|
238
|
+
if not isinstance(x, str) or x.lower() not in ['true', 'yes', 'false', 'no']:
|
|
239
|
+
return None
|
|
240
|
+
|
|
241
|
+
return x.lower() in ['true', 'yes']
|
ecodev_core/settings.py
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module defining a dynamic setting class
|
|
3
|
+
"""
|
|
4
|
+
from contextlib import suppress
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
|
|
7
|
+
from pydantic.v1.utils import deep_update
|
|
8
|
+
from pydantic_settings import BaseSettings
|
|
9
|
+
from pydantic_settings import SettingsConfigDict
|
|
10
|
+
|
|
11
|
+
from ecodev_core.deployment import Deployment
|
|
12
|
+
from ecodev_core.list_utils import dict_to_class
|
|
13
|
+
from ecodev_core.read_write import load_yaml_file
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class DeploymentSetting(BaseSettings):
|
|
17
|
+
"""
|
|
18
|
+
Settings class used to load the deployment type from environment variables.
|
|
19
|
+
"""
|
|
20
|
+
environment: str = 'local'
|
|
21
|
+
base_path: str = '/app'
|
|
22
|
+
model_config = SettingsConfigDict(env_file='.env')
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
DEPLOYMENT_SETTINGS = DeploymentSetting()
|
|
26
|
+
DEPLOYMENT = Deployment(DEPLOYMENT_SETTINGS.environment.lower())
|
|
27
|
+
BASE_PATH = Path(DEPLOYMENT_SETTINGS.base_path)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class Settings:
|
|
31
|
+
"""
|
|
32
|
+
Dynami setting class, loading yaml configuration from config file, possibly overwriting some of
|
|
33
|
+
this configuration with additional information coming from a secret file.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
def __init__(self, base_path: Path = BASE_PATH, deployment: Deployment = DEPLOYMENT):
|
|
37
|
+
"""
|
|
38
|
+
Dynamically setting Settings attributes, doing so recursively. Attributes are loaded
|
|
39
|
+
from config file, possibly overwriting some of this configuration with additional
|
|
40
|
+
information coming from a secret file.
|
|
41
|
+
"""
|
|
42
|
+
self.deployment = deployment
|
|
43
|
+
with suppress(FileNotFoundError):
|
|
44
|
+
data = load_yaml_file(base_path / 'config' / f'{deployment.value}.yaml')
|
|
45
|
+
if (secrets_file := base_path / 'secrets' / f'{deployment.value}.yaml').exists():
|
|
46
|
+
data = deep_update(data, load_yaml_file(secrets_file))
|
|
47
|
+
for k, v in dict_to_class(data).items():
|
|
48
|
+
setattr(self, k, v)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
SETTINGS = Settings()
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
module implementing SQLModel related helper methods. Related to validation
|
|
3
|
+
"""
|
|
4
|
+
from sqlmodel import SQLModel
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class SQLModelWithVal(SQLModel):
|
|
8
|
+
"""
|
|
9
|
+
Helper class to ease validation in SQLModel classes with table=True
|
|
10
|
+
"""
|
|
11
|
+
@classmethod
|
|
12
|
+
def create(cls, **kwargs):
|
|
13
|
+
"""
|
|
14
|
+
Forces validation to take place, even for SQLModel classes with table=True
|
|
15
|
+
"""
|
|
16
|
+
return cls(**cls.__bases__[0](**kwargs).model_dump())
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Module implementing the token ban list table
|
|
3
|
+
"""
|
|
4
|
+
from datetime import datetime
|
|
5
|
+
from typing import Optional
|
|
6
|
+
|
|
7
|
+
from sqlmodel import Field
|
|
8
|
+
from sqlmodel import SQLModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class TokenBanlist(SQLModel, table=True): # type: ignore
|
|
12
|
+
"""
|
|
13
|
+
A token banlist: timestamped banned token.
|
|
14
|
+
"""
|
|
15
|
+
__tablename__ = 'token_banlist'
|
|
16
|
+
id: Optional[int] = Field(default=None, primary_key=True)
|
|
17
|
+
created_at: datetime = Field(default_factory=datetime.utcnow)
|
|
18
|
+
token: str = Field(index=True)
|