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.
Files changed (26) hide show
  1. imbi_plugin_pagerduty-2.11.0/.github/workflows/publish.yml +45 -0
  2. imbi_plugin_pagerduty-2.11.0/.github/workflows/test.yml +54 -0
  3. imbi_plugin_pagerduty-2.11.0/.gitignore +10 -0
  4. imbi_plugin_pagerduty-2.11.0/.pre-commit-config.yaml +39 -0
  5. imbi_plugin_pagerduty-2.11.0/CLAUDE.md +87 -0
  6. imbi_plugin_pagerduty-2.11.0/LICENSE +28 -0
  7. imbi_plugin_pagerduty-2.11.0/PKG-INFO +49 -0
  8. imbi_plugin_pagerduty-2.11.0/README.md +30 -0
  9. imbi_plugin_pagerduty-2.11.0/justfile +59 -0
  10. imbi_plugin_pagerduty-2.11.0/pyproject.toml +123 -0
  11. imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/__init__.py +11 -0
  12. imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/_client.py +91 -0
  13. imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/_services.py +89 -0
  14. imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/incidents.py +117 -0
  15. imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/lifecycle.py +409 -0
  16. imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/models.py +56 -0
  17. imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/py.typed +0 -0
  18. imbi_plugin_pagerduty-2.11.0/src/imbi_plugin_pagerduty/webhook.py +47 -0
  19. imbi_plugin_pagerduty-2.11.0/tests/__init__.py +0 -0
  20. imbi_plugin_pagerduty-2.11.0/tests/test_client.py +52 -0
  21. imbi_plugin_pagerduty-2.11.0/tests/test_incidents.py +118 -0
  22. imbi_plugin_pagerduty-2.11.0/tests/test_lifecycle.py +285 -0
  23. imbi_plugin_pagerduty-2.11.0/tests/test_models.py +55 -0
  24. imbi_plugin_pagerduty-2.11.0/tests/test_services.py +92 -0
  25. imbi_plugin_pagerduty-2.11.0/tests/test_webhook.py +18 -0
  26. 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,10 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ .pytest_cache/
5
+ .coverage
6
+ htmlcov/
7
+ dist/
8
+ build/
9
+ *.egg-info/
10
+ .ruff_cache/
@@ -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
+ )