python-socks 2.8.1__py3-none-any.whl → 3.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (76) hide show
  1. python_socks/__init__.py +11 -13
  2. python_socks/_abc.py +35 -22
  3. python_socks/_connectors/abc.py +5 -3
  4. python_socks/_connectors/factory_async.py +8 -8
  5. python_socks/_connectors/factory_sync.py +8 -8
  6. python_socks/_connectors/http_async.py +7 -7
  7. python_socks/_connectors/http_sync.py +7 -7
  8. python_socks/_connectors/socks4_async.py +10 -10
  9. python_socks/_connectors/socks4_sync.py +10 -10
  10. python_socks/_connectors/socks5_async.py +29 -26
  11. python_socks/_connectors/socks5_sync.py +36 -28
  12. python_socks/_errors.py +15 -2
  13. python_socks/_helpers.py +35 -32
  14. python_socks/_protocols/errors.py +10 -1
  15. python_socks/_protocols/http.py +63 -58
  16. python_socks/_protocols/socks4.py +22 -26
  17. python_socks/_protocols/socks5.py +95 -81
  18. python_socks/_types.py +4 -0
  19. python_socks/_version.py +2 -2
  20. python_socks/async_/__init__.py +0 -3
  21. python_socks/async_/anyio/__init__.py +1 -2
  22. python_socks/async_/anyio/_connect.py +6 -4
  23. python_socks/async_/anyio/_proxy.py +85 -92
  24. python_socks/async_/anyio/_resolver.py +10 -3
  25. python_socks/async_/anyio/_stream.py +11 -26
  26. python_socks/async_/anyio/v2/__init__.py +1 -5
  27. python_socks/async_/anyio/v2/_connect.py +7 -4
  28. python_socks/async_/anyio/v2/_proxy.py +36 -26
  29. python_socks/async_/anyio/v2/_stream.py +11 -11
  30. python_socks/async_/asyncio/__init__.py +1 -2
  31. python_socks/async_/asyncio/_connect.py +12 -7
  32. python_socks/async_/asyncio/_proxy.py +48 -49
  33. python_socks/async_/asyncio/_resolver.py +11 -4
  34. python_socks/async_/asyncio/_stream.py +9 -13
  35. python_socks/async_/asyncio/v2/__init__.py +1 -2
  36. python_socks/async_/asyncio/v2/_connect.py +8 -5
  37. python_socks/async_/asyncio/v2/_proxy.py +38 -28
  38. python_socks/async_/asyncio/v2/_stream.py +16 -18
  39. python_socks/async_/trio/__init__.py +1 -1
  40. python_socks/async_/trio/_connect.py +5 -4
  41. python_socks/async_/trio/_proxy.py +46 -48
  42. python_socks/async_/trio/_resolver.py +11 -3
  43. python_socks/async_/trio/_stream.py +9 -9
  44. python_socks/async_/trio/v2/__init__.py +1 -5
  45. python_socks/async_/trio/v2/_connect.py +3 -2
  46. python_socks/async_/trio/v2/_proxy.py +34 -24
  47. python_socks/async_/trio/v2/_stream.py +13 -13
  48. python_socks/sync/__init__.py +1 -3
  49. python_socks/sync/_connect.py +4 -3
  50. python_socks/sync/_proxy.py +42 -43
  51. python_socks/sync/_resolver.py +17 -5
  52. python_socks/sync/_stream.py +9 -9
  53. python_socks/sync/v2/__init__.py +1 -5
  54. python_socks/sync/v2/_connect.py +5 -3
  55. python_socks/sync/v2/_proxy.py +35 -26
  56. python_socks/sync/v2/_ssl_transport.py +3 -0
  57. python_socks/sync/v2/_stream.py +19 -14
  58. python_socks-3.0.0.dist-info/METADATA +346 -0
  59. python_socks-3.0.0.dist-info/RECORD +65 -0
  60. {python_socks-2.8.1.dist-info → python_socks-3.0.0.dist-info}/WHEEL +1 -1
  61. python_socks/async_/_proxy_chain.py +0 -34
  62. python_socks/async_/anyio/_chain.py +0 -42
  63. python_socks/async_/anyio/v2/_chain.py +0 -32
  64. python_socks/async_/asyncio/v2/_chain.py +0 -32
  65. python_socks/async_/curio/__init__.py +0 -4
  66. python_socks/async_/curio/_connect.py +0 -17
  67. python_socks/async_/curio/_proxy.py +0 -132
  68. python_socks/async_/curio/_resolver.py +0 -25
  69. python_socks/async_/curio/_stream.py +0 -32
  70. python_socks/async_/trio/v2/_chain.py +0 -32
  71. python_socks/sync/_chain.py +0 -32
  72. python_socks/sync/v2/_chain.py +0 -26
  73. python_socks-2.8.1.dist-info/METADATA +0 -295
  74. python_socks-2.8.1.dist-info/RECORD +0 -77
  75. {python_socks-2.8.1.dist-info → python_socks-3.0.0.dist-info}/licenses/LICENSE.txt +0 -0
  76. {python_socks-2.8.1.dist-info → python_socks-3.0.0.dist-info}/top_level.txt +0 -0
