Repository: PagerDuty/pdpyras

Issue:
EventsAPISession uses `X-Routing-Key` instead of setting `routing_key` in payload
EventsAPISession uses the `X-Routing-Key` header to specify the routing key and does not set the `routing_key` parameter in the REST Payload.

API specification requires that the `routing_key` parameter is a required parameter.

https://developer.pagerduty.com/docs/events-api-v2/trigger-events/

Discussion:

Patch:
diff --git a/pdpyras.py b/pdpyras.py
index 506440d..2143baf 100644
--- a/pdpyras.py
+++ b/pdpyras.py
@@ -211,7 +211,7 @@ def tokenize_url_path(url, baseurl='https://api.pagerduty.com'):
     :type method: str
     :type url: str
     :type baseurl: str
-    :rtype: tuple 
+    :rtype: tuple
     """
     urlnparams = url.split('#')[0].split('?') # Ignore all #'s / params
     url_nodes = urlnparams[0].lstrip('/').split('/')
@@ -236,7 +236,7 @@ def tokenize_url_path(url, baseurl='https://api.pagerduty.com'):
     # Tokenize / classify the URL now:
     tokenized_nodes = [path_nodes[0]]
     if len(path_nodes) >= 3:
-        # It's an endpoint like one of the following 
+        # It's an endpoint like one of the following
         # /{resource}/{id}/{sub-resource}
         # We're interested in {resource} and {sub_resource}.
         # More deeply-nested endpoints not known to exist.
@@ -340,7 +340,7 @@ class PDSession(requests.Session):
 
     * ``-1`` to retry infinitely
     * ``0`` to return the `requests.Response`_ object and exit (which is the
-      default behavior) 
+      default behavior)
     * ``n``, where ``n > 0``, to retry ``n`` times (or up
       to :attr:`max_http_attempts` total for all statuses, whichever is
       encountered first), and raise a :class:`PDClientError` after that many
@@ -609,7 +609,7 @@ class PDSession(requests.Session):
     def stagger_cooldown(self, val):
         if type(val) not in [float, int] or val<0:
             raise ValueError("Cooldown randomization factor stagger_cooldown "
-                "must be a positive real number") 
+                "must be a positive real number")
         self._stagger_cooldown = val
 
     @property
@@ -642,7 +642,7 @@ class EventsAPISession(PDSession):
 
     @property
     def auth_header(self):
-        return {'X-Routing-Key': self.api_key}
+        return {}
 
     def acknowledge(self, dedup_key):
         """
@@ -691,10 +691,16 @@ class EventsAPISession(PDSession):
         :returns:
             The deduplication key of the incident, if any.
         """
+
         actions = ('trigger', 'acknowledge', 'resolve')
         if action not in actions:
             raise ValueError("Event action must be one of: "+', '.join(actions))
-        event = {'event_action':action}
+
+        event = {
+            'event_action':action,
+            'routing_key': self.api_key
+        }
+
         event.update(properties)
         if isinstance(dedup_key, string_types):
             event['dedup_key'] = dedup_key
@@ -797,7 +803,7 @@ class APISession(PDSession):
     :members:
     """
 
-    api_call_counts = None 
+    api_call_counts = None
     """A dict object recording the number of API calls per endpoint"""
 
     api_time = None
@@ -834,7 +840,7 @@ class APISession(PDSession):
     @property
     def auth_type(self):
         """
-        Defines the method of API authentication. 
+        Defines the method of API authentication.
 
         By default this is "token"; if "oauth2", the API key will be used.
         """
@@ -917,12 +923,12 @@ class APISession(PDSession):
         query_params.update({'query':query})
         # When determining uniqueness, web/the API doesn't care about case.
         simplify = lambda s: s.lower()
-        search_term = simplify(query) 
+        search_term = simplify(query)
         equiv = lambda s: simplify(s[attribute]) == search_term
         obj_iter = self.iter_all(resource, params=query_params)
         return next(iter(filter(equiv, obj_iter)), None)
 
