marcgrep 0.3.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.
marcgrep/cli.py ADDED
@@ -0,0 +1,55 @@
1
+ import math
2
+ from typing import BinaryIO, Literal
3
+
4
+ import click
5
+ from pymarc import MARCReader
6
+
7
+ from .filter import Filter
8
+
9
+
10
+ @click.command(help="Find MARC records matching patterns in a file.")
11
+ @click.help_option("-h", "--help")
12
+ @click.argument("file", type=click.File("rb"), default="-")
13
+ @click.option("--count", "-c", help="Count matching records", is_flag=True)
14
+ @click.option(
15
+ "--include", "-i", help="Include matching records (repeatable)", multiple=True
16
+ )
17
+ @click.option(
18
+ "--exclude", "-e", help="Exclude matching records (repeatable)", multiple=True
19
+ )
20
+ @click.option("--limit", "-l", help="Limit number of records to process", type=int)
21
+ @click.version_option(package_name="marcgrep", message="%(prog)s %(version)s")
22
+ def main(
23
+ file: BinaryIO,
24
+ count: bool,
25
+ include: list[str],
26
+ exclude: list[str],
27
+ limit: int,
28
+ ):
29
+ counter = 0
30
+ matched_records = 0
31
+ reader = MARCReader(file)
32
+
33
+ # build a list of filters, start with exclusive because they rule out records quicker
34
+ filters = [Filter(pattern, inclusive=False) for pattern in exclude]
35
+ filters.extend(Filter(pattern) for pattern in include)
36
+
37
+ for record in reader:
38
+ if record:
39
+ counter += 1
40
+ if all(f.match(record) for f in filters):
41
+ matched_records += 1
42
+ if not count:
43
+ print(record)
44
+ if limit and counter >= limit:
45
+ break
46
+
47
+ if count:
48
+ print(matched_records)
49
+
50
+ # non-zero exit if no records match
51
+ return exit(0 if matched_records else 1)
52
+
53
+
54
+ if __name__ == "__main__":
55
+ main()
marcgrep/filter.py ADDED
@@ -0,0 +1,94 @@
1
+ import re
2
+
3
+ from pymarc import Record
4
+
5
+
6
+ def parse_pattern(pattern):
7
+ parts = pattern.split(",")
8
+ # Convert empty strings to None to support "123,,,,value" use case
9
+ parts = [part if part else None for part in parts]
10
+ # 3.10 Match-Case would be better here
11
+ # We prioritize the field, then the value, then the subfield, and indicators come last
12
+ if len(parts) == 1:
13
+ return parts[0], None, None, None, None
14
+ if len(parts) == 2:
15
+ return parts[0], None, None, None, parts[1]
16
+ if len(parts) == 3:
17
+ return parts[0], None, None, parts[1], parts[2]
18
+ if len(parts) == 4:
19
+ return parts[0], parts[1], None, parts[2], parts[3]
20
+ # combine the rest into the value, convert Nones past the 4th index back to empty string
21
+ value = ",".join([p if p else "" for p in parts[4:]])
22
+ return parts[0], parts[1], parts[2], parts[3], value
23
+
24
+
25
+ class FilterFormatError(Exception):
26
+ pass
27
+
28
+
29
+ class Filter:
30
+ def __init__(self, pattern, inclusive=True) -> None:
31
+ """Pettern is a comma-separated string like
32
+ FIELD,INDICATOR1,INDICATOR2,SUBFIELD,VALUE
33
+ where only FIELD is required and VALUE can itself contain commas
34
+ e.g. 245,cat in the hat -> field:245, value:cat in the hat
35
+ 245,4,0,a,cat in the hat -> field:245, ind1:4, ind2:0, subfield:a, value:cat in the hat
36
+ """
37
+ self.inclusive = inclusive
38
+ self.pattern = pattern
39
+ parts = parse_pattern(pattern)
40
+ self.field: str | None = parts[0]
41
+ self.ind1: str | None = parts[1]
42
+ self.ind2: str | None = parts[2]
43
+ self.subfield: str | None = parts[3]
44
+ self.value: str | None = parts[4]
45
+ self.validate()
46
+
47
+ def __repr__(self) -> str:
48
+ return f"{'Inclusive' if self.inclusive else 'Exclusive'} Filter: field={self.field}, ind1={self.ind1}, ind2={self.ind2}, subfield={self.subfield}, value={self.value}"
49
+
50
+ def validate(self) -> None:
51
+ if not self.field:
52
+ raise FilterFormatError(
53
+ "Field is required; filter pattern cannot be empty."
54
+ )
55
+ if len(self.field) != 3 or not self.field.isdigit():
56
+ raise FilterFormatError(
57
+ f"Field {self.field} is invalid, must be three digits 000-999."
58
+ )
59
+ if self.ind1 is not None and (len(self.ind1) != 1 or not self.ind1.isdigit()):
60
+ raise FilterFormatError(
61
+ f"Indicator 1 {self.ind1} is invalid, must one one digit 0-9."
62
+ )
63
+ if self.ind2 is not None and (len(self.ind2) != 1 or not self.ind2.isdigit()):
64
+ raise FilterFormatError(
65
+ f"Indicator 2 {self.ind2} is invalid, must one one digit 0-9."
66
+ )
67
+ if self.subfield is not None and len(self.subfield) != 1:
68
+ raise FilterFormatError(
69
+ f"Subfield {self.subfield} is invalid, must be one character."
70
+ )
71
+
72
+ def match(self, record: Record) -> bool:
73
+ result = self.inclusive
74
+ for field in record.get_fields(self.field):
75
+ # print(re.match(self.value, field.format_field()))
76
+ if self.ind1 and field.indicator1 != self.ind1:
77
+ continue
78
+ if self.ind2 and field.indicator2 != self.ind2:
79
+ continue
80
+ if self.subfield:
81
+ subfields = field.get_subfields(self.subfield)
82
+ if (
83
+ self.value
84
+ and not any(
85
+ re.search(self.value, subfield) for subfield in subfields
86
+ )
87
+ or not len(subfields)
88
+ ):
89
+ continue
90
+ if self.value and not re.search(self.value, field.format_field()):
91
+ continue
92
+ # if we get here, the filter matched
93
+ return result
94
+ return not result
@@ -0,0 +1,108 @@
1
+ Metadata-Version: 2.1
2
+ Name: marcgrep
3
+ Version: 0.3.0
4
+ Summary: search MARC files for regex matches
5
+ Home-page: https://github.com/phette23/marcgreppy
6
+ License: MIT
7
+ Keywords: marc,grep,regex,libraries,cli,metadata,bibliographic,cataloging
8
+ Author: phette23
9
+ Author-email: phette23@gmail.com
10
+ Requires-Python: >3.8
11
+ Classifier: Development Status :: 4 - Beta
12
+ Classifier: Environment :: Console
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Operating System :: OS Independent
15
+ Classifier: Programming Language :: Python :: 3
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: Topic :: Utilities
21
+ Requires-Dist: click (==8.1.7)
22
+ Requires-Dist: pymarc (==5.1.2)
23
+ Project-URL: Issues, https://github.com/phette23/marcgreppy/issues
24
+ Project-URL: Repository, https://github.com/phette23/marcgreppy
25
+ Description-Content-Type: text/markdown
26
+
27
+ # marcgrep
28
+
29
+ A CLI for searching MARC files like [MARCgrep.pl](https://pusc.it/bib/MARCgrep) but in Python and a bit different syntax.
30
+
31
+ [marcli](https://github.com/hectorcorrea/marcli) is also a similar project that's faster but a little less flexible.
32
+
33
+ ## Installation
34
+
35
+ Python 3.9 or later. [Poetry](https://python-poetry.org/) is used for development.
36
+
37
+ ```sh
38
+ pipx install marcgrep # install globally with pipx
39
+ pip install marcgrep # or use pip/pip3
40
+ ```
41
+
42
+ ## Usage
43
+
44
+ ```sh
45
+ # general command format
46
+ $ marcgrep OPTIONS FILE.mrc
47
+ $ cat FILE.mrc | marcgrep OPTIONS
48
+ # full usage information
49
+ $ marcgrep -h
50
+ Usage: marcgrep [OPTIONS] [FILE]
51
+
52
+ Find MARC records matching patterns in a file.
53
+
54
+ Options:
55
+ -h, --help Show this message and exit.
56
+ -c, --count Count matching records
57
+ -i, --include TEXT Include matching records (repeatable)
58
+ -e, --exclude TEXT Exclude matching records (repeatable)
59
+ -l, --limit INTEGER Limit number of records to process
60
+ --version Show the version and exit.
61
+ ```
62
+
63
+ The `--include` and `--exclude` flags can be used multiple times to specify multiple criteria. They accept a pattern which is a sort of comma-separated filter expression for matching MARC fields. Examples:
64
+
65
+ ```sh
66
+ # records with a 780 field
67
+ $ marcgrep -i 780 FILE.mrc
68
+ # records with Ulysses in the 245 field
69
+ $ marcgrep -i '245,Ulysses' FILE.mrc
70
+ # titles _without_ "Collected Poems" in the 245 $a subfield
71
+ $ marcgrep -e '245,a,Collected Poems' FILE.mrc
72
+ # titles with second indicator = 4 that do not start with "The "
73
+ $ marcgrep -i '245,,4,,^(?!The )' FILE.mrc
74
+ ```
75
+
76
+ The meaning of the pattern's components depends upon their number:
77
+
78
+ - 1: field, `910` -> 910 is in record
79
+ - 2: field and value (regular expression), `100,Lorde` -> 100 contains string "Lorde"
80
+ - 3: field, subfield, and value, `506,a,Open Access` -> 506$a contains string "Open Access"
81
+ - 4: field, subfield, first indicator, and value, `856,0,u,@lcsh\.gov` -> 856$u with 1st indicator 0 contains string "@lcsh.gov"
82
+ - 5: field, subfield, first & second indicators, and value, `245,0,4,a,The Communist Manifesto`
83
+
84
+ The intention of this syntax is to facilitate searching subfields and field values more easily than MARCgrep.pl since we care about them more often than indicators. To ignore a component but use one of lesser priority, leave the component empty. For instance, `856,s,` refers to records with an `856` field with a `$s` subfield but the trailing comma means we don't care about the subfield's value. The pattern `245,,4,,` refers to records with a `245` field with a second indicator of `4` regardless its subfields or value.
85
+
86
+ Multiple criteria are combined with logical AND. Multiple `--include` flags is narrower than one, as is an `--include` and an `--exclude`.
87
+
88
+ ## Development
89
+
90
+ - [x] -c count
91
+ - [x] -v version
92
+ - [x] -l limit (number of records to process)
93
+ - [x] -i include criteria (multiple)
94
+ - [x] -e exclude criteria (multiple)
95
+ - [ ] -f fields to print
96
+ - [ ] work with MARC leader
97
+ - [ ] regex for all components? e.g. `24.,text in any 240-249 field`
98
+ - [ ] relatedly, specify _not_ to treat value as a regex?
99
+
100
+ ```sh
101
+ poetry install # install dependencies
102
+ poetry run pytest # run tests
103
+ ```
104
+
105
+ ## License
106
+
107
+ [MIT](https://opensource.org/license/mit) © Eric Phetteplace 2024.
108
+
@@ -0,0 +1,6 @@
1
+ marcgrep/cli.py,sha256=Hh0OUY3NriRH7NpwLIMAEcuGzCjPO46c2Qn3ESFo5Gs,1596
2
+ marcgrep/filter.py,sha256=mxZu1D7wwRgUmZNZqYwRD7LmGrL7aw7fS59JK5S65xE,3840
3
+ marcgrep-0.3.0.dist-info/METADATA,sha256=jSmVGPZYjLgRwpJ-CtyfHf-PAFU6XBU-9hXMrNvHSLE,4270
4
+ marcgrep-0.3.0.dist-info/WHEEL,sha256=sP946D7jFCHeNz5Iq4fL4Lu-PrWrFsgfLXbbkciIZwg,88
5
+ marcgrep-0.3.0.dist-info/entry_points.txt,sha256=bSFMiZ0ZiNlC2jZyRSnm7YePHoRKad1QpsnPRpDBDkA,46
6
+ marcgrep-0.3.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: poetry-core 1.9.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ marcgrep=marcgrep.cli:main
3
+