github-dependents-info 2.0.2__tar.gz → 3.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.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-dependents-info
3
- Version: 2.0.2
3
+ Version: 3.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
@@ -18,9 +18,11 @@ Requires-Dist: beautifulsoup4 (==4.14.3)
18
18
  Requires-Dist: click (>=8.3.1,<8.4)
19
19
  Requires-Dist: httpx (>=0.28.1,<0.29.0)
20
20
  Requires-Dist: idna (>=3.11)
21
+ Requires-Dist: litellm (>=1.60.0,<2.0)
21
22
  Requires-Dist: pandas (>=2.3.3,<3.0)
22
23
  Requires-Dist: rich (>=14.2,<14.3)
23
- Requires-Dist: typer[all] (>=0.20,<0.21)
24
+ Requires-Dist: typer-slim (>=0.19,<0.20)
25
+ Requires-Dist: typer[standard] (>=0.19,<0.20)
24
26
  Project-URL: Homepage, https://github.com/nvuillam/github-dependents-info
25
27
  Project-URL: Repository, https://github.com/nvuillam/github-dependents-info
26
28
  Description-Content-Type: text/markdown
@@ -60,6 +62,7 @@ This package uses GitHub HTML to collect dependents information and can:
60
62
  - Output as text
61
63
  - Output as json (including shields.io markdown badges)
62
64
  - Generate summary markdown file
65
+ - Optionally add an AI-generated usage summary (via `litellm`) when an LLM API key is present
63
66
  - Update existing markdown by inserting **Used by** badge within tags
64
67
  - `<!-- gh-dependents-info-used-by-start -->
65
68
  [![Generated by github-dependents-info](https://img.shields.io/static/v1?label=Used%20by&message=22&color=informational&logo=slickpic)](https://github.com/nvuillam/github-dependents-info/blob/main/docs/github-dependents-info.md)<!-- gh-dependents-info-used-by-end -->`
@@ -68,6 +71,24 @@ This package uses GitHub HTML to collect dependents information and can:
68
71
  - Keep huge ecosystems manageable with pagination controls (`--max-scraped-pages`, `--pagination/--no-pagination`, `--page-size`)
69
72
  - Fetch dependents faster thanks to asynchronous `httpx` requests and parallelized page scraping
70
73
 
74
+ ### AI usage summary (optional)
75
+
76
+ If an LLM API key is detected in the environment (for example `OPENAI_API_KEY`), the tool will call a lightweight model (via `litellm`) to generate a short **usage summary** and include it in the generated markdown.
77
+
78
+ - Supported provider env vars (most common):
79
+ - OpenAI: `OPENAI_API_KEY`
80
+ - Azure OpenAI: `AZURE_OPENAI_API_KEY`
81
+ - Anthropic: `ANTHROPIC_API_KEY`
82
+ - Google Gemini: `GEMINI_API_KEY` (or `GOOGLE_API_KEY`)
83
+ - Mistral: `MISTRAL_API_KEY`
84
+ - Cohere: `COHERE_API_KEY`
85
+ - Groq: `GROQ_API_KEY`
86
+
87
+ - Disable with `--no-llm-summary` (or env var `GITHUB_DEPENDENTS_INFO_LLM_SUMMARY=false`)
88
+ - Override model with `--llm-model` (or env var `GITHUB_DEPENDENTS_INFO_LLM_MODEL` / `LITELLM_MODEL`)
89
+ - Adjust max summary length with `--llm-max-words` (or env var `GITHUB_DEPENDENTS_INFO_LLM_MAX_WORDS`)
90
+ - The summary is cached in `--csvdirectory` (file `llm_summary_<repo>.json`) and reused on subsequent runs
91
+
71
92
 
72
93
  Badges example
73
94
 
@@ -79,6 +100,8 @@ Badges example
79
100
  <details>
80
101
  <summary>JSON output</summary>
81
102
 
103
+ Note: when AI usage summary is enabled and an LLM API key is present, the JSON output also includes `llm_summary`.
104
+
82
105
  ```json
83
106
  {
84
107
  "all_public_dependent_repos": [
@@ -263,22 +286,27 @@ _________________
263
286
  github-dependents-info [OPTIONS]
264
287
  ```
265
288
 
