scrapli 2023.7.30__tar.gz → 2024.7.30__tar.gz

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 (93) hide show
  1. {scrapli-2023.7.30/scrapli.egg-info → scrapli-2024.7.30}/PKG-INFO +75 -4
  2. {scrapli-2023.7.30 → scrapli-2024.7.30}/pyproject.toml +12 -2
  3. {scrapli-2023.7.30 → scrapli-2024.7.30}/requirements-dev.txt +7 -7
  4. scrapli-2024.7.30/requirements-genie.txt +2 -0
  5. scrapli-2024.7.30/requirements-paramiko.txt +1 -0
  6. scrapli-2024.7.30/requirements-textfsm.txt +2 -0
  7. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/__init__.py +2 -1
  8. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/channel/__init__.py +1 -0
  9. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/channel/async_channel.py +35 -12
  10. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/channel/base_channel.py +25 -3
  11. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/channel/sync_channel.py +35 -12
  12. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/decorators.py +1 -0
  13. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/__init__.py +1 -0
  14. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/base/__init__.py +1 -0
  15. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/base/async_driver.py +19 -13
  16. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/base/base_driver.py +121 -37
  17. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/base/sync_driver.py +19 -13
  18. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/__init__.py +1 -0
  19. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/arista_eos/__init__.py +1 -0
  20. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/arista_eos/async_driver.py +3 -0
  21. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/arista_eos/base_driver.py +3 -2
  22. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/arista_eos/sync_driver.py +3 -0
  23. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_iosxe/__init__.py +1 -0
  24. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_iosxe/async_driver.py +3 -0
  25. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_iosxe/base_driver.py +1 -0
  26. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_iosxe/sync_driver.py +3 -0
  27. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_iosxr/__init__.py +1 -0
  28. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_iosxr/async_driver.py +3 -0
  29. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_iosxr/base_driver.py +1 -0
  30. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_iosxr/sync_driver.py +3 -0
  31. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_nxos/__init__.py +1 -0
  32. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_nxos/async_driver.py +3 -0
  33. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_nxos/base_driver.py +9 -4
  34. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/cisco_nxos/sync_driver.py +3 -0
  35. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/juniper_junos/__init__.py +1 -0
  36. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/juniper_junos/async_driver.py +3 -0
  37. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/juniper_junos/base_driver.py +1 -0
  38. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/core/juniper_junos/sync_driver.py +3 -0
  39. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/generic/__init__.py +1 -0
  40. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/generic/async_driver.py +45 -3
  41. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/generic/base_driver.py +2 -1
  42. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/generic/sync_driver.py +45 -3
  43. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/network/__init__.py +1 -0
  44. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/network/async_driver.py +27 -0
  45. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/network/base_driver.py +1 -0
  46. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/driver/network/sync_driver.py +27 -0
  47. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/exceptions.py +1 -0
  48. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/factory.py +22 -3
  49. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/helper.py +76 -4
  50. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/logging.py +1 -0
  51. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/response.py +1 -0
  52. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/ssh_config.py +1 -0
  53. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/base/__init__.py +1 -0
  54. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/base/async_transport.py +1 -0
  55. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/base/base_socket.py +1 -0
  56. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/base/base_transport.py +1 -0
  57. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/base/sync_transport.py +1 -0
  58. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/asyncssh/transport.py +4 -0
  59. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/asynctelnet/transport.py +13 -6
  60. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/paramiko/transport.py +1 -0
  61. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/ssh2/transport.py +6 -3
  62. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/system/ptyprocess.py +50 -13
  63. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/system/transport.py +27 -6
  64. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/telnet/transport.py +13 -7
  65. {scrapli-2023.7.30 → scrapli-2024.7.30/scrapli.egg-info}/PKG-INFO +75 -4
  66. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli.egg-info/requires.txt +43 -13
  67. scrapli-2023.7.30/requirements-genie.txt +0 -2
  68. scrapli-2023.7.30/requirements-paramiko.txt +0 -1
  69. scrapli-2023.7.30/requirements-textfsm.txt +0 -2
  70. {scrapli-2023.7.30 → scrapli-2024.7.30}/LICENSE +0 -0
  71. {scrapli-2023.7.30 → scrapli-2024.7.30}/MANIFEST.in +0 -0
  72. {scrapli-2023.7.30 → scrapli-2024.7.30}/README.md +0 -0
  73. {scrapli-2023.7.30 → scrapli-2024.7.30}/requirements-asyncssh.txt +0 -0
  74. {scrapli-2023.7.30 → scrapli-2024.7.30}/requirements-community.txt +0 -0
  75. {scrapli-2023.7.30 → scrapli-2024.7.30}/requirements-docs.txt +0 -0
  76. {scrapli-2023.7.30 → scrapli-2024.7.30}/requirements-ssh2.txt +0 -0
  77. {scrapli-2023.7.30 → scrapli-2024.7.30}/requirements-ttp.txt +0 -0
  78. {scrapli-2023.7.30 → scrapli-2024.7.30}/requirements.txt +0 -0
  79. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/py.typed +0 -0
  80. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/settings.py +0 -0
  81. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/__init__.py +0 -0
  82. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/base/telnet_common.py +0 -0
  83. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/__init__.py +0 -0
  84. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/asyncssh/__init__.py +0 -0
  85. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/asynctelnet/__init__.py +0 -0
  86. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/paramiko/__init__.py +0 -0
  87. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/ssh2/__init__.py +0 -0
  88. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/system/__init__.py +0 -0
  89. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli/transport/plugins/telnet/__init__.py +0 -0
  90. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli.egg-info/SOURCES.txt +0 -0
  91. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli.egg-info/dependency_links.txt +0 -0
  92. {scrapli-2023.7.30 → scrapli-2024.7.30}/scrapli.egg-info/top_level.txt +0 -0
  93. {scrapli-2023.7.30 → scrapli-2024.7.30}/setup.cfg +0 -0
