PDPYRAS: PagerDuty Python REST API Sessions

A minimal, practical Python client for the PagerDuty REST API.

circleci-build

About

This library supplies a class pdpyras.APISession extending requests.Session from the Requests HTTP library. It serves as a practical and simple-as-possible-but-no-simpler HTTP client for accessing the PagerDuty REST API and events API.

It takes care of all of the most common prerequisites and necessities pertaining to accessing the API so that implementers can focus on the request and response body schemas of each particular resource (as documented in our REST API Reference) and do what they need to get done.

Features

  • Efficient API access through automatic HTTP connection pooling and persistence

  • Tested in / support for Python 2.7 through 3.8

  • Configurable cooldown/reattempt logic for rate limiting and transient network problems

  • Inclusion of required HTTP Request Headers for PagerDuty REST API requests

  • Bulk data retrieval and iteration over resource index endpoints with pagination

  • Individual object retrieval by name

  • API request profiling

  • Bonus Events API client

History

This module was borne of necessity for a basic, reusable API client to eliminate code duplication in some of PagerDuty Support’s internal Python-based API tools. We needed something that could get the job done without reinventing the wheel or getting in the way.

We also frequently found ourselves performing REST API requests using beta or non-documented API endpoints for one reason or another, so we also needed a client that provided easy access to features of the underlying HTTP library (i.e. to obtain the response headers, or set special request headers). We needed something that eliminated tedious tasks like querying, pagination and header-setting commonly associated with REST API usage. Finally, we discovered that the way we were using requests wasn’t making use of connection re-use, and wanted a way to easily enforce this as a standard practice.

Ultimately, we deemed most other libraries to be both overkill for this task and also not very conducive to use for experimental API calls.

Warranty

THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Installation

If pip is available, it can be installed via:

pip install pdpyras

Alternately, if requests has already been installed locally, and urllib3 is available, one can simply download pdpyras.py into the directory where it will be used.

Usage Guide

Basic Usage

Some examples of usage:

Basic getting: Obtain a user profile as a dict object:

from pdpyras import APISession
api_token = 'your-token-here'
session = APISession(api_token)

# Using requests.Session.get:
response = session.get('/users/PABC123')
user = None
if response.ok:
    user = response.json()['user']

# Or, more succinctly:
user = session.rget('/users/PABC123')

Iteration (1): Iterate over all users and print their ID, email and name:

from pdpyras import APISession
api_token = 'your-token-here'
session = APISession(api_token)
for user in session.iter_all('users'):
    print(user['id'], user['email'], user['name'])

Iteration (2): Compile a list of all services with “SN” in their name:

from pdpyras import APISession
api_token = 'your-token-here'
session = APISession(api_token)
services = list(session.iter_all('services', params={'query': 'SN'}))

Querying and updating: Find a user exactly matching email address jane@example35.com and update their name to “Jane Doe”:

from pdpyras import APISession
api_token = 'your-token-here'
session = APISession(api_token)
user = session.find('users', 'jane@example35.com', attribute='email')
if user is not None:
    # Update using put directly:
    updated_user = None
    response = session.put(user['self'], json={
        'user':{'type':'user', 'name': 'Jane Doe'}
    })
    if response.ok:
        updated_user = response.json()['user']

    # Alternately / more succinctly:
    try:
        updated_user = session.rput(user['self'], json={
            'type':'user', 'name': 'Jane Doe'
        })
    except PDClientError:
        updated_user = None

Multiple update: acknowledge all triggered incidents assigned to user with ID PHIJ789. Note that to acknowledge, we need to set the From header. This example assumes that admin@example.com corresponds to a user in the PagerDuty account:

from pdpyras import APISession
api_token = 'your-token-here'
session = APISession(api_token, default_from='admin@example.com')
# Query incidents
incidents = session.list_all(
    'incidents',
    params={'user_ids[]':['PHIJ789'],'statuses[]':['triggered']}
)
# Change their state
for i in incidents:
    i['status'] = 'acknowledged'
# PUT the updated list back up to the API
updated_incidents = session.rput('incidents', json=incidents)

Using an OAuth 2 Access Token to Authenticate

When using an OAuth2 token, include the keyword argument auth_type='oauth2' or auth_type='bearer' to the constructor. This tells the client to set the Authorization header appropriately in order to use this type of API credential.

Example:

from pdpyras import APISession
session = APISession(oauth_token_here, auth_type='oauth2')