266
- | Parameter | Type | Description |
267
- |-----------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
268
- | --repo | String | Repository. Example: `oxsecurity/megalinter` |
269
- | -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 -->` … `<!-- gh-dependents-info-used-by-end -->`) |
270
- | -s<br/> --sort | String | _(optional)_ Sort order: name (default) or stars |
271
- | -x<br/> --minstars | String | _(optional)_ If set, filters repositories to keep only those with more than X stars |
272
- | -m<br/> --markdownfile | String | _(optional)_ Output markdown file file |
273
- | -d<br/> --docurl | String | _(optional)_ Hyperlink to use when clicking on badge markdown file badge. (Default: link to markdown file) |
274
- | -p<br/> --mergepackages | String | _(optional)_ In case of multiple packages, merge their stats in a single one in markdown and json output |
275
- | -j<br/> --json | String | _(optional)_ Output in json format |
276
- | -u<br/> --owner | String | _(optional)_ If set, filters repositories to keep only those owned by the specified user/organization |
277
- | -n<br/> --max-scraped-pages | Integer | _(optional)_ Maximum number of GitHub pages to scrape per package (0 uses all available pages) |
278
- | --pagination<br/> --no-pagination | Boolean | _(optional)_ Enable (default) or disable pagination when writing markdown output |
279
- | --page-size | Integer | _(optional)_ Number of repositories per markdown page when pagination is enabled (default: 500) |
280
- | -v<br/> --version | Boolean | _(optional)_ Displays version of github-dependents-info |
281
- | --verbose | Boolean | _(optional)_ Verbose output |
289
+ | Parameter | Type | Description |
290
+ |-------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
291
+ | --repo | String | Repository. Example: `oxsecurity/megalinter` |
292
+ | -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 -->` … `<!-- gh-dependents-info-used-by-end -->`) |
293
+ | -s<br/> --sort | String | _(optional)_ Sort order: name (default) or stars |
294
+ | -x<br/> --minstars | String | _(optional)_ If set, filters repositories to keep only those with more than X stars |
295
+ | -m<br/> --markdownfile | String | _(optional)_ Output markdown file file |
296
+ | -d<br/> --docurl | String | _(optional)_ Hyperlink to use when clicking on badge markdown file badge. (Default: link to markdown file) |
297
+ | -p<br/> --mergepackages | String | _(optional)_ In case of multiple packages, merge their stats in a single one in markdown and json output |
298
+ | -j<br/> --json | String | _(optional)_ Output in json format |
299
+ | -u<br/> --owner | String | _(optional)_ If set, filters repositories to keep only those owned by the specified user/organization |
300
+ | -n<br/> --max-scraped-pages | Integer | _(optional)_ Maximum number of GitHub pages to scrape per package (0 uses all available pages) |
301
+ | --pagination<br/> --no-pagination | Boolean | _(optional)_ Enable (default) or disable pagination when writing markdown output |
302
+ | --page-size | Integer | _(optional)_ Number of repositories per markdown page when pagination is enabled (default: 500) |
303
+ | --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) |
304
+ | --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 |
305
+ | --llm-max-repos | Integer | _(optional)_ Max dependent repos included in the summary prompt payload (default: 500) |
306
+ | --llm-max-words | Integer | _(optional)_ Max words for the generated summary (default: 300) |
307
+ | --llm-timeout | Float | _(optional)_ Timeout (seconds) for the summary LLM call (default: 120) |
308
+ | -v<br/> --version | Boolean | _(optional)_ Displays version of github-dependents-info |
309
+ | --verbose | Boolean | _(optional)_ Verbose output |
282
310
 
283
311
  Badge tags example (the tool replaces everything between the markers):
284
312
 
@@ -328,6 +356,18 @@ _________________
328
356
 
329
357
  github-dependents-info --repo nvuillam/npm-groovy-lint --markdownfile ./docs/package-usage.md --page-size 250 --mergepackages
330
358
 
359
+ - Disable AI usage summary generation
360
+
361
+ github-dependents-info --repo nvuillam/npm-groovy-lint --markdownfile ./docs/package-usage.md --no-llm-summary
362
+
363
+ - Force a specific LiteLLM model for the summary
364
+
365
+ github-dependents-info --repo nvuillam/npm-groovy-lint --markdownfile ./docs/package-usage.md --llm-model gpt-4o-mini
366
+
367
+ - Generate markdown with AI summary using `OPENAI_API_KEY`
368
+
369
+ GEMINI_API_KEY=YOUR_KEY github-dependents-info --repo nvuillam/npm-groovy-lint --markdownfile ./docs/package-usage.md --llm-model gemini-3-flash-preview
370
+
331
371
  ## Use as GitHub Action
332
372
 
333
373
  Allow GitHub Actions to create Pull Requests in **Settings > Actions > General**
@@ -389,8 +429,16 @@ jobs:
389
429
  # badgemarkdownfile: README.md
390
430
  # sort: stars
391
431
  # minstars: "0"
432
+ # llm-summary: "true" # set to "false" to disable AI usage summary
433
+ # llm-model: "" # optional: override LiteLLM model (example: gpt-4o-mini, gemini-3-flash-preview)
434
+ # llm-max-repos: "500" # optional: cap repos sent to the summary prompt
435
+ # llm-max-words: "250" # optional: cap summary length
436
+ # llm-timeout: "120" # optional: timeout (seconds) for the LLM call
392
437
  env:
393
438
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
439
+ # To enable the AI usage summary, provide one of the supported provider API keys, for example:
440
+ # OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
441
+ # GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
394
442
 
395
443
  # Workaround for git issues
396
444
  - name: Prepare commit
@@ -33,6 +33,7 @@ This package uses GitHub HTML to collect dependents information and can:
33
33
  - Output as text
34
34
  - Output as json (including shields.io markdown badges)
35
35
  - Generate summary markdown file
36
+ - Optionally add an AI-generated usage summary (via `litellm`) when an LLM API key is present
36
37
  - Update existing markdown by inserting **Used by** badge within tags
37
38
  - `<!-- gh-dependents-info-used-by-start -->
38
39
  [![Generated by github-dependents-info](https://img.shields.io/static/v1?label=Used%20by&message=22&color=informational&logo=slickpic)](https://github.com/nvuillam/github-dependents-info/blob/main/docs/github-dependents-info.md)<!-- gh-dependents-info-used-by-end -->`
@@ -41,6 +42,24 @@ This package uses GitHub HTML to collect dependents information and can:
41
42
  - Keep huge ecosystems manageable with pagination controls (`--max-scraped-pages`, `--pagination/--no-pagination`, `--page-size`)
42
43
  - Fetch dependents faster thanks to asynchronous `httpx` requests and parallelized page scraping
43
44
 
45
+ ### AI usage summary (optional)
46
+
47
+ If an LLM API key is detected in the environment (for example `OPENAI_API_KEY`), the tool will call a lightweight model (via `litellm`) to generate a short **usage summary** and include it in the generated markdown.
48
+
49
+ - Supported provider env vars (most common):
50
+ - OpenAI: `OPENAI_API_KEY`
51
+ - Azure OpenAI: `AZURE_OPENAI_API_KEY`
52
+ - Anthropic: `ANTHROPIC_API_KEY`
53
+ - Google Gemini: `GEMINI_API_KEY` (or `GOOGLE_API_KEY`)
54
+ - Mistral: `MISTRAL_API_KEY`
55
+ - Cohere: `COHERE_API_KEY`
56
+ - Groq: `GROQ_API_KEY`
57
+
58
+ - Disable with `--no-llm-summary` (or env var `GITHUB_DEPENDENTS_INFO_LLM_SUMMARY=false`)
59
+ - Override model with `--llm-model` (or env var `GITHUB_DEPENDENTS_INFO_LLM_MODEL` / `LITELLM_MODEL`)
60
+ - Adjust max summary length with `--llm-max-words` (or env var `GITHUB_DEPENDENTS_INFO_LLM_MAX_WORDS`)
61
+ - The summary is cached in `--csvdirectory` (file `llm_summary_<repo>.json`) and reused on subsequent runs
62
+
44
63
 
45
64
  Badges example
46
65
 
@@ -52,6 +71,8 @@ Badges example
52
71
  <details>
53
72
  <summary>JSON output</summary>
54
73
 
74
+ Note: when AI usage summary is enabled and an LLM API key is present, the JSON output also includes `llm_summary`.
75
+
55
76
  ```json
56
77
  {
57
78
  "all_public_dependent_repos": [
@@ -236,22 +257,27 @@ _________________
236
257
  github-dependents-info [OPTIONS]
237
258
  ```
238
259
 