@@ -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,25 +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
+ License-File: LICENSE
46
+ Provides-Extra: dev-darwin
47
+ Requires-Dist: black<25.0.0,>=23.3.0; extra == "dev-darwin"
48
+ Requires-Dist: darglint<2.0.0,>=1.8.1; extra == "dev-darwin"
49
+ Requires-Dist: isort<6.0.0,>=5.10.1; extra == "dev-darwin"
50
+ Requires-Dist: mypy<2.0.0,>=1.4.1; extra == "dev-darwin"
51
+ Requires-Dist: nox==2024.4.15; extra == "dev-darwin"
52
+ Requires-Dist: pycodestyle<3.0.0,>=2.8.0; extra == "dev-darwin"
53
+ Requires-Dist: pydocstyle<7.0.0,>=6.1.1; extra == "dev-darwin"
54
+ Requires-Dist: pyfakefs<6.0.0,>=5.4.1; extra == "dev-darwin"
55
+ Requires-Dist: pylama<9.0.0,>=8.4.0; extra == "dev-darwin"
56
+ Requires-Dist: pylint<4.0.0,>=3.0.0; extra == "dev-darwin"
57
+ Requires-Dist: pytest-asyncio<1.0.0,>=0.17.0; extra == "dev-darwin"
58
+ Requires-Dist: pytest-cov<5.0.0,>=3.0.0; extra == "dev-darwin"
59
+ Requires-Dist: pytest<8.0.0,>=7.0.0; extra == "dev-darwin"
60
+ Requires-Dist: scrapli-cfg==2023.7.30; extra == "dev-darwin"
61
+ Requires-Dist: scrapli-replay==2023.7.30; extra == "dev-darwin"
62
+ Requires-Dist: toml<1.0.0,>=0.10.2; extra == "dev-darwin"
63
+ Requires-Dist: types-paramiko<4.0.0,>=2.8.6; extra == "dev-darwin"
64
+ Requires-Dist: types-pkg-resources<1.0.0,>=0.1.3; extra == "dev-darwin"
65
+ Requires-Dist: ntc-templates<5.0.0,>=1.1.0; extra == "dev-darwin"
66
+ Requires-Dist: textfsm<2.0.0,>=1.1.0; extra == "dev-darwin"
67
+ Requires-Dist: genie<24.4,>=20.2; (sys_platform != "win32" and python_version < "3.11") and extra == "dev-darwin"
68
+ Requires-Dist: pyats>=20.2; (sys_platform != "win32" and python_version < "3.11") and extra == "dev-darwin"
69
+ Requires-Dist: ttp<1.0.0,>=0.5.0; extra == "dev-darwin"
70
+ Requires-Dist: paramiko<4.0.0,>=2.6.0; extra == "dev-darwin"
71
+ Requires-Dist: asyncssh<3.0.0,>=2.2.1; extra == "dev-darwin"
72
+ Requires-Dist: scrapli_community>=2021.01.30; extra == "dev-darwin"
45
73
  Provides-Extra: dev
