diff options
Diffstat (limited to 'bsie')
46 files changed, 448 insertions, 320 deletions
diff --git a/bsie/__init__.py b/bsie/__init__.py index c253f39..f6f2ff2 100644 --- a/bsie/__init__.py +++ b/bsie/__init__.py @@ -1,8 +1,4 @@ """The BSIE module extracts triples from files for insertion into a BSFS storage. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import collections diff --git a/bsie/apps/__init__.py b/bsie/apps/__init__.py index 1c3d0f9..2fe4795 100644 --- a/bsie/apps/__init__.py +++ b/bsie/apps/__init__.py @@ -1,12 +1,13 @@ -""" - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 +#!/usr/bin/env python3 +"""BSIE tools. """ # standard imports +import argparse import typing +# bsie imports +import bsie + # inner-module imports from .index import main as index from .info import main as info @@ -15,6 +16,39 @@ from .info import main as info __all__: typing.Sequence[str] = ( 'index', 'info', + 'main', ) +# config +apps = { + 'index' : index, + 'info' : info, + } + + +## code ## + +def main(argv=None): + """Black Star File System maintenance tools.""" + parser = argparse.ArgumentParser(description=main.__doc__, prog='bsie') + # version + parser.add_argument('--version', action='version', + version='%(prog)s version {}.{}.{}'.format(*bsie.version_info)) # pylint: disable=C0209 + # application selection + parser.add_argument('app', choices=apps.keys(), + help='Select the application to run.') + # dangling args + parser.add_argument('rest', nargs=argparse.REMAINDER) + # parse + args = parser.parse_args(argv) + # run application + apps[args.app](args.rest) + + +## main ## + +if __name__ == '__main__': + import sys + main(sys.argv[1:]) + ## EOF ## diff --git a/bsie/apps/_loader.py b/bsie/apps/_loader.py new file mode 100644 index 0000000..6411f10 --- /dev/null +++ b/bsie/apps/_loader.py @@ -0,0 +1,47 @@ + +# standard imports +import typing + +# external imports +import yaml + +# bsie imports +from bsie.extractor import ExtractorBuilder +from bsie.lib import PipelineBuilder +from bsie.lib.pipeline import Pipeline +from bsie.reader import ReaderBuilder + +# constants +DEFAULT_CONFIG_FILE = 'default_config.yaml' + +# exports +__all__: typing.Sequence[str] = ( + 'DEFAULT_CONFIG_FILE', + 'load_pipeline', + ) + + +## code ## + +def load_pipeline(path: str) -> Pipeline: + """Load a pipeline according to a config at *path*.""" + # load config file + with open(path, 'rt', encoding='utf-8') as ifile: + cfg = yaml.safe_load(ifile) + + # reader builder + rbuild = ReaderBuilder(cfg['ReaderBuilder']) + # extractor builder + ebuild = ExtractorBuilder(cfg['ExtractorBuilder']) + # pipeline builder + pbuild = PipelineBuilder( + rbuild, + ebuild, + ) + # build pipeline + pipeline = pbuild.build() + + # return pipeline + return pipeline + +## EOF ## diff --git a/bsie/apps/default_config.yaml b/bsie/apps/default_config.yaml new file mode 100644 index 0000000..a59b0f3 --- /dev/null +++ b/bsie/apps/default_config.yaml @@ -0,0 +1,19 @@ + +ReaderBuilder: {} + +ExtractorBuilder: + + - bsie.extractor.preview.Preview: + max_sides: [50, 100, 200,400] + + - bsie.extractor.generic.path.Path: {} + + - bsie.extractor.generic.stat.Stat: {} + + - bsie.extractor.image.colors_spatial.ColorsSpatial: + width: 32 + height: 32 + exp: 4 + + - bsie.extractor.image.photometrics.Exif: {} + diff --git a/bsie/apps/index.py b/bsie/apps/index.py index 8798c49..d64e8c2 100644 --- a/bsie/apps/index.py +++ b/bsie/apps/index.py @@ -1,20 +1,16 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import argparse import os import typing # bsie imports -from bsie.extractor import ExtractorBuilder -from bsie.lib import BSIE, PipelineBuilder, DefaultNamingPolicy -from bsie.reader import ReaderBuilder +from bsie.lib import BSIE, DefaultNamingPolicy from bsie.utils import bsfs, errors, node as node_ +# inner-module imports +from . import _loader + # exports __all__: typing.Sequence[str] = ( 'main', @@ -26,6 +22,9 @@ __all__: typing.Sequence[str] = ( def main(argv): """Index files or directories into BSFS.""" parser = argparse.ArgumentParser(description=main.__doc__, prog='index') + parser.add_argument('--config', type=str, + default=os.path.join(os.path.dirname(__file__), _loader.DEFAULT_CONFIG_FILE), + help='Path to the config file.') parser.add_argument('--host', type=bsfs.URI, default=bsfs.URI('http://example.com'), help='') parser.add_argument('--user', type=str, default='me', @@ -44,39 +43,8 @@ def main(argv): help='') args = parser.parse_args(argv) - # FIXME: Read reader/extractor configs from a config file - # reader builder - rbuild = ReaderBuilder() - # extractor builder - ebuild = ExtractorBuilder([ - {'bsie.extractor.preview.Preview': { - 'max_sides': [50], - }}, - {'bsie.extractor.generic.path.Path': {}}, - {'bsie.extractor.generic.stat.Stat': {}}, - {'bsie.extractor.generic.constant.Constant': dict( - tuples=[('http://bsfs.ai/schema/Entity#author', 'Me, myself, and I')], - schema=''' - bse:author rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Entity ; - rdfs:range xsd:string ; - bsfs:unique "true"^^xsd:boolean . - ''', - )}, - {'bsie.extractor.image.colors_spatial.ColorsSpatial': { - 'width': 2, - 'height': 2, - 'exp': 2, - }}, - ]) - # pipeline builder - pbuild = PipelineBuilder( - rbuild, - ebuild, - ) - # build pipeline - pipeline = pbuild.build() + pipeline = _loader.load_pipeline(args.config) # build the naming policy naming_policy = DefaultNamingPolicy( host=args.host, @@ -127,7 +95,6 @@ def main(argv): return store - ## main ## if __name__ == '__main__': diff --git a/bsie/apps/info.py b/bsie/apps/info.py index 750aedc..e27b70b 100644 --- a/bsie/apps/info.py +++ b/bsie/apps/info.py @@ -1,20 +1,16 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import argparse +import os import sys import typing # bsie imports -from bsie.extractor import ExtractorBuilder -from bsie.lib import PipelineBuilder -from bsie.reader import ReaderBuilder from bsie.utils import bsfs, errors +# inner-module imports +from . import _loader + # exports __all__: typing.Sequence[str] = ( 'main', @@ -26,43 +22,15 @@ __all__: typing.Sequence[str] = ( def main(argv): """Show information from BSIE.""" parser = argparse.ArgumentParser(description=main.__doc__, prog='info') + parser.add_argument('--config', type=str, + default=os.path.join(os.path.dirname(__file__), _loader.DEFAULT_CONFIG_FILE), + help='Path to the config file.') parser.add_argument('what', choices=('predicates', 'schema'), help='Select what information to show.') args = parser.parse_args(argv) - # FIXME: Read reader/extractor configs from a config file - # reader builder - rbuild = ReaderBuilder() - # extractor builder - ebuild = ExtractorBuilder([ - {'bsie.extractor.preview.Preview': { - 'max_sides': [50, 200], - }}, - {'bsie.extractor.generic.path.Path': {}}, - {'bsie.extractor.generic.stat.Stat': {}}, - {'bsie.extractor.generic.constant.Constant': dict( - tuples=[('http://bsfs.ai/schema/Entity#author', 'Me, myself, and I')], - schema=''' - bse:author rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Entity ; - rdfs:range xsd:string ; - bsfs:unique "true"^^xsd:boolean . - ''', - )}, - {'bsie.extractor.image.colors_spatial.ColorsSpatial': { - 'width': 2, - 'height': 2, - 'exp': 2, - }}, - ]) - # pipeline builder - pbuild = PipelineBuilder( - rbuild, - ebuild, - ) - # build pipeline - pipeline = pbuild.build() + pipeline = _loader.load_pipeline(args.config) # show info if args.what == 'predicates': diff --git a/bsie/extractor/__init__.py b/bsie/extractor/__init__.py index 5f385ee..36fa9ba 100644 --- a/bsie/extractor/__init__.py +++ b/bsie/extractor/__init__.py @@ -2,9 +2,6 @@ Each Extractor class is linked to the Reader class whose content it requires. -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing diff --git a/bsie/extractor/base.py b/bsie/extractor/base.py index 89183f9..f92d7cc 100644 --- a/bsie/extractor/base.py +++ b/bsie/extractor/base.py @@ -1,8 +1,4 @@ """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 """ # standard imports import abc @@ -28,26 +24,32 @@ SCHEMA_PREAMBLE = ''' prefix schema: <http://schema.org/> # common bsfs prefixes - prefix bsfs: <http://bsfs.ai/schema/> - prefix bse: <http://bsfs.ai/schema/Entity#> - prefix bsp: <http://bsfs.ai/schema/Preview#> + prefix bsfs: <https://schema.bsfs.io/core/> + prefix bsl: <https://schema.bsfs.io/core/Literal/> + prefix bsa: <https://schema.bsfs.io/core/Literal/Array/> + prefix bsd: <https://schema.bsfs.io/core/distance#> + + prefix bsie: <https://schema.bsfs.io/ie/> + prefix bsn: <https://schema.bsfs.io/ie/Node/> + prefix bse: <https://schema.bsfs.io/ie/Node/Entity#> + prefix bsp: <https://schema.bsfs.io/ie/Node/Preview#> # 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 ; + bsl:Array rdfs:subClassOf bsfs:Literal . + bsl:Number rdfs:subClassOf bsfs:Literal . + bsl:Time rdfs:subClassOf bsfs:Literal . + bsa:Feature rdfs:subClassOf bsl:Array ; bsfs:dimension "1"^^xsd:integer ; - bsfs:dtype bsfs:f16 ; - bsfs:distance bsfs:euclidean . + bsfs:dtype <https://schema.bsfs.io/core/dtype#f16> ; + bsfs:distance bsd:euclidean . # essential nodes - bsfs:Entity rdfs:subClassOf bsfs:Node . - bsfs:File rdfs:subClassOf bsfs:Entity . + bsn:Entity rdfs:subClassOf bsfs:Node . # common definitions xsd:string rdfs:subClassOf bsfs:Literal . - xsd:integer rdfs:subClassOf bsfs:Number . + xsd:integer rdfs:subClassOf bsl:Number . + xsd:float rdfs:subClassOf bsl:Number . ''' @@ -93,7 +95,7 @@ class Extractor(abc.ABC): @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) + ent = self.schema.node(ns.bsn.Entity) return ( pred for pred diff --git a/bsie/extractor/builder.py b/bsie/extractor/builder.py index 0fd3685..d691b0e 100644 --- a/bsie/extractor/builder.py +++ b/bsie/extractor/builder.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/extractor/generic/__init__.py b/bsie/extractor/generic/__init__.py index 4783949..46a4bd6 100644 --- a/bsie/extractor/generic/__init__.py +++ b/bsie/extractor/generic/__init__.py @@ -3,9 +3,6 @@ files. Examples include file system information (file name and size, mime type, etc.) and information that is independent of the actual file (constant triples, host platform infos, current time, etc.). -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing diff --git a/bsie/extractor/generic/constant.py b/bsie/extractor/generic/constant.py index 938e20c..7acbe95 100644 --- a/bsie/extractor/generic/constant.py +++ b/bsie/extractor/generic/constant.py @@ -1,8 +1,4 @@ """The Constant extractor produces pre-specified triples. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing diff --git a/bsie/extractor/generic/path.py b/bsie/extractor/generic/path.py index c984515..00c1121 100644 --- a/bsie/extractor/generic/path.py +++ b/bsie/extractor/generic/path.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import os import typing @@ -31,11 +26,11 @@ class Path(base.Extractor): def __init__(self): super().__init__(bsfs.schema.from_string(base.SCHEMA_PREAMBLE + ''' bse:filename rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:File ; + rdfs:domain bsn:Entity ; rdfs:range xsd:string ; rdfs:label "File name"^^xsd:string ; schema:description "Filename of entity in some filesystem."^^xsd:string ; - bsfs:unique "false"^^xsd:boolean . + bsfs:unique "true"^^xsd:boolean . ''')) self._callmap = { self.schema.predicate(ns.bse.filename): self.__filename, diff --git a/bsie/extractor/generic/stat.py b/bsie/extractor/generic/stat.py index 9394456..92b51f3 100644 --- a/bsie/extractor/generic/stat.py +++ b/bsie/extractor/generic/stat.py @@ -1,8 +1,4 @@ """Extract information from the file system, such as filesize. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import os @@ -33,11 +29,11 @@ class Stat(base.Extractor): def __init__(self): super().__init__(bsfs.schema.from_string(base.SCHEMA_PREAMBLE + ''' bse:filesize rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:File ; + rdfs:domain bsn:Entity ; rdfs:range xsd:integer ; rdfs:label "File size"^^xsd:string ; schema:description "File size of entity in some filesystem."^^xsd:string ; - bsfs:unique "false"^^xsd:boolean . + bsfs:unique "true"^^xsd:boolean . ''')) self._callmap = { self.schema.predicate(ns.bse.filesize): self.__filesize, diff --git a/bsie/extractor/image/__init__.py b/bsie/extractor/image/__init__.py index 75b118d..f82424a 100644 --- a/bsie/extractor/image/__init__.py +++ b/bsie/extractor/image/__init__.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/extractor/image/colors_spatial.py b/bsie/extractor/image/colors_spatial.py index 15fd281..e6661a9 100644 --- a/bsie/extractor/image/colors_spatial.py +++ b/bsie/extractor/image/colors_spatial.py @@ -1,8 +1,4 @@ """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 @@ -18,8 +14,7 @@ from bsie.utils import bsfs, node, ns from .. import base # constants -FEATURE_NAME = ns.bsf + 'ColorsSpatial' -PREDICATE_NAME = ns.bse + 'colors_spatial' +FEATURE_NAME = ns.bsf.ColorsSpatial() # exports __all__: typing.Sequence[str] = ( @@ -62,16 +57,17 @@ class ColorsSpatial(base.Extractor): 'exp': exp, }) # determine symbol names - instance_name = FEATURE_NAME[uuid] - predicate_name = PREDICATE_NAME[uuid] + instance_name = getattr(FEATURE_NAME, uuid) + predicate_name = getattr(ns.bse, 'colors_spatial_' + 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 ; + <{FEATURE_NAME}> rdfs:subClassOf bsa:Feature ; # annotations rdfs:label "Spatially dominant colors"^^xsd:string ; schema:description "Domiant colors of subregions in an image."^^xsd:string ; + bsfs:distance <https://schema.bsfs.io/core/distance#euclidean> ; bsfs:dtype xsd:integer . <{instance_name}> rdfs:subClassOf <{FEATURE_NAME}> ; @@ -82,7 +78,7 @@ class ColorsSpatial(base.Extractor): <{FEATURE_NAME}/args#exp> "{exp}"^^xsd:float . <{predicate_name}> rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:File ; + rdfs:domain bsn:Entity ; rdfs:range <{instance_name}> ; bsfs:unique "true"^^xsd:boolean . diff --git a/bsie/extractor/image/photometrics.py b/bsie/extractor/image/photometrics.py new file mode 100644 index 0000000..42eb3c8 --- /dev/null +++ b/bsie/extractor/image/photometrics.py @@ -0,0 +1,211 @@ + +# standard imports +from fractions import Fraction +import typing + +# bsie imports +from bsie.utils import bsfs, node, ns + +# inner-module imports +from .. import base + +# exports +__all__: typing.Sequence[str] = ( + 'Exif', + ) + + +## code ## + +def _gps_to_dec(coords: typing.Tuple[float, float, float]) -> float: + """Convert GPS coordinates from exif to float.""" + # unpack args + deg, min, sec = coords # pylint: disable=redefined-builtin # min + # convert to float + deg = float(Fraction(deg)) + min = float(Fraction(min)) + sec = float(Fraction(sec)) + + if float(sec) > 0: + # format is deg+min+sec + return (float(deg) * 3600 + float(min) * 60 + float(sec)) / 3600 + # format is deg+min + return float(deg) + float(min) / 60 + + +class Exif(base.Extractor): + """Extract information from EXIF/IPTC tags of an image file.""" + + CONTENT_READER = 'bsie.reader.exif.Exif' + + def __init__(self): + super().__init__(bsfs.schema.from_string(base.SCHEMA_PREAMBLE + ''' + #bse:t_capture rdfs:subClassOf bsfs:Predicate ; + # rdfs:domain bsn:Entity ; + # rdfs:range xsd:float ; + # bsfs:unique "true"^^xsd:boolean . + bse:exposure rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:float ; + bsfs:unique "true"^^xsd:boolean . + bse:aperture rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:float ; + bsfs:unique "true"^^xsd:boolean . + bse:iso rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + bse:focal_length rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:float ; + bsfs:unique "true"^^xsd:boolean . + bse:width rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + bse:height rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + bse:orientation rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:integer ; + bsfs:unique "true"^^xsd:boolean . + bse:orientation_label rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:string ; + bsfs:unique "true"^^xsd:boolean . + bse:altitude rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:float ; + bsfs:unique "true"^^xsd:boolean . + bse:latitude rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:float ; + bsfs:unique "true"^^xsd:boolean . + bse:longitude rdfs:subClassOf bsfs:Predicate ; + rdfs:domain bsn:Entity ; + rdfs:range xsd:float ; + bsfs:unique "true"^^xsd:boolean . + ''')) + # initialize mapping from predicate to callback + self._callmap = { + #self.schema.predicate(ns.bse.t_capture): self._date, + self.schema.predicate(ns.bse.exposure): self._exposure, + self.schema.predicate(ns.bse.aperture): self._aperture, + self.schema.predicate(ns.bse.iso): self._iso, + self.schema.predicate(ns.bse.focal_length): self._focal_length, + self.schema.predicate(ns.bse.width): self._width, + self.schema.predicate(ns.bse.height): self._height, + self.schema.predicate(ns.bse.orientation): self._orientation, + self.schema.predicate(ns.bse.orientation_label): self._orientation_label, + self.schema.predicate(ns.bse.altitude): self._altitude, + self.schema.predicate(ns.bse.latitude): self._latitude, + self.schema.predicate(ns.bse.longitude): self._longitude, + } + + def extract( + self, + subject: node.Node, + content: dict, + principals: typing.Iterable[bsfs.schema.Predicate], + ) -> typing.Iterator[typing.Tuple[node.Node, bsfs.schema.Predicate, typing.Any]]: + for pred in principals: + # find callback + clbk = self._callmap.get(pred) + if clbk is None: + continue + # get value + value = clbk(content) + if value is None: + continue + # produce triple + yield subject, pred, value + + #def _date(self, content: dict): # FIXME: Return type annotation + # date_keys = ( + # 'Exif.Photo.DateTimeOriginal', + # 'Exif.Photo.DateTimeDigitized', + # 'Exif.Image.DateTime', + # ) + # for key in date_keys: + # if key in content: + # dt = content[key].value + # if dt.tzinfo is None: + # dt = dt.replace(tzinfo=ttime.NoTimeZone) + # return dt + # return None + + + ## photometrics + + def _exposure(self, content: dict) -> typing.Optional[float]: + if 'Exif.Photo.ExposureTime' in content: + return 1.0 / float(Fraction(content['Exif.Photo.ExposureTime'])) + return None + + def _aperture(self, content: dict) -> typing.Optional[float]: + if 'Exif.Photo.FNumber' in content: + return float(Fraction(content['Exif.Photo.FNumber'])) + return None + + def _iso(self, content: dict) -> typing.Optional[int]: + if 'Exif.Photo.ISOSpeedRatings' in content: + return int(content['Exif.Photo.ISOSpeedRatings']) + return None + + def _focal_length(self, content: dict) -> typing.Optional[float]: + if 'Exif.Photo.FocalLength' in content: + return float(Fraction(content['Exif.Photo.FocalLength'])) + return None + + + ## image dimensions + + def _width(self, content: dict) -> typing.Optional[int]: + # FIXME: consider orientation! + if 'Exif.Photo.PixelXDimension' in content: + return int(content['Exif.Photo.PixelXDimension']) + return None + + def _height(self, content: dict) -> typing.Optional[int]: + # FIXME: consider orientation! + if 'Exif.Photo.PixelYDimension' in content: + return int(content['Exif.Photo.PixelYDimension']) + return None + + def _orientation(self, content: dict) -> typing.Optional[int]: + if 'Exif.Image.Orientation' in content: + return int(content['Exif.Image.Orientation']) + return None + + def _orientation_label(self, content: dict) -> typing.Optional[str]: + width = self._width(content) + height = self._height(content) + ori = self._orientation(content) + if width is not None and height is not None and ori is not None: + if ori <= 4: + return 'landscape' if width >= height else 'portrait' + return 'portrait' if width >= height else 'landscape' + return None + + + ## location + + def _altitude(self, content: dict) -> typing.Optional[float]: + if 'Exif.GPSInfo.GPSAltitude' in content: + return float(Fraction(content['Exif.GPSInfo.GPSAltitude'])) + return None + + def _latitude(self, content: dict) -> typing.Optional[float]: + if 'Exif.GPSInfo.GPSLatitude' in content: + return _gps_to_dec(content['Exif.GPSInfo.GPSLatitude'].split()) + return None + + def _longitude(self, content: dict) -> typing.Optional[float]: + if 'Exif.GPSInfo.GPSLongitude' in content: + return _gps_to_dec(content['Exif.GPSInfo.GPSLongitude'].split()) + return None + +## EOF ## diff --git a/bsie/extractor/preview.py b/bsie/extractor/preview.py index 1531d62..145a01a 100644 --- a/bsie/extractor/preview.py +++ b/bsie/extractor/preview.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports import io import typing @@ -33,28 +28,30 @@ class Preview(base.Extractor): def __init__(self, max_sides: typing.Iterable[int]): super().__init__(bsfs.schema.from_string(base.SCHEMA_PREAMBLE + ''' - bsfs:Preview rdfs:subClassOf bsfs:Node . - bsfs:BinaryBlob rdfs:subClassOf bsfs:Literal . - bsfs:JPEG rdfs:subClassOf bsfs:BinaryBlob . + + + bsn:Preview rdfs:subClassOf bsfs:Node . + bsl:BinaryBlob rdfs:subClassOf bsfs:Literal . + <https://schema.bsfs.io/ie/Literal/BinaryBlob/JPEG> rdfs:subClassOf bsl:BinaryBlob . bse:preview rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:File ; - rdfs:range bsfs:Preview ; + rdfs:domain bsn:Entity ; + rdfs:range bsn:Preview ; bsfs:unique "false"^^xsd:boolean . bsp:width rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Preview ; + rdfs:domain bsn:Preview ; rdfs:range xsd:integer ; bsfs:unique "true"^^xsd:boolean . bsp:height rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Preview ; + rdfs:domain bsn:Preview ; rdfs:range xsd:integer ; bsfs:unique "true"^^xsd:boolean . bsp:asset rdfs:subClassOf bsfs:Predicate ; - rdfs:domain bsfs:Preview ; - rdfs:range bsfs:JPEG ; + rdfs:domain bsn:Preview ; + rdfs:range <https://schema.bsfs.io/ie/Literal/BinaryBlob/JPEG> ; bsfs:unique "true"^^xsd:boolean . ''')) @@ -85,7 +82,7 @@ class Preview(base.Extractor): buffer = io.BytesIO() img.save(buffer, format='jpeg') # create a preview node - preview = node.Node(ns.bsfs.Preview, + preview = node.Node(ns.bsn.Preview, ucid=bsfs.uuid.UCID.from_bytes(buffer.getvalue()), size=max_side, source=subject, diff --git a/bsie/lib/__init__.py b/bsie/lib/__init__.py index 48379de..f44fb74 100644 --- a/bsie/lib/__init__.py +++ b/bsie/lib/__init__.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/lib/bsie.py b/bsie/lib/bsie.py index a572525..b02e707 100644 --- a/bsie/lib/bsie.py +++ b/bsie/lib/bsie.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/lib/builder.py b/bsie/lib/builder.py index 39da441..3a15311 100644 --- a/bsie/lib/builder.py +++ b/bsie/lib/builder.py @@ -1,9 +1,4 @@ -""" -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 diff --git a/bsie/lib/naming_policy.py b/bsie/lib/naming_policy.py index 131a70b..9b9a45d 100644 --- a/bsie/lib/naming_policy.py +++ b/bsie/lib/naming_policy.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import abc import os @@ -85,9 +80,9 @@ class DefaultNamingPolicy(NamingPolicy): def handle_node(self, node: Node) -> Node: if node.uri is not None: return node - if node.node_type == ns.bsfs.File: + if node.node_type == ns.bsn.Entity : return self.name_file(node) - if node.node_type == ns.bsfs.Preview: + if node.node_type == ns.bsn.Preview: return self.name_preview(node) raise errors.ProgrammingError('no naming policy available for {node.node_type}') @@ -97,7 +92,7 @@ class DefaultNamingPolicy(NamingPolicy): fragment = node.hints['ucid'] else: # random name fragment = self._uuid() - node.uri = (self._prefix + 'file')[fragment] + node.uri = getattr(self._prefix.file(), fragment) return node def name_preview(self, node: Node) -> Node: @@ -114,7 +109,7 @@ class DefaultNamingPolicy(NamingPolicy): fragment = self._uuid() if 'size' in node.hints: # append size fragment += '_s' + str(node.hints['size']) - node.uri = (self._prefix + 'preview')[fragment] + node.uri = getattr(self._prefix.preview(), fragment) return node ## EOF ## diff --git a/bsie/lib/pipeline.py b/bsie/lib/pipeline.py index 0bc5109..30fd6fd 100644 --- a/bsie/lib/pipeline.py +++ b/bsie/lib/pipeline.py @@ -1,9 +1,4 @@ -""" -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 @@ -109,7 +104,7 @@ class Pipeline(): rdr2ext[rdr].add(ext) # create subject for file - subject = node.Node(ns.bsfs.File, + subject = node.Node(ns.bsn.Entity, ucid=bsfs.uuid.UCID.from_path(path), ) diff --git a/bsie/reader/__init__.py b/bsie/reader/__init__.py index 4163d1c..a1c38a9 100644 --- a/bsie/reader/__init__.py +++ b/bsie/reader/__init__.py @@ -1,8 +1,8 @@ """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. +First, it brokers between multiple libraries and file formats. +Second, it separates multiple aspects of a file into distinct content types. Often, different libraries focus on reading different types of content from a file. E.g. one would use different modules to read file system infos than to @@ -11,9 +11,6 @@ type. Each distinct type can be implemented in a file or submodule that provides a Reader implementation. Through utilization of submodules, different file formats can be supported. -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing @@ -29,4 +26,3 @@ __all__: typing.Sequence[str] = ( ) ## EOF ## -## EOF ## diff --git a/bsie/reader/base.py b/bsie/reader/base.py index 099a327..a775701 100644 --- a/bsie/reader/base.py +++ b/bsie/reader/base.py @@ -1,13 +1,4 @@ -"""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 -""" # standard imports import abc import typing diff --git a/bsie/reader/builder.py b/bsie/reader/builder.py index 8699e75..d32700b 100644 --- a/bsie/reader/builder.py +++ b/bsie/reader/builder.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/reader/chain.py b/bsie/reader/chain.py index 1dbc52b..79b44b4 100644 --- a/bsie/reader/chain.py +++ b/bsie/reader/chain.py @@ -1,9 +1,4 @@ -""" -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 diff --git a/bsie/reader/exif.py b/bsie/reader/exif.py new file mode 100644 index 0000000..2d0428b --- /dev/null +++ b/bsie/reader/exif.py @@ -0,0 +1,44 @@ + +# standard imports +import typing + +# external imports +import pyexiv2 + +# bsie imports +from bsie.utils import errors, filematcher + +# inner-module imports +from . import base + +# constants +MATCH_RULE = 'mime=image/jpeg' + +# exports +__all__: typing.Sequence[str] = ( + 'Exif', + ) + + +## code ## + +class Exif(base.Reader): + """Use pyexiv2 to read exif metadata from image files.""" + + def __init__(self): + self._match = filematcher.parse(MATCH_RULE) + + def __call__(self, path: str) -> dict: + # perform quick checks first + if not self._match(path): + raise errors.UnsupportedFileFormatError(path) + + try: + # open the file + img = pyexiv2.Image(path) + # read metadata + return img.read_exif() + except (TypeError, OSError, RuntimeError) as err: + raise errors.ReaderError(path) from err + +## EOF ## diff --git a/bsie/reader/image/__init__.py b/bsie/reader/image/__init__.py index c5d2a2a..89642f2 100644 --- a/bsie/reader/image/__init__.py +++ b/bsie/reader/image/__init__.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/reader/image/_pillow.py b/bsie/reader/image/_pillow.py index 5b2bdf2..0611d3c 100644 --- a/bsie/reader/image/_pillow.py +++ b/bsie/reader/image/_pillow.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/reader/image/_raw.py b/bsie/reader/image/_raw.py index 257fdb3..e5745aa 100644 --- a/bsie/reader/image/_raw.py +++ b/bsie/reader/image/_raw.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/reader/path.py b/bsie/reader/path.py index 1ca05a0..45eb127 100644 --- a/bsie/reader/path.py +++ b/bsie/reader/path.py @@ -1,8 +1,4 @@ """The Path reader produces a file path. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing diff --git a/bsie/reader/preview/__init__.py b/bsie/reader/preview/__init__.py index 3e69a4a..791a133 100644 --- a/bsie/reader/preview/__init__.py +++ b/bsie/reader/preview/__init__.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # imports import typing diff --git a/bsie/reader/preview/_pg.py b/bsie/reader/preview/_pg.py index 097c513..401b33d 100644 --- a/bsie/reader/preview/_pg.py +++ b/bsie/reader/preview/_pg.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports from functools import partial import contextlib diff --git a/bsie/reader/preview/_pillow.py b/bsie/reader/preview/_pillow.py index 174d509..2b797c6 100644 --- a/bsie/reader/preview/_pillow.py +++ b/bsie/reader/preview/_pillow.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports from functools import partial import typing @@ -38,7 +33,7 @@ class PillowPreviewReader(base.Reader): except PIL.UnidentifiedImageError as err: # failed to open, skip file raise errors.UnsupportedFileFormatError(path) from err - except IOError as err: + except OSError as err: raise errors.ReaderError(path) from err # EOF ## diff --git a/bsie/reader/preview/_rawpy.py b/bsie/reader/preview/_rawpy.py index 2c20a48..16e8675 100644 --- a/bsie/reader/preview/_rawpy.py +++ b/bsie/reader/preview/_rawpy.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports from functools import partial import typing diff --git a/bsie/reader/preview/utils.py b/bsie/reader/preview/utils.py index 2ef1562..82ecc31 100644 --- a/bsie/reader/preview/utils.py +++ b/bsie/reader/preview/utils.py @@ -1,9 +1,4 @@ -""" -Part of the tagit module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/reader/stat.py b/bsie/reader/stat.py index 706dc47..f42e7fb 100644 --- a/bsie/reader/stat.py +++ b/bsie/reader/stat.py @@ -1,8 +1,4 @@ """The Stat reader produces filesystem stat information. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import os diff --git a/bsie/utils/__init__.py b/bsie/utils/__init__.py index 9cb60ed..18c8db7 100644 --- a/bsie/utils/__init__.py +++ b/bsie/utils/__init__.py @@ -1,8 +1,4 @@ """Common tools and definitions. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing diff --git a/bsie/utils/bsfs.py b/bsie/utils/bsfs.py index ef5db31..fc045cc 100644 --- a/bsie/utils/bsfs.py +++ b/bsie/utils/bsfs.py @@ -1,8 +1,4 @@ """BSFS bridge, provides BSFS bindings for BSIE. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing diff --git a/bsie/utils/errors.py b/bsie/utils/errors.py index 8133cd4..7c7e6ed 100644 --- a/bsie/utils/errors.py +++ b/bsie/utils/errors.py @@ -1,8 +1,4 @@ """Common BSIE exceptions. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing @@ -42,7 +38,7 @@ class UnreachableError(ProgrammingError): class ParserError(_BSIEError): """Failed to parse due to invalid syntax or structures.""" -class UnsupportedFileFormatError(ReaderError): +class UnsupportedFileFormatError(_BSIEError): """Failed to read a file format.""" ## EOF ## diff --git a/bsie/utils/filematcher/__init__.py b/bsie/utils/filematcher/__init__.py index 1e23e4e..908de78 100644 --- a/bsie/utils/filematcher/__init__.py +++ b/bsie/utils/filematcher/__init__.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 -""" # standard imports import typing diff --git a/bsie/utils/filematcher/matcher.py b/bsie/utils/filematcher/matcher.py index a279a4b..1fa308e 100644 --- a/bsie/utils/filematcher/matcher.py +++ b/bsie/utils/filematcher/matcher.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2021 -""" # standard imports from collections.abc import Callable, Collection, Hashable import abc diff --git a/bsie/utils/filematcher/parser.py b/bsie/utils/filematcher/parser.py index 2f82875..dc28a0d 100644 --- a/bsie/utils/filematcher/parser.py +++ b/bsie/utils/filematcher/parser.py @@ -1,9 +1,4 @@ -""" -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2021 -""" # standard imports import typing diff --git a/bsie/utils/loading.py b/bsie/utils/loading.py index eb05c35..58202d1 100644 --- a/bsie/utils/loading.py +++ b/bsie/utils/loading.py @@ -1,9 +1,4 @@ -""" -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 @@ -27,14 +22,14 @@ def safe_load(module_name: str, class_name: str): module = importlib.import_module(module_name) except Exception as err: # cannot import module - raise errors.LoaderError(f'cannot load module {module_name}') from err + raise errors.LoaderError(f'cannot load module {module_name} ({err})') 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 + raise errors.LoaderError(f'cannot load class {class_name} from module {module_name} ({err})') from err return cls diff --git a/bsie/utils/namespaces.py b/bsie/utils/namespaces.py index 0af8ece..4a66048 100644 --- a/bsie/utils/namespaces.py +++ b/bsie/utils/namespaces.py @@ -1,8 +1,4 @@ """Default namespaces used throughout BSIE. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing @@ -10,19 +6,31 @@ import typing # inner-module imports from . import bsfs as _bsfs -# constants -bse = _bsfs.Namespace('http://bsfs.ai/schema/Entity') -bsf = _bsfs.Namespace('http://ie.bsfs.ai/schema/Feature') -bsfs = _bsfs.Namespace('http://bsfs.ai/schema', fsep='/') -bsm = _bsfs.Namespace('http://bsfs.ai/schema/Meta') -bsp = _bsfs.Namespace('http://bsfs.ai/schema/Preview') -xsd = _bsfs.Namespace('http://www.w3.org/2001/XMLSchema') +# generic namespaces +xsd = _bsfs.Namespace('http://www.w3.org/2001/XMLSchema')() + +# core bsfs/bsie namespaces +bsfs = _bsfs.Namespace('https://schema.bsfs.io/core') +bsie = _bsfs.Namespace('https://schema.bsfs.io/ie') + +# auxiliary namespaces +bsd = bsie.distance() +bse = bsie.Node.Entity() +bsf = bsie.Literal.Array.Feature +bsl = bsfs.Literal +bsn = bsie.Node +bsp = bsie.Node.Preview() # export __all__: typing.Sequence[str] = ( + 'bsd', 'bse', + 'bsf', 'bsfs', - 'bsm', + 'bsie', + 'bsl', + 'bsl', + 'bsn', 'bsp', 'xsd', ) diff --git a/bsie/utils/node.py b/bsie/utils/node.py index aa62c06..fa34b2e 100644 --- a/bsie/utils/node.py +++ b/bsie/utils/node.py @@ -1,8 +1,4 @@ """Lighweight Node to bridge to BSFS. - -Part of the bsie module. -A copy of the license is provided with the project. -Author: Matthias Baumgartner, 2022 """ # standard imports import typing |