Note, obtaining an access token via the OAuth 2 flow is outside the purview of an API client, and should be performed separately by your application.

For further information on OAuth 2 authentication with PagerDuty, refer to the official documentation:

General Concepts

In all cases, when sending or receiving data through the REST API using pdpyras.APISession, note the following:

URLs

  • There is no need to include the API base URL. Any path relative to the web root, leading slash or no, is automatically appended to the base URL when constructing an API request, i.e. one can specify users/PABC123 or /users/PABC123 instead of https://api.pagerduty.com/users/PABC123.

  • One can also pass the full URL of an API endpoint and it will still work, i.e. the self property of any object can be used, and there is no need to strip out the API base URL.

Request and Response Bodies

Note that when working with the REST API using pdpyras.APISession, the implementer is not insulated from having to work directly with the schemas of requests and responses. Rather, one must follow the REST API Reference which documents the schemas at length, and construct/access objects representing the request and response bodies, while the API client takes care of everything else.

  • Data is represented as dictionary or list objects, and should have a structure that mirrors that of the API schema:

    • If the data type documented in the schema is object, then the corresponding type in Python will be dict.

    • If the data type documented in the schema is array, then the corresponding type in Python will be list.

  • Everything is automatically JSON-encoded and decoded, using it as follows:

    • To send a JSON request body, pass a dict object (or list, where applicable) in the json keyword argument.

    • To get the response body as a dict (or list, if applicable), call the json method of the response object.

    • If using the r{VERB} methods, i.e. rget, the return value will be the dict/list object decoded from the wrapped entity and there is no need to call response.json().

    • Similarly, the j{VERB} methods, i.e. jget, return the object decoded from the JSON string in the response body (but without attempting to unwrap any wrapped entities it may contain).

Using Special Features of Requests

Keyword arguments to the HTTP methods get passed through to the similarly- named functions in requests.Session, so for additional options, please refer to the documentation provided by the Requests project.

Data Access Abstraction

The APISession class, in addition to providing a more convenient way of making the HTTP requests to the API, provides methods that yield/return dicts representing the PagerDuty objects with their defined schemas (see: REST API Reference) without needing to go through enclosing them in a data envelope.

In other words, in the process of getting from an API call to the object representing the desired result, all of the following are taken care of:

  1. Validate that the response HTTP status is not an error.

  2. Predict the name of the envelope property which will contain the object.

  3. Validate that the result contains the predicted envelope property.

  4. Access the property that is encapsulated within the response.

Supported Endpoints

Please note, not all API endpoints are supported for these convenience functions. The general rules are that the name of the wrapped resource property must follow from the innermost resource name for the API path in question, and that the “nodes” in the URL path (between forward slashes) must alternate between resource type and ID.

For instance, for /escalation_policies/{id} the name must be escalation_policy, and or for /users/{id}/notification_rules it must be notification_rules.

For example, with user sessions (one API resource/endpoint that breaks these rules), one will need to use the plain get and post functions, or jget / jpost, because their URLs are formatted as /users/{id}/sessions/{type}/{session_id} and the wrapped resource property name is user_sessions / user_session rather than simply sessions / session.

Iteration

The method pdpyras.APISession.iter_all returns an iterator that yields all results from a resource index, automatically incrementing the offset parameter to advance through each page of data.

Note, one can perform filtering with iteration to constrain constrain the range of results, by passing in a dictionary object as the params keyword argument. Any parameters will be automatically merged with the pagination parameters and serialized into the final URL, so there is no need to manually construct the URL, i.e. append ?key1=value1&key2=value2.

Example: Find all users with “Dav” in their name/email (i.e. Dave/David) in the PagerDuty account:

for dave in session.iter_all('users', params={'query':"Dav"}):
    print("%s <%s>"%(dave['name'], dave['email']))

Also, note, as of version 2.2, there are the methods pdpyras.APISession.list_all and pdpyras.APISession.dict_all which return a list or dictionary of all results, respectively.

Example: Get a dictionary of all users, keyed by email, and use it to find the ID of the user whose email is bob@example.com

users = session.dict_all('users', by='email')
print(users['bob@example.com']['id'])

Disclaimers Regarding Iteration

Regarding Performance:

Because HTTP requests are made synchronously and not in parallel threads, the data will be retrieved one page at a time and the functions list_all and dict_all will not return until after the HTTP response from the final API call is received. Simply put, the functions will take longer to return if the total number of results is higher.

On Updating and Deleting Records:

