pathapi-uri 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.
- pathapi_uri-0.1.0/.gitignore +41 -0
- pathapi_uri-0.1.0/CHANGELOG.md +341 -0
- pathapi_uri-0.1.0/LICENSE +21 -0
- pathapi_uri-0.1.0/PKG-INFO +259 -0
- pathapi_uri-0.1.0/README.md +213 -0
- pathapi_uri-0.1.0/docs/api/cli.md +11 -0
- pathapi_uri-0.1.0/docs/api/exceptions.md +10 -0
- pathapi_uri-0.1.0/docs/api/schemes/archive.md +15 -0
- pathapi_uri-0.1.0/docs/api/schemes/ftp.md +7 -0
- pathapi_uri-0.1.0/docs/api/schemes/http.md +22 -0
- pathapi_uri-0.1.0/docs/api/schemes/local.md +8 -0
- pathapi_uri-0.1.0/docs/api/uri.md +23 -0
- pathapi_uri-0.1.0/docs/benchmarks.md +47 -0
- pathapi_uri-0.1.0/docs/changelog.md +3 -0
- pathapi_uri-0.1.0/docs/divergences.md +59 -0
- pathapi_uri-0.1.0/docs/guides/cli.md +119 -0
- pathapi_uri-0.1.0/docs/guides/extending.md +276 -0
- pathapi_uri-0.1.0/docs/guides/schemes.md +279 -0
- pathapi_uri-0.1.0/docs/index.md +93 -0
- pathapi_uri-0.1.0/examples/data_and_archive.py +98 -0
- pathapi_uri-0.1.0/examples/ftp_listing.py +61 -0
- pathapi_uri-0.1.0/examples/http_listing.py +44 -0
- pathapi_uri-0.1.0/examples/webdav_roundtrip.py +82 -0
- pathapi_uri-0.1.0/mkdocs.yml +64 -0
- pathapi_uri-0.1.0/pyproject.toml +111 -0
- pathapi_uri-0.1.0/src/uripath/AGENTS.md +1094 -0
- pathapi_uri-0.1.0/src/uripath/__init__.py +27 -0
- pathapi_uri-0.1.0/src/uripath/__main__.py +7 -0
- pathapi_uri-0.1.0/src/uripath/_pathtext.py +32 -0
- pathapi_uri-0.1.0/src/uripath/_rfc3986.py +67 -0
- pathapi_uri-0.1.0/src/uripath/_transfer.py +108 -0
- pathapi_uri-0.1.0/src/uripath/_uri.py +1007 -0
- pathapi_uri-0.1.0/src/uripath/_version.py +11 -0
- pathapi_uri-0.1.0/src/uripath/cli/__init__.py +54 -0
- pathapi_uri-0.1.0/src/uripath/cli/_common.py +48 -0
- pathapi_uri-0.1.0/src/uripath/cli/_root.py +70 -0
- pathapi_uri-0.1.0/src/uripath/cli/cp.py +61 -0
- pathapi_uri-0.1.0/src/uripath/cli/read.py +22 -0
- pathapi_uri-0.1.0/src/uripath/cli/rm.py +38 -0
- pathapi_uri-0.1.0/src/uripath/cli/sync.py +78 -0
- pathapi_uri-0.1.0/src/uripath/cli/write.py +49 -0
- pathapi_uri-0.1.0/src/uripath/exceptions.py +38 -0
- pathapi_uri-0.1.0/src/uripath/py.typed +1 -0
- pathapi_uri-0.1.0/src/uripath/query.py +240 -0
- pathapi_uri-0.1.0/src/uripath/schemes/__init__.py +120 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_archive.py +338 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_data.py +99 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_dav.py +459 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_delegate.py +127 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_elsewhere.py +102 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_file.py +186 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_ftp.py +200 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_http/__init__.py +4 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_http/_backend.py +132 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_http/_headers.py +8 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_http/_listing.py +403 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_http/_path.py +371 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_http/_settings.py +6 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_http/_streams.py +230 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_httpdate.py +48 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_path.py +629 -0
- pathapi_uri-0.1.0/src/uripath/schemes/_registry.py +134 -0
- pathapi_uri-0.1.0/src/uripath/source/__init__.py +20 -0
- pathapi_uri-0.1.0/src/uripath/source/_compose.py +212 -0
- pathapi_uri-0.1.0/src/uripath/source/_locality.py +44 -0
- pathapi_uri-0.1.0/src/uripath/source/_parse.py +183 -0
- pathapi_uri-0.1.0/src/uripath/source/_source.py +239 -0
- pathapi_uri-0.1.0/tests/capabilities.py +70 -0
- pathapi_uri-0.1.0/tests/census.py +122 -0
- pathapi_uri-0.1.0/tests/conftest.py +618 -0
- pathapi_uri-0.1.0/tests/fake_distribution.py +55 -0
- pathapi_uri-0.1.0/tests/ftp_loopback.py +286 -0
- pathapi_uri-0.1.0/tests/hermetic.py +261 -0
- pathapi_uri-0.1.0/tests/http_fakes.py +139 -0
- pathapi_uri-0.1.0/tests/http_wire.py +498 -0
- pathapi_uri-0.1.0/tests/mutation/README.md +66 -0
- pathapi_uri-0.1.0/tests/mutation/mutants.py +569 -0
- pathapi_uri-0.1.0/tests/mutation/mutation_check.py +16 -0
- pathapi_uri-0.1.0/tests/mutation/run.py +224 -0
- pathapi_uri-0.1.0/tests/probes/probe_environment_leak.py +12 -0
- pathapi_uri-0.1.0/tests/probes/probe_leaks_a_socket.py +12 -0
- pathapi_uri-0.1.0/tests/probes/probe_session_environment.py +36 -0
- pathapi_uri-0.1.0/tests/server_loops.py +43 -0
- pathapi_uri-0.1.0/tests/test_archive_batch.py +763 -0
- pathapi_uri-0.1.0/tests/test_archive_batch_cost.py +368 -0
- pathapi_uri-0.1.0/tests/test_archive_damage.py +274 -0
- pathapi_uri-0.1.0/tests/test_archive_helpers.py +214 -0
- pathapi_uri-0.1.0/tests/test_archive_lazy.py +262 -0
- pathapi_uri-0.1.0/tests/test_archive_link_listing.py +410 -0
- pathapi_uri-0.1.0/tests/test_archive_links.py +572 -0
- pathapi_uri-0.1.0/tests/test_archive_names.py +69 -0
- pathapi_uri-0.1.0/tests/test_archive_parity.py +400 -0
- pathapi_uri-0.1.0/tests/test_archive_rewrite.py +353 -0
- pathapi_uri-0.1.0/tests/test_archive_safety.py +868 -0
- pathapi_uri-0.1.0/tests/test_archive_scale.py +198 -0
- pathapi_uri-0.1.0/tests/test_archive_schemes.py +99 -0
- pathapi_uri-0.1.0/tests/test_archive_tree.py +396 -0
- pathapi_uri-0.1.0/tests/test_archive_uri.py +516 -0
- pathapi_uri-0.1.0/tests/test_as_path.py +80 -0
- pathapi_uri-0.1.0/tests/test_backend_family.py +331 -0
- pathapi_uri-0.1.0/tests/test_backend_scope.py +401 -0
- pathapi_uri-0.1.0/tests/test_capabilities.py +93 -0
- pathapi_uri-0.1.0/tests/test_census.py +129 -0
- pathapi_uri-0.1.0/tests/test_cli.py +683 -0
- pathapi_uri-0.1.0/tests/test_cli_surface.py +229 -0
- pathapi_uri-0.1.0/tests/test_contract.py +271 -0
- pathapi_uri-0.1.0/tests/test_copy_stream.py +235 -0
- pathapi_uri-0.1.0/tests/test_data_uri.py +207 -0
- pathapi_uri-0.1.0/tests/test_dav.py +47 -0
- pathapi_uri-0.1.0/tests/test_delegating_uri_path.py +104 -0
- pathapi_uri-0.1.0/tests/test_destructive_safety.py +298 -0
- pathapi_uri-0.1.0/tests/test_documentation.py +153 -0
- pathapi_uri-0.1.0/tests/test_documented_examples.py +204 -0
- pathapi_uri-0.1.0/tests/test_elsewhere.py +180 -0
- pathapi_uri-0.1.0/tests/test_examples_skip.py +62 -0
- pathapi_uri-0.1.0/tests/test_exceptions.py +92 -0
- pathapi_uri-0.1.0/tests/test_file_uri_paths.py +294 -0
- pathapi_uri-0.1.0/tests/test_file_uri_text.py +135 -0
- pathapi_uri-0.1.0/tests/test_ftp.py +474 -0
- pathapi_uri-0.1.0/tests/test_ftp_parity.py +297 -0
- pathapi_uri-0.1.0/tests/test_ftp_utime.py +174 -0
- pathapi_uri-0.1.0/tests/test_ftp_wire.py +465 -0
- pathapi_uri-0.1.0/tests/test_generic_pure_parity.py +185 -0
- pathapi_uri-0.1.0/tests/test_glob_edges.py +393 -0
- pathapi_uri-0.1.0/tests/test_glob_parity.py +308 -0
- pathapi_uri-0.1.0/tests/test_hermetic.py +344 -0
- pathapi_uri-0.1.0/tests/test_host_case.py +238 -0
- pathapi_uri-0.1.0/tests/test_http.py +571 -0
- pathapi_uri-0.1.0/tests/test_http_credentials.py +203 -0
- pathapi_uri-0.1.0/tests/test_http_date.py +70 -0
- pathapi_uri-0.1.0/tests/test_http_default.py +171 -0
- pathapi_uri-0.1.0/tests/test_http_listing.py +664 -0
- pathapi_uri-0.1.0/tests/test_http_live.py +131 -0
- pathapi_uri-0.1.0/tests/test_http_parser.py +244 -0
- pathapi_uri-0.1.0/tests/test_http_peer_text.py +59 -0
- pathapi_uri-0.1.0/tests/test_http_redirect_origin.py +151 -0
- pathapi_uri-0.1.0/tests/test_http_redirects.py +78 -0
- pathapi_uri-0.1.0/tests/test_http_reply_limits.py +164 -0
- pathapi_uri-0.1.0/tests/test_http_unclosed_upload.py +58 -0
- pathapi_uri-0.1.0/tests/test_http_writes.py +759 -0
- pathapi_uri-0.1.0/tests/test_httpdav_safety.py +483 -0
- pathapi_uri-0.1.0/tests/test_io_parity.py +105 -0
- pathapi_uri-0.1.0/tests/test_listing_hints.py +255 -0
- pathapi_uri-0.1.0/tests/test_listing_hints_http.py +156 -0
- pathapi_uri-0.1.0/tests/test_listing_names.py +207 -0
- pathapi_uri-0.1.0/tests/test_low_core.py +364 -0
- pathapi_uri-0.1.0/tests/test_low_schemes.py +339 -0
- pathapi_uri-0.1.0/tests/test_low_sync.py +122 -0
- pathapi_uri-0.1.0/tests/test_packaging.py +72 -0
- pathapi_uri-0.1.0/tests/test_path_info.py +93 -0
- pathapi_uri-0.1.0/tests/test_pathname.py +283 -0
- pathapi_uri-0.1.0/tests/test_plugins.py +156 -0
- pathapi_uri-0.1.0/tests/test_properties.py +913 -0
- pathapi_uri-0.1.0/tests/test_provider_seam.py +338 -0
- pathapi_uri-0.1.0/tests/test_public_surface.py +205 -0
- pathapi_uri-0.1.0/tests/test_pure_parity.py +590 -0
- pathapi_uri-0.1.0/tests/test_query.py +237 -0
- pathapi_uri-0.1.0/tests/test_rfc3986.py +144 -0
- pathapi_uri-0.1.0/tests/test_rm_overrides.py +55 -0
- pathapi_uri-0.1.0/tests/test_routing.py +132 -0
- pathapi_uri-0.1.0/tests/test_same_filesystem_uri.py +238 -0
- pathapi_uri-0.1.0/tests/test_scheme_distributions.py +248 -0
- pathapi_uri-0.1.0/tests/test_scheme_registry.py +424 -0
- pathapi_uri-0.1.0/tests/test_shipped_header.py +362 -0
- pathapi_uri-0.1.0/tests/test_smoke.py +146 -0
- pathapi_uri-0.1.0/tests/test_source.py +215 -0
- pathapi_uri-0.1.0/tests/test_source_is_local.py +119 -0
- pathapi_uri-0.1.0/tests/test_sync_safety.py +436 -0
- pathapi_uri-0.1.0/tests/test_tls_warning.py +49 -0
- pathapi_uri-0.1.0/tests/test_transport_security.py +611 -0
- pathapi_uri-0.1.0/tests/test_uri_authority.py +295 -0
- pathapi_uri-0.1.0/tests/test_uri_constructor_options.py +105 -0
- pathapi_uri-0.1.0/tests/test_uri_copies.py +296 -0
- pathapi_uri-0.1.0/tests/test_uri_core_parity.py +222 -0
- pathapi_uri-0.1.0/tests/test_uri_cost.py +91 -0
- pathapi_uri-0.1.0/tests/test_uri_derivation_query.py +115 -0
- pathapi_uri-0.1.0/tests/test_uri_dot_segments.py +207 -0
- pathapi_uri-0.1.0/tests/test_uri_empty_segments.py +94 -0
- pathapi_uri-0.1.0/tests/test_uri_join_shape.py +92 -0
- pathapi_uri-0.1.0/tests/test_uri_missing_extras.py +105 -0
- pathapi_uri-0.1.0/tests/test_uri_parse.py +215 -0
- pathapi_uri-0.1.0/tests/test_uri_path.py +414 -0
- pathapi_uri-0.1.0/tests/test_uri_schemesmap.py +104 -0
- pathapi_uri-0.1.0/tests/test_uri_values.py +297 -0
- pathapi_uri-0.1.0/tests/test_version.py +24 -0
- pathapi_uri-0.1.0/tests/test_webdav.py +453 -0
- pathapi_uri-0.1.0/tests/test_with_session.py +105 -0
- pathapi_uri-0.1.0/tests/test_write_discard.py +184 -0
- pathapi_uri-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,341 @@
|
|
|
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
|
+
Published on PyPI as `pathapi-uri` (`pip install pathapi-uri`); the import
|
|
13
|
+
package and the command are `uripath`.
|
|
14
|
+
|
|
15
|
+
The first release of `uripath`: the URI layer of `pathlib-next` 0.9.14
|
|
16
|
+
(`pathlib_next.uri` and the `uripath` command) as a distribution of its own. It
|
|
17
|
+
needs Python 3.9 or newer, `pathapi>=0.1.0,<0.2` and
|
|
18
|
+
`netimps>=0.4.0,<0.5`, and nothing else; its one extra, `cli`, adds the command
|
|
19
|
+
(`duho>=0.7.3,<0.8`). Every scheme it implements runs on the standard library.
|
|
20
|
+
Under *Changed*, the migration from `pathlib_next.uri` comes in this order: the
|
|
21
|
+
table of names, the table of extras, then every change of behaviour with what to
|
|
22
|
+
do about it.
|
|
23
|
+
|
|
24
|
+
It is built on `pathapi`, which was published as `pathlib-next` up to 0.10.0 (import
|
|
25
|
+
name `pathlib_next`); the old names below are the ones before the move.
|
|
26
|
+
|
|
27
|
+
### Added
|
|
28
|
+
|
|
29
|
+
- `URI`, `URIPath`, `Source` and `Query`, the schemes `file:`, `data:`, `ftp:`,
|
|
30
|
+
`ftps:`, `zip:`, `tar:`, `archive:`, `http:`, `https:`, `dav:` and `davs:`,
|
|
31
|
+
and the `uripath` command, with `uripath.__version__` (read from the
|
|
32
|
+
installed metadata).
|
|
33
|
+
- `pathapi.protocols.supports()` and `capabilities()` tell the truth about a URI
|
|
34
|
+
class: `data:` does not support `Listing`, a scheme that lists through `_listdir()`
|
|
35
|
+
or its own `_scandir()` does, and an instance of a `DelegatingURIPath` scheme
|
|
36
|
+
answers with its core path (`_supports()`), so a delegated hook the core path
|
|
37
|
+
lacks reads as absent.
|
|
38
|
+
- `sftp:`, `s3:`, `gs:`, `az:`, `repo:`, `github:` and `gitlab:` are known
|
|
39
|
+
scheme names that this distribution does not implement. A path of one joins,
|
|
40
|
+
compares and prints without the distribution that provides it; the first
|
|
41
|
+
operation that needs the server (`stat()`, `open()`, `iterdir()`,
|
|
42
|
+
`exists()`, ...) raises an `ImportError` naming what to install:
|
|
43
|
+
`sftppath[paramiko]` (or `[asyncssh]`) for `sftp:`, `objpath[s3]`, `[gs]` or
|
|
44
|
+
`[az]`, and `repopath` for the other three. `URIPath.schemes()` lists the
|
|
45
|
+
seven, and `URIPath.scheme_class()` raises the same error for them. The user
|
|
46
|
+
slot of `repo:`, `github:` and `gitlab:` is an access token, so `str()` and
|
|
47
|
+
`repr()` of a `Source` of those schemes show no userinfo.
|
|
48
|
+
A distribution that registers one of these names in the `uripath.schemes`
|
|
49
|
+
entry-point group takes it.
|
|
50
|
+
- `http:`, `https:`, `dav:` and `davs:` send through the
|
|
51
|
+
HTTP client and transport of `pathapi.http`, which `uripath` does not
|
|
52
|
+
copy: `HTTPTransport`, `HTTPResponse`, the transport contract, the rules for
|
|
53
|
+
redirects and credentials, the cap on a reply read whole
|
|
54
|
+
(`pathapi.http.MAX_REPLY_BYTES`), `PATHAPI_HTTP_TRANSPORT` and the
|
|
55
|
+
entry-point group `pathapi.http_transports` all live there, and so does
|
|
56
|
+
`python -m pathapi.http.testing --http-transports NAME`, the suite that a
|
|
57
|
+
distribution providing a transport runs over it. `HTTPBackend` is a
|
|
58
|
+
`pathapi.http.HTTPClient` that adds what a path needs.
|
|
59
|
+
- `uripath.exceptions`: `URIPathError`, the base of what this package raises
|
|
60
|
+
on its own, with `URISyntaxError` and `UnsupportedSchemeError` (both also
|
|
61
|
+
`ValueError`) and `OutputClosedError` (also `BrokenPipeError`).
|
|
62
|
+
- `uripath.schemes.DelegatingURIPath` is the base of a scheme whose I/O is
|
|
63
|
+
done by a `pathapi.Path` that needs no URI, and the one `file:`, `ftp:`
|
|
64
|
+
and the archive schemes use. A distribution that provides a scheme this way
|
|
65
|
+
(the `sftp:` of `sftppath`) lists the hooks it hands over in `_DELEGATED` and
|
|
66
|
+
builds the path in `_make_core()`; the header documents both under "For
|
|
67
|
+
scheme implementers".
|
|
68
|
+
- `uripath.source.fold_host(host)` returns the form of a host that comparisons
|
|
69
|
+
and caches key on.
|
|
70
|
+
- `DataURIPath.charset` is the charset a `data:` URI declares.
|
|
71
|
+
- `FTPURIBackend` and `BaseFTPURIBackend` have `close()` and are context
|
|
72
|
+
managers: `close()` closes the control connection every thread holds to the
|
|
73
|
+
servers of that backend, may be called twice, and a later use connects
|
|
74
|
+
again. Leaving a `with` block calls it.
|
|
75
|
+
- Building an `FTPURIBackend` with `verify=False` and no `ssl_context` logs a
|
|
76
|
+
warning on the `uripath` logger each time (an `HTTPTransport` does the same on
|
|
77
|
+
the `pathapi` logger).
|
|
78
|
+
- `uripath.as_path(text)` returns the `URIPath` or `LocalPath` that command-line
|
|
79
|
+
style text names (`C:/data`, `12:30.txt` and `notes:draft` are local paths;
|
|
80
|
+
`ftp://h/x` and `data:,abc` are URIs), and `uripath.copy_stream(source,
|
|
81
|
+
target)` copies the bytes of a path or a binary stream into another in chunks,
|
|
82
|
+
with `exclusive=True` for an exclusive create. The `uripath` command is built
|
|
83
|
+
on them. `copy_stream` raises `OutputClosedError` when the reader of a target
|
|
84
|
+
stream went away.
|
|
85
|
+
- `python -m uripath` runs the command, and `uripath --version` prints the
|
|
86
|
+
version. The command describes itself to an agent (`AGENT_HELP=1 uripath
|
|
87
|
+
--help` prints one JSON document) and can be served as tools
|
|
88
|
+
(`URIPATH_MCP=stdio uripath`): `read`, `write`, `rm`, `cp` and `sync`, with
|
|
89
|
+
the schemas their fields give. A tool has no standard input, so `-` as a
|
|
90
|
+
source and `write` without `DATA` fail with a message.
|
|
91
|
+
|
|
92
|
+
### Changed
|
|
93
|
+
|
|
94
|
+
- Import from `uripath` where the code imported from `pathlib_next.uri`. There
|
|
95
|
+
is no alias for an old name or path: change the import. The classes spell
|
|
96
|
+
their acronyms in capitals, and the class of a scheme is named
|
|
97
|
+
`<Scheme>URIPath`, so `<Scheme>Path` stays the class that needs no URI. A
|
|
98
|
+
scheme's module is private: every public name of a scheme is exported by
|
|
99
|
+
`uripath.schemes`. Everything that was renamed or moved:
|
|
100
|
+
|
|
101
|
+
| Was (in `pathlib_next.uri`, `pathlib_next` 0.9.14) | Is |
|
|
102
|
+
| --- | --- |
|
|
103
|
+
| the package `pathlib_next.uri` | `uripath` |
|
|
104
|
+
| `Path`, `Pathname`, `FileStat` imported from `pathlib_next.uri` | the same names in `pathapi` |
|
|
105
|
+
| `Uri`, `UriPath`, `UriLike` | `uripath.URI`, `URIPath`, `URILike` |
|
|
106
|
+
| `source.Source` | `uripath.Source`, also `uripath.source.Source` |
|
|
107
|
+
| `source.compose_uri`, `compose_host`, `percent_encode`, `is_drive`, `SAFE_PATH`, `ERRORS` | the same names in `uripath.source` |
|
|
108
|
+
| `same_authority` | `uripath.source.same_authority` |
|
|
109
|
+
| `DerivedBackend` | `uripath.schemes.DerivedBackend` |
|
|
110
|
+
| `query.Query` | `uripath.Query`, also `uripath.query.Query`; no longer a `str` (see below) |
|
|
111
|
+
| `Query(text)`, `Query.decode()` | `Query.parse(text)`, `Query.to_pairs()` |
|
|
112
|
+
| `Source.from_str(text, strict)` | `Source.parse(text, *, strict=True)`, and `Source.try_parse(text, default=None, *, strict=True)` |
|
|
113
|
+
| `Source.as_str(sanitize=True)` | `str(source)` |
|
|
114
|
+
| `Source.as_str(sanitize=False)` | `Source.reveal()` |
|
|
115
|
+
| the modules `schemes.file`, `data`, `dav`, `ftp`, `http`, `archive` | private; each public name is `uripath.schemes.<Name>` |
|
|
116
|
+
| `schemes.file.FileUri`, `schemes.data.DataUri` | `uripath.schemes.FileURIPath`, `DataURIPath` |
|
|
117
|
+
| `schemes.archive.ZipUri`, `TarUri`, `ArchiveUri` | `uripath.schemes.ZipURIPath`, `TarURIPath`, `ArchiveURIPath` |
|
|
118
|
+
| `schemes.archive.ArchiveZipUri`, `ArchiveTarUri` | `uripath.schemes.ArchiveZipURIPath`, `ArchiveTarURIPath` |
|
|
119
|
+
| `schemes.ftp.FtpPath` | `uripath.schemes.FTPURIPath` |
|
|
120
|
+
| `schemes.ftp.FtpBackend`, `BaseFtpBackend` | `uripath.schemes.FTPURIBackend`, `BaseFTPURIBackend` |
|
|
121
|
+
| `schemes.ftp.DEFAULT_TIMEOUT`, `IDLE_PROBE_SECONDS`; `schemes.archive.MAX_LINKS`, `LinkTarget` | `pathapi.ftp` and `pathapi.archive`, which define them |
|
|
122
|
+
| `schemes.http.HttpPath`, `HttpBackend` | `uripath.schemes.HTTPURIPath`, `HTTPBackend` |
|
|
123
|
+
| `schemes.http.HttpWriteStream`, `HttpAppendStream` | `uripath.schemes.HTTPWriteStream`, `HTTPAppendStream` |
|
|
124
|
+
| `schemes.http.HTTPTransport`, `HTTPResponse` | `pathapi.http.HTTPTransport`, `HTTPResponse` |
|
|
125
|
+
| `schemes.http.MAX_LISTING_BYTES` | `pathapi.http.MAX_REPLY_BYTES` (assign it there) |
|
|
126
|
+
| `schemes.http.DEFAULT_TIMEOUT` | none: the default is the `timeout=` of the backend, `(10, 60)` |
|
|
127
|
+
| `schemes.archive._base.MEMBER_SPOOL_BYTES` | `uripath.schemes.MEMBER_SPOOL_BYTES` |
|
|
128
|
+
| `schemes.dav.DavPath` | `uripath.schemes.DAVURIPath` |
|
|
129
|
+
| `schemes.github.GitHubPath`, `schemes.gitlab.GitLabPath`, `schemes.git.GitPath`, `GitHubGitPath`, `GitLabGitPath` | the distribution `repopath`: `repopath.uri.GitHubURIPath`, `GitLabURIPath` and `RepoURIPath` (the `repo:` scheme); `repopath.GitHubPath` and `GitLabPath` need no URI |
|
|
130
|
+
| `schemes.github.RepoBackend`; `schemes._gitrepo.BaseRepoBackend` | `repopath.RepoBackend`, `BaseRepoBackend` |
|
|
131
|
+
| `schemes.github.InsecureTransportWarning` (also in `schemes.gitlab`) | `repopath.InsecureTransportWarning` |
|
|
132
|
+
| `schemes.sftp`, `schemes.s3`, `schemes.gs`, `schemes.az` and the classes in them | the distributions `sftppath` and `objpath`, which name their own classes |
|
|
133
|
+
| `schemes.http.RequestsTransport` | the distribution `pathapi-requests` |
|
|
134
|
+
| `pathlib_next.tools.uripath`: `main(argv, *, stdin, stdout, stderr)`, `build_parser()` | `uripath.cli.main(argv=None)`; no parser to build |
|
|
135
|
+
| `PATHLIB_NEXT_SFTP_BACKEND` | `SFTPPATH_BACKEND`, read by `sftppath` |
|
|
136
|
+
| the entry-point group `pathlib_next.schemes` | `uripath.schemes` |
|
|
137
|
+
| the script `uripath = pathlib_next.tools.uripath:main` | `uripath = uripath.cli:main`, which needs the `cli` extra; `python -m uripath` runs it too |
|
|
138
|
+
|
|
139
|
+
The `ftp:` backends are not `pathapi.ftp.FTPBackend` and `BaseFTPBackend`:
|
|
140
|
+
those belong to one host, while `FTPURIBackend` connects to whichever server a
|
|
141
|
+
path names.
|
|
142
|
+
|
|
143
|
+
- What to install where `pathlib-next` had an extra. `pathlib-next` 0.10 has no
|
|
144
|
+
`uri`, `http`, `sftp`, `sftp-async`, `s3`, `gs` or `az` extra.
|
|
145
|
+
|
|
146
|
+
| Was | Install now |
|
|
147
|
+
| --- | --- |
|
|
148
|
+
| `pathlib-next[uri]` | `uripath` |
|
|
149
|
+
| `pathlib-next[http]` | `uripath`; add `pathapi-requests` only for a `requests.Session` or `PATHAPI_HTTP_TRANSPORT=requests` |
|
|
150
|
+
| `pathlib-next[sftp]` | `sftppath[paramiko]` |
|
|
151
|
+
| `pathlib-next[sftp-async]` | `sftppath[asyncssh]` |
|
|
152
|
+
| `pathlib-next[s3]`, `[gs]`, `[az]` | `objpath[s3]`, `[gs]`, `[az]` |
|
|
153
|
+
| the `uripath` command (part of `pathlib-next[uri]`) | `uripath[cli]` |
|
|
154
|
+
|
|
155
|
+
The `github:` and `gitlab:` schemes of `pathlib-next[uri]` are in the
|
|
156
|
+
distribution `repopath` (`pip install "repopath[uri]"`), together with
|
|
157
|
+
`repo:`, which takes the place of `git:`. `git:`, `git+github:` and
|
|
158
|
+
`git+gitlab:` do not exist any more: a URI of one of them is a path of an
|
|
159
|
+
unknown scheme, which joins and prints and raises `NotImplementedError` for
|
|
160
|
+
a request. Rewrite the URIs:
|
|
161
|
+
|
|
162
|
+
| Was | Is now |
|
|
163
|
+
| --- | --- |
|
|
164
|
+
| `git://github.com/o/r/f`, `git://gitlab.com/o/r/f` | `repo://github.com/o/r/f`, `repo://gitlab.com/o/r/f` (the kind is detected from the host) |
|
|
165
|
+
| `git+github://host/o/r/f` | `github://host/o/r/f` |
|
|
166
|
+
| `git+gitlab://host/o/r/f` | `gitlab://host/o/r/f` |
|
|
167
|
+
| `github://host/o/r/f`, `gitlab://host/o/r/f` | the same text, with `repopath[uri]` installed |
|
|
168
|
+
|
|
169
|
+
`uritools` is no longer installed with the URI layer; `netimps` is a required
|
|
170
|
+
dependency.
|
|
171
|
+
|
|
172
|
+
- `Query` is an immutable value and no longer a subclass of `str`. Was: a `str`
|
|
173
|
+
that also took pairs, so `len(query)`, `query[0]`, `"x" in query`,
|
|
174
|
+
`query.split("&")` and `query == "a=1"` worked as on a string while iterating
|
|
175
|
+
it gave decoded pairs; equality and hash were those of the text, so
|
|
176
|
+
`Query("a=1;b=2", separator=";") == Query("a=1;b=2")`; `Query(query)` reset the
|
|
177
|
+
separator; `Query("a=%E9").decode()` raised `UnicodeDecodeError`. Is: `str(query)`
|
|
178
|
+
is the text, `Query.parse(text)` builds one from text (kept byte for byte, as
|
|
179
|
+
`URI.query` holds it), `Query(pairs)` from a mapping or pairs, and
|
|
180
|
+
`query.to_pairs()` and `query.to_dict()` give the pairs; two queries are equal
|
|
181
|
+
when they decode to the same pairs, a query is equal to nothing that is not a
|
|
182
|
+
query, and an escape that is not valid UTF-8 survives as a surrogate escape
|
|
183
|
+
instead of raising. A query has no length, items or iteration. What to do:
|
|
184
|
+
`Query("a=1")` becomes `Query.parse("a=1")` (it is a `TypeError` now),
|
|
185
|
+
`query.decode()` becomes `query.to_pairs()`, `for pair in query` becomes
|
|
186
|
+
`for pair in query.to_pairs()`, and a comparison of `uri.query` with text
|
|
187
|
+
becomes `str(uri.query) == text`. `URI.query` is still a `Query` and
|
|
188
|
+
`with_query()` still takes text, a mapping, pairs or a `Query`.
|
|
189
|
+
- `Source` follows the conversion names. Was: `Source.from_str(text, strict)`,
|
|
190
|
+
`source.as_str(sanitize=True)` (the same text as `str(source)`) and
|
|
191
|
+
`source.as_str(sanitize=False)` (the text with the password). Is:
|
|
192
|
+
`Source.parse(text, *, strict=True)`, `Source.try_parse(text, default=None,
|
|
193
|
+
*, strict=True)` (the default instead of the error, and still a `TypeError`
|
|
194
|
+
for what is not text), `str(source)` (the canonical text, without the
|
|
195
|
+
password) and `source.reveal()` (the text with the password, named as an
|
|
196
|
+
action because it carries the secret). `strict` is keyword-only. What to do:
|
|
197
|
+
replace the call. `Source` is still a named tuple, so positional
|
|
198
|
+
construction and unpacking are unchanged.
|
|
199
|
+
- A join keeps the query and the fragment of the path it starts from. Was:
|
|
200
|
+
`Uri("unk://h/d?sig=abc#frag") / "f"` and `joinpath("f")` gave
|
|
201
|
+
`unk://h/d/f`, and so did the children `iterdir()`, `glob()` and `walk()`
|
|
202
|
+
yield, while `parent`, `with_name()`, `with_stem()`, `with_suffix()` and
|
|
203
|
+
`relative_to()` kept both, so `p.parent / p.name == p` was false for a path
|
|
204
|
+
with a query. Is: every derivation that changes only the path keeps both, and
|
|
205
|
+
a token in the query follows the children (`unk://h/d/f?sig=abc#frag`). The
|
|
206
|
+
children of an `http:` or `dav:` listing are the exception: they are the
|
|
207
|
+
`href`s of the reply resolved against the directory, so they carry neither,
|
|
208
|
+
and `?sort=name`, which shapes an index, does not reach the files it lists
|
|
209
|
+
(`directory / "f.txt"` keeps it). A `str` destination of `rename()`,
|
|
210
|
+
`copy()` and `move()` is another resource and takes neither. What to do:
|
|
211
|
+
nothing, unless a caller relied on the join dropping them; call
|
|
212
|
+
`with_query(None)` and `with_fragment("")` on the result (or on the base)
|
|
213
|
+
there.
|
|
214
|
+
- `URI.with_query(None)` removes the query. Was: a `TypeError` ("'NoneType' object
|
|
215
|
+
is not iterable"). `with_fragment("")` removes the fragment as before.
|
|
216
|
+
- `file:` URIs keep an empty authority, and `==` reads a `str` operand as a
|
|
217
|
+
URI. Was: `Uri("file:///etc/passwd")` was written `file:/etc/passwd` and
|
|
218
|
+
`UriPath(LocalPath("C:/Windows/win.ini")).as_uri()` was
|
|
219
|
+
`file:/C:/Windows/win.ini`, where `pathlib.Path.as_uri()` gives
|
|
220
|
+
`file:///C:/Windows/win.ini`; `Uri("file:///etc/passwd") ==
|
|
221
|
+
"file:///etc/passwd"` was false, and so were `Uri("HTTP://H/x") ==
|
|
222
|
+
"HTTP://H/x"` and `Uri("http://h/a b") == "http://h/a b"`, because the text
|
|
223
|
+
was compared as written. Is: an empty authority (`file:///x`,
|
|
224
|
+
`Source.host == ""`) and none (`file:/x`, `Source.host is None`) are
|
|
225
|
+
different URIs, each written as it was read; a path built from a local path
|
|
226
|
+
object is written as `pathlib.Path.as_uri()` writes it (the characters
|
|
227
|
+
`!$&'()*+,;=:@` stay literal in the path, where `pathlib` escapes them); and
|
|
228
|
+
`==` parses a `str` operand before comparing it, so a URI equals any text
|
|
229
|
+
that names it, and text that is not a URI is not equal to anything. What to
|
|
230
|
+
do: compare URIs with `==` instead of comparing their text, and expect
|
|
231
|
+
`file:///` where code tested `file:/`. A `Source` built by hand with host
|
|
232
|
+
`""` now means an empty authority (`file://`): use `None` for none.
|
|
233
|
+
- The path of a `data:` URI is its payload, and `read_text()` uses the
|
|
234
|
+
declared charset. Was: `name`, `parent`, `with_name()` and `/` cut the
|
|
235
|
+
payload at a `/` (`data:text/plain;charset=utf-8,%C3%BCber` had the name
|
|
236
|
+
`plain;charset=utf-8,über` and `d / "x"` was the other file
|
|
237
|
+
`...%C3%BCber/x`), and `read_text()` decoded with the locale (`"über"` on
|
|
238
|
+
Windows). Is: those derivations (`name`, `stem`, `suffix`, `suffixes`,
|
|
239
|
+
`parent`, `parents`, `segments`, `with_name()`, `with_stem()`,
|
|
240
|
+
`with_suffix()`, `with_segments()`, `/`, `joinpath()`, `relative_to()`,
|
|
241
|
+
`is_relative_to()`, `match()` and a constructor given several parts) raise
|
|
242
|
+
`NotImplementedError`, and `open()` in text mode and `read_text()` decode
|
|
243
|
+
with the charset the URI declares (`DataURIPath.charset`, `"utf-8"` when it
|
|
244
|
+
declares none). What to do: name a destination yourself when you copy a
|
|
245
|
+
`data:` URI into a directory, pass `encoding=` to override the charset, and
|
|
246
|
+
read the payload with `read_bytes()` instead of slicing the path.
|
|
247
|
+
- A write stream whose `with` block raised is not committed. Was: leaving a
|
|
248
|
+
`with path.open("wb")` block through an exception closed the `http:` or
|
|
249
|
+
`dav:` stream like a normal exit, so the partial buffer was sent with `PUT`
|
|
250
|
+
and replaced the complete file (`write(b"tru")` then an error left
|
|
251
|
+
`good.bin` holding `tru`). Is: any exception (a `KeyboardInterrupt` too)
|
|
252
|
+
makes the block discard the stream, in binary and in text mode, and nothing
|
|
253
|
+
is sent; the file keeps its content or is not created. `close()` outside a
|
|
254
|
+
block and a block that ended normally send once, as before. The same holds
|
|
255
|
+
for `ftp:` and `zip:`, whose streams are the `pathapi` ones. What to do:
|
|
256
|
+
nothing, unless code relied on the partial upload; a local file still keeps
|
|
257
|
+
what a failed block wrote, as `pathlib` does.
|
|
258
|
+
- A host keeps the spelling it was given. Was: the parser and the composer
|
|
259
|
+
folded a host name to lower case, so `Source.parse("wss://root:pw@nasA:8443").host`
|
|
260
|
+
was `nasa` and `str(Uri("wss://root:pw@nasA:8443/api"))` was
|
|
261
|
+
`wss://root@nasa:8443/api`. Is: `Source.host` and the text keep `nasA`, and
|
|
262
|
+
everything that compares hosts or keys on one folds the case itself: `==`,
|
|
263
|
+
`hash()` and ordering of URIs, `same_authority()`, `is_relative_to()`,
|
|
264
|
+
backend sharing, `rename()`, the same-file decisions of `copy()` and
|
|
265
|
+
`move()`, the redirect origin that keeps `Authorization`, the proxy rules,
|
|
266
|
+
`Source.is_local()` and the connection cache of `ftp:`. `http://NASA/x` and `http://nasa/x` are one
|
|
267
|
+
server and equal URIs; their text differs. Two `Source` values compare as
|
|
268
|
+
tuples, exactly; `compose_host()` and `compose_uri()` keep the case of a
|
|
269
|
+
name; an internationalized name is still written in IDNA form. What to do:
|
|
270
|
+
compare hosts with `uripath.source.same_authority()` or
|
|
271
|
+
`uripath.source.fold_host()` instead of `source.host == "name"`, and key
|
|
272
|
+
anything you keep per server on `fold_host(source.host)`.
|
|
273
|
+
- A constructor raises `TypeError` for a keyword argument it does not name.
|
|
274
|
+
Was: `URI("x", anything=1)` and `URIPath("https://h/x", backnd=backend)`
|
|
275
|
+
were accepted and the keyword ignored, so a misspelt `backend=` meant "use
|
|
276
|
+
the default backend" (and the ambient credentials). Is: `TypeError:
|
|
277
|
+
HTTPURIPath() got an unexpected keyword argument 'backnd'`. `URI` takes no
|
|
278
|
+
keyword; a path takes `backend`, `schemesmap`, `findclass` and the ones its
|
|
279
|
+
scheme class names in `_OPTIONS`; the path of a scheme that another
|
|
280
|
+
distribution implements, when that distribution is not installed, takes any.
|
|
281
|
+
What to do: correct or remove the stray keyword; a scheme class of your own
|
|
282
|
+
that reads keywords lists them (`_OPTIONS = URIPath._OPTIONS | {"flavor"}`).
|
|
283
|
+
- Text that is not a URI raises `URISyntaxError` where it raised a bare
|
|
284
|
+
`ValueError`: a port outside 0-65535, a malformed host or IPv6 address,
|
|
285
|
+
`Source.parse()` of text with a path, a `zip:` URI without the scheme of
|
|
286
|
+
its archive. It is a subclass of
|
|
287
|
+
`ValueError`, so `except ValueError` still catches it. `URIPath.scheme_class()`
|
|
288
|
+
of a scheme nobody registered raises `UnsupportedSchemeError`, also a
|
|
289
|
+
`ValueError`. Both derive from `URIPathError`; a caller's bad argument stays a
|
|
290
|
+
plain `ValueError` or `TypeError`. What to do: nothing; catch `URIPathError`
|
|
291
|
+
to catch what the package raises on its own.
|
|
292
|
+
- Requests of `http:` and `dav:` send
|
|
293
|
+
`User-Agent: uripath/<version>` instead of `User-Agent: pathlib-next`; the
|
|
294
|
+
version is `uripath.__version__`. A backend's `headers=` still replaces it.
|
|
295
|
+
- A plugin registered in the `pathlib_next.schemes` entry-point group is not
|
|
296
|
+
found. What to do: register it in `uripath.schemes`.
|
|
297
|
+
- The `requests` HTTP transport is provided by `pathapi-requests`.
|
|
298
|
+
`HTTPURIPath.with_session()` and `PATHAPI_HTTP_TRANSPORT=requests` raise an `ImportError` naming that
|
|
299
|
+
distribution without it, and a proxy URL the built-in transport cannot use
|
|
300
|
+
(`socks5:`, `https:`) names it in its error too. What to do: install
|
|
301
|
+
`pathapi-requests` where one of them is used; with it installed they
|
|
302
|
+
send through its transport and nothing else is configured.
|
|
303
|
+
- `uritools` is not a dependency. The RFC 3986 split of a URI and its
|
|
304
|
+
percent-encoding and -decoding are done on `urllib.parse`, with the results
|
|
305
|
+
`uritools` gave for the same text: the same safe sets, invalid escapes left as
|
|
306
|
+
they are, non-ASCII text kept, a lone surrogate or an escape that is not UTF-8
|
|
307
|
+
carried through `surrogateescape`, an empty authority told from none.
|
|
308
|
+
`import uripath` loads neither `uritools` nor `netimps`; `netimps` is
|
|
309
|
+
imported where it is used. What to do: code that imports `uritools` itself
|
|
310
|
+
installs it.
|
|
311
|
+
- The `uripath` command is rebuilt on `duho` and needs the `cli` extra:
|
|
312
|
+
`pip install "uripath[cli]"`. Was: part of the base install. Without the
|
|
313
|
+
extra the command (and `python -m uripath`) prints which extra to install on
|
|
314
|
+
stderr and exits with status 1. The subcommands, their options and meanings,
|
|
315
|
+
`-` for stdin and stdout, the exit statuses (0, 1 the operation failed, 2 a
|
|
316
|
+
wrong invocation, 130 Ctrl-C, 141 the reader of stdout went away) and the
|
|
317
|
+
`uripath: <Type>: <message>` line on stderr are as they were. What differs:
|
|
318
|
+
- `uripath.cli.main(argv=None)` returns the status and reads and writes the
|
|
319
|
+
process's standard streams. Was: `main(argv, *, stdin=, stdout=, stderr=)`
|
|
320
|
+
with injectable streams, and `build_parser()` returning an `argparse`
|
|
321
|
+
parser. Neither exists any more; build a command with `duho.parse` to test
|
|
322
|
+
one.
|
|
323
|
+
- Usage, `--help` and the wording of a usage error are `duho`'s, not
|
|
324
|
+
`argparse`'s; a wrong invocation still exits with status 2.
|
|
325
|
+
- Every command takes `-v`, `-q` and `--loglevel`, which set the level of the
|
|
326
|
+
`uripath` logger. `sync -v`, which listed the changes made, is now a count
|
|
327
|
+
(`-vv` is accepted) and lists them while `-v` is given more often than
|
|
328
|
+
`-q`.
|
|
329
|
+
- A boolean option that is on by default has both spellings:
|
|
330
|
+
`--follow-symlinks` beside `--no-follow-symlinks`, `--preserve-metadata`
|
|
331
|
+
beside `--no-preserve-metadata`, `--follow-mounts` beside
|
|
332
|
+
`--no-follow-mounts`.
|
|
333
|
+
- `--` ends the options and accepts no argument after it. Was: `argparse`
|
|
334
|
+
took what followed as positional arguments, so `uripath rm -- -name`
|
|
335
|
+
removed `-name`; write `uripath rm ./-name`.
|
|
336
|
+
- An `http:` or `dav:` write stream that is garbage collected without being closed sends
|
|
337
|
+
nothing and emits a `ResourceWarning`; `pathlib-next` closed it, and so uploaded it.
|
|
338
|
+
What to do: close the file, or use `with`.
|
|
339
|
+
|
|
340
|
+
[Unreleased]: https://github.com/jose-pr/uripath/compare/v0.1.0...HEAD
|
|
341
|
+
[0.1.0]: https://github.com/jose-pr/uripath/releases/tag/v0.1.0
|
|
@@ -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.
|
|
@@ -0,0 +1,259 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: pathapi-uri
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: pathlib-style paths for URIs: file:, data:, ftp:, zip:, tar:, http:, dav:
|
|
5
|
+
Project-URL: Homepage, https://github.com/jose-pr/uripath/
|
|
6
|
+
Project-URL: Documentation, https://jose-pr.github.io/uripath/
|
|
7
|
+
Project-URL: Issues, https://github.com/jose-pr/uripath/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: netimps<0.5,>=0.4.0
|
|
23
|
+
Requires-Dist: pathapi<0.2,>=0.1.0
|
|
24
|
+
Provides-Extra: cli
|
|
25
|
+
Requires-Dist: duho<0.8,>=0.7.3; extra == 'cli'
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: black; extra == 'dev'
|
|
28
|
+
Requires-Dist: build; extra == 'dev'
|
|
29
|
+
Requires-Dist: cheroot; extra == 'dev'
|
|
30
|
+
Requires-Dist: cryptography; extra == 'dev'
|
|
31
|
+
Requires-Dist: duho<0.8,>=0.7.3; extra == 'dev'
|
|
32
|
+
Requires-Dist: hatchling; extra == 'dev'
|
|
33
|
+
Requires-Dist: hypothesis; extra == 'dev'
|
|
34
|
+
Requires-Dist: pyftpdlib; extra == 'dev'
|
|
35
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
36
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
37
|
+
Requires-Dist: requests; extra == 'dev'
|
|
38
|
+
Requires-Dist: twine; extra == 'dev'
|
|
39
|
+
Requires-Dist: uritools<7,>=5.0; extra == 'dev'
|
|
40
|
+
Requires-Dist: wsgidav; extra == 'dev'
|
|
41
|
+
Provides-Extra: docs
|
|
42
|
+
Requires-Dist: mkdocs; extra == 'docs'
|
|
43
|
+
Requires-Dist: mkdocs-material; extra == 'docs'
|
|
44
|
+
Requires-Dist: mkdocstrings[python]; extra == 'docs'
|
|
45
|
+
Description-Content-Type: text/markdown
|
|
46
|
+
|
|
47
|
+
# uripath
|
|
48
|
+
|
|
49
|
+
[](https://pypi.org/project/pathapi-uri/)
|
|
50
|
+
[](https://pypi.org/project/pathapi-uri/)
|
|
51
|
+
[](https://github.com/jose-pr/uripath/blob/main/LICENSE)
|
|
52
|
+
[](https://jose-pr.github.io/uripath/)
|
|
53
|
+
[](https://github.com/jose-pr/uripath/actions/workflows/test.yml)
|
|
54
|
+
|
|
55
|
+
Installed as `pathapi-uri` from PyPI; imported, and run as a command, as `uripath`.
|
|
56
|
+
|
|
57
|
+
**One `pathlib`-style API for URIs.** `URIPath("https://...")`, `URIPath("zip:...")`
|
|
58
|
+
and `URIPath("ftp://...")` return the class registered for the scheme, and each
|
|
59
|
+
has the method names, signatures and exception types of `pathlib.Path`: write
|
|
60
|
+
code once and run it against a local file, an archive member, an FTP or WebDAV
|
|
61
|
+
server, a web directory or, through the `repopath` distribution, a GitHub or GitLab
|
|
62
|
+
repository. It builds on
|
|
63
|
+
[`pathapi`](https://github.com/jose-pr/pathapi) and is documented at
|
|
64
|
+
<https://jose-pr.github.io/uripath/>.
|
|
65
|
+
|
|
66
|
+
## Features
|
|
67
|
+
|
|
68
|
+
| Scheme (class) | Needs | Read | Write | List | Stat | mkdir | Delete | rename |
|
|
69
|
+
| --- | --- | --- | --- | --- | --- | --- | --- | --- |
|
|
70
|
+
| `file:` (`FileURIPath`) | nothing | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
|
71
|
+
| `data:` (`DataURIPath`, RFC 2397) | nothing | Yes | No | No | Yes | No | No | No |
|
|
72
|
+
| `zip:` (`ZipURIPath`) | nothing | Yes | Local `file:` archive only | Yes | Yes | Local archive only | Local archive only | Local archive only |
|
|
73
|
+
| `tar:` (`TarURIPath`, incl. gz/bz2/xz) | nothing | Yes | No | Yes | Yes | No | No | No |
|
|
74
|
+
| `archive:`, `archive+zip:`, `archive+tar:` | nothing | As `zip:`/`tar:` for the detected or pinned format | | | | | | |
|
|
75
|
+
| `ftp:` / `ftps:` (`FTPURIPath`) | nothing | Yes | Yes | Yes (MLSD, NLST fallback) | Yes | Yes | Yes | Yes |
|
|
76
|
+
| `http:` / `https:` (`HTTPURIPath`) | nothing | Yes | Yes (PUT, configurable) | Yes (HTML index) | Yes | No | Yes (DELETE) | No |
|
|
77
|
+
| `dav:` / `davs:` (`DAVURIPath`, WebDAV) | nothing | Yes | Yes (no append) | Yes (PROPFIND) | Yes | Yes | Yes | Yes |
|
|
78
|
+
| `sftp:` | `sftppath[paramiko]` or `[asyncssh]` | Yes | Yes | Yes | Yes | Yes | Yes | Yes |
|
|
79
|
+
| `s3:` | `objpath[s3]` | Yes | Yes (no append) | Yes (prefixes) | Yes | Yes (marker object) | Yes | Yes (same bucket) |
|
|
80
|
+
| `gs:` | `objpath[gs]` | Yes | Yes (no append) | Yes (prefixes) | Yes | Yes (marker object) | Yes | Yes (same bucket) |
|
|
81
|
+
| `az:` | `objpath[az]` | Yes | Yes (no append) | Yes (prefixes) | Yes | Yes (marker blob) | Yes | Yes (same container) |
|
|
82
|
+
| `repo:` | `repopath` | Yes | No | Yes | Yes | No | No | No |
|
|
83
|
+
| `github:` | `repopath` | Yes | No | Yes | Yes | No | No | No |
|
|
84
|
+
| `gitlab:` | `repopath` | Yes | No | Yes | Yes | No | No | No |
|
|
85
|
+
|
|
86
|
+
The last seven rows are other distributions: a path of one of those schemes
|
|
87
|
+
builds, joins and prints without it, and its first use of the server raises an
|
|
88
|
+
`ImportError` that names what to install. The `requests` HTTP transport
|
|
89
|
+
(`with_session()`, `~/.netrc`, SOCKS proxies) is the distribution
|
|
90
|
+
`pathapi-requests`: once it is installed, `with_session()` and
|
|
91
|
+
`PATHAPI_HTTP_TRANSPORT=requests` use it, with nothing to configure.
|
|
92
|
+
|
|
93
|
+
Every scheme gets `glob()`, `walk()`, `copy()`/`move()`, `rm()` and `PathSyncer`
|
|
94
|
+
from the `pathapi` base classes. The per-scheme notes (timeouts,
|
|
95
|
+
certificate verification, authentication, limits) are in
|
|
96
|
+
[Schemes](https://jose-pr.github.io/uripath/guides/schemes/).
|
|
97
|
+
|
|
98
|
+
- **Pure URI algebra**: `URI` parses, joins and compares URIs with no I/O and
|
|
99
|
+
no scheme class needed; `Source` and `Query` are its authority and query.
|
|
100
|
+
- **HTTP on the standard library**: `http:` and `dav:` send through a
|
|
101
|
+
replaceable transport built on `http.client`.
|
|
102
|
+
- **Extensible**: subclass `URIPath` and declare `__SCHEMES` for a new scheme,
|
|
103
|
+
or register it through a `uripath.schemes` entry point.
|
|
104
|
+
- **`uripath` command**: `read`, `write`, `cp`, `rm` and `sync` across local
|
|
105
|
+
paths and URIs, with `-` for stdin/stdout (`pip install "pathapi-uri[cli]"`).
|
|
106
|
+
|
|
107
|
+
## Installation
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
pip install pathapi-uri
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
Requires `pathapi` and `netimps`, which pip installs with it.
|
|
114
|
+
|
|
115
|
+
| Install | Adds |
|
|
116
|
+
| --- | --- |
|
|
117
|
+
| `pip install pathapi-uri` | every scheme whose Needs is nothing in the table above |
|
|
118
|
+
| `pip install "pathapi-uri[cli]"` | the `uripath` command (its only extra) |
|
|
119
|
+
| `pip install "sftppath[paramiko]"` or `[asyncssh]` | `sftp:` |
|
|
120
|
+
| `pip install "objpath[s3]"`, `[gs]` or `[az]` | `s3:`, `gs:`, `az:` |
|
|
121
|
+
| `pip install pathapi-requests` | the `requests` HTTP transport |
|
|
122
|
+
|
|
123
|
+
## Quick start
|
|
124
|
+
|
|
125
|
+
**Any URI**: `URIPath` returns the class registered for the scheme:
|
|
126
|
+
|
|
127
|
+
```python
|
|
128
|
+
from uripath import URIPath
|
|
129
|
+
|
|
130
|
+
report = URIPath("file:data/report.txt") # FileURIPath
|
|
131
|
+
report.parent.mkdir(parents=True, exist_ok=True)
|
|
132
|
+
report.write_text("hello")
|
|
133
|
+
print(report.read_text())
|
|
134
|
+
print(URIPath("data:text/plain;base64,aGVsbG8=").read_text()) # DataURIPath
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
**Archive members**: `<scheme>:<archive-uri>!/<member>`, where the archive
|
|
138
|
+
itself is any URI (`file:`, `http:`, `ftp:`, ...):
|
|
139
|
+
|
|
140
|
+
```python
|
|
141
|
+
import zipfile
|
|
142
|
+
from uripath import URIPath
|
|
143
|
+
|
|
144
|
+
with zipfile.ZipFile("backup.zip", "w") as archive:
|
|
145
|
+
archive.writestr("etc/config.ini", "[main]\n")
|
|
146
|
+
|
|
147
|
+
member = URIPath("zip:file:backup.zip!/etc/config.ini")
|
|
148
|
+
print(member.read_text())
|
|
149
|
+
print([child.name for child in member.parent.iterdir()])
|
|
150
|
+
```
|
|
151
|
+
|
|
152
|
+
**Remote paths** use the same API:
|
|
153
|
+
|
|
154
|
+
<!-- example: skip: reads from remote hosts -->
|
|
155
|
+
```python
|
|
156
|
+
from uripath import URIPath
|
|
157
|
+
|
|
158
|
+
for child in URIPath("https://example.com/data/").iterdir():
|
|
159
|
+
print(child.name, child.stat().st_size)
|
|
160
|
+
|
|
161
|
+
print(URIPath("ftp://user@host/pub/README").read_text())
|
|
162
|
+
```
|
|
163
|
+
|
|
164
|
+
**Copy and sync across schemes**, with the `pathapi` syncer:
|
|
165
|
+
|
|
166
|
+
```python
|
|
167
|
+
from pathapi.sync import PathSyncer
|
|
168
|
+
from uripath import URIPath
|
|
169
|
+
|
|
170
|
+
source = URIPath("file:site")
|
|
171
|
+
(source / "css").mkdir(parents=True)
|
|
172
|
+
(source / "index.html").write_text("<h1>hi</h1>")
|
|
173
|
+
|
|
174
|
+
PathSyncer(remove_missing=True).sync(source, URIPath("file:site-copy"))
|
|
175
|
+
print(sorted(p.name for p in URIPath("file:site-copy").iterdir()))
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
## Command line
|
|
179
|
+
|
|
180
|
+
`uripath` reads, writes, copies, removes and syncs local paths and URIs, with
|
|
181
|
+
`-` for stdin or stdout. It is built on `duho` and needs the `cli` extra,
|
|
182
|
+
`pip install "pathapi-uri[cli]"`; without it the command says so and exits with
|
|
183
|
+
status 1. These commands run as written in an empty directory:
|
|
184
|
+
|
|
185
|
+
```bash
|
|
186
|
+
mkdir site
|
|
187
|
+
uripath write site/index.html "<h1>hi</h1>"
|
|
188
|
+
uripath read site/index.html
|
|
189
|
+
uripath cp site/index.html site/copy.html
|
|
190
|
+
uripath sync --dry-run --remove-missing site site-copy
|
|
191
|
+
uripath sync site site-copy
|
|
192
|
+
uripath rm --recursive site-copy
|
|
193
|
+
```
|
|
194
|
+
|
|
195
|
+
Either side can be a URI instead:
|
|
196
|
+
|
|
197
|
+
<!-- example: skip: needs an FTP host and a web server -->
|
|
198
|
+
```bash
|
|
199
|
+
uripath cp report.txt ftp://host/tmp/report.txt
|
|
200
|
+
uripath sync --dry-run --remove-missing site/ ftp://host/site/
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
The exit status is 0 on success, 1 when the operation failed, 2 for a wrong
|
|
204
|
+
invocation, 130 on Ctrl-C and 141 when the reader of stdout went away. Every
|
|
205
|
+
flag is described in the
|
|
206
|
+
[CLI guide](https://jose-pr.github.io/uripath/guides/cli/), and `uripath` can
|
|
207
|
+
be served to an agent as a set of tools.
|
|
208
|
+
|
|
209
|
+
## API overview
|
|
210
|
+
|
|
211
|
+
| Module | Purpose |
|
|
212
|
+
| --- | --- |
|
|
213
|
+
| `uripath` | `URI`, `URIPath`, `URILike`, `Source`, `Query`, `QueryLike`, the functions `as_path` and `copy_stream`, the errors `URIPathError`, `URISyntaxError` and `UnsupportedSchemeError`, and `__version__` |
|
|
214
|
+
| `uripath.source` | `Source` (scheme, userinfo, host, port), `same_authority` and the helpers that read and compose a URI's text |
|
|
215
|
+
| `uripath.query` | `Query`, an immutable query value, and `QueryLike` |
|
|
216
|
+
| `uripath.exceptions` | The errors above |
|
|
217
|
+
| `uripath.schemes` | The contract and registry of schemes, `DerivedBackend`, `DelegatingURIPath`, and every built-in scheme's classes: `FileURIPath`, `DataURIPath`, `ZipURIPath`, `TarURIPath`, `ArchiveURIPath`, `FTPURIPath`, `HTTPURIPath`, `DAVURIPath` and their backends |
|
|
218
|
+
|
|
219
|
+
The full reference is on the [documentation site](https://jose-pr.github.io/uripath/);
|
|
220
|
+
an agent-oriented API summary ships inside the package as `uripath/AGENTS.md`.
|
|
221
|
+
|
|
222
|
+
## Extending
|
|
223
|
+
|
|
224
|
+
Subclass `URIPath` and declare `__SCHEMES` for a new URI scheme; it is
|
|
225
|
+
dispatched as soon as its module is imported, or through a `uripath.schemes`
|
|
226
|
+
entry point. A scheme name belongs to one class: a second claim raises unless
|
|
227
|
+
the class statement says `replace=True`, and `URIPath.schemes()` /
|
|
228
|
+
`URIPath.scheme_class(name)` read the registry. Worked examples:
|
|
229
|
+
[Extending](https://jose-pr.github.io/uripath/guides/extending/). The pytest
|
|
230
|
+
contracts that verify a scheme are in `pathapi.testing`.
|
|
231
|
+
|
|
232
|
+
## Development
|
|
233
|
+
|
|
234
|
+
```bash
|
|
235
|
+
pip install -e ".[dev]"
|
|
236
|
+
python -m pytest -q -rs
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
Tests for a server that is not installed are skipped, so check the skip list
|
|
240
|
+
(`-rs`). Contributor notes (virtual environments, formatting, CI, releases) are
|
|
241
|
+
in the repository's
|
|
242
|
+
[`AGENTS.md`](https://github.com/jose-pr/uripath/blob/main/AGENTS.md). To
|
|
243
|
+
preview the documentation: `pip install -e ".[docs]"` then `mkdocs serve`.
|
|
244
|
+
|
|
245
|
+
Changes are recorded in the
|
|
246
|
+
[changelog](https://github.com/jose-pr/uripath/blob/main/CHANGELOG.md).
|
|
247
|
+
|
|
248
|
+
### Releasing
|
|
249
|
+
|
|
250
|
+
This project follows [Semantic Versioning](https://semver.org/) and keeps a
|
|
251
|
+
[`CHANGELOG.md`](https://github.com/jose-pr/uripath/blob/main/CHANGELOG.md). Pushing a tag matching `v*` triggers the release
|
|
252
|
+
workflow: test gate → build (checking the tag names the version built) → a strict
|
|
253
|
+
docs build as a gate → GitHub release → publish. The release workflow never deploys
|
|
254
|
+
the docs site itself: for a final release its last job dispatches the docs workflow
|
|
255
|
+
at the tag, which owns every Pages deploy.
|
|
256
|
+
|
|
257
|
+
## License
|
|
258
|
+
|
|
259
|
+
MIT — see [LICENSE](https://github.com/jose-pr/uripath/blob/main/LICENSE).
|