Skip to content

Commit

Permalink
Filter incident list by user' teams #205 (#206)
Browse files Browse the repository at this point in the history
* Cosmethic changes
* Filter by teamid
* Update copyright notice
* Refactoring classes and tests
* Team subcommand
* Fix typo in tests
* Refactoring PDH class
* Cosmethic
* Move svc_list to core
* Update readme
* Bump deps and version to 0.7.0
  • Loading branch information
mbovo authored Jan 2, 2025
1 parent a240aa6 commit ecbc611
Show file tree
Hide file tree
Showing 20 changed files with 735 additions and 632 deletions.
26 changes: 23 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,26 @@ Any other incident currently outstanding:
pdh inc ls -e
```

#### Listing incident by team

Listing only outstanding alerts only if they are assigned to a specific team:

```bash
pdh inc ls -e --teams mine
```

Search for a given team id:

```bash
pdh teams ls
```

Use specific id (list):

```bash
pdh inc ls -e --team-id "P1LONJG,P4SEF5R"
```

### Sorting incident by field

```bash
Expand Down Expand Up @@ -129,7 +149,7 @@ pdh inc ls --watch --new --ack --timeout 10

### List all HIGH priority incidents periodically

List incidents asssigned to all users every 5s
List incidents assigned to all users every 5s

```bash
pdh inc ls --high --everything --watch --timeout 5
Expand Down Expand Up @@ -172,7 +192,7 @@ pdh inc ls -e -o raw
pdh inc apply INCID001 -s /path/to/my/script.py -s /path/to/binary
```

The `apply` subcommand will call the listed executable/script passing along a json to stdin with the incident informations. The called script can apply any type of checks/sideffects and output another json to stout to answer the call.
The `apply` subcommand will call the listed executable/script passing along a json to stdin with the incident information. The called script can apply any type of checks/sideffects and output another json to stout to answer the call.

Even though rules can be written in any language it's very straightforward using python:

