scrapli 2023.7.30__py3-none-any.whl → 2024.7.30__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 (64) hide show
  1. scrapli/__init__.py +2 -1
  2. scrapli/channel/__init__.py +1 -0
  3. scrapli/channel/async_channel.py +35 -12
  4. scrapli/channel/base_channel.py +25 -3
  5. scrapli/channel/sync_channel.py +35 -12
  6. scrapli/decorators.py +1 -0
  7. scrapli/driver/__init__.py +1 -0
  8. scrapli/driver/base/__init__.py +1 -0
  9. scrapli/driver/base/async_driver.py +19 -13
  10. scrapli/driver/base/base_driver.py +121 -37
  11. scrapli/driver/base/sync_driver.py +19 -13
  12. scrapli/driver/core/__init__.py +1 -0
  13. scrapli/driver/core/arista_eos/__init__.py +1 -0
  14. scrapli/driver/core/arista_eos/async_driver.py +3 -0
  15. scrapli/driver/core/arista_eos/base_driver.py +3 -2
  16. scrapli/driver/core/arista_eos/sync_driver.py +3 -0
  17. scrapli/driver/core/cisco_iosxe/__init__.py +1 -0
  18. scrapli/driver/core/cisco_iosxe/async_driver.py +3 -0
  19. scrapli/driver/core/cisco_iosxe/base_driver.py +1 -0
  20. scrapli/driver/core/cisco_iosxe/sync_driver.py +3 -0
  21. scrapli/driver/core/cisco_iosxr/__init__.py +1 -0
  22. scrapli/driver/core/cisco_iosxr/async_driver.py +3 -0
  23. scrapli/driver/core/cisco_iosxr/base_driver.py +1 -0
  24. scrapli/driver/core/cisco_iosxr/sync_driver.py +3 -0
  25. scrapli/driver/core/cisco_nxos/__init__.py +1 -0
  26. scrapli/driver/core/cisco_nxos/async_driver.py +3 -0
  27. scrapli/driver/core/cisco_nxos/base_driver.py +9 -4
  28. scrapli/driver/core/cisco_nxos/sync_driver.py +3 -0
  29. scrapli/driver/core/juniper_junos/__init__.py +1 -0
  30. scrapli/driver/core/juniper_junos/async_driver.py +3 -0
  31. scrapli/driver/core/juniper_junos/base_driver.py +1 -0
  32. scrapli/driver/core/juniper_junos/sync_driver.py +3 -0
  33. scrapli/driver/generic/__init__.py +1 -0
  34. scrapli/driver/generic/async_driver.py +45 -3
  35. scrapli/driver/generic/base_driver.py +2 -1
  36. scrapli/driver/generic/sync_driver.py +45 -3
  37. scrapli/driver/network/__init__.py +1 -0
  38. scrapli/driver/network/async_driver.py +27 -0
  39. scrapli/driver/network/base_driver.py +1 -0
  40. scrapli/driver/network/sync_driver.py +27 -0
  41. scrapli/exceptions.py +1 -0
  42. scrapli/factory.py +22 -3
  43. scrapli/helper.py +76 -4
  44. scrapli/logging.py +1 -0
  45. scrapli/response.py +1 -0
  46. scrapli/ssh_config.py +1 -0
  47. scrapli/transport/base/__init__.py +1 -0
  48. scrapli/transport/base/async_transport.py +1 -0
  49. scrapli/transport/base/base_socket.py +1 -0
  50. scrapli/transport/base/base_transport.py +1 -0
  51. scrapli/transport/base/sync_transport.py +1 -0
  52. scrapli/transport/plugins/asyncssh/transport.py +4 -0
  53. scrapli/transport/plugins/asynctelnet/transport.py +13 -6
  54. scrapli/transport/plugins/paramiko/transport.py +1 -0
  55. scrapli/transport/plugins/ssh2/transport.py +6 -3
  56. scrapli/transport/plugins/system/ptyprocess.py +50 -13
  57. scrapli/transport/plugins/system/transport.py +27 -6
  58. scrapli/transport/plugins/telnet/transport.py +13 -7
  59. {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/METADATA +74 -47
  60. scrapli-2024.7.30.dist-info/RECORD +74 -0
  61. {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/WHEEL +1 -1
  62. scrapli-2023.7.30.dist-info/RECORD +0 -74
  63. {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/LICENSE +0 -0
  64. {scrapli-2023.7.30.dist-info → scrapli-2024.7.30.dist-info}/top_level.txt +0 -0
scrapli/helper.py CHANGED
@@ -1,19 +1,35 @@
1
1
  """scrapli.helper"""
2
+
2
3
  import importlib
4
+ import importlib.resources
5
+ import sys
3
6
  import urllib.request
4
7
  from io import BytesIO, TextIOWrapper
5
8
  from pathlib import Path
6
9
  from shutil import get_terminal_size
7
- from typing import Any, Dict, List, Optional, TextIO, Union
10
+ from typing import Any, Dict, List, Optional, TextIO, Tuple, Union
8
11
  from warnings import warn
9
12
 
10
- import pkg_resources
11
-
12
13
  from scrapli.exceptions import ScrapliValueError
13
14
  from scrapli.logging import logger
14
15
  from scrapli.settings import Settings
15
16
 
16
17
 
18
+ def _textfsm_get_template_directory() -> str:
19
+ if sys.version_info >= (3, 9):
20
+ return f"{importlib.resources.files('ntc_templates')}/templates"
21
+
22
+ if sys.version_info >= (3, 11):
23
+ # https://docs.python.org/3/library/importlib.resources.html#importlib.resources.path
24
+ with importlib.resources.as_file(
25
+ importlib.resources.files("ntc_templates").joinpath("templates")
26
+ ) as path:
27
+ return str(path)
28
+
29
+ with importlib.resources.path("ntc_templates", "templates") as path: # pylint: disable=W4902
30
+ return str(path)
31
+
32
+
17
33
  def _textfsm_get_template(platform: str, command: str) -> Optional[TextIO]:
18
34
  """
19
35
  Find correct TextFSM template based on platform and command executed
@@ -43,7 +59,9 @@ def _textfsm_get_template(platform: str, command: str) -> Optional[TextIO]:
43
59
  )
44
60
  user_warning(title=title, message=message)
45
61
  return None
46
- template_dir = pkg_resources.resource_filename("ntc_templates", "templates")
62
+
63
+ template_dir = _textfsm_get_template_directory()
64
+
47
65
  cli_table = CliTable("index", template_dir)
48
66
  template_index = cli_table.index.GetRowMatch({"Platform": platform, "Command": command})
49
67
  if not template_index:
@@ -296,3 +314,57 @@ def user_warning(title: str, message: str) -> None:
296
314
 
297
315
  if Settings.SUPPRESS_USER_WARNINGS is False:
298
316
  warn(warning_message)
317
+
318
+
319
+ def output_roughly_contains_input(input_: bytes, output: bytes) -> bool:
320
+ """
321
+ Return True if all characters in input are contained in order in the given output.
322
+
323
+ Args:
324
+ input_: the input presented to a device
325
+ output: the output echoed on the channel
326
+
327
+ Returns:
328
+ bool: True if the input is "roughly" contained in the output, otherwise False
329
+
330
+ Raises:
331
+ N/A
332
+
333
+ """
334
+ if output in input_:
335
+ return True
336
+
337
+ if len(output) < len(input_):
338
+ return False
339
+
340
+ for char in input_:
341
+ should_continue, output = _roughly_contains_input_iter_output_for_input_char(char, output)
342
+
343
+ if not should_continue:
344
+ return False
345
+
346
+ return True
347
+
348
+
349
+ def _roughly_contains_input_iter_output_for_input_char(
350
+ char: int, output: bytes
351
+ ) -> Tuple[bool, bytes]:
352
+ """
353
+ Iterate over chars in the output to find input, returns remaining output bytes if input found.
354
+
355
+ Args:
356
+ char: input char to find in output
357
+ output: the output echoed on the channel
358
+
359
+ Returns:
360
+ output: bool indicating char was found, and remaining output chars to continue searching in
361
+
362
+ Raises:
363
+ N/A
364
+
365
+ """
366
+ for index, output_char in enumerate(output):
367
+ if char == output_char:
368
+ return True, output[index + 1 :] # noqa: E203
369
+
370
+ return False, b""
scrapli/logging.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """scrapli.logging"""
2
+
2
3
  from ast import literal_eval
3
4
 
4
5
  # slightly irritating renaming to prevent a cyclic lookup in griffe for mkdocstrings
scrapli/response.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """scrapli.response"""
2
+
2
3
  from collections import UserList
3
4
  from datetime import datetime
4
5
  from io import TextIOWrapper
scrapli/ssh_config.py CHANGED
@@ -1,4 +1,5 @@
1
1
  """scrapli.ssh_config"""
2
+
2
3
  import base64
3
4
  import hmac
4
5
  import os
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.base"""
2
+
2
3
  from scrapli.transport.base.async_transport import AsyncTransport
3
4
  from scrapli.transport.base.base_transport import BasePluginTransportArgs, BaseTransportArgs
4
5
  from scrapli.transport.base.sync_transport import Transport
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.async_transport"""
2
+
2
3
  from abc import ABC, abstractmethod
3
4
 
4
5
  from scrapli.transport.base.base_transport import BaseTransport
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.base.base_socket"""
2
+
2
3
  import socket
3
4
  from contextlib import suppress
4
5
  from typing import Optional, Set
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.base_transport"""
2
+
2
3
  from abc import ABC, abstractmethod
3
4
  from dataclasses import dataclass
4
5
  from typing import Any, Dict
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.base_transport"""
2
+
2
3
  from abc import ABC, abstractmethod
3
4
 
4
5
  from scrapli.transport.base.base_transport import BaseTransport
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.plugins.asyncssh.transport"""
2
+
2
3
  import asyncio
3
4
  from contextlib import suppress
4
5
  from dataclasses import dataclass
@@ -259,6 +260,9 @@ class AsyncsshTransport(AsyncTransport):
259
260
  if not self.stdout:
260
261
  raise ScrapliConnectionNotOpened
261
262
 
263
+ if self.stdout.at_eof():
264
+ raise ScrapliConnectionError("transport at EOF; no more data to be read")
265
+
262
266
  try:
263
267
  buf: bytes = await self.stdout.read(65535)
264
268
  except ConnectionLost as exc:
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.plugins.asynctelnet.transport"""
2
+
2
3
  import asyncio
3
4
  import socket
4
5
  from contextlib import suppress
@@ -116,6 +117,18 @@ class AsynctelnetTransport(AsyncTransport):
116
117
  if not self.stdout:
117
118
  raise ScrapliConnectionNotOpened
118
119
 
120
+ if self._raw_buf.find(NULL) != -1:
121
+ raise ScrapliConnectionNotOpened("server returned EOF, connection not opened")
122
+
123
+ index = self._raw_buf.find(IAC)
124
+ if index == -1:
125
+ self._cooked_buf = self._raw_buf
126
+ self._raw_buf = b""
127
+ return
128
+
129
+ self._cooked_buf = self._raw_buf[:index]
130
+ self._raw_buf = self._raw_buf[index:]
131
+
119
132
  # control_buf is the buffer for control characters, we reset this after being "done" with
120
133
  # responding to a control sequence, so it always represents the "current" control sequence
121
134
  # we are working on responding to
@@ -123,9 +136,6 @@ class AsynctelnetTransport(AsyncTransport):
123
136
 
124
137
  while self._raw_buf:
125
138
  c, self._raw_buf = self._raw_buf[:1], self._raw_buf[1:]
126
- if not c:
127
- raise ScrapliConnectionNotOpened("server returned EOF, connection not opened")
128
-
129
139
  control_buf = self._handle_control_chars_response(control_buf=control_buf, c=c)
130
140
 
131
141
  async def open(self) -> None:
@@ -204,9 +214,6 @@ class AsynctelnetTransport(AsyncTransport):
204
214
  if not self.stdout:
205
215
  raise ScrapliConnectionNotOpened
206
216
 
207
- if self._control_char_sent_counter < self._control_char_sent_limit:
208
- self._handle_control_chars()
209
-
210
217
  while not self._cooked_buf and not self._eof:
211
218
  await self._read()
212
219
  if self._control_char_sent_counter < self._control_char_sent_limit:
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.plugins.paramiko.transport"""
2
+
2
3
  from contextlib import suppress
3
4
  from dataclasses import dataclass
4
5
  from typing import Optional
@@ -1,12 +1,15 @@
1
1
  """scrapli.transport.plugins.ssh2.transport"""
2
+
2
3
  import base64
3
4
  from contextlib import suppress
4
5
  from dataclasses import dataclass
5
6
  from typing import Optional
6
7
 
7
- from ssh2.channel import Channel
8
- from ssh2.exceptions import AuthenticationError, SSH2Error
9
- from ssh2.session import Session
8
+ # ignoring unable to import complaints for linters as ssh2 support is a bit lackluster due to
9
+ # upstream library staleness
10
+ from ssh2.channel import Channel # pylint: disable=E0401,E0611
11
+ from ssh2.exceptions import AuthenticationError, SSH2Error # pylint: disable=E0401,E0611
12
+ from ssh2.session import Session # pylint: disable=E0401,E0611
10
13
 
11
14
  from scrapli.exceptions import (
12
15
  ScrapliAuthenticationFailed,
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.plugins.system.ptyprocess"""
2
+
2
3
  """
3
4
  Ptyprocess is under the ISC license, as code derived from Pexpect.
4
5
  http://opensource.org/licenses/ISC
@@ -33,6 +34,7 @@ from shutil import which
33
34
  from typing import List, Optional, Type, TypeVar
34
35
 
35
36
  from scrapli.exceptions import ScrapliValueError
37
+ from scrapli.helper import user_warning
36
38
 
37
39
 
38
40
  class PtyProcessError(Exception):
@@ -85,7 +87,7 @@ def _make_eof_intr() -> None:
85
87
  raise ValueError("No stream has a fileno")
86
88
  intr = ord(termios.tcgetattr(fd)[6][VINTR])
87
89
  eof = ord(termios.tcgetattr(fd)[6][VEOF])
88
- except (ImportError, OSError, IOError, ValueError, termios.error):
90
+ except (ImportError, OSError, ValueError, termios.error):
89
91
  # unless the controlling process is also not a terminal,
90
92
  # such as cron(1), or when stdin and stdout are both closed.
91
93
  # Fall-back to using CEOF and CINTR. There
@@ -141,9 +143,9 @@ def _setecho(fd: int, state: bool) -> None:
141
143
  None
142
144
 
143
145
  Raises:
144
- IOError: if termios raises an exception getting the fd
146
+ OSError: if termios raises an exception getting the fd or raises an exception setting the
147
+ echo state on the fd
145
148
  termios.error: also if termios rasies an exception gettign fd... unclear why the two errors!
146
- IOError: if termios raises an exception setting the echo state on the fd
147
149
 
148
150
  """
149
151
  import termios
@@ -157,7 +159,7 @@ def _setecho(fd: int, state: bool) -> None:
157
159
  attr = termios.tcgetattr(fd)
158
160
  except termios.error as err:
159
161
  if err.args[0] == errno.EINVAL:
160
- raise IOError(err.args[0], "%s: %s." % (err.args[1], errmsg))
162
+ raise OSError(err.args[0], "{}: {}.".format(err.args[1], errmsg))
161
163
  raise
162
164
 
163
165
  if state:
@@ -169,9 +171,38 @@ def _setecho(fd: int, state: bool) -> None:
169
171
  # I tried TCSADRAIN and TCSAFLUSH, but these were inconsistent and
170
172
  # blocked on some platforms. TCSADRAIN would probably be ideal.
171
173
  termios.tcsetattr(fd, termios.TCSANOW, attr)
172
- except IOError as err:
174
+ except OSError as err:
175
+ if err.args[0] == errno.EINVAL:
176
+ raise OSError(err.args[0], "{}: {}.".format(err.args[1], errmsg))
177
+ raise
178
+
179
+
180
+ def _setonlcr(fd: int, state: bool) -> None:
181
+ import termios
182
+
183
+ try:
184
+ attr = termios.tcgetattr(fd)
185
+ except termios.error as err:
173
186
  if err.args[0] == errno.EINVAL:
174
- raise IOError(err.args[0], "%s: %s." % (err.args[1], errmsg))
187
+ raise OSError(err.args[0], "{}: {}.".format(err.args[1], errmsg))
188
+ raise
189
+
190
+ if state:
191
+ attr[1] = attr[1] | termios.ONLCR
192
+ else:
193
+ attr[1] = attr[1] & ~termios.ONLCR
194
+
195
+ try:
196
+ termios.tcsetattr(fd, termios.TCSANOW, attr)
197
+ except OSError as err:
198
+ if err.args[0] == errno.EINVAL:
199
+ title = "Set ONLCR!"
200
+ message = (
201
+ "_setonlcr() failed -- if you encounter this error please open an issue! unless you "
202
+ "are seeing this when using scrapli_netconf you can *probably* ignore this though!"
203
+ )
204
+
205
+ user_warning(title=title, message=message)
175
206
  raise
176
207
 
177
208
 
@@ -196,8 +227,8 @@ class PtyProcess:
196
227
  _make_eof_intr() # Ensure _EOF and _INTR are calculated
197
228
  self.pid = pid
198
229
  self.fd = fd
199
- readf = io.open(fd, "rb", buffering=0)
200
- writef = io.open(fd, "wb", buffering=0, closefd=False)
230
+ readf = open(fd, "rb", buffering=0)
231
+ writef = open(fd, "wb", buffering=0, closefd=False)
201
232
  self.fileobj = io.BufferedRWPair(readf, writef) # type: ignore
202
233
 
203
234
  self.terminated = False
@@ -244,7 +275,7 @@ class PtyProcess:
244
275
  ScrapliValueError: if no ssh binary found on PATH
245
276
  Exception: IOError - if unable to set window size of child process
246
277
  Exception: OSError - if unable to spawn command in child process
247
- IOError: failing to reset window size
278
+ OSError: failing to reset window size
248
279
  exception: if we get an exception decoding output
249
280
 
250
281
  """
@@ -283,7 +314,7 @@ class PtyProcess:
283
314
  if pid == CHILD:
284
315
  try:
285
316
  _setwinsize(fd=STDIN_FILENO, rows=rows, cols=cols)
286
- except IOError as err:
317
+ except OSError as err:
287
318
  if err.args[0] not in (errno.EINVAL, errno.ENOTTY):
288
319
  raise
289
320
 
@@ -291,7 +322,7 @@ class PtyProcess:
291
322
  if echo is False:
292
323
  try:
293
324
  _setecho(STDIN_FILENO, False)
294
- except (IOError, termios.error) as err:
325
+ except (OSError, termios.error) as err:
295
326
  if err.args[0] not in (errno.EINVAL, errno.ENOTTY):
296
327
  raise
297
328
 
@@ -350,10 +381,16 @@ class PtyProcess:
350
381
 
351
382
  try:
352
383
  inst.setwinsize(rows=rows, cols=cols)
353
- except IOError as err:
384
+ except OSError as err:
354
385
  if err.args[0] not in (errno.EINVAL, errno.ENOTTY, errno.ENXIO):
355
386
  raise
356
387
 
388
+ # attrs = termios.tcgetattr(fd)
389
+ # attrs[1] &= ~termios.ONLCR
390
+ # termios.tcsetattr(fd, termios.TCSANOW, attrs)
391
+
392
+ _setonlcr(fd, True)
393
+
357
394
  return inst
358
395
 
359
396
  def __repr__(self) -> str:
@@ -481,7 +518,7 @@ class PtyProcess:
481
518
  """
482
519
  try:
483
520
  s = self.fileobj.read1(size)
484
- except (OSError, IOError) as err:
521
+ except OSError as err:
485
522
  if err.args[0] == errno.EIO:
486
523
  # Linux-style EOF
487
524
  self.flag_eof = True
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.plugins.system.transport"""
2
+
2
3
  import sys
3
4
  from dataclasses import dataclass
4
5
  from typing import List, Optional
@@ -23,6 +24,9 @@ class PluginTransportArgs(BasePluginTransportArgs):
23
24
 
24
25
 
25
26
  class SystemTransport(Transport):
27
+ SSH_SYSTEM_CONFIG_MAGIC_STRING: str = "SYSTEM_TRANSPORT_SSH_CONFIG_TRUE"
28
+ SSH_SYSTEM_KNOWN_HOSTS_FILE_MAGIC_STRING: str = "SYSTEM_TRANSPORT_KNOWN_HOSTS_TRUE"
29
+
26
30
  def __init__(
27
31
  self, base_transport_args: BaseTransportArgs, plugin_transport_args: PluginTransportArgs
28
32
  ) -> None:
@@ -99,15 +103,32 @@ class SystemTransport(Transport):
99
103
  self.open_cmd.extend(["-o", "UserKnownHostsFile=/dev/null"])
100
104
  else:
101
105
  self.open_cmd.extend(["-o", "StrictHostKeyChecking=yes"])
102
- if self.plugin_transport_args.ssh_known_hosts_file:
106
+
107
+ if (
108
+ self.plugin_transport_args.ssh_known_hosts_file
109
+ == self.SSH_SYSTEM_KNOWN_HOSTS_FILE_MAGIC_STRING
110
+ ):
111
+ self.logger.debug(
112
+ "Using system transport and ssh_known_hosts_file is True, not specifying any "
113
+ "known_hosts file"
114
+ )
115
+ elif self.plugin_transport_args.ssh_known_hosts_file:
103
116
  self.open_cmd.extend(
104
- ["-o", f"UserKnownHostsFile={self.plugin_transport_args.ssh_known_hosts_file}"]
117
+ [
118
+ "-o",
119
+ f"UserKnownHostsFile={self.plugin_transport_args.ssh_known_hosts_file}",
120
+ ]
105
121
  )
106
-
107
- if self.plugin_transport_args.ssh_config_file:
108
- self.open_cmd.extend(["-F", self.plugin_transport_args.ssh_config_file])
109
- else:
122
+ else:
123
+ self.logger.debug("No known hosts file specified")
124
+ if not self.plugin_transport_args.ssh_config_file:
110
125
  self.open_cmd.extend(["-F", "/dev/null"])
126
+ elif self.plugin_transport_args.ssh_config_file == self.SSH_SYSTEM_CONFIG_MAGIC_STRING:
127
+ self.logger.debug(
128
+ "Using system transport and ssh_config is True, not specifying any SSH config"
129
+ )
130
+ else:
131
+ self.open_cmd.extend(["-F", self.plugin_transport_args.ssh_config_file])
111
132
 
112
133
  open_cmd_user_args = self._base_transport_args.transport_options.get("open_cmd", [])
113
134
  if isinstance(open_cmd_user_args, str):
@@ -1,4 +1,5 @@
1
1
  """scrapli.transport.plugins.telnet.transport"""
2
+
2
3
  from dataclasses import dataclass
3
4
  from typing import Optional
4
5
 
@@ -147,13 +148,21 @@ class TelnetTransport(Transport):
147
148
  if not self.socket:
148
149
  raise ScrapliConnectionNotOpened
149
150
 
151
+ if self._raw_buf.find(NULL) != -1:
152
+ raise ScrapliConnectionNotOpened("server returned EOF, connection not opened")
153
+
154
+ index = self._raw_buf.find(IAC)
155
+ if index == -1:
156
+ self._cooked_buf = self._raw_buf
157
+ self._raw_buf = b""
158
+ return
159
+
160
+ self._cooked_buf = self._raw_buf[:index]
161
+ self._raw_buf = self._raw_buf[index:]
150
162
  control_buf = b""
151
163
 
152
164
  while self._raw_buf:
153
165
  c, self._raw_buf = self._raw_buf[:1], self._raw_buf[1:]
154
- if not c:
155
- raise ScrapliConnectionNotOpened("server returned EOF, connection not opened")
156
-
157
166
  control_buf = self._handle_control_chars_response(control_buf=control_buf, c=c)
158
167
 
159
168
  def open(self) -> None:
@@ -217,7 +226,7 @@ class TelnetTransport(Transport):
217
226
  self._raw_buf += buf
218
227
  else:
219
228
  self._cooked_buf += buf
220
- except Exception as exc:
229
+ except EOFError as exc:
221
230
  raise ScrapliConnectionError(
222
231
  "encountered EOF reading from transport; typically means the device closed the "
223
232
  "connection"
@@ -228,9 +237,6 @@ class TelnetTransport(Transport):
228
237
  if not self.socket:
229
238
  raise ScrapliConnectionNotOpened
230
239
 
231
- if self._control_char_sent_counter < self._control_char_sent_limit:
232
- self._handle_control_chars()
233
-
234
240
  while not self._cooked_buf and not self._eof:
235
241
  self._read()
236
242
  if self._control_char_sent_counter < self._control_char_sent_limit:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: scrapli
3
- Version: 2023.7.30
3
+ Version: 2024.7.30
4
4
  Summary: Fast, flexible, sync/async, Python 3.7+ screen scraping client specifically for network devices
5
5
  Author-email: Carl Montanari <carl.r.montanari@gmail.com>
6
6
  License: MIT License
@@ -33,69 +33,96 @@ Classifier: License :: OSI Approved :: MIT License
33
33
  Classifier: Operating System :: POSIX :: Linux
34
34
  Classifier: Operating System :: MacOS
35
35
  Classifier: Programming Language :: Python
36
- Classifier: Programming Language :: Python :: 3.7
37
36
  Classifier: Programming Language :: Python :: 3.8
38
37
  Classifier: Programming Language :: Python :: 3.9
39
38
  Classifier: Programming Language :: Python :: 3.10
40
39
  Classifier: Programming Language :: Python :: 3.11
40
+ Classifier: Programming Language :: Python :: 3.12
41
41
  Classifier: Programming Language :: Python :: 3 :: Only
42
42
  Classifier: Topic :: Software Development :: Libraries :: Python Modules
43
- Requires-Python: >=3.7
43
+ Requires-Python: >=3.8
44
44
  Description-Content-Type: text/markdown
45
45
  License-File: LICENSE
46
46
  Provides-Extra: asyncssh
47
- Requires-Dist: asyncssh (<3.0.0,>=2.2.1) ; extra == 'asyncssh'
47
+ Requires-Dist: asyncssh <3.0.0,>=2.2.1 ; extra == 'asyncssh'
48
48
  Provides-Extra: community
49
- Requires-Dist: scrapli-community (>=2021.01.30) ; extra == 'community'
49
+ Requires-Dist: scrapli-community >=2021.01.30 ; extra == 'community'
50
50
  Provides-Extra: dev
51
- Requires-Dist: black (<24.0.0,>=23.3.0) ; extra == 'dev'
52
- Requires-Dist: darglint (<2.0.0,>=1.8.1) ; extra == 'dev'
53
- Requires-Dist: isort (<6.0.0,>=5.10.1) ; extra == 'dev'
54
- Requires-Dist: mypy (==1.4.1) ; extra == 'dev'
55
- Requires-Dist: nox (==2023.4.22) ; extra == 'dev'
56
- Requires-Dist: pycodestyle (<3.0.0,>=2.8.0) ; extra == 'dev'
57
- Requires-Dist: pydocstyle (<7.0.0,>=6.1.1) ; extra == 'dev'
58
- Requires-Dist: pyfakefs (<6.0.0,>=5.0.0) ; extra == 'dev'
59
- Requires-Dist: pylama (<9.0.0,>=8.4.0) ; extra == 'dev'
60
- Requires-Dist: pylint (==2.17.5) ; extra == 'dev'
61
- Requires-Dist: pytest-asyncio (<1.0.0,>=0.17.0) ; extra == 'dev'
62
- Requires-Dist: pytest-cov (<5.0.0,>=3.0.0) ; extra == 'dev'
63
- Requires-Dist: pytest (<8.0.0,>=7.0.0) ; extra == 'dev'
64
- Requires-Dist: scrapli-cfg (==2022.7.30) ; extra == 'dev'
65
- Requires-Dist: scrapli-replay (==2022.7.30) ; extra == 'dev'
66
- Requires-Dist: toml (<1.0.0,>=0.10.2) ; extra == 'dev'
67
- Requires-Dist: types-paramiko (<4.0.0,>=2.8.6) ; extra == 'dev'
68
- Requires-Dist: types-pkg-resources (<1.0.0,>=0.1.3) ; extra == 'dev'
69
- Requires-Dist: ntc-templates (<4.0.0,>=1.1.0) ; extra == 'dev'
70
- Requires-Dist: textfsm (<2.0.0,>=1.1.0) ; extra == 'dev'
71
- Requires-Dist: ttp (<1.0.0,>=0.5.0) ; extra == 'dev'
72
- Requires-Dist: paramiko (<3.0.0,>=2.6.0) ; extra == 'dev'
73
- Requires-Dist: asyncssh (<3.0.0,>=2.2.1) ; extra == 'dev'
74
- Requires-Dist: scrapli-community (>=2021.01.30) ; extra == 'dev'
75
- Requires-Dist: ssh2-python (<2.0.0,>=0.23.0) ; (python_version < "3.12") and extra == 'dev'
76
- Requires-Dist: genie (>=20.2) ; (sys_platform != "win32" and python_version < "3.11") and extra == 'dev'
77
- Requires-Dist: pyats (>=20.2) ; (sys_platform != "win32" and python_version < "3.11") and extra == 'dev'
51
+ Requires-Dist: black <25.0.0,>=23.3.0 ; extra == 'dev'
52
+ Requires-Dist: darglint <2.0.0,>=1.8.1 ; extra == 'dev'
53
+ Requires-Dist: isort <6.0.0,>=5.10.1 ; extra == 'dev'
54
+ Requires-Dist: mypy <2.0.0,>=1.4.1 ; extra == 'dev'
55
+ Requires-Dist: nox ==2024.4.15 ; extra == 'dev'
56
+ Requires-Dist: pycodestyle <3.0.0,>=2.8.0 ; extra == 'dev'
57
+ Requires-Dist: pydocstyle <7.0.0,>=6.1.1 ; extra == 'dev'
58
+ Requires-Dist: pyfakefs <6.0.0,>=5.4.1 ; extra == 'dev'
59
+ Requires-Dist: pylama <9.0.0,>=8.4.0 ; extra == 'dev'
60
+ Requires-Dist: pylint <4.0.0,>=3.0.0 ; extra == 'dev'
61
+ Requires-Dist: pytest-asyncio <1.0.0,>=0.17.0 ; extra == 'dev'
62
+ Requires-Dist: pytest-cov <5.0.0,>=3.0.0 ; extra == 'dev'
63
+ Requires-Dist: pytest <8.0.0,>=7.0.0 ; extra == 'dev'
64
+ Requires-Dist: scrapli-cfg ==2023.7.30 ; extra == 'dev'
65
+ Requires-Dist: scrapli-replay ==2023.7.30 ; extra == 'dev'
66
+ Requires-Dist: toml <1.0.0,>=0.10.2 ; extra == 'dev'
67
+ Requires-Dist: types-paramiko <4.0.0,>=2.8.6 ; extra == 'dev'
68
+ Requires-Dist: types-pkg-resources <1.0.0,>=0.1.3 ; extra == 'dev'
69
+ Requires-Dist: ntc-templates <5.0.0,>=1.1.0 ; extra == 'dev'
70
+ Requires-Dist: textfsm <2.0.0,>=1.1.0 ; extra == 'dev'
71
+ Requires-Dist: ttp <1.0.0,>=0.5.0 ; extra == 'dev'
72
+ Requires-Dist: paramiko <4.0.0,>=2.6.0 ; extra == 'dev'
73
+ Requires-Dist: asyncssh <3.0.0,>=2.2.1 ; extra == 'dev'
74
+ Requires-Dist: scrapli-community >=2021.01.30 ; extra == 'dev'
75
+ Provides-Extra: dev-darwin
76
+ Requires-Dist: black <25.0.0,>=23.3.0 ; extra == 'dev-darwin'
77
+ Requires-Dist: darglint <2.0.0,>=1.8.1 ; extra == 'dev-darwin'
78
+ Requires-Dist: isort <6.0.0,>=5.10.1 ; extra == 'dev-darwin'
79
+ Requires-Dist: mypy <2.0.0,>=1.4.1 ; extra == 'dev-darwin'
80
+ Requires-Dist: nox ==2024.4.15 ; extra == 'dev-darwin'
81
+ Requires-Dist: pycodestyle <3.0.0,>=2.8.0 ; extra == 'dev-darwin'
82
+ Requires-Dist: pydocstyle <7.0.0,>=6.1.1 ; extra == 'dev-darwin'
83
+ Requires-Dist: pyfakefs <6.0.0,>=5.4.1 ; extra == 'dev-darwin'
84
+ Requires-Dist: pylama <9.0.0,>=8.4.0 ; extra == 'dev-darwin'
85
+ Requires-Dist: pylint <4.0.0,>=3.0.0 ; extra == 'dev-darwin'
86
+ Requires-Dist: pytest-asyncio <1.0.0,>=0.17.0 ; extra == 'dev-darwin'
87
+ Requires-Dist: pytest-cov <5.0.0,>=3.0.0 ; extra == 'dev-darwin'
88
+ Requires-Dist: pytest <8.0.0,>=7.0.0 ; extra == 'dev-darwin'
89
+ Requires-Dist: scrapli-cfg ==2023.7.30 ; extra == 'dev-darwin'
90
+ Requires-Dist: scrapli-replay ==2023.7.30 ; extra == 'dev-darwin'
91
+ Requires-Dist: toml <1.0.0,>=0.10.2 ; extra == 'dev-darwin'
92
+ Requires-Dist: types-paramiko <4.0.0,>=2.8.6 ; extra == 'dev-darwin'
93
+ Requires-Dist: types-pkg-resources <1.0.0,>=0.1.3 ; extra == 'dev-darwin'
94
+ Requires-Dist: ntc-templates <5.0.0,>=1.1.0 ; extra == 'dev-darwin'
95
+ Requires-Dist: textfsm <2.0.0,>=1.1.0 ; extra == 'dev-darwin'
96
+ Requires-Dist: ttp <1.0.0,>=0.5.0 ; extra == 'dev-darwin'
97
+ Requires-Dist: paramiko <4.0.0,>=2.6.0 ; extra == 'dev-darwin'
98
+ Requires-Dist: asyncssh <3.0.0,>=2.2.1 ; extra == 'dev-darwin'
99
+ Requires-Dist: scrapli-community >=2021.01.30 ; extra == 'dev-darwin'
100
+ Requires-Dist: genie <24.4,>=20.2 ; (sys_platform != "win32" and python_version < "3.11") and extra == 'dev-darwin'
101
+ Requires-Dist: pyats >=20.2 ; (sys_platform != "win32" and python_version < "3.11") and extra == 'dev-darwin'
102
+ Requires-Dist: ssh2-python <2.0.0,>=0.23.0 ; (python_version < "3.12") and extra == 'dev'
103
+ Requires-Dist: genie <24.4,>=20.2 ; (sys_platform != "win32" and python_version < "3.11") and extra == 'dev'
104
+ Requires-Dist: pyats >=20.2 ; (sys_platform != "win32" and python_version < "3.11") and extra == 'dev'
78
105
  Provides-Extra: docs
79
- Requires-Dist: mdx-gh-links (<1.0,>=0.2) ; extra == 'docs'
80
- Requires-Dist: mkdocs (<2.0.0,>=1.2.3) ; extra == 'docs'
81
- Requires-Dist: mkdocs-gen-files (<1.0.0,>=0.4.0) ; extra == 'docs'
82
- Requires-Dist: mkdocs-literate-nav (<1.0.0,>=0.5.0) ; extra == 'docs'
83
- Requires-Dist: mkdocs-material (<10.0.0,>=8.1.6) ; extra == 'docs'
84
- Requires-Dist: mkdocs-material-extensions (<2.0.0,>=1.0.3) ; extra == 'docs'
85
- Requires-Dist: mkdocs-section-index (<1.0.0,>=0.3.4) ; extra == 'docs'
86
- Requires-Dist: mkdocstrings[python] (<1.0.0,>=0.19.0) ; extra == 'docs'
106
+ Requires-Dist: mdx-gh-links <1.0,>=0.2 ; extra == 'docs'
107
+ Requires-Dist: mkdocs <2.0.0,>=1.2.3 ; extra == 'docs'
108
+ Requires-Dist: mkdocs-gen-files <1.0.0,>=0.4.0 ; extra == 'docs'
109
+ Requires-Dist: mkdocs-literate-nav <1.0.0,>=0.5.0 ; extra == 'docs'
110
+ Requires-Dist: mkdocs-material <10.0.0,>=8.1.6 ; extra == 'docs'
111
+ Requires-Dist: mkdocs-material-extensions <2.0.0,>=1.0.3 ; extra == 'docs'
112
+ Requires-Dist: mkdocs-section-index <1.0.0,>=0.3.4 ; extra == 'docs'
113
+ Requires-Dist: mkdocstrings[python] <1.0.0,>=0.19.0 ; extra == 'docs'
87
114
  Provides-Extra: genie
88
- Requires-Dist: genie (>=20.2) ; (sys_platform != "win32" and python_version < "3.11") and extra == 'genie'
89
- Requires-Dist: pyats (>=20.2) ; (sys_platform != "win32" and python_version < "3.11") and extra == 'genie'
115
+ Requires-Dist: genie <24.4,>=20.2 ; (sys_platform != "win32" and python_version < "3.11") and extra == 'genie'
116
+ Requires-Dist: pyats >=20.2 ; (sys_platform != "win32" and python_version < "3.11") and extra == 'genie'
90
117
  Provides-Extra: paramiko
91
- Requires-Dist: paramiko (<3.0.0,>=2.6.0) ; extra == 'paramiko'
118
+ Requires-Dist: paramiko <4.0.0,>=2.6.0 ; extra == 'paramiko'
92
119
  Provides-Extra: ssh2
93
- Requires-Dist: ssh2-python (<2.0.0,>=0.23.0) ; (python_version < "3.12") and extra == 'ssh2'
120
+ Requires-Dist: ssh2-python <2.0.0,>=0.23.0 ; (python_version < "3.12") and extra == 'ssh2'
94
121
  Provides-Extra: textfsm
95
- Requires-Dist: ntc-templates (<4.0.0,>=1.1.0) ; extra == 'textfsm'
96
- Requires-Dist: textfsm (<2.0.0,>=1.1.0) ; extra == 'textfsm'
122
+ Requires-Dist: ntc-templates <5.0.0,>=1.1.0 ; extra == 'textfsm'
123
+ Requires-Dist: textfsm <2.0.0,>=1.1.0 ; extra == 'textfsm'
97
124
  Provides-Extra: ttp
98
- Requires-Dist: ttp (<1.0.0,>=0.5.0) ; extra == 'ttp'
125
+ Requires-Dist: ttp <1.0.0,>=0.5.0 ; extra == 'ttp'
99
126
 
100
127
  <p center><a href=""><img src=https://github.com/carlmontanari/scrapli/blob/main/scrapli.svg?sanitize=true/></a></p>
101
128