objpath 0.1.0__tar.gz → 0.1.1__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 → objpath-0.1.1}/CHANGELOG.md +12 -1
  2. {objpath-0.1.0 → objpath-0.1.1}/PKG-INFO +1 -1
  3. {objpath-0.1.0 → objpath-0.1.1}/pyproject.toml +1 -1
  4. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/AGENTS.md +3 -0
  5. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_storepath.py +11 -0
  6. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_uri_store.py +10 -0
  7. {objpath-0.1.0 → objpath-0.1.1}/tests/mutation/mutants.py +27 -0
  8. {objpath-0.1.0 → objpath-0.1.1}/tests/test_az_fake.py +67 -0
  9. {objpath-0.1.0 → objpath-0.1.1}/.gitignore +0 -0
  10. {objpath-0.1.0 → objpath-0.1.1}/LICENSE +0 -0
  11. {objpath-0.1.0 → objpath-0.1.1}/README.md +0 -0
  12. {objpath-0.1.0 → objpath-0.1.1}/docs/api/exceptions.md +0 -0
  13. {objpath-0.1.0 → objpath-0.1.1}/docs/api/paths.md +0 -0
  14. {objpath-0.1.0 → objpath-0.1.1}/docs/api/uri.md +0 -0
  15. {objpath-0.1.0 → objpath-0.1.1}/docs/benchmarks.md +0 -0
  16. {objpath-0.1.0 → objpath-0.1.1}/docs/changelog.md +0 -0
  17. {objpath-0.1.0 → objpath-0.1.1}/docs/divergences.md +0 -0
  18. {objpath-0.1.0 → objpath-0.1.1}/docs/guides/paths.md +0 -0
  19. {objpath-0.1.0 → objpath-0.1.1}/docs/guides/uri.md +0 -0
  20. {objpath-0.1.0 → objpath-0.1.1}/docs/index.md +0 -0
  21. {objpath-0.1.0 → objpath-0.1.1}/examples/az_listing.py +0 -0
  22. {objpath-0.1.0 → objpath-0.1.1}/examples/gs_listing.py +0 -0
  23. {objpath-0.1.0 → objpath-0.1.1}/examples/s3_listing.py +0 -0
  24. {objpath-0.1.0 → objpath-0.1.1}/mkdocs.yml +0 -0
  25. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/__init__.py +0 -0
  26. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_az.py +0 -0
  27. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_az_backend.py +0 -0
  28. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_az_errors.py +0 -0
  29. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_clients.py +0 -0
  30. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_digests.py +0 -0
  31. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_failures.py +0 -0
  32. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_gs.py +0 -0
  33. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_gs_backend.py +0 -0
  34. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_gs_errors.py +0 -0
  35. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_keys.py +0 -0
  36. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_optional.py +0 -0
  37. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_places.py +0 -0
  38. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_reader.py +0 -0
  39. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_s3.py +0 -0
  40. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_s3_backend.py +0 -0
  41. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_s3_errors.py +0 -0
  42. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_version.py +0 -0
  43. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/_writer.py +0 -0
  44. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/exceptions.py +0 -0
  45. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/py.typed +0 -0
  46. {objpath-0.1.0 → objpath-0.1.1}/src/objpath/uri.py +0 -0
  47. {objpath-0.1.0 → objpath-0.1.1}/tests/az_fakes.py +0 -0
  48. {objpath-0.1.0 → objpath-0.1.1}/tests/capabilities.py +0 -0
  49. {objpath-0.1.0 → objpath-0.1.1}/tests/child.py +0 -0
  50. {objpath-0.1.0 → objpath-0.1.1}/tests/conftest.py +0 -0
  51. {objpath-0.1.0 → objpath-0.1.1}/tests/gs_fakes.py +0 -0
  52. {objpath-0.1.0 → objpath-0.1.1}/tests/hermetic.py +0 -0
  53. {objpath-0.1.0 → objpath-0.1.1}/tests/mutation/README.md +0 -0
  54. {objpath-0.1.0 → objpath-0.1.1}/tests/mutation/mutation_check.py +0 -0
  55. {objpath-0.1.0 → objpath-0.1.1}/tests/mutation/run.py +0 -0
  56. {objpath-0.1.0 → objpath-0.1.1}/tests/objstore_fakes.py +0 -0
  57. {objpath-0.1.0 → objpath-0.1.1}/tests/probes/probe_environment_leak.py +0 -0
  58. {objpath-0.1.0 → objpath-0.1.1}/tests/probes/probe_leaks_a_socket.py +0 -0
  59. {objpath-0.1.0 → objpath-0.1.1}/tests/probes/probe_session_environment.py +0 -0
  60. {objpath-0.1.0 → objpath-0.1.1}/tests/server_loops.py +0 -0
  61. {objpath-0.1.0 → objpath-0.1.1}/tests/store_support.py +0 -0
  62. {objpath-0.1.0 → objpath-0.1.1}/tests/test_az.py +0 -0
  63. {objpath-0.1.0 → objpath-0.1.1}/tests/test_az_path.py +0 -0
  64. {objpath-0.1.0 → objpath-0.1.1}/tests/test_backends.py +0 -0
  65. {objpath-0.1.0 → objpath-0.1.1}/tests/test_documentation.py +0 -0
  66. {objpath-0.1.0 → objpath-0.1.1}/tests/test_examples_skip.py +0 -0
  67. {objpath-0.1.0 → objpath-0.1.1}/tests/test_gs.py +0 -0
  68. {objpath-0.1.0 → objpath-0.1.1}/tests/test_gs_fake.py +0 -0
  69. {objpath-0.1.0 → objpath-0.1.1}/tests/test_gs_path.py +0 -0
  70. {objpath-0.1.0 → objpath-0.1.1}/tests/test_hermetic.py +0 -0
  71. {objpath-0.1.0 → objpath-0.1.1}/tests/test_isolation.py +0 -0
  72. {objpath-0.1.0 → objpath-0.1.1}/tests/test_packaging.py +0 -0
  73. {objpath-0.1.0 → objpath-0.1.1}/tests/test_public_surface.py +0 -0
  74. {objpath-0.1.0 → objpath-0.1.1}/tests/test_s3.py +0 -0
  75. {objpath-0.1.0 → objpath-0.1.1}/tests/test_s3_errors.py +0 -0
  76. {objpath-0.1.0 → objpath-0.1.1}/tests/test_s3_parity.py +0 -0
  77. {objpath-0.1.0 → objpath-0.1.1}/tests/test_s3_path.py +0 -0
  78. {objpath-0.1.0 → objpath-0.1.1}/tests/test_shared_helpers.py +0 -0
  79. {objpath-0.1.0 → objpath-0.1.1}/tests/test_shipped_header.py +0 -0
  80. {objpath-0.1.0 → objpath-0.1.1}/tests/test_spool_unclosed.py +0 -0
  81. {objpath-0.1.0 → objpath-0.1.1}/tests/test_uri_az.py +0 -0
  82. {objpath-0.1.0 → objpath-0.1.1}/tests/test_uri_gs.py +0 -0
  83. {objpath-0.1.0 → objpath-0.1.1}/tests/test_uri_s3.py +0 -0
  84. {objpath-0.1.0 → objpath-0.1.1}/tests/test_version.py +0 -0
  85. {objpath-0.1.0 → objpath-0.1.1}/tests/waits.py +0 -0