python_socks/_helpers.py CHANGED
@@ -1,44 +1,45 @@
1
+ from __future__ import annotations
2
+
1
3
  import functools
2
4
  import re
3
- from typing import Optional, Tuple
4
- from urllib.parse import urlparse, unquote
5
+ from urllib.parse import unquote, urlparse
5
6
 
6
7
  from ._types import ProxyType
7
8
 
8
- # pylint:disable-next=invalid-name
9
9
  _ipv4_pattern = (
10
- r'^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}'
11
- r'(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$'
10
+ r"^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}"
11
+ r"(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$"
12
12
  )
13
13
 
14
- # pylint:disable-next=invalid-name
15
14
  _ipv6_pattern = (
16
- r'^(?:(?:(?:[A-F0-9]{1,4}:){6}|(?=(?:[A-F0-9]{0,4}:){0,6}'
17
- r'(?:[0-9]{1,3}\.){3}[0-9]{1,3}$)(([0-9A-F]{1,4}:){0,5}|:)'
18
- r'((:[0-9A-F]{1,4}){1,5}:|:)|::(?:[A-F0-9]{1,4}:){5})'
19
- r'(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}'
20
- r'(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[A-F0-9]{1,4}:){7}'
21
- r'[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)'
22
- r'(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)|(?:[A-F0-9]{1,4}:){7}'
23
- r':|:(:[A-F0-9]{1,4}){7})$'
15
+ r"^(?:(?:(?:[A-F0-9]{1,4}:){6}|(?=(?:[A-F0-9]{0,4}:){0,6}"
16
+ r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}$)(([0-9A-F]{1,4}:){0,5}|:)"
17
+ r"((:[0-9A-F]{1,4}){1,5}:|:)|::(?:[A-F0-9]{1,4}:){5})"
18
+ r"(?:(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])\.){3}"
19
+ r"(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9]?[0-9])|(?:[A-F0-9]{1,4}:){7}"
20
+ r"[A-F0-9]{1,4}|(?=(?:[A-F0-9]{0,4}:){0,7}[A-F0-9]{0,4}$)"
21
+ r"(([0-9A-F]{1,4}:){1,7}|:)((:[0-9A-F]{1,4}){1,7}|:)|(?:[A-F0-9]{1,4}:){7}"
22
+ r":|:(:[A-F0-9]{1,4}){7})$"
24
23
  )
25
24
 
26
25
  _ipv4_regex = re.compile(_ipv4_pattern)
27
26
  _ipv6_regex = re.compile(_ipv6_pattern, flags=re.IGNORECASE)
28
- _ipv4_regexb = re.compile(_ipv4_pattern.encode('ascii'))
29
- _ipv6_regexb = re.compile(_ipv6_pattern.encode('ascii'), flags=re.IGNORECASE)
27
+ _ipv4_regexb = re.compile(_ipv4_pattern.encode("ascii"))
28
+ _ipv6_regexb = re.compile(_ipv6_pattern.encode("ascii"), flags=re.IGNORECASE)
30
29
 
31
30
 
32
- def _is_ip_address(regex, regexb, host):
33
- # if host is None:
34
- # return False
31
+ def _is_ip_address(
32
+ regex: re.Pattern[str],
33
+ regexb: re.Pattern[bytes],
34
+ host: str | bytes | bytearray | memoryview,
35
+ ) -> bool:
35
36
  if isinstance(host, str):
36
37
  return bool(regex.match(host))
