Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Links #210

Merged
merged 7 commits into from
Sep 8, 2023
Merged

Links #210

Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
101 changes: 100 additions & 1 deletion cascade/base/traceable.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"""


from dataclasses import dataclass, asdict
from dataclasses import dataclass, asdict, field
import os
import glob
import socket
Expand All @@ -36,6 +36,15 @@ class Comment:
message: str


@dataclass
class Link:
id: str
name: Union[str, None]
uri: Union[str, None]
meta: Union[PipeMeta, None]
created_at: datetime


class Traceable:
"""
Base class for everything that has metadata in Cascade
Expand Down Expand Up @@ -78,6 +87,7 @@ def __init__(
self.tags = set()

self.comments = list()
self.links = list()

@staticmethod
def _read_meta_from_file(path: str) -> MetaFromFile:
Expand Down Expand Up @@ -114,6 +124,10 @@ def get_meta(self) -> PipeMeta:
comments = [asdict(comment) for comment in self.comments]
meta["comments"] = comments

if hasattr(self, "links"):
links = [asdict(link) for link in self.links]
meta["links"] = links

return [meta]

def update_meta(self, obj: Union[Dict[Any, Any], str]) -> None:
Expand Down Expand Up @@ -163,6 +177,12 @@ def from_meta(self, meta: Dict[str, Any]) -> None:
if "tags" in meta:
self.tag(meta["tags"])

if "links" in meta:
for link in meta["links"]:
self.links.append(
Link(**link)
)

def __repr__(self) -> str:
"""
Returns
Expand Down Expand Up @@ -256,6 +276,85 @@ def remove_comment(self, id: int) -> None:
return
raise ValueError(f"Comment with {id} was not found")

def _find_latest_link_id(self) -> str:
if len(self.links) == 0:
return "0"
return self.links[-1].id

def link(self,
obj: Union["Traceable", None] = None,
name: Union[str, None] = None,
uri: Union[str, None] = None,
meta: Union[PipeMeta, None] = None,
include: bool = True) -> None:
"""
Links another object to this object. Links can contain
name, URI and meta of the object.

To create the link the Traceable can be passed - if include
is True (by default) object's meta and string representation will be taken and saved with
the link.

If include is False get_meta will still be called, but only small set of
specific fields will be taken.

Link can be initialized without meta for example with only name
or only URI.

If name or meta passed with the object at the same time
they will override values from the object.

The get_meta() of an object is resolved in the link method to
prevent circular calls and other problems.

Parameters
----------
obj : Union[Traceable, None]
The object to link
name : Union[str, None], optional
Name of the object, overrides obj name if passed, by default None
uri : Union[str, None], optional
URI of the object, by default None
meta : Union[PipeMeta, None], optional
Meta of the object, overrides obj meta if passed, by default None
include : bool, optional
Whether to include full meta of the object, by default True
"""
if isinstance(obj, Traceable):
if name is None:
name = str(obj)
if meta is None:
if include:
meta = obj.get_meta()
else:
obj_meta = obj.get_meta()
meta = [{
"type": obj_meta[0].get("type"),
"description": obj_meta[0].get("description"),
"tags": obj_meta[0].get("tags"),
"comments": obj_meta[0].get("comments"),
}]

link_id = str(int(self._find_latest_link_id()) + 1)
self.links.append(
Link(link_id, name, uri, meta, pendulum.now(tz="UTC"))
)

def remove_link(self, id: str) -> None:
"""
Removes a link given an index

Parameters
----------
idx : int
Link's index
"""
for i, link in enumerate(self.links):
if link.id == id:
self.links.pop(i)
return
raise ValueError(f"Link with {id} was not found")


class TraceableOnDisk(Traceable):
"""
Expand Down
28 changes: 28 additions & 0 deletions cascade/tests/test_traceable.py
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,34 @@ def test_comments():
assert hasattr(tr.comments[0], "timestamp")


def test_links():
tr = Traceable()
tr2 = Traceable(lol=2)

tr.link(tr2)

assert len(tr.links) == 1
assert tr.links[0].id == "1"
assert tr.links[0].meta == tr2.get_meta()
assert tr.links[0].uri is None

tr.link(name="link2")

assert len(tr.links) == 2
assert tr.links[1].name == "link2"
assert tr.links[1].uri is None

tr.link(tr2, include=False)

assert tr.links[2].meta is not None

tr.remove_link("0")
assert len(tr.links) == 2

with pytest.raises(ValueError):
tr.remove_link("0")


def test_from_meta():
tr = Traceable()
tr.tag("tag")
Expand Down