If performing page-wise operations, i.e. making changes immediately after fetching each page of results, rather than pre-fetching all objects and then operating on them, one must be cautious not to perform any changes to the results that would affect the set over which iteration is taking place.

To elaborate, this happens whenever a resource object is deleted, or it is updated in such a way that the filter parameters in iter_all no longer apply to it. This is because indexes’ contents update in real time. Thus, should any objects be removed from the set (the objects included in the iteration), then the offset when accessing the next page of results will still be incremented, whereas the position of the first object in the next page will shift to a lower rank in the overall list of objects.

In other words: let’s say that one is reading and then tearing pages from a notebook. If the algorithm is “go through 100 pages, do things with the pages, then repeat starting with the 101st page, then with the 201st, etc” but one tears out pages immediately after going through them, then what was originally the 101st page before starting will shift to become the first page after going through the first hundred pages. Thus, when going to the 101st page after finishing tearing out the first hundred pages, the second hundred pages will be skipped over, and similarly for pages 401-500, 601-700 and so on.

Also, note, a similar effect would occur if creating objects during iteration.

As of version 3, this issue is still applicable. To avoid it, do not use iter_all, but use list_all or dict_all to pre-fetch the set of records to be operated on, and then iterate over the results. This still does not constitute a completely bulletproof safeguard against set changes caused by insert/update/delete operations carried out by other simultaneous processes (i.e. a user renaming a service through the web UI).

Reading

The method pdpyras.APISession.rget gets a resource, returning the object within the resource name envelope after JSON-decoding the response body. In other words, if retrieving an individual user (for instance), where one would have to JSON-decode and then access the user key in the resulting dictionary object, that object itself is directly returned.

The rget method can be called with as little as one argument: the URL (or URL path) to request. Example:

service = session.rget('/services/PZYX321')
print("Service PZYX321's name: "+service['name'])

One can also use it on a resource index, although if the goal is to get all results rather than a specific page, pdpyras.APISession.iter_all is recommended for this purpose, as it will automatically iterate through all pages of results, rather than just the first. When using rget in this way, the return value will be a list of dicts instead of a dict.

The method also accepts other keyword arguments, which it will pass along to reqeusts.Session.get, i.e. if requesting an index, params can be used to set a filter:

first_100_daves = session.rget(
    '/users',
    params={'query':"Dave",'limit':100}
)

Creating and Updating

Just as rget eliminates the need to JSON-decode and then pull the data out of the envelope in the response schema, pdpyras.APISession.rpost and pdpyras.APISession.rput return the data in the envelope property. Furthermore, they eliminate the need to enclose the dictionary object representing the data to be transmitted in an envelope, and just like rget, they accept at an absolute minimum one positional argument (the URL), and all keyword arguments are passed through to the underlying request method function.

For instance, instead of having to set the keyword argument json = {"user": {...}} to put, one can pass json = {...} to rput, to update a user. The following function takes a PagerDuty user ID and gives the user the admin role and prints a message when done:

def promote_to_admin(session, uid):
    user = session.rput(
        '/users/'+uid,
        json={'role':'admin'}
    )
    print("%s now has admin superpowers"%user['name'])

Deleting

The rdelete method has no return value, but otherwise behaves in exactly the same way as the other request methods with r prepended to their name. Like the other r* methods, it will raise pdpyras.PDClientError if the API responds with a non-success HTTP status.

Example:

session.rdelete("/services/PI86NOW")
print("Service deleted.")

Managing, a.k.a. Multi-Updating

Introduced in version 2.1 is support for automatic data envelope functionality in multi-update actions.

As of this writing, multi-update is limited to the following actions:

Please note: as of yet, merging incidents is not supported by rput. For this and other unsupported endpoints, you will need to call put directly, or jput to get the response body as a dictionary object.

To use, simply pass in a list of objects or references (dictionaries having a structure according to the API schema reference for that object type) to the json keyword argument of pdpyras.APISession.rput, and the final payload will be an object with one property named after the resource, containing that list.

For instance, to resolve two incidents with IDs PABC123 and PDEF456:

session.rput(
    "incidents",
    json=[{'id':'PABC123','type':'incident_reference', 'status':'resolved'},
          {'id':'PDEF456','type':'incident_reference', 'status':'resolved'}]
)

In this way, a single API request can more efficiently perform multiple update actions.

It is important to note, however, that certain actions such as updating incidents require the From header, which should be the login email address of a valid PagerDuty user. To set this, pass it through using the headers keyword argument, or set the pdpyras.APISession.default_from property.

