repopath 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. repopath-0.1.0/.gitignore +41 -0
  2. repopath-0.1.0/CHANGELOG.md +110 -0
  3. repopath-0.1.0/LICENSE +21 -0
  4. repopath-0.1.0/PKG-INFO +191 -0
  5. repopath-0.1.0/README.md +152 -0
  6. repopath-0.1.0/docs/api/exceptions.md +11 -0
  7. repopath-0.1.0/docs/api/paths.md +24 -0
  8. repopath-0.1.0/docs/api/uri.md +15 -0
  9. repopath-0.1.0/docs/benchmarks.md +58 -0
  10. repopath-0.1.0/docs/changelog.md +3 -0
  11. repopath-0.1.0/docs/divergences.md +28 -0
  12. repopath-0.1.0/docs/guides/paths.md +95 -0
  13. repopath-0.1.0/docs/guides/uri.md +53 -0
  14. repopath-0.1.0/docs/index.md +76 -0
  15. repopath-0.1.0/examples/github_listing.py +58 -0
  16. repopath-0.1.0/examples/gitlab_listing.py +59 -0
  17. repopath-0.1.0/mkdocs.yml +58 -0
  18. repopath-0.1.0/pyproject.toml +90 -0
  19. repopath-0.1.0/src/repopath/AGENTS.md +331 -0
  20. repopath-0.1.0/src/repopath/__init__.py +35 -0
  21. repopath-0.1.0/src/repopath/_backend.py +219 -0
  22. repopath-0.1.0/src/repopath/_github.py +243 -0
  23. repopath-0.1.0/src/repopath/_gitlab.py +243 -0
  24. repopath-0.1.0/src/repopath/_hosted.py +284 -0
  25. repopath-0.1.0/src/repopath/_kinds.py +314 -0
  26. repopath-0.1.0/src/repopath/_version.py +11 -0
  27. repopath-0.1.0/src/repopath/exceptions.py +43 -0
  28. repopath-0.1.0/src/repopath/py.typed +0 -0
  29. repopath-0.1.0/src/repopath/uri.py +253 -0
  30. repopath-0.1.0/tests/api_fakes.py +361 -0
  31. repopath-0.1.0/tests/child.py +45 -0
  32. repopath-0.1.0/tests/conftest.py +185 -0
  33. repopath-0.1.0/tests/fake_distribution.py +55 -0
  34. repopath-0.1.0/tests/hermetic.py +261 -0
  35. repopath-0.1.0/tests/http_fakes.py +109 -0
  36. repopath-0.1.0/tests/http_wire.py +261 -0
  37. repopath-0.1.0/tests/mutation/README.md +66 -0
  38. repopath-0.1.0/tests/mutation/mutants.py +413 -0
  39. repopath-0.1.0/tests/mutation/mutation_check.py +16 -0
  40. repopath-0.1.0/tests/mutation/run.py +224 -0
  41. repopath-0.1.0/tests/probes/probe_environment_leak.py +12 -0
  42. repopath-0.1.0/tests/probes/probe_leaks_a_socket.py +12 -0
  43. repopath-0.1.0/tests/probes/probe_session_environment.py +36 -0
  44. repopath-0.1.0/tests/repo_support.py +69 -0
  45. repopath-0.1.0/tests/test_contract.py +79 -0
  46. repopath-0.1.0/tests/test_documentation.py +161 -0
  47. repopath-0.1.0/tests/test_examples_skip.py +62 -0
  48. repopath-0.1.0/tests/test_github_stat.py +232 -0
  49. repopath-0.1.0/tests/test_hermetic.py +327 -0
  50. repopath-0.1.0/tests/test_hosted_paths.py +354 -0
  51. repopath-0.1.0/tests/test_isolation.py +102 -0
  52. repopath-0.1.0/tests/test_kinds.py +262 -0
  53. repopath-0.1.0/tests/test_listing_hints.py +85 -0
  54. repopath-0.1.0/tests/test_packaging.py +94 -0
  55. repopath-0.1.0/tests/test_public_surface.py +202 -0
  56. repopath-0.1.0/tests/test_replies.py +448 -0
  57. repopath-0.1.0/tests/test_repo.py +718 -0
  58. repopath-0.1.0/tests/test_requests.py +247 -0
  59. repopath-0.1.0/tests/test_session.py +132 -0
  60. repopath-0.1.0/tests/test_shipped_header.py +376 -0
  61. repopath-0.1.0/tests/test_transport.py +251 -0
  62. repopath-0.1.0/tests/test_uri_backend_sharing.py +65 -0
  63. repopath-0.1.0/tests/test_uri_repo.py +514 -0
  64. repopath-0.1.0/tests/test_version.py +24 -0
