bugpipe 3.0.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.
- bugpipe/__init__.py +42 -0
- bugpipe/api/AUDIT.md +331 -0
- bugpipe/api/README.md +728 -0
- bugpipe/api/__init__.py +3 -0
- bugpipe/api/client.py +328 -0
- bugpipe/api/models.py +742 -0
- bugpipe/api/parser.py +720 -0
- bugpipe/cli/__init__.py +69 -0
- bugpipe/cli/cmd.py +304 -0
- bugpipe/cli/term.py +234 -0
- bugpipe/cli/update_checker.py +207 -0
- bugpipe-3.0.1.dist-info/METADATA +52 -0
- bugpipe-3.0.1.dist-info/RECORD +15 -0
- bugpipe-3.0.1.dist-info/WHEEL +4 -0
- bugpipe-3.0.1.dist-info/entry_points.txt +3 -0
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
import re
|
|
5
|
+
import time
|
|
6
|
+
import typing as t
|
|
7
|
+
from importlib.metadata import version
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from tempfile import gettempdir
|
|
10
|
+
|
|
11
|
+
import httpx
|
|
12
|
+
from rich.status import Status
|
|
13
|
+
|
|
14
|
+
__pkg__ = "bugpipe"
|
|
15
|
+
__version__ = version(__pkg__)
|
|
16
|
+
|
|
17
|
+
CACHE_FILE = Path(gettempdir()) / "bugpipe_update_check.json"
|
|
18
|
+
CACHE_TTL = 3600
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def query_pypi(package: str, include_prereleases: bool) -> dict:
|
|
22
|
+
"""
|
|
23
|
+
Query PyPI for the latest version of a package.
|
|
24
|
+
|
|
25
|
+
:param package: The package name to look up on PyPI.
|
|
26
|
+
:param include_prereleases: Whether to consider pre-release versions.
|
|
27
|
+
:return: A dict with ``success`` key, and ``data`` containing
|
|
28
|
+
``version`` and ``upload_time`` on success.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
try:
|
|
32
|
+
response = httpx.get(f"https://pypi.org/pypi/{package}/json", timeout=3)
|
|
33
|
+
except httpx.HTTPError:
|
|
34
|
+
return {"success": False}
|
|
35
|
+
if response.status_code != 200:
|
|
36
|
+
return {"success": False}
|
|
37
|
+
data = response.json()
|
|
38
|
+
versions = list(data["releases"].keys())
|
|
39
|
+
versions.sort(key=parse_version, reverse=True)
|
|
40
|
+
|
|
41
|
+
version = versions[0]
|
|
42
|
+
for tmp_version in versions:
|
|
43
|
+
if include_prereleases or standard_release(tmp_version):
|
|
44
|
+
version = tmp_version
|
|
45
|
+
break
|
|
46
|
+
|
|
47
|
+
upload_time = None
|
|
48
|
+
for file_info in data["releases"][version]:
|
|
49
|
+
if file_info["upload_time"]:
|
|
50
|
+
upload_time = file_info["upload_time"]
|
|
51
|
+
break
|
|
52
|
+
|
|
53
|
+
return {"success": True, "data": {"upload_time": upload_time, "version": version}}
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def cached_query_pypi(package: str, include_prereleases: bool) -> dict:
|
|
57
|
+
"""
|
|
58
|
+
Return the PyPI lookup result, reusing a cached one under an hour old.
|
|
59
|
+
|
|
60
|
+
:param package: The package name to look up on PyPI.
|
|
61
|
+
:param include_prereleases: Whether to consider pre-release versions.
|
|
62
|
+
:return: The same dict as :func:`query_pypi`.
|
|
63
|
+
"""
|
|
64
|
+
|
|
65
|
+
key = {"package": package, "include_prereleases": include_prereleases}
|
|
66
|
+
try:
|
|
67
|
+
cached = json.loads(CACHE_FILE.read_text())
|
|
68
|
+
if cached["key"] == key and time.time() - cached["time"] < CACHE_TTL:
|
|
69
|
+
return cached["data"]
|
|
70
|
+
except (OSError, ValueError, KeyError, TypeError):
|
|
71
|
+
pass # missing or unreadable cache, query PyPI
|
|
72
|
+
|
|
73
|
+
data = query_pypi(package=package, include_prereleases=include_prereleases)
|
|
74
|
+
if data.get("success"):
|
|
75
|
+
try:
|
|
76
|
+
CACHE_FILE.write_text(
|
|
77
|
+
json.dumps({"key": key, "time": time.time(), "data": data})
|
|
78
|
+
)
|
|
79
|
+
except OSError:
|
|
80
|
+
pass # cache is best effort
|
|
81
|
+
return data
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def standard_release(version: str) -> bool:
|
|
85
|
+
"""
|
|
86
|
+
Check whether a version string represents a standard (non-pre-release) release.
|
|
87
|
+
|
|
88
|
+
:param version: The version string to check.
|
|
89
|
+
:return: ``True`` if the version contains only digits and dots.
|
|
90
|
+
"""
|
|
91
|
+
|
|
92
|
+
return version.replace(".", "").isdigit()
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
def check(
|
|
96
|
+
package_name: str = "buganize",
|
|
97
|
+
package_version: str = __version__,
|
|
98
|
+
status: Status | None = None,
|
|
99
|
+
):
|
|
100
|
+
"""Print a notice when PyPI has a newer version.
|
|
101
|
+
|
|
102
|
+
:param package_name: Package to check.
|
|
103
|
+
:param package_version: Running version.
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
if isinstance(status, Status):
|
|
107
|
+
status.update("[dim]Checking for updates…[/dim]")
|
|
108
|
+
|
|
109
|
+
data = cached_query_pypi(
|
|
110
|
+
package=package_name,
|
|
111
|
+
include_prereleases=not standard_release(version=package_version),
|
|
112
|
+
)
|
|
113
|
+
if not data.get("success") or (
|
|
114
|
+
parse_version(string=package_version)
|
|
115
|
+
>= parse_version(string=data["data"]["version"])
|
|
116
|
+
):
|
|
117
|
+
return
|
|
118
|
+
|
|
119
|
+
available = data["data"]["version"]
|
|
120
|
+
message = (
|
|
121
|
+
f"Version {package_version} of {package_name} is outdated. "
|
|
122
|
+
f"Version {available} "
|
|
123
|
+
)
|
|
124
|
+
release_date = data["data"]["upload_time"]
|
|
125
|
+
message += (
|
|
126
|
+
f"was released on {release_date[:10]}." if release_date else "is available."
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
from .term import console
|
|
130
|
+
|
|
131
|
+
console.log(f"[bold blue]⬆[/bold blue] {message}")
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
# The following section of code is taken from setuptools pkg_resources.py (PSF
|
|
135
|
+
# license). Unfortunately importing pkg_resources to directly use the
|
|
136
|
+
# parse_version function results in some undesired side effects.
|
|
137
|
+
|
|
138
|
+
component_re = re.compile(r"(\d+ | [a-z]+ | \.| -)", re.VERBOSE)
|
|
139
|
+
replace = {"pre": "c", "preview": "c", "-": "final-", "rc": "c", "dev": "@"}.get
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _parse_version_parts(version_string: str) -> t.Generator[str]:
|
|
143
|
+
"""
|
|
144
|
+
Yield normalized version parts from a version string.
|
|
145
|
+
|
|
146
|
+
:param version_string: A version string to split into comparable parts.
|
|
147
|
+
"""
|
|
148
|
+
|
|
149
|
+
for part in component_re.split(version_string):
|
|
150
|
+
part = replace(part, part)
|
|
151
|
+
if not part or part == ".":
|
|
152
|
+
continue
|
|
153
|
+
if part[:1] in "0123456789":
|
|
154
|
+
yield part.zfill(8) # pad for numeric comparison
|
|
155
|
+
else:
|
|
156
|
+
yield "*" + part
|
|
157
|
+
|
|
158
|
+
yield "*final" # ensure that alpha/beta/candidate are before final
|
|
159
|
+
|
|
160
|
+
|
|
161
|
+
def parse_version(string: str) -> tuple[str, ...]:
|
|
162
|
+
"""
|
|
163
|
+
Convert a version string to a chronologically-sortable key.
|
|
164
|
+
|
|
165
|
+
This is a rough cross between distutils' StrictVersion and LooseVersion;
|
|
166
|
+
if you give it versions that would work with StrictVersion, then it behaves
|
|
167
|
+
the same; otherwise it acts like a slightly-smarter LooseVersion. It is
|
|
168
|
+
*possible* to create pathological version coding schemes that will fool
|
|
169
|
+
this parser, but they should be very rare in practice.
|
|
170
|
+
|
|
171
|
+
The returned value will be a tuple of strings. Numeric portions of the
|
|
172
|
+
version are padded to 8 digits so they will compare numerically, but
|
|
173
|
+
without relying on how numbers compare relative to strings. Dots are
|
|
174
|
+
dropped, but dashes are retained. Trailing zeros between alpha segments
|
|
175
|
+
or dashes are suppressed, so that e.g. "2.4.0" is considered the same as
|
|
176
|
+
"2.4". Alphanumeric parts are lower-cased.
|
|
177
|
+
|
|
178
|
+
The algorithm assumes that strings like "-" and any alpha string that
|
|
179
|
+
alphabetically follows "final" represents a "patch level". So, "2.4-1"
|
|
180
|
+
is assumed to be a branch or patch of "2.4", and therefore "2.4.1" is
|
|
181
|
+
considered newer than "2.4-1", which in turn is newer than "2.4".
|
|
182
|
+
|
|
183
|
+
Strings like "a", "b", "c", "alpha", "beta", "candidate" and so on (that
|
|
184
|
+
come before "final" alphabetically) are assumed to be pre-release versions,
|
|
185
|
+
so that the version "2.4" is considered newer than "2.4a1".
|
|
186
|
+
|
|
187
|
+
Finally, to handle miscellaneous cases, the strings "pre", "preview", and
|
|
188
|
+
"rc" are treated as if they were "c", i.e. as though they were release
|
|
189
|
+
candidates, and therefore are not as new as a version string that does not
|
|
190
|
+
contain them, and "dev" is replaced with an '@' so that it sorts lower than
|
|
191
|
+
any other pre-release tag.
|
|
192
|
+
|
|
193
|
+
:param string: The version string to parse.
|
|
194
|
+
:return: A tuple of strings suitable for comparison.
|
|
195
|
+
"""
|
|
196
|
+
|
|
197
|
+
parts = []
|
|
198
|
+
for part in _parse_version_parts(string.lower()):
|
|
199
|
+
if part.startswith("*"):
|
|
200
|
+
if part < "*final": # remove '-' before a prerelease tag
|
|
201
|
+
while parts and parts[-1] == "*final-":
|
|
202
|
+
parts.pop()
|
|
203
|
+
# remove trailing zeros from each series of numeric parts
|
|
204
|
+
while parts and parts[-1] == "00000000":
|
|
205
|
+
parts.pop()
|
|
206
|
+
parts.append(part)
|
|
207
|
+
return tuple(parts)
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bugpipe
|
|
3
|
+
Version: 3.0.1
|
|
4
|
+
Summary: Unofficial Python client for Buganizer; the Google Issue Tracking system
|
|
5
|
+
Keywords: buganizer,google-issue-tracker,issue-tracker,bug-tracker,bug-bounty,vulnerability,security,chromium,cve,vrp,google
|
|
6
|
+
Author: Ritchie Mwewa
|
|
7
|
+
Author-email: Ritchie Mwewa <rly0nheart@duck.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
12
|
+
Classifier: Programming Language :: Python :: 3
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
16
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
17
|
+
Classifier: Topic :: Software Development :: Bug Tracking
|
|
18
|
+
Classifier: Framework :: AsyncIO
|
|
19
|
+
Requires-Dist: httpx>=0.28.1
|
|
20
|
+
Requires-Dist: rich>=14.3.3
|
|
21
|
+
Requires-Python: >=3.13
|
|
22
|
+
Project-URL: Changelog, https://github.com/rly0nheart/bugpipe/blob/master/CHANGELOG.md
|
|
23
|
+
Project-URL: Documentation, https://bugpipe.readthedocs.io
|
|
24
|
+
Project-URL: Repository, https://github.com/rly0nheart/bugpipe
|
|
25
|
+
Description-Content-Type: text/markdown
|
|
26
|
+
|
|
27
|
+
<p align="center">
|
|
28
|
+
<img alt="logo" src="https://www.gstatic.com/buganizer/img/v0/logo.svg" width="150" height="150">
|
|
29
|
+
<br>
|
|
30
|
+
<br>
|
|
31
|
+
<strong><i>Unofficial Python client for Buganizer; the Google Issue Tracking system.</i></strong>
|
|
32
|
+
</p>
|
|
33
|
+
|
|
34
|
+
## Quick Start
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
bugpipe search "status:open"
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```python
|
|
41
|
+
from bugpipe import Bugpipe
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
with Bugpipe() as client:
|
|
45
|
+
result = client.search(query="status:open priority:p1", page_size=25)
|
|
46
|
+
for issue in result.issues:
|
|
47
|
+
print(f"#{issue.id} [{issue.status.name}] {issue.title}")
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
## Documentation
|
|
51
|
+
|
|
52
|
+
Refer to [the docs](https://bugpipe.readthedocs.io) for installation, usage and api reference.
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
bugpipe/__init__.py,sha256=QO0tHq-83svpy4MoCN6nPHNcwt3-jpz2gUDKYaOsbfI,732
|
|
2
|
+
bugpipe/api/AUDIT.md,sha256=9g2HYEOtges3nPY7xQBwTwge129KP6ho46fga58LOhM,10668
|
|
3
|
+
bugpipe/api/README.md,sha256=V41sjZ9upbNxZ8iNw4yFoOsqDr_wiwKBpSzMshN_XNM,26191
|
|
4
|
+
bugpipe/api/__init__.py,sha256=uty5L3YBFqcW79GG97ldKgWtHY5I-TaYh6Wje7VfNzc,79
|
|
5
|
+
bugpipe/api/client.py,sha256=RZ3fmQV9yXuz2lc1cpd-vtHJTM7kUHJ-dUCBTo03ryQ,10539
|
|
6
|
+
bugpipe/api/models.py,sha256=cPx30gx1yZmchb6XkN0wv2n8VdtC23raaE5sfqCLokU,23157
|
|
7
|
+
bugpipe/api/parser.py,sha256=xShNVE_ok7rWKzGRsA7Omct2AEOZLWe0zi6IllKDST0,24239
|
|
8
|
+
bugpipe/cli/__init__.py,sha256=-vcD4_pFoFmFfx-Z7Bz-W4RLn5c-XHVDiN-PEnCmBQ4,2167
|
|
9
|
+
bugpipe/cli/cmd.py,sha256=0BSoXTiF6N5vw9g-9NKDRtPvgJ054uxsOJwkzkeWs18,9533
|
|
10
|
+
bugpipe/cli/term.py,sha256=SqjUkuVQCngPE8sxhPZOsqCXhw2VojxCMjOEbH1UXdA,7095
|
|
11
|
+
bugpipe/cli/update_checker.py,sha256=HxOvPo-EgaLlseY-X1DfAP18hSmMBp8QxSsvRn8SAfE,7266
|
|
12
|
+
bugpipe-3.0.1.dist-info/WHEEL,sha256=eh7sammvW2TypMMMGKgsM83HyA_3qQ5Lgg3ynoecH3M,79
|
|
13
|
+
bugpipe-3.0.1.dist-info/entry_points.txt,sha256=wND_xpvRjJOmnH18Cp1s68Bb0CGmLgYVioPtnEnblGU,47
|
|
14
|
+
bugpipe-3.0.1.dist-info/METADATA,sha256=rHQdo9G5FI9x2_EbL-F8YktM23g-KK-E8FOHaNyvuV4,1846
|
|
15
|
+
bugpipe-3.0.1.dist-info/RECORD,,
|