239
- | Parameter | Type | Description |
240
- |-----------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
241
- | --repo | String | Repository. Example: `oxsecurity/megalinter` |
242
- | -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 -->` … `<!-- gh-dependents-info-used-by-end -->`) |
243
- | -s<br/> --sort | String | _(optional)_ Sort order: name (default) or stars |
244
- | -x<br/> --minstars | String | _(optional)_ If set, filters repositories to keep only those with more than X stars |
245
- | -m<br/> --markdownfile | String | _(optional)_ Output markdown file file |
246
- | -d<br/> --docurl | String | _(optional)_ Hyperlink to use when clicking on badge markdown file badge. (Default: link to markdown file) |
247
- | -p<br/> --mergepackages | String | _(optional)_ In case of multiple packages, merge their stats in a single one in markdown and json output |
248
- | -j<br/> --json | String | _(optional)_ Output in json format |
249
- | -u<br/> --owner | String | _(optional)_ If set, filters repositories to keep only those owned by the specified user/organization |
250
- | -n<br/> --max-scraped-pages | Integer | _(optional)_ Maximum number of GitHub pages to scrape per package (0 uses all available pages) |
251
- | --pagination<br/> --no-pagination | Boolean | _(optional)_ Enable (default) or disable pagination when writing markdown output |
252
- | --page-size | Integer | _(optional)_ Number of repositories per markdown page when pagination is enabled (default: 500) |
253
- | -v<br/> --version | Boolean | _(optional)_ Displays version of github-dependents-info |
254
- | --verbose | Boolean | _(optional)_ Verbose output |
260
+ | Parameter | Type | Description |
261
+ |-------------------------------------|---------|-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|
262
+ | --repo | String | Repository. Example: `oxsecurity/megalinter` |
263
+ | -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 -->` … `<!-- gh-dependents-info-used-by-end -->`) |
264
+ | -s<br/> --sort | String | _(optional)_ Sort order: name (default) or stars |
265
+ | -x<br/> --minstars | String | _(optional)_ If set, filters repositories to keep only those with more than X stars |
266
+ | -m<br/> --markdownfile | String | _(optional)_ Output markdown file file |
267
+ | -d<br/> --docurl | String | _(optional)_ Hyperlink to use when clicking on badge markdown file badge. (Default: link to markdown file) |
268
+ | -p<br/> --mergepackages | String | _(optional)_ In case of multiple packages, merge their stats in a single one in markdown and json output |
269
+ | -j<br/> --json | String | _(optional)_ Output in json format |
270
+ | -u<br/> --owner | String | _(optional)_ If set, filters repositories to keep only those owned by the specified user/organization |
271
+ | -n<br/> --max-scraped-pages | Integer | _(optional)_ Maximum number of GitHub pages to scrape per package (0 uses all available pages) |
272
+ | --pagination<br/> --no-pagination | Boolean | _(optional)_ Enable (default) or disable pagination when writing markdown output |
273
+ | --page-size | Integer | _(optional)_ Number of repositories per markdown page when pagination is enabled (default: 500) |
274
+ | --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) |
275
+ | --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 |
276
+ | --llm-max-repos | Integer | _(optional)_ Max dependent repos included in the summary prompt payload (default: 500) |
277
+ | --llm-max-words | Integer | _(optional)_ Max words for the generated summary (default: 300) |
278
+ | --llm-timeout | Float | _(optional)_ Timeout (seconds) for the summary LLM call (default: 120) |
279
+ | -v<br/> --version | Boolean | _(optional)_ Displays version of github-dependents-info |
280
+ | --verbose | Boolean | _(optional)_ Verbose output |
255
281
 
256
282
  Badge tags example (the tool replaces everything between the markers):
257
283
 
@@ -301,6 +327,18 @@ _________________
301
327
 
302
328
  github-dependents-info --repo nvuillam/npm-groovy-lint --markdownfile ./docs/package-usage.md --page-size 250 --mergepackages
303
329
 
330
+ - Disable AI usage summary generation
331
+
332
+ github-dependents-info --repo nvuillam/npm-groovy-lint --markdownfile ./docs/package-usage.md --no-llm-summary
333
+
334
+ - Force a specific LiteLLM model for the summary
335
+
336
+ github-dependents-info --repo nvuillam/npm-groovy-lint --markdownfile ./docs/package-usage.md --llm-model gpt-4o-mini
337
+
338
+ - Generate markdown with AI summary using `OPENAI_API_KEY`
339
+
340
+ GEMINI_API_KEY=YOUR_KEY github-dependents-info --repo nvuillam/npm-groovy-lint --markdownfile ./docs/package-usage.md --llm-model gemini-3-flash-preview
341
+
304
342
  ## Use as GitHub Action
305
343
 
306
344
  Allow GitHub Actions to create Pull Requests in **Settings > Actions > General**
@@ -362,8 +400,16 @@ jobs:
362
400
  # badgemarkdownfile: README.md
363
401
  # sort: stars
364
402
  # minstars: "0"
403
+ # llm-summary: "true" # set to "false" to disable AI usage summary
404
+ # llm-model: "" # optional: override LiteLLM model (example: gpt-4o-mini, gemini-3-flash-preview)
405
+ # llm-max-repos: "500" # optional: cap repos sent to the summary prompt
406
+ # llm-max-words: "250" # optional: cap summary length
407
+ # llm-timeout: "120" # optional: timeout (seconds) for the LLM call
365
408
  env:
366
409
  GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
410
+ # To enable the AI usage summary, provide one of the supported provider API keys, for example:
411
+ # OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
412
+ # GEMINI_API_KEY: ${{ secrets.GEMINI_API_KEY }}
367
413
 
368
414
  # Workaround for git issues
369
415
  - name: Prepare commit
@@ -1,4 +1,5 @@
1
1
  import logging
2
+ from typing import Annotated
2
3
 
3
4
  import typer
4
5
  from github_dependents_info import version
@@ -86,6 +87,39 @@ def main(
86
87
  True, "--pagination/--no-pagination", help="Enable pagination to split results into multiple files"
87
88
  ),
