Skip to content
This repository was archived by the owner on Apr 26, 2024. It is now read-only.

Commit 11c9e17

Browse files
authored
Add type annotations to SimpleHttpClient (#8372)
1 parent 6fdf577 commit 11c9e17

File tree

4 files changed

+143
-61
lines changed

4 files changed

+143
-61
lines changed

changelog.d/8372.misc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
Add type annotations to `SimpleHttpClient`.

synapse/appservice/api.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -178,7 +178,7 @@ async def _get() -> Optional[JsonDict]:
178178
urllib.parse.quote(protocol),
179179
)
180180
try:
181-
info = await self.get_json(uri, {})
181+
info = await self.get_json(uri)
182182

183183
if not _is_valid_3pe_metadata(info):
184184
logger.warning(

synapse/http/client.py

Lines changed: 131 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,18 @@
1717
import logging
1818
import urllib
1919
from io import BytesIO
20+
from typing import (
21+
Any,
22+
BinaryIO,
23+
Dict,
24+
Iterable,
25+
List,
26+
Mapping,
27+
Optional,
28+
Sequence,
29+
Tuple,
30+
Union,
31+
)
2032

2133
import treq
2234
from canonicaljson import encode_canonical_json
@@ -37,6 +49,7 @@
3749
from twisted.web.client import Agent, HTTPConnectionPool, readBody
3850
from twisted.web.http import PotentialDataLoss
3951
from twisted.web.http_headers import Headers
52+
from twisted.web.iweb import IResponse
4053

4154
from synapse.api.errors import Codes, HttpResponseException, SynapseError
4255
from synapse.http import (
@@ -57,6 +70,19 @@
5770
"synapse_http_client_responses", "", ["method", "code"]
5871
)
5972

73+
# the type of the headers list, to be passed to the t.w.h.Headers.
74+
# Actually we can mix str and bytes keys, but Mapping treats 'key' as invariant so
75+
# we simplify.
76+
RawHeaders = Union[Mapping[str, "RawHeaderValue"], Mapping[bytes, "RawHeaderValue"]]
77+
78+
# the value actually has to be a List, but List is invariant so we can't specify that
79+
# the entries can either be Lists or bytes.
80+
RawHeaderValue = Sequence[Union[str, bytes]]
81+
82+
# the type of the query params, to be passed into `urlencode`
83+
QueryParamValue = Union[str, bytes, Iterable[Union[str, bytes]]]
84+
QueryParams = Union[Mapping[str, QueryParamValue], Mapping[bytes, QueryParamValue]]
85+
6086

6187
def check_against_blacklist(ip_address, ip_whitelist, ip_blacklist):
6288
"""
@@ -285,13 +311,26 @@ def __getattr__(_self, attr):
285311
ip_blacklist=self._ip_blacklist,
286312
)
287313

288-
async def request(self, method, uri, data=None, headers=None):
314+
async def request(
315+
self,
316+
method: str,
317+
uri: str,
318+
data: Optional[bytes] = None,
319+
headers: Optional[Headers] = None,
320+
) -> IResponse:
289321
"""
290322
Args:
291-
method (str): HTTP method to use.
292-
uri (str): URI to query.
293-
data (bytes): Data to send in the request body, if applicable.
294-
headers (t.w.http_headers.Headers): Request headers.
323+
method: HTTP method to use.
324+
uri: URI to query.
325+
data: Data to send in the request body, if applicable.
326+
headers: Request headers.
327+
328+
Returns:
329+
Response object, once the headers have been read.
330+
331+
Raises:
332+
RequestTimedOutError if the request times out before the headers are read
333+
295334
"""
296335
# A small wrapper around self.agent.request() so we can easily attach
297336
# counters to it
@@ -324,6 +363,8 @@ async def request(self, method, uri, data=None, headers=None):
324363
headers=headers,
325364
**self._extra_treq_args
326365
)
366+
# we use our own timeout mechanism rather than treq's as a workaround
367+
# for https://twistedmatrix.com/trac/ticket/9534.
327368
request_deferred = timeout_deferred(
328369
request_deferred,
329370
60,
@@ -353,18 +394,26 @@ async def request(self, method, uri, data=None, headers=None):
353394
set_tag("error_reason", e.args[0])
354395
raise
355396

356-
async def post_urlencoded_get_json(self, uri, args={}, headers=None):
397+
async def post_urlencoded_get_json(
398+
self,
399+
uri: str,
400+
args: Mapping[str, Union[str, List[str]]] = {},
401+
headers: Optional[RawHeaders] = None,
402+
) -> Any:
357403
"""
358404
Args:
359-
uri (str):
360-
args (dict[str, str|List[str]]): query params
361-
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
362-
header name to a list of values for that header
405+
uri: uri to query
406+
args: parameters to be url-encoded in the body
407+
headers: a map from header name to a list of values for that header
363408
364409
Returns:
365-
object: parsed json
410+
parsed json
366411
367412
Raises:
413+
RequestTimedOutException: if there is a timeout before the response headers
414+
are received. Note there is currently no timeout on reading the response
415+
body.
416+
368417
HttpResponseException: On a non-2xx HTTP response.
369418
370419
ValueError: if the response was not JSON
@@ -398,19 +447,24 @@ async def post_urlencoded_get_json(self, uri, args={}, headers=None):
398447
response.code, response.phrase.decode("ascii", errors="replace"), body
399448
)
400449