@@ -7,6 +7,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.1] - 2026-10-17
11
+
12
+ ### Added
13
+
14
+ - **`rm()` and `copy()` take `max_workers`**, the keyword of `pathapi`'s recursive
15
+ `rm()` and `copy()` (a class runs serially unless it declares `_threadsafe`,
16
+ which these classes do not). A streamed copy passes it on; the batch delete and the
17
+ store's own copy call ignore it. Nothing changes for a call that does not give it,
18
+ and it works with the `pathapi` release that lacks it as long as it is not given.
19
+
10
20
  ## [0.1.0] - 2026-10-14
11
21
 
12
22
  The first release of `objpath`: the Amazon S3, Google Cloud Storage and Azure Blob
@@ -109,5 +119,6 @@ name `pathlib_next`); the old names below are the ones before the move.
109
119
  and emits a `ResourceWarning`; `pathlib-next` closed it, and so uploaded it. What to do:
110
120
  close the file, or use `with`.
111
121
 
112
- [Unreleased]: https://github.com/jose-pr/objpath/compare/v0.1.0...HEAD
122
+ [Unreleased]: https://github.com/jose-pr/objpath/compare/v0.1.1...HEAD
123
+ [0.1.1]: https://github.com/jose-pr/objpath/compare/v0.1.0...v0.1.1
113
124
  [0.1.0]: https://github.com/jose-pr/objpath/releases/tag/v0.1.0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: objpath
