github-forker 1.0.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- github_forker-1.0.1/.gitignore +15 -0
- github_forker-1.0.1/CHANGELOG.md +40 -0
- github_forker-1.0.1/LICENSE +21 -0
- github_forker-1.0.1/Makefile +26 -0
- github_forker-1.0.1/PKG-INFO +384 -0
- github_forker-1.0.1/README.md +350 -0
- github_forker-1.0.1/pyproject.toml +103 -0
- github_forker-1.0.1/src/pygithub_fork/__init__.py +58 -0
- github_forker-1.0.1/src/pygithub_fork/exceptions.py +39 -0
- github_forker-1.0.1/src/pygithub_fork/forker.py +1000 -0
- github_forker-1.0.1/src/pygithub_fork/models.py +117 -0
- github_forker-1.0.1/tests/__init__.py +0 -0
- github_forker-1.0.1/tests/test_forker.py +564 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Changelog — github-forker
|
|
2
|
+
|
|
3
|
+
> **Note:** This package was originally named `pygithub-fork` but was renamed
|
|
4
|
+
> to `github-forker` before initial release because PyPI's similarity check
|
|
5
|
+
> flagged `pygithub-fork` as too similar to the existing `PyGithub` package
|
|
6
|
+
> (PyPI normalizes `pygithub-fork` → `pygithubfork` which is too close to
|
|
7
|
+
> `pygithub`). The Python module name `pygithub_fork` is unchanged.
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
All notable changes to this project will be documented in this file.
|
|
11
|
+
|
|
12
|
+
The format follows [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
13
|
+
This project uses [Semantic Versioning](https://semver.org/).
|
|
14
|
+
|
|
15
|
+
---
|
|
16
|
+
|
|
17
|
+
## [1.0.1] — 2026-06-19
|
|
18
|
+
|
|
19
|
+
### Added
|
|
20
|
+
|
|
21
|
+
- `GitHubForker.fork()` — synchronous fork with idempotency, retry/backoff, and readiness polling
|
|
22
|
+
- `GitHubForker.fork_async()` — fire-and-forget fork returning a `ForkJob` handle
|
|
23
|
+
- `ForkJob` — non-blocking `.done` / `.status` / `.result` + blocking `.wait(timeout)`
|
|
24
|
+
- `GitHubForker.fork_many()` — bulk fork via `ThreadPoolExecutor` (parallel or sequential)
|
|
25
|
+
- `GitHubForker.fork_iter()` — streaming generator yielding results in completion order
|
|
26
|
+
- `ForkRequest` — per-item declarative config for `fork_many`/`fork_iter` batches
|
|
27
|
+
- `ForkerConfig` — centralized, documented configuration dataclass
|
|
28
|
+
- Post-fork `git remote add upstream` support (`add_upstream_remote`, `local_clone_path`)
|
|
29
|
+
- Post-fork GitHub webhook registration (`register_webhook`, `webhook_url`, `webhook_events`, `webhook_secret`)
|
|
30
|
+
- Idempotent webhook de-duplication (checks existing hooks before creating)
|
|
31
|
+
- Idempotent upstream remote (checks before `git remote add`)
|
|
32
|
+
- Secondary rate limit (403 "abuse") distinguished from hard 403 permission errors
|
|
33
|
+
- PyGithub cross-version compatibility (`RateLimitExceededException` vs `RateLimitExceededError`)
|
|
34
|
+
- Full exception hierarchy: `ForkError`, `ForkTimeoutError`, `ForkPermissionError`, `RepositoryNotFoundError`, `WebhookError`, `UpstreamRemoteError`
|
|
35
|
+
- `on_retry` and `on_fork_done` callbacks in `ForkerConfig`
|
|
36
|
+
- Context manager support (`with GitHubForker(gh) as forker:`) for clean pool shutdown
|
|
37
|
+
- MIT license
|
|
38
|
+
- Full README with API reference and usage examples
|
|
39
|
+
- GitHub Actions CI workflow (lint + test on Python 3.9–3.12)
|
|
40
|
+
- GitHub Actions publish workflow (PyPI on tag push)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Hadi Cahyadi <cumulus13@gmail.com>
|
|
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,26 @@
|
|
|
1
|
+
.PHONY: install test lint build publish clean
|
|
2
|
+
|
|
3
|
+
install:
|
|
4
|
+
pip install -e ".[dev]"
|
|
5
|
+
|
|
6
|
+
test:
|
|
7
|
+
pytest tests/ -v --tb=short --cov=pygithub_fork --cov-report=term-missing
|
|
8
|
+
|
|
9
|
+
lint:
|
|
10
|
+
ruff check src/ tests/
|
|
11
|
+
mypy src/pygithub_fork/
|
|
12
|
+
|
|
13
|
+
build: clean
|
|
14
|
+
python -m build
|
|
15
|
+
|
|
16
|
+
publish: build
|
|
17
|
+
twine check dist/*
|
|
18
|
+
twine upload dist/*
|
|
19
|
+
|
|
20
|
+
# Upload to TestPyPI first to sanity-check before going live:
|
|
21
|
+
publish-test: build
|
|
22
|
+
twine check dist/*
|
|
23
|
+
twine upload --repository testpypi dist/*
|
|
24
|
+
|
|
25
|
+
clean:
|
|
26
|
+
rm -rf dist/ build/ *.egg-info src/*.egg-info .pytest_cache .mypy_cache
|
|
@@ -0,0 +1,384 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: github-forker
|
|
3
|
+
Version: 1.0.1
|
|
4
|
+
Summary: Production-ready GitHub repository forking built on PyGithub — retry, backoff, readiness polling, thread pool, background jobs, upstream remotes, and webhooks.
|
|
5
|
+
Project-URL: Homepage, https://github.com/cumulus13/pygithub-fork
|
|
6
|
+
Project-URL: Repository, https://github.com/cumulus13/pygithub-fork
|
|
7
|
+
Project-URL: Bug Tracker, https://github.com/cumulus13/pygithub-fork/issues
|
|
8
|
+
Project-URL: Changelog, https://github.com/cumulus13/pygithub-fork/blob/main/CHANGELOG.md
|
|
9
|
+
Author-email: Hadi Cahyadi <cumulus13@gmail.com>
|
|
10
|
+
License: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: api,automation,devops,fork,git,github,pygithub,repository
|
|
13
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
14
|
+
Classifier: Intended Audience :: Developers
|
|
15
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
16
|
+
Classifier: Operating System :: OS Independent
|
|
17
|
+
Classifier: Programming Language :: Python :: 3
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
23
|
+
Classifier: Topic :: Software Development :: Version Control :: Git
|
|
24
|
+
Requires-Python: >=3.9
|
|
25
|
+
Requires-Dist: pygithub>=1.55
|
|
26
|
+
Provides-Extra: dev
|
|
27
|
+
Requires-Dist: build; extra == 'dev'
|
|
28
|
+
Requires-Dist: mypy; extra == 'dev'
|
|
29
|
+
Requires-Dist: pytest-cov>=4.0; extra == 'dev'
|
|
30
|
+
Requires-Dist: pytest>=7.0; extra == 'dev'
|
|
31
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
32
|
+
Requires-Dist: twine; extra == 'dev'
|
|
33
|
+
Description-Content-Type: text/markdown
|
|
34
|
+
|
|
35
|
+
# github-forker
|
|
36
|
+
|
|
37
|
+
[](https://pypi.org/project/pygithub-fork/)
|
|
38
|
+
[](https://pypi.org/project/pygithub-fork/)
|
|
39
|
+
[](LICENSE)
|
|
40
|
+
|
|
41
|
+
Production-ready GitHub repository forking built on [PyGithub](https://github.com/PyGithub/PyGithub).
|
|
42
|
+
|
|
43
|
+
A bare `repo.create_fork()` call returns immediately but the fork is not actually usable yet — GitHub builds the copy asynchronously in the background. `github-forker` handles everything you need for real-world use:
|
|
44
|
+
|
|
45
|
+
- **Idempotency** — detects pre-existing forks so re-runs never crash
|
|
46
|
+
- **Retry + exponential backoff with jitter** — survives 5xx, timeouts, rate limits, and GitHub's secondary ("abuse") rate limit
|
|
47
|
+
- **Fork-readiness polling** — waits until the fork is actually populated before returning
|
|
48
|
+
- **Thread pool** — `fork_many()` runs up to N forks concurrently
|
|
49
|
+
- **Background / fire-and-forget** — `fork_async()` returns a `ForkJob` you can query or wait on from any thread
|
|
50
|
+
- **Streaming generator** — `fork_iter()` yields results as each fork completes
|
|
51
|
+
- **Post-fork upstream remote** — runs `git remote add upstream <url>` in your local clone
|
|
52
|
+
- **Post-fork webhook** — registers GitHub push/fork (or any) events on the new fork
|
|
53
|
+
|
|
54
|
+
---
|
|
55
|
+
|
|
56
|
+
## Installation
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install github-forker
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python ≥ 3.9 and PyGithub ≥ 1.55.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Quick start
|
|
67
|
+
|
|
68
|
+
```python
|
|
69
|
+
from github import Github
|
|
70
|
+
from pygithub_fork import GitHubForker
|
|
71
|
+
|
|
72
|
+
gh = Github("ghp_your_token")
|
|
73
|
+
forker = GitHubForker(gh)
|
|
74
|
+
|
|
75
|
+
result = forker.fork("octocat/Hello-World")
|
|
76
|
+
print(result.status) # ForkStatus.READY
|
|
77
|
+
print(result.clone_url) # https://github.com/you/Hello-World.git
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
---
|
|
81
|
+
|
|
82
|
+
## Usage
|
|
83
|
+
|
|
84
|
+
### 1. `fork()` — synchronous, blocking
|
|
85
|
+
|
|
86
|
+
Forks one repo and blocks until it is confirmed ready on GitHub's side.
|
|
87
|
+
|
|
88
|
+
```python
|
|
89
|
+
from pygithub_fork import GitHubForker, ForkerConfig
|
|
90
|
+
|
|
91
|
+
forker = GitHubForker(gh)
|
|
92
|
+
|
|
93
|
+
result = forker.fork("octocat/Hello-World")
|
|
94
|
+
# result.status → ForkStatus.READY
|
|
95
|
+
# result.fork → github.Repository.Repository
|
|
96
|
+
# result.clone_url
|
|
97
|
+
# result.ssh_url
|
|
98
|
+
# result.already_existed → False (or True on re-run)
|
|
99
|
+
# result.elapsed_seconds
|
|
100
|
+
```
|
|
101
|
+
|
|
102
|
+
Fork into an **organization** with a custom **name**:
|
|
103
|
+
|
|
104
|
+
```python
|
|
105
|
+
result = forker.fork(
|
|
106
|
+
"octocat/Hello-World",
|
|
107
|
+
organization="my-org",
|
|
108
|
+
name="hello-world-internal",
|
|
109
|
+
default_branch_only=True,
|
|
110
|
+
)
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
---
|
|
114
|
+
|
|
115
|
+
### 2. `fork_async()` — fire-and-forget, separated process
|
|
116
|
+
|
|
117
|
+
Submit a fork to the background thread pool and **return immediately**.
|
|
118
|
+
Query the `ForkJob` handle from anywhere — the caller is never blocked.
|
|
119
|
+
|
|
120
|
+
```python
|
|
121
|
+
job = forker.fork_async("octocat/Hello-World")
|
|
122
|
+
|
|
123
|
+
# --- do other things in the meantime ---
|
|
124
|
+
|
|
125
|
+
# Non-blocking status check:
|
|
126
|
+
print(job.done) # True / False
|
|
127
|
+
print(job.status) # ForkStatus.PENDING | CREATED | READY | FAILED …
|
|
128
|
+
|
|
129
|
+
# Access result without blocking (returns None if still running):
|
|
130
|
+
result = job.result # ForkResult | None
|
|
131
|
+
|
|
132
|
+
# Block when you actually need the answer:
|
|
133
|
+
result = job.wait() # blocks until done, returns ForkResult
|
|
134
|
+
result = job.wait(timeout=30) # TimeoutError after 30s if not done
|
|
135
|
+
```
|
|
136
|
+
|
|
137
|
+
This is the answer to **"fork then get status in a separate process"** — submit with `fork_async()` and poll `job.done` / `job.status` from any thread at any time without blocking.
|
|
138
|
+
|
|
139
|
+
**Concrete pattern — submit all, poll separately:**
|
|
140
|
+
|
|
141
|
+
```python
|
|
142
|
+
jobs = [forker.fork_async(repo) for repo in ["owner/a", "owner/b", "owner/c"]]
|
|
143
|
+
|
|
144
|
+
# ... do other work ...
|
|
145
|
+
|
|
146
|
+
# Later, collect all results:
|
|
147
|
+
results = [job.wait() for job in jobs]
|
|
148
|
+
|
|
149
|
+
# Or poll individually without waiting:
|
|
150
|
+
for job in jobs:
|
|
151
|
+
if job.done:
|
|
152
|
+
print(job.source_full_name, job.status)
|
|
153
|
+
else:
|
|
154
|
+
print(job.source_full_name, "still running")
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
159
|
+
### 3. `fork_many()` — bulk fork with thread pool
|
|
160
|
+
|
|
161
|
+
Fork a list in parallel (default) or sequentially:
|
|
162
|
+
|
|
163
|
+
```python
|
|
164
|
+
results = forker.fork_many([
|
|
165
|
+
"owner/repo-a",
|
|
166
|
+
"owner/repo-b",
|
|
167
|
+
"owner/repo-c",
|
|
168
|
+
])
|
|
169
|
+
|
|
170
|
+
for r in results:
|
|
171
|
+
print(r.source_full_name, r.status, r.succeeded)
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
**Parallel vs sequential:**
|
|
175
|
+
|
|
176
|
+
```python
|
|
177
|
+
# Parallel (default) — up to config.pool_workers concurrent forks
|
|
178
|
+
results = forker.fork_many(repos, parallel=True)
|
|
179
|
+
|
|
180
|
+
# Sequential — one at a time, guaranteed order, easier to debug
|
|
181
|
+
results = forker.fork_many(repos, parallel=False)
|
|
182
|
+
```
|
|
183
|
+
|
|
184
|
+
**Per-item control with `ForkRequest`:**
|
|
185
|
+
|
|
186
|
+
```python
|
|
187
|
+
from pygithub_fork import ForkRequest
|
|
188
|
+
|
|
189
|
+
requests = [
|
|
190
|
+
ForkRequest("owner/public-repo", organization="my-org"),
|
|
191
|
+
ForkRequest("owner/private-repo", name="private-fork", default_branch_only=True),
|
|
192
|
+
ForkRequest("owner/widget", organization="other-org", register_webhook=True,
|
|
193
|
+
webhook_url="https://ci.example.com/hooks/github"),
|
|
194
|
+
]
|
|
195
|
+
results = forker.fork_many(requests)
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
**Stop on first failure:**
|
|
199
|
+
|
|
200
|
+
```python
|
|
201
|
+
results = forker.fork_many(repos, stop_on_error=True)
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
---
|
|
205
|
+
|
|
206
|
+
### 4. `fork_iter()` — streaming results (completion order)
|
|
207
|
+
|
|
208
|
+
Yields each `ForkResult` as soon as it finishes — useful for large batches
|
|
209
|
+
where you want to start processing early:
|
|
210
|
+
|
|
211
|
+
```python
|
|
212
|
+
for result in forker.fork_iter(["owner/a", "owner/b", "owner/c"]):
|
|
213
|
+
# results arrive in completion order, not submission order
|
|
214
|
+
print(result.source_full_name, result.status)
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
---
|
|
218
|
+
|
|
219
|
+
### 5. Post-fork: upstream remote
|
|
220
|
+
|
|
221
|
+
After forking, automatically run `git remote add upstream <source_url>` in a
|
|
222
|
+
local clone:
|
|
223
|
+
|
|
224
|
+
```python
|
|
225
|
+
from pygithub_fork import ForkerConfig
|
|
226
|
+
|
|
227
|
+
config = ForkerConfig(
|
|
228
|
+
add_upstream_remote=True,
|
|
229
|
+
local_clone_path="/path/to/your/local/clone",
|
|
230
|
+
)
|
|
231
|
+
forker = GitHubForker(gh, config)
|
|
232
|
+
result = forker.fork("octocat/Hello-World")
|
|
233
|
+
|
|
234
|
+
print(result.upstream_remote_added) # True
|
|
235
|
+
# Now: git remote -v shows `upstream → https://github.com/octocat/Hello-World.git`
|
|
236
|
+
```
|
|
237
|
+
|
|
238
|
+
Override per-call:
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
result = forker.fork(
|
|
242
|
+
"octocat/Hello-World",
|
|
243
|
+
add_upstream_remote=True,
|
|
244
|
+
local_path="/path/to/clone",
|
|
245
|
+
)
|
|
246
|
+
```
|
|
247
|
+
|
|
248
|
+
---
|
|
249
|
+
|
|
250
|
+
### 6. Post-fork: webhook registration
|
|
251
|
+
|
|
252
|
+
Register a GitHub webhook on the new fork immediately after creation:
|
|
253
|
+
|
|
254
|
+
```python
|
|
255
|
+
config = ForkerConfig(
|
|
256
|
+
register_webhook=True,
|
|
257
|
+
webhook_url="https://ci.example.com/hooks/github",
|
|
258
|
+
webhook_events=["push", "pull_request", "fork"],
|
|
259
|
+
webhook_secret="s3cr3t",
|
|
260
|
+
)
|
|
261
|
+
forker = GitHubForker(gh, config)
|
|
262
|
+
result = forker.fork("octocat/Hello-World")
|
|
263
|
+
|
|
264
|
+
print(result.webhook_id) # GitHub hook ID
|
|
265
|
+
```
|
|
266
|
+
|
|
267
|
+
Override per-call:
|
|
268
|
+
|
|
269
|
+
```python
|
|
270
|
+
result = forker.fork(
|
|
271
|
+
"octocat/Hello-World",
|
|
272
|
+
register_webhook=True,
|
|
273
|
+
webhook_url="https://ci.example.com/hooks/github",
|
|
274
|
+
webhook_events=["push"],
|
|
275
|
+
)
|
|
276
|
+
```
|
|
277
|
+
|
|
278
|
+
---
|
|
279
|
+
|
|
280
|
+
### 7. Advanced configuration
|
|
281
|
+
|
|
282
|
+
```python
|
|
283
|
+
from pygithub_fork import ForkerConfig
|
|
284
|
+
|
|
285
|
+
config = ForkerConfig(
|
|
286
|
+
# Retry
|
|
287
|
+
max_retries=8,
|
|
288
|
+
base_backoff_seconds=2.0,
|
|
289
|
+
max_backoff_seconds=120.0,
|
|
290
|
+
|
|
291
|
+
# Readiness polling
|
|
292
|
+
wait_for_ready=True,
|
|
293
|
+
ready_timeout_seconds=120.0,
|
|
294
|
+
ready_poll_interval_seconds=3.0,
|
|
295
|
+
|
|
296
|
+
# Thread pool size (keep ≤ 4 to avoid GitHub secondary rate limits)
|
|
297
|
+
pool_workers=4,
|
|
298
|
+
|
|
299
|
+
# Post-fork actions
|
|
300
|
+
add_upstream_remote=True,
|
|
301
|
+
local_clone_path="/repos/my-clone",
|
|
302
|
+
register_webhook=True,
|
|
303
|
+
webhook_url="https://ci.example.com/hooks/github",
|
|
304
|
+
webhook_events=["push", "fork"],
|
|
305
|
+
webhook_secret="s3cr3t",
|
|
306
|
+
|
|
307
|
+
# Callbacks
|
|
308
|
+
on_retry=lambda attempt, exc, sleep: print(f"retry {attempt}: {exc}"),
|
|
309
|
+
on_fork_done=lambda result: print(f"done: {result.source_full_name}"),
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
forker = GitHubForker(gh, config)
|
|
313
|
+
```
|
|
314
|
+
|
|
315
|
+
---
|
|
316
|
+
|
|
317
|
+
### 8. Context manager (pool cleanup)
|
|
318
|
+
|
|
319
|
+
```python
|
|
320
|
+
with GitHubForker(gh) as forker:
|
|
321
|
+
results = forker.fork_many(repos)
|
|
322
|
+
# Thread pool is shut down cleanly here
|
|
323
|
+
```
|
|
324
|
+
|
|
325
|
+
---
|
|
326
|
+
|
|
327
|
+
## ForkResult fields
|
|
328
|
+
|
|
329
|
+
| Field | Type | Description |
|
|
330
|
+
|---|---|---|
|
|
331
|
+
| `source_full_name` | `str` | `"owner/repo"` of the source |
|
|
332
|
+
| `fork` | `Repository \| None` | The forked repo object (PyGithub) |
|
|
333
|
+
| `status` | `ForkStatus` | `READY`, `CREATED`, `ALREADY_EXISTED`, `TIMED_OUT_WAITING`, `FAILED` |
|
|
334
|
+
| `already_existed` | `bool` | True if the fork pre-existed |
|
|
335
|
+
| `attempts` | `int` | How many API attempts were made |
|
|
336
|
+
| `elapsed_seconds` | `float` | Wall time from call to return |
|
|
337
|
+
| `clone_url` | `str \| None` | HTTPS clone URL of the fork |
|
|
338
|
+
| `ssh_url` | `str \| None` | SSH URL of the fork |
|
|
339
|
+
| `upstream_remote_added` | `bool` | Whether `git remote add upstream` ran |
|
|
340
|
+
| `webhook_id` | `int \| None` | GitHub hook ID if registered |
|
|
341
|
+
| `error` | `Exception \| None` | Set on failure; None on success |
|
|
342
|
+
| `succeeded` | `bool` | `fork is not None and error is None` |
|
|
343
|
+
|
|
344
|
+
---
|
|
345
|
+
|
|
346
|
+
## ForkJob fields / methods (fork_async)
|
|
347
|
+
|
|
348
|
+
| | Description |
|
|
349
|
+
|---|---|
|
|
350
|
+
| `.done` | `bool` — non-blocking check |
|
|
351
|
+
| `.status` | `ForkStatus` — `PENDING` while running, real status when done |
|
|
352
|
+
| `.result` | `ForkResult \| None` — non-blocking; `None` if still running |
|
|
353
|
+
| `.wait(timeout=None)` | Block and return `ForkResult`; raises `ForkError` on failure |
|
|
354
|
+
| `.source_full_name` | The `"owner/repo"` string passed in |
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## Exception hierarchy
|
|
359
|
+
|
|
360
|
+
```
|
|
361
|
+
ForkError
|
|
362
|
+
├── ForkTimeoutError # readiness timeout
|
|
363
|
+
├── ForkPermissionError # 401/403 (distinct from secondary rate limit)
|
|
364
|
+
├── RepositoryNotFoundError
|
|
365
|
+
├── WebhookError # webhook registration failed
|
|
366
|
+
└── UpstreamRemoteError # git remote add upstream failed
|
|
367
|
+
```
|
|
368
|
+
|
|
369
|
+
---
|
|
370
|
+
|
|
371
|
+
## License
|
|
372
|
+
|
|
373
|
+
MIT © [Hadi Cahyadi](https://github.com/cumulus13)
|
|
374
|
+
|
|
375
|
+
## 👤 Author
|
|
376
|
+
|
|
377
|
+
[Hadi Cahyadi](mailto:cumulus13@gmail.com)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
[](https://www.buymeacoffee.com/cumulus13)
|
|
381
|
+
|
|
382
|
+
[](https://ko-fi.com/cumulus13)
|
|
383
|
+
|
|
384
|
+
[Support me on Patreon](https://www.patreon.com/cumulus13)
|