293 lines
11 KiB
Python
293 lines
11 KiB
Python
import core.builtin_helpers
|
|
from cache.Cache import Cache
|
|
from cache.SetCache import SetCache
|
|
from core.ast.nodes import python_to_concept
|
|
from core.builtin_concepts import BuiltinConcepts
|
|
from core.concept import Concept, ConceptParts, ensure_concept, DEFINITION_TYPE_BNF
|
|
from core.sheerka.services.SheerkaModifyConcept import SheerkaModifyConcept
|
|
from core.sheerka.services.sheerka_service import BaseService
|
|
|
|
GROUP_PREFIX = 'All_'
|
|
|
|
|
|
class SheerkaSetsManager(BaseService):
|
|
NAME = "SetsManager"
|
|
CONCEPTS_GROUPS_ENTRY = "SetsManager:Concepts_Groups"
|
|
CONCEPTS_IN_GROUPS_ENTRY = "SetsManager:Concepts_In_Groups" # cache for get_set_elements()
|
|
|
|
def __init__(self, sheerka):
|
|
super().__init__(sheerka)
|
|
self.sets = SetCache(default=lambda k: self.sheerka.sdp.get(self.CONCEPTS_GROUPS_ENTRY, k))
|
|
self.concepts_in_set = Cache()
|
|
|
|
def initialize(self):
|
|
self.sheerka.bind_service_method(self.set_isa, True)
|
|
self.sheerka.bind_service_method(self.get_set_elements, True) # concepts are evaluated
|
|
self.sheerka.bind_service_method(self.add_concept_to_set, True)
|
|
self.sheerka.bind_service_method(self.isinset, False)
|
|
self.sheerka.bind_service_method(self.isa, False)
|
|
self.sheerka.bind_service_method(self.isaset, True) # concept is evaluated, need to change the code
|
|
|
|
self.sheerka.cache_manager.register_cache(self.CONCEPTS_GROUPS_ENTRY, self.sets)
|
|
self.sheerka.cache_manager.register_cache(self.CONCEPTS_IN_GROUPS_ENTRY, self.concepts_in_set, persist=False)
|
|
|
|
def set_isa(self, context, concept, concept_set):
|
|
"""
|
|
Defines that concept a is b is another concept
|
|
:param context:
|
|
:param concept:
|
|
:param concept_set:
|
|
:return:
|
|
"""
|
|
|
|
context.log(f"Setting concept {concept} is a {concept_set}", who=self.NAME)
|
|
ensure_concept(concept, concept_set)
|
|
|
|
if BuiltinConcepts.ISA in concept.metadata.props and concept_set in concept.metadata.props[BuiltinConcepts.ISA]:
|
|
return self.sheerka.ret(
|
|
self.NAME,
|
|
False,
|
|
self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET, body=concept, concept_set=concept_set))
|
|
|
|
# KSI 20200709 add the concept, not its 'id' or 'key'
|
|
# It will allow conditions handling if concept set has its WHERE or PRE set to something
|
|
concept.add_prop(BuiltinConcepts.ISA, concept_set)
|
|
|
|
res = self.sheerka.modify_concept(context, concept)
|
|
if not res.status:
|
|
return res
|
|
|
|
res = self.add_concept_to_set(context, concept, concept_set)
|
|
|
|
return res
|
|
|
|
def add_concept_to_set(self, context, concept, concept_set):
|
|
"""
|
|
Add an entry in sdp to tell that concept isa concept_set
|
|
:param context:
|
|
:param concept:
|
|
:param concept_set:
|
|
:return:
|
|
"""
|
|
|
|
context.log(f"Adding concept {concept} to set {concept_set}", who=self.NAME)
|
|
ensure_concept(concept, concept_set)
|
|
|
|
set_elements = self.sets.get(concept_set.id)
|
|
if set_elements and concept.id in set_elements:
|
|
return self.sheerka.ret(
|
|
self.NAME,
|
|
False,
|
|
self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET, body=concept, concept_set=concept_set))
|
|
|
|
self.sets.put(concept_set.id, concept.id)
|
|
|
|
# invalidate the cache of what contains concept_set
|
|
self.concepts_in_set.delete(concept_set.id)
|
|
|
|
# update concept_set references
|
|
self.sheerka.services[SheerkaModifyConcept.NAME].update_references(context, concept_set)
|
|
|
|
return self.sheerka.ret(self.NAME, True, self.sheerka.new(BuiltinConcepts.SUCCESS))
|
|
|
|
def add_concepts_to_set(self, context, concepts, concept_set):
|
|
"""
|
|
Adding multiple concepts at the same time
|
|
******** THIS METHOD IS FOR TEST ONLY *************
|
|
As it is not optimized. It needs to be rewritten in case of production usage
|
|
"""
|
|
|
|
context.log(f"Adding concepts {concepts} to set {concept_set}", who=self.NAME)
|
|
ensure_concept(concept_set)
|
|
already_in_set = []
|
|
for concept in concepts:
|
|
res = self.add_concept_to_set(context, concept, concept_set)
|
|
if self.sheerka.isinstance(res.body, BuiltinConcepts.CONCEPT_ALREADY_IN_SET):
|
|
already_in_set.append(res.body.body)
|
|
|
|
if already_in_set:
|
|
body = self.sheerka.new(BuiltinConcepts.CONCEPT_ALREADY_IN_SET,
|
|
body=already_in_set,
|
|
concept_set=concept_set)
|
|
else:
|
|
body = self.sheerka.new(BuiltinConcepts.SUCCESS)
|
|
self.concepts_in_set.delete(concept_set.id)
|
|
|
|
return self.sheerka.ret(self.NAME, len(already_in_set) != len(concepts), body)
|
|
|
|
def get_set_elements(self, context, concept):
|
|
"""
|
|
Concept is supposed to be a set
|
|
Returns all elements if the set
|
|
:param context:
|
|
:param concept:
|
|
:return:
|
|
"""
|
|
|
|
ensure_concept(concept)
|
|
|
|
def _get_set_elements(sub_concept):
|
|
if not self.isaset(context, sub_concept):
|
|
return self.sheerka.new(BuiltinConcepts.NOT_A_SET, body=concept)
|
|
|
|
# first, try to see if sub_concept has it's own group entry
|
|
ids = self.sets.get(sub_concept.id)
|
|
concepts = self._get_concepts(context, ids, True)
|
|
|
|
# aggregate with en entries from its body
|
|
sub_concept = core.builtin_helpers.ensure_evaluated(context, sub_concept)
|
|
if not self.sheerka.is_success(sub_concept):
|
|
return sub_concept
|
|
|
|
if self.isaset(context, sub_concept.body):
|
|
other_concepts = _get_set_elements(sub_concept.body)
|
|
if not self.sheerka.is_success(other_concepts):
|
|
return other_concepts
|
|
concepts.extend(other_concepts)
|
|
|
|
# apply the where clause if any
|
|
if sub_concept.metadata.where:
|
|
new_condition = self._validate_where_clause(sub_concept)
|
|
if not new_condition:
|
|
return self.sheerka.new(BuiltinConcepts.CONDITION_FAILED, body=sub_concept)
|
|
|
|
# This methods sucks, but I don't have enough tools (like proper AST manipulation functions)
|
|
# to do it properly now. It will be enhanced later
|
|
globals_ = {"xx__concepts__xx": concepts, "sheerka": self.sheerka}
|
|
locals_ = {}
|
|
exec(new_condition, globals_, locals_)
|
|
concepts = locals_["result"]
|
|
|
|
return concepts
|
|
|
|
# already in cache ?
|
|
if res := self.concepts_in_set.get(concept.id):
|
|
return res
|
|
|
|
res = _get_set_elements(concept)
|
|
|
|
# put in cache
|
|
self.concepts_in_set.put(concept.id, res)
|
|
return res
|
|
|
|
def isinset(self, a, b):
|
|
"""
|
|
return true if the concept a is a b
|
|
Will handle when the keyword isa will be implemented
|
|
:param a:
|
|
:param b:
|
|
:return:
|
|
"""
|
|
|
|
ensure_concept(a, b)
|
|
|
|
# TODO, first check the 'isa' property of a
|
|
if not (a.id and b.id):
|
|
return False
|
|
|
|
group_elements = self.sets.get(b.id)
|
|
return group_elements and a.id in group_elements
|
|
|
|
def isa(self, a, b):
|
|
|
|
ensure_concept(a, b)
|
|
if BuiltinConcepts.ISA not in a.metadata.props:
|
|
return False
|
|
|
|
for c in a.metadata.props[BuiltinConcepts.ISA]:
|
|
if c == b:
|
|
return True
|
|
if self.isa(c, b):
|
|
return True
|
|
|
|
return False
|
|
|
|
def isaset(self, context, concept):
|
|
"""
|
|
True if exists All_<concept_id> in sdp or if concept references to a concept that has all_<concept_id>
|
|
:param context:
|
|
:param concept:
|
|
:return:
|
|
"""
|
|
""""""
|
|
|
|
if not (isinstance(concept, Concept) and concept.id):
|
|
return False
|
|
|
|
# KSI 20200629
|
|
# To resolve infinite recursion between group concepts and BNF concepts
|
|
if concept.metadata.definition_type == DEFINITION_TYPE_BNF:
|
|
return False
|
|
|
|
# check if it has a group
|
|
# TODO: use cache instead of directly requesting sdp
|
|
if self.sets.get(concept.id):
|
|
return True
|
|
|
|
# it may be a concept that references a set
|
|
concept = core.builtin_helpers.ensure_evaluated(context, concept)
|
|
if not context.sheerka.is_success(concept):
|
|
return False
|
|
|
|
return self.isaset(context, concept.body)
|
|
|
|
def _validate_where_clause(self, concept):
|
|
python_parser_result = [r for r in concept.compiled[ConceptParts.WHERE] if r.who == "parsers.Python"]
|
|
if not python_parser_result or not python_parser_result[0].status:
|
|
return None
|
|
|
|
ast_ = python_parser_result[0].body.body.ast_
|
|
ast_as_concepts = python_to_concept(ast_)
|
|
names = core.builtin_helpers.get_names(self.sheerka, ast_as_concepts)
|
|
if len(names) != 1 or names[0] != concept.metadata.body:
|
|
return None
|
|
|
|
condition = concept.metadata.where.replace(concept.metadata.body, "sheerka.objvalue(x)")
|
|
expression = f"""
|
|
result=[]
|
|
for x in xx__concepts__xx:
|
|
try:
|
|
if {condition}:
|
|
result.append(x)
|
|
except Exception:
|
|
pass
|
|
"""
|
|
return expression
|
|
|
|
def _get_concepts(self, context, ids, evaluate):
|
|
"""
|
|
Gets concepts from a list of concepts ids
|
|
:param ids:
|
|
:param evaluate: if True, all the elements are evaluated before returned
|
|
:return:
|
|
"""
|
|
|
|
if not ids:
|
|
return []
|
|
|
|
if not evaluate:
|
|
return [self.sheerka.get_by_id(element_id) for element_id in ids]
|
|
|
|
result = []
|
|
with context.push(BuiltinConcepts.EVALUATE_CONCEPT,
|
|
{"ids": ids},
|
|
desc=f"Evaluating concepts of a set") as sub_context:
|
|
sub_context.add_inputs(ids=ids)
|
|
sub_context.protected_hints.add(BuiltinConcepts.EVAL_BODY_REQUESTED)
|
|
errors = []
|
|
for element_id in ids:
|
|
concept = self.sheerka.get_by_id(element_id)
|
|
if len(concept.metadata.variables) == 0:
|
|
# The concepts are directly taken from Sheerka.get_by_id, so variable cannot be filled
|
|
# It's the reason why we only evaluate concept with no variable
|
|
evaluated = self.sheerka.evaluate_concept(sub_context, concept)
|
|
if context.sheerka.is_success(evaluated):
|
|
result.append(evaluated)
|
|
else:
|
|
errors.append(evaluated)
|
|
else:
|
|
result.append(concept)
|
|
sub_context.add_values(return_value=result)
|
|
sub_context.add_values(errors=errors)
|
|
return result
|