74
+ Requires-Dist: black<25.0.0,>=23.3.0; extra == "dev"
75
+ Requires-Dist: darglint<2.0.0,>=1.8.1; extra == "dev"
76
+ Requires-Dist: isort<6.0.0,>=5.10.1; extra == "dev"
77
+ Requires-Dist: mypy<2.0.0,>=1.4.1; extra == "dev"
78
+ Requires-Dist: nox==2024.4.15; extra == "dev"
79
+ Requires-Dist: pycodestyle<3.0.0,>=2.8.0; extra == "dev"
80
+ Requires-Dist: pydocstyle<7.0.0,>=6.1.1; extra == "dev"
81
+ Requires-Dist: pyfakefs<6.0.0,>=5.4.1; extra == "dev"
82
+ Requires-Dist: pylama<9.0.0,>=8.4.0; extra == "dev"
83
+ Requires-Dist: pylint<4.0.0,>=3.0.0; extra == "dev"
84
+ Requires-Dist: pytest-asyncio<1.0.0,>=0.17.0; extra == "dev"
85
+ Requires-Dist: pytest-cov<5.0.0,>=3.0.0; extra == "dev"
86
+ Requires-Dist: pytest<8.0.0,>=7.0.0; extra == "dev"
87
+ Requires-Dist: scrapli-cfg==2023.7.30; extra == "dev"
88
+ Requires-Dist: scrapli-replay==2023.7.30; extra == "dev"
89
+ Requires-Dist: toml<1.0.0,>=0.10.2; extra == "dev"
90
+ Requires-Dist: types-paramiko<4.0.0,>=2.8.6; extra == "dev"
91
+ Requires-Dist: types-pkg-resources<1.0.0,>=0.1.3; extra == "dev"
92
+ Requires-Dist: ntc-templates<5.0.0,>=1.1.0; extra == "dev"
93
+ Requires-Dist: textfsm<2.0.0,>=1.1.0; extra == "dev"
94
+ Requires-Dist: genie<24.4,>=20.2; (sys_platform != "win32" and python_version < "3.11") and extra == "dev"
95
+ Requires-Dist: pyats>=20.2; (sys_platform != "win32" and python_version < "3.11") and extra == "dev"
96
+ Requires-Dist: ttp<1.0.0,>=0.5.0; extra == "dev"
97
+ Requires-Dist: paramiko<4.0.0,>=2.6.0; extra == "dev"
98
+ Requires-Dist: ssh2-python<2.0.0,>=0.23.0; python_version < "3.12" and extra == "dev"
99
+ Requires-Dist: asyncssh<3.0.0,>=2.2.1; extra == "dev"
100
+ Requires-Dist: scrapli_community>=2021.01.30; extra == "dev"
46
101
  Provides-Extra: docs
102
+ Requires-Dist: mdx-gh-links<1.0,>=0.2; extra == "docs"
103
+ Requires-Dist: mkdocs<2.0.0,>=1.2.3; extra == "docs"
104
+ Requires-Dist: mkdocs-gen-files<1.0.0,>=0.4.0; extra == "docs"
105
+ Requires-Dist: mkdocs-literate-nav<1.0.0,>=0.5.0; extra == "docs"
106
+ Requires-Dist: mkdocs-material<10.0.0,>=8.1.6; extra == "docs"
107
+ Requires-Dist: mkdocs-material-extensions<2.0.0,>=1.0.3; extra == "docs"
108
+ Requires-Dist: mkdocs-section-index<1.0.0,>=0.3.4; extra == "docs"
109
+ Requires-Dist: mkdocstrings[python]<1.0.0,>=0.19.0; extra == "docs"
47
110
  Provides-Extra: textfsm
111
+ Requires-Dist: ntc-templates<5.0.0,>=1.1.0; extra == "textfsm"
112
+ Requires-Dist: textfsm<2.0.0,>=1.1.0; extra == "textfsm"
48
113
  Provides-Extra: genie
114
+ Requires-Dist: genie<24.4,>=20.2; (sys_platform != "win32" and python_version < "3.11") and extra == "genie"
115
+ Requires-Dist: pyats>=20.2; (sys_platform != "win32" and python_version < "3.11") and extra == "genie"
49
116
  Provides-Extra: ttp
117
+ Requires-Dist: ttp<1.0.0,>=0.5.0; extra == "ttp"
50
118
  Provides-Extra: paramiko
119
+ Requires-Dist: paramiko<4.0.0,>=2.6.0; extra == "paramiko"
51
120
  Provides-Extra: ssh2
121
+ Requires-Dist: ssh2-python<2.0.0,>=0.23.0; python_version < "3.12" and extra == "ssh2"
52
122
  Provides-Extra: asyncssh
123
+ Requires-Dist: asyncssh<3.0.0,>=2.2.1; extra == "asyncssh"
53
124
  Provides-Extra: community
