From ed2074ae88f2db6cb6b38716b43b35e29eb2e16c Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Fri, 23 Dec 2022 16:25:51 +0100 Subject: filematcher: check file properties, formulate them as a string --- bsie/base/errors.py | 3 + bsie/utils/__init__.py | 2 + bsie/utils/filematcher/__init__.py | 20 +++++ bsie/utils/filematcher/matcher.py | 177 +++++++++++++++++++++++++++++++++++++ bsie/utils/filematcher/parser.py | 148 +++++++++++++++++++++++++++++++ 5 files changed, 350 insertions(+) create mode 100644 bsie/utils/filematcher/__init__.py create mode 100644 bsie/utils/filematcher/matcher.py create mode 100644 bsie/utils/filematcher/parser.py (limited to 'bsie') diff --git a/bsie/base/errors.py b/bsie/base/errors.py index dc3c30e..5fafd5b 100644 --- a/bsie/base/errors.py +++ b/bsie/base/errors.py @@ -39,4 +39,7 @@ class ProgrammingError(_BSIEError): class UnreachableError(ProgrammingError): """Bravo, you've reached a point in code that should logically not be reachable.""" +class ParserError(_BSIEError): + """Failed to parse due to invalid syntax or structures.""" + ## EOF ## diff --git a/bsie/utils/__init__.py b/bsie/utils/__init__.py index bd22236..3981dc7 100644 --- a/bsie/utils/__init__.py +++ b/bsie/utils/__init__.py @@ -11,9 +11,11 @@ import typing from . import bsfs from . import namespaces as ns from . import node +from . import filematcher # exports __all__: typing.Sequence[str] = ( + 'filematcher', 'bsfs', 'node', 'ns', diff --git a/bsie/utils/filematcher/__init__.py b/bsie/utils/filematcher/__init__.py new file mode 100644 index 0000000..b1c1b45 --- /dev/null +++ b/bsie/utils/filematcher/__init__.py @@ -0,0 +1,20 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import typing + +# inner-module imports +from .matcher import Matcher +from .parser import parse + +# exports +__all__: typing.Sequence[str] = ( + 'Matcher', + 'parse', + ) + +## EOF ## diff --git a/bsie/utils/filematcher/matcher.py b/bsie/utils/filematcher/matcher.py new file mode 100644 index 0000000..164beeb --- /dev/null +++ b/bsie/utils/filematcher/matcher.py @@ -0,0 +1,177 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2021 +""" +# imports +from collections.abc import Callable, Collection, Hashable +import abc +import os +import typing +import magic + +# exports +__all__: typing.Sequence[str] = [] + + +## code ## + +# abstract nodes + +class Matcher(abc.ABC, Hashable, Callable, Collection): # type: ignore [misc] # Invalid base class Callable + """Matcher node base class.""" + + # child expressions or terminals + _childs: typing.Set[typing.Any] + + def __init__(self, *childs: typing.Any): + if len(childs) == 1 and isinstance(childs[0], (list, tuple, set)): + self._childs = set(childs[0]) + else: + self._childs = set(childs) + + def __contains__(self, needle: typing.Any) -> bool: + return needle in self._childs + + def __iter__(self) -> typing.Iterator[typing.Any]: + return iter(self._childs) + + def __len__(self) -> int: + return len(self._childs) + + def __repr__(self) -> str: + return f'{type(self).__name__}({self._childs})' + + def __hash__(self) -> int: + return hash((type(self), tuple(set(self._childs)))) + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, type(self)) \ + and self._childs == other._childs + + @abc.abstractmethod + def __call__(self, path: str) -> bool: # pylint: disable=arguments-differ + """Check if *path* satisfies the conditions set by the Matcher instance.""" + +class NOT(Matcher): + """Invert a matcher result.""" + def __init__(self, expr: Matcher): + super().__init__(expr) + def __call__(self, path: str) -> bool: + return not next(iter(self._childs))(path) + +# aggregate nodes + +class Aggregate(Matcher): # pylint: disable=too-few-public-methods # Yeah, it's an interface... + """Aggregation function base class (And, Or).""" + +class And(Aggregate): + """Accept only if all conditions are satisfied.""" + def __call__(self, path: str) -> bool: + for itm in self: + if not itm(path): + return False + return True + +class Or(Aggregate): + """Accept only if at least one condition is satisfied.""" + def __call__(self, path: str) -> bool: + for itm in self: + if itm(path): + return True + return False + + +# criteria nodes + +class Criterion(Matcher): + """Criterion base class. Limits acceptance to certain values.""" + def accepted(self) -> typing.Set[typing.Any]: + """Return a set of accepted values.""" + return self._childs + +# criteria w/o value (valueless) + +class Any(Criterion): + """Accepts anything.""" + def __call__(self, path: str) -> bool: + return True + +class Nothing(Criterion): + """Accepts nothing.""" + def __call__(self, path: str) -> bool: + return False + +class Exists(Criterion): + """Filters by existence.""" + def __call__(self, path: str) -> bool: + return os.path.exists(path) + +class IsFile(Criterion): + """Checks if the path is a regular file.""" + def __call__(self, path: str) -> bool: + return os.path.isfile(path) + +class IsDir(Criterion): + """Checks if the path is a directory.""" + def __call__(self, path: str) -> bool: + return os.path.isdir(path) + +class IsLink(Criterion): + """Checks if the path is a link.""" + def __call__(self, path: str) -> bool: + return os.path.islink(path) + +class IsAbs(Criterion): + """Checks if the path is an absolute path.""" + def __call__(self, path: str) -> bool: + return os.path.isabs(path) + +class IsRel(Criterion): + """Checks if the path is a relative path.""" + def __call__(self, path: str) -> bool: + return not os.path.isabs(path) + +class IsMount(Criterion): + """Checks if the path is a mount point.""" + def __call__(self, path: str) -> bool: + return os.path.ismount(path) + +class IsEmpty(Criterion): + """Checks if the path is an empty file.""" + def __call__(self, path: str) -> bool: + return os.path.exists(path) and os.stat(path).st_size == 0 + +class IsReadable(Criterion): + """Checks if the path is readable.""" + def __call__(self, path: str) -> bool: + return os.path.exists(path) and os.access(path, os.R_OK) + +class IsWritable(Criterion): + """Checks if the path is writable.""" + def __call__(self, path: str) -> bool: + return os.path.exists(path) and os.access(path, os.W_OK) + +class IsExecutable(Criterion): + """Checks if the path is executable.""" + def __call__(self, path: str) -> bool: + return os.path.exists(path) and os.access(path, os.X_OK) + +# criteria w/ value + +class Extension(Criterion): + """Filters by file extension (without the dot).""" + def __call__(self, path: str) -> bool: + _, ext = os.path.splitext(path) + return ext[1:] in self.accepted() + +class Mime(Criterion): + """Filters by mime type.""" + def __call__(self, path: str) -> bool: + try: + return magic.from_file(path, mime=True).lower() in self.accepted() + except FileNotFoundError: + return False + +## EOF ## diff --git a/bsie/utils/filematcher/parser.py b/bsie/utils/filematcher/parser.py new file mode 100644 index 0000000..0654742 --- /dev/null +++ b/bsie/utils/filematcher/parser.py @@ -0,0 +1,148 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2021 +""" +# standard imports +import typing + +# non-standard imports +import pyparsing +from pyparsing import printables, alphas8bit, punc8bit, QuotedString, Word, \ + delimitedList, Or, CaselessKeyword, Group, oneOf, Optional + +# bsie imports +from bsie.base import errors + +# inner-module imports +from . import matcher + +# exports +__all__: typing.Sequence[str] = ( + 'parse', + ) + + +## code ## + +class FileMatcherParser(): + """ + EXPR := RULES | RULES "|" RULES + RULESET := RULE | RULE, RULE + RULE := CRITERION OP VALUE | CRITERION OP {VALUES} | VALUELESS + OP := != | = + VALUES := VALUE | VALUE, VALUE + VALUE := [word] + CRITERION := mime | extension | ... + """ + + # criteria matcher nodes w/ arguments + _CRITERIA: typing.Dict[str, typing.Type[matcher.Matcher]] = { + 'extension': matcher.Extension, + 'mime': matcher.Mime, + } + + # criteria matcher nodes w/o arguments + _VALUELESS: typing.Dict[str, typing.Type[matcher.Matcher]] = { + 'any': matcher.Any, + 'nothing': matcher.Nothing, + 'exists': matcher.Exists, + 'isfile': matcher.IsFile, + 'isdir': matcher.IsDir, + 'islink': matcher.IsLink, + 'isabs': matcher.IsAbs, + 'isrel': matcher.IsRel, + 'ismount': matcher.IsMount, + 'emtpy': matcher.IsEmpty, + 'readable': matcher.IsReadable, + 'writable': matcher.IsWritable, + 'executable': matcher.IsExecutable, + } + + # pyparsing parser instance. + _parser: pyparsing.ParseExpression + + def __init__(self): + # build the parser + # VALUE := [word] + alphabet = (printables + alphas8bit + punc8bit).translate(str.maketrans('', '', ',{}|=')) + value = QuotedString(quoteChar='"', escChar='\\') ^ Word(alphabet) + # CRITERION := mime | extension | ... + criterion = Or([CaselessKeyword(p) for p in self._CRITERIA]).setResultsName('criterion') + valueless = Or([CaselessKeyword(p) for p in self._VALUELESS]).setResultsName('criterion') + # VALUES := VALUE | VALUE, VALUE + values = delimitedList(value, delim=',').setResultsName('value') + # OP := '=' | '!=' + eqop = oneOf('= !=').setResultsName('op') + # RULE := CRITERION OP VALUE | CRITERION OP {VALUES} | VALUELESS + rule_none = Group(Optional('!').setResultsName('op') + valueless).setResultsName('rule_none') + rule_one = Group(criterion + eqop + value.setResultsName('value')).setResultsName('rule_one') + rule_few = Group(criterion + eqop + '{' + values + '}').setResultsName('rule_few') + # RULESET := RULE | RULE, RULE + ruleset = Group(delimitedList(rule_none ^ rule_one ^ rule_few, delim=',')) + # EXPR := RULESET | RULESET \| RULESET + self._parser = delimitedList(ruleset, delim='|') + + def parse(self, query: str) -> matcher.Matcher: # pylint: disable=too-many-branches + """Build a file matcher from a rule definition.""" + # preprocess the query + query = query.strip() + + # empty query + if len(query) == 0: + return matcher.Any() + + try: + parsed = self._parser.parseString(query, parseAll=True) + except pyparsing.ParseException as err: + raise errors.ParserError(f'Cannot parse query {err}') + + # convert to Matcher + rules = [] + for exp in parsed: + tokens = [] + for rule in exp: + # fetch accepted values + if rule.getName() == 'rule_none': + accepted = [] + elif rule.getName() == 'rule_one': + accepted = [rule.value] + elif rule.getName() == 'rule_few': + accepted = list(rule.value) + else: # prevented by grammar + raise errors.UnreachableError('Invalid rule definition') + + # build criterion + if rule.criterion in self._VALUELESS: + cls = self._VALUELESS[rule.criterion] + if rule.op == '!': + tokens.append(matcher.NOT(cls())) + else: + tokens.append(cls()) + elif rule.criterion in self._CRITERIA: + cls = self._CRITERIA[rule.criterion] + if rule.op == '!=': + tokens.append(matcher.NOT(cls(accepted))) + else: + tokens.append(cls(accepted)) + else: # prevented by grammar + raise errors.UnreachableError(f'Invalid condition "{rule.criterion}"') + + # And-aggregate rules in one ruleset (if needed) + tokens = matcher.And(tokens) if len(tokens) > 1 else tokens[0] + rules.append(tokens) + + # Or-aggregate rulesets + expr = matcher.Or(rules) if len(rules) > 1 else rules[0] + + return expr + +# build default instance +file_match_parser = FileMatcherParser() + +def parse(query: str) -> matcher.Matcher: + """Shortcut for FileMatcherParser()(query).""" + return file_match_parser.parse(query) + +## EOF ## -- cgit v1.2.3 From 266c2c9a072bf3289fd7f2d75278b7d59528378c Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 24 Dec 2022 10:27:09 +0100 Subject: package restructuring: base * Reader and Extractor to respective reader/extractor modules * ReaderBuilder to reader module * ExtractorBuilder to extractor module * Loading module in utils (safe_load, unpack_name) * Pipeline and PipelineBuilder to lib module * errors to utils * documentation: "standard import" and "external import" --- bsie/apps/index.py | 16 +-- bsie/apps/info.py | 16 +-- bsie/base/__init__.py | 24 ---- bsie/base/errors.py | 45 -------- bsie/base/extractor.py | 103 ----------------- bsie/base/reader.py | 47 -------- bsie/extractor/__init__.py | 11 +- bsie/extractor/base.py | 103 +++++++++++++++++ bsie/extractor/builder.py | 77 +++++++++++++ bsie/extractor/generic/constant.py | 10 +- bsie/extractor/generic/path.py | 8 +- bsie/extractor/generic/stat.py | 10 +- bsie/lib/__init__.py | 4 +- bsie/lib/bsie.py | 6 +- bsie/lib/builder.py | 85 ++++++++++++++ bsie/lib/pipeline.py | 145 ++++++++++++++++++++++++ bsie/reader/__init__.py | 13 +++ bsie/reader/base.py | 47 ++++++++ bsie/reader/builder.py | 74 ++++++++++++ bsie/reader/path.py | 8 +- bsie/reader/stat.py | 9 +- bsie/tools/__init__.py | 20 ---- bsie/tools/builder.py | 226 ------------------------------------- bsie/tools/pipeline.py | 144 ----------------------- bsie/utils/__init__.py | 9 +- bsie/utils/errors.py | 45 ++++++++ bsie/utils/filematcher/parser.py | 6 +- bsie/utils/loading.py | 54 +++++++++ 28 files changed, 710 insertions(+), 655 deletions(-) delete mode 100644 bsie/base/__init__.py delete mode 100644 bsie/base/errors.py delete mode 100644 bsie/base/extractor.py delete mode 100644 bsie/base/reader.py create mode 100644 bsie/extractor/base.py create mode 100644 bsie/extractor/builder.py create mode 100644 bsie/lib/builder.py create mode 100644 bsie/lib/pipeline.py create mode 100644 bsie/reader/base.py create mode 100644 bsie/reader/builder.py delete mode 100644 bsie/tools/__init__.py delete mode 100644 bsie/tools/builder.py delete mode 100644 bsie/tools/pipeline.py create mode 100644 bsie/utils/errors.py create mode 100644 bsie/utils/loading.py (limited to 'bsie') diff --git a/bsie/apps/index.py b/bsie/apps/index.py index 1dbfdd8..0c6296f 100644 --- a/bsie/apps/index.py +++ b/bsie/apps/index.py @@ -4,16 +4,16 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import argparse import os import typing # bsie imports -from bsie.base import errors -from bsie.lib import BSIE -from bsie.tools import builder -from bsie.utils import bsfs +from bsie.extractor import ExtractorBuilder +from bsie.lib import BSIE, PipelineBuilder +from bsie.reader import ReaderBuilder +from bsie.utils import bsfs, errors # exports __all__: typing.Sequence[str] = ( @@ -44,9 +44,9 @@ def main(argv): # FIXME: Read reader/extractor configs from a config file # reader builder - rbuild = builder.ReaderBuilder({}) + rbuild = ReaderBuilder({}) # extractor builder - ebuild = builder.ExtractorBuilder([ + ebuild = ExtractorBuilder([ {'bsie.extractor.generic.path.Path': {}}, {'bsie.extractor.generic.stat.Stat': {}}, {'bsie.extractor.generic.constant.Constant': dict( @@ -60,7 +60,7 @@ def main(argv): )}, ]) # pipeline builder - pbuild = builder.PipelineBuilder( + pbuild = PipelineBuilder( bsfs.Namespace(args.user + ('/' if not args.user.endswith('/') else '')), rbuild, ebuild, diff --git a/bsie/apps/info.py b/bsie/apps/info.py index eaf1f71..a4e611c 100644 --- a/bsie/apps/info.py +++ b/bsie/apps/info.py @@ -4,15 +4,16 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import argparse import sys import typing # bsie imports -from bsie.base import errors -from bsie.tools import builder -from bsie.utils import bsfs +from bsie.extractor import ExtractorBuilder +from bsie.lib import PipelineBuilder +from bsie.reader import ReaderBuilder +from bsie.utils import bsfs, errors # exports __all__: typing.Sequence[str] = ( @@ -31,9 +32,10 @@ def main(argv): # FIXME: Read reader/extractor configs from a config file # reader builder - rbuild = builder.ReaderBuilder({}) + rbuild = ReaderBuilder({ + }) # extractor builder - ebuild = builder.ExtractorBuilder([ + ebuild = ExtractorBuilder([ {'bsie.extractor.generic.path.Path': {}}, {'bsie.extractor.generic.stat.Stat': {}}, {'bsie.extractor.generic.constant.Constant': dict( @@ -47,7 +49,7 @@ def main(argv): )}, ]) # pipeline builder - pbuild = builder.PipelineBuilder( + pbuild = PipelineBuilder( bsfs.Namespace('http://example.com/me/'), # not actually used rbuild, ebuild, diff --git a/bsie/base/__init__.py b/bsie/base/__init__.py deleted file mode 100644 index 0d362cd..0000000 --- a/bsie/base/__init__.py +++ /dev/null @@ -1,24 +0,0 @@ -"""The base module defines the BSIE interfaces. - -You'll mostly find abstract classes here. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" -# imports -import typing - -# inner-module imports -from . import errors -from .extractor import Extractor -from .reader import Reader - -# exports -__all__: typing.Sequence[str] = ( - 'Extractor', - 'Reader', - 'errors', - ) - -## EOF ## diff --git a/bsie/base/errors.py b/bsie/base/errors.py deleted file mode 100644 index 5fafd5b..0000000 --- a/bsie/base/errors.py +++ /dev/null @@ -1,45 +0,0 @@ -"""Common BSIE exceptions. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" -# imports -import typing - -# exports -__all__: typing.Sequence[str] = ( - 'BuilderError', - 'ExtractorError', - 'LoaderError', - 'ReaderError', - ) - - -## code ## - -class _BSIEError(Exception): - """Generic BSIE error.""" - -class BuilderError(_BSIEError): - """The Builder failed to create an instance.""" - -class LoaderError(BuilderError): - """Failed to load a module or class.""" - -class ExtractorError(_BSIEError): - """The Extractor failed to process the given content.""" - -class ReaderError(_BSIEError): - """The Reader failed to read the given file.""" - -class ProgrammingError(_BSIEError): - """An assertion-like error that indicates a code-base issue.""" - -class UnreachableError(ProgrammingError): - """Bravo, you've reached a point in code that should logically not be reachable.""" - -class ParserError(_BSIEError): - """Failed to parse due to invalid syntax or structures.""" - -## EOF ## diff --git a/bsie/base/extractor.py b/bsie/base/extractor.py deleted file mode 100644 index c44021b..0000000 --- a/bsie/base/extractor.py +++ /dev/null @@ -1,103 +0,0 @@ -"""The Extractor classes transform content into triples. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" -# imports -import abc -import typing - -# bsie imports -from bsie.utils import bsfs, node, ns - -# exports -__all__: typing.Sequence[str] = ( - 'Extractor', - ) - -# constants - -# essential definitions typically used in extractor schemas. -# NOTE: This preamble is only for convenience; Each Extractor must implement its use, if so desired. -SCHEMA_PREAMBLE = ''' - # common external prefixes - prefix rdf: - prefix rdfs: - prefix xsd: - prefix schema: - - # common bsfs prefixes - prefix bsfs: - prefix bse: - - # essential nodes - bsfs:Entity rdfs:subClassOf bsfs:Node . - bsfs:File rdfs:subClassOf bsfs:Entity . - - # common definitions - xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . - - ''' - - -## code ## - -class Extractor(abc.ABC): - """Produce (subject, predicate, value)-triples from some content. - The Extractor produces princpal predicates that provide information - about the content itself (i.e., triples that include the subject), - and may also generate triples with auxiliary predicates if the - extracted value is a node itself. - """ - - # what type of content is expected (i.e. reader subclass). - CONTENT_READER: typing.Optional[str] = None - - # extractor schema. - _schema: bsfs.schema.Schema - - def __init__(self, schema: bsfs.schema.Schema): - self._schema = schema - - def __str__(self) -> str: - return bsfs.typename(self) - - def __repr__(self) -> str: - return f'{bsfs.typename(self)}()' - - def __eq__(self, other: typing.Any) -> bool: - return isinstance(other, type(self)) \ - and self.CONTENT_READER == other.CONTENT_READER \ - and self.schema == other.schema - - def __hash__(self) -> int: - return hash((type(self), self.CONTENT_READER, self.schema)) - - @property - def schema(self) -> bsfs.schema.Schema: - """Return the extractor's schema.""" - return self._schema - - @property - def principals(self) -> typing.Iterator[bsfs.schema.Predicate]: - """Return the principal predicates, i.e., relations from/to the extraction subject.""" - ent = self.schema.node(ns.bsfs.Entity) - return ( - pred - for pred - in self.schema.predicates() - if pred.domain <= ent or (pred.range is not None and pred.range <= ent) - ) - - @abc.abstractmethod - def extract( - self, - subject: node.Node, - content: typing.Any, - principals: typing.Iterable[bsfs.schema.Predicate], - ) -> typing.Iterator[typing.Tuple[node.Node, bsfs.schema.Predicate, typing.Any]]: - """Return (node, predicate, value) triples.""" - -## EOF ## diff --git a/bsie/base/reader.py b/bsie/base/reader.py deleted file mode 100644 index cbabd36..0000000 --- a/bsie/base/reader.py +++ /dev/null @@ -1,47 +0,0 @@ -"""The Reader classes return high-level content structures from files. - -The Reader fulfills two purposes: - First, it brokers between multiple libraries and file formats. - Second, it separates multiple aspects of a file into distinct content types. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" -# imports -import abc -import typing - -# bsie imports -from bsie.utils import bsfs - -# exports -__all__: typing.Sequence[str] = ( - 'Reader', - ) - - -## code ## - -class Reader(abc.ABC): - """Read and return some content from a file.""" - - def __str__(self) -> str: - return bsfs.typename(self) - - def __repr__(self) -> str: - return f'{bsfs.typename(self)}()' - - def __eq__(self, other: typing.Any) -> bool: - return isinstance(other, type(self)) - - def __hash__(self) -> int: - return hash(type(self)) - - @abc.abstractmethod - def __call__(self, path: bsfs.URI) -> typing.Any: - """Return some content of the file at *path*. - Raises a `ReaderError` if the reader cannot make sense of the file format. - """ - -## EOF ## diff --git a/bsie/extractor/__init__.py b/bsie/extractor/__init__.py index ef31343..5f385ee 100644 --- a/bsie/extractor/__init__.py +++ b/bsie/extractor/__init__.py @@ -6,10 +6,17 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing +# inner-module imports +from .base import Extractor +from .builder import ExtractorBuilder + # exports -__all__: typing.Sequence[str] = [] +__all__: typing.Sequence[str] = ( + 'Extractor', + 'ExtractorBuilder', + ) ## EOF ## diff --git a/bsie/extractor/base.py b/bsie/extractor/base.py new file mode 100644 index 0000000..c44021b --- /dev/null +++ b/bsie/extractor/base.py @@ -0,0 +1,103 @@ +"""The Extractor classes transform content into triples. + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import abc +import typing + +# bsie imports +from bsie.utils import bsfs, node, ns + +# exports +__all__: typing.Sequence[str] = ( + 'Extractor', + ) + +# constants + +# essential definitions typically used in extractor schemas. +# NOTE: This preamble is only for convenience; Each Extractor must implement its use, if so desired. +SCHEMA_PREAMBLE = ''' + # common external prefixes + prefix rdf: + prefix rdfs: + prefix xsd: + prefix schema: + + # common bsfs prefixes + prefix bsfs: + prefix bse: + + # essential nodes + bsfs:Entity rdfs:subClassOf bsfs:Node . + bsfs:File rdfs:subClassOf bsfs:Entity . + + # common definitions + xsd:string rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Literal . + + ''' + + +## code ## + +class Extractor(abc.ABC): + """Produce (subject, predicate, value)-triples from some content. + The Extractor produces princpal predicates that provide information + about the content itself (i.e., triples that include the subject), + and may also generate triples with auxiliary predicates if the + extracted value is a node itself. + """ + + # what type of content is expected (i.e. reader subclass). + CONTENT_READER: typing.Optional[str] = None + + # extractor schema. + _schema: bsfs.schema.Schema + + def __init__(self, schema: bsfs.schema.Schema): + self._schema = schema + + def __str__(self) -> str: + return bsfs.typename(self) + + def __repr__(self) -> str: + return f'{bsfs.typename(self)}()' + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, type(self)) \ + and self.CONTENT_READER == other.CONTENT_READER \ + and self.schema == other.schema + + def __hash__(self) -> int: + return hash((type(self), self.CONTENT_READER, self.schema)) + + @property + def schema(self) -> bsfs.schema.Schema: + """Return the extractor's schema.""" + return self._schema + + @property + def principals(self) -> typing.Iterator[bsfs.schema.Predicate]: + """Return the principal predicates, i.e., relations from/to the extraction subject.""" + ent = self.schema.node(ns.bsfs.Entity) + return ( + pred + for pred + in self.schema.predicates() + if pred.domain <= ent or (pred.range is not None and pred.range <= ent) + ) + + @abc.abstractmethod + def extract( + self, + subject: node.Node, + content: typing.Any, + principals: typing.Iterable[bsfs.schema.Predicate], + ) -> typing.Iterator[typing.Tuple[node.Node, bsfs.schema.Predicate, typing.Any]]: + """Return (node, predicate, value) triples.""" + +## EOF ## diff --git a/bsie/extractor/builder.py b/bsie/extractor/builder.py new file mode 100644 index 0000000..0fd3685 --- /dev/null +++ b/bsie/extractor/builder.py @@ -0,0 +1,77 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# bsie imports +from bsie.utils import bsfs, errors, safe_load, unpack_qualified_name + +# inner-module imports +from . import base + +# exports +__all__: typing.Sequence[str] = ( + 'ExtractorBuilder', + ) + + +## code ## + +class ExtractorBuilder(): + """Build `bsie.base.Extractor instances. + + It is permissible to build multiple instances of the same extractor + (typically with different arguments), hence the ExtractorBuilder + receives a list of build specifications. Each specification is + a dict with a single key (extractor's qualified name) and a dict + to be used as keyword arguments. + Example: [{'bsie.extractor.generic.path.Path': {}}, ] + + """ + + # build specifications + _specs: typing.List[typing.Dict[str, typing.Dict[str, typing.Any]]] + + def __init__(self, specs: typing.List[typing.Dict[str, typing.Dict[str, typing.Any]]]): + self._specs = specs + + def __iter__(self) -> typing.Iterator[int]: + """Iterate over extractor specifications.""" + return iter(range(len(self._specs))) + + def build(self, index: int) -> base.Extractor: + """Return an instance of the n'th extractor (n=*index*).""" + # get build instructions + specs = self._specs[index] + + # check specs structure. expecting[{name: {kwargs}}] + if not isinstance(specs, dict): + raise TypeError(f'expected a dict, found {bsfs.typename(specs)}') + if len(specs) != 1: + raise TypeError(f'expected a dict of length one, found {len(specs)}') + + # get name and args from specs + name = next(iter(specs.keys())) + kwargs = specs[name] + + # check kwargs structure + if not isinstance(kwargs, dict): + raise TypeError(f'expected a dict, found {bsfs.typename(kwargs)}') + + # check name and get module/class components + module_name, class_name = unpack_qualified_name(name) + + # import extractor class + cls = safe_load(module_name, class_name) + + try: # build and return instance + return cls(**kwargs) + + except Exception as err: + raise errors.BuilderError(f'failed to build extractor {name} due to {bsfs.typename(err)}: {err}') from err + +## EOF ## diff --git a/bsie/extractor/generic/constant.py b/bsie/extractor/generic/constant.py index 11384e6..7b1d942 100644 --- a/bsie/extractor/generic/constant.py +++ b/bsie/extractor/generic/constant.py @@ -4,13 +4,15 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # bsie imports -from bsie.base import extractor from bsie.utils import bsfs, node +# inner-module imports +from .. import base + # exports __all__: typing.Sequence[str] = ( 'Constant', @@ -19,7 +21,7 @@ __all__: typing.Sequence[str] = ( ## code ## -class Constant(extractor.Extractor): +class Constant(base.Extractor): """Extract information from file's path.""" CONTENT_READER = None @@ -32,7 +34,7 @@ class Constant(extractor.Extractor): schema: str, tuples: typing.Iterable[typing.Tuple[bsfs.URI, typing.Any]], ): - super().__init__(bsfs.schema.Schema.from_string(extractor.SCHEMA_PREAMBLE + schema)) + super().__init__(bsfs.schema.Schema.from_string(base.SCHEMA_PREAMBLE + schema)) # NOTE: Raises a KeyError if the predicate is not part of the schema self._tuples = tuple((self.schema.predicate(p_uri), value) for p_uri, value in tuples) # TODO: use schema instance for value checking diff --git a/bsie/extractor/generic/path.py b/bsie/extractor/generic/path.py index 7018e12..295715f 100644 --- a/bsie/extractor/generic/path.py +++ b/bsie/extractor/generic/path.py @@ -4,12 +4,12 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import os import typing # bsie imports -from bsie.base import extractor +from bsie.extractor import base from bsie.utils import bsfs, node, ns # exports @@ -20,7 +20,7 @@ __all__: typing.Sequence[str] = ( ## code ## -class Path(extractor.Extractor): +class Path(base.Extractor): """Extract information from file's path.""" CONTENT_READER = 'bsie.reader.path.Path' @@ -29,7 +29,7 @@ class Path(extractor.Extractor): _callmap: typing.Dict[bsfs.schema.Predicate, typing.Callable[[str], typing.Any]] def __init__(self): - super().__init__(bsfs.schema.Schema.from_string(extractor.SCHEMA_PREAMBLE + ''' + super().__init__(bsfs.schema.Schema.from_string(base.SCHEMA_PREAMBLE + ''' bse:filename rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:File ; rdfs:range xsd:string ; diff --git a/bsie/extractor/generic/stat.py b/bsie/extractor/generic/stat.py index 0b9ce29..1381fe2 100644 --- a/bsie/extractor/generic/stat.py +++ b/bsie/extractor/generic/stat.py @@ -4,14 +4,16 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import os import typing # bsie imports -from bsie.base import extractor from bsie.utils import bsfs, node, ns +# inner-module imports +from .. import base + # exports __all__: typing.Sequence[str] = ( 'Stat', @@ -20,7 +22,7 @@ __all__: typing.Sequence[str] = ( ## code ## -class Stat(extractor.Extractor): +class Stat(base.Extractor): """Extract information from the file system.""" CONTENT_READER = 'bsie.reader.stat.Stat' @@ -29,7 +31,7 @@ class Stat(extractor.Extractor): _callmap: typing.Dict[bsfs.schema.Predicate, typing.Callable[[os.stat_result], typing.Any]] def __init__(self): - super().__init__(bsfs.schema.Schema.from_string(extractor.SCHEMA_PREAMBLE + ''' + super().__init__(bsfs.schema.Schema.from_string(base.SCHEMA_PREAMBLE + ''' bse:filesize rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:File ; rdfs:range xsd:integer ; diff --git a/bsie/lib/__init__.py b/bsie/lib/__init__.py index 578c2c4..4239d3b 100644 --- a/bsie/lib/__init__.py +++ b/bsie/lib/__init__.py @@ -4,15 +4,17 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # inner-module imports from .bsie import BSIE +from .builder import PipelineBuilder # exports __all__: typing.Sequence[str] = ( 'BSIE', + 'PipelineBuilder', ) ## EOF ## diff --git a/bsie/lib/bsie.py b/bsie/lib/bsie.py index e087fa9..668783d 100644 --- a/bsie/lib/bsie.py +++ b/bsie/lib/bsie.py @@ -4,13 +4,15 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # bsie imports -from bsie.tools import Pipeline from bsie.utils import bsfs, node, ns +# inner-module imports +from .pipeline import Pipeline + # exports __all__: typing.Sequence[str] = ( 'BSIE', diff --git a/bsie/lib/builder.py b/bsie/lib/builder.py new file mode 100644 index 0000000..c2abffe --- /dev/null +++ b/bsie/lib/builder.py @@ -0,0 +1,85 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import logging +import typing + +# bsie imports +from bsie.extractor import ExtractorBuilder +from bsie.reader import ReaderBuilder +from bsie.utils import bsfs, errors + +# inner-module imports +from . import pipeline + +# exports +__all__: typing.Sequence[str] = ( + 'PipelineBuilder', + ) + + +## code ## + +logger = logging.getLogger(__name__) + +class PipelineBuilder(): + """Build `bsie.tools.pipeline.Pipeline` instances.""" + + # Prefix to be used in the Pipeline. + prefix: bsfs.Namespace + + # builder for Readers. + rbuild: ReaderBuilder + + # builder for Extractors. + ebuild: ExtractorBuilder + + def __init__( + self, + prefix: bsfs.Namespace, + reader_builder: ReaderBuilder, + extractor_builder: ExtractorBuilder, + ): + self.prefix = prefix + self.rbuild = reader_builder + self.ebuild = extractor_builder + + def build(self) -> pipeline.Pipeline: + """Return a Pipeline instance.""" + ext2rdr = {} + + for eidx in self.ebuild: + # build extractor + try: + ext = self.ebuild.build(eidx) + + except errors.LoaderError as err: # failed to load extractor; skip + logger.error('failed to load extractor: %s', err) + continue + + except errors.BuilderError as err: # failed to build instance; skip + logger.error(str(err)) + continue + + try: + # get reader required by extractor + if ext.CONTENT_READER is not None: + rdr = self.rbuild.build(ext.CONTENT_READER) + else: + rdr = None + # store extractor + ext2rdr[ext] = rdr + + except errors.LoaderError as err: # failed to load reader + logger.error('failed to load reader: %s', err) + + except errors.BuilderError as err: # failed to build reader + logger.error(str(err)) + + return pipeline.Pipeline(self.prefix, ext2rdr) + +## EOF ## diff --git a/bsie/lib/pipeline.py b/bsie/lib/pipeline.py new file mode 100644 index 0000000..e5ce1b7 --- /dev/null +++ b/bsie/lib/pipeline.py @@ -0,0 +1,145 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +from collections import defaultdict +import logging +import typing + +# bsie imports +from bsie.extractor import Extractor +from bsie.reader import Reader +from bsie.utils import bsfs, errors, node, ns + +# exports +__all__: typing.Sequence[str] = ( + 'Pipeline', + ) + +# constants +FILE_PREFIX = 'file#' + +## code ## + +logger = logging.getLogger(__name__) + +class Pipeline(): + """Extraction pipeline to generate triples from files. + + The Pipeline binds readers and extractors, and performs + the necessary operations to produce triples from a file. + It takes a best-effort approach to extract as many triples + as possible. Errors during the extraction are passed over + and reported to the log. + + """ + + # combined extractor schemas. + _schema: bsfs.schema.Schema + + # node prefix. + _prefix: bsfs.Namespace + + # extractor -> reader mapping + _ext2rdr: typing.Dict[Extractor, typing.Optional[Reader]] + + def __init__( + self, + prefix: bsfs.Namespace, + ext2rdr: typing.Dict[Extractor, typing.Optional[Reader]] + ): + # store core members + self._prefix = prefix + FILE_PREFIX + self._ext2rdr = ext2rdr + # compile schema from all extractors + self._schema = bsfs.schema.Schema.Union(ext.schema for ext in ext2rdr) + + def __str__(self) -> str: + return bsfs.typename(self) + + def __repr__(self) -> str: + return f'{bsfs.typename(self)}(...)' + + def __hash__(self) -> int: + return hash((type(self), self._prefix, self._schema, tuple(self._ext2rdr), tuple(self._ext2rdr.values()))) + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, type(self)) \ + and self._schema == other._schema \ + and self._prefix == other._prefix \ + and self._ext2rdr == other._ext2rdr + + @property + def schema(self) -> bsfs.schema.Schema: + """Return the pipeline's schema (combined from all extractors).""" + return self._schema + + @property + def principals(self) -> typing.Iterator[bsfs.schema.Predicate]: + """Return the principal predicates that can be extracted.""" + return iter({pred for ext in self._ext2rdr for pred in ext.principals}) + + def subschema(self, principals: typing.Iterable[bsfs.schema.Predicate]) -> bsfs.schema.Schema: + """Return the subset of the schema that supports the given *principals*.""" + # materialize principals + principals = set(principals) + # collect and combine schemas from extractors + return bsfs.schema.Schema.Union({ + ext.schema + for ext + in self._ext2rdr + if not set(ext.principals).isdisjoint(principals) + }) + + def __call__( + self, + path: bsfs.URI, + principals: typing.Optional[typing.Iterable[bsfs.schema.Predicate]] = None, + ) -> typing.Iterator[typing.Tuple[node.Node, bsfs.schema.Predicate, typing.Any]]: + """Extract triples from the file at *path*. Optionally, limit triples to *principals*.""" + # get principals + principals = set(principals) if principals is not None else set(self.schema.predicates()) + + # get extractors + extractors = {ext for ext in self._ext2rdr if not set(ext.principals).isdisjoint(principals)} + + # corner-case short-cut + if len(extractors) == 0: + return + + # get readers -> extractors mapping + rdr2ext = defaultdict(set) + for ext in extractors: + rdr = self._ext2rdr[ext] + rdr2ext[rdr].add(ext) + + # create subject for file + uuid = bsfs.uuid.UCID.from_path(path) + subject = node.Node(ns.bsfs.File, self._prefix[uuid]) + + # extract information + for rdr, extrs in rdr2ext.items(): + try: + # get content + content = rdr(path) if rdr is not None else None + + # apply extractors on this content + for ext in extrs: + try: + # get predicate/value tuples + for subject, pred, value in ext.extract(subject, content, principals): + yield subject, pred, value + + except errors.ExtractorError as err: + # critical extractor failure. + logger.error('%s failed to extract triples from content: %s', ext, err) + + except errors.ReaderError as err: + # failed to read any content. skip. + logger.error('%s failed to read content: %s', rdr, err) + + +## EOF ## diff --git a/bsie/reader/__init__.py b/bsie/reader/__init__.py index a45f22b..4163d1c 100644 --- a/bsie/reader/__init__.py +++ b/bsie/reader/__init__.py @@ -15,5 +15,18 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ +# standard imports +import typing +# inner-module imports +from .base import Reader +from .builder import ReaderBuilder + +# exports +__all__: typing.Sequence[str] = ( + 'Reader', + 'ReaderBuilder', + ) + +## EOF ## ## EOF ## diff --git a/bsie/reader/base.py b/bsie/reader/base.py new file mode 100644 index 0000000..cbabd36 --- /dev/null +++ b/bsie/reader/base.py @@ -0,0 +1,47 @@ +"""The Reader classes return high-level content structures from files. + +The Reader fulfills two purposes: + First, it brokers between multiple libraries and file formats. + Second, it separates multiple aspects of a file into distinct content types. + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import abc +import typing + +# bsie imports +from bsie.utils import bsfs + +# exports +__all__: typing.Sequence[str] = ( + 'Reader', + ) + + +## code ## + +class Reader(abc.ABC): + """Read and return some content from a file.""" + + def __str__(self) -> str: + return bsfs.typename(self) + + def __repr__(self) -> str: + return f'{bsfs.typename(self)}()' + + def __eq__(self, other: typing.Any) -> bool: + return isinstance(other, type(self)) + + def __hash__(self) -> int: + return hash(type(self)) + + @abc.abstractmethod + def __call__(self, path: bsfs.URI) -> typing.Any: + """Return some content of the file at *path*. + Raises a `ReaderError` if the reader cannot make sense of the file format. + """ + +## EOF ## diff --git a/bsie/reader/builder.py b/bsie/reader/builder.py new file mode 100644 index 0000000..bce5397 --- /dev/null +++ b/bsie/reader/builder.py @@ -0,0 +1,74 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# bsie imports +from bsie.utils import bsfs, errors, safe_load, unpack_qualified_name + +# inner-module imports +from . import base + +# exports +__all__: typing.Sequence[str] = ( + 'ReaderBuilder', + ) + + +## code ## + +class ReaderBuilder(): + """Build `bsie.base.Reader` instances. + + Readers are defined via their qualified class name + (e.g., bsie.reader.path.Path) and optional keyword + arguments that are passed to the constructor via + the *kwargs* argument (name as key, kwargs as value). + The ReaderBuilder keeps a cache of previously built + reader instances, as they are anyway built with + identical keyword arguments. + + """ + + # keyword arguments + _kwargs: typing.Dict[str, typing.Dict[str, typing.Any]] + + # cached readers + _cache: typing.Dict[str, base.Reader] + + def __init__(self, kwargs: typing.Dict[str, typing.Dict[str, typing.Any]]): + self._kwargs = kwargs + self._cache = {} + + def build(self, name: str) -> base.Reader: + """Return an instance for the qualified class name.""" + # return cached instance + if name in self._cache: + return self._cache[name] + + # check name and get module/class components + module_name, class_name = unpack_qualified_name(name) + + # import reader class + cls = safe_load(module_name, class_name) + + # get kwargs + kwargs = self._kwargs.get(name, {}) + if not isinstance(kwargs, dict): + raise TypeError(f'expected a kwargs dict, found {bsfs.typename(kwargs)}') + + try: # build, cache, and return instance + obj = cls(**kwargs) + # cache instance + self._cache[name] = obj + # return instance + return obj + + except Exception as err: + raise errors.BuilderError(f'failed to build reader {name} due to {bsfs.typename(err)}: {err}') from err + +## EOF ## diff --git a/bsie/reader/path.py b/bsie/reader/path.py index d60f187..1ca05a0 100644 --- a/bsie/reader/path.py +++ b/bsie/reader/path.py @@ -4,11 +4,11 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing -# bsie imports -from bsie.base import reader +# inner-module imports +from . import base # exports __all__: typing.Sequence[str] = ( @@ -18,7 +18,7 @@ __all__: typing.Sequence[str] = ( ## code ## -class Path(reader.Reader): +class Path(base.Reader): """Return the path.""" def __call__(self, path: str) -> str: diff --git a/bsie/reader/stat.py b/bsie/reader/stat.py index fc5fb24..706dc47 100644 --- a/bsie/reader/stat.py +++ b/bsie/reader/stat.py @@ -4,12 +4,15 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import os import typing # bsie imports -from bsie.base import errors, reader +from bsie.utils import errors + +# inner-module imports +from . import base # exports __all__: typing.Sequence[str] = ( @@ -19,7 +22,7 @@ __all__: typing.Sequence[str] = ( ## code ## -class Stat(reader.Reader): +class Stat(base.Reader): """Read and return the filesystem's stat infos.""" def __call__(self, path: str) -> os.stat_result: diff --git a/bsie/tools/__init__.py b/bsie/tools/__init__.py deleted file mode 100644 index 803c321..0000000 --- a/bsie/tools/__init__.py +++ /dev/null @@ -1,20 +0,0 @@ -""" - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" -# imports -import typing - -# inner-module imports -from . import builder -from .pipeline import Pipeline - -# exports -__all__: typing.Sequence[str] = ( - 'builder', - 'Pipeline', - ) - -## EOF ## diff --git a/bsie/tools/builder.py b/bsie/tools/builder.py deleted file mode 100644 index 190d9bf..0000000 --- a/bsie/tools/builder.py +++ /dev/null @@ -1,226 +0,0 @@ -""" - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" -# imports -import importlib -import logging -import typing - -# bsie imports -from bsie import base -from bsie.base import errors -from bsie.utils import bsfs - -# inner-module imports -from . import pipeline - -# exports -__all__: typing.Sequence[str] = ( - 'ExtractorBuilder', - 'PipelineBuilder', - 'ReaderBuilder', - ) - - -## code ## - -logger = logging.getLogger(__name__) - -def _safe_load(module_name: str, class_name: str): - """Get a class from a module. Raise BuilderError if anything goes wrong.""" - try: - # load the module - module = importlib.import_module(module_name) - except Exception as err: - # cannot import module - raise errors.LoaderError(f'cannot load module {module_name}') from err - - try: - # get the class from the module - cls = getattr(module, class_name) - except Exception as err: - # cannot find the class - raise errors.LoaderError(f'cannot load class {class_name} from module {module_name}') from err - - return cls - - -def _unpack_name(name): - """Split a name into its module and class component (dot-separated).""" - if not isinstance(name, str): - raise TypeError(name) - if '.' not in name: - raise ValueError('name must be a qualified class name.') - module_name, class_name = name[:name.rfind('.')], name[name.rfind('.')+1:] - if module_name == '': - raise ValueError('name must be a qualified class name.') - return module_name, class_name - - -class ReaderBuilder(): - """Build `bsie.base.Reader` instances. - - Readers are defined via their qualified class name - (e.g., bsie.reader.path.Path) and optional keyword - arguments that are passed to the constructor via - the *kwargs* argument (name as key, kwargs as value). - The ReaderBuilder keeps a cache of previously built - reader instances, as they are anyway built with - identical keyword arguments. - - """ - - # keyword arguments - _kwargs: typing.Dict[str, typing.Dict[str, typing.Any]] - - # cached readers - _cache: typing.Dict[str, base.Reader] - - def __init__(self, kwargs: typing.Dict[str, typing.Dict[str, typing.Any]]): - self._kwargs = kwargs - self._cache = {} - - def build(self, name: str) -> base.Reader: - """Return an instance for the qualified class name.""" - # return cached instance - if name in self._cache: - return self._cache[name] - - # check name and get module/class components - module_name, class_name = _unpack_name(name) - - # import reader class - cls = _safe_load(module_name, class_name) - - # get kwargs - kwargs = self._kwargs.get(name, {}) - if not isinstance(kwargs, dict): - raise TypeError(f'expected a kwargs dict, found {bsfs.typename(kwargs)}') - - try: # build, cache, and return instance - obj = cls(**kwargs) - # cache instance - self._cache[name] = obj - # return instance - return obj - - except Exception as err: - raise errors.BuilderError(f'failed to build reader {name} due to {bsfs.typename(err)}: {err}') from err - - -class ExtractorBuilder(): - """Build `bsie.base.Extractor instances. - - It is permissible to build multiple instances of the same extractor - (typically with different arguments), hence the ExtractorBuilder - receives a list of build specifications. Each specification is - a dict with a single key (extractor's qualified name) and a dict - to be used as keyword arguments. - Example: [{'bsie.extractor.generic.path.Path': {}}, ] - - """ - - # build specifications - _specs: typing.List[typing.Dict[str, typing.Dict[str, typing.Any]]] - - def __init__(self, specs: typing.List[typing.Dict[str, typing.Dict[str, typing.Any]]]): - self._specs = specs - - def __iter__(self) -> typing.Iterator[int]: - """Iterate over extractor specifications.""" - return iter(range(len(self._specs))) - - def build(self, index: int) -> base.Extractor: - """Return an instance of the n'th extractor (n=*index*).""" - # get build instructions - specs = self._specs[index] - - # check specs structure. expecting[{name: {kwargs}}] - if not isinstance(specs, dict): - raise TypeError(f'expected a dict, found {bsfs.typename(specs)}') - if len(specs) != 1: - raise TypeError(f'expected a dict of length one, found {len(specs)}') - - # get name and args from specs - name = next(iter(specs.keys())) - kwargs = specs[name] - - # check kwargs structure - if not isinstance(kwargs, dict): - raise TypeError(f'expected a dict, found {bsfs.typename(kwargs)}') - - # check name and get module/class components - module_name, class_name = _unpack_name(name) - - # import extractor class - cls = _safe_load(module_name, class_name) - - try: # build and return instance - return cls(**kwargs) - - except Exception as err: - raise errors.BuilderError(f'failed to build extractor {name} due to {bsfs.typename(err)}: {err}') from err - - -class PipelineBuilder(): - """Build `bsie.tools.pipeline.Pipeline` instances.""" - - # Prefix to be used in the Pipeline. - prefix: bsfs.Namespace - - # builder for Readers. - rbuild: ReaderBuilder - - # builder for Extractors. - ebuild: ExtractorBuilder - - def __init__( - self, - prefix: bsfs.Namespace, - reader_builder: ReaderBuilder, - extractor_builder: ExtractorBuilder, - ): - self.prefix = prefix - self.rbuild = reader_builder - self.ebuild = extractor_builder - - def build(self) -> pipeline.Pipeline: - """Return a Pipeline instance.""" - ext2rdr = {} - - for eidx in self.ebuild: - # build extractor - try: - ext = self.ebuild.build(eidx) - - except errors.LoaderError as err: # failed to load extractor; skip - logger.error('failed to load extractor: %s', err) - continue - - except errors.BuilderError as err: # failed to build instance; skip - logger.error(str(err)) - continue - - try: - # get reader required by extractor - if ext.CONTENT_READER is not None: - rdr = self.rbuild.build(ext.CONTENT_READER) - else: - rdr = None - # store extractor - ext2rdr[ext] = rdr - - except errors.LoaderError as err: # failed to load reader - logger.error('failed to load reader: %s', err) - - except errors.BuilderError as err: # failed to build reader - logger.error(str(err)) - - return pipeline.Pipeline(self.prefix, ext2rdr) - - - -## EOF ## diff --git a/bsie/tools/pipeline.py b/bsie/tools/pipeline.py deleted file mode 100644 index 20e8ddf..0000000 --- a/bsie/tools/pipeline.py +++ /dev/null @@ -1,144 +0,0 @@ -""" - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" -# imports -from collections import defaultdict -import logging -import typing - -# bsie imports -from bsie import base -from bsie.utils import bsfs, node, ns - -# exports -__all__: typing.Sequence[str] = ( - 'Pipeline', - ) - -# constants -FILE_PREFIX = 'file#' - -## code ## - -logger = logging.getLogger(__name__) - -class Pipeline(): - """Extraction pipeline to generate triples from files. - - The Pipeline binds readers and extractors, and performs - the necessary operations to produce triples from a file. - It takes a best-effort approach to extract as many triples - as possible. Errors during the extraction are passed over - and reported to the log. - - """ - - # combined extractor schemas. - _schema: bsfs.schema.Schema - - # node prefix. - _prefix: bsfs.Namespace - - # extractor -> reader mapping - _ext2rdr: typing.Dict[base.extractor.Extractor, typing.Optional[base.reader.Reader]] - - def __init__( - self, - prefix: bsfs.Namespace, - ext2rdr: typing.Dict[base.extractor.Extractor, typing.Optional[base.reader.Reader]] - ): - # store core members - self._prefix = prefix + FILE_PREFIX - self._ext2rdr = ext2rdr - # compile schema from all extractors - self._schema = bsfs.schema.Schema.Union(ext.schema for ext in ext2rdr) - - def __str__(self) -> str: - return bsfs.typename(self) - - def __repr__(self) -> str: - return f'{bsfs.typename(self)}(...)' - - def __hash__(self) -> int: - return hash((type(self), self._prefix, self._schema, tuple(self._ext2rdr), tuple(self._ext2rdr.values()))) - - def __eq__(self, other: typing.Any) -> bool: - return isinstance(other, type(self)) \ - and self._schema == other._schema \ - and self._prefix == other._prefix \ - and self._ext2rdr == other._ext2rdr - - @property - def schema(self) -> bsfs.schema.Schema: - """Return the pipeline's schema (combined from all extractors).""" - return self._schema - - @property - def principals(self) -> typing.Iterator[bsfs.schema.Predicate]: - """Return the principal predicates that can be extracted.""" - return iter({pred for ext in self._ext2rdr for pred in ext.principals}) - - def subschema(self, principals: typing.Iterable[bsfs.schema.Predicate]) -> bsfs.schema.Schema: - """Return the subset of the schema that supports the given *principals*.""" - # materialize principals - principals = set(principals) - # collect and combine schemas from extractors - return bsfs.schema.Schema.Union({ - ext.schema - for ext - in self._ext2rdr - if not set(ext.principals).isdisjoint(principals) - }) - - def __call__( - self, - path: bsfs.URI, - principals: typing.Optional[typing.Iterable[bsfs.schema.Predicate]] = None, - ) -> typing.Iterator[typing.Tuple[node.Node, bsfs.schema.Predicate, typing.Any]]: - """Extract triples from the file at *path*. Optionally, limit triples to *principals*.""" - # get principals - principals = set(principals) if principals is not None else set(self.schema.predicates()) - - # get extractors - extractors = {ext for ext in self._ext2rdr if not set(ext.principals).isdisjoint(principals)} - - # corner-case short-cut - if len(extractors) == 0: - return - - # get readers -> extractors mapping - rdr2ext = defaultdict(set) - for ext in extractors: - rdr = self._ext2rdr[ext] - rdr2ext[rdr].add(ext) - - # create subject for file - uuid = bsfs.uuid.UCID.from_path(path) - subject = node.Node(ns.bsfs.File, self._prefix[uuid]) - - # extract information - for rdr, extrs in rdr2ext.items(): - try: - # get content - content = rdr(path) if rdr is not None else None - - # apply extractors on this content - for ext in extrs: - try: - # get predicate/value tuples - for subject, pred, value in ext.extract(subject, content, principals): - yield subject, pred, value - - except base.errors.ExtractorError as err: - # critical extractor failure. - logger.error('%s failed to extract triples from content: %s', ext, err) - - except base.errors.ReaderError as err: - # failed to read any content. skip. - logger.error('%s failed to read content: %s', rdr, err) - - -## EOF ## diff --git a/bsie/utils/__init__.py b/bsie/utils/__init__.py index 3981dc7..9cb60ed 100644 --- a/bsie/utils/__init__.py +++ b/bsie/utils/__init__.py @@ -4,21 +4,24 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # inner-module imports from . import bsfs +from . import filematcher from . import namespaces as ns from . import node -from . import filematcher +from .loading import safe_load, unpack_qualified_name # exports __all__: typing.Sequence[str] = ( - 'filematcher', 'bsfs', + 'filematcher', 'node', 'ns', + 'safe_load', + 'unpack_qualified_name', ) ## EOF ## diff --git a/bsie/utils/errors.py b/bsie/utils/errors.py new file mode 100644 index 0000000..5fafd5b --- /dev/null +++ b/bsie/utils/errors.py @@ -0,0 +1,45 @@ +"""Common BSIE exceptions. + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# imports +import typing + +# exports +__all__: typing.Sequence[str] = ( + 'BuilderError', + 'ExtractorError', + 'LoaderError', + 'ReaderError', + ) + + +## code ## + +class _BSIEError(Exception): + """Generic BSIE error.""" + +class BuilderError(_BSIEError): + """The Builder failed to create an instance.""" + +class LoaderError(BuilderError): + """Failed to load a module or class.""" + +class ExtractorError(_BSIEError): + """The Extractor failed to process the given content.""" + +class ReaderError(_BSIEError): + """The Reader failed to read the given file.""" + +class ProgrammingError(_BSIEError): + """An assertion-like error that indicates a code-base issue.""" + +class UnreachableError(ProgrammingError): + """Bravo, you've reached a point in code that should logically not be reachable.""" + +class ParserError(_BSIEError): + """Failed to parse due to invalid syntax or structures.""" + +## EOF ## diff --git a/bsie/utils/filematcher/parser.py b/bsie/utils/filematcher/parser.py index 0654742..2f82875 100644 --- a/bsie/utils/filematcher/parser.py +++ b/bsie/utils/filematcher/parser.py @@ -7,16 +7,14 @@ Author: Matthias Baumgartner, 2021 # standard imports import typing -# non-standard imports +# external imports import pyparsing from pyparsing import printables, alphas8bit, punc8bit, QuotedString, Word, \ delimitedList, Or, CaselessKeyword, Group, oneOf, Optional -# bsie imports -from bsie.base import errors - # inner-module imports from . import matcher +from .. import errors # exports __all__: typing.Sequence[str] = ( diff --git a/bsie/utils/loading.py b/bsie/utils/loading.py new file mode 100644 index 0000000..eb05c35 --- /dev/null +++ b/bsie/utils/loading.py @@ -0,0 +1,54 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import importlib +import typing + +# inner-module imports +from . import errors + +# exports +__all__: typing.Sequence[str] = ( + 'safe_load', + 'unpack_qualified_name', + ) + + +## code ## + +def safe_load(module_name: str, class_name: str): + """Get a class from a module. Raise BuilderError if anything goes wrong.""" + try: + # load the module + module = importlib.import_module(module_name) + except Exception as err: + # cannot import module + raise errors.LoaderError(f'cannot load module {module_name}') from err + + try: + # get the class from the module + cls = getattr(module, class_name) + except Exception as err: + # cannot find the class + raise errors.LoaderError(f'cannot load class {class_name} from module {module_name}') from err + + return cls + + +def unpack_qualified_name(name): + """Split a name into its module and class component (dot-separated).""" + if not isinstance(name, str): + raise TypeError(name) + if '.' not in name: + raise ValueError('name must be a qualified class name.') + module_name, class_name = name[:name.rfind('.')], name[name.rfind('.')+1:] + if module_name == '': + raise ValueError('name must be a qualified class name.') + return module_name, class_name + + +## EOF ## -- cgit v1.2.3 From 07219685d01f803dc46c8d5465fa542c1d822cb4 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 24 Dec 2022 10:39:51 +0100 Subject: documentation: standard vs external import --- bsie/__init__.py | 2 +- bsie/apps/__init__.py | 2 +- bsie/extractor/base.py | 2 +- bsie/extractor/generic/__init__.py | 2 +- bsie/reader/base.py | 2 +- bsie/utils/bsfs.py | 2 +- bsie/utils/errors.py | 2 +- bsie/utils/filematcher/__init__.py | 2 +- bsie/utils/filematcher/matcher.py | 4 +++- bsie/utils/namespaces.py | 2 +- bsie/utils/node.py | 2 +- 11 files changed, 13 insertions(+), 11 deletions(-) (limited to 'bsie') diff --git a/bsie/__init__.py b/bsie/__init__.py index 8d2308c..c253f39 100644 --- a/bsie/__init__.py +++ b/bsie/__init__.py @@ -4,7 +4,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import collections import typing diff --git a/bsie/apps/__init__.py b/bsie/apps/__init__.py index a548c3c..1c3d0f9 100644 --- a/bsie/apps/__init__.py +++ b/bsie/apps/__init__.py @@ -4,7 +4,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # inner-module imports diff --git a/bsie/extractor/base.py b/bsie/extractor/base.py index c44021b..95689a5 100644 --- a/bsie/extractor/base.py +++ b/bsie/extractor/base.py @@ -4,7 +4,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import abc import typing diff --git a/bsie/extractor/generic/__init__.py b/bsie/extractor/generic/__init__.py index 0cb7e7f..4783949 100644 --- a/bsie/extractor/generic/__init__.py +++ b/bsie/extractor/generic/__init__.py @@ -7,7 +7,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # exports diff --git a/bsie/reader/base.py b/bsie/reader/base.py index cbabd36..08d6cc6 100644 --- a/bsie/reader/base.py +++ b/bsie/reader/base.py @@ -8,7 +8,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import abc import typing diff --git a/bsie/utils/bsfs.py b/bsie/utils/bsfs.py index 0b88479..ef5db31 100644 --- a/bsie/utils/bsfs.py +++ b/bsie/utils/bsfs.py @@ -4,7 +4,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # bsfs imports diff --git a/bsie/utils/errors.py b/bsie/utils/errors.py index 5fafd5b..fbc16f7 100644 --- a/bsie/utils/errors.py +++ b/bsie/utils/errors.py @@ -4,7 +4,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # exports diff --git a/bsie/utils/filematcher/__init__.py b/bsie/utils/filematcher/__init__.py index b1c1b45..1e23e4e 100644 --- a/bsie/utils/filematcher/__init__.py +++ b/bsie/utils/filematcher/__init__.py @@ -4,7 +4,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # inner-module imports diff --git a/bsie/utils/filematcher/matcher.py b/bsie/utils/filematcher/matcher.py index 164beeb..a279a4b 100644 --- a/bsie/utils/filematcher/matcher.py +++ b/bsie/utils/filematcher/matcher.py @@ -4,11 +4,13 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2021 """ -# imports +# standard imports from collections.abc import Callable, Collection, Hashable import abc import os import typing + +# external imports import magic # exports diff --git a/bsie/utils/namespaces.py b/bsie/utils/namespaces.py index a29fc1b..2d0b535 100644 --- a/bsie/utils/namespaces.py +++ b/bsie/utils/namespaces.py @@ -4,7 +4,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # inner-module imports diff --git a/bsie/utils/node.py b/bsie/utils/node.py index ecf39cd..91e4f37 100644 --- a/bsie/utils/node.py +++ b/bsie/utils/node.py @@ -4,7 +4,7 @@ Part of the bsie module. A copy of the license is provided with the project. Author: Matthias Baumgartner, 2022 """ -# imports +# standard imports import typing # bsie imports -- cgit v1.2.3 From 17f03ae3d3dc53fe973f37fe4dea4a831b4f97d7 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Sat, 24 Dec 2022 16:06:16 +0100 Subject: ReaderChain and image reader --- bsie/reader/base.py | 2 +- bsie/reader/chain.py | 85 +++++++++++++++++++++++++++++++++++++++++++ bsie/reader/image/__init__.py | 36 ++++++++++++++++++ bsie/reader/image/_pillow.py | 37 +++++++++++++++++++ bsie/reader/image/_raw.py | 61 +++++++++++++++++++++++++++++++ 5 files changed, 220 insertions(+), 1 deletion(-) create mode 100644 bsie/reader/chain.py create mode 100644 bsie/reader/image/__init__.py create mode 100644 bsie/reader/image/_pillow.py create mode 100644 bsie/reader/image/_raw.py (limited to 'bsie') diff --git a/bsie/reader/base.py b/bsie/reader/base.py index 08d6cc6..099a327 100644 --- a/bsie/reader/base.py +++ b/bsie/reader/base.py @@ -39,7 +39,7 @@ class Reader(abc.ABC): return hash(type(self)) @abc.abstractmethod - def __call__(self, path: bsfs.URI) -> typing.Any: + def __call__(self, path: str) -> typing.Any: """Return some content of the file at *path*. Raises a `ReaderError` if the reader cannot make sense of the file format. """ diff --git a/bsie/reader/chain.py b/bsie/reader/chain.py new file mode 100644 index 0000000..8e900e1 --- /dev/null +++ b/bsie/reader/chain.py @@ -0,0 +1,85 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import logging +import typing + +# bsie imports +from bsie.utils import bsfs, errors + +# inner-module imports +from . import base +from . import builder + +# exports +__all__: typing.Sequence[str] = ( + 'ReaderChain', + ) + + +## code ## + +logger = logging.getLogger(__name__) + +# Content type. +T_CONTENT = typing.TypeVar('T_CONTENT') # pylint: disable=invalid-name + +class ReaderChain(base.Reader, typing.Generic[T_CONTENT]): + """Read an image.""" + + # sub-readers for specific file formats. + _children: typing.Tuple[base.Reader, ...] + + def __init__( + self, + subreader_names: typing.Iterable[str], + cfg: typing.Any, + ): + rbuild = builder.ReaderBuilder(cfg) + children = [] + for name in subreader_names: + try: + # build sub-reader + children.append(rbuild.build(name)) + except (ValueError, + TypeError, + errors.LoaderError, + errors.BuilderError) as err: + # failed to build a child; skip and notify + logger.warning('failed to load reader: %s', err) + + if len(children) == 0: + logger.warning('%s failed to load any sub-readers.', bsfs.typename(self)) + + # copy children to member + self._children = tuple(children) + + def __str__(self) -> str: + substr = ', '.join(str(child) for child in self._children) + return f'{bsfs.typename(self)}({substr})' + + def __repr__(self) -> str: + return f'{bsfs.typename(self)}({self._children})' + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) \ + and self._children == other._children + + def __hash__(self) -> int: + return hash((super().__hash__(), self._children)) + + def __call__(self, path: str) -> T_CONTENT: + for child in self._children: + try: + return child(path) + except errors.ReaderError: + # child cannot read the file, skip. + pass + + raise errors.ReaderError(path) + +## EOF ## diff --git a/bsie/reader/image/__init__.py b/bsie/reader/image/__init__.py new file mode 100644 index 0000000..85dad85 --- /dev/null +++ b/bsie/reader/image/__init__.py @@ -0,0 +1,36 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# external imports +import PIL.Image + +# inner-module imports +from .. import chain + +# constants +_FILE_FORMAT_READERS: typing.Sequence[str] = ( + __package__ + '._raw.RawImage', + __package__ + '._pillow.PillowImage', + ) + +# exports +__all__: typing.Sequence[str] = ( + 'Image', + ) + + +## code ## + +class Image(chain.ReaderChain[PIL.Image]): # pylint: disable=too-few-public-methods + """Read an image file.""" + + def __init__(self, cfg): + super().__init__(_FILE_FORMAT_READERS, cfg) + +## EOF ## diff --git a/bsie/reader/image/_pillow.py b/bsie/reader/image/_pillow.py new file mode 100644 index 0000000..ee0662d --- /dev/null +++ b/bsie/reader/image/_pillow.py @@ -0,0 +1,37 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# external imports +import PIL.Image + +# bsie imports +from bsie.utils import errors + +# inner-module imports +from .. import base + +# exports +__all__: typing.Sequence[str] = ( + 'PillowImage', + ) + + +## code ## + +class PillowImage(base.Reader): + """Use PIL to read content of a variety of image file types.""" + + def __call__(self, path: str) -> PIL.Image: + try: + # open file with PIL + return PIL.Image.open(path) + except IOError as err: + raise errors.ReaderError(path) from err + +# EOF ## diff --git a/bsie/reader/image/_raw.py b/bsie/reader/image/_raw.py new file mode 100644 index 0000000..77be357 --- /dev/null +++ b/bsie/reader/image/_raw.py @@ -0,0 +1,61 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# external imports +import PIL.Image +import rawpy + +# bsie imports +from bsie.utils import errors, filematcher + +# inner-module imports +from .. import base + +# constants +MATCH_RULE = 'mime={image/x-nikon-nef} | extension={nef}' + +# exports +__all__: typing.Sequence[str] = ( + 'RawImage', + ) + + +## code ## + +class RawImage(base.Reader): + """Use rawpy to read content of raw image file types.""" + + # file matcher + match: filematcher.Matcher + + # additional kwargs to rawpy's postprocess + rawpy_kwargs: typing.Dict[str, typing.Any] + + def __init__(self, **rawpy_kwargs): + match_rule = rawpy_kwargs.pop('file_match_rule', MATCH_RULE) + self._match = filematcher.parse(match_rule) + self._rawpy_kwargs = rawpy_kwargs + + def __call__(self, path: str) -> PIL.Image: + # perform quick checks first + if not self._match(path): + raise errors.ReaderError(path) + + try: + # open file with rawpy + ary = rawpy.imread(path).postprocess(**self._rawpy_kwargs) + # convert to PIL.Image + return PIL.Image.fromarray(ary) + except (rawpy.LibRawFatalError, # pylint: disable=no-member # pylint doesn't find the errors + rawpy.NotSupportedError, # pylint: disable=no-member + rawpy.LibRawNonFatalError, # pylint: disable=no-member + ) as err: + raise errors.ReaderError(path) from err + +## EOF ## -- cgit v1.2.3 From 76f2cc9206276ca21a395dd9417ff7dfed0467fd Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 08:53:41 +0100 Subject: schema serialization adjustments and app --- bsie/apps/info.py | 5 ++++- bsie/extractor/base.py | 11 ++++++++++- bsie/extractor/generic/constant.py | 2 +- bsie/extractor/generic/path.py | 2 +- bsie/extractor/generic/stat.py | 2 +- 5 files changed, 17 insertions(+), 5 deletions(-) (limited to 'bsie') diff --git a/bsie/apps/info.py b/bsie/apps/info.py index a4e611c..cd28685 100644 --- a/bsie/apps/info.py +++ b/bsie/apps/info.py @@ -26,7 +26,7 @@ __all__: typing.Sequence[str] = ( def main(argv): """Show information from BSIE.""" parser = argparse.ArgumentParser(description=main.__doc__, prog='info') - parser.add_argument('what', choices=('predicates', ), + parser.add_argument('what', choices=('predicates', 'schema'), help='Select what information to show.') args = parser.parse_args(argv) @@ -63,6 +63,9 @@ def main(argv): # show predicates for pred in pipeline.schema.predicates(): print(pred.uri) + elif args.what == 'schema': + # show schema + print(bsfs.schema.to_string(pipeline.schema)) else: # args.what is already checked by argparse raise errors.UnreachableError() diff --git a/bsie/extractor/base.py b/bsie/extractor/base.py index 95689a5..8ab1124 100644 --- a/bsie/extractor/base.py +++ b/bsie/extractor/base.py @@ -31,13 +31,22 @@ SCHEMA_PREAMBLE = ''' prefix bsfs: prefix bse: + # default definitions + bsfs:Array rdfs:subClassOf bsfs:Literal . + bsfs:Number rdfs:subClassOf bsfs:Literal . + bsfs:Time rdfs:subClassOf bsfs:Literal . + bsfs:Feature rdfs:subClassOf bsfs:Array ; + bsfs:dimension "1"^^xsd:integer ; + bsfs:dtype bsfs:f16 ; + bsfs:distance bsfs:euclidean . + # essential nodes bsfs:Entity rdfs:subClassOf bsfs:Node . bsfs:File rdfs:subClassOf bsfs:Entity . # common definitions xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Literal . + xsd:integer rdfs:subClassOf bsfs:Number . ''' diff --git a/bsie/extractor/generic/constant.py b/bsie/extractor/generic/constant.py index 7b1d942..938e20c 100644 --- a/bsie/extractor/generic/constant.py +++ b/bsie/extractor/generic/constant.py @@ -34,7 +34,7 @@ class Constant(base.Extractor): schema: str, tuples: typing.Iterable[typing.Tuple[bsfs.URI, typing.Any]], ): - super().__init__(bsfs.schema.Schema.from_string(base.SCHEMA_PREAMBLE + schema)) + super().__init__(bsfs.schema.from_string(base.SCHEMA_PREAMBLE + schema)) # NOTE: Raises a KeyError if the predicate is not part of the schema self._tuples = tuple((self.schema.predicate(p_uri), value) for p_uri, value in tuples) # TODO: use schema instance for value checking diff --git a/bsie/extractor/generic/path.py b/bsie/extractor/generic/path.py index 295715f..c984515 100644 --- a/bsie/extractor/generic/path.py +++ b/bsie/extractor/generic/path.py @@ -29,7 +29,7 @@ class Path(base.Extractor): _callmap: typing.Dict[bsfs.schema.Predicate, typing.Callable[[str], typing.Any]] def __init__(self): - super().__init__(bsfs.schema.Schema.from_string(base.SCHEMA_PREAMBLE + ''' + super().__init__(bsfs.schema.from_string(base.SCHEMA_PREAMBLE + ''' bse:filename rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:File ; rdfs:range xsd:string ; diff --git a/bsie/extractor/generic/stat.py b/bsie/extractor/generic/stat.py index 1381fe2..9394456 100644 --- a/bsie/extractor/generic/stat.py +++ b/bsie/extractor/generic/stat.py @@ -31,7 +31,7 @@ class Stat(base.Extractor): _callmap: typing.Dict[bsfs.schema.Predicate, typing.Callable[[os.stat_result], typing.Any]] def __init__(self): - super().__init__(bsfs.schema.Schema.from_string(base.SCHEMA_PREAMBLE + ''' + super().__init__(bsfs.schema.from_string(base.SCHEMA_PREAMBLE + ''' bse:filesize rdfs:subClassOf bsfs:Predicate ; rdfs:domain bsfs:File ; rdfs:range xsd:integer ; -- cgit v1.2.3 From 5d7fa2716009bc32c08f27e686cd92ca4c02b670 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 14:38:01 +0100 Subject: colors spatial feature --- bsie/extractor/image/__init__.py | 13 +++ bsie/extractor/image/colors_spatial.py | 155 +++++++++++++++++++++++++++++++++ bsie/utils/namespaces.py | 1 + 3 files changed, 169 insertions(+) create mode 100644 bsie/extractor/image/__init__.py create mode 100644 bsie/extractor/image/colors_spatial.py (limited to 'bsie') diff --git a/bsie/extractor/image/__init__.py b/bsie/extractor/image/__init__.py new file mode 100644 index 0000000..75b118d --- /dev/null +++ b/bsie/extractor/image/__init__.py @@ -0,0 +1,13 @@ +""" + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# exports +__all__: typing.Sequence[str] = [] + +## EOF ## diff --git a/bsie/extractor/image/colors_spatial.py b/bsie/extractor/image/colors_spatial.py new file mode 100644 index 0000000..fa31ea7 --- /dev/null +++ b/bsie/extractor/image/colors_spatial.py @@ -0,0 +1,155 @@ +"""Spatial color features. + +Part of the bsie module. +A copy of the license is provided with the project. +Author: Matthias Baumgartner, 2022 +""" +# standard imports +import typing + +# external imports +import PIL.Image +import numpy as np + +# bsie imports +from bsie.utils import bsfs, node, ns + +# inner-module imports +from .. import base + +# constants +FEATURE_NAME = ns.bsf + 'ColorsSpatial' +PREDICATE_NAME = ns.bse + 'colors_spatial' + +# exports +__all__: typing.Sequence[str] = ( + 'ColorsSpatial', + ) + + +## code ## + +class ColorsSpatial(base.Extractor): + """Determine dominant colors of subregions in the image. + + Computes the domiant color of increasingly smaller subregions of the image. + """ + + CONTENT_READER = 'bsie.reader.image.Image' + + # Initial subregion width. + width: int + + # Initial subregion height. + height: int + + # Decrement exponent. + exp: float + + # Principal predicate's URI. + _predicate_name: bsfs.URI + + def __init__( + self, + width: int = 32, + height: int = 32, + exp: float = 4., + ): + # instance identifier + uuid = bsfs.uuid.UCID.from_dict({ + 'width': width, + 'height': height, + 'exp': exp, + }) + # determine symbol names + instance_name = FEATURE_NAME[uuid] + predicate_name = PREDICATE_NAME[uuid] + # get vector dimension + dimension = self.dimension(width, height, exp) + # initialize parent with the schema + super().__init__(bsfs.schema.from_string(base.SCHEMA_PREAMBLE + f''' + <{FEATURE_NAME}> rdfs:subClassOf bsfs:Feature ; + # annotations + rdfs:label "Spatially dominant colors"^^xsd:string ; + schema:description "Domiant colors of subregions in an image."^^xsd:string . + + <{instance_name}> rdfs:subClassOf <{FEATURE_NAME}> ; + bsfs:dimension "{dimension}"^^xsd:integer ; + # annotations + <{FEATURE_NAME}/args#width> "{width}"^^xsd:integer ; + <{FEATURE_NAME}/args#height> "{height}"^^xsd:integer ; + <{FEATURE_NAME}/args#exp> "{exp}"^^xsd:float . + + <{predicate_name}> rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsfs:File ; + rdfs:range <{instance_name}> ; + bsfs:unique "true"^^xsd:boolean . + + ''')) + # assign extra members + self.width = width + self.height = height + self.exp = exp + self._predicate_name = predicate_name + + def __repr__(self) -> str: + return f'{bsfs.typename(self)}({self.width}, {self.height}, {self.exp})' + + def __eq__(self, other: typing.Any) -> bool: + return super().__eq__(other) \ + and self.width == other.width \ + and self.height == other.height \ + and self.exp == other.exp + + def __hash__(self) -> int: + return hash((super().__hash__(), self.width, self.height, self.exp)) + + @staticmethod + def dimension(width: int, height: int, exp: float) -> int: + """Return the feature vector dimension.""" + # FIXME: replace with a proper formula + dim = 0 + while width >= 1 and height >= 1: + dim += width * height + width = np.floor(width / exp) + height = np.floor(height / exp) + dim *= 3 # per band + return int(dim) + + def extract( + self, + subject: node.Node, + content: PIL.Image, + principals: typing.Iterable[bsfs.schema.Predicate], + ) -> typing.Iterator[typing.Tuple[node.Node, bsfs.schema.Predicate, typing.List[float]]]: + # check principals + if self.schema.predicate(self._predicate_name) not in principals: + # nothing to do; abort + return + + # convert to HSV + content = content.convert('HSV') + + # get dimensions + width, height = self.width, self.height + num_bands = len(content.getbands()) # it's three since we converted to HSV before + + features = [] + while width >= 1 and height >= 1: + # downsample + img = content.resize((width, height), resample=PIL.Image.Resampling.BOX) + # feature vector + features.append( + np.array(img.getdata()).reshape((width * height, num_bands))) + # iterate + width = int(np.floor(width / self.exp)) + height = int(np.floor(height / self.exp)) + + # combine features + value = np.vstack(features) + # convert features + value = value.reshape(-1).tolist() # several bands + # return triple with feature vector as value + yield subject, self.schema.predicate(self._predicate_name), value + +## EOF ## diff --git a/bsie/utils/namespaces.py b/bsie/utils/namespaces.py index 2d0b535..393b436 100644 --- a/bsie/utils/namespaces.py +++ b/bsie/utils/namespaces.py @@ -15,6 +15,7 @@ bse = _bsfs.Namespace('http://bsfs.ai/schema/Entity') bsfs = _bsfs.Namespace('http://bsfs.ai/schema', fsep='/') bsm = _bsfs.Namespace('http://bsfs.ai/schema/Meta') xsd = _bsfs.Namespace('http://www.w3.org/2001/XMLSchema') +bsf = _bsfs.Namespace('http://ie.bsfs.ai/schema/Feature') # export __all__: typing.Sequence[str] = ( -- cgit v1.2.3 From 3f93be488638fdf6668e0e03e2b1634bb969ca80 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 15:39:16 +0100 Subject: random fixes --- bsie/apps/info.py | 4 ++++ bsie/reader/image/__init__.py | 3 ++- 2 files changed, 6 insertions(+), 1 deletion(-) (limited to 'bsie') diff --git a/bsie/apps/info.py b/bsie/apps/info.py index cd28685..5b6fb0e 100644 --- a/bsie/apps/info.py +++ b/bsie/apps/info.py @@ -33,6 +33,10 @@ def main(argv): # FIXME: Read reader/extractor configs from a config file # reader builder rbuild = ReaderBuilder({ + 'bsie.reader.image.Image': { + 'bsie.reader.image._raw.RawImage': { + } + } }) # extractor builder ebuild = ExtractorBuilder([ diff --git a/bsie/reader/image/__init__.py b/bsie/reader/image/__init__.py index 85dad85..b7587e7 100644 --- a/bsie/reader/image/__init__.py +++ b/bsie/reader/image/__init__.py @@ -27,7 +27,8 @@ __all__: typing.Sequence[str] = ( ## code ## -class Image(chain.ReaderChain[PIL.Image]): # pylint: disable=too-few-public-methods +# FIXME: Check if PIL.Image or PIL.Image.Image, or if version-dependent +class Image(chain.ReaderChain[PIL.Image.Image]): # pylint: disable=too-few-public-methods """Read an image file.""" def __init__(self, cfg): -- cgit v1.2.3 From afd165000c1661a9cca117a4844ad3f89d926fdb Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 20:53:39 +0100 Subject: unsupported file format exception --- bsie/lib/pipeline.py | 7 +++++++ bsie/reader/chain.py | 7 +++++-- bsie/reader/image/_pillow.py | 2 ++ bsie/reader/image/_raw.py | 2 +- bsie/utils/errors.py | 3 +++ 5 files changed, 18 insertions(+), 3 deletions(-) (limited to 'bsie') diff --git a/bsie/lib/pipeline.py b/bsie/lib/pipeline.py index e5ce1b7..02119bc 100644 --- a/bsie/lib/pipeline.py +++ b/bsie/lib/pipeline.py @@ -126,6 +126,8 @@ class Pipeline(): # get content content = rdr(path) if rdr is not None else None + #logger.info('extracted %s from %s', rdr, path) + # apply extractors on this content for ext in extrs: try: @@ -137,6 +139,11 @@ class Pipeline(): # critical extractor failure. logger.error('%s failed to extract triples from content: %s', ext, err) + except errors.UnsupportedFileFormatError as err: + # failed to read the file format. skip. + #logger.warning('%s could not process the file format of %s', rdr, err) + pass + except errors.ReaderError as err: # failed to read any content. skip. logger.error('%s failed to read content: %s', rdr, err) diff --git a/bsie/reader/chain.py b/bsie/reader/chain.py index 8e900e1..db7c2d5 100644 --- a/bsie/reader/chain.py +++ b/bsie/reader/chain.py @@ -73,13 +73,16 @@ class ReaderChain(base.Reader, typing.Generic[T_CONTENT]): return hash((super().__hash__(), self._children)) def __call__(self, path: str) -> T_CONTENT: + raise_error = errors.UnsupportedFileFormatError for child in self._children: try: return child(path) + except errors.UnsupportedFileFormatError: + pass except errors.ReaderError: # child cannot read the file, skip. - pass + raise_error = errors.ReaderError # type: ignore [assignment] # mypy is confused - raise errors.ReaderError(path) + raise raise_error(path) ## EOF ## diff --git a/bsie/reader/image/_pillow.py b/bsie/reader/image/_pillow.py index ee0662d..3144509 100644 --- a/bsie/reader/image/_pillow.py +++ b/bsie/reader/image/_pillow.py @@ -31,6 +31,8 @@ class PillowImage(base.Reader): try: # open file with PIL return PIL.Image.open(path) + except PIL.UnidentifiedImageError as err: + raise errors.UnsupportedFileFormatError(path) from err except IOError as err: raise errors.ReaderError(path) from err diff --git a/bsie/reader/image/_raw.py b/bsie/reader/image/_raw.py index 77be357..cd60453 100644 --- a/bsie/reader/image/_raw.py +++ b/bsie/reader/image/_raw.py @@ -45,7 +45,7 @@ class RawImage(base.Reader): def __call__(self, path: str) -> PIL.Image: # perform quick checks first if not self._match(path): - raise errors.ReaderError(path) + raise errors.UnsupportedFileFormatError(path) try: # open file with rawpy diff --git a/bsie/utils/errors.py b/bsie/utils/errors.py index fbc16f7..8133cd4 100644 --- a/bsie/utils/errors.py +++ b/bsie/utils/errors.py @@ -42,4 +42,7 @@ class UnreachableError(ProgrammingError): class ParserError(_BSIEError): """Failed to parse due to invalid syntax or structures.""" +class UnsupportedFileFormatError(ReaderError): + """Failed to read a file format.""" + ## EOF ## -- cgit v1.2.3 From bffe6bb52d00e60665b4e8e2144ab91e4465173e Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 20:54:25 +0100 Subject: minor bugfixes --- bsie/apps/index.py | 4 +++- bsie/extractor/base.py | 1 + bsie/extractor/image/colors_spatial.py | 8 +++----- 3 files changed, 7 insertions(+), 6 deletions(-) (limited to 'bsie') diff --git a/bsie/apps/index.py b/bsie/apps/index.py index 0c6296f..7cf94d3 100644 --- a/bsie/apps/index.py +++ b/bsie/apps/index.py @@ -82,7 +82,9 @@ def main(argv): # index input paths for path in args.input_file: - if os.path.isdir(path) and args.recursive: + if not os.path.exists(path): + pass # FIXME: notify the user + elif os.path.isdir(path) and args.recursive: for dirpath, _, filenames in os.walk(path, topdown=True, followlinks=args.follow): for filename in filenames: for node, pred, value in bsie.from_file(os.path.join(dirpath, filename)): diff --git a/bsie/extractor/base.py b/bsie/extractor/base.py index 8ab1124..7401244 100644 --- a/bsie/extractor/base.py +++ b/bsie/extractor/base.py @@ -108,5 +108,6 @@ class Extractor(abc.ABC): principals: typing.Iterable[bsfs.schema.Predicate], ) -> typing.Iterator[typing.Tuple[node.Node, bsfs.schema.Predicate, typing.Any]]: """Return (node, predicate, value) triples.""" + # FIXME: type annotation could be more strict: value is Hashable ## EOF ## diff --git a/bsie/extractor/image/colors_spatial.py b/bsie/extractor/image/colors_spatial.py index fa31ea7..38f1db4 100644 --- a/bsie/extractor/image/colors_spatial.py +++ b/bsie/extractor/image/colors_spatial.py @@ -121,7 +121,7 @@ class ColorsSpatial(base.Extractor): subject: node.Node, content: PIL.Image, principals: typing.Iterable[bsfs.schema.Predicate], - ) -> typing.Iterator[typing.Tuple[node.Node, bsfs.schema.Predicate, typing.List[float]]]: + ) -> typing.Iterator[typing.Tuple[node.Node, bsfs.schema.Predicate, typing.Any]]: # check principals if self.schema.predicate(self._predicate_name) not in principals: # nothing to do; abort @@ -145,10 +145,8 @@ class ColorsSpatial(base.Extractor): width = int(np.floor(width / self.exp)) height = int(np.floor(height / self.exp)) - # combine features - value = np.vstack(features) - # convert features - value = value.reshape(-1).tolist() # several bands + # combine bands and convert features to tuple + value = tuple(np.vstack(features).reshape(-1)) # return triple with feature vector as value yield subject, self.schema.predicate(self._predicate_name), value -- cgit v1.2.3 From 02bbad817077e9a23f7b24b82845fcde24de63a9 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 20:55:13 +0100 Subject: image feature integration test --- bsie/apps/index.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) (limited to 'bsie') diff --git a/bsie/apps/index.py b/bsie/apps/index.py index 7cf94d3..25e006f 100644 --- a/bsie/apps/index.py +++ b/bsie/apps/index.py @@ -44,7 +44,10 @@ def main(argv): # FIXME: Read reader/extractor configs from a config file # reader builder - rbuild = ReaderBuilder({}) + rbuild = ReaderBuilder({ + 'bsie.reader.image.Image': { + 'cfg': {}}, # FIXME: cfg should be optional! + }) # extractor builder ebuild = ExtractorBuilder([ {'bsie.extractor.generic.path.Path': {}}, @@ -58,6 +61,11 @@ def main(argv): bsfs:unique "true"^^xsd:boolean . ''', )}, + {'bsie.extractor.image.colors_spatial.ColorsSpatial': { + 'width': 2, + 'height': 2, + 'exp': 2, + }}, ]) # pipeline builder pbuild = PipelineBuilder( -- cgit v1.2.3 From 8439089807bbad92e95ad9062dc74c3d71f5d7eb Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 21:35:19 +0100 Subject: ReaderBuilder optional config --- bsie/apps/index.py | 5 +---- bsie/apps/info.py | 7 +------ bsie/reader/builder.py | 6 +++++- bsie/reader/chain.py | 2 +- bsie/reader/image/__init__.py | 2 +- 5 files changed, 9 insertions(+), 13 deletions(-) (limited to 'bsie') diff --git a/bsie/apps/index.py b/bsie/apps/index.py index 25e006f..21c2318 100644 --- a/bsie/apps/index.py +++ b/bsie/apps/index.py @@ -44,10 +44,7 @@ def main(argv): # FIXME: Read reader/extractor configs from a config file # reader builder - rbuild = ReaderBuilder({ - 'bsie.reader.image.Image': { - 'cfg': {}}, # FIXME: cfg should be optional! - }) + rbuild = ReaderBuilder() # extractor builder ebuild = ExtractorBuilder([ {'bsie.extractor.generic.path.Path': {}}, diff --git a/bsie/apps/info.py b/bsie/apps/info.py index 5b6fb0e..d8a70a6 100644 --- a/bsie/apps/info.py +++ b/bsie/apps/info.py @@ -32,12 +32,7 @@ def main(argv): # FIXME: Read reader/extractor configs from a config file # reader builder - rbuild = ReaderBuilder({ - 'bsie.reader.image.Image': { - 'bsie.reader.image._raw.RawImage': { - } - } - }) + rbuild = ReaderBuilder() # extractor builder ebuild = ExtractorBuilder([ {'bsie.extractor.generic.path.Path': {}}, diff --git a/bsie/reader/builder.py b/bsie/reader/builder.py index bce5397..8699e75 100644 --- a/bsie/reader/builder.py +++ b/bsie/reader/builder.py @@ -40,7 +40,11 @@ class ReaderBuilder(): # cached readers _cache: typing.Dict[str, base.Reader] - def __init__(self, kwargs: typing.Dict[str, typing.Dict[str, typing.Any]]): + def __init__( + self, + kwargs: typing.Optional[typing.Dict[str, typing.Dict[str, typing.Any]]] = None): + if kwargs is None: + kwargs = {} self._kwargs = kwargs self._cache = {} diff --git a/bsie/reader/chain.py b/bsie/reader/chain.py index db7c2d5..5e9e0d5 100644 --- a/bsie/reader/chain.py +++ b/bsie/reader/chain.py @@ -37,7 +37,7 @@ class ReaderChain(base.Reader, typing.Generic[T_CONTENT]): def __init__( self, subreader_names: typing.Iterable[str], - cfg: typing.Any, + cfg: typing.Optional[typing.Any] = None, ): rbuild = builder.ReaderBuilder(cfg) children = [] diff --git a/bsie/reader/image/__init__.py b/bsie/reader/image/__init__.py index b7587e7..1f290b5 100644 --- a/bsie/reader/image/__init__.py +++ b/bsie/reader/image/__init__.py @@ -31,7 +31,7 @@ __all__: typing.Sequence[str] = ( class Image(chain.ReaderChain[PIL.Image.Image]): # pylint: disable=too-few-public-methods """Read an image file.""" - def __init__(self, cfg): + def __init__(self, cfg: typing.Optional[typing.Any] = None): super().__init__(_FILE_FORMAT_READERS, cfg) ## EOF ## -- cgit v1.2.3 From 4f868bcb3be2658960eace3222563cc9a819366a Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 21:36:11 +0100 Subject: info schema and feature tests --- bsie/apps/info.py | 5 +++++ 1 file changed, 5 insertions(+) (limited to 'bsie') diff --git a/bsie/apps/info.py b/bsie/apps/info.py index d8a70a6..64a4eba 100644 --- a/bsie/apps/info.py +++ b/bsie/apps/info.py @@ -46,6 +46,11 @@ def main(argv): bsfs:unique "true"^^xsd:boolean . ''', )}, + {'bsie.extractor.image.colors_spatial.ColorsSpatial': { + 'width': 2, + 'height': 2, + 'exp': 2, + }}, ]) # pipeline builder pbuild = PipelineBuilder( -- cgit v1.2.3 From 58aaa864f9747d27c065739256d4c6635ca9b751 Mon Sep 17 00:00:00 2001 From: Matthias Baumgartner Date: Mon, 16 Jan 2023 21:36:50 +0100 Subject: minor fixes --- bsie/extractor/image/colors_spatial.py | 3 ++- bsie/lib/pipeline.py | 3 +-- 2 files changed, 3 insertions(+), 3 deletions(-) (limited to 'bsie') diff --git a/bsie/extractor/image/colors_spatial.py b/bsie/extractor/image/colors_spatial.py index 38f1db4..ce5b9f2 100644 --- a/bsie/extractor/image/colors_spatial.py +++ b/bsie/extractor/image/colors_spatial.py @@ -71,7 +71,8 @@ class ColorsSpatial(base.Extractor): <{FEATURE_NAME}> rdfs:subClassOf bsfs:Feature ; # annotations rdfs:label "Spatially dominant colors"^^xsd:string ; - schema:description "Domiant colors of subregions in an image."^^xsd:string . + schema:description "Domiant colors of subregions in an image."^^xsd:string ; + bsfs:dtype xsd:integer . <{instance_name}> rdfs:subClassOf <{FEATURE_NAME}> ; bsfs:dimension "{dimension}"^^xsd:integer ; diff --git a/bsie/lib/pipeline.py b/bsie/lib/pipeline.py index 02119bc..44685ba 100644 --- a/bsie/lib/pipeline.py +++ b/bsie/lib/pipeline.py @@ -125,7 +125,6 @@ class Pipeline(): try: # get content content = rdr(path) if rdr is not None else None - #logger.info('extracted %s from %s', rdr, path) # apply extractors on this content @@ -139,7 +138,7 @@ class Pipeline(): # critical extractor failure. logger.error('%s failed to extract triples from content: %s', ext, err) - except errors.UnsupportedFileFormatError as err: + except errors.UnsupportedFileFormatError: # failed to read the file format. skip. #logger.warning('%s could not process the file format of %s', rdr, err) pass -- cgit v1.2.3