diff --git a/.travis.yml b/.travis.yml index a7803a70da..250dcb8c18 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,15 +1,23 @@ language: python python: - - "2.6" - - "2.7" - - "3.2" - - "3.3" - - "3.4" + - '2.6' + - '2.7' + - '3.2' + - '3.3' + - '3.4' install: - pip install . --use-mirrors - pip install -r requirements.txt --use-mirrors - pip install -r tests/requirements.txt --use-mirrors -script: +script: - flake8 --ignore=F401 twilio - flake8 --ignore=E123,E126,E128,E501 tests - nosetests +sudo: false +notifications: + slack: + on_success: change + on_failure: change + rooms: + - secure: TcDlBcKXtqmMdVsa3Lsofdqc1uVjqhZouwNETC260rByRb74gTHGZ1Da7PRkv+AZIFUq7S1uWTZXTXJTm154hi4aQb9SE2UowVwTJMjIKyy4P79s1eoyADP92cFEcpqSs4iVpU3t5srTj8cg2fVks8EsV5pDVJut1oBH4qYJEZw= + - secure: qqtcwaS0y5e2SVm5g/zSRMgo7FcZ8Oa44bxQUDvJh/84/DHMD3zZoAv/A4+Vlbs0tCjnSKxMDuLxTzpiPgru4KPH7yj4fEXf7+sfwiLD//WBVWpGMYLa+PNCGS6hhnAuFkA2psZCmmzkbJbX0N03EdWiWFzV79WPgNI+WzpYIVQ= diff --git a/CHANGES.md b/CHANGES.md index 11b8e7dd71..94e2225e03 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -3,6 +3,25 @@ twilio-python Changelog Here you can see the full list of changes between each twilio-python release. +Version 4.6.0 +------------- + +Released September 23, 2015: + +- Allow fetching TaskRouter reservations by Worker +- Add missing Enqueue->Task TwiML generation +- Add Worflow construction + +Version 4.5.0 +------------- + +Released August 11, 2015: + +- Add support for new Taskrouter JWT Functionality, JWTs now grant access to + - Workspace + - Worker + - TaskQueue + Version 4.4.0 ------------- diff --git a/Makefile b/Makefile index 9f51e8826a..68d3c452e8 100644 --- a/Makefile +++ b/Makefile @@ -4,7 +4,7 @@ venv: virtualenv venv install: venv - . venv/bin/activate; pip install . --use-mirrors + . venv/bin/activate; pip install . test-install: install . venv/bin/activate; pip install -r tests/requirements.txt diff --git a/docs/appengine.rst b/docs/appengine.rst index 0cdf9db9c8..74a26a4fcd 100644 --- a/docs/appengine.rst +++ b/docs/appengine.rst @@ -102,7 +102,7 @@ The key is to lay out your project in a way that makes sense. the settings you want in Google App Engine - Note the folder path ends with ``twilio-demo/src``. - .. image:: https://www.evernote.com/shard/s265/sh/1b9407b0-c89b-464d-b352-dbf8fc7a7f41/f536b8e79747f43220fc12e0e0026ee2/res/5b2f83af-8a7f-451f-afba-db092c55aa44/skitch.png + .. image:: http://howtodocs.s3.amazonaws.com/helpers/appengine.png Once App Engine is running locally, in your browser, you should be able to navigate to ``http://localhost`` + the provided Port and view the twilio diff --git a/docs/usage/taskrouter.rst b/docs/usage/taskrouter.rst index 5915a4744e..76de468ed3 100644 --- a/docs/usage/taskrouter.rst +++ b/docs/usage/taskrouter.rst @@ -38,6 +38,71 @@ its unique ID. ) print workspace.sid +.. + +The following code will get an instance of an existing :class:`workspace` resource + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + workspace = client.workspaces.get(WORKSPACE_SID) + print workspace.friendly_name +.. + +The following code will get the list of all existing :class:`workspace` resources + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + for workspace in client.workspaces.list() + print workspace.friendly_name +.. + +The following code will create a update an existing :class:`Workspace` resource + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + workspace = client.workspaces.update( + WORKSPACE_SID, + friendly_name='Test Workspace', + event_callback_uri="http://www.example.com", + template='FIFO') + +.. +The following code will delete an existing :class:`workspace` resource + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + client.workspaces.delete(WORKSPACE_SID) +.. + + Workflows --------- @@ -87,13 +152,139 @@ unique ID: client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) - workspace = client.workflows(WORKSPACE_SID).create( + workflow = client.workflows(WORKSPACE_SID).create( friendly_name="Incoming Call Flow", assignment_callback_url="https://example.com/callback", fallback_assignment_callback_url="https://example.com/callback2", configuration=CONFIG ) - print workspace.sid + print workflow.sid + +.. + +The following code will get a instance of an existing :class:`workflow` resource + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + + workflow = client.workflows(WORKSPACE_SID).get(WORKFLOW_SID) + print workflow.friendly_name + +.. + + + +The following code will get a list of all existing :class:`workflow` resources + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + + for workflow in client.workflows(WORKSPACE_SID).list() + print workflow.friendly_name + +.. + +The following code will update an existing :class:`workflow` resource + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + # Some JSON to configure the Workflow. See the documentation at + # http://www.twilio.com/docs/taskrouter for more details. + CONFIG = """ + { + "task_routing":{ + "filters":[ + { + "friendly_name":"Gold Tickets", + "expression":"customer_value == 'Gold' AND type == 'ticket'", + "targets":[ + { + "task_queue_sid":"WQ0123456789abcdef0123456789abcdef", + "priority":"2" + } + ] + }, + { + "targets": [ + { + "queue": "WQ2acd4c1a41ffadce5d1bac9e1ce2fa9f", + "priority": "1" + } + ], + "friendly_name": "Marketing", + "expression": "type == 'marketing'" + } + ], + "default_filter":{ + "task_queue_sid":"WQabcdef01234567890123456789abcdef" + } + } + } + """ + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + + workflow = client.workflows(WORKSPACE_SID).update( + WORKFLOW_SID, + friendly_name="Incoming Call Flow", + assignment_callback_url="https://example.com/callback", + fallback_assignment_callback_url="https://example.com/callback2", + configuration=CONFIG + ) + print workflow.sid + +.. + +The following code will delete an existing :class:`Workflow` + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + + client.workflows(WORKSPACE_SID).delete( + WORKFLOW_SID + ) + + +.. Activities @@ -122,6 +313,87 @@ To create a new :class:`Activity`: available=False, # Whether workers are available to handle tasks during this activity ) +.. + +To get an existing :class:`activity` resource + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + activity = client.activities(WORKSPACE_SID).get(ACTIVITY_SID) + print activity.friendly_name + +.. + +To get a list of existing :class:`activity` resources + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + for activity in client.activities(WORKSPACE_SID).list() + print activity.friendly_name + +.. + +To update an existing :class:`Activity` + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + activity = client.activities(WORKSPACE_SID).update( + ACTIVITY_SID, + friendly_name="Coffee Break", + available=True, + ) + +.. + +To delete an existing :class:`Activity` + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + activity = client.activities(WORKSPACE_SID).delete( + ACTIVITY_SID + ) + +.. Workers ------- @@ -153,6 +425,92 @@ To create a new :class:`Worker`: ) print worker.sid +.. + +To get an existing :class:`worker` instance + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + worker = client.workers(WORKSPACE_SID).get(WORKER_SID) + print worker_friendly_name; +.. + + +To get an existing :class:`worker` list + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + for worker in client.workers(WORKSPACE_SID).list() + print worker_friendly_name; +.. + + +To update an existing :class:`Worker` + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + worker = client.workers(WORKSPACE_SID).update( + WORKER_SID, + friendly_name="Jamie Howe", + attributes="""{ + "phone": "+14155551234", + "languages": ["EN", "ES","DE"] + } + """ + ) + print worker.sid + +.. + +To delete an existing :class:`Worker` + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + client.workers(WORKSPACE_SID).delete( + WORKER_SID + ) + +.. TaskQueues ---------- @@ -186,6 +544,100 @@ To create a new :class:`TaskQueue`: ) print queue.sid +.. + +To get an existing :class`TaskQueue` instance + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + + queue = client.task_queues(WORKSPACE_SID).get(TASKQUEUE_SID) + print queue.sid + +.. + + + +To get an existing :class`TaskQueue` list + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + + for queue in client.task_queues(WORKSPACE_SID).list() + print queue.sid + +.. + + +To update an existing :class:`TaskQueue` + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + + queue = client.task_queues(WORKSPACE_SID).update( + TASKQUEUE_SID, + friendly_name="Sales+Pre-Sales", + # The Activity to assign workers when a task is reserved for them + reservation_activity_sid="WA11111111111", + # The Activity to assign workers when a task is assigned to them + assignment_activity_sid="WA222222222222", + ) + print queue.sid + +.. + +To delete an existing :class:`TaskQueue` + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + + queue = client.task_queues(WORKSPACE_SID).delete( + TASKQUEUE_SID + ) + print queue.sid + +.. + Tasks ----- @@ -223,3 +675,161 @@ To create a new :class:`Task` via the REST API: workflow_sid=WORKFLOW_SID ) print task.sid +.. + +To get an existing :class:`Task` instance + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + WORKFLOW_SID = "WWXXXXXXXXXXXXXX" + # Some JSON containing attributes for this task. User-defined. + TASK_ATTRIBUTES = """{ + "type": "call", + "contact": "+2014068777", + "customer-value": "gold", + "task-reason": "support", + "callSid": "CA42ed11..." + }""" + + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + task = client.tasks(WORKSPACE_SID).delete(TASK_SID) + print task.attributes +.. + + +To get an existing :class:`Task` list + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + WORKFLOW_SID = "WWXXXXXXXXXXXXXX" + # Some JSON containing attributes for this task. User-defined. + TASK_ATTRIBUTES = """{ + "type": "call", + "contact": "+2014068777", + "customer-value": "gold", + "task-reason": "support", + "callSid": "CA42ed11..." + }""" + + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + for task in client.tasks(WORKSPACE_SID).list() + print task.attributes +.. + +To update an existing :class:`Task` + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + WORKFLOW_SID = "WWXXXXXXXXXXXXXX" + # Some JSON containing attributes for this task. User-defined. + TASK_ATTRIBUTES = """{ + "type": "call", + "contact": "+2014068777", + "customer-value": "gold", + "task-reason": "support", + "callSid": "CA42ed11..." + }""" + + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + task = client.tasks(WORKSPACE_SID).update( + TASK_SID, + attributes=TASK_ATTRIBUTES, + assignment_status='pending', + workflow_sid=WORKFLOW_SID + ) + print task.sid +.. + +To delete an existing :class:`Task` + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + WORKFLOW_SID = "WWXXXXXXXXXXXXXX" + # Some JSON containing attributes for this task. User-defined. + TASK_ATTRIBUTES = """{ + "type": "call", + "contact": "+2014068777", + "customer-value": "gold", + "task-reason": "support", + "callSid": "CA42ed11..." + }""" + + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + client.tasks(WORKSPACE_SID).delete( + TASK_SID + ) + +.. + + +Using Workflow builder helper classes to create a :class:`Workflow` resource. + +.. code-block:: python + + from twilio.rest import TwilioTaskRouterClient + + # To find these visit https://www.twilio.com/user/account + ACCOUNT_SID = "ACXXXXXXXXXXXXXXXXX" + AUTH_TOKEN = "YYYYYYYYYYYYYYYYYY" + + # See previous examples to create a Workspace + WORKSPACE_SID = "WSZZZZZZZZZZZZZZ" + + rules = [ + WorkflowRule("1==1", [WorkflowRuleTarget("WQeae4fc2f4db7f377c5d3758fb08b79b7", "1==1", 1, 20)],"SomeQ"), + WorkflowRule("1==1", [WorkflowRuleTarget("WQ19ebe92fb33522f018b5a31d805d94da", "1==1", 1, 210)], "SomeOtherQ") + ] + default_target = WorkflowRuleTarget("WQ9963154bf3122d0a0558f3763951d916", "1==1", None, None) + config = WorkflowConfig(rules, default_target) + print config.to_json() + + client = TwilioTaskRouterClient(ACCOUNT_SID, AUTH_TOKEN) + + workflow = client.workflows(WORKSPACE_SID).create( + friendly_name= "Incoming Call Flow", + assignment_callback_url= "https://example.com/callback", + fallback_assignment_callback_url= "https://example.com/callback2", + configuration= config.to_json() + ) + + print workflow.sid + + + +.. \ No newline at end of file diff --git a/tests/task_router/test_capability.py b/tests/task_router/test_capability.py index abfb25b0f5..de105db281 100644 --- a/tests/task_router/test_capability.py +++ b/tests/task_router/test_capability.py @@ -55,10 +55,23 @@ def test_defaults(self): self.assertTrue(decoded is not None) websocket_url = ( - 'https://event-bridge.twilio.com/v1/wschannels/%s/%s' % - (self.account_sid, self.worker_sid) + 'https://event-bridge.twilio.com/v1/wschannels/{0}/{1}'.format(self.account_sid, self.worker_sid) ) expected = [ + { + 'url': 'https://taskrouter.twilio.com/v1/Workspaces/WS456/Activities', + 'method': 'GET', + 'allow': True, + 'query_filter': {}, + 'post_filter': {}, + }, + { + 'url': 'https://taskrouter.twilio.com/v1/Workspaces/{0}/Tasks/**'.format(self.workspace_sid), + 'method': 'GET', + 'allow': True, + 'query_filter': {}, + 'post_filter': {}, + }, { 'url': websocket_url, 'method': 'GET', @@ -74,13 +87,12 @@ def test_defaults(self): 'post_filter': {}, }, { - 'url': - 'https://taskrouter.twilio.com/v1/Workspaces/WS456/Activities', + 'url': 'https://taskrouter.twilio.com/v1/Workspaces/{0}/Workers/{1}'.format(self.workspace_sid, self.worker_sid), 'method': 'GET', 'allow': True, 'query_filter': {}, 'post_filter': {}, - }, + } ] self.assertEqual(expected, decoded['policies']) @@ -90,7 +102,7 @@ def test_allow_worker_activity_updates(self): decoded = jwt.decode(token, self.auth_token) self.assertTrue(decoded is not None) - url = 'https://taskrouter.twilio.com/v1/Workspaces/%s/Workers/%s' % ( + url = 'https://taskrouter.twilio.com/v1/Workspaces/{0}/Workers/{1}'.format( self.workspace_sid, self.worker_sid, ) @@ -110,7 +122,7 @@ def test_allow_worker_fetch_attributes(self): decoded = jwt.decode(token, self.auth_token) self.assertTrue(decoded is not None) - url = 'https://taskrouter.twilio.com/v1/Workspaces/%s/Workers/%s' % ( + url = 'https://taskrouter.twilio.com/v1/Workspaces/{0}/Workers/{1}'.format( self.workspace_sid, self.worker_sid, ) @@ -131,7 +143,7 @@ def test_allow_task_reservation_updates(self): decoded = jwt.decode(token, self.auth_token) self.assertTrue(decoded is not None) - url = 'https://taskrouter.twilio.com/v1/Workspaces/%s/Tasks/**' % ( + url = 'https://taskrouter.twilio.com/v1/Workspaces/{0}/Tasks/**'.format( self.workspace_sid, ) @@ -140,6 +152,6 @@ def test_allow_task_reservation_updates(self): 'method': 'POST', 'allow': True, 'query_filter': {}, - 'post_filter': {'ReservationStatus': {'required': True}}, + 'post_filter': {}, } self.assertEqual(expected, decoded['policies'][-1]) diff --git a/tests/task_router/test_reservations.py b/tests/task_router/test_reservations.py index ff85ffa316..5d635dad9a 100644 --- a/tests/task_router/test_reservations.py +++ b/tests/task_router/test_reservations.py @@ -8,6 +8,7 @@ AUTH = ("AC123", "token") BASE_URI = "https://taskrouter.twilio.com/v1/Accounts/AC123/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Tasks/WTaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" +BASE_WORKER_URI = "https://taskrouter.twilio.com/v1/Accounts/AC123/Workspaces/WSaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa/Workers/WKaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" RESERVATION_SID = "WRaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" @@ -36,6 +37,18 @@ def test_list(self, request): request.assert_called_with("GET", uri, params={}, auth=AUTH, use_json_extension=False) + @patch('twilio.rest.resources.base.make_twilio_request') + def test_list_for_worker(self, request): + resp = create_mock_json('tests/resources/task_router/reservations_list.json') + resp.status_code = 200 + request.return_value = resp + + uri = "{0}/Reservations".format(BASE_WORKER_URI) + list_resource = Reservations(BASE_WORKER_URI, AUTH) + list_resource.list() + request.assert_called_with("GET", uri, params={}, auth=AUTH, + use_json_extension=False) + @patch('twilio.rest.resources.base.make_twilio_request') def test_update_instance(self, request): resp = create_mock_json('tests/resources/task_router/reservations_instance.json') diff --git a/tests/task_router/test_task_router_capability.py b/tests/task_router/test_task_router_capability.py new file mode 100644 index 0000000000..78116c0db1 --- /dev/null +++ b/tests/task_router/test_task_router_capability.py @@ -0,0 +1,166 @@ +import unittest +import warnings + +from twilio import jwt +from twilio.task_router import TaskRouterCapability + + +class TaskRouterCapabilityTest(unittest.TestCase): + + def check_policy(self, method, url, policy): + self.assertEqual(url, policy['url']) + self.assertEqual(method, policy['method']) + self.assertTrue(policy['allow']) + self.assertEqual({}, policy['query_filter']) + self.assertEqual({}, policy['post_filter']) + + def check_decoded(self, decoded, account_sid, workspace_sid, channel_id, channel_sid=None): + self.assertEqual(decoded["iss"], account_sid) + self.assertEqual(decoded["account_sid"], account_sid) + self.assertEqual(decoded["workspace_sid"], workspace_sid) + self.assertEqual(decoded["channel"], channel_id) + self.assertEqual(decoded["version"], "v1") + self.assertEqual(decoded["friendly_name"], channel_id) + + if 'worker_sid' in decoded.keys(): + self.assertEqual(decoded['worker_sid'], channel_sid) + if 'taskqueue_sid' in decoded.keys(): + self.assertEqual(decoded['taskqueue_sid'], channel_sid) + + def test_workspace_default(self): + account_sid = "AC123" + auth_token = "foobar" + workspace_sid = "WS456" + channel_id = "WS456" + capability = TaskRouterCapability(account_sid, auth_token, workspace_sid, channel_id) + + capability.generate_token() + + token = capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, auth_token) + self.assertNotEqual(None, decoded) + + self.check_decoded(decoded, account_sid, workspace_sid, channel_id) + + policies = decoded['policies'] + self.assertEqual(len(policies), 3) + + for method, url, policy in [ + ('GET', "https://event-bridge.twilio.com/v1/wschannels/AC123/WS456", policies[0]), + ('POST', "https://event-bridge.twilio.com/v1/wschannels/AC123/WS456", policies[1]), + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456", policies[2]), + ]: + yield self.check_policy, method, url, policy + + def test_worker_default(self): + account_sid = "AC123" + auth_token = "foobar" + workspace_sid = "WS456" + worker_sid = "WK789" + capability = TaskRouterCapability(account_sid, auth_token, workspace_sid, worker_sid) + + capability.generate_token() + + token = capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, auth_token) + self.assertNotEqual(None, decoded) + + self.check_decoded(decoded, account_sid, workspace_sid, worker_sid, worker_sid) + + policies = decoded['policies'] + self.assertEqual(len(policies), 5) + + for method, url, policy in [ + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/Activities", policies[0]), + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/Tasks/**", policies[1]), + ('GET', "https://taskrouter.twilio.com/v1/wschannels/AC123/WK789", policies[2]), + ('POST', "https://event-bridge.twilio.com/v1/wschannels/AC123/WK789", policies[3]), + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/Workers/WK789", policies[4]) + ]: + yield self.check_policy, method, url, policy + + def test_task_queue_default(self): + account_sid = "AC123" + auth_token = "foobar" + workspace_sid = "WS456" + taskqueue_sid = "WQ789" + capability = TaskRouterCapability(account_sid, auth_token, workspace_sid, taskqueue_sid) + + capability.generate_token() + + token = capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, auth_token) + self.assertNotEqual(None, decoded) + + self.check_decoded(decoded, account_sid, workspace_sid, taskqueue_sid, taskqueue_sid) + + policies = decoded['policies'] + self.assertEqual(len(policies), 3) + + for method, url, policy in [ + ('GET', "https://event-bridge.twilio.com/v1/wschannels/AC123/WQ789", policies[0]), + ('POST', "https://event-bridge.twilio.com/v1/wschannels/AC123/WQ789", policies[1]) + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/TaskQueues/WQ789", policies[2]) + ]: + yield self.check_policy, method, url, policy + + def test_deprecated_worker(self): + account_sid = "AC123" + auth_token = "foobar" + workspace_sid = "WS456" + worker_sid = "WK789" + capability = TaskRouterCapability(account_sid, auth_token, workspace_sid, worker_sid) + + capability.generate_token() + + token = capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, auth_token) + self.assertNotEqual(None, decoded) + + self.check_decoded(decoded, account_sid, workspace_sid, worker_sid, worker_sid) + + policies = decoded['policies'] + self.assertEqual(len(policies), 5) + + # should expect 5 policies + for method, url, policy in [ + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/Activities", policies[0]), + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/Tasks/**", policies[1]), + ('GET', "https://event-bridge.twilio.com/v1/wschannels/AC123/WK789", policies[2]), + ('POST', "https://event-bridge.twilio.com/v1/wschannels/AC123/WK789", policies[3]), + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/Workers/WK789", policies[4]) + ]: + yield self.check_policy, method, url, policy + + # check deprecated warnings + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + capability.allow_worker_fetch_attributes() + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "deprecated" in str(w[-1].message) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + capability.allow_worker_activity_updates() + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "deprecated" in str(w[-1].message) + + with warnings.catch_warnings(record=True) as w: + warnings.simplefilter("always") + capability.allow_task_reservation_updates() + assert len(w) == 1 + assert issubclass(w[-1].category, DeprecationWarning) + assert "deprecated" in str(w[-1].message) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/task_router/test_task_router_taskqueue_capability.py b/tests/task_router/test_task_router_taskqueue_capability.py new file mode 100644 index 0000000000..42ed9597e6 --- /dev/null +++ b/tests/task_router/test_task_router_taskqueue_capability.py @@ -0,0 +1,132 @@ +import time +import unittest + +from twilio import jwt +from twilio.task_router import TaskRouterTaskQueueCapability + + +class TaskRouterTaskQueueCapabilityTest(unittest.TestCase): + + def setUp(self): + self.account_sid = "AC123" + self.auth_token = "foobar" + self.workspace_sid = "WS456" + self.taskqueue_sid = "WQ789" + self.capability = TaskRouterTaskQueueCapability(self.account_sid, self.auth_token, self.workspace_sid, self.taskqueue_sid) + + def test_generate_token(self): + + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + self.assertEqual(decoded["iss"], self.account_sid) + self.assertEqual(decoded["account_sid"], self.account_sid) + self.assertEqual(decoded["workspace_sid"], self.workspace_sid) + self.assertEqual(decoded["taskqueue_sid"], self.taskqueue_sid) + self.assertEqual(decoded["channel"], self.taskqueue_sid) + self.assertEqual(decoded["version"], "v1") + self.assertEqual(decoded["friendly_name"], self.taskqueue_sid) + + def test_generate_token_with_default_ttl(self): + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + self.assertEqual(int(time.time()) + 3600, decoded["exp"]) + + def test_generate_token_with_custom_ttl(self): + ttl = 10000 + + token = self.capability.generate_token(ttl) + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + self.assertEqual(int(time.time()) + 10000, decoded["exp"]) + + def test_default(self): + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + policies = decoded['policies'] + self.assertEqual(len(policies), 3) + + # websocket GET + get_policy = policies[0] + self.assertEqual("https://event-bridge.twilio.com/v1/wschannels/AC123/WQ789", get_policy['url']) + self.assertEqual("GET", get_policy['method']) + self.assertTrue(get_policy['allow']) + self.assertEqual({}, get_policy['query_filter']) + self.assertEqual({}, get_policy['post_filter']) + + # websocket POST + post_policy = policies[1] + self.assertEqual("https://event-bridge.twilio.com/v1/wschannels/AC123/WQ789", post_policy['url']) + self.assertEqual("POST", post_policy['method']) + self.assertTrue(post_policy['allow']) + self.assertEqual({}, post_policy['query_filter']) + self.assertEqual({}, post_policy['post_filter']) + + # fetch GET + fetch_policy = policies[2] + self.assertEqual("https://taskrouter.twilio.com/v1/Workspaces/WS456/TaskQueues/WQ789", fetch_policy['url']) + self.assertEqual("GET", fetch_policy['method']) + self.assertTrue(fetch_policy['allow']) + self.assertEqual({}, fetch_policy['query_filter']) + self.assertEqual({}, fetch_policy['post_filter']) + + def test_allow_fetch_subresources(self): + self.capability.allow_fetch_subresources() + + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + policies = decoded['policies'] + self.assertEqual(len(policies), 4) + + # confirm the additional policy generated with allow_fetch_subresources() + + policy = policies[3] + + self.assertEqual(policy['url'], "https://taskrouter.twilio.com/v1/Workspaces/WS456/TaskQueues/WQ789/**") + self.assertEqual(policy['method'], "GET") + self.assertTrue(policy['allow']) + self.assertEqual({}, policy['query_filter']) + self.assertEqual({}, policy['post_filter']) + + def test_allow_updates_subresources(self): + self.capability.allow_updates_subresources() + + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + policies = decoded['policies'] + self.assertEqual(len(policies), 4) + + # confirm the additional policy generated with allow_updates_subresources() + + policy = policies[3] + + self.assertEqual(policy['url'], "https://taskrouter.twilio.com/v1/Workspaces/WS456/TaskQueues/WQ789/**") + self.assertEqual(policy['method'], "POST") + self.assertTrue(policy['allow']) + self.assertEqual({}, policy['query_filter']) + self.assertEqual({}, policy['post_filter']) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/task_router/test_task_router_worker_capability.py b/tests/task_router/test_task_router_worker_capability.py new file mode 100644 index 0000000000..83feaf6b8c --- /dev/null +++ b/tests/task_router/test_task_router_worker_capability.py @@ -0,0 +1,133 @@ +import time +import unittest + +from twilio import jwt +from twilio.task_router import TaskRouterWorkerCapability + + +class TaskRouterWorkerCapabilityTest(unittest.TestCase): + def check_policy(self, method, url, policy): + self.assertEqual(url, policy['url']) + self.assertEqual(method, policy['method']) + self.assertTrue(policy['allow']) + self.assertEqual({}, policy['query_filter']) + self.assertEqual({}, policy['post_filter']) + + def check_decoded(self, decoded, account_sid, workspace_sid, channel_id, channel_sid=None): + self.assertEqual(decoded["iss"], account_sid) + self.assertEqual(decoded["account_sid"], account_sid) + self.assertEqual(decoded["workspace_sid"], workspace_sid) + self.assertEqual(decoded["channel"], channel_id) + self.assertEqual(decoded["version"], "v1") + self.assertEqual(decoded["friendly_name"], channel_id) + + if 'worker_sid' in decoded.keys(): + self.assertEqual(decoded['worker_sid'], channel_sid) + if 'taskqueue_sid' in decoded.keys(): + self.assertEqual(decoded['taskqueue_sid'], channel_sid) + + def setUp(self): + self.account_sid = "AC123" + self.auth_token = "foobar" + self.workspace_sid = "WS456" + self.worker_sid = "WK789" + self.capability = TaskRouterWorkerCapability(self.account_sid, self.auth_token, self.workspace_sid, self.worker_sid) + + def test_generate_token(self): + + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + self.check_decoded(decoded, self.account_sid, self.workspace_sid, self.worker_sid, self.worker_sid) + + def test_generate_token_with_default_ttl(self): + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + self.assertEqual(int(time.time()) + 3600, decoded["exp"]) + + def test_generate_token_with_custom_ttl(self): + ttl = 10000 + + token = self.capability.generate_token(ttl) + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + self.assertEqual(int(time.time()) + 10000, decoded["exp"]) + + def test_defaults(self): + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + websocket_url = 'https://event-bridge.twilio.com/v1/wschannels/{0}/{1}'.format(self.account_sid, self.worker_sid) + + # expect 5 policies + policies = decoded['policies'] + self.assertEqual(len(policies), 5) + + # should expect 5 policies + for method, url, policy in [ + ('GET', websocket_url, policies[0]), + ('POST', websocket_url, policies[1]), + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/Workers/WK789", policies[2]), + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/Tasks/**", policies[3]), + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/Activities", policies[4]) + ]: + yield self.check_policy, method, url, policy + + def test_allow_activity_updates(self): + + # allow activity updates to the worker + self.capability.allow_activity_updates() + + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + policies = decoded['policies'] + self.assertEqual(len(policies), 6) + policy = policies[5] + + url = "https://taskrouter.twilio.com/v1/Workspaces/{0}/Workers/{1}".format(self.workspace_sid, self.worker_sid) + + self.assertEqual(url, policy["url"]) + self.assertEqual("POST", policy["method"]) + self.assertTrue(policy["allow"]) + self.assertNotEqual(None, policy['post_filter']) + self.assertEqual({}, policy['query_filter']) + self.assertTrue(policy['post_filter']['ActivitySid']) + + def test_allow_reservation_updates(self): + # allow reservation updates + self.capability.allow_reservation_updates() + + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + policies = decoded['policies'] + self.assertEqual(len(policies), 6) + + policy = policies[5] + + url = "https://taskrouter.twilio.com/v1/Workspaces/{0}/Tasks/**".format(self.workspace_sid) + + self.check_policy('POST', url, policy) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/task_router/test_task_router_workspace_capability.py b/tests/task_router/test_task_router_workspace_capability.py new file mode 100644 index 0000000000..048611166e --- /dev/null +++ b/tests/task_router/test_task_router_workspace_capability.py @@ -0,0 +1,117 @@ +import time +import unittest + +from twilio import jwt +from twilio.task_router import TaskRouterWorkspaceCapability + + +class TaskRouterWorkspaceCapabilityTest(unittest.TestCase): + def check_policy(self, method, url, policy): + self.assertEqual(url, policy['url']) + self.assertEqual(method, policy['method']) + self.assertTrue(policy['allow']) + self.assertEqual({}, policy['query_filter']) + self.assertEqual({}, policy['post_filter']) + + def check_decoded(self, decoded, account_sid, workspace_sid, channel_id, channel_sid=None): + self.assertEqual(decoded["iss"], account_sid) + self.assertEqual(decoded["account_sid"], account_sid) + self.assertEqual(decoded["workspace_sid"], workspace_sid) + self.assertEqual(decoded["channel"], channel_id) + self.assertEqual(decoded["version"], "v1") + self.assertEqual(decoded["friendly_name"], channel_id) + + if 'worker_sid' in decoded.keys(): + self.assertEqual(decoded['worker_sid'], channel_sid) + if 'taskqueue_sid' in decoded.keys(): + self.assertEqual(decoded['taskqueue_sid'], channel_sid) + + def setUp(self): + self.account_sid = "AC123" + self.auth_token = "foobar" + self.workspace_sid = "WS456" + self.capability = TaskRouterWorkspaceCapability(self.account_sid, self.auth_token, self.workspace_sid) + + def test_generate_token(self): + + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + self.check_decoded(decoded, self.account_sid, self.workspace_sid, self.workspace_sid) + + def test_generate_token_with_default_ttl(self): + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + self.assertEqual(int(time.time()) + 3600, decoded["exp"]) + + def test_generate_token_with_custom_ttl(self): + ttl = 10000 + + token = self.capability.generate_token(ttl) + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + self.assertEqual(int(time.time()) + 10000, decoded["exp"]) + + def test_default(self): + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + policies = decoded['policies'] + self.assertEqual(len(policies), 3) + + for method, url, policy in [ + ('GET', "https://event-bridge.twilio.com/v1/wschannels/AC123/WS456", policies[0]), + ('POST', "https://event-bridge.twilio.com/v1/wschannels/AC123/WS456", policies[1]), + ('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456", policies[2]) + ]: + yield self.check_policy, method, url, policy + + def test_allow_fetch_subresources(self): + self.capability.allow_fetch_subresources() + + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + policies = decoded['policies'] + self.assertEqual(len(policies), 4) + + # confirm the additional policy generated with allow_fetch_subresources() + policy = policies[3] + + self.check_policy('GET', "https://taskrouter.twilio.com/v1/Workspaces/WS456/**", policy) + + def test_allow_updates_subresources(self): + self.capability.allow_updates_subresources() + + token = self.capability.generate_token() + self.assertNotEqual(None, token) + + decoded = jwt.decode(token, self.auth_token) + self.assertNotEqual(None, decoded) + + policies = decoded['policies'] + self.assertEqual(len(policies), 4) + + # confirm the additional policy generated with allow_updates_subresources() + policy = policies[3] + + self.check_policy('POST', "https://taskrouter.twilio.com/v1/Workspaces/WS456/**", policy) + +if __name__ == "__main__": + unittest.main() diff --git a/tests/task_router/test_workflow_config.py b/tests/task_router/test_workflow_config.py new file mode 100644 index 0000000000..458fecc045 --- /dev/null +++ b/tests/task_router/test_workflow_config.py @@ -0,0 +1,87 @@ +import unittest +import json + +from twilio.task_router.workflow_config import WorkflowConfig +from twilio.task_router.workflow_rule import WorkflowRule +from twilio.task_router.workflow_ruletarget import WorkflowRuleTarget + + +class WorkflowConfigTest(unittest.TestCase): + def test_to_json(self): + rules = [ + WorkflowRule("1==1", [WorkflowRuleTarget("WQeae4fc2f4db7f377c5d3758fb08b79b7", "1==1", 1, 20)], "SomeQ"), + WorkflowRule("1==1", [WorkflowRuleTarget("WQ19ebe92fb33522f018b5a31d805d94da", "1==1", 1, 210)], "SomeOtherQ") + ] + def_target = WorkflowRuleTarget("WQ9963154bf3122d0a0558f3763951d916", "1==1", None, None) + config = WorkflowConfig(rules, def_target) + self.assertEqual(True, self.is_json(config.to_json())) + + def test_from_json(self): + + data = { + 'task_routing': + { + 'filters': [ + { + 'expression': 'type == "sales"', + 'friendly_name': 'Sales', + 'targets': [ + { + 'queue': 'WQec62de0e1148b8477f2e24579779c8b1', + 'expression': 'task.language IN worker.languages' + } + ] + }, + { + 'expression': 'type == "marketing"', + 'friendly_name': 'Marketing', + 'targets': [ + { + 'queue': 'WQ2acd4c1a41ffadce5d1bac9e1ce2fa9f', + 'expression': 'task.language IN worker.languages' + } + ] + }, + { + 'expression': 'type == "support"', + 'friendly_name': 'Support', + 'targets': [ + { + 'queue': 'WQe5eb317eb23500ade45087ea6522896c', + 'expression': 'task.language IN worker.languages' + } + ] + } + ], + 'default_filter': + { + 'queue': 'WQ05f810d2d130344fd56e3c91ece2e594' + } + } + } + + config = WorkflowConfig.json2obj(json.dumps(data)) + self.assertEqual(3, len(config.task_routing.filters)) + self.assertEqual(1, len(config.task_routing.default_filter)) + + def test_from_json2(self): + + data = {'task_routing': {'filters': [{'friendly_name': 'SomeQ', 'expression': '1==1', 'targets': [ + {'priority': 1, 'queue': 'WQXXXX', 'expression': '1==1', 'timeout': 20}]}, + {'friendly_name': 'SomeOtherQ', 'expression': '1==1', + 'targets': [ + {'priority': 1, 'queue': 'WQXXXX', 'expression': '1==1', + 'timeout': 20}]}], + 'default_filter': {'priority': None, 'queue': 'WQYYYYY', 'expression': None, + 'timeout': None}}} + config = WorkflowConfig.json2obj(json.dumps(data)) + self.assertEqual(2, len(config.task_routing.filters)) + self.assertEqual(4, len(config.task_routing.default_filter)) + + def is_json(self, myjson): + try: + json.loads(myjson) + except ValueError as e: + print(e) + return False + return True diff --git a/tests/test_twiml.py b/tests/test_twiml.py index 7bab2ad1e9..c022baeda0 100644 --- a/tests/test_twiml.py +++ b/tests/test_twiml.py @@ -367,16 +367,16 @@ def setUp(self): # parse twiml XML string with Element Tree and inspect # structure tree = ET.fromstring(xml) - self.conf = tree.find(".//Queue") + self.queue = tree.find(".//Queue") - def test_conf_text(self): - self.assertEqual(self.conf.text.strip(), "TestQueueAttribute") + def test_queue_text(self): + self.assertEqual(self.queue.text.strip(), "TestQueueAttribute") - def test_conf_waiturl(self): - self.assertEqual(self.conf.get('url'), "") + def test_queue_waiturl(self): + self.assertEqual(self.queue.get('url'), "") - def test_conf_method(self): - self.assertEqual(self.conf.get('method'), "GET") + def test_queue_method(self): + self.assertEqual(self.queue.get('method'), "GET") class TestEnqueue(TwilioTest): @@ -390,22 +390,53 @@ def setUp(self): # parse twiml XML string with Element Tree and inspect # structure tree = ET.fromstring(xml) - self.conf = tree.find("./Enqueue") + self.enqueue = tree.find("./Enqueue") - def test_conf_text(self): - self.assertEqual(self.conf.text.strip(), "TestEnqueueAttribute") + def test_enqueue_text(self): + self.assertEqual(self.enqueue.text.strip(), "TestEnqueueAttribute") - def test_conf_waiturl(self): - self.assertEqual(self.conf.get('waitUrl'), "wait") + def test_enqueue_waiturl(self): + self.assertEqual(self.enqueue.get('waitUrl'), "wait") + + def test_enqueue_method(self): + self.assertEqual(self.enqueue.get('method'), "GET") + + def test_enqueue_action(self): + self.assertEqual(self.enqueue.get('action'), "act") + + def test_enqueue_waitmethod(self): + self.assertEqual(self.enqueue.get('waitUrlMethod'), "POST") + + +class TestEnqueueTask(TwilioTest): + + def setUp(self): + r = Response() + with r.enqueue(None, workflowSid="Workflow1") as e: + e.task('{"selected_language":"en"}', priority="10", timeout="50") + + xml = r.toxml() + + # parse twiml XML string with Element Tree and inspect + # structure + tree = ET.fromstring(xml) + self.enqueue = tree.find("./Enqueue") + self.task = self.enqueue.find(".//Task") + + def test_found_task(self): + self.assertNotEqual(None, self.task) + + def test_enqueue_workflow_sid(self): + self.assertEqual(self.enqueue.get('workflowSid'), "Workflow1") - def test_conf_method(self): - self.assertEqual(self.conf.get('method'), "GET") + def test_enqueue_task_attributes(self): + self.assertEqual(self.task.text.strip(), '{"selected_language":"en"}') - def test_conf_action(self): - self.assertEqual(self.conf.get('action'), "act") + def test_enqueue_task_priority(self): + self.assertEqual(self.task.get('priority'), "10") - def test_conf_waitmethod(self): - self.assertEqual(self.conf.get('waitUrlMethod'), "POST") + def test_enqueue_task_timeout(self): + self.assertEqual(self.task.get('timeout'), "50") class TestDial(TwilioTest): diff --git a/twilio/rest/resources/task_router/workers.py b/twilio/rest/resources/task_router/workers.py index f257435cb9..6383a57d0c 100644 --- a/twilio/rest/resources/task_router/workers.py +++ b/twilio/rest/resources/task_router/workers.py @@ -1,5 +1,6 @@ from .. import NextGenInstanceResource, NextGenListResource from .statistics import Statistics +from .reservations import Reservations class Worker(NextGenInstanceResource): @@ -68,7 +69,8 @@ class Worker(NextGenInstanceResource): calculate :class: `Workflow` statistics. """ subresources = [ - Statistics + Statistics, + Reservations ] def delete(self): diff --git a/twilio/rest/task_router.py b/twilio/rest/task_router.py index f9672d2820..eec74857e8 100644 --- a/twilio/rest/task_router.py +++ b/twilio/rest/task_router.py @@ -61,6 +61,15 @@ def reservations(self, workspace_sid, task_sid): workspace_sid, task_sid) return Reservations(base_uri, self.auth, self.timeout) + def worker_reservations(self, workspace_sid, worker_sid): + """ + Return a :class:`Reservations` instance for the :class:`Reservation` + with the given workspace_sid ans worker_sid + """ + base_uri = "{0}/{1}/Workers/{2}".format(self.workspace_uri, + workspace_sid, worker_sid) + return Reservations(base_uri, self.auth, self.timeout) + def task_queues(self, workspace_sid): """ Return a :class:`TaskQueues` instance for the :class:`TaskQueue` with diff --git a/twilio/task_router/__init__.py b/twilio/task_router/__init__.py index 17975c1cba..c77feeca29 100644 --- a/twilio/task_router/__init__.py +++ b/twilio/task_router/__init__.py @@ -1,120 +1,199 @@ import time - from .. import jwt +import warnings +warnings.simplefilter('always', DeprecationWarning) TASK_ROUTER_BASE_URL = 'https://taskrouter.twilio.com' -TASK_ROUTER_BASE_WS_URL = 'https://event-bridge.twilio.com/v1/wschannels' +TASK_ROUTER_BASE_EVENTS_URL = 'https://event-bridge.twilio.com/v1/wschannels' +TASK_ROUTER_VERSION = "v1" REQUIRED = {'required': True} OPTIONAL = {'required': False} +def deprecated(func): + def log_warning(*args, **kwargs): + # stacklevel = 2 makes the warning refer to the caller of the + # deprecation rather than the source of deprecation itself + warnings.warn("Call to deprecated function {0}.". + format(func.__name__), + stacklevel=2, + category=DeprecationWarning) + return func(*args, **kwargs) + return log_warning + + class TaskRouterCapability(object): - """ - A token to control permissions for the TaskRouter service. - - :param str account_sid: The account to generate a token for - :param str auth_token: The auth token for the account. Used to sign the - token and will not be included in generated output. - :param str workspace_sid: The workspace to grant capabilities over - :param str worker_sid: The worker sid to grant capabilities over - :param str base_url: The base TaskRouter API URL - :param str base_ws_url: The base TaskRouter event stream URL - """ - def __init__(self, account_sid, auth_token, workspace_sid, worker_sid, - base_url=TASK_ROUTER_BASE_URL, - version='v1', - base_ws_url=TASK_ROUTER_BASE_WS_URL): + def __init__(self, account_sid, auth_token, workspace_sid, channel_id): self.account_sid = account_sid self.auth_token = auth_token - self.workspace_sid = workspace_sid - self.worker_sid = worker_sid - self.version = version - self.base_url = '%s/%s' % (base_url, self.version) - self.base_ws_url = base_ws_url self.policies = [] - self._allow_worker_websocket_urls() - self._allow_activity_list_fetch() + self.workspace_sid = workspace_sid + self.channel_id = channel_id + self.base_url = "{0}/{1}/Workspaces/{2}".format(TASK_ROUTER_BASE_URL, + TASK_ROUTER_VERSION, + workspace_sid) - @property - def workspace_url(self): - return '%s/Workspaces/%s' % (self.base_url, self.workspace_sid) + # validate the JWT + self.validate_jwt() + + # set up resources + self.setup_resource() + + # add permissions to GET and POST to the event-bridge channel + self.allow_web_sockets(channel_id) + + # add permissions to fetch the instance resource + self.add_policy(self.resource_url, "GET", True) @property - def worker_url(self): - return '%s/Workers/%s' % (self.workspace_url, self.worker_sid) - - def _allow_worker_websocket_urls(self): - worker_event_url = '%s/%s/%s' % ( - self.base_ws_url, - self.account_sid, - self.worker_sid, - ) - self.policies.append(make_policy( - worker_event_url, - 'GET', - )) - self.policies.append(make_policy( - worker_event_url, - 'POST', - )) + def channel_prefix(self): + return self.channel_id[0:2] - def _allow_activity_list_fetch(self): - self.policies.append(make_policy( - '%s/Activities' % self.workspace_url, - 'GET', - )) + def setup_resource(self): + if self.channel_prefix == "WS": + self.resource_url = self.base_url + elif self.channel_prefix == "WK": + self.resource_url = self.base_url + "/Workers/" + self.channel_id - def allow_worker_activity_updates(self): - self.policies.append(make_policy( - self.worker_url, - 'POST', - post_filter={'ActivitySid': REQUIRED}, - )) + activity_url = self.base_url + "/Activities" + self.allow(activity_url, "GET") + + reservations_url = self.base_url + "/Tasks/**" + self.allow(reservations_url, "GET") + + elif self.channel_prefix == "WQ": + self.resource_url = "{0}/TaskQueues/{1}".format( + self.base_url, self.channel_id) + def allow_web_sockets(self, channel_id): + web_socket_url = "{0}/{1}/{2}".format(TASK_ROUTER_BASE_EVENTS_URL, + self.account_sid, + self.channel_id) + + self.policies.append(self.make_policy(web_socket_url, "GET", True)) + self.policies.append(self.make_policy(web_socket_url, "POST", True)) + + def validate_jwt(self): + if self.account_sid is None or self.account_sid[0:2] != "AC": + raise ValueError('Invalid AccountSid provided: ' + + self.account_sid) + if self.workspace_sid is None or self.workspace_sid[0:2] != "WS": + raise ValueError('Invalid WorkspaceSid provided: ' + + self.workspace_sid) + if self.channel_id is None: + raise ValueError('ChannelId not provided') + + if self.channel_prefix != "WS" and self.channel_prefix != "WK" \ + and self.channel_prefix != "WQ": + raise ValueError('Invalid ChannelId provided: ' + self.channel_id) + + def allow_fetch_subresources(self): + self.allow(self.resource_url + "/**", "GET") + + def allow_updates(self): + self.allow(self.resource_url, "POST") + + def allow_updates_subresources(self): + self.allow(self.resource_url + "/**", "POST") + + def allow_delete(self): + self.allow(self.resource_url, "DELETE") + + def allow_delete_subresources(self): + self.allow(self.resource_url + "/**", "DELETE") + + @deprecated def allow_worker_fetch_attributes(self): - self.policies.append(make_policy( - self.worker_url, - 'GET', - )) + if self.channel_prefix != "WK": + raise ValueError("Deprecated func not applicable to non Worker") + else: + self.policies.append(self.make_policy( + self.resource_url, + 'GET')) + + @deprecated + def allow_worker_activity_updates(self): + if self.channel_prefix == "WK": + self.policies.append(self.make_policy( + self.resource_url, + 'POST', + True, + post_filter={'ActivitySid': REQUIRED})) + else: + raise ValueError("Deprecated func not applicable to non Worker") + @deprecated def allow_task_reservation_updates(self): - tasks_url = '%s/Tasks/**' % self.workspace_url - self.policies.append(make_policy( - tasks_url, - 'POST', - post_filter={'ReservationStatus': REQUIRED}, - )) + if self.channel_prefix == "WK": + tasks_url = self.base_url + "/Tasks/**" + self.policies.append(self.make_policy( + tasks_url, + 'POST', + True)) + else: + raise ValueError("Deprecated func not applicable to non Worker") - def generate_token(self, ttl=3600, attributes=None): - """ - Generate a token string based on the credentials and permissions - previously configured on this object. + def add_policy(self, url, method, + allowed, query_filter=None, post_filter=None): - :param int ttl: Expiration time in seconds of the token. Defaults to - 3600 seconds (1 hour). - :param dict attributes: Extra attributes to pass into the token. + policy = self.make_policy(url, method, + allowed, query_filter, post_filter) + self.policies.append(policy) + + def allow(self, url, method, query_filter=None, post_filter=None): + self.add_policy(url, method, True, query_filter, post_filter) + + def deny(self, url, method, query_filter=None, post_filter=None): + self.add_policy(url, method, False, query_filter, post_filter) + + def make_policy(self, url, method, + allowed=True, query_filter=None, post_filter=None): + + """Create a policy dictionary for the given resource and method. + :param str url: the resource URL to grant or deny access to + :param str method: the HTTP method to allow or deny + :param allowed bool: whether this request is allowed + :param dict query_filter: specific GET parameter names + to require or allow + :param dict post_filter: POST parameter names + to require or allow """ - return self._generate_token( - ttl, - { - 'account_sid': self.account_sid, - 'channel': self.worker_sid, - 'worker_sid': self.worker_sid, - 'workspace_sid': self.workspace_sid, - } - ) + return { + 'url': url, + 'method': method, + 'allow': allowed, + 'query_filter': query_filter or {}, + 'post_filter': post_filter or {} + } + + def get_resource_url(self): + return self.resource_url + + def generate_token(self, ttl=3600): + task_router_attributes = { + 'account_sid': self.account_sid, + 'workspace_sid': self.workspace_sid, + 'channel': self.channel_id + } + + if self.channel_prefix == "WK": + task_router_attributes["worker_sid"] = self.channel_id + elif self.channel_prefix == "WQ": + task_router_attributes["taskqueue_sid"] = self.channel_id + + return self._generate_token(ttl, task_router_attributes) def _generate_token(self, ttl, attributes=None): payload = { - 'version': self.version, - 'friendly_name': self.worker_sid, - 'policies': self.policies, 'iss': self.account_sid, 'exp': int(time.time()) + ttl, + 'version': TASK_ROUTER_VERSION, + 'friendly_name': self.channel_id, + 'policies': self.policies, } if attributes is not None: @@ -123,22 +202,64 @@ def _generate_token(self, ttl, attributes=None): return jwt.encode(payload, self.auth_token, 'HS256') -def make_policy(url, method, query_filter=None, post_filter=None, - allowed=True): - """ - Create a policy dictionary for the given resource and method. - - :param str url: the resource URL to grant or deny access to - :param str method: the HTTP method to allow or deny - :param dict query_filter: specific GET parameter names to require or allow - :param dict post_filter: POST parameter names to require or allow - :param allowed bool: whether this request is allowed - """ - - return { - 'url': url, - 'method': method, - 'allow': allowed, - 'query_filter': query_filter or {}, - 'post_filter': post_filter or {}, - } +class TaskRouterWorkerCapability(TaskRouterCapability): + def __init__(self, account_sid, auth_token, workspace_sid, worker_sid): + super(TaskRouterWorkerCapability, self).__init__(account_sid, + auth_token, + workspace_sid, + worker_sid) + + self.reservations_url = self.base_url + "/Tasks/**" + self.activity_url = self.base_url + "/Activities" + + # add permissions to fetch the list of activities and + # list of worker reservations + self.allow(self.reservations_url, "GET") + self.allow(self.activity_url, "GET") + + def setup_resource(self): + self.resource_url = self.base_url + "/Workers/" + self.channel_id + + def allow_activity_updates(self): + self.policies.append(self.make_policy( + self.resource_url, + 'POST', + True, + post_filter={'ActivitySid': REQUIRED})) + + def allow_reservation_updates(self): + self.policies.append(self.make_policy( + self.reservations_url, + 'POST', + True)) + + +class TaskRouterTaskQueueCapability(TaskRouterCapability): + def setup_resource(self): + self.resource_url = self.base_url + "/TaskQueues/" + self.channel_id + + +class TaskRouterWorkspaceCapability(TaskRouterCapability): + def __init__(self, account_sid, auth_token, workspace_sid): + super(TaskRouterWorkspaceCapability, self).__init__(account_sid, + auth_token, + workspace_sid, + workspace_sid) + + def setup_resource(self): + self.resource_url = self.base_url + +from .taskrouter_config import ( + TaskRouterConfig +) + +from .workflow_config import ( + WorkflowConfig +) + +from .workflow_ruletarget import ( + WorkflowRuleTarget +) +from .workflow_rule import ( + WorkflowRule +) diff --git a/twilio/task_router/taskrouter_config.py b/twilio/task_router/taskrouter_config.py new file mode 100644 index 0000000000..b4e8eb7b55 --- /dev/null +++ b/twilio/task_router/taskrouter_config.py @@ -0,0 +1,17 @@ +from .workflow_rule import WorkflowRule +from .workflow_ruletarget import WorkflowRuleTarget + + +class TaskRouterConfig: + + """ + TaskRouterConfig represents the filter and default_filter + of a workflow configuration of taskrouter + """ + + def __init__(self, rules, default_target): + self.filters = rules + self.default_filter = default_target + + def __repr__(self): + return self.__dict__ diff --git a/twilio/task_router/workflow_config.py b/twilio/task_router/workflow_config.py new file mode 100644 index 0000000000..e9c27aa378 --- /dev/null +++ b/twilio/task_router/workflow_config.py @@ -0,0 +1,26 @@ +from .taskrouter_config import TaskRouterConfig +import json + + +class WorkflowConfig: + + """ + WorkflowConfig represents the whole workflow config json which contains + filters and default_filter. + """ + + def __init__(self, workflow_rules, default_target): + # filters and default_filters + self.task_routing = TaskRouterConfig(workflow_rules, default_target) + + def to_json(self): + return json.dumps(self, + default=lambda o: o.__dict__, + sort_keys=True, + indent=4) + + @staticmethod + def json2obj(data): + m = json.loads(data) + return WorkflowConfig(m['task_routing']['filters'], + m['task_routing']['default_filter']) diff --git a/twilio/task_router/workflow_rule.py b/twilio/task_router/workflow_rule.py new file mode 100644 index 0000000000..3cae68ec80 --- /dev/null +++ b/twilio/task_router/workflow_rule.py @@ -0,0 +1,34 @@ +from .workflow_ruletarget import WorkflowRuleTarget + + +class WorkflowRule: + + """ + WorkflowRule represents the top level filter + which contains a 1 or more targets + + ..attribute::expression + + The expression at the top level filter + + ..attribute::targets + + The list of targets under the filter + + ..attribute::friendlyName + + The name of the filter + """ + + def __init__(self, expression, targets, friendly_name): + + self.expression = expression + self.targets = targets + self.friendly_name = friendly_name + + def __repr__(self): + return str({ + 'expression': self.expression, + 'friendly_name': self.friendly_name, + 'target': self.target, + }) diff --git a/twilio/task_router/workflow_ruletarget.py b/twilio/task_router/workflow_ruletarget.py new file mode 100644 index 0000000000..1cee506c30 --- /dev/null +++ b/twilio/task_router/workflow_ruletarget.py @@ -0,0 +1,27 @@ +class WorkflowRuleTarget: + """ + Workflow Rule target which is encompassed + inside targets + + ..attribute::queue + + The queue which will handle the task matching this filter target + + ..attribute::expression + + The dynamic expression if any for this matching + + ..attribute::priority + + The priority for the target + + ..attribute::timeout + + The timeout before the reservation expires. + """ + def __init__(self, queue, expression, priority, timeout): + + self.queue = queue + self.expression = expression + self.priority = priority + self.timeout = timeout diff --git a/twilio/twiml.py b/twilio/twiml.py index f05015842d..cbac1122da 100644 --- a/twilio/twiml.py +++ b/twilio/twiml.py @@ -524,10 +524,25 @@ class Enqueue(Verb): GET = 'GET' POST = 'POST' + nestables = ['Task'] + def __init__(self, name, **kwargs): super(Enqueue, self).__init__(**kwargs) self.body = name + def task(self, attributes, **kwargs): + return self.append(Task(attributes, **kwargs)) + + +class Task(Verb): + """Specify the task attributes when enqueuing a call + + :param attributes: Attributes for a task + """ + def __init__(self, attributes, **kwargs): + super(Task, self).__init__(**kwargs) + self.body = attributes + class Leave(Verb): """Signals the call to leave its queue diff --git a/twilio/version.py b/twilio/version.py index 953ebe5125..9e8a4df44c 100644 --- a/twilio/version.py +++ b/twilio/version.py @@ -1,2 +1,2 @@ -__version_info__ = ('4', '4', '0') +__version_info__ = ('4', '6', '0') __version__ = '.'.join(__version_info__)