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.
- objpath-0.1.0/.gitignore +41 -0
- objpath-0.1.0/CHANGELOG.md +113 -0
- objpath-0.1.0/LICENSE +21 -0
- objpath-0.1.0/PKG-INFO +204 -0
- objpath-0.1.0/README.md +153 -0
- objpath-0.1.0/docs/api/exceptions.md +10 -0
- objpath-0.1.0/docs/api/paths.md +18 -0
- objpath-0.1.0/docs/api/uri.md +8 -0
- objpath-0.1.0/docs/benchmarks.md +49 -0
- objpath-0.1.0/docs/changelog.md +3 -0
- objpath-0.1.0/docs/divergences.md +30 -0
- objpath-0.1.0/docs/guides/paths.md +76 -0
- objpath-0.1.0/docs/guides/uri.md +35 -0
- objpath-0.1.0/docs/index.md +72 -0
- objpath-0.1.0/examples/az_listing.py +49 -0
- objpath-0.1.0/examples/gs_listing.py +45 -0
- objpath-0.1.0/examples/s3_listing.py +46 -0
- objpath-0.1.0/mkdocs.yml +58 -0
- objpath-0.1.0/pyproject.toml +106 -0
- objpath-0.1.0/src/objpath/AGENTS.md +311 -0
- objpath-0.1.0/src/objpath/__init__.py +68 -0
- objpath-0.1.0/src/objpath/_az.py +547 -0
- objpath-0.1.0/src/objpath/_az_backend.py +130 -0
- objpath-0.1.0/src/objpath/_az_errors.py +110 -0
- objpath-0.1.0/src/objpath/_clients.py +108 -0
- objpath-0.1.0/src/objpath/_digests.py +10 -0
- objpath-0.1.0/src/objpath/_failures.py +84 -0
- objpath-0.1.0/src/objpath/_gs.py +447 -0
- objpath-0.1.0/src/objpath/_gs_backend.py +95 -0
- objpath-0.1.0/src/objpath/_gs_errors.py +112 -0
- objpath-0.1.0/src/objpath/_keys.py +119 -0
- objpath-0.1.0/src/objpath/_optional.py +60 -0
- objpath-0.1.0/src/objpath/_places.py +26 -0
- objpath-0.1.0/src/objpath/_reader.py +83 -0
- objpath-0.1.0/src/objpath/_s3.py +489 -0
- objpath-0.1.0/src/objpath/_s3_backend.py +68 -0
- objpath-0.1.0/src/objpath/_s3_errors.py +106 -0
- objpath-0.1.0/src/objpath/_storepath.py +579 -0
- objpath-0.1.0/src/objpath/_uri_store.py +162 -0
- objpath-0.1.0/src/objpath/_version.py +11 -0
- objpath-0.1.0/src/objpath/_writer.py +113 -0
- objpath-0.1.0/src/objpath/exceptions.py +31 -0
- objpath-0.1.0/src/objpath/py.typed +0 -0
- objpath-0.1.0/src/objpath/uri.py +213 -0
- objpath-0.1.0/tests/az_fakes.py +251 -0
- objpath-0.1.0/tests/capabilities.py +19 -0
- objpath-0.1.0/tests/child.py +45 -0
- objpath-0.1.0/tests/conftest.py +929 -0
- objpath-0.1.0/tests/gs_fakes.py +306 -0
- objpath-0.1.0/tests/hermetic.py +261 -0
- objpath-0.1.0/tests/mutation/README.md +66 -0
- objpath-0.1.0/tests/mutation/mutants.py +436 -0
- objpath-0.1.0/tests/mutation/mutation_check.py +16 -0
- objpath-0.1.0/tests/mutation/run.py +224 -0
- objpath-0.1.0/tests/objstore_fakes.py +198 -0
- objpath-0.1.0/tests/probes/probe_environment_leak.py +12 -0
- objpath-0.1.0/tests/probes/probe_leaks_a_socket.py +12 -0
- objpath-0.1.0/tests/probes/probe_session_environment.py +36 -0
- objpath-0.1.0/tests/server_loops.py +43 -0
- objpath-0.1.0/tests/store_support.py +57 -0
- objpath-0.1.0/tests/test_az.py +628 -0
- objpath-0.1.0/tests/test_az_fake.py +1424 -0
- objpath-0.1.0/tests/test_az_path.py +563 -0
- objpath-0.1.0/tests/test_backends.py +321 -0
- objpath-0.1.0/tests/test_documentation.py +161 -0
- objpath-0.1.0/tests/test_examples_skip.py +62 -0
- objpath-0.1.0/tests/test_gs.py +684 -0
- objpath-0.1.0/tests/test_gs_fake.py +1462 -0
- objpath-0.1.0/tests/test_gs_path.py +476 -0
- objpath-0.1.0/tests/test_hermetic.py +327 -0
- objpath-0.1.0/tests/test_isolation.py +139 -0
- objpath-0.1.0/tests/test_packaging.py +100 -0
- objpath-0.1.0/tests/test_public_surface.py +174 -0
- objpath-0.1.0/tests/test_s3.py +1470 -0
- objpath-0.1.0/tests/test_s3_errors.py +637 -0
- objpath-0.1.0/tests/test_s3_parity.py +309 -0
- objpath-0.1.0/tests/test_s3_path.py +479 -0
- objpath-0.1.0/tests/test_shared_helpers.py +170 -0
- objpath-0.1.0/tests/test_shipped_header.py +372 -0
- objpath-0.1.0/tests/test_spool_unclosed.py +55 -0
- objpath-0.1.0/tests/test_uri_az.py +514 -0
- objpath-0.1.0/tests/test_uri_gs.py +346 -0
- objpath-0.1.0/tests/test_uri_s3.py +529 -0
- objpath-0.1.0/tests/test_version.py +24 -0
- objpath-0.1.0/tests/waits.py +17 -0
objpath-0.1.0/.gitignore
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/objpath/)
|
|
55
|
+
[](https://pypi.org/project/objpath/)
|
|
56
|
+
[](https://github.com/jose-pr/objpath/blob/main/LICENSE)
|
|
57
|
+
[](https://jose-pr.github.io/objpath/)
|
|
58
|
+
[](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).
|
objpath-0.1.0/README.md
ADDED
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
# objpath
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/objpath/)
|
|
4
|
+
[](https://pypi.org/project/objpath/)
|
|
5
|
+
[](https://github.com/jose-pr/objpath/blob/main/LICENSE)
|
|
6
|
+
[](https://jose-pr.github.io/objpath/)
|
|
7
|
+
[](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,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.
|