python-socks 2.8.2__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 +55 -60
  16. python_socks/_protocols/socks4.py +22 -26
  17. python_socks/_protocols/socks5.py +77 -70
  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.2.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.2.dist-info/METADATA +0 -295
  74. python_socks-2.8.2.dist-info/RECORD +0 -77
  75. {python_socks-2.8.2.dist-info → python_socks-3.0.0.dist-info}/licenses/LICENSE.txt +0 -0
  76. {python_socks-2.8.2.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,86 +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 .._version import __title__, __version__
9
8
  from .._helpers import is_ipv6_address
10
-
9
+ from .._version import __title__, __version__
11
10
  from .errors import ReplyError
12
11
 
13
- DEFAULT_USER_AGENT = 'Python/{0[0]}.{0[1]} {1}/{2}'.format(
14
- sys.version_info,
15
- __title__,
16
- __version__,
12
+ DEFAULT_USER_AGENT = (
13
+ f"Python/{sys.version_info[0]}.{sys.version_info[1]} {__title__}/{__version__}"
17
14
  )
18
-
19
- CRLF = '\r\n'
15
+ CRLF = "\r\n"
20
16
 
21
17
 
22
- class BasicAuth(namedtuple('BasicAuth', ['login', 'password', 'encoding'])):
23
- """Http basic authentication helper."""
18
+ @dataclass
19
+ class BasicAuth:
20
+ login: str
21
+ password: str
22
+ encoding: str = "latin1"
24
23
 
25
- def __new__(cls, login: str, password: str = '', encoding: str = 'latin1') -> 'BasicAuth':
26
- if login is None:
27
- 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")
28
27
 
29
- if password is None:
30
- 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")
31
30
 
32
- if ':' in login:
31
+ if ":" in self.login:
33
32
  raise ValueError('A ":" is not allowed in login (RFC 1945#section-11.1)')
34
33
 
35
- # noinspection PyTypeChecker,PyArgumentList
36
- return super().__new__(cls, login, password, encoding)
37
-
38
34
  @classmethod
39
- def decode(cls, auth_header: str, encoding: str = 'latin1') -> 'BasicAuth':
35
+ def decode(cls, auth_header: str, encoding: str = "latin1") -> BasicAuth:
40
36
  """Create a BasicAuth object from an Authorization HTTP header."""
41
37
  try:
42
- auth_type, encoded_credentials = auth_header.split(' ', 1)
38
+ auth_type, encoded_credentials = auth_header.split(" ", 1)
43
39
  except ValueError:
44
- raise ValueError('Could not parse authorization header.')
40
+ raise ValueError("Could not parse authorization header.") # noqa: B904
45
41
 
46
- if auth_type.lower() != 'basic':
47
- raise ValueError('Unknown authorization method %s' % auth_type)
42
+ if auth_type.lower() != "basic":
43
+ raise ValueError(f"Unknown authorization method {auth_type}")
48
44
 
49
45
  try:
50
- decoded = base64.b64decode(encoded_credentials.encode('ascii'), validate=True).decode(
51
- encoding
52
- )
46
+ decoded = base64.b64decode(
47
+ encoded_credentials.encode("ascii"),
48
+ validate=True,
49
+ ).decode(encoding)
53
50
  except binascii.Error:
54
- raise ValueError('Invalid base64 encoding.')
51
+ raise ValueError("Invalid base64 encoding.") # noqa: B904
55
52
 
56
53
  try:
57
54
  # RFC 2617 HTTP Authentication
58
55
  # https://www.ietf.org/rfc/rfc2617.txt
59
56
  # the colon must be present, but the username and password may be
60
57
  # otherwise blank.
61
- username, password = decoded.split(':', 1)
58
+ username, password = decoded.split(":", 1)
62
59
  except ValueError:
63
- raise ValueError('Invalid credentials.')
60
+ raise ValueError("Invalid credentials.") # noqa: B904
64
61
 
65
- # noinspection PyTypeChecker
66
- return cls(username, password, encoding=encoding)
62
+ return cls(login=username, password=password, encoding=encoding)
67
63
 
68
64
  def encode(self) -> str:
69
65
  """Encode credentials."""
70
- creds = ('%s:%s' % (self.login, self.password)).encode(self.encoding)
71
- 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)}"
72
68
 
