diff --git a/mistral/api/controllers/resource.py b/mistral/api/controllers/resource.py index 947574b19..a9dcb23e2 100644 --- a/mistral/api/controllers/resource.py +++ b/mistral/api/controllers/resource.py @@ -51,8 +51,13 @@ class Resource(wtypes.Base): return cls.from_tuples(d.items()) @classmethod - def from_db_model(cls, db_model): - return cls.from_tuples(db_model.iter_columns()) + def from_db_model(cls, db_model, fields=()): + if isinstance(db_model, tuple): + db_tuples = zip(fields, db_model) + else: + db_tuples = db_model.iter_columns(fields=fields) + + return cls.from_tuples(db_tuples) def __str__(self): """WSME based implementation of __str__.""" diff --git a/mistral/api/controllers/v2/action.py b/mistral/api/controllers/v2/action.py index 11d79aede..d2f50dce7 100644 --- a/mistral/api/controllers/v2/action.py +++ b/mistral/api/controllers/v2/action.py @@ -79,6 +79,9 @@ class ActionsController(rest.RestController, hooks.HookController): :param identifier: ID or name of the Action to get. :param namespace: The namespace of the action. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's not provided. """ acl.enforce('actions:get', context.ctx()) @@ -251,7 +254,7 @@ class ActionsController(rest.RestController, hooks.HookController): Default: asc. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param name: Optional. Keep only resources with a specific name. :param scope: Optional. Keep only resources with a specific scope. diff --git a/mistral/api/controllers/v2/action_execution.py b/mistral/api/controllers/v2/action_execution.py index 33cce07be..9701b6c1e 100644 --- a/mistral/api/controllers/v2/action_execution.py +++ b/mistral/api/controllers/v2/action_execution.py @@ -52,25 +52,40 @@ def _load_deferred_output_field(action_ex): # Use retries to prevent possible failures. @rest_utils.rest_retry_on_db_error -def _get_action_execution(id): +def _get_action_execution(id, fields=()): + if fields and 'id' not in fields: + fields.insert(0, 'id') + with db_api.transaction(): - return _get_action_execution_resource(db_api.get_action_execution(id)) + return _get_action_execution_resource( + db_api.get_action_execution(id), + fields=fields + ) -def _get_action_execution_resource(action_ex): +def _get_action_execution_resource(action_ex, fields=()): _load_deferred_output_field(action_ex) - return _get_action_execution_resource_for_list(action_ex) + if fields and 'id' not in fields: + fields.insert(0, 'id') + + return _get_action_execution_resource_for_list(action_ex, fields=fields) -def _get_action_execution_resource_for_list(action_ex): +def _get_action_execution_resource_for_list(action_ex, fields=()): # TODO(nmakhotkin): Get rid of using dicts for constructing resources. # TODO(nmakhotkin): Use db_model for this instead. - res = resources.ActionExecution.from_db_model(action_ex) - task_name = (action_ex.task_execution.name - if action_ex.task_execution else None) - setattr(res, 'task_name', task_name) + # field_task_name_needed = 'task_name' in fields + # if field_task_name_needed: + # fields.remove('task_name') + + res = resources.ActionExecution.from_db_model(action_ex, fields=fields) + + # if not fields or field_task_name_needed: + # task_name = (action_ex.task_execution.name + # if action_ex.task_execution else None) + # setattr(res, 'task_name', task_name) return res @@ -95,7 +110,7 @@ def _get_action_executions(task_execution_id=None, marker=None, limit=None, or less than that of sort_keys. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param filters: Optional. A list of filters to apply to the result. """ @@ -124,17 +139,21 @@ def _get_action_executions(task_execution_id=None, marker=None, limit=None, class ActionExecutionsController(rest.RestController): @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.ActionExecution, wtypes.text) - def get(self, id): + @wsme_pecan.wsexpose(resources.ActionExecution, wtypes.text, + types.uniquelist) + def get(self, id, fields=None): """Return the specified action_execution. :param id: UUID of action execution to retrieve + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's not provided. """ acl.enforce('action_executions:get', context.ctx()) LOG.debug("Fetch action_execution [id=%s]", id) - return _get_action_execution(id) + return _get_action_execution(id, fields=fields) @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose(resources.ActionExecution, @@ -248,7 +267,7 @@ class ActionExecutionsController(rest.RestController): or less than that of sort_keys. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param name: Optional. Keep only resources with a specific name. :param workflow_name: Optional. Keep only resources with a specific @@ -379,7 +398,7 @@ class TasksActionExecutionController(rest.RestController): or less than that of sort_keys. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param name: Optional. Keep only resources with a specific name. :param workflow_name: Optional. Keep only resources with a specific @@ -444,15 +463,19 @@ class TasksActionExecutionController(rest.RestController): ) @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.ActionExecution, wtypes.text, wtypes.text) - def get(self, task_execution_id, action_ex_id): + @wsme_pecan.wsexpose(resources.ActionExecution, wtypes.text, wtypes.text, + types.uniquelist) + def get(self, task_execution_id, action_ex_id, fields=()): """Return the specified action_execution. :param task_execution_id: Task execution UUID :param action_ex_id: Action execution UUID + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's not provided. """ acl.enforce('action_executions:get', context.ctx()) LOG.debug("Fetch action_execution [id=%s]", action_ex_id) - return _get_action_execution(action_ex_id) + return _get_action_execution(action_ex_id, fields=fields) diff --git a/mistral/api/controllers/v2/code_source.py b/mistral/api/controllers/v2/code_source.py index 94c94697a..dda521a4e 100644 --- a/mistral/api/controllers/v2/code_source.py +++ b/mistral/api/controllers/v2/code_source.py @@ -187,8 +187,9 @@ class CodeSourcesController(rest.RestController, hooks.HookController): ) @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.CodeSource, wtypes.text, wtypes.text) - def get(self, identifier, namespace=''): + @wsme_pecan.wsexpose(resources.CodeSource, wtypes.text, + wtypes.text, types.uniquelist) + def get(self, identifier, namespace='', fields=''): """Return a code source. :param identifier: Name or UUID of the code source to retrieve. @@ -202,13 +203,17 @@ class CodeSourcesController(rest.RestController, hooks.HookController): identifier, namespace ) + if fields and 'id' not in fields: + fields.insert(0, 'id') db_model = rest_utils.rest_retry_on_db_error( db_api.get_code_source)( identifier=identifier, - namespace=namespace + namespace=namespace, + fields=fields ) - + if fields: + return resources.CodeSource.from_tuples(zip(fields, db_model)) return resources.CodeSource.from_db_model(db_model) @rest_utils.wrap_pecan_controller_exception diff --git a/mistral/api/controllers/v2/cron_trigger.py b/mistral/api/controllers/v2/cron_trigger.py index 69feb785f..eb17e5c4f 100644 --- a/mistral/api/controllers/v2/cron_trigger.py +++ b/mistral/api/controllers/v2/cron_trigger.py @@ -31,22 +31,30 @@ LOG = logging.getLogger(__name__) class CronTriggersController(rest.RestController): @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.CronTrigger, wtypes.text) - def get(self, identifier): + @wsme_pecan.wsexpose(resources.CronTrigger, + wtypes.text, types.uniquelist) + def get(self, identifier, fields=''): """Returns the named cron_trigger. :param identifier: Id or name of cron trigger to retrieve + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's not provided. """ acl.enforce('cron_triggers:get', context.ctx()) LOG.debug('Fetch cron trigger [identifier=%s]', identifier) + if fields and 'id' not in fields: + fields.insert(0, 'id') + # Use retries to prevent possible failures. db_model = rest_utils.rest_retry_on_db_error( db_api.get_cron_trigger - )(identifier) - - return resources.CronTrigger.from_db_model(db_model) + )(identifier, fields=fields) + if fields: + return resources.CronTrigger.from_tuples(zip(fields, db_model)) + return resources.CronTrigger.from_db_model(db_model, fields=fields) @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose( @@ -124,7 +132,7 @@ class CronTriggersController(rest.RestController): or less than that of sort_keys. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param name: Optional. Keep only resources with a specific name. :param workflow_name: Optional. Keep only resources with a specific diff --git a/mistral/api/controllers/v2/dynamic_action.py b/mistral/api/controllers/v2/dynamic_action.py index 065b458db..44f98cc2d 100644 --- a/mistral/api/controllers/v2/dynamic_action.py +++ b/mistral/api/controllers/v2/dynamic_action.py @@ -208,8 +208,9 @@ class DynamicActionsController(rest.RestController, hooks.HookController): ) @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.DynamicAction, wtypes.text, wtypes.text) - def get(self, identifier, namespace=''): + @wsme_pecan.wsexpose(resources.DynamicAction, wtypes.text, + wtypes.text, types.uniquelist) + def get(self, identifier, namespace='', fields=''): """Return the named action. :param identifier: Name or UUID of the action to retrieve. @@ -222,14 +223,19 @@ class DynamicActionsController(rest.RestController, hooks.HookController): identifier, namespace ) + if fields and 'id' not in fields: + fields.insert(0, 'id') db_model = rest_utils.rest_retry_on_db_error( db_api.get_dynamic_action_definition )( identifier=identifier, - namespace=namespace + namespace=namespace, + fields=fields ) + if fields: + return resources.DynamicAction.from_tuples(zip(fields, db_model)) return resources.DynamicAction.from_db_model(db_model) @rest_utils.wrap_pecan_controller_exception diff --git a/mistral/api/controllers/v2/environment.py b/mistral/api/controllers/v2/environment.py index 0ed3226c2..07258d376 100644 --- a/mistral/api/controllers/v2/environment.py +++ b/mistral/api/controllers/v2/environment.py @@ -59,7 +59,7 @@ class EnvironmentController(rest.RestController): or less than that of sort_keys. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param name: Optional. Keep only resources with a specific name. :param description: Optional. Keep only resources with a specific @@ -101,21 +101,28 @@ class EnvironmentController(rest.RestController): ) @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.Environment, wtypes.text) - def get(self, name): + @wsme_pecan.wsexpose(resources.Environment, wtypes.text, types.uniquelist) + def get(self, name, fields=''): """Return the named environment. :param name: Name of environment to retrieve + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's not provided. """ acl.enforce('environments:get', context.ctx()) LOG.debug("Fetch environment [name=%s]", name) + if fields and 'id' not in fields: + fields.insert(0, 'id') + # Use retries to prevent possible failures. r = rest_utils.create_db_retry_object() - db_model = r.call(db_api.get_environment, name) - - return resources.Environment.from_db_model(db_model) + db_model = r.call(db_api.get_environment, name, fields=fields) + if fields: + return resources.Environment.from_tuples(zip(fields, db_model)) + return resources.Environment.from_db_model(db_model, fields=fields) @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose( diff --git a/mistral/api/controllers/v2/event_trigger.py b/mistral/api/controllers/v2/event_trigger.py index 6dd7cc303..ecc98a41d 100644 --- a/mistral/api/controllers/v2/event_trigger.py +++ b/mistral/api/controllers/v2/event_trigger.py @@ -34,18 +34,22 @@ CREATE_MANDATORY = set(['exchange', 'topic', 'event', 'workflow_id']) class EventTriggersController(rest.RestController): @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.EventTrigger, types.uuid) - def get(self, id): + @wsme_pecan.wsexpose(resources.EventTrigger, types.uuid, types.uniquelist) + def get(self, id, fields=''): """Returns the specified event_trigger.""" acl.enforce('event_triggers:get', auth_ctx.ctx()) LOG.debug('Fetch event trigger [id=%s]', id) + if fields and 'id' not in fields: + fields.insert(0, 'id') + # Use retries to prevent possible failures. r = rest_utils.create_db_retry_object() - db_model = r.call(db_api.get_event_trigger, id) - - return resources.EventTrigger.from_db_model(db_model) + db_model = r.call(db_api.get_event_trigger, id, fields=fields) + if fields: + return resources.EventTrigger.from_tuples(zip(fields, db_model)) + return resources.EventTrigger.from_db_model(db_model, fields=fields) @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose(resources.EventTrigger, body=resources.EventTrigger, diff --git a/mistral/api/controllers/v2/execution.py b/mistral/api/controllers/v2/execution.py index bef75404a..7d9bbe4ea 100644 --- a/mistral/api/controllers/v2/execution.py +++ b/mistral/api/controllers/v2/execution.py @@ -68,12 +68,20 @@ def _get_workflow_execution_resource(wf_ex): # Use retries to prevent possible failures. @rest_utils.rest_retry_on_db_error -def _get_workflow_execution(id, must_exist=True): +def _get_workflow_execution(id, must_exist=True, fields=None): + if fields and 'id' not in fields: + fields.insert(0, 'id') + + fields_tuple = rest_utils.fields_list_to_cls_fields_tuple( + db_models.WorkflowExecution, + fields + ) + with db_api.transaction(): if must_exist: - wf_ex = db_api.get_workflow_execution(id) + wf_ex = db_api.get_workflow_execution(id, fields=fields_tuple) else: - wf_ex = db_api.load_workflow_execution(id) + wf_ex = db_api.load_workflow_execution(id, fields=fields_tuple) return rest_utils.load_deferred_fields( wf_ex, @@ -90,19 +98,25 @@ class ExecutionsController(rest.RestController): executions = sub_execution.SubExecutionsController() @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.Execution, wtypes.text) - def get(self, id): + @wsme_pecan.wsexpose(resources.Execution, wtypes.text, types.uniquelist) + def get(self, id, fields=None): """Return the specified Execution. :param id: UUID of execution to retrieve. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's not provided. """ acl.enforce("executions:get", context.ctx()) LOG.debug("Fetch execution [id=%s]", id) - wf_ex = _get_workflow_execution(id) + wf_ex = _get_workflow_execution(id, fields=fields) - resource = resources.Execution.from_db_model(wf_ex) + if fields: + return resources.Execution.from_tuples(zip(fields, wf_ex)) + + resource = resources.Execution.from_db_model(wf_ex, fields=fields) resource.published_global = ( data_flow.get_workflow_execution_published_global(wf_ex) @@ -363,7 +377,7 @@ class ExecutionsController(rest.RestController): or less than that of sort_keys. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param workflow_name: Optional. Keep only resources with a specific workflow name. diff --git a/mistral/api/controllers/v2/resources.py b/mistral/api/controllers/v2/resources.py index f9183a87c..6f7d8d24e 100644 --- a/mistral/api/controllers/v2/resources.py +++ b/mistral/api/controllers/v2/resources.py @@ -159,8 +159,8 @@ class Workflow(resource.Resource, ScopedResource): return obj @classmethod - def from_db_model(cls, db_model): - obj = super(Workflow, cls).from_db_model(db_model) + def from_db_model(cls, db_model, fields=()): + obj = super(Workflow, cls).from_db_model(db_model, fields=fields) obj.set_attributes_from_spec(db_model.get('spec')) diff --git a/mistral/api/controllers/v2/task.py b/mistral/api/controllers/v2/task.py index 7c9e4ea50..a5f6f8865 100644 --- a/mistral/api/controllers/v2/task.py +++ b/mistral/api/controllers/v2/task.py @@ -49,19 +49,22 @@ STATE_TYPES = wtypes.Enum( ) -def _get_task_resource_with_result(task_ex): - task = resources.Task.from_db_model(task_ex) - - task.result = json.dumps(data_flow.get_task_execution_result(task_ex)) +def _get_task_resource_with_result(task_ex, fields=()): + task = resources.Task.from_db_model(task_ex, fields=fields) + if 'result' in fields or not fields: + task.result = json.dumps(data_flow.get_task_execution_result(task_ex)) return task # Use retries to prevent possible failures. @rest_utils.rest_retry_on_db_error -def _get_task_execution(id): +def _get_task_execution(id, fields=()): + if fields and 'id' not in fields: + fields.insert(0, 'id') + with db_api.transaction(): - task_ex = db_api.get_task_execution(id) + task_ex = db_api.get_task_execution(id, fields=fields) rest_utils.load_deferred_fields(task_ex, ['workflow_execution']) rest_utils.load_deferred_fields( @@ -74,7 +77,7 @@ def _get_task_execution(id): ['params'] ) - return _get_task_resource_with_result(task_ex), task_ex + return _get_task_resource_with_result(task_ex, fields), task_ex def get_published_global(task_ex, wf_ex=None): @@ -141,7 +144,7 @@ class TaskExecutionsController(rest.RestController): or less than that of sort_keys. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param workflow_name: Optional. Keep only resources with a specific workflow name. @@ -204,18 +207,26 @@ class TasksController(rest.RestController): executions = sub_execution.SubExecutionsController() @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.Task, wtypes.text) - def get(self, id): + @wsme_pecan.wsexpose(resources.Task, wtypes.text, types.uniquelist) + def get(self, id, fields=''): """Return the specified task. :param id: UUID of task to retrieve + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's not provided. """ acl.enforce('tasks:get', context.ctx()) LOG.debug("Fetch task [id=%s]", id) - task, task_ex = _get_task_execution(id) - - return _task_with_published_global(task, task_ex) + task, task_ex = _get_task_execution(id, ()) + task = _task_with_published_global(task, task_ex) + if fields: + if 'id' not in fields: + fields.insert(0, 'id') + task_dict = {field: task.to_dict()[field] for field in fields} + task = resources.Task.from_dict(task_dict) + return task @rest_utils.wrap_wsme_controller_exception @wsme_pecan.wsexpose(resources.Tasks, types.uuid, int, types.uniquelist, @@ -249,7 +260,7 @@ class TasksController(rest.RestController): or less than that of sort_keys. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param name: Optional. Keep only resources with a specific name. :param workflow_name: Optional. Keep only resources with a specific @@ -412,7 +423,7 @@ class ExecutionTasksController(rest.RestController): or less than that of sort_keys. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param name: Optional. Keep only resources with a specific name. :param workflow_name: Optional. Keep only resources with a specific diff --git a/mistral/api/controllers/v2/workbook.py b/mistral/api/controllers/v2/workbook.py index c55592804..c4d86263b 100644 --- a/mistral/api/controllers/v2/workbook.py +++ b/mistral/api/controllers/v2/workbook.py @@ -44,27 +44,36 @@ class WorkbooksController(rest.RestController, hooks.HookController): ) @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.Workbook, wtypes.text, wtypes.text) - def get(self, name, namespace=''): + @wsme_pecan.wsexpose(resources.Workbook, wtypes.text, wtypes.text, + types.uniquelist) + def get(self, name, namespace='', fields=''): """Return the named workbook. :param name: Name of workbook to retrieve. :param namespace: Optional. Namespace of workbook to retrieve. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's not provided. """ acl.enforce('workbooks:get', context.ctx()) LOG.debug("Fetch workbook [name=%s, namespace=%s]", name, namespace) + if fields and 'id' not in fields: + fields.insert(0, 'id') + # Use retries to prevent possible failures. r = rest_utils.create_db_retry_object() db_model = r.call( db_api.get_workbook, name, - namespace=namespace + namespace=namespace, + fields=fields ) - - return resources.Workbook.from_db_model(db_model) + if fields: + return resources.Workbook.from_tuples(zip(fields, db_model)) + return resources.Workbook.from_db_model(db_model, fields=fields) @rest_utils.wrap_pecan_controller_exception @pecan.expose(content_type="text/plain") @@ -171,7 +180,7 @@ class WorkbooksController(rest.RestController, hooks.HookController): Default: asc. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param name: Optional. Keep only resources with a specific name. :param definition: Optional. Keep only resources with a specific diff --git a/mistral/api/controllers/v2/workflow.py b/mistral/api/controllers/v2/workflow.py index 57896a8a4..871f0458f 100644 --- a/mistral/api/controllers/v2/workflow.py +++ b/mistral/api/controllers/v2/workflow.py @@ -77,26 +77,36 @@ class WorkflowsController(rest.RestController, hooks.HookController): ) @rest_utils.wrap_wsme_controller_exception - @wsme_pecan.wsexpose(resources.Workflow, wtypes.text, wtypes.text) - def get(self, identifier, namespace=''): + @wsme_pecan.wsexpose(resources.Workflow, wtypes.text, wtypes.text, + types.uniquelist) + def get(self, identifier, namespace='', fields=''): """Return the named workflow. :param identifier: Name or UUID of the workflow to retrieve. :param namespace: Optional. Namespace of the workflow to retrieve. + :param fields: Optional. A specified list of fields of the resource to + be returned. 'id' will be included automatically in + fields if it's not provided. """ acl.enforce('workflows:get', context.ctx()) LOG.debug("Fetch workflow [identifier=%s]", identifier) + if fields and 'id' not in fields: + fields.insert(0, 'id') + # Use retries to prevent possible failures. r = rest_utils.create_db_retry_object() db_model = r.call( db_api.get_workflow_definition, identifier, - namespace=namespace + namespace=namespace, + fields=fields, ) - return resources.Workflow.from_db_model(db_model) + if fields: + return resources.Workflow.from_tuples(zip(fields, db_model)) + return resources.Workflow.from_db_model(db_model, fields=fields) @rest_utils.wrap_pecan_controller_exception @pecan.expose(content_type="text/plain") @@ -240,7 +250,7 @@ class WorkflowsController(rest.RestController, hooks.HookController): Default: asc. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param name: Optional. Keep only resources with a specific name. :param namespace: Optional. Keep only resources with a specific diff --git a/mistral/db/sqlalchemy/model_base.py b/mistral/db/sqlalchemy/model_base.py index fb3b4a0d9..3a24f2c3c 100644 --- a/mistral/db/sqlalchemy/model_base.py +++ b/mistral/db/sqlalchemy/model_base.py @@ -87,15 +87,18 @@ class _MistralModelBase(oslo_models.ModelBase, oslo_models.TimestampMixin): if col.name not in unloaded and hasattr(self, col.name): yield col.name - def iter_columns(self): + def iter_columns(self, fields=()): """Returns an iterator for loaded columns. + :param fields: names of fields to return + :type fields: tuple, list or set :return: A generator function that generates tuples (column name, column value). """ for col_name in self.iter_column_names(): - yield col_name, getattr(self, col_name) + if not fields or col_name in fields: + yield col_name, getattr(self, col_name) def get_clone(self): """Clones current object, loads all fields and returns the result.""" diff --git a/mistral/db/v2/api.py b/mistral/db/v2/api.py index c613dfbeb..fde104592 100644 --- a/mistral/db/v2/api.py +++ b/mistral/db/v2/api.py @@ -524,17 +524,17 @@ def get_scheduled_jobs_count(**kwargs): # Cron triggers. -def get_cron_trigger(identifier): - return IMPL.get_cron_trigger(identifier) +def get_cron_trigger(identifier, fields=()): + return IMPL.get_cron_trigger(identifier, fields=fields) -def get_cron_trigger_by_id(id): - return IMPL.get_cron_trigger_by_id(id) +def get_cron_trigger_by_id(id, fields=()): + return IMPL.get_cron_trigger_by_id(id, fields=fields) -def load_cron_trigger(identifier): +def load_cron_trigger(identifier, fields=()): """Unlike get_cron_trigger this method is allowed to return None.""" - return IMPL.load_cron_trigger(identifier) + return IMPL.load_cron_trigger(identifier, fields=fields) def get_cron_triggers(**kwargs): @@ -593,13 +593,13 @@ def delete_cron_triggers(**kwargs): # Environments. -def get_environment(name): - return IMPL.get_environment(name) +def get_environment(name, fields=()): + return IMPL.get_environment(name, fields=fields) -def load_environment(name): +def load_environment(name, fields=()): """Unlike get_environment this method is allowed to return None.""" - return IMPL.load_environment(name) + return IMPL.load_environment(name, fields=fields) def get_environments(limit=None, marker=None, sort_keys=None, @@ -667,12 +667,12 @@ def delete_resource_members(**kwargs): # Event triggers. -def get_event_trigger(id, insecure=False): - return IMPL.get_event_trigger(id, insecure) +def get_event_trigger(id, insecure=False, fields=()): + return IMPL.get_event_trigger(id, insecure, fields=fields) -def load_event_trigger(id, insecure=False): - return IMPL.load_event_trigger(id, insecure) +def load_event_trigger(id, insecure=False, fields=()): + return IMPL.load_event_trigger(id, insecure, fields=fields) def get_event_triggers(insecure=False, limit=None, marker=None, sort_keys=None, diff --git a/mistral/db/v2/sqlalchemy/api.py b/mistral/db/v2/sqlalchemy/api.py index b24bd9892..6a5f5d8f9 100644 --- a/mistral/db/v2/sqlalchemy/api.py +++ b/mistral/db/v2/sqlalchemy/api.py @@ -307,6 +307,11 @@ def _get_db_object_by_name(model, name, columns=()): def _get_db_object_by_id(model, id, insecure=False, columns=()): + columns = ( + tuple([getattr(model, f) for f in columns if hasattr(model, f)]) + if columns and isinstance(columns, list) else columns + ) + query = ( b.model_query(model, columns=columns) if insecure @@ -319,6 +324,10 @@ def _get_db_object_by_id(model, id, insecure=False, columns=()): def _get_db_object_by_name_and_namespace_or_id(model, identifier, namespace=None, insecure=False, columns=()): + columns = ( + tuple([getattr(model, f) for f in columns if hasattr(model, f)]) + if columns and isinstance(columns, list) else columns + ) query = ( b.model_query(model, columns=columns) if insecure @@ -1599,13 +1608,14 @@ def _get_completed_root_executions_query(columns): @b.session_aware() -def get_cron_trigger(identifier, session=None): +def get_cron_trigger(identifier, session=None, fields=()): ctx = context.ctx() cron_trigger = _get_db_object_by_name_and_namespace_or_id( models.CronTrigger, identifier, - insecure=ctx.is_admin + insecure=ctx.is_admin, + columns=fields, ) if not cron_trigger: @@ -1617,10 +1627,11 @@ def get_cron_trigger(identifier, session=None): @b.session_aware() -def get_cron_trigger_by_id(id, session=None): +def get_cron_trigger_by_id(id, session=None, fields=()): ctx = context.ctx() cron_trigger = _get_db_object_by_id(models.CronTrigger, id, - insecure=ctx.is_admin) + insecure=ctx.is_admin, + columns=fields) if not cron_trigger: raise exc.DBEntityNotFoundError( "Cron trigger not found [id=%s]" % id @@ -1630,10 +1641,11 @@ def get_cron_trigger_by_id(id, session=None): @b.session_aware() -def load_cron_trigger(identifier, session=None): +def load_cron_trigger(identifier, session=None, fields=()): return _get_db_object_by_name_and_namespace_or_id( models.CronTrigger, - identifier + identifier, + columns=fields, ) @@ -1744,8 +1756,8 @@ def delete_cron_triggers(session=None, **kwargs): # Environments. @b.session_aware() -def get_environment(name, session=None): - env = _get_db_object_by_name(models.Environment, name) +def get_environment(name, session=None, fields=()): + env = _get_db_object_by_name(models.Environment, name, columns=fields) if not env: raise exc.DBEntityNotFoundError( @@ -1756,8 +1768,8 @@ def get_environment(name, session=None): @b.session_aware() -def load_environment(name, session=None): - return _get_db_object_by_name(models.Environment, name) +def load_environment(name, session=None, fields=()): + return _get_db_object_by_name(models.Environment, name, columns=fields) @b.session_aware() @@ -1982,8 +1994,9 @@ def _get_accepted_resources(res_type): # Event triggers. @b.session_aware() -def get_event_trigger(id, insecure=False, session=None): - event_trigger = _get_db_object_by_id(models.EventTrigger, id, insecure) +def get_event_trigger(id, insecure=False, session=None, fields=()): + event_trigger = _get_db_object_by_id(models.EventTrigger, id, insecure, + columns=fields) if not event_trigger: raise exc.DBEntityNotFoundError( @@ -1994,8 +2007,9 @@ def get_event_trigger(id, insecure=False, session=None): @b.session_aware() -def load_event_trigger(id, insecure=False, session=None): - return _get_db_object_by_id(models.EventTrigger, id, insecure) +def load_event_trigger(id, insecure=False, session=None, fields=()): + return _get_db_object_by_id(models.EventTrigger, id, insecure, + columns=fields) @b.session_aware() diff --git a/mistral/tests/unit/api/v2/test_action_executions.py b/mistral/tests/unit/api/v2/test_action_executions.py index a069b52ad..8812625b8 100644 --- a/mistral/tests/unit/api/v2/test_action_executions.py +++ b/mistral/tests/unit/api/v2/test_action_executions.py @@ -226,9 +226,22 @@ class TestActionExecutionsController(base.APITest): @mock.patch.object(db_api, 'get_action_execution', MOCK_ACTION) def test_get(self): resp = self.app.get('/v2/action_executions/123') + action_exec = copy.deepcopy(ACTION_EX) + del action_exec['task_name'] + self.assertEqual(200, resp.status_int) + self.assertDictEqual(action_exec, resp.json) + + @mock.patch('mistral.db.v2.api.get_action_execution') + def test_get_with_fields_filter(self, mocked_get): + mocked_get.return_value = (ACTION_EX['id'], ACTION_EX['name'],) + resp = self.app.get('/v2/action_executions/123?fields=name') + expected = { + 'id': ACTION_EX['id'], + 'name': ACTION_EX['name'], + } self.assertEqual(200, resp.status_int) - self.assertDictEqual(ACTION_EX, resp.json) + self.assertDictEqual(expected, resp.json) @mock.patch.object(db_api, 'get_action_execution') def test_get_operational_error(self, mocked_get): @@ -239,9 +252,10 @@ class TestActionExecutionsController(base.APITest): ] resp = self.app.get('/v2/action_executions/123') - + action_exec = copy.deepcopy(ACTION_EX) + del action_exec['task_name'] self.assertEqual(200, resp.status_int) - self.assertDictEqual(ACTION_EX, resp.json) + self.assertDictEqual(action_exec, resp.json) def test_basic_get(self): resp = self.app.get('/v2/action_executions/') @@ -259,7 +273,7 @@ class TestActionExecutionsController(base.APITest): resp = self.app.get('/v2/action_executions/123') self.assertEqual(200, resp.status_int) - self.assertTrue('project_id' in resp.json) + self.assertIn('project_id', resp.json) @mock.patch.object(oslo_client.OsloRPCClient, 'sync_call', mock.MagicMock(side_effect=oslo_exc.MessagingTimeout)) @@ -561,11 +575,12 @@ class TestActionExecutionsController(base.APITest): @mock.patch.object(db_api, 'get_action_executions', MOCK_ACTIONS) def test_get_all(self): resp = self.app.get('/v2/action_executions') - + action_exec = copy.deepcopy(ACTION_EX) + del action_exec['task_name'] self.assertEqual(200, resp.status_int) self.assertEqual(1, len(resp.json['action_executions'])) - self.assertDictEqual(ACTION_EX, resp.json['action_executions'][0]) + self.assertDictEqual(action_exec, resp.json['action_executions'][0]) @mock.patch.object(db_api, 'get_action_executions') def test_get_all_operational_error(self, mocked_get_all): @@ -576,11 +591,13 @@ class TestActionExecutionsController(base.APITest): ] resp = self.app.get('/v2/action_executions') + action_exec = copy.deepcopy(ACTION_EX) + del action_exec['task_name'] self.assertEqual(200, resp.status_int) self.assertEqual(1, len(resp.json['action_executions'])) - self.assertDictEqual(ACTION_EX, resp.json['action_executions'][0]) + self.assertDictEqual(action_exec, resp.json['action_executions'][0]) @mock.patch.object(rest_utils, 'get_all', return_value=resources.ActionExecutions()) diff --git a/mistral/tests/unit/api/v2/test_cron_triggers.py b/mistral/tests/unit/api/v2/test_cron_triggers.py index 1053e0008..f824e59b4 100644 --- a/mistral/tests/unit/api/v2/test_cron_triggers.py +++ b/mistral/tests/unit/api/v2/test_cron_triggers.py @@ -79,6 +79,18 @@ class TestCronTriggerController(base.APITest): self.assertEqual(200, resp.status_int) self.assertDictEqual(TRIGGER, resp.json) + @mock.patch('mistral.db.v2.api.get_cron_trigger') + def test_get_with_fields_filter(self, mocked_get): + mocked_get.return_value = (TRIGGER['id'], TRIGGER['name'],) + resp = self.app.get('/v2/cron_triggers/my_cron_trigger?fields=name') + expected = { + 'id': TRIGGER['id'], + 'name': TRIGGER['name'], + } + + self.assertEqual(200, resp.status_int) + self.assertDictEqual(expected, resp.json) + @mock.patch.object(db_api, 'get_cron_trigger') def test_get_operational_error(self, mocked_get): mocked_get.side_effect = [ diff --git a/mistral/tests/unit/api/v2/test_environment.py b/mistral/tests/unit/api/v2/test_environment.py index 425082065..76e3c5de2 100644 --- a/mistral/tests/unit/api/v2/test_environment.py +++ b/mistral/tests/unit/api/v2/test_environment.py @@ -188,6 +188,20 @@ class TestEnvironmentController(base.APITest): self.assertEqual(200, resp.status_int) self._assert_dict_equal(ENVIRONMENT, resp.json) + @mock.patch('mistral.db.v2.api.get_environment') + def test_get_with_fields_filter(self, mocked_get): + mocked_get.return_value = ( + ENVIRONMENT['id'], ENVIRONMENT['name'], + ) + resp = self.app.get('/v2/environments/123?fields=name') + expected = { + 'id': ENVIRONMENT['id'], + 'name': ENVIRONMENT['name'], + } + + self.assertEqual(200, resp.status_int) + self.assertDictEqual(expected, resp.json) + @mock.patch.object(db_api, 'get_environment') def test_get_operational_error(self, mocked_get): mocked_get.side_effect = [ diff --git a/mistral/tests/unit/api/v2/test_event_trigger.py b/mistral/tests/unit/api/v2/test_event_trigger.py index 12e1e7e65..624cb0b5d 100644 --- a/mistral/tests/unit/api/v2/test_event_trigger.py +++ b/mistral/tests/unit/api/v2/test_event_trigger.py @@ -77,6 +77,21 @@ class TestEventTriggerController(base.APITest): self.assertEqual(200, resp.status_int) self.assertDictEqual(TRIGGER, resp.json) + @mock.patch('mistral.db.v2.api.get_event_trigger') + def test_get_with_fields_filter(self, mocked_get): + mocked_get.return_value = (TRIGGER['id'], TRIGGER['name'],) + resp = self.app.get( + '/v2/event_triggers/09cc56a9-d15e-4494-a6e2-c4ec8bdaacae' + '?fields=name' + ) + expected = { + 'id': TRIGGER['id'], + 'name': TRIGGER['name'], + } + + self.assertEqual(200, resp.status_int) + self.assertDictEqual(expected, resp.json) + @mock.patch.object(db_api, 'get_event_trigger') def test_get_operational_error(self, mocked_get): mocked_get.side_effect = [ diff --git a/mistral/tests/unit/api/v2/test_executions.py b/mistral/tests/unit/api/v2/test_executions.py index a3b1b787f..908d19af0 100644 --- a/mistral/tests/unit/api/v2/test_executions.py +++ b/mistral/tests/unit/api/v2/test_executions.py @@ -169,6 +169,20 @@ class TestExecutionsController(base.APITest): self.assertDictEqual(expected, resp.json) + @mock.patch('mistral.db.v2.api.get_workflow_execution') + def test_get_with_fields_filter(self, mocked_get): + mocked_get.return_value = ( + WF_EX_JSON_WITH_DESC['id'], WF_EX_JSON_WITH_DESC['description'], + ) + resp = self.app.get('/v2/executions/123?fields=description') + expected = { + 'id': WF_EX_JSON_WITH_DESC['id'], + 'description': WF_EX_JSON_WITH_DESC['description'], + } + + self.assertEqual(200, resp.status_int) + self.assertDictEqual(expected, resp.json) + @mock.patch.object(db_api, 'get_workflow_execution') def test_get_operational_error(self, mocked_get): mocked_get.side_effect = [ @@ -564,7 +578,8 @@ class TestExecutionsController(base.APITest): self.assertEqual(201, resp.status_int) self.assertDictEqual(expected_json, resp.json) - load_wf_ex_func.assert_called_once_with(expected_json['id']) + load_wf_ex_func.assert_called_once_with(expected_json['id'], + fields=()) kwargs = json.loads(expected_json['params']) kwargs['description'] = expected_json['description'] @@ -600,7 +615,8 @@ class TestExecutionsController(base.APITest): self.assertEqual(201, resp.status_int) self.assertDictEqual(expected_json, resp.json) - load_wf_ex_func.assert_called_once_with(expected_json['id']) + load_wf_ex_func.assert_called_once_with(expected_json['id'], + fields=()) # Note that "start_workflow" method on engine API should not be called # in this case because we passed execution ID to the endpoint and the diff --git a/mistral/tests/unit/api/v2/test_tasks.py b/mistral/tests/unit/api/v2/test_tasks.py index 0835f8776..ce3ace892 100644 --- a/mistral/tests/unit/api/v2/test_tasks.py +++ b/mistral/tests/unit/api/v2/test_tasks.py @@ -171,6 +171,18 @@ class TestTasksController(base.APITest): self.assertEqual(200, resp.status_int) self.assertDictEqual(TASK, resp.json) + @mock.patch('mistral.db.v2.api.get_task_execution') + def test_get_with_fields_filter(self, mocked_get): + mocked_get.return_value = TASK_EX + resp = self.app.get('/v2/tasks/123?fields=name') + expected = { + 'id': TASK['id'], + 'name': TASK['name'], + } + + self.assertEqual(200, resp.status_int) + self.assertDictEqual(expected, resp.json) + @mock.patch.object(db_api, 'get_task_execution') def test_get_operational_error(self, mocked_get): mocked_get.side_effect = [ diff --git a/mistral/tests/unit/api/v2/test_workbooks.py b/mistral/tests/unit/api/v2/test_workbooks.py index 8ce1822ab..ed550affb 100644 --- a/mistral/tests/unit/api/v2/test_workbooks.py +++ b/mistral/tests/unit/api/v2/test_workbooks.py @@ -169,6 +169,18 @@ class TestWorkbooksController(base.APITest): self.assertEqual(200, resp.status_int) self.assertDictEqual(WORKBOOK, resp.json) + @mock.patch('mistral.db.v2.api.get_workbook') + def test_get_with_fields_filter(self, mocked_get): + mocked_get.return_value = (WORKBOOK['id'], WORKBOOK['name'],) + resp = self.app.get('/v2/workbooks/123?fields=name') + expected = { + 'id': WORKBOOK['id'], + 'name': WORKBOOK['name'], + } + + self.assertEqual(200, resp.status_int) + self.assertDictEqual(expected, resp.json) + @mock.patch.object(db_api, "get_workbook", MOCK_WB_WITH_NAMESPACE) def test_get_with_namespace(self): resp = self.app.get('/v2/workbooks/123?namespace=xyz') diff --git a/mistral/tests/unit/api/v2/test_workflows.py b/mistral/tests/unit/api/v2/test_workflows.py index 38e641d33..0d5f824eb 100644 --- a/mistral/tests/unit/api/v2/test_workflows.py +++ b/mistral/tests/unit/api/v2/test_workflows.py @@ -264,6 +264,18 @@ class TestWorkflowsController(base.APITest): self.assertEqual(200, resp.status_int) self.assertDictEqual(WF, resp_json) + @mock.patch('mistral.db.v2.api.get_workflow_definition') + def test_get_with_fields_filter(self, mocked_get): + mocked_get.return_value = (WF['id'], WF['name'],) + resp = self.app.get('/v2/workflows/123?fields=name') + expected = { + 'id': WF['id'], + 'name': WF['name'], + } + + self.assertEqual(200, resp.status_int) + self.assertDictEqual(expected, resp.json) + @mock.patch.object(db_api, 'get_workflow_definition') def test_get_operational_error(self, mocked_get): mocked_get.side_effect = [ @@ -886,4 +898,4 @@ class TestWorkflowsController(base.APITest): resp = self.app.get( '/v2/workflows/123e4567-e89b-12d3-a456-426655440000') self.assertEqual(200, resp.status_int) - self.assertTrue('project_id' in resp.json) + self.assertIn('project_id', resp.json) diff --git a/mistral/utils/rest_utils.py b/mistral/utils/rest_utils.py index 6cae86b7c..8ef4a02cd 100644 --- a/mistral/utils/rest_utils.py +++ b/mistral/utils/rest_utils.py @@ -121,6 +121,14 @@ def validate_fields(fields, object_fields): ) +def fields_list_to_cls_fields_tuple(model, f): + if not f: + return () + return tuple( + [getattr(model, str(field)) for field in f] + ) + + def filters_to_dict(**kwargs): """Return only non-null values @@ -157,7 +165,7 @@ def get_all(list_cls, cls, get_all_function, get_function, Default: ['asc']. :param fields: Optional. A specified list of fields of the resource to be returned. 'id' will be included automatically in - fields if it's provided, since it will be used when + fields if it's not provided, since it will be used when constructing 'next' link. :param filters: Optional. A specified dictionary of filters to match. :param all_projects: Optional. Get resources of all projects.