objpath 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 (85) hide show
  1. objpath-0.1.0/.gitignore +41 -0
  2. objpath-0.1.0/CHANGELOG.md +113 -0
  3. objpath-0.1.0/LICENSE +21 -0
  4. objpath-0.1.0/PKG-INFO +204 -0
  5. objpath-0.1.0/README.md +153 -0
  6. objpath-0.1.0/docs/api/exceptions.md +10 -0
  7. objpath-0.1.0/docs/api/paths.md +18 -0
  8. objpath-0.1.0/docs/api/uri.md +8 -0
  9. objpath-0.1.0/docs/benchmarks.md +49 -0
  10. objpath-0.1.0/docs/changelog.md +3 -0
  11. objpath-0.1.0/docs/divergences.md +30 -0
  12. objpath-0.1.0/docs/guides/paths.md +76 -0
  13. objpath-0.1.0/docs/guides/uri.md +35 -0
  14. objpath-0.1.0/docs/index.md +72 -0
  15. objpath-0.1.0/examples/az_listing.py +49 -0
  16. objpath-0.1.0/examples/gs_listing.py +45 -0
  17. objpath-0.1.0/examples/s3_listing.py +46 -0
  18. objpath-0.1.0/mkdocs.yml +58 -0
  19. objpath-0.1.0/pyproject.toml +106 -0
  20. objpath-0.1.0/src/objpath/AGENTS.md +311 -0
  21. objpath-0.1.0/src/objpath/__init__.py +68 -0
  22. objpath-0.1.0/src/objpath/_az.py +547 -0
  23. objpath-0.1.0/src/objpath/_az_backend.py +130 -0
  24. objpath-0.1.0/src/objpath/_az_errors.py +110 -0
  25. objpath-0.1.0/src/objpath/_clients.py +108 -0
  26. objpath-0.1.0/src/objpath/_digests.py +10 -0
  27. objpath-0.1.0/src/objpath/_failures.py +84 -0
  28. objpath-0.1.0/src/objpath/_gs.py +447 -0
  29. objpath-0.1.0/src/objpath/_gs_backend.py +95 -0
  30. objpath-0.1.0/src/objpath/_gs_errors.py +112 -0
  31. objpath-0.1.0/src/objpath/_keys.py +119 -0
  32. objpath-0.1.0/src/objpath/_optional.py +60 -0
  33. objpath-0.1.0/src/objpath/_places.py +26 -0
  34. objpath-0.1.0/src/objpath/_reader.py +83 -0
  35. objpath-0.1.0/src/objpath/_s3.py +489 -0
  36. objpath-0.1.0/src/objpath/_s3_backend.py +68 -0
  37. objpath-0.1.0/src/objpath/_s3_errors.py +106 -0
  38. objpath-0.1.0/src/objpath/_storepath.py +579 -0
  39. objpath-0.1.0/src/objpath/_uri_store.py +162 -0
  40. objpath-0.1.0/src/objpath/_version.py +11 -0
  41. objpath-0.1.0/src/objpath/_writer.py +113 -0
  42. objpath-0.1.0/src/objpath/exceptions.py +31 -0
  43. objpath-0.1.0/src/objpath/py.typed +0 -0
  44. objpath-0.1.0/src/objpath/uri.py +213 -0
  45. objpath-0.1.0/tests/az_fakes.py +251 -0
  46. objpath-0.1.0/tests/capabilities.py +19 -0
  47. objpath-0.1.0/tests/child.py +45 -0
  48. objpath-0.1.0/tests/conftest.py +929 -0
  49. objpath-0.1.0/tests/gs_fakes.py +306 -0
  50. objpath-0.1.0/tests/hermetic.py +261 -0
  51. objpath-0.1.0/tests/mutation/README.md +66 -0
  52. objpath-0.1.0/tests/mutation/mutants.py +436 -0
  53. objpath-0.1.0/tests/mutation/mutation_check.py +16 -0
  54. objpath-0.1.0/tests/mutation/run.py +224 -0
  55. objpath-0.1.0/tests/objstore_fakes.py +198 -0
  56. objpath-0.1.0/tests/probes/probe_environment_leak.py +12 -0
  57. objpath-0.1.0/tests/probes/probe_leaks_a_socket.py +12 -0
  58. objpath-0.1.0/tests/probes/probe_session_environment.py +36 -0
  59. objpath-0.1.0/tests/server_loops.py +43 -0
  60. objpath-0.1.0/tests/store_support.py +57 -0
  61. objpath-0.1.0/tests/test_az.py +628 -0
  62. objpath-0.1.0/tests/test_az_fake.py +1424 -0
  63. objpath-0.1.0/tests/test_az_path.py +563 -0
  64. objpath-0.1.0/tests/test_backends.py +321 -0
  65. objpath-0.1.0/tests/test_documentation.py +161 -0
  66. objpath-0.1.0/tests/test_examples_skip.py +62 -0
  67. objpath-0.1.0/tests/test_gs.py +684 -0
  68. objpath-0.1.0/tests/test_gs_fake.py +1462 -0
  69. objpath-0.1.0/tests/test_gs_path.py +476 -0
  70. objpath-0.1.0/tests/test_hermetic.py +327 -0
  71. objpath-0.1.0/tests/test_isolation.py +139 -0
  72. objpath-0.1.0/tests/test_packaging.py +100 -0
  73. objpath-0.1.0/tests/test_public_surface.py +174 -0
  74. objpath-0.1.0/tests/test_s3.py +1470 -0
  75. objpath-0.1.0/tests/test_s3_errors.py +637 -0
  76. objpath-0.1.0/tests/test_s3_parity.py +309 -0
  77. objpath-0.1.0/tests/test_s3_path.py +479 -0
  78. objpath-0.1.0/tests/test_shared_helpers.py +170 -0
  79. objpath-0.1.0/tests/test_shipped_header.py +372 -0
  80. objpath-0.1.0/tests/test_spool_unclosed.py +55 -0
  81. objpath-0.1.0/tests/test_uri_az.py +514 -0
  82. objpath-0.1.0/tests/test_uri_gs.py +346 -0
  83. objpath-0.1.0/tests/test_uri_s3.py +529 -0
  84. objpath-0.1.0/tests/test_version.py +24 -0
  85. objpath-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,113 @@
