desdeo 1.1.3__py3-none-any.whl → 2.0.0__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.
- desdeo/__init__.py +8 -8
- desdeo/api/README.md +73 -0
- desdeo/api/__init__.py +15 -0
- desdeo/api/app.py +40 -0
- desdeo/api/config.py +69 -0
- desdeo/api/config.toml +53 -0
- desdeo/api/db.py +25 -0
- desdeo/api/db_init.py +79 -0
- desdeo/api/db_models.py +164 -0
- desdeo/api/malaga_db_init.py +27 -0
- desdeo/api/models/__init__.py +66 -0
- desdeo/api/models/archive.py +34 -0
- desdeo/api/models/preference.py +90 -0
- desdeo/api/models/problem.py +507 -0
- desdeo/api/models/reference_point_method.py +18 -0
- desdeo/api/models/session.py +46 -0
- desdeo/api/models/state.py +96 -0
- desdeo/api/models/user.py +51 -0
- desdeo/api/routers/_NAUTILUS.py +245 -0
- desdeo/api/routers/_NAUTILUS_navigator.py +233 -0
- desdeo/api/routers/_NIMBUS.py +762 -0
- desdeo/api/routers/__init__.py +5 -0
- desdeo/api/routers/problem.py +110 -0
- desdeo/api/routers/reference_point_method.py +117 -0
- desdeo/api/routers/session.py +76 -0
- desdeo/api/routers/test.py +16 -0
- desdeo/api/routers/user_authentication.py +366 -0
- desdeo/api/schema.py +94 -0
- desdeo/api/tests/__init__.py +0 -0
- desdeo/api/tests/conftest.py +59 -0
- desdeo/api/tests/test_models.py +701 -0
- desdeo/api/tests/test_routes.py +216 -0
- desdeo/api/utils/database.py +274 -0
- desdeo/api/utils/logger.py +29 -0
- desdeo/core.py +27 -0
- desdeo/emo/__init__.py +29 -0
- desdeo/emo/hooks/archivers.py +172 -0
- desdeo/emo/methods/EAs.py +418 -0
- desdeo/emo/methods/__init__.py +0 -0
- desdeo/emo/methods/bases.py +59 -0
- desdeo/emo/operators/__init__.py +1 -0
- desdeo/emo/operators/crossover.py +780 -0
- desdeo/emo/operators/evaluator.py +118 -0
- desdeo/emo/operators/generator.py +356 -0
- desdeo/emo/operators/mutation.py +1053 -0
- desdeo/emo/operators/selection.py +1036 -0
- desdeo/emo/operators/termination.py +178 -0
- desdeo/explanations/__init__.py +6 -0
- desdeo/explanations/explainer.py +100 -0
- desdeo/explanations/utils.py +90 -0
- desdeo/mcdm/__init__.py +19 -0
- desdeo/mcdm/nautili.py +345 -0
- desdeo/mcdm/nautilus.py +477 -0
- desdeo/mcdm/nautilus_navigator.py +655 -0
- desdeo/mcdm/nimbus.py +417 -0
- desdeo/mcdm/pareto_navigator.py +269 -0
- desdeo/mcdm/reference_point_method.py +116 -0
- desdeo/problem/__init__.py +79 -0
- desdeo/problem/evaluator.py +561 -0
- desdeo/problem/gurobipy_evaluator.py +562 -0
- desdeo/problem/infix_parser.py +341 -0
- desdeo/problem/json_parser.py +944 -0
- desdeo/problem/pyomo_evaluator.py +468 -0
- desdeo/problem/schema.py +1808 -0
- desdeo/problem/simulator_evaluator.py +298 -0
- desdeo/problem/sympy_evaluator.py +244 -0
- desdeo/problem/testproblems/__init__.py +73 -0
- desdeo/problem/testproblems/binh_and_korn_problem.py +88 -0
- desdeo/problem/testproblems/dtlz2_problem.py +102 -0
- desdeo/problem/testproblems/forest_problem.py +275 -0
- desdeo/problem/testproblems/knapsack_problem.py +163 -0
- desdeo/problem/testproblems/mcwb_problem.py +831 -0
- desdeo/problem/testproblems/mixed_variable_dimenrions_problem.py +83 -0
- desdeo/problem/testproblems/momip_problem.py +172 -0
- desdeo/problem/testproblems/nimbus_problem.py +143 -0
- desdeo/problem/testproblems/pareto_navigator_problem.py +89 -0
- desdeo/problem/testproblems/re_problem.py +492 -0
- desdeo/problem/testproblems/river_pollution_problem.py +434 -0
- desdeo/problem/testproblems/rocket_injector_design_problem.py +140 -0
- desdeo/problem/testproblems/simple_problem.py +351 -0
- desdeo/problem/testproblems/simulator_problem.py +92 -0
- desdeo/problem/testproblems/spanish_sustainability_problem.py +945 -0
- desdeo/problem/testproblems/zdt_problem.py +271 -0
- desdeo/problem/utils.py +245 -0
- desdeo/tools/GenerateReferencePoints.py +181 -0
- desdeo/tools/__init__.py +102 -0
- desdeo/tools/generics.py +145 -0
- desdeo/tools/gurobipy_solver_interfaces.py +258 -0
- desdeo/tools/indicators_binary.py +11 -0
- desdeo/tools/indicators_unary.py +375 -0
- desdeo/tools/interaction_schema.py +38 -0
- desdeo/tools/intersection.py +54 -0
- desdeo/tools/iterative_pareto_representer.py +99 -0
- desdeo/tools/message.py +234 -0
- desdeo/tools/ng_solver_interfaces.py +199 -0
- desdeo/tools/non_dominated_sorting.py +133 -0
- desdeo/tools/patterns.py +281 -0
- desdeo/tools/proximal_solver.py +99 -0
- desdeo/tools/pyomo_solver_interfaces.py +464 -0
- desdeo/tools/reference_vectors.py +462 -0
- desdeo/tools/scalarization.py +3138 -0
- desdeo/tools/scipy_solver_interfaces.py +454 -0
- desdeo/tools/score_bands.py +464 -0
- desdeo/tools/utils.py +320 -0
- desdeo/utopia_stuff/__init__.py +0 -0
- desdeo/utopia_stuff/data/1.json +15 -0
- desdeo/utopia_stuff/data/2.json +13 -0
- desdeo/utopia_stuff/data/3.json +15 -0
- desdeo/utopia_stuff/data/4.json +17 -0
- desdeo/utopia_stuff/data/5.json +15 -0
- desdeo/utopia_stuff/from_json.py +40 -0
- desdeo/utopia_stuff/reinit_user.py +38 -0
- desdeo/utopia_stuff/utopia_db_init.py +212 -0
- desdeo/utopia_stuff/utopia_problem.py +403 -0
- desdeo/utopia_stuff/utopia_problem_old.py +415 -0
- desdeo/utopia_stuff/utopia_reference_solutions.py +79 -0
- desdeo-2.0.0.dist-info/LICENSE +21 -0
- desdeo-2.0.0.dist-info/METADATA +168 -0
- desdeo-2.0.0.dist-info/RECORD +120 -0
- {desdeo-1.1.3.dist-info → desdeo-2.0.0.dist-info}/WHEEL +1 -1
- desdeo-1.1.3.dist-info/METADATA +0 -18
- desdeo-1.1.3.dist-info/RECORD +0 -4
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"""Tests related to routes and routers."""
|
|
2
|
+
|
|
3
|
+
from fastapi import status
|
|
4
|
+
from fastapi.testclient import TestClient
|
|
5
|
+
|
|
6
|
+
from desdeo.api.models import (
|
|
7
|
+
CreateSessionRequest,
|
|
8
|
+
GetSessionRequest,
|
|
9
|
+
InteractiveSessionDB,
|
|
10
|
+
ProblemGetRequest,
|
|
11
|
+
ProblemInfo,
|
|
12
|
+
ReferencePoint,
|
|
13
|
+
RPMSolveRequest,
|
|
14
|
+
User,
|
|
15
|
+
)
|
|
16
|
+
from desdeo.api.routers.user_authentication import create_access_token
|
|
17
|
+
from desdeo.problem.testproblems import simple_knapsack_vectors
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def login(client: TestClient, username="analyst", password="analyst") -> str: # noqa: S107
|
|
21
|
+
"""Login, returns the access token."""
|
|
22
|
+
response_login = client.post(
|
|
23
|
+
"/login",
|
|
24
|
+
data={"username": username, "password": password, "grant_type": "password"},
|
|
25
|
+
headers={"content-type": "application/x-www-form-urlencoded"},
|
|
26
|
+
).json()
|
|
27
|
+
|
|
28
|
+
return response_login["access_token"]
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
def post_json(client: TestClient, endpoint: str, json: dict, access_token: str):
|
|
32
|
+
"""Makes a post request and returns the response."""
|
|
33
|
+
return client.post(
|
|
34
|
+
endpoint,
|
|
35
|
+
json=json,
|
|
36
|
+
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def get_json(client: TestClient, endpoint: str, access_token: str):
|
|
41
|
+
"""Makes a get request and returns the response."""
|
|
42
|
+
return client.get(
|
|
43
|
+
endpoint,
|
|
44
|
+
headers={"Authorization": f"Bearer {access_token}", "Content-Type": "application/json"},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def test_user_login(client: TestClient):
|
|
49
|
+
"""Test that login works."""
|
|
50
|
+
response = client.post(
|
|
51
|
+
"/login",
|
|
52
|
+
data={"username": "analyst", "password": "analyst", "grant_type": "password"},
|
|
53
|
+
headers={"content-type": "application/x-www-form-urlencoded"},
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
assert response.status_code == 200
|
|
57
|
+
assert "access_token" in response.json()
|
|
58
|
+
|
|
59
|
+
# wrong login
|
|
60
|
+
response = client.post(
|
|
61
|
+
"/login",
|
|
62
|
+
data={"username": "analyst", "password": "anallyst", "grant_type": "password"},
|
|
63
|
+
headers={"content-type": "application/x-www-form-urlencoded"},
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
assert response.status_code == 401
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def test_tokens():
|
|
70
|
+
"""Test token generation."""
|
|
71
|
+
token_1 = create_access_token({"id": 1, "sub": "analyst"})
|
|
72
|
+
token_2 = create_access_token({"id": 1, "sub": "analyst"})
|
|
73
|
+
|
|
74
|
+
assert token_1 != token_2
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def test_refresh(client: TestClient):
|
|
78
|
+
"""Test that refreshing the access token works."""
|
|
79
|
+
# check that no previous cookies exist
|
|
80
|
+
assert len(client.cookies) == 0
|
|
81
|
+
|
|
82
|
+
# no cookie
|
|
83
|
+
response_bad = client.post("/refresh")
|
|
84
|
+
|
|
85
|
+
response_good = client.post(
|
|
86
|
+
"/login",
|
|
87
|
+
data={"username": "analyst", "password": "analyst", "grant_type": "password"},
|
|
88
|
+
headers={"content-type": "application/x-www-form-urlencoded"},
|
|
89
|
+
)
|
|
90
|
+
|
|
91
|
+
assert response_bad.status_code == 401
|
|
92
|
+
assert response_good.status_code == 200
|
|
93
|
+
|
|
94
|
+
assert "access_token" in response_good.json()
|
|
95
|
+
assert len(client.cookies) == 1
|
|
96
|
+
assert "refresh_token" in client.cookies
|
|
97
|
+
|
|
98
|
+
response_refresh = client.post("/refresh")
|
|
99
|
+
|
|
100
|
+
assert "access_token" in response_refresh.json()
|
|
101
|
+
|
|
102
|
+
assert response_good.json()["access_token"] != response_refresh.json()["access_token"]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
def test_get_problem(client: TestClient):
|
|
106
|
+
"""Test fetching specific problems based on their id."""
|
|
107
|
+
access_token = login(client)
|
|
108
|
+
|
|
109
|
+
response = post_json(client, "/problem/get", ProblemGetRequest(problem_id=1).model_dump(), access_token)
|
|
110
|
+
|
|
111
|
+
assert response.status_code == 200
|
|
112
|
+
|
|
113
|
+
info = ProblemInfo.model_validate(response.json())
|
|
114
|
+
|
|
115
|
+
assert info.id == 1
|
|
116
|
+
assert info.name == "dtlz2"
|
|
117
|
+
|
|
118
|
+
response = post_json(client, "problem/get", ProblemGetRequest(problem_id=2).model_dump(), access_token)
|
|
119
|
+
|
|
120
|
+
assert response.status_code == 200
|
|
121
|
+
|
|
122
|
+
info = ProblemInfo.model_validate(response.json())
|
|
123
|
+
|
|
124
|
+
assert info.id == 2
|
|
125
|
+
assert info.name == "The river pollution problem"
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
def test_add_problem(client: TestClient):
|
|
129
|
+
"""Test that adding a problem to the database works."""
|
|
130
|
+
access_token = login(client)
|
|
131
|
+
|
|
132
|
+
problem = simple_knapsack_vectors()
|
|
133
|
+
|
|
134
|
+
response = post_json(client, "/problem/add", problem.model_dump(), access_token)
|
|
135
|
+
|
|
136
|
+
assert response.status_code == status.HTTP_200_OK
|
|
137
|
+
|
|
138
|
+
problem_info: ProblemInfo = ProblemInfo.model_validate(response.json())
|
|
139
|
+
|
|
140
|
+
assert problem_info.name == "Simple two-objective Knapsack problem"
|
|
141
|
+
|
|
142
|
+
response = get_json(client, "problem/all_info", access_token)
|
|
143
|
+
|
|
144
|
+
assert response.status_code == status.HTTP_200_OK
|
|
145
|
+
|
|
146
|
+
problems = response.json()
|
|
147
|
+
|
|
148
|
+
assert len(problems) == 3
|
|
149
|
+
|
|
150
|
+
|
|
151
|
+
def test_new_session(client: TestClient, session_and_user: dict):
|
|
152
|
+
"""Test that creating a new session works as expected."""
|
|
153
|
+
user: User = session_and_user["user"]
|
|
154
|
+
session = session_and_user["session"]
|
|
155
|
+
|
|
156
|
+
assert user.active_session_id is None
|
|
157
|
+
|
|
158
|
+
access_token = login(client)
|
|
159
|
+
|
|
160
|
+
request = CreateSessionRequest(info="My session")
|
|
161
|
+
|
|
162
|
+
response = post_json(client, "/session/new", request.model_dump(), access_token)
|
|
163
|
+
|
|
164
|
+
assert response.status_code == status.HTTP_200_OK
|
|
165
|
+
|
|
166
|
+
assert user.active_session_id == 1
|
|
167
|
+
isession = session.get(InteractiveSessionDB, 1)
|
|
168
|
+
|
|
169
|
+
assert isession.info == "My session"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def test_get_session(client: TestClient, session_and_user: dict):
|
|
173
|
+
"""Test that getting a session works as intended."""
|
|
174
|
+
user: User = session_and_user["user"]
|
|
175
|
+
|
|
176
|
+
access_token = login(client)
|
|
177
|
+
|
|
178
|
+
# no sessions
|
|
179
|
+
request = GetSessionRequest(session_id=1)
|
|
180
|
+
response = post_json(client, "/session/get", request.model_dump(), access_token)
|
|
181
|
+
assert response.status_code == status.HTTP_404_NOT_FOUND
|
|
182
|
+
|
|
183
|
+
# add some sessions
|
|
184
|
+
request = CreateSessionRequest(info="My session")
|
|
185
|
+
response = post_json(client, "/session/new", request.model_dump(), access_token)
|
|
186
|
+
assert response.status_code == status.HTTP_200_OK
|
|
187
|
+
|
|
188
|
+
assert user.active_session_id == 1
|
|
189
|
+
|
|
190
|
+
request = CreateSessionRequest(info="My session")
|
|
191
|
+
response = post_json(client, "/session/new", request.model_dump(), access_token)
|
|
192
|
+
assert response.status_code == status.HTTP_200_OK
|
|
193
|
+
|
|
194
|
+
assert user.active_session_id == 2
|
|
195
|
+
|
|
196
|
+
# sessions with id 1 and 2 should exist
|
|
197
|
+
request = GetSessionRequest(session_id=1)
|
|
198
|
+
response = post_json(client, "/session/get", request.model_dump(), access_token)
|
|
199
|
+
assert response.status_code == status.HTTP_200_OK
|
|
200
|
+
|
|
201
|
+
request = GetSessionRequest(session_id=2)
|
|
202
|
+
response = post_json(client, "/session/get", request.model_dump(), access_token)
|
|
203
|
+
assert response.status_code == status.HTTP_200_OK
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
def test_rpm_solve(client: TestClient):
|
|
207
|
+
"""Test that using the reference point method works as expected."""
|
|
208
|
+
access_token = login(client)
|
|
209
|
+
|
|
210
|
+
request = RPMSolveRequest(
|
|
211
|
+
problem_id=1, preference=ReferencePoint(aspiration_levels={"f_1": 0.5, "f_2": 0.3, "f_3": 0.4})
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
response = post_json(client, "/method/rpm/solve", request.model_dump(), access_token)
|
|
215
|
+
|
|
216
|
+
assert response.status_code == status.HTTP_200_OK
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
"""Database utils for the API."""
|
|
2
|
+
|
|
3
|
+
from os import getenv
|
|
4
|
+
from typing import TypeVar
|
|
5
|
+
|
|
6
|
+
from sqlalchemy.engine import URL, Result
|
|
7
|
+
from sqlalchemy.ext.asyncio import (
|
|
8
|
+
AsyncEngine,
|
|
9
|
+
AsyncScalarResult,
|
|
10
|
+
AsyncSession,
|
|
11
|
+
create_async_engine,
|
|
12
|
+
)
|
|
13
|
+
from sqlalchemy.future import select as sa_select
|
|
14
|
+
from sqlalchemy.orm import DeclarativeMeta, declarative_base, sessionmaker
|
|
15
|
+
from sqlalchemy.pool import NullPool
|
|
16
|
+
from sqlalchemy.sql import Executable
|
|
17
|
+
from sqlalchemy.sql.expression import Delete
|
|
18
|
+
from sqlalchemy.sql.expression import delete as sa_delete
|
|
19
|
+
from sqlalchemy.sql.expression import exists as sa_exists
|
|
20
|
+
from sqlalchemy.sql.functions import count
|
|
21
|
+
from sqlalchemy.sql.selectable import Exists, Select
|
|
22
|
+
|
|
23
|
+
from desdeo.api.config import SettingsConfig
|
|
24
|
+
|
|
25
|
+
from .logger import get_logger
|
|
26
|
+
|
|
27
|
+
if SettingsConfig.debug:
|
|
28
|
+
from desdeo.api.config import DatabaseDebugConfig
|
|
29
|
+
|
|
30
|
+
DBConfig = DatabaseDebugConfig
|
|
31
|
+
else:
|
|
32
|
+
# set development setting, e.g., DBConfig = DatabaseDeployConfig
|
|
33
|
+
pass
|
|
34
|
+
|
|
35
|
+
T = TypeVar("T")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def select(entity) -> Select:
|
|
39
|
+
"""Shortcut for :meth:`sqlalchemy.future.select`.
|
|
40
|
+
|
|
41
|
+
Args:
|
|
42
|
+
entity (sqlalchemy.sql.expression.FromClause): Entities / DB model to SELECT from.
|
|
43
|
+
|
|
44
|
+
Returns:
|
|
45
|
+
Select: A Select class object
|
|
46
|
+
"""
|
|
47
|
+
return sa_select(entity)
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
def filter_by(cls, *args, **kwargs) -> Select:
|
|
51
|
+
"""Shortcut for :meth:`sqlalchemy.future.Select.filter_by`.
|
|
52
|
+
|
|
53
|
+
Args:
|
|
54
|
+
cls (sqlalchemy.sql.expression.FromClause): Entities / DB model to SELECT from.
|
|
55
|
+
*args (tuple): Positional arguments.
|
|
56
|
+
**kwargs (dict): Keyword arguments.
|
|
57
|
+
|
|
58
|
+
Returns:
|
|
59
|
+
Select: A Select class object with a WHERE clause
|
|
60
|
+
"""
|
|
61
|
+
return select(cls, *args).filter_by(**kwargs)
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def exists(*entities, **kwargs) -> Exists:
|
|
65
|
+
"""Shortcut for :meth:`sqlalchemy.sql.expression.exists`.
|
|
66
|
+
|
|
67
|
+
Args:
|
|
68
|
+
*entities (tuple): Positional arguments.
|
|
69
|
+
**kwargs (dict): Keyword arguments.
|
|
70
|
+
|
|
71
|
+
Returns:
|
|
72
|
+
Exists: A new Exists construct
|
|
73
|
+
"""
|
|
74
|
+
return sa_exists(*entities, **kwargs)
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
def delete(table) -> Delete:
|
|
78
|
+
"""Shortcut for :meth:`sqlalchemy.sql.expression.delete`.
|
|
79
|
+
|
|
80
|
+
Args:
|
|
81
|
+
table (sqlalchemy.sql.expression.FromClause): Entities / DB model.
|
|
82
|
+
|
|
83
|
+
Returns:
|
|
84
|
+
Delete: A Delete object
|
|
85
|
+
"""
|
|
86
|
+
return sa_delete(table)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
class DB:
|
|
90
|
+
"""An async SQLAlchemy ORM wrapper."""
|
|
91
|
+
|
|
92
|
+
Base: DeclarativeMeta
|
|
93
|
+
_engine: AsyncEngine
|
|
94
|
+
_session: AsyncSession
|
|
95
|
+
|
|
96
|
+
def __init__(self, driver: str, options: dict | None = None, **kwargs):
|
|
97
|
+
"""Init the class.
|
|
98
|
+
|
|
99
|
+
Args:
|
|
100
|
+
driver (str): Drivername for db url.
|
|
101
|
+
options (dict, optional): Options for AsyncEngine instance.
|
|
102
|
+
**kwargs (dict): Keyword arguments.
|
|
103
|
+
"""
|
|
104
|
+
if options is None:
|
|
105
|
+
options = {"pool_size": 20, "max_overflow": 20}
|
|
106
|
+
|
|
107
|
+
url: str = URL.create(drivername=driver, **kwargs)
|
|
108
|
+
self._engine = create_async_engine(url, echo=True, pool_pre_ping=True, pool_recycle=300, **options)
|
|
109
|
+
self.Base = declarative_base()
|
|
110
|
+
self._session: AsyncSession = sessionmaker(self._engine, expire_on_commit=False, class_=AsyncSession)()
|
|
111
|
+
|
|
112
|
+
async def create_tables(self):
|
|
113
|
+
"""Creates all Model Tables."""
|
|
114
|
+
async with self._engine.begin() as conn:
|
|
115
|
+
await conn.run_sync(self.Base.metadata.create_all)
|
|
116
|
+
|
|
117
|
+
async def add(self, obj: T) -> T:
|
|
118
|
+
"""Adds an Row to the Database.
|
|
119
|
+
|
|
120
|
+
Args:
|
|
121
|
+
obj (T): Object to add.
|
|
122
|
+
|
|
123
|
+
Returns:
|
|
124
|
+
T: Added object.
|
|
125
|
+
"""
|
|
126
|
+
self._session.add(obj)
|
|
127
|
+
return obj
|
|
128
|
+
|
|
129
|
+
async def delete(self, obj: T) -> T:
|
|
130
|
+
"""Deletes a Row from the Database.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
obj (T): Object to delete.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
T: Deleted object.
|
|
137
|
+
"""
|
|
138
|
+
await self._session.delete(obj)
|
|
139
|
+
return obj
|
|
140
|
+
|
|
141
|
+
async def exec(self, statement: Executable, *args, **kwargs) -> Result:
|
|
142
|
+
"""Executes a SQL Statement.
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
statement (Executable): SQL statement.
|
|
146
|
+
*args (tuple): Positional arguments.
|
|
147
|
+
**kwargs (dict): Keyword arguments.
|
|
148
|
+
|
|
149
|
+
Returns:
|
|
150
|
+
Result: A buffered Result object.
|
|
151
|
+
"""
|
|
152
|
+
return await self._session.execute(statement, *args, **kwargs)
|
|
153
|
+
|
|
154
|
+
async def stream(self, statement: Executable, *args, **kwargs) -> AsyncScalarResult:
|
|
155
|
+
"""Returns an Stream of Query Results.
|
|
156
|
+
|
|
157
|
+
Args:
|
|
158
|
+
statement (Executable): SQL statement.
|
|
159
|
+
*args (tuple): Positional arguments.
|
|
160
|
+
**kwargs (dict): Keyword arguments.
|
|
161
|
+
|
|
162
|
+
Returns:
|
|
163
|
+
AsyncScalarResult: An AsyncScalarResult filtering object.
|
|
164
|
+
"""
|
|
165
|
+
return (await self._session.stream(statement, *args, **kwargs)).scalars()
|
|
166
|
+
|
|
167
|
+
async def all(self, statement: Executable, *args, **kwargs) -> list[T]:
|
|
168
|
+
"""Returns all matches for a Query.
|
|
169
|
+
|
|
170
|
+
Args:
|
|
171
|
+
statement (Executable): SQL statement.
|
|
172
|
+
*args (tuple): Positional arguments.
|
|
173
|
+
**kwargs (dict): Keyword arguments.
|
|
174
|
+
|
|
175
|
+
Returns:
|
|
176
|
+
list[T]: List of rows.
|
|
177
|
+
"""
|
|
178
|
+
return [x async for x in await self.stream(statement, *args, **kwargs)]
|
|
179
|
+
|
|
180
|
+
async def first(self, *args, **kwargs) -> dict | None:
|
|
181
|
+
"""Returns first match for a Query.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
*args (tuple): Positional arguments.
|
|
185
|
+
**kwargs (dict): Keyword arguments.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
dict | None: First match for the Query, or None if there is no match.
|
|
189
|
+
"""
|
|
190
|
+
return (await self.exec(*args, **kwargs)).scalar()
|
|
191
|
+
|
|
192
|
+
async def exists(self, *args, **kwargs) -> bool:
|
|
193
|
+
"""Checks if there is a match for this Query.
|
|
194
|
+
|
|
195
|
+
Args:
|
|
196
|
+
*args (tuple): Positional arguments.
|
|
197
|
+
**kwargs (dict): Keyword arguments.
|
|
198
|
+
|
|
199
|
+
Returns:
|
|
200
|
+
bool: Whether there is a match for this Query
|
|
201
|
+
"""
|
|
202
|
+
return await self.first(exists(*args, **kwargs).select())
|
|
203
|
+
|
|
204
|
+
async def count(self, *args, **kwargs) -> int:
|
|
205
|
+
"""Counts matches for a Query.
|
|
206
|
+
|
|
207
|
+
Args:
|
|
208
|
+
*args (tuple): Positional arguments.
|
|
209
|
+
**kwargs (dict): Keyword arguments.
|
|
210
|
+
|
|
211
|
+
Returns:
|
|
212
|
+
int: Number of matches for a Query
|
|
213
|
+
"""
|
|
214
|
+
return await self.first(select(count()).select_from(*args, **kwargs))
|
|
215
|
+
|
|
216
|
+
async def commit(self):
|
|
217
|
+
"""Commits/Saves changes to Database."""
|
|
218
|
+
await self._session.commit()
|
|
219
|
+
|
|
220
|
+
|
|
221
|
+
logger = get_logger(__name__)
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class DatabaseDependency:
|
|
225
|
+
"""A class to represent a database dependency."""
|
|
226
|
+
|
|
227
|
+
db: DB
|
|
228
|
+
database_url_options: dict
|
|
229
|
+
engine_options: dict
|
|
230
|
+
initialized: bool = False
|
|
231
|
+
|
|
232
|
+
def __init__(self):
|
|
233
|
+
"""Initialize the class."""
|
|
234
|
+
self.database_url_options = {
|
|
235
|
+
"host": DBConfig.db_host,
|
|
236
|
+
"port": DBConfig.db_port,
|
|
237
|
+
"database": DBConfig.db_database,
|
|
238
|
+
"username": DBConfig.db_username,
|
|
239
|
+
"password": DBConfig.db_password,
|
|
240
|
+
}
|
|
241
|
+
self.database_url_options = {k: v for k, v in self.database_url_options.items() if v != ""}
|
|
242
|
+
self.engine_options: dict = {
|
|
243
|
+
# "pool_size": DB_POOL_SIZE, ## not used by sqlite
|
|
244
|
+
# "max_overflow": DB_MAX_OVERFLOW, ## not used by sqlite
|
|
245
|
+
"poolclass": DBConfig.db_pool,
|
|
246
|
+
}
|
|
247
|
+
self.engine_options = {k: int(v) for k, v in self.engine_options.items() if v != ""}
|
|
248
|
+
if self.engine_options["poolclass"] == 0:
|
|
249
|
+
self.engine_options["poolclass"] = NullPool
|
|
250
|
+
else:
|
|
251
|
+
del self.engine_options["poolclass"]
|
|
252
|
+
self.db = DB(
|
|
253
|
+
getenv("DB_DRIVER", "postgresql+asyncpg"), options=self.engine_options, **self.database_url_options
|
|
254
|
+
)
|
|
255
|
+
logger.info("Connected to Database")
|
|
256
|
+
|
|
257
|
+
async def init(self) -> None:
|
|
258
|
+
"""Set whether the dependency is initialized or not."""
|
|
259
|
+
if self.initialized:
|
|
260
|
+
return
|
|
261
|
+
|
|
262
|
+
# logger.info("Create Tables")
|
|
263
|
+
self.initialized = True
|
|
264
|
+
# await self.db.create_tables()
|
|
265
|
+
|
|
266
|
+
async def __call__(self) -> DB:
|
|
267
|
+
"""Define __call__."""
|
|
268
|
+
if not self.initialized:
|
|
269
|
+
await self.init()
|
|
270
|
+
return self.db
|
|
271
|
+
|
|
272
|
+
|
|
273
|
+
database_dependency: DatabaseDependency = DatabaseDependency()
|
|
274
|
+
Base: DeclarativeMeta = database_dependency.db.Base
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import logging
|
|
2
|
+
import sys
|
|
3
|
+
from dotenv import load_dotenv
|
|
4
|
+
from os import getenv
|
|
5
|
+
|
|
6
|
+
load_dotenv()
|
|
7
|
+
LOG_LEVEL = getenv("LOG_LEVEL", "INFO")
|
|
8
|
+
|
|
9
|
+
logging_formatter = logging.Formatter("[%(asctime)s] [%(levelname)s] %(message)s")
|
|
10
|
+
|
|
11
|
+
logging_handler = logging.StreamHandler(sys.stdout)
|
|
12
|
+
logging_handler.setFormatter(logging_formatter)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_logger(name: str) -> logging.Logger:
|
|
16
|
+
"""Get a logger with a given name.
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
name (str): Logger name.
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
logging.Logger: logging.Logger object.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
logger: logging.Logger = logging.getLogger(name)
|
|
26
|
+
logger.addHandler(logging_handler)
|
|
27
|
+
logger.setLevel(LOG_LEVEL.upper())
|
|
28
|
+
|
|
29
|
+
return logger
|
desdeo/core.py
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"""Functionalities related to managing and building the project."""
|
|
2
|
+
|
|
3
|
+
import shutil
|
|
4
|
+
import warnings
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
def _check_executables(executable_list):
|
|
8
|
+
missing_executables = []
|
|
9
|
+
for executable in executable_list:
|
|
10
|
+
if shutil.which(executable) is None:
|
|
11
|
+
missing_executables.append(executable)
|
|
12
|
+
|
|
13
|
+
if missing_executables:
|
|
14
|
+
warnings.warn(
|
|
15
|
+
f"""
|
|
16
|
+
\nThe following required highly recommended executables cannot be found: {", ".join(missing_executables)}\n
|
|
17
|
+
DESDEO relies on powerful 3rd party solvers to solve multiobjective
|
|
18
|
+
optimization problems. Without these solvers, sub-optimal defaults
|
|
19
|
+
will be used instead, which can lead to not optimal, or even wrong,
|
|
20
|
+
results.\n
|
|
21
|
+
It is highly recommended to have these solvers available
|
|
22
|
+
in the environment DESDEO is utilized!\n
|
|
23
|
+
For more information, see DESDEO's documentation: https://desdeo.readthedocs.io/en/latest/howtoguides/installing/#third-party-optimizers
|
|
24
|
+
""",
|
|
25
|
+
UserWarning,
|
|
26
|
+
stacklevel=2,
|
|
27
|
+
)
|
desdeo/emo/__init__.py
ADDED
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
"""Imports available from the desdeo emo."""
|
|
2
|
+
|
|
3
|
+
__all__ = [
|
|
4
|
+
"rvea",
|
|
5
|
+
"nsga3",
|
|
6
|
+
"template1",
|
|
7
|
+
"NSGAIII_select",
|
|
8
|
+
"RVEASelector",
|
|
9
|
+
"SimulatedBinaryCrossover",
|
|
10
|
+
"BoundedPolynomialMutation",
|
|
11
|
+
"LHSGenerator",
|
|
12
|
+
"RandomGenerator",
|
|
13
|
+
"EMOEvaluator",
|
|
14
|
+
"MaxEvaluationsTerminator",
|
|
15
|
+
"MaxGenerationsTerminator",
|
|
16
|
+
"Archive",
|
|
17
|
+
"FeasibleArchive",
|
|
18
|
+
"NonDominatedArchive",
|
|
19
|
+
]
|
|
20
|
+
|
|
21
|
+
from .hooks.archivers import Archive, FeasibleArchive, NonDominatedArchive
|
|
22
|
+
from .methods.bases import template1
|
|
23
|
+
from .methods.EAs import nsga3, rvea
|
|
24
|
+
from .operators.crossover import SimulatedBinaryCrossover
|
|
25
|
+
from .operators.evaluator import EMOEvaluator
|
|
26
|
+
from .operators.generator import LHSGenerator, RandomGenerator
|
|
27
|
+
from .operators.mutation import BoundedPolynomialMutation
|
|
28
|
+
from .operators.selection import NSGAIII_select, RVEASelector
|
|
29
|
+
from .operators.termination import MaxEvaluationsTerminator, MaxGenerationsTerminator
|