From c4e903706ccfc018267c156dc1618d7e53079a8a Mon Sep 17 00:00:00 2001 From: Jean-Baptiste Barth Date: Wed, 30 Sep 2015 19:26:42 +0200 Subject: [PATCH] Add SWF endpoints: RegisterWorkflowType, DeprecateWorkflowType, ListWorkflowTypes, DescribeWorkflowType --- moto/swf/exceptions.py | 8 +- moto/swf/models.py | 110 ++++++++++++++--- moto/swf/responses.py | 129 +++++++++++++++----- tests/test_swf/test_workflow_types.py | 162 ++++++++++++++++++++++++++ 4 files changed, 364 insertions(+), 45 deletions(-) create mode 100644 tests/test_swf/test_workflow_types.py diff --git a/moto/swf/exceptions.py b/moto/swf/exceptions.py index 61066d294..5510f1a88 100644 --- a/moto/swf/exceptions.py +++ b/moto/swf/exceptions.py @@ -44,15 +44,15 @@ class SWFSerializationException(JSONResponseError): class SWFTypeAlreadyExistsFault(SWFClientError): - def __init__(self, name, version): + def __init__(self, _type): super(SWFTypeAlreadyExistsFault, self).__init__( - "ActivityType=[name={}, version={}]".format(name, version), + "{}=[name={}, version={}]".format(_type.__class__.__name__, _type.name, _type.version), "com.amazonaws.swf.base.model#TypeAlreadyExistsFault") class SWFTypeDeprecatedFault(SWFClientError): - def __init__(self, name, version): + def __init__(self, _type): super(SWFTypeDeprecatedFault, self).__init__( - "ActivityType=[name={}, version={}]".format(name, version), + "{}=[name={}, version={}]".format(_type.__class__.__name__, _type.name, _type.version), "com.amazonaws.swf.base.model#TypeDeprecatedFault") diff --git a/moto/swf/models.py b/moto/swf/models.py index 2a2980177..95ee5e82a 100644 --- a/moto/swf/models.py +++ b/moto/swf/models.py @@ -21,6 +21,7 @@ class Domain(object): self.description = description self.status = "REGISTERED" self.activity_types = defaultdict(dict) + self.workflow_types = defaultdict(dict) def __repr__(self): return "Domain(name: %(name)s, status: %(status)s)" % self.__dict__ @@ -35,15 +36,39 @@ class Domain(object): "ActivityType=[name={}, version={}]".format(name, version) ) - def add_activity_type(self, actype): - self.activity_types[actype.name][actype.version] = actype + def add_activity_type(self, _type): + self.activity_types[_type.name][_type.version] = _type def find_activity_types(self, status): _all = [] for _, family in self.activity_types.iteritems(): - for _, actype in family.iteritems(): - if actype.status == status: - _all.append(actype) + for _, _type in family.iteritems(): + if _type.status == status: + _all.append(_type) + return _all + + # TODO: refactor it with get_activity_type() + def get_workflow_type(self, name, version, ignore_empty=False): + try: + return self.workflow_types[name][version] + except KeyError: + if not ignore_empty: + raise SWFUnknownResourceFault( + "type", + "WorkflowType=[name={}, version={}]".format(name, version) + ) + + # TODO: refactor it with add_activity_type() + def add_workflow_type(self, _type): + self.workflow_types[_type.name][_type.version] = _type + + # TODO: refactor it with find_activity_types() + def find_workflow_types(self, status): + _all = [] + for _, family in self.workflow_types.iteritems(): + for _, _type in family.iteritems(): + if _type.status == status: + _all.append(_type) return _all @@ -59,6 +84,18 @@ class ActivityType(object): return "ActivityType(name: %(name)s, version: %(version)s)" % self.__dict__ +class WorkflowType(object): + def __init__(self, name, version, **kwargs): + self.name = name + self.version = version + self.status = "REGISTERED" + for key, value in kwargs.iteritems(): + self.__setattr__(key, value) + + def __repr__(self): + return "WorkflowType(name: %(name)s, version: %(version)s)" % self.__dict__ + + class SWFBackend(BaseBackend): def __init__(self, region_name): self.region_name = region_name @@ -118,11 +155,11 @@ class SWFBackend(BaseBackend): self._check_string(domain_name) self._check_string(status) domain = self._get_domain(domain_name) - actypes = domain.find_activity_types(status) - actypes = sorted(actypes, key=lambda domain: domain.name) + _types = domain.find_activity_types(status) + _types = sorted(_types, key=lambda domain: domain.name) if reverse_order: - actypes = reversed(actypes) - return actypes + _types = reversed(_types) + return _types def register_activity_type(self, domain_name, name, version, **kwargs): self._check_string(domain_name) @@ -134,8 +171,9 @@ class SWFBackend(BaseBackend): if value is not None: self._check_string(value) domain = self._get_domain(domain_name) - if domain.get_activity_type(name, version, ignore_empty=True): - raise SWFTypeAlreadyExistsFault(name, version) + _type = domain.get_activity_type(name, version, ignore_empty=True) + if _type: + raise SWFTypeAlreadyExistsFault(_type) activity_type = ActivityType(name, version, **kwargs) domain.add_activity_type(activity_type) @@ -144,10 +182,10 @@ class SWFBackend(BaseBackend): self._check_string(name) self._check_string(version) domain = self._get_domain(domain_name) - actype = domain.get_activity_type(name, version) - if actype.status == "DEPRECATED": - raise SWFTypeDeprecatedFault(name, version) - actype.status = "DEPRECATED" + _type = domain.get_activity_type(name, version) + if _type.status == "DEPRECATED": + raise SWFTypeDeprecatedFault(_type) + _type.status = "DEPRECATED" def describe_activity_type(self, domain_name, name, version): self._check_string(domain_name) @@ -156,6 +194,48 @@ class SWFBackend(BaseBackend): domain = self._get_domain(domain_name) return domain.get_activity_type(name, version) + def list_workflow_types(self, domain_name, status, reverse_order=None): + self._check_string(domain_name) + self._check_string(status) + domain = self._get_domain(domain_name) + _types = domain.find_workflow_types(status) + _types = sorted(_types, key=lambda domain: domain.name) + if reverse_order: + _types = reversed(_types) + return _types + + def register_workflow_type(self, domain_name, name, version, **kwargs): + self._check_string(domain_name) + self._check_string(name) + self._check_string(version) + for _, value in kwargs.iteritems(): + if value == (None,): + print _ + if value is not None: + self._check_string(value) + domain = self._get_domain(domain_name) + _type = domain.get_workflow_type(name, version, ignore_empty=True) + if _type: + raise SWFTypeAlreadyExistsFault(_type) + workflow_type = WorkflowType(name, version, **kwargs) + domain.add_workflow_type(workflow_type) + + def deprecate_workflow_type(self, domain_name, name, version): + self._check_string(domain_name) + self._check_string(name) + self._check_string(version) + domain = self._get_domain(domain_name) + _type = domain.get_workflow_type(name, version) + if _type.status == "DEPRECATED": + raise SWFTypeDeprecatedFault(_type) + _type.status = "DEPRECATED" + + def describe_workflow_type(self, domain_name, name, version): + self._check_string(domain_name) + self._check_string(name) + self._check_string(version) + domain = self._get_domain(domain_name) + return domain.get_workflow_type(name, version) swf_backends = {} for region in boto.swf.regions(): diff --git a/moto/swf/responses.py b/moto/swf/responses.py index f27d34211..1f00d5646 100644 --- a/moto/swf/responses.py +++ b/moto/swf/responses.py @@ -85,9 +85,9 @@ class SWFResponse(BaseResponse): domain_name = self._params.get("domain") status = self._params.get("registrationStatus") reverse_order = self._params.get("reverseOrder", None) - actypes = self.swf_backend.list_activity_types(domain_name, status, reverse_order=reverse_order) + types = self.swf_backend.list_activity_types(domain_name, status, reverse_order=reverse_order) template = self.response_template(LIST_ACTIVITY_TYPES_TEMPLATE) - return template.render(actypes=actypes) + return template.render(types=types) def register_activity_type(self): domain = self._params.get("domain") @@ -117,22 +117,78 @@ class SWFResponse(BaseResponse): def deprecate_activity_type(self): domain = self._params.get("domain") - actype = self._params.get("activityType") - name = actype["name"] - version = actype["version"] + _type = self._params.get("activityType") + name = _type["name"] + version = _type["version"] domain = self.swf_backend.deprecate_activity_type(domain, name, version) template = self.response_template("") return template.render() def describe_activity_type(self): domain = self._params.get("domain") - actype = self._params.get("activityType") + _type = self._params.get("activityType") - name = actype["name"] - version = actype["version"] - actype = self.swf_backend.describe_activity_type(domain, name, version) + name = _type["name"] + version = _type["version"] + _type = self.swf_backend.describe_activity_type(domain, name, version) template = self.response_template(DESCRIBE_ACTIVITY_TYPE_TEMPLATE) - return template.render(actype=actype) + return template.render(_type=_type) + + # TODO: implement pagination + # TODO: refactor with list_activity_types() + def list_workflow_types(self): + domain_name = self._params.get("domain") + status = self._params.get("registrationStatus") + reverse_order = self._params.get("reverseOrder", None) + types = self.swf_backend.list_workflow_types(domain_name, status, reverse_order=reverse_order) + template = self.response_template(LIST_WORKFLOW_TYPES_TEMPLATE) + return template.render(types=types) + + def register_workflow_type(self): + domain = self._params.get("domain") + name = self._params.get("name") + version = self._params.get("version") + default_task_list = self._params.get("defaultTaskList") + if default_task_list: + task_list = default_task_list.get("name") + else: + task_list = None + default_child_policy = self._params.get("defaultChildPolicy") + default_task_start_to_close_timeout = self._params.get("defaultTaskStartToCloseTimeout") + default_execution_start_to_close_timeout = self._params.get("defaultTaskExecutionStartToCloseTimeout") + description = self._params.get("description") + # TODO: add defaultTaskPriority when boto gets to support it + # TODO: add defaultLambdaRole when boto gets to support it + workflow_type = self.swf_backend.register_workflow_type( + domain, name, version, task_list=task_list, + default_child_policy=default_child_policy, + default_task_start_to_close_timeout=default_task_start_to_close_timeout, + default_execution_start_to_close_timeout=default_execution_start_to_close_timeout, + description=description, + ) + template = self.response_template("") + return template.render() + + # TODO: refactor with deprecate_activity_type() + def deprecate_workflow_type(self): + domain = self._params.get("domain") + _type = self._params.get("workflowType") + name = _type["name"] + version = _type["version"] + domain = self.swf_backend.deprecate_workflow_type(domain, name, version) + template = self.response_template("") + return template.render() + + # TODO: refactor with describe_activity_type() + def describe_workflow_type(self): + domain = self._params.get("domain") + _type = self._params.get("workflowType") + + name = _type["name"] + version = _type["version"] + _type = self.swf_backend.describe_workflow_type(domain, name, version) + template = self.response_template(DESCRIBE_WORKFLOW_TYPE_TEMPLATE) + return template.render(_type=_type) LIST_DOMAINS_TEMPLATE = """{ @@ -160,16 +216,16 @@ DESCRIBE_DOMAIN_TEMPLATE = """{ LIST_ACTIVITY_TYPES_TEMPLATE = """{ "typeInfos": [ - {%- for actype in actypes %} + {%- for _type in types %} { "activityType": { - "name": "{{ actype.name }}", - "version": "{{ actype.version }}" + "name": "{{ _type.name }}", + "version": "{{ _type.version }}" }, "creationDate": 1420066800, - {% if actype.status == "DEPRECATED" %}"deprecationDate": 1422745200,{% endif %} - {% if actype.description %}"description": "{{ actype.description }}",{% endif %} - "status": "{{ actype.status }}" + {% if _type.status == "DEPRECATED" %}"deprecationDate": 1422745200,{% endif %} + {% if _type.description %}"description": "{{ _type.description }}",{% endif %} + "status": "{{ _type.status }}" }{% if not loop.last %},{% endif %} {%- endfor %} ] @@ -177,22 +233,43 @@ LIST_ACTIVITY_TYPES_TEMPLATE = """{ DESCRIBE_ACTIVITY_TYPE_TEMPLATE = """{ "configuration": { - {% if actype.default_task_heartbeat_timeout %}"defaultTaskHeartbeatTimeout": "{{ actype.default_task_heartbeat_timeout }}",{% endif %} - {% if actype.task_list %}"defaultTaskList": { "name": "{{ actype.task_list }}" },{% endif %} - {% if actype.default_task_schedule_to_close_timeout %}"defaultTaskScheduleToCloseTimeout": "{{ actype.default_task_schedule_to_close_timeout }}",{% endif %} - {% if actype.default_task_schedule_to_start_timeout %}"defaultTaskScheduleToStartTimeout": "{{ actype.default_task_schedule_to_start_timeout }}",{% endif %} - {% if actype.default_task_start_to_close_timeout %}"defaultTaskStartToCloseTimeout": "{{ actype.default_task_start_to_close_timeout }}",{% endif %} + {% if _type.default_task_heartbeat_timeout %}"defaultTaskHeartbeatTimeout": "{{ _type.default_task_heartbeat_timeout }}",{% endif %} + {% if _type.task_list %}"defaultTaskList": { "name": "{{ _type.task_list }}" },{% endif %} + {% if _type.default_task_schedule_to_close_timeout %}"defaultTaskScheduleToCloseTimeout": "{{ _type.default_task_schedule_to_close_timeout }}",{% endif %} + {% if _type.default_task_schedule_to_start_timeout %}"defaultTaskScheduleToStartTimeout": "{{ _type.default_task_schedule_to_start_timeout }}",{% endif %} + {% if _type.default_task_start_to_close_timeout %}"defaultTaskStartToCloseTimeout": "{{ _type.default_task_start_to_close_timeout }}",{% endif %} "__moto_placeholder": "(avoid dealing with coma in json)" }, "typeInfo": { "activityType": { - "name": "{{ actype.name }}", - "version": "{{ actype.version }}" + "name": "{{ _type.name }}", + "version": "{{ _type.version }}" }, "creationDate": 1420066800, - {% if actype.status == "DEPRECATED" %}"deprecationDate": 1422745200,{% endif %} - {% if actype.description %}"description": "{{ actype.description }}",{% endif %} - "status": "{{ actype.status }}" + {% if _type.status == "DEPRECATED" %}"deprecationDate": 1422745200,{% endif %} + {% if _type.description %}"description": "{{ _type.description }}",{% endif %} + "status": "{{ _type.status }}" } }""" +LIST_WORKFLOW_TYPES_TEMPLATE = LIST_ACTIVITY_TYPES_TEMPLATE.replace("activityType", "workflowType") + +DESCRIBE_WORKFLOW_TYPE_TEMPLATE = """{ + "configuration": { + {% if _type.default_child_policy %}"defaultChildPolicy": "{{ _type.default_child_policy }}",{% endif %} + {% if _type.default_execution_start_to_close_timeout %}"defaultExecutionStartToCloseTimeout": "{{ _type.default_execution_start_to_close_timeout }}",{% endif %} + {% if _type.task_list %}"defaultTaskList": { "name": "{{ _type.task_list }}" },{% endif %} + {% if _type.default_task_start_to_close_timeout %}"defaultTaskStartToCloseTimeout": "{{ _type.default_task_start_to_close_timeout }}",{% endif %} + "__moto_placeholder": "(avoid dealing with coma in json)" + }, + "typeInfo": { + "workflowType": { + "name": "{{ _type.name }}", + "version": "{{ _type.version }}" + }, + "creationDate": 1420066800, + {% if _type.status == "DEPRECATED" %}"deprecationDate": 1422745200,{% endif %} + {% if _type.description %}"description": "{{ _type.description }}",{% endif %} + "status": "{{ _type.status }}" + } +}""" diff --git a/tests/test_swf/test_workflow_types.py b/tests/test_swf/test_workflow_types.py new file mode 100644 index 000000000..67424710f --- /dev/null +++ b/tests/test_swf/test_workflow_types.py @@ -0,0 +1,162 @@ +import boto +from nose.tools import assert_raises +from sure import expect + +from moto import mock_swf +from moto.swf.exceptions import ( + SWFUnknownResourceFault, + SWFTypeAlreadyExistsFault, + SWFTypeDeprecatedFault, + SWFSerializationException, +) + + +# RegisterWorkflowType endpoint +@mock_swf +def test_register_workflow_type(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + conn.register_workflow_type("test-domain", "test-workflow", "v1.0") + + types = conn.list_workflow_types("test-domain", "REGISTERED") + actype = types["typeInfos"][0] + actype["workflowType"]["name"].should.equal("test-workflow") + actype["workflowType"]["version"].should.equal("v1.0") + +@mock_swf +def test_register_already_existing_workflow_type(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + conn.register_workflow_type("test-domain", "test-workflow", "v1.0") + + with assert_raises(SWFTypeAlreadyExistsFault) as err: + conn.register_workflow_type("test-domain", "test-workflow", "v1.0") + + ex = err.exception + ex.status.should.equal(400) + ex.error_code.should.equal("TypeAlreadyExistsFault") + ex.body.should.equal({ + "__type": "com.amazonaws.swf.base.model#TypeAlreadyExistsFault", + "message": "WorkflowType=[name=test-workflow, version=v1.0]" + }) + +@mock_swf +def test_register_with_wrong_parameter_type(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + + with assert_raises(SWFSerializationException) as err: + conn.register_workflow_type("test-domain", "test-workflow", 12) + + ex = err.exception + ex.status.should.equal(400) + ex.error_code.should.equal("SerializationException") + ex.body["__type"].should.equal("com.amazonaws.swf.base.model#SerializationException") + + +# ListWorkflowTypes endpoint +@mock_swf +def test_list_workflow_types(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + conn.register_workflow_type("test-domain", "b-test-workflow", "v1.0") + conn.register_workflow_type("test-domain", "a-test-workflow", "v1.0") + conn.register_workflow_type("test-domain", "c-test-workflow", "v1.0") + + all_workflow_types = conn.list_workflow_types("test-domain", "REGISTERED") + names = [activity_type["workflowType"]["name"] for activity_type in all_workflow_types["typeInfos"]] + names.should.equal(["a-test-workflow", "b-test-workflow", "c-test-workflow"]) + +@mock_swf +def test_list_workflow_types_reverse_order(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + conn.register_workflow_type("test-domain", "b-test-workflow", "v1.0") + conn.register_workflow_type("test-domain", "a-test-workflow", "v1.0") + conn.register_workflow_type("test-domain", "c-test-workflow", "v1.0") + + all_workflow_types = conn.list_workflow_types("test-domain", "REGISTERED", + reverse_order=True) + names = [activity_type["workflowType"]["name"] for activity_type in all_workflow_types["typeInfos"]] + names.should.equal(["c-test-workflow", "b-test-workflow", "a-test-workflow"]) + + +# DeprecateWorkflowType endpoint +@mock_swf +def test_deprecate_workflow_type(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + conn.register_workflow_type("test-domain", "test-workflow", "v1.0") + conn.deprecate_workflow_type("test-domain", "test-workflow", "v1.0") + + actypes = conn.list_workflow_types("test-domain", "DEPRECATED") + actype = actypes["typeInfos"][0] + actype["workflowType"]["name"].should.equal("test-workflow") + actype["workflowType"]["version"].should.equal("v1.0") + +@mock_swf +def test_deprecate_already_deprecated_workflow_type(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + conn.register_workflow_type("test-domain", "test-workflow", "v1.0") + conn.deprecate_workflow_type("test-domain", "test-workflow", "v1.0") + + with assert_raises(SWFTypeDeprecatedFault) as err: + conn.deprecate_workflow_type("test-domain", "test-workflow", "v1.0") + + ex = err.exception + ex.status.should.equal(400) + ex.error_code.should.equal("TypeDeprecatedFault") + ex.body.should.equal({ + "__type": "com.amazonaws.swf.base.model#TypeDeprecatedFault", + "message": "WorkflowType=[name=test-workflow, version=v1.0]" + }) + +@mock_swf +def test_deprecate_non_existent_workflow_type(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + + with assert_raises(SWFUnknownResourceFault) as err: + conn.deprecate_workflow_type("test-domain", "non-existent", "v1.0") + + ex = err.exception + ex.status.should.equal(400) + ex.error_code.should.equal("UnknownResourceFault") + ex.body.should.equal({ + "__type": "com.amazonaws.swf.base.model#UnknownResourceFault", + "message": "Unknown type: WorkflowType=[name=non-existent, version=v1.0]" + }) + +# DescribeWorkflowType endpoint +@mock_swf +def test_describe_workflow_type(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + conn.register_workflow_type("test-domain", "test-workflow", "v1.0", + task_list="foo", default_child_policy="TERMINATE") + + actype = conn.describe_workflow_type("test-domain", "test-workflow", "v1.0") + actype["configuration"]["defaultTaskList"]["name"].should.equal("foo") + actype["configuration"]["defaultChildPolicy"].should.equal("TERMINATE") + actype["configuration"].keys().should_not.contain("defaultTaskStartToCloseTimeout") + infos = actype["typeInfo"] + infos["workflowType"]["name"].should.equal("test-workflow") + infos["workflowType"]["version"].should.equal("v1.0") + infos["status"].should.equal("REGISTERED") + +@mock_swf +def test_describe_non_existent_workflow_type(): + conn = boto.connect_swf("the_key", "the_secret") + conn.register_domain("test-domain", "60") + + with assert_raises(SWFUnknownResourceFault) as err: + conn.describe_workflow_type("test-domain", "non-existent", "v1.0") + + ex = err.exception + ex.status.should.equal(400) + ex.error_code.should.equal("UnknownResourceFault") + ex.body.should.equal({ + "__type": "com.amazonaws.swf.base.model#UnknownResourceFault", + "message": "Unknown type: WorkflowType=[name=non-existent, version=v1.0]" + })