Skip to content
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
18 changes: 18 additions & 0 deletions .pre-commit-config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
repos:
- repo: local
hooks:
- id: ruff-check
name: ruff-check
entry: uv run ruff check --fix .
language: system
pass_filenames: false
- id: ruff-format
name: ruff-format
entry: uv run ruff format .
language: system
pass_filenames: false
# - id: ty-check
# name: ty-check
# entry: uv run ty check .
# language: system
# pass_filenames: false
6 changes: 6 additions & 0 deletions .vscode/extensions.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"recommendations": [
// Linting and formatting
"charliermarsh.ruff",
]
}
13 changes: 13 additions & 0 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"python.defaultInterpreterPath": "${workspaceFolder}/.venv/bin/python",
"[python]": {
"editor.defaultFormatter": "charliermarsh.ruff",
"editor.formatOnSave": true,
"editor.codeActionsOnSave": {
"source.fixAll": "explicit",
"source.organizeImports": "explicit"
}
},
// Disable flake8 extension completely
"flake8.enabled": false,
}
44 changes: 22 additions & 22 deletions docs/source/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,15 @@
import citrine
import os
import sys
sys.path.insert(0, os.path.abspath('../../src/citrine'))

sys.path.insert(0, os.path.abspath("../../src/citrine"))


# -- Project information -----------------------------------------------------

project = 'Citrine Python'
copyright = '2019, Citrine Informatics'
author = 'Citrine Informatics'
project = "Citrine Python"
copyright = "2019, Citrine Informatics"
author = "Citrine Informatics"

# The short X.Y version.
version = citrine.__version__
Expand All @@ -33,28 +34,30 @@
# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom
# ones.
extensions = [
'sphinxcontrib.apidoc',
'sphinx.ext.napoleon',
'sphinx.ext.intersphinx',
'sphinx_rtd_theme'
"sphinxcontrib.apidoc",
"sphinx.ext.napoleon",
"sphinx.ext.intersphinx",
"sphinx_rtd_theme",
]

# Use the sphinxcontrib.apidoc extension to wire in the sphinx-apidoc invocation
# to the build process, eliminating the need for an extra call during the
# build.
#
# See: https://github.com/sphinx-contrib/apidoc
apidoc_module_dir = '../../src/citrine'
apidoc_output_dir = 'reference'
apidoc_excluded_paths = ['tests']
apidoc_module_dir = "../../src/citrine"
apidoc_output_dir = "reference"
apidoc_excluded_paths = ["tests"]
apidoc_separate_modules = True

# Use intersphinx to link to classes in Sphinx docs for other libraries
# See: https://www.sphinx-doc.org/en/master/usage/extensions/intersphinx.html
intersphinx_mapping = {'gemd-python': ('https://citrineinformatics.github.io/gemd-python/', None)}
intersphinx_mapping = {
"gemd-python": ("https://citrineinformatics.github.io/gemd-python/", None)
}

# Add any paths that contain templates here, relative to this directory.
templates_path = ['_templates']
templates_path = ["_templates"]

# List of patterns, relative to source directory, that match files and
# directories to ignore when looking for source files.
Expand All @@ -71,19 +74,16 @@
# Add any paths that contain custom static files (such as style sheets) here,
# relative to this directory. They are copied after the builtin static files,
# so a file named "default.css" will overwrite the builtin "default.css".
html_static_path = ['_static']
html_static_path = ["_static"]

# These paths are either relative to html_static_path or fully qualified paths (eg. https://...)
html_css_files = [
'css/custom.css',
"css/custom.css",
]

autodoc_member_order = 'groupwise'
autodoc_member_order = "groupwise"
autodoc_mock_imports = [] # autodoc_mock_imports allows Spyinx to ignore any external modules listed in the array

html_favicon = '_static/favicon.png'
html_logo = '_static/logo.png'
html_theme_options = {
'sticky_navigation': False,
'logo_only': True
}
html_favicon = "_static/favicon.png"
html_logo = "_static/logo.png"
html_theme_options = {"sticky_navigation": False, "logo_only": True}
35 changes: 35 additions & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
[build-system]
requires = ["hatchling"]
build-backend = "hatchling.build"

[tool.hatch.build.targets.wheel]
packages = ["src"]

[project]
name = "citrine"
version = "0.1.0"
description = "Python library for the Citrine Platform"
requires-python = ">=3.12"

dependencies = [
"arrow",
"boto3",
"deprecation",
"gemd",
"requests",
"tqdm",
"urllib3",

]

