Skip to content

Add paginated access to children of nodes #6

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

Draft
wants to merge 6 commits into
base: main
Choose a base branch
from
Draft
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
12 changes: 10 additions & 2 deletions src/cript/nodes/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,11 @@
from jsonschema.exceptions import best_match
from uuid import uuid4

import cript
from cript import Cript, NotFoundError, camel_case_to_snake_case, extract_node_from_result
from .schema import cript_schema


logger = logging.getLogger(__name__)


Expand Down Expand Up @@ -218,7 +220,13 @@ def __getattr__(self, key):
try:
return self.__getitem__(key)
except KeyError:
raise AttributeError(key)
# TODO consider a caching of these paginators
if key in self.children:
child_paginator = cript.resources.child.ChildPaginator(self, key)
self.__dict__[key] = child_paginator
return self.__dict__[key]
else:
raise AttributeError(key)

def __setattr__(self, key, value):
self.__setitem__(key, value)
Expand Down Expand Up @@ -351,7 +359,7 @@ def retrieve_child(self, parent, child):
try:
if not self._primary_key:
result = self.__dict__["client"].nodes.retrieve_children(
node=parent.name_url, uuid=parent.uuid, child_node=child.name_url
node=parent.name_url, uuid=parent.uuid, child_node=child.name_url,
)
else:
result = self.__dict__["client"].search.exact.child_node(
Expand Down
5 changes: 5 additions & 0 deletions src/cript/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@
AsyncControlledVocabulariesResourceWithStreamingResponse,
)

from .child import (
ChildPaginator,
)

__all__ = [
"SchemaResource",
"AsyncSchemaResource",
Expand All @@ -58,4 +62,5 @@
"AsyncControlledVocabulariesResourceWithRawResponse",
"ControlledVocabulariesResourceWithStreamingResponse",
"AsyncControlledVocabulariesResourceWithStreamingResponse",
"ChildPaginator",
]
100 changes: 100 additions & 0 deletions src/cript/resources/child.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
import httpx
from .._types import NOT_GIVEN, Body, Query, Headers, NotGiven
from .._utils import (
maybe_transform,
async_maybe_transform,
)
from .._compat import cached_property
from .._response import (
to_raw_response_wrapper,
to_streamed_response_wrapper,
async_to_raw_response_wrapper,
async_to_streamed_response_wrapper,
)
from .._base_client import (
make_request_options,
)
from ..types.shared.search import Search
from .._resource import SyncAPIResource

class ChildPaginator:
# TODO consider writing operations
def __init__(self, parent, child, client=None):
if client is None:
client = parent.client
self._client = client
self._parent = parent
self._child = child

self._current_child_list = []
self._current_child_position = 0
# TODO change to after
self._current_page = 0
self._count = None

def __iter__(self):
self._current_child_position = 0
return self

def __next__(self):
if self._current_child_position >= len(self._current_child_list):
self._fetch_next_page()
try:
next_node = self._current_child_list[self._current_child_position]
except IndexError:
raise StopIteration

self._current_child_position += 1

return next_node

def _fetch_next_page(self):
if self._finished_fetching:
raise StopIteration

# TODO change to after
response = self._client.nodes.retrieve_children(uuid=self._parent.uuid, node=self._parent.name_url, child_node=self._child, page=self._current_page)
self._current_page += 1

if self._count is not None and self._count != int(response.data.count):
raise RuntimeError("The number of elements for a child iteration changed during pagination. This may lead to inconsistencies. Please try again.")
self._count = int(response.data.count)

self._current_child_list += response.data.result

# Make it a random access iterator, since ppl expect it to behave list a list
def __getitem__(self, key):
key_index = int(key)
previous_pos = self._current_child_position
try:
if key_index < 0:
while not self._finished_fetching:
next(self)

while len(self._current_child_list) <= key_index:
try:
next(self)
except StopIteration:
break
finally:
self._current_child_position = previous_pos
# We don't need explicit bounds checking, since the list access does that for us.
return self._current_child_list[key_index]

def __len__(self):
previous_pos = self._current_child_position
try:
if self._count is None:
try:
next(iter(self))
except StopIteration:
self._count = 0
finally:
self._current_child_position = previous_pos
return self._count

