philoch-bib-sdk 0.1.2__py3-none-any.whl → 0.1.4__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.
- philoch_bib_sdk/adapters/tabular_data/read_journal_volume_number_index.py +59 -0
- philoch_bib_sdk/converters/latex.py +6 -0
- philoch_bib_sdk/converters/plaintext/author/formatter.py +31 -0
- philoch_bib_sdk/converters/plaintext/author/parser.py +72 -0
- philoch_bib_sdk/converters/plaintext/bib_string_formatter.py +8 -0
- philoch_bib_sdk/converters/plaintext/bibitem/bibkey_formatter.py +21 -0
- philoch_bib_sdk/converters/plaintext/bibitem/bibkey_parser.py +144 -0
- philoch_bib_sdk/converters/plaintext/bibitem/date_formatter.py +37 -0
- philoch_bib_sdk/converters/plaintext/bibitem/date_parser.py +62 -0
- philoch_bib_sdk/converters/plaintext/bibitem/formatter.py +182 -0
- philoch_bib_sdk/converters/plaintext/bibitem/pages_formatter.py +13 -0
- philoch_bib_sdk/converters/plaintext/bibitem/pages_parser.py +63 -0
- philoch_bib_sdk/converters/plaintext/bibitem/parser.py +3 -0
- philoch_bib_sdk/converters/plaintext/journal/formatter.py +25 -0
- philoch_bib_sdk/converters/plaintext/journal/parser.py +36 -0
- philoch_bib_sdk/converters/plaintext/shared/renderable_formatter.py +25 -0
- philoch_bib_sdk/logic/default_models.py +315 -0
- philoch_bib_sdk/logic/functions/comparator.py +134 -0
- philoch_bib_sdk/logic/functions/journal_article_matcher.py +40 -0
- philoch_bib_sdk/logic/literals.py +7 -3
- philoch_bib_sdk/logic/models.py +226 -219
- {philoch_bib_sdk-0.1.2.dist-info → philoch_bib_sdk-0.1.4.dist-info}/METADATA +2 -1
- philoch_bib_sdk-0.1.4.dist-info/RECORD +28 -0
- {philoch_bib_sdk-0.1.2.dist-info → philoch_bib_sdk-0.1.4.dist-info}/WHEEL +1 -1
- philoch_bib_sdk-0.1.4.dist-info/entry_points.txt +3 -0
- philoch_bib_sdk-0.1.2.dist-info/RECORD +0 -8
- {philoch_bib_sdk-0.1.2.dist-info → philoch_bib_sdk-0.1.4.dist-info}/LICENSE +0 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
from functools import partial
|
|
2
|
+
from typing import Callable, NamedTuple
|
|
3
|
+
|
|
4
|
+
from philoch_bib_sdk.logic.functions.journal_article_matcher import (
|
|
5
|
+
TBibkey,
|
|
6
|
+
TJournalBibkeyIndex,
|
|
7
|
+
TJournalName,
|
|
8
|
+
TNumber,
|
|
9
|
+
TReadIndex,
|
|
10
|
+
TVolume,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class ColumnNames(NamedTuple):
|
|
15
|
+
bibkey: TBibkey
|
|
16
|
+
journal: TJournalName
|
|
17
|
+
volume: TVolume
|
|
18
|
+
number: TNumber
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _read_from_ods(
|
|
22
|
+
column_names: ColumnNames,
|
|
23
|
+
file_path: str,
|
|
24
|
+
) -> TJournalBibkeyIndex:
|
|
25
|
+
"""
|
|
26
|
+
Reads the specified columns from an ODS file and returns a TJournalBibkeyIndex dictionary.
|
|
27
|
+
Args:
|
|
28
|
+
column_names (ColumnNames): The names of the columns to read (journal, volume, number, bibkey).
|
|
29
|
+
file_path (str): The path to the ODS file.
|
|
30
|
+
Returns:
|
|
31
|
+
TJournalBibkeyIndex: A dictionary mapping (journal, volume, number) tuples to bibkey values.
|
|
32
|
+
"""
|
|
33
|
+
import polars as pl
|
|
34
|
+
|
|
35
|
+
df = pl.read_ods(
|
|
36
|
+
source=file_path,
|
|
37
|
+
has_header=True,
|
|
38
|
+
columns=[column_names.journal, column_names.volume, column_names.number, column_names.bibkey],
|
|
39
|
+
schema_overrides={
|
|
40
|
+
column_names.journal: pl.Utf8,
|
|
41
|
+
column_names.volume: pl.Utf8,
|
|
42
|
+
column_names.number: pl.Utf8,
|
|
43
|
+
column_names.bibkey: pl.Utf8,
|
|
44
|
+
},
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
if df.is_empty():
|
|
48
|
+
raise ValueError(
|
|
49
|
+
f"Tabular data at '{file_path}' is empty or does not contain the expected columns: {column_names}"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
(row[column_names.journal], row[column_names.volume], row[column_names.number]): row[column_names.bibkey]
|
|
54
|
+
for row in df.to_dicts()
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
type THOFReadFromOds = Callable[[ColumnNames], TReadIndex]
|
|
59
|
+
hof_read_from_ods: THOFReadFromOds = lambda column_names: partial(_read_from_ods, column_names)
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
from aletk.utils import get_logger
|
|
3
|
+
from philoch_bib_sdk.logic.models import Author, TBibString
|
|
4
|
+
|
|
5
|
+
lgr = get_logger(__name__)
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def _full_name_generic(given_name: str, family_name: str, mononym: str) -> str:
|
|
9
|
+
if mononym:
|
|
10
|
+
return mononym
|
|
11
|
+
|
|
12
|
+
if not given_name:
|
|
13
|
+
return ""
|
|
14
|
+
|
|
15
|
+
if not family_name:
|
|
16
|
+
return given_name
|
|
17
|
+
|
|
18
|
+
return f"{family_name}, {given_name}"
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _format_single(author: Author, bibstring_type: TBibString) -> str:
|
|
22
|
+
given_name = f"{getattr(author.given_name, bibstring_type)}"
|
|
23
|
+
family_name = f"{getattr(author.family_name, bibstring_type)}"
|
|
24
|
+
mononym = f"{getattr(author.mononym, bibstring_type)}"
|
|
25
|
+
|
|
26
|
+
return _full_name_generic(given_name, family_name, mononym)
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def format_author(authors: Tuple[Author, ...], bibstring_type: TBibString) -> str:
|
|
30
|
+
names = (_format_single(author, bibstring_type=bibstring_type) for author in authors)
|
|
31
|
+
return " and ".join(name for name in names if name)
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
from aletk.ResultMonad import Ok, Err
|
|
4
|
+
from aletk.utils import get_logger, remove_extra_whitespace
|
|
5
|
+
from philoch_bib_sdk.logic.models import Author, BibStringAttr, TBibString
|
|
6
|
+
|
|
7
|
+
lgr = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _parse_normalize(text: str) -> Tuple[str, str, str]:
|
|
11
|
+
"""
|
|
12
|
+
Return a tuple of two strings, the first of which is the given name, and the second of which is the family name. If only one name is found, the second string will be empty.
|
|
13
|
+
|
|
14
|
+
Fails if more than two names are found.
|
|
15
|
+
"""
|
|
16
|
+
parts = tuple(remove_extra_whitespace(part) for part in text.split(","))
|
|
17
|
+
|
|
18
|
+
if len(parts) > 2:
|
|
19
|
+
raise ValueError(f"Unexpected number of author parts found in '{text}': '{parts}'. Expected 2 or less.")
|
|
20
|
+
|
|
21
|
+
elif len(parts) == 0:
|
|
22
|
+
return ("", "", "")
|
|
23
|
+
|
|
24
|
+
elif len(parts) == 1:
|
|
25
|
+
# Mononym
|
|
26
|
+
return ("", "", parts[0])
|
|
27
|
+
|
|
28
|
+
else:
|
|
29
|
+
# Full name
|
|
30
|
+
return (parts[1], parts[0], "")
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _parse_single(normalized_name_parts: Tuple[str, str, str], bib_string_type: TBibString) -> Author:
|
|
34
|
+
"""
|
|
35
|
+
Parse a single author from a string.
|
|
36
|
+
"""
|
|
37
|
+
_given_name, _family_name, _mononym = normalized_name_parts
|
|
38
|
+
|
|
39
|
+
return Author(
|
|
40
|
+
given_name=BibStringAttr(**{str(bib_string_type): _given_name}),
|
|
41
|
+
family_name=BibStringAttr(**{str(bib_string_type): _family_name}),
|
|
42
|
+
mononym=BibStringAttr(**{str(bib_string_type): _mononym}),
|
|
43
|
+
shorthand=BibStringAttr(),
|
|
44
|
+
famous_name=BibStringAttr(),
|
|
45
|
+
publications=(),
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def parse_author(text: str, bibstring_type: TBibString) -> Ok[Tuple[Author, ...]] | Err:
|
|
50
|
+
"""
|
|
51
|
+
Return either a tuple of Author objects or an error.
|
|
52
|
+
The input string is expected to be an ' and '-separated list of authors, with each author in the format "family_name, given_name" or "mononym".
|
|
53
|
+
"""
|
|
54
|
+
try:
|
|
55
|
+
if text == "":
|
|
56
|
+
lgr.debug("Empty author string, returning empty tuple.")
|
|
57
|
+
return Ok(())
|
|
58
|
+
|
|
59
|
+
parts = tuple(remove_extra_whitespace(part) for part in text.split("and"))
|
|
60
|
+
parts_normalized = (_parse_normalize(part) for part in parts)
|
|
61
|
+
|
|
62
|
+
authors = tuple(_parse_single(part, bibstring_type) for part in parts_normalized)
|
|
63
|
+
|
|
64
|
+
return Ok(authors)
|
|
65
|
+
|
|
66
|
+
except Exception as e:
|
|
67
|
+
return Err(
|
|
68
|
+
message=f"Could not parse 'author' field with value [[ {text} ]]. {e.__class__.__name__}: {e}",
|
|
69
|
+
code=-1,
|
|
70
|
+
error_type="ParsingError",
|
|
71
|
+
error_trace=f"{traceback.format_exc()}",
|
|
72
|
+
)
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
from philoch_bib_sdk.logic.models import BibStringAttr, MaybeStr, TBibString
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_bib_string_attr(bib_string: MaybeStr[BibStringAttr], bibstring_type: TBibString) -> str:
|
|
5
|
+
"""
|
|
6
|
+
Format a BibStringAttr into a string representation.
|
|
7
|
+
"""
|
|
8
|
+
return "" if not bib_string else getattr(bib_string, bibstring_type, "")
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from philoch_bib_sdk.logic.models import BibKeyAttr, MaybeStr
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def format_bibkey(bibkey: MaybeStr[BibKeyAttr]) -> str:
|
|
5
|
+
|
|
6
|
+
if bibkey == "":
|
|
7
|
+
return ""
|
|
8
|
+
|
|
9
|
+
if bibkey.other_authors:
|
|
10
|
+
authors_l = [bibkey.first_author, bibkey.other_authors]
|
|
11
|
+
else:
|
|
12
|
+
authors_l = [bibkey.first_author]
|
|
13
|
+
|
|
14
|
+
authors = "-".join(authors_l)
|
|
15
|
+
|
|
16
|
+
if isinstance(bibkey.date, int):
|
|
17
|
+
year = f"{bibkey.date}{bibkey.date_suffix}"
|
|
18
|
+
else:
|
|
19
|
+
year = f"{bibkey.date}-{bibkey.date_suffix}" if bibkey.date_suffix else bibkey.date
|
|
20
|
+
|
|
21
|
+
return f"{authors}:{year}"
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
import traceback
|
|
2
|
+
from typing import Tuple
|
|
3
|
+
from aletk.ResultMonad import Ok, Err
|
|
4
|
+
from aletk.utils import get_logger
|
|
5
|
+
from philoch_bib_sdk.logic.literals import TBasicPubState
|
|
6
|
+
from philoch_bib_sdk.logic.models import BibKeyAttr
|
|
7
|
+
|
|
8
|
+
lgr = get_logger(__name__)
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def _parse_bibkey_author(text: str) -> Tuple[str, str]:
|
|
12
|
+
|
|
13
|
+
author_parts = text.split("-")
|
|
14
|
+
|
|
15
|
+
if len(author_parts) == 1:
|
|
16
|
+
first_author = author_parts[0]
|
|
17
|
+
other_authors = ""
|
|
18
|
+
elif len(author_parts) == 2:
|
|
19
|
+
first_author = author_parts[0]
|
|
20
|
+
other_authors = author_parts[1]
|
|
21
|
+
else:
|
|
22
|
+
raise ValueError(
|
|
23
|
+
f"Unexpected bibkey author parts in [[ {text} ]]. Found [[ {author_parts} ]]. Expected 1 author, or 2 authors separated by '-'."
|
|
24
|
+
)
|
|
25
|
+
|
|
26
|
+
return first_author, other_authors
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _parse_bibkey_date_int_part(text: str) -> Tuple[int | None, int | None]:
|
|
30
|
+
|
|
31
|
+
char_index_type_d = {i: (char, char.isdigit()) for i, char in enumerate(text)}
|
|
32
|
+
|
|
33
|
+
year_l: list[str] = []
|
|
34
|
+
int_breakpoint = None
|
|
35
|
+
for i, (char, is_digit) in char_index_type_d.items():
|
|
36
|
+
if is_digit:
|
|
37
|
+
year_l.append(char)
|
|
38
|
+
int_breakpoint = i
|
|
39
|
+
else:
|
|
40
|
+
break
|
|
41
|
+
|
|
42
|
+
if year_l != []:
|
|
43
|
+
year_int = int(f"{''.join(year_l)}")
|
|
44
|
+
else:
|
|
45
|
+
year_int = None
|
|
46
|
+
|
|
47
|
+
if year_int and len(f"{year_int}") > 4:
|
|
48
|
+
raise ValueError(f"Unexpected year value in '{text}': is not a valid year or publication state")
|
|
49
|
+
|
|
50
|
+
return year_int, int_breakpoint
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def _parse_bibkey_date_suffix_part(
|
|
54
|
+
date_parts: str, year_int: int | None, int_breakpoint: int | None
|
|
55
|
+
) -> Tuple[int | TBasicPubState, str]:
|
|
56
|
+
|
|
57
|
+
# Case 1. The first part of the year is a digit
|
|
58
|
+
if int_breakpoint is not None:
|
|
59
|
+
if year_int is None:
|
|
60
|
+
raise ValueError(
|
|
61
|
+
f"Unexpected case! year_int is None but int_breakpoint is not None. This should not happen."
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
date_suffix_raw = date_parts[int_breakpoint + 1 :]
|
|
65
|
+
return (
|
|
66
|
+
year_int,
|
|
67
|
+
date_suffix_raw,
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
if year_int is not None:
|
|
71
|
+
raise ValueError(f"Unexpected case! year_int is None but int_breakpoint is not None. This should not happen.")
|
|
72
|
+
|
|
73
|
+
# Case 2. first characters are non-digits
|
|
74
|
+
# has to start with either "unpub" or "forthcoming" then
|
|
75
|
+
date_suffix_raw = "".join(date_parts)
|
|
76
|
+
|
|
77
|
+
if not (date_suffix_raw.startswith("forthcoming") or date_suffix_raw.startswith("unpub")):
|
|
78
|
+
raise ValueError(f"Unexpected year value in '{date_parts}': it is not a valid publication state.")
|
|
79
|
+
|
|
80
|
+
date_suffix_parts = date_suffix_raw.split("-")
|
|
81
|
+
|
|
82
|
+
if len(date_suffix_parts) == 2:
|
|
83
|
+
suffix = date_suffix_parts[1]
|
|
84
|
+
if not suffix:
|
|
85
|
+
raise ValueError(
|
|
86
|
+
f"Unexpected year value in '{date_parts}': it is not a valid publication state. Expected a suffix after '-'."
|
|
87
|
+
)
|
|
88
|
+
elif len(date_suffix_parts) == 1:
|
|
89
|
+
suffix = ""
|
|
90
|
+
else:
|
|
91
|
+
raise ValueError(f"Unexpected year value in '{date_parts}': it is not a valid publication state.")
|
|
92
|
+
|
|
93
|
+
pubstate: TBasicPubState = ""
|
|
94
|
+
if date_suffix_parts[0] == "unpub":
|
|
95
|
+
pubstate = "unpub"
|
|
96
|
+
elif date_suffix_parts[0] == "forthcoming":
|
|
97
|
+
pubstate = "forthcoming"
|
|
98
|
+
else:
|
|
99
|
+
raise ValueError(f"Unexpected year value in '{date_parts}': it is not a valid publication state.")
|
|
100
|
+
|
|
101
|
+
return pubstate, suffix
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
def parse_bibkey(text: str) -> Ok[BibKeyAttr] | Err:
|
|
105
|
+
"""
|
|
106
|
+
Return either a Bibkey object, or a BibkeyError object to indicate a parsing error.
|
|
107
|
+
"""
|
|
108
|
+
|
|
109
|
+
try:
|
|
110
|
+
bibkey_parts = text.split(":")
|
|
111
|
+
if len(bibkey_parts) != 2:
|
|
112
|
+
raise ValueError(
|
|
113
|
+
f"Unexpected number of bibkey parts in [[ {text} ]]. Expected only two parts separated by ':'."
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
# Parse the author part
|
|
117
|
+
first_author, other_authors = _parse_bibkey_author(bibkey_parts[0])
|
|
118
|
+
|
|
119
|
+
# Parse the date part
|
|
120
|
+
date_parts = bibkey_parts[1]
|
|
121
|
+
|
|
122
|
+
year_int, int_breakpoint = _parse_bibkey_date_int_part(date_parts)
|
|
123
|
+
|
|
124
|
+
# Parse the date suffix part
|
|
125
|
+
date, date_suffix = _parse_bibkey_date_suffix_part(date_parts, year_int, int_breakpoint)
|
|
126
|
+
|
|
127
|
+
return Ok(
|
|
128
|
+
BibKeyAttr(
|
|
129
|
+
first_author=first_author,
|
|
130
|
+
other_authors=other_authors,
|
|
131
|
+
date=date,
|
|
132
|
+
date_suffix=date_suffix,
|
|
133
|
+
)
|
|
134
|
+
)
|
|
135
|
+
|
|
136
|
+
except Exception as e:
|
|
137
|
+
error_message = f"Could not parse bibkey for '{text}'"
|
|
138
|
+
|
|
139
|
+
return Err(
|
|
140
|
+
message=error_message,
|
|
141
|
+
code=-1,
|
|
142
|
+
error_type="BibkeyError",
|
|
143
|
+
error_trace=f"{traceback.format_exc()}",
|
|
144
|
+
)
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
from philoch_bib_sdk.logic.models import VALID_DATE_FORMATS, BibItemDateAttr
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def format_date(date: BibItemDateAttr | Literal["no date"]) -> str:
|
|
6
|
+
|
|
7
|
+
if date == "no date":
|
|
8
|
+
return "no date"
|
|
9
|
+
|
|
10
|
+
match date:
|
|
11
|
+
case BibItemDateAttr(year=year, year_part_2_hyphen=None, year_part_2_slash=None, month=None, day=None):
|
|
12
|
+
return str(year)
|
|
13
|
+
|
|
14
|
+
case BibItemDateAttr(year=year, year_part_2_hyphen=None, year_part_2_slash=None, month=month, day=day) if (
|
|
15
|
+
month is not None and day is not None
|
|
16
|
+
):
|
|
17
|
+
return f"{year}-{str(month).zfill(2)}-{str(day).zfill(2)}"
|
|
18
|
+
|
|
19
|
+
case BibItemDateAttr(year=year, year_part_2_hyphen=None, year_part_2_slash=None, month=month, day=None) if (
|
|
20
|
+
month is not None
|
|
21
|
+
):
|
|
22
|
+
return f"{year}-{str(month).zfill(2)}"
|
|
23
|
+
|
|
24
|
+
case BibItemDateAttr(
|
|
25
|
+
year=year, year_part_2_hyphen=year_part_2_hyphen, year_part_2_slash=None, month=None, day=None
|
|
26
|
+
) if (year_part_2_hyphen is not None):
|
|
27
|
+
return f"{year}-{year_part_2_hyphen}"
|
|
28
|
+
|
|
29
|
+
case BibItemDateAttr(
|
|
30
|
+
year=year, year_part_2_hyphen=None, year_part_2_slash=year_part_2_slash, month=None, day=None
|
|
31
|
+
) if (year_part_2_slash is not None):
|
|
32
|
+
return f"{year}/{year_part_2_slash}"
|
|
33
|
+
|
|
34
|
+
case _:
|
|
35
|
+
raise ValueError(
|
|
36
|
+
f"Invalid date format. Expected one of {', '.join(VALID_DATE_FORMATS)}, but found '{date}'."
|
|
37
|
+
)
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
from aletk.utils import remove_extra_whitespace, get_logger
|
|
2
|
+
from aletk.ResultMonad import Ok, Err
|
|
3
|
+
from typing import Literal
|
|
4
|
+
from philoch_bib_sdk.logic.models import VALID_DATE_FORMATS, BibItemDateAttr
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
lgr = get_logger(__name__)
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def _parse_date(text: str) -> BibItemDateAttr | Literal["no date"]:
|
|
11
|
+
"""
|
|
12
|
+
Parse a single date attribute from a string.
|
|
13
|
+
"""
|
|
14
|
+
text = remove_extra_whitespace(text)
|
|
15
|
+
|
|
16
|
+
if remove_extra_whitespace(text).lower() == "no date":
|
|
17
|
+
return "no date"
|
|
18
|
+
|
|
19
|
+
# Split by potential delimiters (hyphens or slashes)
|
|
20
|
+
parts = text.replace("-", "/").split("/")
|
|
21
|
+
|
|
22
|
+
# Handle the number of parts (could be year, year-year2, year/year_2, year-month-day)
|
|
23
|
+
if len(parts) == 1:
|
|
24
|
+
return BibItemDateAttr(
|
|
25
|
+
year=int(parts[0]), year_part_2_hyphen=None, year_part_2_slash=None, month=None, day=None
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
elif len(parts) == 2 and "-" in text:
|
|
29
|
+
return BibItemDateAttr(
|
|
30
|
+
year=int(parts[0]), year_part_2_hyphen=int(parts[1]), year_part_2_slash=None, month=None, day=None
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
elif len(parts) == 2 and "/" in text:
|
|
34
|
+
return BibItemDateAttr(
|
|
35
|
+
year=int(parts[0]), year_part_2_hyphen=None, year_part_2_slash=int(parts[1]), month=None, day=None
|
|
36
|
+
)
|
|
37
|
+
|
|
38
|
+
elif len(parts) == 3 and "-" in text and len(parts[1]) <= 2 and len(parts[2]) <= 2:
|
|
39
|
+
return BibItemDateAttr(
|
|
40
|
+
year=int(parts[0]), year_part_2_hyphen=None, year_part_2_slash=None, month=int(parts[1]), day=int(parts[2])
|
|
41
|
+
)
|
|
42
|
+
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f"Invalid date format found in '{text}'. Expected one of {', '.join(VALID_DATE_FORMATS)}.")
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
def parse_date(text: str) -> Ok[BibItemDateAttr | Literal["no date"]] | Err:
|
|
48
|
+
"""
|
|
49
|
+
Parse a single date string into a BibItemDateAttr object.
|
|
50
|
+
The input is expected to be a single date, either in the format '<year>' or '<year>-<month>' or '<year>-<month>-<day>' (or slashes instead of hyphens).
|
|
51
|
+
"""
|
|
52
|
+
try:
|
|
53
|
+
return Ok(_parse_date(text))
|
|
54
|
+
|
|
55
|
+
except Exception as e:
|
|
56
|
+
error_message = f"Error parsing date from '{text}': {e}"
|
|
57
|
+
return Err(
|
|
58
|
+
message=error_message,
|
|
59
|
+
code=-1,
|
|
60
|
+
error_type=f"{e.__class__.__name__}",
|
|
61
|
+
error_trace="",
|
|
62
|
+
)
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
from typing import TypedDict
|
|
2
|
+
from aletk.utils import get_logger
|
|
3
|
+
|
|
4
|
+
from philoch_bib_sdk.converters.plaintext.author.formatter import format_author
|
|
5
|
+
from philoch_bib_sdk.converters.plaintext.bib_string_formatter import format_bib_string_attr
|
|
6
|
+
from philoch_bib_sdk.converters.plaintext.bibitem.bibkey_formatter import format_bibkey
|
|
7
|
+
from philoch_bib_sdk.converters.plaintext.bibitem.date_formatter import format_date
|
|
8
|
+
from philoch_bib_sdk.converters.plaintext.bibitem.pages_formatter import format_pages
|
|
9
|
+
from philoch_bib_sdk.converters.plaintext.journal.formatter import format_journal
|
|
10
|
+
from philoch_bib_sdk.logic.literals import TBibTeXEntryType
|
|
11
|
+
from philoch_bib_sdk.logic.models import BibItem
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
lgr = get_logger(__name__)
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
def format_entry_type(entry_type: TBibTeXEntryType) -> str:
|
|
18
|
+
"""
|
|
19
|
+
Format the entry type for the BibItem.
|
|
20
|
+
"""
|
|
21
|
+
match entry_type:
|
|
22
|
+
case "UNKNOWN":
|
|
23
|
+
return "UNKNOWN"
|
|
24
|
+
case _ if entry_type:
|
|
25
|
+
return f"@{entry_type}"
|
|
26
|
+
case _:
|
|
27
|
+
return ""
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class FormattedBibItem(TypedDict, total=True):
|
|
31
|
+
_to_do_general: str
|
|
32
|
+
_change_request: str
|
|
33
|
+
entry_type: str
|
|
34
|
+
bibkey: str
|
|
35
|
+
author: str
|
|
36
|
+
_author_ids: str
|
|
37
|
+
editor: str
|
|
38
|
+
_editor_ids: str
|
|
39
|
+
author_ids: str
|
|
40
|
+
options: str
|
|
41
|
+
shorthand: str
|
|
42
|
+
date: str
|
|
43
|
+
pubstate: str
|
|
44
|
+
title: str
|
|
45
|
+
_title_unicode: str
|
|
46
|
+
booktitle: str
|
|
47
|
+
crossref: str
|
|
48
|
+
journal: str
|
|
49
|
+
journal_id: str
|
|
50
|
+
volume: str
|
|
51
|
+
number: str
|
|
52
|
+
pages: str
|
|
53
|
+
eid: str
|
|
54
|
+
series: str
|
|
55
|
+
address: str
|
|
56
|
+
institution: str
|
|
57
|
+
school: str
|
|
58
|
+
publisher: str
|
|
59
|
+
publisher_id: str
|
|
60
|
+
type: str
|
|
61
|
+
edition: str
|
|
62
|
+
note: str
|
|
63
|
+
_issuetitle: str
|
|
64
|
+
_guesteditor: str
|
|
65
|
+
_extra_note: str
|
|
66
|
+
urn: str
|
|
67
|
+
eprint: str
|
|
68
|
+
doi: str
|
|
69
|
+
url: str
|
|
70
|
+
_kw_level1: str
|
|
71
|
+
_kw_level2: str
|
|
72
|
+
_kw_level3: str
|
|
73
|
+
_epoch: str
|
|
74
|
+
_person: str
|
|
75
|
+
_comm_for_profile_bib: str
|
|
76
|
+
_langid: str
|
|
77
|
+
_lang_der: str
|
|
78
|
+
_further_refs: str
|
|
79
|
+
_depends_on: str
|
|
80
|
+
_dltc_num: str
|
|
81
|
+
_spec_interest: str
|
|
82
|
+
_note_perso: str
|
|
83
|
+
_note_stock: str
|
|
84
|
+
_note_status: str
|
|
85
|
+
_num_inwork_coll: str
|
|
86
|
+
_num_inwork: str
|
|
87
|
+
_num_coll: str
|
|
88
|
+
_dltc_copyediting_note: str
|
|
89
|
+
_note_missing: str
|
|
90
|
+
_num_sort: str
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def format_bibitem(bibitem: BibItem) -> FormattedBibItem:
|
|
94
|
+
|
|
95
|
+
bibkey = format_bibkey(bibitem.bibkey)
|
|
96
|
+
|
|
97
|
+
author = format_author(bibitem.author, "latex")
|
|
98
|
+
editor = format_author(bibitem.editor, "latex")
|
|
99
|
+
person = format_author((bibitem._person,), "latex") if bibitem._person else ""
|
|
100
|
+
|
|
101
|
+
shorthand = ", ".join([author.mononym.latex for author in bibitem.author if author.mononym.latex])
|
|
102
|
+
date = format_date(bibitem.date)
|
|
103
|
+
|
|
104
|
+
pages = format_pages(bibitem.pages)
|
|
105
|
+
|
|
106
|
+
journal = format_journal(bibitem.journal, "latex")
|
|
107
|
+
|
|
108
|
+
crossref = format_bibkey(bibitem.crossref.bibkey) if bibitem.crossref else ""
|
|
109
|
+
|
|
110
|
+
_kw_level1, kw_level2, kw_level3 = (
|
|
111
|
+
bibitem._kws.level_1.name if bibitem._kws else "",
|
|
112
|
+
bibitem._kws.level_2.name if bibitem._kws else "",
|
|
113
|
+
bibitem._kws.level_3.name if bibitem._kws else "",
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
further_refs = ", ".join([format_bibkey(ref) for ref in bibitem._further_refs])
|
|
117
|
+
depends_on = ", ".join([format_bibkey(dep) for dep in bibitem._depends_on])
|
|
118
|
+
|
|
119
|
+
formatted: FormattedBibItem = {
|
|
120
|
+
"_to_do_general": bibitem._to_do_general,
|
|
121
|
+
"_change_request": bibitem._change_request,
|
|
122
|
+
"entry_type": format_entry_type(bibitem.entry_type),
|
|
123
|
+
"bibkey": bibkey,
|
|
124
|
+
"author": author,
|
|
125
|
+
"_author_ids": "",
|
|
126
|
+
"editor": editor,
|
|
127
|
+
"_editor_ids": "",
|
|
128
|
+
"author_ids": "",
|
|
129
|
+
"options": ", ".join(bibitem.options),
|
|
130
|
+
"shorthand": shorthand,
|
|
131
|
+
"date": date,
|
|
132
|
+
"pubstate": bibitem.pubstate,
|
|
133
|
+
"title": format_bib_string_attr(bibitem.title, "latex"),
|
|
134
|
+
"_title_unicode": format_bib_string_attr(bibitem.title, "unicode"),
|
|
135
|
+
"booktitle": format_bib_string_attr(bibitem.booktitle, "latex"),
|
|
136
|
+
"crossref": crossref,
|
|
137
|
+
"journal": journal,
|
|
138
|
+
"journal_id": "",
|
|
139
|
+
"volume": bibitem.volume,
|
|
140
|
+
"number": bibitem.number,
|
|
141
|
+
"pages": pages,
|
|
142
|
+
"eid": bibitem.eid,
|
|
143
|
+
"series": format_bib_string_attr(bibitem.series.name, "latex") if bibitem.series else "",
|
|
144
|
+
"address": format_bib_string_attr(bibitem.address, "latex"),
|
|
145
|
+
"institution": format_bib_string_attr(bibitem.institution, "latex"),
|
|
146
|
+
"school": format_bib_string_attr(bibitem.school, "latex"),
|
|
147
|
+
"publisher": format_bib_string_attr(bibitem.publisher, "latex"),
|
|
148
|
+
"publisher_id": "",
|
|
149
|
+
"type": format_bib_string_attr(bibitem.type, "latex"),
|
|
150
|
+
"edition": str(bibitem.edition) if bibitem.edition is not None else "",
|
|
151
|
+
"note": format_bib_string_attr(bibitem.note, "latex"),
|
|
152
|
+
"_issuetitle": format_bib_string_attr(bibitem.issuetitle, "latex") if bibitem.issuetitle else "",
|
|
153
|
+
"_guesteditor": ", ".join(format_author(tuple(author for author in bibitem._guesteditor), "latex")),
|
|
154
|
+
"_extra_note": format_bib_string_attr(bibitem._extra_note, "latex") if bibitem._extra_note else "",
|
|
155
|
+
"urn": bibitem.urn,
|
|
156
|
+
"eprint": bibitem.eprint,
|
|
157
|
+
"doi": bibitem.doi,
|
|
158
|
+
"url": bibitem.url,
|
|
159
|
+
"_kw_level1": _kw_level1,
|
|
160
|
+
"_kw_level2": kw_level2,
|
|
161
|
+
"_kw_level3": kw_level3,
|
|
162
|
+
"_epoch": bibitem._epoch,
|
|
163
|
+
"_person": person,
|
|
164
|
+
"_comm_for_profile_bib": bibitem._comm_for_profile_bib,
|
|
165
|
+
"_langid": bibitem._langid,
|
|
166
|
+
"_lang_der": bibitem._lang_der,
|
|
167
|
+
"_further_refs": further_refs,
|
|
168
|
+
"_depends_on": depends_on,
|
|
169
|
+
"_dltc_num": str(bibitem._dltc_num) if bibitem._dltc_num is not None else "",
|
|
170
|
+
"_spec_interest": bibitem._spec_interest,
|
|
171
|
+
"_note_perso": bibitem._note_perso,
|
|
172
|
+
"_note_stock": bibitem._note_stock,
|
|
173
|
+
"_note_status": bibitem._note_status,
|
|
174
|
+
"_num_inwork_coll": str(bibitem._num_inwork_coll) if bibitem._num_inwork_coll is not None else "",
|
|
175
|
+
"_num_inwork": bibitem._num_inwork,
|
|
176
|
+
"_num_coll": str(bibitem._num_coll) if bibitem._num_coll is not None else "",
|
|
177
|
+
"_dltc_copyediting_note": bibitem._dltc_copyediting_note,
|
|
178
|
+
"_note_missing": bibitem._note_missing,
|
|
179
|
+
"_num_sort": str(bibitem._num_sort) if bibitem._num_sort is not None else "",
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
return formatted
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
from typing import Tuple
|
|
2
|
+
from philoch_bib_sdk.logic.models import PageAttr
|
|
3
|
+
|
|
4
|
+
|
|
5
|
+
def _pages_single_str(page_pair: PageAttr) -> str:
|
|
6
|
+
return "--".join((page_pair.start, page_pair.end)) if page_pair.end else page_pair.start
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def format_pages(pages: Tuple[PageAttr, ...]) -> str:
|
|
10
|
+
if pages is tuple():
|
|
11
|
+
return ""
|
|
12
|
+
|
|
13
|
+
return ", ".join((_pages_single_str(page_pair) for page_pair in pages))
|