3
- Version: 0.1.0
3
+ Version: 0.1.1
4
4
  Summary: pathlib-style paths for S3, Google Cloud Storage and Azure Blob Storage, and the s3:, gs: and az: schemes for uripath
5
5
  Project-URL: Homepage, https://github.com/jose-pr/objpath/
6
6
  Project-URL: Documentation, https://jose-pr.github.io/objpath/
@@ -5,7 +5,7 @@ build-backend = "hatchling.build"
5
5
 
6
6
  [project]
7
7
  name = "objpath"
8
- version = "0.1.0"
8
+ version = "0.1.1"
9
9
  authors = [{ name = "Jose A" }]
10
10
  description = "pathlib-style paths for S3, Google Cloud Storage and Azure Blob Storage, and the s3:, gs: and az: schemes for uripath"
11
11
  readme = "README.md"
@@ -160,6 +160,9 @@ Reading and writing:
160
160
  with that key's path, one error per key. `rm(recursive=True)` of a missing bucket is
161
161
  a missing path (`missing_ok`, `ignore_error` decide). There are no symlinks or
162
162
  bindings: `follow_symlinks=` and `follow_binds=` are checked and decide nothing.
163
+ `rm()` and `copy()` take `max_workers=None` (`pathapi`'s keyword, passed on to the
164
+ generic walks only when given; the batch delete and the store's copy call ignore it,
165
+ and a path never declares `_threadsafe`).
163
166
  - `checksum(algorithm="md5")` (`NativeChecksum`) is the digest the store keeps, read with
164
167
  one request and no byte of the content. S3: the plain MD5 that the ETag is for an object
165
168
  one request wrote and KMS or a customer key does not encrypt, or the stored full-object
@@ -371,7 +371,10 @@ class ObjectStorePath(TreePath):
371
371
  *,
372
372
  follow_symlinks=False,
373
373
  follow_binds=False,
374
+ max_workers=None,
374
375
  ):
376
+ """Remove as `Path.rm()` does. `max_workers` is `Path.rm()`'s and is
377
+ ignored: a recursive removal is the store's batch delete."""
375
378
  # An object store holds no symlinks or bindings: the policies are
376
379
  # checked and have nothing to decide.
377
380
  check_follow("follow_symlinks", follow_symlinks)
@@ -383,6 +386,8 @@ class ObjectStorePath(TreePath):
383
386
  ignore_error=ignore_error,
384
387
  follow_symlinks=follow_symlinks,
385
388
  follow_binds=follow_binds,
389
+ # Only when given: pathapi 0.1.1 has no such keyword.
390
+ **({} if max_workers is None else {"max_workers": max_workers}),
386
391
  )
387
392
 
388
393
  def on_error(error, path=None):
@@ -461,7 +466,11 @@ class ObjectStorePath(TreePath):
461
466
  ignore_error=None,
462
467
  progress=None,
463
468
  on_error=None,
469
+ max_workers=None,
464
470
  ):
471
+ """Copy as `Path.copy()` does, with the store's own copy call for one
472
+ object of the same store. `max_workers` is `Path.copy()`'s and applies to
473
+ the streamed copy only; the store's copy call ignores it."""
465
474
  # A prefix is copied by walking it; one that holds keys the walk
466
475
  # cannot reach is refused up front rather than copied incompletely.