37
- elif isinstance(host, (bytes, bytearray, memoryview)):
38
+ elif isinstance(host, (bytes, bytearray, memoryview)): # noqa: RET505
38
39
  return bool(regexb.match(host))
39
40
  else:
40
- raise TypeError(
41
- '{} [{}] is not a str or bytes'.format(host, type(host)) # pragma: no cover
41
+ raise TypeError( # pragma: no cover
42
+ f"{host} [{type(host)}] is not a str or bytes"
42
43
  )
43
44
 
44
45
 
@@ -46,36 +47,38 @@ is_ipv4_address = functools.partial(_is_ip_address, _ipv4_regex, _ipv4_regexb)
46
47
  is_ipv6_address = functools.partial(_is_ip_address, _ipv6_regex, _ipv6_regexb)
47
48
 
48
49
 
49
- def is_ip_address(host):
50
+ def is_ip_address(host: str | bytes) -> bool:
50
51
  return is_ipv4_address(host) or is_ipv6_address(host)
51
52
 
52
53
 
53
- def parse_proxy_url(url: str) -> Tuple[ProxyType, str, int, Optional[str], Optional[str]]:
54
+ def parse_proxy_url(
55
+ url: str,
56
+ ) -> tuple[ProxyType, str, int, str | None, str | None]:
54
57
  parsed = urlparse(url)
55
58
 
56
59
  scheme = parsed.scheme
57
- if scheme == 'socks5':
60
+ if scheme == "socks5":
58
61
  proxy_type = ProxyType.SOCKS5
59
- elif scheme == 'socks4':
62
+ elif scheme == "socks4":
60
63
  proxy_type = ProxyType.SOCKS4
61
- elif scheme == 'http':
64
+ elif scheme == "http":
62
65
  proxy_type = ProxyType.HTTP
63
66
  else:
64
- raise ValueError(f'Invalid scheme component: {scheme}') # pragma: no cover
67
+ raise ValueError(f"Invalid scheme component: {scheme}") # pragma: no cover
65
68
 
66
69
  host = parsed.hostname
67
70
  if not host:
68
- raise ValueError('Empty host component') # pragma: no cover
71
+ raise ValueError("Empty host component") # pragma: no cover
69
72
 
70
73
  try:
71
74
  port = parsed.port
72
75
  assert port is not None
73
76
  except (ValueError, TypeError, AssertionError) as e: # pragma: no cover
74
- raise ValueError('Invalid port component') from e
77
+ raise ValueError("Invalid port component") from e
75
78
 
76
79
  try:
77
- username, password = (unquote(parsed.username), unquote(parsed.password))
80
+ username, password = (unquote(parsed.username), unquote(parsed.password)) # type:ignore[arg-type]
78
81
  except (AttributeError, TypeError):
79
- username, password = '', ''
82
+ username, password = "", ""
80
83
 
81
84
  return proxy_type, host, port, username, password
@@ -1,4 +1,13 @@
1
+ from __future__ import annotations
2
+
3
+ from typing import Any
4
+
5
+
1
6
  class ReplyError(Exception):
2
- def __init__(self, message, error_code=None):
7
+ def __init__(
8
+ self,
9
+ message: Any,
10
+ error_code: int | None = None,
11
+ ) -> None:
3
12
  super().__init__(message)
4
13
  self.error_code = error_code
@@ -1,85 +1,82 @@
1
- import sys
2
- from dataclasses import dataclass
1
+ from __future__ import annotations
2
+
3
3
  import base64
4
4
  import binascii
5
- from collections import namedtuple
6
- from typing import Optional
5
+ import sys
6
+ from dataclasses import dataclass
7
7
 
8
+ from .._helpers import is_ipv6_address
8
9
  from .._version import __title__, __version__
9
-
10
10
  from .errors import ReplyError
11
11
 
12
- DEFAULT_USER_AGENT = 'Python/{0[0]}.{0[1]} {1}/{2}'.format(
13
- sys.version_info,
14
- __title__,
15
- __version__,
12
+ DEFAULT_USER_AGENT = (
13
+ f"Python/{sys.version_info[0]}.{sys.version_info[1]} {__title__}/{__version__}"
16
14
  )
17
-
18
- CRLF = '\r\n'
15
+ CRLF = "\r\n"
19
16
 
20
17
 
21
- class BasicAuth(namedtuple('BasicAuth', ['login', 'password', 'encoding'])):
22
- """Http basic authentication helper."""
18
+ @dataclass
19
+ class BasicAuth:
20
+ login: str
21
+ password: str
22
+ encoding: str = "latin1"
23
23
 
24
- def __new__(cls, login: str, password: str = '', encoding: str = 'latin1') -> 'BasicAuth':
25
- if login is None:
26
- raise ValueError('None is not allowed as login value')
24
+ def __post_init__(self) -> None:
25
+ if self.login is None:
26
+ raise ValueError("None is not allowed as login value")
27
27
 
28
- if password is None:
29
- raise ValueError('None is not allowed as password value')
28
+ if self.password is None:
29
+ raise ValueError("None is not allowed as password value")
30
30
 
31
- if ':' in login:
31
+ if ":" in self.login:
32
32
  raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)')
33
33
 
34
- # noinspection PyTypeChecker,PyArgumentList
35
- return super().__new__(cls, login, password, encoding)
36
-
37
34
  @classmethod
38
- def decode(cls, auth_header: str, encoding: str = 'latin1') -> 'BasicAuth':
35
+ def decode(cls, auth_header: str, encoding: str = "latin1") -> BasicAuth:
39
36
  """Create a BasicAuth object from an Authorization HTTP header."""
40
37
  try:
41
- auth_type, encoded_credentials = auth_header.split(' ', 1)
38
+ auth_type, encoded_credentials = auth_header.split(" ", 1)
42
39
  except ValueError:
43
- raise ValueError('Could not parse authorization header.')
40
+ raise ValueError("Could not parse authorization header.") # noqa: B904
44
41
 
45
- if auth_type.lower() != 'basic':
46
- raise ValueError('Unknown authorization method %s' % auth_type)
42
+ if auth_type.lower() != "basic":
43
+ raise ValueError(f"Unknown authorization method {auth_type}")
47
44
 
48
45
  try:
49
- decoded = base64.b64decode(encoded_credentials.encode('ascii'), validate=True).decode(
50
- encoding
51
- )
46
+ decoded = base64.b64decode(
47
+ encoded_credentials.encode("ascii"),
48
+ validate=True,
49
+ ).decode(encoding)
52
50
  except binascii.Error:
53
- raise ValueError('Invalid base64 encoding.')
51
+ raise ValueError("Invalid base64 encoding.") # noqa: B904
54
52
 
55
53
  try:
56
54
  # RFC 2617 HTTP Authentication
57
55
  # https://www.ietf.org/rfc/rfc2617.txt
58
56
  # the colon must be present, but the username and password may be
59
57
  # otherwise blank.
60
- username, password = decoded.split(':', 1)
58
+ username, password = decoded.split(":", 1)
61
59
  except ValueError:
62
- raise ValueError('Invalid credentials.')
60
+ raise ValueError("Invalid credentials.") # noqa: B904
63
61
 
64
- # noinspection PyTypeChecker
65
- return cls(username, password, encoding=encoding)
62
+ return cls(login=username, password=password, encoding=encoding)
66
63
 
67
64
  def encode(self) -> str:
68
65
  """Encode credentials."""
69
- creds = ('%s:%s' % (self.login, self.password)).encode(self.encoding)
70
- return 'Basic %s' % base64.b64encode(creds).decode(self.encoding)
66
+ creds = f"{self.login}:{self.password}".encode(self.encoding)
67
+ return f"Basic {base64.b64encode(creds).decode(self.encoding)}"
71
68
 
72
69
 
73
70
  class _Buffer:
74
- def __init__(self, encoding: str = 'utf-8'):
71
+ def __init__(self, encoding: str = "utf-8") -> None:
75
72
  self._encoding = encoding
76
73
  self._buffer = bytearray()
77
74
 
78
- def append_line(self, line: str = ""):
75
+ def append_line(self, line: str = "") -> None:
79
76
  if line:
80
77
  self._buffer.extend(line.encode(self._encoding))
81
78
 
82
- self._buffer.extend(CRLF.encode('ascii'))
79
+ self._buffer.extend(CRLF.encode("ascii"))
83
80
 
84
81
  def dumps(self) -> bytes:
85
82
  return bytes(self._buffer)
@@ -89,18 +86,27 @@ class _Buffer:
89
86
  class ConnectRequest:
90
87
  host: str
91
88
  port: int
92
- username: Optional[str]
93
- password: Optional[str]
89
+ username: str | None
90
+ password: str | None
94
91
 
95
92
  def dumps(self) -> bytes:
96
93
  buff = _Buffer()
97
- buff.append_line(f'CONNECT {self.host}:{self.port} HTTP/1.1')
98
- buff.append_line(f'Host: {self.host}:{self.port}')
99
- buff.append_line(f'User-Agent: {DEFAULT_USER_AGENT}')
94
+ # RFC 3986 § 3.2.2 / RFC 7230 § 5.4: a literal IPv6 address in an
95
+ # authority component must be enclosed in square brackets so the
96
+ # `:` between literal and port is unambiguous. Without brackets,
97
+ # `2001:db8::1:443` could be parsed as either the address
98
+ # `2001:db8::1` with port `443` or the address `2001:db8::1:443`
99
+ # with no port — proxies disagree, and several misroute the
100
+ # unbracketed form (typically returning `200 Connection
101
+ # established` against a connection that was never actually made).
102
+ host = f"[{self.host}]" if is_ipv6_address(self.host) else self.host
103
+ buff.append_line(f"CONNECT {host}:{self.port} HTTP/1.1")
104
+ buff.append_line(f"Host: {host}:{self.port}")
105
+ buff.append_line(f"User-Agent: {DEFAULT_USER_AGENT}")
100
106
 
101
107
  if self.username and self.password:
102
108
  auth = BasicAuth(self.username, self.password)
103
- buff.append_line(f'Proxy-Authorization: {auth.encode()}')
109
+ buff.append_line(f"Proxy-Authorization: {auth.encode()}")
104
110
 
105
111
  buff.append_line()
106
112
 
@@ -113,33 +119,32 @@ class ConnectReply:
113
119
  message: str
114
120
 
115
121
  @classmethod
116
- def loads(cls, data: bytes) -> 'ConnectReply':
122
+ def loads(cls, data: bytes) -> ConnectReply:
117
123
  if not data:
118
- raise ReplyError('Invalid proxy response') # pragma: no cover
124
+ raise ReplyError("Invalid proxy response") # pragma: no cover
119
125
 
120
- line = data.split(CRLF.encode('ascii'), 1)[0]
121
- line = line.decode('utf-8', 'surrogateescape')
126
+ line = data.split(CRLF.encode("ascii"), 1)[0]
127
+ line = line.decode("utf-8", "surrogateescape")
122
128
 
123
129
  try:
124
- version, code, *reason = line.split()
125
- except ValueError: # pragma: no cover
126
- raise ReplyError(f'Invalid status line: {line}')
130
+ _version, code, *reason = line.split()
131
+ except ValueError as e: # pragma: no cover
132
+ raise ReplyError(f"Invalid status line: {line}") from e
127
133
 
128
134
  try:
129
135
  status_code = int(code)
130
- except ValueError: # pragma: no cover
131
- raise ReplyError(f'Invalid status code: {code}')
136
+ except ValueError as e: # pragma: no cover
137
+ raise ReplyError(f"Invalid status code: {code}") from e
132
138
 
133
139
  status_message = " ".join(reason)
134
140
 
135
- if status_code != 200:
136
- msg = f'{status_code} {status_message}'
141
+ if status_code != 200: # noqa: PLR2004
142
+ msg = f"{status_code} {status_message}"
137
143
  raise ReplyError(msg, error_code=status_code)
138
144
 
139
145
  return cls(status_code=status_code, message=status_message)
140
146
 
141
147
 
142
- # noinspection PyMethodMayBeStatic
143
148
  class Connection:
144
149
  def send(self, request: ConnectRequest) -> bytes:
145
150
  return request.dumps()
@@ -1,11 +1,12 @@
1
+ from __future__ import annotations
2
+
1
3
  import enum
2
4
  import ipaddress
3
5
  import socket
4
6
  from dataclasses import dataclass
5
- from typing import Optional
6
7
 
7
- from .errors import ReplyError
8
8
  from .._helpers import is_ipv4_address
9
+ from .errors import ReplyError
9
10
 
10
11
  RSV = NULL = 0x00
11
12
  SOCKS_VER = 0x04
@@ -24,14 +25,10 @@ class ReplyCode(enum.IntEnum):
24
25
 
25
26
 
26
27
  ReplyMessages = {
27
- ReplyCode.REQUEST_GRANTED: 'Request granted',
28
- ReplyCode.REQUEST_REJECTED_OR_FAILED: 'Request rejected or failed',
29
- ReplyCode.CONNECTION_FAILED: (
30
- 'Request rejected because SOCKS server cannot connect to identd on the client'
31
- ),
32
- ReplyCode.AUTHENTICATION_FAILED: (
33
- 'Request rejected because the client program and identd report different user-ids'
34
- ),
28
+ ReplyCode.REQUEST_GRANTED: "Request granted",
29
+ ReplyCode.REQUEST_REJECTED_OR_FAILED: "Request rejected or failed",
30
+ ReplyCode.CONNECTION_FAILED: "Request rejected because SOCKS server cannot connect to identd on the client", # noqa: E501
31
+ ReplyCode.AUTHENTICATION_FAILED: "Request rejected because the client program and identd report different user-ids", # noqa: E501,
35
32
  }
36
33
 
37
34
 
@@ -39,10 +36,10 @@ ReplyMessages = {
39
36
  class ConnectRequest:
40
37
  host: str # hostname or IPv4 address
41
38
  port: int
42
- user_id: Optional[str]
39
+ user_id: str | None
43
40
 
44
- def dumps(self):
45
- port_bytes = self.port.to_bytes(2, 'big')
41
+ def dumps(self) -> bytes:
42
+ port_bytes = self.port.to_bytes(2, "big")
46
43
  include_hostname = False
47
44
 
48
45
  if is_ipv4_address(self.host):
@@ -56,12 +53,12 @@ class ConnectRequest:
56
53
  data += host_bytes
57
54
 
58
55
  if self.user_id:
59
- data += self.user_id.encode('ascii')
56
+ data += self.user_id.encode("ascii")
60
57
 
61
58
  data.append(NULL)
62
59
 
63
60
  if include_hostname:
64
- data += self.host.encode('idna')
61
+ data += self.host.encode("idna")
65
62
  data.append(NULL)
66
63
 
67
64
  return bytes(data)
@@ -77,37 +74,36 @@ class ConnectReply:
77
74
  port: int # should be ignored when using Command.CONNECT
78
75
 
79
76
  @classmethod
80
- def loads(cls, data: bytes) -> 'ConnectReply':
77
+ def loads(cls, data: bytes) -> ConnectReply:
81
78
  if len(data) != cls.SIZE:
82
- raise ReplyError('Malformed connect reply')
79
+ raise ReplyError("Malformed connect reply")
83
80
 
84
81
  rsv = data[0]
85
82
  if rsv != RSV: # pragma: no cover
86
- raise ReplyError(f'Unexpected reply version: {data[0]:#02X}')
83
+ raise ReplyError(f"Unexpected reply version: {data[0]:#02X}")
87
84
 
88
85
  try:
89
86
  reply = ReplyCode(data[1])
90
- except ValueError:
91
- raise ReplyError(f'Invalid reply code: {data[1]:#02X}')
87
+ except ValueError as e:
88
+ raise ReplyError(f"Invalid reply code: {data[1]:#02X}") from e
92
89
 
93
90
  if reply != ReplyCode.REQUEST_GRANTED: # pragma: no cover
94
- msg = ReplyMessages.get(reply, 'Unknown error')
91
+ msg = ReplyMessages.get(reply, "Unknown error")
95
92
  raise ReplyError(msg, error_code=reply)
96
93
 
97
94
  try:
98
95
  port = int.from_bytes(data[2:4], byteorder="big")
99
- except ValueError:
100
- raise ReplyError('Invalid port data')
96
+ except ValueError as e:
97
+ raise ReplyError("Invalid port data") from e
101
98
 
102
99
  try:
103
100
  host = socket.inet_ntop(socket.AF_INET, data[4:8])
104
- except ValueError:
105
- raise ReplyError('Invalid port data')
101
+ except ValueError as e:
102
+ raise ReplyError("Invalid port data") from e
106
103
 
107
104
  return cls(rsv=rsv, reply=reply, host=host, port=port)
108
105
 
109
106
 
110
- # noinspection PyMethodMayBeStatic
111
107
  class Connection:
112
108
  def send(self, request: ConnectRequest) -> bytes:
113
109
  return request.dumps()