Error Handling

What happens when, for any of the r* methods, the API responds with a non-success HTTP status? Obviously in this case, they cannot return the JSON-decoded response, because the response would not be the sought-after data but a different schema altogether (see: Errors), and this would put the onus on the end user to distinguish between success and error based on the structure of the returned dictionary object (yuck).

Instead, when this happens, a pdpyras.PDClientError exception is raised. The advantage of this design lies in how the methods can always be expected to return the same sort of data, and if they can’t, the program flow that depends on getting this specific structure of data is appropriately interrupted. Moreover, because (as of version 2) this exception class will have the requests.Response object as its response property (whenever the exception pertains to a HTTP error), the end user can define specialized error handling logic in which the REST API response data (i.e. headers, code and body) are directly available.

For instance, the following code prints “User not found” in the event of a 404, raises the underlying exception in the event of an incorrect API access token (401 Unauthorized) or non-transient network error, prints out the user’s email if the user exists, and does nothing otherwise:

try:
    user = session.rget("/users/PJKL678")
    print(user['email'])
except PDClientError as e:
    if e.response:
        if e.response.status_code == 404:
            print("User not found")
        elif e.response.status_code == 401:
            raise e
    else:
        raise e

Just make sure to import PDClientError or reference it throught he namespace, i.e.

from pdpyras import APISession, PDClientError

except PDClientError as e:

Or:

import pdpyras

...

except pdpyras.PDClientError as e:
...

HTTP Retry Logic

By default, after receiving a response, pdpyras.PDSession.request will return the requests.Response object unless its status is 429 (rate limiting), in which case it will retry until it gets a status other than 429.

The property pdpyras.PDSession.retry allows customization in this regard, so that the client can be made to retry on other statuses (i.e. 502/400), up to a set number of times. The total number of HTTP error responses that the client will tolerate before returning the response object is defined in pdpyras.PDSession.max_http_attempts, and this will supersede the maximum number of retries defined in pdpyras.PDSession.retry.

Example:

The following will take about 30 seconds plus API request time (carrying out four attempts, with 2, 4, 8 and 16 second pauses between them), before finally returning with the status 404 requests.Response object:

session.retry[404] = 5
session.max_http_attempts = 4
session.sleep_timer = 1
session.sleep_timer_base = 2
# isinstance(session, pdpyras.APISession)
response = session.get('/users/PNOEXST')

Default Behavior:

Note that without specifying any retry behavior for status 429 (rate limiting), it will retry indefinitely. This is a sane approach; if it is ever responding with 429, this means that the REST API is receiving (for the given REST API key) too many requests, and the issue should by nature be transient.

Similarly, there is hard-coded default behavior for status 401 (unauthorized): immediately raise pdpyras.PDClientError (as this can be considered in all cases a completely non-transient error).

It is still possible to override these behaviors using pdpyras.PDSession.retry, but it is not recommended.

Events API Usage

As an added bonus, pdpyras provides an additional Session class for submitting alert data to the Events API and triggering incidents asynchronously: pdpyras.EventsAPISession. It has most of the same features as pdpyras.APISession:

  • Connection persistence

  • Automatic cooldown and retry in the event of rate limiting or a transient network error

  • Setting all required headers

  • Configurable HTTP retry logic

To transmit alerts and perform actions through the events API, one would use:

To instantiate a session object, pass the constructor the routing key:

import pdpyras
routing_key = '0123456789abcdef0123456789abcdef'
session = pdpyras.EventsAPISession(routing_key)

Example 1: Trigger an event and use the PagerDuty-supplied deduplication key to resolve it later:

dedup_key = session.trigger("Server is on fire", 'dusty.old.server.net')
# ...
session.resolve(dedup_key)

Example 2: Trigger an event, specifying a dedup key, and use it to later acknowledge the incident

session.trigger("Server is on fire", 'dusty.old.server.net',
    dedup_key='abc123')
# ...
session.acknowledge('abc123')

Contributing

Bug reports and pull requests to fix issues are always welcome.

If adding features, or making changes, it is recommended to update or add tests and assertions to the appropriate test case class in test_pdpyras.py to ensure code coverage. If the change(s) fix a bug, please add assertions that reproduce the bug along with code changes themselves, and include the GitHub issue number in the commit message.

Code author: Demitri Morgan <demitri@pagerduty.com>

Developer Interface

Generic API Client

This base class implements features common to both pdpyras.APISession and pdpyras.EventsAPISession.

