blob: bb3645455fe8401fa1442b7c150f1421aab86cf6 [file] [log] [blame]
Simon Glass2c266d82025-04-29 07:22:13 -06001# SPDX-License-Identifier: GPL-2.0+
2#
3# Copyright 2025 Simon Glass <sjg@chromium.org>
4#
5"""Provides a basic API for the patchwork server
6"""
7
8import asyncio
Simon Glass232eefd2025-04-29 07:22:14 -06009import re
Simon Glass2c266d82025-04-29 07:22:13 -060010
11import aiohttp
12
13# Number of retries
14RETRIES = 3
15
16# Max concurrent request
17MAX_CONCURRENT = 50
18
Simon Glass232eefd2025-04-29 07:22:14 -060019# Patches which are part of a multi-patch series are shown with a prefix like
20# [prefix, version, sequence], for example '[RFC, v2, 3/5]'. All but the last
21# part is optional. This decodes the string into groups. For single patches
22# the [] part is not present:
23# Groups: (ignore, ignore, ignore, prefix, version, sequence, subject)
24RE_PATCH = re.compile(r'(\[(((.*),)?(.*),)?(.*)\]\s)?(.*)$')
25
26# This decodes the sequence string into a patch number and patch count
27RE_SEQ = re.compile(r'(\d+)/(\d+)')
28
29
30class Patch(dict):
31 """Models a patch in patchwork
32
33 This class records information obtained from patchwork
34
35 Some of this information comes from the 'Patch' column:
36
37 [RFC,v2,1/3] dm: Driver and uclass changes for tiny-dm
38
39 This shows the prefix, version, seq, count and subject.
40
41 The other properties come from other columns in the display.
42
43 Properties:
44 pid (str): ID of the patch (typically an integer)
45 seq (int): Sequence number within series (1=first) parsed from sequence
46 string
47 count (int): Number of patches in series, parsed from sequence string
48 raw_subject (str): Entire subject line, e.g.
49 "[1/2,v2] efi_loader: Sort header file ordering"
50 prefix (str): Prefix string or None (e.g. 'RFC')
51 version (str): Version string or None (e.g. 'v2')
52 raw_subject (str): Raw patch subject
53 subject (str): Patch subject with [..] part removed (same as commit
54 subject)
55 """
56 def __init__(self, pid):
57 super().__init__()
58 self.id = pid # Use 'id' to match what the Rest API provides
59 self.seq = None
60 self.count = None
61 self.prefix = None
62 self.version = None
63 self.raw_subject = None
64 self.subject = None
65
66 # These make us more like a dictionary
67 def __setattr__(self, name, value):
68 self[name] = value
69
70 def __getattr__(self, name):
71 return self[name]
72
73 def __hash__(self):
74 return hash(frozenset(self.items()))
75
76 def __str__(self):
77 return self.raw_subject
78
79 def parse_subject(self, raw_subject):
80 """Parse the subject of a patch into its component parts
81
82 See RE_PATCH for details. The parsed info is placed into seq, count,
83 prefix, version, subject
84
85 Args:
86 raw_subject (str): Subject string to parse
87
88 Raises:
89 ValueError: the subject cannot be parsed
90 """
91 self.raw_subject = raw_subject.strip()
92 mat = RE_PATCH.search(raw_subject.strip())
93 if not mat:
94 raise ValueError(f"Cannot parse subject '{raw_subject}'")
95 self.prefix, self.version, seq_info, self.subject = mat.groups()[3:]
96 mat_seq = RE_SEQ.match(seq_info) if seq_info else False
97 if mat_seq is None:
98 self.version = seq_info
99 seq_info = None
100 if self.version and not self.version.startswith('v'):
101 self.prefix = self.version
102 self.version = None
103 if seq_info:
104 if mat_seq:
105 self.seq = int(mat_seq.group(1))
106 self.count = int(mat_seq.group(2))
107 else:
108 self.seq = 1
109 self.count = 1
110
111
112class Review:
113 """Represents a single review email collected in Patchwork
114
115 Patches can attract multiple reviews. Each consists of an author/date and
116 a variable number of 'snippets', which are groups of quoted and unquoted
117 text.
118 """
119 def __init__(self, meta, snippets):
120 """Create new Review object
121
122 Args:
123 meta (str): Text containing review author and date
124 snippets (list): List of snippets in th review, each a list of text
125 lines
126 """
127 self.meta = ' : '.join([line for line in meta.splitlines() if line])
128 self.snippets = snippets
129
130
Simon Glass2c266d82025-04-29 07:22:13 -0600131class Patchwork:
132 """Class to handle communication with patchwork
133 """
134 def __init__(self, url, show_progress=True):
135 """Set up a new patchwork handler
136
137 Args:
138 url (str): URL of patchwork server, e.g.
139 'https://patchwork.ozlabs.org'
140 """
141 self.url = url
142 self.proj_id = None
143 self.link_name = None
144 self._show_progress = show_progress
145 self.semaphore = asyncio.Semaphore(MAX_CONCURRENT)
146 self.request_count = 0
147
148 async def _request(self, client, subpath):
149 """Call the patchwork API and return the result as JSON
150
151 Args:
152 client (aiohttp.ClientSession): Session to use
153 subpath (str): URL subpath to use
154
155 Returns:
156 dict: Json result
157
158 Raises:
159 ValueError: the URL could not be read
160 """
161 # print('subpath', subpath)
162 self.request_count += 1
163
164 full_url = f'{self.url}/api/1.2/{subpath}'
165 async with self.semaphore:
166 # print('full_url', full_url)
167 for i in range(RETRIES + 1):
168 try:
169 async with client.get(full_url) as response:
170 if response.status != 200:
171 raise ValueError(
172 f"Could not read URL '{full_url}'")
173 result = await response.json()
174 # print('- done', full_url)
175 return result
176 break
177 except aiohttp.client_exceptions.ServerDisconnectedError:
178 if i == RETRIES:
179 raise