opencode-bioresearcher 1.6.0

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 (40) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +103 -0
  3. package/agents/bioresearcher-dr-worker.md +54 -0
  4. package/connector-meta.json +23 -0
  5. package/index.js +77 -0
  6. package/loader.js +3 -0
  7. package/package.json +42 -0
  8. package/skill-bundle.json +12 -0
  9. package/skills/bioresearcher-deep-research/SKILL.md +330 -0
  10. package/skills/bioresearcher-deep-research/references/analysis-methods.md +90 -0
  11. package/skills/bioresearcher-deep-research/references/article-literature.md +89 -0
  12. package/skills/bioresearcher-deep-research/references/best-practices.md +102 -0
  13. package/skills/bioresearcher-deep-research/references/citations.md +146 -0
  14. package/skills/bioresearcher-deep-research/references/clinical-trials.md +87 -0
  15. package/skills/bioresearcher-deep-research/references/diseases.md +94 -0
  16. package/skills/bioresearcher-deep-research/references/drugs.md +88 -0
  17. package/skills/bioresearcher-deep-research/references/ensembl-pdb.md +134 -0
  18. package/skills/bioresearcher-deep-research/references/functional-genomics.md +118 -0
  19. package/skills/bioresearcher-deep-research/references/genes.md +93 -0
  20. package/skills/bioresearcher-deep-research/references/optional-analysis.md +108 -0
  21. package/skills/bioresearcher-deep-research/references/patents.md +92 -0
  22. package/skills/bioresearcher-deep-research/references/rate-limiting-auth.md +95 -0
  23. package/skills/bioresearcher-deep-research/references/report-template.md +117 -0
  24. package/skills/bioresearcher-deep-research/references/tool-selection.md +142 -0
  25. package/skills/bioresearcher-deep-research/references/utility-config.md +116 -0
  26. package/skills/bioresearcher-deep-research/references/variants.md +109 -0
  27. package/skills/bioresearcher-deep-research/references/worker-protocol.md +110 -0
  28. package/skills/bioresearcher-deep-research/scripts/markdown-to-html.py +86 -0
  29. package/skills/bioresearcher-plot-making/SKILL.md +97 -0
  30. package/skills/bioresearcher-plot-making/references/literature-search-method-summary.md +163 -0
  31. package/skills/bioresearcher-plot-making/references/qa-gates-and-gotchas.md +156 -0
  32. package/skills/bioresearcher-plot-making/references/structural-biology-binder-visualization.md +206 -0
  33. package/skills/bioresearcher-plot-making/scripts/audit_figure_collisions.py +742 -0
  34. package/skills/bioresearcher-plot-making/scripts/audit_panel_alignment.py +935 -0
  35. package/skills/bioresearcher-plot-making/scripts/audit_pdf_text.py +152 -0
  36. package/skills/bioresearcher-plot-making/scripts/plot_helpers.py +177 -0
  37. package/skills/bioresearcher-pubmed-weekly/SKILL.md +223 -0
  38. package/skills/bioresearcher-pubmed-weekly/scripts/parse_updatefiles.py +272 -0
  39. package/skills/bioresearcher-pubmed-weekly/scripts/pubmed_weekly.py +493 -0
  40. package/skills/bioresearcher-python-setup-uv/SKILL.md +184 -0
@@ -0,0 +1,272 @@
1
+ #!/usr/bin/env python3
2
+ """Streaming parser for PubMed updatefiles XML / XML.gz into one Excel workbook.
3
+
4
+ Reads one or more NCBI PubMed updatefiles (``pubmedNNnNNNN.xml`` or
5
+ ``.xml.gz``) and produces a single ``.xlsx`` workbook:
6
+
7
+ - Sheet ``PubMed Articles``: columns PMID, DOI, Title, Journal, ISSN, PubDate,
8
+ FirstAuthor, LastAuthor, PublicationTypes (exact frozen order).
9
+ - Sheet ``Deleted PMIDs``: single column PMID.
10
+
11
+ Updatefiles interleave ``<PubmedArticle>`` and ``<DeleteCitation>`` records;
12
+ both are handled. Parsing is memory-bounded: ``xml.etree.iterparse`` with
13
+ element clearing plus an ``openpyxl`` write_only workbook. Output ordering is
14
+ deterministic: input file order on the command line, document order within
15
+ each file. Depends only on the Python standard library and openpyxl.
16
+ """
17
+
18
+ import argparse
19
+ import gzip
20
+ import json
21
+ import os
22
+ import sys
23
+ import xml.etree.ElementTree as ET
24
+
25
+ from openpyxl import Workbook
26
+
27
+ ARTICLE_SHEET = "PubMed Articles"
28
+ DELETED_SHEET = "Deleted PMIDs"
29
+
30
+ COLUMNS = [
31
+ "PMID",
32
+ "DOI",
33
+ "Title",
34
+ "Journal",
35
+ "ISSN",
36
+ "PubDate",
37
+ "FirstAuthor",
38
+ "LastAuthor",
39
+ "PublicationTypes",
40
+ ]
41
+
42
+ MONTHS = {
43
+ "jan": 1, "feb": 2, "mar": 3, "apr": 4, "may": 5, "jun": 6,
44
+ "jul": 7, "aug": 8, "sep": 9, "oct": 10, "nov": 11, "dec": 12,
45
+ "january": 1, "february": 2, "march": 3, "april": 4, "june": 6,
46
+ "july": 7, "august": 8, "september": 9, "october": 10,
47
+ "november": 11, "december": 12,
48
+ }
49
+
50
+ GZIP_MAGIC = b"\x1f\x8b"
51
+
52
+
53
+ def open_maybe_gzip(path):
54
+ """Open ``path`` as a gzip stream if it carries the gzip magic bytes."""
55
+ with open(path, "rb") as probe:
56
+ magic = probe.read(2)
57
+ if magic == GZIP_MAGIC:
58
+ return gzip.open(path, "rb")
59
+ return open(path, "rb")
60
+
61
+
62
+ def join_text(elem):
63
+ """Return concatenated (markup-flattened) stripped text of ``elem``."""
64
+ if elem is None:
65
+ return ""
66
+ return "".join(elem.itertext()).strip()
67
+
68
+
69
+ def normalize_month(value):
70
+ """Map a Month element value (name or number) to a 1-12 int, or None."""
71
+ if not value:
72
+ return None
73
+ text = value.strip()
74
+ if text.isdigit():
75
+ number = int(text)
76
+ if 1 <= number <= 12:
77
+ return number
78
+ return None
79
+ return MONTHS.get(text.lower())
80
+
81
+
82
+ def normalize_pubdate(pubdate):
83
+ """Normalize JournalIssue/PubDate to ``YYYY-MM`` or ``YYYY`` (or ``""``).
84
+
85
+ Accepts Year plus Month given as a name (``Jan`` / ``January``) or a
86
+ number (``1`` / ``01``). Day is consumed by PubMed but intentionally not
87
+ emitted. Records carrying only MedlineDate (no Year) yield ``""``.
88
+ """
89
+ if pubdate is None:
90
+ return ""
91
+ year_text = join_text(pubdate.find("Year"))
92
+ if not year_text:
93
+ return ""
94
+ try:
95
+ year = f"{int(year_text):04d}"
96
+ except ValueError:
97
+ return ""
98
+ month = normalize_month(join_text(pubdate.find("Month")))
99
+ if month is not None:
100
+ return f"{year}-{month:02d}"
101
+ return year
102
+
103
+
104
+ def format_author(author):
105
+ """Format an Author element as ``LastName Initials`` (LastInitials)."""
106
+ if author is None:
107
+ return ""
108
+ last = join_text(author.find("LastName"))
109
+ if not last:
110
+ return join_text(author.find("CollectiveName"))
111
+ initials = join_text(author.find("Initials"))
112
+ if initials:
113
+ return f"{last} {initials}"
114
+ return last
115
+
116
+
117
+ def parse_article(elem):
118
+ """Extract one row (dict keyed by COLUMNS) from a PubmedArticle element."""
119
+ article = elem.find("./MedlineCitation/Article")
120
+ journal = article.find("Journal") if article is not None else None
121
+ authors = (
122
+ article.findall("./AuthorList/Author") if article is not None else []
123
+ )
124
+ pub_types = (
125
+ [
126
+ join_text(pt)
127
+ for pt in article.findall("./PublicationTypeList/PublicationType")
128
+ ]
129
+ if article is not None
130
+ else []
131
+ )
132
+ doi = elem.find("./PubmedData/ArticleIdList/ArticleId[@IdType='doi']")
133
+ pub_date_elem = (
134
+ journal.find("./JournalIssue/PubDate") if journal is not None else None
135
+ )
136
+ return {
137
+ "PMID": join_text(elem.find("./MedlineCitation/PMID")),
138
+ "DOI": join_text(doi),
139
+ "Title": join_text(article.find("ArticleTitle"))
140
+ if article is not None
141
+ else "",
142
+ "Journal": join_text(journal.find("Title"))
143
+ if journal is not None
144
+ else "",
145
+ "ISSN": join_text(journal.find("ISSN")) if journal is not None else "",
146
+ "PubDate": normalize_pubdate(pub_date_elem),
147
+ "FirstAuthor": format_author(authors[0]) if authors else "",
148
+ "LastAuthor": format_author(authors[-1]) if authors else "",
149
+ "PublicationTypes": ";".join(pt for pt in pub_types if pt),
150
+ }
151
+
152
+
153
+ def iter_records(stream):
154
+ """Yield (tag, element) for top-level records, clearing processed memory.
155
+
156
+ Uses ElementTree iterparse; after each yielded record is consumed, the
157
+ element is cleared and detached from the root so memory stays bounded.
158
+ """
159
+ root = None
160
+ for event, elem in ET.iterparse(stream, events=("start", "end")):
161
+ if event == "start":
162
+ if root is None:
163
+ root = elem
164
+ continue
165
+ if elem.tag in ("PubmedArticle", "DeleteCitation"):
166
+ yield elem.tag, elem
167
+ elem.clear()
168
+ if root is not None:
169
+ try:
170
+ root.remove(elem)
171
+ except ValueError:
172
+ pass
173
+
174
+
175
+ def parse_files(files, output, summary_json=None):
176
+ """Parse updatefiles into one workbook; return and optionally dump summary.
177
+
178
+ Args:
179
+ files: Input ``.xml`` / ``.xml.gz`` paths (order preserved in output).
180
+ output: Destination ``.xlsx`` path.
181
+ summary_json: Optional path for the JSON summary.
182
+
183
+ Returns:
184
+ Dict: {source_files, article_count, deleted_pmids, first_rows}.
185
+ """
186
+ rows = []
187
+ deleted_pmids = []
188
+
189
+ for path in files:
190
+ with open_maybe_gzip(path) as stream:
191
+ for tag, elem in iter_records(stream):
192
+ if tag == "PubmedArticle":
193
+ rows.append(parse_article(elem))
194
+ else:
195
+ deleted_pmids.extend(
196
+ join_text(p) for p in elem.findall("./PMID")
197
+ )
198
+
199
+ workbook = Workbook(write_only=True)
200
+
201
+ articles_sheet = workbook.create_sheet(ARTICLE_SHEET)
202
+ articles_sheet.append(COLUMNS)
203
+ for row in rows:
204
+ articles_sheet.append([row[column] for column in COLUMNS])
205
+
206
+ deleted_sheet = workbook.create_sheet(DELETED_SHEET)
207
+ deleted_sheet.append(["PMID"])
208
+ for pmid in deleted_pmids:
209
+ deleted_sheet.append([pmid])
210
+
211
+ workbook.save(output)
212
+
213
+ summary = {
214
+ "source_files": [os.path.basename(path) for path in files],
215
+ "article_count": len(rows),
216
+ "deleted_pmids": deleted_pmids,
217
+ "first_rows": rows[:3],
218
+ }
219
+ if summary_json:
220
+ with open(summary_json, "w", encoding="utf-8") as handle:
221
+ json.dump(summary, handle, ensure_ascii=False, indent=2)
222
+ return summary
223
+
224
+
225
+ def main(argv=None):
226
+ """Command-line entry point."""
227
+ parser = argparse.ArgumentParser(
228
+ description=(
229
+ "Streaming parser for PubMed updatefiles XML(.gz) into one "
230
+ "Excel workbook (PubMed Articles + Deleted PMIDs sheets)."
231
+ )
232
+ )
233
+ parser.add_argument(
234
+ "files",
235
+ nargs="+",
236
+ help="Input PubMed updatefiles (.xml or .xml.gz), order preserved",
237
+ )
238
+ parser.add_argument(
239
+ "-o",
240
+ "--output",
241
+ required=True,
242
+ help="Output .xlsx path (e.g. combined.xlsx)",
243
+ )
244
+ parser.add_argument(
245
+ "--summary-json",
246
+ dest="summary_json",
247
+ default=None,
248
+ help="Optional path to write the JSON summary",
249
+ )
250
+ args = parser.parse_args(argv)
251
+
252
+ try:
253
+ summary = parse_files(args.files, args.output, args.summary_json)
254
+ except FileNotFoundError as exc:
255
+ print(f"Error: input file not found: {exc}", file=sys.stderr)
256
+ return 1
257
+
258
+ print(
259
+ "Parsed {} article(s) and {} deleted PMID(s) from {} file(s)".format(
260
+ summary["article_count"],
261
+ len(summary["deleted_pmids"]),
262
+ len(summary["source_files"]),
263
+ )
264
+ )
265
+ print(f"Output: {args.output}")
266
+ if args.summary_json:
267
+ print(f"Summary: {args.summary_json}")
268
+ return 0
269
+
270
+
271
+ if __name__ == "__main__":
272
+ sys.exit(main())