sshscript 4.0.1__py3-none-any.whl → 5.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.
sshscript/_version.py CHANGED
@@ -1,3 +1,3 @@
1
1
  """Single source of the SSHScript package version."""
2
2
 
3
- __version__ = "4.0.1"
3
+ __version__ = "5.0.0"
sshscript/session.py CHANGED
@@ -23,6 +23,7 @@ else:
23
23
  import patching
24
24
 
25
25
  import ast
26
+ import base64
26
27
  import contextvars
27
28
  import threading
28
29
  import paramiko
@@ -402,6 +403,9 @@ class Session(object):
402
403
  # A child shares its tree lock; independent roots do not block each other.
403
404
  self._lifecycle_lock = parent._lifecycle_lock if parent is not None else threading.RLock()
404
405
  self._settings = parent.get() if parent is not None else root_defaults()
406
+ if parent is not None:
407
+ # A path belongs to this host, not to the host of the parent.
408
+ self._settings['known_hosts_path'] = None
405
409
  self.logger = SessionLogger(self._settings)
406
410
  ## when exec_command() or withdollar() was called,
407
411
  ## this value was stored, so user can access its stdout, stderr and exitcode
@@ -458,6 +462,8 @@ class Session(object):
458
462
  verbose_stderr = property(lambda self: self.get('verbose_stderr'), lambda self, v: self.set(verbose_stderr=v))
459
463
  log_level = property(lambda self: self.get('log_level'), lambda self, v: self.set(log_level=v))
460
464
  policy = property(lambda self: self.get('policy'), lambda self, v: self.set(policy=v))
465
+ known_hosts = property(lambda self: self.get('known_hosts'), lambda self, v: self.set(known_hosts=v))
466
+ known_hosts_path = property(lambda self: self.get('known_hosts_path'), lambda self, v: self.set(known_hosts_path=v))
461
467
 
462
468
  ## added in v2.0.3
463
469
  ## always return the session which is not connected to execute commands by subprocess at localhost
@@ -677,8 +683,88 @@ class Session(object):
677
683
  self.logger.debug('Script requested exit (exit_code=%s)', code)
678
684
  raise SSHScriptExit(message,code)
679
685
 
