adsfolio 0.5.1__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 (97) hide show
  1. adsfolio/__init__.py +16 -0
  2. adsfolio/api/__init__.py +9 -0
  3. adsfolio/api/export.py +293 -0
  4. adsfolio/api/journals.py +138 -0
  5. adsfolio/api/libraries.py +505 -0
  6. adsfolio/api/metrics.py +151 -0
  7. adsfolio/api/search.py +324 -0
  8. adsfolio/bibtex/__init__.py +22 -0
  9. adsfolio/bibtex/formatter.py +105 -0
  10. adsfolio/bibtex/io.py +118 -0
  11. adsfolio/bibtex/parser.py +287 -0
  12. adsfolio/bibtex/sanitize.py +188 -0
  13. adsfolio/citations.py +579 -0
  14. adsfolio/cli/__init__.py +12 -0
  15. adsfolio/cli/__main__.py +5 -0
  16. adsfolio/cli/commands.py +926 -0
  17. adsfolio/client.py +762 -0
  18. adsfolio/config.py +478 -0
  19. adsfolio/cv/__init__.py +70 -0
  20. adsfolio/cv/cli.py +303 -0
  21. adsfolio/cv/config.py +442 -0
  22. adsfolio/cv/config_starter.py +108 -0
  23. adsfolio/cv/query.py +103 -0
  24. adsfolio/cv/render_factory.py +43 -0
  25. adsfolio/cv/render_legend.py +64 -0
  26. adsfolio/cv/renderer.py +396 -0
  27. adsfolio/cv/template.py +372 -0
  28. adsfolio/data_migration.py +119 -0
  29. adsfolio/formatting.py +396 -0
  30. adsfolio/gui/__init__.py +1 -0
  31. adsfolio/gui/about.py +200 -0
  32. adsfolio/gui/ads_library_dialog.py +681 -0
  33. adsfolio/gui/ads_library_export_dialog.py +382 -0
  34. adsfolio/gui/ads_library_jobs.py +269 -0
  35. adsfolio/gui/annotation_merge.py +529 -0
  36. adsfolio/gui/app.py +55 -0
  37. adsfolio/gui/appearance.py +626 -0
  38. adsfolio/gui/assets/adsfolio.icns +0 -0
  39. adsfolio/gui/assets/adsfolio.ico +0 -0
  40. adsfolio/gui/assets/adsfolio.png +0 -0
  41. adsfolio/gui/assets/adsfolio.svg +108 -0
  42. adsfolio/gui/citation_styles.py +637 -0
  43. adsfolio/gui/config.py +483 -0
  44. adsfolio/gui/cv_dialog.py +1160 -0
  45. adsfolio/gui/detail_pane.py +1660 -0
  46. adsfolio/gui/dialogs.py +1014 -0
  47. adsfolio/gui/icons.py +157 -0
  48. adsfolio/gui/jobs.py +196 -0
  49. adsfolio/gui/library.py +321 -0
  50. adsfolio/gui/main_window.py +3583 -0
  51. adsfolio/gui/models.py +562 -0
  52. adsfolio/gui/notifications.py +230 -0
  53. adsfolio/gui/osint.py +297 -0
  54. adsfolio/gui/references_dialog.py +435 -0
  55. adsfolio/gui/refresh_job.py +82 -0
  56. adsfolio/gui/token_dialog.py +186 -0
  57. adsfolio/gui/update_dialog.py +382 -0
  58. adsfolio/gui/updates.py +538 -0
  59. adsfolio/gui/watch.py +699 -0
  60. adsfolio/gui/welcome.py +149 -0
  61. adsfolio/journal_utils.py +272 -0
  62. adsfolio/library_sync.py +531 -0
  63. adsfolio/pdf/__init__.py +38 -0
  64. adsfolio/pdf/annotation_merge.py +771 -0
  65. adsfolio/pdf/annotations.py +393 -0
  66. adsfolio/pdf/arxiv.py +85 -0
  67. adsfolio/pdf/catalog.py +2008 -0
  68. adsfolio/pdf/cli.py +2267 -0
  69. adsfolio/pdf/cli_common.py +99 -0
  70. adsfolio/pdf/cli_upgrade.py +81 -0
  71. adsfolio/pdf/doi_providers.py +436 -0
  72. adsfolio/pdf/files.py +70 -0
  73. adsfolio/pdf/identifier.py +1694 -0
  74. adsfolio/pdf/identify.py +731 -0
  75. adsfolio/pdf/identify_settings.py +316 -0
  76. adsfolio/pdf/obsidian.py +208 -0
  77. adsfolio/pdf/prefetch.py +324 -0
  78. adsfolio/pdf/readiness.py +109 -0
  79. adsfolio/pdf/registry.py +792 -0
  80. adsfolio/pdf/rename_settings.py +187 -0
  81. adsfolio/pdf/renamer.py +454 -0
  82. adsfolio/pdf/sidecar.py +420 -0
  83. adsfolio/pdf/tracked_move.py +150 -0
  84. adsfolio/pdf/upgrade.py +364 -0
  85. adsfolio/pdf/upgrade_utils.py +318 -0
  86. adsfolio/pdf/watch_session.py +738 -0
  87. adsfolio/pdf/watcher.py +676 -0
  88. adsfolio/pubcache.py +146 -0
  89. adsfolio/reference_cli.py +230 -0
  90. adsfolio/references.py +438 -0
  91. adsfolio/user_profile.py +225 -0
  92. adsfolio-0.5.1.dist-info/METADATA +168 -0
  93. adsfolio-0.5.1.dist-info/RECORD +97 -0
  94. adsfolio-0.5.1.dist-info/WHEEL +5 -0
  95. adsfolio-0.5.1.dist-info/entry_points.txt +3 -0
  96. adsfolio-0.5.1.dist-info/licenses/LICENSE +28 -0
  97. adsfolio-0.5.1.dist-info/top_level.txt +1 -0
adsfolio/__init__.py ADDED
@@ -0,0 +1,16 @@
1
+ """ADSfolio research library tools and NASA ADS client."""
2
+
3
+ from .client import ADSClient
4
+ from .config import Config
5
+ from .formatting import format_author_list_cv, format_title_latex
6
+ from .journal_utils import format_journal
7
+
8
+ __version__ = "0.5.1"
9
+ __all__ = [
10
+ "ADSClient",
11
+ "Config",
12
+ "__version__",
13
+ "format_author_list_cv",
14
+ "format_journal",
15
+ "format_title_latex",
16
+ ]
@@ -0,0 +1,9 @@
1
+ """API module exports."""
2
+
3
+ from .export import ExportAPI
4
+ from .journals import JournalsAPI
5
+ from .libraries import LibrariesAPI
6
+ from .metrics import MetricsAPI
7
+ from .search import SearchAPI
8
+
9
+ __all__ = ["ExportAPI", "JournalsAPI", "LibrariesAPI", "MetricsAPI", "SearchAPI"]
adsfolio/api/export.py ADDED
@@ -0,0 +1,293 @@
1
+ """
2
+ Export API wrapper for ADS.
3
+
4
+ Provides access to bibliography export in various formats:
5
+ - BibTeX, AASTeX, MNRAS, Icarus, etc.
6
+ - Custom format strings
7
+ """
8
+
9
+ from collections.abc import Callable, Sequence
10
+ from concurrent.futures import CancelledError
11
+ from typing import TYPE_CHECKING, ClassVar
12
+
13
+ from ..bibtex.sanitize import BIBTEX_FORMATS, repair_bibtex
14
+ from ..config import ADSError
15
+
16
+ if TYPE_CHECKING:
17
+ from ..client import BaseClient
18
+
19
+ __all__ = ["ExportAPI"]
20
+
21
+
22
+ class ExportAPI:
23
+ """
24
+ Export API for retrieving bibliographic data in various formats.
25
+
26
+ Examples
27
+ --------
28
+ >>> api = ExportAPI(client)
29
+ >>> bibtex = api.export(["2020ApJ...900..100S"], format="bibtex")
30
+ >>> print(bibtex)
31
+ """
32
+
33
+ # ADS export_service/config.py specifies a maximum of 2,000 Solr records
34
+ # per request. Keep this separate from the library pagination limit.
35
+ MAX_RECORDS_PER_REQUEST = 2000
36
+ BIBLIOGRAPHY_FORMATS = (
37
+ "bibtex",
38
+ "bibtexabs",
39
+ "ris",
40
+ "endnote",
41
+ "aastex",
42
+ "mnras",
43
+ "icarus",
44
+ "soph",
45
+ "ads",
46
+ "procite",
47
+ "refworks",
48
+ "medlars",
49
+ )
50
+
51
+ # Supported export formats
52
+ FORMATS: ClassVar[list[str]] = [
53
+ "bibtex", # Standard BibTeX
54
+ "bibtexabs", # BibTeX with abstracts
55
+ "ads", # ADS format
56
+ "aastex", # AASTeX format
57
+ "icarus", # Icarus journal format
58
+ "mnras", # MNRAS format
59
+ "soph", # Solar Physics format
60
+ "endnote", # EndNote format
61
+ "procite", # ProCite format
62
+ "ris", # RIS format
63
+ "refworks", # RefWorks format
64
+ "rss", # RSS format
65
+ "medlars", # MEDLARS format
66
+ "dcxml", # Dublin Core XML
67
+ "refxml", # Reference XML
68
+ "refabsxml", # Reference with abstract XML
69
+ "votable", # VOTable format
70
+ "custom", # Custom format string
71
+ ]
72
+
73
+ def __init__(self, client: "BaseClient"):
74
+ self._client = client
75
+
76
+ def export(
77
+ self,
78
+ bibcodes: list[str],
79
+ format: str = "bibtex",
80
+ sort: str | None = None,
81
+ custom_format: str | None = None,
82
+ ) -> str:
83
+ """
84
+ Export bibcodes in the specified format.
85
+
86
+ Parameters
87
+ ----------
88
+ bibcodes : list[str]
89
+ List of bibcodes to export.
90
+ format : str, optional
91
+ Export format. Default: "bibtex".
92
+ See FORMATS for available options.
93
+ sort : str, optional
94
+ Sort order, e.g., "first_author asc".
95
+ custom_format : str, optional
96
+ Custom format string (required if format="custom").
97
+ See ADS documentation for format codes.
98
+
99
+ Returns
100
+ -------
101
+ str
102
+ Exported bibliography in requested format.
103
+
104
+ Examples
105
+ --------
106
+ >>> bibtex = api.export(["2020ApJ...900..100S"], format="bibtex")
107
+ >>> custom = api.export(bibcodes, format="custom",
108
+ ... custom_format="%R %T")
109
+ """
110
+ payload = {"bibcode": bibcodes}
111
+ if sort:
112
+ payload["sort"] = sort
113
+ if format == "custom" or format not in self.FORMATS or custom_format:
114
+ format_string = custom_format or (format if format != "custom" else "")
115
+ if not format_string.strip():
116
+ raise ValueError("custom_format is required for a custom export.")
117
+ payload["format"] = format_string
118
+ endpoint = "/export/custom"
119
+ else:
120
+ endpoint = f"/export/{format}"
121
+
122
+ response = self._client.post(endpoint, data=payload)
123
+ text = response.get("export", "")
124
+ if endpoint == f"/export/{format}" and format in BIBTEX_FORMATS:
125
+ text = repair_bibtex(text)
126
+ return text
127
+
128
+ def export_complete(
129
+ self,
130
+ bibcodes: Sequence[str],
131
+ format: str = "bibtex",
132
+ *,
133
+ cancelled: Callable[[], bool] | None = None,
134
+ ) -> str:
135
+ """Export every requested record as one bibliography, or fail.
136
+
137
+ Parameters
138
+ ----------
139
+ bibcodes : sequence of str
140
+ Unique ADS Bibcodes. The input order is preserved across batches.
141
+ format : str, optional
142
+ A format in `BIBLIOGRAPHY_FORMATS`. Defaults to ``bibtex``.
143
+ XML and custom formats require different assembly rules and are
144
+ supported separately by ``export()``.
145
+ cancelled : callable, optional
146
+ Cancellation check before and after each network request.
147
+
148
+ Returns
149
+ -------
150
+ str
151
+ Complete bibliography. An empty input returns an empty string.
152
+
153
+ Raises
154
+ ------
155
+ ValueError
156
+ If the format is unsupported or Bibcodes are empty or duplicated.
157
+ TypeError
158
+ If `bibcodes` is a string rather than a sequence of identifiers.
159
+ ADSError
160
+ If ADS returns missing, repeated, or malformed records.
161
+ concurrent.futures.CancelledError
162
+ If cancellation is requested. No partial bibliography is returned.
163
+ """
164
+ if format not in self.BIBLIOGRAPHY_FORMATS:
165
+ raise ValueError(f"Unsupported bibliography format: {format}")
166
+ if isinstance(bibcodes, str):
167
+ raise TypeError("Pass a sequence of Bibcodes, not a single string.")
168
+ codes = tuple(code.strip() for code in bibcodes)
169
+ if any(not code for code in codes) or len(
170
+ {code.lower() for code in codes}
171
+ ) != len(codes):
172
+ raise ValueError("Bibcodes must be non-empty and unique.")
173
+ references: list[str] = []
174
+ wrappers: tuple[str, str] | None = None
175
+ for start in range(0, len(codes), self.MAX_RECORDS_PER_REQUEST):
176
+ if cancelled is not None and cancelled():
177
+ raise CancelledError("Bibliography export cancelled.")
178
+ batch = codes[start : start + self.MAX_RECORDS_PER_REQUEST]
179
+ response = self._client.post(
180
+ f"/export/{format}",
181
+ data={"bibcode": list(batch), "sort": "no sort", "outputformat": 2},
182
+ )
183
+ if cancelled is not None and cancelled():
184
+ raise CancelledError("Bibliography export cancelled.")
185
+ documents = response.get("docs")
186
+ count = response.get("num_docs")
187
+ if (
188
+ type(count) is not int
189
+ or count != len(batch)
190
+ or not isinstance(documents, list)
191
+ or len(documents) != len(batch)
192
+ ):
193
+ raise ADSError(
194
+ "ADS returned an incomplete bibliography; no file was saved."
195
+ )
196
+ by_bibcode: dict[str, str] = {}
197
+ for document in documents:
198
+ if not isinstance(document, dict):
199
+ raise ADSError("ADS returned a malformed bibliography record.")
200
+ code = document.get("bibcode")
201
+ reference = document.get("reference")
202
+ if (
203
+ not isinstance(code, str)
204
+ or not isinstance(reference, str)
205
+ or not reference.strip()
206
+ or code.lower() in by_bibcode
207
+ ):
208
+ raise ADSError(
209
+ "ADS returned a missing or repeated bibliography record."
210
+ )
211
+ if format in BIBTEX_FORMATS:
212
+ reference = repair_bibtex(reference)
213
+ by_bibcode[code.lower()] = reference
214
+ if set(by_bibcode) != {code.lower() for code in batch}:
215
+ raise ADSError(
216
+ "ADS bibliography records did not match the requested library."
217
+ )
218
+ header, footer = response.get("header", ""), response.get("footer", "")
219
+ if not isinstance(header, str) or not isinstance(footer, str):
220
+ raise ADSError("ADS returned malformed bibliography headers.")
221
+ if wrappers is not None and wrappers != (header, footer):
222
+ raise ADSError("ADS bibliography headers changed between batches.")
223
+ wrappers = (header, footer)
224
+ references.extend(by_bibcode[code.lower()] for code in batch)
225
+ if not references:
226
+ return ""
227
+ assert wrappers is not None
228
+ return (
229
+ "\n\n".join(
230
+ part.rstrip("\r\n")
231
+ for part in (wrappers[0], *references, wrappers[1])
232
+ if part
233
+ )
234
+ + "\n"
235
+ )
236
+
237
+ def bibtex(
238
+ self,
239
+ bibcodes: list[str],
240
+ sort: str | None = None,
241
+ with_abstract: bool = False,
242
+ ) -> str:
243
+ """
244
+ Convenience method for BibTeX export.
245
+
246
+ Parameters
247
+ ----------
248
+ bibcodes : list[str]
249
+ List of bibcodes.
250
+ sort : str, optional
251
+ Sort order.
252
+ with_abstract : bool, optional
253
+ Include abstracts in output. Default: False.
254
+
255
+ Returns
256
+ -------
257
+ str
258
+ BibTeX formatted bibliography.
259
+ """
260
+ fmt = "bibtexabs" if with_abstract else "bibtex"
261
+ return self.export(bibcodes, format=fmt, sort=sort)
262
+
263
+ def aastex(self, bibcodes: list[str], sort: str | None = None) -> str:
264
+ """Export in AASTeX format."""
265
+ return self.export(bibcodes, format="aastex", sort=sort)
266
+
267
+ def custom(
268
+ self,
269
+ bibcodes: list[str],
270
+ format_string: str,
271
+ sort: str | None = None,
272
+ ) -> str:
273
+ """
274
+ Export with custom format string.
275
+
276
+ Parameters
277
+ ----------
278
+ bibcodes : list[str]
279
+ List of bibcodes.
280
+ format_string : str
281
+ Custom format string using ADS format codes.
282
+ Examples: "%R %T", "%m %Y", "%10.5N_%Y_%q_%V_%p"
283
+ sort : str, optional
284
+ Sort order.
285
+
286
+ Returns
287
+ -------
288
+ str
289
+ Custom formatted output.
290
+ """
291
+ return self.export(
292
+ bibcodes, format="custom", custom_format=format_string, sort=sort
293
+ )
@@ -0,0 +1,138 @@
1
+ """
2
+ Journals API wrapper for ADS.
3
+
4
+ Provides access to journal metadata:
5
+ - Summary information by bibstem
6
+ - Journal name search
7
+ - Holdings and reference sources
8
+ - ISSN lookup
9
+ """
10
+
11
+ from typing import TYPE_CHECKING
12
+
13
+ if TYPE_CHECKING:
14
+ from ..client import BaseClient
15
+
16
+ __all__ = ["JournalsAPI"]
17
+
18
+
19
+ class JournalsAPI:
20
+ """
21
+ Journals API for querying journal metadata.
22
+
23
+ Examples
24
+ --------
25
+ >>> api = JournalsAPI(client)
26
+ >>> summary = api.summary("ApJ")
27
+ >>> print(summary["master"]["journal_name"])
28
+ """
29
+
30
+ def __init__(self, client: "BaseClient"):
31
+ self._client = client
32
+
33
+ def summary(self, bibstem: str) -> dict:
34
+ """
35
+ Get summary information for a journal by bibstem.
36
+
37
+ Parameters
38
+ ----------
39
+ bibstem : str
40
+ Journal bibstem (case-sensitive), e.g., "ApJ", "MNRAS".
41
+
42
+ Returns
43
+ -------
44
+ dict
45
+ Journal summary including master record, identifiers, abbreviations.
46
+ """
47
+ response = self._client.get(f"/journals/summary/{bibstem}")
48
+ return response.get("summary", {})
49
+
50
+ def search(self, query: str) -> list[dict]:
51
+ """
52
+ Search for journals by name or abbreviation.
53
+
54
+ Parameters
55
+ ----------
56
+ query : str
57
+ Search text to match against journal names.
58
+
59
+ Returns
60
+ -------
61
+ list[dict]
62
+ List of matching journal records.
63
+ """
64
+ from urllib.parse import quote
65
+
66
+ encoded_query = quote(query)
67
+ response = self._client.get(f"/journals/journal/{encoded_query}")
68
+ return response.get("journal", [])
69
+
70
+ def refsource(self, bibstem: str) -> dict:
71
+ """
72
+ Get citation data sources by volume for a journal.
73
+
74
+ Parameters
75
+ ----------
76
+ bibstem : str
77
+ Journal bibstem.
78
+
79
+ Returns
80
+ -------
81
+ dict
82
+ Reference source data per volume.
83
+ """
84
+ response = self._client.get(f"/journals/refsource/{bibstem}")
85
+ return response.get("refsource", {})
86
+
87
+ def holdings(self, bibstem: str, volume: str) -> dict:
88
+ """
89
+ Get available electronic sources for a journal/volume.
90
+
91
+ Parameters
92
+ ----------
93
+ bibstem : str
94
+ Journal bibstem.
95
+ volume : str
96
+ Volume number.
97
+
98
+ Returns
99
+ -------
100
+ dict
101
+ Holdings data with available sources.
102
+ """
103
+ response = self._client.get(f"/journals/holdings/{bibstem}/{volume}")
104
+ return response.get("holdings", {})
105
+
106
+ def issn(self, issn_number: str) -> str | None:
107
+ """
108
+ Look up bibstem by ISSN.
109
+
110
+ Parameters
111
+ ----------
112
+ issn_number : str
113
+ ISSN identifier (with or without hyphens).
114
+
115
+ Returns
116
+ -------
117
+ str or None
118
+ Matching bibstem, or None if not found.
119
+ """
120
+ response = self._client.get(f"/journals/issn/{issn_number}")
121
+ return response.get("issn", {}).get("bibstem")
122
+
123
+ def browse(self, bibstem: str) -> dict:
124
+ """
125
+ Get abbreviated summary for a journal.
126
+
127
+ Parameters
128
+ ----------
129
+ bibstem : str
130
+ Journal bibstem.
131
+
132
+ Returns
133
+ -------
134
+ dict
135
+ Abbreviated journal summary.
136
+ """
137
+ response = self._client.get(f"/journals/browse/{bibstem}")
138
+ return response.get("browse", {})