54
- License-File: LICENSE
125
+ Requires-Dist: scrapli_community>=2021.01.30; extra == "community"
55
126
 
56
127
  <p center><a href=""><img src=https://github.com/carlmontanari/scrapli/blob/main/scrapli.svg?sanitize=true/></a></p>
57
128
 
@@ -28,17 +28,17 @@ license = { file = "LICENSE" }
28
28
  authors = [
29
29
  { name = "Carl Montanari", email = "carl.r.montanari@gmail.com" },
30
30
  ]
31
- requires-python = ">=3.7"
31
+ requires-python = ">=3.8"
32
32
  classifiers = [
33
33
  "License :: OSI Approved :: MIT License",
34
34
  "Operating System :: POSIX :: Linux",
35
35
  "Operating System :: MacOS",
36
36
  "Programming Language :: Python",
37
- "Programming Language :: Python :: 3.7",
38
37
  "Programming Language :: Python :: 3.8",
39
38
  "Programming Language :: Python :: 3.9",
40
39
  "Programming Language :: Python :: 3.10",
41
40
  "Programming Language :: Python :: 3.11",
41
+ "Programming Language :: Python :: 3.12",
42
42
  "Programming Language :: Python :: 3 :: Only",
43
43
  "Topic :: Software Development :: Libraries :: Python Modules",
44
44
  ]
@@ -55,6 +55,16 @@ Homepage = "https://github.com/carlmontanari/scrapli"
55
55
  [tool.setuptools.dynamic]
56
56
  version = { attr = "scrapli.__version__" }
57
57
  dependencies = { file = "requirements.txt" }
