Skip to content

Commit 1998be9

Browse files
acervstephenfin
authored andcommitted
Add event-list command to query Patchwork events
The Patchwork REST API exposes an events endpoint that can be filtered by category, project and date. Add an event-list command so users can query events such as series-completed directly from the CLI. Supports format strings for scriptable output. Signed-off-by: Andrea Cervesato <andrea.cervesato@suse.com>
1 parent 47dc0fb commit 1998be9

6 files changed

Lines changed: 164 additions & 0 deletions

File tree

pwclient/api.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,12 @@ def check_create(
135135
):
136136
pass
137137

138+
# events
139+
140+
@abc.abstractmethod
141+
def event_list(self, project=None, category=None, since=None):
142+
pass
143+
138144

139145
class XMLRPC(API):
140146
def __init__(
@@ -428,6 +434,11 @@ def check_create(
428434
except xmlrpclib.Fault as f:
429435
raise exceptions.APIError(f'Error creating check: {f.faultString}')
430436

437+
def event_list(self, project=None, category=None, since=None):
438+
raise NotImplementedError(
439+
'Events are not supported by the XML-RPC API'
440+
)
441+
431442

432443
class REST(API):
433444
def __init__(
@@ -975,3 +986,38 @@ def check_create(
975986
'description': description,
976987
},
977988
)
989+
990+
@staticmethod
991+
def _event_to_dict(obj):
992+
"""Serialize an event response."""
993+
result = {
994+
'id': obj['id'],
995+
'category': obj['category'],
996+
'date': obj['date'],
997+
}
998+
999+
series = obj.get('payload', {}).get('series', {})
1000+
if series:
1001+
result['series_id'] = series.get('id', '')
1002+
result['series_url'] = series.get('url', '')
1003+
result['series_name'] = series.get('name', '')
1004+
result['series_mbox'] = series.get('mbox', '')
1005+
1006+
return result
1007+
1008+
def event_list(self, project=None, category=None, since=None):
1009+
filters = {}
1010+
1011+
if project:
1012+
filters['project'] = project
1013+
1014+
if category:
1015+
filters['category'] = category
1016+
1017+
if since:
1018+
filters['since'] = since
1019+
1020+
return (
1021+
self._event_to_dict(event)
1022+
for event in self._list('events', params=filters)
1023+
)

pwclient/events.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Patchwork command line client
2+
# Copyright (C) 2026 Andrea Cervesato <andrea.cervesato@suse.com>
3+
#
4+
# SPDX-License-Identifier: GPL-2.0-or-later
5+
6+
import re
7+
8+
9+
def action_list(api, project=None, category=None, since=None, format_str=None):
10+
events = api.event_list(project=project, category=category, since=since)
11+
12+
if format_str:
13+
format_field_re = re.compile('%{([a-z0-9_]+)}')
14+
15+
def event_field(matchobj):
16+
fieldname = matchobj.group(1)
17+
return str(event[fieldname])
18+
19+
for event in events:
20+
print(format_field_re.sub(event_field, format_str))
21+
else:
22+
print("%-10s %-24s %-24s %s" % ("ID", "Category", "Date", "Series"))
23+
print("%-10s %-24s %-24s %s" % ("--", "--------", "----", "------"))
24+
for event in events:
25+
print(
26+
"%-10d %-24s %-24s %s"
27+
% (
28+
event['id'],
29+
event['category'],
30+
event['date'],
31+
event.get('series_name', ''),
32+
)
33+
)

pwclient/parser.py

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,38 @@ def get_parser():
279279
)
280280
check_create_parser.set_defaults(subcmd='check_create')
281281

282+
event_list_parser = subparsers.add_parser(
283+
'event-list',
284+
help="list events",
285+
)
286+
event_list_parser.add_argument(
287+
'-p',
288+
'--project',
289+
metavar='PROJECT',
290+
help="filter by project name (see 'projects' for list)",
291+
)
292+
event_list_parser.add_argument(
293+
'-c',
294+
'--category',
295+
metavar='CATEGORY',
296+
help="filter by event category (e.g. 'series-completed')",
297+
)
298+
event_list_parser.add_argument(
299+
'--since',
300+
metavar='SINCE',
301+
help="show only events since a given date in ISO 8601 format",
302+
)
303+
event_list_parser.add_argument(
304+
'-f',
305+
'--format',
306+
metavar='FORMAT',
307+
help=(
308+
"print output in the given format. You can use tags matching "
309+
"fields, e.g. %%{id}, %%{category}, or %%{series_id}."
310+
),
311+
)
312+
event_list_parser.set_defaults(subcmd='event_list')
313+
282314
states_parser = subparsers.add_parser(
283315
'states', help="show list of potential patch states"
284316
)

pwclient/shell.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111

1212
from . import api as pw_api
1313
from . import checks
14+
from . import events
1415
from . import exceptions
1516
from . import parser
1617
from . import patches
@@ -174,6 +175,15 @@ def main(argv=sys.argv[1:]):
174175
format_str=args.format,
175176
)
176177

178+
elif action == 'event_list':
179+
events.action_list(
180+
api,
181+
project=args.project,
182+
category=args.category,
183+
since=args.since,
184+
format_str=args.format,
185+
)
186+
177187
elif action.startswith('project'):
178188
projects.action_list(api)
179189

tests/fakes.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,3 +114,17 @@ def fake_states():
114114
'name': 'New',
115115
}
116116
]
117+
118+
119+
def fake_events():
120+
return [
121+
{
122+
'id': 1,
123+
'category': 'series-completed',
124+
'date': '2026-04-10T06:00:02',
125+
'series_id': 499401,
126+
'series_name': '[v3] growfiles: fix test failure',
127+
'series_url': 'http://patchwork.ozlabs.org/api/series/499401/',
128+
'series_mbox': 'http://patchwork.ozlabs.org/series/499401/mbox/',
129+
},
130+
]

tests/test_shell.py

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
from pwclient import api
66
from pwclient import checks
7+
from pwclient import events
78
from pwclient import exceptions
89
from pwclient import patches
910
from pwclient import projects
@@ -339,6 +340,34 @@ def test_check_list(mock_action, mock_api, mock_config):
339340
mock_action.assert_called_once_with(mock_api.return_value, None, None)
340341

341342

343+
@mock.patch.object(utils.configparser, 'ConfigParser')
344+
@mock.patch.object(shell.os.path, 'exists', new=mock.Mock(return_value=True))
345+
@mock.patch.object(api, 'XMLRPC')
346+
@mock.patch.object(events, 'action_list')
347+
def test_event_list(mock_action, mock_api, mock_config):
348+
mock_config.return_value = FakeConfig()
349+
350+
shell.main(
351+
[
352+
'event-list',
353+
'-p',
354+
DEFAULT_PROJECT,
355+
'-c',
356+
'series-completed',
357+
'--since',
358+
'2026-04-09T00:00:00Z',
359+
]
360+
)
361+
362+
mock_action.assert_called_once_with(
363+
mock_api.return_value,
364+
project=DEFAULT_PROJECT,
365+
category='series-completed',
366+
since='2026-04-09T00:00:00Z',
367+
format_str=None,
368+
)
369+
370+
342371
@mock.patch.object(utils.configparser, 'ConfigParser')
343372
@mock.patch.object(shell.os.path, 'exists', new=mock.Mock(return_value=True))
344373
@mock.patch.object(api, 'XMLRPC')

0 commit comments

Comments
 (0)