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.
- repopath-0.1.0/.gitignore +41 -0
- repopath-0.1.0/CHANGELOG.md +110 -0
- repopath-0.1.0/LICENSE +21 -0
- repopath-0.1.0/PKG-INFO +191 -0
- repopath-0.1.0/README.md +152 -0
- repopath-0.1.0/docs/api/exceptions.md +11 -0
- repopath-0.1.0/docs/api/paths.md +24 -0
- repopath-0.1.0/docs/api/uri.md +15 -0
- repopath-0.1.0/docs/benchmarks.md +58 -0
- repopath-0.1.0/docs/changelog.md +3 -0
- repopath-0.1.0/docs/divergences.md +28 -0
- repopath-0.1.0/docs/guides/paths.md +95 -0
- repopath-0.1.0/docs/guides/uri.md +53 -0
- repopath-0.1.0/docs/index.md +76 -0
- repopath-0.1.0/examples/github_listing.py +58 -0
- repopath-0.1.0/examples/gitlab_listing.py +59 -0
- repopath-0.1.0/mkdocs.yml +58 -0
- repopath-0.1.0/pyproject.toml +90 -0
- repopath-0.1.0/src/repopath/AGENTS.md +331 -0
- repopath-0.1.0/src/repopath/__init__.py +35 -0
- repopath-0.1.0/src/repopath/_backend.py +219 -0
- repopath-0.1.0/src/repopath/_github.py +243 -0
- repopath-0.1.0/src/repopath/_gitlab.py +243 -0
- repopath-0.1.0/src/repopath/_hosted.py +284 -0
- repopath-0.1.0/src/repopath/_kinds.py +314 -0
- repopath-0.1.0/src/repopath/_version.py +11 -0
- repopath-0.1.0/src/repopath/exceptions.py +43 -0
- repopath-0.1.0/src/repopath/py.typed +0 -0
- repopath-0.1.0/src/repopath/uri.py +253 -0
- repopath-0.1.0/tests/api_fakes.py +361 -0
- repopath-0.1.0/tests/child.py +45 -0
- repopath-0.1.0/tests/conftest.py +185 -0
- repopath-0.1.0/tests/fake_distribution.py +55 -0
- repopath-0.1.0/tests/hermetic.py +261 -0
- repopath-0.1.0/tests/http_fakes.py +109 -0
- repopath-0.1.0/tests/http_wire.py +261 -0
- repopath-0.1.0/tests/mutation/README.md +66 -0
- repopath-0.1.0/tests/mutation/mutants.py +413 -0
- repopath-0.1.0/tests/mutation/mutation_check.py +16 -0
- repopath-0.1.0/tests/mutation/run.py +224 -0
- repopath-0.1.0/tests/probes/probe_environment_leak.py +12 -0
- repopath-0.1.0/tests/probes/probe_leaks_a_socket.py +12 -0
- repopath-0.1.0/tests/probes/probe_session_environment.py +36 -0
- repopath-0.1.0/tests/repo_support.py +69 -0
- repopath-0.1.0/tests/test_contract.py +79 -0
- repopath-0.1.0/tests/test_documentation.py +161 -0
- repopath-0.1.0/tests/test_examples_skip.py +62 -0
- repopath-0.1.0/tests/test_github_stat.py +232 -0
- repopath-0.1.0/tests/test_hermetic.py +327 -0
- repopath-0.1.0/tests/test_hosted_paths.py +354 -0
- repopath-0.1.0/tests/test_isolation.py +102 -0
- repopath-0.1.0/tests/test_kinds.py +262 -0
- repopath-0.1.0/tests/test_listing_hints.py +85 -0
- repopath-0.1.0/tests/test_packaging.py +94 -0
- repopath-0.1.0/tests/test_public_surface.py +202 -0
- repopath-0.1.0/tests/test_replies.py +448 -0
- repopath-0.1.0/tests/test_repo.py +718 -0
- repopath-0.1.0/tests/test_requests.py +247 -0
- repopath-0.1.0/tests/test_session.py +132 -0
- repopath-0.1.0/tests/test_shipped_header.py +376 -0
- repopath-0.1.0/tests/test_transport.py +251 -0
- repopath-0.1.0/tests/test_uri_backend_sharing.py +65 -0
- repopath-0.1.0/tests/test_uri_repo.py +514 -0
- 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.
|
repopath-0.1.0/PKG-INFO
ADDED
|
@@ -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
|
+
[](https://pypi.org/project/repopath/)
|
|
43
|
+
[](https://pypi.org/project/repopath/)
|
|
44
|
+
[](https://github.com/jose-pr/repopath/blob/main/LICENSE)
|
|
45
|
+
[](https://jose-pr.github.io/repopath/)
|
|
46
|
+
[](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).
|
repopath-0.1.0/README.md
ADDED
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# repopath
|
|
2
|
+
|
|
3
|
+
[](https://pypi.org/project/repopath/)
|
|
4
|
+
[](https://pypi.org/project/repopath/)
|
|
5
|
+
[](https://github.com/jose-pr/repopath/blob/main/LICENSE)
|
|
6
|
+
[](https://jose-pr.github.io/repopath/)
|
|
7
|
+
[](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.
|