imbi-plugin-pagerduty 2.11.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- imbi_plugin_pagerduty-2.11.0/.github/workflows/publish.yml +45 -0
- imbi_plugin_pagerduty-2.11.0/.github/workflows/test.yml +54 -0
- imbi_plugin_pagerduty-2.11.0/.gitignore +10 -0
- imbi_plugin_pagerduty-2.11.0/.pre-commit-config.yaml +39 -0
- imbi_plugin_pagerduty-2.11.0/CLAUDE.md +87 -0
- imbi_plugin_pagerduty-2.11.0/LICENSE +28 -0
- imbi_plugin_pagerduty-2.11.0/PKG-INFO +49 -0
- imbi_plugin_pagerduty-2.11.0/README.md +30 -0
- imbi_plugin_pagerduty-2.11.0/justfile +59 -0
- imbi_plugin_pagerduty-2.11.0/pyproject.toml +123 -0
- imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/__init__.py +11 -0
- imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/_client.py +91 -0
- imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/_services.py +89 -0
- imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/incidents.py +117 -0
- imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/lifecycle.py +409 -0
- imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/models.py +56 -0
- imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/py.typed +0 -0
- imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/webhook.py +47 -0
- imbi_plugin_pagerduty-2.11.0/tests/__init__.py +0 -0
- imbi_plugin_pagerduty-2.11.0/tests/test_client.py +52 -0
- imbi_plugin_pagerduty-2.11.0/tests/test_incidents.py +118 -0
- imbi_plugin_pagerduty-2.11.0/tests/test_lifecycle.py +285 -0
- imbi_plugin_pagerduty-2.11.0/tests/test_models.py +55 -0
- imbi_plugin_pagerduty-2.11.0/tests/test_services.py +92 -0
- imbi_plugin_pagerduty-2.11.0/tests/test_webhook.py +18 -0
- imbi_plugin_pagerduty-2.11.0/uv.lock +2058 -0
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
name: Publish to PyPI
|
|
2
|
+
on:
|
|
3
|
+
release:
|
|
4
|
+
types: [published]
|
|
5
|
+
|
|
6
|
+
permissions:
|
|
7
|
+
id-token: write
|
|
8
|
+
|
|
9
|
+
jobs:
|
|
10
|
+
build:
|
|
11
|
+
runs-on: ubuntu-latest
|
|
12
|
+
steps:
|
|
13
|
+
- name: Checkout repository
|
|
14
|
+
uses: actions/checkout@v6
|
|
15
|
+
with:
|
|
16
|
+
fetch-depth: 0
|
|
17
|
+
|
|
18
|
+
- name: Install uv
|
|
19
|
+
uses: astral-sh/setup-uv@v5
|
|
20
|
+
|
|
21
|
+
- name: Set up Python
|
|
22
|
+
run: uv python install 3.14
|
|
23
|
+
|
|
24
|
+
- name: Build
|
|
25
|
+
run: uv build
|
|
26
|
+
|
|
27
|
+
- name: Upload dist
|
|
28
|
+
uses: actions/upload-artifact@v4
|
|
29
|
+
with:
|
|
30
|
+
name: dist
|
|
31
|
+
path: dist/
|
|
32
|
+
|
|
33
|
+
publish:
|
|
34
|
+
needs: build
|
|
35
|
+
runs-on: ubuntu-latest
|
|
36
|
+
environment: pypi
|
|
37
|
+
steps:
|
|
38
|
+
- name: Download dist
|
|
39
|
+
uses: actions/download-artifact@v4
|
|
40
|
+
with:
|
|
41
|
+
name: dist
|
|
42
|
+
path: dist/
|
|
43
|
+
|
|
44
|
+
- name: Publish package
|
|
45
|
+
uses: pypa/gh-action-pypi-publish@release/v1
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
name: Testing
|
|
2
|
+
on:
|
|
3
|
+
pull_request:
|
|
4
|
+
push:
|
|
5
|
+
branches: [ "main" ]
|
|
6
|
+
paths-ignore:
|
|
7
|
+
- 'docs/**'
|
|
8
|
+
- '*.md'
|
|
9
|
+
tags-ignore: [ "*" ]
|
|
10
|
+
|
|
11
|
+
jobs:
|
|
12
|
+
lint:
|
|
13
|
+
name: "Static Analysis"
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
steps:
|
|
16
|
+
- name: Checkout repository
|
|
17
|
+
uses: actions/checkout@v5
|
|
18
|
+
- name: Install just
|
|
19
|
+
uses: extractions/setup-just@v2
|
|
20
|
+
with:
|
|
21
|
+
just-version: '1.47.1'
|
|
22
|
+
- name: Install uv
|
|
23
|
+
uses: astral-sh/setup-uv@v7
|
|
24
|
+
with:
|
|
25
|
+
enable-cache: true
|
|
26
|
+
- name: Run linters
|
|
27
|
+
run: just lint
|
|
28
|
+
|
|
29
|
+
test:
|
|
30
|
+
name: "Automated Tests"
|
|
31
|
+
runs-on: ubuntu-latest
|
|
32
|
+
strategy:
|
|
33
|
+
fail-fast: false
|
|
34
|
+
matrix:
|
|
35
|
+
python: [ "3.14" ]
|
|
36
|
+
steps:
|
|
37
|
+
- name: Checkout repository
|
|
38
|
+
uses: actions/checkout@v5
|
|
39
|
+
- name: Install just
|
|
40
|
+
uses: extractions/setup-just@v2
|
|
41
|
+
with:
|
|
42
|
+
just-version: '1.47.1'
|
|
43
|
+
- name: Install uv
|
|
44
|
+
uses: astral-sh/setup-uv@v7
|
|
45
|
+
with:
|
|
46
|
+
python-version: "${{ matrix.python }}"
|
|
47
|
+
enable-cache: true
|
|
48
|
+
- name: Test with python ${{ matrix.python }}
|
|
49
|
+
run: just test
|
|
50
|
+
- name: Upload Coverage
|
|
51
|
+
uses: codecov/codecov-action@v5
|
|
52
|
+
with:
|
|
53
|
+
files: ./build/coverage.xml
|
|
54
|
+
flags: unittests
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
repos:
|
|
2
|
+
- repo: https://github.com/pre-commit/pre-commit-hooks
|
|
3
|
+
rev: v5.0.0
|
|
4
|
+
hooks:
|
|
5
|
+
- id: trailing-whitespace
|
|
6
|
+
- id: end-of-file-fixer
|
|
7
|
+
- id: check-yaml
|
|
8
|
+
- id: check-added-large-files
|
|
9
|
+
- id: check-merge-conflict
|
|
10
|
+
- id: check-toml
|
|
11
|
+
- id: debug-statements
|
|
12
|
+
|
|
13
|
+
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
14
|
+
rev: v0.14.6
|
|
15
|
+
hooks:
|
|
16
|
+
- id: ruff-check
|
|
17
|
+
args: [--fix]
|
|
18
|
+
- id: ruff-format
|
|
19
|
+
|
|
20
|
+
- repo: https://github.com/tombi-toml/tombi-pre-commit
|
|
21
|
+
rev: v0.7.25
|
|
22
|
+
hooks:
|
|
23
|
+
- id: tombi-format
|
|
24
|
+
# uv owns uv.lock's formatting; tombi reflows it (2- vs 4-space),
|
|
25
|
+
# producing whole-file churn on every dependency change.
|
|
26
|
+
exclude: ^uv\.lock$
|
|
27
|
+
|
|
28
|
+
- repo: local
|
|
29
|
+
hooks:
|
|
30
|
+
- id: basedpyright
|
|
31
|
+
name: basedpyright
|
|
32
|
+
language: system
|
|
33
|
+
types: [python]
|
|
34
|
+
pass_filenames: false
|
|
35
|
+
entry: uv
|
|
36
|
+
args:
|
|
37
|
+
- run
|
|
38
|
+
- basedpyright
|
|
39
|
+
- src
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
# CLAUDE.md
|
|
2
|
+
|
|
3
|
+
This file provides guidance to Claude Code (claude.ai/code) when working
|
|
4
|
+
with code in this repository.
|
|
5
|
+
|
|
6
|
+
## What This Is
|
|
7
|
+
|
|
8
|
+
PagerDuty plugins for the Imbi platform, distributed as a single Python
|
|
9
|
+
package (`imbi_plugin_pagerduty`). It ships **three plugin types** —
|
|
10
|
+
lifecycle, webhook, incidents — discovered by the Imbi host through the
|
|
11
|
+
`imbi.plugins` entry points in `pyproject.toml`. That entry-point table
|
|
12
|
+
is the registration surface: adding a plugin class means adding an entry
|
|
13
|
+
point there.
|
|
14
|
+
|
|
15
|
+
All plugin base classes (`LifecyclePlugin`, `WebhookActionPlugin`,
|
|
16
|
+
`IncidentsPlugin`, `PluginContext`, `PluginManifest`, the result /
|
|
17
|
+
writeback types) come from `imbi_common.plugins.base`. Read that module
|
|
18
|
+
(in the sibling `imbi-common` repo) before changing a plugin's method
|
|
19
|
+
signatures.
|
|
20
|
+
|
|
21
|
+
Unlike the GitHub plugins, **PagerDuty is cloud-only**: a single
|
|
22
|
+
`api.pagerduty.com` host and one REST API key credential, so there is no
|
|
23
|
+
host-flavor base/subclass pattern — one concrete class per plugin type.
|
|
24
|
+
|
|
25
|
+
## Commands
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
just setup # uv sync --all-groups --all-extras + pre-commit hooks
|
|
29
|
+
just test # coverage run -m pytest (fails under 85%)
|
|
30
|
+
just test tests/test_lifecycle.py # one file (no coverage)
|
|
31
|
+
just lint # pre-commit (ruff, ruff-format, tombi, basedpyright)
|
|
32
|
+
just format [FILES] # ruff-format + tombi-format
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
Python is pinned to **3.14**; ruff uses single quotes and a 79-char line
|
|
36
|
+
length; type checking is `basedpyright` in **strict** mode over `src`.
|
|
37
|
+
|
|
38
|
+
## Architecture
|
|
39
|
+
|
|
40
|
+
- `_client.py` — the shared PagerDuty REST client factory
|
|
41
|
+
(`api.pagerduty.com`, `Authorization: Token token=<key>`). An httpx
|
|
42
|
+
response hook maps `401` → `PluginAuthenticationFailed` and `429` →
|
|
43
|
+
`PluginRateLimited` (absolute resume epoch from `ratelimit-reset`).
|
|
44
|
+
- `_services.py` — resolves a project's PagerDuty service: the
|
|
45
|
+
`pagerduty-service` link first, then an exact-name lookup on the
|
|
46
|
+
project slug (and the pre-rename slug).
|
|
47
|
+
- `lifecycle.py` — `PagerDutyLifecyclePlugin`. Manages the service +
|
|
48
|
+
escalation-policy routing (`team_escalation_policy_mapping`) + a
|
|
49
|
+
per-service V3 webhook subscription. Writes results through
|
|
50
|
+
`ctx.service_writeback`: the host persists the `EXISTS_IN` edge
|
|
51
|
+
(service id + the **encrypted** webhook signing secret the gateway
|
|
52
|
+
verifies against) and the `pagerduty-service` dashboard link. The
|
|
53
|
+
subscription's signing secret is returned by PagerDuty **only once**,
|
|
54
|
+
at creation, and is encrypted (via `imbi_common` `TokenEncryption`)
|
|
55
|
+
before it leaves the plugin. `on_project_deleted` finds the
|
|
56
|
+
subscription by listing `/webhook_subscriptions` and matching
|
|
57
|
+
`filter.id` (no subscription id is persisted).
|
|
58
|
+
- `webhook.py` — `PagerDutyWebhookPlugin`, a v1 stub (`actions() -> []`);
|
|
59
|
+
incident events are captured by the gateway's built-in recording.
|
|
60
|
+
- `incidents.py` — `PagerDutyIncidentsPlugin.list_incidents`, a paginated
|
|
61
|
+
live query of `/incidents` for the project's service.
|
|
62
|
+
- `models.py` — maps PagerDuty incident payloads to `IncidentView`.
|
|
63
|
+
|
|
64
|
+
### PagerDuty-API assumptions to confirm against a live tenant
|
|
65
|
+
|
|
66
|
+
- The create-subscription response exposes the signing secret at
|
|
67
|
+
`webhook_subscription.delivery_method.secret`.
|
|
68
|
+
- A service's webhook subscriptions are found by listing
|
|
69
|
+
`/webhook_subscriptions` and matching `filter.id` (there is no
|
|
70
|
+
documented server-side service filter).
|
|
71
|
+
- `/incidents` paginates via `offset`/`more`; the opaque tab cursor is
|
|
72
|
+
the next `offset`.
|
|
73
|
+
|
|
74
|
+
## Testing
|
|
75
|
+
|
|
76
|
+
Tests mock PagerDuty's HTTP with **respx** (`asyncio_mode = auto`, so
|
|
77
|
+
async tests need no decorator). Build a `PluginContext` with the
|
|
78
|
+
relevant project links / options and pass `{'api_key': ...}`
|
|
79
|
+
credentials, mirroring how the host calls in. Coverage must stay ≥ 85%.
|
|
80
|
+
|
|
81
|
+
## Dependency Management
|
|
82
|
+
|
|
83
|
+
**NEVER edit `pyproject.toml` dependencies by hand** — use `uv add` /
|
|
84
|
+
`uv remove`. The package pins to PyPI with a 7-day `exclude-newer`
|
|
85
|
+
holdback; `imbi-common` is tracked from git `main` until 2.11 ships (see
|
|
86
|
+
the `[tool.uv.sources]` comment), then the pin moves to `==2.11.x`. After
|
|
87
|
+
changing dependencies, run `uv sync` and verify `uv.lock`.
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
BSD 3-Clause License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Imbi
|
|
4
|
+
|
|
5
|
+
Redistribution and use in source and binary forms, with or without
|
|
6
|
+
modification, are permitted provided that the following conditions are met:
|
|
7
|
+
|
|
8
|
+
1. Redistributions of source code must retain the above copyright notice, this
|
|
9
|
+
list of conditions and the following disclaimer.
|
|
10
|
+
|
|
11
|
+
2. Redistributions in binary form must reproduce the above copyright notice,
|
|
12
|
+
this list of conditions and the following disclaimer in the documentation
|
|
13
|
+
and/or other materials provided with the distribution.
|
|
14
|
+
|
|
15
|
+
3. Neither the name of the copyright holder nor the names of its
|
|
16
|
+
contributors may be used to endorse or promote products derived from
|
|
17
|
+
this software without specific prior written permission.
|
|
18
|
+
|
|
19
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
|
|
20
|
+
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
|
|
21
|
+
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
|
|
22
|
+
DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
|
|
23
|
+
FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
24
|
+
DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
|
|
25
|
+
SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
|
|
26
|
+
CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
|
|
27
|
+
OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
|
|
28
|
+
OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: imbi-plugin-pagerduty
|
|
3
|
+
Version: 2.11.0
|
|
4
|
+
Summary: PagerDuty lifecycle, webhook, and incidents plugins for Imbi
|
|
5
|
+
Author-email: "Gavin M. Roy" <gavinr@aweber.com>
|
|
6
|
+
License: BSD-3-Clause
|
|
7
|
+
License-File: LICENSE
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Natural Language :: English
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
14
|
+
Requires-Python: >=3.14
|
|
15
|
+
Requires-Dist: httpx>=0.27
|
|
16
|
+
Requires-Dist: imbi-common[databases]==2.11.0
|
|
17
|
+
Requires-Dist: pydantic>=2
|
|
18
|
+
Description-Content-Type: text/markdown
|
|
19
|
+
|
|
20
|
+
# imbi-plugin-pagerduty
|
|
21
|
+
|
|
22
|
+
PagerDuty plugins for the [Imbi](https://github.com/AWeber-Imbi) platform,
|
|
23
|
+
distributed as a single Python package (`imbi_plugin_pagerduty`) shipping
|
|
24
|
+
three plugin types:
|
|
25
|
+
|
|
26
|
+
- **`pagerduty-lifecycle`** — provisions and maintains a PagerDuty
|
|
27
|
+
*service* for each project, routed to the owning team's escalation
|
|
28
|
+
policy (via the `team_escalation_policy_mapping` option), and a
|
|
29
|
+
per-service V3 webhook subscription back to Imbi.
|
|
30
|
+
- **`pagerduty-webhook`** — receives PagerDuty incident webhooks. v1
|
|
31
|
+
records events through the gateway and advertises no custom actions.
|
|
32
|
+
- **`pagerduty-incidents`** — live-queries PagerDuty for the incidents on
|
|
33
|
+
a project's service for the project-detail Incidents tab.
|
|
34
|
+
|
|
35
|
+
The Imbi host discovers these through the `imbi.plugins` entry points in
|
|
36
|
+
`pyproject.toml`. All plugin base classes come from
|
|
37
|
+
`imbi_common.plugins.base`.
|
|
38
|
+
|
|
39
|
+
## Development
|
|
40
|
+
|
|
41
|
+
```bash
|
|
42
|
+
just setup # uv sync + pre-commit hooks
|
|
43
|
+
just test # coverage (fails under 85%)
|
|
44
|
+
just lint # ruff, ruff-format, basedpyright
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
Authentication is a PagerDuty REST API key (`auth_type='api_token'`),
|
|
48
|
+
configured as an encrypted plugin credential. PagerDuty is cloud-only, so
|
|
49
|
+
there is no host-flavor routing.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# imbi-plugin-pagerduty
|
|
2
|
+
|
|
3
|
+
PagerDuty plugins for the [Imbi](https://github.com/AWeber-Imbi) platform,
|
|
4
|
+
distributed as a single Python package (`imbi_plugin_pagerduty`) shipping
|
|
5
|
+
three plugin types:
|
|
6
|
+
|
|
7
|
+
- **`pagerduty-lifecycle`** — provisions and maintains a PagerDuty
|
|
8
|
+
*service* for each project, routed to the owning team's escalation
|
|
9
|
+
policy (via the `team_escalation_policy_mapping` option), and a
|
|
10
|
+
per-service V3 webhook subscription back to Imbi.
|
|
11
|
+
- **`pagerduty-webhook`** — receives PagerDuty incident webhooks. v1
|
|
12
|
+
records events through the gateway and advertises no custom actions.
|
|
13
|
+
- **`pagerduty-incidents`** — live-queries PagerDuty for the incidents on
|
|
14
|
+
a project's service for the project-detail Incidents tab.
|
|
15
|
+
|
|
16
|
+
The Imbi host discovers these through the `imbi.plugins` entry points in
|
|
17
|
+
`pyproject.toml`. All plugin base classes come from
|
|
18
|
+
`imbi_common.plugins.base`.
|
|
19
|
+
|
|
20
|
+
## Development
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
just setup # uv sync + pre-commit hooks
|
|
24
|
+
just test # coverage (fails under 85%)
|
|
25
|
+
just lint # ruff, ruff-format, basedpyright
|
|
26
|
+
```
|
|
27
|
+
|
|
28
|
+
Authentication is a PagerDuty REST API key (`auth_type='api_token'`),
|
|
29
|
+
configured as an encrypted plugin credential. PagerDuty is cloud-only, so
|
|
30
|
+
there is no host-flavor routing.
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
[default]
|
|
2
|
+
[private]
|
|
3
|
+
@help:
|
|
4
|
+
just --list
|
|
5
|
+
|
|
6
|
+
[private]
|
|
7
|
+
ci: lint test
|
|
8
|
+
|
|
9
|
+
[doc("Set up your development environment")]
|
|
10
|
+
[group("Environment")]
|
|
11
|
+
setup:
|
|
12
|
+
uv sync --all-groups --all-extras --frozen
|
|
13
|
+
uv run pre-commit install --install-hooks --overwrite
|
|
14
|
+
|
|
15
|
+
[doc("Run tests")]
|
|
16
|
+
[group("Testing")]
|
|
17
|
+
test *FILES: setup
|
|
18
|
+
#!/usr/bin/env sh
|
|
19
|
+
set -e
|
|
20
|
+
env_args=""
|
|
21
|
+
if [ -f .env ]; then
|
|
22
|
+
env_args="--env-file .env"
|
|
23
|
+
fi
|
|
24
|
+
if [ -z "{{ FILES }}" ]; then
|
|
25
|
+
uv run $env_args coverage run -m pytest tests
|
|
26
|
+
uv run coverage report
|
|
27
|
+
uv run coverage xml -o build/coverage.xml
|
|
28
|
+
else
|
|
29
|
+
uv run $env_args pytest {{ FILES }}
|
|
30
|
+
fi
|
|
31
|
+
|
|
32
|
+
[doc("Run linters")]
|
|
33
|
+
[group("Testing")]
|
|
34
|
+
lint: setup
|
|
35
|
+
uv run pre-commit run --all-files
|
|
36
|
+
|
|
37
|
+
[doc("Reformat code (optionally pass specific files)")]
|
|
38
|
+
[group("Development")]
|
|
39
|
+
format *FILES: setup
|
|
40
|
+
#!/usr/bin/env sh
|
|
41
|
+
if [ "{{ FILES }}" = '' ]; then
|
|
42
|
+
args='--all-files'
|
|
43
|
+
else
|
|
44
|
+
args='--files {{ FILES }}'
|
|
45
|
+
fi
|
|
46
|
+
uv run pre-commit run ruff-format $args
|
|
47
|
+
uv run pre-commit run tombi-format $args
|
|
48
|
+
|
|
49
|
+
[doc("Remove runtime development artifacts")]
|
|
50
|
+
[group("Environment")]
|
|
51
|
+
clean:
|
|
52
|
+
rm -f .coverage .env
|
|
53
|
+
rm -fR build
|
|
54
|
+
|
|
55
|
+
[confirm]
|
|
56
|
+
[doc("Remove caches, virtual env, and output files")]
|
|
57
|
+
[group("Environment")]
|
|
58
|
+
real-clean: clean
|
|
59
|
+
rm -fR .venv .*_cache dist
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "imbi-plugin-pagerduty"
|
|
3
|
+
version = "2.11.0"
|
|
4
|
+
description = "PagerDuty lifecycle, webhook, and incidents plugins for Imbi"
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.14"
|
|
7
|
+
license = { text = "BSD-3-Clause" }
|
|
8
|
+
authors = [{ name = "Gavin M. Roy", email = "gavinr@aweber.com" }]
|
|
9
|
+
classifiers = [
|
|
10
|
+
"Development Status :: 3 - Alpha",
|
|
11
|
+
"Intended Audience :: Developers",
|
|
12
|
+
"Natural Language :: English",
|
|
13
|
+
"Operating System :: OS Independent",
|
|
14
|
+
"Programming Language :: Python :: 3",
|
|
15
|
+
"Programming Language :: Python :: 3.14",
|
|
16
|
+
]
|
|
17
|
+
dependencies = [
|
|
18
|
+
"httpx>=0.27",
|
|
19
|
+
"imbi-common[databases]==2.11.0",
|
|
20
|
+
"pydantic>=2",
|
|
21
|
+
]
|
|
22
|
+
|
|
23
|
+
[project.entry-points."imbi.plugins"]
|
|
24
|
+
pagerduty-lifecycle = "imbi_plugin_pagerduty.lifecycle:PagerDutyLifecyclePlugin"
|
|
25
|
+
pagerduty-webhook = "imbi_plugin_pagerduty.webhook:PagerDutyWebhookPlugin"
|
|
26
|
+
pagerduty-incidents = "imbi_plugin_pagerduty.incidents:PagerDutyIncidentsPlugin"
|
|
27
|
+
|
|
28
|
+
[dependency-groups]
|
|
29
|
+
dev = [
|
|
30
|
+
"basedpyright",
|
|
31
|
+
"coverage[toml]",
|
|
32
|
+
"imbi-common[server,databases]==2.11.0",
|
|
33
|
+
"pre-commit",
|
|
34
|
+
"pytest",
|
|
35
|
+
"pytest-asyncio",
|
|
36
|
+
"respx",
|
|
37
|
+
"ruff~=0.14.6",
|
|
38
|
+
]
|
|
39
|
+
dist = ["build", "twine", "wheel"]
|
|
40
|
+
|
|
41
|
+
[build-system]
|
|
42
|
+
requires = ["hatchling"]
|
|
43
|
+
build-backend = "hatchling.build"
|
|
44
|
+
|
|
45
|
+
[tool.coverage.report]
|
|
46
|
+
exclude_also = ["typing.TYPE_CHECKING"]
|
|
47
|
+
fail_under = 85
|
|
48
|
+
show_missing = true
|
|
49
|
+
|
|
50
|
+
[tool.coverage.run]
|
|
51
|
+
branch = true
|
|
52
|
+
source = ["src/imbi_plugin_pagerduty"]
|
|
53
|
+
|
|
54
|
+
[tool.hatch.build.targets.wheel]
|
|
55
|
+
packages = ["src/imbi_plugin_pagerduty"]
|
|
56
|
+
|
|
57
|
+
[tool.pyright]
|
|
58
|
+
executionEnvironments = [
|
|
59
|
+
{ root = "src" },
|
|
60
|
+
{ root = "tests", reportPrivateUsage = false },
|
|
61
|
+
]
|
|
62
|
+
deprecateTypingAliases = true
|
|
63
|
+
reportMissingSuperCall = "hint"
|
|
64
|
+
reportMissingTypeStubs = false
|
|
65
|
+
typeCheckingMode = "strict"
|
|
66
|
+
|
|
67
|
+
[tool.pytest.ini_options]
|
|
68
|
+
asyncio_mode = "auto"
|
|
69
|
+
asyncio_default_fixture_loop_scope = "function"
|
|
70
|
+
|
|
71
|
+
[tool.ruff]
|
|
72
|
+
line-length = 79
|
|
73
|
+
target-version = "py314"
|
|
74
|
+
|
|
75
|
+
[tool.ruff.format]
|
|
76
|
+
quote-style = "single"
|
|
77
|
+
|
|
78
|
+
[tool.ruff.lint]
|
|
79
|
+
select = [
|
|
80
|
+
"BLE",
|
|
81
|
+
"C4",
|
|
82
|
+
"C90",
|
|
83
|
+
"E",
|
|
84
|
+
"W",
|
|
85
|
+
"F",
|
|
86
|
+
"G",
|
|
87
|
+
"I",
|
|
88
|
+
"N",
|
|
89
|
+
"Q",
|
|
90
|
+
"S",
|
|
91
|
+
"ASYNC",
|
|
92
|
+
"B",
|
|
93
|
+
"DTZ",
|
|
94
|
+
"FURB",
|
|
95
|
+
"RUF",
|
|
96
|
+
"T20",
|
|
97
|
+
"UP",
|
|
98
|
+
]
|
|
99
|
+
ignore = [
|
|
100
|
+
"ASYNC109",
|
|
101
|
+
"N818",
|
|
102
|
+
"RSE",
|
|
103
|
+
"S105",
|
|
104
|
+
"TRY003",
|
|
105
|
+
"TRY400",
|
|
106
|
+
"UP047",
|
|
107
|
+
]
|
|
108
|
+
flake8-quotes = { inline-quotes = "single" }
|
|
109
|
+
|
|
110
|
+
[tool.ruff.lint.mccabe]
|
|
111
|
+
max-complexity = 15
|
|
112
|
+
|
|
113
|
+
[tool.ruff.lint.per-file-ignores]
|
|
114
|
+
"tests/**/*.py" = ["S"]
|
|
115
|
+
|
|
116
|
+
[tool.uv]
|
|
117
|
+
default-groups = ["dev"]
|
|
118
|
+
exclude-newer = "7 days"
|
|
119
|
+
exclude-newer-package = { "clickhouse-connect" = false, "imbi-common" = false, "imbi-plugin-aws" = false, "imbi-plugin-github" = false, "imbi-plugin-logzio" = false, "imbi-plugin-oidc" = false, "imbi-plugin-pagerduty" = false, "imbi-plugin-sentry" = false, "imbi-plugin-sonarqube" = false }
|
|
120
|
+
|
|
121
|
+
[[tool.uv.index]]
|
|
122
|
+
url = "https://pypi.org/simple"
|
|
123
|
+
default = true
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""Imbi PagerDuty plugins (lifecycle, webhook, incidents)."""
|
|
2
|
+
|
|
3
|
+
from imbi_plugin_pagerduty.incidents import PagerDutyIncidentsPlugin
|
|
4
|
+
from imbi_plugin_pagerduty.lifecycle import PagerDutyLifecyclePlugin
|
|
5
|
+
from imbi_plugin_pagerduty.webhook import PagerDutyWebhookPlugin
|
|
6
|
+
|
|
7
|
+
__all__ = [
|
|
8
|
+
'PagerDutyIncidentsPlugin',
|
|
9
|
+
'PagerDutyLifecyclePlugin',
|
|
10
|
+
'PagerDutyWebhookPlugin',
|
|
11
|
+
]
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""Shared PagerDuty REST client for the plugin family.
|
|
2
|
+
|
|
3
|
+
PagerDuty is cloud-only (a single ``api.pagerduty.com`` host), so unlike
|
|
4
|
+
the GitHub plugins there is no host-flavor routing -- every plugin uses
|
|
5
|
+
the same base URL and a REST API key credential.
|
|
6
|
+
|
|
7
|
+
The client converts the two responses the host cares about into the
|
|
8
|
+
shared plugin exceptions via an httpx response hook:
|
|
9
|
+
|
|
10
|
+
* ``401`` -> :class:`PluginAuthenticationFailed` (a bad / revoked REST
|
|
11
|
+
key; surfaces to the operator rather than 500ing the request).
|
|
12
|
+
* ``429`` -> :class:`PluginRateLimited` carrying the absolute epoch to
|
|
13
|
+
resume from, derived from ``ratelimit-reset`` (seconds-until-reset) or
|
|
14
|
+
a ``retry-after`` header, so the host's sync fan-out can back off
|
|
15
|
+
instead of hammering the 960 req/min per-key limit.
|
|
16
|
+
"""
|
|
17
|
+
|
|
18
|
+
from __future__ import annotations
|
|
19
|
+
|
|
20
|
+
import time
|
|
21
|
+
|
|
22
|
+
import httpx
|
|
23
|
+
from imbi_common.plugins.errors import (
|
|
24
|
+
PluginAuthenticationFailed,
|
|
25
|
+
PluginRateLimited,
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
API_BASE = 'https://api.pagerduty.com'
|
|
29
|
+
_HTTP_TIMEOUT_SECONDS = 10.0
|
|
30
|
+
#: Small cushion added to a rate-limit reset so the resume lands just
|
|
31
|
+
#: after the window rolls over rather than on the boundary.
|
|
32
|
+
_RATE_LIMIT_PADDING_SECONDS = 1.0
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _retry_at(response: httpx.Response) -> float:
|
|
36
|
+
"""Absolute epoch to resume from after a 429, from the headers."""
|
|
37
|
+
for header in ('ratelimit-reset', 'retry-after'):
|
|
38
|
+
raw = response.headers.get(header)
|
|
39
|
+
if raw is None:
|
|
40
|
+
continue
|
|
41
|
+
try:
|
|
42
|
+
seconds = max(0.0, float(raw))
|
|
43
|
+
except ValueError:
|
|
44
|
+
continue
|
|
45
|
+
return time.time() + seconds + _RATE_LIMIT_PADDING_SECONDS
|
|
46
|
+
# No usable header: PagerDuty's window is one minute, so wait that.
|
|
47
|
+
return time.time() + 60.0
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
async def _raise_on_error(response: httpx.Response) -> None:
|
|
51
|
+
"""Map auth / rate-limit responses to shared plugin exceptions."""
|
|
52
|
+
if response.status_code == 401:
|
|
53
|
+
await response.aread()
|
|
54
|
+
raise PluginAuthenticationFailed(
|
|
55
|
+
f'PagerDuty 401 from {response.request.url}: {response.text}'
|
|
56
|
+
)
|
|
57
|
+
if response.status_code == 429:
|
|
58
|
+
await response.aread()
|
|
59
|
+
raise PluginRateLimited(
|
|
60
|
+
_retry_at(response),
|
|
61
|
+
f'PagerDuty rate limit hit at {response.request.url}',
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def api_key(credentials: dict[str, str]) -> str:
|
|
66
|
+
"""Return the REST API key from ``credentials`` or raise.
|
|
67
|
+
|
|
68
|
+
The manifest declares an ``api_key`` credential field; the host
|
|
69
|
+
decrypts the plugin configuration and passes it through here.
|
|
70
|
+
"""
|
|
71
|
+
key = credentials.get('api_key')
|
|
72
|
+
if not key:
|
|
73
|
+
raise ValueError(
|
|
74
|
+
'PagerDuty plugin requires a REST API key; expected '
|
|
75
|
+
'``credentials["api_key"]``'
|
|
76
|
+
)
|
|
77
|
+
return key
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def client(credentials: dict[str, str]) -> httpx.AsyncClient:
|
|
81
|
+
"""Build an :class:`httpx.AsyncClient` for the PagerDuty REST API."""
|
|
82
|
+
return httpx.AsyncClient(
|
|
83
|
+
base_url=API_BASE,
|
|
84
|
+
timeout=_HTTP_TIMEOUT_SECONDS,
|
|
85
|
+
headers={
|
|
86
|
+
'Authorization': f'Token token={api_key(credentials)}',
|
|
87
|
+
'Accept': 'application/vnd.pagerduty+json;version=2',
|
|
88
|
+
'Content-Type': 'application/json',
|
|
89
|
+
},
|
|
90
|
+
event_hooks={'response': [_raise_on_error]},
|
|
91
|
+
)
|