class pdpyras.PDSession(api_key, name=None)

Base class for making HTTP requests to PagerDuty APIs.

Instances of this class are essentially the same as requests.Session objects, but with a few modifications:

  • The client will reattempt the request with configurable, auto-increasing cooldown/retry intervals if encountering a network error or rate limit

  • When making requests, headers specified ad-hoc in calls to HTTP verb functions will not replace, but will be merged with, default headers.

  • The request URL, if it doesn’t already start with the REST API base URL, will be prepended with the default REST API base URL.

  • It will only perform requests with methods as given in the permitted_methods list, and will raise PDClientError for any other HTTP methods.

after_set_api_key()

Setter hook for setting or updating the API key.

Child classes should implement this to perform additional steps.

property api_key

API Key property getter.

Returns the _api_key attribute’s value.

property api_key_access

Memoized API key access type getter.

Will be “user” if the API key is a user-level token (all users should have permission to create an API key with the same permissions as they have in the PagerDuty web UI).

If the API key in use is an account-level API token (as only a global administrator user can create), this property will be “account”.

property auth_header

Generates the header with the API credential used for authentication.

log = None

A logging.Logger object for printing messages.

max_http_attempts = 10

The number of times that the client will retry after error statuses, for any that are defined greater than zero in retry.

max_network_attempts = 3

The number of times that connecting to the API will be attempted before treating the failure as non-transient; a PDClientError exception will be raised if this happens.

parent = None

The super object (requests.Session)

postprocess(response)

Perform supplemental actions immediately after receiving a response.

prepare_headers(method)

Append special additional per-request headers.

Parameters

method – The HTTP method, in upper case.

raise_if_http_error = True

Raise an exception upon receiving an error response from the server.

This affects iteration (in the REST API) as well as the generic request method itself.

In the general case: if set to True, then upon receiving a non-transient HTTP error (from too many retries), an exception will be raised. Otherwise, the response object will be returned.

In iteration: if set to true, an exception will be raised in iter_all if a HTTP error is encountered. This is the default behavior in versions >= 2.1.0. If False, the behavior is to halt iteration upon receiving a HTTP error.

request(method, url, **kwargs)

Make a generic PagerDuty API request.

Parameters
  • method (str) – The request method to use. Case-insensitive. May be one of get, put, post or delete.

  • url (str) – The path/URL to request. If it does not start with the base URL, the base URL will be prepended.

  • **kwargs

    Additional keyword arguments to pass to requests.Session.request

Returns

the HTTP response object

Return type

requests.Response

retry = None

A dict defining the retry behavior for each HTTP response status code.

Each key in this dictionary represents a HTTP response code. The behavior is specified by the value at each key as follows:

  • -1 to retry infinitely

  • 0 to return the requests.Response object and exit (which is the default behavior)

  • n, where n > 0, to retry n times (or up to max_http_attempts total for all statuses, whichever is encountered first), and raise a PDClientError after that many attempts. For each successive attempt, the wait time will increase by a factor of sleep_timer_base.

The default behavior is to retry infinitely on a 429, and return the response in any other case (assuming a HTTP response was received from the server).

set_api_key(api_key)

(Deprecated) set the API key/token.

Parameters

api_key (str) – The API key to use

sleep_timer = 1.5

Default initial cooldown time factor for rate limiting and network errors.

Each time that the request makes a followup request, there will be a delay in seconds equal to this number times sleep_timer_base to the power of how many attempts have already been made so far.

sleep_timer_base = 2

After each retry, the time to sleep before reattempting the API connection and request will increase by a factor of this amount.

property stagger_cooldown

Randomizing factor for wait times between retries during rate limiting.

If set to number greater than 0, the sleep time for rate limiting will (for each successive sleep) be adjusted by a factor of one plus a uniformly-distributed random number between 0 and 1 times this number, on top of the base sleep timer sleep_timer_base.

For example:

  • If this is 1, and sleep_timer_base is 2 (default), then after each status 429 response, the sleep time will change overall by a random factor between 2 and 4, whereas if it is zero, it will change by a factor of 2.

  • If sleep_timer_base is 1, then the cooldown time will be adjusted by a random factor between one and one plus this number.

If the number is set to zero, then this behavior is effectively disabled, and the cooldown factor (by which the sleep time is adjusted) will just be sleep_timer_base

Setting this to a nonzero number helps avoid the “thundering herd” effect that can potentially be caused by many API clients making simultaneous concurrent API requests and consequently waiting for the same amount of time before retrying. It is currently zero by default for consistent behavior with previous versions.