401-
async def post_json_get_json(self, uri, post_json, headers=None):
450+
async def post_json_get_json(
451+
self, uri: str, post_json: Any, headers: Optional[RawHeaders] = None
452+
) -> Any:
402453
"""
403454
404455
Args:
405-
uri (str):
406-
post_json (object):
407-
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
408-
header name to a list of values for that header
456+
uri: URI to query.
457+
post_json: request body, to be encoded as json
458+
headers: a map from header name to a list of values for that header
409459
410460
Returns:
411-
object: parsed json
461+
parsed json
412462
413463
Raises:
464+
RequestTimedOutException: if there is a timeout before the response headers
465+
are received. Note there is currently no timeout on reading the response
466+
body.
467+
414468
HttpResponseException: On a non-2xx HTTP response.
415469
416470
ValueError: if the response was not JSON
@@ -440,21 +494,22 @@ async def post_json_get_json(self, uri, post_json, headers=None):
440494
response.code, response.phrase.decode("ascii", errors="replace"), body
441495
)
442496

443-
async def get_json(self, uri, args={}, headers=None):
444-
""" Gets some json from the given URI.
497+
async def get_json(
498+
self, uri: str, args: QueryParams = {}, headers: Optional[RawHeaders] = None,
499+
) -> Any:
500+
"""Gets some json from the given URI.
445501
446502
Args:
447-
uri (str): The URI to request, not including query parameters
448-
args (dict): A dictionary used to create query strings, defaults to
449-
None.
450-
**Note**: The value of each key is assumed to be an iterable
451-
and *not* a string.
452-
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
453-
header name to a list of values for that header
503+
uri: The URI to request, not including query parameters
504+
args: A dictionary used to create query string
505+
headers: a map from header name to a list of values for that header
454506
Returns:
455-
Succeeds when we get *any* 2xx HTTP response, with the
456-
HTTP body as JSON.
507+
Succeeds when we get a 2xx HTTP response, with the HTTP body as JSON.
457508
Raises:
509+
RequestTimedOutException: if there is a timeout before the response headers
510+
are received. Note there is currently no timeout on reading the response
511+
body.
512+
458513
HttpResponseException On a non-2xx HTTP response.
459514
460515
ValueError: if the response was not JSON
@@ -466,22 +521,27 @@ async def get_json(self, uri, args={}, headers=None):
466521
body = await self.get_raw(uri, args, headers=headers)
467522
return json_decoder.decode(body.decode("utf-8"))
468523

469-
async def put_json(self, uri, json_body, args={}, headers=None):
470-
""" Puts some json to the given URI.
524+
async def put_json(
525+
self,
526+
uri: str,
527+
json_body: Any,
528+
args: QueryParams = {},
529+
headers: RawHeaders = None,
530+
) -> Any:
531+
"""Puts some json to the given URI.
471532
472533
Args:
473-
uri (str): The URI to request, not including query parameters
474-
json_body (dict): The JSON to put in the HTTP body,
475-
args (dict): A dictionary used to create query strings, defaults to
476-
None.
477-
**Note**: The value of each key is assumed to be an iterable
478-
and *not* a string.
479-
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
480-
header name to a list of values for that header
534+
uri: The URI to request, not including query parameters
535+
json_body: The JSON to put in the HTTP body,
536+
args: A dictionary used to create query strings
537+
headers: a map from header name to a list of values for that header
481538
Returns:
482-
Succeeds when we get *any* 2xx HTTP response, with the
483-
HTTP body as JSON.
539+
Succeeds when we get a 2xx HTTP response, with the HTTP body as JSON.
484540
Raises:
541+
RequestTimedOutException: if there is a timeout before the response headers
542+
are received. Note there is currently no timeout on reading the response
543+
body.
544+
485545
HttpResponseException On a non-2xx HTTP response.
486546
487547
ValueError: if the response was not JSON
@@ -513,21 +573,23 @@ async def put_json(self, uri, json_body, args={}, headers=None):
513573
response.code, response.phrase.decode("ascii", errors="replace"), body
514574
)
515575

516-
async def get_raw(self, uri, args={}, headers=None):
517-
""" Gets raw text from the given URI.
576+
async def get_raw(
577+
self, uri: str, args: QueryParams = {}, headers: Optional[RawHeaders] = None
578+
) -> bytes:
579+
"""Gets raw text from the given URI.
518580
519581
Args:
520-
uri (str): The URI to request, not including query parameters
521-
args (dict): A dictionary used to create query strings, defaults to
522-
None.
523-
**Note**: The value of each key is assumed to be an iterable
524-
and *not* a string.
525-
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
526-
header name to a list of values for that header
582+
uri: The URI to request, not including query parameters
583+
args: A dictionary used to create query strings
584+
headers: a map from header name to a list of values for that header
527585
Returns:
528-
Succeeds when we get *any* 2xx HTTP response, with the
586+
Succeeds when we get a 2xx HTTP response, with the
529587
HTTP body as bytes.
530588
Raises:
589+
RequestTimedOutException: if there is a timeout before the response headers
590+
are received. Note there is currently no timeout on reading the response
591+
body.
592+
531593
HttpResponseException on a non-2xx HTTP response.
532594
"""
533595
if len(args):
@@ -552,16 +614,29 @@ async def get_raw(self, uri, args={}, headers=None):
552614
# XXX: FIXME: This is horribly copy-pasted from matrixfederationclient.
553615
# The two should be factored out.
554616