1
+ # Changelog
2
+
3
+ All notable changes to this project will be documented in this file.
4
+
5
+ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
6
+ and this project adheres to [Semantic Versioning](https://semver.org/).
7
+
8
+ ## [Unreleased]
9
+
10
+ ## [0.1.0] - 2026-10-14
11
+
12
+ The first release of `objpath`: the Amazon S3, Google Cloud Storage and Azure Blob
13
+ Storage support of `pathlib-next` 0.9.14 (`pathlib_next.uri.schemes.s3`, `.gs` and
14
+ `.az`) as a distribution of its own, with paths that need no URI. It needs Python 3.9 or
15
+ newer and `pathapi>=0.1.0,<0.2`. No store SDK is required to import it: the extras
16
+ `s3` (`boto3>=1.36.0,<2`), `gs` (`google-cloud-storage>=3.0,<4`) and `az`
17
+ (`azure-storage-blob>=12.4.0,<13` and `azure-identity>=1.0,<2`) add one, and `uri`
18
+ (`pathapi-uri>=0.1.0,<0.2`) adds the `s3:`, `gs:` and `az:` schemes. Under *Changed*, the
19
+ migration from `pathlib_next.uri.schemes` comes in this order: the table of names, the
20
+ table of extras, then every change of behaviour with what to do about it.
21
+
22
+ It is built on `pathapi`, which was published as `pathlib-next` up to 0.10.0 (import
23
+ name `pathlib_next`); the old names below are the ones before the move.
24
+
25
+ ### Added
26
+
27
+ - `S3Path`, `GSPath` and `AZPath`, `pathapi.Path` classes for the objects of a
28
+ bucket or container, built from keywords:
29
+ `S3Path("dir/key", bucket="bkt")`, `GSPath("dir/key", bucket="bkt")`,
30
+ `AZPath("dir/key", container="cont", account="acct")`. Nothing connects until the
31
+ first request. Client options (`region_name=`, `project=`, `credential=`, ...) build
32
+ the path's own backend, `backend=` shares one between paths built separately (giving it
33
+ together with a client option is a `TypeError`), and a
34
+ path joined from another keeps its bucket and its backend (`bucket=` or `container=`
35
+ on a joined path moves it to another bucket of the same backend). Paths built without
36
+ either use one default backend per process (`AZPath`: per account).
37
+ - `S3Backend`, `GSBackend`, `AZBackend` and their contracts `BaseS3Backend`,
38
+ `BaseGSBackend` and `BaseAZBackend`. Every backend is a context manager: `close()`
39
+ closes the SDK client it built, the next use builds another, and calling it twice is
40
+ harmless.
41
+ - `objpath.exceptions`: `ObjPathError`, a `pathapi.PathError`, with
42
+ `ObjectChangedError` and `UnreachableKeysError`, both also `OSError` (see *Changed*).
43
+ - `objpath.uri`: `S3URIPath`, `GSURIPath` and `AZURIPath`, the `s3:`, `gs:` and `az:`
44
+ schemes of `uripath`, found by `uripath` through the entry points `s3`, `gs` and `az` of
45
+ the group `uripath.schemes`. Importing `objpath.uri` without `uripath` raises an
46
+ `ImportError` that names `pip install "objpath[uri]"`; `import objpath` never imports
47
+ `uripath` or an SDK.
48
+ - `AZBackend.account`: the account that `account=`, else `connection_string=` or
49
+ `account_url=`, names.
50
+ - `objpath.__version__`, read from the installed metadata.
51
+
52
+ ### Changed
53
+
54
+ - The names. `pathlib_next.uri.schemes.s3`, `.gs` and `.az` are gone with no alias.
55
+
56
+ | Was | Now |
57
+ | --- | --- |
58
+ | `S3Path` (a `UriPath`) | `objpath.S3Path` for a path built from keywords; `objpath.uri.S3URIPath` for `URIPath("s3://...")` |
59
+ | `GsPath` | `objpath.GSPath`; `objpath.uri.GSURIPath` |
60
+ | `AzPath` | `objpath.AZPath`; `objpath.uri.AZURIPath` |
61
+ | `S3Backend`, `BaseS3Backend` | `objpath.S3Backend`, `objpath.BaseS3Backend` |
62
+ | `GsBackend`, `BaseGsBackend` | `objpath.GSBackend`, `objpath.BaseGSBackend` |
63
+ | `AzBackend`, `BaseAzBackend` | `objpath.AZBackend`, `objpath.BaseAZBackend` |
64
+ | `GsPath.bucket_name` | `GSPath.bucket` (and `GSURIPath.bucket`) |
65
+ | the entry-point group `pathlib_next.schemes`, entries `s3`, `gs`, `az` | the group `uripath.schemes`, entries `s3 = "objpath.uri:S3URIPath"`, `gs = "objpath.uri:GSURIPath"`, `az = "objpath.uri:AZURIPath"` |
66
+
67
+ - What to install where `pathlib-next` had an extra. `pathlib-next` 0.10 has no `s3`,
68
+ `gs` or `az` extra.
69
+
70
+ | Was | Install now |
71
+ | --- | --- |
72
+ | `pathlib-next[s3]` | `objpath[s3]` |
73
+ | `pathlib-next[gs]` | `objpath[gs]` |
74
+ | `pathlib-next[az]` | `objpath[az]` |
75
+ | any of them, for `UriPath("s3://...")`, `gs://` or `az://` | add `objpath[uri]` (or install `uripath` beside it) |
76
+
77
+ - A path is built from keywords, not from a URL. Was: `S3Path("s3://bkt/dir/key")`
78
+ parsed the URL, and the bucket came from its host. Now `S3Path("dir/key", bucket="bkt")`,
79
+ `GSPath("dir/key", bucket="bkt")` and `AZPath("dir/key", container="cont",
80
+ account="acct")`; a URL is a `URIPath` (`URIPath("s3://bkt/dir/key")`, which needs the
81
+ `uri` extra). A path without a bucket is a `TypeError` where it is built; an
82
+ `S3URIPath` with no bucket still raises `FileNotFoundError` when it is used.
83
+ - `str(path)` of an `S3Path`, `GSPath` and `AZPath` is the URL (`s3://bkt/dir/key`), as
84
+ the URI paths' is. `repr()` is `S3Path('dir/key', bucket='bkt')`.
85
+ - Two paths are equal when their class, bucket (account and container, for `AZPath`)
86
+ and key are; `x` and `/x` are one key.
87
+ - An `AZPath` needs one of `account=`, `backend=`, a client option or an `AZPath` to join
88
+ from; without one it is a `TypeError` where it is built. Was: the account was the host of the
89
+ URL, and a URL without one raised `FileNotFoundError` when it was used (an `AZURIPath` still
90
+ does). For URI paths nothing else changes: `backend=` is used as it is for the path it is
91
+ given to, whatever bucket the URI names.
92
+ - The account level of Azure is a URI feature. Was: `AzPath("az://account")` listed the
93
+ containers of the account. Now `AZPath` always names a container; `URIPath("az://account")`
94
+ is still a directory whose children are the containers.
95
+ - A stream whose object was replaced while it was being read raises `ObjectChangedError`,
96
+ and a copy or removal of a prefix that holds keys a walk cannot reach raises
97
+ `UnreachableKeysError`. Was: a plain `OSError` (`ESTALE`, `EINVAL`). Both are still
98
+ `OSError` with the same `errno`, so `except OSError` and `errno` checks keep working.
99
+ - `AZBackend(connection_string=...)` and `AZBackend(account_url=...)` carry the account they
100
+ name in `account`; `account` was `None` for them.
101
+ - The Azure tuning settings. Was: the module settings `schemes.az.READ_CHUNK` (512 KiB per
102
+ ranged read request) and `schemes.az.COPY_POLL_TIMEOUT` (300 s to wait for a service copy).
103
+ Now: the class attributes `AZBackend.read_chunk` and `AZBackend.copy_timeout`
104
+ (`BaseAZBackend` holds the defaults); assign them on `AZBackend` or on a subclass of it.
105
+ - `is_local()` of a store path is `False` without resolving anything. Was: the URI path resolved
106
+ the bucket or account name as if it were a host name, so `PathSyncer(quick_check=True)` made a
107
+ DNS lookup of it and read a name that resolved to this machine as local.
108
+ - A write stream of a store that is garbage collected without being closed uploads nothing
109
+ and emits a `ResourceWarning`; `pathlib-next` closed it, and so uploaded it. What to do:
110
+ close the file, or use `with`.
111
+
112
+ [Unreleased]: https://github.com/jose-pr/objpath/compare/v0.1.0...HEAD
113
+ [0.1.0]: https://github.com/jose-pr/objpath/releases/tag/v0.1.0
objpath-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 Jose A
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
objpath-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,204 @@
1
+ Metadata-Version: 2.5
2
+ Name: objpath
3
+ Version: 0.1.0
4
+ Summary: pathlib-style paths for S3, Google Cloud Storage and Azure Blob Storage, and the s3:, gs: and az: schemes for uripath
5
+ Project-URL: Homepage, https://github.com/jose-pr/objpath/
6
+ Project-URL: Documentation, https://jose-pr.github.io/objpath/
7
+ Project-URL: Issues, https://github.com/jose-pr/objpath/issues
8
+ Author: Jose A
9
+ License-Expression: MIT
10
+ License-File: LICENSE
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Operating System :: OS Independent
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.9
15
+ Classifier: Programming Language :: Python :: 3.10
16
+ Classifier: Programming Language :: Python :: 3.11
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Typing :: Typed
21
+ Requires-Python: >=3.9
22
+ Requires-Dist: pathapi<0.2,>=0.1.0
23
+ Provides-Extra: all
24
+ Requires-Dist: azure-identity<2,>=1.0; extra == 'all'
25
+ Requires-Dist: azure-storage-blob<13,>=12.4.0; extra == 'all'
26
+ Requires-Dist: boto3<2,>=1.36.0; extra == 'all'
27
+ Requires-Dist: google-cloud-storage<4,>=3.0; extra == 'all'
28
+ Requires-Dist: pathapi-uri<0.2,>=0.1.0; extra == 'all'
29
+ Provides-Extra: az
30
+ Requires-Dist: azure-identity<2,>=1.0; extra == 'az'
31
+ Requires-Dist: azure-storage-blob<13,>=12.4.0; extra == 'az'
32
+ Provides-Extra: dev
33
+ Requires-Dist: black; extra == 'dev'
34
+ Requires-Dist: build; extra == 'dev'
35
+ Requires-Dist: hatchling; extra == 'dev'
36
+ Requires-Dist: moto[s3]; extra == 'dev'
37
+ Requires-Dist: pytest; extra == 'dev'
38
+ Requires-Dist: pytest-cov; extra == 'dev'
39
+ Requires-Dist: twine; extra == 'dev'
40
+ Provides-Extra: docs
41
+ Requires-Dist: mkdocs; extra == 'docs'
42
+ Requires-Dist: mkdocs-material; extra == 'docs'
43
+ Requires-Dist: mkdocstrings[python]; extra == 'docs'
44
+ Provides-Extra: gs
45
+ Requires-Dist: google-cloud-storage<4,>=3.0; extra == 'gs'
46
+ Provides-Extra: s3
47
+ Requires-Dist: boto3<2,>=1.36.0; extra == 's3'
48
+ Provides-Extra: uri
49
+ Requires-Dist: pathapi-uri<0.2,>=0.1.0; extra == 'uri'
50
+ Description-Content-Type: text/markdown
51
+
52
+ # objpath
53
+
54
+ [![Version](https://img.shields.io/pypi/v/objpath.svg)](https://pypi.org/project/objpath/)
55
+ [![Python versions](https://img.shields.io/pypi/pyversions/objpath.svg)](https://pypi.org/project/objpath/)
56
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/objpath/blob/main/LICENSE)
57
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/objpath/)
58
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/objpath/test.yml)](https://github.com/jose-pr/objpath/actions/workflows/test.yml)
59
+
60
+ **`pathlib` for S3, Google Cloud Storage and Azure Blob buckets.**
61
+ `S3Path("reports/2026.csv", bucket="data")` has the method names, signatures and
62
+ exception types of `pathlib.Path`, and `GSPath` and `AZPath` do the same for the
63
+ other two stores; none needs a URI. They build on
64
+ [`pathapi`](https://github.com/jose-pr/pathapi) alone; with
65
+ [`uripath`](https://github.com/jose-pr/uripath) installed the same package also
66
+ provides the `s3:`, `gs:` and `az:` schemes. It is documented at
67
+ <https://jose-pr.github.io/objpath/>.
68
+
69
+ ## Features
70
+
71
+ | Store | Class | Extra | Directories | Server-side copy | Checksum | Append |
72
+ | --- | --- | --- | --- | --- | --- | --- |
73
+ | Amazon S3 and S3-compatible | `S3Path` | `s3` | key prefixes, `mkdir()` writes a marker object | `CopyObject`, same client | ETag MD5, `crc32`, `crc32c`, `crc64nvme`, `sha1`, `sha256` | no |
74
+ | Google Cloud Storage | `GSPath` | `gs` | key prefixes, `mkdir()` writes a marker object | `copy_blob`, same client | `md5`, `crc32c` | no |
75
+ | Azure Blob Storage | `AZPath` | `az` | key prefixes, `mkdir()` writes a marker blob | service copy, same container and client | `Content-MD5` | no |
76
+
77
+ - **A `pathlib` path on a bucket**: `open`, `read_text`, `iterdir`, `glob`, `stat`,
78
+ `mkdir`, `rename` and the rest, plus `walk`, `copy`, `move`, `rm` and `PathSyncer` from
79
+ `pathapi`. Nothing connects until the first request.
80
+ - **Directories without directories**: `is_dir()` is a prefix test, listings use a
81
+ delimiter and carry the size and time of each object, and the keys a walk cannot reach
82
+ (an empty segment, a name that is both an object and a prefix) are named and refused
83
+ instead of copied or removed half way.
84
+ - **Batch deletes and server-side copies**: `rm(recursive=True)` deletes a thousand keys a
85
+ request, and a copy within a store moves no byte through your process.
86
+ - **Safe writes**: uploads are spooled and sent on `close()`; a `with` block that raised
87
+ uploads nothing. A read is one version of the object, and an object replaced meanwhile
88
+ is an error, not a mix.
89
+ - **The SDKs stay out of the way**: `import objpath` loads no SDK and not `uripath`; a
90
+ store used without its extra raises an `ImportError` naming it. No SDK exception crosses
91
+ the API: errors are the `OSError` types `pathlib` raises, `TimeoutError` and
92
+ `ConnectionError`.
93
+ - **The `s3:`, `gs:` and `az:` schemes** for `uripath`: `URIPath("s3://data/reports")` is
94
+ an `S3URIPath` that delegates to an `S3Path`.
95
+
96
+ ## Installation
97
+
98
+ ```bash
99
+ pip install "objpath[s3]"
100
+ ```
101
+
102
+ Requires `pathapi`, which pip installs with it, and Python 3.9 or newer.
103
+
104
+ | Install | Adds |
105
+ | --- | --- |
106
+ | `pip install objpath` | the paths and the backend contracts; no SDK |
107
+ | `pip install "objpath[s3]"` | `boto3>=1.36.0,<2` for `S3Path` |
108
+ | `pip install "objpath[gs]"` | `google-cloud-storage>=3.0,<4` for `GSPath` |
109
+ | `pip install "objpath[az]"` | `azure-storage-blob` and `azure-identity` for `AZPath` |
110
+ | `pip install "objpath[uri]"` | `uripath` and with it the `s3:`, `gs:` and `az:` schemes |
111
+ | `pip install "objpath[all]"` | all of the above |
112
+
113
+ ## Quick start
114
+
115
+ **A path in a bucket**: the bucket is a keyword, the segments are the key, and the
116
+ credentials are the ones boto3 finds:
117
+
118
+ <!-- example: skip: connects to S3 -->
119
+ ```python
120
+ from objpath import S3Path
121
+
122
+ report = S3Path("reports/2026.csv", bucket="data")
123
+ print(report.read_text())
124
+ for child in report.parent.iterdir():
125
+ print(child.name, child.stat().st_size)
126
+ ```
127
+
128
+ **Another store, or another endpoint**: client options build the path's own backend, and a
129
+ backend shared between paths is one client and one connection pool:
130
+
131
+ <!-- example: skip: connects to a store -->
132
+ ```python
133
+ from objpath import AZPath, GSPath, S3Backend, S3Path
134
+
135
+ blob = AZPath("logs/app.log", container="ops", account="mycompany")
136
+ gcs = GSPath("models/latest", bucket="ml", project="my-project")
137
+
138
+ with S3Backend(endpoint_url="https://minio.example.org", region_name="us-east-1") as backend:
139
+ root = S3Path(bucket="backups", backend=backend)
140
+ (root / "2026").mkdir(exist_ok=True)
141
+ (root / "2026" / "n.txt").write_text("1")
142
+ root.copy(S3Path("mirror", bucket="backups", backend=backend), recursive=True)
143
+ ```
144
+
145
+ **Mirror a tree** with `PathSyncer`, onto a local directory:
146
+
147
+ <!-- example: skip: connects to S3 -->
148
+ ```python
149
+ from pathapi import LocalPath
150
+ from pathapi.sync import PathSyncer
151
+ from objpath import S3Path
152
+
153
+ PathSyncer(remove_missing=True).sync(S3Path("logs", bucket="data"), LocalPath("mirror"))
154
+ ```
155
+
156
+ **The schemes**, with `uripath` installed:
157
+
158
+ <!-- example: skip: connects to S3 -->
159
+ ```python
160
+ from uripath import URIPath
161
+
162
+ print(URIPath("s3://data/reports/2026.csv").read_text())
163
+ ```
164
+
165
+ ## API overview
166
+
167
+ | Module | Purpose |
168
+ | --- | --- |
169
+ | `objpath` | `S3Path`, `GSPath`, `AZPath`, the backends `S3Backend`, `GSBackend`, `AZBackend` and the contracts `BaseS3Backend`, `BaseGSBackend`, `BaseAZBackend`, the exceptions and `__version__` |
170
+ | `objpath.exceptions` | `ObjPathError`, `ObjectChangedError` and `UnreachableKeysError` |
171
+ | `objpath.uri` | `S3URIPath`, `GSURIPath` and `AZURIPath`, the schemes of `uripath` (needs the `uri` extra) |
172
+
173
+ The full reference is on the [documentation site](https://jose-pr.github.io/objpath/);
174
+ an agent-oriented API summary ships inside the package as `objpath/AGENTS.md`.
175
+
176
+ ## Development
177
+
178
+ ```bash
179
+ pip install -e ".[dev,all]"
180
+ python -m pytest -q -rs
181
+ ```
182
+
183
+ The S3 tests run against `moto` in the test process, and the Google Cloud Storage and
184
+ Azure tests against fake servers on the loopback interface; none reaches another host. A
185
+ test for an extra that is not installed is skipped, so check the skip list (`-rs`).
186
+ Contributor notes (virtual environments, formatting, CI, releases) are in the repository's
187
+ [`AGENTS.md`](https://github.com/jose-pr/objpath/blob/main/AGENTS.md). To preview the
188
+ documentation: `pip install -e ".[all,docs]"` then `mkdocs serve`.
189
+
190
+ Changes are recorded in the
191
+ [changelog](https://github.com/jose-pr/objpath/blob/main/CHANGELOG.md).
192
+
193
+ ### Releasing
194
+
195
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
196
+ [`CHANGELOG.md`](https://github.com/jose-pr/objpath/blob/main/CHANGELOG.md). Pushing a tag matching `v*` triggers the release
197
+ workflow: test gate → build (checking the tag names the version built) → a strict
198
+ docs build as a gate → GitHub release → publish. The release workflow never deploys
199
+ the docs site itself: for a final release its last job dispatches the docs workflow
200
+ at the tag, which owns every Pages deploy.
201
+
202
+ ## License
203
+
204
+ MIT — see [LICENSE](https://github.com/jose-pr/objpath/blob/main/LICENSE).
@@ -0,0 +1,153 @@
1
+ # objpath
2
+
3
+ [![Version](https://img.shields.io/pypi/v/objpath.svg)](https://pypi.org/project/objpath/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/objpath.svg)](https://pypi.org/project/objpath/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/objpath/blob/main/LICENSE)
6
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/objpath/)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/objpath/test.yml)](https://github.com/jose-pr/objpath/actions/workflows/test.yml)
8
+
9
+ **`pathlib` for S3, Google Cloud Storage and Azure Blob buckets.**
10
+ `S3Path("reports/2026.csv", bucket="data")` has the method names, signatures and
11
+ exception types of `pathlib.Path`, and `GSPath` and `AZPath` do the same for the
12
+ other two stores; none needs a URI. They build on
13
+ [`pathapi`](https://github.com/jose-pr/pathapi) alone; with
14
+ [`uripath`](https://github.com/jose-pr/uripath) installed the same package also
15
+ provides the `s3:`, `gs:` and `az:` schemes. It is documented at
16
+ <https://jose-pr.github.io/objpath/>.
17
+
18
+ ## Features
19
+
20
+ | Store | Class | Extra | Directories | Server-side copy | Checksum | Append |
21
+ | --- | --- | --- | --- | --- | --- | --- |
22
+ | Amazon S3 and S3-compatible | `S3Path` | `s3` | key prefixes, `mkdir()` writes a marker object | `CopyObject`, same client | ETag MD5, `crc32`, `crc32c`, `crc64nvme`, `sha1`, `sha256` | no |
23
+ | Google Cloud Storage | `GSPath` | `gs` | key prefixes, `mkdir()` writes a marker object | `copy_blob`, same client | `md5`, `crc32c` | no |
24
+ | Azure Blob Storage | `AZPath` | `az` | key prefixes, `mkdir()` writes a marker blob | service copy, same container and client | `Content-MD5` | no |
25
+
26
+ - **A `pathlib` path on a bucket**: `open`, `read_text`, `iterdir`, `glob`, `stat`,
27
+ `mkdir`, `rename` and the rest, plus `walk`, `copy`, `move`, `rm` and `PathSyncer` from
28
+ `pathapi`. Nothing connects until the first request.
29
+ - **Directories without directories**: `is_dir()` is a prefix test, listings use a
30
+ delimiter and carry the size and time of each object, and the keys a walk cannot reach
31
+ (an empty segment, a name that is both an object and a prefix) are named and refused
32
+ instead of copied or removed half way.
33
+ - **Batch deletes and server-side copies**: `rm(recursive=True)` deletes a thousand keys a
34
+ request, and a copy within a store moves no byte through your process.
35
+ - **Safe writes**: uploads are spooled and sent on `close()`; a `with` block that raised
36
+ uploads nothing. A read is one version of the object, and an object replaced meanwhile
37
+ is an error, not a mix.
38
+ - **The SDKs stay out of the way**: `import objpath` loads no SDK and not `uripath`; a
39
+ store used without its extra raises an `ImportError` naming it. No SDK exception crosses
40
+ the API: errors are the `OSError` types `pathlib` raises, `TimeoutError` and
41
+ `ConnectionError`.
42
+ - **The `s3:`, `gs:` and `az:` schemes** for `uripath`: `URIPath("s3://data/reports")` is
43
+ an `S3URIPath` that delegates to an `S3Path`.
44
+
45
+ ## Installation
46
+
47
+ ```bash
48
+ pip install "objpath[s3]"
49
+ ```
50
+
51
+ Requires `pathapi`, which pip installs with it, and Python 3.9 or newer.
52
+
53
+ | Install | Adds |
54
+ | --- | --- |
55
+ | `pip install objpath` | the paths and the backend contracts; no SDK |
56
+ | `pip install "objpath[s3]"` | `boto3>=1.36.0,<2` for `S3Path` |
57
+ | `pip install "objpath[gs]"` | `google-cloud-storage>=3.0,<4` for `GSPath` |
58
+ | `pip install "objpath[az]"` | `azure-storage-blob` and `azure-identity` for `AZPath` |
59
+ | `pip install "objpath[uri]"` | `uripath` and with it the `s3:`, `gs:` and `az:` schemes |
60
+ | `pip install "objpath[all]"` | all of the above |
61
+
62
+ ## Quick start
63
+
64
+ **A path in a bucket**: the bucket is a keyword, the segments are the key, and the
65
+ credentials are the ones boto3 finds:
66
+
67
+ <!-- example: skip: connects to S3 -->
68
+ ```python
69
+ from objpath import S3Path
70
+
71
+ report = S3Path("reports/2026.csv", bucket="data")
72
+ print(report.read_text())
73
+ for child in report.parent.iterdir():
74
+ print(child.name, child.stat().st_size)
75
+ ```
76
+
77
+ **Another store, or another endpoint**: client options build the path's own backend, and a
78
+ backend shared between paths is one client and one connection pool:
79
+
80
+ <!-- example: skip: connects to a store -->
81
+ ```python
82
+ from objpath import AZPath, GSPath, S3Backend, S3Path
83
+
84
+ blob = AZPath("logs/app.log", container="ops", account="mycompany")
85
+ gcs = GSPath("models/latest", bucket="ml", project="my-project")
86
+
87
+ with S3Backend(endpoint_url="https://minio.example.org", region_name="us-east-1") as backend:
88
+ root = S3Path(bucket="backups", backend=backend)
89
+ (root / "2026").mkdir(exist_ok=True)
90
+ (root / "2026" / "n.txt").write_text("1")
91
+ root.copy(S3Path("mirror", bucket="backups", backend=backend), recursive=True)
92
+ ```
93
+
94
+ **Mirror a tree** with `PathSyncer`, onto a local directory:
95
+
96
+ <!-- example: skip: connects to S3 -->
97
+ ```python
98
+ from pathapi import LocalPath
99
+ from pathapi.sync import PathSyncer
100
+ from objpath import S3Path
101
+
102
+ PathSyncer(remove_missing=True).sync(S3Path("logs", bucket="data"), LocalPath("mirror"))
103
+ ```
104
+
105
+ **The schemes**, with `uripath` installed:
106
+
107
+ <!-- example: skip: connects to S3 -->
108
+ ```python
109
+ from uripath import URIPath
110
+
111
+ print(URIPath("s3://data/reports/2026.csv").read_text())
112
+ ```
113
+
114
+ ## API overview
115
+
116
+ | Module | Purpose |
117
+ | --- | --- |
118
+ | `objpath` | `S3Path`, `GSPath`, `AZPath`, the backends `S3Backend`, `GSBackend`, `AZBackend` and the contracts `BaseS3Backend`, `BaseGSBackend`, `BaseAZBackend`, the exceptions and `__version__` |
119
+ | `objpath.exceptions` | `ObjPathError`, `ObjectChangedError` and `UnreachableKeysError` |
120
+ | `objpath.uri` | `S3URIPath`, `GSURIPath` and `AZURIPath`, the schemes of `uripath` (needs the `uri` extra) |
121
+
122
+ The full reference is on the [documentation site](https://jose-pr.github.io/objpath/);
123
+ an agent-oriented API summary ships inside the package as `objpath/AGENTS.md`.
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ pip install -e ".[dev,all]"
129
+ python -m pytest -q -rs
130
+ ```
131
+
132
+ The S3 tests run against `moto` in the test process, and the Google Cloud Storage and
133
+ Azure tests against fake servers on the loopback interface; none reaches another host. A
134
+ test for an extra that is not installed is skipped, so check the skip list (`-rs`).
135
+ Contributor notes (virtual environments, formatting, CI, releases) are in the repository's
136
+ [`AGENTS.md`](https://github.com/jose-pr/objpath/blob/main/AGENTS.md). To preview the
137
+ documentation: `pip install -e ".[all,docs]"` then `mkdocs serve`.
138
+
139
+ Changes are recorded in the
140
+ [changelog](https://github.com/jose-pr/objpath/blob/main/CHANGELOG.md).
141
+
142
+ ### Releasing
143
+
144
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
145
+ [`CHANGELOG.md`](https://github.com/jose-pr/objpath/blob/main/CHANGELOG.md). Pushing a tag matching `v*` triggers the release
146
+ workflow: test gate → build (checking the tag names the version built) → a strict
147
+ docs build as a gate → GitHub release → publish. The release workflow never deploys
148
+ the docs site itself: for a final release its last job dispatches the docs workflow
149
+ at the tag, which owns every Pages deploy.
150
+
151
+ ## License
152
+
153
+ MIT — see [LICENSE](https://github.com/jose-pr/objpath/blob/main/LICENSE).
@@ -0,0 +1,10 @@
1
+ # Exceptions
2
+
3
+ `ObjPathError` is the base of what this package raises on its own. A caller's bad
4
+ argument is a plain `ValueError` or `TypeError`, and a failed request is the `OSError`
5
+ of the kind `pathlib` raises.
6
+
7
+ ::: objpath.exceptions
8
+ options:
9
+ show_if_no_docstring: true
10
+ filters: ["!^_"]
@@ -0,0 +1,18 @@
1
+ # Paths and backends
2
+
3
+ The paths need no URI. Each store needs its own extra: `s3`, `gs` or `az`.
4
+
5
+ ::: objpath
6
+ options:
7
+ show_if_no_docstring: true
8
+ filters: ["!^_"]
9
+ members:
10
+ - S3Path
11
+ - S3Backend
12
+ - BaseS3Backend
13
+ - GSPath
14
+ - GSBackend
15
+ - BaseGSBackend
16
+ - AZPath
17
+ - AZBackend
18
+ - BaseAZBackend
@@ -0,0 +1,8 @@
1
+ # The `s3:`, `gs:` and `az:` schemes
2
+
3
+ Needs `uripath`: `pip install "objpath[uri]"`.
4
+
5
+ ::: objpath.uri
6
+ options:
7
+ show_if_no_docstring: true
8
+ filters: ["!^_"]
@@ -0,0 +1,49 @@
1
+ # Benchmarks
2
+
3
+ `objpath` includes a benchmark harness at `benchmarks/bench.py` that times `S3Path`, and the
4
+ `s3:` scheme of `uripath` on the same operations. It needs `moto` and `boto3`:
5
+ `pip install -e ".[dev,s3,uri]"`.
6
+
7
+ ## Run It
8
+
9
+ ```bash
10
+ python benchmarks/bench.py # run the benchmarks
11
+ python benchmarks/bench.py --help # options; runs nothing
12
+ ```
13
+
14
+ ## Saved Results
15
+
16
+ Every metric is timed over several samples. `--save` writes the run as JSON to
17
+ `benchmarks/results/<name>.json`, with the minimum, median and maximum milliseconds per
18
+ call of each metric plus the interpreter, platform and git commit it ran on:
19
+
20
+ ```bash
21
+ python benchmarks/bench.py --save
22
+ python benchmarks/bench.py --save --samples 9
23
+ ```
24
+
25
+ `--samples N` applies one sample count to every case; without it each case takes five.
26
+ The result schema and the committed results are described in
27
+ [`benchmarks/README.md`](https://github.com/jose-pr/objpath/blob/main/benchmarks/README.md).
28
+
29
+ Compare two runs on their medians, and only between runs from the same machine and
30
+ interpreter. A run on a developer machine is a sanity check; performance claims in release
31
+ notes come from CI runs.
32
+
33
+ ## What It Covers
34
+
35
+ - `stat()` of an object, `iterdir()` and `walk()` of 72 entries
36
+ - `read_bytes()` and `write_bytes()` of 256 KiB, `mkdir()`, `rename()`, `unlink()` and a
37
+ server-side `copy()`
38
+ - `rm(recursive=True)` of a 33-object tree, and the number of requests it sends
39
+ (one `HeadObject`, one `ListObjectsV2` and one `DeleteObjects`)
40
+ - each of the above through `S3Path` (metric group `s3`) and through
41
+ `URIPath("s3://...")` (group `s3-uri`), so the cost of the URI layer is the difference
42
+ between the two groups
43
+
44
+ ## Caveats
45
+
46
+ The bucket is held by `moto` in the benchmark's own process, with no network latency, so
47
+ the numbers show the overhead of the path layer and of moto, not how a store behaves over
48
+ a link. Timings depend on the machine, the interpreter and what else is running; compare
49
+ medians, as described above.
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ --8<-- "CHANGELOG.md"