Skip to content

Commit

Permalink
Merge pull request #339 from singnet/senna-qe-338-1
Browse files Browse the repository at this point in the history
[#338] Change get_links() API to use link filters instead of optional parameters
  • Loading branch information
andre-senna authored Sep 11, 2024
2 parents 3f67e4a + 1b68c9f commit 2a21045
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 489 deletions.
2 changes: 1 addition & 1 deletion CHANGELOG
Original file line number Diff line number Diff line change
@@ -1 +1 @@

[#338] Change get_links() API to use link filters instead of optional parameters
79 changes: 4 additions & 75 deletions hyperon_das/cache/iterators.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from hyperon_das_atomdb import WILDCARD

import hyperon_das.link_filters as link_filters
from hyperon_das.query_engines.query_engine_protocol import QueryEngine
from hyperon_das.utils import Assignment, QueryAnswer

Expand Down Expand Up @@ -124,7 +125,9 @@ def __next__(self):
wildcard_flag = True
else:
target_handle.append(target["handle"])
das_query_answer = self.query_engine.get_links(self.link_type, None, target_handle)
das_query_answer = self.query_engine.get_links(
link_filters.Targets(target_handle, self.link_type)
)
lazy_query_answer = []
for answer in das_query_answer:
assignment = None
Expand Down Expand Up @@ -305,80 +308,6 @@ def get_fetch_data(self, **kwargs) -> tuple:
return self.backend.get_incoming_links(self.atom_handle, **kwargs)


class LocalGetLinks(BaseLinksIterator):
def __init__(self, source: ListIterator, **kwargs) -> None:
self.link_type = kwargs.get('link_type')
self.target_types = kwargs.get('target_types')
self.link_targets = kwargs.get('link_targets')
self.toplevel_only = kwargs.get('toplevel_only')
super().__init__(source, **kwargs)

def get_next_value(self) -> Any:
if not self.is_empty() and self.backend:
value = next(self.iterator)
self.current_value = self.backend._to_link_dict_list([value])[0]
return self.current_value

def get_current_value(self) -> Any:
if self.backend:
try:
value = self.source.get()
return self.backend._to_link_dict_list([value])[0]
except StopIteration:
return None

def get_fetch_data_kwargs(self) -> Dict[str, Any]:
return {
'cursor': self.cursor,
'chunk_size': self.chunk_size,
'toplevel_only': self.toplevel_only,
}

def get_fetch_data(self, **kwargs) -> tuple:
if self.backend:
return self.backend._get_related_links(
self.link_type, self.target_types, self.link_targets, **kwargs
)


class RemoteGetLinks(BaseLinksIterator):
def __init__(self, source: ListIterator, **kwargs) -> None:
self.link_type = kwargs.get('link_type')
self.target_types = kwargs.get('target_types')
self.link_targets = kwargs.get('link_targets')
self.toplevel_only = kwargs.get('toplevel_only')
self.returned_handles = set()
super().__init__(source, **kwargs)

def get_next_value(self) -> Any:
if not self.is_empty():
value = next(self.iterator)
handle = value.get('handle')
if handle not in self.returned_handles:
self.returned_handles.add(handle)
self.current_value = value
return self.current_value

def get_current_value(self) -> Any:
try:
return self.source.get()
except StopIteration:
return None

def get_fetch_data_kwargs(self) -> Dict[str, Any]:
return {
'cursor': self.cursor,
'chunk_size': self.chunk_size,
'toplevel_only': self.toplevel_only,
}

def get_fetch_data(self, **kwargs) -> tuple:
if self.backend:
return self.backend.get_links(
self.link_type, self.target_types, self.link_targets, **kwargs
)


class CustomQuery(BaseLinksIterator):
def __init__(self, source: ListIterator, **kwargs) -> None:
self.index_id = kwargs.pop('index_id', None)
Expand Down
101 changes: 21 additions & 80 deletions hyperon_das/das.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

from hyperon_das_atomdb import AtomDB, AtomDoesNotExist
from hyperon_das_atomdb.adapters import InMemoryDB, RedisMongoDB
from hyperon_das_atomdb.database import IncomingLinksT
from hyperon_das_atomdb.database import AtomT, IncomingLinksT, LinkT, NodeT
from hyperon_das_atomdb.exceptions import InvalidAtomDB

from hyperon_das.cache.cache_controller import CacheController
Expand All @@ -13,6 +13,7 @@
InvalidDASParameters,
InvalidQueryEngine,
)
from hyperon_das.link_filters import LinkFilter
from hyperon_das.logger import logger
from hyperon_das.query_engines.local_query_engine import LocalQueryEngine
from hyperon_das.query_engines.remote_query_engine import RemoteQueryEngine
Expand Down Expand Up @@ -208,7 +209,7 @@ def compute_link_handle(link_type: str, link_targets: List[str]) -> str:
"""
return AtomDB.link_handle(link_type, link_targets)

def get_atom(self, handle: str) -> Dict[str, Any]:
def get_atom(self, handle: str) -> AtomT:
"""
Retrieve an atom given its handle.
Expand Down Expand Up @@ -244,7 +245,7 @@ def get_atom(self, handle: str) -> Dict[str, Any]:
"""
return self.query_engine.get_atom(handle, no_target_format=True)

def get_atoms(self, handles: List[str]) -> List[Dict[str, Any]]:
def get_atoms(self, handles: List[str]) -> List[AtomT]:
"""
Retrieve atoms given a list of handles.
Expand Down Expand Up @@ -286,7 +287,7 @@ def get_atoms(self, handles: List[str]) -> List[Dict[str, Any]]:
"""
return self.query_engine.get_atoms(handles, no_target_format=True)

def get_node(self, node_type: str, node_name: str) -> Dict[str, Any]:
def get_node(self, node_type: str, node_name: str) -> NodeT:
"""
Retrieve a node given its type and name.
Expand Down Expand Up @@ -317,7 +318,7 @@ def get_node(self, node_type: str, node_name: str) -> Dict[str, Any]:
node_handle = self.backend.node_handle(node_type, node_name)
return self.get_atom(node_handle)

def get_link(self, link_type: str, link_targets: List[str]) -> Dict[str, Any]:
def get_link(self, link_type: str, link_targets: List[str]) -> LinkT:
"""
Retrieve a link given its type and list of targets.
Targets are hashes of the nodes these hashes or handles can be created using the function 'compute_node_handle'.
Expand Down Expand Up @@ -361,89 +362,29 @@ def get_link(self, link_type: str, link_targets: List[str]) -> Dict[str, Any]:
link_handle = self.backend.link_handle(link_type, link_targets)
return self.get_atom(link_handle)

def get_links(
self,
link_type: str,
target_types: list[str] | None = None,
link_targets: list[str] | None = None,
**kwargs,
) -> Union[Iterator, List[str], List[Dict], tuple[int, List[Dict]]]: # TODO: simplify
def get_links(self, link_filter: LinkFilter) -> List[LinkT]:
"""
Retrieve all links that match the passed search criteria.
This method can be used in four different ways.
1. Retrieve all the links of a given type
Set link_type to the desired type and set target_types=None and
link_targets=None.
Retrieves all links that match the passed filtering criteria.
2. Retrieve all the links of a given type whose targets are of given types.
Set link_type to the desired type and target_types to a list with the desired
types os each target.
Args:
link_filter (LinkFilter): Filtering criteria to be used to select links
3. Retrieve all the links of a given type whose targets match a given list of
handles
Returns:
List[LinkT]: A list of link documents
"""
return self.query_engine.get_links(link_filter)

Set link_type to the desired type (or pass link_type='*' to retrieve links
of any type) and set link_targets to a list of handles. Any handle in this
list can be '*' meaning that any handle in that position of the targets list
is a match for the query. Set target_types=None.
def get_link_handles(self, link_filter: LinkFilter) -> List[str]:
"""
Retrieve the handle of all links that match the passed filtering criteria.
Args:
link_type (str): Link type being searched (can be '*' when link_targets is not None).
target_types (List[str], optional): Template of target types being searched.
link_targets (List[str], optional): Template of targets being searched (handles or '*').
Keyword Args:
no_iterator (bool, optional): Set False to return an iterator otherwise it will return a
list of Dict[str, Any].
If the query_engine is set to 'local' it always return an iterator.
Defaults to True.
cursor (int, optional): Cursor position in the iterator, starts retrieving links from redis at the cursor
position. Defaults to 0.
chunk_size (int, optional): Chunk size. Defaults to 1000.
top_level_only (bool optional): Set to True to filter top level links. Defaults to False.
link_filter (LinkFilter): Filtering criteria to be used to select links
Returns:
Union[Iterator, List[Dict[str, Any]]]: A list of dictionaries containing detailed
information of the links
Examples:
1. Retrieve all the links of a given type
>>> das = DistributedAtomSpace()
>>> links = das.get_links(link_type='Inheritance')
>>> for link in links:
>>> print(link['type'], link['targets'])
Inheritance ['5b34c54bee150c04f9fa584b899dc030', 'bdfe4e7a431f73386f37c6448afe5840']
Inheritance ['b94941d8cd1c0ee4ad3dd3dcab52b964', '80aff30094874e75028033a38ce677bb']
Inheritance ['bb34ce95f161a6b37ff54b3d4c817857', '0a32b476852eeb954979b87f5f6cb7af']
...
2. Retrieve all the links of a given type whose targets are of given types.
>>> links = das.get_links(link_type='Inheritance', target_types=['Concept', 'Concept'])
>>> for link in links:
>>> print(link['type'], link['targets'])
Inheritance ['5b34c54bee150c04f9fa584b899dc030', 'bdfe4e7a431f73386f37c6448afe5840']
Inheritance ['b94941d8cd1c0ee4ad3dd3dcab52b964', '80aff30094874e75028033a38ce677bb']
Inheritance ['bb34ce95f161a6b37ff54b3d4c817857', '0a32b476852eeb954979b87f5f6cb7af']
...
3. Retrieve all the links of a given type whose targets match a given list of
handles
>>> snake = das.compute_node_handle('Concept', 'snake')
>>> links = das.get_links(link_type='Similarity', link_targets=[snake, '*'])
>>> for link in links:
>>> print(link['type'], link['targets'])
Similarity ['c1db9b517073e51eb7ef6fed608ec204', 'b94941d8cd1c0ee4ad3dd3dcab52b964']
Similarity ['c1db9b517073e51eb7ef6fed608ec204', 'bb34ce95f161a6b37ff54b3d4c817857']
"""
return self.query_engine.get_links(link_type, target_types, link_targets, **kwargs)
List[str]: A list of link handles
"""
return self.query_engine.get_link_handles(link_filter)

def get_incoming_links(
self, atom_handle: str, **kwargs
Expand Down
62 changes: 62 additions & 0 deletions hyperon_das/link_filters.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
from dataclasses import dataclass
from enum import Enum, auto

from hyperon_das_atomdb import WILDCARD


class LinkFilterType(int, Enum):
NAMED_TYPE = auto()
FLAT_TYPE_TEMPLATE = auto()
TARGETS = auto()


@dataclass
class LinkFilter:
filter_type: LinkFilterType
toplevel_only: bool
link_type: str = None
target_types: list[str] = None
targets: list[str] = None


@dataclass
class NamedType(LinkFilter):
"""
All links of a given type. Optionally, only toplevel links are selected.
"""

def __init__(self, link_type: str, toplevel_only: bool = False):
self.filter_type = LinkFilterType.NAMED_TYPE
self.link_type = link_type
self.toplevel_only = toplevel_only


@dataclass
class FlatTypeTemplate(LinkFilter):
"""
All links of a given type whose targets are of given types. Any number of wildcards '*' can
be used as link or target types.
"""

def __init__(
self, target_types: list[str], link_type: str = WILDCARD, toplevel_only: bool = False
):

self.filter_type = LinkFilterType.FLAT_TYPE_TEMPLATE
self.link_type = link_type
self.target_types = target_types
self.toplevel_only = toplevel_only


@dataclass
class Targets(LinkFilter):
"""
All links of a given type whose targets match a given list of handles. Any number of
wildcards '*' can be used as link type or target handle.
"""

def __init__(self, targets: list[str], link_type: str = WILDCARD, toplevel_only: bool = False):
self.filter_type = LinkFilterType.TARGETS
self.link_type = link_type
self.targets = targets
self.toplevel_only = toplevel_only
49 changes: 16 additions & 33 deletions hyperon_das/query_engines/local_query_engine.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
AtomT,
HandlesListT,
IncomingLinksT,
LinkT,
MatchedLinksResultT,
MatchedTargetsListT,
)
Expand All @@ -19,13 +20,13 @@
CustomQuery,
LazyQueryEvaluator,
ListIterator,
LocalGetLinks,
LocalIncomingLinks,
QueryAnswerIterator,
)
from hyperon_das.client import FunctionsClient
from hyperon_das.context import Context
from hyperon_das.exceptions import UnexpectedQueryFormat
from hyperon_das.link_filters import LinkFilter
from hyperon_das.logger import logger
from hyperon_das.query_engines.query_engine_protocol import QueryEngine
from hyperon_das.type_alias import Query
Expand Down Expand Up @@ -211,38 +212,20 @@ def get_atom(self, handle: str, **kwargs) -> Dict[str, Any]:
def get_atoms(self, handles: str, **kwargs) -> List[Dict[str, Any]]:
return [self.local_backend.get_atom(handle, **kwargs) for handle in handles]