686
+ def _read_known_hosts(self):
687
+ """Read a host-owned trust file strictly; never execute a remote shell."""
688
+ self._ensure_open()
689
+ path = self.known_hosts_path or '~/.ssh/known_hosts'
690
+ source = self.host if self._client is not None else 'localhost'
691
+ try:
692
+ if self._client is not None:
693
+ sftp = self.sftp
694
+ if path == '~' or path.startswith('~/'):
695
+ path = posixpath.join(sftp.normalize('.'), path[2:] if path != '~' else '')
696
+ elif path.startswith('~'):
697
+ raise ValueError('remote ~user paths are not supported')
698
+ with sftp.open(path, 'rb') as stream:
699
+ data = stream.read()
700
+ else:
701
+ path = os.path.expanduser(path)
702
+ with open(path, 'rb') as stream:
703
+ data = stream.read()
704
+ text = data.decode('utf-8') if isinstance(data, bytes) else data
705
+ except Exception as exc:
706
+ raise SSHScriptException(
707
+ f'Unable to read known_hosts on {source}: {path} ({type(exc).__name__})'
708
+ ) from exc
709
+
710
+ keys = paramiko.HostKeys()
711
+ for lineno, line in enumerate(text.splitlines(), 1):
712
+ line = line.strip()
713
+ if not line or line.startswith('#'):
714
+ continue
715
+ try:
716
+ fields = line.split()
717
+ if len(fields) < 3 or fields[0].startswith('@'):
718
+ raise ValueError('expected a plain host-key entry')
719
+ # Paramiko matches literal names and hashed names, not patterns.
720
+ if any(c in fields[0] for c in '*?!'):
721
+ raise ValueError('host patterns are not supported')
722
+ base64.b64decode(fields[2], validate=True)
723
+ entry = paramiko.hostkeys.HostKeyEntry.from_line(' '.join(fields[:3]), lineno)
724
+ if entry is None or entry.key is None or entry.key.get_name() != fields[1]:
725
+ raise ValueError('unsupported host-key entry')
726
+ for name in entry.hostnames:
727
+ if not name:
728
+ raise ValueError('empty hostname')
729
+ if name.startswith('|'):
730
+ # Validate without relying on Paramiko's assertions (-O).
731
+ parts = name.split('|')
732
+ if (len(parts) != 4 or parts[:2] != ['', '1'] or
733
+ any(len(base64.b64decode(part, validate=True)) != 20
734
+ for part in parts[2:])):
735
+ raise ValueError('invalid hashed hostname')
736
+ keys.add(name, entry.key.get_name(), entry.key)
737
+ except Exception:
738
+ # Do not include raw key lines in error reports.
739
+ raise SSHScriptException(
740
+ f'Invalid known_hosts on {source}: {path}, line {lineno}'
741
+ ) from None
742
+ return keys
743
+
744
+ def _load_connection_host_keys(self, client, host, port, strategy):
745
+ if strategy == 'local':
746
+ sources = [self.local_session]
747
+ elif strategy == 'parent':
748
+ sources = [self]
749
+ else:
750
+ sources = []
751
+ source = self
752
+ while source is not None:
753
+ sources.append(source)
754
+ source = source.parent
755
+ identity = host if port == 22 else f'[{host}]:{port}'
756
+ for source in sources:
757
+ keys = source._read_known_hosts()
758
+ found = keys.lookup(identity)
759
+ if found is not None:
760
+ # Only this layer is installed. Paramiko negotiates its key types
761
+ # and rejects a mismatch without consulting more distant layers.
762
+ for key_type, key in found.items():
763
+ client.get_host_keys().add(identity, key_type, key)
764
+ return
765
+
680
766
  @export2Dollar
681
- def connect(self,host,username=None,password=None,port=None,policy=UNSET,*,ssh_config=None,**kw):
767
+ def connect(self,host,username=None,password=None,port=None,policy=UNSET,*,ssh_config=None,known_hosts=UNSET,**kw):
682
768
  """Return a connected child session, optionally through this session's SSH link.
683
769
 
684
770
  Use with session.connect("user@host") as remote to scope the connection.
@@ -701,6 +787,15 @@ class Session(object):
701
787
  parent; its connect/banner/auth timeouts default to 30 seconds and may be
702
788
  overridden. KEEPALIVE_INTERVAL sets keepalive seconds (default 60; 0 disables).
703
789
  Connection failures propagate.
790
+
791
+ known_hosts selects local, parent (default: this Session's host), or
792
+ chain (this host, then ancestors through localhost). Children inherit
793
+ the setting; a connect override affects only that connection. Each
794
+ source uses its own known_hosts_path or ~/.ssh/known_hosts. Paths are
795
+ not inherited. The nearest source containing the target is authoritative:
796
+ a mismatch never falls back. Read/parse failures propagate in all modes.
797
+ Unknown keys use policy only after all selected sources lack the target.
798
+ These sources are read-only, including with AutoAddPolicy.
704
799
  """
705
800
 
706
801
  if self.closed:
@@ -708,6 +803,9 @@ class Session(object):
708
803
 
709
804
  if policy is UNSET:
710
805
  policy = self.policy
806
+ if known_hosts is UNSET:
807
+ known_hosts = self.known_hosts
808
+ validate({'known_hosts': known_hosts})
711
809
 
712
810
  ## host might be in format of "username@hostname"
713
811
  if isinstance(host, str) and '@' in host:
@@ -742,7 +840,7 @@ class Session(object):
742
840
  def connect_client(host,username,password,port,policy,**kw):
743
841
  client = paramiko.SSHClient()
744
842
  try:
745
- client.load_system_host_keys()
843
+ self._load_connection_host_keys(client, host, port, known_hosts)
746
844
  if policy is not None:
747
845
  client.set_missing_host_key_policy(policy)
748
846
  client.connect(host,username=username,password=password,port=port,**kw)
sshscript/session.pyi CHANGED
@@ -8,12 +8,15 @@ from .sessionwrapper import SessionWrapper
8
8
 
9
9
  Command = str | list[str] | tuple[str, ...]
10
10
  HostKeyPolicy = MissingHostKeyPolicy | type[MissingHostKeyPolicy] | None
11
+ KnownHosts = Literal['local', 'parent', 'chain']
11
12
  class SessionSettings(TypedDict):
12
13
  check: bool
13
14
  verbose: bool
14
15
  verbose_stderr: bool
15
16
  log_level: int
16
17
  policy: HostKeyPolicy
18
+ known_hosts: KnownHosts
19
+ known_hosts_path: str | None
17
20
 
18
21
  class Session:
19
22
  closed: bool
@@ -24,12 +27,17 @@ class Session:
24
27
  verbose: bool
25
28
  verbose_stderr: bool
26
29
  policy: HostKeyPolicy
30
+ known_hosts: KnownHosts
31
+ @property
32
+ def known_hosts_path(self) -> str | None: ...
33
+ @known_hosts_path.setter
34
+ def known_hosts_path(self, value: str | PathLike[str] | None) -> None: ...
27
35
  @property
28
36
  def log_level(self) -> int: ...
29
37
  @log_level.setter
30
38
  def log_level(self, value: int | str) -> None: ...
31
39
  def __init__(self, parent: Session | None = ...) -> None: ...
32
- def set(self, *, check: bool = ..., verbose: bool = ..., verbose_stderr: bool = ..., log_level: int | str = ..., policy: HostKeyPolicy = ...) -> None: ...
40
+ def set(self, *, check: bool = ..., verbose: bool = ..., verbose_stderr: bool = ..., log_level: int | str = ..., policy: HostKeyPolicy = ..., known_hosts: KnownHosts = ..., known_hosts_path: str | PathLike[str] | None = ...) -> None: ...
33
41
  @overload
34
42
  def get(self, name: None = ...) -> SessionSettings: ...
35
43
  @overload
@@ -39,7 +47,11 @@ class Session:
39
47
  @overload
40
48
  def get(self, name: Literal['policy']) -> HostKeyPolicy: ...
41
49
  @overload
42
- def get(self, name: str) -> bool | int | HostKeyPolicy: ...
50
+ def get(self, name: Literal['known_hosts']) -> KnownHosts: ...
51
+ @overload
52
+ def get(self, name: Literal['known_hosts_path']) -> str | None: ...
53
+ @overload
54
+ def get(self, name: str) -> bool | int | str | HostKeyPolicy: ...
43
55
  @property
44
56
  def host(self) -> str | None: ...
45
57
  @property
@@ -57,7 +69,7 @@ class Session:
57
69
  __call__ = exec_command
58
70
  send_line = exec_command
59
71
  def start(self, cmd: Command, *, shell: bool | str | None = ..., shell_executable: str | None = ..., timeout: float | None = ..., stop_timeout: float = ..., capture_limit: int = ..., check: bool = ..., input: str | bytes | None = ..., env: dict[str, str] | None = ..., get_pty: bool = ...) -> CommandJob: ...
60
- def connect(self, host: str, username: str | None = ..., password: str | None = ..., port: int | None = ..., policy: Any = ..., *, ssh_config: str | PathLike[str] | bool | None = ..., **kw: Any) -> Session: ...
72
+ def connect(self, host: str, username: str | None = ..., password: str | None = ..., port: int | None = ..., policy: Any = ..., *, ssh_config: str | PathLike[str] | bool | None = ..., known_hosts: KnownHosts = ..., **kw: Any) -> Session: ...
61
73
  @staticmethod
62
74
  def resolve_connection(host: str, **kwargs: Any) -> dict[str, Any]: ...