555-
async def get_file(self, url, output_stream, max_size=None, headers=None):
617+
async def get_file(
618+
self,
619+
url: str,
620+
output_stream: BinaryIO,
621+
max_size: Optional[int] = None,
622+
headers: Optional[RawHeaders] = None,
623+
) -> Tuple[int, Dict[bytes, List[bytes]], str, int]:
556624
"""GETs a file from a given URL
557625
Args:
558-
url (str): The URL to GET
559-
output_stream (file): File to write the response body to.
560-
headers (dict[str|bytes, List[str|bytes]]|None): If not None, a map from
561-
header name to a list of values for that header
626+
url: The URL to GET
627+
output_stream: File to write the response body to.
628+
headers: A map from header name to a list of values for that header
562629
Returns:
563-
A (int,dict,string,int) tuple of the file length, dict of the response
630+
A tuple of the file length, dict of the response
564631
headers, absolute URI of the response and HTTP response code.
632+
633+
Raises:
634+
RequestTimedOutException: if there is a timeout before the response headers
635+
are received. Note there is currently no timeout on reading the response
636+
body.
637+
638+
SynapseError: if the response is not a 2xx, the remote file is too large, or
639+
another exception happens during the download.
565640
"""
566641

567642
actual_headers = {b"User-Agent": [self.user_agent]}

synapse/rest/media/v1/preview_url_resource.py

Lines changed: 10 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -450,7 +450,7 @@ async def _get_oembed_content(self, endpoint: str, url: str) -> OEmbedResult:
450450
logger.warning("Error downloading oEmbed metadata from %s: %r", url, e)
451451
raise OEmbedError() from e
452452

453-
async def _download_url(self, url, user):
453+
async def _download_url(self, url: str, user):
454454
# TODO: we should probably honour robots.txt... except in practice
455455
# we're most likely being explicitly triggered by a human rather than a
456456
# bot, so are we really a robot?
@@ -460,7 +460,7 @@ async def _download_url(self, url, user):
460460
file_info = FileInfo(server_name=None, file_id=file_id, url_cache=True)
461461

462462
# If this URL can be accessed via oEmbed, use that instead.
463-
url_to_download = url
463+
url_to_download = url # type: Optional[str]
464464
oembed_url = self._get_oembed_url(url)
465465
if oembed_url:
466466
# The result might be a new URL to download, or it might be HTML content.
@@ -520,9 +520,15 @@ async def _download_url(self, url, user):
520520
# FIXME: we should calculate a proper expiration based on the
521521
# Cache-Control and Expire headers. But for now, assume 1 hour.
522522
expires = ONE_HOUR
523-
etag = headers["ETag"][0] if "ETag" in headers else None
523+
etag = (
524+
headers[b"ETag"][0].decode("ascii") if b"ETag" in headers else None
525+
)
524526
else:
525-
html_bytes = oembed_result.html.encode("utf-8") # type: ignore
527+
# we can only get here if we did an oembed request and have an oembed_result.html
528+
assert oembed_result.html is not None
529+
assert oembed_url is not None
530+
531+
html_bytes = oembed_result.html.encode("utf-8")
526532
with self.media_storage.store_into_file(file_info) as (f, fname, finish):
527533
f.write(html_bytes)
528534
await finish()

0 commit comments

Comments
 (0)