github-dependents-info 3.2.1__tar.gz → 4.0.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.
- {github_dependents_info-3.2.1 → github_dependents_info-4.0.0}/PKG-INFO +25 -23
- {github_dependents_info-3.2.1 → github_dependents_info-4.0.0}/README.md +19 -18
- {github_dependents_info-3.2.1 → github_dependents_info-4.0.0}/github_dependents_info/__main__.py +4 -2
- {github_dependents_info-3.2.1 → github_dependents_info-4.0.0}/github_dependents_info/gh_dependents_info.py +145 -56
- {github_dependents_info-3.2.1 → github_dependents_info-4.0.0}/pyproject.toml +14 -13
- {github_dependents_info-3.2.1 → github_dependents_info-4.0.0}/LICENSE +0 -0
- {github_dependents_info-3.2.1 → github_dependents_info-4.0.0}/github_dependents_info/__init__.py +0 -0
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github-dependents-info
|
|
3
|
-
Version:
|
|
3
|
+
Version: 4.0.0
|
|
4
4
|
Summary: Collect information about dependencies between a github repo and other repositories. Results available in JSON, markdown and badges.
|
|
5
5
|
License-Expression: MIT
|
|
6
6
|
License-File: LICENSE
|
|
7
7
|
Keywords: github,dependent,used-by,repository,stats,dependencies,using
|
|
8
8
|
Author: nvuillam
|
|
9
9
|
Author-email: nicolas.vuillamy@gmail.com
|
|
10
|
-
Requires-Python: >=3.
|
|
10
|
+
Requires-Python: >=3.11,<4.0
|
|
11
11
|
Classifier: Development Status :: 3 - Alpha
|
|
12
12
|
Classifier: Intended Audience :: Developers
|
|
13
13
|
Classifier: Operating System :: OS Independent
|
|
@@ -15,12 +15,13 @@ Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
|
15
15
|
Classifier: Programming Language :: Python :: 3
|
|
16
16
|
Classifier: Programming Language :: Python :: 3.9
|
|
17
17
|
Requires-Dist: beautifulsoup4 (==4.15.0)
|
|
18
|
-
Requires-Dist: click (>=8.
|
|
18
|
+
Requires-Dist: click (>=8.5,<8.6)
|
|
19
19
|
Requires-Dist: httpx (>=0.28.1,<0.29.0)
|
|
20
20
|
Requires-Dist: idna (>=3.14)
|
|
21
|
-
Requires-Dist: pandas (>=
|
|
21
|
+
Requires-Dist: pandas (>=3.0.5,<3.1)
|
|
22
22
|
Requires-Dist: rich (>=15,<15.1)
|
|
23
|
-
Requires-Dist:
|
|
23
|
+
Requires-Dist: soupsieve (>=2.8.4)
|
|
24
|
+
Requires-Dist: typer (>=0.27.1,<0.28)
|
|
24
25
|
Project-URL: Homepage, https://github.com/nvuillam/github-dependents-info
|
|
25
26
|
Project-URL: Repository, https://github.com/nvuillam/github-dependents-info
|
|
26
27
|
Description-Content-Type: text/markdown
|
|
@@ -268,25 +269,25 @@ _________________
|
|
|
268
269
|
| Parameter | Type | Description |
|
|
269
270
|
|-----------|--------|----------------------------------------------|
|
|
270
271
|
| --repo | String | Repository. Example: `oxsecurity/megalinter` |
|
|
271
|
-
| -b<br/> --badgemarkdownfile
|
|
272
|
+
| -b<br/> --badgemarkdownfile | String | _(optional)_ Path to markdown file where to insert/update **Used by** badge <br/> (must contain tags `<!-- gh-dependents-info-used-by-start -->
|
|
272
273
|
[](https://github.com/nvuillam/github-dependents-info/blob/main/docs/github-dependents-info.md)<!-- gh-dependents-info-used-by-end -->`) |
|
|
273
|
-
| -s<br/> --sort
|
|
274
|
-
| -x<br/> --minstars
|
|
275
|
-
| -m<br/> --markdownfile
|
|
276
|
-
| -d<br/> --docurl
|
|
277
|
-
| -p<br/> --mergepackages
|
|
278
|
-
| -j<br/> --json
|
|
279
|
-
| -u<br/> --owner
|
|
280
|
-
| -n<br/> --max-scraped-pages
|
|
281
|
-
| --pagination<br/> --no-pagination
|
|
282
|
-
| --page-size
|
|
283
|
-
| --llm-summary<br/> --no-llm-summary | Boolean | _(optional)_ Generate an AI usage summary in markdown output when an LLM API key is present (default: enabled)
|
|
284
|
-
| --llm-model
|
|
285
|
-
| --llm-max-repos
|
|
286
|
-
| --llm-max-words
|
|
287
|
-
| --llm-timeout
|
|
288
|
-
| -v<br/> --version
|
|
289
|
-
| --verbose
|
|
274
|
+
| -s<br/> --sort | String | _(optional)_ Sort order: name (default) or stars |
|
|
275
|
+
| -x<br/> --minstars | String | _(optional)_ If set, filters repositories to keep only those with more than X stars |
|
|
276
|
+
| -m<br/> --markdownfile | String | _(optional)_ Output markdown file file |
|
|
277
|
+
| -d<br/> --docurl | String | _(optional)_ Hyperlink to use when clicking on badge markdown file badge. (Default: link to markdown file) |
|
|
278
|
+
| -p<br/> --mergepackages | String | _(optional)_ In case of multiple packages, merge their stats in a single one in markdown and json output |
|
|
279
|
+
| -j<br/> --json | String | _(optional)_ Output in json format |
|
|
280
|
+
| -u<br/> --owner | String | _(optional)_ If set, filters repositories to keep only those owned by the specified user/organization |
|
|
281
|
+
| -n<br/> --max-scraped-pages | Integer | _(optional)_ Maximum number of GitHub pages to scrape per package (0 uses all available pages) |
|
|
282
|
+
| --pagination<br/> --no-pagination | Boolean | _(optional)_ Enable (default) or disable pagination when writing markdown output |
|
|
283
|
+
| --page-size | Integer | _(optional)_ Number of repositories per markdown page when pagination is enabled (default: 500) |
|
|
284
|
+
| --llm-summary<br/> --no-llm-summary | Boolean | _(optional)_ Generate an AI usage summary in markdown output when an LLM API key is present (default: enabled) |
|
|
285
|
+
| --llm-model | String | _(optional)_ LiteLLM model to use for the summary. If not set, a lightweight model is selected based on the detected API key provider |
|
|
286
|
+
| --llm-max-repos | Integer | _(optional)_ Max dependent repos included in the summary prompt payload (default: 500) |
|
|
287
|
+
| --llm-max-words | Integer | _(optional)_ Max words for the generated summary (default: 300) |
|
|
288
|
+
| --llm-timeout | Float | _(optional)_ Timeout (seconds) for the summary LLM call (default: 120) |
|
|
289
|
+
| -v<br/> --version | Boolean | _(optional)_ Displays version of github-dependents-info |
|
|
290
|
+
| --verbose | Boolean | _(optional)_ Verbose output |
|
|
290
291
|
|
|
291
292
|
Badge tags example (the tool replaces everything between the markers):
|
|
292
293
|
|
|
@@ -367,6 +368,7 @@ If an LLM API key is detected in the environment (for example `OPENAI_API_KEY`),
|
|
|
367
368
|
- Override model with `--llm-model` (or env var `GITHUB_DEPENDENTS_INFO_LLM_MODEL` / `LITELLM_MODEL`)
|
|
368
369
|
- Adjust max summary length with `--llm-max-words` (or env var `GITHUB_DEPENDENTS_INFO_LLM_MAX_WORDS`)
|
|
369
370
|
- The summary is cached in `--csvdirectory` (file `llm_summary_<repo>.json`) and reused on subsequent runs
|
|
371
|
+
- A previously generated summary is never lost: if a new summary can not be generated (no API key anymore, no credit left, rate limit, timeout...), the summary found in the cache or in the existing `--markdownfile` is reused
|
|
370
372
|
|
|
371
373
|
|
|
372
374
|
## Use as GitHub Action
|
|
@@ -241,25 +241,25 @@ _________________
|
|
|
241
241
|
| Parameter | Type | Description |
|
|
242
242
|
|-----------|--------|----------------------------------------------|
|
|
243
243
|
| --repo | String | Repository. Example: `oxsecurity/megalinter` |
|
|
244
|
-
| -b<br/> --badgemarkdownfile
|
|
244
|
+
| -b<br/> --badgemarkdownfile | String | _(optional)_ Path to markdown file where to insert/update **Used by** badge <br/> (must contain tags `<!-- gh-dependents-info-used-by-start -->
|
|
245
245
|
[](https://github.com/nvuillam/github-dependents-info/blob/main/docs/github-dependents-info.md)<!-- gh-dependents-info-used-by-end -->`) |
|
|
246
|
-
| -s<br/> --sort
|
|
247
|
-
| -x<br/> --minstars
|
|
248
|
-
| -m<br/> --markdownfile
|
|
249
|
-
| -d<br/> --docurl
|
|
250
|
-
| -p<br/> --mergepackages
|
|
251
|
-
| -j<br/> --json
|
|
252
|
-
| -u<br/> --owner
|
|
253
|
-
| -n<br/> --max-scraped-pages
|
|
254
|
-
| --pagination<br/> --no-pagination
|
|
255
|
-
| --page-size
|
|
256
|
-
| --llm-summary<br/> --no-llm-summary | Boolean | _(optional)_ Generate an AI usage summary in markdown output when an LLM API key is present (default: enabled)
|
|
257
|
-
| --llm-model
|
|
258
|
-
| --llm-max-repos
|
|
259
|
-
| --llm-max-words
|
|
260
|
-
| --llm-timeout
|
|
261
|
-
| -v<br/> --version
|
|
262
|
-
| --verbose
|
|
246
|
+
| -s<br/> --sort | String | _(optional)_ Sort order: name (default) or stars |
|
|
247
|
+
| -x<br/> --minstars | String | _(optional)_ If set, filters repositories to keep only those with more than X stars |
|
|
248
|
+
| -m<br/> --markdownfile | String | _(optional)_ Output markdown file file |
|
|
249
|
+
| -d<br/> --docurl | String | _(optional)_ Hyperlink to use when clicking on badge markdown file badge. (Default: link to markdown file) |
|
|
250
|
+
| -p<br/> --mergepackages | String | _(optional)_ In case of multiple packages, merge their stats in a single one in markdown and json output |
|
|
251
|
+
| -j<br/> --json | String | _(optional)_ Output in json format |
|
|
252
|
+
| -u<br/> --owner | String | _(optional)_ If set, filters repositories to keep only those owned by the specified user/organization |
|
|
253
|
+
| -n<br/> --max-scraped-pages | Integer | _(optional)_ Maximum number of GitHub pages to scrape per package (0 uses all available pages) |
|
|
254
|
+
| --pagination<br/> --no-pagination | Boolean | _(optional)_ Enable (default) or disable pagination when writing markdown output |
|
|
255
|
+
| --page-size | Integer | _(optional)_ Number of repositories per markdown page when pagination is enabled (default: 500) |
|
|
256
|
+
| --llm-summary<br/> --no-llm-summary | Boolean | _(optional)_ Generate an AI usage summary in markdown output when an LLM API key is present (default: enabled) |
|
|
257
|
+
| --llm-model | String | _(optional)_ LiteLLM model to use for the summary. If not set, a lightweight model is selected based on the detected API key provider |
|
|
258
|
+
| --llm-max-repos | Integer | _(optional)_ Max dependent repos included in the summary prompt payload (default: 500) |
|
|
259
|
+
| --llm-max-words | Integer | _(optional)_ Max words for the generated summary (default: 300) |
|
|
260
|
+
| --llm-timeout | Float | _(optional)_ Timeout (seconds) for the summary LLM call (default: 120) |
|
|
261
|
+
| -v<br/> --version | Boolean | _(optional)_ Displays version of github-dependents-info |
|
|
262
|
+
| --verbose | Boolean | _(optional)_ Verbose output |
|
|
263
263
|
|
|
264
264
|
Badge tags example (the tool replaces everything between the markers):
|
|
265
265
|
|
|
@@ -340,6 +340,7 @@ If an LLM API key is detected in the environment (for example `OPENAI_API_KEY`),
|
|
|
340
340
|
- Override model with `--llm-model` (or env var `GITHUB_DEPENDENTS_INFO_LLM_MODEL` / `LITELLM_MODEL`)
|
|
341
341
|
- Adjust max summary length with `--llm-max-words` (or env var `GITHUB_DEPENDENTS_INFO_LLM_MAX_WORDS`)
|
|
342
342
|
- The summary is cached in `--csvdirectory` (file `llm_summary_<repo>.json`) and reused on subsequent runs
|
|
343
|
+
- A previously generated summary is never lost: if a new summary can not be generated (no API key anymore, no credit left, rate limit, timeout...), the summary found in the cache or in the existing `--markdownfile` is reused
|
|
343
344
|
|
|
344
345
|
|
|
345
346
|
## Use as GitHub Action
|
{github_dependents_info-3.2.1 → github_dependents_info-4.0.0}/github_dependents_info/__main__.py
RENAMED
|
@@ -1,10 +1,12 @@
|
|
|
1
|
-
import logging
|
|
2
1
|
from typing import Annotated
|
|
3
2
|
|
|
3
|
+
import logging
|
|
4
|
+
|
|
4
5
|
import typer
|
|
6
|
+
from rich.console import Console
|
|
7
|
+
|
|
5
8
|
from github_dependents_info import version
|
|
6
9
|
from github_dependents_info.gh_dependents_info import GithubDependentsInfo
|
|
7
|
-
from rich.console import Console
|
|
8
10
|
|
|
9
11
|
app = typer.Typer(
|
|
10
12
|
name="github-dependents-info",
|
|
@@ -13,35 +13,40 @@ import numpy as np
|
|
|
13
13
|
import pandas as pd
|
|
14
14
|
from bs4 import BeautifulSoup
|
|
15
15
|
|
|
16
|
+
logger = logging.getLogger(__name__)
|
|
17
|
+
|
|
18
|
+
# Markers surrounding the AI summary in generated markdown, so a previous summary
|
|
19
|
+
# can be found again and reused when a new one can not be generated.
|
|
20
|
+
LLM_SUMMARY_START_MARKER = "<!-- gh-dependents-info-summary-start -->"
|
|
21
|
+
LLM_SUMMARY_END_MARKER = "<!-- gh-dependents-info-summary-end -->"
|
|
22
|
+
|
|
16
23
|
|
|
17
24
|
class GithubDependentsInfo:
|
|
18
25
|
def __init__(self, repo, **options) -> None:
|
|
19
26
|
self.repo = repo
|
|
20
|
-
self.outputrepo =
|
|
27
|
+
self.outputrepo = options.get("outputrepo", self.repo)
|
|
21
28
|
if self.outputrepo is None or self.outputrepo == "" or len(self.outputrepo) < 4:
|
|
22
29
|
self.outputrepo = self.repo
|
|
23
30
|
self.url_init = f"https://github.com/{self.repo}/network/dependents"
|
|
24
31
|
self.url_starts_with = f"/{self.repo}/network/dependents" + "?package_id="
|
|
25
|
-
self.sort_key = "
|
|
26
|
-
self.min_stars =
|
|
27
|
-
self.json_output =
|
|
28
|
-
self.merge_packages =
|
|
29
|
-
self.doc_url = options
|
|
30
|
-
self.markdown_file = options
|
|
31
|
-
self.badge_color = options
|
|
32
|
-
self.debug =
|
|
33
|
-
self.overwrite_progress = (
|
|
34
|
-
True if "overwrite_progress" in options and options["overwrite_progress"] is True else False
|
|
35
|
-
)
|
|
32
|
+
self.sort_key = options.get("sort_key", "name")
|
|
33
|
+
self.min_stars = options.get("min_stars", None)
|
|
34
|
+
self.json_output = options.get("json_output") is True
|
|
35
|
+
self.merge_packages = options.get("merge_packages") is True
|
|
36
|
+
self.doc_url = options.get("doc_url", None)
|
|
37
|
+
self.markdown_file = options.get("markdown_file", None)
|
|
38
|
+
self.badge_color = options.get("badge_color", "informational")
|
|
39
|
+
self.debug = options.get("debug") is True
|
|
40
|
+
self.overwrite_progress = options.get("overwrite_progress") is True
|
|
36
41
|
self.csv_directory = (
|
|
37
42
|
Path(options["csv_directory"])
|
|
38
43
|
if ("csv_directory" in options and options["csv_directory"] is not None)
|
|
39
44
|
else None
|
|
40
45
|
)
|
|
41
|
-
self.owner = options
|
|
42
|
-
self.max_scraped_pages = options
|
|
46
|
+
self.owner = options.get("owner", None)
|
|
47
|
+
self.max_scraped_pages = options.get("max_scraped_pages", 0)
|
|
43
48
|
self.max_concurrent_requests = options.get("max_concurrent_requests", 10)
|
|
44
|
-
self.pagination =
|
|
49
|
+
self.pagination = options.get("pagination", True)
|
|
45
50
|
self.page_size = options.get("page_size", 500)
|
|
46
51
|
self.total_sum = 0
|
|
47
52
|
self.total_public_sum = 0
|
|
@@ -51,7 +56,7 @@ class GithubDependentsInfo:
|
|
|
51
56
|
self.all_public_dependent_repos = []
|
|
52
57
|
self.badges = {}
|
|
53
58
|
self.result = {}
|
|
54
|
-
self.time_delay = options
|
|
59
|
+
self.time_delay = options.get("time_delay", 0.1)
|
|
55
60
|
self.http_retry_attempts = options.get("http_retry_attempts", 5)
|
|
56
61
|
self.http_retry_initial_delay = options.get("http_retry_initial_delay", max(self.time_delay, 1.0))
|
|
57
62
|
self.http_retry_backoff = options.get("http_retry_backoff", 2.0)
|
|
@@ -66,12 +71,13 @@ class GithubDependentsInfo:
|
|
|
66
71
|
self.llm_model = (
|
|
67
72
|
options.get("llm_model") or os.getenv("GITHUB_DEPENDENTS_INFO_LLM_MODEL") or os.getenv("LITELLM_MODEL")
|
|
68
73
|
)
|
|
69
|
-
self.llm_max_repos = int(options.get("llm_max_repos", os.getenv("GITHUB_DEPENDENTS_INFO_LLM_MAX_REPOS", 500)))
|
|
70
|
-
self.llm_max_words = int(options.get("llm_max_words", os.getenv("GITHUB_DEPENDENTS_INFO_LLM_MAX_WORDS", 300)))
|
|
71
|
-
self.llm_timeout = float(options.get("llm_timeout", os.getenv("GITHUB_DEPENDENTS_INFO_LLM_TIMEOUT", 120)))
|
|
74
|
+
self.llm_max_repos = int(options.get("llm_max_repos", os.getenv("GITHUB_DEPENDENTS_INFO_LLM_MAX_REPOS", "500")))
|
|
75
|
+
self.llm_max_words = int(options.get("llm_max_words", os.getenv("GITHUB_DEPENDENTS_INFO_LLM_MAX_WORDS", "300")))
|
|
76
|
+
self.llm_timeout = float(options.get("llm_timeout", os.getenv("GITHUB_DEPENDENTS_INFO_LLM_TIMEOUT", "120")))
|
|
72
77
|
self.llm_model_used: str | None = None
|
|
73
78
|
self.llm_summary: str | None = None
|
|
74
79
|
self.llm_summary_error: str | None = None
|
|
80
|
+
self.llm_summary_reused: bool = False
|
|
75
81
|
|
|
76
82
|
def collect(self):
|
|
77
83
|
"""Main entry point - synchronous wrapper for async collection."""
|
|
@@ -95,13 +101,13 @@ class GithubDependentsInfo:
|
|
|
95
101
|
if self.owner:
|
|
96
102
|
url += "&owner=" + self.owner
|
|
97
103
|
if self.debug is True:
|
|
98
|
-
|
|
104
|
+
logger.info("Package " + package["name"] + ": browsing " + url + " ...")
|
|
99
105
|
else:
|
|
100
106
|
url = self.url_init
|
|
101
107
|
if self.owner:
|
|
102
108
|
url += "?owner=" + self.owner
|
|
103
109
|
if self.debug is True:
|
|
104
|
-
|
|
110
|
+
logger.info("Package " + self.repo + ": browsing " + url + " ...")
|
|
105
111
|
package["url"] = url
|
|
106
112
|
package["public_dependent_stars"] = 0
|
|
107
113
|
|
|
@@ -115,7 +121,7 @@ class GithubDependentsInfo:
|
|
|
115
121
|
result = sorted(result, key=lambda d: d[self.sort_key])
|
|
116
122
|
if self.debug is True:
|
|
117
123
|
for r in result:
|
|
118
|
-
|
|
124
|
+
logger.info(r)
|
|
119
125
|
|
|
120
126
|
# Build package stats
|
|
121
127
|
total_public_dependents = len(result)
|
|
@@ -151,8 +157,8 @@ class GithubDependentsInfo:
|
|
|
151
157
|
|
|
152
158
|
# Output
|
|
153
159
|
if self.debug is True:
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
logger.info("Total for package: " + str(total_public_dependents))
|
|
161
|
+
logger.info("")
|
|
156
162
|
# Save crawl progress
|
|
157
163
|
self.save_progress(package) # only saves if csv_directory is provided
|
|
158
164
|
|
|
@@ -225,10 +231,73 @@ class GithubDependentsInfo:
|
|
|
225
231
|
if summary:
|
|
226
232
|
self.llm_summary = summary
|
|
227
233
|
return True
|
|
228
|
-
except Exception as exc:
|
|
229
|
-
|
|
234
|
+
except Exception as exc: # noqa: BLE001 - a corrupted cache must never break the run
|
|
235
|
+
logger.warning("Failed to load cached LLM summary: %s", exc)
|
|
230
236
|
return False
|
|
231
237
|
|
|
238
|
+
def _extract_summary_from_markdown_content(self, content: str) -> str | None:
|
|
239
|
+
"""Extract a summary previously written by github-dependents-info in a markdown content."""
|
|
240
|
+
marked = re.search(
|
|
241
|
+
re.escape(LLM_SUMMARY_START_MARKER) + "(.*?)" + re.escape(LLM_SUMMARY_END_MARKER),
|
|
242
|
+
content,
|
|
243
|
+
re.DOTALL,
|
|
244
|
+
)
|
|
245
|
+
if marked is not None:
|
|
246
|
+
block = marked.group(1)
|
|
247
|
+
else:
|
|
248
|
+
# Markdown generated before summary markers existed: fallback on the "## Summary" section
|
|
249
|
+
heading = re.search(r"^##\s+Summary\s*$", content, re.MULTILINE)
|
|
250
|
+
if heading is None:
|
|
251
|
+
return None
|
|
252
|
+
block = content[heading.end() :]
|
|
253
|
+
block = re.sub(r"^\s*##\s+Summary\s*$", "", block, count=1, flags=re.MULTILINE)
|
|
254
|
+
# Keep only the summary text: stop at the next heading, badge, table or footer
|
|
255
|
+
summary_lines: list[str] = []
|
|
256
|
+
for line in block.splitlines():
|
|
257
|
+
stripped = line.strip()
|
|
258
|
+
if stripped.startswith(("#", "[![", "|", "_Generated using [")):
|
|
259
|
+
break
|
|
260
|
+
summary_lines.append(line)
|
|
261
|
+
return "\n".join(summary_lines).strip() or None
|
|
262
|
+
|
|
263
|
+
def _load_previous_markdown_summary(self) -> str | None:
|
|
264
|
+
"""Read the summary generated by a previous run in the output markdown file."""
|
|
265
|
+
if not self.markdown_file:
|
|
266
|
+
return None
|
|
267
|
+
try:
|
|
268
|
+
if not os.path.isfile(self.markdown_file):
|
|
269
|
+
return None
|
|
270
|
+
with open(self.markdown_file, encoding="utf-8") as f:
|
|
271
|
+
content = f.read()
|
|
272
|
+
except (OSError, UnicodeDecodeError) as exc:
|
|
273
|
+
logger.warning("Failed to read previous summary from %s: %s", self.markdown_file, exc)
|
|
274
|
+
return None
|
|
275
|
+
return self._extract_summary_from_markdown_content(content)
|
|
276
|
+
|
|
277
|
+
def reuse_previous_llm_summary(self, reason: str) -> bool:
|
|
278
|
+
"""Reuse the summary of a previous run when a new one can not be generated.
|
|
279
|
+
|
|
280
|
+
This avoids losing an existing summary when the LLM call is impossible
|
|
281
|
+
(no API key anymore, no credit left, rate limit, timeout...).
|
|
282
|
+
"""
|
|
283
|
+
if self.llm_summary:
|
|
284
|
+
return True
|
|
285
|
+
if self.load_llm_summary():
|
|
286
|
+
self.llm_summary_reused = True
|
|
287
|
+
else:
|
|
288
|
+
previous_summary = self._load_previous_markdown_summary()
|
|
289
|
+
if not previous_summary:
|
|
290
|
+
return False
|
|
291
|
+
self.llm_summary = previous_summary
|
|
292
|
+
self.llm_summary_reused = True
|
|
293
|
+
# Keep it in the progress directory so next runs can find it faster
|
|
294
|
+
self.save_llm_summary()
|
|
295
|
+
logger.warning(
|
|
296
|
+
"Unable to generate a new AI summary (%s): reusing the summary generated by a previous run",
|
|
297
|
+
reason,
|
|
298
|
+
)
|
|
299
|
+
return True
|
|
300
|
+
|
|
232
301
|
def save_llm_summary(self) -> None:
|
|
233
302
|
"""Persist LLM summary into progress directory if enabled."""
|
|
234
303
|
cache_path = self._llm_summary_cache_path()
|
|
@@ -245,9 +314,9 @@ class GithubDependentsInfo:
|
|
|
245
314
|
}
|
|
246
315
|
with open(cache_path, "w", encoding="utf-8") as f:
|
|
247
316
|
json.dump(payload, f, indent=2)
|
|
248
|
-
except Exception as exc:
|
|
317
|
+
except Exception as exc: # noqa: BLE001 - caching is best effort
|
|
249
318
|
if self.debug:
|
|
250
|
-
|
|
319
|
+
logger.warning("Failed to save cached LLM summary: %s", exc)
|
|
251
320
|
|
|
252
321
|
def _prepare_llm_summary_payload(self) -> dict:
|
|
253
322
|
"""Prepare a compact data payload for the LLM prompt."""
|
|
@@ -295,6 +364,7 @@ class GithubDependentsInfo:
|
|
|
295
364
|
|
|
296
365
|
provider_info = self._detect_llm_provider()
|
|
297
366
|
if provider_info is None:
|
|
367
|
+
self.reuse_previous_llm_summary("no LLM API key found in environment variables")
|
|
298
368
|
return
|
|
299
369
|
|
|
300
370
|
# Default model if none was provided
|
|
@@ -346,9 +416,13 @@ class GithubDependentsInfo:
|
|
|
346
416
|
if content:
|
|
347
417
|
self.llm_summary = str(content).strip()
|
|
348
418
|
self.save_llm_summary()
|
|
349
|
-
|
|
419
|
+
else:
|
|
420
|
+
self.llm_summary_error = "LLM returned an empty summary"
|
|
421
|
+
self.reuse_previous_llm_summary(self.llm_summary_error)
|
|
422
|
+
except Exception as exc: # noqa: BLE001 - any provider error must not break the run
|
|
350
423
|
self.llm_summary_error = str(exc)
|
|
351
|
-
|
|
424
|
+
logger.warning("Failed to generate LLM summary: %s", exc)
|
|
425
|
+
self.reuse_previous_llm_summary(str(exc))
|
|
352
426
|
finally:
|
|
353
427
|
# LiteLLM can keep async clients around. Ensure they're closed before
|
|
354
428
|
# asyncio.run() tears down the event loop to avoid:
|
|
@@ -375,9 +449,9 @@ class GithubDependentsInfo:
|
|
|
375
449
|
result = close_fn()
|
|
376
450
|
if inspect.isawaitable(result):
|
|
377
451
|
await result
|
|
378
|
-
except Exception as exc:
|
|
452
|
+
except Exception as exc: # noqa: BLE001 - cleanup is best effort
|
|
379
453
|
if self.debug:
|
|
380
|
-
|
|
454
|
+
logger.debug("LiteLLM async client cleanup skipped: %s", exc)
|
|
381
455
|
|
|
382
456
|
def _extract_owner_repo(self, dependent_row):
|
|
383
457
|
repo_anchor = dependent_row.find("a", {"data-hovercard-type": "repository"})
|
|
@@ -426,7 +500,7 @@ class GithubDependentsInfo:
|
|
|
426
500
|
if "{{" in package_name:
|
|
427
501
|
continue
|
|
428
502
|
if self.debug is True:
|
|
429
|
-
|
|
503
|
+
logger.info(package_name)
|
|
430
504
|
self.packages += [{"id": package_id, "name": package_name}]
|
|
431
505
|
if len(self.packages) == 0:
|
|
432
506
|
self.packages = [{"id": None, "name": self.repo}]
|
|
@@ -452,7 +526,10 @@ class GithubDependentsInfo:
|
|
|
452
526
|
sources_all_df.set_index("name", inplace=True)
|
|
453
527
|
source_df = pd.json_normalize(source_info).set_index("name", drop=True)
|
|
454
528
|
for column in source_df.columns:
|
|
455
|
-
|
|
529
|
+
# pandas >= 3 infers text columns as StringDtype rather than object, so a
|
|
530
|
+
# `dtype == object` test misses them and writing text into a numeric column
|
|
531
|
+
# raises instead of upcasting. Widen the target column for any non-numeric value.
|
|
532
|
+
if column in sources_all_df.columns and not pd.api.types.is_numeric_dtype(source_df[column]):
|
|
456
533
|
sources_all_df[column] = sources_all_df[column].astype("object")
|
|
457
534
|
sources_all_df.loc[source_df.index, column] = source_df[column]
|
|
458
535
|
sources_all_df.reset_index(inplace=True, drop=False)
|
|
@@ -567,9 +644,26 @@ class GithubDependentsInfo:
|
|
|
567
644
|
"""Build the standard footer for markdown files."""
|
|
568
645
|
return [
|
|
569
646
|
"",
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
647
|
+
(
|
|
648
|
+
"_Generated using [github-dependents-info]"
|
|
649
|
+
"(https://github.com/nvuillam/github-dependents-info), "
|
|
650
|
+
"by [Nicolas Vuillamy](https://github.com/nvuillam)_"
|
|
651
|
+
),
|
|
652
|
+
]
|
|
653
|
+
|
|
654
|
+
def _append_llm_summary(self, md_lines: list) -> None:
|
|
655
|
+
"""Append the AI summary section, surrounded by markers so it can be reused by later runs."""
|
|
656
|
+
if not self.llm_summary:
|
|
657
|
+
return
|
|
658
|
+
md_lines += [
|
|
659
|
+
LLM_SUMMARY_START_MARKER,
|
|
660
|
+
"",
|
|
661
|
+
"## Summary",
|
|
662
|
+
"",
|
|
663
|
+
self.llm_summary.strip(),
|
|
664
|
+
"",
|
|
665
|
+
LLM_SUMMARY_END_MARKER,
|
|
666
|
+
"",
|
|
573
667
|
]
|
|
574
668
|
|
|
575
669
|
def _append_summary_table(self, md_lines: list) -> None:
|
|
@@ -606,8 +700,7 @@ class GithubDependentsInfo:
|
|
|
606
700
|
self._append_summary_table(md_lines)
|
|
607
701
|
|
|
608
702
|
# Optional LLM summary
|
|
609
|
-
|
|
610
|
-
md_lines += ["## Summary", "", self.llm_summary.strip(), ""]
|
|
703
|
+
self._append_llm_summary(md_lines)
|
|
611
704
|
|
|
612
705
|
# Single dependents list
|
|
613
706
|
if self.merge_packages is True:
|
|
@@ -702,8 +795,7 @@ class GithubDependentsInfo:
|
|
|
702
795
|
self._append_summary_table(md_lines)
|
|
703
796
|
|
|
704
797
|
# Optional LLM summary (only on first page)
|
|
705
|
-
|
|
706
|
-
md_lines += ["## Summary", "", self.llm_summary.strip(), ""]
|
|
798
|
+
self._append_llm_summary(md_lines)
|
|
707
799
|
|
|
708
800
|
# Calculate start and end indices for this page
|
|
709
801
|
start_idx = (page_num - 1) * self.page_size
|
|
@@ -840,12 +932,9 @@ class GithubDependentsInfo:
|
|
|
840
932
|
md_lines += [f"|{image_md} [{repo_label}](https://github.com/{repo_label}) | {repo_stars} |"]
|
|
841
933
|
|
|
842
934
|
def build_badge(self, label, nb, **options):
|
|
843
|
-
|
|
844
|
-
url = options["url"]
|
|
845
|
-
else:
|
|
846
|
-
url = f"https://github.com/{self.repo}/network/dependents"
|
|
935
|
+
url = options.get("url", f"https://github.com/{self.repo}/network/dependents")
|
|
847
936
|
return (
|
|
848
|
-
f"[]({url})"
|
|
850
939
|
)
|
|
851
940
|
|
|
@@ -890,7 +979,7 @@ class GithubDependentsInfo:
|
|
|
890
979
|
raise
|
|
891
980
|
delay = self._compute_retry_delay(attempt, response=exc.response)
|
|
892
981
|
if self.debug:
|
|
893
|
-
|
|
982
|
+
logger.warning(
|
|
894
983
|
"HTTP %s while fetching %s (attempt %s/%s). Retrying in %.1fs",
|
|
895
984
|
status_code,
|
|
896
985
|
url,
|
|
@@ -905,7 +994,7 @@ class GithubDependentsInfo:
|
|
|
905
994
|
raise
|
|
906
995
|
delay = self._compute_retry_delay(attempt)
|
|
907
996
|
if self.debug:
|
|
908
|
-
|
|
997
|
+
logger.warning(
|
|
909
998
|
"Request error while fetching %s (attempt %s/%s): %s. Retrying in %.1fs",
|
|
910
999
|
url,
|
|
911
1000
|
attempt,
|
|
@@ -952,12 +1041,12 @@ class GithubDependentsInfo:
|
|
|
952
1041
|
# Check if we've reached the max pages limit
|
|
953
1042
|
if self.max_scraped_pages > 0 and page_number >= self.max_scraped_pages:
|
|
954
1043
|
if self.debug is True:
|
|
955
|
-
|
|
1044
|
+
logger.info(f" - reached max scraped pages limit ({self.max_scraped_pages})")
|
|
956
1045
|
break
|
|
957
1046
|
next_link = u["href"]
|
|
958
1047
|
page_number += 1
|
|
959
1048
|
if self.debug is True:
|
|
960
|
-
|
|
1049
|
+
logger.info(f" - discovered page {page_number}")
|
|
961
1050
|
break
|
|
962
1051
|
|
|
963
1052
|
if next_link is None:
|
|
@@ -970,13 +1059,13 @@ class GithubDependentsInfo:
|
|
|
970
1059
|
current_soup = BeautifulSoup(current_content, "html.parser")
|
|
971
1060
|
except (httpx.RequestError, httpx.TimeoutException) as e:
|
|
972
1061
|
if self.debug:
|
|
973
|
-
|
|
1062
|
+
logger.warning(f"Failed to fetch page during discovery: {e}")
|
|
974
1063
|
break
|
|
975
1064
|
|
|
976
1065
|
# Fetch additional pages in parallel if any
|
|
977
1066
|
if urls_to_fetch:
|
|
978
1067
|
if self.debug:
|
|
979
|
-
|
|
1068
|
+
logger.info(f" - fetching {len(urls_to_fetch)} additional pages in parallel...")
|
|
980
1069
|
tasks = [self.fetch_page(client, page_url, semaphore) for page_url in urls_to_fetch]
|
|
981
1070
|
additional_pages = await asyncio.gather(*tasks, return_exceptions=True)
|
|
982
1071
|
pages_content += additional_pages
|
|
@@ -987,7 +1076,7 @@ class GithubDependentsInfo:
|
|
|
987
1076
|
for page_content in pages_content:
|
|
988
1077
|
if isinstance(page_content, Exception):
|
|
989
1078
|
if self.debug:
|
|
990
|
-
|
|
1079
|
+
logger.warning(f"Failed to fetch page: {page_content}")
|
|
991
1080
|
continue
|
|
992
1081
|
|
|
993
1082
|
soup = BeautifulSoup(page_content, "html.parser")
|
|
@@ -997,7 +1086,7 @@ class GithubDependentsInfo:
|
|
|
997
1086
|
owner_repo = self._extract_owner_repo(t)
|
|
998
1087
|
if owner_repo is None:
|
|
999
1088
|
if self.debug:
|
|
1000
|
-
|
|
1089
|
+
logger.warning("Skipping dependent row without repository link")
|
|
1001
1090
|
continue
|
|
1002
1091
|
owner_name, repo_name = owner_repo
|
|
1003
1092
|
star_svg = t.find("svg", {"class": "octicon-star"})
|
|
@@ -1068,6 +1157,6 @@ class GithubDependentsInfo:
|
|
|
1068
1157
|
number_as_string = number_as_string.replace(",", "").replace(" ", "")
|
|
1069
1158
|
try:
|
|
1070
1159
|
return int(number_as_string)
|
|
1071
|
-
except
|
|
1072
|
-
|
|
1160
|
+
except (ValueError, TypeError):
|
|
1161
|
+
logger.warning(f'WARNING: Unable to get integer from "{number_as_string}"')
|
|
1073
1162
|
return 0
|
|
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
5
5
|
|
|
6
6
|
[project]
|
|
7
7
|
name = "github-dependents-info"
|
|
8
|
-
version = "
|
|
8
|
+
version = "4.0.0"
|
|
9
9
|
description = "Collect information about dependencies between a github repo and other repositories. Results available in JSON, markdown and badges."
|
|
10
10
|
readme = "README.md"
|
|
11
11
|
license = "MIT"
|
|
@@ -13,7 +13,7 @@ license-files = ["LICENSE"]
|
|
|
13
13
|
authors = [
|
|
14
14
|
{name = "nvuillam", email = "nicolas.vuillamy@gmail.com"},
|
|
15
15
|
]
|
|
16
|
-
requires-python = ">=3.
|
|
16
|
+
requires-python = ">=3.11,<4.0"
|
|
17
17
|
|
|
18
18
|
# Keywords description https://python-poetry.org/docs/pyproject/#keywords
|
|
19
19
|
keywords = ["github", "dependent", "used-by", "repository", "stats", "dependencies", "using"]
|
|
@@ -37,41 +37,42 @@ Repository = "https://github.com/nvuillam/github-dependents-info"
|
|
|
37
37
|
"github-dependents-info" = "github_dependents_info.__main__:app"
|
|
38
38
|
|
|
39
39
|
[tool.poetry.dependencies]
|
|
40
|
-
python = ">=3.
|
|
40
|
+
python = ">=3.11,<4.0"
|
|
41
41
|
|
|
42
|
-
click = ">=8.
|
|
43
|
-
typer = ">=0.
|
|
42
|
+
click = ">=8.5,<8.6"
|
|
43
|
+
typer = ">=0.27.1,<0.28"
|
|
44
44
|
rich = ">=15,<15.1"
|
|
45
45
|
beautifulsoup4 = "4.15.0"
|
|
46
|
-
pandas = ">=
|
|
46
|
+
pandas = ">=3.0.5,<3.1"
|
|
47
47
|
httpx = "^0.28.1"
|
|
48
48
|
idna = ">=3.14"
|
|
49
|
+
soupsieve = ">=2.8.4" # security floor (CVE-2026-49476, CVE-2026-49477), pulled by beautifulsoup4
|
|
49
50
|
|
|
50
51
|
[tool.poetry.group.dev.dependencies]
|
|
51
52
|
bandit = "^1.7.5"
|
|
52
53
|
black = ">=26.3.1"
|
|
53
54
|
darglint = "^1.8.1"
|
|
54
|
-
isort = {extras = ["colors"], version = "^
|
|
55
|
+
isort = {extras = ["colors"], version = "^9.0.0"}
|
|
55
56
|
mypy = "^2.0"
|
|
56
57
|
mypy-extensions = "^1.0.0"
|
|
57
58
|
pre-commit = "^4.0.0"
|
|
58
59
|
pydocstyle = "^6.3.0"
|
|
59
60
|
pylint = "^4.0.0"
|
|
60
|
-
pytest = "^9.
|
|
61
|
+
pytest = "^9.1.1"
|
|
61
62
|
pyupgrade = "^3.4.0"
|
|
62
|
-
safety = "^3.8.1"
|
|
63
63
|
coverage = "^7.3.4"
|
|
64
|
-
|
|
65
|
-
cryptography = ">=
|
|
64
|
+
anybadge = "^1.16.0" # replaces coverage-badge, which imports pkg_resources (removed in setuptools 82)
|
|
65
|
+
cryptography = ">=50.0.0"
|
|
66
66
|
pytest-html = "^4.1.1"
|
|
67
67
|
pytest-cov = "^7.0.0"
|
|
68
|
-
marshmallow = ">=4.3,<4.4"
|
|
68
|
+
marshmallow = ">=4.3.1,<4.4"
|
|
69
69
|
authlib = ">=1.7.0"
|
|
70
70
|
requests = ">=2.34.2"
|
|
71
71
|
urllib3 = ">=2.6.3"
|
|
72
72
|
filelock = ">=3.20.4"
|
|
73
73
|
virtualenv = ">=21.3.0"
|
|
74
|
-
nltk = ">=3.
|
|
74
|
+
nltk = ">=3.10.0"
|
|
75
|
+
setuptools = ">=83.0.0"
|
|
75
76
|
|
|
76
77
|
|
|
77
78
|
[[tool.poetry.source]]
|
|
File without changes
|
{github_dependents_info-3.2.1 → github_dependents_info-4.0.0}/github_dependents_info/__init__.py
RENAMED
|
File without changes
|