73
69
 
74
70
  class _Buffer:
75
- def __init__(self, encoding: str = 'utf-8'):
71
+ def __init__(self, encoding: str = "utf-8") -> None:
76
72
  self._encoding = encoding
77
73
  self._buffer = bytearray()
78
74
 
79
- def append_line(self, line: str = ""):
75
+ def append_line(self, line: str = "") -> None:
80
76
  if line:
81
77
  self._buffer.extend(line.encode(self._encoding))
82
78
 
83
- self._buffer.extend(CRLF.encode('ascii'))
79
+ self._buffer.extend(CRLF.encode("ascii"))
84
80
 
85
81
  def dumps(self) -> bytes:
86
82
  return bytes(self._buffer)
@@ -90,8 +86,8 @@ class _Buffer:
90
86
  class ConnectRequest:
91
87
  host: str
92
88
  port: int
93
- username: Optional[str]
94
- password: Optional[str]
89
+ username: str | None
90
+ password: str | None
95
91
 
96
92
  def dumps(self) -> bytes:
97
93
  buff = _Buffer()
@@ -103,14 +99,14 @@ class ConnectRequest:
103
99
  # with no port — proxies disagree, and several misroute the
104
100
  # unbracketed form (typically returning `200 Connection
105
101
  # established` against a connection that was never actually made).
106
- host = f'[{self.host}]' if is_ipv6_address(self.host) else self.host
107
- buff.append_line(f'CONNECT {host}:{self.port} HTTP/1.1')
108
- buff.append_line(f'Host: {host}:{self.port}')
109
- buff.append_line(f'User-Agent: {DEFAULT_USER_AGENT}')
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}")
110
106
 
111
107
  if self.username and self.password:
112
108
  auth = BasicAuth(self.username, self.password)
113
- buff.append_line(f'Proxy-Authorization: {auth.encode()}')
109
+ buff.append_line(f"Proxy-Authorization: {auth.encode()}")
114
110
 
115
111
  buff.append_line()
116
112
 
@@ -123,33 +119,32 @@ class ConnectReply:
123
119
  message: str
124
120
 
125
121
  @classmethod
126
- def loads(cls, data: bytes) -> 'ConnectReply':
122
+ def loads(cls, data: bytes) -> ConnectReply:
127
123
  if not data:
128
- raise ReplyError('Invalid proxy response') # pragma: no cover
124
+ raise ReplyError("Invalid proxy response") # pragma: no cover
129
125
 
130
- line = data.split(CRLF.encode('ascii'), 1)[0]
131
- line = line.decode('utf-8', 'surrogateescape')
126
+ line = data.split(CRLF.encode("ascii"), 1)[0]
127
+ line = line.decode("utf-8", "surrogateescape")
132
128
 
133
129
  try:
134
- version, code, *reason = line.split()
135
- except ValueError: # pragma: no cover
136
- 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
137
133
 
138
134
  try:
139
135
  status_code = int(code)
140
- except ValueError: # pragma: no cover
141
- 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
142
138
 
143
139
  status_message = " ".join(reason)
144
140
 
145
- if status_code != 200:
146
- msg = f'{status_code} {status_message}'
141
+ if status_code != 200: # noqa: PLR2004
142
+ msg = f"{status_code} {status_message}"
147
143
  raise ReplyError(msg, error_code=status_code)
148
144
 
149
145
  return cls(status_code=status_code, message=status_message)
150
146
 
151
147
 
152
- # noinspection PyMethodMayBeStatic
153
148
  class Connection:
154
149
  def send(self, request: ConnectRequest) -> bytes:
155
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()