sftppath 0.1.0__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.
- sftppath-0.1.0/.gitignore +41 -0
- sftppath-0.1.0/CHANGELOG.md +104 -0
- sftppath-0.1.0/LICENSE +21 -0
- sftppath-0.1.0/PKG-INFO +190 -0
- sftppath-0.1.0/README.md +143 -0
- sftppath-0.1.0/docs/api/exceptions.md +10 -0
- sftppath-0.1.0/docs/api/sftp.md +17 -0
- sftppath-0.1.0/docs/api/uri.md +8 -0
- sftppath-0.1.0/docs/benchmarks.md +61 -0
- sftppath-0.1.0/docs/changelog.md +3 -0
- sftppath-0.1.0/docs/divergences.md +24 -0
- sftppath-0.1.0/docs/guides/paths.md +74 -0
- sftppath-0.1.0/docs/guides/uri.md +62 -0
- sftppath-0.1.0/docs/index.md +75 -0
- sftppath-0.1.0/examples/sftp_sync.py +60 -0
- sftppath-0.1.0/mkdocs.yml +58 -0
- sftppath-0.1.0/pyproject.toml +95 -0
- sftppath-0.1.0/src/sftppath/AGENTS.md +411 -0
- sftppath-0.1.0/src/sftppath/__init__.py +70 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/__init__.py +7 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/_attrs.py +99 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/_backend.py +212 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/_cache.py +130 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/_client.py +187 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/_connect.py +75 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/_file.py +113 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/_loop.py +164 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/_translate.py +98 -0
- sftppath-0.1.0/src/sftppath/_asyncssh/_walk.py +442 -0
- sftppath-0.1.0/src/sftppath/_backend.py +133 -0
- sftppath-0.1.0/src/sftppath/_checkfile.py +216 -0
- sftppath-0.1.0/src/sftppath/_errors.py +104 -0
- sftppath-0.1.0/src/sftppath/_exit.py +57 -0
- sftppath-0.1.0/src/sftppath/_namespace.py +35 -0
- sftppath-0.1.0/src/sftppath/_paramiko.py +564 -0
- sftppath-0.1.0/src/sftppath/_paramiko_config.py +65 -0
- sftppath-0.1.0/src/sftppath/_path.py +732 -0
- sftppath-0.1.0/src/sftppath/_rename.py +33 -0
- sftppath-0.1.0/src/sftppath/_select.py +128 -0
- sftppath-0.1.0/src/sftppath/_sshconfig.py +258 -0
- sftppath-0.1.0/src/sftppath/_version.py +11 -0
- sftppath-0.1.0/src/sftppath/exceptions.py +28 -0
- sftppath-0.1.0/src/sftppath/py.typed +0 -0
- sftppath-0.1.0/src/sftppath/uri.py +478 -0
- sftppath-0.1.0/tests/capabilities.py +52 -0
- sftppath-0.1.0/tests/child.py +45 -0
- sftppath-0.1.0/tests/conftest.py +279 -0
- sftppath-0.1.0/tests/hermetic.py +261 -0
- sftppath-0.1.0/tests/mutation/README.md +66 -0
- sftppath-0.1.0/tests/mutation/mutants.py +302 -0
- sftppath-0.1.0/tests/mutation/mutation_check.py +16 -0
- sftppath-0.1.0/tests/mutation/run.py +224 -0
- sftppath-0.1.0/tests/probes/probe_environment_leak.py +12 -0
- sftppath-0.1.0/tests/probes/probe_leaks_a_socket.py +12 -0
- sftppath-0.1.0/tests/probes/probe_session_environment.py +36 -0
- sftppath-0.1.0/tests/server_loops.py +43 -0
- sftppath-0.1.0/tests/sftp_loopback.py +310 -0
- sftppath-0.1.0/tests/sftp_support.py +80 -0
- sftppath-0.1.0/tests/test_contract.py +30 -0
- sftppath-0.1.0/tests/test_documentation.py +171 -0
- sftppath-0.1.0/tests/test_examples_skip.py +62 -0
- sftppath-0.1.0/tests/test_hermetic.py +329 -0
- sftppath-0.1.0/tests/test_isolation.py +138 -0
- sftppath-0.1.0/tests/test_packaging.py +93 -0
- sftppath-0.1.0/tests/test_public_surface.py +168 -0
- sftppath-0.1.0/tests/test_sftp.py +2173 -0
- sftppath-0.1.0/tests/test_sftp_asyncssh.py +1195 -0
- sftppath-0.1.0/tests/test_sftp_attributes.py +633 -0
- sftppath-0.1.0/tests/test_sftp_cost.py +271 -0
- sftppath-0.1.0/tests/test_sftp_errors.py +449 -0
- sftppath-0.1.0/tests/test_sftp_failed_write.py +54 -0
- sftppath-0.1.0/tests/test_sftp_native_walk.py +1165 -0
- sftppath-0.1.0/tests/test_sftp_paramiko_cache.py +81 -0
- sftppath-0.1.0/tests/test_sftp_replace.py +292 -0
- sftppath-0.1.0/tests/test_sftp_ssh_config.py +884 -0
- sftppath-0.1.0/tests/test_sftp_transport.py +956 -0
- sftppath-0.1.0/tests/test_sftp_utime.py +144 -0
- sftppath-0.1.0/tests/test_shipped_header.py +455 -0
- sftppath-0.1.0/tests/test_sync_sftp.py +147 -0
- sftppath-0.1.0/tests/test_sync_sftp_parity.py +656 -0
- sftppath-0.1.0/tests/test_uri_config_scope.py +200 -0
- sftppath-0.1.0/tests/test_uri_scheme.py +440 -0
- sftppath-0.1.0/tests/test_uri_sftp.py +755 -0
- sftppath-0.1.0/tests/test_version.py +24 -0
- sftppath-0.1.0/tests/waits.py +17 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
# Category rule: nothing dot-prefixed at the repo root gets tracked except the
|
|
2
|
+
# files re-included below (a category beats a list). Root-anchored on purpose;
|
|
3
|
+
# nested dotfiles are governed by the specific rules further down.
|
|
4
|
+
/.*
|
|
5
|
+
!/.gitignore
|
|
6
|
+
!/.gitattributes
|
|
7
|
+
!/.github/
|
|
8
|
+
|
|
9
|
+
# Private agent config. No trailing slash on `.agents` on purpose: it may be a
|
|
10
|
+
# symlink, which git treats as a file, so a directory-only `.agents/` would not
|
|
11
|
+
# match it. These stay even though /.* covers the root: they also catch nested
|
|
12
|
+
# copies. *.local.* is an unshared, machine- or user-specific override and is
|
|
13
|
+
# never committed (it is also excluded from the build in pyproject.toml).
|
|
14
|
+
.agents
|
|
15
|
+
*.local.*
|
|
16
|
+
CLAUDE*
|
|
17
|
+
.claude
|
|
18
|
+
|
|
19
|
+
# Build/dist output and generated docs site
|
|
20
|
+
build/
|
|
21
|
+
dist/
|
|
22
|
+
site/
|
|
23
|
+
|
|
24
|
+
# Python
|
|
25
|
+
__pycache__/
|
|
26
|
+
*.py[cod]
|
|
27
|
+
.pytest_cache/
|
|
28
|
+
.hypothesis/
|
|
29
|
+
.mypy_cache/
|
|
30
|
+
.ruff_cache/
|
|
31
|
+
.coverage*
|
|
32
|
+
htmlcov/
|
|
33
|
+
*.egg-info/
|
|
34
|
+
.venv/
|
|
35
|
+
.pyvenv/
|
|
36
|
+
|
|
37
|
+
# OS/editor
|
|
38
|
+
.DS_Store
|
|
39
|
+
Thumbs.db
|
|
40
|
+
.idea/
|
|
41
|
+
.vscode/
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/).
|
|
7
|
+
|
|
8
|
+
## [Unreleased]
|
|
9
|
+
|
|
10
|
+
## [0.1.0] - 2026-10-14
|
|
11
|
+
|
|
12
|
+
The first release of `sftppath`: the SFTP support of `pathlib-next` 0.9.14
|
|
13
|
+
(`pathlib_next.uri.schemes.sftp`) as a distribution of its own, with a path that
|
|
14
|
+
needs no URI. It needs Python 3.9 or newer and `pathapi>=0.1.0,<0.2`.
|
|
15
|
+
Neither SSH library is required to import it: the extras `paramiko`
|
|
16
|
+
(`paramiko>=5.0,<6`) and `asyncssh` (`asyncssh>=2.20.0`, below 3) add one, and
|
|
17
|
+
`uri` (`pathapi-uri>=0.1.0,<0.2`) adds the `sftp:` scheme. With neither SSH library a
|
|
18
|
+
path can still be built, joined and compared, and its first request raises an
|
|
19
|
+
`ImportError` that names both extras; naming `SFTPBackend` or `AsyncSSHBackend`
|
|
20
|
+
without its library raises one that names its extra. Under *Changed*, the
|
|
21
|
+
migration from `pathlib_next.uri.schemes.sftp` comes in this order: the table of
|
|
22
|
+
names, the table of extras, then every change of behaviour with what to do about
|
|
23
|
+
it.
|
|
24
|
+
|
|
25
|
+
It is built on `pathapi`, which was published as `pathlib-next` up to 0.10.0 (import
|
|
26
|
+
name `pathlib_next`); the old names below are the ones before the move.
|
|
27
|
+
|
|
28
|
+
### Added
|
|
29
|
+
|
|
30
|
+
- `SFTPPath`, a `pathapi.Path` for a directory tree on an SFTP server, built
|
|
31
|
+
from keywords: `SFTPPath("/srv/x", host="h", port=2222, user="u", password="p")`.
|
|
32
|
+
Nothing connects until the first request. `backend=` shares one connection
|
|
33
|
+
between paths built separately; a path joined from another keeps its backend.
|
|
34
|
+
- `SFTPBackend` (paramiko), `AsyncSSHBackend` (asyncssh), `BaseSFTPBackend` and
|
|
35
|
+
`CheckFileSFTPBackend`, each the connections to one server and account. Every
|
|
36
|
+
backend is a context manager, `close()` returns when the connections are
|
|
37
|
+
closed, and may be called twice. `SFTPEndpoint`, `DEFAULT_PORT` and
|
|
38
|
+
`DEFAULT_SSH_CONFIG` are exported too.
|
|
39
|
+
- `sftppath.exceptions`: `SFTPPathError`, a `pathapi.PathError`, with
|
|
40
|
+
`SFTPAuthenticationError` (also `PermissionError`) and `SFTPHostKeyError`
|
|
41
|
+
(also `ConnectionError`).
|
|
42
|
+
- `sftppath.uri`: `SFTPURIPath`, the `sftp:` scheme of `uripath`, found by
|
|
43
|
+
`uripath` through the entry point `sftp` of the group `uripath.schemes`, and
|
|
44
|
+
`SFTPURIBackend`, which builds the backend of each server a URI names. Importing
|
|
45
|
+
`sftppath.uri` without `uripath` raises an `ImportError` that names
|
|
46
|
+
`pip install "sftppath[uri]"`; `import sftppath` never imports `uripath`.
|
|
47
|
+
- `SFTPPath.is_local()`: true for a loopback address, `localhost` and a backend
|
|
48
|
+
with no host, so `PathSyncer(quick_check=True)` applies to a real server.
|
|
49
|
+
- `sftppath.__version__`, read from the installed metadata.
|
|
50
|
+
|
|
51
|
+
### Changed
|
|
52
|
+
|
|
53
|
+
- The names. `pathlib_next.uri.schemes.sftp` is gone with no alias.
|
|
54
|
+
|
|
55
|
+
| Was | Now |
|
|
56
|
+
| --- | --- |
|
|
57
|
+
| `SftpPath` (a `UriPath`) | `sftppath.SFTPPath` for a path built from keywords; `sftppath.uri.SFTPURIPath` for `URIPath("sftp://...")` |
|
|
58
|
+
| `SftpBackend` | `sftppath.SFTPBackend` |
|
|
59
|
+
| `AsyncsshSftpBackend` | `sftppath.AsyncSSHBackend` |
|
|
60
|
+
| `BaseSftpBackend` | `sftppath.BaseSFTPBackend` |
|
|
61
|
+
| `CheckFileSftpBackend` | `sftppath.CheckFileSFTPBackend` |
|
|
62
|
+
| `SftpAuthenticationError`, `SftpHostKeyError` | `sftppath.SFTPAuthenticationError`, `sftppath.SFTPHostKeyError` (in `sftppath.exceptions`) |
|
|
63
|
+
| `_DEFAULT_SSH_CONFIG` | `sftppath.DEFAULT_SSH_CONFIG` |
|
|
64
|
+
| `PATHLIB_NEXT_SFTP_BACKEND` | `SFTPPATH_BACKEND`; the old variable is not read |
|
|
65
|
+
| the logger `pathlib_next.sftp` | the logger `sftppath` |
|
|
66
|
+
| the entry-point group `pathlib_next.schemes`, entry `sftp` | the group `uripath.schemes`, entry `sftp = "sftppath.uri:SFTPURIPath"` |
|
|
67
|
+
|
|
68
|
+
- What to install where `pathlib-next` had an extra. `pathlib-next` 0.10 has no
|
|
69
|
+
`sftp` or `sftp-async` extra.
|
|
70
|
+
|
|
71
|
+
| Was | Install now |
|
|
72
|
+
| --- | --- |
|
|
73
|
+
| `pathlib-next[sftp]` | `sftppath[paramiko]` |
|
|
74
|
+
| `pathlib-next[sftp-async]` | `sftppath[asyncssh]` |
|
|
75
|
+
| either, for `UriPath("sftp://...")` | add `sftppath[uri]` (or install `uripath` beside it) |
|
|
76
|
+
|
|
77
|
+
- A backend is the connections to one server and account. Was: one backend
|
|
78
|
+
served every host a path named, and `client(source)`, `opts(source)` and
|
|
79
|
+
`transport(source)` took the `Source` of the URI; a backend was built with its
|
|
80
|
+
options only (`SftpBackend(connect_opts, hostkeypolicy, ...)`). Now the host,
|
|
81
|
+
port, user and password are arguments of the backend
|
|
82
|
+
(`SFTPBackend("h", 2222, user="u", password="p", connect_opts=...)`,
|
|
83
|
+
`AsyncSSHBackend("h", ...)`), and `client()`, `opts()` and `transport()` take
|
|
84
|
+
nothing. A `BaseSFTPBackend` subclass overrides `client(self)`. For URI paths,
|
|
85
|
+
give `backend=` an `SFTPURIBackend(SFTPBackend, connect_opts=...)` (the
|
|
86
|
+
options of the backend class; the user, password and port come from the URI)
|
|
87
|
+
or a `BaseSFTPBackend` for the one server. `BaseSftpBackend.default()` and the
|
|
88
|
+
`default(ssh_config=)` classmethods are removed.
|
|
89
|
+
- A relative or empty path is read from the server root. Was: `sftp://host` (no
|
|
90
|
+
path) listed the home directory of the account. Now it is `/`, and
|
|
91
|
+
`SFTPPath("x", host=h)` is `/x`. Name the directory you mean
|
|
92
|
+
(`/home/user`).
|
|
93
|
+
- `SFTPPath.readlink()` returns a path on the same backend, so dot segments of
|
|
94
|
+
the target collapse as they do for `pathlib`. The `SFTPURIPath` keeps the text
|
|
95
|
+
verbatim in `.path`.
|
|
96
|
+
- `SFTPPath.iterdir()` yields children without the attributes the listing
|
|
97
|
+
carried, so `is_dir()` on each costs a request; `walk()`, `glob()` and
|
|
98
|
+
`rm(recursive=True)` still use the listing. The children an `SFTPURIPath`
|
|
99
|
+
yields carry them, as before.
|
|
100
|
+
- An error that has a file name and no second one prints only the first; it
|
|
101
|
+
printed `-> None` after it.
|
|
102
|
+
|
|
103
|
+
[Unreleased]: https://github.com/jose-pr/sftppath/compare/v0.1.0...HEAD
|
|
104
|
+
[0.1.0]: https://github.com/jose-pr/sftppath/releases/tag/v0.1.0
|
sftppath-0.1.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2024 Jose A
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
sftppath-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: sftppath
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pathlib-style paths for SFTP servers, on paramiko or asyncssh, and the sftp: scheme for uripath
|
|
5
|
+
Project-URL: Homepage, https://github.com/jose-pr/sftppath/
|
|
6
|
+
Project-URL: Documentation, https://jose-pr.github.io/sftppath/
|
|
7
|
+
Project-URL: Issues, https://github.com/jose-pr/sftppath/issues
|
|
8
|
+
Author: Jose A
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Classifier: Development Status :: 4 - Beta
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
20
|
+
Classifier: Typing :: Typed
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Requires-Dist: pathapi<0.2,>=0.1.0
|
|
23
|
+
Provides-Extra: all
|
|
24
|
+
Requires-Dist: asyncssh<2.22,>=2.20.0; (python_version < '3.10') and extra == 'all'
|
|
25
|
+
Requires-Dist: asyncssh<3,>=2.20.0; (python_version >= '3.10') and extra == 'all'
|
|
26
|
+
Requires-Dist: paramiko<6,>=5.0; extra == 'all'
|
|
27
|
+
Requires-Dist: pathapi-uri<0.2,>=0.1.0; extra == 'all'
|
|
28
|
+
Provides-Extra: asyncssh
|
|
29
|
+
Requires-Dist: asyncssh<2.22,>=2.20.0; (python_version < '3.10') and extra == 'asyncssh'
|
|
30
|
+
Requires-Dist: asyncssh<3,>=2.20.0; (python_version >= '3.10') and extra == 'asyncssh'
|
|
31
|
+
Provides-Extra: dev
|
|
32
|
+
Requires-Dist: black; extra == 'dev'
|
|
33
|
+
Requires-Dist: build; extra == 'dev'
|
|
34
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
37
|
+
Requires-Dist: twine; extra == 'dev'
|
|
38
|
+
Provides-Extra: docs
|
|
39
|
+
Requires-Dist: mkdocs; extra == 'docs'
|
|
40
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
41
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
42
|
+
Provides-Extra: paramiko
|
|
43
|
+
Requires-Dist: paramiko<6,>=5.0; extra == 'paramiko'
|
|
44
|
+
Provides-Extra: uri
|
|
45
|
+
Requires-Dist: pathapi-uri<0.2,>=0.1.0; extra == 'uri'
|
|
46
|
+
Description-Content-Type: text/markdown
|
|
47
|
+
|
|
48
|
+
# sftppath
|
|
49
|
+
|
|
50
|
+
[](https://pypi.org/project/sftppath/)
|
|
51
|
+
[](https://pypi.org/project/sftppath/)
|
|
52
|
+
[](https://github.com/jose-pr/sftppath/blob/main/LICENSE)
|
|
53
|
+
[](https://jose-pr.github.io/sftppath/)
|
|
54
|
+
[](https://github.com/jose-pr/sftppath/actions/workflows/test.yml)
|
|
55
|
+
|
|
56
|
+
**`pathlib` for SFTP servers.** `SFTPPath("/srv/data/report.txt", host="files.example.org")`
|
|
57
|
+
has the method names, signatures and exception types of `pathlib.Path`, over
|
|
58
|
+
paramiko or asyncssh, and needs no URI. It builds on
|
|
59
|
+
[`pathapi`](https://github.com/jose-pr/pathapi) alone; with
|
|
60
|
+
[`uripath`](https://github.com/jose-pr/uripath) installed it also provides the
|
|
61
|
+
`sftp:` scheme. It is documented at <https://jose-pr.github.io/sftppath/>.
|
|
62
|
+
|
|
63
|
+
## Features
|
|
64
|
+
|
|
65
|
+
- **A `pathlib` path on a server**: `open`, `read_text`, `iterdir`, `glob`,
|
|
66
|
+
`stat`, `mkdir`, `rename`, `replace`, `chmod`, `symlink_to`, `readlink` and
|
|
67
|
+
the rest, plus `walk`, `copy`, `move`, `rm` and `PathSyncer` from
|
|
68
|
+
`pathapi`. Nothing connects until the first request.
|
|
69
|
+
- **Two backends, one behaviour**: `SFTPBackend` (paramiko) and `AsyncSSHBackend`
|
|
70
|
+
(asyncssh) raise the same exceptions for the same server answers; the choice is
|
|
71
|
+
a keyword, a subclass attribute or `SFTPPATH_BACKEND`.
|
|
72
|
+
- **Native tree operations**: on asyncssh a recursive `copy()` and `rm()` run as
|
|
73
|
+
concurrent requests on one connection, bounded by `max_concurrency`.
|
|
74
|
+
- **ssh_config**: `HostName`, `Port`, `User`, `IdentityFile`, `ProxyCommand` and
|
|
75
|
+
`Include` apply to the host you name; host keys are verified.
|
|
76
|
+
- **Server-side checksums**: `checksum()` uses the `check-file-handle`
|
|
77
|
+
extension where the server has it, and `PathSyncer` falls back to streaming
|
|
78
|
+
where it does not.
|
|
79
|
+
- **No SSH library at import**: `import sftppath` loads neither library nor
|
|
80
|
+
`uripath`. Errors are `OSError` subclasses, `SFTPAuthenticationError` and
|
|
81
|
+
`SFTPHostKeyError`; no library exception crosses the API.
|
|
82
|
+
- **The `sftp:` scheme** for `uripath`: `URIPath("sftp://user@host/dir")` is an
|
|
83
|
+
`SFTPURIPath` that delegates to an `SFTPPath`.
|
|
84
|
+
|
|
85
|
+
## Installation
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
pip install "sftppath[paramiko]"
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
Requires `pathapi`, which pip installs with it, and Python 3.9 or newer.
|
|
92
|
+
|
|
93
|
+
| Install | Adds |
|
|
94
|
+
| --- | --- |
|
|
95
|
+
| `pip install sftppath` | the path and the backend contract; no SSH library |
|
|
96
|
+
| `pip install "sftppath[paramiko]"` | the paramiko backend (`paramiko>=5.0,<6`) |
|
|
97
|
+
| `pip install "sftppath[asyncssh]"` | the asyncssh backend and the native tree operations (`asyncssh>=2.20`) |
|
|
98
|
+
| `pip install "sftppath[uri]"` | `uripath` and with it the `sftp:` scheme |
|
|
99
|
+
| `pip install "sftppath[all]"` | the three above |
|
|
100
|
+
|
|
101
|
+
## Quick start
|
|
102
|
+
|
|
103
|
+
**A path on a server**: the endpoint goes in as keywords; the user, password and
|
|
104
|
+
port may come from your `~/.ssh/config` instead:
|
|
105
|
+
|
|
106
|
+
<!-- example: skip: connects to a server -->
|
|
107
|
+
```python
|
|
108
|
+
from sftppath import SFTPPath
|
|
109
|
+
|
|
110
|
+
report = SFTPPath("/srv/data/report.txt", host="files.example.org", user="deploy")
|
|
111
|
+
print(report.read_text())
|
|
112
|
+
for child in report.parent.iterdir():
|
|
113
|
+
print(child.name, child.stat().st_size)
|
|
114
|
+
```
|
|
115
|
+
|
|
116
|
+
**Share a connection**: a backend is the connections to one server and
|
|
117
|
+
account, and a context manager:
|
|
118
|
+
|
|
119
|
+
<!-- example: skip: connects to a server -->
|
|
120
|
+
```python
|
|
121
|
+
from sftppath import SFTPBackend, SFTPPath
|
|
122
|
+
|
|
123
|
+
with SFTPBackend("files.example.org", user="deploy", timeout=10) as backend:
|
|
124
|
+
root = SFTPPath("/srv/data", backend=backend)
|
|
125
|
+
(root / "out").mkdir(exist_ok=True)
|
|
126
|
+
(root / "out" / "n.txt").write_text("1")
|
|
127
|
+
root.copy(SFTPPath("/srv/backup", backend=backend), recursive=True)
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
**Mirror a tree** with `PathSyncer`, onto a local directory:
|
|
131
|
+
|
|
132
|
+
<!-- example: skip: connects to a server -->
|
|
133
|
+
```python
|
|
134
|
+
from pathapi import LocalPath
|
|
135
|
+
from pathapi.sync import PathSyncer
|
|
136
|
+
from sftppath import SFTPPath
|
|
137
|
+
|
|
138
|
+
remote = SFTPPath("/var/log", host="files.example.org", user="deploy")
|
|
139
|
+
PathSyncer(remove_missing=True).sync(remote, LocalPath("mirror"))
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
**The scheme**, with `uripath` installed:
|
|
143
|
+
|
|
144
|
+
<!-- example: skip: connects to a server -->
|
|
145
|
+
```python
|
|
146
|
+
from uripath import URIPath
|
|
147
|
+
|
|
148
|
+
print(URIPath("sftp://deploy@files.example.org/srv/data/report.txt").read_text())
|
|
149
|
+
```
|
|
150
|
+
|
|
151
|
+
## API overview
|
|
152
|
+
|
|
153
|
+
| Module | Purpose |
|
|
154
|
+
| --- | --- |
|
|
155
|
+
| `sftppath` | `SFTPPath`, the backends `SFTPBackend`, `AsyncSSHBackend`, `BaseSFTPBackend` and `CheckFileSFTPBackend`, `SFTPEndpoint`, `DEFAULT_PORT`, `DEFAULT_SSH_CONFIG`, the errors and `__version__` |
|
|
156
|
+
| `sftppath.exceptions` | `SFTPPathError`, `SFTPAuthenticationError` and `SFTPHostKeyError` |
|
|
157
|
+
| `sftppath.uri` | `SFTPURIPath` and `SFTPURIBackend`, the `sftp:` scheme of `uripath` (needs the `uri` extra) |
|
|
158
|
+
|
|
159
|
+
The full reference is on the [documentation site](https://jose-pr.github.io/sftppath/);
|
|
160
|
+
an agent-oriented API summary ships inside the package as `sftppath/AGENTS.md`.
|
|
161
|
+
|
|
162
|
+
## Development
|
|
163
|
+
|
|
164
|
+
```bash
|
|
165
|
+
pip install -e ".[dev,all]"
|
|
166
|
+
python -m pytest -q -rs
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
The tests start SSH servers of their own on the loopback interface with
|
|
170
|
+
throwaway keys; they never reach another host. A test for a library or an extra
|
|
171
|
+
that is not installed is skipped, so check the skip list (`-rs`). Contributor
|
|
172
|
+
notes (virtual environments, formatting, CI, releases) are in the repository's
|
|
173
|
+
[`AGENTS.md`](https://github.com/jose-pr/sftppath/blob/main/AGENTS.md). To
|
|
174
|
+
preview the documentation: `pip install -e ".[all,docs]"` then `mkdocs serve`.
|
|
175
|
+
|
|
176
|
+
Changes are recorded in the
|
|
177
|
+
[changelog](https://github.com/jose-pr/sftppath/blob/main/CHANGELOG.md).
|
|
178
|
+
|
|
179
|
+
### Releasing
|
|
180
|
+
|
|
181
|
+
This project follows [Semantic Versioning](https://semver.org/) and keeps a
|
|
182
|
+
[`CHANGELOG.md`](https://github.com/jose-pr/sftppath/blob/main/CHANGELOG.md). Pushing a tag matching `v*` triggers the release
|
|
183
|
+
workflow: test gate → build (checking the tag names the version built) → a strict
|
|
184
|
+
docs build as a gate → GitHub release → publish. The release workflow never deploys
|
|
185
|
+
the docs site itself: for a final release its last job dispatches the docs workflow
|
|
186
|
+
at the tag, which owns every Pages deploy.
|
|
187
|
+
|
|
188
|
+
## License
|
|
189
|
+
|
|
190
|
+
MIT — see [LICENSE](https://github.com/jose-pr/sftppath/blob/main/LICENSE).
|
sftppath-0.1.0/README.md
ADDED
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
# sftppath
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/sftppath/)
|
|
4
|
+
[](https://pypi.org/project/sftppath/)
|
|
5
|
+
[](https://github.com/jose-pr/sftppath/blob/main/LICENSE)
|
|
6
|
+
[](https://jose-pr.github.io/sftppath/)
|
|
7
|
+
[](https://github.com/jose-pr/sftppath/actions/workflows/test.yml)
|
|
8
|
+
|
|
9
|
+
**`pathlib` for SFTP servers.** `SFTPPath("/srv/data/report.txt", host="files.example.org")`
|
|
10
|
+
has the method names, signatures and exception types of `pathlib.Path`, over
|
|
11
|
+
paramiko or asyncssh, and needs no URI. It builds on
|
|
12
|
+
[`pathapi`](https://github.com/jose-pr/pathapi) alone; with
|
|
13
|
+
[`uripath`](https://github.com/jose-pr/uripath) installed it also provides the
|
|
14
|
+
`sftp:` scheme. It is documented at <https://jose-pr.github.io/sftppath/>.
|
|
15
|
+
|
|
16
|
+
## Features
|
|
17
|
+
|
|
18
|
+
- **A `pathlib` path on a server**: `open`, `read_text`, `iterdir`, `glob`,
|
|
19
|
+
`stat`, `mkdir`, `rename`, `replace`, `chmod`, `symlink_to`, `readlink` and
|
|
20
|
+
the rest, plus `walk`, `copy`, `move`, `rm` and `PathSyncer` from
|
|
21
|
+
`pathapi`. Nothing connects until the first request.
|
|
22
|
+
- **Two backends, one behaviour**: `SFTPBackend` (paramiko) and `AsyncSSHBackend`
|
|
23
|
+
(asyncssh) raise the same exceptions for the same server answers; the choice is
|
|
24
|
+
a keyword, a subclass attribute or `SFTPPATH_BACKEND`.
|
|
25
|
+
- **Native tree operations**: on asyncssh a recursive `copy()` and `rm()` run as
|
|
26
|
+
concurrent requests on one connection, bounded by `max_concurrency`.
|
|
27
|
+
- **ssh_config**: `HostName`, `Port`, `User`, `IdentityFile`, `ProxyCommand` and
|
|
28
|
+
`Include` apply to the host you name; host keys are verified.
|
|
29
|
+
- **Server-side checksums**: `checksum()` uses the `check-file-handle`
|
|
30
|
+
extension where the server has it, and `PathSyncer` falls back to streaming
|
|
31
|
+
where it does not.
|
|
32
|
+
- **No SSH library at import**: `import sftppath` loads neither library nor
|
|
33
|
+
`uripath`. Errors are `OSError` subclasses, `SFTPAuthenticationError` and
|
|
34
|
+
`SFTPHostKeyError`; no library exception crosses the API.
|
|
35
|
+
- **The `sftp:` scheme** for `uripath`: `URIPath("sftp://user@host/dir")` is an
|
|
36
|
+
`SFTPURIPath` that delegates to an `SFTPPath`.
|
|
37
|
+
|
|
38
|
+
## Installation
|
|
39
|
+
|
|
40
|
+
```bash
|
|
41
|
+
pip install "sftppath[paramiko]"
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Requires `pathapi`, which pip installs with it, and Python 3.9 or newer.
|
|
45
|
+
|
|
46
|
+
| Install | Adds |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `pip install sftppath` | the path and the backend contract; no SSH library |
|
|
49
|
+
| `pip install "sftppath[paramiko]"` | the paramiko backend (`paramiko>=5.0,<6`) |
|
|
50
|
+
| `pip install "sftppath[asyncssh]"` | the asyncssh backend and the native tree operations (`asyncssh>=2.20`) |
|
|
51
|
+
| `pip install "sftppath[uri]"` | `uripath` and with it the `sftp:` scheme |
|
|
52
|
+
| `pip install "sftppath[all]"` | the three above |
|
|
53
|
+
|
|
54
|
+
## Quick start
|
|
55
|
+
|
|
56
|
+
**A path on a server**: the endpoint goes in as keywords; the user, password and
|
|
57
|
+
port may come from your `~/.ssh/config` instead:
|
|
58
|
+
|
|
59
|
+
<!-- example: skip: connects to a server -->
|
|
60
|
+
```python
|
|
61
|
+
from sftppath import SFTPPath
|
|
62
|
+
|
|
63
|
+
report = SFTPPath("/srv/data/report.txt", host="files.example.org", user="deploy")
|
|
64
|
+
print(report.read_text())
|
|
65
|
+
for child in report.parent.iterdir():
|
|
66
|
+
print(child.name, child.stat().st_size)
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
**Share a connection**: a backend is the connections to one server and
|
|
70
|
+
account, and a context manager:
|
|
71
|
+
|
|
72
|
+
<!-- example: skip: connects to a server -->
|
|
73
|
+
```python
|
|
74
|
+
from sftppath import SFTPBackend, SFTPPath
|
|
75
|
+
|
|
76
|
+
with SFTPBackend("files.example.org", user="deploy", timeout=10) as backend:
|
|
77
|
+
root = SFTPPath("/srv/data", backend=backend)
|
|
78
|
+
(root / "out").mkdir(exist_ok=True)
|
|
79
|
+
(root / "out" / "n.txt").write_text("1")
|
|
80
|
+
root.copy(SFTPPath("/srv/backup", backend=backend), recursive=True)
|
|
81
|
+
```
|
|
82
|
+
|
|
83
|
+
**Mirror a tree** with `PathSyncer`, onto a local directory:
|
|
84
|
+
|
|
85
|
+
<!-- example: skip: connects to a server -->
|
|
86
|
+
```python
|
|
87
|
+
from pathapi import LocalPath
|
|
88
|
+
from pathapi.sync import PathSyncer
|
|
89
|
+
from sftppath import SFTPPath
|
|
90
|
+
|
|
91
|
+
remote = SFTPPath("/var/log", host="files.example.org", user="deploy")
|
|
92
|
+
PathSyncer(remove_missing=True).sync(remote, LocalPath("mirror"))
|
|
93
|
+
```
|
|
94
|
+
|
|
95
|
+
**The scheme**, with `uripath` installed:
|
|
96
|
+
|
|
97
|
+
<!-- example: skip: connects to a server -->
|
|
98
|
+
```python
|
|
99
|
+
from uripath import URIPath
|
|
100
|
+
|
|
101
|
+
print(URIPath("sftp://deploy@files.example.org/srv/data/report.txt").read_text())
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
## API overview
|
|
105
|
+
|
|
106
|
+
| Module | Purpose |
|
|
107
|
+
| --- | --- |
|
|
108
|
+
| `sftppath` | `SFTPPath`, the backends `SFTPBackend`, `AsyncSSHBackend`, `BaseSFTPBackend` and `CheckFileSFTPBackend`, `SFTPEndpoint`, `DEFAULT_PORT`, `DEFAULT_SSH_CONFIG`, the errors and `__version__` |
|
|
109
|
+
| `sftppath.exceptions` | `SFTPPathError`, `SFTPAuthenticationError` and `SFTPHostKeyError` |
|
|
110
|
+
| `sftppath.uri` | `SFTPURIPath` and `SFTPURIBackend`, the `sftp:` scheme of `uripath` (needs the `uri` extra) |
|
|
111
|
+
|
|
112
|
+
The full reference is on the [documentation site](https://jose-pr.github.io/sftppath/);
|
|
113
|
+
an agent-oriented API summary ships inside the package as `sftppath/AGENTS.md`.
|
|
114
|
+
|
|
115
|
+
## Development
|
|
116
|
+
|
|
117
|
+
```bash
|
|
118
|
+
pip install -e ".[dev,all]"
|
|
119
|
+
python -m pytest -q -rs
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
The tests start SSH servers of their own on the loopback interface with
|
|
123
|
+
throwaway keys; they never reach another host. A test for a library or an extra
|
|
124
|
+
that is not installed is skipped, so check the skip list (`-rs`). Contributor
|
|
125
|
+
notes (virtual environments, formatting, CI, releases) are in the repository's
|
|
126
|
+
[`AGENTS.md`](https://github.com/jose-pr/sftppath/blob/main/AGENTS.md). To
|
|
127
|
+
preview the documentation: `pip install -e ".[all,docs]"` then `mkdocs serve`.
|
|
128
|
+
|
|
129
|
+
Changes are recorded in the
|
|
130
|
+
[changelog](https://github.com/jose-pr/sftppath/blob/main/CHANGELOG.md).
|
|
131
|
+
|
|
132
|
+
### Releasing
|
|
133
|
+
|
|
134
|
+
This project follows [Semantic Versioning](https://semver.org/) and keeps a
|
|
135
|
+
[`CHANGELOG.md`](https://github.com/jose-pr/sftppath/blob/main/CHANGELOG.md). Pushing a tag matching `v*` triggers the release
|
|
136
|
+
workflow: test gate → build (checking the tag names the version built) → a strict
|
|
137
|
+
docs build as a gate → GitHub release → publish. The release workflow never deploys
|
|
138
|
+
the docs site itself: for a final release its last job dispatches the docs workflow
|
|
139
|
+
at the tag, which owns every Pages deploy.
|
|
140
|
+
|
|
141
|
+
## License
|
|
142
|
+
|
|
143
|
+
MIT — see [LICENSE](https://github.com/jose-pr/sftppath/blob/main/LICENSE).
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# Exceptions
|
|
2
|
+
|
|
3
|
+
`SFTPPathError` is the base of what this package raises on its own. A caller's
|
|
4
|
+
bad argument is a plain `ValueError` or `TypeError`, and a failed request is the
|
|
5
|
+
`OSError` of the kind `pathlib` raises.
|
|
6
|
+
|
|
7
|
+
::: sftppath.exceptions
|
|
8
|
+
options:
|
|
9
|
+
show_if_no_docstring: true
|
|
10
|
+
filters: ["!^_"]
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# SFTP paths
|
|
2
|
+
|
|
3
|
+
::: sftppath
|
|
4
|
+
options:
|
|
5
|
+
show_if_no_docstring: true
|
|
6
|
+
filters: ["!^_"]
|
|
7
|
+
members:
|
|
8
|
+
- SFTPPath
|
|
9
|
+
- SFTPEndpoint
|
|
10
|
+
- BaseSFTPBackend
|
|
11
|
+
- CheckFileSFTPBackend
|
|
12
|
+
- DEFAULT_PORT
|
|
13
|
+
- DEFAULT_SSH_CONFIG
|
|
14
|
+
|
|
15
|
+
::: sftppath.SFTPBackend
|
|
16
|
+
|
|
17
|
+
::: sftppath.AsyncSSHBackend
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
# Benchmarks
|
|
2
|
+
|
|
3
|
+
`sftppath` includes a benchmark harness at `benchmarks/bench.py` that times
|
|
4
|
+
`SFTPPath` over paramiko and over asyncssh, and the `sftp:` scheme of `uripath`
|
|
5
|
+
on the same operations. It needs both SSH libraries and `uripath`:
|
|
6
|
+
`pip install -e ".[dev,all]"`.
|
|
7
|
+
|
|
8
|
+
## Run It
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
python benchmarks/bench.py # run the benchmarks
|
|
12
|
+
python benchmarks/bench.py --help # options and suites; runs nothing
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
A suite name after the options runs part of the matrix: `sftp-batch`,
|
|
16
|
+
`sftp-recursive`, `sftp-recursive-copy` or `sftp-recursive-large`.
|
|
17
|
+
|
|
18
|
+
## Saved Results
|
|
19
|
+
|
|
20
|
+
Every metric is timed over several samples. `--save` writes the run as JSON to
|
|
21
|
+
`benchmarks/results/<name>.json`, with the minimum, median and maximum
|
|
22
|
+
milliseconds per call of each metric plus the interpreter, platform and git
|
|
23
|
+
commit it ran on:
|
|
24
|
+
|
|
25
|
+
```bash
|
|
26
|
+
python benchmarks/bench.py --save
|
|
27
|
+
python benchmarks/bench.py --save --samples 9
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
`--samples N` applies one sample count to every case; without it each case uses
|
|
31
|
+
its own (1-5). The result schema, the suites and the committed results are
|
|
32
|
+
described in
|
|
33
|
+
[`benchmarks/README.md`](https://github.com/jose-pr/sftppath/blob/main/benchmarks/README.md).
|
|
34
|
+
|
|
35
|
+
Compare two runs on their medians, and only between runs from the same machine
|
|
36
|
+
and interpreter. A run on a developer machine is a sanity check; performance
|
|
37
|
+
claims in release notes come from CI runs.
|
|
38
|
+
|
|
39
|
+
## What It Covers
|
|
40
|
+
|
|
41
|
+
- `stat()`, listing (`iterdir()`), `walk()` and `glob()` of a small tree
|
|
42
|
+
- `read_bytes()`, `write_bytes()` of 256 KiB, `mkdir()`, `rename()`, `unlink()`
|
|
43
|
+
and a single-file `copy()`
|
|
44
|
+
- recursive `copy()` and `rm()` of 4-, 9- and 128-file trees, and the asyncssh
|
|
45
|
+
recursive copy at several `max_concurrency` values
|
|
46
|
+
- a cold connect and `stat()` on a new backend
|
|
47
|
+
- each of the above on a paramiko backend and on an asyncssh backend, and for
|
|
48
|
+
both through `SFTPPath` (metric group `sftp`) and through
|
|
49
|
+
`URIPath("sftp://...", backend=SFTPURIBackend(...))` (group `sftp-uri`), so
|
|
50
|
+
the cost of the URI layer is the difference between the two groups
|
|
51
|
+
|
|
52
|
+
The cases run against an in-process asyncssh server on 127.0.0.1, so nothing
|
|
53
|
+
leaves the machine. Without `uripath` the `sftp-uri` group is skipped and the
|
|
54
|
+
run records why.
|
|
55
|
+
|
|
56
|
+
## Caveats
|
|
57
|
+
|
|
58
|
+
The server and the client share one machine and one interpreter, with no
|
|
59
|
+
network latency, so the numbers show the overhead of a backend and of the path
|
|
60
|
+
layer, not how a remote link behaves. Timings depend on the machine, the
|
|
61
|
+
interpreter and what else is running; compare medians, as described above.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Divergences from `pathlib`
|
|
2
|
+
|
|
3
|
+
`sftppath` targets `pathlib.Path` parity: same method names, signatures,
|
|
4
|
+
semantics and exception types wherever a `pathlib.Path` equivalent exists.
|
|
5
|
+
Extensions (extra optional kwargs, new methods like `rm()` and `copy()`) are
|
|
6
|
+
allowed. Any *behavioral* divergence from `pathlib` on a method that exists in
|
|
7
|
+
both must be listed here -- no silent divergence.
|
|
8
|
+
|
|
9
|
+
The divergences below apply to `SFTPPath` and `SFTPURIPath`. The ones of the
|
|
10
|
+
generic `Path` operations that every path inherits (`touch()` on a non-local
|
|
11
|
+
backend, `rm()` and its policies, `chown()`, `Path.replace()`, a `with` block
|
|
12
|
+
that raised while writing) are on the documentation site of `pathapi`,
|
|
13
|
+
<https://jose-pr.github.io/pathapi/divergences/>.
|
|
14
|
+
|
|
15
|
+
| Method | `pathlib` | `SFTPPath` | Why |
|
|
16
|
+
| --- | --- | --- | --- |
|
|
17
|
+
| A relative path, and the empty path | relative to the current working directory | names are read from the server root: `SFTPPath("x", host=h)` is `/x`, the empty path is `/` | A tree served by a backend has no working directory, and two spellings of one name must be one file (`==`, `copy()` and `move()` ask). |
|
|
18
|
+
| `rename(target)` onto an existing file | POSIX `rename(2)`: replaces | Replaces through `posix-rename@openssh.com` where the server supports it; otherwise raises `FileExistsError`. | Plain SFTPv3 rename refuses an existing target with an uninformative failure. |
|
|
19
|
+
| `rename(str)` | a relative `str` is relative to the current working directory | a `str` is a sibling name (relative to `self.parent`); an absolute one is a path on the same server | There is no working directory; a sibling is what a rename within a directory means. |
|
|
20
|
+
| `replace(target)` onto an existing file | `os.replace` is one atomic call | One `posix-rename@openssh.com` request where the server has it (atomic; a refusal leaves both entries as they were). Without it the target is removed and then this entry renamed, so a failure between the two loses the target; a bare failure from the server is treated as the extension being absent. | Removing first left a window in which the target did not exist on every overwrite. |
|
|
21
|
+
| `chmod(mode, follow_symlinks=False)` | supported where the platform has `lchmod` | `NotImplementedError` on the paramiko backend; supported on asyncssh | paramiko has no call for it. |
|
|
22
|
+
| `hardlink_to(target)` | supported | `NotImplementedError` on the paramiko backend; supported on asyncssh, and only onto a path of the same server | SFTPv3 has no core hard-link operation. |
|
|
23
|
+
| `chown(uid, gid)` | not a `pathlib` method | numeric ids only; a partial change reads the other field first and raises `NotImplementedError` when the server names owners instead of numbering them | SFTPv3 carries the owner as one numeric pair. |
|
|
24
|
+
| `os.fspath()` of an `SFTPURIPath` | the local path | the path on the *server* (`.path`); `host_fspath()` says so | Meant for building a command line that runs on that host. `os.fspath()` of an `SFTPPath` is `NotImplementedError`. |
|