property trunc_key

Truncated key for secure display/identification purposes.

REST API Client

The APISession class inherits from pdpyras.PDSession and implements features specific to usage of the REST API.

class pdpyras.APISession(api_key, name=None, default_from=None, auth_type='token')

Reusable PagerDuty REST API session objects for making API requests.

Includes some convenience functions as well, i.e. rget, find and iter_all, to eliminate some repetitive tasks associated with making API calls.

Inherits from PDSession.

Parameters
  • api_key – REST API access token to use for HTTP requests

  • name (str or None) – Optional name identifier for logging. If unspecified or None, it will be the last four characters of the REST API token.

  • default_from (str or None) – Email address of a valid PagerDuty user to use in API requests by default as the From header (see: HTTP Request Headers)

Members

rdelete(self, path, **kw)
rget(self, path, **kw)
rpost(self, path, **kw)
rput(self, path, **kw)
after_set_api_key()

Setter hook for setting or updating the API key.

Child classes should implement this to perform additional steps.

api_call_counts = None

A dict object recording the number of API calls per endpoint

api_time = None

A dict object recording the total time of API calls to each endpoint

property auth_header

Generates the header with the API credential used for authentication.

property auth_type

Defines the method of API authentication.

By default this is “token”; if “oauth2”, the API key will be used.

default_from = None

The default value to use as the From request header

default_page_size = 100

This will be the default number of results requested in each page when iterating/querying an index (the limit parameter). See: pagination.

dict_all(path, **kw)

Returns a dictionary of all objects from a given index endpoint.

With the exception of by, all keyword arguments passed to this method are also passed to iter_all; see the documentation on that method for further details.

Parameters
  • path – The index endpoint URL to use.

  • by – The attribute of each object to use for the key values of the dictionary. This is id by default. Please note, there is no uniqueness validation, so if you use an attribute that is not distinct for the data set, this function will omit some data in the results.

  • params – Additional URL parameters to include.

  • paginate – If True, use pagination to get through all available results. If False, ignore / don’t page through more than the first 100 results. Useful for special index endpoints that don’t fully support pagination yet, i.e. “nested” endpoints like /users/{id}/contact_methods and /services/{id}/integrations

find(resource, query, attribute='name', params=None)

Finds an object of a given resource exactly matching a query.

Will query a given resource index endpoint using the query parameter supported by most indexes.

Returns a dict if a result is found. The structure will be that of an entry in the index endpoint schema’s array of results. Otherwise, it will return None if no result is found or an error is encountered.

Parameters
  • resource (str) – The name of the resource endpoint to query, i.e. escalation_policies

  • query (str) – The string to query for in the the index.

  • attribute (str) – The property of each result to compare against the query value when searching for an exact match. By default it is name, but when searching for user by email (for example) it can be set to email

  • params (dict or None) – Optional additional parameters to use when querying.

Return type

dict

iter_all(path, params=None, paginate=True, page_size=None, item_hook=None, total=False)

Iterator for the contents of an index endpoint or query.

Automatically paginates and yields the results in each page, until all matching results have been yielded or a HTTP error response is received.

Each yielded value is a dict object representing a result returned from the index. For example, if requesting the /users endpoint, each yielded value will be an entry of the users array property in the response; see: List Users

Parameters
  • path (str) – The index endpoint URL to use.

  • params (dict or None) – Additional URL parameters to include.

  • paginate (bool) – If True, use pagination to get through all available results. If False, ignore / don’t page through more than the first 100 results. Useful for special index endpoints that don’t fully support pagination yet, i.e. “nested” endpoints like (as of this writing): /users/{id}/contact_methods and /services/{id}/integrations

  • page_size (int or None) – 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.

  • item_hook – Callable object that will be invoked for each iteration, i.e. for printing progress. It will be called with three parameters: a dict representing a given result in the iteration, the number of the item, and the total number of items in the series.

  • total (bool) – If True, the total parameter will be included in API calls, and the value for the third parameter to the item hook will be the total count of records that match the query. Leaving this as False confers a small performance advantage, as the API in this case does not have to compute the total count of results in the query.

Yields

Results from the index endpoint.

Return type

dict

list_all(path, **kw)

Returns a list of all objects from a given index endpoint.

All keyword arguments passed to this function are also passed directly to iter_all; see the documentation on that method for details.

Parameters

path – The index endpoint URL to use.

