270 lines
7.9 KiB
Python
270 lines
7.9 KiB
Python
import hashlib
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from core.sheerka_logger import get_logger
|
|
|
|
import core.utils
|
|
from core.tokenizer import Tokenizer, TokenKind
|
|
|
|
PROPERTIES_FOR_DIGEST = ("name", "key",
|
|
"definition", "definition_type",
|
|
"is_builtin", "is_unique",
|
|
"where", "pre", "post", "body",
|
|
"desc")
|
|
PROPERTIES_TO_SERIALIZE = PROPERTIES_FOR_DIGEST + tuple(["id"])
|
|
PROPERTIES_FOR_NEW = ("where", "pre", "post", "body", "desc")
|
|
VARIABLE_PREFIX = "__var__"
|
|
|
|
|
|
class ConceptParts(Enum):
|
|
"""
|
|
Lists metadata that can contains some code
|
|
"""
|
|
WHERE = "where"
|
|
PRE = "pre"
|
|
POST = "post"
|
|
BODY = "body"
|
|
|
|
@staticmethod
|
|
def get_parts():
|
|
return set(item.value for item in ConceptParts)
|
|
|
|
|
|
@dataclass
|
|
class ConceptMetadata:
|
|
name: str
|
|
is_builtin: bool
|
|
is_unique: bool
|
|
key: str # name od the concept, where prop are replaced. to ease search
|
|
body: str # main method, can also be the value of the concept
|
|
where: str # condition to recognize variables in name
|
|
pre: str # list of pre conditions before calling the main function
|
|
post: str # list of post conditions after calling the main function
|
|
definition: str # regex used to define the concept
|
|
definition_type: str # definition can be done with something else than regex
|
|
desc: str # possible description for the concept
|
|
id: str # unique identifier for a concept. The id will never be modified (but the key can)
|
|
is_evaluated: bool = False # True is the concept is evaluated by sheerka.eval_concept()
|
|
|
|
|
|
class Concept:
|
|
"""
|
|
Default concept object
|
|
A concept is a the base object of our universe
|
|
Everything is a concept
|
|
"""
|
|
|
|
def __init__(self, name=None,
|
|
is_builtin=False,
|
|
is_unique=False,
|
|
key=None,
|
|
body=None,
|
|
where=None,
|
|
pre=None,
|
|
post=None,
|
|
definition=None,
|
|
definition_type=None,
|
|
desc=None,
|
|
id=None):
|
|
|
|
metadata = ConceptMetadata(
|
|
str(name) if name else None,
|
|
is_builtin,
|
|
is_unique,
|
|
str(key) if key else None,
|
|
body,
|
|
where,
|
|
pre,
|
|
post,
|
|
definition,
|
|
definition_type,
|
|
desc,
|
|
id
|
|
)
|
|
|
|
self.metadata = metadata
|
|
self.props = {} # list of Property for this concept
|
|
self.cached_asts = {} # cached ast for the where, pre, post and body parts
|
|
self.bnf = None
|
|
self.log = get_logger("core." + self.__class__.__name__)
|
|
self.init_log = get_logger("init.core." + self.__class__.__name__)
|
|
|
|
def __repr__(self):
|
|
return f"({self.metadata.id}){self.metadata.name}"
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, Concept):
|
|
return False
|
|
|
|
# check the attributes
|
|
for prop in PROPERTIES_TO_SERIALIZE:
|
|
if getattr(self.metadata, prop) != getattr(other.metadata, prop):
|
|
# print(prop) # use full to know which id does not match
|
|
return False
|
|
|
|
# check the props (Concept variables)
|
|
for var_name, p in self.props.items():
|
|
if p != other.props[var_name]:
|
|
return False
|
|
|
|
return True
|
|
|
|
def __hash__(self):
|
|
return hash(self.metadata.name)
|
|
|
|
@property
|
|
def name(self):
|
|
return self.metadata.name
|
|
|
|
@property
|
|
def id(self):
|
|
return self.metadata.id
|
|
|
|
@property
|
|
def key(self):
|
|
return self.metadata.key
|
|
|
|
def init_key(self, tokens=None):
|
|
"""
|
|
Create the key for this concept.
|
|
Must be called only when the concept if fully initialized
|
|
|
|
The method is not called set_key to make sure that no other class set the key by mistake
|
|
:param tokens:
|
|
:return:
|
|
"""
|
|
if self.metadata.key is not None:
|
|
return self
|
|
|
|
if tokens is None:
|
|
tokens = list(Tokenizer(self.metadata.name))
|
|
|
|
variables = list(self.props.keys()) if len(core.utils.strip_tokens(tokens, True)) > 1 else []
|
|
|
|
key = ""
|
|
first = True
|
|
for token in tokens:
|
|
if token.type == TokenKind.EOF:
|
|
break
|
|
if token.type == TokenKind.WHITESPACE:
|
|
continue
|
|
if not first:
|
|
key += " "
|
|
if variables is not None and token.value in variables:
|
|
key += VARIABLE_PREFIX + str(variables.index(token.value))
|
|
else:
|
|
key += token.value[1:-1] if token.type == TokenKind.STRING else token.value
|
|
first = False
|
|
|
|
self.metadata.key = key
|
|
return self
|
|
|
|
@property
|
|
def body(self):
|
|
return self.metadata.body
|
|
|
|
def add_codes(self, codes):
|
|
"""
|
|
Gets the ASTs for 'where', 'pre', 'post' and 'body'
|
|
There ASTs are know when the concept is freshly parsed.
|
|
So the values are kept in cache.
|
|
|
|
For concepts loaded from sdp, these ASTs must be created again
|
|
TODO : Seems to be a service method. Can be put somewhere else
|
|
:param codes:
|
|
:return:
|
|
"""
|
|
if codes is None:
|
|
return
|
|
|
|
for key in codes:
|
|
self.cached_asts[key] = codes[key]
|
|
|
|
return self
|
|
|
|
def get_digest(self):
|
|
"""
|
|
Returns the digest of the event
|
|
:return: hexa form of the sha256
|
|
"""
|
|
return hashlib.sha256(f"Concept:{self.to_dict(PROPERTIES_FOR_DIGEST)}".encode("utf-8")).hexdigest()
|
|
|
|
def to_dict(self, props_to_use=None):
|
|
"""
|
|
Returns a dict representing 'self'
|
|
:return:
|
|
"""
|
|
|
|
props_to_use = props_to_use or PROPERTIES_TO_SERIALIZE
|
|
|
|
props_as_dict = dict((prop, getattr(self.metadata, prop)) for prop in props_to_use)
|
|
props_as_dict["props"] = [(p, self.props[p].value) for p in self.props]
|
|
return props_as_dict
|
|
|
|
def from_dict(self, as_dict):
|
|
"""
|
|
Initializes 'self' from a dict
|
|
:param as_dict:
|
|
:return:
|
|
"""
|
|
for prop in PROPERTIES_TO_SERIALIZE:
|
|
if prop in as_dict:
|
|
setattr(self.metadata, prop, as_dict[prop])
|
|
if "props" in as_dict:
|
|
for n, v in as_dict["props"]:
|
|
self.set_prop(n, v)
|
|
return self
|
|
|
|
def update_from(self, other):
|
|
"""
|
|
Update self using the properties of another concept
|
|
This method is to mimic the class to instance pattern
|
|
'other' is the class, the template, and 'self' is a new instance
|
|
:param other:
|
|
:return:
|
|
"""
|
|
if other is None:
|
|
return self
|
|
|
|
self.from_dict(other.to_dict())
|
|
# for prop in self.props_to_serialize:
|
|
# setattr(self, prop, getattr(other, prop))
|
|
|
|
return self
|
|
|
|
def set_prop(self, prop_name: str, prop_value=None):
|
|
self.props[prop_name] = Property(prop_name, prop_value) # Python 3.x order is kept in dictionaries
|
|
return self
|
|
|
|
def set_prop_by_index(self, index: int, prop_value):
|
|
prop_name = list(self.props.keys())[index]
|
|
self.props[prop_name] = Property(prop_name, prop_value)
|
|
return self
|
|
|
|
def get_prop(self, prop_name: str):
|
|
return self.props[prop_name].value
|
|
|
|
|
|
class Property:
|
|
"""
|
|
Defines the variables of a concept
|
|
It as its specific class, because from experience,
|
|
property management is more complex than a key/value pair
|
|
"""
|
|
|
|
def __init__(self, name, value):
|
|
self.name = name
|
|
self.value = value
|
|
|
|
def __repr__(self):
|
|
return f"{self.name}={self.value}"
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, Property):
|
|
return False
|
|
|
|
return self.name == other.name and self.value == other.value
|
|
|
|
def __hash__(self):
|
|
return hash((self.name, self.value))
|