63
75
  def shell(self, command: str | None = ..., funcname: str = ..., get_pty: bool = ..., *args: Any, **kw: Any) -> ContextManager[SessionWrapper]: ...
@@ -7,7 +7,8 @@ from paramiko import MissingHostKeyPolicy
7
7
  from types import MappingProxyType
8
8
 
9
9
  DEFAULTS = MappingProxyType(dict(check=False, verbose=False, verbose_stderr=False,
10
- log_level=logging.INFO, policy=None))
10
+ log_level=logging.INFO, policy=None,
11
+ known_hosts='parent', known_hosts_path=None))
11
12
  UNSET = object()
12
13
  _defaults = ContextVar('sshscript_execution_defaults', default={})
13
14
 
@@ -23,6 +24,21 @@ def validate(values):
23
24
  (isinstance(value, type) and issubclass(value, MissingHostKeyPolicy))
24
25
  ):
25
26
  raise TypeError('policy must be a MissingHostKeyPolicy instance, subclass, or None')
27
+ elif key == 'known_hosts':
28
+ if not isinstance(value, str):
29
+ raise TypeError('known_hosts must be local, parent, or chain')
30
+ if value not in ('local', 'parent', 'chain'):
31
+ raise ValueError('known_hosts must be local, parent, or chain')
32
+ elif key == 'known_hosts_path':
33
+ if value is not None:
34
+ if not isinstance(value, (str, os.PathLike)):
35
+ raise TypeError('known_hosts_path must be a text path or None')
36
+ value = os.fspath(value)
37
+ if not isinstance(value, str):
38
+ raise TypeError('known_hosts_path must be a text path or None')
39
+ if not value or '\x00' in value:
40
+ raise ValueError('known_hosts_path must be nonempty and contain no NUL')
41
+ result[key] = value
26
42
  elif key == 'log_level':
27
43
  if isinstance(value, str):
28
44
  value = logging.getLevelName(value.upper())
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sshscript
3
- Version: 4.0.1
3
+ Version: 5.0.0
4
4
  Summary: Python automation for local processes, SSH, and SSHScript .spy files
5
5
  Author-email: "Yeh, Hsin-Yuan" <iapyeh@gmail.com>
6
6
  License-Expression: MIT
@@ -37,9 +37,12 @@ Dynamic: license-file
37
37
  [![CodeQL](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml/badge.svg?branch=release)](https://github.com/iapyeh/sshscript/actions/workflows/codeql.yml)
38
38
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/iapyeh/sshscript/blob/release/LICENSE.txt)
39
39
 
40
+ **AI agent ready automation for local and remote commands.**
41
+
40
42
  SSHScript is a Python automation library for running commands locally and over
41
- SSH through one `Session` API. It also provides optional dollar syntax for
42
- compact `.spy` automation files.
43
+ SSH through one `Session` API. It provides an agent guide, a portable skill
44
+ bundle, and copyable prompts to help your AI agent use its execution contract.
45
+ Optional dollar syntax supports compact `.spy` automation files.
43
46
 
44
47
  SSHScript aims to help engineers and AI agents execute, understand, and hand
45
48
  off automation reliably: identify the execution host, retain each command's
@@ -47,7 +50,7 @@ output and status, distinguish failure from timeout, and make cleanup and
47
50
  unknown termination explicit. A successful command exit is evidence about
48
51
  that command; verify the deployment or other application outcome separately.
49
52
 
