pathapi 0.1.0__tar.gz → 0.1.2__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 (250) hide show
  1. {pathapi-0.1.0 → pathapi-0.1.2}/CHANGELOG.md +124 -1
  2. {pathapi-0.1.0 → pathapi-0.1.2}/PKG-INFO +27 -9
  3. {pathapi-0.1.0 → pathapi-0.1.2}/README.md +26 -8
  4. {pathapi-0.1.0 → pathapi-0.1.2}/docs/benchmarks.md +4 -0
  5. {pathapi-0.1.0 → pathapi-0.1.2}/docs/divergences.md +11 -3
  6. {pathapi-0.1.0 → pathapi-0.1.2}/docs/index.md +0 -4
  7. {pathapi-0.1.0 → pathapi-0.1.2}/pyproject.toml +8 -1
  8. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/AGENTS.md +443 -45
  9. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/__init__.py +7 -3
  10. pathapi-0.1.2/src/pathapi/_fanout.py +123 -0
  11. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_write.py +4 -0
  12. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/fspath.py +2 -0
  13. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/ftp/_path.py +3 -0
  14. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/__init__.py +24 -4
  15. pathapi-0.1.2/src/pathapi/http/_async_client.py +212 -0
  16. pathapi-0.1.2/src/pathapi/http/_async_follow.py +130 -0
  17. pathapi-0.1.2/src/pathapi/http/_async_response.py +164 -0
  18. pathapi-0.1.2/src/pathapi/http/_async_transports.py +205 -0
  19. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_client.py +38 -5
  20. pathapi-0.1.2/src/pathapi/http/_deadline.py +96 -0
  21. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_follow.py +25 -3
  22. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_headers.py +7 -2
  23. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_proxies.py +4 -1
  24. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_transports.py +51 -43
  25. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/testing/__init__.py +10 -2
  26. pathapi-0.1.2/src/pathapi/path/__init__.py +36 -0
  27. pathapi-0.1.2/src/pathapi/path/_async.py +509 -0
  28. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_path.py +5 -0
  29. pathapi-0.1.2/src/pathapi/protocols/_copytree.py +160 -0
  30. pathapi-0.1.2/src/pathapi/protocols/_moving.py +154 -0
  31. pathapi-0.1.2/src/pathapi/protocols/_removing.py +141 -0
  32. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_transfer.py +9 -0
  33. pathapi-0.1.2/src/pathapi/protocols/_writing.py +153 -0
  34. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/transfer.py +118 -145
  35. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/tree.py +65 -107
  36. pathapi-0.1.2/src/pathapi/sync/_entry.py +567 -0
  37. pathapi-0.1.2/src/pathapi/sync/_syncer.py +529 -0
  38. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/testing/__init__.py +25 -0
  39. pathapi-0.1.2/src/pathapi/testing/_async.py +353 -0
  40. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/testing/_contract.py +42 -1
  41. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/testing/_read.py +38 -24
  42. {pathapi-0.1.0 → pathapi-0.1.2}/tests/mutation/mutants.py +435 -59
  43. pathapi-0.1.2/tests/test_async_contract.py +104 -0
  44. pathapi-0.1.2/tests/test_async_path.py +497 -0
  45. pathapi-0.1.2/tests/test_copy_from_names.py +83 -0
  46. pathapi-0.1.2/tests/test_copy_overwrite.py +141 -0
  47. pathapi-0.1.2/tests/test_copy_symlink_overwrite.py +188 -0
  48. pathapi-0.1.2/tests/test_fanout.py +444 -0
  49. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_end_to_end.py +27 -0
  50. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_http_testing.py +64 -1
  51. pathapi-0.1.2/tests/test_move_overwrite_secures.py +352 -0
  52. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_packaging.py +4 -1
  53. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_public_surface.py +13 -0
  54. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_shipped_header.py +7 -1
  55. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/conftest.py +44 -0
  56. pathapi-0.1.2/tests/transport/test_async_client.py +278 -0
  57. pathapi-0.1.2/tests/transport/test_async_registry.py +290 -0
  58. pathapi-0.1.2/tests/transport/test_async_transport.py +486 -0
  59. pathapi-0.1.2/tests/transport/test_async_units.py +415 -0
  60. pathapi-0.1.2/tests/transport/test_client_deadline.py +184 -0
  61. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_default.py +2 -1
  62. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_prepare.py +14 -1
  63. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_proxy.py +3 -2
  64. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_registry.py +49 -18
  65. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_send.py +14 -1
  66. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/transport_fakes.py +50 -1
  67. pathapi-0.1.0/src/pathapi/path/__init__.py +0 -19
  68. pathapi-0.1.0/src/pathapi/sync/_syncer.py +0 -989
  69. {pathapi-0.1.0 → pathapi-0.1.2}/.gitignore +0 -0
  70. {pathapi-0.1.0 → pathapi-0.1.2}/LICENSE +0 -0
  71. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/archive.md +0 -0
  72. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/checksum.md +0 -0
  73. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/exceptions.md +0 -0
  74. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/ftp.md +0 -0
  75. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/glob.md +0 -0
  76. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/http.md +0 -0
  77. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/mempath.md +0 -0
  78. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/path.md +0 -0
  79. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/protocols.md +0 -0
  80. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/providers.md +0 -0
  81. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/sync.md +0 -0
  82. {pathapi-0.1.0 → pathapi-0.1.2}/docs/api/testing.md +0 -0
  83. {pathapi-0.1.0 → pathapi-0.1.2}/docs/changelog.md +0 -0
  84. {pathapi-0.1.0 → pathapi-0.1.2}/docs/guides/extending.md +0 -0
  85. {pathapi-0.1.0 → pathapi-0.1.2}/examples/archive_members.py +0 -0
  86. {pathapi-0.1.0 → pathapi-0.1.2}/examples/ftp_listing.py +0 -0
  87. {pathapi-0.1.0 → pathapi-0.1.2}/examples/http_request.py +0 -0
  88. {pathapi-0.1.0 → pathapi-0.1.2}/examples/local_and_mem.py +0 -0
  89. {pathapi-0.1.0 → pathapi-0.1.2}/mkdocs.yml +0 -0
  90. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_commit.py +0 -0
  91. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_discard.py +0 -0
  92. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_errorpolicy.py +0 -0
  93. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_filestat.py +0 -0
  94. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_lru.py +0 -0
  95. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_names.py +0 -0
  96. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_permissions.py +0 -0
  97. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_stubs.py +0 -0
  98. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/_tree.py +0 -0
  99. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/__init__.py +0 -0
  100. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_backend.py +0 -0
  101. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_names.py +0 -0
  102. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_pack.py +0 -0
  103. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_path.py +0 -0
  104. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_snapshot.py +0 -0
  105. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_tar.py +0 -0
  106. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_zip.py +0 -0
  107. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/archive/_zipentries.py +0 -0
  108. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/checksum.py +0 -0
  109. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/exceptions.py +0 -0
  110. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/ftp/__init__.py +0 -0
  111. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/ftp/_backend.py +0 -0
  112. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/ftp/_facts.py +0 -0
  113. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/__init__.py +0 -0
  114. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/_entries.py +0 -0
  115. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/_patterns.py +0 -0
  116. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/_recursion.py +0 -0
  117. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/glob/_selection.py +0 -0
  118. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_body.py +0 -0
  119. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_builtin.py +0 -0
  120. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_errors.py +0 -0
  121. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_pool.py +0 -0
  122. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_response.py +0 -0
  123. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_settings.py +0 -0
  124. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/_status.py +0 -0
  125. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/http/testing/__main__.py +0 -0
  126. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/mempath.py +0 -0
  127. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_info.py +0 -0
  128. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_pure.py +0 -0
  129. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_relation.py +0 -0
  130. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/path/_transfer.py +0 -0
  131. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/__init__.py +0 -0
  132. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_bundles.py +0 -0
  133. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_errors.py +0 -0
  134. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_info.py +0 -0
  135. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_relation.py +0 -0
  136. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_removal.py +0 -0
  137. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/_support.py +0 -0
  138. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/checksum.py +0 -0
  139. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/fs.py +0 -0
  140. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/io.py +0 -0
  141. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/links.py +0 -0
  142. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/pure.py +0 -0
  143. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/protocols/resolve.py +0 -0
  144. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/providers.py +0 -0
  145. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/py.typed +0 -0
  146. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/__init__.py +0 -0
  147. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_compare.py +0 -0
  148. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_events.py +0 -0
  149. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_links.py +0 -0
  150. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_offer.py +0 -0
  151. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_paths.py +0 -0
  152. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/sync/_replace.py +0 -0
  153. {pathapi-0.1.0 → pathapi-0.1.2}/src/pathapi/testing/_fixtures.py +0 -0
  154. {pathapi-0.1.0 → pathapi-0.1.2}/tests/capabilities.py +0 -0
  155. {pathapi-0.1.0 → pathapi-0.1.2}/tests/census.py +0 -0
  156. {pathapi-0.1.0 → pathapi-0.1.2}/tests/conftest.py +0 -0
  157. {pathapi-0.1.0 → pathapi-0.1.2}/tests/ftp_loopback.py +0 -0
  158. {pathapi-0.1.0 → pathapi-0.1.2}/tests/hermetic.py +0 -0
  159. {pathapi-0.1.0 → pathapi-0.1.2}/tests/mutation/README.md +0 -0
  160. {pathapi-0.1.0 → pathapi-0.1.2}/tests/mutation/mutation_check.py +0 -0
  161. {pathapi-0.1.0 → pathapi-0.1.2}/tests/mutation/run.py +0 -0
  162. {pathapi-0.1.0 → pathapi-0.1.2}/tests/probes/probe_environment_leak.py +0 -0
  163. {pathapi-0.1.0 → pathapi-0.1.2}/tests/probes/probe_leaks_a_socket.py +0 -0
  164. {pathapi-0.1.0 → pathapi-0.1.2}/tests/probes/probe_session_environment.py +0 -0
  165. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_batch.py +0 -0
  166. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_batch_cost.py +0 -0
  167. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_damage.py +0 -0
  168. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_helpers.py +0 -0
  169. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_lazy.py +0 -0
  170. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_link_listing.py +0 -0
  171. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_links.py +0 -0
  172. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_names.py +0 -0
  173. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_parity.py +0 -0
  174. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_path.py +0 -0
  175. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_rewrite.py +0 -0
  176. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_safety.py +0 -0
  177. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_scale.py +0 -0
  178. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_archive_tree.py +0 -0
  179. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_census.py +0 -0
  180. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_checksum.py +0 -0
  181. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_commit_on_close.py +0 -0
  182. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_composed_protocols.py +0 -0
  183. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_consumer_needs.py +0 -0
  184. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_contract.py +0 -0
  185. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_contract_helpers.py +0 -0
  186. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_copy_move_fidelity.py +0 -0
  187. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_copy_round_trips.py +0 -0
  188. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_destructive_safety.py +0 -0
  189. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_documented_examples.py +0 -0
  190. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_error_predicate.py +0 -0
  191. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_examples_skip.py +0 -0
  192. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_exceptions.py +0 -0
  193. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_filestat.py +0 -0
  194. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp.py +0 -0
  195. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_path.py +0 -0
  196. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_security.py +0 -0
  197. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_utime.py +0 -0
  198. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_ftp_wire.py +0 -0
  199. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_generic_pure_parity.py +0 -0
  200. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_glob.py +0 -0
  201. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_glob_edges.py +0 -0
  202. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_glob_parity.py +0 -0
  203. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_hermetic.py +0 -0
  204. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_io_parity.py +0 -0
  205. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_listing_hints.py +0 -0
  206. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_listing_names.py +0 -0
  207. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_local.py +0 -0
  208. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_local_stat_errors.py +0 -0
  209. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_low_core.py +0 -0
  210. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_low_sync.py +0 -0
  211. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_mempath.py +0 -0
  212. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_move_stats.py +0 -0
  213. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_mro_precedence.py +0 -0
  214. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_parity_io.py +0 -0
  215. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_parity_pure.py +0 -0
  216. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_path_gaps.py +0 -0
  217. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_path_info.py +0 -0
  218. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_path_moving.py +0 -0
  219. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_pathname.py +0 -0
  220. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_properties.py +0 -0
  221. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_protocol_composition.py +0 -0
  222. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_protocol_support.py +0 -0
  223. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_provider_seam.py +0 -0
  224. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_providers.py +0 -0
  225. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_pure_parity.py +0 -0
  226. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_resolve.py +0 -0
  227. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_same_filesystem.py +0 -0
  228. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_smoke.py +0 -0
  229. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_symlink_target.py +0 -0
  230. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_sync.py +0 -0
  231. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_sync_cost.py +0 -0
  232. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_sync_mtime.py +0 -0
  233. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_sync_safety.py +0 -0
  234. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_walk.py +0 -0
  235. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_walk_junction.py +0 -0
  236. {pathapi-0.1.0 → pathapi-0.1.2}/tests/test_write_discard.py +0 -0
  237. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_errors.py +0 -0
  238. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_peer_text.py +0 -0
  239. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_redirect_origin.py +0 -0
  240. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_redirects.py +0 -0
  241. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_reply_limits.py +0 -0
  242. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_urls.py +0 -0
  243. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_client_write_redirects.py +0 -0
  244. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_body.py +0 -0
  245. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_pool.py +0 -0
  246. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/test_transport_tls.py +0 -0
  247. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/transport_distribution.py +0 -0
  248. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/transport_guard.py +0 -0
  249. {pathapi-0.1.0 → pathapi-0.1.2}/tests/transport/transport_wire.py +0 -0
  250. {pathapi-0.1.0 → pathapi-0.1.2}/tests/waits.py +0 -0
