dump-github 0.2.0__py3-none-any.whl → 0.2.1__py3-none-any.whl
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.
- dump_github/_version.py +1 -1
- dump_github/lib.py +202 -15
- dump_github/main.py +26 -5
- {dump_github-0.2.0.dist-info → dump_github-0.2.1.dist-info}/METADATA +14 -2
- dump_github-0.2.1.dist-info/RECORD +10 -0
- dump_github-0.2.0.dist-info/RECORD +0 -10
- {dump_github-0.2.0.dist-info → dump_github-0.2.1.dist-info}/WHEEL +0 -0
- {dump_github-0.2.0.dist-info → dump_github-0.2.1.dist-info}/entry_points.txt +0 -0
- {dump_github-0.2.0.dist-info → dump_github-0.2.1.dist-info}/licenses/LICENSE +0 -0
- {dump_github-0.2.0.dist-info → dump_github-0.2.1.dist-info}/top_level.txt +0 -0
dump_github/_version.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "0.2.
|
|
1
|
+
__version__ = "0.2.1"
|
dump_github/lib.py
CHANGED
|
@@ -1,9 +1,22 @@
|
|
|
1
|
+
import re
|
|
2
|
+
import warnings
|
|
1
3
|
from urllib.parse import quote
|
|
2
4
|
|
|
3
5
|
import requests
|
|
4
6
|
|
|
5
7
|
GITHUB_API = "https://api.github.com"
|
|
8
|
+
GITHUB_WEB = "https://github.com"
|
|
6
9
|
PER_PAGE = 100
|
|
10
|
+
WEB_USER_AGENT = "dump-github (+https://github.com/Core2002/dump_github)"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class GitHubAPIError(RuntimeError):
|
|
14
|
+
"""GitHub could not be read through the REST API (or the web fallback)."""
|
|
15
|
+
|
|
16
|
+
def __init__(self, status_code, message):
|
|
17
|
+
self.status_code = status_code
|
|
18
|
+
self.message = message
|
|
19
|
+
super().__init__(message)
|
|
7
20
|
|
|
8
21
|
|
|
9
22
|
def _github_auth_headers(token):
|
|
@@ -28,26 +41,42 @@ def _api_headers(token=""):
|
|
|
28
41
|
|
|
29
42
|
|
|
30
43
|
def _api_get(path, token=""):
|
|
31
|
-
"""GET one GitHub REST API path and return the decoded JSON
|
|
44
|
+
"""GET one GitHub REST API path and return the decoded JSON.
|
|
45
|
+
|
|
46
|
+
Raises GitHubAPIError instead of silently returning an empty result, so
|
|
47
|
+
callers can distinguish "no repositories" from "request failed".
|
|
48
|
+
"""
|
|
32
49
|
try:
|
|
33
50
|
response = requests.get(
|
|
34
51
|
GITHUB_API + path, headers=_api_headers(token), timeout=30
|
|
35
52
|
)
|
|
36
|
-
except requests.RequestException:
|
|
37
|
-
|
|
53
|
+
except requests.RequestException as exc:
|
|
54
|
+
raise GitHubAPIError(None, "request failed: {}".format(exc)) from exc
|
|
38
55
|
if response.status_code != 200:
|
|
39
|
-
|
|
56
|
+
message = "HTTP {}".format(response.status_code)
|
|
57
|
+
try:
|
|
58
|
+
payload = response.json()
|
|
59
|
+
except ValueError:
|
|
60
|
+
payload = None
|
|
61
|
+
if isinstance(payload, dict) and payload.get("message"):
|
|
62
|
+
message = "{} (HTTP {})".format(payload["message"], response.status_code)
|
|
63
|
+
raise GitHubAPIError(response.status_code, message)
|
|
40
64
|
try:
|
|
41
65
|
return response.json()
|
|
42
|
-
except ValueError:
|
|
43
|
-
|
|
66
|
+
except ValueError as exc:
|
|
67
|
+
raise GitHubAPIError(
|
|
68
|
+
response.status_code, "invalid JSON response from GitHub"
|
|
69
|
+
) from exc
|
|
44
70
|
|
|
45
71
|
|
|
46
72
|
def get_authenticated_login(token=""):
|
|
47
73
|
"""Return the login of the token owner, or None if there is no usable token."""
|
|
48
74
|
if not _github_auth_headers(token):
|
|
49
75
|
return None
|
|
50
|
-
|
|
76
|
+
try:
|
|
77
|
+
account = _api_get("/user", token)
|
|
78
|
+
except GitHubAPIError:
|
|
79
|
+
return None
|
|
51
80
|
if not isinstance(account, dict):
|
|
52
81
|
return None
|
|
53
82
|
return account.get("login") or None
|
|
@@ -56,12 +85,18 @@ def get_authenticated_login(token=""):
|
|
|
56
85
|
def get_account_type(username, token=""):
|
|
57
86
|
"""Return ``"Organization"``/``"User"`` for ``username``, or None if unknown."""
|
|
58
87
|
quoted = quote(username, safe="")
|
|
59
|
-
|
|
88
|
+
try:
|
|
89
|
+
account = _api_get("/users/{}".format(quoted), token)
|
|
90
|
+
except GitHubAPIError:
|
|
91
|
+
account = None
|
|
60
92
|
if isinstance(account, dict) and account.get("type"):
|
|
61
93
|
return account["type"]
|
|
62
94
|
# ``/users/{org}`` is not guaranteed to resolve for organisations, so fall
|
|
63
95
|
# back to the org endpoint, which unambiguously identifies one.
|
|
64
|
-
|
|
96
|
+
try:
|
|
97
|
+
org = _api_get("/orgs/{}".format(quoted), token)
|
|
98
|
+
except GitHubAPIError:
|
|
99
|
+
org = None
|
|
65
100
|
if isinstance(org, dict) and org.get("login"):
|
|
66
101
|
return "Organization"
|
|
67
102
|
return None
|
|
@@ -131,13 +166,133 @@ def get_page_of_repos(username, page, token="", authenticated_login=None,
|
|
|
131
166
|
return [repo for repo in repos if _is_owned_by(repo, username)]
|
|
132
167
|
|
|
133
168
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
169
|
+
# --- public web fallback -------------------------------------------------
|
|
170
|
+
#
|
|
171
|
+
# api.github.com allows only 60 unauthenticated requests per hour *per IP*.
|
|
172
|
+
# Shared/NATed networks and CI runners exhaust that quickly, which used to make
|
|
173
|
+
# ``dump-github user`` silently return nothing. github.com's repository pages
|
|
174
|
+
# are not subject to that API quota, so they are used as a read-only fallback
|
|
175
|
+
# for public repositories when no token is configured.
|
|
176
|
+
|
|
177
|
+
_USER_REPO_TAG_RE = re.compile(r'<a\b[^>]*itemprop="name codeRepository"[^>]*>')
|
|
178
|
+
_WEB_REPO_HREF_RE = re.compile(r'href="/([^"/]+)/([^"/?#]+)"')
|
|
179
|
+
_WEB_NEXT_RE = re.compile(r'rel="next"')
|
|
180
|
+
_WEB_NON_REPO_PATHS = {
|
|
181
|
+
"followers",
|
|
182
|
+
"following",
|
|
183
|
+
"people",
|
|
184
|
+
"packages",
|
|
185
|
+
"projects",
|
|
186
|
+
"repositories",
|
|
187
|
+
"settings",
|
|
188
|
+
"sponsoring",
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
|
|
192
|
+
def _web_get(url):
|
|
193
|
+
try:
|
|
194
|
+
response = requests.get(
|
|
195
|
+
url, headers={"User-Agent": WEB_USER_AGENT}, timeout=30
|
|
196
|
+
)
|
|
197
|
+
except requests.RequestException as exc:
|
|
198
|
+
raise GitHubAPIError(None, "web request failed: {}".format(exc)) from exc
|
|
199
|
+
if response.status_code != 200:
|
|
200
|
+
raise GitHubAPIError(
|
|
201
|
+
response.status_code,
|
|
202
|
+
"github.com returned HTTP {}".format(response.status_code),
|
|
203
|
+
)
|
|
204
|
+
return response.text
|
|
205
|
+
|
|
206
|
+
|
|
207
|
+
def _web_repo(owner, name):
|
|
208
|
+
return {
|
|
209
|
+
"name": name,
|
|
210
|
+
"full_name": "{}/{}".format(owner, name),
|
|
211
|
+
"owner": {"login": owner},
|
|
212
|
+
"clone_url": "{}/{}/{}.git".format(GITHUB_WEB, owner, name),
|
|
213
|
+
# The default branch is not exposed by the web listing; zip downloads
|
|
214
|
+
# then use the HEAD archive instead.
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def _parse_user_repos_page(html, username):
|
|
219
|
+
repos = []
|
|
220
|
+
seen = set()
|
|
221
|
+
for tag in _USER_REPO_TAG_RE.findall(html):
|
|
222
|
+
match = _WEB_REPO_HREF_RE.search(tag)
|
|
223
|
+
if not match:
|
|
224
|
+
continue
|
|
225
|
+
owner, name = match.group(1), match.group(2)
|
|
226
|
+
if owner.lower() != username.lower() or name in seen:
|
|
227
|
+
continue
|
|
228
|
+
seen.add(name)
|
|
229
|
+
repos.append(_web_repo(owner, name))
|
|
230
|
+
return repos
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
def _parse_org_repos_page(html, org):
|
|
234
|
+
repos = []
|
|
235
|
+
seen = set()
|
|
236
|
+
for owner, name in _WEB_REPO_HREF_RE.findall(html):
|
|
237
|
+
if owner.lower() != org.lower():
|
|
238
|
+
continue
|
|
239
|
+
if name.lower() in _WEB_NON_REPO_PATHS or name in seen:
|
|
240
|
+
continue
|
|
241
|
+
seen.add(name)
|
|
242
|
+
repos.append(_web_repo(owner, name))
|
|
243
|
+
return repos
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def _web_page_url(username, page, is_org):
|
|
247
|
+
quoted = quote(username, safe="")
|
|
248
|
+
if is_org:
|
|
249
|
+
return "{}/orgs/{}/repositories?page={}".format(GITHUB_WEB, quoted, page)
|
|
250
|
+
return "{}/{}?tab=repositories&page={}".format(GITHUB_WEB, quoted, page)
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def _parse_web_page(html, username, is_org):
|
|
254
|
+
if is_org:
|
|
255
|
+
return _parse_org_repos_page(html, username)
|
|
256
|
+
return _parse_user_repos_page(html, username)
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _search_via_web(username, account_type):
|
|
260
|
+
"""List public repositories from github.com when the REST API is unavailable."""
|
|
261
|
+
is_org = (account_type or "").lower() == "organization"
|
|
262
|
+
first = _web_get(_web_page_url(username, 1, is_org))
|
|
263
|
+
repos = _parse_web_page(first, username, is_org)
|
|
264
|
+
if not repos and account_type is None:
|
|
265
|
+
# The account may be an organisation: its profile page redirects to
|
|
266
|
+
# github.com/{org} and does not contain the plain repository list.
|
|
267
|
+
try:
|
|
268
|
+
org_first = _web_get(_web_page_url(username, 1, True))
|
|
269
|
+
except GitHubAPIError as exc:
|
|
270
|
+
if exc.status_code == 404:
|
|
271
|
+
return []
|
|
272
|
+
raise
|
|
273
|
+
org_repos = _parse_org_repos_page(org_first, username)
|
|
274
|
+
if not org_repos:
|
|
275
|
+
return []
|
|
276
|
+
is_org = True
|
|
277
|
+
repos = org_repos
|
|
278
|
+
first = org_first
|
|
279
|
+
if not repos:
|
|
280
|
+
return []
|
|
281
|
+
page = 1
|
|
282
|
+
while _WEB_NEXT_RE.search(first):
|
|
283
|
+
page += 1
|
|
284
|
+
html = _web_get(_web_page_url(username, page, is_org))
|
|
285
|
+
page_repos = _parse_web_page(html, username, is_org)
|
|
286
|
+
if not page_repos:
|
|
287
|
+
break
|
|
288
|
+
repos.extend(page_repos)
|
|
289
|
+
first = html
|
|
290
|
+
return repos
|
|
291
|
+
|
|
292
|
+
|
|
293
|
+
def _search_via_api(username, token, authenticated_login, account_type):
|
|
138
294
|
page = 0
|
|
139
295
|
res_repos = []
|
|
140
|
-
|
|
141
296
|
while True:
|
|
142
297
|
page += 1
|
|
143
298
|
repos = _fetch_repos_page(
|
|
@@ -148,10 +303,42 @@ def search_user_repos(username, token=""):
|
|
|
148
303
|
res_repos += repos
|
|
149
304
|
if len(repos) < PER_PAGE:
|
|
150
305
|
break
|
|
306
|
+
return res_repos
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def search_user_repos(username, token=""):
|
|
310
|
+
username = username.strip()
|
|
311
|
+
authenticated_login = get_authenticated_login(token)
|
|
312
|
+
account_type = get_account_type(username, token)
|
|
313
|
+
try:
|
|
314
|
+
repos = _search_via_api(
|
|
315
|
+
username, token, authenticated_login, account_type
|
|
316
|
+
)
|
|
317
|
+
except GitHubAPIError as exc:
|
|
318
|
+
if token or exc.status_code == 404:
|
|
319
|
+
raise
|
|
320
|
+
warnings.warn(
|
|
321
|
+
"GitHub API request failed ({}); falling back to the public GitHub "
|
|
322
|
+
"web listing. Only public repositories can be listed without a "
|
|
323
|
+
"token.".format(exc),
|
|
324
|
+
RuntimeWarning,
|
|
325
|
+
stacklevel=2,
|
|
326
|
+
)
|
|
327
|
+
repos = _search_via_web(username, account_type)
|
|
151
328
|
|
|
152
329
|
# Never return repositories that belong to somebody else (e.g. a user repo
|
|
153
330
|
# returned for an organisation, or an organisation the token owner is in).
|
|
154
|
-
|
|
331
|
+
result = []
|
|
332
|
+
seen = set()
|
|
333
|
+
for repo in repos:
|
|
334
|
+
if not _is_owned_by(repo, username):
|
|
335
|
+
continue
|
|
336
|
+
key = (repo.get("full_name") or repo.get("name") or "").lower()
|
|
337
|
+
if key in seen:
|
|
338
|
+
continue
|
|
339
|
+
seen.add(key)
|
|
340
|
+
result.append(repo)
|
|
341
|
+
return result
|
|
155
342
|
|
|
156
343
|
|
|
157
344
|
# Backward-compatible alias for the historical typo
|
dump_github/main.py
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import argparse
|
|
2
2
|
import os
|
|
3
3
|
import subprocess
|
|
4
|
+
import sys
|
|
4
5
|
|
|
5
6
|
from ._version import __version__
|
|
6
|
-
from .lib import search_user_repos
|
|
7
|
+
from .lib import GitHubAPIError, search_user_repos
|
|
7
8
|
|
|
8
9
|
# One-shot credential helper: git asks for credentials, the helper answers with
|
|
9
10
|
# the token from the environment. The token is therefore never part of the
|
|
@@ -67,7 +68,7 @@ def clone_repo(repo, token=""):
|
|
|
67
68
|
|
|
68
69
|
|
|
69
70
|
def download_zip(user_name, reop, token="", limit_size=100):
|
|
70
|
-
ref = reop.get("default_branch")
|
|
71
|
+
ref = reop.get("default_branch")
|
|
71
72
|
# Always archive the repository the API actually listed. Using the
|
|
72
73
|
# positional username here would point at the wrong owner whenever the
|
|
73
74
|
# repo does not belong to that user.
|
|
@@ -88,7 +89,9 @@ def download_zip(user_name, reop, token="", limit_size=100):
|
|
|
88
89
|
if max_size > 0:
|
|
89
90
|
curl_cmd.extend(["--max-filesize", str(max_size)])
|
|
90
91
|
if token:
|
|
91
|
-
url = "https://api.github.com/repos/{}/zipball
|
|
92
|
+
url = "https://api.github.com/repos/{}/zipball".format(full_name)
|
|
93
|
+
if ref:
|
|
94
|
+
url += "/{}".format(ref)
|
|
92
95
|
curl_cmd.extend(
|
|
93
96
|
[
|
|
94
97
|
"-H",
|
|
@@ -101,7 +104,12 @@ def download_zip(user_name, reop, token="", limit_size=100):
|
|
|
101
104
|
]
|
|
102
105
|
)
|
|
103
106
|
else:
|
|
104
|
-
|
|
107
|
+
# The web listing does not know the default branch; GitHub resolves
|
|
108
|
+
# HEAD to it.
|
|
109
|
+
if ref:
|
|
110
|
+
url = "https://github.com/{}/archive/refs/heads/{}.zip".format(full_name, ref)
|
|
111
|
+
else:
|
|
112
|
+
url = "https://github.com/{}/archive/HEAD.zip".format(full_name)
|
|
105
113
|
curl_cmd.extend(["-o", file_name, url])
|
|
106
114
|
print("Download {} : {}".format(reop["name"], url))
|
|
107
115
|
subprocess.run(curl_cmd, check=False)
|
|
@@ -117,7 +125,20 @@ def download_zip(user_name, reop, token="", limit_size=100):
|
|
|
117
125
|
def main(argv=None):
|
|
118
126
|
args = parser.parse_args(argv)
|
|
119
127
|
token = (args.token or "").strip() or (os.environ.get("GITHUB_TOKEN") or "").strip()
|
|
120
|
-
|
|
128
|
+
try:
|
|
129
|
+
repos = search_user_repos(args.username, token)
|
|
130
|
+
except GitHubAPIError as exc:
|
|
131
|
+
print(
|
|
132
|
+
"error: cannot list repositories for {}: {}".format(args.username, exc),
|
|
133
|
+
file=sys.stderr,
|
|
134
|
+
)
|
|
135
|
+
raise SystemExit(1)
|
|
136
|
+
if not repos:
|
|
137
|
+
print(
|
|
138
|
+
"No public repositories found for {}.".format(args.username),
|
|
139
|
+
file=sys.stderr,
|
|
140
|
+
)
|
|
141
|
+
return
|
|
121
142
|
for repo in repos:
|
|
122
143
|
if args.print:
|
|
123
144
|
print(repo["clone_url"])
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: dump-github
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.1
|
|
4
4
|
Summary: Back up users' GitHub repositories in case they need them later.
|
|
5
5
|
Author-email: core2002 <core2002@aliyun.com>
|
|
6
6
|
License-Expression: MIT
|
|
@@ -81,6 +81,15 @@ GitHub’s API requires a proper `Authorization: Bearer <token>` header. This to
|
|
|
81
81
|
- **Private repositories**: Without a token, only public repos are listed and can be cloned or archived. With a token that has the right scopes, you can include private repos you are allowed to access (for example when backing up your own account).
|
|
82
82
|
- **Rate limits**: Authenticated API requests get a much higher limit than anonymous requests.
|
|
83
83
|
|
|
84
|
+
**Without a token**
|
|
85
|
+
|
|
86
|
+
Only public repositories can be listed and downloaded. The anonymous GitHub API allows
|
|
87
|
+
60 requests per hour per IP, which shared/NATed networks and CI runners exhaust quickly;
|
|
88
|
+
if `api.github.com` is rate-limited or otherwise unavailable, the tool automatically
|
|
89
|
+
falls back to the public GitHub web listing
|
|
90
|
+
(`github.com/{user}?tab=repositories` / `github.com/orgs/{org}/repositories`), prints a
|
|
91
|
+
warning and keeps going. Private repositories still require a token.
|
|
92
|
+
|
|
84
93
|
**How the token is used**
|
|
85
94
|
|
|
86
95
|
- **Listing repos**: sent as `Bearer` on the REST API and scoped strictly to the requested account:
|
|
@@ -97,9 +106,12 @@ Create a token under GitHub → **Settings** → **Developer settings** → **Pe
|
|
|
97
106
|
**Examples**
|
|
98
107
|
|
|
99
108
|
```bash
|
|
100
|
-
# Public repos only (no token)
|
|
109
|
+
# Public repos only (no token; falls back to the web listing if the API is rate-limited)
|
|
101
110
|
dump-github Core2002 -p
|
|
102
111
|
|
|
112
|
+
# Public backup without any credentials
|
|
113
|
+
dump-github Core2002 -d
|
|
114
|
+
|
|
103
115
|
# Token on the command line (private + higher API limits)
|
|
104
116
|
dump-github username --token ghp_xxxxxxxxxxxx
|
|
105
117
|
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
dump_github/__init__.py,sha256=7bV8yfP0ii_gvNXKfDzFhBj2V5nC82mcJrlZqs9fRZ0,92
|
|
2
|
+
dump_github/_version.py,sha256=HfjVOrpTnmZ-xVFCYSVmX50EXaBQeJteUHG-PD6iQs8,22
|
|
3
|
+
dump_github/lib.py,sha256=elhVYzMSK2abVKPWqQUukQCCNdxdkCjGQPiW7kauGkA,11662
|
|
4
|
+
dump_github/main.py,sha256=unifRRUhyvu_2e7UMmoIxtRbVOANUGUUIQJbhaDQ_I8,4768
|
|
5
|
+
dump_github-0.2.1.dist-info/licenses/LICENSE,sha256=5tQa-lQLqvLhjTuELEk4bm70I5hcdjUeOn4qf3MK84E,1064
|
|
6
|
+
dump_github-0.2.1.dist-info/METADATA,sha256=-Ol1P5UNwJe6OjSiLD6LkFHQKbUO-jHYI5yEV-D7Ru8,7373
|
|
7
|
+
dump_github-0.2.1.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
8
|
+
dump_github-0.2.1.dist-info/entry_points.txt,sha256=2OAP2WnA1p54LWyCw3tHe44Pf4aR7Thsxf9uWKWfWl8,49
|
|
9
|
+
dump_github-0.2.1.dist-info/top_level.txt,sha256=6oWfdkP2U1AN9zL6AUIUTxe6hYIeo_Nf8M8SWBxElUw,12
|
|
10
|
+
dump_github-0.2.1.dist-info/RECORD,,
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
dump_github/__init__.py,sha256=7bV8yfP0ii_gvNXKfDzFhBj2V5nC82mcJrlZqs9fRZ0,92
|
|
2
|
-
dump_github/_version.py,sha256=Zn1KFblwuFHiDRdRAiRnDBRkbPttWh44jKa5zG2ov0E,22
|
|
3
|
-
dump_github/lib.py,sha256=l35zDGPRRKdMv6KzzcqJXnCmOMLrPY07wnsijcwbfCA,5485
|
|
4
|
-
dump_github/main.py,sha256=lYSma1C2m-7Pl5r4zwbEfeZyKqgVf2H2jFwD-98hnYg,4122
|
|
5
|
-
dump_github-0.2.0.dist-info/licenses/LICENSE,sha256=5tQa-lQLqvLhjTuELEk4bm70I5hcdjUeOn4qf3MK84E,1064
|
|
6
|
-
dump_github-0.2.0.dist-info/METADATA,sha256=vjimbmFSa9hnjYn8L58iX79KvNbL3tq104VVsIXg09I,6766
|
|
7
|
-
dump_github-0.2.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
8
|
-
dump_github-0.2.0.dist-info/entry_points.txt,sha256=2OAP2WnA1p54LWyCw3tHe44Pf4aR7Thsxf9uWKWfWl8,49
|
|
9
|
-
dump_github-0.2.0.dist-info/top_level.txt,sha256=6oWfdkP2U1AN9zL6AUIUTxe6hYIeo_Nf8M8SWBxElUw,12
|
|
10
|
-
dump_github-0.2.0.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|