napistu 0.1.0__py3-none-any.whl → 0.2.4.dev2__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.
Files changed (59) hide show
  1. napistu/__init__.py +1 -1
  2. napistu/consensus.py +1010 -513
  3. napistu/constants.py +24 -0
  4. napistu/gcs/constants.py +2 -2
  5. napistu/gcs/downloads.py +57 -25
  6. napistu/gcs/utils.py +21 -0
  7. napistu/identifiers.py +105 -6
  8. napistu/ingestion/constants.py +0 -1
  9. napistu/ingestion/obo.py +24 -8
  10. napistu/ingestion/psi_mi.py +20 -5
  11. napistu/ingestion/reactome.py +8 -32
  12. napistu/mcp/__init__.py +69 -0
  13. napistu/mcp/__main__.py +180 -0
  14. napistu/mcp/codebase.py +182 -0
  15. napistu/mcp/codebase_utils.py +298 -0
  16. napistu/mcp/constants.py +72 -0
  17. napistu/mcp/documentation.py +166 -0
  18. napistu/mcp/documentation_utils.py +235 -0
  19. napistu/mcp/execution.py +382 -0
  20. napistu/mcp/profiles.py +73 -0
  21. napistu/mcp/server.py +86 -0
  22. napistu/mcp/tutorials.py +124 -0
  23. napistu/mcp/tutorials_utils.py +230 -0
  24. napistu/mcp/utils.py +47 -0
  25. napistu/mechanism_matching.py +782 -26
  26. napistu/modify/constants.py +41 -0
  27. napistu/modify/curation.py +4 -1
  28. napistu/modify/gaps.py +243 -156
  29. napistu/modify/pathwayannot.py +26 -8
  30. napistu/network/neighborhoods.py +16 -7
  31. napistu/network/net_create.py +209 -54
  32. napistu/network/net_propagation.py +118 -0
  33. napistu/network/net_utils.py +1 -32
  34. napistu/rpy2/netcontextr.py +10 -7
  35. napistu/rpy2/rids.py +7 -5
  36. napistu/sbml_dfs_core.py +46 -29
  37. napistu/sbml_dfs_utils.py +37 -1
  38. napistu/source.py +8 -2
  39. napistu/utils.py +67 -8
  40. napistu-0.2.4.dev2.dist-info/METADATA +84 -0
  41. napistu-0.2.4.dev2.dist-info/RECORD +95 -0
  42. {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/WHEEL +1 -1
  43. tests/conftest.py +11 -5
  44. tests/test_consensus.py +4 -1
  45. tests/test_gaps.py +127 -0
  46. tests/test_gcs.py +3 -2
  47. tests/test_igraph.py +14 -0
  48. tests/test_mcp_documentation_utils.py +13 -0
  49. tests/test_mechanism_matching.py +658 -0
  50. tests/test_net_propagation.py +89 -0
  51. tests/test_net_utils.py +83 -0
  52. tests/test_sbml.py +2 -0
  53. tests/{test_sbml_dfs_create.py → test_sbml_dfs_core.py} +68 -4
  54. tests/test_utils.py +81 -0
  55. napistu-0.1.0.dist-info/METADATA +0 -56
  56. napistu-0.1.0.dist-info/RECORD +0 -77
  57. {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/entry_points.txt +0 -0
  58. {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/licenses/LICENSE +0 -0
  59. {napistu-0.1.0.dist-info → napistu-0.2.4.dev2.dist-info}/top_level.txt +0 -0
@@ -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