oss-issue-scout 0.2.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.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Ruifeng Xue
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,116 @@
1
+ Metadata-Version: 2.4
2
+ Name: oss-issue-scout
3
+ Version: 0.2.0
4
+ Summary: Find worthwhile open-source issues.
5
+ Author: Ruifeng Xue
6
+ License: MIT License
7
+
8
+ Copyright (c) 2026 Ruifeng Xue
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
27
+
28
+ Requires-Python: >=3.10
29
+ Description-Content-Type: text/markdown
30
+ License-File: LICENSE
31
+ Dynamic: license-file
32
+
33
+ # oss-issue-scout
34
+
35
+ Find worthwhile open-source issues
36
+
37
+ The current version calls the GitHub API, searches open issues, and applies a simple score based on repository activity, issue activity, comments, labels, and related signals.
38
+ It is currently aimed at junior to intermediate developers who want a faster way to find approachable issues.
39
+
40
+ ## Features
41
+
42
+ - Search GitHub open issues
43
+ - Filter by language, label, stars, and update recency
44
+ - Skip issues that already have linked PRs
45
+ - Recommend only unassigned issues by default
46
+ - Skip repositories with fewer than 100 stars by default
47
+ - Render results as `table`, `markdown`, or `json`
48
+ - No third-party dependencies
49
+
50
+ ## Usage
51
+
52
+ ```powershell
53
+ python -m pip install -e .
54
+ oss-issue-scout search --language python --label "good first issue" --limit 5
55
+ ```
56
+
57
+ ## Options
58
+
59
+ ```text
60
+ --language Repository primary language, such as python
61
+ --stars-min Minimum repository stars; defaults to at least 100
62
+ --label Issue label, such as "good first issue"
63
+ --updated-days Issue updated within the last N days
64
+ --repo-updated-days Repository had issue activity within the last N days
65
+ --limit Number of results, default 10
66
+ --format Output format: table, markdown, json
67
+ ```
68
+
69
+ Examples:
70
+
71
+ ```powershell
72
+ oss-issue-scout search --language python
73
+ oss-issue-scout search --language python --label "help wanted" --stars-min 500 --limit 10
74
+ oss-issue-scout search --language python --format json
75
+ oss-issue-scout search --language "C++" --label "good first issue" --repo-updated-days 7
76
+ ```
77
+
78
+ ## Scoring
79
+
80
+ The current score is intentionally simple. It considers:
81
+
82
+ - Repository stars: moderately active repos get a boost; very large repos may be penalized
83
+ - Issue update recency: recently updated issues get a boost; stale issues are penalized
84
+ - Repository issue activity: recent issue activity gets a boost
85
+ - Beginner-friendly labels: `good first issue` / `help wanted` only add points when the repo has at least 3 open issues with those labels
86
+ - Comment count: low discussion volume gets a boost; long discussions are penalized
87
+
88
+ The search step filters out:
89
+
90
+ - Closed issues
91
+ - Archived repositories
92
+ - Issues with linked PRs
93
+ - Assigned issues
94
+ - Repositories with fewer than 100 stars
95
+
96
+ ## Tests
97
+
98
+ ```powershell
99
+ python -m unittest discover
100
+ ```
101
+
102
+ Tests use mocked GitHub responses and do not call the real GitHub API.
103
+
104
+ ## Next
105
+
106
+ This project is still small. If it helps you, please consider giving it a ⭐. Discussions will be opened after the project reaches 16+ ⭐.
107
+
108
+ If you have suggestions or run into problems, please open an issue.
109
+
110
+ Future versions will continue to improve recommendation quality and usability.
111
+
112
+ ## Contributors
113
+
114
+ <a href="https://github.com/Yong-yuan-X/oss-issue-scout/graphs/contributors">
115
+ <img src="https://contrib.rocks/image?repo=Yong-yuan-X/oss-issue-scout" alt="Contributors" />
116
+ </a>
@@ -0,0 +1,84 @@
1
+ # oss-issue-scout
2
+
3
+ Find worthwhile open-source issues
4
+
5
+ The current version calls the GitHub API, searches open issues, and applies a simple score based on repository activity, issue activity, comments, labels, and related signals.
6
+ It is currently aimed at junior to intermediate developers who want a faster way to find approachable issues.
7
+
8
+ ## Features
9
+
10
+ - Search GitHub open issues
11
+ - Filter by language, label, stars, and update recency
12
+ - Skip issues that already have linked PRs
13
+ - Recommend only unassigned issues by default
14
+ - Skip repositories with fewer than 100 stars by default
15
+ - Render results as `table`, `markdown`, or `json`
16
+ - No third-party dependencies
17
+
18
+ ## Usage
19
+
20
+ ```powershell
21
+ python -m pip install -e .
22
+ oss-issue-scout search --language python --label "good first issue" --limit 5
23
+ ```
24
+
25
+ ## Options
26
+
27
+ ```text
28
+ --language Repository primary language, such as python
29
+ --stars-min Minimum repository stars; defaults to at least 100
30
+ --label Issue label, such as "good first issue"
31
+ --updated-days Issue updated within the last N days
32
+ --repo-updated-days Repository had issue activity within the last N days
33
+ --limit Number of results, default 10
34
+ --format Output format: table, markdown, json
35
+ ```
36
+
37
+ Examples:
38
+
39
+ ```powershell
40
+ oss-issue-scout search --language python
41
+ oss-issue-scout search --language python --label "help wanted" --stars-min 500 --limit 10
42
+ oss-issue-scout search --language python --format json
43
+ oss-issue-scout search --language "C++" --label "good first issue" --repo-updated-days 7
44
+ ```
45
+
46
+ ## Scoring
47
+
48
+ The current score is intentionally simple. It considers:
49
+
50
+ - Repository stars: moderately active repos get a boost; very large repos may be penalized
51
+ - Issue update recency: recently updated issues get a boost; stale issues are penalized
52
+ - Repository issue activity: recent issue activity gets a boost
53
+ - Beginner-friendly labels: `good first issue` / `help wanted` only add points when the repo has at least 3 open issues with those labels
54
+ - Comment count: low discussion volume gets a boost; long discussions are penalized
55
+
56
+ The search step filters out:
57
+
58
+ - Closed issues
59
+ - Archived repositories
60
+ - Issues with linked PRs
61
+ - Assigned issues
62
+ - Repositories with fewer than 100 stars
63
+
64
+ ## Tests
65
+
66
+ ```powershell
67
+ python -m unittest discover
68
+ ```
69
+
70
+ Tests use mocked GitHub responses and do not call the real GitHub API.
71
+
72
+ ## Next
73
+
74
+ This project is still small. If it helps you, please consider giving it a ⭐. Discussions will be opened after the project reaches 16+ ⭐.
75
+
76
+ If you have suggestions or run into problems, please open an issue.
77
+
78
+ Future versions will continue to improve recommendation quality and usability.
79
+
80
+ ## Contributors
81
+
82
+ <a href="https://github.com/Yong-yuan-X/oss-issue-scout/graphs/contributors">
83
+ <img src="https://contrib.rocks/image?repo=Yong-yuan-X/oss-issue-scout" alt="Contributors" />
84
+ </a>
@@ -0,0 +1,86 @@
1
+ # oss-issue-scout
2
+
3
+ 发现值得贡献的开源 issues
4
+
5
+ [English README](README.en.md)
6
+
7
+ 当前版本会调用 GitHub API 搜索 open issues,并根据项目活跃度、issue 活跃度、评论数量、标签等信号做一个简单评分
8
+ 它主要面向初中级开发者,用来快速筛出更可能适合贡献的 issue
9
+
10
+ ## 功能
11
+
12
+ - 搜索 GitHub open issues
13
+ - 支持按语言、标签、stars、更新时间过滤
14
+ - 默认跳过已有关联 PR 的 issue
15
+ - 默认只推荐未指派的 issue
16
+ - 默认过滤 stars 少于 100 的 repo
17
+ - 支持 `table`、`markdown`、`json` 输出
18
+ - 不依赖第三方包
19
+
20
+ ## 使用
21
+
22
+ ```powershell
23
+ python -m pip install -e .
24
+ oss-issue-scout search --language python --label "good first issue" --limit 5
25
+ ```
26
+
27
+ ## 参数
28
+
29
+ ```text
30
+ --language 仓库主要语言,例如 python
31
+ --stars-min 仓库最低 stars,默认至少 100
32
+ --label issue 标签,例如 "good first issue"
33
+ --updated-days 当前 issue 最近多少天内更新过
34
+ --repo-updated-days issue 所在 repo 最近多少天内有 issue 活动
35
+ --limit 返回数量,默认 10
36
+ --format 输出格式:table、markdown、json
37
+ ```
38
+
39
+ 示例:
40
+
41
+ ```powershell
42
+ oss-issue-scout search --language python
43
+ oss-issue-scout search --language python --label "help wanted" --stars-min 500 --limit 10
44
+ oss-issue-scout search --language python --format json
45
+ oss-issue-scout search --language "C++" --label "good first issue" --repo-updated-days 7
46
+ ```
47
+
48
+ ## 推荐规则
49
+
50
+ 当前评分比较简单,主要参考:
51
+
52
+ - repo stars:中等活跃项目加分,超大型项目可能扣分
53
+ - issue 更新时间:近期更新加分,长期未更新扣分
54
+ - repo issue 活动:近期有 issue 活动加分
55
+ - beginner 标签:当前 issue 有 `good first issue` / `help wanted`,且 repo 中至少有 3 个同类 open issues 时加分
56
+ - 评论数量:评论少加分,讨论过长扣分
57
+
58
+ 搜索阶段会直接排除:
59
+
60
+ - closed issues
61
+ - archived repos
62
+ - 已 linked PR 的 issues
63
+ - 已指派 assignee 的 issues
64
+ - stars 少于 100 的 repos
65
+
66
+ ## 测试
67
+
68
+ ```powershell
69
+ python -m unittest discover
70
+ ```
71
+
72
+ 测试使用 mock 数据,不会请求真实 GitHub API
73
+
74
+ ## 后续
75
+
76
+ 现在项目使用的人还比较少,如果你觉得它对你有帮助,欢迎点一个 ⭐。达到 16+ ⭐ 后会开启 Discussions
77
+
78
+ 如果有改进建议或使用问题,可以在 issues 中提出
79
+
80
+ 后续会逐步进行版本迭代,继续优化推荐质量和使用体验
81
+
82
+ ## 贡献者
83
+
84
+ <a href="https://github.com/Yong-yuan-X/oss-issue-scout/graphs/contributors">
85
+ <img src="https://contrib.rocks/image?repo=Yong-yuan-X/oss-issue-scout" alt="Contributors" />
86
+ </a>
@@ -0,0 +1,3 @@
1
+ """Find worthwhile open-source issues."""
2
+
3
+ __version__ = "0.2.0"
@@ -0,0 +1,63 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import sys
5
+
6
+ from .github_api import GitHubAPIError, search_issues
7
+ from .output import render_results
8
+ from .scoring import score_issues
9
+
10
+
11
+ def build_parser() -> argparse.ArgumentParser:
12
+ parser = argparse.ArgumentParser(prog="oss-issue-scout")
13
+ subparsers = parser.add_subparsers(dest="command", required=True)
14
+
15
+ search_parser = subparsers.add_parser("search", help="search GitHub issues")
16
+ search_parser.add_argument("--language")
17
+ search_parser.add_argument("--stars-min", type=int)
18
+ search_parser.add_argument("--label")
19
+ search_parser.add_argument("--updated-days", type=int)
20
+ search_parser.add_argument("--repo-updated-days", type=int)
21
+ search_parser.add_argument("--limit", type=_positive_int, default=10)
22
+ search_parser.add_argument(
23
+ "--format",
24
+ choices=("table", "markdown", "json"),
25
+ default="table",
26
+ )
27
+ search_parser.set_defaults(func=_run_search)
28
+ return parser
29
+
30
+
31
+ def _positive_int(value: str) -> int:
32
+ parsed = int(value)
33
+ if parsed <= 0:
34
+ raise argparse.ArgumentTypeError("must be greater than 0")
35
+ return parsed
36
+
37
+
38
+ def _run_search(args: argparse.Namespace) -> int:
39
+ try:
40
+ issues = search_issues(
41
+ language=args.language,
42
+ stars_min=args.stars_min,
43
+ label=args.label,
44
+ updated_days=args.updated_days,
45
+ repo_updated_days=args.repo_updated_days,
46
+ limit=args.limit,
47
+ )
48
+ except GitHubAPIError as error:
49
+ print(f"error: {error}", file=sys.stderr)
50
+ return 1
51
+ results = score_issues(issues)[: args.limit]
52
+ print(render_results(results, args.format))
53
+ return 0
54
+
55
+
56
+ def main(argv: list[str] | None = None) -> int:
57
+ parser = build_parser()
58
+ args = parser.parse_args(argv)
59
+ return args.func(args)
60
+
61
+
62
+ if __name__ == "__main__":
63
+ raise SystemExit(main())
@@ -0,0 +1,255 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+ import os
5
+ from dataclasses import dataclass
6
+ from datetime import datetime, timedelta, timezone
7
+ from typing import Any
8
+ from urllib.error import HTTPError, URLError
9
+ from urllib.parse import quote, urlencode
10
+ from urllib.request import Request, urlopen
11
+
12
+
13
+ GITHUB_API_BASE = "https://api.github.com"
14
+ GITHUB_API_VERSION = "2026-03-10"
15
+ DEFAULT_STARS_MIN = 100
16
+ MAX_SEARCH_PAGES = 5
17
+
18
+
19
+ class GitHubAPIError(RuntimeError):
20
+ pass
21
+
22
+
23
+ @dataclass(frozen=True)
24
+ class Issue:
25
+ repo: str
26
+ title: str
27
+ url: str
28
+ language: str
29
+ stars: int
30
+ labels: tuple[str, ...]
31
+ updated_days: int
32
+ repo_last_issue_updated_days: int
33
+ repo_beginner_issue_count: int
34
+ comments: int
35
+ has_open_pr: bool
36
+
37
+
38
+ def search_issues(
39
+ *,
40
+ language: str | None = None,
41
+ stars_min: int | None = None,
42
+ label: str | None = None,
43
+ updated_days: int | None = None,
44
+ repo_updated_days: int | None = None,
45
+ limit: int = 10,
46
+ ) -> list[Issue]:
47
+ if limit <= 0:
48
+ return []
49
+
50
+ effective_stars_min = max(stars_min or DEFAULT_STARS_MIN, DEFAULT_STARS_MIN)
51
+
52
+ query = _build_issue_query(
53
+ language=language,
54
+ stars_min=effective_stars_min,
55
+ label=label,
56
+ updated_days=updated_days,
57
+ )
58
+ repo_cache: dict[str, dict[str, Any]] = {}
59
+ repo_activity_cache: dict[str, int] = {}
60
+ repo_beginner_issue_count_cache: dict[str, int] = {}
61
+ issues: list[Issue] = []
62
+ # Search candidates match the GitHub query first, then local filters below
63
+ # enforce repo metadata and repo activity rules.
64
+ per_page = min(max(limit * 10, 30), 100)
65
+
66
+ for page in range(1, MAX_SEARCH_PAGES + 1):
67
+ data = _request_json(
68
+ "/search/issues",
69
+ {
70
+ "q": query,
71
+ "sort": "updated",
72
+ "order": "desc",
73
+ "per_page": str(per_page),
74
+ "page": str(page),
75
+ },
76
+ )
77
+ items = data.get("items", [])
78
+ if not items:
79
+ break
80
+
81
+ for item in items:
82
+ if "pull_request" in item:
83
+ continue
84
+
85
+ repo = _repo_full_name(item)
86
+ if repo is None:
87
+ continue
88
+
89
+ if repo not in repo_cache:
90
+ repo_cache[repo] = _repo_info_from_search_result(item) or _get_repo(repo)
91
+ repo_info = repo_cache[repo]
92
+ stars = int(repo_info.get("stargazers_count") or 0)
93
+ if stars < effective_stars_min:
94
+ continue
95
+
96
+ repo_language = str(repo_info.get("language") or "")
97
+ if language and repo_language.casefold() != language.casefold():
98
+ continue
99
+
100
+ if repo not in repo_activity_cache:
101
+ repo_activity_cache[repo] = _get_repo_last_issue_updated_days(repo)
102
+ if repo not in repo_beginner_issue_count_cache:
103
+ repo_beginner_issue_count_cache[repo] = _get_repo_beginner_issue_count(repo)
104
+ if (
105
+ repo_updated_days is not None
106
+ and repo_activity_cache[repo] > repo_updated_days
107
+ ):
108
+ continue
109
+
110
+ issue = Issue(
111
+ repo=repo,
112
+ title=str(item.get("title") or ""),
113
+ url=str(item.get("html_url") or ""),
114
+ language=repo_language,
115
+ stars=stars,
116
+ labels=_labels(item),
117
+ updated_days=_days_since(str(item.get("updated_at") or "")),
118
+ repo_last_issue_updated_days=repo_activity_cache[repo],
119
+ repo_beginner_issue_count=repo_beginner_issue_count_cache[repo],
120
+ comments=int(item.get("comments") or 0),
121
+ has_open_pr=False,
122
+ )
123
+ issues.append(issue)
124
+ if len(issues) >= limit:
125
+ break
126
+
127
+ if len(issues) >= limit or len(items) < per_page:
128
+ break
129
+
130
+ return issues
131
+
132
+
133
+ def _build_issue_query(
134
+ *,
135
+ language: str | None,
136
+ stars_min: int | None,
137
+ label: str | None,
138
+ updated_days: int | None,
139
+ ) -> str:
140
+ parts = ["is:issue", "is:open", "archived:false", "-linked:pr", "no:assignee"]
141
+ if language:
142
+ parts.append(f"language:{_quote_query_value(language)}")
143
+ if label:
144
+ parts.append(f"label:{_quote_query_value(label)}")
145
+ if updated_days is not None:
146
+ cutoff = datetime.now(timezone.utc).date() - timedelta(days=updated_days)
147
+ parts.append(f"updated:>={cutoff.isoformat()}")
148
+ return " ".join(parts)
149
+
150
+
151
+ def _request_json(path: str, params: dict[str, str] | None = None) -> dict[str, Any]:
152
+ url = f"{GITHUB_API_BASE}{path}"
153
+ if params:
154
+ url = f"{url}?{urlencode(params)}"
155
+
156
+ headers = {
157
+ "Accept": "application/vnd.github+json",
158
+ "X-GitHub-Api-Version": GITHUB_API_VERSION,
159
+ "User-Agent": "oss-issue-scout",
160
+ }
161
+ token = os.environ.get("GITHUB_TOKEN")
162
+ if token:
163
+ headers["Authorization"] = f"Bearer {token}"
164
+
165
+ request = Request(url, headers=headers)
166
+ try:
167
+ with urlopen(request, timeout=20) as response:
168
+ return json.loads(response.read().decode("utf-8"))
169
+ except HTTPError as error:
170
+ message = error.read().decode("utf-8", errors="replace")
171
+ if error.code == 403 and "rate limit" in message.casefold():
172
+ raise GitHubAPIError(
173
+ "GitHub API rate limit exceeded. Set a GITHUB_TOKEN environment "
174
+ "variable to get a higher rate limit, then try again."
175
+ ) from error
176
+ raise GitHubAPIError(f"GitHub API request failed: HTTP {error.code} {message}") from error
177
+ except URLError as error:
178
+ raise GitHubAPIError(f"GitHub API request failed: {error.reason}") from error
179
+
180
+
181
+ def _get_repo(repo: str) -> dict[str, Any]:
182
+ owner, name = repo.split("/", 1)
183
+ return _request_json(f"/repos/{quote(owner)}/{quote(name)}")
184
+
185
+
186
+ def _get_repo_last_issue_updated_days(repo: str) -> int:
187
+ data = _request_json(
188
+ "/search/issues",
189
+ {
190
+ "q": f"repo:{repo} is:issue",
191
+ "sort": "updated",
192
+ "order": "desc",
193
+ "per_page": "1",
194
+ },
195
+ )
196
+ items = data.get("items", [])
197
+ if not items:
198
+ return 9999
199
+ return _days_since(str(items[0].get("updated_at") or ""))
200
+
201
+
202
+ def _get_repo_beginner_issue_count(repo: str) -> int:
203
+ data = _request_json(
204
+ "/search/issues",
205
+ {
206
+ "q": f'repo:{repo} is:issue is:open label:"good first issue","help wanted"',
207
+ "per_page": "1",
208
+ },
209
+ )
210
+ return int(data.get("total_count") or 0)
211
+
212
+
213
+ def _repo_full_name(item: dict[str, Any]) -> str | None:
214
+ repository_url = str(item.get("repository_url") or "")
215
+ marker = f"{GITHUB_API_BASE}/repos/"
216
+ if repository_url.startswith(marker):
217
+ return repository_url.removeprefix(marker)
218
+ repository = item.get("repository")
219
+ if isinstance(repository, dict):
220
+ full_name = repository.get("full_name")
221
+ if isinstance(full_name, str):
222
+ return full_name
223
+ return None
224
+
225
+
226
+ def _repo_info_from_search_result(item: dict[str, Any]) -> dict[str, Any] | None:
227
+ repository = item.get("repository")
228
+ if not isinstance(repository, dict):
229
+ return None
230
+ if "stargazers_count" not in repository or "language" not in repository:
231
+ return None
232
+ return repository
233
+
234
+
235
+ def _labels(item: dict[str, Any]) -> tuple[str, ...]:
236
+ labels = item.get("labels") or []
237
+ names: list[str] = []
238
+ for label in labels:
239
+ if isinstance(label, dict) and isinstance(label.get("name"), str):
240
+ names.append(label["name"])
241
+ return tuple(names)
242
+
243
+
244
+ def _days_since(value: str) -> int:
245
+ if not value:
246
+ return 9999
247
+ timestamp = datetime.fromisoformat(value.replace("Z", "+00:00"))
248
+ delta = datetime.now(timezone.utc) - timestamp
249
+ return max(delta.days, 0)
250
+
251
+
252
+ def _quote_query_value(value: str) -> str:
253
+ if " " in value:
254
+ return f'"{value}"'
255
+ return value