9749: Move experimental Select API code into a branch
This commit is contained in:
parent
ce4d4ca31d
commit
2993d59c2e
@ -98,13 +98,6 @@ The following packages are optional:
|
|||||||
|
|
||||||
More font support in the reports
|
More font support in the reports
|
||||||
|
|
||||||
* **Meta**
|
|
||||||
|
|
||||||
Required for experimental "where" clause creation. This functionality
|
|
||||||
is not yet in main-line code so it is not needed by users. If the package
|
|
||||||
will be used by gramps developers, to support further development, then
|
|
||||||
it may be included. Install with pypi: https://pypi.python.org/pypi/meta.
|
|
||||||
|
|
||||||
Optional packages required by Third-party Addons
|
Optional packages required by Third-party Addons
|
||||||
------------------------------------------------
|
------------------------------------------------
|
||||||
|
|
||||||
|
@ -1214,173 +1214,6 @@ class DbReadBase:
|
|||||||
"""
|
"""
|
||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def _select(self, table, fields=None, start=0, limit=-1,
|
|
||||||
where=None, order_by=None):
|
|
||||||
"""
|
|
||||||
Default implementation of a select for those databases
|
|
||||||
that don't support SQL. Returns a list of dicts, total,
|
|
||||||
and time.
|
|
||||||
|
|
||||||
table - Person, Family, etc.
|
|
||||||
fields - used by object.get_field()
|
|
||||||
start - position to start
|
|
||||||
limit - count to get; -1 for all
|
|
||||||
where - (field, SQL string_operator, value) |
|
|
||||||
["AND", [where, where, ...]] |
|
|
||||||
["OR", [where, where, ...]] |
|
|
||||||
["NOT", where]
|
|
||||||
order_by - [[fieldname, "ASC" | "DESC"], ...]
|
|
||||||
"""
|
|
||||||
def compare(v, op, value):
|
|
||||||
"""
|
|
||||||
Compare values in a SQL-like way
|
|
||||||
"""
|
|
||||||
if isinstance(v, (list, tuple)) and len(v) > 0: # join, or multi-values
|
|
||||||
# If any is true:
|
|
||||||
for item in v:
|
|
||||||
if compare(item, op, value):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
if op in ["=", "=="]:
|
|
||||||
matched = v == value
|
|
||||||
elif op == ">":
|
|
||||||
matched = v > value
|
|
||||||
elif op == ">=":
|
|
||||||
matched = v >= value
|
|
||||||
elif op == "<":
|
|
||||||
matched = v < value
|
|
||||||
elif op == "<=":
|
|
||||||
matched = v <= value
|
|
||||||
elif op == "IN":
|
|
||||||
matched = v in value
|
|
||||||
elif op == "IS":
|
|
||||||
matched = v is value
|
|
||||||
elif op == "IS NOT":
|
|
||||||
matched = v is not value
|
|
||||||
elif op == "IS NULL":
|
|
||||||
matched = v is None
|
|
||||||
elif op == "IS NOT NULL":
|
|
||||||
matched = v is not None
|
|
||||||
elif op == "BETWEEN":
|
|
||||||
matched = value[0] <= v <= value[1]
|
|
||||||
elif op in ["<>", "!="]:
|
|
||||||
matched = v != value
|
|
||||||
elif op == "LIKE":
|
|
||||||
if value and v:
|
|
||||||
value = value.replace("%", "(.*)").replace("_", ".")
|
|
||||||
## FIXME: allow a case-insensitive version
|
|
||||||
matched = re.match("^" + value + "$", v, re.MULTILINE)
|
|
||||||
else:
|
|
||||||
matched = False
|
|
||||||
elif op == "REGEXP":
|
|
||||||
if value and v:
|
|
||||||
matched = re.search(value, v, re.MULTILINE) is not None
|
|
||||||
else:
|
|
||||||
matched = False
|
|
||||||
else:
|
|
||||||
raise Exception("invalid select operator: '%s'" % op)
|
|
||||||
return True if matched else False
|
|
||||||
|
|
||||||
def evaluate_values(condition, item, db, table, env):
|
|
||||||
"""
|
|
||||||
Evaluates the names in all conditions.
|
|
||||||
"""
|
|
||||||
if len(condition) == 2: # ["AND" [...]] | ["OR" [...]] | ["NOT" expr]
|
|
||||||
connector, exprs = condition
|
|
||||||
if connector in ["AND", "OR"]:
|
|
||||||
for expr in exprs:
|
|
||||||
evaluate_values(expr, item, db, table, env)
|
|
||||||
else: # "NOT"
|
|
||||||
evaluate_values(exprs, item, db, table, env)
|
|
||||||
elif len(condition) == 3: # (name, op, value)
|
|
||||||
(name, op, value) = condition
|
|
||||||
# just the ones we need for where
|
|
||||||
hname = self._hash_name(table, name)
|
|
||||||
if hname not in env:
|
|
||||||
value = item.get_field(name, db, ignore_errors=True)
|
|
||||||
env[hname] = value
|
|
||||||
|
|
||||||
def evaluate_truth(condition, item, db, table, env):
|
|
||||||
if len(condition) == 2: # ["AND"|"OR" [...]]
|
|
||||||
connector, exprs = condition
|
|
||||||
if connector == "AND": # all must be true
|
|
||||||
for expr in exprs:
|
|
||||||
if not evaluate_truth(expr, item, db, table, env):
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
elif connector == "OR": # any will return true
|
|
||||||
for expr in exprs:
|
|
||||||
if evaluate_truth(expr, item, db, table, env):
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
elif connector == "NOT": # return not of single value
|
|
||||||
return not evaluate_truth(exprs, item, db, table, env)
|
|
||||||
else:
|
|
||||||
raise Exception("No such connector: '%s'" % connector)
|
|
||||||
elif len(condition) == 3: # (name, op, value)
|
|
||||||
(name, op, value) = condition
|
|
||||||
v = env.get(self._hash_name(table, name))
|
|
||||||
return compare(v, op, value)
|
|
||||||
|
|
||||||
# Fields is None or list, maybe containing "*":
|
|
||||||
if fields is None:
|
|
||||||
pass # ok
|
|
||||||
elif not isinstance(fields, (list, tuple)):
|
|
||||||
raise Exception("fields must be a list/tuple of field names")
|
|
||||||
elif "*" in fields:
|
|
||||||
fields.remove("*")
|
|
||||||
fields.extend(self.get_table_func(table,"class_func").get_schema().keys())
|
|
||||||
get_count_only = (fields is not None and fields[0] == "count(1)")
|
|
||||||
position = 0
|
|
||||||
selected = 0
|
|
||||||
if get_count_only:
|
|
||||||
if where or limit != -1 or start != 0:
|
|
||||||
# no need to order for a count
|
|
||||||
data = self.get_table_func(table,"iter_func")()
|
|
||||||
else:
|
|
||||||
yield self.get_table_func(table,"count_func")()
|
|
||||||
else:
|
|
||||||
data = self.get_table_func(table, "iter_func")(order_by=order_by)
|
|
||||||
if where:
|
|
||||||
for item in data:
|
|
||||||
# Go through all fliters and evaluate the fields:
|
|
||||||
env = {}
|
|
||||||
evaluate_values(where, item, self, table, env)
|
|
||||||
matched = evaluate_truth(where, item, self, table, env)
|
|
||||||
if matched:
|
|
||||||
if ((selected < limit) or (limit == -1)) and start <= position:
|
|
||||||
selected += 1
|
|
||||||
if not get_count_only:
|
|
||||||
if fields:
|
|
||||||
row = {}
|
|
||||||
for field in fields:
|
|
||||||
value = item.get_field(field, self, ignore_errors=True)
|
|
||||||
row[field.replace("__", ".")] = value
|
|
||||||
yield row
|
|
||||||
else:
|
|
||||||
yield item
|
|
||||||
position += 1
|
|
||||||
if get_count_only:
|
|
||||||
yield selected
|
|
||||||
else: # no where
|
|
||||||
for item in data:
|
|
||||||
if position >= start:
|
|
||||||
if ((selected >= limit) and (limit != -1)):
|
|
||||||
break
|
|
||||||
selected += 1
|
|
||||||
if not get_count_only:
|
|
||||||
if fields:
|
|
||||||
row = {}
|
|
||||||
for field in fields:
|
|
||||||
value = item.get_field(field, self, ignore_errors=True)
|
|
||||||
row[field.replace("__", ".")] = value
|
|
||||||
yield row
|
|
||||||
else:
|
|
||||||
yield item
|
|
||||||
position += 1
|
|
||||||
if get_count_only:
|
|
||||||
yield selected
|
|
||||||
|
|
||||||
def _hash_name(self, table, name):
|
def _hash_name(self, table, name):
|
||||||
"""
|
"""
|
||||||
Used in SQL functions to eval expressions involving selected
|
Used in SQL functions to eval expressions involving selected
|
||||||
@ -1389,17 +1222,6 @@ class DbReadBase:
|
|||||||
name = self.get_table_func(table,"class_func").get_field_alias(name)
|
name = self.get_table_func(table,"class_func").get_field_alias(name)
|
||||||
return name.replace(".", "__")
|
return name.replace(".", "__")
|
||||||
|
|
||||||
Person = property(lambda self: QuerySet(self, "Person"))
|
|
||||||
Family = property(lambda self: QuerySet(self, "Family"))
|
|
||||||
Note = property(lambda self: QuerySet(self, "Note"))
|
|
||||||
Citation = property(lambda self: QuerySet(self, "Citation"))
|
|
||||||
Source = property(lambda self: QuerySet(self, "Source"))
|
|
||||||
Repository = property(lambda self: QuerySet(self, "Repository"))
|
|
||||||
Place = property(lambda self: QuerySet(self, "Place"))
|
|
||||||
Event = property(lambda self: QuerySet(self, "Event"))
|
|
||||||
Tag = property(lambda self: QuerySet(self, "Tag"))
|
|
||||||
Media = property(lambda self: QuerySet(self, "Media"))
|
|
||||||
|
|
||||||
class DbWriteBase(DbReadBase):
|
class DbWriteBase(DbReadBase):
|
||||||
"""
|
"""
|
||||||
Gramps database object. This object is a base class for all
|
Gramps database object. This object is a base class for all
|
||||||
@ -1976,12 +1798,6 @@ class DbWriteBase(DbReadBase):
|
|||||||
else:
|
else:
|
||||||
raise ValueError("invalid instance type: %s" % instance.__class__.__name__)
|
raise ValueError("invalid instance type: %s" % instance.__class__.__name__)
|
||||||
|
|
||||||
def get_queryset_by_table_name(self, table_name):
|
|
||||||
"""
|
|
||||||
Get Person, Family queryset by name.
|
|
||||||
"""
|
|
||||||
return getattr(self, table_name)
|
|
||||||
|
|
||||||
def autobackup(self, user=None):
|
def autobackup(self, user=None):
|
||||||
"""
|
"""
|
||||||
Backup the current file as a backup file.
|
Backup the current file as a backup file.
|
||||||
@ -2001,222 +1817,3 @@ class DbWriteBase(DbReadBase):
|
|||||||
if user.uistate:
|
if user.uistate:
|
||||||
user.uistate.set_busy_cursor(False)
|
user.uistate.set_busy_cursor(False)
|
||||||
user.uistate.progress.hide()
|
user.uistate.progress.hide()
|
||||||
|
|
||||||
class QuerySet:
|
|
||||||
"""
|
|
||||||
A container for selection criteria before being actually
|
|
||||||
applied to a database.
|
|
||||||
"""
|
|
||||||
def __init__(self, database, table):
|
|
||||||
self.database = database
|
|
||||||
self.table = table
|
|
||||||
self.generator = None
|
|
||||||
self.where_by = None
|
|
||||||
self.order_by = None
|
|
||||||
self.limit_by = -1
|
|
||||||
self.start = 0
|
|
||||||
self.needs_to_run = False
|
|
||||||
|
|
||||||
def limit(self, start=None, count=None):
|
|
||||||
"""
|
|
||||||
Put limits on the selection.
|
|
||||||
"""
|
|
||||||
if start is not None:
|
|
||||||
self.start = start
|
|
||||||
if count is not None:
|
|
||||||
self.limit_by = count
|
|
||||||
self.needs_to_run = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def order(self, *args):
|
|
||||||
"""
|
|
||||||
Put an ordering on the selection.
|
|
||||||
"""
|
|
||||||
for arg in args:
|
|
||||||
if self.order_by is None:
|
|
||||||
self.order_by = []
|
|
||||||
if arg.startswith("-"):
|
|
||||||
self.order_by.append((arg[1:], "DESC"))
|
|
||||||
else:
|
|
||||||
self.order_by.append((arg, "ASC"))
|
|
||||||
self.needs_to_run = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def _add_where_clause(self, *args):
|
|
||||||
"""
|
|
||||||
Add a condition to the where clause.
|
|
||||||
"""
|
|
||||||
# First, handle AND, OR, NOT args:
|
|
||||||
and_expr = []
|
|
||||||
for expr in args:
|
|
||||||
and_expr.append(expr)
|
|
||||||
# Next, handle kwargs:
|
|
||||||
if and_expr:
|
|
||||||
if self.where_by:
|
|
||||||
self.where_by = ["AND", [self.where_by] + and_expr]
|
|
||||||
elif len(and_expr) == 1:
|
|
||||||
self.where_by = and_expr[0]
|
|
||||||
else:
|
|
||||||
self.where_by = ["AND", and_expr]
|
|
||||||
self.needs_to_run = True
|
|
||||||
return self
|
|
||||||
|
|
||||||
def count(self):
|
|
||||||
"""
|
|
||||||
Run query with just where, start, limit to get count.
|
|
||||||
"""
|
|
||||||
if self.generator and self.needs_to_run:
|
|
||||||
raise Exception("Queries in invalid order")
|
|
||||||
elif self.generator:
|
|
||||||
return len(list(self.generator))
|
|
||||||
else:
|
|
||||||
generator = self.database._select(self.table,
|
|
||||||
["count(1)"],
|
|
||||||
where=self.where_by,
|
|
||||||
start=self.start,
|
|
||||||
limit=self.limit_by)
|
|
||||||
return next(generator)
|
|
||||||
|
|
||||||
def _generate(self, args=None):
|
|
||||||
"""
|
|
||||||
Create a generator from current options.
|
|
||||||
"""
|
|
||||||
generator = self.database._select(self.table,
|
|
||||||
args,
|
|
||||||
order_by=self.order_by,
|
|
||||||
where=self.where_by,
|
|
||||||
start=self.start,
|
|
||||||
limit=self.limit_by)
|
|
||||||
# Reset all criteria
|
|
||||||
self.where_by = None
|
|
||||||
self.order_by = None
|
|
||||||
self.limit_by = -1
|
|
||||||
self.start = 0
|
|
||||||
self.needs_to_run = False
|
|
||||||
return generator
|
|
||||||
|
|
||||||
def select(self, *args):
|
|
||||||
"""
|
|
||||||
Actually touch the database.
|
|
||||||
"""
|
|
||||||
if len(args) == 0:
|
|
||||||
args = None
|
|
||||||
if self.generator and self.needs_to_run:
|
|
||||||
## problem
|
|
||||||
raise Exception("Queries in invalid order")
|
|
||||||
elif self.generator:
|
|
||||||
if args: # there is a generator, with args
|
|
||||||
for i in self.generator:
|
|
||||||
yield [i.get_field(arg) for arg in args]
|
|
||||||
else: # generator, no args
|
|
||||||
for i in self.generator:
|
|
||||||
yield i
|
|
||||||
else: # need to run or not
|
|
||||||
self.generator = self._generate(args)
|
|
||||||
for i in self.generator:
|
|
||||||
yield i
|
|
||||||
|
|
||||||
def proxy(self, proxy_name, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
Apply a named proxy to the db.
|
|
||||||
"""
|
|
||||||
from gramps.gen.proxy import (LivingProxyDb, PrivateProxyDb,
|
|
||||||
ReferencedBySelectionProxyDb)
|
|
||||||
if proxy_name == "living":
|
|
||||||
proxy_class = LivingProxyDb
|
|
||||||
elif proxy_name == "private":
|
|
||||||
proxy_class = PrivateProxyDb
|
|
||||||
elif proxy_name == "referenced":
|
|
||||||
proxy_class = ReferencedBySelectionProxyDb
|
|
||||||
else:
|
|
||||||
raise Exception("No such proxy name: '%s'" % proxy_name)
|
|
||||||
self.database = proxy_class(self.database, *args, **kwargs)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def where(self, where_clause):
|
|
||||||
"""
|
|
||||||
Apply a where_clause (closure) to the selection process.
|
|
||||||
"""
|
|
||||||
from gramps.gen.db.where import eval_where
|
|
||||||
# if there is already a generator, then error:
|
|
||||||
if self.generator:
|
|
||||||
raise Exception("Queries in invalid order")
|
|
||||||
where_by = eval_where(where_clause)
|
|
||||||
self._add_where_clause(where_by)
|
|
||||||
return self
|
|
||||||
|
|
||||||
def filter(self, *args):
|
|
||||||
"""
|
|
||||||
Apply a filter to the database.
|
|
||||||
"""
|
|
||||||
from gramps.gen.proxy import FilterProxyDb
|
|
||||||
from gramps.gen.filters import GenericFilter
|
|
||||||
from gramps.gen.db.where import eval_where
|
|
||||||
for i in range(len(args)):
|
|
||||||
arg = args[i]
|
|
||||||
if isinstance(arg, GenericFilter):
|
|
||||||
self.database = FilterProxyDb(self.database, arg, *args[i+1:])
|
|
||||||
if hasattr(arg, "where"):
|
|
||||||
where_by = eval_where(arg.where)
|
|
||||||
self._add_where_clause(where_by)
|
|
||||||
elif callable(arg):
|
|
||||||
if self.generator and self.needs_to_run:
|
|
||||||
## error
|
|
||||||
raise Exception("Queries in invalid order")
|
|
||||||
elif self.generator:
|
|
||||||
pass # ok
|
|
||||||
else:
|
|
||||||
self.generator = self._generate()
|
|
||||||
self.generator = filter(arg, self.generator)
|
|
||||||
else:
|
|
||||||
pass # ignore, may have been arg from previous Filter
|
|
||||||
return self
|
|
||||||
|
|
||||||
def map(self, f):
|
|
||||||
"""
|
|
||||||
Apply the function f to the selected items and return results.
|
|
||||||
"""
|
|
||||||
if self.generator and self.needs_to_run:
|
|
||||||
raise Exception("Queries in invalid order")
|
|
||||||
elif self.generator:
|
|
||||||
pass # ok
|
|
||||||
else:
|
|
||||||
self.generator = self._generate()
|
|
||||||
previous_generator = self.generator
|
|
||||||
def generator():
|
|
||||||
for item in previous_generator:
|
|
||||||
yield f(item)
|
|
||||||
self.generator = generator()
|
|
||||||
return self
|
|
||||||
|
|
||||||
def tag(self, tag_text, remove=False):
|
|
||||||
"""
|
|
||||||
Tag or untag the selected items with the tag name.
|
|
||||||
"""
|
|
||||||
if self.generator and self.needs_to_run:
|
|
||||||
raise Exception("Queries in invalid order")
|
|
||||||
elif self.generator:
|
|
||||||
pass # ok
|
|
||||||
else:
|
|
||||||
self.generator = self._generate()
|
|
||||||
tag = self.database.get_tag_from_name(tag_text)
|
|
||||||
if (not tag and remove):
|
|
||||||
# no tag by this name, and want to remove it
|
|
||||||
# nothing to do
|
|
||||||
return
|
|
||||||
trans_class = self.database.get_transaction_class()
|
|
||||||
with trans_class("Tag Selected Items", self.database, batch=False) as trans:
|
|
||||||
if tag is None:
|
|
||||||
tag = self.database.get_table_func("Tag","class_func")()
|
|
||||||
tag.set_name(tag_text)
|
|
||||||
self.database.add_tag(tag, trans)
|
|
||||||
commit_func = self.database.get_table_func(self.table,"commit_func")
|
|
||||||
for item in self.generator:
|
|
||||||
if remove and (tag.handle in item.tag_list):
|
|
||||||
item.remove_tag(tag.handle)
|
|
||||||
elif (not remove) and (tag.handle not in item.tag_list):
|
|
||||||
item.add_tag(tag.handle)
|
|
||||||
else:
|
|
||||||
continue
|
|
||||||
commit_func(item, trans)
|
|
||||||
|
|
||||||
|
@ -53,7 +53,6 @@ from gramps.gen.db import (DbReadBase, DbWriteBase, DbTxn, DbUndo,
|
|||||||
PLACE_KEY, REPOSITORY_KEY, NOTE_KEY,
|
PLACE_KEY, REPOSITORY_KEY, NOTE_KEY,
|
||||||
TAG_KEY, eval_order_by)
|
TAG_KEY, eval_order_by)
|
||||||
from gramps.gen.errors import HandleError
|
from gramps.gen.errors import HandleError
|
||||||
from gramps.gen.db.base import QuerySet
|
|
||||||
from gramps.gen.utils.callback import Callback
|
from gramps.gen.utils.callback import Callback
|
||||||
from gramps.gen.updatecallback import UpdateCallback
|
from gramps.gen.updatecallback import UpdateCallback
|
||||||
from gramps.gen.db.dbconst import *
|
from gramps.gen.db.dbconst import *
|
||||||
@ -2260,7 +2259,6 @@ class DbGeneric(DbWriteBase, DbReadBase, UpdateCallback, Callback):
|
|||||||
Add a new table and funcs to the database.
|
Add a new table and funcs to the database.
|
||||||
"""
|
"""
|
||||||
self.__tables[table] = funcs
|
self.__tables[table] = funcs
|
||||||
setattr(DbGeneric, table, property(lambda self: QuerySet(self, table)))
|
|
||||||
|
|
||||||
def get_version(self):
|
def get_version(self):
|
||||||
"""
|
"""
|
||||||
|
@ -1,110 +0,0 @@
|
|||||||
#
|
|
||||||
# Gramps - a GTK+/GNOME based genealogy program
|
|
||||||
#
|
|
||||||
# Copyright (C) 2016 Gramps Development Team
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
#
|
|
||||||
|
|
||||||
from gramps.gen.db.where import eval_where
|
|
||||||
from gramps.gen.lib import Person
|
|
||||||
import unittest
|
|
||||||
|
|
||||||
##########
|
|
||||||
# Tests:
|
|
||||||
|
|
||||||
def make_closure(surname):
|
|
||||||
"""
|
|
||||||
Test closure.
|
|
||||||
"""
|
|
||||||
from gramps.gen.lib import Person
|
|
||||||
return (lambda person:
|
|
||||||
(person.primary_name.surname_list[0].surname == surname and
|
|
||||||
person.gender == Person.MALE))
|
|
||||||
|
|
||||||
class Thing:
|
|
||||||
def __init__(self):
|
|
||||||
self.list = ["I0", "I1", "I2"]
|
|
||||||
|
|
||||||
def where(self):
|
|
||||||
return lambda person: person.gramps_id == self.list[1]
|
|
||||||
|
|
||||||
def apply(self, db, person):
|
|
||||||
return person.gender == Person.MALE
|
|
||||||
|
|
||||||
class ClosureTest(unittest.TestCase):
|
|
||||||
def check(self, test):
|
|
||||||
result = eval_where(test[0])
|
|
||||||
self.assertTrue(result == test[1], "%s is not %s" % (result, test[1]))
|
|
||||||
|
|
||||||
def test_01(self):
|
|
||||||
self.check(
|
|
||||||
(lambda family: (family.private and
|
|
||||||
family.mother_handle.gramps_id != "I0001"),
|
|
||||||
['AND', [['private', '==', True],
|
|
||||||
['mother_handle.gramps_id', '!=', 'I0001']]]))
|
|
||||||
|
|
||||||
def test_02(self):
|
|
||||||
self.check(
|
|
||||||
(lambda person: LIKE(person.gramps_id, "I0001"),
|
|
||||||
['gramps_id', 'LIKE', 'I0001']))
|
|
||||||
|
|
||||||
def test_03(self):
|
|
||||||
self.check(
|
|
||||||
(lambda note: note.gramps_id == "N0001",
|
|
||||||
['gramps_id', '==', 'N0001']))
|
|
||||||
|
|
||||||
def test_04(self):
|
|
||||||
self.check(
|
|
||||||
(lambda person: person.event_ref_list.ref.gramps_id == "E0001",
|
|
||||||
['event_ref_list.ref.gramps_id', '==', 'E0001']))
|
|
||||||
|
|
||||||
def test_05(self):
|
|
||||||
self.check(
|
|
||||||
(lambda person: LIKE(person.gramps_id, "I0001") or person.private,
|
|
||||||
["OR", [['gramps_id', 'LIKE', 'I0001'],
|
|
||||||
["private", "==", True]]]))
|
|
||||||
|
|
||||||
def test_06(self):
|
|
||||||
self.check(
|
|
||||||
(lambda person: person.event_ref_list <= 0,
|
|
||||||
["event_ref_list", "<=", 0]))
|
|
||||||
|
|
||||||
def test_07(self):
|
|
||||||
self.check(
|
|
||||||
(lambda person: person.primary_name.surname_list[0].surname == "Smith",
|
|
||||||
["primary_name.surname_list.0.surname", "==", "Smith"]))
|
|
||||||
|
|
||||||
def test_08(self):
|
|
||||||
self.check(
|
|
||||||
(make_closure("Smith"),
|
|
||||||
["AND", [["primary_name.surname_list.0.surname", "==", "Smith"],
|
|
||||||
["gender", "==", 1]]]))
|
|
||||||
|
|
||||||
def test_09(self):
|
|
||||||
self.check(
|
|
||||||
[Thing().where(), ["gramps_id", "==", "I1"]])
|
|
||||||
|
|
||||||
def test_10(self):
|
|
||||||
self.check(
|
|
||||||
(lambda person: LIKE(person.gramps_id, "I000%"),
|
|
||||||
["gramps_id", "LIKE", "I000%"]))
|
|
||||||
|
|
||||||
def test_11(self):
|
|
||||||
self.check(
|
|
||||||
[Thing().apply, ["gender", "==", 1]])
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
unittest.main()
|
|
@ -1,222 +0,0 @@
|
|||||||
#
|
|
||||||
# Gramps - a GTK+/GNOME based genealogy program
|
|
||||||
#
|
|
||||||
# Copyright (C) 2016 Gramps Development Team
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA
|
|
||||||
#
|
|
||||||
|
|
||||||
from meta.asttools import Visitor
|
|
||||||
from meta.decompiler import _ast, decompile_func
|
|
||||||
|
|
||||||
import copy
|
|
||||||
|
|
||||||
class ParseFilter(Visitor):
|
|
||||||
"""
|
|
||||||
This class is used to turn Python lambda expressions into AST
|
|
||||||
which is used as a SELECT statements in databases via the .where()
|
|
||||||
method. This is used by both BSDDB and SQL-based databases.
|
|
||||||
|
|
||||||
Not all Python is allowed as a where-clause, and some functions
|
|
||||||
used here are not real Python functions.
|
|
||||||
|
|
||||||
Examples:
|
|
||||||
|
|
||||||
db.Person.where(
|
|
||||||
lambda person: person.gramps_id == "I0001"
|
|
||||||
).select()
|
|
||||||
|
|
||||||
Some uses look (and evaluate) like regular Python.
|
|
||||||
|
|
||||||
db.Person.where(
|
|
||||||
lambda person: LIKE(person.gramps_id, "I000%")
|
|
||||||
).select()
|
|
||||||
|
|
||||||
LIKE is not a real Python function, but the syntax is used to
|
|
||||||
indicate a fuzzy match.
|
|
||||||
|
|
||||||
db.Family.where(
|
|
||||||
lambda family: LIKE(family.mother_handle.gramps_id, "I003%")
|
|
||||||
).select()
|
|
||||||
|
|
||||||
LIKE uses % as a wildcard matching character, like ".*" in re.
|
|
||||||
|
|
||||||
db.Family.where(
|
|
||||||
lambda family: family.mother_handle.event_ref_list.ref.gramps_id == 'E0156'
|
|
||||||
).select()
|
|
||||||
|
|
||||||
Here, property chaining is shown without having to check to see if
|
|
||||||
values actually exist. The checking for valid/existing properties
|
|
||||||
is done by the select system.
|
|
||||||
|
|
||||||
db.Family.where(
|
|
||||||
lambda family: family.mother_handle.event_ref_list[0] != None
|
|
||||||
).select()
|
|
||||||
|
|
||||||
Indexing and use of None is allowed.
|
|
||||||
|
|
||||||
db.Person.where(
|
|
||||||
lambda person: person.private == True
|
|
||||||
).select()
|
|
||||||
|
|
||||||
One current limitiation is that it cannot detect a boolean value,
|
|
||||||
so we must use the "== True" to make sure the proper code is
|
|
||||||
generated.
|
|
||||||
|
|
||||||
The following method names are dictated by meta's Visitor. Additional
|
|
||||||
methods can be added if an error is received such as:
|
|
||||||
|
|
||||||
AttributeError: visitXXX does not exist
|
|
||||||
|
|
||||||
The method must be added, return the proper value for that
|
|
||||||
syntax. May require recursive calls to process_ITEM().
|
|
||||||
|
|
||||||
Please see meta for more information:
|
|
||||||
http://srossross.github.io/Meta/html/index.html
|
|
||||||
"""
|
|
||||||
|
|
||||||
def visitName(self, node):
|
|
||||||
return node.id
|
|
||||||
|
|
||||||
def visitNum(self, node):
|
|
||||||
return node.n
|
|
||||||
|
|
||||||
def visitlong(self, node):
|
|
||||||
return node
|
|
||||||
|
|
||||||
def process_expression(self, expr):
|
|
||||||
if isinstance(expr, str):
|
|
||||||
# boolean
|
|
||||||
return [self.process_field(expr), "==", True]
|
|
||||||
elif len(expr) == 3:
|
|
||||||
# (field, op, value)
|
|
||||||
return [self.process_field(expr[0]),
|
|
||||||
expr[1],
|
|
||||||
self.process_value(expr[2])]
|
|
||||||
else:
|
|
||||||
# list of exprs
|
|
||||||
return [self.process_expression(exp) for
|
|
||||||
exp in expr]
|
|
||||||
|
|
||||||
def process_value(self, value):
|
|
||||||
try:
|
|
||||||
return eval(value, self.env)
|
|
||||||
except:
|
|
||||||
return value
|
|
||||||
|
|
||||||
def process_field(self, field):
|
|
||||||
field = field.replace("[", ".").replace("]", "")
|
|
||||||
if field.startswith(self.parameter + "."):
|
|
||||||
return field[len(self.parameter) + 1:]
|
|
||||||
else:
|
|
||||||
return field
|
|
||||||
|
|
||||||
def visitCall(self, node):
|
|
||||||
"""
|
|
||||||
Handle LIKE()
|
|
||||||
"""
|
|
||||||
return [self.process_field(self.visit(node.args[0])),
|
|
||||||
self.visit(node.func),
|
|
||||||
self.process_value(self.visit(node.args[1]))]
|
|
||||||
|
|
||||||
def visitStr(self, node):
|
|
||||||
return node.s
|
|
||||||
|
|
||||||
def visitlist(self, list):
|
|
||||||
return [self.visit(node) for node in list]
|
|
||||||
|
|
||||||
def visitCompare(self, node):
|
|
||||||
return [self.process_field(self.visit(node.left)),
|
|
||||||
" ".join(self.visit(node.ops)),
|
|
||||||
self.process_value(self.visit(node.comparators[0]))]
|
|
||||||
|
|
||||||
def visitAttribute(self, node):
|
|
||||||
return "%s.%s" % (self.visit(node.value), node.attr)
|
|
||||||
|
|
||||||
def get_boolean_op(self, node):
|
|
||||||
if isinstance(node, _ast.And):
|
|
||||||
return "AND"
|
|
||||||
elif isinstance(node, _ast.Or):
|
|
||||||
return "OR"
|
|
||||||
else:
|
|
||||||
raise Exception("invalid boolean")
|
|
||||||
|
|
||||||
def visitNotEq(self, node):
|
|
||||||
return "!="
|
|
||||||
|
|
||||||
def visitLtE(self, node):
|
|
||||||
return "<="
|
|
||||||
|
|
||||||
def visitGtE(self, node):
|
|
||||||
return ">="
|
|
||||||
|
|
||||||
def visitEq(self, node):
|
|
||||||
return "=="
|
|
||||||
|
|
||||||
def visitBoolOp(self, node):
|
|
||||||
"""
|
|
||||||
BoolOp: boolean operator
|
|
||||||
"""
|
|
||||||
op = self.get_boolean_op(node.op)
|
|
||||||
values = list(node.values)
|
|
||||||
return [op, self.process_expression(
|
|
||||||
[self.visit(value) for value in values])]
|
|
||||||
|
|
||||||
def visitLambda(self, node):
|
|
||||||
self.parameter = self.visit(node.args)[0]
|
|
||||||
return self.visit(node.body)
|
|
||||||
|
|
||||||
def visitFunctionDef(self, node):
|
|
||||||
self.parameter = self.visit(node.args)[2] # ['self', 'db', 'person']
|
|
||||||
return self.visit(node.body)[0]
|
|
||||||
|
|
||||||
def visitReturn(self, node):
|
|
||||||
return self.visit(node.value)
|
|
||||||
|
|
||||||
def visitarguments(self, node):
|
|
||||||
return [self.visit(arg) for arg in node.args]
|
|
||||||
|
|
||||||
def visitarg(self, node):
|
|
||||||
return node.arg
|
|
||||||
|
|
||||||
def visitSubscript(self, node):
|
|
||||||
return "%s[%s]" % (self.visit(node.value),
|
|
||||||
self.visit(node.slice))
|
|
||||||
|
|
||||||
def visitIndex(self, node):
|
|
||||||
return self.visit(node.value)
|
|
||||||
|
|
||||||
def make_env(closure):
|
|
||||||
"""
|
|
||||||
Create an environment from the closure.
|
|
||||||
"""
|
|
||||||
env = copy.copy(closure.__globals__)
|
|
||||||
if closure.__closure__:
|
|
||||||
for i in range(len(closure.__closure__)):
|
|
||||||
env[closure.__code__.co_freevars[i]] = closure.__closure__[i].cell_contents
|
|
||||||
return env
|
|
||||||
|
|
||||||
def eval_where(closure):
|
|
||||||
"""
|
|
||||||
Given a closure, parse and evaluate it.
|
|
||||||
Return a WHERE expression.
|
|
||||||
|
|
||||||
See ParseFilter.__doc__ for more information and examples.
|
|
||||||
"""
|
|
||||||
parser = ParseFilter()
|
|
||||||
parser.env = make_env(closure)
|
|
||||||
ast_top = decompile_func(closure)
|
|
||||||
result = parser.visit(ast_top)
|
|
||||||
return result
|
|
@ -1,359 +0,0 @@
|
|||||||
#
|
|
||||||
# Gramps - a GTK+/GNOME based genealogy program
|
|
||||||
#
|
|
||||||
# Copyright (C) 2015 Gramps Development Team
|
|
||||||
#
|
|
||||||
# This program is free software; you can redistribute it and/or modify
|
|
||||||
# it under the terms of the GNU General Public License as published by
|
|
||||||
# the Free Software Foundation; either version 2 of the License, or
|
|
||||||
# (at your option) any later version.
|
|
||||||
#
|
|
||||||
# This program is distributed in the hope that it will be useful,
|
|
||||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
||||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
||||||
# GNU General Public License for more details.
|
|
||||||
#
|
|
||||||
# You should have received a copy of the GNU General Public License
|
|
||||||
# along with this program; if not, write to the Free Software
|
|
||||||
# Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
|
|
||||||
#
|
|
||||||
|
|
||||||
from gramps.gen.lib.handle import HandleClass
|
|
||||||
|
|
||||||
def from_struct(struct):
|
|
||||||
return Struct.instance_from_struct(struct)
|
|
||||||
|
|
||||||
class Struct:
|
|
||||||
"""
|
|
||||||
Class for getting and setting parts of a struct by dotted path.
|
|
||||||
|
|
||||||
>>> s = Struct({"gramps_id": "I0001", ...}, database)
|
|
||||||
>>> s.primary_name.surname_list[0].surname
|
|
||||||
Jones
|
|
||||||
>>> s.primary_name.surname_list[0].surname = "Smith"
|
|
||||||
>>> s.primary_name.surname_list[0]surname
|
|
||||||
Smith
|
|
||||||
"""
|
|
||||||
def __init__(self, struct, db=None):
|
|
||||||
self.struct = struct
|
|
||||||
self.db = db
|
|
||||||
if self.db:
|
|
||||||
self.transaction = db.get_transaction_class()
|
|
||||||
else:
|
|
||||||
self.transaction = None
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def wrap(cls, instance, db=None):
|
|
||||||
return Struct(instance.to_struct(), db)
|
|
||||||
|
|
||||||
def __setitem__(self, item, value):
|
|
||||||
self.struct[item] = value
|
|
||||||
|
|
||||||
def __eq__(self, other):
|
|
||||||
if isinstance(other, Struct):
|
|
||||||
return self.struct == other.struct
|
|
||||||
elif isinstance(self.struct, list):
|
|
||||||
## FIXME: self.struct can be a dict, list, etc
|
|
||||||
for item in self.struct:
|
|
||||||
if item == other:
|
|
||||||
return True
|
|
||||||
return False
|
|
||||||
else:
|
|
||||||
return self.struct == other
|
|
||||||
|
|
||||||
def __lt__(self, other):
|
|
||||||
if isinstance(other, Struct):
|
|
||||||
return self.struct < other.struct
|
|
||||||
else:
|
|
||||||
return self.struct < other
|
|
||||||
|
|
||||||
def __gt__(self, other):
|
|
||||||
if isinstance(other, Struct):
|
|
||||||
return self.struct > other.struct
|
|
||||||
else:
|
|
||||||
return self.struct > other
|
|
||||||
|
|
||||||
def __le__(self, other):
|
|
||||||
if isinstance(other, Struct):
|
|
||||||
return self.struct <= other.struct
|
|
||||||
else:
|
|
||||||
return self.struct <= other
|
|
||||||
|
|
||||||
def __ge__(self, other):
|
|
||||||
if isinstance(other, Struct):
|
|
||||||
return self.struct >= other.struct
|
|
||||||
else:
|
|
||||||
return self.struct >= other
|
|
||||||
|
|
||||||
def __ne__(self, other):
|
|
||||||
if isinstance(other, Struct):
|
|
||||||
return self.struct != other.struct
|
|
||||||
else:
|
|
||||||
return self.struct != other
|
|
||||||
|
|
||||||
def __len__(self):
|
|
||||||
return len(self.struct)
|
|
||||||
|
|
||||||
def __contains__(self, item):
|
|
||||||
return item in self.struct
|
|
||||||
|
|
||||||
def __call__(self, *args, **kwargs):
|
|
||||||
"""
|
|
||||||
You can use this to select and filter a list of structs.
|
|
||||||
|
|
||||||
args are dotted strings of what componets of the structs to
|
|
||||||
select, and kwargs is the selection criteria, double-under
|
|
||||||
scores represent dots.
|
|
||||||
|
|
||||||
If no args are given, all are provided.
|
|
||||||
"""
|
|
||||||
selected = self.struct # better be dicts
|
|
||||||
# First, find elements of the list that match any given
|
|
||||||
# selection criteria:
|
|
||||||
selected = self.struct # assume dicts
|
|
||||||
# assume True
|
|
||||||
to_delete = []
|
|
||||||
for key in kwargs: # value="Social Security Number"
|
|
||||||
parts = self.getitem_from_path(key.split("__")) # returns all
|
|
||||||
# This will return a list; we keep the ones that match
|
|
||||||
for p in range(len(parts)):
|
|
||||||
# if it matches, keep it:
|
|
||||||
if parts[p] != kwargs[key]:
|
|
||||||
to_delete.append(p)
|
|
||||||
# delete from highest to lowest, to use pop:
|
|
||||||
for p in reversed(to_delete):
|
|
||||||
selected.pop(p)
|
|
||||||
# now select which parts to show:
|
|
||||||
if args: # just some of the parts, ["type.string", ...]
|
|
||||||
results = []
|
|
||||||
for select in selected: # dict in dicts
|
|
||||||
parts = []
|
|
||||||
for item in args: # ["type.string"]
|
|
||||||
items = item.split(".") # "type.string"
|
|
||||||
values = Struct(select, self.db).getitem_from_path(items)
|
|
||||||
if values:
|
|
||||||
parts.append((item, values))
|
|
||||||
results.append(parts) # return [["type.string", "Social Security Number"], ...]
|
|
||||||
else: # return all
|
|
||||||
results = selected
|
|
||||||
# return them
|
|
||||||
return results
|
|
||||||
|
|
||||||
def select(self, thing1, thing2):
|
|
||||||
if thing2 == "*":
|
|
||||||
return thing1
|
|
||||||
elif thing2 in thing1:
|
|
||||||
return thing2
|
|
||||||
elif thing1 == thing2:
|
|
||||||
return thing1
|
|
||||||
else:
|
|
||||||
return None
|
|
||||||
|
|
||||||
def __getattr__(self, attr):
|
|
||||||
"""
|
|
||||||
Called when getattr fails. Lookup attr in struct; returns Struct
|
|
||||||
if more struct.
|
|
||||||
|
|
||||||
>>> Struct({}, db).primary_name
|
|
||||||
returns: Struct([], db) or value
|
|
||||||
|
|
||||||
struct can be list/tuple, dict with _class, or value (including dict).
|
|
||||||
|
|
||||||
self.setitem_from_path(path, v) should be used to set value of
|
|
||||||
item.
|
|
||||||
"""
|
|
||||||
if isinstance(self.struct, dict) and "_class" in self.struct.keys():
|
|
||||||
# this is representing an object
|
|
||||||
if attr in self.struct.keys():
|
|
||||||
return self.handle_join(self.struct[attr])
|
|
||||||
else:
|
|
||||||
raise AttributeError("attempt to access a property of an object: '%s', '%s'" % (self.struct, attr))
|
|
||||||
elif isinstance(self.struct, HandleClass):
|
|
||||||
struct = self.handle_join(self.struct)
|
|
||||||
return getattr(struct, attr)
|
|
||||||
elif isinstance(self.struct, (list, tuple)):
|
|
||||||
# get first item in list that matches:
|
|
||||||
sublist = [getattr(Struct(item, self.db), attr) for item in self.struct]
|
|
||||||
return Struct(sublist, self.db)
|
|
||||||
elif hasattr(self.struct, attr):
|
|
||||||
# better be a property of the list/tuple/dict/value:
|
|
||||||
return getattr(self.struct, attr)
|
|
||||||
else:
|
|
||||||
return Struct({}, self.db) # dummy, extending a previous dummy
|
|
||||||
|
|
||||||
def __getitem__(self, item):
|
|
||||||
"""
|
|
||||||
Called when getitem fails. Lookup item in struct; returns Struct
|
|
||||||
if more struct.
|
|
||||||
|
|
||||||
>>> Struct({}, db)[12]
|
|
||||||
returns: Struct([], db) or value
|
|
||||||
|
|
||||||
struct can be list/tuple, dict with _class, or value (including dict).
|
|
||||||
"""
|
|
||||||
if isinstance(item, str) and isinstance(self.struct, (list, tuple)):
|
|
||||||
fields = [field.strip() for field in item.split(",")]
|
|
||||||
results = []
|
|
||||||
for item in self.struct:
|
|
||||||
sublist = [getattr(Struct(item, self.db), field) for field in fields]
|
|
||||||
if any(sublist):
|
|
||||||
results.append(tuple(sublist))
|
|
||||||
return results if results else None
|
|
||||||
else:
|
|
||||||
return self.handle_join(self.struct[item])
|
|
||||||
|
|
||||||
def getitem_from_path(self, items):
|
|
||||||
"""
|
|
||||||
path is a list
|
|
||||||
"""
|
|
||||||
current = self
|
|
||||||
for item in items:
|
|
||||||
current = getattr(current, item)
|
|
||||||
return current
|
|
||||||
|
|
||||||
def handle_join(self, item):
|
|
||||||
"""
|
|
||||||
If the item is a handle, look up reference object.
|
|
||||||
"""
|
|
||||||
if isinstance(item, HandleClass) and self.db:
|
|
||||||
obj = self.db.get_from_name_and_handle(item.classname, str(item))
|
|
||||||
if obj:
|
|
||||||
return Struct(obj.to_struct(), self.db)
|
|
||||||
else:
|
|
||||||
return Struct({}, self.db) # dummy, a db error
|
|
||||||
elif isinstance(item, (list, tuple)):
|
|
||||||
return Struct(item, self.db)
|
|
||||||
elif isinstance(item, dict) and "_class" in item.keys():
|
|
||||||
return Struct(item, self.db)
|
|
||||||
else:
|
|
||||||
return item
|
|
||||||
|
|
||||||
def setitem(self, path, value, trans=None):
|
|
||||||
"""
|
|
||||||
Given a path to a struct part, set the last part to value.
|
|
||||||
|
|
||||||
>>> Struct(struct).setitem("primary_name.surname_list.0.surname", "Smith")
|
|
||||||
"""
|
|
||||||
return self.setitem_from_path(parse(path), value, trans)
|
|
||||||
|
|
||||||
def primary_object_q(self, _class):
|
|
||||||
return _class in ["Person", "Family", "Event", "Source", "Citation",
|
|
||||||
"Tag", "Repository", "Note", "Media"]
|
|
||||||
|
|
||||||
def setitem_from_path(self, path, value, trans=None):
|
|
||||||
"""
|
|
||||||
Given a path to a struct part, set the last part to value.
|
|
||||||
|
|
||||||
>>> Struct(struct).setitem_from_path(["primary_name", "surname_list", "[0]", "surname"], "Smith", transaction)
|
|
||||||
"""
|
|
||||||
path, item = path[:-1], path[-1]
|
|
||||||
if item.startswith("["):
|
|
||||||
item = item[1:-1]
|
|
||||||
struct = self.struct
|
|
||||||
primary_obj = struct
|
|
||||||
for p in range(len(path)):
|
|
||||||
part = path[p]
|
|
||||||
if part.startswith("["): # getitem
|
|
||||||
struct = struct[eval(part[1:-1])] # for int or string use
|
|
||||||
else: # getattr
|
|
||||||
struct = struct[part]
|
|
||||||
if struct is None: # invalid part to set, skip
|
|
||||||
return
|
|
||||||
if isinstance(struct, HandleClass):
|
|
||||||
obj = self.db.get_from_name_and_handle(struct.classname, str(struct))
|
|
||||||
struct = obj.to_struct()
|
|
||||||
# keep track of primary object for update, below
|
|
||||||
if isinstance(struct, dict) and "_class" in struct and self.primary_object_q(struct["_class"]):
|
|
||||||
primary_obj = struct
|
|
||||||
# struct is now set
|
|
||||||
if item in struct and isinstance(struct[item], list): # assigning to a list
|
|
||||||
if value is not None:
|
|
||||||
struct[item].append(value) # we append the value
|
|
||||||
else:
|
|
||||||
struct[item] = []
|
|
||||||
elif isinstance(struct, (list, tuple)):
|
|
||||||
pos = int(item)
|
|
||||||
if pos < len(struct):
|
|
||||||
if value is not None:
|
|
||||||
struct[int(item)] = value
|
|
||||||
else:
|
|
||||||
struct.pop(int(item))
|
|
||||||
elif isinstance(struct, dict):
|
|
||||||
if item in struct.keys():
|
|
||||||
struct[item] = value
|
|
||||||
elif hasattr(struct, item):
|
|
||||||
setattr(struct, item, value)
|
|
||||||
else:
|
|
||||||
return
|
|
||||||
self.update_db(primary_obj, trans)
|
|
||||||
|
|
||||||
def update_db(self, struct, trans=None):
|
|
||||||
if self.db:
|
|
||||||
if trans is None:
|
|
||||||
with self.transaction("Struct Update", self.db, batch=True) as trans:
|
|
||||||
new_obj = Struct.instance_from_struct(struct)
|
|
||||||
name, handle = struct["_class"], struct["handle"]
|
|
||||||
old_obj = self.db.get_from_name_and_handle(name, handle)
|
|
||||||
if old_obj:
|
|
||||||
commit_func = self.db.get_table_func(name,"commit_func")
|
|
||||||
commit_func(new_obj, trans)
|
|
||||||
else:
|
|
||||||
add_func = self.db.get_table_func(name,"add_func")
|
|
||||||
add_func(new_obj, trans)
|
|
||||||
else:
|
|
||||||
new_obj = Struct.instance_from_struct(struct)
|
|
||||||
name, handle = struct["_class"], struct["handle"]
|
|
||||||
old_obj = self.db.get_from_name_and_handle(name, handle)
|
|
||||||
if old_obj:
|
|
||||||
commit_func = self.db.get_table_func(name,"commit_func")
|
|
||||||
commit_func(new_obj, trans)
|
|
||||||
else:
|
|
||||||
add_func = self.db.get_table_func(name,"add_func")
|
|
||||||
add_func(new_obj, trans)
|
|
||||||
|
|
||||||
def from_struct(self):
|
|
||||||
return Struct.instance_from_struct(self.struct)
|
|
||||||
|
|
||||||
@classmethod
|
|
||||||
def instance_from_struct(cls, struct):
|
|
||||||
"""
|
|
||||||
Given a struct with metadata, create a Gramps object.
|
|
||||||
|
|
||||||
self is class when called as a classmethod.
|
|
||||||
"""
|
|
||||||
from gramps.gen.lib import (Person, Family, Event, Source, Place, Citation,
|
|
||||||
Repository, Media, Note, Tag, Date)
|
|
||||||
if isinstance(struct, dict):
|
|
||||||
if "_class" in struct.keys():
|
|
||||||
if struct["_class"] == "Person":
|
|
||||||
return Person.create(Person.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Family":
|
|
||||||
return Family.create(Family.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Event":
|
|
||||||
return Event.create(Event.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Source":
|
|
||||||
return Source.create(Source.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Place":
|
|
||||||
return Place.create(Place.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Citation":
|
|
||||||
return Citation.create(Citation.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Repository":
|
|
||||||
return Repository.create(Repository.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Media":
|
|
||||||
return Media.create(Media.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Note":
|
|
||||||
return Note.create(Note.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Tag":
|
|
||||||
return Tag.create(Tag.from_struct(struct))
|
|
||||||
elif struct["_class"] == "Date":
|
|
||||||
return Date().unserialize(Date.from_struct(struct, full=True))
|
|
||||||
raise AttributeError("invalid struct: %s" % struct)
|
|
||||||
|
|
||||||
def __str__(self):
|
|
||||||
return str(self.struct)
|
|
||||||
|
|
||||||
def __repr__(self):
|
|
||||||
if "_class" in self.struct:
|
|
||||||
return "<%s struct instance>" % self._class
|
|
||||||
else:
|
|
||||||
return repr(self.struct)
|
|
@ -25,7 +25,6 @@ import os
|
|||||||
|
|
||||||
from .. import (Person, Family, Event, Source, Place, Citation,
|
from .. import (Person, Family, Event, Source, Place, Citation,
|
||||||
Repository, Media, Note, Tag)
|
Repository, Media, Note, Tag)
|
||||||
from gramps.gen.lib.struct import Struct
|
|
||||||
from gramps.gen.merge.diff import import_as_dict
|
from gramps.gen.merge.diff import import_as_dict
|
||||||
from gramps.cli.user import User
|
from gramps.cli.user import User
|
||||||
from gramps.gen.merge.diff import *
|
from gramps.gen.merge.diff import *
|
||||||
@ -119,18 +118,5 @@ for table in db.get_table_func():
|
|||||||
obj = db.get_table_func(table,"handle_func")(handle)
|
obj = db.get_table_func(table,"handle_func")(handle)
|
||||||
generate_case(obj)
|
generate_case(obj)
|
||||||
|
|
||||||
class StructTest(unittest.TestCase):
|
|
||||||
def test(self):
|
|
||||||
family = db.get_family_from_gramps_id("F0001")
|
|
||||||
s = Struct(family.to_struct(), db)
|
|
||||||
self.assertEqual(s["gramps_id"], "F0001")
|
|
||||||
s["gramps_id"] = "TEST"
|
|
||||||
self.assertEqual(s["gramps_id"], "TEST")
|
|
||||||
self.assertEqual(s.father_handle.primary_name.first_name,
|
|
||||||
"Allen Carl")
|
|
||||||
s["father_handle.primary_name.first_name"] = "Edward"
|
|
||||||
self.assertEqual(s["father_handle.primary_name.first_name"],
|
|
||||||
"Edward")
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
unittest.main()
|
unittest.main()
|
||||||
|
@ -2080,63 +2080,6 @@ class DBAPI(DbGeneric):
|
|||||||
else:
|
else:
|
||||||
return repr(value)
|
return repr(value)
|
||||||
|
|
||||||
def _build_where_clause_recursive(self, table, where):
|
|
||||||
"""
|
|
||||||
where - (field, op, value)
|
|
||||||
- ["NOT", where]
|
|
||||||
- ["AND", (where, ...)]
|
|
||||||
- ["OR", (where, ...)]
|
|
||||||
"""
|
|
||||||
if where is None:
|
|
||||||
return ""
|
|
||||||
elif len(where) == 3:
|
|
||||||
field, db_op, value = where
|
|
||||||
return "(%s %s %s)" % (self._hash_name(table, field),
|
|
||||||
db_op, self._sql_repr(value))
|
|
||||||
elif where[0] in ["AND", "OR"]:
|
|
||||||
parts = [self._build_where_clause_recursive(table, part)
|
|
||||||
for part in where[1]]
|
|
||||||
return "(%s)" % ((" %s " % where[0]).join(parts))
|
|
||||||
else:
|
|
||||||
return "(NOT %s)" % self._build_where_clause_recursive(table,
|
|
||||||
where[1])
|
|
||||||
|
|
||||||
def _build_where_clause(self, table, where):
|
|
||||||
"""
|
|
||||||
where - a list in where format
|
|
||||||
return - "WHERE conditions..."
|
|
||||||
"""
|
|
||||||
parts = self._build_where_clause_recursive(table, where)
|
|
||||||
if parts:
|
|
||||||
return "WHERE " + parts
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _build_order_clause(self, table, order_by):
|
|
||||||
"""
|
|
||||||
order_by - [(field, "ASC" | "DESC"), ...]
|
|
||||||
"""
|
|
||||||
if order_by:
|
|
||||||
order_clause = ", ".join(["%s %s" % (self._hash_name(table, field),
|
|
||||||
dir)
|
|
||||||
for (field, dir) in order_by])
|
|
||||||
return "ORDER BY " + order_clause
|
|
||||||
else:
|
|
||||||
return ""
|
|
||||||
|
|
||||||
def _build_select_fields(self, table, select_fields, secondary_fields):
|
|
||||||
"""
|
|
||||||
fields - [field, ...]
|
|
||||||
return: "field, field, field"
|
|
||||||
"""
|
|
||||||
all_available = all([(field in secondary_fields)
|
|
||||||
for field in select_fields])
|
|
||||||
if all_available: # we can get them without expanding
|
|
||||||
return select_fields
|
|
||||||
else:
|
|
||||||
# nope, we'll have to expand blob to get all fields
|
|
||||||
return ["blob_data"]
|
|
||||||
|
|
||||||
def _check_order_by_fields(self, table, order_by, secondary_fields):
|
def _check_order_by_fields(self, table, order_by, secondary_fields):
|
||||||
"""
|
"""
|
||||||
Check to make sure all order_by fields are defined. If not, then
|
Check to make sure all order_by fields are defined. If not, then
|
||||||
@ -2150,128 +2093,6 @@ class DBAPI(DbGeneric):
|
|||||||
return False
|
return False
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def _check_where_fields(self, table, where, secondary_fields):
|
|
||||||
"""
|
|
||||||
Check to make sure all where fields are defined. If not, then
|
|
||||||
we need to do the Python-based select.
|
|
||||||
|
|
||||||
secondary_fields are hashed.
|
|
||||||
"""
|
|
||||||
if where is None:
|
|
||||||
return True
|
|
||||||
elif len(where) == 2: # ["AND" [...]] | ["OR" [...]] | ["NOT" expr]
|
|
||||||
connector, exprs = where
|
|
||||||
if connector in ["AND", "OR"]:
|
|
||||||
for expr in exprs:
|
|
||||||
value = self._check_where_fields(table, expr,
|
|
||||||
secondary_fields)
|
|
||||||
if value == False:
|
|
||||||
return False
|
|
||||||
return True
|
|
||||||
else: # "NOT"
|
|
||||||
return self._check_where_fields(table, exprs, secondary_fields)
|
|
||||||
elif len(where) == 3: # (name, db_op, value)
|
|
||||||
(name, db_op, value) = where
|
|
||||||
# just the ones we need for where
|
|
||||||
return self._hash_name(table, name) in secondary_fields
|
|
||||||
|
|
||||||
def _select(self, table, fields=None, start=0, limit=-1,
|
|
||||||
where=None, order_by=None):
|
|
||||||
"""
|
|
||||||
Default implementation of a select for those databases
|
|
||||||
that don't support SQL. Returns a list of dicts, total,
|
|
||||||
and time.
|
|
||||||
|
|
||||||
table - Person, Family, etc.
|
|
||||||
fields - used by object.get_field()
|
|
||||||
start - position to start
|
|
||||||
limit - count to get; -1 for all
|
|
||||||
where - (field, SQL string_operator, value) |
|
|
||||||
["AND", [where, where, ...]] |
|
|
||||||
["OR", [where, where, ...]] |
|
|
||||||
["NOT", where]
|
|
||||||
order_by - [[fieldname, "ASC" | "DESC"], ...]
|
|
||||||
"""
|
|
||||||
secondary_fields = ([self._hash_name(table, field)
|
|
||||||
for (field, ptype)
|
|
||||||
in self.get_table_func(
|
|
||||||
table, "class_func").get_secondary_fields()]
|
|
||||||
+ ["handle"])
|
|
||||||
# handle is a sql field, but not listed in secondaries
|
|
||||||
# If no fields, then we need objects:
|
|
||||||
# Check to see if where matches SQL fields:
|
|
||||||
table_name = table.lower()
|
|
||||||
if ((not self._check_where_fields(table, where, secondary_fields))
|
|
||||||
or (not self._check_order_by_fields(table, order_by,
|
|
||||||
secondary_fields))):
|
|
||||||
# If not, then need to do select via Python:
|
|
||||||
generator = super()._select(table, fields, start,
|
|
||||||
limit, where, order_by)
|
|
||||||
for item in generator:
|
|
||||||
yield item
|
|
||||||
return
|
|
||||||
# Otherwise, we are SQL
|
|
||||||
if fields is None:
|
|
||||||
fields = ["blob_data"]
|
|
||||||
get_count_only = False
|
|
||||||
if fields[0] == "count(1)":
|
|
||||||
hashed_fields = ["count(1)"]
|
|
||||||
fields = ["count(1)"]
|
|
||||||
select_fields = ["count(1)"]
|
|
||||||
get_count_only = True
|
|
||||||
else:
|
|
||||||
hashed_fields = [self._hash_name(table, field) for field in fields]
|
|
||||||
fields = hashed_fields
|
|
||||||
select_fields = self._build_select_fields(table, fields,
|
|
||||||
secondary_fields)
|
|
||||||
where_clause = self._build_where_clause(table, where)
|
|
||||||
order_clause = self._build_order_clause(table, order_by)
|
|
||||||
if get_count_only:
|
|
||||||
select_fields = ["1"]
|
|
||||||
if start:
|
|
||||||
query = "SELECT %s FROM %s %s %s LIMIT %s, %s " % (
|
|
||||||
", ".join(select_fields),
|
|
||||||
table_name, where_clause, order_clause, start, limit
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
query = "SELECT %s FROM %s %s %s LIMIT %s" % (
|
|
||||||
", ".join(select_fields),
|
|
||||||
table_name, where_clause, order_clause, limit
|
|
||||||
)
|
|
||||||
if get_count_only:
|
|
||||||
self.dbapi.execute("SELECT count(1) from (%s) AS temp_select;"
|
|
||||||
% query)
|
|
||||||
rows = self.dbapi.fetchall()
|
|
||||||
yield rows[0][0]
|
|
||||||
return
|
|
||||||
self.dbapi.execute(query)
|
|
||||||
rows = self.dbapi.fetchall()
|
|
||||||
for row in rows:
|
|
||||||
if fields[0] != "blob_data":
|
|
||||||
obj = None # don't build it if you don't need it
|
|
||||||
data = {}
|
|
||||||
for field in fields:
|
|
||||||
if field in select_fields:
|
|
||||||
data[field.replace("__", ".")
|
|
||||||
] = row[select_fields.index(field)]
|
|
||||||
else:
|
|
||||||
if obj is None: # we need it! create it and cache it:
|
|
||||||
obj = self.get_table_func(table,
|
|
||||||
"class_func").create(
|
|
||||||
pickle.loads(row[0]))
|
|
||||||
# get the field, even if we need to do a join:
|
|
||||||
# FIXME: possible optimize:
|
|
||||||
# do a join in select for this if needed:
|
|
||||||
field = field.replace("__", ".")
|
|
||||||
data[field] = obj.get_field(field, self,
|
|
||||||
ignore_errors=True)
|
|
||||||
yield data
|
|
||||||
else:
|
|
||||||
obj = self.get_table_func(table,
|
|
||||||
"class_func").create(
|
|
||||||
pickle.loads(row[0]))
|
|
||||||
yield obj
|
|
||||||
|
|
||||||
def get_summary(self):
|
def get_summary(self):
|
||||||
"""
|
"""
|
||||||
Returns dictionary of summary item.
|
Returns dictionary of summary item.
|
||||||
|
Loading…
Reference in New Issue
Block a user