cornflow 1.0.11a1__py3-none-any.whl → 1.1.0a2__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.
Files changed (34) hide show
  1. cornflow/cli/service.py +4 -0
  2. cornflow/commands/__init__.py +1 -1
  3. cornflow/commands/schemas.py +31 -0
  4. cornflow/config.py +6 -0
  5. cornflow/endpoints/__init__.py +15 -20
  6. cornflow/endpoints/example_data.py +64 -13
  7. cornflow/endpoints/execution.py +2 -1
  8. cornflow/endpoints/login.py +16 -13
  9. cornflow/endpoints/user.py +2 -2
  10. cornflow/migrations/versions/991b98e24225_.py +33 -0
  11. cornflow/models/user.py +4 -0
  12. cornflow/schemas/example_data.py +7 -2
  13. cornflow/schemas/execution.py +8 -1
  14. cornflow/schemas/solution_log.py +11 -5
  15. cornflow/schemas/user.py +3 -0
  16. cornflow/shared/authentication/auth.py +1 -1
  17. cornflow/shared/licenses.py +17 -54
  18. cornflow/tests/custom_test_case.py +17 -3
  19. cornflow/tests/integration/test_cornflowclient.py +20 -14
  20. cornflow/tests/unit/test_cases.py +95 -6
  21. cornflow/tests/unit/test_cli.py +5 -5
  22. cornflow/tests/unit/test_dags.py +48 -1
  23. cornflow/tests/unit/test_example_data.py +85 -12
  24. cornflow/tests/unit/test_executions.py +98 -8
  25. cornflow/tests/unit/test_instances.py +43 -5
  26. cornflow/tests/unit/test_main_alarms.py +8 -8
  27. cornflow/tests/unit/test_schemas.py +12 -1
  28. cornflow/tests/unit/test_token.py +17 -0
  29. cornflow/tests/unit/test_users.py +16 -0
  30. {cornflow-1.0.11a1.dist-info → cornflow-1.1.0a2.dist-info}/METADATA +2 -2
  31. {cornflow-1.0.11a1.dist-info → cornflow-1.1.0a2.dist-info}/RECORD +34 -33
  32. {cornflow-1.0.11a1.dist-info → cornflow-1.1.0a2.dist-info}/WHEEL +0 -0
  33. {cornflow-1.0.11a1.dist-info → cornflow-1.1.0a2.dist-info}/entry_points.txt +0 -0
  34. {cornflow-1.0.11a1.dist-info → cornflow-1.1.0a2.dist-info}/top_level.txt +0 -0
cornflow/cli/service.py CHANGED
@@ -14,6 +14,7 @@ from cornflow.commands import (
14
14
  register_deployed_dags_command,
15
15
  register_dag_permissions_command,
16
16
  update_schemas_command,
17
+ update_dag_registry_command,
17
18
  )
18
19
  from cornflow.shared.const import AUTH_DB, ADMIN_ROLE, SERVICE_ROLE
19
20
  from cornflow.shared import db
@@ -211,6 +212,9 @@ def init_cornflow_service():
211
212
  )
212
213
  register_dag_permissions_command(open_deployment, verbose=True)
213
214
  update_schemas_command(airflow_url, airflow_user, airflow_pwd, verbose=True)
215
+ update_dag_registry_command(
216
+ airflow_url, airflow_user, airflow_pwd, verbose=True
217
+ )
214
218
 
215
219
  os.system(
216
220
  f"/usr/local/bin/gunicorn -c python:cornflow.gunicorn "
@@ -6,7 +6,7 @@ from .permissions import (
6
6
  register_dag_permissions_command,
7
7
  )
8
8
  from .roles import register_roles_command
9
- from .schemas import update_schemas_command
9
+ from .schemas import update_schemas_command, update_dag_registry_command
10
10
  from .users import (
11
11
  create_user_with_role,
12
12
  create_service_user_command,
@@ -27,3 +27,34 @@ def update_schemas_command(url, user, pwd, verbose: bool = False):
27
27
  current_app.logger.info("The DAGs schemas were not updated properly")
28
28
 
29
29
  return True
30
+
31
+
32
+ def update_dag_registry_command(url, user, pwd, verbose: bool = False):
33
+ import time
34
+ from flask import current_app
35
+
36
+ from cornflow_client.airflow.api import Airflow
37
+
38
+ af_client = Airflow(url, user, pwd)
39
+ max_attempts = 20
40
+ attempts = 0
41
+ while not af_client.is_alive() and attempts < max_attempts:
42
+ attempts += 1
43
+ if verbose == 1:
44
+ current_app.logger.info(f"Airflow is not reachable (attempt {attempts})")
45
+ time.sleep(15)
46
+
47
+ if not af_client.is_alive():
48
+ if verbose == 1:
49
+ current_app.logger.info("Airflow is not reachable")
50
+ return False
51
+
52
+ response = af_client.update_dag_registry()
53
+ if response.status_code == 200:
54
+ if verbose:
55
+ current_app.logger.info("DAGs schemas updated on cornflow")
56
+ else:
57
+ if verbose:
58
+ current_app.logger.info("The DAGs schemas were not updated properly")
59
+
60
+ return True
cornflow/config.py CHANGED
@@ -76,6 +76,12 @@ class DefaultConfig(object):
76
76
  # Alarms endpoints
77
77
  ALARMS_ENDPOINTS = os.getenv("CF_ALARMS_ENDPOINT", 0)
78
78
 
79
+ # Token duration in hours
80
+ TOKEN_DURATION = os.getenv("TOKEN_DURATION", 24)
81
+
82
+ # Password rotation time in days
83
+ PWD_ROTATION_TIME = os.getenv("PWD_ROTATION_TIME", 120)
84
+
79
85
 
80
86
  class Development(DefaultConfig):
81
87
 
@@ -4,8 +4,8 @@ All references to endpoints should be imported from here
4
4
  The login resource gets created on app startup as it depends on configuration
5
5
  """
6
6
  from .action import ActionListEndpoint
7
+ from .alarms import AlarmsEndpoint
7
8
  from .apiview import ApiViewListEndpoint
8
-
9
9
  from .case import (
10
10
  CaseEndpoint,
11
11
  CaseFromInstanceExecutionEndpoint,
@@ -15,7 +15,6 @@ from .case import (
15
15
  CaseToInstance,
16
16
  CaseCompare,
17
17
  )
18
-
19
18
  from .dag import (
20
19
  DAGDetailEndpoint,
21
20
  DAGEndpointManual,
@@ -24,7 +23,12 @@ from .dag import (
24
23
  DeployedDAGEndpoint,
25
24
  DeployedDagDetailEndpoint,
26
25
  )
27
-
26
+ from .data_check import (
27
+ DataCheckExecutionEndpoint,
28
+ DataCheckInstanceEndpoint,
29
+ DataCheckCaseEndpoint,
30
+ )
31
+ from .example_data import ExampleDataListEndpoint, ExampleDataDetailEndpoint
28
32
  from .execution import (
29
33
  ExecutionEndpoint,
30
34
  ExecutionDetailsEndpoint,
@@ -33,36 +37,22 @@ from .execution import (
33
37
  ExecutionLogEndpoint,
34
38
  ExecutionRelaunchEndpoint,
35
39
  )
36
-
37
40
  from .health import HealthEndpoint
38
-
39
41
  from .instance import (
40
42
  InstanceEndpoint,
41
43
  InstanceDetailsEndpoint,
42
44
  InstanceFileEndpoint,
43
45
  InstanceDataEndpoint,
44
46
  )
45
-
46
- from .data_check import (
47
- DataCheckExecutionEndpoint,
48
- DataCheckInstanceEndpoint,
49
- DataCheckCaseEndpoint,
50
- )
51
47
  from .licenses import LicensesEndpoint
48
+ from .main_alarms import MainAlarmsEndpoint
52
49
  from .permission import PermissionsViewRoleEndpoint, PermissionsViewRoleDetailEndpoint
53
-
54
50
  from .roles import RolesListEndpoint, RoleDetailEndpoint
55
-
56
51
  from .schemas import SchemaDetailsEndpoint, SchemaEndpoint
52
+ from .tables import TablesEndpoint, TablesDetailsEndpoint
57
53
  from .token import TokenEndpoint
58
- from .example_data import ExampleDataDetailsEndpoint
59
54
  from .user import UserEndpoint, UserDetailsEndpoint, ToggleUserAdmin, RecoverPassword
60
55
  from .user_role import UserRoleListEndpoint, UserRoleDetailEndpoint
61
- from .alarms import AlarmsEndpoint
62
- from .main_alarms import MainAlarmsEndpoint
63
-
64
- from .tables import TablesEndpoint, TablesDetailsEndpoint
65
-
66
56
 
67
57
  resources = [
68
58
  dict(resource=InstanceEndpoint, urls="/instance/", endpoint="instance"),
@@ -157,10 +147,15 @@ resources = [
157
147
  endpoint="schema-details",
158
148
  ),
159
149
  dict(
160
- resource=ExampleDataDetailsEndpoint,
150
+ resource=ExampleDataListEndpoint,
161
151
  urls="/example/<string:dag_name>/",
162
152
  endpoint="example-data",
163
153
  ),
154
+ dict(
155
+ resource=ExampleDataDetailEndpoint,
156
+ urls="/example/<string:dag_name>/<string:example_name>/",
157
+ endpoint="example-data-detail",
158
+ ),
164
159
  dict(resource=HealthEndpoint, urls="/health/", endpoint="health"),
165
160
  dict(
166
161
  resource=CaseFromInstanceExecutionEndpoint,
@@ -1,36 +1,78 @@
1
1
  """
2
2
  Endpoints to get the example data from a DAG
3
3
  """
4
+ import json
4
5
 
5
- # Import from libraries
6
6
  from cornflow_client.airflow.api import Airflow
7
7
  from flask import current_app, request
8
8
  from flask_apispec import marshal_with, doc
9
- import json
10
9
 
11
- # Import from internal modules
12
10
  from cornflow.endpoints.meta_resource import BaseMetaResource
13
11
  from cornflow.models import PermissionsDAG
14
- from cornflow.schemas.example_data import ExampleData
12
+ from cornflow.schemas.example_data import ExampleListData, ExampleDetailData
15
13
  from cornflow.shared.authentication import Auth, authenticate
16
14
  from cornflow.shared.const import VIEWER_ROLE, PLANNER_ROLE, ADMIN_ROLE
17
- from cornflow.shared.exceptions import AirflowError, NoPermission
15
+ from cornflow.shared.exceptions import AirflowError, NoPermission, ObjectDoesNotExist
18
16
 
19
17
 
20
- class ExampleDataDetailsEndpoint(BaseMetaResource):
18
+ class ExampleDataListEndpoint(BaseMetaResource):
21
19
  """
22
20
  Endpoint used to obtain schemas for one app
23
21
  """
24
22
 
25
23
  ROLES_WITH_ACCESS = [VIEWER_ROLE, PLANNER_ROLE, ADMIN_ROLE]
26
24
 
27
- @doc(description="Get example data from DAG", tags=["DAG"])
25
+ @doc(description="Get lsit of example data from DAG", tags=["DAG"])
28
26
  @authenticate(auth_class=Auth())
29
- @marshal_with(ExampleData)
27
+ @marshal_with(ExampleListData(many=True))
30
28
  def get(self, dag_name):
31
29
  """
32
30
  API method to get example data for a given dag
33
31
 
32
+ :return: A dictionary with the names and descriptions of available data examples
33
+ and an integer with the HTTP status code
34
+ :rtype: Tuple(dict, integer)
35
+ """
36
+ user = Auth().get_user_from_header(request.headers)
37
+ permission = PermissionsDAG.check_if_has_permissions(
38
+ user_id=user.id, dag_id=dag_name
39
+ )
40
+
41
+ if permission:
42
+ af_client = Airflow.from_config(current_app.config)
43
+ if not af_client.is_alive():
44
+ current_app.logger.error(
45
+ "Airflow not accessible when getting data {}".format(dag_name)
46
+ )
47
+ raise AirflowError(error="Airflow is not accessible")
48
+
49
+ # try airflow and see if dag_name exists
50
+ af_client.get_dag_info(dag_name)
51
+
52
+ current_app.logger.info("User gets example data from {}".format(dag_name))
53
+
54
+ variable_name = f"z_{dag_name}_examples"
55
+ response = af_client.get_one_variable(variable_name)
56
+
57
+ return json.loads(response["value"])
58
+ else:
59
+ err = "User does not have permission to access this dag."
60
+ raise NoPermission(
61
+ error=err,
62
+ status_code=403,
63
+ log_txt=f"Error while user {user} tries to get example data for dag {dag_name}. "
64
+ + err,
65
+ )
66
+
67
+
68
+ class ExampleDataDetailEndpoint(BaseMetaResource):
69
+ @doc(description="Get example data from DAG", tags=["DAG"])
70
+ @authenticate(auth_class=Auth())
71
+ @marshal_with(ExampleDetailData)
72
+ def get(self, dag_name, example_name):
73
+ """
74
+ API method to get one example data for a given dag
75
+
34
76
  :return: A dictionary with a message and a integer with the HTTP status code
35
77
  :rtype: Tuple(dict, integer)
36
78
  """
@@ -54,15 +96,24 @@ class ExampleDataDetailsEndpoint(BaseMetaResource):
54
96
 
55
97
  variable_name = f"z_{dag_name}_examples"
56
98
  response = af_client.get_one_variable(variable_name)
57
- result = dict()
58
- result["examples"] = json.loads(response["value"])
59
- result["name"] = response["key"]
60
99
 
61
- return result
100
+ example = None
101
+ for item in json.loads(response["value"]):
102
+ if item["name"] == example_name:
103
+ example = item
104
+ break
105
+
106
+ if example is None:
107
+ raise ObjectDoesNotExist(
108
+ error="The example does not exist", status_code=404
109
+ )
110
+
111
+ return example
62
112
  else:
63
113
  err = "User does not have permission to access this dag."
64
114
  raise NoPermission(
65
115
  error=err,
66
116
  status_code=403,
67
- log_txt=f"Error while user {user} tries to get example data for dag {dag_name}. " + err
117
+ log_txt=f"Error while user {user} tries to get example data for dag {dag_name}. "
118
+ + err,
68
119
  )
@@ -24,6 +24,7 @@ from cornflow.schemas.execution import (
24
24
  ExecutionEditRequest,
25
25
  QueryFiltersExecution,
26
26
  ReLaunchExecutionRequest,
27
+ ExecutionDetailsWithIndicatorsAndLogResponse
27
28
  )
28
29
  from cornflow.shared.authentication import Auth, authenticate
29
30
  from cornflow.shared.compress import compressed
@@ -58,7 +59,7 @@ class ExecutionEndpoint(BaseMetaResource):
58
59
 
59
60
  @doc(description="Get all executions", tags=["Executions"])
60
61
  @authenticate(auth_class=Auth())
61
- @marshal_with(ExecutionDetailsEndpointWithIndicatorsResponse(many=True))
62
+ @marshal_with(ExecutionDetailsWithIndicatorsAndLogResponse(many=True))
62
63
  @use_kwargs(QueryFiltersExecution, location="query")
63
64
  def get(self, **kwargs):
64
65
  """
@@ -6,10 +6,11 @@ External endpoint for the user to login to the cornflow webserver
6
6
  from flask import current_app
7
7
  from flask_apispec import use_kwargs, doc
8
8
  from sqlalchemy.exc import IntegrityError, DBAPIError
9
+ from datetime import datetime, timedelta
9
10
 
10
11
  # Import from internal modules
11
12
  from cornflow.endpoints.meta_resource import BaseMetaResource
12
- from cornflow.models import PermissionsDAG, UserModel, UserRoleModel
13
+ from cornflow.models import UserModel, UserRoleModel
13
14
  from cornflow.schemas.user import LoginEndpointRequest, LoginOpenAuthRequest
14
15
  from cornflow.shared import db
15
16
  from cornflow.shared.authentication import Auth, LDAPBase
@@ -47,9 +48,11 @@ class LoginBaseEndpoint(BaseMetaResource):
47
48
  :rtype: dict
48
49
  """
49
50
  auth_type = current_app.config["AUTH_TYPE"]
51
+ response = {}
50
52
 
51
53
  if auth_type == AUTH_DB:
52
54
  user = self.auth_db_authenticate(**kwargs)
55
+ response.update({"change_password": check_last_password_change(user)})
53
56
  elif auth_type == AUTH_LDAP:
54
57
  user = self.auth_ldap_authenticate(**kwargs)
55
58
  elif auth_type == AUTH_OID:
@@ -62,7 +65,9 @@ class LoginBaseEndpoint(BaseMetaResource):
62
65
  except Exception as e:
63
66
  raise InvalidUsage(f"Error in generating user token: {str(e)}", 400)
64
67
 
65
- return {"token": token, "id": user.id}, 200
68
+ response.update({"token": token, "id": user.id})
69
+
70
+ return response, 200
66
71
 
67
72
  def auth_db_authenticate(self, username, password):
68
73
  """
@@ -176,6 +181,13 @@ class LoginBaseEndpoint(BaseMetaResource):
176
181
  return user
177
182
 
178
183
 
184
+ def check_last_password_change(user):
185
+ if user.pwd_last_change:
186
+ if user.pwd_last_change + timedelta(days=int(current_app.config["PWD_ROTATION_TIME"])) < datetime.utcnow():
187
+ return True
188
+ return False
189
+
190
+
179
191
  class LoginEndpoint(LoginBaseEndpoint):
180
192
  """
181
193
  Endpoint used to do the login to the cornflow webserver
@@ -198,11 +210,7 @@ class LoginEndpoint(LoginBaseEndpoint):
198
210
  :rtype: Tuple(dict, integer)
199
211
  """
200
212
 
201
- content, status = self.log_in(**kwargs)
202
- if int(current_app.config["OPEN_DEPLOYMENT"]) == 1:
203
- PermissionsDAG.delete_all_permissions_from_user(content["id"])
204
- PermissionsDAG.add_all_permissions_to_user(content["id"])
205
- return content, status
213
+ return self.log_in(**kwargs)
206
214
 
207
215
 
208
216
  class LoginOpenAuthEndpoint(LoginBaseEndpoint):
@@ -218,9 +226,4 @@ class LoginOpenAuthEndpoint(LoginBaseEndpoint):
218
226
  @use_kwargs(LoginOpenAuthRequest, location="json")
219
227
  def post(self, **kwargs):
220
228
  """ """
221
-
222
- content, status = self.log_in(**kwargs)
223
- if int(current_app.config["OPEN_DEPLOYMENT"]) == 1:
224
- PermissionsDAG.delete_all_permissions_from_user(content["id"])
225
- PermissionsDAG.add_all_permissions_to_user(content["id"])
226
- return content, status
229
+ return self.log_in(**kwargs)
@@ -170,7 +170,7 @@ class UserDetailsEndpoint(BaseMetaResource):
170
170
  f"To edit a user, go to the OID provider.",
171
171
  )
172
172
 
173
- if data.get("password"):
173
+ if data.get("password") is not None:
174
174
  check, msg = check_password_pattern(data.get("password"))
175
175
  if not check:
176
176
  raise InvalidCredentials(
@@ -179,7 +179,7 @@ class UserDetailsEndpoint(BaseMetaResource):
179
179
  f"The new password is not valid.",
180
180
  )
181
181
 
182
- if data.get("email"):
182
+ if data.get("email") is not None:
183
183
  check, msg = check_email_pattern(data.get("email"))
184
184
  if not check:
185
185
  raise InvalidCredentials(
@@ -0,0 +1,33 @@
1
+ """
2
+ Added pwd_last_change column to users table
3
+
4
+ Revision ID: 991b98e24225
5
+ Revises: ebdd955fcc5e
6
+ Create Date: 2024-01-31 19:17:18.009264
7
+
8
+ """
9
+ from alembic import op
10
+ import sqlalchemy as sa
11
+
12
+
13
+ # revision identifiers, used by Alembic.
14
+ revision = "991b98e24225"
15
+ down_revision = "ebdd955fcc5e"
16
+ branch_labels = None
17
+ depends_on = None
18
+
19
+
20
+ def upgrade():
21
+ # ### commands auto generated by Alembic - please adjust! ###
22
+ with op.batch_alter_table("users", schema=None) as batch_op:
23
+ batch_op.add_column(sa.Column("pwd_last_change", sa.DateTime(), nullable=True))
24
+
25
+ # ### end Alembic commands ###
26
+
27
+
28
+ def downgrade():
29
+ # ### commands auto generated by Alembic - please adjust! ###
30
+ with op.batch_alter_table("users", schema=None) as batch_op:
31
+ batch_op.drop_column("pwd_last_change")
32
+
33
+ # ### end Alembic commands ###
cornflow/models/user.py CHANGED
@@ -4,6 +4,7 @@ This file contains the UserModel
4
4
  # Imports from external libraries
5
5
  import random
6
6
  import string
7
+ from datetime import datetime
7
8
 
8
9
  # Imports from internal modules
9
10
  from cornflow.models.meta_models import TraceAttributesModel
@@ -51,6 +52,7 @@ class UserModel(TraceAttributesModel):
51
52
  last_name = db.Column(db.String(128), nullable=True)
52
53
  username = db.Column(db.String(128), nullable=False, unique=True)
53
54
  password = db.Column(db.String(128), nullable=True)
55
+ pwd_last_change = db.Column(db.DateTime, nullable=True)
54
56
  email = db.Column(db.String(128), nullable=False, unique=True)
55
57
 
56
58
  user_roles = db.relationship(
@@ -93,6 +95,7 @@ class UserModel(TraceAttributesModel):
93
95
  self.first_name = data.get("first_name")
94
96
  self.last_name = data.get("last_name")
95
97
  self.username = data.get("username")
98
+ self.pwd_last_change = datetime.utcnow()
96
99
  # TODO: handle better None passwords that can be found when using ldap
97
100
  check_pass, msg = check_password_pattern(data.get("password"))
98
101
  if check_pass:
@@ -123,6 +126,7 @@ class UserModel(TraceAttributesModel):
123
126
  if new_password:
124
127
  new_password = self.__generate_hash(new_password)
125
128
  data["password"] = new_password
129
+ data["pwd_last_change"] = datetime.utcnow()
126
130
  super().update(data)
127
131
 
128
132
  def comes_from_external_provider(self):
@@ -1,6 +1,11 @@
1
1
  from marshmallow import fields, Schema
2
2
 
3
3
 
4
- class ExampleData(Schema):
4
+ class ExampleListData(Schema):
5
5
  name = fields.Str(required=True)
6
- examples = fields.Raw(required=True)
6
+ description = fields.Str(required=False)
7
+
8
+
9
+ class ExampleDetailData(ExampleListData):
10
+ instance = fields.Raw(required=True)
11
+ solution = fields.Raw(required=False)
@@ -4,7 +4,7 @@ from marshmallow import fields, Schema, validate
4
4
  # Imports from internal modules
5
5
  from cornflow.shared.const import MIN_EXECUTION_STATUS_CODE, MAX_EXECUTION_STATUS_CODE
6
6
  from .common import QueryFilters, BaseDataEndpointResponse
7
- from .solution_log import LogSchema
7
+ from .solution_log import LogSchema, BasicLogSchema
8
8
 
9
9
 
10
10
  class QueryFiltersExecution(QueryFilters):
@@ -114,6 +114,12 @@ class ExecutionDetailsEndpointWithIndicatorsResponse(ExecutionDetailsEndpointRes
114
114
  indicators = fields.Method("get_indicators")
115
115
 
116
116
 
117
+ class ExecutionDetailsWithIndicatorsAndLogResponse(
118
+ ExecutionDetailsEndpointWithIndicatorsResponse
119
+ ):
120
+ log = fields.Nested(BasicLogSchema, attribute="log_json")
121
+
122
+
117
123
  class ExecutionStatusEndpointResponse(Schema):
118
124
  id = fields.Str()
119
125
  state = fields.Int()
@@ -129,6 +135,7 @@ class ExecutionStatusEndpointUpdate(Schema):
129
135
  class ExecutionDataEndpointResponse(ExecutionDetailsEndpointResponse):
130
136
  data = fields.Raw()
131
137
  checks = fields.Raw()
138
+ log = fields.Nested(BasicLogSchema, attribute="log_json")
132
139
 
133
140
 
134
141
  class ExecutionLogEndpointResponse(ExecutionDetailsEndpointWithIndicatorsResponse):
@@ -1,4 +1,4 @@
1
- from marshmallow import fields, Schema
1
+ from marshmallow import fields, Schema, EXCLUDE
2
2
 
3
3
  options = dict(required=True, allow_none=True)
4
4
  log_options = dict(required=False, allow_none=True)
@@ -49,10 +49,18 @@ class FirstSolution(Schema):
49
49
  CutsBestBound = fields.Float(**options)
50
50
 
51
51
 
52
- class LogSchema(Schema):
52
+ class BasicLogSchema(Schema):
53
+ status = fields.Str(**log_options)
54
+ status_code = fields.Int(**log_options)
55
+ sol_code = fields.Int(**log_options)
56
+
57
+
58
+ class LogSchema(BasicLogSchema):
59
+ class Meta:
60
+ unknown = EXCLUDE
61
+
53
62
  version = fields.Str(**log_options)
54
63
  solver = fields.Str(**log_options)
55
- status = fields.Str(**log_options)
56
64
  best_bound = fields.Float(**log_options)
57
65
  best_solution = fields.Float(**log_options)
58
66
  gap = fields.Float(**log_options)
@@ -63,8 +71,6 @@ class LogSchema(Schema):
63
71
  presolve = fields.Nested(PresolveSchema, **log_options)
64
72
  first_relaxed = fields.Float(**log_options)
65
73
  first_solution = fields.Nested(FirstSolution, **log_options)
66
- status_code = fields.Int(**log_options)
67
- sol_code = fields.Int(**log_options)
68
74
  nodes = fields.Int(**log_options)
69
75
  progress = fields.Nested(ProgressSchema, required=False)
70
76
  cut_info = fields.Raw(**log_options)
cornflow/schemas/user.py CHANGED
@@ -25,6 +25,7 @@ class UserEndpointResponse(Schema):
25
25
  last_name = fields.Str()
26
26
  email = fields.Str()
27
27
  created_at = fields.Str()
28
+ pwd_last_change = fields.Str()
28
29
 
29
30
 
30
31
  class UserDetailsEndpointResponse(Schema):
@@ -33,6 +34,7 @@ class UserDetailsEndpointResponse(Schema):
33
34
  last_name = fields.Str()
34
35
  username = fields.Str()
35
36
  email = fields.Str()
37
+ pwd_last_change = fields.Str()
36
38
 
37
39
 
38
40
  class TokenEndpointResponse(Schema):
@@ -49,6 +51,7 @@ class UserEditRequest(Schema):
49
51
  last_name = fields.Str(required=False)
50
52
  email = fields.Str(required=False)
51
53
  password = fields.Str(required=False)
54
+ pwd_last_change = fields.DateTime(required=False)
52
55
 
53
56
 
54
57
  class LoginEndpointRequest(Schema):
@@ -103,7 +103,7 @@ class Auth:
103
103
  )
104
104
 
105
105
  payload = {
106
- "exp": datetime.utcnow() + timedelta(days=1),
106
+ "exp": datetime.utcnow() + timedelta(hours=float(current_app.config["TOKEN_DURATION"])),
107
107
  "iat": datetime.utcnow(),
108
108
  "sub": user_id,
109
109
  }
@@ -1,63 +1,21 @@
1
- import pkg_resources
1
+ import importlib.metadata as metadata
2
2
 
3
3
 
4
- def get_license_txt(pkg):
5
- if pkg.has_metadata("LICENSE"):
6
- lic = pkg.get_metadata("LICENSE")
7
- else:
8
- lic = "(license detail not found)"
9
- return lic
10
-
11
-
12
- def get_info(name, lines):
4
+ def get_info(name, pkg):
13
5
  """
14
- Search information in a list of lines.
6
+ Search information in a package metadata.
15
7
  The expected format of the line is "name: info"
16
- This function search the name and return the info.
8
+ This function searches for the name and returns the info.
17
9
 
18
- :param name: name to be search at the beginning of a line.
19
- :param lines: list of strings.
10
+ :param name: name to be searched.
11
+ :param pkg: a dictionary representing the package metadata.
20
12
  :return: the info part of the line for the given name.
21
13
  """
22
- sep = name + ": "
23
- for line in lines:
24
- if line.startswith(sep):
25
- return line.split(sep, maxsplit=1)[1]
14
+ if name in pkg:
15
+ return pkg[name]
26
16
  return f"({name} not found)"
27
17
 
28
18
 
29
- def get_main_info(pkg):
30
- """
31
- Get information from libraries.
32
-
33
- :param pkg: a package object from pkg_resources.working_set
34
- :return: a dict with library, license, version, author, description and home page.
35
- """
36
- lines1 = []
37
- lines2 = []
38
- # Find info in metadata
39
- if pkg.has_metadata("METADATA"):
40
- lines1 = pkg.get_metadata_lines("METADATA")
41
- # find info in PKG-INFO
42
- if pkg.has_metadata("PKG-INFO"):
43
- lines2 = pkg.get_metadata_lines("PKG-INFO")
44
- # Transform lines into list
45
- lines = [l for l in lines1] + [l for l in lines2]
46
-
47
- # Manage case where license is UNKNOWN
48
- lic = get_info("License", lines)
49
- if lic == "UNKNOWN":
50
- lic = get_info("Classifier: License :", lines)
51
- return {
52
- "library": get_info("Name", lines),
53
- "license": lic,
54
- "version": get_info("Version", lines),
55
- "author": get_info("Author", lines),
56
- "description": get_info("Summary", lines),
57
- "home page": get_info("Home-page", lines),
58
- }
59
-
60
-
61
19
  def get_licenses_summary():
62
20
  """
63
21
  Get a list of dicts with licenses and library information.
@@ -65,12 +23,17 @@ def get_licenses_summary():
65
23
  :return: a list of dicts with library, license, version, author, description, home page and license text.
66
24
  """
67
25
  license_list = []
68
- # TODO: pkg_resources.working_set is deprecated, find a better way to get the list of packages
69
- for pkg in sorted(pkg_resources.working_set, key=lambda x: str(x).lower()):
26
+ for pkg in sorted(metadata.distributions(), key=lambda x: x.metadata['Name'].lower()):
27
+ pkg_metadata = dict(pkg.metadata.items())
70
28
  license_list += [
71
29
  {
72
- **get_main_info(pkg),
73
- "license_text": get_license_txt(pkg),
30
+ "library": get_info("Name", pkg_metadata),
31
+ "license": get_info("License", pkg_metadata),
32
+ "version": get_info("Version", pkg_metadata),
33
+ "author": get_info("Author", pkg_metadata),
34
+ "description": get_info("Summary", pkg_metadata),
35
+ "home page": get_info("Home-page", pkg_metadata),
74
36
  }
75
37
  ]
38
+
76
39
  return license_list