versiref 0.1.0__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.
versiref/ref_style.py ADDED
@@ -0,0 +1,127 @@
1
+ """RefStyle definitions for Bible reference formatting and parsing.
2
+
3
+ This module provides the RefStyle class which defines how Bible references
4
+ are converted to and from strings.
5
+ """
6
+
7
+ import json
8
+ from dataclasses import dataclass, field
9
+ from importlib import resources
10
+ from typing import Union
11
+
12
+
13
+ def _invert(d: dict[str, str]) -> dict[str, str]:
14
+ """Invert an ID->name dictionary, resolving conflicts if possible.
15
+
16
+ In the event of a PSA/PSAS conflict or a EST/ESG conflict, the former of the
17
+ pair is preferred. Any other conflict will raise a ValueError.
18
+ """
19
+ inverted: dict[str, str] = {}
20
+ for k, v in d.items():
21
+ if v in inverted:
22
+ if inverted[v] == "PSA" or inverted[v] == "PSAS":
23
+ inverted[v] = "PSA"
24
+ elif inverted[v] == "EST" or inverted[v] == "ESG":
25
+ inverted[v] = "EST"
26
+ else:
27
+ raise ValueError(f"Both {inverted[v]} and {k} are abbreviated as {v}.")
28
+ else:
29
+ inverted[v] = k
30
+ return inverted
31
+
32
+
33
+ @dataclass
34
+ class RefStyle:
35
+ """Defines how a SimpleBibleRef is converted to and from strings.
36
+
37
+ A RefStyle primarily holds data that specifies the formatting conventions
38
+ for Bible references. Formatting and parsing is done by other classes
39
+ that use a RefStyle as a specification.
40
+
41
+ Attributes:
42
+ names: Maps Bible book IDs to string abbreviations or full names
43
+ chapter_verse_separator: Separates chapter number from verse ranges
44
+ range_separator: Separates the ends of a range. Defaults to an en dash.
45
+ following_verse: indicates the range ends at the verse following the start
46
+ following_verses: indicates the range continues for an unspecified number of verses
47
+ verse_range_separator: Separates ranges of verses in a single chapter
48
+ chapter_separator: Separates ranges of verses in different chapters
49
+ recognized_names: Maps abbreviations/names to Bible book IDs for parsing
50
+
51
+ """
52
+
53
+ names: dict[str, str]
54
+ chapter_verse_separator: str = ":"
55
+ range_separator: str = "–"
56
+ following_verse: str = "f"
57
+ following_verses: str = "ff"
58
+ verse_range_separator: str = ", "
59
+ chapter_separator: str = "; "
60
+ recognized_names: dict[str, str] = field(default_factory=dict)
61
+
62
+ def __post_init__(self) -> None:
63
+ """Initialize recognized_names if not provided.
64
+
65
+ By default, recognized_names is the inverse of names, allowing
66
+ parsing of the same abbreviations used for formatting.
67
+ """
68
+ if not self.recognized_names:
69
+ self.recognized_names = _invert(self.names)
70
+
71
+ def also_recognize(self, names: Union[dict[str, str], str]) -> None:
72
+ """Add a set of book names to the recognized_names mapping.
73
+
74
+ In the event of a conflict, the existing name will be preferred.
75
+
76
+ Args:
77
+ names: Either dictionary mapping names or abbreviations to book IDs
78
+ or a string that names a standard set of names, e.g.,
79
+ "en-sbl_abbreviations".
80
+
81
+ """
82
+ if isinstance(names, str):
83
+ names = _invert(standard_names(names))
84
+ self.recognized_names.update(
85
+ {
86
+ name: id
87
+ for name, id in names.items()
88
+ if name not in self.recognized_names
89
+ }
90
+ )
91
+
92
+
93
+ def standard_names(identifier: str) -> dict[str, str]:
94
+ """Load and return a standard set of book names.
95
+
96
+ These can be passed to RefStyle(). Since the return value is freshly
97
+ created, it can be modified to customize the abbreviations (e.g,
98
+ names["SNG"] = "Cant") without fear of changing the set of names for other
99
+ callers.
100
+
101
+ Args:
102
+ identifier: The identifier for the names file, e.g.,
103
+ "en-sbl_abbreviations"
104
+
105
+ Returns:
106
+ A dictionary mapping book IDs to names or abbreviations.
107
+
108
+ Raises:
109
+ FileNotFoundError: If the names file doesn't exist
110
+ json.JSONDecodeError: if the file contains invalid JSON
111
+ ValueError: If the JSON is not in the expected format
112
+ The latter two represent internal errors in the package.
113
+
114
+ """
115
+ # Use importlib.resources to find the file
116
+ data = json.loads(
117
+ resources.files("versiref")
118
+ .joinpath("data", "book_names", f"{identifier}.json")
119
+ .read_text()
120
+ )
121
+ if not isinstance(data, dict):
122
+ raise ValueError(f"Invalid format in {identifier}.json: not a dictionary")
123
+ if not all(isinstance(k, str) and isinstance(v, str) for k, v in data.items()):
124
+ raise ValueError(
125
+ f"Invalid format in {identifier}.json: all keys and values must be strings"
126
+ )
127
+ return data
@@ -0,0 +1,135 @@
1
+ """Versification class for handling Bible chapter and verse divisions."""
2
+
3
+ import json
4
+ from dataclasses import dataclass, field
5
+ from importlib import resources
6
+ from typing import Optional
7
+
8
+
9
+ @dataclass
10
+ class Versification:
11
+ """Represents a way of dividing the text of the Bible into chapters and verses.
12
+
13
+ Versifications are defined by JSON data that is loaded from a file when an instance is created.
14
+ The class provides methods to query information about the versification, such as the last verse
15
+ of a given chapter in a given book.
16
+ """
17
+
18
+ max_verses: dict[str, list[int]] = field(default_factory=dict)
19
+ identifier: Optional[str] = None
20
+
21
+ @classmethod
22
+ def from_file(
23
+ cls, file_path: str, identifier: Optional[str] = None
24
+ ) -> "Versification":
25
+ """Create an instance from a JSON file.
26
+
27
+ Args:
28
+ file_path: path to a JSON file containing an object with maxVerses
29
+ identifier: identifier to store in the constructed Versififaction
30
+ Raises:
31
+ FileNotFoundError: file_path does not exist
32
+ json.JSONDecodeError: file_path is not well-formed JSON
33
+ ValueError: file_path does not match schema
34
+ Returns:
35
+ Newly constructed Versification
36
+
37
+ """
38
+ with open(file_path, "r", encoding="utf-8") as f:
39
+ data = json.load(f)
40
+ if "maxVerses" in data:
41
+ # Convert string verse counts to integers
42
+ max_verses = {}
43
+ for book, verses in data["maxVerses"].items():
44
+ max_verses[book] = [int(v) for v in verses]
45
+ return cls(max_verses, identifier)
46
+ else:
47
+ raise ValueError("Versification file does not match schema")
48
+
49
+ @classmethod
50
+ def named(cls, identifier: str) -> "Versification":
51
+ """Create an instance of a standard versification.
52
+
53
+ Constructs an instance by loading JSON data from the package's data
54
+ directory.
55
+
56
+ Args:
57
+ identifier: Standard versification identifier (e.g., "org", "eng",
58
+ "LXX", "Vulgata")
59
+ This is converted to lowercase to find the file to load.
60
+
61
+ Raises:
62
+ FileNotFoundError: If the named file doesn't exist
63
+ json.JSONDecodeError: if the file contains invalid JSON ValueError: If the JSON is not in the expected format
64
+ The latter two represent internal errors in the package.
65
+
66
+ Returns:
67
+ A newly constructed Versification
68
+
69
+ """
70
+ filename = f"{identifier.lower()}.json"
71
+
72
+ path = resources.files("versiref").joinpath("data", "versifications", filename)
73
+ if path.is_file():
74
+ return cls.from_file(str(path), identifier)
75
+ else:
76
+ raise FileNotFoundError(f"Unknown versification identifier: {identifier}")
77
+
78
+ def includes(self, book_id: str) -> bool:
79
+ """Check if the given book ID is included in this versification.
80
+
81
+ Args:
82
+ book_id: The book ID (using Paratext three-letter codes)
83
+
84
+ Returns:
85
+ True if the book is included in this versification, False otherwise.
86
+
87
+ """
88
+ if book_id == "PSAS": # Plural form of PSA
89
+ book_id = "PSA"
90
+ return book_id in self.max_verses
91
+
92
+ def is_single_chapter(self, book: str) -> bool:
93
+ """Check if the given book is a single-chapter book.
94
+
95
+ Args:
96
+ book: The book ID (using Paratext three-letter codes)
97
+
98
+ Returns:
99
+ True if the book has only one chapter, False otherwise.
100
+
101
+ """
102
+ if book not in self.max_verses:
103
+ return False
104
+ # The plural form of PSA requires special handling.
105
+ if book == "PSAS":
106
+ book = "PSA"
107
+ return len(self.max_verses[book]) == 1
108
+
109
+ def last_verse(self, book: str, chapter: int) -> int:
110
+ """Return the number of the last verse of the given chapter of the given book.
111
+
112
+ Args:
113
+ book: The book ID (using Paratext three-letter codes)
114
+ chapter: The chapter number
115
+
116
+ Returns:
117
+ The number of the last verse, or -1 if the book or chapter doesn't exist
118
+
119
+ """
120
+ # Trivial implementation returns 99 for any book and chapter
121
+ if not self.max_verses:
122
+ return 99
123
+
124
+ # Check if the book exists in the versification
125
+ if book == "PSAS": # plural of PSA
126
+ book = "PSA"
127
+ if book not in self.max_verses:
128
+ return -1
129
+
130
+ # Check if the chapter exists in the book
131
+ if chapter < 0 or chapter > len(self.max_verses[book]):
132
+ return -1
133
+
134
+ # Return the verse count as an integer
135
+ return self.max_verses[book][chapter - 1]
@@ -0,0 +1,127 @@
1
+ Metadata-Version: 2.4
2
+ Name: versiref
3
+ Version: 0.1.0
4
+ Summary: A package for sophisticated parsing, manipulation, and printing of references to the Bible
5
+ Project-URL: Homepage, https://github.com/fiapps/versiref
6
+ Project-URL: Issues, https://github.com/fiapps/versiref/issues
7
+ Author-email: "Fr. John Lawrence M. Polis" <emptier-sank-dose@duck.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Development Status :: 3 - Alpha
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: Intended Audience :: Religion
13
+ Classifier: Operating System :: OS Independent
14
+ Classifier: Programming Language :: Python :: 3
15
+ Classifier: Programming Language :: Python :: 3 :: Only
16
+ Classifier: Programming Language :: Python :: 3.9
17
+ Classifier: Programming Language :: Python :: 3.10
18
+ Classifier: Programming Language :: Python :: 3.11
19
+ Classifier: Programming Language :: Python :: 3.12
20
+ Classifier: Programming Language :: Python :: 3.13
21
+ Classifier: Topic :: Religion
22
+ Classifier: Topic :: Text Processing
23
+ Requires-Python: >=3.9
24
+ Requires-Dist: pyparsing>=3.2.3
25
+ Description-Content-Type: text/markdown
26
+
27
+ # VersiRef
28
+
29
+ VersiRef is a Python package for sophisticated parsing, manipulation, and printing of references to the Bible.
30
+
31
+ ## Features
32
+
33
+ - Parse and manipulate Bible references.
34
+ - Support for different versification systems (e.g., original language \[BHS + UBS GNT\], "English," LXX, Vulgate).
35
+ - Query information about chapters and verses in different books of the Bible.
36
+
37
+ ## Overview of `versiref` classes
38
+
39
+ - `BibleRef` represents a sequence of verse ranges within one or more books of the Bible.
40
+ - It has a `Versification` and a list of `SimpleBibleRef`s.
41
+ - `Versification` represents a division of the Bible into chapters and verses. Different texts of the Bible do this differently.
42
+ - `SimpleBibleRef` represents a sequence of verse ranges within a single book of the Bible.
43
+ - It can be used independently of a `BibleRef`, but since it has no `Versification`, you need to supply a `Versification` for all operations that require one.
44
+ - It contains a list of `VerseRange`s: this is a data class not intended to be used independently.
45
+ - `RefStyle` defines separators and book names or abbreviations to use in formatting or parsing a reference.
46
+ - This gives the package the versatility to handle different styles and languages.
47
+ - `RefParser` creates PEG parsers based on a `RefStyle` and `Versification`.
48
+ - These can be used to parse a single reference or to find all references in a long string, e.g., the content of a text file.
49
+
50
+ ## Examples
51
+
52
+ ### Convert references from one style to another
53
+
54
+ ```python
55
+ from pyparsing import ParseException
56
+ from versiref import RefParser, RefStyle, standard_names, Versification
57
+
58
+ def parse_reference(reference: str) -> None:
59
+ """Parse argument and print result."""
60
+ # Expect SBL abbreviations in the input
61
+ sbl_abbrevs = standard_names("en-sbl_abbreviations")
62
+ sbl_style = RefStyle(names=sbl_abbrevs)
63
+ # Use typical versification for English Bibles
64
+ versification = Versification.named("eng")
65
+ # Build parser for style
66
+ parser = RefParser(sbl_style, versification)
67
+ # Use Italian CEI Bible style for output
68
+ cei_abbrevs = standard_names("it-cei_abbreviazioni")
69
+ cei_style = RefStyle(names=cei_abbrevs, chapter_verse_separator=",", verse_range_separator=".")
70
+ try:
71
+ ref = parser.parse(reference, silent=False)
72
+ # Check whether it refers to verse ranges that exist
73
+ if not ref.is_valid():
74
+ print(f"Warning: {reference} is not a valid reference.")
75
+ print(ref.format(cei_style))
76
+ except ParseException as e:
77
+ print(f"Could not parse reference: {reference}")
78
+ print(e)
79
+
80
+ # Prints "Gv 7,53–8,11"
81
+ parse_reference("John 7:53–8:11")
82
+ ```
83
+
84
+ ### Look for references in a string
85
+
86
+ ```python
87
+ from versiref import RefParser, RefStyle, Versification, standard_names
88
+
89
+ def scan_string(content: str) -> None:
90
+ """Count and print the Bible references in content."""
91
+ # Use SBL abbreviations
92
+ sbl_abbrevs = standard_names("en-sbl_abbreviations")
93
+ style = RefStyle(names=sbl_abbrevs)
94
+ # But also recognize abbreviations from the Chicago Manual of Style
95
+ style.also_recognize("en-cmos_short")
96
+ # Use typical versification for English Bibles
97
+ versification = Versification.named("eng")
98
+ # Build parser for style
99
+ parser = RefParser(style, versification)
100
+ count = 0
101
+ for ref, start, end in parser.scan_string(content):
102
+ # Check for out-of-range chapter or verse
103
+ if not ref.is_valid():
104
+ print(f"Warning: {ref.format(style)} is not a valid reference.")
105
+ else:
106
+ # Print in SBL style
107
+ print(ref.format(style))
108
+ count += 1
109
+ print(f"Found {count} references.")
110
+
111
+ text = """
112
+ Today's readings are Acts 2:1-11; Ps 104:1,24,29-30,31,34; 1 Cor 12:3b-7,12-13; Jn 20:19-23.
113
+ The prophecy in Is 7:10-14 is important background for Lk 1:26-38.
114
+ The Septuagint has a Ps 118:120, but English Bibles don't.
115
+ """
116
+ scan_string(text)
117
+ ```
118
+
119
+ Output:
120
+
121
+ ```
122
+ Acts 2:1–11; Ps 104:1, 24, 29–30, 31, 34; 1 Cor 12:3b–7, 12–13; John 20:19–23
123
+ Isa 7:10–14
124
+ Luke 1:26–38
125
+ Warning: Ps 118:120 is not a valid reference.
126
+ Found 4 references.
127
+ ```
@@ -0,0 +1,24 @@
1
+ versiref/__init__.py,sha256=3uQ8CnJzBRxQLgoi52Mo4enh3MvarDyya9SVzZdDrp4,601
2
+ versiref/bible_ref.py,sha256=12H25dlIBbZWElsQYNz_ZujFOzr0TXncuDB_23edjW8,16067
3
+ versiref/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
4
+ versiref/ref_parser.py,sha256=ef359WpR_tlTumym28_MaH6Zp0dyVixTEm8eHkmIyqQ,22266
5
+ versiref/ref_style.py,sha256=iRPCPqbgJDDvRPmFixLU2IGMDA0inMOZ3mdAB57354Q,4672
6
+ versiref/versification.py,sha256=FQsKJ35YspRLBzkjWSRs6jVOVx1xVqELICJYGQW3LGk,4712
7
+ versiref/data/book_names/en-cmos_long.json,sha256=nxSutcEoBVXHvA3UwzKi3Y8z7NEqr_8lTAMelsoDSWc,1517
8
+ versiref/data/book_names/en-cmos_short.json,sha256=8ZO3d3wXGL3FSYj38PBdepjOkbQoSPBNq8_zw-PDGTc,1403
9
+ versiref/data/book_names/en-sbl_abbreviations.json,sha256=8GenBCjOqFUbH25Kzw1eaxb2LMxqJBh8F0UI5peqhzc,1575
10
+ versiref/data/book_names/en-sbl_names.json,sha256=DL3EnhZzAlvYQW0m7rRfrZhDWakVaWrMk5Y-nSivnjQ,1979
11
+ versiref/data/book_names/it-cei_abbreviazioni.json,sha256=U9j2SkqMjJfKb9QHz0rjxjZov6Da7p4tGqDd5p34Sf0,1157
12
+ versiref/data/book_names/it-cei_nomi.json,sha256=oDAmVxZF-8bvjsUb0uU1KpYFaqHE3Qv887ttY_FWTiI,1524
13
+ versiref/data/versifications/eng.json,sha256=UWICP44GBPzpaknX-4zqc-9pmSiEbRDWIhC9SILoEzg,27681
14
+ versiref/data/versifications/lxx.json,sha256=SWYDojZV6Wub4wFCt1snrpxp5LTZXPYKwgpvSejR9Ak,33953
15
+ versiref/data/versifications/nabre.json,sha256=K66K6csQhh5TssMFz9Tsmnu-X04dkea-VEt7q8seFjk,15468
16
+ versiref/data/versifications/nova-vulgata.json,sha256=4J6gAuiaqGzfvdTLabH_LHUHCAHgZIw3rvWA1PyNBqA,33317
17
+ versiref/data/versifications/org.json,sha256=O44GX7WSEAJl_5686nH1N2kn-gPAhYRPMFjUY6Lpysc,21077
18
+ versiref/data/versifications/rsc.json,sha256=znZgOjXbV_0OD9A3Oqxu9N-luzjwjXmlv67JiGsxPok,23139
19
+ versiref/data/versifications/rso.json,sha256=Re9PapvlfZs-bogtcEmRp8Sxtk2ftWGoQkAbqIJjNAU,29748
20
+ versiref/data/versifications/vulgata.json,sha256=MI4u2oQbBhPchjuJOunUsNJg1MeAqStPF7DcU42w-MY,33617
21
+ versiref-0.1.0.dist-info/METADATA,sha256=WAzGnjJaNnFLL4r0sWT2v-EdNhYEEH6l8xrV7lK0LWw,5228
22
+ versiref-0.1.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
23
+ versiref-0.1.0.dist-info/licenses/LICENSE,sha256=15zx8RJP7jG7okR50pC9aCpkaWcCzGDBzKif3PMBXPo,1073
24
+ versiref-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.27.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2025 Michael F. Polis
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.