88
89
  page_size: int = typer.Option(500, "--page-size", help="Number of results per page when pagination is enabled"),
90
+ llm_summary: Annotated[
91
+ bool | None,
92
+ typer.Option(
93
+ "--llm-summary/--no-llm-summary",
94
+ help=(
95
+ "Generate an AI usage summary in the markdown output when an LLM API key is present "
96
+ "(default: enabled)."
97
+ ),
98
+ ),
99
+ ] = None,
100
+ llm_model: str = typer.Option(
101
+ None,
102
+ "--llm-model",
103
+ help=(
104
+ "LiteLLM model to use for summary generation. If not set, a lightweight model is selected "
105
+ "based on the API key provider."
106
+ ),
107
+ ),
108
+ llm_max_repos: int = typer.Option(
109
+ None,
110
+ "--llm-max-repos",
111
+ help="Max dependent repos to include in the LLM prompt payload (default: 80).",
112
+ ),
113
+ llm_max_words: int = typer.Option(
114
+ None,
115
+ "--llm-max-words",
116
+ help="Max words for the generated summary (default: 300).",
117
+ ),
118
+ llm_timeout: float = typer.Option(
119
+ None,
120
+ "--llm-timeout",
121
+ help="Timeout (seconds) for the LLM call (default: 120).",
122
+ ),
89
123
  ) -> None:
90
124
  # Init logger
91
125
  if verbose is True:
@@ -104,26 +138,38 @@ def main(
104
138
  if min_stars is None:
105
139
  min_stars = 0
106
140
  # Create GithubDependentsInfo instance
107
- gh_deps_info = GithubDependentsInfo(
108
- repo,
109
- outputrepo=outputrepo,
110
- debug=verbose,
111
- overwrite_progress=overwrite,
112
- sort_key=sort_key,
113
- min_stars=min_stars,
114
- json_output=json_output,
115
- csv_directory=csv_directory,
116
- badge_markdown_file=badge_markdown_file,
117
- doc_url=doc_url,
118
- markdown_file=markdown_file,
119
- badge_color=badge_color,
120
- merge_packages=merge_packages,
121
- owner=owner,
122
- time_delay=time_delay,
123
- max_scraped_pages=max_scraped_pages,
124
- pagination=pagination,
125
- page_size=page_size,
126
- )
141
+ gh_options = {
142
+ "outputrepo": outputrepo,
143
+ "debug": verbose,
144
+ "overwrite_progress": overwrite,
145
+ "sort_key": sort_key,
146
+ "min_stars": min_stars,
147
+ "json_output": json_output,
148
+ "csv_directory": csv_directory,
149
+ "badge_markdown_file": badge_markdown_file,
150
+ "doc_url": doc_url,
151
+ "markdown_file": markdown_file,
152
+ "badge_color": badge_color,
153
+ "merge_packages": merge_packages,
154
+ "owner": owner,
155
+ "time_delay": time_delay,
156
+ "max_scraped_pages": max_scraped_pages,
157
+ "pagination": pagination,
158
+ "page_size": page_size,
159
+ }
160
+ # Only pass LLM options if explicitly provided, to keep env-based defaults working
161
+ if llm_summary is not None:
162
+ gh_options["llm_summary"] = llm_summary
163
+ if llm_model is not None:
164
+ gh_options["llm_model"] = llm_model
165
+ if llm_max_repos is not None:
166
+ gh_options["llm_max_repos"] = llm_max_repos
167
+ if llm_max_words is not None:
168
+ gh_options["llm_max_words"] = llm_max_words
169
+ if llm_timeout is not None:
170
+ gh_options["llm_timeout"] = llm_timeout
171
+
172
+ gh_deps_info = GithubDependentsInfo(repo, **gh_options)
127
173
  # Collect data
128
174
  gh_deps_info.collect()
129
175
  # Write output markdown
@@ -1,9 +1,11 @@
1
1
  import asyncio
2
+ import inspect
2
3
  import json
3
4
  import logging
4
5
  import math
5
6
  import os
6
7
  import re
8
+ from collections import Counter
7
9
  from pathlib import Path
8
10
 
9
11
  import httpx
@@ -55,6 +57,22 @@ class GithubDependentsInfo:
55
57
  self.http_retry_backoff = options.get("http_retry_backoff", 2.0)
56
58
  self.http_retry_max_delay = options.get("http_retry_max_delay", 60.0)
57
59
 
60
+ # LLM summary options (used only when an API key is present)
61
+ llm_summary_env = os.getenv("GITHUB_DEPENDENTS_INFO_LLM_SUMMARY")
62
+ llm_summary_default = True
63
+ if llm_summary_env is not None:
64
+ llm_summary_default = llm_summary_env.strip().lower() not in {"0", "false", "no", "off"}
65
+ self.llm_summary_enabled = options.get("llm_summary", llm_summary_default)
66
+ self.llm_model = (
67
+ options.get("llm_model") or os.getenv("GITHUB_DEPENDENTS_INFO_LLM_MODEL") or os.getenv("LITELLM_MODEL")
68
+ )
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)))
72
+ self.llm_model_used: str | None = None
73
+ self.llm_summary: str | None = None
74
+ self.llm_summary_error: str | None = None
75
+
58
76
  def collect(self):
