Skip to content
Open
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: 12 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,18 @@ docs = [
"sphinxcontrib-apidoc==0.6.0",
]

[tool.ruff]
line-length = 99

[tool.ruff.format]
# Collapse collections that fit on one line even if they carry a trailing comma,
# instead of exploding them across multiple lines.
skip-magic-trailing-comma = true

[tool.ruff.lint.isort]
# Required to be false when format.skip-magic-trailing-comma is true.
split-on-trailing-comma = false

[tool.pytest.ini_options]
testpaths = [
"tests",
Expand Down
2 changes: 2 additions & 0 deletions src/citrine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,7 @@
https://citrineinformatics.github.io/citrine-python/index.html

"""

from citrine.citrine import Citrine # noqa: F401

from .__version__ import __version__ # noqa: F401
2 changes: 1 addition & 1 deletion src/citrine/__version__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "5.0.0"
__version__ = "5.0.1"
4 changes: 1 addition & 3 deletions src/citrine/_rest/admin_collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@
class AdminCollection(Collection[ResourceType]):
"""Abstract class for representing collections of REST resources with as_admin access."""

def list(
self, *, per_page: int = 100, as_admin: bool = False
) -> Iterator[ResourceType]:
def list(self, *, per_page: int = 100, as_admin: bool = False) -> Iterator[ResourceType]:
"""
Paginate over the elements of the collection.

Expand Down
25 changes: 13 additions & 12 deletions src/citrine/_rest/ai_resource_metadata.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
from citrine.resources.status_detail import StatusDetail
from citrine._serialization import properties
from citrine.resources.status_detail import StatusDetail


class AIResourceMetadata():
class AIResourceMetadata:
"""Abstract class for representing common metadata for Resources."""

created_by = properties.Optional(properties.UUID, 'created_by', serializable=False)
created_by = properties.Optional(properties.UUID, "created_by", serializable=False)
""":UUID | None: id of the user who created the resource"""
create_time = properties.Optional(properties.Datetime, 'create_time', serializable=False)
create_time = properties.Optional(properties.Datetime, "create_time", serializable=False)
""":datetime | None: date and time at which the resource was created"""

updated_by = properties.Optional(properties.UUID, 'updated_by', serializable=False)
updated_by = properties.Optional(properties.UUID, "updated_by", serializable=False)
""":UUID | None: id of the user who most recently updated the resource,
if it has been updated"""
update_time = properties.Optional(properties.Datetime, 'update_time', serializable=False)
update_time = properties.Optional(properties.Datetime, "update_time", serializable=False)
""":datetime | None: date and time at which the resource was most recently updated,
if it has been updated"""

archived = properties.Boolean('archived', default=False)
archived = properties.Boolean("archived", default=False)
""":bool: whether the resource is archived (hidden but not deleted)"""
archived_by = properties.Optional(properties.UUID, 'archived_by', serializable=False)
archived_by = properties.Optional(properties.UUID, "archived_by", serializable=False)
""":UUID | None: id of the user who archived the resource, if it has been archived"""
archive_time = properties.Optional(properties.Datetime, 'archive_time', serializable=False)
archive_time = properties.Optional(properties.Datetime, "archive_time", serializable=False)
""":datetime | None: date and time at which the resource was archived,
if it has been archived"""

status = properties.Optional(properties.String(), 'status', serializable=False)
status = properties.Optional(properties.String(), "status", serializable=False)
""":str | None: short description of the resource's status"""
status_detail = properties.List(properties.Object(StatusDetail), 'status_detail', default=[],
serializable=False)
status_detail = properties.List(
properties.Object(StatusDetail), "status_detail", default=[], serializable=False
)
""":list[StatusDetail]: a list of structured status info, containing the message and level"""
37 changes: 20 additions & 17 deletions src/citrine/_rest/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@
from citrine.exceptions import ModuleRegistrationFailedException, NonRetryableException
from citrine.resources.response import Response

ResourceType = TypeVar('ResourceType', bound=Resource)
ResourceType = TypeVar("ResourceType", bound=Resource)

# Python does not support a TypeVar being used as a bound for another TypeVar.
# Thus, this will never be particularly type safe on its own. The solution is to
# have subclasses override the create method.
CreationType = TypeVar('CreationType', bound='Resource')
CreationType = TypeVar("CreationType", bound="Resource")


class Collection(Generic[ResourceType], Pageable):
Expand All @@ -25,21 +25,23 @@ class Collection(Generic[ResourceType], Pageable):
_dataset_agnostic_path_template: str = NotImplemented
_individual_key: str = NotImplemented
_resource: ResourceType = NotImplemented
_collection_key: str = 'entries'
_collection_key: str = "entries"
_paginator: Paginator = Paginator()
_api_version: str = "v1"

def _get_path(self,
uid: UUID | str | None = None,
*,
ignore_dataset: bool = False,
action: str | Sequence[str] = [],
query_terms: dict[str, str] = {},
) -> str:
def _get_path(
self,
uid: UUID | str | None = None,
*,
ignore_dataset: bool = False,
action: str | Sequence[str] = [],
query_terms: dict[str, str] = {},
) -> str:
"""Construct a url from __base_path__ and, optionally, id and/or action."""
base = self._dataset_agnostic_path_template if ignore_dataset else self._path_template
return resource_path(path_template=base, uid=uid, action=action, query_terms=query_terms,
**self.__dict__)
return resource_path(
path_template=base, uid=uid, action=action, query_terms=query_terms, **self.__dict__
)

@abstractmethod
def build(self, data: dict):
Expand Down Expand Up @@ -85,9 +87,11 @@ def list(self, *, per_page: int = 100) -> Iterator[ResourceType]:
Use list() to force evaluation of all results into an in-memory list.

"""
return self._paginator.paginate(page_fetcher=self._fetch_page,
collection_builder=self._build_collection_elements,
per_page=per_page)
return self._paginator.paginate(
page_fetcher=self._fetch_page,
collection_builder=self._build_collection_elements,
per_page=per_page,
)

def update(self, model: CreationType) -> CreationType:
"""Update a particular element of the collection."""
Expand All @@ -102,8 +106,7 @@ def delete(self, uid: UUID | str) -> Response:
data = self.session.delete_resource(url, version=self._api_version)
return Response(body=data)

def _build_collection_elements(self,
collection: Iterable[dict]) -> Iterator[ResourceType]:
def _build_collection_elements(self, collection: Iterable[dict]) -> Iterator[ResourceType]:
"""
For each element in the collection, build the appropriate resource type.

Expand Down
38 changes: 21 additions & 17 deletions src/citrine/_rest/engine_resource.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,32 +5,35 @@
from citrine._serialization.include_parent_properties import IncludeParentProperties
from citrine.resources.status_detail import StatusDetail

Self = TypeVar('Self', bound='Resource')
Self = TypeVar("Self", bound="Resource")


class EngineResourceWithoutStatus(Resource[Self]):
"""Base resource for metadata from stand-alone AI Engine modules."""

created_by = properties.Optional(properties.UUID, 'metadata.created.user', serializable=False)
created_by = properties.Optional(properties.UUID, "metadata.created.user", serializable=False)
""":UUID | None: id of the user who created the resource"""
create_time = properties.Optional(properties.Datetime, 'metadata.created.time',
serializable=False)
create_time = properties.Optional(
properties.Datetime, "metadata.created.time", serializable=False
)
""":datetime | None: date and time at which the resource was created"""

updated_by = properties.Optional(properties.UUID, 'metadata.updated.user',
serializable=False)
updated_by = properties.Optional(properties.UUID, "metadata.updated.user", serializable=False)
""":UUID | None: id of the user who most recently updated the resource,
if it has been updated"""
update_time = properties.Optional(properties.Datetime, 'metadata.updated.time',
serializable=False)
update_time = properties.Optional(
properties.Datetime, "metadata.updated.time", serializable=False
)
""":datetime | None: date and time at which the resource was most recently updated,
if it has been updated"""

archived_by = properties.Optional(properties.UUID, 'metadata.archived.user',
serializable=False)
archived_by = properties.Optional(
properties.UUID, "metadata.archived.user", serializable=False
)
""":UUID | None: id of the user who archived the resource, if it has been archived"""
archive_time = properties.Optional(properties.Datetime, 'metadata.archived.time',
serializable=False)
archive_time = properties.Optional(
properties.Datetime, "metadata.archived.time", serializable=False
)
""":datetime | None: date and time at which the resource was archived,
if it has been archived"""

Expand Down Expand Up @@ -59,10 +62,11 @@ def _post_dump(self, data: dict) -> dict:
class EngineResource(EngineResourceWithoutStatus[Self], IncludeParentProperties[Self]):
"""Base resource for metadata from stand-alone AI Engine modules."""

status = properties.Optional(properties.String(), 'metadata.status.name', serializable=False)
status = properties.Optional(properties.String(), "metadata.status.name", serializable=False)
""":str | None: short description of the resource's status"""
status_detail = properties.List(properties.Object(StatusDetail), 'metadata.status.detail',
default=[], serializable=False)
status_detail = properties.List(
properties.Object(StatusDetail), "metadata.status.detail", default=[], serializable=False
)
""":list[StatusDetail]: a list of structured status info, containing the message and level"""

@classmethod
Expand All @@ -75,10 +79,10 @@ class VersionedEngineResource(EngineResource[Self], IncludeParentProperties[Self
"""Base resource for metadata from stand-alone AI Engine modules which support versioning."""

""":Integer: The version number of the resource."""
version = properties.Optional(properties.Integer, 'metadata.version', serializable=False)
version = properties.Optional(properties.Integer, "metadata.version", serializable=False)

""":Boolean: The draft status of the resource."""
draft = properties.Optional(properties.Boolean, 'metadata.draft', serializable=False)
draft = properties.Optional(properties.Boolean, "metadata.draft", serializable=False)

@classmethod
def build(cls, data: dict):
Expand Down
47 changes: 24 additions & 23 deletions src/citrine/_rest/pageable.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,32 +2,34 @@
from uuid import UUID


class Pageable():
class Pageable:
"""Class that allows paging."""

_collection_key: str = NotImplemented
_api_version: str = "v1"

def _get_path(self,
uid: UUID | str | None = None,
*,
ignore_dataset: bool = False,
action: str | Sequence[str] = [],
query_terms: dict[str, str] = {},
) -> str:
def _get_path(
self,
uid: UUID | str | None = None,
*,
ignore_dataset: bool = False,
action: str | Sequence[str] = [],
query_terms: dict[str, str] = {},
) -> str:
"""Construct a url from __base_path__ and, optionally, id."""
raise NotImplementedError # pragma: no cover

def _fetch_page(self,
path: str | None = None,
fetch_func: Callable[..., dict] | None = None,
page: int | None = None,
per_page: int | None = None,
json_body: dict | None = None,
additional_params: dict | None = None,
*,
version: str | None = None
) -> tuple[Iterable[dict], str]:
def _fetch_page(
self,
path: str | None = None,
fetch_func: Callable[..., dict] | None = None,
page: int | None = None,
per_page: int | None = None,
json_body: dict | None = None,
additional_params: dict | None = None,
*,
version: str | None = None,
) -> tuple[Iterable[dict], str]:
"""
Fetch visible elements. This does not handle pagination.

Expand Down Expand Up @@ -85,7 +87,7 @@ def _fetch_page(self,
data = fetch_func(path, params=params, version=version, **json_body)

try:
next_uri = data.get('next', "")
next_uri = data.get("next", "")
except AttributeError:
next_uri = ""

Expand All @@ -99,10 +101,9 @@ def _fetch_page(self,

return collection, next_uri

def _page_params(self,
page: int | None,
per_page: int | None,
module_type: str | None = None) -> dict[str, int]:
def _page_params(
self, page: int | None, per_page: int | None, module_type: str | None = None
) -> dict[str, int]:
params = {}
if page is not None:
params["page"] = page
Expand Down
29 changes: 15 additions & 14 deletions src/citrine/_rest/paginator.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
from typing import Any, Generic, TypeVar
from uuid import uuid4

ResourceType = TypeVar('ResourceType')
ResourceType = TypeVar("ResourceType")


class Paginator(Generic[ResourceType]):
Expand All @@ -13,12 +13,14 @@ class Paginator(Generic[ResourceType]):
that will be extracted for comparison purposes (to avoid looping on the same items).
"""

def paginate(self,
page_fetcher: Callable[[int | None, int], tuple[Iterable[dict], str]],
collection_builder: Callable[[Iterable[dict]], Iterable[ResourceType]],
per_page: int = 100,
search_params: dict | None = None,
deduplicate: bool = True) -> Iterator[ResourceType]:
def paginate(
self,
page_fetcher: Callable[[int | None, int], tuple[Iterable[dict], str]],
collection_builder: Callable[[Iterable[dict]], Iterable[ResourceType]],
per_page: int = 100,
search_params: dict | None = None,
deduplicate: bool = True,
) -> Iterator[ResourceType]:
"""
A generic support class to paginate requests into an iterable of a built object.

Expand Down Expand Up @@ -54,26 +56,25 @@ def paginate(self,
"""
# To avoid setting default to {} -> reduce mutation risk, and to make more extensible. Also
# making 'search_params' key of outermost dict for keyword expansion by page_fetcher func
search_params = {} if search_params is None else {'search_params': search_params}
search_params = {} if search_params is None else {"search_params": search_params}

first_entity = None
page_idx = 1
uids = set()

while True:
subset_collection, next_uri = page_fetcher(page=page_idx, per_page=per_page,
**search_params)
subset_collection, next_uri = page_fetcher(
page=page_idx, per_page=per_page, **search_params
)

subset = collection_builder(subset_collection)

count = 0
for idx, element in enumerate(subset):

# escaping from infinite loops where page/per_page are not
# honored and are returning the same results regardless of page:
current_entity = self._comparison_fields(element)
if first_entity is not None and \
first_entity == current_entity:
if first_entity is not None and first_entity == current_entity:
# TODO: raise an exception once the APIs that ignore pagination are fixed
break

Expand Down Expand Up @@ -107,4 +108,4 @@ def _comparison_fields(self, entity: ResourceType) -> Any:

If the 'uid' here isn't found, default to comparing the entire entity.
"""
return getattr(entity, 'uid', entity)
return getattr(entity, "uid", entity)
Loading
Loading