467
476
  # That refusal is not a per-child error: `on_error` is never asked.
@@ -486,6 +495,8 @@ class ObjectStorePath(TreePath):
486
495
  ignore_error=ignore_error,
487
496
  progress=progress,
488
497
  **({} if on_error is None else {"on_error": on_error}),
498
+ # Only when given: pathapi 0.1.1 has no such keyword.
499
+ **({} if max_workers is None else {"max_workers": max_workers}),
489
500
  )
490
501
 
491
502
  def _store_copy_possible(self, target) -> bool:
@@ -92,7 +92,10 @@ class ObjectStoreURIPath(DelegatingURIPath):
92
92
  *,
93
93
  follow_symlinks=False,
94
94
  follow_binds=False,
95
+ max_workers=None,
95
96
  ):
97
+ """Remove as `Path.rm()` does. `max_workers` is `Path.rm()`'s and is
98
+ ignored: a recursive removal is the store's batch delete."""
96
99
  check_follow("follow_symlinks", follow_symlinks)
97
100
  check_follow("follow_binds", follow_binds)
98
101
  on_error = ignore_error
@@ -110,6 +113,7 @@ class ObjectStoreURIPath(DelegatingURIPath):
110
113
  ignore_error=on_error,
111
114
  follow_symlinks=follow_symlinks,
112
115
  follow_binds=follow_binds,
116
+ max_workers=max_workers,
113
117
  )
114
118
 
115
119
  def copy(
@@ -123,7 +127,11 @@ class ObjectStoreURIPath(DelegatingURIPath):
123
127
  ignore_error=None,
124
128
  progress=None,
125
129
  on_error=None,
130
+ max_workers=None,
126
131
  ):
132
+ """Copy as `Path.copy()` does, with the store's own copy call for one
133
+ object of the same store. `max_workers` is `Path.copy()`'s and applies to
134
+ the streamed copy only; the store's copy call ignores it."""
127
135
  # The core path's walk of a prefix is not used: children are URI paths
128
136
  # and are copied through this method, so the refusal of a prefix that
129
137
  # holds keys a walk cannot reach is made here, once.
@@ -159,4 +167,6 @@ class ObjectStoreURIPath(DelegatingURIPath):
159
167
  ignore_error=ignore_error,
160
168
  progress=progress,
161
169
  **({} if offering is None else {"on_error": offering}),
170
+ # Only when given: pathapi 0.1.1 has no such keyword.
171
+ **({} if max_workers is None else {"max_workers": max_workers}),
162
172
  )
@@ -434,3 +434,30 @@ mutant(
434
434
  "tests/test_spool_unclosed.py::test_an_unclosed_stream_that_was_not_modified_is_collected_silently",
435
435
  "a spooled stream with nothing to upload warns when collected",
436
436
  )
437
+
438
+ mutant(
439
+ "objpath-store-rm-always-passes-max-workers",
440
+ "_storepath.py",
441
+ ' # Only when given: pathapi 0.1.1 has no such keyword.\n **({} if max_workers is None else {"max_workers": max_workers}),\n )\n\n def on_error(',
442
+ " max_workers=max_workers,\n )\n\n def on_error(",
443
+ "tests/test_az_fake.py::test_the_generic_calls_get_max_workers_only_when_it_is_given",
444
+ "rm() hands pathapi a max_workers it was not given, which a release without the keyword refuses",
445
+ )
446
+
447
+ mutant(
448
+ "objpath-store-copy-always-passes-max-workers",
449
+ "_storepath.py",
450
+ ' # Only when given: pathapi 0.1.1 has no such keyword.\n **({} if max_workers is None else {"max_workers": max_workers}),\n )\n\n def _store_copy_possible',
451
+ " max_workers=max_workers,\n )\n\n def _store_copy_possible",
452
+ "tests/test_az_fake.py::test_the_generic_calls_get_max_workers_only_when_it_is_given",
453
+ "copy() hands pathapi a max_workers it was not given, which a release without the keyword refuses",
454
+ )
455
+
456
+ mutant(
457
+ "objpath-uri-copy-always-passes-max-workers",
458
+ "_uri_store.py",
459
+ ' # Only when given: pathapi 0.1.1 has no such keyword.\n **({} if max_workers is None else {"max_workers": max_workers}),\n',
460
+ " max_workers=max_workers,\n",
461
+ "tests/test_az_fake.py::test_the_generic_calls_get_max_workers_only_when_it_is_given",
462
+ "the URI copy() hands pathapi a max_workers it was not given",
463
+ )
@@ -1422,3 +1422,70 @@ def test_r_plus_reads_its_initial_content_as_one_version(az, monkeypatch):
1422
1422
  az("az://account/container/big.bin", backend).open("r+b")
