aboutsummaryrefslogtreecommitdiffstats
path: root/tagit
diff options
context:
space:
mode:
authorMatthias Baumgartner <dev@igsor.net>2023-02-02 10:04:03 +0100
committerMatthias Baumgartner <dev@igsor.net>2023-02-02 10:04:03 +0100
commitc6856aa6fe2ad478dd5bc6285fb2544c150b2033 (patch)
treeb084d75afbca13c34f2b71b609fd2c63a160522d /tagit
parent57327d3df562736cad9e278e13beeb55bf3b52ed (diff)
downloadtagit-c6856aa6fe2ad478dd5bc6285fb2544c150b2033.tar.gz
tagit-c6856aa6fe2ad478dd5bc6285fb2544c150b2033.tar.bz2
tagit-c6856aa6fe2ad478dd5bc6285fb2544c150b2033.zip
filter port
Diffstat (limited to 'tagit')
-rw-r--r--tagit/actions/__init__.py4
-rw-r--r--tagit/actions/filter.py20
-rw-r--r--tagit/apps/port-config.yaml8
-rw-r--r--tagit/parsing/filter/__init__.py4
-rw-r--r--tagit/parsing/filter/from_string.py20
-rw-r--r--tagit/parsing/filter/to_string.py255
-rw-r--r--tagit/utils/bsfs.py2
-rw-r--r--tagit/widgets/filter.py39
-rw-r--r--tagit/widgets/session.py1
9 files changed, 313 insertions, 40 deletions
diff --git a/tagit/actions/__init__.py b/tagit/actions/__init__.py
index fa2bed0..b2ab6bd 100644
--- a/tagit/actions/__init__.py
+++ b/tagit/actions/__init__.py
@@ -64,8 +64,8 @@ class ActionBuilder(BuilderBase):
'GoBack': filter.GoBack,
'GoForth': filter.GoForth,
'JumpToToken': filter.JumpToToken,
- #'SearchByAddressOnce': filter.SearchByAddressOnce,
- #'SearchmodeSwitch': filter.SearchmodeSwitch,
+ 'SearchByAddressOnce': filter.SearchByAddressOnce,
+ 'SearchmodeSwitch': filter.SearchmodeSwitch,
## grouping
'CreateGroup': grouping.CreateGroup,
'DissolveGroup': grouping.DissolveGroup,
diff --git a/tagit/actions/filter.py b/tagit/actions/filter.py
index e878952..c5cc912 100644
--- a/tagit/actions/filter.py
+++ b/tagit/actions/filter.py
@@ -13,11 +13,10 @@ import kivy.properties as kp
# tagit imports
from tagit import config, dialogues
-from tagit.utils import errors, Frame
+from tagit.utils import errors, ns, Frame
+from tagit.utils.bsfs import ast
from tagit.widgets import Binding
from tagit.widgets.filter import FilterAwareMixin
-#from tagit.parsing.search import ast_to_string, ast # FIXME: mb/port
-#from tagit.storage.base import ns # FIXME: mb/port
# inner-module imports
from .action import Action
@@ -91,16 +90,13 @@ class AddToken(Action):
def apply(self, token=None):
if token is None:
- #sugg = {node.label for node in self.root.session.storage.all(ns.tagit.storage.base.Tag)}
- # FIXME: mb/port/bsfs
- #sugg = set(self.root.session.storage.all(ns.bsfs.Tag).label())
- sugg = {'hello', 'world'}
+ sugg = self.root.session.storage.all(ns.bsfs.Tag).label(node=False)
dlg = dialogues.TokenEdit(suggestions=sugg)
dlg.bind(on_ok=lambda wx: self.add_from_string(wx.text))
dlg.open()
elif isinstance(token, str):
self.add_from_string(token)
- elif isinstance(token, ast.ASTNode):
+ elif isinstance(token, ast.filter.FilterExpression):
self.add_token([token])
def add_from_string(self, text):
@@ -129,10 +125,8 @@ class EditToken(Action):
text = kp.StringProperty('Edit token')
def apply(self, token):
- #sugg = {node.label for node in self.root.session.storage.all(ns.tagit.storage.base.Tag)} # FIXME: mb/port
- sugg = {'hello', 'world'} # FIXME: mb/port
- #text = ast_to_string(token)
- text = 'hello world'
+ sugg = self.root.session.storage.all(ns.bsfs.Tag).label(node=False)
+ text = self.root.session.filter_to_string(token)
dlg = dialogues.TokenEdit(text=text, suggestions=sugg)
dlg.bind(on_ok=lambda obj: self.on_ok(token, obj))
dlg.open()
@@ -140,7 +134,7 @@ class EditToken(Action):
def on_ok(self, token, obj):
with self.root.filter as filter:
try:
- tokens_from_text = self.root.session.filter_from_string(obj.text) # FIXME: mb/port
+ tokens_from_text = self.root.session.filter_from_string(obj.text)
except errors.ParserError as e:
dialogues.Error(text=f'Invalid token: {e}').open()
return
diff --git a/tagit/apps/port-config.yaml b/tagit/apps/port-config.yaml
index 5e874ef..a9907b7 100644
--- a/tagit/apps/port-config.yaml
+++ b/tagit/apps/port-config.yaml
@@ -40,10 +40,18 @@ ui:
- SelectNone
- SelectMulti
- SelectRange
+ - AddToken
+ - GoBack
+ - GoForth
+ - SearchByAddressOnce
+ - SearchmodeSwitch
- AddTag
- EditTag
- OpenGroup
#- RepresentGroup
+ - Search
+ - ShowSelected
+ - RemoveSelected
- OpenExternal
- ShowHelp
browser:
diff --git a/tagit/parsing/filter/__init__.py b/tagit/parsing/filter/__init__.py
index 88b6256..defb332 100644
--- a/tagit/parsing/filter/__init__.py
+++ b/tagit/parsing/filter/__init__.py
@@ -6,12 +6,12 @@ Author: Matthias Baumgartner, 2022
"""
# inner-module imports
from .from_string import FromString
-#from .to_string import ToString
+from .to_string import ToString
# exports
__all__ = (
'FromString',
- #'ToString',
+ 'ToString',
)
## EOF ##
diff --git a/tagit/parsing/filter/from_string.py b/tagit/parsing/filter/from_string.py
index 5a38723..ed24f63 100644
--- a/tagit/parsing/filter/from_string.py
+++ b/tagit/parsing/filter/from_string.py
@@ -16,7 +16,7 @@ from pyparsing import CaselessKeyword, Combine, Group, Optional, Or, Word, delim
# tagit imports
from tagit.parsing.datefmt import parse_datetime
from tagit.utils import bsfs, errors, ns, ttime
-from tagit.utils.bsfs import ast
+from tagit.utils.bsfs import ast, URI
# constants
SEARCH_DELIM = '/'
@@ -36,6 +36,9 @@ class FromString():
_DATETIME_PREDICATES = None
_QUERY = None
+ # current schema.
+ schema: bsfs.schema.Schema
+
def __init__(self, schema: bsfs.schema.Schema):
self.schema = schema
@@ -51,7 +54,6 @@ class FromString():
def build_parser(self):
"""
"""
- # valid predicates per type, as supplied by tagit.library
# FIXME:
# * range / type constraints
# * how to filter predicates
@@ -74,11 +76,11 @@ class FromString():
self._abb2uri = {pred.uri.fragment: pred.uri for pred in predicates} # FIXME: tie-breaking for duplicates
self._uri2abb = {uri: fragment for fragment, uri in self._abb2uri.items()}
# all predicates
- _PREDICATES = {self._uri2abb[pred.uri] for pred in predicates}
+ _PREDICATES = {self._uri2abb[pred.uri] for pred in predicates} | {'id', 'group'} # FIXME: properly document additions
# numeric predicates
- _PREDICATES_NUMERIC = {self._uri2abb[pred.uri] for pred in predicates if isinstance(pred.range, bsfs.schema.Literal) and pred.range <= self.schema.literal(ns.bsfs.Number)} # FIXME: type check might become unnecessary
+ _PREDICATES_NUMERIC = {self._uri2abb[pred.uri] for pred in predicates if pred.range <= self.schema.literal(ns.bsfs.Number)}
# datetime predicates
- self._DATETIME_PREDICATES = {pred.uri for pred in predicates if isinstance(pred.range, bsfs.schema.Literal) and pred.range <= self.schema.literal(ns.bsfs.Time)} # FIXME: type check might become unnecessary
+ self._DATETIME_PREDICATES = {pred.uri for pred in predicates if pred.range <= self.schema.literal(ns.bsfs.Time)}
_PREDICATES_DATETIME = {self._uri2abb[pred] for pred in self._DATETIME_PREDICATES}
@@ -203,6 +205,14 @@ class FromString():
raise errors.ParserError('Invalid operator ({})'.format(exp.op), exp)
tokens.append(tok)
+ elif exp.getName() == 'categorical' and exp.predicate.lower() == 'id':
+ values = [URI(s.strip()) for s in exp.value]
+ tokens.append(ast.filter.IsIn(*values))
+
+ elif exp.getName() == 'categorical' and exp.predicate.lower() == 'group':
+ values = [URI(s.strip()) for s in exp.value]
+ tokens.append(ast.filter.Any(ns.bse.group, ast.filter.IsIn(*values)))
+
elif exp.getName() == 'categorical':
pred = self._abb2uri[exp.predicate.lower()]
approx = False
diff --git a/tagit/parsing/filter/to_string.py b/tagit/parsing/filter/to_string.py
new file mode 100644
index 0000000..0b1a3e1
--- /dev/null
+++ b/tagit/parsing/filter/to_string.py
@@ -0,0 +1,255 @@
+"""
+
+Part of the tagit module.
+A copy of the license is provided with the project.
+Author: Matthias Baumgartner, 2022
+"""
+# tagit imports
+from tagit.utils.bsfs import ast, matcher, URI
+from tagit.utils import errors, ns
+
+# exports
+__all__ = ('ToString', )
+
+
+## code ##
+
+class ToString():
+
+ def __init__(self, schema):
+ self.matches = matcher.Filter()
+
+ self.schema = schema
+ predicates = {pred for pred in self.schema.predicates() if pred.domain <= self.schema.node(ns.bsfs.Entity)}
+ # shortcuts
+ self._abb2uri = {pred.uri.fragment: pred.uri for pred in predicates} # FIXME: tie-breaking for duplicates
+ self._uri2abb = {uri: fragment for fragment, uri in self._abb2uri.items()}
+
+ def __call__(self, query):
+ """
+ """
+ # FIXME: test query class type
+ if self.matches(query, ast.filter.And(matcher.Rest())):
+ return ' / '.join(self._parse(sub) for sub in query)
+ return self._parse(query)
+
+ def _parse(self, query):
+ cases = (
+ self._has,
+ self._entity,
+ self._group,
+ self._tag,
+ self._range,
+ self._categorical,
+ )
+ for clbk in cases:
+ result = clbk(query)
+ if result is not None:
+ return result
+
+ raise errors.BackendError()
+
+ def _has(self, query):
+ # Has(<pred>) <-> has <pred>
+ # Not(Has(<pred>)) <-> has no <pred>
+ has = ast.filter.Has(
+ matcher.Partial(ast.filter.Predicate),
+ ast.filter.GreaterThan(1, strict=False))
+ if self.matches(query, has):
+ # FIXME: guard against predicate mismatch
+ return f'has {self._uri2abb[query.predicate.predicate]}'
+ if self.matches(query, ast.filter.Not(has)):
+ # FIXME: guard against predicate mismatch
+ return f'has no {self._uri2abb[query.predicate.predicate]}'
+ return None
+
+ def _categorical(self, query):
+ if not isinstance(query, ast.filter._Branch):
+ return None
+
+ # shortcuts
+ expr = query.expr
+ pred = self._uri2abb.get(query.predicate.predicate, None)
+ if pred is None:
+ return None
+
+ # positive constraints
+ if isinstance(query, ast.filter.Any):
+ # approximate positive constraint
+ # Any(<pred>, Includes(<values>, approx=True)) -> pred ~ ("...", ...)
+ if self.matches(expr, matcher.Partial(ast.filter.Substring)):
+ return f'{pred} ~ {expr.value}'
+ if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring)))):
+ values = '", "'.join(sub.value for sub in expr)
+ return f'{pred} ~ ("{values}")'
+
+ # exact positive constraint
+ # ast.filter.Any(<pred>, ast.filter.Includes(<values>, approx=False)) -> pred = ("...", ...)
+ if self.matches(expr, matcher.Partial(ast.filter.Equals)):
+ return f'{pred} = {expr.value}'
+ if self.matches(query, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals)))):
+ values = '", "'.join(sub.value for sub in expr)
+ return f'{pred} = ("{values}")'
+
+ # negative constraints
+ if isinstance(query, ast.filter.All):
+ # approximate negative constraint
+ # ast.filter.All(<pred>, ast.filter.Excludes(<values>, approx=True)) -> pred !~ ("...", ...)
+ if self.matches(query, ast.filter.Not(matcher.Partial(ast.filter.Substring))):
+ return f'{pred} !~ "{expr.value}"'
+ if self.matches(query, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring))))):
+ values = '", "'.join(sub.value for sub in expr)
+ return f'{pred} !~ ("{values}")'
+
+ # exact negative constraint
+ # ast.filter.All(<pred>, ast.filter.Excludes(<values>, approx=False)) -> pred != ("...", ...)
+ if self.matches(query, ast.filter.Not(matcher.Partial(ast.filter.Equals))):
+ return f'{pred} != "{expr.value}"'
+ if self.matches(query, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals))))):
+ values = '", "'.join(sub.value for sub in expr)
+ return f'{pred} != ("{values}")'
+
+ return None
+
+ def _tag(self, query):
+ # positive constraint
+ # ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Includes(..., approx=?))) <-> "...", ...; ~ "...", ...
+ if self.matches(query, ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, matcher.Any()))):
+ expr = query.expr.expr
+ # approximate positive constraint
+ if self.matches(expr, matcher.Partial(ast.filter.Substring)):
+ return f'~ {expr.value}'
+ if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring)))):
+ values = '", "'.join(sub.value for sub in expr)
+ return f'~ "{values}"'
+ # exact positive constraint
+ if self.matches(expr, matcher.Partial(ast.filter.Equals)):
+ return f'{expr.value}'
+ if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals)))):
+ values = '", "'.join(sub.value for sub in expr)
+ return f'"{values}"'
+
+ # negative constraint
+ # ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Excludes(..., approx=?))) <-> ! "...", ... ; !~ "...", ...
+ if self.matches(query, ast.filter.All(ns.bse.tag, ast.filter.Any(ns.bst.label, ast.filter.Not(matcher.Any())))):
+ expr = query.expr.expr.expr
+ # approximate negative constraint
+ if self.matches(expr, matcher.Partial(ast.filter.Substring)):
+ return f'!~ {expr.value}'
+ if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Substring)))):
+ values = '", "'.join(sub.value for sub in expr)
+ return f'!~ "{values}"'
+ # exact negative constraint
+ if self.matches(expr, matcher.Partial(ast.filter.Equals)):
+ return f'! {expr.value}'
+ if self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Equals)))):
+ values = '", "'.join(sub.value for sub in expr)
+ return f'! "{values}"'
+
+ return None
+
+ def _range(self, query):
+ # FIXME: handle dates and times!
+ # FIXME: use default/configurable separators from from_string
+ if not isinstance(query, ast.filter.Any):
+ return None
+
+ expr = query.expr
+ pred = self._uri2abb.get(query.predicate.predicate, None)
+ if pred is None:
+ return None
+
+ if self.matches(expr, matcher.Partial(ast.filter.Equals)):
+ return f'{pred} = {expr.value}'
+ if self.matches(expr, matcher.Partial(ast.filter.GreaterThan, strict=True)):
+ return f'{pred} > {expr.threshold}'
+ if self.matches(expr, matcher.Partial(ast.filter.GreaterThan, strict=False)):
+ return f'{pred} >= {expr.threshold}'
+ if self.matches(expr, matcher.Partial(ast.filter.LessThan, strict=True)):
+ return f'{pred} < {expr.threshold}'
+ if self.matches(expr, matcher.Partial(ast.filter.LessThan, strict=False)):
+ return f'{pred} <= {expr.threshold}'
+ if self.matches(expr, ast.filter.And(
+ matcher.Partial(ast.filter.GreaterThan),
+ matcher.Partial(ast.filter.LessThan))):
+ lo, hi = list(expr)
+ if self.matches(lo, matcher.Partial(ast.filter.LessThan)):
+ lo, hi = hi, lo
+ b_open = '(' if lo.strict else '['
+ b_close = ')' if hi.strict else ']'
+ return f'{pred} = {b_open}{lo.threshold} - {hi.threshold}{b_close}'
+ """
+ ast.filter.Any(<pred>, ast.filter.Between(lo, hi, lo_strict, hi_strict))
+ pred <? hi
+ pred >? hi
+ pred = [lo, hi]
+ pred = (lo, hi)
+ pred = [lo, hi)
+ pred = (lo, hi]
+ """
+ return None
+
+ def _entity(self, query):
+ # defaults
+ negated = False
+ guids = set()
+
+ def get_guids(value):
+ if isinstance(value, URI):
+ return {value}
+ else: # elif isinstance(query.value, Nodes):
+ return set(value.guids)
+
+ if self.matches(query, matcher.Partial(ast.filter.Is)):
+ guids = get_guids(query.value)
+ elif self.matches(query, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))):
+ guids = {guid for sub in query for guid in get_guids(sub.value) }
+ elif self.matches(query, ast.filter.Not(matcher.Partial(ast.filter.Is))):
+ negated = True
+ guids = get_guids(query.value)
+ elif self.matches(query, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))):
+ negated = True
+ guids = {guid for sub in query for guid in get_guids(sub.value) }
+
+ if len(guids) == 0:
+ # no matches
+ return None
+ # some matches
+ cmp = 'not in' if negated else 'in'
+ values = '", "'.join(guids)
+ return f'id {cmp} "{values}"'
+
+ def _group(self, query):
+ # ast.filter.Any(ns.bse.group, ast.filter.Is(...)) <-> group = ("...", ...)
+ if not self.matches(query, ast.filter.Any(ns.bse.group, matcher.Any())):
+ return None
+
+ def get_guids(value):
+ if isinstance(value, URI):
+ return {value}
+ else: # elif isinstance(query.value, Nodes):
+ return set(value.guids)
+
+ expr = query.expr
+ guids = set()
+ negated = False
+
+ if self.matches(expr, matcher.Partial(ast.filter.Is)):
+ guids = get_guids(expr.value)
+ elif self.matches(expr, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))):
+ guids = {guid for sub in expr for guid in get_guids(sub.value) }
+ elif self.matches(expr, ast.filter.Not(matcher.Partial(ast.filter.Is))):
+ negated = True
+ guids = get_guids(expr.value)
+ elif self.matches(expr, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))):
+ negated = True
+ guids = {guid for sub in expr for guid in get_guids(sub.value) }
+
+ if len(guids) == 0: # no matches
+ return None
+ # some matches
+ cmp = 'not in' if negated else 'in'
+ values = '", "'.join(guids)
+ return f'group {cmp} "{values}"'
+
+## EOF ##
diff --git a/tagit/utils/bsfs.py b/tagit/utils/bsfs.py
index 3c2b826..ab8baa5 100644
--- a/tagit/utils/bsfs.py
+++ b/tagit/utils/bsfs.py
@@ -10,7 +10,7 @@ import typing
# bsfs imports
from bsfs import schema, Open
from bsfs.namespace import Namespace
-from bsfs.query import ast
+from bsfs.query import ast, matcher
from bsfs.utils import URI, uuid
# exports
diff --git a/tagit/widgets/filter.py b/tagit/widgets/filter.py
index 8a7c1a2..15aefd6 100644
--- a/tagit/widgets/filter.py
+++ b/tagit/widgets/filter.py
@@ -22,8 +22,8 @@ import kivy.properties as kp
# tagit imports
from tagit import config
-#from tagit.parsing.search import ast, ast_to_string # FIXME: mb/port
-from tagit.utils import bsfs, errors
+from tagit.utils import bsfs, errors, ns
+from tagit.utils.bsfs import ast, matcher
# inner-module imports
from .session import ConfigAwareMixin
@@ -120,20 +120,25 @@ class Filter(BoxLayout, ConfigAwareMixin):
return query, sort
def abbreviate(self, token):
- return 'T'
- # FIXME: mb/port/parsing
- if token.predicate() == 'tag':
- return ','.join(list(token.condition()))
- elif token.predicate() == 'entity':
- return 'R' if isinstance(token.condition(), ast.SetInclude) else 'E'
- else:
- return {
- 'group' : 'G',
- 'time' : 'T',
- 'altitude' : 'Alt',
- 'longitude' : 'Lon',
- 'latitude' : 'Lat',
- }.get(token.predicate(), token.predicate().title())
+ matches = matcher.Filter()
+ if matches(token, ast.filter.Any(ns.bse.tag, ast.filter.Any(ns.bst.label, matcher.Any()))):
+ # tag token
+ return self.root.session.filter_to_string(token)
+ if matches(token, matcher.Partial(ast.filter.Is)) or \
+ matches(token, ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is)))):
+ # exclusive token
+ return 'E'
+ if matches(token, ast.filter.Not(matcher.Partial(ast.filter.Is))) or \
+ matches(token, ast.filter.Not(ast.filter.Or(matcher.Rest(matcher.Partial(ast.filter.Is))))):
+ # reduce token
+ return 'R'
+ if matches(token, ast.filter.Any(ns.bse.group, matcher.Any())):
+ # group token
+ return 'G'
+ if matches(token, ast.filter.Any(matcher.Partial(ast.filter.Predicate), matcher.Any())):
+ # generic token
+ return token.predicate.predicate.get('fragment', '?').title()
+ return '?'
def show_address_once(self):
"""Single-shot address mode without changing the search mode."""
@@ -272,7 +277,7 @@ class Addressbar(TextInput):
def __init__(self, tokens, **kwargs):
super(Addressbar, self).__init__(**kwargs)
- self.text = ast_to_string(ast.AND(tokens))
+ self.text = self.root.session.filter_to_string(bsfs.ast.filter.And(tokens))
self._last_text = self.text
def on_text_validate(self):
diff --git a/tagit/widgets/session.py b/tagit/widgets/session.py
index e97a688..c233a15 100644
--- a/tagit/widgets/session.py
+++ b/tagit/widgets/session.py
@@ -40,6 +40,7 @@ class Session(Widget):
self.log = log
# derived members
self.filter_from_string = parsing.filter.FromString(self.storage.schema)
+ self.filter_to_string = parsing.filter.ToString(self.storage.schema)
#self.sort_from_string = parsing.Sort(self.storage.schema) # FIXME: mb/port/parsing
def __enter__(self):