Expand Down Expand Up @@ -204,7 +224,7 @@ This is the simplest rule you can write, reading the input and simply output a n
└────────┴────────────────────────────────────────────────────────────────────┴──────────────────────────────────────────────────────────────────────────────────────┘
```

The default output is `table` with one line for each script runned and with one column per each element in the returned object
The default output is `table` with one line for each script run and with one column per each element in the returned object

### Rules: more examples

Expand Down
827 changes: 405 additions & 422 deletions poetry.lock

Large diffs are not rendered by default.

12 changes: 6 additions & 6 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ ignore = [

[tool.poetry]
name = "pdh"
version = "0.6.1"
version = "0.7.0"
description = "Pagerduty CLI for Humans"
authors = ["Manuel Bovo <[email protected]>"]
license = "GPL-3.0-or-later"
Expand All @@ -33,11 +33,11 @@ readme = "README.md"
python = "^3.11"
click = "^8.1.7"
colorama = "^0.4.6"
pdpyras = "^5.2.0"
rich = "^13.8.1"
pdpyras = "^5.4.0"
rich = "^13.9.4"
PyYAML = "^6.0.2"
humanize = "^4.10.0"
jsonpath-ng = "^1.6.1"
jsonpath-ng = "^1.7.0"
deprecation = "^2.1.0"
setuptools = "^72.1.0" # needed for poetry2nix and nix build since the provided version by nixpkgs unstable is still 72.x
dikdik = "^0.1.6"
Expand All @@ -47,11 +47,11 @@ pdh = "pdh.main:main"

[tool.poetry.group.dev.dependencies]
pylint = "^3.3.1"
black = "^24.8.0"
black = "^24.10.0"
pytest = "^8.3.3"
pytest-cov = "^4.0.0"
pytest-mock = "^3.14.0"
ruff = "^0.6.8"
ruff = "^0.8.4"

[build-system]
requires = ["poetry-core>=1.0.0"]
Expand Down
2 changes: 1 addition & 1 deletion rules/examples.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
#!/usr/bin/env python3
#
# This file is part of the pdh (https://github.com/mbovo/pdh).
# Copyright (c) 2020-2024 Manuel Bovo.
# Copyright (c) 2020-2025 Manuel Bovo.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand Down
2 changes: 1 addition & 1 deletion src/pdh/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the pdh (https://github.com/mbovo/pdh).
# Copyright (c) 2020-2024 Manuel Bovo.
# Copyright (c) 2020-2025 Manuel Bovo.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand Down
2 changes: 1 addition & 1 deletion src/pdh/config.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the pdh (https://github.com/mbovo/pdh).
# Copyright (c) 2020-2024 Manuel Bovo.
# Copyright (c) 2020-2025 Manuel Bovo.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand Down
141 changes: 121 additions & 20 deletions src/pdh/core.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the pdh (https://github.com/mbovo/pdh).
# Copyright (c) 2020-2024 Manuel Bovo.
# Copyright (c) 2020-2025 Manuel Bovo.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand All @@ -14,12 +14,13 @@
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.
#
from pdh import Transformations
from .filters import Filter
from .pd import UnauthorizedException, Users, Incidents
from . import Transformations
from .pd import PagerDuty, UnauthorizedException
from .config import Config
from .output import print, print_items

from typing import List
from datetime import timezone

class PDH(object):

Expand All @@ -32,7 +33,7 @@ def list_user(cfg: Config, output: str, fields: list | None = None) -> bool:
if isinstance(fields, str):
fields = fields.split(",")

users = Users(cfg).list()
users = PagerDuty(cfg).users.list()

if output == "raw":
filtered = users
Expand All @@ -53,7 +54,7 @@ def list_user(cfg: Config, output: str, fields: list | None = None) -> bool:
@staticmethod
def get_user(cfg: Config, user: str, output: str, fields: list | None = None):
try:
u = Users(cfg)
u = PagerDuty(cfg).users
users = u.search(user)
if len(users) == 0:
users = u.search(user, "id")
Expand Down Expand Up @@ -81,47 +82,147 @@ def get_user(cfg: Config, user: str, output: str, fields: list | None = None):
print(f"[red]{e}[/red]")
return False

@staticmethod
def list_teams(cfg: Config, mine: bool = True, output='table', fields=None) -> bool:
try:
pd = PagerDuty(cfg)
if mine:
teams = dict(pd.me())['teams']
else:
teams = pd.teams.list()

# set fields that will be displayed
if type(fields) is str:
fields = fields.lower().strip().split(",")
else:
fields = ["id", "summary", "html_url"]

def plain_print_f(i):
s = ""
for f in fields:
s += f"{i[f]}\t"
print(s)

if output != "raw":
transformations = dict()

for f in fields:
transformations[f] = Transformations.extract(f)

filtered = Transformations.apply(teams, transformations)
else:
filtered = teams

print_items(filtered, output, plain_print_f=plain_print_f)
return True
except UnauthorizedException as e:
print(f"[red]{e}[/red]")
return False

@staticmethod
def list_services(cfg: Config, output: str = 'table', fields: List | None = None, sort_by: str| None = None, reverse_sort: bool = False, status: str = "active,warning,critical") -> bool:
try:
pd = PagerDuty(cfg)
svcs = pd.services.list()

svcs = Filter.apply(svcs, [Filter.inList("status", status.split(","))])

# set fields that will be displayed
if type(fields) is str:
fields = fields.lower().strip().split(",")
else:
fields = ["id", "name", "description", "status","created_at", "updated_at", "html_url"]

if output != "raw":
transformations = dict()

for f in fields:
transformations[f] = Transformations.extract(f)
# special cases
if f == "status":
transformations[f] = Transformations.extract_decorate("status", color_map={"active": "green", "warning": "yellow", "critical": "red", "unknown": "gray", "disabled": "gray"}, change_map={
"active": "OK", "warning": "WARN", "critical": "CRIT", "unknown": "❔", "disabled": "off"})
if f == "url":
transformations[f] = Transformations.extract("html_url")
if f in ["created_at", "updated_at"]:
transformations[f] = Transformations.extract_date(
f, "%Y-%m-%dT%H:%M:%S%z", timezone.utc)

filtered = Transformations.apply(svcs, transformations)
else:
# raw output, using json format
filtered = svcs

# define here how print in "plain" way (ie if output=plain)
def plain_print_f(i):
s = ""
for f in fields:
s += f"{i[f]}\t"
print(s)

if sort_by:
sort_fields: str | list[str] = sort_by.split(",") if ',' in sort_by else sort_by

if isinstance(sort_fields, list) and len(sort_fields) > 1:
filtered = sorted(filtered, key=lambda x: [
x[k] for k in sort_fields], reverse=reverse_sort)
else:
filtered = sorted(
filtered, key=lambda x: x[sort_fields], reverse=reverse_sort)

print_items(filtered, output, plain_print_f=plain_print_f)
return True

except UnauthorizedException as e:
print(f"[red]{e}[/red]")
return False
except KeyError:
print(f"[red]Invalid sort field: {sort_by}[/red]")
ff = ", ".join(fields) if fields else ""
print(f"[yellow]Available fields: {ff}[/yellow]")
return False

@staticmethod
def ack(cfg: Config, incIDs: list = []) -> None:
pd = Incidents(cfg)
incs = pd.list()
pd = PagerDuty(cfg)
incs = pd.incidents.list()
incs = Filter.apply(incs, filters=[Filter.inList("id", incIDs)])
for i in incs:
print(f"[yellow]✔[/yellow] {i['id']} [grey50]{i['title']}[/grey50]")
pd.ack(incs)
pd.incidents.ack(incs)

@staticmethod
def resolve(cfg: Config, incIDs: list = []) -> None:
pd = Incidents(cfg)
incs = pd.list()
pd = PagerDuty(cfg)
incs = pd.incidents.list()
incs = Filter.apply(incs, filters=[Filter.inList("id", incIDs)])
for i in incs:
print(f"[green]✅[/green] {i['id']} [grey50]{i['title']}[/grey50]")
pd.resolve(incs)
pd.incidents.resolve(incs)

@staticmethod
def snooze(cfg: Config, incIDs: list = [], duration: int = 14400) -> None:
pd = Incidents(cfg)
pd = PagerDuty(cfg)
import datetime

incs = pd.list()
incs = pd.incidents.list()
incs = Filter.apply(incs, filters=[Filter.inList("id", incIDs)])
for id in incIDs:
print(f"Snoozing incident {id} for { str(datetime.timedelta(seconds=duration))}")

pd.snooze(incs, duration)
pd.incidents.snooze(incs, duration)

@staticmethod
def reassign(cfg: Config, incIDs: list = [], user: str | None = None):
pd = Incidents(cfg)
incs = pd.list()
pd = PagerDuty(cfg)
incs = pd.incidents.list()
incs = Filter.apply(incs, filters=[Filter.inList("id", incIDs)])

users = Users(cfg).userID_by_name(user)
users = pd.users.id(user)
if users is None or len(users) == 0:
users = Users(cfg).userID_by_name(user)
users = pd.users.id(user)

for id in incIDs:
print(f"Reassign incident {id} to {users}")

pd.reassign(incs, users)
pd.incidents.reassign(incs, users)
2 changes: 1 addition & 1 deletion src/pdh/filters.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
#
# This file is part of the pdh (https://github.com/mbovo/pdh).
# Copyright (c) 2020-2024 Manuel Bovo.
# Copyright (c) 2020-2025 Manuel Bovo.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
Expand Down
Loading

0 comments on commit ecbc611

Please sign in to comment.