50
- **Current release:** [4.0.1](https://github.com/iapyeh/sshscript/releases/tag/v4.0.1)
53
+ **Current release:** [5.0.0](https://github.com/iapyeh/sshscript/releases/tag/v5.0.0)
51
54
  (Production/Stable) · **Python:** 3.11 or newer · **Tested:** Python
52
55
  3.11–3.14 on Linux and macOS
53
56
 
@@ -57,10 +60,45 @@ that command; verify the deployment or other application outcome separately.
57
60
  [Security](https://github.com/iapyeh/sshscript/security/policy) ·
58
61
  [Support](https://github.com/iapyeh/sshscript/blob/release/SUPPORT.md)
59
62
 
60
- **Version 4.0.1** introduces the incompatible 4.0 execution contract.
63
+ **Version 5.0.0** preserves the 4.0 command execution contract and changes the
64
+ default host-key source to the calling host. Provision its known_hosts file or
65
+ select `known_hosts="local"` when migrating existing nested connections.
61
66
  Upgrade 3.1.5 automation using the migration guide and application regressions.
62
67
  See [version policy and migration](VERSIONING.md) before upgrading automation.
63
68
 
69
+ ## AI agent ready
70
+
71
+ **Give your agent one URL:**
72
+ [SSHScript for AI Agents](https://iapyeh.github.io/sshscript/v3a/ai-agents/).
73
+ Prepare it for future automation or give it a task to complete now.
74
+
75
+ - **A guide for agents:** version-aware API selection, exact input handling,
76
+ retained command results, timeouts, authentication and cleanup.
77
+ - **A portable skill bundle:** `SKILL.md` with the agent guide and API and
78
+ version-policy references, ready to install through your agent's skill
79
+ mechanism.
80
+ - **Prompts for humans:** copy a setup prompt to request skill installation and
81
+ a saved tool preference, or a task prompt to describe hosts, inputs and
82
+ expected results.
83
+
84
+ Start with this prompt, even before you have a task:
85
+
86
+ ```text
87
+ Read https://iapyeh.github.io/sshscript/v3a/ai-agents/ and the Agent Guide.
88
+ If you support skills, download and install the complete skill bundle. If you
89
+ support persistent preferences, save this preference: for future local or
90
+ remote command execution and automation, first assess whether SSHScript is
91
+ suitable and use it when appropriate. Report whether the skill can be loaded
92
+ and the preference was saved, and explain any limits.
93
+ For now, do not connect to remote hosts or run demonstration commands.
94
+ ```
95
+
96
+ The guides are ordinary Markdown and web pages for agents that can read files
97
+ or URLs. Skill installation and persistent preferences depend on the agent's
98
+ capabilities. See the [repository AI entry](AI_GUIDE.md),
99
+ [agent guide](skills/sshscript/references/agent-guide.md) and
100
+ [skill instructions](skills/sshscript/SKILL.md) for the source documents.
101
+
64
102
  ## Recommended API
65
103
 
66
104
  Start with [the canonical API guide](API_GUIDE.md): version-labelled examples,
@@ -82,6 +120,7 @@ and [the cross-system test guide](src/sshscript/unittest/README.console-authenti
82
120
 
83
121
  ## Why SSHScript?
84
122
 
123
+ - Prepare your AI agent with a dedicated guide, portable skill and setup prompts.
85
124
  - Use the same interface for local subprocesses and remote SSH commands.
86
125
  - Traverse nested SSH connections without rebuilding connection logic.
87
126
  - Keep ordinary Python functions, packages, exceptions, data processing, and
@@ -114,7 +153,7 @@ pin SSHScript and all transitive dependencies in your application's lock file.
114
153
  To install this release explicitly:
115
154
 
116
155
  ```sh
117
- python3 -m pip install "sshscript==3.1.5"
156
+ python3 -m pip install "sshscript==5.0.0"
118
157
  ```
119
158
 
120
159
  Use `python3 -m pip install --upgrade sshscript` to upgrade. The optional
@@ -123,7 +162,8 @@ it never installs an update by itself.
123
162
 
124
163
  ## 60-second local quickstart
125
164
 
126
- This example uses the published 3.1.5 command API.
165
+ This example runs on 5.0.0 and retains compatibility with the 3.1.5 command API.
166
+ The metadata below records that older compatibility baseline.
127
167
 
128
168
  <!-- example: {"id":"readme-quickstart", "profile":"3.1.5", "stdout":"sshscript is ready\nexit code: 0\n"} -->
129
169
  ```python
@@ -374,10 +414,28 @@ Unknown and changed host keys are rejected unless the caller explicitly
374
414
  supplies a different Paramiko policy. Do not use automatic key acceptance in a
375
415
  production workflow unless a separate trusted bootstrap process has already
376
416
  verified the key. See the
377
- [SSHScript v3 documentation](https://iapyeh.github.io/sshscript/v3a/) for
417
+ [SSHScript documentation](https://iapyeh.github.io/sshscript/v3a/) for
378
418
  nested connections, timeouts, file transfer, `sudo`, `su`, and interactive
379
419
  programs.
380
420
 
421
+ ## Host-key sources in 5.0
422
+
423
+ SSHScript 5.0.0 uses `known_hosts="parent"` as the default: a local
424
+ Session reads localhost's trust file; `host1.connect("host2")` reads host1's
425
+ trust file over SFTP. This matches the host ownership of `pkey_path`.
426
+ SSHScript 4.0.2 and earlier use localhost for all host-key checks.
427
+
428
+ Set `session.set(known_hosts="chain")` to search from the calling Session
429
+ through its ancestors to localhost. The nearest layer with a target record
430
+ wins; a mismatch or read/parse error stops the connection. Set
431
+ `session.set(known_hosts="local")` to preserve previous behavior.
432
+ Each Session can set its own `known_hosts_path`; this path is not inherited.
433
+ `connect(known_hosts=...)` overrides only one connection. `pkey_path` remains
434
+ on the calling host for every strategy. `policy` handles only unknown keys;
435
+ `AutoAddPolicy` does not write these trust files. See the
436
+ [host-key source contract](API_GUIDE.md#host-key-sources-unreleased) for supported
437
+ file formats, strict error handling and migration.
438
+
381
439
  ## Reusing SSH configuration
382
440
 
383
441
  Connections from a local session read `~/.ssh/config` if it exists. Supported
@@ -2,7 +2,7 @@ _sshscript_cli.py,sha256=Z3BwvYFLKJAmy8Wi6xDUqHiIbXT498_uX4WEiguD9n0,576
2
2
  sshscript/__init__.py,sha256=bVqkgcifvePikqOTTMBC8bTSFz-UeFjSV-12seFONgw,3114
3
3
  sshscript/__init__.pyi,sha256=HOvKjFhwqm6OqTfBDkSGHyo2pGcmAB0cHOPz2jEow2c,810
4
4
  sshscript/_runtime.py,sha256=wJt8XSxK21Ntt-oSUhEF4lN8Nx10H1X8JyIG2ebaPSo,443
5
- sshscript/_version.py,sha256=FOd3i930sPEqWg4DRqF9NGf5NRS5l8kyFdhjy3nUsJA,77
5
+ sshscript/_version.py,sha256=ssRZ5iGxPpxYOxW6pHi-PtvxOGEaaBmJt3P5ZoMxybk,77
6
6
  sshscript/channelgeneric.py,sha256=J_tBZa4xrXLjDN3xjfkHukzynhkT0-bKkz-Ds3RKIaA,75822
7
7
  sshscript/channelssh.py,sha256=aVdI3KsUoSOujCDLpuI0I3m5YDWZCvIeB8a4J93P7r0,12038
8
8
  sshscript/channelsubprocess.py,sha256=mWPMxFMV6ZMx-VPWPNgtRvOAW9pjRZNtXddl0qY7UQk,12162
@@ -18,9 +18,9 @@ sshscript/dollarparser.py,sha256=7cl4QgffzfueeGOPN-NeoLP_3bKm04I9cVtCyag7JcE,963
18
18
  sshscript/errorutils.py,sha256=MgwYoPipEVCa9L33FmVoYXl77CD-Mrq958q_ft3cuvw,16386
19
19
  sshscript/patching.py,sha256=OM6vjGeP0iyBnzpai5BHn9qqkzJp9375npVb97D06LY,6581
20
20
  sshscript/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
21
- sshscript/session.py,sha256=APQbRmWFq1UJsgSPWFtuLBv2aw6j42q-LXiBzLYiick,73294
22
- sshscript/session.pyi,sha256=siuWub2nYV-vqZpOOBh0LrNzjvljL0u7vijWP850L1M,4260
23
- sshscript/sessionsettings.py,sha256=Uknk-qBboMaSOyA1s79vfHjOFxNU9J9FZSh6KJTXHVw,3670
21
+ sshscript/session.py,sha256=AvaqFhWbNRERE5qJfYp0uWdR8mQlzl2aT32FQSwqcIs,78406
22
+ sshscript/session.pyi,sha256=gK0INrScwjTk0WqKGX458Iw-toOkRikRtSvdTvX0_-s,4857
23
+ sshscript/sessionsettings.py,sha256=QJnjQtPpJBNUbIboICKcuNxZh3yN4vAuL_yw_5LL3d4,4612
24
24
  sshscript/sessionwrapper.py,sha256=r6BI6xBwrA-nqqa6YsXzaUUtxz5pv_483Xu6Ywlzs4I,19019
25
25
  sshscript/sessionwrapper.pyi,sha256=vzdJjl-vPB1bP57uXe78jGpjC4ag2t_Aqowg_4ggbVY,1585
26
26
  sshscript/spyimporter.py,sha256=ASAs2nPBU1mtpI7FquXYJctLFQR_PeGMnhjcw1G6LPg,5574
@@ -28,9 +28,9 @@ sshscript/sshconfig.py,sha256=aPfVOd-bx4k7gBuDeSb55Y812G4QNGeL3sbZPLPdiic,6295
28
28
  sshscript/sshscript.py,sha256=BXQG1yA-9M3l0JDbCwR3Lg5CCpn3qnJhQ1Xx6Q9-2eA,16940
29
29
  sshscript/stdio.py,sha256=Ql_7wX7oJ5vY74uOlCaotwoyqXrNteYdSl-aM4ersVM,16040
30
30
  sshscript/tokenparser.py,sha256=dGHY2s8Twjz0Q-r6nanmAL8VCQxZWqmM6g4KvTgIuzs,22396
31
- sshscript-4.0.1.dist-info/licenses/LICENSE.txt,sha256=FPqC-mLQDBkQ26LK_CAUGEUQQ7eyFTbftywlk2GMlls,1075
32
- sshscript-4.0.1.dist-info/METADATA,sha256=UQq-6x9apa6Z0g763e37qQwTyioFQibXrVPz9VyM7e4,26238
33
- sshscript-4.0.1.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
34
- sshscript-4.0.1.dist-info/entry_points.txt,sha256=JLkqym2UQso2R41RvFz9RAnCKFY8-nnrYgB4ER-zTGo,50
35
- sshscript-4.0.1.dist-info/top_level.txt,sha256=5d41PAJBhS-f-8aJKJegdhs8BIg_jpigSG83tCQhhfg,25
36
- sshscript-4.0.1.dist-info/RECORD,,
31
+ sshscript-5.0.0.dist-info/licenses/LICENSE.txt,sha256=FPqC-mLQDBkQ26LK_CAUGEUQQ7eyFTbftywlk2GMlls,1075
32
+ sshscript-5.0.0.dist-info/METADATA,sha256=WrYqMxN3lzhnL8whxN7gYOdVw7WKrrAoteeEue7tHBo,29353
33
+ sshscript-5.0.0.dist-info/WHEEL,sha256=YVMoNqKzERt-wjUZwJ33xBGAwnFl-4cqbYkTtWa4itE,91
34
+ sshscript-5.0.0.dist-info/entry_points.txt,sha256=JLkqym2UQso2R41RvFz9RAnCKFY8-nnrYgB4ER-zTGo,50
35
+ sshscript-5.0.0.dist-info/top_level.txt,sha256=5d41PAJBhS-f-8aJKJegdhs8BIg_jpigSG83tCQhhfg,25
36
+ sshscript-5.0.0.dist-info/RECORD,,