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.
Files changed (189) hide show
  1. pathapi_uri-0.1.0/.gitignore +41 -0
  2. pathapi_uri-0.1.0/CHANGELOG.md +341 -0
  3. pathapi_uri-0.1.0/LICENSE +21 -0
  4. pathapi_uri-0.1.0/PKG-INFO +259 -0
  5. pathapi_uri-0.1.0/README.md +213 -0
  6. pathapi_uri-0.1.0/docs/api/cli.md +11 -0
  7. pathapi_uri-0.1.0/docs/api/exceptions.md +10 -0
  8. pathapi_uri-0.1.0/docs/api/schemes/archive.md +15 -0
  9. pathapi_uri-0.1.0/docs/api/schemes/ftp.md +7 -0
  10. pathapi_uri-0.1.0/docs/api/schemes/http.md +22 -0
  11. pathapi_uri-0.1.0/docs/api/schemes/local.md +8 -0
  12. pathapi_uri-0.1.0/docs/api/uri.md +23 -0
  13. pathapi_uri-0.1.0/docs/benchmarks.md +47 -0
  14. pathapi_uri-0.1.0/docs/changelog.md +3 -0
  15. pathapi_uri-0.1.0/docs/divergences.md +59 -0
  16. pathapi_uri-0.1.0/docs/guides/cli.md +119 -0
  17. pathapi_uri-0.1.0/docs/guides/extending.md +276 -0
  18. pathapi_uri-0.1.0/docs/guides/schemes.md +279 -0
  19. pathapi_uri-0.1.0/docs/index.md +93 -0
  20. pathapi_uri-0.1.0/examples/data_and_archive.py +98 -0
  21. pathapi_uri-0.1.0/examples/ftp_listing.py +61 -0
  22. pathapi_uri-0.1.0/examples/http_listing.py +44 -0
  23. pathapi_uri-0.1.0/examples/webdav_roundtrip.py +82 -0
  24. pathapi_uri-0.1.0/mkdocs.yml +64 -0
  25. pathapi_uri-0.1.0/pyproject.toml +111 -0
  26. pathapi_uri-0.1.0/src/uripath/AGENTS.md +1094 -0
  27. pathapi_uri-0.1.0/src/uripath/__init__.py +27 -0
  28. pathapi_uri-0.1.0/src/uripath/__main__.py +7 -0
  29. pathapi_uri-0.1.0/src/uripath/_pathtext.py +32 -0
  30. pathapi_uri-0.1.0/src/uripath/_rfc3986.py +67 -0
  31. pathapi_uri-0.1.0/src/uripath/_transfer.py +108 -0
  32. pathapi_uri-0.1.0/src/uripath/_uri.py +1007 -0
  33. pathapi_uri-0.1.0/src/uripath/_version.py +11 -0
  34. pathapi_uri-0.1.0/src/uripath/cli/__init__.py +54 -0
  35. pathapi_uri-0.1.0/src/uripath/cli/_common.py +48 -0
  36. pathapi_uri-0.1.0/src/uripath/cli/_root.py +70 -0
  37. pathapi_uri-0.1.0/src/uripath/cli/cp.py +61 -0
  38. pathapi_uri-0.1.0/src/uripath/cli/read.py +22 -0
  39. pathapi_uri-0.1.0/src/uripath/cli/rm.py +38 -0
  40. pathapi_uri-0.1.0/src/uripath/cli/sync.py +78 -0
  41. pathapi_uri-0.1.0/src/uripath/cli/write.py +49 -0
  42. pathapi_uri-0.1.0/src/uripath/exceptions.py +38 -0
  43. pathapi_uri-0.1.0/src/uripath/py.typed +1 -0
  44. pathapi_uri-0.1.0/src/uripath/query.py +240 -0
  45. pathapi_uri-0.1.0/src/uripath/schemes/__init__.py +120 -0
  46. pathapi_uri-0.1.0/src/uripath/schemes/_archive.py +338 -0
  47. pathapi_uri-0.1.0/src/uripath/schemes/_data.py +99 -0
  48. pathapi_uri-0.1.0/src/uripath/schemes/_dav.py +459 -0
  49. pathapi_uri-0.1.0/src/uripath/schemes/_delegate.py +127 -0
  50. pathapi_uri-0.1.0/src/uripath/schemes/_elsewhere.py +102 -0
  51. pathapi_uri-0.1.0/src/uripath/schemes/_file.py +186 -0
  52. pathapi_uri-0.1.0/src/uripath/schemes/_ftp.py +200 -0
  53. pathapi_uri-0.1.0/src/uripath/schemes/_http/__init__.py +4 -0
  54. pathapi_uri-0.1.0/src/uripath/schemes/_http/_backend.py +132 -0
  55. pathapi_uri-0.1.0/src/uripath/schemes/_http/_headers.py +8 -0
  56. pathapi_uri-0.1.0/src/uripath/schemes/_http/_listing.py +403 -0
  57. pathapi_uri-0.1.0/src/uripath/schemes/_http/_path.py +371 -0
  58. pathapi_uri-0.1.0/src/uripath/schemes/_http/_settings.py +6 -0
  59. pathapi_uri-0.1.0/src/uripath/schemes/_http/_streams.py +230 -0
  60. pathapi_uri-0.1.0/src/uripath/schemes/_httpdate.py +48 -0
  61. pathapi_uri-0.1.0/src/uripath/schemes/_path.py +629 -0
  62. pathapi_uri-0.1.0/src/uripath/schemes/_registry.py +134 -0
  63. pathapi_uri-0.1.0/src/uripath/source/__init__.py +20 -0
  64. pathapi_uri-0.1.0/src/uripath/source/_compose.py +212 -0
  65. pathapi_uri-0.1.0/src/uripath/source/_locality.py +44 -0
  66. pathapi_uri-0.1.0/src/uripath/source/_parse.py +183 -0
  67. pathapi_uri-0.1.0/src/uripath/source/_source.py +239 -0
  68. pathapi_uri-0.1.0/tests/capabilities.py +70 -0
  69. pathapi_uri-0.1.0/tests/census.py +122 -0
  70. pathapi_uri-0.1.0/tests/conftest.py +618 -0
  71. pathapi_uri-0.1.0/tests/fake_distribution.py +55 -0
  72. pathapi_uri-0.1.0/tests/ftp_loopback.py +286 -0
  73. pathapi_uri-0.1.0/tests/hermetic.py +261 -0
  74. pathapi_uri-0.1.0/tests/http_fakes.py +139 -0
  75. pathapi_uri-0.1.0/tests/http_wire.py +498 -0
  76. pathapi_uri-0.1.0/tests/mutation/README.md +66 -0
  77. pathapi_uri-0.1.0/tests/mutation/mutants.py +569 -0
  78. pathapi_uri-0.1.0/tests/mutation/mutation_check.py +16 -0
  79. pathapi_uri-0.1.0/tests/mutation/run.py +224 -0
  80. pathapi_uri-0.1.0/tests/probes/probe_environment_leak.py +12 -0
  81. pathapi_uri-0.1.0/tests/probes/probe_leaks_a_socket.py +12 -0
  82. pathapi_uri-0.1.0/tests/probes/probe_session_environment.py +36 -0
  83. pathapi_uri-0.1.0/tests/server_loops.py +43 -0
  84. pathapi_uri-0.1.0/tests/test_archive_batch.py +763 -0
  85. pathapi_uri-0.1.0/tests/test_archive_batch_cost.py +368 -0
  86. pathapi_uri-0.1.0/tests/test_archive_damage.py +274 -0
  87. pathapi_uri-0.1.0/tests/test_archive_helpers.py +214 -0
  88. pathapi_uri-0.1.0/tests/test_archive_lazy.py +262 -0
  89. pathapi_uri-0.1.0/tests/test_archive_link_listing.py +410 -0
  90. pathapi_uri-0.1.0/tests/test_archive_links.py +572 -0
  91. pathapi_uri-0.1.0/tests/test_archive_names.py +69 -0
  92. pathapi_uri-0.1.0/tests/test_archive_parity.py +400 -0
  93. pathapi_uri-0.1.0/tests/test_archive_rewrite.py +353 -0
  94. pathapi_uri-0.1.0/tests/test_archive_safety.py +868 -0
  95. pathapi_uri-0.1.0/tests/test_archive_scale.py +198 -0
  96. pathapi_uri-0.1.0/tests/test_archive_schemes.py +99 -0
  97. pathapi_uri-0.1.0/tests/test_archive_tree.py +396 -0
  98. pathapi_uri-0.1.0/tests/test_archive_uri.py +516 -0
  99. pathapi_uri-0.1.0/tests/test_as_path.py +80 -0
  100. pathapi_uri-0.1.0/tests/test_backend_family.py +331 -0
  101. pathapi_uri-0.1.0/tests/test_backend_scope.py +401 -0
  102. pathapi_uri-0.1.0/tests/test_capabilities.py +93 -0
  103. pathapi_uri-0.1.0/tests/test_census.py +129 -0
  104. pathapi_uri-0.1.0/tests/test_cli.py +683 -0
  105. pathapi_uri-0.1.0/tests/test_cli_surface.py +229 -0
  106. pathapi_uri-0.1.0/tests/test_contract.py +271 -0
  107. pathapi_uri-0.1.0/tests/test_copy_stream.py +235 -0
  108. pathapi_uri-0.1.0/tests/test_data_uri.py +207 -0
  109. pathapi_uri-0.1.0/tests/test_dav.py +47 -0
  110. pathapi_uri-0.1.0/tests/test_delegating_uri_path.py +104 -0
  111. pathapi_uri-0.1.0/tests/test_destructive_safety.py +298 -0
  112. pathapi_uri-0.1.0/tests/test_documentation.py +153 -0
  113. pathapi_uri-0.1.0/tests/test_documented_examples.py +204 -0
  114. pathapi_uri-0.1.0/tests/test_elsewhere.py +180 -0
  115. pathapi_uri-0.1.0/tests/test_examples_skip.py +62 -0
  116. pathapi_uri-0.1.0/tests/test_exceptions.py +92 -0
  117. pathapi_uri-0.1.0/tests/test_file_uri_paths.py +294 -0
  118. pathapi_uri-0.1.0/tests/test_file_uri_text.py +135 -0
  119. pathapi_uri-0.1.0/tests/test_ftp.py +474 -0
  120. pathapi_uri-0.1.0/tests/test_ftp_parity.py +297 -0
  121. pathapi_uri-0.1.0/tests/test_ftp_utime.py +174 -0
  122. pathapi_uri-0.1.0/tests/test_ftp_wire.py +465 -0
  123. pathapi_uri-0.1.0/tests/test_generic_pure_parity.py +185 -0
  124. pathapi_uri-0.1.0/tests/test_glob_edges.py +393 -0
  125. pathapi_uri-0.1.0/tests/test_glob_parity.py +308 -0
  126. pathapi_uri-0.1.0/tests/test_hermetic.py +344 -0
  127. pathapi_uri-0.1.0/tests/test_host_case.py +238 -0
  128. pathapi_uri-0.1.0/tests/test_http.py +571 -0
  129. pathapi_uri-0.1.0/tests/test_http_credentials.py +203 -0
  130. pathapi_uri-0.1.0/tests/test_http_date.py +70 -0
  131. pathapi_uri-0.1.0/tests/test_http_default.py +171 -0
  132. pathapi_uri-0.1.0/tests/test_http_listing.py +664 -0
  133. pathapi_uri-0.1.0/tests/test_http_live.py +131 -0
  134. pathapi_uri-0.1.0/tests/test_http_parser.py +244 -0
  135. pathapi_uri-0.1.0/tests/test_http_peer_text.py +59 -0
  136. pathapi_uri-0.1.0/tests/test_http_redirect_origin.py +151 -0
  137. pathapi_uri-0.1.0/tests/test_http_redirects.py +78 -0
  138. pathapi_uri-0.1.0/tests/test_http_reply_limits.py +164 -0
  139. pathapi_uri-0.1.0/tests/test_http_unclosed_upload.py +58 -0
  140. pathapi_uri-0.1.0/tests/test_http_writes.py +759 -0
  141. pathapi_uri-0.1.0/tests/test_httpdav_safety.py +483 -0
  142. pathapi_uri-0.1.0/tests/test_io_parity.py +105 -0
  143. pathapi_uri-0.1.0/tests/test_listing_hints.py +255 -0
  144. pathapi_uri-0.1.0/tests/test_listing_hints_http.py +156 -0
  145. pathapi_uri-0.1.0/tests/test_listing_names.py +207 -0
  146. pathapi_uri-0.1.0/tests/test_low_core.py +364 -0
  147. pathapi_uri-0.1.0/tests/test_low_schemes.py +339 -0
  148. pathapi_uri-0.1.0/tests/test_low_sync.py +122 -0
  149. pathapi_uri-0.1.0/tests/test_packaging.py +72 -0
  150. pathapi_uri-0.1.0/tests/test_path_info.py +93 -0
  151. pathapi_uri-0.1.0/tests/test_pathname.py +283 -0
  152. pathapi_uri-0.1.0/tests/test_plugins.py +156 -0
  153. pathapi_uri-0.1.0/tests/test_properties.py +913 -0
  154. pathapi_uri-0.1.0/tests/test_provider_seam.py +338 -0
  155. pathapi_uri-0.1.0/tests/test_public_surface.py +205 -0
  156. pathapi_uri-0.1.0/tests/test_pure_parity.py +590 -0
  157. pathapi_uri-0.1.0/tests/test_query.py +237 -0
  158. pathapi_uri-0.1.0/tests/test_rfc3986.py +144 -0
  159. pathapi_uri-0.1.0/tests/test_rm_overrides.py +55 -0
  160. pathapi_uri-0.1.0/tests/test_routing.py +132 -0
  161. pathapi_uri-0.1.0/tests/test_same_filesystem_uri.py +238 -0
  162. pathapi_uri-0.1.0/tests/test_scheme_distributions.py +248 -0
  163. pathapi_uri-0.1.0/tests/test_scheme_registry.py +424 -0
  164. pathapi_uri-0.1.0/tests/test_shipped_header.py +362 -0
  165. pathapi_uri-0.1.0/tests/test_smoke.py +146 -0
  166. pathapi_uri-0.1.0/tests/test_source.py +215 -0
  167. pathapi_uri-0.1.0/tests/test_source_is_local.py +119 -0
  168. pathapi_uri-0.1.0/tests/test_sync_safety.py +436 -0
  169. pathapi_uri-0.1.0/tests/test_tls_warning.py +49 -0
  170. pathapi_uri-0.1.0/tests/test_transport_security.py +611 -0
  171. pathapi_uri-0.1.0/tests/test_uri_authority.py +295 -0
  172. pathapi_uri-0.1.0/tests/test_uri_constructor_options.py +105 -0
  173. pathapi_uri-0.1.0/tests/test_uri_copies.py +296 -0
  174. pathapi_uri-0.1.0/tests/test_uri_core_parity.py +222 -0
  175. pathapi_uri-0.1.0/tests/test_uri_cost.py +91 -0
  176. pathapi_uri-0.1.0/tests/test_uri_derivation_query.py +115 -0
  177. pathapi_uri-0.1.0/tests/test_uri_dot_segments.py +207 -0
  178. pathapi_uri-0.1.0/tests/test_uri_empty_segments.py +94 -0
  179. pathapi_uri-0.1.0/tests/test_uri_join_shape.py +92 -0
  180. pathapi_uri-0.1.0/tests/test_uri_missing_extras.py +105 -0
  181. pathapi_uri-0.1.0/tests/test_uri_parse.py +215 -0
  182. pathapi_uri-0.1.0/tests/test_uri_path.py +414 -0
  183. pathapi_uri-0.1.0/tests/test_uri_schemesmap.py +104 -0
  184. pathapi_uri-0.1.0/tests/test_uri_values.py +297 -0
  185. pathapi_uri-0.1.0/tests/test_version.py +24 -0
  186. pathapi_uri-0.1.0/tests/test_webdav.py +453 -0
  187. pathapi_uri-0.1.0/tests/test_with_session.py +105 -0
  188. pathapi_uri-0.1.0/tests/test_write_discard.py +184 -0
  189. 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
+ [![Version](https://img.shields.io/pypi/v/pathapi-uri.svg)](https://pypi.org/project/pathapi-uri/)
50
+ [![Python versions](https://img.shields.io/pypi/pyversions/pathapi-uri.svg)](https://pypi.org/project/pathapi-uri/)
51
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/uripath/blob/main/LICENSE)
52
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/uripath/)
53
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/uripath/test.yml)](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).