@@ -7,6 +7,127 @@ and this project adheres to [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [0.1.2] - 2026-10-17
11
+
12
+ ### Added
13
+
14
+ - **`max_workers` for recursive `copy()`, `rm()` and `PathSyncer`.** `copy()` (and
15
+ `copy_into()`), `rm()` and `PathSyncer(...)` take `max_workers=None`. `None`, the
16
+ default, is one thread, exactly as before. An int of 2 or more hands the files
17
+ and links of each directory to a pool of at most that many threads, which
18
+ helps a backend that pays a round trip per entry. It applies to a class that sets
19
+ the new class attribute `_threadsafe = True` (`LocalPath` and `FTPPath` do); any other
20
+ class, including every class of another distribution until it sets the attribute,
21
+ runs serially whatever was asked. Callbacks (`progress`, `on_error`, `ignore_error`,
22
+ the sync `hook`, follow policies) run one at a time but on pool threads, in no
23
+ fixed order, and the error that ends the operation is the first to happen. A value
24
+ below 1 is `ValueError`, one that is not an int is `TypeError`. `PathContract`
25
+ has a `supports_concurrent_use` switch for the matching test.
26
+ - **`AsyncPath` and `AsyncFile`: a path for use from an event loop.** `AsyncPath(path)`
27
+ wraps any `Path`; the pure members (`name`, `parent`, `/`, `with_*()`, `match()`, ...)
28
+ are plain and give `AsyncPath`, every method that does I/O is a coroutine of the same
29
+ name and arguments, `iterdir()`, `glob()`, `rglob()` and `walk()` return async iterators,
30
+ and `open()` returns an object that is awaited or used with `async with` and gives an
31
+ `AsyncFile` (`read`, `readline`, `write`, `seek`, ..., `aclose()`). They are exported
32
+ from `pathapi` and `pathapi.path`. Each call runs the wrapped path's sync method on a
33
+ worker thread (`asyncio.to_thread()`), so every backend can be used from a loop and the
34
+ loop is not blocked; cancelling an `await` abandons the wait and the worker finishes the
35
+ call. Recursive `copy()`, `rm()` and `move()` are one such call, so `max_workers=` is the
36
+ sync fan-out. `supports()` and `capabilities()` of an `AsyncPath` answer for the wrapped
37
+ path. A subclass gets its own class for every path derived from it, built by the
38
+ classmethod `AsyncPath._adopt(path)` (`cls(path)`), which a subclass with another
39
+ constructor overrides.
40
+ - **`AsyncHTTPClient`: the HTTP client for an event loop.** `pathapi.http.AsyncHTTPClient`
41
+ is the sibling of `HTTPClient`: the same constructor arguments, redirect and
42
+ `Authorization` rules, `timeout`, `deadline`, reply limit and errors, with `request()`
43
+ and `redirect()` as coroutines, `aclose()` and `async with`. It returns an
44
+ `AsyncHTTPResponse` (`await read()`, `iter_bytes()` as an async iterator, `await
45
+ aclose()`). It sends through an async transport, an object with `async send()` and
46
+ `async aclose()`. The default one, `ThreadedTransport`, runs the synchronous transport
47
+ `PATHAPI_HTTP_TRANSPORT` names on worker threads, so every synchronous transport works
48
+ from a loop; a native one is declared in the entry-point group
49
+ `pathapi.http_async_transports` and chosen by `PATHAPI_HTTP_ASYNC_TRANSPORT` (same
50
+ rules and messages as the synchronous registry; `async_transport_names()`,
51
+ `get_async_transport()` and `default_async_transport()`).
52
+ - **The async half of the transport suite.** `python -m pathapi.http.testing
53
+ --http-async-transports NAME` holds an async transport to the promises of the
54
+ synchronous suite (`test_async_transport.py`, `test_async_client.py`). Without the
55
+ option the async tests run over `builtin` when `--http-transports` is left out too,
56
+ and are not collected when it names a transport: a distribution that runs
57
+ `--http-transports NAME` gets the tests it got before.
58
+ - **`pathapi.testing.AsyncPathContract`.** The pytest contract of `AsyncPath` over a
59
+ backend: it takes the `root` fixture of `PathContract` and checks each coroutine
60
+ against the sync path's result, with `asyncio.run()` and no extra test dependency. Its
61
+ class attribute `async_path_class` (default `AsyncPath`) names the class under test, so a
62
+ subclass of `AsyncPath` is tested with the paths it derives.
63
+
64
+ ### Changed
65
+
66
+ - **An unknown HTTP transport name is told where to look, not which package to
67
+ install.** `get_transport()` and `PATHAPI_HTTP_TRANSPORT` raise `ValueError` for a
68
+ name no installed distribution declares in the group `pathapi.http_transports`,
69
+ naming the group and suggesting `pathapi-NAME` as a name to look for. The error is
70
+ a `ValueError` that is also an `ImportError`, so code that caught either still
71
+ does. `requests` is no longer special: `transport_names()` lists it only when an
72
+ installed distribution declares it. The refusal of a proxy that is not `http://`
73
+ points at a transport of that group that supports it. Install the distribution
74
+ that provides the transport.
75
+
76
+ ### Fixed
77
+
78
+ - **A `nan` or infinite HTTP timeout is refused with `ValueError`.** `timeout=float("nan")`
79
+ passed the check, and the built-in transport then failed in `socket.settimeout()` after
80
+ it had opened the connection, which leaked the socket. Pass a positive finite number of
81
+ seconds, or `None`.
82
+ - **`move(overwrite=True)` no longer destroys the old target when the move fails.**
83
+ An existing target was removed before the source was renamed or copied over it, so
84
+ a failing rename or copy lost the target. The target now stays until the source
85
+ has taken its place: a local file is replaced atomically, `rename()` is tried
86
+ over a file elsewhere, a backend that refuses has the target renamed aside and put
87
+ back on failure, and a copy and delete finishes the new file or tree beside the
88
+ target first. A directory target is swapped in by rename where its class has one.
89
+ If you list the directory while a move runs, you may see a hidden
90
+ `.NAME.<hex>.pathapi-tmp` sibling.
91
+ - **`copy(follow_symlinks=False, overwrite=True)` of a link no longer destroys the old
92
+ target when the new link cannot be made.** The existing target was unlinked before
93
+ `symlink_to()`, so a refused link lost it. Where the target's class can rename, the
94
+ link is now made in a hidden sibling and renamed over the target; a class that
95
+ cannot rename still unlinks first. A target class that cannot make links now raises
96
+ `NotImplementedError` before anything is removed.
97
+
98
+ ## [0.1.1] - 2026-10-16
99
+
100
+ ### Added
101
+
102
+ - **`deadline` for a whole HTTP request.** `HTTPClient(deadline=...)` and the
103
+ `deadline=` keyword of `request()` and `redirect()` take the seconds a request may
104
+ take, redirects and the reading of the body included (`None`, the default, is no
105
+ limit). Each wait's timeout is held to the time that remains and `TimeoutError` is
106
+ raised once it has passed; `timeout` still bounds each wait. A read already blocked
107
+ can overrun the deadline by at most its timeout. A value that is not a number is
108
+ `TypeError`, one that is not positive `ValueError`. It works over every transport.
109
+
110
+ ### Fixed
111
+
112
+ - **`copy(overwrite=True)` no longer destroys the old target when the copy fails.**
113
+ An existing file was removed before the new content was written, so a source that
114
+ failed half way left no file, or a partial one. The old file is kept until the
115
+ new content is complete: it is written to a hidden `.NAME.<hex>.pathapi-tmp`
116
+ sibling and renamed over it, and a class whose write stream commits on close is
117
+ written to directly. A class that cannot rename writes the sibling and then writes
118
+ it over the target. If you list the directory while a copy runs, you may see that
119
+ sibling.
120
+ - **`pathlib.Path(src).copy(target)` (3.14) onto a `Path` of this library refuses
121
+ unsafe child names.** `_copy_from()` joined every listed name onto the target; a
122
+ name that is not one component (`..`, a separator, on a Windows target a drive or
123
+ a trailing dot) now raises `ValueError` before anything is written for it, as
124
+ `copy(recursive=True)` does. A source that lists such names needs a copy that
125
+ skips them.
126
+ - **The shipped transport suite names the file of its tests wherever it is run
127
+ from.** `python -m pathapi.http.testing` (and `run()`) roots pytest at the suite's
128
+ own directory unless `--rootdir` is given, so node ids keep their file when the
129
+ checkout and the installed package are on different drives.
130
+
10
131
  ## [0.1.0] - 2026-10-15
11
132
 
12
133
  Published as `pathlib-next` up to 0.10.0; from 0.1.0 as `pathapi`. The released
@@ -3539,7 +3660,9 @@ behaviour with what to do about it.
3539
3660
  - Sync error handling.
3540
3661
  - Generic Path Protocol based pathlib implementation for URI paths with file access support for sftp, http, file schemes.
3541
3662
 
3542
- [Unreleased]: https://github.com/jose-pr/pathapi/compare/v0.1.0...HEAD
3663
+ [Unreleased]: https://github.com/jose-pr/pathapi/compare/v0.1.2...HEAD
3664
+ [0.1.2]: https://github.com/jose-pr/pathapi/compare/v0.1.1...v0.1.2
3665
+ [0.1.1]: https://github.com/jose-pr/pathapi/compare/v0.1.0...v0.1.1
3543
3666
  [0.1.0]: https://github.com/jose-pr/pathapi/compare/v0.10.0...v0.1.0
3544
3667
  [0.10.0]: https://github.com/jose-pr/pathlib-next/compare/v0.9.14...v0.10.0
3545
3668
  [0.9.14]: https://github.com/jose-pr/pathlib-next/compare/v0.9.13...v0.9.14
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pathapi
3
- Version: 0.1.0
3
+ Version: 0.1.2
4
4
  Summary: Generic Path Protocol based pathlib
5
5
  Project-URL: Homepage, https://github.com/jose-pr/pathapi/
6
6
  Project-URL: Documentation, https://jose-pr.github.io/pathapi/
@@ -78,8 +78,10 @@ where the backend has a faster way.
78
78
  - **`pathapi.http`**: `HTTPClient` and a transport contract for HTTP
79
79
  requests on the standard library (redirects that never carry
80
80
  `Authorization` to another origin, TLS verified by default, a `proxies=`
81
- provider); the `requests` library plugs in as a transport through the
82
- `pathapi-requests` distribution.
81
+ provider); another HTTP library plugs in as a transport through the
82
+ `pathapi.http_transports` entry-point group. `AsyncHTTPClient` is the same
83
+ client for an event loop, over async transports registered in
84
+ `pathapi.http_async_transports`.
83
85
  - **`PathSyncer`**: one-way, content-compared tree sync between any two
