Contribution Guide
Bug reports and pull requests to fix issues are always welcome, as are contributions to the built-in documentation.
If adding features, or making changes, it is recommended to update or add tests
and assertions to the appropriate test case class in test_pagerduty.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.
Initial Setup and Unit Tests
To be able to rebuild the documentation, apply formatting and release new versions, first make sure you have make installed in your local development environment, as well as [uv](https://docs.astral.sh/uv/) for dependency management.
Next, run test.sh in the root path of the repository to validate that unit
tests can be run locally.
Maintaining Entity Wrapper Configuration
Typically, but not for all endpoints, the key (“wrapper name”) in the request or response schema can be inferred from the last or second to last node of the endpoint URL’s path. The wrapper name is a singular noun for an individual resource or plural for a collection of resources.
When new endpoints are added to REST API v2, and they don’t follow this orthodox schema pattern, the client’s pagination and entity wrapping methods have no a-priori way of supporting them because the wrapper name cannot be inferred from the endpoint path.
Introduction
To support the growing list of schema antipatterns in the PagerDuty product
REST API v2, a system was created (originally in pdpyras version 5.0.0) to
work around them and codify the deviations from orthodox patterns with minimal
hard-coded changes to the client. This system works by identifying endpoints
according to their “canonical path”, that is to say the path portion of the
endpoint URL with {name} placeholders for variable/identifiers. The
canonical path is then used as an identifier to perform a hash lookup of
antipattern-handling configuration.
This system requires two global variables that must be manually maintained:
pagerduty.rest_api_v2_client.CANONICAL_PATHS, the list of canonical pathspagerduty.rest_api_v2_client.ENTITY_WRAPPER_CONFIG, a dictionary of exceptions to entity wrapping and schema conventions
Limitations
There are three main categories of antipatterns:
Entity wrapping is present but doesn’t follow the original schema convention
There may or may not be wrapping but pagination is not implemented according to standards
There is no entity wrapping
In the first case, If the endpoint’s schema wraps entities but the wrapper name doesn’t follow from the path, entity wrapping can still be supported. If classic pagination or cursor-based pagination is correctly implemented in the new API, the automatic pagination methods can also support it once the antipattern configuration entry is added.
However, if there is no entity wrapping, or pagination is not implemented according to documented standards, automatic pagination cannot be supported for resource collection endpoints.
Updating the Canonical Path Set
The first step for adding support for new APIs is to have a copy of the API
Reference source code (this is a private GitHub repository owned by the
PagerDuty org). The script scripts/get_path_list/get_path_list.py can then
be used to automatically generate definitions of the global variables
pagerduty.rest_api_v2_client.CANONICAL_PATHS and
pagerduty.rest_api_v2_client.CURSOR_BASED_PAGINATION_PATHS that can be copied into the
source code to replace the existing definitions. The script takes one argument:
a path to the file reference/v2/Index.yaml within the reference source
repository.
Evaluating New Endpoints For Support
The next step is to look at the request and response schemas in the API
reference for each new endpoint added to the canonical path list, to see if it
follows classic schema conventions for entity wrapping. If any new path does
not, adding support for it will also require adding entries to
pagerduty.rest_api_v2_client.ENTITY_WRAPPER_CONFIG. “Classic schema conventions” refers to
the logic codified in pagerduty.infer_entity_wrapper and
pagerduty.unwrap (where a “node” is a component of the path component
of the URL, separated by forward slashes):
1: If the last node of the path is an opaque identifier, then the path corresponds
to an individual PagerDuty resource, and the request and response wrapper names
are both the singular form of the second-to-last node. Examples: PUT
/escalation_policies/{id} (wrapper = escalation_policy), GET
/users/{id} (wrapper = user).
2: If the last node of the path is not an opaque identifier, and the
request method is POST, then the request and response wrapper names are both
the singular form of the last node. Examples: POST /schedules (wrapper =
schedule), POST /incidents (wrapper = incident)
3: Otherwise (the last node of the path is not an opaque identifier and the
request method is not POST), the request and response wrapper names are both
the same as the last node of the path. Examples: GET /services (wrapper =
services), PUT /incidents (wrapper = incidents)
If all of the above apply to new endpoints for all request methods, then no new
entries need to be added to
pagerduty.rest_api_v2_client.ENTITY_WRAPPER_CONFIG to support them;
they are supported automatically by virtue of following preexisting
already-supported API patterns. Their corresponding entries in
pagerduty.rest_api_v2_client.CANONICAL_PATHS officiates their support
for entity-wrapping-aware functions.
Adding Support for Non-Conforming Endpoints
If the new endpoints do not follow classic schema conventions for entity
wrapping, entries for them must be added to
pagerduty.rest_api_v2_client.ENTITY_WRAPPER_CONFIG in order to support them. As described
in the documentation of that attribute, each key is a combination of the
request method (or “*” for the configuration entry to apply to all methods) and
the canonical path in question, and each value is a string (for the same
wrapper name in the request and response bodies), None if entity wrapping
is not applicable, and a tuple if the entity wrapping differs between the
request and response bodies.
Following the same examples as given in the User Guide: the entry in
pagerduty.rest_api_v2_client.ENTITY_WRAPPER_CONFIG to handle the “Create Business Service
Subscribers” looks like this:
'POST /business_services/{id}/subscribers': ('subscribers', 'subscriptions'),
The “Create one or more overrides” API endpoint entry looks like this:
'POST /schedules/{id}/overrides': ('overrides', None),
Updating Documentation
The .rst files in sphinx/source are where most of the documentation
lives. To rebuild the HTML documentation from the source, run:
make docs
To force a rebuild, run touch CHANGELOG.rst first.
NOTE: Python version 3.13 or later must be used when rebuilding
documentation, or the version number in the documentation will be
[V].?.?-metadata-unavailable, where [V] is the current major version.
Releasing a New Version
For this process, you will need, at minimum:
to run
make buildand commit changes touv.lockbefore merging, to validate that building succeeds,the ability to create tags on the repository
valid user accounts on both
pypi.organdtest.pypi.orgthat have the “Maintainer” role on the project, as well as the requirements installed (see above) and:an API token to upload new releases to PyPI, with write access to the project.
To use a token for uv publish (which will be invoked in this process), set the
environment variable UV_PUBLISH_PASSWORD when running publish or
test-publish commands, i.e.
Run
read -s UV_PUBLISH_PASSWORDto set the variable without echoing the tokenPaste in the token and hit enter
Immediately afterwards, run
export !$
Perform end-to-end publish and installation testing
This series of tests may not always be necessary, but it is a good idea to perform them when making significant or breaking changes. This testing step will to ensure that installation and upgrading isn’t broken, and thus help avert a scenario where we have to yank a version because it breaks projects.
To test publishing and installing from the package index, first make sure you
have a valid user account on test.pypi.org that has publisher access to the
project as on pypi.org. When ready to begin, set the environemnt variable
token for the test index as instructed above (it will differ from the live
pypi.org index).
Note, once a release is uploaded, it is no longer possible to upload a release
with the same version number, even if that release is deleted. For that reason,
it is a good idea to first add a suffix that can be arbitrarily updated to
iterate, i.e. -rc1, to the version in pyproject.toml while testing,
and then revert the changes (including changes to uv.lock) when done.
Once the above is done, to perform end-to-end tests, run make testpublish,
which will perform the following:
Build the Python package at the test version
Upload the new arbitrary version to
test.pypi.orgTest-install the library from
test.pypi.orginto a temporary Python virtualenv that does not already have the library installed, to test installing for the first timeTest-install the library from
test.pypi.orginto a temporary Python virtualenv where the mainline library version is already installed, to test upgrading an existing install.
The script should print out the test version and success messages. Errors
should be investigated and mitigated before publishing. To test again,
temporarily change the version in pyproject.toml so that it counts as a new
release and gets uploaded. Be sure to remember to set it back to the desired
final version before the actual release, and revert any changes to uv.lock.
Merge changes and tag
A pull request for releasing a new version should be created, which along with the functional changes should also include at least:
An update to
CHANGELOG.rstdescribing the changes in the new releaseA change in the version number in
pyproject.tomlto a new version that follows Semantic Versioning.Rebuilt HTML documentation via
make docs.
After rebuilding the documentation, it can then be viewed by opening the file
docs/index.html in a web browser. Including rebuilt documentation avoids
the need for a follow-up pull request with a doc rebuild, but also helps
reviewers by not requiring them to have the documentation-building tools.
Remember to commit any changes to docs/ and uv.lock before merging.
Once the pull request is approved, merge. Then (locally) checkout main and tag,
with the format v{version}, i.e. v6.1.0, and push the tag i.e. with
git push --tags.
Publishing
Once the changes are merged and tagged, make sure your local repository clone
has the main branch checked out at the latest available commit, and the
local file tree is clean (has no uncommitted changes). Then, set the publish
token environment variable as described above and run:
make publish
Finally, create a new release, and select the latest tag. details:
Select “Choose a tag” and select the new latest tag.
If a new patch version is being released, update the existing release for that major and minor version.