Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
46 changes: 46 additions & 0 deletions pwclient/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,12 @@ def check_create(
):
pass

# events

@abc.abstractmethod
def event_list(self, project=None, category=None, since=None):
pass


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

def event_list(self, project=None, category=None, since=None):
raise NotImplementedError(
'Events are not supported by the XML-RPC API'
)


class REST(API):
def __init__(
Expand Down Expand Up @@ -975,3 +986,38 @@ def check_create(
'description': description,
},
)

@staticmethod
def _event_to_dict(obj):
"""Serialize an event response."""
result = {
'id': obj['id'],
'category': obj['category'],
'date': obj['date'],
}

series = obj.get('payload', {}).get('series', {})
if series:
result['series_id'] = series.get('id', '')
result['series_url'] = series.get('url', '')
result['series_name'] = series.get('name', '')
result['series_mbox'] = series.get('mbox', '')

return result

def event_list(self, project=None, category=None, since=None):
filters = {}

if project:
filters['project'] = project

if category:
filters['category'] = category

if since:
filters['since'] = since

return (
self._event_to_dict(event)
for event in self._list('events', params=filters)
)
33 changes: 33 additions & 0 deletions pwclient/events.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# Patchwork command line client
# Copyright (C) 2026 Andrea Cervesato <andrea.cervesato@suse.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later

import re


def action_list(api, project=None, category=None, since=None, format_str=None):
events = api.event_list(project=project, category=category, since=since)

if format_str:
format_field_re = re.compile('%{([a-z0-9_]+)}')

def event_field(matchobj):
fieldname = matchobj.group(1)
return str(event[fieldname])

for event in events:
print(format_field_re.sub(event_field, format_str))
else:
print("%-10s %-24s %-24s %s" % ("ID", "Category", "Date", "Series"))
print("%-10s %-24s %-24s %s" % ("--", "--------", "----", "------"))
for event in events:
print(
"%-10d %-24s %-24s %s"
% (
event['id'],
event['category'],
event['date'],
event.get('series_name', ''),
)
)
32 changes: 32 additions & 0 deletions pwclient/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,6 +279,38 @@ def get_parser():
)
check_create_parser.set_defaults(subcmd='check_create')

event_list_parser = subparsers.add_parser(
'event-list',
help="list events",
)
event_list_parser.add_argument(
'-p',
'--project',
metavar='PROJECT',
help="filter by project name (see 'projects' for list)",
)
event_list_parser.add_argument(
'-c',
'--category',
metavar='CATEGORY',
help="filter by event category (e.g. 'series-completed')",
)
event_list_parser.add_argument(
'--since',
metavar='SINCE',
help="show only events since a given date in ISO 8601 format",
)
event_list_parser.add_argument(
'-f',
'--format',
metavar='FORMAT',
help=(
"print output in the given format. You can use tags matching "
"fields, e.g. %%{id}, %%{category}, or %%{series_id}."
),
)
event_list_parser.set_defaults(subcmd='event_list')

states_parser = subparsers.add_parser(
'states', help="show list of potential patch states"
)
Expand Down
10 changes: 10 additions & 0 deletions pwclient/shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

from . import api as pw_api
from . import checks
from . import events
from . import exceptions
from . import parser
from . import patches
Expand Down Expand Up @@ -174,6 +175,15 @@ def main(argv=sys.argv[1:]):
format_str=args.format,
)

elif action == 'event_list':
events.action_list(
api,
project=args.project,
category=args.category,
since=args.since,
format_str=args.format,
)

elif action.startswith('project'):
projects.action_list(api)

Expand Down
14 changes: 14 additions & 0 deletions tests/fakes.py
Original file line number Diff line number Diff line change
Expand Up @@ -114,3 +114,17 @@ def fake_states():
'name': 'New',
}
]


def fake_events():
return [
{
'id': 1,
'category': 'series-completed',
'date': '2026-04-10T06:00:02',
'series_id': 499401,
'series_name': '[v3] growfiles: fix test failure',
'series_url': 'http://patchwork.ozlabs.org/api/series/499401/',
'series_mbox': 'http://patchwork.ozlabs.org/series/499401/mbox/',
},
]
29 changes: 29 additions & 0 deletions tests/test_shell.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from pwclient import api
from pwclient import checks
from pwclient import events
from pwclient import exceptions
from pwclient import patches
from pwclient import projects
Expand Down Expand Up @@ -339,6 +340,34 @@ def test_check_list(mock_action, mock_api, mock_config):
mock_action.assert_called_once_with(mock_api.return_value, None, None)


@mock.patch.object(utils.configparser, 'ConfigParser')
@mock.patch.object(shell.os.path, 'exists', new=mock.Mock(return_value=True))
@mock.patch.object(api, 'XMLRPC')
@mock.patch.object(events, 'action_list')
def test_event_list(mock_action, mock_api, mock_config):
mock_config.return_value = FakeConfig()

shell.main(
[
'event-list',
'-p',
DEFAULT_PROJECT,
'-c',
'series-completed',
'--since',
'2026-04-09T00:00:00Z',
]
)

mock_action.assert_called_once_with(
mock_api.return_value,
project=DEFAULT_PROJECT,
category='series-completed',
since='2026-04-09T00:00:00Z',
format_str=None,
)


@mock.patch.object(utils.configparser, 'ConfigParser')
@mock.patch.object(shell.os.path, 'exists', new=mock.Mock(return_value=True))
@mock.patch.object(api, 'XMLRPC')
Expand Down
Loading