xref: /aosp_15_r20/external/federated-compute/fcp/demo/http_actions.py (revision 14675a029014e728ec732f129a32e299b2da0601)
1*14675a02SAndroid Build Coastguard Worker# Copyright 2022 Google LLC
2*14675a02SAndroid Build Coastguard Worker#
3*14675a02SAndroid Build Coastguard Worker# Licensed under the Apache License, Version 2.0 (the "License");
4*14675a02SAndroid Build Coastguard Worker# you may not use this file except in compliance with the License.
5*14675a02SAndroid Build Coastguard Worker# You may obtain a copy of the License at
6*14675a02SAndroid Build Coastguard Worker#
7*14675a02SAndroid Build Coastguard Worker#      http://www.apache.org/licenses/LICENSE-2.0
8*14675a02SAndroid Build Coastguard Worker#
9*14675a02SAndroid Build Coastguard Worker# Unless required by applicable law or agreed to in writing, software
10*14675a02SAndroid Build Coastguard Worker# distributed under the License is distributed on an "AS IS" BASIS,
11*14675a02SAndroid Build Coastguard Worker# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12*14675a02SAndroid Build Coastguard Worker# See the License for the specific language governing permissions and
13*14675a02SAndroid Build Coastguard Worker# limitations under the License.
14*14675a02SAndroid Build Coastguard Worker"""Utilities for creating proto service and HTTP action handlers.
15*14675a02SAndroid Build Coastguard Worker
16*14675a02SAndroid Build Coastguard WorkerThe `@proto_action` function annotates a method as implementing a proto service
17*14675a02SAndroid Build Coastguard Workermethod. The annotated method should have the type
18*14675a02SAndroid Build Coastguard Worker`Callable[[RequestMessage], ResponseMessage]`. The decorator will take care of
19*14675a02SAndroid Build Coastguard Workertranscoding to/from a HTTP request, similar to
20*14675a02SAndroid Build Coastguard Workerhttps://cloud.google.com/endpoints/docs/grpc/transcoding. The transcoding only
21*14675a02SAndroid Build Coastguard Workersupports proto-over-http ('?alt=proto').
22*14675a02SAndroid Build Coastguard Worker
23*14675a02SAndroid Build Coastguard WorkerThe `@http_action` function annotates a method as implementing a HTTP action at
24*14675a02SAndroid Build Coastguard Workersome request path. The annotated method will receive the request body, and
25*14675a02SAndroid Build Coastguard Workershould return a `HttpResponse`.
26*14675a02SAndroid Build Coastguard Worker
27*14675a02SAndroid Build Coastguard WorkerThe `create_handler` function merges one or more objects with decorated methods
28*14675a02SAndroid Build Coastguard Workerinto a single request handler that's compatible with `http.server`.
29*14675a02SAndroid Build Coastguard Worker"""
30*14675a02SAndroid Build Coastguard Worker
31*14675a02SAndroid Build Coastguard Workerimport collections
32*14675a02SAndroid Build Coastguard Workerimport dataclasses
33*14675a02SAndroid Build Coastguard Workerimport enum
34*14675a02SAndroid Build Coastguard Workerimport gzip
35*14675a02SAndroid Build Coastguard Workerimport http
36*14675a02SAndroid Build Coastguard Workerimport http.server
37*14675a02SAndroid Build Coastguard Workerimport re
38*14675a02SAndroid Build Coastguard Workerfrom typing import Any, Callable, Mapping, Match, Pattern, Type, TypeVar
39*14675a02SAndroid Build Coastguard Workerimport urllib.parse
40*14675a02SAndroid Build Coastguard Workerimport zlib
41*14675a02SAndroid Build Coastguard Worker
42*14675a02SAndroid Build Coastguard Workerfrom absl import logging
43*14675a02SAndroid Build Coastguard Worker
44*14675a02SAndroid Build Coastguard Workerfrom google.api import annotations_pb2
45*14675a02SAndroid Build Coastguard Workerfrom google.protobuf import descriptor_pool
46*14675a02SAndroid Build Coastguard Workerfrom google.protobuf import message
47*14675a02SAndroid Build Coastguard Workerfrom google.protobuf import message_factory
48*14675a02SAndroid Build Coastguard Worker
49*14675a02SAndroid Build Coastguard Worker_CallableT = TypeVar('_CallableT', bound=Callable)
50*14675a02SAndroid Build Coastguard Worker
51*14675a02SAndroid Build Coastguard Worker_HTTP_ACTION_ATTR = '_http_action_data'
52*14675a02SAndroid Build Coastguard Worker_FACTORY = message_factory.MessageFactory(descriptor_pool.Default())
53*14675a02SAndroid Build Coastguard Worker
54*14675a02SAndroid Build Coastguard Worker
55*14675a02SAndroid Build Coastguard Worker@dataclasses.dataclass(frozen=True)
56*14675a02SAndroid Build Coastguard Workerclass HttpError(Exception):
57*14675a02SAndroid Build Coastguard Worker  """An Exception specifying the HTTP error to return."""
58*14675a02SAndroid Build Coastguard Worker  code: http.HTTPStatus
59*14675a02SAndroid Build Coastguard Worker
60*14675a02SAndroid Build Coastguard Worker
61*14675a02SAndroid Build Coastguard Worker@dataclasses.dataclass(frozen=True)
62*14675a02SAndroid Build Coastguard Workerclass HttpResponse:
63*14675a02SAndroid Build Coastguard Worker  """Information for a successful HTTP response."""
64*14675a02SAndroid Build Coastguard Worker  body: bytes
65*14675a02SAndroid Build Coastguard Worker  headers: Mapping[str, str] = dataclasses.field(default_factory=lambda: {})
66*14675a02SAndroid Build Coastguard Worker
67*14675a02SAndroid Build Coastguard Worker
68*14675a02SAndroid Build Coastguard Workerdef proto_action(*,
69*14675a02SAndroid Build Coastguard Worker                 service=str,
70*14675a02SAndroid Build Coastguard Worker                 method=str) -> Callable[[_CallableT], _CallableT]:
71*14675a02SAndroid Build Coastguard Worker  """Decorator annotating a method as handling a proto service method.
72*14675a02SAndroid Build Coastguard Worker
73*14675a02SAndroid Build Coastguard Worker  The `google.api.http` annotation on the method will determine what requests
74*14675a02SAndroid Build Coastguard Worker  will be handled by the decorated function. Only a subset of methods and path
75*14675a02SAndroid Build Coastguard Worker  patterns are currently supported.
76*14675a02SAndroid Build Coastguard Worker
77*14675a02SAndroid Build Coastguard Worker  The decorated method will be called with the request message; it should return
78*14675a02SAndroid Build Coastguard Worker  a response message or or throw an `HttpError`.
79*14675a02SAndroid Build Coastguard Worker
80*14675a02SAndroid Build Coastguard Worker  Args:
81*14675a02SAndroid Build Coastguard Worker    service: The full name of the proto service.
82*14675a02SAndroid Build Coastguard Worker    method: The name of the method.
83*14675a02SAndroid Build Coastguard Worker
84*14675a02SAndroid Build Coastguard Worker  Returns:
85*14675a02SAndroid Build Coastguard Worker    An annotated function.
86*14675a02SAndroid Build Coastguard Worker  """
87*14675a02SAndroid Build Coastguard Worker  try:
88*14675a02SAndroid Build Coastguard Worker    desc = _FACTORY.pool.FindServiceByName(service).FindMethodByName(method)
89*14675a02SAndroid Build Coastguard Worker  except KeyError as e:
90*14675a02SAndroid Build Coastguard Worker    raise ValueError(f'Unable to find /{service}.{method}.') from e
91*14675a02SAndroid Build Coastguard Worker
92*14675a02SAndroid Build Coastguard Worker  rule = desc.GetOptions().Extensions[annotations_pb2.http]
93*14675a02SAndroid Build Coastguard Worker  pattern_kind = rule.WhichOneof('pattern')
94*14675a02SAndroid Build Coastguard Worker  try:
95*14675a02SAndroid Build Coastguard Worker    http_method = _HttpMethod[pattern_kind.upper()]
96*14675a02SAndroid Build Coastguard Worker  except KeyError as e:
97*14675a02SAndroid Build Coastguard Worker    raise ValueError(
98*14675a02SAndroid Build Coastguard Worker        f'The google.api.http annotation on /{service}.{method} is invalid '
99*14675a02SAndroid Build Coastguard Worker        'or unsupported.') from e
100*14675a02SAndroid Build Coastguard Worker  path = _convert_pattern(getattr(rule, pattern_kind), alt_proto=True)
101*14675a02SAndroid Build Coastguard Worker
102*14675a02SAndroid Build Coastguard Worker  def handler(match: Match[str], body: bytes,
103*14675a02SAndroid Build Coastguard Worker              fn: Callable[[message.Message], message.Message]) -> HttpResponse:
104*14675a02SAndroid Build Coastguard Worker    request = _FACTORY.GetPrototype(desc.input_type)()
105*14675a02SAndroid Build Coastguard Worker    if rule.body == '*':
106*14675a02SAndroid Build Coastguard Worker      try:
107*14675a02SAndroid Build Coastguard Worker        request.ParseFromString(body)
108*14675a02SAndroid Build Coastguard Worker      except message.DecodeError as e:
109*14675a02SAndroid Build Coastguard Worker        raise HttpError(code=http.HTTPStatus.BAD_REQUEST) from e
110*14675a02SAndroid Build Coastguard Worker    elif rule.body:
111*14675a02SAndroid Build Coastguard Worker      setattr(request, rule.body, body)
112*14675a02SAndroid Build Coastguard Worker    # Set any fields from the request path.
113*14675a02SAndroid Build Coastguard Worker    for prop, value in match.groupdict().items():
114*14675a02SAndroid Build Coastguard Worker      try:
115*14675a02SAndroid Build Coastguard Worker        unescaped = urllib.parse.unquote(value)
116*14675a02SAndroid Build Coastguard Worker      except UnicodeError as e:
117*14675a02SAndroid Build Coastguard Worker        raise HttpError(code=http.HTTPStatus.BAD_REQUEST) from e
118*14675a02SAndroid Build Coastguard Worker      setattr(request, prop, unescaped)
119*14675a02SAndroid Build Coastguard Worker
120*14675a02SAndroid Build Coastguard Worker    response_body = fn(request).SerializeToString()
121*14675a02SAndroid Build Coastguard Worker    return HttpResponse(
122*14675a02SAndroid Build Coastguard Worker        body=response_body,
123*14675a02SAndroid Build Coastguard Worker        headers={
124*14675a02SAndroid Build Coastguard Worker            'Content-Length': len(response_body),
125*14675a02SAndroid Build Coastguard Worker            'Content-Type': 'application/x-protobuf'
126*14675a02SAndroid Build Coastguard Worker        })
127*14675a02SAndroid Build Coastguard Worker
128*14675a02SAndroid Build Coastguard Worker  def annotate_method(func: _CallableT) -> _CallableT:
129*14675a02SAndroid Build Coastguard Worker    setattr(func, _HTTP_ACTION_ATTR,
130*14675a02SAndroid Build Coastguard Worker            _HttpActionData(method=http_method, path=path, handler=handler))
131*14675a02SAndroid Build Coastguard Worker    return func
132*14675a02SAndroid Build Coastguard Worker
133*14675a02SAndroid Build Coastguard Worker  return annotate_method
134*14675a02SAndroid Build Coastguard Worker
135*14675a02SAndroid Build Coastguard Worker
136*14675a02SAndroid Build Coastguard Workerdef http_action(*, method: str,
137*14675a02SAndroid Build Coastguard Worker                pattern: str) -> Callable[[_CallableT], _CallableT]:
138*14675a02SAndroid Build Coastguard Worker  """Decorator annotating a method as an HTTP action handler.
139*14675a02SAndroid Build Coastguard Worker
140*14675a02SAndroid Build Coastguard Worker  Request matching the method and pattern will be handled by the decorated
141*14675a02SAndroid Build Coastguard Worker  method. The pattern may contain bracket-enclosed keywords (e.g.,
142*14675a02SAndroid Build Coastguard Worker  '/data/{path}'), which will be matched against the request and passed
143*14675a02SAndroid Build Coastguard Worker  to the decorated function as keyword arguments.
144*14675a02SAndroid Build Coastguard Worker
145*14675a02SAndroid Build Coastguard Worker  The decorated method will be called with the request body (if any) and any
146*14675a02SAndroid Build Coastguard Worker  keyword args from the path pattern; it should return a `HttpResponse` or throw
147*14675a02SAndroid Build Coastguard Worker  an `HttpError`.
148*14675a02SAndroid Build Coastguard Worker
149*14675a02SAndroid Build Coastguard Worker  Args:
150*14675a02SAndroid Build Coastguard Worker    method: The type of HTTP method ('GET' or 'POST').
151*14675a02SAndroid Build Coastguard Worker    pattern: The url pattern to match.
152*14675a02SAndroid Build Coastguard Worker
153*14675a02SAndroid Build Coastguard Worker  Returns:
154*14675a02SAndroid Build Coastguard Worker    An annotated function.
155*14675a02SAndroid Build Coastguard Worker  """
156*14675a02SAndroid Build Coastguard Worker  try:
157*14675a02SAndroid Build Coastguard Worker    http_method = _HttpMethod[method.upper()]
158*14675a02SAndroid Build Coastguard Worker  except KeyError as e:
159*14675a02SAndroid Build Coastguard Worker    raise ValueError(f'unsupported HTTP method `{method}`') from e
160*14675a02SAndroid Build Coastguard Worker  path = _convert_pattern(pattern)
161*14675a02SAndroid Build Coastguard Worker
162*14675a02SAndroid Build Coastguard Worker  def handler(match: Match[str], body: bytes,
163*14675a02SAndroid Build Coastguard Worker              fn: Callable[[bytes], HttpResponse]) -> HttpResponse:
164*14675a02SAndroid Build Coastguard Worker    try:
165*14675a02SAndroid Build Coastguard Worker      args = {k: urllib.parse.unquote(v) for k, v in match.groupdict().items()}
166*14675a02SAndroid Build Coastguard Worker    except UnicodeError as e:
167*14675a02SAndroid Build Coastguard Worker      raise HttpError(code=http.HTTPStatus.BAD_REQUEST) from e
168*14675a02SAndroid Build Coastguard Worker    return fn(body, **args)
169*14675a02SAndroid Build Coastguard Worker
170*14675a02SAndroid Build Coastguard Worker  def annotate_method(func: _CallableT) -> _CallableT:
171*14675a02SAndroid Build Coastguard Worker    setattr(func, _HTTP_ACTION_ATTR,
172*14675a02SAndroid Build Coastguard Worker            _HttpActionData(method=http_method, path=path, handler=handler))
173*14675a02SAndroid Build Coastguard Worker    return func
174*14675a02SAndroid Build Coastguard Worker
175*14675a02SAndroid Build Coastguard Worker  return annotate_method
176*14675a02SAndroid Build Coastguard Worker
177*14675a02SAndroid Build Coastguard Worker
178*14675a02SAndroid Build Coastguard Workerdef create_handler(*services: Any) -> Type[http.server.BaseHTTPRequestHandler]:
179*14675a02SAndroid Build Coastguard Worker  """Builds a BaseHTTPRequestHandler that delegates to decorated methods.
180*14675a02SAndroid Build Coastguard Worker
181*14675a02SAndroid Build Coastguard Worker  The returned BaseHTTPRequestHandler class will route requests to decorated
182*14675a02SAndroid Build Coastguard Worker  methods of the provided services, or return 404 if the request path does not
183*14675a02SAndroid Build Coastguard Worker  match any action handlers. If the request path matches multiple registered
184*14675a02SAndroid Build Coastguard Worker  action handlers, it's unspecified which will be invoked.
185*14675a02SAndroid Build Coastguard Worker
186*14675a02SAndroid Build Coastguard Worker  Args:
187*14675a02SAndroid Build Coastguard Worker    *services: A list of objects with methods decorated with `@proto_action` or
188*14675a02SAndroid Build Coastguard Worker      `@http_action`.
189*14675a02SAndroid Build Coastguard Worker
190*14675a02SAndroid Build Coastguard Worker  Returns:
191*14675a02SAndroid Build Coastguard Worker    A BaseHTTPRequestHandler subclass.
192*14675a02SAndroid Build Coastguard Worker  """
193*14675a02SAndroid Build Coastguard Worker
194*14675a02SAndroid Build Coastguard Worker  # Collect all handlers, keyed by HTTP method.
195*14675a02SAndroid Build Coastguard Worker  handlers = collections.defaultdict(lambda: [])
196*14675a02SAndroid Build Coastguard Worker  for service in services:
197*14675a02SAndroid Build Coastguard Worker    for attr_name in dir(service):
198*14675a02SAndroid Build Coastguard Worker      attr = getattr(service, attr_name)
199*14675a02SAndroid Build Coastguard Worker      if not callable(attr):
200*14675a02SAndroid Build Coastguard Worker        continue
201*14675a02SAndroid Build Coastguard Worker      data = getattr(attr, _HTTP_ACTION_ATTR, None)
202*14675a02SAndroid Build Coastguard Worker      if isinstance(data, _HttpActionData):
203*14675a02SAndroid Build Coastguard Worker        handlers[data.method].append((data, attr))
204*14675a02SAndroid Build Coastguard Worker
205*14675a02SAndroid Build Coastguard Worker  format_handlers = lambda h: ''.join([f'\n  * {e[0].path.pattern}' for e in h])
206*14675a02SAndroid Build Coastguard Worker  logging.debug(
207*14675a02SAndroid Build Coastguard Worker      'Creating HTTP request handler for path patterns:\nGET:%s\nPOST:%s',
208*14675a02SAndroid Build Coastguard Worker      format_handlers(handlers[_HttpMethod.GET]),
209*14675a02SAndroid Build Coastguard Worker      format_handlers(handlers[_HttpMethod.POST]))
210*14675a02SAndroid Build Coastguard Worker
211*14675a02SAndroid Build Coastguard Worker  class RequestHandler(http.server.BaseHTTPRequestHandler):
212*14675a02SAndroid Build Coastguard Worker    """Handler that delegates to `handlers`."""
213*14675a02SAndroid Build Coastguard Worker
214*14675a02SAndroid Build Coastguard Worker    def do_GET(self) -> None:  # pylint:disable=invalid-name (override)
215*14675a02SAndroid Build Coastguard Worker      self._handle_request(_HttpMethod.GET, read_body=False)
216*14675a02SAndroid Build Coastguard Worker
217*14675a02SAndroid Build Coastguard Worker    def do_POST(self) -> None:  # pylint:disable=invalid-name (override)
218*14675a02SAndroid Build Coastguard Worker      self._handle_request(_HttpMethod.POST)
219*14675a02SAndroid Build Coastguard Worker
220*14675a02SAndroid Build Coastguard Worker    def _handle_request(self,
221*14675a02SAndroid Build Coastguard Worker                        method: _HttpMethod,
222*14675a02SAndroid Build Coastguard Worker                        read_body: bool = True) -> None:
223*14675a02SAndroid Build Coastguard Worker      """Reads and delegates an incoming request to a registered handler."""
224*14675a02SAndroid Build Coastguard Worker      for data, fn in handlers[method]:
225*14675a02SAndroid Build Coastguard Worker        match = data.path.fullmatch(self.path)
226*14675a02SAndroid Build Coastguard Worker        if match is None:
227*14675a02SAndroid Build Coastguard Worker          continue
228*14675a02SAndroid Build Coastguard Worker
229*14675a02SAndroid Build Coastguard Worker        try:
230*14675a02SAndroid Build Coastguard Worker          body = self._read_body() if read_body else b''
231*14675a02SAndroid Build Coastguard Worker          response = data.handler(match, body, fn)
232*14675a02SAndroid Build Coastguard Worker        except HttpError as e:
233*14675a02SAndroid Build Coastguard Worker          logging.debug('%s error: %s', self.path, e)
234*14675a02SAndroid Build Coastguard Worker          return self.send_error(e.code)
235*14675a02SAndroid Build Coastguard Worker        return self._send_response(response)
236*14675a02SAndroid Build Coastguard Worker
237*14675a02SAndroid Build Coastguard Worker      # If no handler matched the path, return an error.
238*14675a02SAndroid Build Coastguard Worker      self.send_error(http.HTTPStatus.NOT_FOUND)
239*14675a02SAndroid Build Coastguard Worker
240*14675a02SAndroid Build Coastguard Worker    def _read_body(self) -> bytes:
241*14675a02SAndroid Build Coastguard Worker      """Reads the body of the request."""
242*14675a02SAndroid Build Coastguard Worker      body = self.rfile.read(int(self.headers['Content-Length']))
243*14675a02SAndroid Build Coastguard Worker      if self.headers['Content-Encoding'] == 'gzip':
244*14675a02SAndroid Build Coastguard Worker        try:
245*14675a02SAndroid Build Coastguard Worker          body = gzip.decompress(body)
246*14675a02SAndroid Build Coastguard Worker        except (gzip.BadGzipFile, zlib.error) as e:
247*14675a02SAndroid Build Coastguard Worker          raise HttpError(http.HTTPStatus.BAD_REQUEST) from e
248*14675a02SAndroid Build Coastguard Worker      elif self.headers['Content-Encoding']:
249*14675a02SAndroid Build Coastguard Worker        logging.warning('Unsupported content encoding %s',
250*14675a02SAndroid Build Coastguard Worker                        self.headers['Content-Encoding'])
251*14675a02SAndroid Build Coastguard Worker        raise HttpError(http.HTTPStatus.BAD_REQUEST)
252*14675a02SAndroid Build Coastguard Worker      return body
253*14675a02SAndroid Build Coastguard Worker
254*14675a02SAndroid Build Coastguard Worker    def _send_response(self, response: HttpResponse) -> None:
255*14675a02SAndroid Build Coastguard Worker      """Sends a successful response message."""
256*14675a02SAndroid Build Coastguard Worker      self.send_response(http.HTTPStatus.OK)
257*14675a02SAndroid Build Coastguard Worker      for keyword, value in response.headers.items():
258*14675a02SAndroid Build Coastguard Worker        self.send_header(keyword, value)
259*14675a02SAndroid Build Coastguard Worker      self.end_headers()
260*14675a02SAndroid Build Coastguard Worker      self.wfile.write(response.body)
261*14675a02SAndroid Build Coastguard Worker
262*14675a02SAndroid Build Coastguard Worker  return RequestHandler
263*14675a02SAndroid Build Coastguard Worker
264*14675a02SAndroid Build Coastguard Worker
265*14675a02SAndroid Build Coastguard Workerclass _HttpMethod(enum.Enum):
266*14675a02SAndroid Build Coastguard Worker  GET = 1
267*14675a02SAndroid Build Coastguard Worker  POST = 2
268*14675a02SAndroid Build Coastguard Worker
269*14675a02SAndroid Build Coastguard Worker
270*14675a02SAndroid Build Coastguard Worker@dataclasses.dataclass(frozen=True)
271*14675a02SAndroid Build Coastguard Workerclass _HttpActionData:
272*14675a02SAndroid Build Coastguard Worker  """Data tracked for HTTP actions.
273*14675a02SAndroid Build Coastguard Worker
274*14675a02SAndroid Build Coastguard Worker  Attributes:
275*14675a02SAndroid Build Coastguard Worker    method: The name of the HTTP method to handle.
276*14675a02SAndroid Build Coastguard Worker    path: Requests matching this pattern will be handled.
277*14675a02SAndroid Build Coastguard Worker    handler: The handler function, which receives the path match, request body,
278*14675a02SAndroid Build Coastguard Worker      and decorated function.
279*14675a02SAndroid Build Coastguard Worker  """
280*14675a02SAndroid Build Coastguard Worker  method: _HttpMethod
281*14675a02SAndroid Build Coastguard Worker  path: Pattern[str]
282*14675a02SAndroid Build Coastguard Worker  handler: Callable[[Match[str], bytes, Callable[..., Any]], HttpResponse]
283*14675a02SAndroid Build Coastguard Worker
284*14675a02SAndroid Build Coastguard Worker
285*14675a02SAndroid Build Coastguard Workerdef _convert_pattern(pattern: str, alt_proto=False) -> Pattern[str]:
286*14675a02SAndroid Build Coastguard Worker  """Converts a Google API pattern to a regexp with named groups."""
287*14675a02SAndroid Build Coastguard Worker  # Subfields are not supported and will generate a regexp compilation error.
288*14675a02SAndroid Build Coastguard Worker  pattern_regexp = re.sub(r'\\\{(.+?)\\\}', r'(?P<\1>[^/?]*)',
289*14675a02SAndroid Build Coastguard Worker                          re.escape(pattern))
290*14675a02SAndroid Build Coastguard Worker  if alt_proto:
291*14675a02SAndroid Build Coastguard Worker    pattern_regexp += r'\?%24alt=proto'
292*14675a02SAndroid Build Coastguard Worker  try:
293*14675a02SAndroid Build Coastguard Worker    return re.compile(pattern_regexp)
294*14675a02SAndroid Build Coastguard Worker  except re.error as e:
295*14675a02SAndroid Build Coastguard Worker    raise ValueError(f'unable to convert `{pattern}` to a regexp') from e
296