napistu 0.1.0__py3-none-any.whl → 0.2.4.dev3__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.
- napistu/__init__.py +1 -1
- napistu/consensus.py +1010 -513
- napistu/constants.py +24 -0
- napistu/gcs/constants.py +2 -2
- napistu/gcs/downloads.py +57 -25
- napistu/gcs/utils.py +21 -0
- napistu/identifiers.py +105 -6
- napistu/ingestion/constants.py +0 -1
- napistu/ingestion/obo.py +24 -8
- napistu/ingestion/psi_mi.py +20 -5
- napistu/ingestion/reactome.py +8 -32
- napistu/mcp/__init__.py +69 -0
- napistu/mcp/__main__.py +180 -0
- napistu/mcp/codebase.py +182 -0
- napistu/mcp/codebase_utils.py +298 -0
- napistu/mcp/constants.py +72 -0
- napistu/mcp/documentation.py +166 -0
- napistu/mcp/documentation_utils.py +235 -0
- napistu/mcp/execution.py +382 -0
- napistu/mcp/profiles.py +73 -0
- napistu/mcp/server.py +86 -0
- napistu/mcp/tutorials.py +124 -0
- napistu/mcp/tutorials_utils.py +230 -0
- napistu/mcp/utils.py +47 -0
- napistu/mechanism_matching.py +782 -26
- napistu/modify/constants.py +41 -0
- napistu/modify/curation.py +4 -1
- napistu/modify/gaps.py +243 -156
- napistu/modify/pathwayannot.py +26 -8
- napistu/network/neighborhoods.py +16 -7
- napistu/network/net_create.py +209 -54
- napistu/network/net_propagation.py +118 -0
- napistu/network/net_utils.py +1 -32
- napistu/rpy2/netcontextr.py +10 -7
- napistu/rpy2/rids.py +7 -5
- napistu/sbml_dfs_core.py +46 -29
- napistu/sbml_dfs_utils.py +37 -1
- napistu/source.py +8 -2
- napistu/utils.py +67 -8
- napistu-0.2.4.dev3.dist-info/METADATA +84 -0
- napistu-0.2.4.dev3.dist-info/RECORD +95 -0
- {napistu-0.1.0.dist-info → napistu-0.2.4.dev3.dist-info}/WHEEL +1 -1
- tests/conftest.py +11 -5
- tests/test_consensus.py +4 -1
- tests/test_gaps.py +127 -0
- tests/test_gcs.py +3 -2
- tests/test_igraph.py +14 -0
- tests/test_mcp_documentation_utils.py +13 -0
- tests/test_mechanism_matching.py +658 -0
- tests/test_net_propagation.py +89 -0
- tests/test_net_utils.py +83 -0
- tests/test_sbml.py +2 -0
- tests/{test_sbml_dfs_create.py → test_sbml_dfs_core.py} +68 -4
- tests/test_utils.py +81 -0
- napistu-0.1.0.dist-info/METADATA +0 -56
- napistu-0.1.0.dist-info/RECORD +0 -77
- {napistu-0.1.0.dist-info → napistu-0.2.4.dev3.dist-info}/entry_points.txt +0 -0
- {napistu-0.1.0.dist-info → napistu-0.2.4.dev3.dist-info}/licenses/LICENSE +0 -0
- {napistu-0.1.0.dist-info → napistu-0.2.4.dev3.dist-info}/top_level.txt +0 -0
napistu/mcp/constants.py
ADDED
@@ -0,0 +1,72 @@
|
|
1
|
+
import os
|
2
|
+
from types import SimpleNamespace
|
3
|
+
|
4
|
+
from napistu.constants import PACKAGE_DEFS
|
5
|
+
|
6
|
+
MCP_COMPONENTS = SimpleNamespace(
|
7
|
+
CODEBASE="codebase",
|
8
|
+
DOCUMENTATION="documentation",
|
9
|
+
EXECUTION="execution",
|
10
|
+
TUTORIALS="tutorials",
|
11
|
+
)
|
12
|
+
|
13
|
+
DOCUMENTATION = SimpleNamespace(
|
14
|
+
README="readme",
|
15
|
+
WIKI="wiki",
|
16
|
+
ISSUES="issues",
|
17
|
+
PRS="prs",
|
18
|
+
PACKAGEDOWN="packagedown",
|
19
|
+
)
|
20
|
+
|
21
|
+
EXECUTION = SimpleNamespace(
|
22
|
+
NOTEBOOKS="notebooks",
|
23
|
+
)
|
24
|
+
|
25
|
+
TUTORIALS = SimpleNamespace(
|
26
|
+
TUTORIALS="tutorials",
|
27
|
+
)
|
28
|
+
|
29
|
+
TOOL_VARS = SimpleNamespace(
|
30
|
+
NAME="name",
|
31
|
+
SNIPPET="snippet",
|
32
|
+
)
|
33
|
+
|
34
|
+
READMES = {
|
35
|
+
"napistu": "https://raw.githubusercontent.com/napistu/napistu/main/README.md",
|
36
|
+
"napistu-py": "https://raw.githubusercontent.com/napistu/napistu-py/main/README.md",
|
37
|
+
"napistu-r": "https://raw.githubusercontent.com/napistu/napistu-r/main/README.md",
|
38
|
+
"napistu/tutorials": "https://raw.githubusercontent.com/napistu/napistu/main/tutorials/README.md",
|
39
|
+
}
|
40
|
+
|
41
|
+
WIKI_ROOT = "https://raw.githubusercontent.com/napistu/napistu/main/docs/wiki"
|
42
|
+
|
43
|
+
NAPISTU_PY_READTHEDOCS = "https://napistu.readthedocs.io/en/latest"
|
44
|
+
NAPISTU_PY_READTHEDOCS_API = NAPISTU_PY_READTHEDOCS + "/api.html"
|
45
|
+
READTHEDOCS_TOC_CSS_SELECTOR = "td"
|
46
|
+
|
47
|
+
DEFAULT_GITHUB_API = "https://api.github.com"
|
48
|
+
|
49
|
+
REPOS_WITH_ISSUES = [
|
50
|
+
PACKAGE_DEFS.GITHUB_PROJECT_REPO,
|
51
|
+
PACKAGE_DEFS.GITHUB_NAPISTU_PY,
|
52
|
+
PACKAGE_DEFS.GITHUB_NAPISTU_R,
|
53
|
+
]
|
54
|
+
|
55
|
+
GITHUB_ISSUES_INDEXED = "all"
|
56
|
+
GITHUB_PRS_INDEXED = "all"
|
57
|
+
|
58
|
+
REPOS_WITH_WIKI = [PACKAGE_DEFS.GITHUB_PROJECT_REPO]
|
59
|
+
|
60
|
+
# Example mapping: tutorial_id -> raw GitHub URL
|
61
|
+
TUTORIAL_URLS = {
|
62
|
+
"adding_data_to_graphs": "https://raw.githubusercontent.com/napistu/napistu/refs/heads/main/tutorials/adding_data_to_graphs.ipynb",
|
63
|
+
"downloading_pathway_data": "https://raw.githubusercontent.com/napistu/napistu/refs/heads/main/tutorials/downloading_pathway_data.ipynb",
|
64
|
+
"formatting_sbml_dfs_as_cpr_graphs": "https://raw.githubusercontent.com/napistu/napistu/refs/heads/main/tutorials/formatting_sbml_dfs_as_cpr_graphs.ipynb",
|
65
|
+
"merging_models_into_a_consensus": "https://raw.githubusercontent.com/napistu/napistu/refs/heads/main/tutorials/merging_models_into_a_consensus.ipynb",
|
66
|
+
"r_based_network_visualization": "https://raw.githubusercontent.com/napistu/napistu/refs/heads/main/tutorials/r_based_network_visualization.ipynb",
|
67
|
+
"suggesting_mechanisms_with_networks": "https://raw.githubusercontent.com/napistu/napistu/refs/heads/main/tutorials/suggesting_mechanisms_with_networks.ipynb",
|
68
|
+
"understanding_sbml_dfs": "https://raw.githubusercontent.com/napistu/napistu/refs/heads/main/tutorials/understanding_sbml_dfs.ipynb",
|
69
|
+
"working_with_genome_scale_networks": "https://raw.githubusercontent.com/napistu/napistu/refs/heads/main/tutorials/working_with_genome_scale_networks.ipynb",
|
70
|
+
}
|
71
|
+
|
72
|
+
TUTORIALS_CACHE_DIR = os.path.join(PACKAGE_DEFS.CACHE_DIR, TUTORIALS.TUTORIALS)
|
@@ -0,0 +1,166 @@
|
|
1
|
+
"""
|
2
|
+
Documentation components for the Napistu MCP server.
|
3
|
+
"""
|
4
|
+
|
5
|
+
from fastmcp import FastMCP
|
6
|
+
|
7
|
+
from napistu.mcp import documentation_utils
|
8
|
+
from napistu.mcp import utils as mcp_utils
|
9
|
+
from napistu.mcp.constants import DOCUMENTATION, READMES, REPOS_WITH_ISSUES
|
10
|
+
|
11
|
+
# Global cache for documentation content
|
12
|
+
_docs_cache = {
|
13
|
+
DOCUMENTATION.README: {},
|
14
|
+
DOCUMENTATION.WIKI: {},
|
15
|
+
DOCUMENTATION.ISSUES: {},
|
16
|
+
DOCUMENTATION.PRS: {},
|
17
|
+
DOCUMENTATION.PACKAGEDOWN: {},
|
18
|
+
}
|
19
|
+
|
20
|
+
|
21
|
+
async def initialize_components() -> bool:
|
22
|
+
"""
|
23
|
+
Initialize documentation components.
|
24
|
+
Loads documentation content and should be called before the server starts handling requests.
|
25
|
+
|
26
|
+
Returns
|
27
|
+
-------
|
28
|
+
bool
|
29
|
+
True if initialization is successful.
|
30
|
+
"""
|
31
|
+
global _docs_cache
|
32
|
+
# Load documentation from the READMES dict
|
33
|
+
for name, url in READMES.items():
|
34
|
+
_docs_cache[DOCUMENTATION.README][name] = (
|
35
|
+
await documentation_utils.load_readme_content(url)
|
36
|
+
)
|
37
|
+
# Load issue and PR summaries with the GitHub API
|
38
|
+
for repo in REPOS_WITH_ISSUES:
|
39
|
+
_docs_cache[DOCUMENTATION.ISSUES][repo] = await documentation_utils.list_issues(
|
40
|
+
repo
|
41
|
+
)
|
42
|
+
_docs_cache[DOCUMENTATION.PRS][repo] = (
|
43
|
+
await documentation_utils.list_pull_requests(repo)
|
44
|
+
)
|
45
|
+
return True
|
46
|
+
|
47
|
+
|
48
|
+
def register_components(mcp: FastMCP):
|
49
|
+
"""
|
50
|
+
Register documentation components with the MCP server.
|
51
|
+
|
52
|
+
Args:
|
53
|
+
mcp: FastMCP server instance
|
54
|
+
"""
|
55
|
+
|
56
|
+
# Register resources
|
57
|
+
@mcp.resource("napistu://documentation/summary")
|
58
|
+
async def get_documentation_summary():
|
59
|
+
"""
|
60
|
+
Get a summary of all available documentation.
|
61
|
+
"""
|
62
|
+
return {
|
63
|
+
"readme_files": list(_docs_cache[DOCUMENTATION.README].keys()),
|
64
|
+
"issues": list(_docs_cache[DOCUMENTATION.ISSUES].keys()),
|
65
|
+
"prs": list(_docs_cache[DOCUMENTATION.PRS].keys()),
|
66
|
+
"wiki_pages": list(_docs_cache[DOCUMENTATION.WIKI].keys()),
|
67
|
+
"packagedown_sections": list(_docs_cache[DOCUMENTATION.PACKAGEDOWN].keys()),
|
68
|
+
}
|
69
|
+
|
70
|
+
@mcp.resource("napistu://documentation/readme/{file_name}")
|
71
|
+
async def get_readme_content(file_name: str):
|
72
|
+
"""
|
73
|
+
Get the content of a specific README file.
|
74
|
+
|
75
|
+
Args:
|
76
|
+
file_name: Name of the README file
|
77
|
+
"""
|
78
|
+
if file_name not in _docs_cache[DOCUMENTATION.README]:
|
79
|
+
return {"error": f"README file {file_name} not found"}
|
80
|
+
|
81
|
+
return {
|
82
|
+
"content": _docs_cache[DOCUMENTATION.README][file_name],
|
83
|
+
"format": "markdown",
|
84
|
+
}
|
85
|
+
|
86
|
+
@mcp.resource("napistu://documentation/issues/{repo}")
|
87
|
+
async def get_issues(repo: str):
|
88
|
+
"""
|
89
|
+
Get the list of issues for a given repository.
|
90
|
+
"""
|
91
|
+
return _docs_cache[DOCUMENTATION.ISSUES].get(repo, [])
|
92
|
+
|
93
|
+
@mcp.resource("napistu://documentation/prs/{repo}")
|
94
|
+
async def get_prs(repo: str):
|
95
|
+
"""
|
96
|
+
Get the list of pull requests for a given repository.
|
97
|
+
"""
|
98
|
+
return _docs_cache[DOCUMENTATION.PRS].get(repo, [])
|
99
|
+
|
100
|
+
@mcp.resource("napistu://documentation/issue/{repo}/{number}")
|
101
|
+
async def get_issue_resource(repo: str, number: int):
|
102
|
+
"""
|
103
|
+
Get a single issue by number for a given repository.
|
104
|
+
"""
|
105
|
+
# Try cache first
|
106
|
+
cached = next(
|
107
|
+
(
|
108
|
+
i
|
109
|
+
for i in _docs_cache[DOCUMENTATION.ISSUES].get(repo, [])
|
110
|
+
if i["number"] == number
|
111
|
+
),
|
112
|
+
None,
|
113
|
+
)
|
114
|
+
if cached:
|
115
|
+
return cached
|
116
|
+
# Fallback to live fetch
|
117
|
+
return await documentation_utils.get_issue(repo, number)
|
118
|
+
|
119
|
+
@mcp.resource("napistu://documentation/pr/{repo}/{number}")
|
120
|
+
async def get_pr_resource(repo: str, number: int):
|
121
|
+
"""
|
122
|
+
Get a single pull request by number for a given repository.
|
123
|
+
"""
|
124
|
+
# Try cache first
|
125
|
+
cached = next(
|
126
|
+
(
|
127
|
+
pr
|
128
|
+
for pr in _docs_cache[DOCUMENTATION.PRS].get(repo, [])
|
129
|
+
if pr["number"] == number
|
130
|
+
),
|
131
|
+
None,
|
132
|
+
)
|
133
|
+
if cached:
|
134
|
+
return cached
|
135
|
+
# Fallback to live fetch
|
136
|
+
return await documentation_utils.get_issue(repo, number)
|
137
|
+
|
138
|
+
# Register tools
|
139
|
+
@mcp.tool("search_documentation")
|
140
|
+
async def search_documentation(query: str):
|
141
|
+
"""
|
142
|
+
Search all documentation for a specific query.
|
143
|
+
|
144
|
+
Args:
|
145
|
+
query: Search term
|
146
|
+
|
147
|
+
Returns:
|
148
|
+
Dictionary with search results organized by documentation type
|
149
|
+
"""
|
150
|
+
results = {
|
151
|
+
DOCUMENTATION.README: [],
|
152
|
+
DOCUMENTATION.WIKI: [],
|
153
|
+
DOCUMENTATION.ISSUES: [],
|
154
|
+
DOCUMENTATION.PRS: [],
|
155
|
+
DOCUMENTATION.PACKAGEDOWN: [],
|
156
|
+
}
|
157
|
+
# Simple text search
|
158
|
+
for readme_name, content in _docs_cache[DOCUMENTATION.README].items():
|
159
|
+
if query.lower() in content.lower():
|
160
|
+
results[DOCUMENTATION.README].append(
|
161
|
+
{
|
162
|
+
"name": readme_name,
|
163
|
+
"snippet": mcp_utils.get_snippet(content, query),
|
164
|
+
}
|
165
|
+
)
|
166
|
+
return results
|
@@ -0,0 +1,235 @@
|
|
1
|
+
"""
|
2
|
+
Utilities for loading and processing documentation.
|
3
|
+
"""
|
4
|
+
|
5
|
+
import base64
|
6
|
+
import httpx
|
7
|
+
import os
|
8
|
+
from napistu.constants import PACKAGE_DEFS
|
9
|
+
from napistu.mcp.constants import (
|
10
|
+
DEFAULT_GITHUB_API,
|
11
|
+
GITHUB_ISSUES_INDEXED,
|
12
|
+
GITHUB_PRS_INDEXED,
|
13
|
+
)
|
14
|
+
|
15
|
+
|
16
|
+
async def load_readme_content(readme_url: str) -> str:
|
17
|
+
if readme_url.startswith("http://") or readme_url.startswith("https://"):
|
18
|
+
async with httpx.AsyncClient() as client:
|
19
|
+
response = await client.get(readme_url)
|
20
|
+
response.raise_for_status()
|
21
|
+
return response.text
|
22
|
+
else:
|
23
|
+
raise ValueError(
|
24
|
+
f"Only HTTP(S) URLs are supported for documentation paths: {readme_url}"
|
25
|
+
)
|
26
|
+
|
27
|
+
|
28
|
+
async def list_wiki_pages(
|
29
|
+
repo: str,
|
30
|
+
owner: str = PACKAGE_DEFS.GITHUB_OWNER,
|
31
|
+
github_api: str = DEFAULT_GITHUB_API,
|
32
|
+
) -> list:
|
33
|
+
"""
|
34
|
+
List all Markdown (.md) page names in a GitHub wiki using the GitHub API.
|
35
|
+
|
36
|
+
Parameters
|
37
|
+
----------
|
38
|
+
repo : str, optional
|
39
|
+
The repository name.
|
40
|
+
owner : str, optional
|
41
|
+
The GitHub username or organization (default is 'napistu').
|
42
|
+
github_api : str, optional
|
43
|
+
The GitHub API base URL (default is 'https://api.github.com').
|
44
|
+
|
45
|
+
Returns
|
46
|
+
-------
|
47
|
+
list of str
|
48
|
+
List of page names (e.g., ['Home.md', 'Some-Page.md'])
|
49
|
+
"""
|
50
|
+
wiki_repo = f"{repo}.wiki"
|
51
|
+
url = f"{github_api}/repos/{owner}/{wiki_repo}/contents/"
|
52
|
+
async with httpx.AsyncClient(headers=_get_github_headers()) as client:
|
53
|
+
resp = await client.get(url)
|
54
|
+
resp.raise_for_status()
|
55
|
+
return [item["name"] for item in resp.json() if item["name"].endswith(".md")]
|
56
|
+
|
57
|
+
|
58
|
+
async def fetch_wiki_page(
|
59
|
+
page_name: str,
|
60
|
+
repo: str,
|
61
|
+
owner: str = PACKAGE_DEFS.GITHUB_OWNER,
|
62
|
+
github_api: str = DEFAULT_GITHUB_API,
|
63
|
+
) -> str:
|
64
|
+
"""
|
65
|
+
Fetch the decoded Markdown content of a given wiki page from a GitHub wiki.
|
66
|
+
|
67
|
+
Parameters
|
68
|
+
----------
|
69
|
+
page_name : str
|
70
|
+
The name of the page file (e.g., 'Home.md').
|
71
|
+
repo : str, optional
|
72
|
+
The repository name.
|
73
|
+
owner : str, optional
|
74
|
+
The GitHub username or organization (default is 'napistu').
|
75
|
+
github_api : str, optional
|
76
|
+
The GitHub API base URL (default is 'https://api.github.com').
|
77
|
+
|
78
|
+
Returns
|
79
|
+
-------
|
80
|
+
str
|
81
|
+
The Markdown content as a string.
|
82
|
+
"""
|
83
|
+
wiki_repo = f"{repo}.wiki"
|
84
|
+
url = f"{github_api}/repos/{owner}/{wiki_repo}/contents/{page_name}"
|
85
|
+
async with httpx.AsyncClient(headers=_get_github_headers()) as client:
|
86
|
+
resp = await client.get(url)
|
87
|
+
resp.raise_for_status()
|
88
|
+
data = resp.json()
|
89
|
+
content = base64.b64decode(data["content"]).decode("utf-8")
|
90
|
+
return content
|
91
|
+
|
92
|
+
|
93
|
+
async def list_issues(
|
94
|
+
repo: str,
|
95
|
+
owner: str = PACKAGE_DEFS.GITHUB_OWNER,
|
96
|
+
github_api: str = DEFAULT_GITHUB_API,
|
97
|
+
state: str = GITHUB_ISSUES_INDEXED,
|
98
|
+
include_prs: bool = False,
|
99
|
+
) -> list:
|
100
|
+
"""
|
101
|
+
List issues (and optionally PRs) for a given GitHub repository using the GitHub API.
|
102
|
+
|
103
|
+
Parameters
|
104
|
+
----------
|
105
|
+
repo : str, optional
|
106
|
+
The repository name.
|
107
|
+
owner : str, optional
|
108
|
+
The GitHub username or organization (default is 'napistu').
|
109
|
+
github_api : str, optional
|
110
|
+
The GitHub API base URL (default is 'https://api.github.com').
|
111
|
+
state : str, optional
|
112
|
+
The state of the issues to return. Can be 'open', 'closed', or 'all'. Default is 'open'.
|
113
|
+
include_prs : bool, optional
|
114
|
+
If True, include pull requests in the results. Default is False.
|
115
|
+
|
116
|
+
Returns
|
117
|
+
-------
|
118
|
+
list of dict
|
119
|
+
Each dict contains: number, title, state, url, and a truncated body (max 500 chars).
|
120
|
+
"""
|
121
|
+
url = f"{github_api}/repos/{owner}/{repo}/issues?state={state}"
|
122
|
+
filter_func = (
|
123
|
+
(lambda item: True)
|
124
|
+
if include_prs
|
125
|
+
else (lambda item: "pull_request" not in item)
|
126
|
+
)
|
127
|
+
return await _fetch_github_items(url, filter_func=filter_func)
|
128
|
+
|
129
|
+
|
130
|
+
async def list_pull_requests(
|
131
|
+
repo: str,
|
132
|
+
owner: str = PACKAGE_DEFS.GITHUB_OWNER,
|
133
|
+
github_api: str = DEFAULT_GITHUB_API,
|
134
|
+
state: str = GITHUB_PRS_INDEXED,
|
135
|
+
) -> list:
|
136
|
+
"""
|
137
|
+
List pull requests for a given GitHub repository using the GitHub API.
|
138
|
+
|
139
|
+
Parameters
|
140
|
+
----------
|
141
|
+
repo : str, optional
|
142
|
+
The repository name.
|
143
|
+
owner : str, optional
|
144
|
+
The GitHub username or organization (default is 'napistu').
|
145
|
+
github_api : str, optional
|
146
|
+
The GitHub API base URL (default is 'https://api.github.com').
|
147
|
+
state : str, optional
|
148
|
+
The state of the PRs to return. Can be 'open', 'closed', or 'all'. Default is 'open'.
|
149
|
+
|
150
|
+
Returns
|
151
|
+
-------
|
152
|
+
list of dict
|
153
|
+
Each dict contains: number, title, state, url, and a truncated body (max 500 chars).
|
154
|
+
"""
|
155
|
+
url = f"{github_api}/repos/{owner}/{repo}/pulls?state={state}"
|
156
|
+
return await _fetch_github_items(url)
|
157
|
+
|
158
|
+
|
159
|
+
async def get_issue(
|
160
|
+
repo: str,
|
161
|
+
number: int,
|
162
|
+
owner: str = PACKAGE_DEFS.GITHUB_OWNER,
|
163
|
+
github_api: str = DEFAULT_GITHUB_API,
|
164
|
+
) -> dict:
|
165
|
+
"""
|
166
|
+
Get a single issue (or PR) by number from a GitHub repository.
|
167
|
+
|
168
|
+
Parameters
|
169
|
+
----------
|
170
|
+
repo : str
|
171
|
+
The repository name.
|
172
|
+
number : int
|
173
|
+
The issue or PR number.
|
174
|
+
owner : str, optional
|
175
|
+
The GitHub username or organization (default is 'napistu').
|
176
|
+
github_api : str, optional
|
177
|
+
The GitHub API base URL (default is 'https://api.github.com').
|
178
|
+
|
179
|
+
Returns
|
180
|
+
-------
|
181
|
+
dict
|
182
|
+
The issue or PR details as a dictionary.
|
183
|
+
"""
|
184
|
+
url = f"{github_api}/repos/{owner}/{repo}/issues/{number}"
|
185
|
+
async with httpx.AsyncClient(headers=_get_github_headers()) as client:
|
186
|
+
resp = await client.get(url)
|
187
|
+
resp.raise_for_status()
|
188
|
+
item = resp.json()
|
189
|
+
return _format_github_issue(item)
|
190
|
+
|
191
|
+
|
192
|
+
def _get_github_headers():
|
193
|
+
"""
|
194
|
+
Return headers for GitHub API requests, including Authorization if GITHUB_TOKEN is set.
|
195
|
+
"""
|
196
|
+
headers = {}
|
197
|
+
|
198
|
+
GITHUB_TOKEN = os.getenv("GITHUB_TOKEN")
|
199
|
+
if GITHUB_TOKEN:
|
200
|
+
print("Using token from environment variable GITHUB_TOKEN")
|
201
|
+
headers["Authorization"] = f"token {GITHUB_TOKEN}"
|
202
|
+
return headers
|
203
|
+
|
204
|
+
|
205
|
+
def _format_github_issue(item):
|
206
|
+
"""
|
207
|
+
Format a GitHub issue or PR item into a standard dict.
|
208
|
+
"""
|
209
|
+
return {
|
210
|
+
"number": item["number"],
|
211
|
+
"title": item["title"],
|
212
|
+
"state": item["state"],
|
213
|
+
"url": item["html_url"],
|
214
|
+
"body": (
|
215
|
+
(item["body"][:500] + "...")
|
216
|
+
if item.get("body") and len(item["body"]) > 500
|
217
|
+
else item.get("body")
|
218
|
+
),
|
219
|
+
"is_pr": "pull_request" in item or "merged_at" in item,
|
220
|
+
}
|
221
|
+
|
222
|
+
|
223
|
+
async def _fetch_github_items(url, filter_func=None):
|
224
|
+
"""
|
225
|
+
Fetch and format a list of GitHub issues or PRs from a given API endpoint.
|
226
|
+
"""
|
227
|
+
async with httpx.AsyncClient(headers=_get_github_headers()) as client:
|
228
|
+
resp = await client.get(url)
|
229
|
+
resp.raise_for_status()
|
230
|
+
items = []
|
231
|
+
for item in resp.json():
|
232
|
+
if filter_func and not filter_func(item):
|
233
|
+
continue
|
234
|
+
items.append(_format_github_issue(item))
|
235
|
+
return items
|