postprocess(response, suffix=None)

Records performance information / request metadata about the API call.

This method is called automatically by request() for all requests, and can be extended in child classes.

Parameters
  • method (str) – Method of the request

  • response (requests.Response) – Response object

  • suffix (str or None) – Optional suffix to append to the key

prepare_headers(method)

Append special additional per-request headers.

Parameters

method – The HTTP method, in upper case.

profiler_key(method, path, suffix=None)

Generates a fixed-format key to classify a request, i.e. for profiling.

Returns a string that will have all instances of IDs replaced with {id}, and will begin with the method in lower case followed by a colon, i.e. get:escalation_policies/{id}

Parameters
  • method (str) – The request method

  • path (str) – The path/URI to classify

  • suffix (str) – Optional suffix to append to the key

Return type

str

property subdomain

Subdomain of the PagerDuty account of the API access token.

Type

str or None

property total_call_count

The total number of API calls made by this instance.

property total_call_time

The total time spent making API calls.

property trunc_token

Truncated token for secure display/identification purposes.

url = 'https://api.pagerduty.com'

Base URL of the REST API

Events API Client

Class EventsAPISession implements methods for submitting events to PagerDuty through the Events API and inherits from pdpyras.PDSession.

class pdpyras.EventsAPISession(api_key, name=None)

Session class for submitting events to the PagerDuty v2 Events API.

Provides methods for submitting events to the Events API.

Inherits from PDSession.

acknowledge(dedup_key)

Acknowledge an alert via Events API.

Parameters

dedup_key – The deduplication key of the alert to set to the acknowledged state.

property auth_header

Generates the header with the API credential used for authentication.

prepare_headers(method)

Add user agent and content type headers for Events API requests.

resolve(dedup_key)

Resolve an alert via Events API.

Parameters

dedup_key – The deduplication key of the alert to resolve.

send_event(action, dedup_key=None, **properties)

Send an event to the v2 Events API.

See: https://v2.developer.pagerduty.com/docs/send-an-event-events-api-v2

Parameters
  • action (str) – The action to perform through the Events API: trigger, acknowledge or resolve.

  • dedup_key (str) – The deduplication key; used for determining event uniqueness and associating actions with existing incidents.

  • **properties – Additional properties to set, i.e. if action is trigger this would include payload

Returns

The deduplication key of the incident, if any.

trigger(summary, source, dedup_key=None, severity='critical', payload=None, custom_details=None, images=None, links=None)

Trigger an incident

Parameters
  • summary (str) – Summary / brief description of what is wrong.

  • source (str) – A human-readable name identifying the system that is affected.

  • dedup_key (str) – The deduplication key; used for determining event uniqueness and associating actions with existing incidents.

  • severity (str) – Alert severity. Sets the payload.severity property.

  • payload (dict) – Set the payload directly. Can be used in conjunction with other parameters that also set payload properties; these properties will be merged into the default payload, and any properties in this parameter will take precedence except with regard to custom_details.

  • custom_details (dict) – The payload.custom_details property of the payload. Will override the property set in the payload parameter if given.

  • images (list) – Set the images property of the event.

  • links (list) – Set the links property of the event.

Return type

str

Errors

class pdpyras.PDClientError(message, response=None)

General API errors base class.

response = None

The HTTP response object, if a response was successfully received.

In the case of network errors, this property will be None.

Functions

pdpyras.auto_json(method)

Function decorator that makes methods return the full response JSON

pdpyras.last_4(secret)

Returns an abbreviation of the input

pdpyras.object_type(r_name)

Derives an object type (i.e. user) from a resource name (i.e. users)

Parameters

r_name – Resource name, i.e. would be users for the resource index URL https://api.pagerduty.com/users

Returns

The object type name; usually the type property of an instance of the given resource.

Return type

str

pdpyras.raise_on_error(r)

Raise an exception if a HTTP error response has error status.

Parameters

r (requests.Response) – Response object corresponding to the response received.

Returns

The response object, if its status was success

Return type

requests.Response

pdpyras.random() → x in the interval [0, 1).
pdpyras.resource_envelope(method)

Convenience and consistency decorator for HTTP verb functions.

This makes the request methods GET, POST and PUT always return a dictionary object representing the resource at the envelope property (i.e. the {...} from {"escalation_policy":{...}} in a get/put request to an escalation policy) rather than a requests.Response object.

Methods using this decorator will raise a PDClientError with its response property being being the requests.Response object in the case of any error, so that the implementer can access it by catching the exception, and thus design their own custom logic around different types of error responses.