58
+ # dev-darwin is same as dev but excludes ssh2-python
59
+ optional-dependencies.dev-darwin = { file = [
60
+ "requirements-dev.txt",
61
+ "requirements-textfsm.txt",
62
+ "requirements-genie.txt",
63
+ "requirements-ttp.txt",
64
+ "requirements-paramiko.txt",
65
+ "requirements-asyncssh.txt",
66
+ "requirements-community.txt",
67
+ ] }
58
68
  optional-dependencies.dev = { file = [
59
69
  "requirements-dev.txt",
60
70
  "requirements-textfsm.txt",
@@ -1,18 +1,18 @@
1
- black>=23.3.0,<24.0.0
1
+ black>=23.3.0,<25.0.0
2
2
  darglint>=1.8.1,<2.0.0
3
3
  isort>=5.10.1,<6.0.0
4
- mypy==1.4.1
5
- nox==2023.4.22
4
+ mypy>=1.4.1,<2.0.0
5
+ nox==2024.4.15
6
6
  pycodestyle>=2.8.0,<3.0.0
7
7
  pydocstyle>=6.1.1,<7.0.0
8
- pyfakefs>=5.0.0,<6.0.0
8
+ pyfakefs>=5.4.1,<6.0.0
9
9
  pylama>=8.4.0,<9.0.0
10
- pylint==2.17.5
10
+ pylint>=3.0.0,<4.0.0
11
11
  pytest-asyncio>=0.17.0,<1.0.0
12
12
  pytest-cov>=3.0.0,<5.0.0
13
13
  pytest>=7.0.0,<8.0.0
14
- scrapli-cfg==2022.7.30
15
- scrapli-replay==2022.7.30
14
+ scrapli-cfg==2023.7.30
15
+ scrapli-replay==2023.7.30
16
16
  toml>=0.10.2,<1.0.0
17
17
  types-paramiko>=2.8.6,<4.0.0
18
18
  types-pkg-resources>=0.1.3,<1.0.0
@@ -0,0 +1,2 @@
1
+ genie>=20.2,<24.4 ; sys_platform != "win32" and python_version < "3.11"
2
+ pyats>=20.2 ; sys_platform != "win32" and python_version < "3.11"
@@ -0,0 +1 @@
1
+ paramiko>=2.6.0,<4.0.0
@@ -0,0 +1,2 @@
1
+ ntc-templates>=1.1.0,<5.0.0
2
+ textfsm>=1.1.0,<2.0.0
@@ -1,8 +1,9 @@
1
1
  """scrapli"""
2
+
2
3
  from scrapli.driver.base import AsyncDriver, Driver
3
4
  from scrapli.factory import AsyncScrapli, Scrapli
4
5
 
5
- __version__ = "2023.07.30"
6
+ __version__ = "2024.07.30"
6
7
 
7
8
  __all__ = (
8
9
  "AsyncDriver",
@@ -1,4 +1,5 @@
1
1
  """scrapli.channel"""
2
+
2
3
  from scrapli.channel.async_channel import AsyncChannel
3
4
  from scrapli.channel.sync_channel import Channel
4
5
 
@@ -1,4 +1,5 @@
1
1
  """scrapli.channel.async_channel"""
2
+
2
3
  import asyncio
3
4
  import re
4
5
  import time
@@ -10,6 +11,7 @@ from typing import AsyncIterator, List, Optional, Tuple
10
11
  from scrapli.channel.base_channel import BaseChannel, BaseChannelArgs
11
12
  from scrapli.decorators import timeout_wrapper
12
13
  from scrapli.exceptions import ScrapliAuthenticationFailed, ScrapliTimeout
14
+ from scrapli.helper import output_roughly_contains_input
13
15
  from scrapli.transport.base import AsyncTransport
14
16
 
15
17
 
@@ -69,7 +71,7 @@ class AsyncChannel(BaseChannel):
69
71
  buf = await self.transport.read()
70
72
  buf = buf.replace(b"\r", b"")
71
73
 
72
- self.logger.debug(f"read: {buf!r}")
74
+ self.logger.debug("read: %r", buf)
73
75
 
74
76
  if self.channel_log:
75
77
  self.channel_log.write(buf)
@@ -104,9 +106,16 @@ class AsyncChannel(BaseChannel):
104
106
  while True:
105
107
  buf += await self.read()
106
108
 
107
- # replace any backspace chars (particular problem w/ junos), and remove any added spaces
108
- # this is just for comparison of the inputs to what was read from channel
109
- if processed_channel_input in b"".join(buf.lower().replace(b"\x08", b"").split()):
109
+ if not self._base_channel_args.comms_roughly_match_inputs:
110
+ # replace any backspace chars (particular problem w/ junos), and remove any added
111
+ # spaces this is just for comparison of the inputs to what was read from channel
112
+ # note (2024) this would be worked around by using the roughly contains search,
113
+ # *but* that is slower (probably immaterially for most people but... ya know...)
114
+ processed_buf = b"".join(buf.lower().replace(b"\x08", b"").split())
115
+
116
+ if processed_channel_input in processed_buf:
117
+ return buf
118
+ elif output_roughly_contains_input(input_=processed_channel_input, output=buf):
110
119
  return buf
111
120
 
112
121
  async def _read_until_prompt(self, buf: bytes = b"") -> bytes:
@@ -241,7 +250,7 @@ class AsyncChannel(BaseChannel):
241
250
 
242
251
  if (time.time() - start) > read_duration:
243
252
  break
244
- if any((channel_output in search_buf for channel_output in channel_outputs)):
253
+ if any(channel_output in search_buf for channel_output in channel_outputs):
245
254
  break
246
255
  if re.search(pattern=regex_channel_outputs_pattern, string=search_buf):
247
256
  break
@@ -455,6 +464,7 @@ class AsyncChannel(BaseChannel):
455
464
  *,
456
465
  strip_prompt: bool = True,
457
466
  eager: bool = False,
467
+ eager_input: bool = False,
458
468
  ) -> Tuple[bytes, bytes]:
459
469
  """
460
470
  Primary entry point to send data to devices in shell mode; accept input and returns result
@@ -465,6 +475,8 @@ class AsyncChannel(BaseChannel):
465
475
  eager: eager mode reads and returns the `_read_until_input` value, but does not attempt
466
476
  to read to the prompt pattern -- this should not be used manually! (only used by
467
477
  `send_configs` with the eager flag set)
478
+ eager_input: when true does *not* try to read our input off the channel -- generally
479
+ this should be left alone unless you know what you are doing!
468
480
 
469
481
  Returns:
470
482
  Tuple[bytes, bytes]: tuple of "raw" output and "processed" (cleaned up/stripped) output
@@ -479,12 +491,18 @@ class AsyncChannel(BaseChannel):
479
491
  bytes_channel_input = channel_input.encode()
480
492
 
481
493
  self.logger.info(
482
- f"sending channel input: {channel_input}; strip_prompt: {strip_prompt}; eager: {eager}"
494
+ "sending channel input: %s; strip_prompt: %s; eager: %s",
495
+ channel_input,
496
+ strip_prompt,
497
+ eager,
483
498
  )
484
499
 
485
500
  async with self._channel_lock():
486
501
  self.write(channel_input=channel_input)
487
- _buf_until_input = await self._read_until_input(channel_input=bytes_channel_input)
502
+
503
+ if not eager_input:
504
+ _buf_until_input = await self._read_until_input(channel_input=bytes_channel_input)
505
+
488
506
  self.send_return()
489
507
 
490
508
  if not eager:
@@ -531,8 +549,12 @@ class AsyncChannel(BaseChannel):
531
549
  ]
532
550
 
533
551
  self.logger.info(
534
- f"sending channel input and read: {channel_input}; strip_prompt: {strip_prompt}; "
535
- f"expected_outputs: {expected_outputs}; read_duration: {read_duration}"
552
+ "sending channel input and read: %s; strip_prompt: %s; "
553
+ "expected_outputs: %s; read_duration: %s",
554
+ channel_input,
555
+ strip_prompt,
556
+ expected_outputs,
557
+ read_duration,
536
558
  )
537
559
 
538
560
  async with self._channel_lock():
@@ -642,9 +664,10 @@ class AsyncChannel(BaseChannel):
642
664
 
643
665
  _channel_input = channel_input if not hidden_input else "REDACTED"
644
666
  self.logger.info(
645
- f"sending interactive input: {_channel_input}; "
646
- f"expecting: {channel_response}; "
647
- f"hidden_input: {hidden_input}"
667
+ "sending interactive input: %s; expecting: %s; hidden_input: %s",
668
+ _channel_input,
669
+ channel_response,
670
+ hidden_input,
648
671
  )
649
672
 
650
673
  self.write(channel_input=channel_input, redacted=bool(hidden_input))
@@ -1,4 +1,5 @@
1
1
  """scrapli.channel.base_channel"""
2
+
2
3
  import re
3
4
  from dataclasses import dataclass
4
5
  from datetime import datetime
@@ -10,7 +11,19 @@ from scrapli.exceptions import ScrapliAuthenticationFailed, ScrapliTypeError, Sc
10
11
  from scrapli.logging import get_instance_logger
11
12
  from scrapli.transport.base import AsyncTransport, Transport
12
13
 
13
- ANSI_ESCAPE_PATTERN = re.compile(rb"\x1b(\[.*?[@-~]|\].*?(\x07|\x1b\\)|E)")
14
+ ANSI_ESCAPE_PATTERN = re.compile(
15
+ pattern=rb"[\x1B\x9B\x9D](\s)?" # Prefix ESC (^) or CSI (^[) or OSC (^)
16
+ rb"("
17
+ rb"([78ME])" # control cursor position
18
+ rb"|"
19
+ rb"((\]\d).*?[\x07])" # BEL (Terminal bell)
20
+ rb"|"
21
+ rb"(\[.*?[@-~])" # control codes starts with `[` e.x. ESC [2;37;41m
22
+ rb"|"
23
+ rb"(\[.*?[0-9;]m)" # Select Graphic Rendition (SGR) control sequence
24
+ rb")",
25
+ flags=re.VERBOSE,
26
+ )
14
27
 
15
28
 
16
29
  @dataclass()
@@ -31,6 +44,12 @@ class BaseChannelArgs:
31
44
  comms_prompt_search_depth: depth of the buffer to search in for searching for the prompt
32
45
  in "read_until_prompt"; smaller number here will generally be faster, though may be less
33
46
  reliable; default value is 1000
47
+ comms_roughly_match_inputs: indicates if the channel should "roughly" match inputs sent
48
+ to the device. If False (default) inputs are strictly checked, as in any input
49
+ *must* be read back exactly on the channel. When set to True all input chars *must*
50
+ be read back in order in the output and all chars must be present, but the *exact*
51
+ input string does not need to be seen. This can be useful if a device echoes back
52
+ extra characters or rewrites the terminal during command input.
34
53
  timeout_ops: timeout_ops to assign to the channel, see above
35
54
  channel_log: log "channel" output -- this would be the output you would normally see on a
36
55
  terminal. If `True` logs to `scrapli_channel.log`, if a string is provided, logs to
@@ -53,6 +72,7 @@ class BaseChannelArgs:
53
72
  comms_prompt_pattern: str = r"^[a-z0-9.\-@()/:]{1,32}[#>$]$"
54
73
  comms_return_char: str = "\n"
55
74
  comms_prompt_search_depth: int = 1000
75
+ comms_roughly_match_inputs: bool = False
56
76
  timeout_ops: float = 30.0
57
77
  channel_log: Union[str, bool, BytesIO] = False
58
78
  channel_log_mode: str = "write"
@@ -348,8 +368,10 @@ class BaseChannel:
348
368
  N/A
349
369
 
350
370
  """
351
- log_output = "REDACTED" if redacted else repr(channel_input)
352
- self.logger.debug(f"write: {log_output}")
371
+ if redacted:
372
+ self.logger.debug("write: REDACTED")
373
+ else:
374
+ self.logger.debug("write: %r", channel_input)
353
375
 
354
376
  self.transport.write(channel_input=channel_input.encode())
355
377
 
@@ -1,4 +1,5 @@
1
1
  """scrapli.channel.sync_channel"""
2
+
2
3
  import re
3
4
  import time
4
5
  from contextlib import contextmanager, suppress
@@ -10,6 +11,7 @@ from typing import Iterator, List, Optional, Tuple
10
11
  from scrapli.channel.base_channel import BaseChannel, BaseChannelArgs
11
12
  from scrapli.decorators import timeout_wrapper
12
13
  from scrapli.exceptions import ScrapliAuthenticationFailed, ScrapliConnectionError, ScrapliTimeout
14
+ from scrapli.helper import output_roughly_contains_input
13
15
  from scrapli.transport.base import Transport
14
16
 
15
17
 
@@ -69,7 +71,7 @@ class Channel(BaseChannel):
69
71
  buf = self.transport.read()
70
72
  buf = buf.replace(b"\r", b"")
71
73
 
72
- self.logger.debug(f"read: {buf!r}")
74
+ self.logger.debug("read: %r", buf)
73
75
 
74
76
  if self.channel_log:
75
77
  self.channel_log.write(buf)
@@ -104,9 +106,16 @@ class Channel(BaseChannel):
104
106
  while True:
105
107
  buf += self.read()
106
108
 
107
- # replace any backspace chars (particular problem w/ junos), and remove any added spaces
108
- # this is just for comparison of the inputs to what was read from channel
109
- if processed_channel_input in b"".join(buf.lower().replace(b"\x08", b"").split()):
109
+ if not self._base_channel_args.comms_roughly_match_inputs:
110
+ # replace any backspace chars (particular problem w/ junos), and remove any added
111
+ # spaces this is just for comparison of the inputs to what was read from channel
112
+ # note (2024) this would be worked around by using the roughly contains search,
113
+ # *but* that is slower (probably immaterially for most people but... ya know...)
114
+ processed_buf = b"".join(buf.lower().replace(b"\x08", b"").split())
115
+
116
+ if processed_channel_input in processed_buf:
117
+ return buf
118
+ elif output_roughly_contains_input(input_=processed_channel_input, output=buf):
110
119
  return buf
111
120
 
112
121
  def _read_until_prompt(self, buf: bytes = b"") -> bytes:
@@ -238,7 +247,7 @@ class Channel(BaseChannel):
238
247
 
239
248
  if (time.time() - start) > read_duration:
240
249
  break
241
- if any((channel_output in search_buf for channel_output in channel_outputs)):
250
+ if any(channel_output in search_buf for channel_output in channel_outputs):
242
251
  break
243
252
  if re.search(pattern=regex_channel_outputs_pattern, string=search_buf):
244
253
  break
@@ -456,6 +465,7 @@ class Channel(BaseChannel):
456
465
  *,
457
466
  strip_prompt: bool = True,
458
467
  eager: bool = False,
468
+ eager_input: bool = False,
459
469
  ) -> Tuple[bytes, bytes]:
460
470
  """
461
471
  Primary entry point to send data to devices in shell mode; accept input and returns result
@@ -466,6 +476,8 @@ class Channel(BaseChannel):
466
476
  eager: eager mode reads and returns the `_read_until_input` value, but does not attempt
467
477
  to read to the prompt pattern -- this should not be used manually! (only used by
468
478
  `send_configs` with the eager flag set)
479
+ eager_input: when true does *not* try to read our input off the channel -- generally
480
+ this should be left alone unless you know what you are doing!
469
481
 
470
482
  Returns:
471
483
  Tuple[bytes, bytes]: tuple of "raw" output and "processed" (cleaned up/stripped) output
@@ -480,12 +492,18 @@ class Channel(BaseChannel):
480
492
  bytes_channel_input = channel_input.encode()
481
493
 
482
494
  self.logger.info(
483
- f"sending channel input: {channel_input}; strip_prompt: {strip_prompt}; eager: {eager}"
495
+ "sending channel input: %s; strip_prompt: %s; eager: %s",
496
+ channel_input,
497
+ strip_prompt,
498
+ eager,
484
499
  )
485
500
 
486
501
  with self._channel_lock():
487
502
  self.write(channel_input=channel_input)
488
- _buf_until_input = self._read_until_input(channel_input=bytes_channel_input)
503
+
504
+ if not eager_input:
505
+ _buf_until_input = self._read_until_input(channel_input=bytes_channel_input)
506
+
489
507
  self.send_return()
490
508
 
491
509
  if not eager:
@@ -532,8 +550,12 @@ class Channel(BaseChannel):
532
550
  ]
533
551
 
534
552
  self.logger.info(
535
- f"sending channel input and read: {channel_input}; strip_prompt: {strip_prompt}; "
536
- f"expected_outputs: {expected_outputs}; read_duration: {read_duration}"
553
+ "sending channel input and read: %s; strip_prompt: %s; "
554
+ "expected_outputs: %s; read_duration: %s",
555
+ channel_input,
556
+ strip_prompt,
557
+ expected_outputs,
558
+ read_duration,
537
559
  )
538
560
 
539
561
  with self._channel_lock():
@@ -643,9 +665,10 @@ class Channel(BaseChannel):
643
665
 
644
666
  _channel_input = channel_input if not hidden_input else "REDACTED"
645
667
  self.logger.info(
646
- f"sending interactive input: {_channel_input}; "
647
- f"expecting: {channel_response}; "
648
- f"hidden_input: {hidden_input}"
668
+ "sending interactive input: %s; expecting: %s; hidden_input: %s",
669
+ _channel_input,
670
+ channel_response,
671
+ hidden_input,
649
672
  )
650
673
 
651
674
  self.write(channel_input=channel_input, redacted=bool(hidden_input))
@@ -1,4 +1,5 @@
1
1
  """scrapli.decorators"""
2
+
2
3
  import asyncio
3
4
  import signal
4
5
  import sys
@@ -1,4 +1,5 @@
1
1
  """scrapli.driver"""
2
+
2
3
  from scrapli.driver.base import AsyncDriver, Driver
3
4
  from scrapli.driver.generic import AsyncGenericDriver, GenericDriver
4
5
  from scrapli.driver.network import AsyncNetworkDriver, NetworkDriver
@@ -1,4 +1,5 @@
1
1
  """scrapli.driver.base"""
2
+
2
3
  from scrapli.driver.base.async_driver import AsyncDriver
3
4
  from scrapli.driver.base.sync_driver import Driver
4
5
 
@@ -1,11 +1,12 @@
1
1
  """scrapli.driver.base.async_driver"""
2
+
2
3
  from types import TracebackType
3
4
  from typing import Any, Optional, Type, TypeVar
4
5
 
5
6
  from scrapli.channel import AsyncChannel
6
7
  from scrapli.driver.base.base_driver import BaseDriver
7
- from scrapli.exceptions import ScrapliValueError
8
- from scrapli.transport import ASYNCIO_TRANSPORTS
8
+ from scrapli.exceptions import ScrapliConnectionError, ScrapliValueError
9
+ from scrapli.transport import ASYNCIO_TRANSPORTS, CORE_TRANSPORTS
9
10
 
10
11
  _T = TypeVar("_T", bound="AsyncDriver")
11
12
 
@@ -14,7 +15,7 @@ class AsyncDriver(BaseDriver):
14
15
  def __init__(self, **kwargs: Any):
15
16
  super().__init__(**kwargs)
16
17
 
17
- if self.transport_name not in ASYNCIO_TRANSPORTS:
18
+ if self.transport_name in CORE_TRANSPORTS and self.transport_name not in ASYNCIO_TRANSPORTS:
18
19
  raise ScrapliValueError(
19
20
  "provided transport is *not* an asyncio transport, must use an async transport with"
20
21
  " the AsyncDriver(s)"
@@ -39,10 +40,22 @@ class AsyncDriver(BaseDriver):
39
40
  _T: a concrete implementation of the opened AsyncDriver object
40
41
 
41
42
  Raises:
42
- N/A
43
+ ScrapliConnectionError: if an exception occurs during opening
43
44
 
44
45
  """
45
- await self.open()
46
+ try:
47
+ await self.open()
48
+ except Exception as exc:
49
+ self.logger.critical(
50
+ "encountered exception during open in context manager,"
51
+ " attempting to close transport and channel"
52
+ )
53
+
54
+ self.transport.close()
55
+ self.channel.close()
56
+
57
+ raise ScrapliConnectionError(exc) from exc
58
+
46
59
  return self
47
60
 
48
61
  async def __aexit__(
@@ -87,14 +100,7 @@ class AsyncDriver(BaseDriver):
87
100
  await self.transport.open()
88
101
  self.channel.open()
89
102
 
90
- if (
91
- self.transport_name
92
- in (
93
- "telnet",
94
- "asynctelnet",
95
- )
96
- and not self.auth_bypass
97
- ):
103
+ if "telnet" in self.transport_name and not self.auth_bypass:
98
104
  await self.channel.channel_authenticate_telnet(
99
105
  auth_username=self.auth_username, auth_password=self.auth_password
100
106
  )