[project.optional-dependencies]
test = [
"factory-boy",
"mock",
"pandas",
"pre-commit",
"pytest",
"requests-mock",
"ruff",
"ty"
]
11 changes: 7 additions & 4 deletions scripts/validate_version_bump.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@

def main():
repo_dir = popen("git rev-parse --show-toplevel", mode="r").read().rstrip()
version_path = relpath(f'{repo_dir}/src/citrine/__version__.py', getcwd())
version_path = relpath(f"{repo_dir}/src/citrine/__version__.py", getcwd())

try:
with open(version_path, "r") as fh:
Expand All @@ -18,10 +18,13 @@ def main():
raise ValueError(f"Couldn't extract version from {version_path}") from e

try:
with popen(f"git fetch origin && git show origin/main:src/citrine/__version__.py", mode="r") as fh:
with popen(
"git fetch origin && git show origin/main:src/citrine/__version__.py",
mode="r",
) as fh:
old_version = extract_version(fh)
except Exception as e:
raise ValueError(f"Couldn't extract version from main branch") from e
raise ValueError("Couldn't extract version from main branch") from e

if new_version.major != old_version.major:
number = "major"
Expand Down Expand Up @@ -50,7 +53,7 @@ def main():
def extract_version(handle: TextIO) -> Version:
text = handle.read()
if not re.search(r"\S", text):
raise ValueError(f"No content")
raise ValueError("No content")
match = re.search(r"""^\s*__version__\s*=\s*(['"])(\S+)\1""", text, re.MULTILINE)
if match:
return Version(match.group(2))
Expand Down
1 change: 1 addition & 0 deletions src/citrine/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,5 +5,6 @@
https://citrineinformatics.github.io/citrine-python/index.html

"""

from citrine.citrine import Citrine # noqa: F401
from .__version__ import __version__ # noqa: F401
31 changes: 20 additions & 11 deletions src/citrine/_rest/ai_resource_metadata.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,31 +2,40 @@
from citrine._serialization import properties


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)
""":Optional[UUID]: 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
)
""":Optional[datetime]: 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)
""":Optional[UUID]: 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
)
""":Optional[datetime]: 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
)
""":Optional[UUID]: 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
)
""":Optional[datetime]: 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)
""":Optional[str]: 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"""
61 changes: 40 additions & 21 deletions src/citrine/_rest/collection.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,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 @@ -24,7 +24,7 @@ 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"

Expand All @@ -33,17 +33,27 @@ def _put_resource_ref(self, subpath: str, uid: Union[UUID, str]):
ref = ResourceRef(uid)
return self.session.put_resource(url, ref.dump(), version=self._api_version)

def _get_path(self,
uid: Optional[Union[UUID, str]] = None,
*,
ignore_dataset: bool = False,
action: Union[str, Sequence[str]] = [],
query_terms: Dict[str, str] = {},
) -> str:
def _get_path(
self,
uid: Optional[Union[UUID, str]] = None,
*,
ignore_dataset: bool = False,
action: Union[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__)
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__,
)

@abstractmethod
def build(self, data: dict):
Expand All @@ -52,7 +62,9 @@ def build(self, data: dict):
def get(self, uid: Union[UUID, str]) -> ResourceType:
"""Get a particular element of the collection."""
if uid is None:
raise ValueError("Cannot get when uid=None. Are you using a registered resource?")
raise ValueError(
"Cannot get when uid=None. Are you using a registered resource?"
)
path = self._get_path(uid)
data = self.session.get_resource(path, version=self._api_version)
data = data[self._individual_key] if self._individual_key else data
Expand All @@ -62,7 +74,9 @@ def register(self, model: CreationType) -> CreationType:
"""Create a new element of the collection by registering an existing resource."""
path = self._get_path()
try:
data = self.session.post_resource(path, model.dump(), version=self._api_version)
data = self.session.post_resource(
path, model.dump(), version=self._api_version
)
data = data[self._individual_key] if self._individual_key else data
return self.build(data)
except NonRetryableException as e:
Expand All @@ -89,14 +103,18 @@ 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."""
url = self._get_path(model.uid)
updated = self.session.put_resource(url, model.dump(), version=self._api_version)
updated = self.session.put_resource(
url, model.dump(), version=self._api_version
)
data = updated[self._individual_key] if self._individual_key else updated
return self.build(data)

Expand All @@ -106,8 +124,9 @@ def delete(self, uid: Union[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
Loading
Loading