It allows creation of methods that can provide more succinct ways of making API calls. In particular, the methods using this decorator don’t require checking for a success status, JSON-decoding the response body and then pulling the essential data out of the envelope (i.e. for GET /escalation_policies/{id} one would have to access the escalation_policy property of the object decoded from the response body, assuming nothing went wrong in the whole process).

These methods are APISession.rget, APISession.rpost and APISession.rput.

Parameters

method – Method being decorated. Must take one positional argument after self that is the URL/path to the resource, and must return an object of class requests.Response, and be named after the HTTP method but with “r” prepended.

Returns

A callable object; the reformed method

pdpyras.resource_name(obj_type)

Transforms an object type into a resource name

Parameters

obj_type – The object type, i.e. user or user_reference

Returns

The name of the resource, i.e. the last part of the URL for the resource’s index URL

Return type

str

pdpyras.tokenize_url_path(url, baseurl='https://api.pagerduty.com')

Classifies a URL according to some global patterns in the API.

If the URL is to access a specific individual resource by ID, the node type will be identified as {id}, whereas if it is an index, it will be identified as {index}.

For instance, https://api.pagerduty.com/users would be classified as ("users", "{index}"), and https://api.pagerduty.com/users/PABC123 would be classified as ("users", "{id}")

Parameters
  • url (str) – The URL (or path) to be classified; the function should accept either

  • baseurl (str) – API base URL

Return type

tuple

pdpyras.try_decoding(r)

JSON-decode a response body and raise PDClientError if it fails.

Parameters

rrequests.Response object

Changelog

2020-02-04: Version 4.0

  • Added support for using OAuth 2 access tokens to authenticate (#23)

  • Added a property that indicates the access level/scope of a given API credential (#22)

2020-01-10: version 3.2.1

2019-10-31: version 3.2

  • The page size (limit) parameter can now be set on a per-call basis in any of the *_all methods (i.e. PDSession.iter_all) by passing the page_size keyword argument. If the argument is not present, the default page size will be used.

  • The X-Request-Id header in responses is now captured in log messages to make it easier to identify API calls when communicating with PagerDuty Support

  • Extended API call metadata is also now logged.

  • The cooldown time between rate limit responses can optionally be randomized by setting PDSession.stagger_cooldown to a positive number.

2019-10-01: version 3.1.2

  • Fixed regression bug / departure from documentation (#17): the payload parameter does not merge with but rather completely replaces the default payload

2019-04-05: version 3.1.1

  • Changed behavior of HTTP retry that caused issues with some internal tools: raising PDClientError in the event of non-transient HTTP error, in the request method, versus returning the request object and logging it. The previous behavior was:
    • Not the intended design

    • At odds with the documentated behavior

2019-04-05: version 3.1:

  • Introduction of a custom User-Agent header to distinguish the API client as such, for the purposes of usage analytics

2019-04-02: version 3.0.2:

Important bug fixes to the custom HTTP retry logic:

  • Fixed KeyError in APISession.request

  • Fixed incorrect behavior (retrying more than the specified amount of times) due to faulty comparison logic

2019-03-14: version 3.0.1:

A light Events API client methods refactor:

  • All keyword arguments specific to sending trigger events have been refactored out of the generic EventsAPISession.send_event method

  • Now, instead, send_event and uses a catch-all keyword argument to set event properties.

  • The keyword arguments specific to triggering incidents are in the method EventsAPISession.trigger method.

2019-03-12: version 3.0:

  • Added new Events API session class that still has most of the same functional features as the REST API session class.

2019-01-28: version 2.4.1:

  • Fixed bug: unpacking wrapped entities does not work with /log_entries

2019-01-10: version 2.4:

  • Whitelisting of endpoints supported by the r* / *_all methods has been rescinded, and documentation has been updated with how to identify endpoints that these methods can be used with.

2019-01-03: version 2.3:

  • More helpful error messaging when using r* / *_all methods on endpoints they don’t support

  • Resource envelope auto-unpacking no longer validates for the presence of a type property in order to support posting to business impact metrics

2018-12-04: version 2.2:

  • Methods list_all and dict_all turn all results from an index into a list/dict to save a bit of effort

2018-11-28: version 2.1:

  • Support for performing multi-update actions (i.e. Manage Incidents) via the rput method.

  • The default behavior of iter_all is now to raise an exception if an error response is received from the API during iteration.

Changelog Started 2018-11-28