@@ -0,0 +1,41 @@
1
+ # Category rule: nothing dot-prefixed at the repo root gets tracked except the
2
+ # files re-included below (a category beats a list). Root-anchored on purpose;
3
+ # nested dotfiles are governed by the specific rules further down.
4
+ /.*
5
+ !/.gitignore
6
+ !/.gitattributes
7
+ !/.github/
8
+
9
+ # Private agent config. No trailing slash on `.agents` on purpose: it may be a
10
+ # symlink, which git treats as a file, so a directory-only `.agents/` would not
11
+ # match it. These stay even though /.* covers the root: they also catch nested
12
+ # copies. *.local.* is an unshared, machine- or user-specific override and is
13
+ # never committed (it is also excluded from the build in pyproject.toml).
14
+ .agents
15
+ *.local.*
16
+ CLAUDE*
17
+ .claude
18
+
19
+ # Build/dist output and generated docs site
20
+ build/
21
+ dist/
22
+ site/
23
+
24
+ # Python
25
+ __pycache__/
26
+ *.py[cod]
27
+ .pytest_cache/
28
+ .hypothesis/
29
+ .mypy_cache/
30
+ .ruff_cache/
31
+ .coverage*
32
+ htmlcov/
33
+ *.egg-info/
34
+ .venv/
35
+ .pyvenv/
36
+
37
+ # OS/editor
38
+ .DS_Store
39
+ Thumbs.db
40
+ .idea/
41
+ .vscode/
@@ -0,0 +1,110 @@
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 `repopath`: the `github:`, `gitlab:` and `git:` schemes of
13
+ `pathlib-next` 0.9 (`pathlib_next.uri.schemes`) as a distribution of their own, and as
14
+ paths that need no URI. It needs Python 3.9 or newer and `pathapi>=0.1.0,<0.2`,
15
+ and nothing else; its one extra, `uri`, adds `pathapi-uri>=0.1.0,<0.2` and with it the
16
+ schemes. Under *Changed*, the migration from `pathlib-next` comes in this order: the
17
+ table of names, the table of URIs, then every change of behaviour with what to do about
18
+ it.
19
+
20
+ It is built on `pathapi`, which was published as `pathlib-next` up to 0.10.0 (import
21
+ name `pathlib_next`); the old names below are the ones before the move.
22
+
23
+ ### Added
24
+
25
+ - `RepoPath`, `GitHubPath` and `GitLabPath`: read-only `pathapi.Path` classes for a
26
+ repository tree on GitHub (and GitHub Enterprise) or GitLab (and self-hosted GitLab),
27
+ over the services' REST APIs, with no URI and no library beyond `pathapi`.
28
+ `GitHubPath("src/x.py", owner="acme", repo="widgets", ref="v1.2", instance="github.com",
29
+ token=None, backend=None)`; `GitLabPath` takes the same keywords and an owner that may
30
+ be a group path (`"acme/team"`).
31
+ - `repo_owner`, the property of a path and of a URI path that names the account or group
32
+ that owns the repository. It is not called `owner`, the name `pathlib.Path.owner()` has for
33
+ the user that owns a file.
34
+ - `RepoPath(*segments, instance=..., type=None, **options)` returns a `GitHubPath` or a
35
+ `GitLabPath`: `type` names the kind (`"github"`, `"gitlab"`), and without it the kind
36
+ is detected from `instance` (`github.com` and `gitlab.com`, in any case, with or
37
+ without `www.`). Any other instance without `type` is a `ValueError` that lists the
38
+ choices. `RepoPath.kinds()` lists the registered kinds, and a kind of your own is a
39
+ subclass whose class body sets `TYPE` and `INSTANCES`.
40
+ - `repopath.uri`, for `uripath`: `RepoURIPath` (the `repo:` scheme, with the kind
41
+ detected from the host), `GitHubURIPath` (`github:`) and `GitLabURIPath` (`gitlab:`),
42
+ which delegate to the paths above. The entry points are declared whether or not
43
+ `uripath` is installed, and `import repopath` never imports it. Importing
44
+ `repopath.uri` without `uripath` raises an `ImportError` that names
45
+ `pip install "repopath[uri]"`.
46
+ - `repopath.exceptions`: `RepoPathError`, the base of what this package raises on its
47
+ own, with `RateLimitError` (also a `BlockingIOError`, errno `EAGAIN`) and
48
+ `BadReplyError` (also an `OSError`, errno `EIO`); `InsecureTransportWarning` is there
49
+ too.
50
+ - `RepoBackend` and `BaseRepoBackend` are in `repopath`. `RepoBackend` is a context
51
+ manager and sends through `pathapi.http`.
52
+
53
+ ### Changed
54
+
55
+ - The names. `pathlib_next.uri.schemes` named the classes of the three schemes with a
56
+ `Path` suffix:
57
+
58
+ | Was | Is now |
59
+ | --- | --- |
60
+ | `schemes.github.GitHubPath` (`github:`) | `repopath.uri.GitHubURIPath`; the path that needs no URI is `repopath.GitHubPath` |
61
+ | `schemes.gitlab.GitLabPath` (`gitlab:`) | `repopath.uri.GitLabURIPath`; without a URI, `repopath.GitLabPath` |
62
+ | `schemes.git.GitPath` (`git:`) | `repopath.uri.RepoURIPath` (`repo:`); without a URI, `repopath.RepoPath` |
63
+ | `schemes.git.GitHubGitPath`, `GitLabGitPath` (`git+github:`, `git+gitlab:`) | `repopath.uri.GitHubURIPath`, `GitLabURIPath` (`github:`, `gitlab:`) |
64
+ | `schemes.github.RepoBackend`; `schemes._gitrepo.BaseRepoBackend` | `repopath.RepoBackend`, `BaseRepoBackend` |
65
+ | `schemes.github.InsecureTransportWarning` (also in `schemes.gitlab`) | `repopath.InsecureTransportWarning` |
66
+ | `pathlib-next[uri]`, `pathlib-next[http]` | `repopath[uri]` |
67
+
68
+ - The URIs. `git:`, `git+github:` and `git+gitlab:` do not exist any more, and `uripath`
69
+ treats a URI of one of them as a path of an unknown scheme. What to do: rewrite the
70
+ URIs.
71
+
72
+ | Was | Is now |
73
+ | --- | --- |
74
+ | `git://github.com/o/r/f`, `git://gitlab.com/o/r/f` | `repo://github.com/o/r/f`, `repo://gitlab.com/o/r/f` (the kind is detected from the host) |
75
+ | `git+github://host/o/r/f` | `github://host/o/r/f` |
76
+ | `git+gitlab://host/o/r/f` | `gitlab://host/o/r/f` |
77
+ | `github://host/o/r/f`, `gitlab://host/o/r/f` | the same text |
78
+
79
+ `repo://` with a host that is not `github.com` or `gitlab.com` raises `URISyntaxError`
80
+ (a `ValueError`), as `git://` did, and names `github://host/...` and
81
+ `gitlab://host/...`.
82
+ - A path that has an owner and a repository is built from them, not from a URI:
83
+ `GitHubPath("src/x.py", owner="acme", repo="widgets")`. The URI classes keep the
84
+ properties `repo_owner`, `repo`, `repo_path` and `ref`, and have `instance` and `kind`.
85
+ - Rate limits and replies of the wrong shape have classes of their own. Was:
86
+ `OSError(EAGAIN)` and `OSError(EIO)`. Is: `RateLimitError` and `BadReplyError`, which
87
+ are those `OSError` types and carry the path as `filename`. What to do: nothing;
88
+ catch `RepoPathError` to catch both.
89
+ - An error raised through a URI path names the URI in its message, whatever scheme
90
+ spelling built the path.
91
+ - A URI that holds no owner and no repository (`github://github.com/acme`) is a path that
92
+ does not exist (`FileNotFoundError`, `exists()` is `False`) and no request is sent. Was:
93
+ a request for a repository with an empty name.
94
+ - An owner or a repository that is empty, is a `.` or `..`, or (for `GitHubPath`) holds a
95
+ `/` is refused by the constructor with `ValueError`. Was: refused when the API URL was
96
+ built.
97
+ - `is_local()` of a URI path of these schemes is `False` without resolving the host.
98
+ Was: the host name was looked up.
99
+ - Requests send `User-Agent: repopath/<version>` instead of `User-Agent: pathlib-next`;
100
+ the version is `repopath.__version__`. A backend's `headers=` still replaces it.
101
+ - The `requests` HTTP transport is provided by `pathapi-requests`.
102
+ `RepoBackend(session=...)` and `RepoBackend(**requests_args)` raise an `ImportError`
103
+ naming that distribution without it. What to do: install `pathapi-requests`
104
+ where one of them is used; with it installed they send through its transport and
105
+ nothing else is configured.
106
+ - `uritools` is not a dependency of anything here, and `requests` is not installed with
107
+ the package.
108
+
109
+ [Unreleased]: https://github.com/jose-pr/repopath/compare/v0.1.0...HEAD
110
+ [0.1.0]: https://github.com/jose-pr/repopath/releases/tag/v0.1.0
repopath-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.
@@ -0,0 +1,191 @@
1
+ Metadata-Version: 2.5
2
+ Name: repopath
3
+ Version: 0.1.0
4
+ Summary: pathlib-style, read-only paths for the repository trees of GitHub and GitLab, and the repo:, github: and gitlab: schemes for uripath
5
+ Project-URL: Homepage, https://github.com/jose-pr/repopath/
6
+ Project-URL: Documentation, https://jose-pr.github.io/repopath/
7
+ Project-URL: Issues, https://github.com/jose-pr/repopath/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: pathapi-uri<0.2,>=0.1.0; extra == 'all'
25
+ Provides-Extra: dev
26
+ Requires-Dist: black; extra == 'dev'
27
+ Requires-Dist: build; extra == 'dev'
28
+ Requires-Dist: hatchling; extra == 'dev'
29
+ Requires-Dist: pytest; extra == 'dev'
30
+ Requires-Dist: pytest-cov; extra == 'dev'
31
+ Requires-Dist: twine; extra == 'dev'
32
+ Provides-Extra: docs
33
+ Requires-Dist: mkdocs; extra == 'docs'
34
+ Requires-Dist: mkdocs-material; extra == 'docs'
35
+ Requires-Dist: mkdocstrings[python]; extra == 'docs'
36
+ Provides-Extra: uri
37
+ Requires-Dist: pathapi-uri<0.2,>=0.1.0; extra == 'uri'
38
+ Description-Content-Type: text/markdown
39
+
40
+ # repopath
41
+
42
+ [![Version](https://img.shields.io/pypi/v/repopath.svg)](https://pypi.org/project/repopath/)
43
+ [![Python versions](https://img.shields.io/pypi/pyversions/repopath.svg)](https://pypi.org/project/repopath/)
44
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/repopath/blob/main/LICENSE)
45
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/repopath/)
46
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/repopath/test.yml)](https://github.com/jose-pr/repopath/actions/workflows/test.yml)
47
+
48
+ **`pathlib` for the repository trees of GitHub and GitLab, read-only.**
49
+ `GitHubPath("src/mod.py", owner="acme", repo="widgets", ref="v1.2")` has the method
50
+ names, signatures and exception types of `pathlib.Path`, and `GitLabPath` does the same
51
+ for GitLab; `RepoPath` is the class that returns either. None needs a URI, and all
52
+ build on [`pathapi`](https://github.com/jose-pr/pathapi) alone; with
53
+ [`uripath`](https://github.com/jose-pr/uripath) installed the same package also
54
+ provides the `repo:`, `github:` and `gitlab:` schemes. It is documented at
55
+ <https://jose-pr.github.io/repopath/>.
56
+
57
+ The classes are clients of the hosting services' REST APIs. They speak no git
58
+ protocol: there is no clone, no commit and no `git:` URL, and the tree is read-only.
59
+
60
+ ## Features
61
+
62
+ | Kind | Class | `type=` | Detected for | API |
63
+ | --- | --- | --- | --- | --- |
64
+ | GitHub and GitHub Enterprise | `GitHubPath` | `"github"` | `github.com` | contents API at `api.github.com`, or `https://<instance>/api/v3` |
65
+ | GitLab and self-hosted GitLab | `GitLabPath` | `"gitlab"` | `gitlab.com` | REST API v4 at `https://<instance>/api/v4` |
66
+
67
+ - **A `pathlib` path in a repository**: `open`, `read_text`, `iterdir`, `glob`, `stat`
68
+ and the rest, plus `walk`, `copy` out of the tree and `PathSyncer` as a source, from
69
+ `pathapi`. Nothing connects until the first request.
70
+ - **One class for the kind**: `RepoPath("f", owner="acme", repo="widgets",
71
+ instance="github.com")` is a `GitHubPath`; another instance needs `type=`, and
72
+ `RepoPath.kinds()` lists the kinds. A kind is a subclass that registers itself.
73
+ - **Cheap listings**: a directory is one request and each child carries the size and
74
+ type the listing gave, so `walk()` and `glob()` cost one request per directory;
75
+ `stat()` of a file is a `HEAD`. Listings past GitHub's 1,000-entry cap and GitLab's
76
+ pages are read to the end.
77
+ - **Authentication that stays out of sight**: `token=` is sent as a bearer token to the
78
+ API's own origin only, and shown by no `str()`, `repr()`, pickle or error message. A
79
+ token over plain `http://` to another host warns.
80
+ - **No library to install**: requests go through `pathapi.http`, on the standard
81
+ library, with a replaceable transport. Errors are the `OSError` types `pathlib` raises,
82
+ `RateLimitError` (a `BlockingIOError`) for a rate limit and `BadReplyError` for a reply
83
+ that is not the shape the API documents.
84
+ - **The `repo:`, `github:` and `gitlab:` schemes** for `uripath`:
85
+ `URIPath("github://github.com/acme/widgets/src/mod.py?ref=v1.2")` is a
86
+ `GitHubURIPath` that delegates to a `GitHubPath`.
87
+
88
+ ## Installation
89
+
90
+ ```bash
91
+ pip install repopath
92
+ ```
93
+
94
+ Requires `pathapi`, which pip installs with it, and Python 3.9 or newer.
95
+
96
+ | Install | Adds |
97
+ | --- | --- |
98
+ | `pip install repopath` | the paths and the backends; no other library |
99
+ | `pip install "repopath[uri]"` | `uripath` and with it the `repo:`, `github:` and `gitlab:` schemes |
100
+ | `pip install "repopath[all]"` | all of the above |
101
+
102
+ ## Quick start
103
+
104
+ **A path in a repository**: the owner and repository are keywords, the segments are the
105
+ path inside it, and `ref` is a branch, a tag or a commit:
106
+
107
+ <!-- example: skip: reads from GitHub -->
108
+ ```python
109
+ from repopath import GitHubPath
110
+
111
+ readme = GitHubPath("README.md", owner="psf", repo="requests")
112
+ print(readme.read_text()[:200])
113
+ for child in readme.parent.iterdir():
114
+ print(child.name, child.is_dir())
115
+ ```
116
+
117
+ **Another kind, or another instance**: `instance` names the server, `type` the kind when
118
+ the instance does not tell, and `token` is the credential:
119
+
120
+ <!-- example: skip: reads from GitLab -->
121
+ ```python
122
+ from repopath import GitLabPath, RepoPath
123
+
124
+ doc = RepoPath("doc", owner="gitlab-org", repo="gitlab", instance="gitlab.com")
125
+ private = RepoPath(
126
+ "src", owner="team/tools", repo="app", instance="git.example.org",
127
+ type="gitlab", token="glpat-...",
128
+ )
129
+ assert isinstance(doc, GitLabPath)
130
+ ```
131
+
132
+ **Copy a tree** out of a repository with `pathapi`:
133
+
134
+ <!-- example: skip: reads from GitHub -->
135
+ ```python
136
+ from pathapi import LocalPath
137
+ from repopath import GitHubPath
138
+
139
+ GitHubPath("docs", owner="psf", repo="requests", ref="main").copy(
140
+ LocalPath("docs-copy"), recursive=True
141
+ )
142
+ ```
143
+
144
+ **The schemes**, with `uripath` installed:
145
+
146
+ <!-- example: skip: reads from GitHub -->
147
+ ```python
148
+ from uripath import URIPath
149
+
150
+ print(URIPath("repo://github.com/psf/requests/README.md?ref=main").read_text()[:200])
151
+ ```
152
+
153
+ ## API overview
154
+
155
+ | Module | Purpose |
156
+ | --- | --- |
157
+ | `repopath` | `RepoPath`, `GitHubPath`, `GitLabPath`, the backends `RepoBackend` and `BaseRepoBackend`, the exceptions `RepoPathError`, `RateLimitError` and `BadReplyError`, the warning `InsecureTransportWarning` and `__version__` |
158
+ | `repopath.exceptions` | The exceptions and the warning |
159
+ | `repopath.uri` | `RepoURIPath`, `GitHubURIPath` and `GitLabURIPath`, the schemes of `uripath` (needs the `uri` extra) |
160
+
161
+ The full reference is on the [documentation site](https://jose-pr.github.io/repopath/);
162
+ an agent-oriented API summary ships inside the package as `repopath/AGENTS.md`.
163
+
164
+ ## Development
165
+
166
+ ```bash
167
+ pip install -e ".[dev,all]"
168
+ python -m pytest -q -rs
169
+ ```
170
+
171
+ The tests send every request to a fake of the API on the loopback interface; none
172
+ reaches another host. A test that needs `uripath` is skipped without it, so check the
173
+ skip list (`-rs`). Contributor notes (virtual environments, formatting, CI, releases) are
174
+ in the repository's [`AGENTS.md`](https://github.com/jose-pr/repopath/blob/main/AGENTS.md).
175
+ To preview the documentation: `pip install -e ".[all,docs]"` then `mkdocs serve`.
176
+
177
+ Changes are recorded in the
178
+ [changelog](https://github.com/jose-pr/repopath/blob/main/CHANGELOG.md).
179
+
180
+ ### Releasing
181
+
182
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
183
+ [`CHANGELOG.md`](https://github.com/jose-pr/repopath/blob/main/CHANGELOG.md). Pushing a tag matching `v*` triggers the release
184
+ workflow: test gate → build (checking the tag names the version built) → a strict
185
+ docs build as a gate → GitHub release → publish. The release workflow never deploys
186
+ the docs site itself: for a final release its last job dispatches the docs workflow
187
+ at the tag, which owns every Pages deploy.
188
+
189
+ ## License
190
+
191
+ MIT — see [LICENSE](https://github.com/jose-pr/repopath/blob/main/LICENSE).
@@ -0,0 +1,152 @@
1
+ # repopath
2
+
3
+ [![Version](https://img.shields.io/pypi/v/repopath.svg)](https://pypi.org/project/repopath/)
4
+ [![Python versions](https://img.shields.io/pypi/pyversions/repopath.svg)](https://pypi.org/project/repopath/)
5
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/jose-pr/repopath/blob/main/LICENSE)
6
+ [![Docs](https://img.shields.io/badge/docs-latest-blue.svg)](https://jose-pr.github.io/repopath/)
7
+ [![CI](https://img.shields.io/github/actions/workflow/status/jose-pr/repopath/test.yml)](https://github.com/jose-pr/repopath/actions/workflows/test.yml)
8
+
9
+ **`pathlib` for the repository trees of GitHub and GitLab, read-only.**
10
+ `GitHubPath("src/mod.py", owner="acme", repo="widgets", ref="v1.2")` has the method
11
+ names, signatures and exception types of `pathlib.Path`, and `GitLabPath` does the same
12
+ for GitLab; `RepoPath` is the class that returns either. None needs a URI, and all
13
+ build on [`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 `repo:`, `github:` and `gitlab:` schemes. It is documented at
16
+ <https://jose-pr.github.io/repopath/>.
17
+
18
+ The classes are clients of the hosting services' REST APIs. They speak no git
19
+ protocol: there is no clone, no commit and no `git:` URL, and the tree is read-only.
20
+
21
+ ## Features
22
+
23
+ | Kind | Class | `type=` | Detected for | API |
24
+ | --- | --- | --- | --- | --- |
25
+ | GitHub and GitHub Enterprise | `GitHubPath` | `"github"` | `github.com` | contents API at `api.github.com`, or `https://<instance>/api/v3` |
26
+ | GitLab and self-hosted GitLab | `GitLabPath` | `"gitlab"` | `gitlab.com` | REST API v4 at `https://<instance>/api/v4` |
27
+
28
+ - **A `pathlib` path in a repository**: `open`, `read_text`, `iterdir`, `glob`, `stat`
29
+ and the rest, plus `walk`, `copy` out of the tree and `PathSyncer` as a source, from
30
+ `pathapi`. Nothing connects until the first request.
31
+ - **One class for the kind**: `RepoPath("f", owner="acme", repo="widgets",
32
+ instance="github.com")` is a `GitHubPath`; another instance needs `type=`, and
33
+ `RepoPath.kinds()` lists the kinds. A kind is a subclass that registers itself.
34
+ - **Cheap listings**: a directory is one request and each child carries the size and
35
+ type the listing gave, so `walk()` and `glob()` cost one request per directory;
36
+ `stat()` of a file is a `HEAD`. Listings past GitHub's 1,000-entry cap and GitLab's
37
+ pages are read to the end.
38
+ - **Authentication that stays out of sight**: `token=` is sent as a bearer token to the
39
+ API's own origin only, and shown by no `str()`, `repr()`, pickle or error message. A
40
+ token over plain `http://` to another host warns.
41
+ - **No library to install**: requests go through `pathapi.http`, on the standard
42
+ library, with a replaceable transport. Errors are the `OSError` types `pathlib` raises,
43
+ `RateLimitError` (a `BlockingIOError`) for a rate limit and `BadReplyError` for a reply
44
+ that is not the shape the API documents.
45
+ - **The `repo:`, `github:` and `gitlab:` schemes** for `uripath`:
46
+ `URIPath("github://github.com/acme/widgets/src/mod.py?ref=v1.2")` is a
47
+ `GitHubURIPath` that delegates to a `GitHubPath`.
48
+
49
+ ## Installation
50
+
51
+ ```bash
52
+ pip install repopath
53
+ ```
54
+
55
+ Requires `pathapi`, which pip installs with it, and Python 3.9 or newer.
56
+
57
+ | Install | Adds |
58
+ | --- | --- |
59
+ | `pip install repopath` | the paths and the backends; no other library |
60
+ | `pip install "repopath[uri]"` | `uripath` and with it the `repo:`, `github:` and `gitlab:` schemes |
61
+ | `pip install "repopath[all]"` | all of the above |
62
+
63
+ ## Quick start
64
+
65
+ **A path in a repository**: the owner and repository are keywords, the segments are the
66
+ path inside it, and `ref` is a branch, a tag or a commit:
67
+
68
+ <!-- example: skip: reads from GitHub -->
69
+ ```python
70
+ from repopath import GitHubPath
71
+
72
+ readme = GitHubPath("README.md", owner="psf", repo="requests")
73
+ print(readme.read_text()[:200])
74
+ for child in readme.parent.iterdir():
75
+ print(child.name, child.is_dir())
76
+ ```
77
+
78
+ **Another kind, or another instance**: `instance` names the server, `type` the kind when
79
+ the instance does not tell, and `token` is the credential:
80
+
81
+ <!-- example: skip: reads from GitLab -->
82
+ ```python
83
+ from repopath import GitLabPath, RepoPath
84
+
85
+ doc = RepoPath("doc", owner="gitlab-org", repo="gitlab", instance="gitlab.com")
86
+ private = RepoPath(
87
+ "src", owner="team/tools", repo="app", instance="git.example.org",
88
+ type="gitlab", token="glpat-...",
89
+ )
90
+ assert isinstance(doc, GitLabPath)
91
+ ```
92
+
93
+ **Copy a tree** out of a repository with `pathapi`:
94
+
95
+ <!-- example: skip: reads from GitHub -->
96
+ ```python
97
+ from pathapi import LocalPath
98
+ from repopath import GitHubPath
99
+
100
+ GitHubPath("docs", owner="psf", repo="requests", ref="main").copy(
101
+ LocalPath("docs-copy"), recursive=True
102
+ )
103
+ ```
104
+
105
+ **The schemes**, with `uripath` installed:
106
+
107
+ <!-- example: skip: reads from GitHub -->
108
+ ```python
109
+ from uripath import URIPath
110
+
111
+ print(URIPath("repo://github.com/psf/requests/README.md?ref=main").read_text()[:200])
112
+ ```
113
+
114
+ ## API overview
115
+
116
+ | Module | Purpose |
117
+ | --- | --- |
118
+ | `repopath` | `RepoPath`, `GitHubPath`, `GitLabPath`, the backends `RepoBackend` and `BaseRepoBackend`, the exceptions `RepoPathError`, `RateLimitError` and `BadReplyError`, the warning `InsecureTransportWarning` and `__version__` |
119
+ | `repopath.exceptions` | The exceptions and the warning |
120
+ | `repopath.uri` | `RepoURIPath`, `GitHubURIPath` and `GitLabURIPath`, the schemes of `uripath` (needs the `uri` extra) |
121
+
122
+ The full reference is on the [documentation site](https://jose-pr.github.io/repopath/);
123
+ an agent-oriented API summary ships inside the package as `repopath/AGENTS.md`.
124
+
125
+ ## Development
126
+
127
+ ```bash
128
+ pip install -e ".[dev,all]"
129
+ python -m pytest -q -rs
130
+ ```
131
+
132
+ The tests send every request to a fake of the API on the loopback interface; none
133
+ reaches another host. A test that needs `uripath` is skipped without it, so check the
134
+ skip list (`-rs`). Contributor notes (virtual environments, formatting, CI, releases) are
135
+ in the repository's [`AGENTS.md`](https://github.com/jose-pr/repopath/blob/main/AGENTS.md).
136
+ To preview the documentation: `pip install -e ".[all,docs]"` then `mkdocs serve`.
137
+
138
+ Changes are recorded in the
139
+ [changelog](https://github.com/jose-pr/repopath/blob/main/CHANGELOG.md).
140
+
141
+ ### Releasing
142
+
143
+ This project follows [Semantic Versioning](https://semver.org/) and keeps a
144
+ [`CHANGELOG.md`](https://github.com/jose-pr/repopath/blob/main/CHANGELOG.md). Pushing a tag matching `v*` triggers the release
145
+ workflow: test gate → build (checking the tag names the version built) → a strict
146
+ docs build as a gate → GitHub release → publish. The release workflow never deploys
147
+ the docs site itself: for a final release its last job dispatches the docs workflow
148
+ at the tag, which owns every Pages deploy.
149
+
150
+ ## License
151
+
152
+ MIT — see [LICENSE](https://github.com/jose-pr/repopath/blob/main/LICENSE).
@@ -0,0 +1,11 @@
1
+ # Exceptions
2
+
3
+ `RepoPathError` 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; the two failures no builtin names are `RateLimitError`
6
+ and `BadReplyError`.
7
+
8
+ ::: repopath.exceptions
9
+ options:
10
+ show_if_no_docstring: true
11
+ filters: ["!^_"]
@@ -0,0 +1,24 @@
1
+ # Paths and backends
2
+
3
+ The paths need no URI and no library beyond `pathapi`.
4
+
5
+ `GitHubPath` and `GitLabPath` share a base class: the properties it defines
6
+ (`owner`, `repo`, `ref`, `instance`, `repo_path`) are listed under each of them.
7
+
8
+ ::: repopath
9
+ options:
10
+ show_if_no_docstring: true
11
+ filters: ["!^_"]
12
+ inherited_members:
13
+ - repo_owner
14
+ - repo
15
+ - ref
16
+ - instance
17
+ - repo_path
18
+ - as_uri
19
+ members:
20
+ - RepoPath
21
+ - GitHubPath
22
+ - GitLabPath
23
+ - RepoBackend
24
+ - BaseRepoBackend
@@ -0,0 +1,15 @@
1
+ # The `repo:`, `github:` and `gitlab:` schemes
2
+
3
+ Needs `uripath`: `pip install "repopath[uri]"`.
4
+
5
+ ::: repopath.uri
6
+ options:
7
+ show_if_no_docstring: true
8
+ filters: ["!^_"]
9
+ inherited_members:
10
+ - kind
11
+ - instance
12
+ - repo_owner
13
+ - repo
14
+ - repo_path
15
+ - ref
@@ -0,0 +1,58 @@
1
+ # Benchmarks
2
+
3
+ `repopath` includes a benchmark harness at `benchmarks/bench.py` that times `GitHubPath`,
4
+ `GitLabPath` and `RepoPath`, and the `github:`, `gitlab:` and `repo:` schemes of `uripath`
5
+ on the same operations. It needs only the `uri` extra for the URI classes:
6
+ `pip install -e ".[dev,uri]"`.
7
+
8
+ ## Run It
9
+
10
+ ```bash
11
+ python benchmarks/bench.py # run the benchmarks
12
+ python benchmarks/bench.py --help # options; runs nothing
13
+ python benchmarks/bench.py --suite pure
14
+ ```
15
+
16
+ ## Saved Results
17
+
18
+ Every metric is timed over several samples. `--save` writes the run as JSON to
19
+ `benchmarks/results/<name>.json`, with the minimum, median and maximum milliseconds per
20
+ call of each metric plus the interpreter, platform and git commit it ran on:
21
+
22
+ ```bash
23
+ python benchmarks/bench.py --save
24
+ python benchmarks/bench.py --save --samples 9
25
+ ```
26
+
27
+ `--samples N` applies one sample count to every case; without it the pure-path operations
28
+ take nine and the ones that send requests take five. The result schema and the committed
29
+ results are described in
30
+ [`benchmarks/README.md`](https://github.com/jose-pr/repopath/blob/main/benchmarks/README.md).
31
+
32
+ Compare two runs on their medians, and only between runs from the same machine and
33
+ interpreter. A run on a developer machine is a sanity check; performance claims in release
34
+ notes come from CI runs.
35
+
36
+ ## What It Covers
37
+
38
+ - the pure-path operations (construct, join with `/`, `parent`, `str()`, `as_uri()`, `==`
39
+ and `hash()`, and the parse of a URI) on a backend that refuses every request
40
+ - `stat()` of a file and of a directory, `iterdir()` of 100 entries and `read_bytes()` of a
41
+ small file
42
+ - `walk()` of a tree of 197 files in 19 directories, and the number of requests it sends:
43
+ one per directory, none per file
44
+ - GitLab's default-branch lookup: a `stat()` that has to look it up, and the number of
45
+ lookups over several reads when the paths share one backend and when they do not
46
+ - each of the above through the path classes (metric groups `github`, `gitlab`, `repo`) and
47
+ through the URI classes (`github-uri`, `gitlab-uri`, `repo-uri`), so the cost of the URI
48
+ layer is the difference between the two groups
49
+
50
+ ## Caveats
51
+
52
+ The API is a loopback fake in the benchmark's own process, with no network latency, so the
53
+ numbers show the overhead of the path layer and of the fake, not how a hosting service
54
+ behaves over a link. The fake reads its whole tree from disk on every request, so a
55
+ request costs the same few milliseconds whatever the path layer does: the timings of the
56
+ operations that send requests are mostly that, and the request counts show the difference a
57
+ change makes. Timings depend on the machine, the interpreter and what else is running;
58
+ compare medians, as described above.
@@ -0,0 +1,3 @@
1
+ # Changelog
2
+
3
+ --8<-- "CHANGELOG.md"