1423
1423
  assert info.value.errno == _stale_errno()
1424
1424
  assert container.objects["big.bin"] == NEW and container.uploads == []
1425
+
1426
+
1427
+ # --- max_workers ---------------------------------------------------------------------
1428
+
1429
+
1430
+ def _generic_owner(path, name):
1431
+ """The class whose `name` the override of `path`'s class reaches with `super()`."""
1432
+ # A URI path hands `rm()` to its core path, whose `super()` is the generic one.
1433
+ if name == "rm":
1434
+ path = getattr(path, "_core_path", path)
1435
+ owners = [base for base in type(path).__mro__ if name in vars(base)]
1436
+ return owners[1]
1437
+
1438
+
1439
+ @pytest.mark.parametrize("name", ["rm", "copy"])
1440
+ def test_rm_and_copy_have_the_parameters_of_the_path_ones_and_max_workers(az, name):
1441
+ import inspect
1442
+
1443
+ from pathapi import Path
1444
+
1445
+ expected = inspect.signature(getattr(Path, name)).parameters
1446
+ actual = inspect.signature(getattr(type(az("az://account/c/k")), name)).parameters
1447
+ assert [n for n in actual if n != "max_workers"] == [
1448
+ n for n in expected if n != "max_workers"
1449
+ ]
1450
+ assert actual["max_workers"].kind is inspect.Parameter.KEYWORD_ONLY
1451
+ assert actual["max_workers"].default is None
1452
+ for pname, parameter in expected.items():
1453
+ assert actual[pname].kind == parameter.kind, pname
1454
+ assert actual[pname].default == parameter.default, pname
1455
+
1456
+
1457
+ def test_the_batch_delete_and_the_service_copy_ignore_max_workers(az):
1458
+ backend, container = _container_with(**{"dir/a.txt": b"a", "dir/b.txt": b"b"})
1459
+
1460
+ az("az://account/container/dir/a.txt", backend).copy(
1461
+ az("az://account/container/c.txt", backend), max_workers=4
1462
+ )
1463
+ az("az://account/container/dir", backend).rm(recursive=True, max_workers=4)
1464
+
1465
+ assert container.copies == [("dir/a.txt", "c.txt")]
1466
+ assert sorted(container.objects) == ["c.txt"]
1467
+
1468
+
1469
+ @pytest.mark.parametrize("name", ["rm", "copy"])
1470
+ @pytest.mark.parametrize("workers", [None, 4])
1471
+ def test_the_generic_calls_get_max_workers_only_when_it_is_given(
1472
+ az, monkeypatch, name, workers
1473
+ ):
1474
+ seen = []
1475
+ source = az("az://account/container/a.txt")
1476
+ monkeypatch.setattr(
1477
+ _generic_owner(source, name),
1478
+ name,
1479
+ lambda self, *args, **kw: seen.append(kw),
1480
+ )
1481
+ extra = {} if workers is None else {"max_workers": workers}
1482
+
1483
+ if name == "rm":
1484
+ source.rm(**extra)
1485
+ else:
1486
+ # Another backend: the streamed copy, which is the generic one.
1487
+ source.copy(az("az://account/container/b.txt"), **extra)
1488
+
1489
+ assert [kw.get("max_workers", "absent") for kw in seen] == [
1490
+ "absent" if workers is None else workers
1491
+ ]
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes