citeable 2026.2.25a0__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.
- citeable/__init__.py +40 -0
- citeable/_entries.py +585 -0
- citeable/_json.py +32 -0
- citeable/_keys.py +59 -0
- citeable/_parser.py +207 -0
- citeable/_validate.py +37 -0
- citeable/py.typed +0 -0
- citeable-2026.2.25a0.dist-info/METADATA +448 -0
- citeable-2026.2.25a0.dist-info/RECORD +11 -0
- citeable-2026.2.25a0.dist-info/WHEEL +4 -0
- citeable-2026.2.25a0.dist-info/licenses/LICENSE +28 -0
citeable/__init__.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
"""citeable — structured BibTeX citations for cogent3 plugins."""
|
|
2
|
+
|
|
3
|
+
from importlib import metadata as _metadata
|
|
4
|
+
|
|
5
|
+
from citeable._entries import (
|
|
6
|
+
Article,
|
|
7
|
+
Book,
|
|
8
|
+
Citation,
|
|
9
|
+
CitationBase,
|
|
10
|
+
InProceedings,
|
|
11
|
+
Misc,
|
|
12
|
+
Software,
|
|
13
|
+
TechReport,
|
|
14
|
+
Thesis,
|
|
15
|
+
)
|
|
16
|
+
from citeable._json import from_jsons, load_json, to_jsons, write_json
|
|
17
|
+
from citeable._keys import assign_unique_keys, write_bibtex
|
|
18
|
+
from citeable._parser import from_bibtex_string
|
|
19
|
+
|
|
20
|
+
__version__ = _metadata.version("citeable")
|
|
21
|
+
|
|
22
|
+
__all__ = [
|
|
23
|
+
"Article",
|
|
24
|
+
"Book",
|
|
25
|
+
"Citation",
|
|
26
|
+
"CitationBase",
|
|
27
|
+
"InProceedings",
|
|
28
|
+
"Misc",
|
|
29
|
+
"Software",
|
|
30
|
+
"TechReport",
|
|
31
|
+
"Thesis",
|
|
32
|
+
"__version__",
|
|
33
|
+
"assign_unique_keys",
|
|
34
|
+
"from_bibtex_string",
|
|
35
|
+
"from_jsons",
|
|
36
|
+
"load_json",
|
|
37
|
+
"to_jsons",
|
|
38
|
+
"write_bibtex",
|
|
39
|
+
"write_json",
|
|
40
|
+
]
|
citeable/_entries.py
ADDED
|
@@ -0,0 +1,585 @@
|
|
|
1
|
+
"""Citation entry type classes for all supported BibTeX types."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from abc import ABC, abstractmethod
|
|
6
|
+
|
|
7
|
+
from citeable._keys import generate_key
|
|
8
|
+
from citeable._validate import extract_surname, require_field, require_non_empty_authors
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _format_bibtex_field(name: str, value: str) -> str:
|
|
12
|
+
return f" {name:<10}= {{{value}}},"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def _author_str(authors: list[str]) -> str:
|
|
16
|
+
return " and ".join(authors)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
def _author_summary(authors: list[str]) -> str:
|
|
20
|
+
surname = extract_surname(authors[0])
|
|
21
|
+
return f"{surname} et al." if len(authors) > 1 else surname
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _title_excerpt(title: str, max_len: int = 50) -> str:
|
|
25
|
+
return title if len(title) <= max_len else title[:max_len] + "\u2026"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _content_fields(obj: object, exclude: set[str]) -> tuple[object, ...]:
|
|
29
|
+
"""Return a tuple of content field values for equality/hashing.
|
|
30
|
+
|
|
31
|
+
Lists are converted to tuples so the result is hashable.
|
|
32
|
+
"""
|
|
33
|
+
vals: list[object] = []
|
|
34
|
+
vals.extend(
|
|
35
|
+
tuple(v) if isinstance(v, list) else v
|
|
36
|
+
for k, v in sorted(vars(obj).items())
|
|
37
|
+
if k not in exclude
|
|
38
|
+
)
|
|
39
|
+
return tuple(vals)
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
_EXCLUDED: set[str] = {"key", "app"}
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class CitationBase(ABC):
|
|
46
|
+
"""Abstract base class for all citation entry types."""
|
|
47
|
+
|
|
48
|
+
author: list[str]
|
|
49
|
+
title: str
|
|
50
|
+
year: int
|
|
51
|
+
doi: str | None
|
|
52
|
+
url: str | None
|
|
53
|
+
note: str | None
|
|
54
|
+
key: str
|
|
55
|
+
app: str | None
|
|
56
|
+
|
|
57
|
+
def _init_base(
|
|
58
|
+
self,
|
|
59
|
+
author: list[str],
|
|
60
|
+
title: str,
|
|
61
|
+
year: int,
|
|
62
|
+
*,
|
|
63
|
+
doi: str | None = None,
|
|
64
|
+
url: str | None = None,
|
|
65
|
+
note: str | None = None,
|
|
66
|
+
key: str | None = None,
|
|
67
|
+
app: str | None = None,
|
|
68
|
+
) -> None:
|
|
69
|
+
"""Set common fields shared by all citation types."""
|
|
70
|
+
require_non_empty_authors(author, type(self).__name__)
|
|
71
|
+
self.author = author
|
|
72
|
+
self.title = title
|
|
73
|
+
self.year = year
|
|
74
|
+
self.doi = doi
|
|
75
|
+
self.url = url
|
|
76
|
+
self.note = note
|
|
77
|
+
self.key = key if key is not None else generate_key(author, year)
|
|
78
|
+
self.app = app
|
|
79
|
+
|
|
80
|
+
def __eq__(self, other: object) -> bool:
|
|
81
|
+
if self is other:
|
|
82
|
+
return True
|
|
83
|
+
if type(self) is not type(other):
|
|
84
|
+
return NotImplemented
|
|
85
|
+
return _content_fields(self, _EXCLUDED) == _content_fields(other, _EXCLUDED)
|
|
86
|
+
|
|
87
|
+
def __hash__(self) -> int:
|
|
88
|
+
return hash((type(self).__name__, _content_fields(self, _EXCLUDED)))
|
|
89
|
+
|
|
90
|
+
def summary(self) -> tuple[str, str]:
|
|
91
|
+
"""Return ``(app_name, citation_string)``."""
|
|
92
|
+
app_name = self.app if self.app is not None else ""
|
|
93
|
+
auth = _author_summary(self.author)
|
|
94
|
+
excerpt = _title_excerpt(self.title)
|
|
95
|
+
return (app_name, f"{auth} {self.year} {excerpt}")
|
|
96
|
+
|
|
97
|
+
def __repr__(self) -> str:
|
|
98
|
+
fields = self._repr_fields()
|
|
99
|
+
auto_key = generate_key(self.author, self.year)
|
|
100
|
+
if self.key != auto_key:
|
|
101
|
+
fields.insert(0, ("key", self.key))
|
|
102
|
+
parts = [f" {name}={value!r}," for name, value in fields]
|
|
103
|
+
body = "\n".join(parts)
|
|
104
|
+
return f"{type(self).__name__}(\n{body}\n)"
|
|
105
|
+
|
|
106
|
+
def _append_common_bibtex(self, lines: list[str]) -> None:
|
|
107
|
+
"""Append doi/url/note BibTeX fields if set."""
|
|
108
|
+
if self.doi is not None:
|
|
109
|
+
lines.append(_format_bibtex_field("doi", self.doi))
|
|
110
|
+
if self.url is not None:
|
|
111
|
+
lines.append(_format_bibtex_field("url", self.url))
|
|
112
|
+
if self.note is not None:
|
|
113
|
+
lines.append(_format_bibtex_field("note", self.note))
|
|
114
|
+
|
|
115
|
+
def _append_common_optional_repr(self, fields: list[tuple[str, object]]) -> None:
|
|
116
|
+
"""Append doi/url/note to repr field list if set."""
|
|
117
|
+
if self.doi is not None:
|
|
118
|
+
fields.append(("doi", self.doi))
|
|
119
|
+
if self.url is not None:
|
|
120
|
+
fields.append(("url", self.url))
|
|
121
|
+
if self.note is not None:
|
|
122
|
+
fields.append(("note", self.note))
|
|
123
|
+
|
|
124
|
+
def to_dict(self) -> dict[str, object]:
|
|
125
|
+
"""Return a JSON-serialisable dict including a ``"type"`` discriminator."""
|
|
126
|
+
return {"type": type(self).__name__, **vars(self)}
|
|
127
|
+
|
|
128
|
+
@classmethod
|
|
129
|
+
def from_dict(cls, data: dict[str, object]) -> CitationBase:
|
|
130
|
+
"""Reconstruct a citation from a dict produced by :meth:`to_dict`.
|
|
131
|
+
|
|
132
|
+
Raises ``ValueError`` if the ``"type"`` key is missing or unknown.
|
|
133
|
+
"""
|
|
134
|
+
data = dict(data) # shallow copy so we don't mutate the caller's dict
|
|
135
|
+
type_name = data.pop("type", None)
|
|
136
|
+
if type_name is None:
|
|
137
|
+
msg = "dict is missing required 'type' key"
|
|
138
|
+
raise ValueError(msg)
|
|
139
|
+
entry_cls = _ENTRY_TYPES.get(str(type_name))
|
|
140
|
+
if entry_cls is None:
|
|
141
|
+
msg = f"unknown citation type {type_name!r}"
|
|
142
|
+
raise ValueError(msg)
|
|
143
|
+
return entry_cls(**data)
|
|
144
|
+
|
|
145
|
+
@abstractmethod
|
|
146
|
+
def _repr_fields(self) -> list[tuple[str, object]]:
|
|
147
|
+
"""Return the list of ``(name, value)`` pairs for ``__repr__``."""
|
|
148
|
+
|
|
149
|
+
|
|
150
|
+
# Keep Citation as a public alias for the base class.
|
|
151
|
+
Citation = CitationBase
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
class Article(CitationBase):
|
|
155
|
+
"""An ``@article`` BibTeX entry."""
|
|
156
|
+
|
|
157
|
+
journal: str
|
|
158
|
+
volume: int
|
|
159
|
+
pages: str | None
|
|
160
|
+
article_number: str | None
|
|
161
|
+
number: int | None
|
|
162
|
+
|
|
163
|
+
def __init__(
|
|
164
|
+
self,
|
|
165
|
+
author: list[str],
|
|
166
|
+
title: str,
|
|
167
|
+
year: int,
|
|
168
|
+
journal: str,
|
|
169
|
+
volume: int,
|
|
170
|
+
*,
|
|
171
|
+
pages: str | None = None,
|
|
172
|
+
article_number: str | None = None,
|
|
173
|
+
number: int | None = None,
|
|
174
|
+
doi: str | None = None,
|
|
175
|
+
url: str | None = None,
|
|
176
|
+
note: str | None = None,
|
|
177
|
+
key: str | None = None,
|
|
178
|
+
app: str | None = None,
|
|
179
|
+
) -> None:
|
|
180
|
+
require_field(journal, "journal", "Article")
|
|
181
|
+
require_field(volume, "volume", "Article")
|
|
182
|
+
if pages is None and article_number is None:
|
|
183
|
+
msg = "Article requires 'pages' or 'article_number'; both are None"
|
|
184
|
+
raise ValueError(msg)
|
|
185
|
+
|
|
186
|
+
self._init_base(
|
|
187
|
+
author, title, year, doi=doi, url=url, note=note, key=key, app=app
|
|
188
|
+
)
|
|
189
|
+
self.journal = journal
|
|
190
|
+
self.volume = volume
|
|
191
|
+
self.pages = pages
|
|
192
|
+
self.article_number = article_number
|
|
193
|
+
self.number = number
|
|
194
|
+
|
|
195
|
+
def __str__(self) -> str:
|
|
196
|
+
lines = [
|
|
197
|
+
f"@article{{{self.key},",
|
|
198
|
+
_format_bibtex_field("author", _author_str(self.author)),
|
|
199
|
+
_format_bibtex_field("title", self.title),
|
|
200
|
+
_format_bibtex_field("journal", self.journal),
|
|
201
|
+
_format_bibtex_field("year", str(self.year)),
|
|
202
|
+
_format_bibtex_field("volume", str(self.volume)),
|
|
203
|
+
]
|
|
204
|
+
if self.number is not None:
|
|
205
|
+
lines.append(_format_bibtex_field("number", str(self.number)))
|
|
206
|
+
if self.pages is not None:
|
|
207
|
+
lines.append(_format_bibtex_field("pages", self.pages))
|
|
208
|
+
if self.article_number is not None:
|
|
209
|
+
lines.append(_format_bibtex_field("article_number", self.article_number))
|
|
210
|
+
self._append_common_bibtex(lines)
|
|
211
|
+
lines.append("}")
|
|
212
|
+
return "\n".join(lines)
|
|
213
|
+
|
|
214
|
+
def _repr_fields(self) -> list[tuple[str, object]]:
|
|
215
|
+
fields: list[tuple[str, object]] = [
|
|
216
|
+
("author", self.author),
|
|
217
|
+
("title", self.title),
|
|
218
|
+
("year", self.year),
|
|
219
|
+
("journal", self.journal),
|
|
220
|
+
("volume", self.volume),
|
|
221
|
+
]
|
|
222
|
+
if self.pages is not None:
|
|
223
|
+
fields.append(("pages", self.pages))
|
|
224
|
+
if self.article_number is not None:
|
|
225
|
+
fields.append(("article_number", self.article_number))
|
|
226
|
+
if self.number is not None:
|
|
227
|
+
fields.append(("number", self.number))
|
|
228
|
+
self._append_common_optional_repr(fields)
|
|
229
|
+
return fields
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
class Book(CitationBase):
|
|
233
|
+
"""A ``@book`` BibTeX entry."""
|
|
234
|
+
|
|
235
|
+
publisher: str
|
|
236
|
+
edition: str | None
|
|
237
|
+
editor: list[str] | None
|
|
238
|
+
|
|
239
|
+
def __init__(
|
|
240
|
+
self,
|
|
241
|
+
author: list[str],
|
|
242
|
+
title: str,
|
|
243
|
+
year: int,
|
|
244
|
+
publisher: str,
|
|
245
|
+
*,
|
|
246
|
+
edition: str | None = None,
|
|
247
|
+
editor: list[str] | None = None,
|
|
248
|
+
doi: str | None = None,
|
|
249
|
+
url: str | None = None,
|
|
250
|
+
note: str | None = None,
|
|
251
|
+
key: str | None = None,
|
|
252
|
+
app: str | None = None,
|
|
253
|
+
) -> None:
|
|
254
|
+
require_field(publisher, "publisher", "Book")
|
|
255
|
+
|
|
256
|
+
self._init_base(
|
|
257
|
+
author, title, year, doi=doi, url=url, note=note, key=key, app=app
|
|
258
|
+
)
|
|
259
|
+
self.publisher = publisher
|
|
260
|
+
self.edition = edition
|
|
261
|
+
self.editor = editor
|
|
262
|
+
|
|
263
|
+
def __str__(self) -> str:
|
|
264
|
+
lines = [
|
|
265
|
+
f"@book{{{self.key},",
|
|
266
|
+
_format_bibtex_field("author", _author_str(self.author)),
|
|
267
|
+
_format_bibtex_field("title", self.title),
|
|
268
|
+
_format_bibtex_field("publisher", self.publisher),
|
|
269
|
+
_format_bibtex_field("year", str(self.year)),
|
|
270
|
+
]
|
|
271
|
+
if self.edition is not None:
|
|
272
|
+
lines.append(_format_bibtex_field("edition", self.edition))
|
|
273
|
+
if self.editor is not None:
|
|
274
|
+
lines.append(_format_bibtex_field("editor", _author_str(self.editor)))
|
|
275
|
+
self._append_common_bibtex(lines)
|
|
276
|
+
lines.append("}")
|
|
277
|
+
return "\n".join(lines)
|
|
278
|
+
|
|
279
|
+
def _repr_fields(self) -> list[tuple[str, object]]:
|
|
280
|
+
fields: list[tuple[str, object]] = [
|
|
281
|
+
("author", self.author),
|
|
282
|
+
("title", self.title),
|
|
283
|
+
("year", self.year),
|
|
284
|
+
("publisher", self.publisher),
|
|
285
|
+
]
|
|
286
|
+
if self.edition is not None:
|
|
287
|
+
fields.append(("edition", self.edition))
|
|
288
|
+
if self.editor is not None:
|
|
289
|
+
fields.append(("editor", self.editor))
|
|
290
|
+
self._append_common_optional_repr(fields)
|
|
291
|
+
return fields
|
|
292
|
+
|
|
293
|
+
|
|
294
|
+
class InProceedings(CitationBase):
|
|
295
|
+
"""An ``@inproceedings`` BibTeX entry."""
|
|
296
|
+
|
|
297
|
+
booktitle: str
|
|
298
|
+
pages: str | None
|
|
299
|
+
publisher: str | None
|
|
300
|
+
editor: list[str] | None
|
|
301
|
+
|
|
302
|
+
def __init__(
|
|
303
|
+
self,
|
|
304
|
+
author: list[str],
|
|
305
|
+
title: str,
|
|
306
|
+
year: int,
|
|
307
|
+
booktitle: str,
|
|
308
|
+
*,
|
|
309
|
+
pages: str | None = None,
|
|
310
|
+
publisher: str | None = None,
|
|
311
|
+
editor: list[str] | None = None,
|
|
312
|
+
doi: str | None = None,
|
|
313
|
+
url: str | None = None,
|
|
314
|
+
note: str | None = None,
|
|
315
|
+
key: str | None = None,
|
|
316
|
+
app: str | None = None,
|
|
317
|
+
) -> None:
|
|
318
|
+
require_field(booktitle, "booktitle", "InProceedings")
|
|
319
|
+
|
|
320
|
+
self._init_base(
|
|
321
|
+
author, title, year, doi=doi, url=url, note=note, key=key, app=app
|
|
322
|
+
)
|
|
323
|
+
self.booktitle = booktitle
|
|
324
|
+
self.pages = pages
|
|
325
|
+
self.publisher = publisher
|
|
326
|
+
self.editor = editor
|
|
327
|
+
|
|
328
|
+
def __str__(self) -> str:
|
|
329
|
+
lines = [
|
|
330
|
+
f"@inproceedings{{{self.key},",
|
|
331
|
+
_format_bibtex_field("author", _author_str(self.author)),
|
|
332
|
+
_format_bibtex_field("title", self.title),
|
|
333
|
+
_format_bibtex_field("booktitle", self.booktitle),
|
|
334
|
+
_format_bibtex_field("year", str(self.year)),
|
|
335
|
+
]
|
|
336
|
+
if self.pages is not None:
|
|
337
|
+
lines.append(_format_bibtex_field("pages", self.pages))
|
|
338
|
+
if self.publisher is not None:
|
|
339
|
+
lines.append(_format_bibtex_field("publisher", self.publisher))
|
|
340
|
+
if self.editor is not None:
|
|
341
|
+
lines.append(_format_bibtex_field("editor", _author_str(self.editor)))
|
|
342
|
+
self._append_common_bibtex(lines)
|
|
343
|
+
lines.append("}")
|
|
344
|
+
return "\n".join(lines)
|
|
345
|
+
|
|
346
|
+
def _repr_fields(self) -> list[tuple[str, object]]:
|
|
347
|
+
fields: list[tuple[str, object]] = [
|
|
348
|
+
("author", self.author),
|
|
349
|
+
("title", self.title),
|
|
350
|
+
("year", self.year),
|
|
351
|
+
("booktitle", self.booktitle),
|
|
352
|
+
]
|
|
353
|
+
if self.pages is not None:
|
|
354
|
+
fields.append(("pages", self.pages))
|
|
355
|
+
if self.publisher is not None:
|
|
356
|
+
fields.append(("publisher", self.publisher))
|
|
357
|
+
if self.editor is not None:
|
|
358
|
+
fields.append(("editor", self.editor))
|
|
359
|
+
self._append_common_optional_repr(fields)
|
|
360
|
+
return fields
|
|
361
|
+
|
|
362
|
+
|
|
363
|
+
class TechReport(CitationBase):
|
|
364
|
+
"""A ``@techreport`` BibTeX entry."""
|
|
365
|
+
|
|
366
|
+
institution: str
|
|
367
|
+
number: str | None
|
|
368
|
+
|
|
369
|
+
def __init__(
|
|
370
|
+
self,
|
|
371
|
+
author: list[str],
|
|
372
|
+
title: str,
|
|
373
|
+
year: int,
|
|
374
|
+
institution: str,
|
|
375
|
+
*,
|
|
376
|
+
number: str | None = None,
|
|
377
|
+
doi: str | None = None,
|
|
378
|
+
url: str | None = None,
|
|
379
|
+
note: str | None = None,
|
|
380
|
+
key: str | None = None,
|
|
381
|
+
app: str | None = None,
|
|
382
|
+
) -> None:
|
|
383
|
+
require_field(institution, "institution", "TechReport")
|
|
384
|
+
|
|
385
|
+
self._init_base(
|
|
386
|
+
author, title, year, doi=doi, url=url, note=note, key=key, app=app
|
|
387
|
+
)
|
|
388
|
+
self.institution = institution
|
|
389
|
+
self.number = number
|
|
390
|
+
|
|
391
|
+
def __str__(self) -> str:
|
|
392
|
+
lines = [
|
|
393
|
+
f"@techreport{{{self.key},",
|
|
394
|
+
_format_bibtex_field("author", _author_str(self.author)),
|
|
395
|
+
_format_bibtex_field("title", self.title),
|
|
396
|
+
_format_bibtex_field("institution", self.institution),
|
|
397
|
+
_format_bibtex_field("year", str(self.year)),
|
|
398
|
+
]
|
|
399
|
+
if self.number is not None:
|
|
400
|
+
lines.append(_format_bibtex_field("number", self.number))
|
|
401
|
+
self._append_common_bibtex(lines)
|
|
402
|
+
lines.append("}")
|
|
403
|
+
return "\n".join(lines)
|
|
404
|
+
|
|
405
|
+
def _repr_fields(self) -> list[tuple[str, object]]:
|
|
406
|
+
fields: list[tuple[str, object]] = [
|
|
407
|
+
("author", self.author),
|
|
408
|
+
("title", self.title),
|
|
409
|
+
("year", self.year),
|
|
410
|
+
("institution", self.institution),
|
|
411
|
+
]
|
|
412
|
+
if self.number is not None:
|
|
413
|
+
fields.append(("number", self.number))
|
|
414
|
+
self._append_common_optional_repr(fields)
|
|
415
|
+
return fields
|
|
416
|
+
|
|
417
|
+
|
|
418
|
+
class Thesis(CitationBase):
|
|
419
|
+
"""A ``@phdthesis`` or ``@mastersthesis`` BibTeX entry."""
|
|
420
|
+
|
|
421
|
+
school: str
|
|
422
|
+
thesis_type: str
|
|
423
|
+
|
|
424
|
+
def __init__(
|
|
425
|
+
self,
|
|
426
|
+
author: list[str],
|
|
427
|
+
title: str,
|
|
428
|
+
year: int,
|
|
429
|
+
school: str,
|
|
430
|
+
thesis_type: str,
|
|
431
|
+
*,
|
|
432
|
+
doi: str | None = None,
|
|
433
|
+
url: str | None = None,
|
|
434
|
+
note: str | None = None,
|
|
435
|
+
key: str | None = None,
|
|
436
|
+
app: str | None = None,
|
|
437
|
+
) -> None:
|
|
438
|
+
require_field(school, "school", "Thesis")
|
|
439
|
+
require_field(thesis_type, "thesis_type", "Thesis")
|
|
440
|
+
if thesis_type not in ("phd", "masters"):
|
|
441
|
+
msg = f"Thesis thesis_type must be 'phd' or 'masters'; got {thesis_type!r}"
|
|
442
|
+
raise ValueError(msg)
|
|
443
|
+
|
|
444
|
+
self._init_base(
|
|
445
|
+
author, title, year, doi=doi, url=url, note=note, key=key, app=app
|
|
446
|
+
)
|
|
447
|
+
self.school = school
|
|
448
|
+
self.thesis_type = thesis_type
|
|
449
|
+
|
|
450
|
+
def __str__(self) -> str:
|
|
451
|
+
bib_type = "phdthesis" if self.thesis_type == "phd" else "mastersthesis"
|
|
452
|
+
lines = [
|
|
453
|
+
f"@{bib_type}{{{self.key},",
|
|
454
|
+
_format_bibtex_field("author", _author_str(self.author)),
|
|
455
|
+
_format_bibtex_field("title", self.title),
|
|
456
|
+
_format_bibtex_field("school", self.school),
|
|
457
|
+
_format_bibtex_field("year", str(self.year)),
|
|
458
|
+
]
|
|
459
|
+
self._append_common_bibtex(lines)
|
|
460
|
+
lines.append("}")
|
|
461
|
+
return "\n".join(lines)
|
|
462
|
+
|
|
463
|
+
def _repr_fields(self) -> list[tuple[str, object]]:
|
|
464
|
+
fields: list[tuple[str, object]] = [
|
|
465
|
+
("author", self.author),
|
|
466
|
+
("title", self.title),
|
|
467
|
+
("year", self.year),
|
|
468
|
+
("school", self.school),
|
|
469
|
+
("thesis_type", self.thesis_type),
|
|
470
|
+
]
|
|
471
|
+
self._append_common_optional_repr(fields)
|
|
472
|
+
return fields
|
|
473
|
+
|
|
474
|
+
|
|
475
|
+
class Software(CitationBase):
|
|
476
|
+
"""A ``@software`` BibTeX entry."""
|
|
477
|
+
|
|
478
|
+
publisher: str | None
|
|
479
|
+
version: str | None
|
|
480
|
+
license: str | None
|
|
481
|
+
|
|
482
|
+
def __init__(
|
|
483
|
+
self,
|
|
484
|
+
author: list[str],
|
|
485
|
+
title: str,
|
|
486
|
+
year: int,
|
|
487
|
+
*,
|
|
488
|
+
publisher: str | None = None,
|
|
489
|
+
version: str | None = None,
|
|
490
|
+
license: str | None = None,
|
|
491
|
+
doi: str | None = None,
|
|
492
|
+
url: str | None = None,
|
|
493
|
+
note: str | None = None,
|
|
494
|
+
key: str | None = None,
|
|
495
|
+
app: str | None = None,
|
|
496
|
+
) -> None:
|
|
497
|
+
self._init_base(
|
|
498
|
+
author, title, year, doi=doi, url=url, note=note, key=key, app=app
|
|
499
|
+
)
|
|
500
|
+
self.publisher = publisher
|
|
501
|
+
self.version = version
|
|
502
|
+
self.license = license
|
|
503
|
+
|
|
504
|
+
def __str__(self) -> str:
|
|
505
|
+
lines = [
|
|
506
|
+
f"@software{{{self.key},",
|
|
507
|
+
_format_bibtex_field("author", _author_str(self.author)),
|
|
508
|
+
_format_bibtex_field("title", self.title),
|
|
509
|
+
_format_bibtex_field("year", str(self.year)),
|
|
510
|
+
]
|
|
511
|
+
if self.publisher is not None:
|
|
512
|
+
lines.append(_format_bibtex_field("publisher", self.publisher))
|
|
513
|
+
if self.version is not None:
|
|
514
|
+
lines.append(_format_bibtex_field("version", self.version))
|
|
515
|
+
if self.license is not None:
|
|
516
|
+
lines.append(_format_bibtex_field("license", self.license))
|
|
517
|
+
self._append_common_bibtex(lines)
|
|
518
|
+
lines.append("}")
|
|
519
|
+
return "\n".join(lines)
|
|
520
|
+
|
|
521
|
+
def _repr_fields(self) -> list[tuple[str, object]]:
|
|
522
|
+
fields: list[tuple[str, object]] = [
|
|
523
|
+
("author", self.author),
|
|
524
|
+
("title", self.title),
|
|
525
|
+
("year", self.year),
|
|
526
|
+
]
|
|
527
|
+
if self.publisher is not None:
|
|
528
|
+
fields.append(("publisher", self.publisher))
|
|
529
|
+
if self.version is not None:
|
|
530
|
+
fields.append(("version", self.version))
|
|
531
|
+
if self.license is not None:
|
|
532
|
+
fields.append(("license", self.license))
|
|
533
|
+
self._append_common_optional_repr(fields)
|
|
534
|
+
return fields
|
|
535
|
+
|
|
536
|
+
|
|
537
|
+
class Misc(CitationBase):
|
|
538
|
+
"""A ``@misc`` BibTeX entry."""
|
|
539
|
+
|
|
540
|
+
def __init__(
|
|
541
|
+
self,
|
|
542
|
+
author: list[str],
|
|
543
|
+
title: str,
|
|
544
|
+
year: int,
|
|
545
|
+
*,
|
|
546
|
+
doi: str | None = None,
|
|
547
|
+
url: str | None = None,
|
|
548
|
+
note: str | None = None,
|
|
549
|
+
key: str | None = None,
|
|
550
|
+
app: str | None = None,
|
|
551
|
+
) -> None:
|
|
552
|
+
self._init_base(
|
|
553
|
+
author, title, year, doi=doi, url=url, note=note, key=key, app=app
|
|
554
|
+
)
|
|
555
|
+
|
|
556
|
+
def __str__(self) -> str:
|
|
557
|
+
lines = [
|
|
558
|
+
f"@misc{{{self.key},",
|
|
559
|
+
_format_bibtex_field("author", _author_str(self.author)),
|
|
560
|
+
_format_bibtex_field("title", self.title),
|
|
561
|
+
_format_bibtex_field("year", str(self.year)),
|
|
562
|
+
]
|
|
563
|
+
self._append_common_bibtex(lines)
|
|
564
|
+
lines.append("}")
|
|
565
|
+
return "\n".join(lines)
|
|
566
|
+
|
|
567
|
+
def _repr_fields(self) -> list[tuple[str, object]]:
|
|
568
|
+
fields: list[tuple[str, object]] = [
|
|
569
|
+
("author", self.author),
|
|
570
|
+
("title", self.title),
|
|
571
|
+
("year", self.year),
|
|
572
|
+
]
|
|
573
|
+
self._append_common_optional_repr(fields)
|
|
574
|
+
return fields
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
_ENTRY_TYPES: dict[str, type[CitationBase]] = {
|
|
578
|
+
"Article": Article,
|
|
579
|
+
"Book": Book,
|
|
580
|
+
"InProceedings": InProceedings,
|
|
581
|
+
"TechReport": TechReport,
|
|
582
|
+
"Thesis": Thesis,
|
|
583
|
+
"Software": Software,
|
|
584
|
+
"Misc": Misc,
|
|
585
|
+
}
|
citeable/_json.py
ADDED
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""JSON serialisation helpers for citation collections."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import pathlib
|
|
7
|
+
from typing import TYPE_CHECKING
|
|
8
|
+
|
|
9
|
+
if TYPE_CHECKING:
|
|
10
|
+
from collections.abc import Iterable
|
|
11
|
+
|
|
12
|
+
from citeable._entries import CitationBase
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def to_jsons(citations: Iterable[CitationBase]) -> str:
|
|
16
|
+
"""Return a JSON string from an iterable of citations."""
|
|
17
|
+
return json.dumps([c.to_dict() for c in citations])
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def from_jsons(data: str) -> list[CitationBase]:
|
|
21
|
+
"""Return a list of citations from a JSON string."""
|
|
22
|
+
return [CitationBase.from_dict(d) for d in json.loads(data)]
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def write_json(*, citations: Iterable[CitationBase], path: str | pathlib.Path) -> None:
|
|
26
|
+
"""Write citations to a JSON file at *path*."""
|
|
27
|
+
pathlib.Path(path).write_text(to_jsons(citations))
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def load_json(path: str | pathlib.Path) -> list[CitationBase]:
|
|
31
|
+
"""Read citations from a JSON file at *path*."""
|
|
32
|
+
return from_jsons(pathlib.Path(path).read_text())
|