59
77
  """Main entry point - synchronous wrapper for async collection."""
60
78
  return asyncio.run(self.collect_async())
@@ -164,9 +182,203 @@ class GithubDependentsInfo:
164
182
  self.badges["public"] = self.build_badge("Used%20by%20(public)", self.total_public_sum)
165
183
  self.badges["private"] = self.build_badge("Used%20by%20(private)", self.total_private_sum)
166
184
  self.badges["stars"] = self.build_badge("Used%20by%20(stars)", self.total_stars_sum)
185
+
186
+ # Optional: generate an LLM summary if an API key is present (and reuse cached summary when available)
187
+ await self.maybe_generate_llm_summary()
167
188
  # Build final result
168
189
  return self.build_result()
169
190
 
191
+ def _detect_llm_provider(self) -> dict | None:
192
+ """Detect which provider API key is present and propose a lightweight default model."""
193
+ candidates: list[tuple[str, str, str]] = [
194
+ ("openai", "OPENAI_API_KEY", "gpt-5-mini"),
195
+ ("azure_openai", "AZURE_OPENAI_API_KEY", "gpt-5-mini"),
196
+ ("anthropic", "ANTHROPIC_API_KEY", "claude-3-5-haiku-latest"),
197
+ ("gemini", "GEMINI_API_KEY", "gemini-3-flash-preview"),
198
+ ("google", "GOOGLE_API_KEY", "gemini-3-flash-preview"),
199
+ ("mistral", "MISTRAL_API_KEY", "mistral-small-latest"),
200
+ ("cohere", "COHERE_API_KEY", "command-r"),
201
+ ("groq", "GROQ_API_KEY", "groq/llama-3.1-8b-instant"),
202
+ ]
203
+ for provider, env_var, default_model in candidates:
204
+ if os.getenv(env_var):
205
+ return {"provider": provider, "env_var": env_var, "default_model": default_model}
206
+ return None
207
+
208
+ def _llm_api_key_present(self) -> bool:
209
+ return self._detect_llm_provider() is not None
210
+
211
+ def _llm_summary_cache_path(self) -> Path | None:
212
+ if self.csv_directory is None:
213
+ return None
214
+ return self.csv_directory / f"llm_summary_{self.repo}.json".replace("/", "-")
215
+
216
+ def load_llm_summary(self) -> bool:
217
+ """Load cached LLM summary from progress directory if present."""
218
+ cache_path = self._llm_summary_cache_path()
219
+ if cache_path is None or not cache_path.exists():
220
+ return False
221
+ try:
222
+ with open(cache_path, encoding="utf-8") as f:
223
+ payload = json.load(f)
224
+ summary = (payload.get("summary") or "").strip()
225
+ if summary:
226
+ self.llm_summary = summary
227
+ return True
228
+ except Exception as exc:
229
+ logging.warning("Failed to load cached LLM summary: %s", exc)
230
+ return False
231
+
232
+ def save_llm_summary(self) -> None:
233
+ """Persist LLM summary into progress directory if enabled."""
234
+ cache_path = self._llm_summary_cache_path()
235
+ if cache_path is None:
236
+ return
237
+ if not self.llm_summary:
238
+ return
239
+ try:
240
+ self.csv_directory.mkdir(parents=False, exist_ok=True)
241
+ payload = {
242
+ "repo": self.repo,
243
+ "model": self.llm_model_used or self.llm_model,
244
+ "summary": self.llm_summary,
245
+ }
246
+ with open(cache_path, "w", encoding="utf-8") as f:
247
+ json.dump(payload, f, indent=2)
248
+ except Exception as exc:
249
+ if self.debug:
250
+ logging.warning("Failed to save cached LLM summary: %s", exc)
251
+
252
+ def _prepare_llm_summary_payload(self) -> dict:
253
+ """Prepare a compact data payload for the LLM prompt."""
254
+ repos_sorted = sorted(self.all_public_dependent_repos, key=lambda r: r.get("stars", 0), reverse=True)
255
+ repos_top = repos_sorted[: max(0, self.llm_max_repos)]
256
+
257
+ owners = [r.get("owner") for r in self.all_public_dependent_repos if r.get("owner")]
258
+ owners_counter = Counter(owners)
259
+ owners_top = owners_counter.most_common(25)
260
+
261
+ owner_stars: dict[str, int] = {}
262
+ for r in self.all_public_dependent_repos:
263
+ owner = r.get("owner")
264
+ if not owner:
265
+ continue
266
+ owner_stars[owner] = owner_stars.get(owner, 0) + int(r.get("stars", 0) or 0)
267
+ owners_top_by_stars = sorted(owner_stars.items(), key=lambda kv: kv[1], reverse=True)[:25]
268
+
269
+ return {
270
+ "source_repo": self.repo,
271
+ "packages": [p.get("name") for p in self.packages] if self.packages else [self.repo],
272
+ "totals": {
273
+ "dependents_total": self.total_sum,
274
+ "dependents_public": self.total_public_sum,
275
+ "dependents_private": self.total_private_sum,
276
+ "public_dependents_total_stars": self.total_stars_sum,
277
+ },
278
+ "top_dependents_by_stars": [
279
+ {"name": r.get("name"), "stars": int(r.get("stars", 0) or 0)} for r in repos_top
280
+ ],
281
+ "top_owners_by_dependent_count": [{"owner": o, "count": c} for (o, c) in owners_top],
282
+ "top_owners_by_total_stars": [{"owner": o, "stars": s} for (o, s) in owners_top_by_stars],
283
+ }
284
+
285
+ async def maybe_generate_llm_summary(self) -> None:
286
+ """Generate an LLM-based summary if possible; otherwise do nothing."""
287
+ if not self.llm_summary_enabled:
288
+ return
289
+ if self.llm_summary:
290
+ return
291
+
292
+ # Reuse cached summary when resuming from CSV progress
293
+ if self.load_llm_summary():
294
+ return
295
+
296
+ provider_info = self._detect_llm_provider()
297
+ if provider_info is None:
298
+ return
299
+
300
+ # Default model if none was provided
301
+ model = self.llm_model or provider_info.get("default_model") or "gpt-4o-mini"
302
+ self.llm_model_used = model
303
+
304
+ # Add provider prefix if missing (for LiteLLM compatibility)
305
+ if "/" not in model:
306
+ model = f"{provider_info['provider']}/{model}"
307
+
308
+ payload = self._prepare_llm_summary_payload()
309
+ system_prompt = (
310
+ "You summarize GitHub 'Used by' dependents for a package. "
311
+ "Write concise, factual Markdown. Do not invent data. "
312
+ "Use only the provided JSON data. "
313
+ "Do not include headings (no H1/H2/H3/H4). "
314
+ "Add blank lines before bullet points for readability. "
315
+ "Avoid any mention of pagination/navigation words like 'Page', 'Next', or 'Previous'."
316
+ )
317
+ user_prompt = (
318
+ "Given this JSON data, write a short summary that highlights: "
319
+ "(1) popular companies/organizations using the package, "
320
+ "(2) popular tools/ecosystems (infer from repo names), "
321
+ "(3) notable high-star dependent repositories. "
322
+ "Do not repeat data between sections (1), (2), and (3). "
323
+ "Format as Markdown with short sentences and bullet points. "
324
+ "Write in bold the names of companies/organizations and tools/ecosystems. "
325
+ f"Add blank lines before bullet points for readability. Keep under {self.llm_max_words} words.\n\n"
326
+ + json.dumps(payload, ensure_ascii=False)
327
+ )
328
+
329
+ try:
330
+ from litellm import acompletion # type: ignore
331
+
332
+ response = await acompletion(
333
+ model=model,
334
+ messages=[
335
+ {"role": "system", "content": system_prompt},
336
+ {"role": "user", "content": user_prompt},
337
+ ],
338
+ temperature=0.2,
339
+ timeout=self.llm_timeout,
340
+ )
341
+
342
+ content = None
343
+ if hasattr(response, "choices") and response.choices:
344
+ message = response.choices[0].message
345
+ content = getattr(message, "content", None)
346
+ if content:
347
+ self.llm_summary = str(content).strip()
348
+ self.save_llm_summary()
349
+ except Exception as exc:
350
+ self.llm_summary_error = str(exc)
351
+ logging.warning("Failed to generate LLM summary: %s", exc)
352
+ finally:
353
+ # LiteLLM can keep async clients around. Ensure they're closed before
354
+ # asyncio.run() tears down the event loop to avoid:
355
+ # RuntimeWarning: coroutine 'close_litellm_async_clients' was never awaited
356
+ await self._maybe_close_litellm_async_clients()
357
+
358
+ async def _maybe_close_litellm_async_clients(self) -> None:
359
+ """Best-effort cleanup for LiteLLM async clients."""
360
+ try:
361
+ import litellm # type: ignore
362
+
363
+ close_fn = getattr(litellm, "close_litellm_async_clients", None)
364
+ if close_fn is None:
365
+ utils = getattr(litellm, "utils", None)
366
+ close_fn = getattr(utils, "close_litellm_async_clients", None) if utils else None
367
+
368
+ if close_fn is None:
369
+ return
370
+
371
+ if inspect.iscoroutinefunction(close_fn):
372
+ await close_fn()
373
+ return
374
+
375
+ result = close_fn()
376
+ if inspect.isawaitable(result):
377
+ await result
378
+ except Exception as exc:
379
+ if self.debug:
380
+ logging.debug("LiteLLM async client cleanup skipped: %s", exc)
381
+
170
382
  def _extract_owner_repo(self, dependent_row):
171
383
  repo_anchor = dependent_row.find("a", {"data-hovercard-type": "repository"})
172
384
  if repo_anchor is None:
@@ -296,6 +508,8 @@ class GithubDependentsInfo:
296
508
  self.total_stars_sum += (
297
509
  package["public_dependent_stars"] if package["public_dependent_stars"] else 0
298
510
  )
511
+ # Load cached summary if present
512
+ self.load_llm_summary()
299
513
  return len(self.packages) > 0
300
514
 
301
515
  # Build result
@@ -307,6 +521,7 @@ class GithubDependentsInfo:
307
521
  "private_dependents_number": self.total_private_sum,
308
522
  "public_dependents_stars": self.total_stars_sum,
309
523
  "badges": self.badges,
524
+ "llm_summary": self.llm_summary,
310
525
  }
311
526
  if self.merge_packages is False:
312
527
  self.result["packages"] = (self.packages,)
@@ -317,6 +532,10 @@ class GithubDependentsInfo:
317
532
  if self.json_output is True:
318
533
  print(json.dumps(self.result, indent=4))
319
534
  else:
535
+ if self.llm_summary_enabled and self.llm_summary:
536
+ print("LLM Summary:\n")
537
+ print(self.llm_summary)
538
+ print("\n")
320
539
  print("Total: " + str(self.total_sum))
321
540
  print("Public: " + str(self.total_public_sum) + " (" + str(self.total_stars_sum) + " stars)")
322
541
  print("Private: " + str(self.total_private_sum))
@@ -386,6 +605,10 @@ class GithubDependentsInfo:
386
605
  # Summary table
387
606
  self._append_summary_table(md_lines)
388
607
 
608
+ # Optional LLM summary
609
+ if self.llm_summary:
610
+ md_lines += ["## Summary", "", self.llm_summary.strip(), ""]
611
+
389
612
  # Single dependents list
390
613
  if self.merge_packages is True:
391
614
  md_lines += [
@@ -478,6 +701,10 @@ class GithubDependentsInfo:
478
701
  if page_num == 1:
479
702
  self._append_summary_table(md_lines)
480
703
 
704
+ # Optional LLM summary (only on first page)
705
+ if self.llm_summary:
706
+ md_lines += ["## Summary", "", self.llm_summary.strip(), ""]
707
+
481
708
  # Calculate start and end indices for this page
482
709
  start_idx = (page_num - 1) * self.page_size
483
710
  end_idx = start_idx + self.page_size
@@ -5,7 +5,7 @@ build-backend = "poetry.core.masonry.api"
5
5
 
6
6
  [project]
7
7
  name = "github-dependents-info"
8
- version = "2.0.2"
8
+ version = "3.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"
@@ -40,12 +40,14 @@ Repository = "https://github.com/nvuillam/github-dependents-info"
40
40
  python = ">=3.10,<4.0"
41
41
 
42
42
  click = ">=8.3.1,<8.4"
43
- typer = {extras = ["all"], version = ">=0.20,<0.21"}
43
+ typer = {extras = ["standard"], version = ">=0.19,<0.20"}
44
+ typer-slim = ">=0.19,<0.20"
44
45
  rich = ">=14.2,<14.3"
45
46
  beautifulsoup4 = "4.14.3"
46
47
  pandas = ">=2.3.3,<3.0"
47
48
  httpx = "^0.28.1"
48
49
  idna = ">=3.11"
50
+ litellm = ">=1.60.0,<2.0"
49
51
 
50
52
  [tool.poetry.group.dev.dependencies]
51
53
  bandit = "^1.7.5"
@@ -59,7 +61,7 @@ pydocstyle = "^6.3.0"
59
61
  pylint = "^4.0.0"
60
62
  pytest = "^9.0.0"
61
63
  pyupgrade = "^3.4.0"
62
- safety = "^3.0.1"
64
+ safety = "^3.7.0"
63
65
  coverage = "^7.3.4"
64
66
  coverage-badge = "^1.1.0"
65
67
  cryptography = ">=44.0.1"