84
86
  implementations, with dry runs and event hooks.
85
87
  - **Extensible**: subclass `Path` for any resource, and verify it with the
@@ -90,10 +92,6 @@ Paths for other places are separate distributions built on this one:
90
92
  | Distribution | Provides |
91
93
  | --- | --- |
92
94
  | [`uripath`](https://github.com/jose-pr/uripath) | `URIPath`: one class for any URI, with the schemes `file:`, `data:`, `ftp:`, `zip:`, `tar:`, `http:` and `dav:`, and the `uripath` command |
93
- | [`sftppath`](https://github.com/jose-pr/sftppath) | `SFTPPath` (paramiko or asyncssh) |
94
- | [`objpath`](https://github.com/jose-pr/objpath) | `S3Path`, `GSPath` and `AZPath` (Amazon S3, Google Cloud Storage, Azure Blob Storage) |
95
- | [`repopath`](https://github.com/jose-pr/repopath) | `RepoPath`, `GitHubPath` and `GitLabPath` (the REST APIs of the hosting services) |
96
- | [`pathapi-requests`](https://github.com/jose-pr/pathapi-requests) | `RequestsTransport`, the HTTP transport that sends through a `requests.Session` |
97
95
 
98
96
  ## Installation
99
97
 
@@ -178,7 +176,7 @@ print(sorted(p.name for p in Path("site-copy").iterdir()))
178
176
  | `pathapi.mempath` | `MemPath` in-memory filesystem |
179
177
  | `pathapi.ftp` | `FTPPath`, `FTPBackend` (FTP and FTPS on `ftplib`) |
180
178
  | `pathapi.archive` | `ZipPath`, `TarPath`, `ArchivePath` (members of a zip or tar file), `make_archive`, `unpack_archive` |
181
- | `pathapi.http` | `HTTPClient`, `HTTPTransport`, `HTTPResponse` (HTTP requests on `http.client`) |
179
+ | `pathapi.http` | `HTTPClient`, `HTTPTransport`, `HTTPResponse`, `AsyncHTTPClient`, `ThreadedTransport` (HTTP requests on `http.client`, and from an event loop) |
182
180
  | `pathapi.protocols` | `Stat`, `Chmod`, `BinaryOpen`, `NativeChecksum` |
183
181
  | `pathapi.sync` | `PathSyncer`, `SyncEvent`, `PathAndStat` |
184
182
  | `pathapi.glob` | the glob engine behind `Path.glob()` |
@@ -192,13 +190,33 @@ The full reference is on the [documentation site](https://jose-pr.github.io/path
192
190
  an agent-oriented API summary ships inside the package as
193
191
  `pathapi/AGENTS.md`.
194
192
 
193
+ ## Async
194
+
195
+ `AsyncPath(path)` wraps any `pathapi` path for use inside an event loop: the
196
+ pure members stay plain, every method that does I/O is a coroutine of the same
197
+ name, listings are async iterators and `open()` gives an `AsyncFile`. Each call
198
+ runs the sync method on a worker thread, so every backend works at once and the
199
+ loop is never blocked; it is not a native async client.
200
+
201
+ ```python
202
+ import asyncio
203
+ from pathapi import AsyncPath, LocalPath
204
+
205
+ async def main():
206
+ root = AsyncPath(LocalPath("."))
207
+ async for entry in root.iterdir():
208
+ print(entry.name, await entry.is_dir())
209
+
210
+ asyncio.run(main())
211
+ ```
212
+
195
213
  ## Extending
196
214
 
197
215
  Subclass `Path` for a resource of your own (`MemPath` is the reference), as
198
216
  worked through in
199
217
  [Extending](https://jose-pr.github.io/pathapi/guides/extending/).
200
218
  `pathapi.testing` provides the pytest contracts (`PurePathContract`,
201
- `ReadPathContract`, `PathContract`) and `populate_fixture_tree()` that every
219
+ `ReadPathContract`, `PathContract`, `AsyncPathContract`) and `populate_fixture_tree()` that every
202
220
  class of the package and of the distributions above is verified with.
203
221
 
204
222
  ## Supported Python versions
@@ -41,8 +41,10 @@ where the backend has a faster way.
41
41
  - **`pathapi.http`**: `HTTPClient` and a transport contract for HTTP
42
42
  requests on the standard library (redirects that never carry
43
43
  `Authorization` to another origin, TLS verified by default, a `proxies=`
44
- provider); the `requests` library plugs in as a transport through the
45
- `pathapi-requests` distribution.
44
+ provider); another HTTP library plugs in as a transport through the
45
+ `pathapi.http_transports` entry-point group. `AsyncHTTPClient` is the same
46
+ client for an event loop, over async transports registered in
47
+ `pathapi.http_async_transports`.
46
48
  - **`PathSyncer`**: one-way, content-compared tree sync between any two
47
49
  implementations, with dry runs and event hooks.
48
50
  - **Extensible**: subclass `Path` for any resource, and verify it with the
@@ -53,10 +55,6 @@ Paths for other places are separate distributions built on this one:
53
55
  | Distribution | Provides |
54
56
  | --- | --- |
55
57
  | [`uripath`](https://github.com/jose-pr/uripath) | `URIPath`: one class for any URI, with the schemes `file:`, `data:`, `ftp:`, `zip:`, `tar:`, `http:` and `dav:`, and the `uripath` command |
56
- | [`sftppath`](https://github.com/jose-pr/sftppath) | `SFTPPath` (paramiko or asyncssh) |
57
- | [`objpath`](https://github.com/jose-pr/objpath) | `S3Path`, `GSPath` and `AZPath` (Amazon S3, Google Cloud Storage, Azure Blob Storage) |
58
- | [`repopath`](https://github.com/jose-pr/repopath) | `RepoPath`, `GitHubPath` and `GitLabPath` (the REST APIs of the hosting services) |
59
- | [`pathapi-requests`](https://github.com/jose-pr/pathapi-requests) | `RequestsTransport`, the HTTP transport that sends through a `requests.Session` |
60
58
 
61
59
  ## Installation
62
60
 
@@ -141,7 +139,7 @@ print(sorted(p.name for p in Path("site-copy").iterdir()))
141
139
  | `pathapi.mempath` | `MemPath` in-memory filesystem |
142
140
  | `pathapi.ftp` | `FTPPath`, `FTPBackend` (FTP and FTPS on `ftplib`) |
143
141
  | `pathapi.archive` | `ZipPath`, `TarPath`, `ArchivePath` (members of a zip or tar file), `make_archive`, `unpack_archive` |
144
- | `pathapi.http` | `HTTPClient`, `HTTPTransport`, `HTTPResponse` (HTTP requests on `http.client`) |
142
+ | `pathapi.http` | `HTTPClient`, `HTTPTransport`, `HTTPResponse`, `AsyncHTTPClient`, `ThreadedTransport` (HTTP requests on `http.client`, and from an event loop) |
145
143
  | `pathapi.protocols` | `Stat`, `Chmod`, `BinaryOpen`, `NativeChecksum` |
146
144
  | `pathapi.sync` | `PathSyncer`, `SyncEvent`, `PathAndStat` |
147
145
  | `pathapi.glob` | the glob engine behind `Path.glob()` |
@@ -155,13 +153,33 @@ The full reference is on the [documentation site](https://jose-pr.github.io/path
155
153
  an agent-oriented API summary ships inside the package as
156
154
  `pathapi/AGENTS.md`.
157
155
 
156
+ ## Async
157
+
158
+ `AsyncPath(path)` wraps any `pathapi` path for use inside an event loop: the
159
+ pure members stay plain, every method that does I/O is a coroutine of the same
160
+ name, listings are async iterators and `open()` gives an `AsyncFile`. Each call
161
+ runs the sync method on a worker thread, so every backend works at once and the
162
+ loop is never blocked; it is not a native async client.
163
+
164
+ ```python
165
+ import asyncio
166
+ from pathapi import AsyncPath, LocalPath
167
+
168
+ async def main():
169
+ root = AsyncPath(LocalPath("."))
170
+ async for entry in root.iterdir():
171
+ print(entry.name, await entry.is_dir())
172
+
173
+ asyncio.run(main())
174
+ ```
175
+
158
176
  ## Extending
159
177
 
160
178
  Subclass `Path` for a resource of your own (`MemPath` is the reference), as
161
179
  worked through in
162
180
  [Extending](https://jose-pr.github.io/pathapi/guides/extending/).
163
181
  `pathapi.testing` provides the pytest contracts (`PurePathContract`,
164
- `ReadPathContract`, `PathContract`) and `populate_fixture_tree()` that every
182
+ `ReadPathContract`, `PathContract`, `AsyncPathContract`) and `populate_fixture_tree()` that every
165
183
  class of the package and of the distributions above is verified with.
166
184
 
167
185
  ## Supported Python versions
@@ -17,6 +17,7 @@ Narrower suites:
17
17
  ```bash
18
18
  python benchmarks/bench.py syncer
19
19
  python benchmarks/bench.py recursive-matrix
20
+ python benchmarks/bench.py fanout
20
21
  ```
21
22
 
22
23
  ## Saved Results
@@ -47,6 +48,9 @@ claims in release notes come from CI runs.
47
48
  - `LocalPath` vs `pathlib.Path` on common local operations
48
49
  - `PathSyncer` on local trees, and recursive copy/remove on local and
49
50
  in-memory trees
51
+ - the `fanout` suite: recursive copy, remove and a first sync of a tree of
52
+ small files on a loopback FTP server and on a simulated slow backend (a
53
+ `MemPath` that waits 2 ms per call, not a measurement of a real one)
50
54
 
51
55
  The benchmarks of the classes in other distributions (URIs, HTTP listings,
52
56
  SFTP, object stores) live with those distributions.
@@ -70,8 +70,8 @@ these operations itself always keeps its own implementation.
70
70
  | `FTPPath.stat().st_mode` | pathlib reports the real mode | Taken from MLSD `unix.mode`, else derived from the `perm` fact (read/write bits only). | MLSD is the only mode source most servers offer. |
71
71
  | `Path.touch(mode=None, exist_ok=True)` on non-local backends | `pathlib.Path.touch(mode=0o666)` creates with the mode masked by the umask and updates an existing file's mtime | `mode` defaults to `None`: a new file keeps the backend's default permissions, and an explicit `mode` is applied with `chmod()` unmasked (a remote umask is unknown); a class that cannot `chmod()` raises `NotImplementedError` and removes the file it created, so a mode that was asked for is never silently dropped. An existing file's access and modification times are set to now through the overridable `_utime()` hook (`MemPath` and `FTPPath` on a server that lists `MFMT` implement it: whole seconds, no access time on FTP); a class that cannot set a time leaves the file as it is. `LocalPath` uses pathlib's `touch()` unchanged. | Always chmod'ing 0o666 left new files world-writable on FTP and other remote backends; a remote backend cannot apply the local umask. |
72
72
  | `Path.__iter__` | `pathlib.Path` is not iterable (no `__iter__`) | `iter(path)` is `path.iterdir()` | Deliberate extension for ergonomic `for child in path:` loops. **Caution:** on remote backends (FTP) this is a network call. **User decision, 2026-07-11.** |
73
- | `Path.copy(target, ...)` | CPython 3.14 `Path.copy(target, *, follow_symlinks=True, preserve_metadata=False)`: overwrites an existing file, copies a directory tree without being asked, returns the new path | Ours predates 3.14. Signature: `copy(target, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None, on_error=None)`; returns `None`. `ignore_error=` keeps its notify-only callable, and `on_error=` is `rm()`'s predicate `(error, path) -> bool` for a child error of a recursive copy (`True` skips the entry, `False` raises, a predicate that raises ends the copy with its own exception); passing both raises `TypeError`. An existing target raises `FileExistsError` unless `overwrite=True`; a directory needs `recursive=True`. `overwrite=True` unlinks an existing non-directory target with `unlink(missing_ok=True)`, but only once the source is open (a missing or unreadable source never destroys or creates the target; a copy that fails mid-stream removes its partial target; a backend whose write replaces the file and which cannot delete makes `unlink(missing_ok=True)` return without removing); copying onto the same file, or a directory into itself, raises `OSError(EINVAL)`: two files of this machine (`LocalPath`, a subclass, `pathlib.Path`, a class that names a local file through `filepath`) are compared by identity whatever their classes, a type that answers the overridable `_node_key()` hook (`MemPath`, archive members) by its normalized node, and anything else by `samefile()` where `stat()` has `st_dev`/`st_ino`, otherwise equal paths of one type that the overridable `_same_filesystem()` hook places on the same host or store (two paths of one class that name one server, built separately, count as the same file); `preserve_metadata` defaults to **True** (opposite of 3.14) and only propagates `st_mode`, not timestamps/xattrs: the permission bits of a file, and of each directory of a recursive copy once its children are copied (as `shutil.copytree` and 3.14 do), with the setuid, setgid and sticky bits dropped when source and target are different classes; a recreated symlink (`follow_symlinks=False`) carries only its target text, not its own mode, times or owner; a dangling symlink at the target is written through, as `shutil.copyfile` and 3.14 do, with or without `overwrite`, while a live one counts as existing and `overwrite=True` replaces the link itself | Argument names aligned with 3.14 where cheap; `preserve_metadata=True` default kept for backward compat with this method's pre-existing (pre-3.14-alignment) behavior of always copying the mode bits. Full metadata preservation (timestamps, xattrs) is not implemented. |
74
- | `Path.move(target, ...)` | CPython 3.14 `Path.move(target)`: `os.replace` semantics (replaces an existing file), falls back to copy + delete across filesystems, returns the new path | Ours predates 3.14 and keeps `overwrite=False`: an existing target raises `FileExistsError`. Returns whatever `rename()` returns (`None` on the copy fallback). Our own extension: tries `rename()` (by default only onto a path of the same class; `LocalPath` and classes for URIs decide for themselves), falls back to copy+unlink, which recreates a symlink and the links inside a moved tree wherever the target can hold links, so the result does not depend on whether `rename()` was available. Validates before touching the target: a missing source raises `FileNotFoundError`, a file onto a directory raises `IsADirectoryError`, a target that holds the source (`OSError(ENOTEMPTY)`), lies inside it (`OSError(EINVAL)`) or is a link to or from the same file (`OSError(EINVAL)`) is refused with both trees intact, and the same file under another spelling (a case-only rename, another class, a relative `MemPath`, another spelling of the same archive) is renamed in place. "Same" is decided as for `copy()`. Two hard links of one file are two names: an existing target needs `overwrite=True`, and the source name is then removed. `overwrite=True` replaces a local file target atomically (`os.replace`); on other backends it unlinks the target immediately before `rename()`, so a failing rename can still lose it. | N/A -- pure extension, no pathlib method to diverge from. Removing the target before checking the source deleted it on a typo or a locked source. |
73
+ | `Path.copy(target, ...)` | CPython 3.14 `Path.copy(target, *, follow_symlinks=True, preserve_metadata=False)`: overwrites an existing file, copies a directory tree without being asked, returns the new path | Ours predates 3.14. Signature: `copy(target, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None, on_error=None)`; returns `None`. `ignore_error=` keeps its notify-only callable, and `on_error=` is `rm()`'s predicate `(error, path) -> bool` for a child error of a recursive copy (`True` skips the entry, `False` raises, a predicate that raises ends the copy with its own exception); passing both raises `TypeError`. An existing target raises `FileExistsError` unless `overwrite=True`; a directory needs `recursive=True`. `overwrite=True` never removes or truncates the old file before the new content is secured: a missing, unreadable or half-read source leaves the target as it was, and a new file whose copy fails is removed. The new content is finished in a hidden temporary sibling and renamed over the old file (`os.replace` for a local path); a class that cannot rename writes the temporary sibling and then writes it over the target, which truncates it in place for the moment of that second write (the temporary file is kept and named if that fails), and a class whose write stream commits on close is written to directly, since a failed write leaves the old file. 3.14's `Path.copy()` truncates an existing target in place; copying onto the same file, or a directory into itself, raises `OSError(EINVAL)`: two files of this machine (`LocalPath`, a subclass, `pathlib.Path`, a class that names a local file through `filepath`) are compared by identity whatever their classes, a type that answers the overridable `_node_key()` hook (`MemPath`, archive members) by its normalized node, and anything else by `samefile()` where `stat()` has `st_dev`/`st_ino`, otherwise equal paths of one type that the overridable `_same_filesystem()` hook places on the same host or store (two paths of one class that name one server, built separately, count as the same file); `preserve_metadata` defaults to **True** (opposite of 3.14) and only propagates `st_mode`, not timestamps/xattrs: the permission bits of a file, and of each directory of a recursive copy once its children are copied (as `shutil.copytree` and 3.14 do), with the setuid, setgid and sticky bits dropped when source and target are different classes; a recreated symlink (`follow_symlinks=False`) carries only its target text, not its own mode, times or owner, and with `overwrite=True` it is made in a hidden sibling and renamed over the old entry where the class can rename (a failed link leaves the old entry), while a class that cannot rename unlinks the old entry first and so loses it if the new link then fails; a dangling symlink at the target is written through, as `shutil.copyfile` and 3.14 do, with or without `overwrite`, while a live one counts as existing and `overwrite=True` replaces the link itself | Argument names aligned with 3.14 where cheap; `preserve_metadata=True` default kept for backward compat with this method's pre-existing (pre-3.14-alignment) behavior of always copying the mode bits. Full metadata preservation (timestamps, xattrs) is not implemented. |
74
+ | `Path.move(target, ...)` | CPython 3.14 `Path.move(target)`: `os.replace` semantics (replaces an existing file), falls back to copy + delete across filesystems, returns the new path | Ours predates 3.14 and keeps `overwrite=False`: an existing target raises `FileExistsError`. Returns whatever `rename()` returns (`None` on the copy fallback). Our own extension: tries `rename()` (by default only onto a path of the same class; `LocalPath` and classes for URIs decide for themselves), falls back to copy+unlink, which recreates a symlink and the links inside a moved tree wherever the target can hold links, so the result does not depend on whether `rename()` was available. Validates before touching the target: a missing source raises `FileNotFoundError`, a file onto a directory raises `IsADirectoryError`, a target that holds the source (`OSError(ENOTEMPTY)`), lies inside it (`OSError(EINVAL)`) or is a link to or from the same file (`OSError(EINVAL)`) is refused with both trees intact, and the same file under another spelling (a case-only rename, another class, a relative `MemPath`, another spelling of the same archive) is renamed in place. "Same" is decided as for `copy()`. Two hard links of one file are two names: an existing target needs `overwrite=True`, and the source name is then removed. `overwrite=True` keeps an existing target until what replaces it is in place: a local file is replaced atomically (`os.replace`); elsewhere the source is renamed over it, or, where the backend refuses, the target is renamed aside, the source renamed in and the target put back if that fails; a copy and delete finishes the new file or tree beside the target first (a directory target is swapped in by rename where its class has one, and otherwise removed only after the copy beside it is complete). A hidden `.NAME.<hex>.pathapi-tmp` sibling exists meanwhile. | N/A -- pure extension, no pathlib method to diverge from. Removing the target before checking the source deleted it on a typo or a locked source. |
75
75
  | `Path.is_junction()` / `is_mount()` / `is_dir_binding()` | `pathlib` has `is_junction()` (3.12+) and `is_mount()`, but only for local paths and with no shared concept | Both are part of the `Path` contract (False by default, answered by `LocalPath`), and `is_dir_binding()` is their union: a directory that is another tree's second NAME rather than part of this one. A junction is the Windows spelling of a bind mount, and neither is a symlink -- `is_symlink()` is False and a non-following stat reports a plain directory. | Walking code needs the distinction: a symlink announces itself and every walker here already declines to follow one, while a binding looks exactly like an ordinary directory and its contents belong to whoever mounted or junctioned it. |
76
76
  | `Path.rm(recursive=, missing_ok=, ignore_error=)` | Not in `pathlib` (closest: `shutil.rmtree`) | Our own extension. Recursive removal deletes bottom-up and uses non-following stat/listing metadata, so a symlink to a directory -- or a binding (`is_dir_binding()`: a Windows junction, a mount point), which a non-following stat still reports as a directory -- is unlinked or `rmdir()`'d rather than traversed -- `rm -r` semantics. `follow_symlinks=`/`follow_binds=` select that per call: `False` (default), `True` (remove what is behind it, then the entry: a followed symlink is unlinked), `None` (leave it), or a callable asked per entry -- the same keyword `stat()`/`walk()`/`copy()` use for the same idea. They decide the path `rm()` is called on as they decide an entry met on the way down. A policy that is not one of those, or a callable's answer that is not, raises `ValueError` and is never offered to `ignore_error`. Every class that overrides `rm()` takes both keywords; a backend with no symlinks or bindings decides nothing with them, and one with a concurrent native walk runs it only for `follow_symlinks=False`. `rmdir()` on a live mount fails, which is the intended answer. | N/A -- pure extension. Non-following recursive deletion avoids deleting through symlinked directory targets and lets backends with metadata-rich listings remove trees without a stat round trip per child. |
77
77
  | `Path.symlink_to(target, target_is_directory=False, *, force=False)` | `pathlib.Path.symlink_to(target, target_is_directory=False)` -- raises `FileExistsError` if anything already exists at the link path | Adds a keyword-only `force=`. `force=False` (the default) is stdlib-exact. `force=True` unlinks an existing **non-directory** entry at the link path first, then creates the symlink; an existing *directory* is never removed and the underlying error propagates. Not atomic: no filesystem or transport offers "replace a symlink" as one operation, so the path briefly does not exist between the unlink and the symlink. | Additive extension (an extra optional kwarg, per the parity contract). No backend can offer this atomically, so every consumer was re-implementing the same unlink-then-symlink dance -- it is path semantics, not transport semantics, so it belongs at the `Path` layer where one implementation serves every backend. Backends implement only the `_symlink_to()` primitive (same `_mkdir`/`_open` shape) and get `force=` for free. Because no stdlib version accepts the keyword, `symlink_to` is in `_OPERATION_NAMES` so `LocalPath` honors it too. |
@@ -94,7 +94,7 @@ these operations itself always keeps its own implementation.
94
94
  | `FTPPath`, `ZipPath`, `TarPath`, `ArchivePath`: `==`, rooted and relative spellings | `PurePosixPath("a") != PurePosixPath("/a")`, and equal paths are the same file because there is one filesystem | `==` and `hash` compare the names only (as `MemPath` does), so two paths of different servers or archives are equal; `copy()`, `move()` and `PathSyncer` ask the overridable `_same_filesystem()` and `_node_key()` hooks before they call two equal paths one file. `a/b` and `/a/b` are two spellings of one node, because the tree has no working directory and is read from its root: the server root for an `FTPPath`, the archive root for the archive paths (`_node_key()` is equal; `==` is not). An `FTPPath` sends `..` as written, and an archive path applies it to the member name and refuses a name that leaves the archive. | A remote tree and an archive hold names, not a current directory, and carry their connection or handle in the object (the model is `MemPath`). Making `==` compare the server too would make a path unequal to its own copy on a second connection, and would break `is_relative_to()`, which decides by `==`. |
95
95
  | `full_match()` on a generic path, with a bracket expression | `PurePath.full_match` (3.13+) compiles the whole pattern to one regular expression, so a negated bracket such as `[!x]` can match a separator: `PurePosixPath("a/b").full_match("a[!x]b")` and `PurePosixPath("/").full_match("[!a]")` are `True` | `Pathname.full_match` compares one component at a time and a bracket expression never matches a separator: both are `False` | A component-wise automaton keeps `**` linear in the path and the pattern (a regular expression backtracks exponentially on repeated `**`). A pattern that reads a separator out of a bracket is not a pattern anyone writes on purpose. Everything else, the root included, answers as `pathlib` does. |
96
96
  | `copy_into()`, `move_into()`, `replace()` | 3.14: `copy_into(target_dir, **kwargs)` and `move_into(target_dir)` call `copy()`/`move()` and return the new path; `copy()` overwrites an existing file and copies trees. `replace(target)` is `os.replace()` and returns the target | Defined once on `Path` over `copy()`/`move()`, on every backend and version. `copy_into(target_dir, *, overwrite=False, follow_symlinks=True, preserve_metadata=True, recursive=False, ignore_error=None, progress=None, on_error=None)` has the keywords and **the defaults of `copy()`**: an existing target needs `overwrite=True` and a directory needs `recursive=True`. `move_into(target_dir, *, overwrite=False)` has those of `move()`. Both return the new path. `replace(target)` is `move(target, overwrite=True)` and returns `target`: a directory is replaced only when empty (`OSError(ENOTEMPTY)` otherwise, nothing removed). On 3.14 `LocalPath` uses these rather than stdlib's `copy_into`/`move_into`; `LocalPath.replace` stays stdlib's `os.replace()` on every version. | One behaviour on every backend, interpreter and `Path` subclass, and one that cannot overwrite or recurse by accident (3.14's default replaces a file silently). A caller that wants 3.14's behaviour passes `overwrite=True, recursive=True`. |
97
- | `Path.info` and `pathlib.Path(...).copy(target)` onto a `Path` of this library (3.14) | 3.14 `Path.info` is a `PathInfo` kept on the path (its answers are cached for the life of the path object); `pathlib.Path.copy(target)` calls `target._copy_from(source, **kwargs)` on any target with `with_segments()`, so a target without it raises `AttributeError` | `info` is defined on every `Path`, on every version: an object with `exists()`, `is_dir()`, `is_file()` (each with `follow_symlinks=True`) and `is_symlink()`, over at most one following and one non-following `stat()`. A path holds no state, so its answers are cached on the `info` object, not on the path: each `path.info` asks again. `LocalPath` keeps stdlib's `info` on 3.14. `Path._copy_from(source, follow_symlinks=True, preserve_metadata=False)` makes a stdlib path's `copy()` work onto any `Path` of this library (overwrites a file and raises on an existing directory, as 3.14 does; `preserve_metadata` copies the permission bits and the modification time where the class can set them). A class that mixes a stdlib path keeps stdlib's. The private `pathlib` ABCs (`_ReadablePath`, ...) and `__open_rb__`/`__open_wb__` are not implemented. | Code written for 3.14's `PathInfo` and `Path.copy()` accepts these paths. Only the hooks `Path.copy()` reads from its target are carried, since the ABCs are private in 3.14 and have changed between `pathlib-abc` releases. |
97
+ | `Path.info` and `pathlib.Path(...).copy(target)` onto a `Path` of this library (3.14) | 3.14 `Path.info` is a `PathInfo` kept on the path (its answers are cached for the life of the path object); `pathlib.Path.copy(target)` calls `target._copy_from(source, **kwargs)` on any target with `with_segments()`, so a target without it raises `AttributeError` | `info` is defined on every `Path`, on every version: an object with `exists()`, `is_dir()`, `is_file()` (each with `follow_symlinks=True`) and `is_symlink()`, over at most one following and one non-following `stat()`. A path holds no state, so its answers are cached on the `info` object, not on the path: each `path.info` asks again. `LocalPath` keeps stdlib's `info` on 3.14. `Path._copy_from(source, follow_symlinks=True, preserve_metadata=False)` makes a stdlib path's `copy()` work onto any `Path` of this library (overwrites a file and raises on an existing directory, as 3.14 does; a listed child name that is not one safe component raises `ValueError`, as in `copy(recursive=True)`; `preserve_metadata` copies the permission bits and the modification time where the class can set them). A class that mixes a stdlib path keeps stdlib's. The private `pathlib` ABCs (`_ReadablePath`, ...) and `__open_rb__`/`__open_wb__` are not implemented. | Code written for 3.14's `PathInfo` and `Path.copy()` accepts these paths. Only the hooks `Path.copy()` reads from its target are carried, since the ABCs are private in 3.14 and have changed between `pathlib-abc` releases. |
98
98
  | `Path.walk()` and a Windows junction | 3.14: `Path.walk()` (`os.walk` with symlinks as files) lists a junction in `filenames` and does not enter it unless `follow_symlinks=True` | A non-following `walk()` lists a junction in `filenames` and does not enter it, on every version and for every class whose `is_junction()` says so (`LocalPath`, and a class for `file:` URIs). `follow_symlinks=True` enters it, as pathlib does. | A junction, like a symlink, is a second name for another tree; a junction that points at its own parent was walked 64 levels deep, until the path got too long. 3.14's behaviour is the safe one and the only one that can be had on 3.9-3.11, where `Path.walk` does not exist. |
99
99
  | `LocalPath.symlink_to(target)` with a relative `str` target, on Windows | `pathlib` stores the text as given, `../real.txt` included, and Windows cannot resolve a link whose relative target spells its separators `/` | The path class rewrites the text into Windows separators (`..
100
100
  eal.txt`) before the link is made; on POSIX the text is stored as given | A link that cannot be followed is never what a caller wants, and `PathSyncer` recreates links from `as_posix()` text. |
@@ -171,6 +171,14 @@ because a behavioral decision needed documenting:
171
171
  (`copy(recursive=True)` on a directory) -- do not invoke `progress`; this
172
172
  was a deliberate scope decision for the first cut (generic-stream-only),
173
173
  not an oversight. **2026-07-28.**
174
+ - `max_workers=None` on `Path.copy(recursive=True)`, `Path.rm(recursive=True)` and
175
+ `PathSyncer(...)`: no pathlib equivalent. `None` is one thread, as before. An int
176
+ of 2 or more hands the files and links of each directory to a pool of that many
177
+ threads, for a class that sets `_threadsafe = True` (`LocalPath`, `FTPPath`); any
178
+ other class runs serially. Callbacks (`progress`, `on_error`, `ignore_error`, the
179
+ sync `hook`, follow policies) are called one at a time but from pool threads and in
180
+ no fixed order, and the error that ends the operation is the first to happen, not
181
+ the first in listing order.
174
182
  - `protocols.checksum.NativeChecksum` (`checksum(algorithm="md5") -> str`) --
175
183
  an entirely new, optional protocol with no pathlib equivalent. A `Path`
176
184
  subclass may implement it to compute a file digest server-side (e.g.
@@ -28,10 +28,6 @@ Paths for other places are separate distributions built on this one:
28
28
  | Distribution | Provides |
29
29
  | --- | --- |
30
30
  | [`uripath`](https://github.com/jose-pr/uripath) | `URIPath`: one class for any URI (`file:`, `data:`, `ftp:`, `zip:`, `tar:`, `http:`, `dav:`), and the `uripath` command |
31
- | [`sftppath`](https://github.com/jose-pr/sftppath) | `SFTPPath` (paramiko or asyncssh) |
32
- | [`objpath`](https://github.com/jose-pr/objpath) | `S3Path`, `GSPath` and `AZPath` |
33
- | [`repopath`](https://github.com/jose-pr/repopath) | `RepoPath`, `GitHubPath` and `GitLabPath` |
34
- | [`pathapi-requests`](https://github.com/jose-pr/pathapi-requests) | `RequestsTransport` for `pathapi.http` |
35
31
 
36
32
  ## Tour
37
33
 
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
6
6
  [project]
7
7
  # The distribution and the import package are both `pathapi`.
8
8
  name = "pathapi"
9
- version = "0.1.0"
9
+ version = "0.1.2"
10
10
  authors = [{ name = "Jose A" }]
11
11
  description = "Generic Path Protocol based pathlib"
12
12
  readme = "README.md"
@@ -43,6 +43,11 @@ docs = ['mkdocs', 'mkdocs-material', 'mkdocstrings[python]']
43
43
  [project.entry-points."pathapi.http_transports"]
44
44
  builtin = "pathapi.http:HTTPTransport"
45
45
 
46
+ # The async HTTP transports `pathapi.http.AsyncHTTPClient` can send through;
47
+ # `pathapi.http.async_transport_names()` lists them without importing them.
48
+ [project.entry-points."pathapi.http_async_transports"]
49
+ builtin = "pathapi.http:ThreadedTransport"
50
+
46
51
  [project.urls]
47
52
  Homepage = "https://github.com/jose-pr/pathapi/"
48
53
  Documentation = "https://jose-pr.github.io/pathapi/"
@@ -57,6 +62,8 @@ Issues = "https://github.com/jose-pr/pathapi/issues"
57
62
  [tool.hatch.build.targets.wheel.force-include]
58
63
  "README.md" = "pathapi/README.md"
59
64
  # The transport suite (`python -m pathapi.http.testing`) runs from the installed package.
65
+ "tests/transport/test_async_client.py" = "pathapi/http/testing/suite/test_async_client.py"
66
+ "tests/transport/test_async_transport.py" = "pathapi/http/testing/suite/test_async_transport.py"
60
67
  "tests/transport/test_client_default.py" = "pathapi/http/testing/suite/test_client_default.py"
61
68
  "tests/transport/test_client_errors.py" = "pathapi/http/testing/suite/test_client_errors.py"
62
69
  "tests/transport/test_client_peer_text.py" = "pathapi/http/testing/suite/test_client_peer_text.py"