cornflow 1.0.7__py3-none-any.whl → 1.0.8a1__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.
cornflow/cli/schemas.py CHANGED
@@ -25,7 +25,7 @@ def schemas():
25
25
 
26
26
  @schemas.command(
27
27
  name="generate_from_schema",
28
- help="Command to generate models, endpoints and schemas from a jsonschema"
28
+ help="Command to generate models, endpoints and schemas from a jsonschema",
29
29
  )
30
30
  @click.option(
31
31
  "--path", "-p", type=str, help="The absolute path to the JSONSchema", required=True
@@ -70,7 +70,13 @@ def schemas():
70
70
  required=False,
71
71
  )
72
72
  def generate_from_schema(
73
- path, app_name, output_path, remove_methods, one, endpoints_methods, endpoints_access
73
+ path,
74
+ app_name,
75
+ output_path,
76
+ remove_methods,
77
+ one,
78
+ endpoints_methods,
79
+ endpoints_access,
74
80
  ):
75
81
  """
76
82
  This method is executed for the command and creates all the files for the REST API from the provided JSONSchema
@@ -126,59 +132,64 @@ def generate_from_schema(
126
132
  output_path=output_path,
127
133
  options=methods_to_add,
128
134
  name_table=name_table,
129
- endpoints_access=dict_endpoints_access
135
+ endpoints_access=dict_endpoints_access,
130
136
  ).main()
131
137
 
132
138
 
133
- @schemas.command(name="schema_from_models", help="Command to generate a jsonschema from a set of models")
134
- @click.option(
135
- "--path",
136
- "-p",
137
- type=str,
138
- help="The absolute path to folder containing the models",
139
- required=True,
140
- )
141
- @click.option("--output-path", "-o", type=str, help="The output path", required=False)
142
- @click.option(
143
- "--ignore-files",
144
- "-i",
145
- type=str,
146
- help="Files that will be ignored (with the .py extension). "
147
- "__init__.py files are automatically ignored. Ex: 'instance.py'",
148
- multiple=True,
149
- required=False,
150
- )
151
- @click.option(
152
- "--leave-bases/--no-leave-bases",
153
- "-l/-nl",
154
- default=False,
155
- help="Use this option to leave the bases classes BaseDataModel, "
156
- "EmptyModel and TraceAttributes in the schema. By default, they will be deleted",
157
- )
158
- def schema_from_models(path, output_path, ignore_files, leave_bases):
159
- """
160
-
161
- :param str path: the path to the folder that contains the models
162
- :param output_path: the output path where the JSONSchema should be placed
163
- :param str ignore_files: files to be ignored.
164
- :param str leave_bases: if the JSONSchema should have abstract classes used as the base for other clases.
165
- :return: a click status code
166
- :rtype: int
167
- """
168
- path = path.replace("\\", "/")
169
- output = None
170
- if output_path:
171
- output = output_path.replace("\\", "/")
172
-
173
- if ignore_files:
174
- ignore_files = list(ignore_files)
175
-
176
- click.echo("Generating JSONSchema file from the REST API")
177
- click.echo(f"The path to the JSONSchema is {path}")
178
- click.echo(f"The output_path is {output}")
179
- click.echo(f"The ignore_files is {ignore_files}")
180
- click.echo(f"The leave_bases is {leave_bases}")
181
-
182
- SchemaGenerator(
183
- path, output_path=output, ignore_files=ignore_files, leave_bases=leave_bases
184
- ).main()
139
+ # @schemas.command(
140
+ # name="schema_from_models",
141
+ # help="Command to generate a jsonschema from a set of models",
142
+ # )
143
+ # @click.option(
144
+ # "--path",
145
+ # "-p",
146
+ # type=str,
147
+ # help="The absolute path to folder containing the models",
148
+ # required=True,
149
+ # )
150
+ # @click.option("--output-path", "-o", type=str, help="The output path", required=False)
151
+ # @click.option(
152
+ # "--ignore-files",
153
+ # "-i",
154
+ # type=str,
155
+ # help="Files that will be ignored (with the .py extension). "
156
+ # "__init__.py files are automatically ignored. Ex: 'instance.py'",
157
+ # multiple=True,
158
+ # required=False,
159
+ # )
160
+ # @click.option(
161
+ # "--leave-bases/--no-leave-bases",
162
+ # "-l/-nl",
163
+ # default=False,
164
+ # help="Use this option to leave the bases classes BaseDataModel, "
165
+ # "EmptyModel and TraceAttributes in the schema. By default, they will be deleted",
166
+ # )
167
+ # def schema_from_models(path, output_path, ignore_files, leave_bases):
168
+ # """
169
+ #
170
+ # :param str path: the path to the folder that contains the models
171
+ # :param output_path: the output path where the JSONSchema should be placed
172
+ # :param str ignore_files: files to be ignored.
173
+ # :param str leave_bases: if the JSONSchema should have abstract classes used as the base for other clases.
174
+ # :return: a click status code
175
+ # :rtype: int
176
+ # """
177
+ # path = path.replace("\\", "/")
178
+ # output = None
179
+ # if output_path:
180
+ # output = output_path.replace("\\", "/")
181
+ #
182
+ # if ignore_files:
183
+ # ignore_files = list(ignore_files)
184
+ #
185
+ # click.echo("Generating JSONSchema file from the REST API")
186
+ # click.echo(f"The path to the JSONSchema is {path}")
187
+ # click.echo(f"The output_path is {output}")
188
+ # click.echo(f"The ignore_files is {ignore_files}")
189
+ # click.echo(f"The leave_bases is {leave_bases}")
190
+ #
191
+ # SchemaGenerator(
192
+ # path, output_path=output, ignore_files=ignore_files, leave_bases=leave_bases
193
+ # ).main()
194
+ #
195
+ # return True
@@ -6,6 +6,8 @@ import json
6
6
  import importlib.util
7
7
  from distutils.dir_util import copy_tree
8
8
  from unittest.mock import MagicMock
9
+
10
+ import click
9
11
  from flask_sqlalchemy import SQLAlchemy
10
12
  import shutil
11
13
  from pytups import TupList, SuperDict
@@ -15,7 +17,6 @@ from sqlalchemy.sql.sqltypes import Integer
15
17
 
16
18
  class SchemaGenerator:
17
19
  def __init__(self, path, output_path=None, ignore_files=None, leave_bases=False):
18
-
19
20
  self.path = path
20
21
  self.tmp_path = os.path.join(os.getcwd(), "tmp_files")
21
22
  self.output_path = output_path or "./output_schema.json"
@@ -76,7 +77,6 @@ class SchemaGenerator:
76
77
  db = SQLAlchemy()
77
78
  try:
78
79
  for file_path, file_name in files:
79
-
80
80
  spec = importlib.util.spec_from_file_location(file_name, file_path)
81
81
  mod = importlib.util.module_from_spec(spec)
82
82
 
@@ -142,7 +142,7 @@ class SchemaGenerator:
142
142
 
143
143
  db.session.close()
144
144
  except Exception as err:
145
- print(err)
145
+ click.echo(err)
146
146
 
147
147
  def inherit(self):
148
148
  all_classes = set(self.parents.keys())
@@ -39,7 +39,11 @@ from cornflow.shared.const import (
39
39
  EXEC_STATE_QUEUED,
40
40
  )
41
41
  from cornflow.shared.exceptions import AirflowError, ObjectDoesNotExist, InvalidData
42
- from cornflow.shared.validators import json_schema_validate_as_string
42
+ from cornflow.shared.validators import (
43
+ json_schema_validate_as_string,
44
+ json_schema_extend_and_validate_as_string,
45
+ )
46
+
43
47
 
44
48
  class ExecutionEndpoint(BaseMetaResource):
45
49
  """
@@ -76,12 +80,10 @@ class ExecutionEndpoint(BaseMetaResource):
76
80
  ]
77
81
 
78
82
  running_executions = [
79
- execution for execution in executions
80
- if execution.state in [
81
- EXEC_STATE_RUNNING,
82
- EXEC_STATE_QUEUED,
83
- EXEC_STATE_UNKNOWN
84
- ]
83
+ execution
84
+ for execution in executions
85
+ if execution.state
86
+ in [EXEC_STATE_RUNNING, EXEC_STATE_QUEUED, EXEC_STATE_UNKNOWN]
85
87
  ]
86
88
 
87
89
  for execution in running_executions:
@@ -146,14 +148,6 @@ class ExecutionEndpoint(BaseMetaResource):
146
148
  user=self.get_user(), idx=execution.instance_id
147
149
  )
148
150
 
149
- if instance is None:
150
- err = "The instance to solve does not exist"
151
- raise ObjectDoesNotExist(
152
- error=err,
153
- log_txt=f"Error while user {self.get_user()} tries to create an execution "
154
- f"for instance {execution.instance_id}. " + err,
155
- )
156
-
157
151
  current_app.logger.debug(f"The request is: {request.args.get('run')}")
158
152
  # this allows testing without airflow interaction:
159
153
  if request.args.get("run", "1") == "0":
@@ -184,7 +178,9 @@ class ExecutionEndpoint(BaseMetaResource):
184
178
 
185
179
  # Validate config before running the dag
186
180
  config_schema = DeployedDAG.get_one_schema(config, schema, CONFIG_SCHEMA)
187
- config_errors = json_schema_validate_as_string(config_schema, kwargs["config"])
181
+ new_config, config_errors = json_schema_extend_and_validate_as_string(
182
+ config_schema, kwargs["config"]
183
+ )
188
184
  if config_errors:
189
185
  execution.update_state(
190
186
  EXEC_STATE_ERROR_START,
@@ -197,6 +193,8 @@ class ExecutionEndpoint(BaseMetaResource):
197
193
  log_txt=f"Error while user {self.get_user()} tries to create an execution. "
198
194
  f"Configuration data does not match the jsonschema.",
199
195
  )
196
+ elif new_config != kwargs["config"]:
197
+ execution.update_config(new_config)
200
198
 
201
199
  # Validate instance data before running the dag
202
200
  instance_schema = DeployedDAG.get_one_schema(config, schema, INSTANCE_SCHEMA)
@@ -326,7 +324,9 @@ class ExecutionRelaunchEndpoint(BaseMetaResource):
326
324
  }, 201
327
325
 
328
326
  # Validate config before running the dag
329
- config_schema = DeployedDAG.get_one_schema(config, kwargs["schema"], CONFIG_SCHEMA)
327
+ config_schema = DeployedDAG.get_one_schema(
328
+ config, kwargs["schema"], CONFIG_SCHEMA
329
+ )
330
330
  config_errors = json_schema_validate_as_string(config_schema, kwargs["config"])
331
331
  if config_errors:
332
332
  raise InvalidData(
@@ -444,14 +444,18 @@ class ExecutionDetailsEndpoint(ExecutionDetailsEndpointBase):
444
444
  schema = ExecutionModel.get_one_object(user=self.get_user(), idx=idx).schema
445
445
 
446
446
  if data.get("data") is not None and schema is not None:
447
- data_jsonschema = DeployedDAG.get_one_schema(config, schema, SOLUTION_SCHEMA)
448
- validation_errors = json_schema_validate_as_string(data_jsonschema, data["data"])
447
+ data_jsonschema = DeployedDAG.get_one_schema(
448
+ config, schema, SOLUTION_SCHEMA
449
+ )
450
+ validation_errors = json_schema_validate_as_string(
451
+ data_jsonschema, data["data"]
452
+ )
449
453
 
450
454
  if validation_errors:
451
455
  raise InvalidData(
452
456
  payload=dict(jsonschema_errors=validation_errors),
453
457
  log_txt=f"Error while user {self.get_user()} tries to edit execution {idx}. "
454
- f"Solution data does not match the jsonschema.",
458
+ f"Solution data does not match the jsonschema.",
455
459
  )
456
460
 
457
461
  current_app.logger.info(f"User {self.get_user()} edits execution {idx}")
cornflow/models/dag.py CHANGED
@@ -71,7 +71,8 @@ class DeployedDAG(TraceAttributesModel):
71
71
  jsonschema = item.instance_checks_schema
72
72
  elif schema == SOLUTION_CHECKS_SCHEMA:
73
73
  jsonschema = item.solution_checks_schema
74
- else: # schema == CONFIG_SCHEMA
74
+ # schema == CONFIG_SCHEMA
75
+ else:
75
76
  jsonschema = item.config_schema
76
77
 
77
78
  if jsonschema is None:
@@ -99,6 +99,16 @@ class ExecutionModel(BaseDataModel):
99
99
  self.checks = None
100
100
  super().update(data)
101
101
 
102
+ def update_config(self, config: dict):
103
+ """
104
+ Method to update the config of the execution after extending with default values
105
+
106
+ :param dict config: The config to store
107
+ :return: nothing
108
+ """
109
+ self.config = config
110
+ super().update({})
111
+
102
112
  def update_state(self, code, message=None):
103
113
  """
104
114
  Method to update the state code and message of an execution
@@ -4,8 +4,9 @@ This file has several validators
4
4
  import re
5
5
  from typing import Tuple, Union
6
6
 
7
- from jsonschema import Draft7Validator
7
+ from jsonschema import Draft7Validator, validators
8
8
  from disposable_email_domains import blocklist
9
+ from jsonschema.protocols import Validator
9
10
 
10
11
 
11
12
  def is_special_character(character):
@@ -66,6 +67,27 @@ def check_email_pattern(email: str) -> Tuple[bool, Union[str, None]]:
66
67
  return True, None
67
68
 
68
69
 
70
+ def extend_with_default(validator_class):
71
+ """
72
+ Method to extend a validator, so it extends the data with the default values defined on the jsonschema
73
+ """
74
+ validate_properties = validator_class.VALIDATORS["properties"]
75
+
76
+ def set_defaults(validator, properties, instance, schema):
77
+ for prop, sub in properties.items():
78
+ if "default" in sub:
79
+ instance.setdefault(prop, sub["default"])
80
+ for error in validate_properties(
81
+ validator,
82
+ properties,
83
+ instance,
84
+ schema,
85
+ ):
86
+ yield error
87
+
88
+ return validators.extend(validator_class, {"properties": set_defaults})
89
+
90
+
69
91
  def json_schema_validate(schema: dict, data: dict) -> list:
70
92
  """
71
93
  Method to validate some data against a json schema
@@ -81,6 +103,23 @@ def json_schema_validate(schema: dict, data: dict) -> list:
81
103
  return []
82
104
 
83
105
 
106
+ def json_schema_extend_and_validate(schema: dict, data: dict) -> Tuple[dict, list]:
107
+ """
108
+ Method to validate som data, extend it with default values and give back the processed errors
109
+
110
+ :param dict schema: the json schema in dict format.
111
+ :param dict data: the data to validate in dict format
112
+ :return: a tuple with the data extended and the errors found
113
+ :rtype: tuple
114
+ """
115
+ data_cp = dict(data)
116
+ default_validator = extend_with_default(Draft7Validator)
117
+ validator = default_validator(schema)
118
+ if not validator.is_valid(data_cp):
119
+ return data_cp, [e for e in validator.iter_errors(data_cp)]
120
+ return data_cp, []
121
+
122
+
84
123
  def json_schema_validate_as_string(schema: dict, data: dict) -> list:
85
124
  """
86
125
  Method to validate some data against a json schema
@@ -91,3 +130,18 @@ def json_schema_validate_as_string(schema: dict, data: dict) -> list:
91
130
  :rtype: list
92
131
  """
93
132
  return [str(e) for e in json_schema_validate(schema, data)]
133
+
134
+
135
+ def json_schema_extend_and_validate_as_string(
136
+ schema: dict, data: dict
137
+ ) -> Tuple[dict, list]:
138
+ """
139
+ Method to extend the schema with default values and give back the processed error
140
+
141
+ :param dict schema: the json schema in dict format.
142
+ :param dict data: the data to validate in dict format
143
+ :return: a tuple with the data extended and the errors found
144
+ :rtype: tuple
145
+ """
146
+ data_cp, errors = json_schema_extend_and_validate(schema, data)
147
+ return data_cp, [str(e) for e in errors]
cornflow/tests/const.py CHANGED
@@ -16,6 +16,8 @@ INSTANCE_GC_20 = _get_file("./data/gc_20_7.json")
16
16
  INSTANCE_FILE_FAIL = _get_file("./unit/test_instances.py")
17
17
 
18
18
  EXECUTION_PATH = _get_file("./data/new_execution.json")
19
+ BAD_EXECUTION_PATH = _get_file("./data/bad_execution.json")
20
+ EXECUTION_SOLUTION_PATH = _get_file("./data/new_execution_solution.json")
19
21
  EXECUTIONS_LIST = [EXECUTION_PATH, _get_file("./data/new_execution_2.json")]
20
22
  EXECUTION_URL = PREFIX + "/execution/"
21
23
  EXECUTION_URL_NORUN = EXECUTION_URL + "?run=0"
@@ -63,6 +65,8 @@ TABLES_URL = PREFIX + "/table/"
63
65
  ALARMS_URL = PREFIX + "/alarms/"
64
66
  MAIN_ALARMS_URL = PREFIX + "/main-alarms/"
65
67
 
68
+ LICENSES_URL = PREFIX + "/licences/"
69
+
66
70
  PUBLIC_DAGS = [
67
71
  "solve_model_dag",
68
72
  "gc",
@@ -16,6 +16,8 @@ from cornflow.tests.const import (
16
16
  EXECUTION_URL_NORUN,
17
17
  INSTANCE_URL,
18
18
  DAG_URL,
19
+ BAD_EXECUTION_PATH,
20
+ EXECUTION_SOLUTION_PATH,
19
21
  )
20
22
  from cornflow.tests.custom_test_case import CustomTestCase, BaseTestCases
21
23
  from cornflow.tests.unit.tools import patch_af_client
@@ -38,7 +40,9 @@ class TestExecutionsListEndpoint(BaseTestCases.ListFilters):
38
40
  return temp
39
41
 
40
42
  self.payload = load_file_fk(EXECUTION_PATH)
43
+ self.bad_payload = load_file_fk(BAD_EXECUTION_PATH)
41
44
  self.payloads = [load_file_fk(f) for f in EXECUTIONS_LIST]
45
+ self.solution = load_file_fk(EXECUTION_SOLUTION_PATH)
42
46
 
43
47
  def test_new_execution(self):
44
48
  self.create_new_row(self.url, self.model, payload=self.payload)
@@ -49,6 +53,55 @@ class TestExecutionsListEndpoint(BaseTestCases.ListFilters):
49
53
 
50
54
  self.create_new_row(EXECUTION_URL, self.model, payload=self.payload)
51
55
 
56
+ @patch("cornflow.endpoints.execution.Airflow")
57
+ def test_new_execution_bad_config(self, af_client_class):
58
+ patch_af_client(af_client_class)
59
+ response = self.create_new_row(
60
+ EXECUTION_URL,
61
+ self.model,
62
+ payload=self.bad_payload,
63
+ expected_status=400,
64
+ check_payload=False,
65
+ )
66
+ self.assertIn("error", response)
67
+ self.assertIn("jsonschema_errors", response)
68
+
69
+ @patch("cornflow.endpoints.execution.Airflow")
70
+ def test_new_execution_partial_config(self, af_client_class):
71
+ patch_af_client(af_client_class)
72
+ self.payload["config"].pop("solver")
73
+ response = self.create_new_row(
74
+ EXECUTION_URL, self.model, payload=self.payload, check_payload=False
75
+ )
76
+ self.assertIn("solver", response["config"])
77
+ self.assertEqual(response["config"]["solver"], "cbc")
78
+
79
+ @patch("cornflow.endpoints.execution.Airflow")
80
+ def test_new_execution_with_solution(self, af_client_class):
81
+ patch_af_client(af_client_class)
82
+ self.payload["data"] = self.solution
83
+ response = self.create_new_row(
84
+ EXECUTION_URL,
85
+ self.model,
86
+ payload=self.payload,
87
+ check_payload=False,
88
+ )
89
+
90
+ @patch("cornflow.endpoints.execution.Airflow")
91
+ def test_new_execution_with_solution_bad(self, af_client_class):
92
+ patch_af_client(af_client_class)
93
+ patch_af_client(af_client_class)
94
+ self.payload["data"] = {"message": "THIS IS NOT A VALID SOLUTION"}
95
+ response = self.create_new_row(
96
+ EXECUTION_URL,
97
+ self.model,
98
+ payload=self.payload,
99
+ check_payload=False,
100
+ expected_status=400,
101
+ )
102
+ self.assertIn("error", response)
103
+ self.assertIn("jsonschema_errors", response)
104
+
52
105
  def test_new_execution_no_instance(self):
53
106
  payload = dict(self.payload)
54
107
  payload["instance_id"] = "bad_id"
@@ -281,11 +334,7 @@ class TestExecutionsDetailEndpoint(
281
334
  def test_stop_execution(self, af_client_class):
282
335
  patch_af_client(af_client_class)
283
336
 
284
- idx = self.create_new_row(
285
- EXECUTION_URL,
286
- self.model,
287
- payload=self.payload
288
- )
337
+ idx = self.create_new_row(EXECUTION_URL, self.model, payload=self.payload)
289
338
 
290
339
  response = self.client.post(
291
340
  self.url + str(idx) + "/",
@@ -347,11 +396,13 @@ class TestExecutionsStatusEndpoint(TestExecutionsDetailEndpointMock):
347
396
  @patch("cornflow.endpoints.execution.Airflow")
348
397
  def test_get_one_status(self, af_client_class):
349
398
  patch_af_client(af_client_class)
350
-
399
+
351
400
  idx = self.create_new_row(EXECUTION_URL, self.model, self.payload)
352
401
  payload = dict(self.payload)
353
402
  payload["id"] = idx
354
- data = self.get_one_row(EXECUTION_URL + idx + "/status/", payload, check_payload=False)
403
+ data = self.get_one_row(
404
+ EXECUTION_URL + idx + "/status/", payload, check_payload=False
405
+ )
355
406
  self.assertEqual(data["state"], 1)
356
407
 
357
408
  @patch("cornflow.endpoints.execution.Airflow")
@@ -0,0 +1,45 @@
1
+ from cornflow.endpoints import LicensesEndpoint
2
+ from cornflow.tests.const import LICENSES_URL, _get_file
3
+ from cornflow.tests.custom_test_case import CustomTestCase
4
+
5
+
6
+ class TestLicensesListEndpoint(CustomTestCase):
7
+ @staticmethod
8
+ def read_requirements():
9
+ with open(_get_file("../../requirements.txt")) as req:
10
+ content = req.read()
11
+ requirements = content.split("\n")
12
+
13
+ requirements = [
14
+ r.split("=")[0].split(">")[0].split("<")[0].lower()
15
+ for r in requirements
16
+ if r != ""
17
+ ]
18
+ return requirements
19
+
20
+ def setUp(self):
21
+ super().setUp()
22
+ self.roles_with_access = LicensesEndpoint.ROLES_WITH_ACCESS
23
+ self.libraries = self.read_requirements()
24
+
25
+ def tearDown(self):
26
+ super().tearDown()
27
+
28
+ def test_get_licenses(self):
29
+ for role in self.roles_with_access:
30
+ self.token = self.create_user_with_role(role)
31
+ response = self.client.get(
32
+ LICENSES_URL,
33
+ follow_redirects=True,
34
+ headers={
35
+ "Content-Type": "application/json",
36
+ "Authorization": "Bearer " + self.token,
37
+ },
38
+ )
39
+
40
+ self.assertEqual(200, response.status_code)
41
+ self.assertIsInstance(response.json, list)
42
+ libraries = [k["library"].lower() for k in response.json]
43
+
44
+ for lib in self.libraries:
45
+ self.assertIn(lib, libraries)
@@ -1,124 +1,135 @@
1
- import unittest
2
- import json
3
- import os
4
-
5
- from click.testing import CliRunner
6
-
7
- from cornflow.cli import cli
8
-
9
- path_to_tests = os.path.dirname(os.path.abspath(__file__))
10
-
11
-
12
- class SchemaFromModelsTests(unittest.TestCase):
13
- def setUp(self):
14
- super().setUp()
15
- self.models_path = self._get_path("../data/models")
16
- self.output_path = self._get_path(os.path.join(os.getcwd(), "test_output.json"))
17
-
18
- @staticmethod
19
- def import_schema(path):
20
- with open(path, "r") as fd:
21
- schema = json.load(fd)
22
- return schema
23
-
24
- @staticmethod
25
- def _get_path(rel_path):
26
- return os.path.join(path_to_tests, rel_path)
27
-
28
- def tearDown(self):
29
- if os.path.exists(self.output_path):
30
- os.remove(self.output_path)
31
-
32
- def test_base(self):
33
- runner = CliRunner()
34
- result = runner.invoke(
35
- cli,
36
- [
37
- "schemas",
38
- "schema_from_models",
39
- "-p",
40
- self.models_path,
41
- "-o",
42
- self.output_path
43
- ]
44
- )
45
-
46
- self.assertEqual(result.exit_code, 0)
47
-
48
- schema = self.import_schema(self._get_path(self.output_path))
49
-
50
- tables = {
51
- "instances": {
52
- "id": "string",
53
- "data": "object",
54
- "checks": "object",
55
- "name": "string",
56
- "description": "string",
57
- },
58
- "actions": {"id": "integer", "name": "string"},
59
- "permission_dag": {
60
- "id": "integer",
61
- "dag_id": "string",
62
- "user_id": "integer",
63
- },
64
- "permission_view": {
65
- "id": "integer",
66
- "action_id": "integer",
67
- "api_view_id": "integer",
68
- "role_id": "integer",
69
- },
70
- }
71
- required_instance = {"id", "name", "data_hash"}
72
- foreign_keys = [
73
- ("permission_dag", "dag_id", "deployed_dags.id"),
74
- ("permission_dag", "user_id", "users.id"),
75
- ("permission_view", "action_id", "actions.id"),
76
- ("permission_view", "api_view_id", "api_view.id"),
77
- ]
78
- for tab_name, tab_checks in tables.items():
79
- # All tables exist
80
- self.assertIn(tab_name, schema["properties"])
81
- # The properties have correct types
82
- for prop, type_prop in tab_checks.items():
83
- table_props = schema["properties"][tab_name]["items"]["properties"]
84
- self.assertIn(prop, table_props)
85
- self.assertIn("type", table_props.get(prop, {}).keys())
86
- self.assertEqual(
87
- type_prop, table_props.get(prop, {}).get("type", "null")
88
- )
89
- # The foreign keys are correct
90
- for tab, key, foreign_key in foreign_keys:
91
- self.assertIn(
92
- "foreign_key", schema["properties"][tab]["items"]["properties"][key]
93
- )
94
- self.assertEqual(
95
- schema["properties"][tab]["items"]["properties"][key]["foreign_key"],
96
- foreign_key,
97
- )
98
- # The required property is correct
99
- self.assertEqual(
100
- required_instance,
101
- set(schema["properties"]["instances"]["items"]["required"]),
102
- )
103
-
104
- def test_ignore(self):
105
- runner = CliRunner()
106
- result = runner.invoke(
107
- cli,
108
- [
109
- "schemas",
110
- "schema_from_models",
111
- "-p",
112
- self.models_path,
113
- "-o",
114
- self.output_path,
115
- "-i",
116
- "instance.py"
117
- ]
118
- )
119
-
120
- self.assertEqual(result.exit_code, 0)
121
-
122
- schema = self.import_schema(self.output_path)
123
-
124
- self.assertNotIn("instances", schema["properties"].keys())
1
+ # import json
2
+ # import os
3
+ # import unittest
4
+ #
5
+ # from click.testing import CliRunner
6
+ # from flask_testing import TestCase
7
+ #
8
+ # from cornflow.app import create_app
9
+ # from cornflow.cli import cli
10
+ # from cornflow.shared import db
11
+ #
12
+ # path_to_tests = os.path.dirname(os.path.abspath(__file__))
13
+ #
14
+ #
15
+ # class SchemaFromModelsTests(TestCase):
16
+ # def create_app(self):
17
+ # app = create_app("testing")
18
+ # return app
19
+ #
20
+ # def setUp(self):
21
+ # db.create_all()
22
+ # self.models_path = self._get_path("../../models")
23
+ # print(self.models_path)
24
+ # self.output_path = self._get_path(os.path.join(os.getcwd(), "test_output.json"))
25
+ # print(self.output_path)
26
+ #
27
+ # @staticmethod
28
+ # def import_schema(path):
29
+ # with open(path, "r") as fd:
30
+ # schema = json.load(fd)
31
+ # return schema
32
+ #
33
+ # @staticmethod
34
+ # def _get_path(rel_path):
35
+ # return os.path.join(path_to_tests, rel_path)
36
+ #
37
+ # def tearDown(self):
38
+ # db.session.remove()
39
+ # db.drop_all()
40
+ # if os.path.exists(self.output_path):
41
+ # os.remove(self.output_path)
42
+ #
43
+ # def test_base(self):
44
+ # runner = CliRunner()
45
+ # result = runner.invoke(
46
+ # cli,
47
+ # [
48
+ # "schemas",
49
+ # "schema_from_models",
50
+ # "-p",
51
+ # self.models_path,
52
+ # "-o",
53
+ # self.output_path,
54
+ # ],
55
+ # )
56
+ #
57
+ # self.assertEqual(result.exit_code, True)
58
+ #
59
+ # schema = self.import_schema(self._get_path(self.output_path))
60
+ #
61
+ # tables = {
62
+ # "instances": {
63
+ # "id": "string",
64
+ # "data": "object",
65
+ # "checks": "object",
66
+ # "name": "string",
67
+ # "description": "string",
68
+ # },
69
+ # "actions": {"id": "integer", "name": "string"},
70
+ # "permission_dag": {
71
+ # "id": "integer",
72
+ # "dag_id": "string",
73
+ # "user_id": "integer",
74
+ # },
75
+ # "permission_view": {
76
+ # "id": "integer",
77
+ # "action_id": "integer",
78
+ # "api_view_id": "integer",
79
+ # "role_id": "integer",
80
+ # },
81
+ # }
82
+ # required_instance = {"id", "name", "data_hash"}
83
+ # foreign_keys = [
84
+ # ("permission_dag", "dag_id", "deployed_dags.id"),
85
+ # ("permission_dag", "user_id", "users.id"),
86
+ # ("permission_view", "action_id", "actions.id"),
87
+ # ("permission_view", "api_view_id", "api_view.id"),
88
+ # ]
89
+ # for tab_name, tab_checks in tables.items():
90
+ # # All tables exist
91
+ # self.assertIn(tab_name, schema["properties"])
92
+ # # The properties have correct types
93
+ # for prop, type_prop in tab_checks.items():
94
+ # table_props = schema["properties"][tab_name]["items"]["properties"]
95
+ # self.assertIn(prop, table_props)
96
+ # self.assertIn("type", table_props.get(prop, {}).keys())
97
+ # self.assertEqual(
98
+ # type_prop, table_props.get(prop, {}).get("type", "null")
99
+ # )
100
+ # # The foreign keys are correct
101
+ # for tab, key, foreign_key in foreign_keys:
102
+ # self.assertIn(
103
+ # "foreign_key", schema["properties"][tab]["items"]["properties"][key]
104
+ # )
105
+ # self.assertEqual(
106
+ # schema["properties"][tab]["items"]["properties"][key]["foreign_key"],
107
+ # foreign_key,
108
+ # )
109
+ # # The required property is correct
110
+ # self.assertEqual(
111
+ # required_instance,
112
+ # set(schema["properties"]["instances"]["items"]["required"]),
113
+ # )
114
+ #
115
+ # def test_ignore(self):
116
+ # runner = CliRunner()
117
+ # result = runner.invoke(
118
+ # cli,
119
+ # [
120
+ # "schemas",
121
+ # "schema_from_models",
122
+ # "-p",
123
+ # self.models_path,
124
+ # "-o",
125
+ # self.output_path,
126
+ # "-i",
127
+ # "instance.py",
128
+ # ],
129
+ # )
130
+ #
131
+ # self.assertEqual(result.exit_code, True)
132
+ #
133
+ # schema = self.import_schema(self.output_path)
134
+ #
135
+ # self.assertNotIn("instances", schema["properties"].keys())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: cornflow
3
- Version: 1.0.7
3
+ Version: 1.0.8a1
4
4
  Summary: Cornflow is an open source multi-solver optimization server with a REST API built using flask.
5
5
  Home-page: https://github.com/baobabsoluciones/cornflow
6
6
  Author: baobab soluciones
@@ -15,7 +15,7 @@ Requires-Python: >=3.8
15
15
  Requires-Dist: alembic ==1.9.2
16
16
  Requires-Dist: apispec <=6.2.0
17
17
  Requires-Dist: click <=8.1.3
18
- Requires-Dist: cornflow-client <=1.0.14
18
+ Requires-Dist: cornflow-client ==1.0.16a1
19
19
  Requires-Dist: cryptography <=39.0.2
20
20
  Requires-Dist: disposable-email-domains >=0.0.86
21
21
  Requires-Dist: Flask ==2.3.2
@@ -28,7 +28,6 @@ Requires-Dist: Flask-Migrate <=4.0.4
28
28
  Requires-Dist: Flask-RESTful <=0.3.9
29
29
  Requires-Dist: Flask-SQLAlchemy ==2.5.1
30
30
  Requires-Dist: gevent ==23.9.1
31
- Requires-Dist: greenlet <=2.0.2
32
31
  Requires-Dist: gunicorn <=20.1.0
33
32
  Requires-Dist: jsonpatch <=1.32
34
33
  Requires-Dist: ldap3 <=2.9.1
@@ -41,6 +40,8 @@ Requires-Dist: requests <=2.29.0
41
40
  Requires-Dist: SQLAlchemy ==1.3.21
42
41
  Requires-Dist: webargs <=8.2.0
43
42
  Requires-Dist: Werkzeug <=2.3.3
43
+ Requires-Dist: greenlet <=2.0.2 ; python_version < "3.11"
44
+ Requires-Dist: greenlet ==3.0.0 ; python_version >= "3.11"
44
45
 
45
46
  Cornflow
46
47
  =========
@@ -84,19 +85,33 @@ Cornflow helps you formalize your problem by proposing development guidelines. I
84
85
  Installation instructions
85
86
  -------------------------------
86
87
 
87
- Cornflow is tested with Ubuntu 20.04, python >= 3.5 and git.
88
+ Cornflow is tested with Ubuntu 20.04, python >= 3.8 and git.
88
89
 
89
90
  Download the Cornflow project and install requirements::
90
91
 
91
- git clone git@github.com:baobabsoluciones/cornflow.git
92
- cd cornflow-server
93
92
  python3 -m venv venv
94
- venv/bin/pip3 install -r requirements-dev.txt
93
+ venv/bin/pip3 install cornflow
94
+
95
+ initialize the sqlite database::
96
+
97
+ source venv/bin/activate
98
+ export FLASK_APP=cornflow.app
99
+ export DATABASE_URL=sqlite:///cornflow.db
100
+ flask db upgrade
101
+ flask access_init
102
+ flask create_service_user -u airflow -e airflow_test@admin.com -p airflow_test_password
103
+ flask create_admin_user -u cornflow -e cornflow_admin@admin.com -p cornflow_admin_password
104
+
95
105
 
96
106
  activate the virtual environment and run Cornflow::
97
107
 
98
108
  source venv/bin/activate
99
109
  export FLASK_APP=cornflow.app
110
+ export SECRET_KEY=THISNEEDSTOBECHANGED
111
+ export DATABASE_URL=sqlite:///cornflow.db
112
+ export AIRFLOW_URL=http://127.0.0.1:8080/
113
+ export AIRFLOW_USER=airflow_user
114
+ export AIRFLOW_PWD=airflow_pwd
100
115
  flask run
101
116
 
102
117
  **Cornflow needs a running installation of Airflow to operate and more configuration**. Check `the installation docs <https://baobabsoluciones.github.io/cornflow/main/install.html>`_ for more details on installing airflow, configuring the application and initializing the database.
@@ -179,8 +194,7 @@ Using cornflow to deploy a solution method
179
194
 
180
195
  To deploy a cornflow solution method, the following tasks need to be accomplished:
181
196
 
182
- #. Create I/O schemas for the new problem (e.g., “TSP format”).
183
- #. Create a solve function (e.g., a 2-opt heuristic).
197
+ #. Create an Application for the new problem
184
198
  #. Do a PR to a compatible repo linked to a server instance (e.g., like `this one <https://github.com/baobabsoluciones/cornflow>`_).
185
199
 
186
200
  For more details on each part, check the `deployment guide <https://baobabsoluciones.github.io/cornflow/guides/deploy_solver.html>`_.
@@ -14,7 +14,7 @@ cornflow/cli/config.py,sha256=_7Y6tDo5uu4riymkzMYHnTR9IYxBG_FsjwmB84Du90U,1148
14
14
  cornflow/cli/migrations.py,sha256=Stc8H99rG8vgo3yRJcck11zBY_EA4WqyVybglfl8zJE,1624
15
15
  cornflow/cli/permissions.py,sha256=4KXKysH4g8YYQIZcPuXFS2g0xEErp-e8I_FAqMGaV7U,1006
16
16
  cornflow/cli/roles.py,sha256=NFG__qrlyOT0h4L4nwo9FSV4DKjGtMVh3gwiJxwM37w,411
17
- cornflow/cli/schemas.py,sha256=M9sbVPgPFgMscrS6OLGSDfiYxfUE7VTgI9_tRFQcvCU,5922
17
+ cornflow/cli/schemas.py,sha256=sxuJOZf12SBZAXDiAYNPB-n9LSxzSwkB3xyhgS_4K9A,6086
18
18
  cornflow/cli/service.py,sha256=yhQT860oXmQFU_maHHL276tYubKUs6YcwheWv7x7oh0,9135
19
19
  cornflow/cli/users.py,sha256=QvvFJw_tjgYB4GjwXmGTAH_FT_Z6nATg9gmkCbcyLJ8,748
20
20
  cornflow/cli/utils.py,sha256=0tF41gTt6LL9XGOizTQg2GXuOXbqLg6gapCr-HWjJ0Q,733
@@ -23,7 +23,7 @@ cornflow/cli/tools/__init__.py,sha256=JtyhYfUfirw78BV1mCsdeY0W25fDPWTmZhNBWdDh0w
23
23
  cornflow/cli/tools/api_generator.py,sha256=7ZEEGBdL9Anbj5gnPm3m_eHQm0ehz7Y7YaD952mGh58,16344
24
24
  cornflow/cli/tools/endpoint_tools.py,sha256=Xnv-IQWx6szgMzyD7r3unV050S-J7CxVR5p_cMHMO30,12640
25
25
  cornflow/cli/tools/models_tools.py,sha256=GGqO8fmksA37jhP_xm1dMMAJRZ8_k9j-0V2J05hJAog,5398
26
- cornflow/cli/tools/schema_generator.py,sha256=g-TmppguBIOKIo7hn2vefBB_SG2cJL2pSmD_8R5xhik,7540
26
+ cornflow/cli/tools/schema_generator.py,sha256=3zfxQUej8BwPwmrGttjpK0R_oSnXIJQWOqoott-E78E,7557
27
27
  cornflow/cli/tools/schemas_tools.py,sha256=56VQfsUC6b1Hm5Ran1X6X4QF4HfTxrcOjWlGUCL7BAw,2245
28
28
  cornflow/cli/tools/tools.py,sha256=Qm0X-wHN12vXYJNRHONGjqDZewwXyXy4R_j4UT_XMLs,929
29
29
  cornflow/commands/__init__.py,sha256=E-IgJGF9NNYVhyWYh5ZYP6o0QucWVpJ9N2sbSxFkDmI,515
@@ -44,7 +44,7 @@ cornflow/endpoints/case.py,sha256=80Fpv9p8mwIXzjQFuyq1PnPTz3RaOUk932sCUfw7yGA,18
44
44
  cornflow/endpoints/dag.py,sha256=MRthA2pnZCAFfoPbHCLDW2j1BsQ3WdjRGC17Szl4b28,10390
45
45
  cornflow/endpoints/data_check.py,sha256=ZyYR84IT9snjXxUrQfrlv_RzOec_AYeTsijuHYdLAcA,16496
46
46
  cornflow/endpoints/example_data.py,sha256=fzolSYl1sYCSCR4Ctr7QD9JM3NdxCjBiiQLVxCPCbJg,2441
47
- cornflow/endpoints/execution.py,sha256=9KOh0OkwYPEoDhqC0ot8Tsh4HpyNQwBqcPKET5iBZEQ,28043
47
+ cornflow/endpoints/execution.py,sha256=TQ-JtgFtYXVXJIbVs12qxWwGigfLrg9PzdTP0fK6k4g,27951
48
48
  cornflow/endpoints/health.py,sha256=TWmWjKdQOoDzpqwcfksuaAGOLIb2idxzPQcGMWrdkCY,1610
49
49
  cornflow/endpoints/instance.py,sha256=WAnloocXFxSW4vunBJo3CIHx4NzC_0GPJh5bj3ETd9U,11615
50
50
  cornflow/endpoints/licenses.py,sha256=82hHWGYvVIiyw9mlwGtMwJMDJ-ShHOi9rvuM6KvfE4U,873
@@ -82,9 +82,9 @@ cornflow/models/action.py,sha256=8MYzQ2qX5bG0zk28OufypzThkR7AU1J1el-5ABoTurg,120
82
82
  cornflow/models/alarms.py,sha256=R_g3tkWNSJaAG4gSvthgJlyrueY9VDuIZPoVHk5lDvU,1682
83
83
  cornflow/models/base_data_model.py,sha256=WykAv5_t5-xTP--ajYniIyZYN-ahqpRKLlW6pG3ghGg,5467
84
84
  cornflow/models/case.py,sha256=CW1UvsrBvKK4ilQ5bWaqPwZbF0P3A1M_910E495nndA,7202
85
- cornflow/models/dag.py,sha256=jgT_ty91rxKBNxFx8WZM28ZQWZQGROnXvPWbp6BL4tQ,2932
85
+ cornflow/models/dag.py,sha256=DhHpBqJXrLzPoVSyrS_rYI7BotdzITopJDKsql3mQnQ,2930
86
86
  cornflow/models/dag_permissions.py,sha256=QkhPxSLKxH5elIMdf-rnRtV_CEZBQDFFKUOWv15RgwI,1716
87
- cornflow/models/execution.py,sha256=PnIuJroi1-AFhzRD3wmnrsZ7nVbTEX8xLt-6b2stkwU,5687
87
+ cornflow/models/execution.py,sha256=Q0b_m9ACJbgw0lhQ6gkzqY3CjBxRmHO2uU5G75y7wGo,5974
88
88
  cornflow/models/instance.py,sha256=JKQndz413eGw6J9HwzyIynVQrqtS4egrJY0_jTBuwZM,3788
89
89
  cornflow/models/main_alarms.py,sha256=9S-Ohr2kYFFWB0HomrpSdDIoUr85Eu1rt90Om_Pa8VY,1748
90
90
  cornflow/models/meta_models.py,sha256=qeliGdpw0_q0GCeZzansF-09Ay5pueaT-QQPVPZ5aj4,12000
@@ -125,13 +125,13 @@ cornflow/shared/log_config.py,sha256=FM2ajjp2MB4BlFbUHklnWInT7-LLjtrqQ0mo3k_HRmE
125
125
  cornflow/shared/query_tools.py,sha256=6yGLCWjv-I2a_ZU4A0IymyJq67fZPZdRcCGOGQQpSXg,1199
126
126
  cornflow/shared/utils.py,sha256=g2ZsD3SwsqIHXZ7GWVAVB0F9gX7mT9dQkPgR2Ahsh6M,860
127
127
  cornflow/shared/utils_tables.py,sha256=A7bjpt7Metkb0FP7tKXMqOkak2fgi3O9dejYvoJBpb0,2236
128
- cornflow/shared/validators.py,sha256=uckA39cgSJjJPCj90L6unf18z82IdsGhPZ5cofaQ5wg,2899
128
+ cornflow/shared/validators.py,sha256=aFKAAJ2diElUA8WdDyCcXJI6r3FV7HFVzoOTC6t4f8Y,4803
129
129
  cornflow/shared/authentication/__init__.py,sha256=cJIChk5X6hbA_16usEvfHr8g4JDFI6WKo0GPVOOiYHA,137
130
130
  cornflow/shared/authentication/auth.py,sha256=VuM8kmjSi2G_4BR9sop6vn13SFEHiixFBOhB_Pm196M,16840
131
131
  cornflow/shared/authentication/decorators.py,sha256=_QpwOU1kYzpaK85Dl0Btdj5hG8Ps47PFgySp_gqhlgk,1276
132
132
  cornflow/shared/authentication/ldap.py,sha256=QfdC2X_ZMcIJabKC5pYWDGMhS5pIOJJvdZXuuiruq-M,4853
133
133
  cornflow/tests/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
134
- cornflow/tests/const.py,sha256=c58TRNAQcErq5Git1HKt4xwuhsu_-8XiuLIymR4HNqo,2327
134
+ cornflow/tests/const.py,sha256=_5BYFGN42Xg0PXMR8UU5DBL6dYmYn5rgRBgPyptrKso,2499
135
135
  cornflow/tests/custom_liveServer.py,sha256=I_0YNrcKIwVmRov3zCQMWwcCWkMe5V246Hpa4gS8AZE,3079
136
136
  cornflow/tests/custom_test_case.py,sha256=3nHflrRoBXSaws0XSdbBaV21mTYL8LzDJs6wOyFBdmQ,24773
137
137
  cornflow/tests/integration/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
@@ -149,24 +149,25 @@ cornflow/tests/unit/test_commands.py,sha256=QwGHTOxBOwiIYYQg8wcmSR11lKQk0I8Ltr3s
149
149
  cornflow/tests/unit/test_dags.py,sha256=Vww3_TDmHUnPvX2BJac6fXMp4y4UOWqnkYuzdVkBa4k,9034
150
150
  cornflow/tests/unit/test_data_checks.py,sha256=VjB3AAQOHlqnaRT2jI9L2mNLDAcda6llpiZWkW7nnkk,5471
151
151
  cornflow/tests/unit/test_example_data.py,sha256=uaF66FpFNHAgHYH_6RH7Ylu1A6JcBU_qT7pNwS5IMOk,1518
152
- cornflow/tests/unit/test_executions.py,sha256=1GAfbwDo0hfJU-MTHiKcXh4SgRIerm9g5jjiPfIbBWY,13339
152
+ cornflow/tests/unit/test_executions.py,sha256=tNzkeOgZIdS-11FXNXLODIdfHCKZ5B1I4XuvyN8irj0,15333
153
153
  cornflow/tests/unit/test_generate_from_schema.py,sha256=L1EdnASbDJ8SjrX1V4WnUKKwV0sRTwVnNYnxSpyeSeQ,15376
154
154
  cornflow/tests/unit/test_health.py,sha256=0E0HXMb63_Z8drbLZdxnJwtTbQyaZS9ZEHut6qsDbh8,1033
155
155
  cornflow/tests/unit/test_instances.py,sha256=FWNxeP9LMkeCAoniEHioSFN2W_4yqbI-69pAMLkh-sY,9650
156
156
  cornflow/tests/unit/test_instances_file.py,sha256=zXxSlOM_MMkFvpWNX-iatD40xoIAOGQkinCLf1txb0M,1986
157
+ cornflow/tests/unit/test_licenses.py,sha256=9vYkJZw7Ubza2_pvpgrbyiat6hpLsglgoCPdOzdammo,1494
157
158
  cornflow/tests/unit/test_log_in.py,sha256=zwVCNO0sGQhpVcUaJnh8cVv2z-qPEYCdI98y61CNyfE,979
158
159
  cornflow/tests/unit/test_main_alarms.py,sha256=H1LWQETlSQMsC6Rb3BB9ds7xL705t9aiPyp4gCWMwDQ,1970
159
160
  cornflow/tests/unit/test_permissions.py,sha256=4mLj3GI0Bvhy927eXu_RyAmK8i2XD7raYc6W8lyAO04,8782
160
161
  cornflow/tests/unit/test_roles.py,sha256=xZ3TohL_sv1ZBPvHv_nnYSsKEhBlrzIchx9soaTb5Ow,16581
161
- cornflow/tests/unit/test_schema_from_models.py,sha256=EqdlCSNTsuDzNtGPs-zTMDUy8zeE_2KGnTBBuQAUemc,3871
162
+ cornflow/tests/unit/test_schema_from_models.py,sha256=7IfycOGO3U06baX8I-OPJfu-3ZAn5cv8RCdj9wvalMk,4421
162
163
  cornflow/tests/unit/test_schemas.py,sha256=nX78H0QhG3fThnw8CqS7EhDyuXWwMiA0pXVy52qhlUk,7177
163
164
  cornflow/tests/unit/test_sign_up.py,sha256=-i6VO9z1FwqRHFvaSrpWAzOZx6qa8mHUEmmsjuMXjn8,3481
164
165
  cornflow/tests/unit/test_tables.py,sha256=dY55YgaCkyqwJnqn0LbZHNeXBoL4ZxXWwKkCoTF4WVE,8947
165
166
  cornflow/tests/unit/test_token.py,sha256=r0hzozPTlYmmWsxbFXQ5QovXIg5sES6b2--2dzlEreI,2068
166
167
  cornflow/tests/unit/test_users.py,sha256=_GGnT3ltVEDWJcBUulUflhEx2eKIjbSFi74zY211k1I,21677
167
168
  cornflow/tests/unit/tools.py,sha256=ag3sWv2WLi498R1GL5AOUnXqSsszD3UugzLZLC5NqAw,585
168
- cornflow-1.0.7.dist-info/METADATA,sha256=kxSntrT04xAa0htZNJXZo9Iho07h2F_75ewRt01CFBs,8905
169
- cornflow-1.0.7.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
170
- cornflow-1.0.7.dist-info/entry_points.txt,sha256=r5wKLHpuyVLMUIZ5I29_tpqYf-RuP-3w_8DhFi8_blQ,47
171
- cornflow-1.0.7.dist-info/top_level.txt,sha256=Qj9kLFJW1PLb-ZV2s_aCkQ-Wi5W6KC6fFR-LTBrx-rU,24
172
- cornflow-1.0.7.dist-info/RECORD,,
169
+ cornflow-1.0.8a1.dist-info/METADATA,sha256=7i-UJ_t05Rzs6f3Zji_yrLcyP-r0Hx-brHryZtOYa5A,9404
170
+ cornflow-1.0.8a1.dist-info/WHEEL,sha256=yQN5g4mg4AybRjkgi-9yy4iQEFibGQmlz78Pik5Or-A,92
171
+ cornflow-1.0.8a1.dist-info/entry_points.txt,sha256=r5wKLHpuyVLMUIZ5I29_tpqYf-RuP-3w_8DhFi8_blQ,47
172
+ cornflow-1.0.8a1.dist-info/top_level.txt,sha256=Qj9kLFJW1PLb-ZV2s_aCkQ-Wi5W6KC6fFR-LTBrx-rU,24
173
+ cornflow-1.0.8a1.dist-info/RECORD,,