def get_links(
self,
link_type: str,
target_types: list[str] | None = None,
link_targets: list[str] | None = None,
**kwargs,
) -> Union[Iterator, List[str], List[Dict], tuple[int, List[Dict]]]: # TODO: simplify
if kwargs.get('no_iterator', True):
cursor, answer = self._get_related_links(
link_type, target_types, link_targets, **kwargs
)
if not answer: # atom does not exist
return []
if isinstance(answer[0], tuple):
if isinstance(cursor, int):
return cursor, self._to_link_dict_list(answer)
else:
return self._to_link_dict_list(answer)
return self._to_link_dict_list(answer)
else:
if kwargs.get('cursor') is None:
kwargs['cursor'] = 0
cursor, answer = self._get_related_links(
link_type, target_types, link_targets, **kwargs
)
if cursor:
kwargs['cursor'] = cursor
kwargs['backend'] = self
kwargs['link_type'] = link_type
kwargs['target_types'] = target_types
kwargs['link_targets'] = link_targets
return LocalGetLinks(ListIterator(answer), **kwargs)
def get_link_handles(self, link_filter: LinkFilter) -> List[str]:
_, answer = self._get_related_links(
link_type=link_filter.link_type,
target_types=link_filter.target_types,
link_targets=link_filter.targets,
toplevel_only=link_filter.toplevel_only,
)
return answer

def get_links(self, link_filter: LinkFilter) -> List[LinkT]:
handles = self.get_link_handles(link_filter)
if not handles:
return []
return self._to_link_dict_list(handles)

def get_incoming_links(
self, atom_handle: str, **kwargs
Expand Down
Loading

0 comments on commit 2a21045

Please sign in to comment.