@property
def _finished_fetching(self):
if self._count is None:
return False
return len(self._current_child_list) == self._count
13 changes: 12 additions & 1 deletion src/cript/resources/nodes.py
Original file line number Diff line number Diff line change
Expand Up @@ -151,6 +151,8 @@ def retrieve_children(
*,
node: str,
child_node: str,
# TODO change to after
page: int | None = None,
# Use the following arguments if you need to pass additional parameters to the API that aren't available via kwargs.
# The extra values given here take precedence over values defined on the client or passed to this method.
extra_headers: Headers | None = None,
Expand All @@ -176,10 +178,19 @@ def retrieve_children(
raise ValueError(f"Expected a non-empty value for `uuid` but received {uuid!r}")
if not child_node:
raise ValueError(f"Expected a non-empty value for `child_node` but received {child_node!r}")
# TODO change to after
if page is not None:
query = {"page": page}
else:
query = {} # Does it make sense to allow non-paginated retrieval? The current Code uses it.
return self._get(
f"/{node}/{uuid}/{child_node}",
options=make_request_options(
extra_headers=extra_headers, extra_query=extra_query, extra_body=extra_body, timeout=timeout
extra_headers=extra_headers,
extra_query=extra_query,
extra_body=extra_body,
timeout=timeout,
query=query,
),
cast_to=Search,
)
Expand Down
63 changes: 62 additions & 1 deletion tests/api_resources/test_cript.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import pytest

import cript
from cript import *

base_url = os.environ.get("TEST_API_BASE_URL", "http://127.0.0.1:4010")
Expand Down Expand Up @@ -48,7 +49,7 @@ def test_create_project(self) -> None:
notes="my notes",
)
assert node.get("name") is not None

def test_create_collection_exisiting_project(self) -> None:
col1=Collection(name=generic_collection)
proj = Project(uuid=CREATED_UUID, collection=[col1])
Expand All @@ -66,6 +67,9 @@ def test_create_experiment_exisiting_collection(self) -> None:
col1 = Collection(name=generic_collection, experiment=[exp1])
proj1 = Project(uuid=CREATED_UUID, collection=[col1])
assert exp1.get("name") == generic_experiment
assert proj1.collection[0].get("name") == col1.name
# TODO full node access
# assert proj1.collection[0].experiment[0].name == exp1.name


def test_create_material(self) -> None:
Expand Down Expand Up @@ -246,6 +250,63 @@ def test_unlink_all_children(self) -> None:
proj1.delete(material=None)
assert proj1.get("material") is None

def test_child_paginator(self)->None:
material_list = []
num_mat = 15
for i in range(num_mat):
mat1 = Material(
name=f"{generic_material1} #{i}",
bigsmiles="{[][<]CCO[>][]}",
)
material_list += [mat1]
proj1 = Project(uuid=CREATED_UUID, material=material_list)

paginator_iter = proj1.material
for i, child in enumerate(paginator_iter):
assert child.get("name").endswith(f"#{i}")

paginator_len = cript.resources.child.ChildPaginator(proj1, "material")
# First time is doing it on empty
assert len(paginator_len) == num_mat
# Second time, it should have fetched
assert len(paginator_len) == num_mat

paginator_rand = cript.resources.child.ChildPaginator(proj1, "material")

# Accessing the second page right away
idx = 12
child = paginator_rand[idx]
assert child.get("name").endswith(f"#{idx}")
# And again, shouldn't trigger a new fetch
idx = 13
child = paginator_rand[idx]
assert child.get("name").endswith(f"#{idx}")

with pytest.raises(IndexError):
paginator_rand[num_mat+4]

paginator_neg = cript.resources.child.ChildPaginator(proj1, "material")

# Accessing the second page right away
idx = -1
child = paginator_neg[idx]
assert child.get("name").endswith(f"#{num_mat-1}")

# Test list conversion
paginator_list = cript.resources.child.ChildPaginator(proj1, "material")
fetched_material_list = list(paginator_list)
for i, child in enumerate(fetched_material_list):
assert child.get("name").endswith(f"#{i}")

# Test empty paginator
paginator_empty = cript.resources.child.ChildPaginator(proj1, "inventory")
assert len(paginator_empty) == 0

# Test empty paginator
paginator_non_exist = cript.resources.child.ChildPaginator(proj1, "non-existent attribute")
with pytest.raises(cript.NotFoundError):
len(paginator_non_exist)

def test_delete_node(self) -> None:
proj1 = Project(uuid=CREATED_UUID)
proj1.delete()
Expand Down