-    def iter_all(self, path, params=None, paginate=True, page_size=None, 
+    def iter_all(self, path, params=None, paginate=True, page_size=None,
             item_hook=None, total=False):
         """
         Iterator for the contents of an index endpoint or query.
@@ -947,8 +953,8 @@ class APISession(PDSession):
             pagination yet, i.e. "nested" endpoints like (as of this writing):
             ``/users/{id}/contact_methods`` and ``/services/{id}/integrations``
         :param page_size:
-            If set, the ``page_size`` argument will override the ``default_page_size`` 
-            parameter on the session and set the ``limit`` parameter to a custom 
+            If set, the ``page_size`` argument will override the ``default_page_size``
+            parameter on the session and set the ``limit`` parameter to a custom
             value (default is 100), altering the number of pagination results.
         :param item_hook:
             Callable object that will be invoked for each iteration, i.e. for
@@ -1017,7 +1023,7 @@ class APISession(PDSession):
                 break
             try:
                 response = r.json()
-            except ValueError: 
+            except ValueError:
                 self.log.debug("Stopping iteration on endpoint \"%s\"; API "
                     "responded with invalid JSON.", path)
                 break
@@ -1035,7 +1041,7 @@ class APISession(PDSession):
                         "however many can be gotten, will be included.", path)
                 if 'total' in response:
                     total_count = response['total']
-                else: 
+                else:
                     self.log.debug("Pagination and the \"total\" parameter "
                         "are enabled in iteration, but the index endpoint %s "
                         "responded with no \"total\" property in the response. "
@@ -1043,7 +1049,7 @@ class APISession(PDSession):
                         "first retrieving all records.", path)
                 offset += data['limit']
             for result in response[r_name]:
-                n += 1 
+                n += 1
                 # Call a callable object for each item, i.e. to print progress:
                 if hasattr(item_hook, '__call__'):
                     item_hook(result, n, total_count)
@@ -1054,7 +1060,7 @@ class APISession(PDSession):
         """
         Performs a GET request, returning the JSON-decoded body as a dictionary
 
-        :raises PDClientError: In the event of HTTP error 
+        :raises PDClientError: In the event of HTTP error
         """
         return self.get(path, **kw)
 
@@ -1063,7 +1069,7 @@ class APISession(PDSession):
         """
         Performs a POST request, returning the JSON-decoded body as a dictionary
 
-        :raises PDClientError: In the event of HTTP error 
+        :raises PDClientError: In the event of HTTP error
         """
         return self.post(path, **kw)
 
@@ -1130,9 +1136,9 @@ class APISession(PDSession):
 
         :param method:
             Method of the request
-        :param response: 
+        :param response:
             Response object
-        :param suffix: 
+        :param suffix:
             Optional suffix to append to the key
         :type method: str
         :type response: `requests.Response`_
@@ -1296,7 +1302,7 @@ class APISession(PDSession):
         """Truncated token for secure display/identification purposes."""
         return last_4(self.api_key)
 
-class PDClientError(Exception): 
+class PDClientError(Exception):
     """
     General API errors base class.
     """


Test patch:
diff --git a/test_pdpyras.py b/test_pdpyras.py
index 76217fd..a2d1899 100755
--- a/test_pdpyras.py
+++ b/test_pdpyras.py
@@ -91,12 +91,15 @@ class EventsSessionTest(SessionTest):
                 'https://events.pagerduty.com/v2/enqueue',
                 parent.request.call_args[0][1])
             self.assertDictContainsSubset(
-                {'Content-Type': 'application/json',
-                 'X-Routing-Key': 'routingkey'},
+                {'Content-Type': 'application/json'},
+                parent.request.call_args[1]['headers'])
+            self.assertNotIn(
+                'X-Routing-Key',
                 parent.request.call_args[1]['headers'])
             self.assertEqual(
                 {
                     'event_action':'trigger',
+                    'routing_key':'routingkey',
                     'payload':{
                         'summary': 'testing 123',
                         'source': 'triggered.from.pdpyras',
@@ -108,12 +111,20 @@ class EventsSessionTest(SessionTest):
                 parent.request.call_args[1]['json'])
             ddk = sess.resolve('abc123')
             self.assertEqual(
-                {'event_action':'resolve', 'dedup_key':'abc123'},
+                {
+                    'event_action':'resolve',
+                    'dedup_key':'abc123',
+                    'routing_key':'routingkey',
+                },
                 parent.request.call_args[1]['json'])
 
             ddk = sess.acknowledge('abc123')
             self.assertEqual(
-                {'event_action':'acknowledge', 'dedup_key':'abc123'},
+                {
+                    'event_action':'acknowledge',
+                    'dedup_key':'abc123',
+                    'routing_key':'routingkey',
+                },
                 parent.request.call_args[1]['json'])
 
 class APISessionTest(SessionTest):