diff --git a/README.md b/docs/README.md similarity index 80% rename from README.md rename to docs/README.md index e587af3d..ce0941ef 100644 --- a/README.md +++ b/docs/README.md @@ -88,20 +88,4 @@ You can lint with: And you can run tests with - make cli-test - -### Trying out Numerous app engine development - -In the `examples/numerous` folder are two apps `action.py` (containing -`ActionTool`), and `parameters.py` (containing `ParameterTool`). These can be -used to test the Numerous app engine development features. - -**Note: You need an activate python environment with the python SDK installed.** -See the [python sdk development section](#development-of-python-sdk-) for -information about how to install it. - -For example, if you built using `make cli-build`, you can run - -``` -./build/numerous dev examples/numerous/parameters.py:ParameterApp -``` + make cli-test \ No newline at end of file diff --git a/docs/collections.md b/docs/collections.md index 3f8174fd..a62afd31 100644 --- a/docs/collections.md +++ b/docs/collections.md @@ -9,12 +9,10 @@ Collections acts as a schemaless database, where users can store, retrieve, and 4. Tag documents and files with key/value tags, and filter documents and files by these tags. -!!! info - This feature only supports apps that are deployed to Numerous. -!!! info - Remember to add `numerous` as a dependency in your project; most likely to - your `requirements.txt` file. +This feature only supports apps that are deployed to Numerous. + +Remember to add `numerous` as a dependency in your project; most likely to your `requirements.txt` file. ## Basic usage @@ -59,6 +57,7 @@ else: ``` -Below is the API reference for working with collections in Numerous: +## API Reference -::: numerous.collection +::: numerous.collections +::: numerous.collections.collection.collection diff --git a/examples/numerous/action.py b/examples/numerous/action.py deleted file mode 100644 index b07103ba..00000000 --- a/examples/numerous/action.py +++ /dev/null @@ -1,13 +0,0 @@ -from numerous import action, app - - -@app -class ActionApp: - count: float - message: str - - @action - def increment(self) -> None: - self.count += 1 - self.message = f"Count now: {self.count}" - print("Incrementing count:", self.count) diff --git a/examples/numerous/container.py b/examples/numerous/container.py deleted file mode 100644 index eef0487d..00000000 --- a/examples/numerous/container.py +++ /dev/null @@ -1,25 +0,0 @@ -from numerous import action, app, container - - -@container -class MyNestedContainer: - nested_child: float - - -@container -class MyContainer: - child: str - nested: MyNestedContainer - - def child_updated(self) -> None: - print("wow") - - -@app -class MyContainerApp: - my_container: MyContainer - output: str - - @action - def set_output(self) -> None: - self.output = f"First child is {self.my_container.child}, deep child is {self.my_container.nested.nested_child}" diff --git a/examples/numerous/fields.py b/examples/numerous/fields.py deleted file mode 100644 index 02cfedca..00000000 --- a/examples/numerous/fields.py +++ /dev/null @@ -1,22 +0,0 @@ -from numerous import app, field - - -@app -class FieldApp: - field_text_default: str = field() - field_number_default: float = field() - - my_text_field: str = field(label="Text Field Label") - my_number_field: float = field(label="Number Field Label") - - my_text_field_no_label: str = field(default="My text field") - my_number_field_no_label: float = field(default=42.0) - - my_text_field_with_default_value: str = field( - label="Text Field Label", - default="My text field", - ) - my_number_field_with_default_value: float = field( - label="Number Field Label", - default=42.0, - ) diff --git a/examples/numerous/html.py b/examples/numerous/html.py deleted file mode 100644 index fee6a44c..00000000 --- a/examples/numerous/html.py +++ /dev/null @@ -1,32 +0,0 @@ -from numerous import app, html - -HTML_TEMPLATE = """ -
- {title}: A HTML story -

- There once was a mark-up language with which you could create anything you liked. -

-

{story}

-

The end!

-
-""" -DEFAULT_TITLE = "Numerous and the markup" -DEFAULT_STORY = "And then, one day you found numerous to empower it even further!" - - -@app -class HTMLApp: - title: str = DEFAULT_TITLE - story: str = DEFAULT_STORY - html_example: str = html( - default=HTML_TEMPLATE.format(title=DEFAULT_TITLE, story=DEFAULT_STORY), - ) - - def set_html(self) -> None: - self.html_example = HTML_TEMPLATE.format(title=self.title, story=self.story) - - def title_updated(self) -> None: - self.set_html() - - def story_updated(self) -> None: - self.set_html() diff --git a/examples/numerous/importing.py b/examples/numerous/importing.py deleted file mode 100644 index 64fe400d..00000000 --- a/examples/numerous/importing.py +++ /dev/null @@ -1 +0,0 @@ -from parameters import ParameterApp # noqa: F401 diff --git a/examples/numerous/parameters.py b/examples/numerous/parameters.py deleted file mode 100644 index c6f2fb2e..00000000 --- a/examples/numerous/parameters.py +++ /dev/null @@ -1,12 +0,0 @@ -from numerous import app - - -@app -class ParameterApp: - param1: str - param2: float - output: str - - def param1_updated(self): - print("Got an update:", self.param1) - self.output = f"output: {self.param1}" diff --git a/examples/numerous/plot.py b/examples/numerous/plot.py deleted file mode 100644 index e4fa255d..00000000 --- a/examples/numerous/plot.py +++ /dev/null @@ -1,40 +0,0 @@ -import math - -from numerous import app, field, slider -from plotly import graph_objects as go - - -@app -class PlotlyExample: - phase_shift: float = slider(default=1, label="Phase Shift") - vertical_shift: float = slider(default=1, label="Vertical Shift") - period: float = slider(default=1, min_value=1, max_value=100, label="Period") - amplitude: float = slider(default=1, label="Amplitude") - graph: go.Figure = field() - - def phase_shift_updated(self): - print("Updated phase shift", self.phase_shift) - self._graph() - - def vertical_shift_updated(self): - print("Updated vertical shift", self.vertical_shift) - self._graph() - - def period_updated(self): - print("Updated period", self.period) - self._graph() - - def amplitude_updated(self): - print("Updated amplitude", self.amplitude) - self._graph() - - def _graph(self): - xs = [i / 10.0 for i in range(1000)] - ys = [self._sin(x) for x in xs] - self.graph = go.Figure(go.Scatter(x=xs, y=ys)) - - def _sin(self, t: float): - b = 2.0 * math.pi / self.period - return ( - self.amplitude * math.sin(b * (t + self.phase_shift)) + self.vertical_shift - ) diff --git a/examples/numerous/sliders.py b/examples/numerous/sliders.py deleted file mode 100644 index 61c382a3..00000000 --- a/examples/numerous/sliders.py +++ /dev/null @@ -1,24 +0,0 @@ -from numerous import app, slider - - -@app -class SliderApp: - result: float - default_slider: float = slider() - custom_slider: float = slider( - default=10.0, - label="My custom label", - min_value=-20.0, - max_value=20.0, - ) - - def _compute(self): - self.result = self.default_slider * self.custom_slider - - def default_slider_updated(self): - self._compute() - print("default slider updated", self.default_slider, self.result) - - def custom_slider_updated(self): - self._compute() - print("custom slider updated", self.custom_slider, self.result) diff --git a/examples/numerous/syntax.py b/examples/numerous/syntax.py deleted file mode 100644 index 984c78e6..00000000 --- a/examples/numerous/syntax.py +++ /dev/null @@ -1,9 +0,0 @@ -# mypy: ignore-errors - -from numerous import app - - -@app -class SyntaxErrorApp: - my_syntax_error str # noqa: E999 ] - my_field: str diff --git a/python/src/numerous/__init__.py b/python/src/numerous/__init__.py index 2be0be7d..90c640bb 100644 --- a/python/src/numerous/__init__.py +++ b/python/src/numerous/__init__.py @@ -1,3 +1,3 @@ """The python SDK for numerous.""" -from .collection import collection +from .collections import collection diff --git a/python/src/numerous/_client/_get_client.py b/python/src/numerous/_client/_get_client.py index 562d5c07..50124840 100644 --- a/python/src/numerous/_client/_get_client.py +++ b/python/src/numerous/_client/_get_client.py @@ -2,7 +2,7 @@ from pathlib import Path from typing import Optional -from numerous.collection._client import Client +from numerous.collections._client import Client from numerous.generated.graphql.client import Client as GQLClient from ._fs_client import FileSystemClient diff --git a/python/src/numerous/_client/_graphql_client.py b/python/src/numerous/_client/_graphql_client.py index 5baf4ecb..9d61a010 100644 --- a/python/src/numerous/_client/_graphql_client.py +++ b/python/src/numerous/_client/_graphql_client.py @@ -3,7 +3,7 @@ import os from typing import Optional, Union -from numerous.collection.exceptions import ParentCollectionNotFoundError +from numerous.collections.exceptions import ParentCollectionNotFoundError from numerous.generated.graphql.client import Client as GQLClient from numerous.generated.graphql.collection_collections import ( CollectionCollectionsCollectionCreateCollection, diff --git a/python/src/numerous/appdev/__init__.py b/python/src/numerous/appdev/__init__.py deleted file mode 100644 index ebf8da88..00000000 --- a/python/src/numerous/appdev/__init__.py +++ /dev/null @@ -1 +0,0 @@ -"""Functionality related to the app development CLI.""" diff --git a/python/src/numerous/appdev/commands.py b/python/src/numerous/appdev/commands.py deleted file mode 100644 index 7cb606ca..00000000 --- a/python/src/numerous/appdev/commands.py +++ /dev/null @@ -1,208 +0,0 @@ -"""App development CLI command implementations.""" - -import json -import logging -import sys -import traceback -from dataclasses import asdict, dataclass -from pathlib import Path -from textwrap import indent -from typing import Any, Optional, TextIO, Type - -from numerous.data_model import dump_data_model -from numerous.generated.graphql import Client -from numerous.session import Session -from numerous.utils import AppT - - -log = logging.getLogger(__name__) - - -@dataclass -class AppNotFoundError(Exception): - app: str - existing_apps: list[str] - - -@dataclass -class AppLoadRaisedError(Exception): - traceback: str - typename: str - - -@dataclass -class AppSyntaxError(Exception): - msg: str - context: str - lineno: int - offset: int - - -def read_app(app_module: Path, app_class: str, output: TextIO = sys.stdout) -> None: - try: - app_cls = _read_app_definition(app_module, app_class) - except Exception as e: # noqa: BLE001 - print_error(output, e) - else: - print_app(output, app_cls) - - -def _transform_lineno(lineno: Optional[int]) -> int: - if lineno is None: - return 0 - return lineno - 2 - - -def _transform_offset(offset: Optional[int]) -> int: - if offset is None: - return 0 - return offset - 4 - - -def _read_app_definition(app_module: Path, app_class: str) -> Any: # noqa: ANN401 - scope: dict[str, Any] = {} - module_text = app_module.read_text() - - # Check for syntax errors with raw code text - try: - compile(module_text, str(app_module), "exec") - except SyntaxError as e: - text = e.text or "" - error_pointer = " " * ((e.offset or 0) - 1) + "^" - - # ensure newline between text and pointer - if not text.endswith("\n"): - text += "\n" - - raise AppSyntaxError( - msg=e.msg, - context=text + error_pointer, - lineno=e.lineno or 0, - offset=e.offset or 0, - ) from e - - indented_module_text = indent(module_text, " ") - exception_handled_module_text = ( - "try:\n" - f"{indented_module_text}\n" - "except ModuleNotFoundError:\n" - " raise\n" - "except BaseException as e:\n" - " __numerous_read_error__ = e\n" - ) - - code = compile(exception_handled_module_text, str(app_module), "exec") - - exec(code, scope) # noqa: S102 - - unknown_error = scope.get("__numerous_read_error__") - if isinstance(unknown_error, BaseException): - tb = traceback.TracebackException.from_exception(unknown_error) - - # handle inserted exception handler offsetting position - for frame in tb.stack: - if frame.filename == str(app_module): - frame.lineno = _transform_lineno(frame.lineno) - if sys.version_info >= (3, 11): - frame.colno = _transform_offset(frame.colno) - frame.end_lineno = _transform_lineno(frame.end_lineno) - frame.end_colno = _transform_offset(frame.end_colno) - - raise AppLoadRaisedError( - typename=type(unknown_error).__name__, - traceback="".join(tb.format()), - ) - - try: - return scope[app_class] - except KeyError as err: - raise AppNotFoundError( - app_class, - [ - app.__name__ - for app in scope.values() - if getattr(app, "__numerous_app__", False) - ], - ) from err - - -def print_app(output: TextIO, cls: Type[AppT]) -> None: - data_model = dump_data_model(cls) - output.write(json.dumps({"app": asdict(data_model)})) - output.flush() - - -def print_error(output: TextIO, error: Exception) -> None: - if isinstance(error, AppNotFoundError): - output.write( - json.dumps( - { - "error": { - "appnotfound": { - "app": error.app, - "found_apps": error.existing_apps, - }, - }, - }, - ), - ) - elif isinstance(error, AppSyntaxError): - output.write( - json.dumps( - { - "error": { - "appsyntax": { - "msg": error.msg, - "context": error.context, - "pos": { - "line": error.lineno, - "offset": error.offset, - }, - }, - }, - }, - ), - ) - output.flush() - elif isinstance(error, ModuleNotFoundError): - output.write( - json.dumps( - { - "error": { - "modulenotfound": { - "module": error.name, - }, - }, - }, - ), - ) - output.flush() - elif isinstance(error, AppLoadRaisedError): - output.write( - json.dumps( - { - "error": { - "unknown": { - "typename": error.typename, - "traceback": error.traceback, - }, - }, - }, - ), - ) - output.flush() - - -async def run_app_session( - graphql_url: str, - graphql_ws_url: str, - session_id: str, - app_module: Path, - app_class: str, -) -> None: - log.info("running %s:%s in session %s", app_module, app_class, session_id) - gql = Client(graphql_url, ws_url=graphql_ws_url) - app_cls = _read_app_definition(app_module, app_class) - session = await Session.initialize(session_id, gql, app_cls) - await session.run() - log.info("app session stopped") diff --git a/python/src/numerous/apps.py b/python/src/numerous/apps.py deleted file mode 100644 index 8a2f4c89..00000000 --- a/python/src/numerous/apps.py +++ /dev/null @@ -1,155 +0,0 @@ -"""Define applications with a dataclass-like interface.""" - -from dataclasses import dataclass -from types import MappingProxyType -from typing import Any, Callable, Optional, Type, Union, overload - -from typing_extensions import dataclass_transform - -from numerous.utils import MISSING, AppT - - -class HTML: - def __init__(self, default: str) -> None: - self.default = default - - -def html( # type: ignore[no-untyped-def] # noqa: ANN201, PLR0913 - *, - default: str = MISSING, # type: ignore[assignment] - default_factory: Callable[[], str] = MISSING, # type: ignore[assignment] # noqa: ARG001 - init: bool = True, # noqa: ARG001 - repr: bool = True, # noqa: ARG001, A002 - hash: Optional[bool] = None, # noqa: ARG001, A002 - compare: bool = True, # noqa: ARG001 - metadata: Optional[MappingProxyType[str, Any]] = None, # noqa: ARG001 - kw_only: bool = MISSING, # type: ignore[assignment] # noqa: ARG001 -): - return HTML(default) - - -DEFAULT_FLOAT_MIN = 0.0 -DEFAULT_FLOAT_MAX = 100.0 - - -class Slider: - def __init__( # noqa: PLR0913 - self, - *, - default: float = MISSING, # type: ignore[assignment] - default_factory: Callable[[], float] = MISSING, # type: ignore[assignment] # noqa: ARG002 - init: bool = True, # noqa: ARG002 - repr: bool = True, # noqa: ARG002, A002 - hash: Optional[bool] = None, # noqa: ARG002, A002 - compare: bool = True, # noqa: ARG002 - metadata: Optional[MappingProxyType[str, Any]] = None, # noqa: ARG002 - kw_only: bool = MISSING, # type: ignore[assignment] # noqa: ARG002 - label: Optional[str] = None, - min_value: float = DEFAULT_FLOAT_MIN, - max_value: float = DEFAULT_FLOAT_MAX, - ) -> None: - self.default = default - self.label = label - self.min_value = min_value - self.max_value = max_value - - -def slider( # type: ignore[no-untyped-def] # noqa: ANN201, PLR0913 - *, - default: float = MISSING, # type: ignore[assignment] - default_factory: Callable[[], float] = MISSING, # type: ignore[assignment] # noqa: ARG001 - init: bool = True, # noqa: ARG001 - repr: bool = True, # noqa: ARG001, A002 - hash: Optional[bool] = None, # noqa: ARG001, A002 - compare: bool = True, # noqa: ARG001 - metadata: Optional[MappingProxyType[str, Any]] = None, # noqa: ARG001 - kw_only: bool = MISSING, # type: ignore[assignment] # noqa: ARG001 - label: Optional[str] = None, - min_value: float = DEFAULT_FLOAT_MIN, - max_value: float = DEFAULT_FLOAT_MAX, -): - return Slider( - default=default, - label=label, - min_value=min_value, - max_value=max_value, - ) - - -class Field: - def __init__( # noqa: PLR0913 - self, - *, - default: Union[str, float] = MISSING, # type: ignore[assignment] - default_factory: Callable[[], Union[str, float]] = MISSING, # type: ignore[assignment] - init: bool = True, # noqa: ARG002 - repr: bool = True, # noqa: ARG002, A002 - hash: Optional[bool] = None, # noqa: ARG002, A002 - compare: bool = True, # noqa: ARG002 - metadata: Optional[MappingProxyType[str, Any]] = None, # noqa: ARG002 - kw_only: bool = MISSING, # type: ignore[assignment] # noqa: ARG002 - label: Optional[str] = None, - ) -> None: - if default is MISSING and default_factory is not MISSING: # type: ignore[comparison-overlap] - default = default_factory() - self.default = default - self.label = label - - -def field( # type: ignore[no-untyped-def] # noqa: ANN201, PLR0913 - *, - default: Union[str, float] = MISSING, # type: ignore[assignment] - default_factory: Callable[[], float] = MISSING, # type: ignore[assignment] - init: bool = True, # noqa: ARG001 - repr: bool = True, # noqa: ARG001, A002 - hash: Optional[bool] = None, # noqa: ARG001, A002 - compare: bool = True, # noqa: ARG001 - metadata: Optional[MappingProxyType[str, Any]] = None, # noqa: ARG001 - kw_only: bool = MISSING, # type: ignore[assignment] # noqa: ARG001 - label: Optional[str] = None, -): - return Field(default=default, default_factory=default_factory, label=label) - - -@dataclass_transform() -def container(cls: Type[AppT]) -> Type[AppT]: - """Define a container.""" - cls.__container__ = True # type: ignore[attr-defined] - return dataclass(cls) - - -def action(action: Callable[[AppT], Any]) -> Callable[[AppT], Any]: - """Define an action.""" - action.__action__ = True # type: ignore[attr-defined] - return action - - -@overload -def app(cls: Type[AppT]) -> Type[AppT]: ... - - -@overload -def app(title: str = ...) -> Callable[[Type[AppT]], Type[AppT]]: ... - - -def app( - *args: Any, - **kwargs: Any, -) -> Union[Type[AppT], Callable[[Type[AppT]], Type[AppT]]]: - invalid_error_message = "Invalid @app usage" - if len(args) == 1 and not kwargs: - return app_decorator()(args[0]) - if len(args) == 0 and "title" in kwargs: - return app_decorator(**kwargs) - raise ValueError(invalid_error_message) - - -def app_decorator(**kwargs: dict[str, Any]) -> Callable[[Type[AppT]], Type[AppT]]: - @dataclass_transform(field_specifiers=(field, html, slider)) - def decorator(cls: Type[AppT]) -> Type[AppT]: - cls.__numerous_app__ = True # type: ignore[attr-defined] - if title := kwargs.get("title"): - cls.__title__ = title # type: ignore[attr-defined] - return dataclass(cls) - - return decorator diff --git a/python/src/numerous/collection/collection.py b/python/src/numerous/collection/collection.py deleted file mode 100644 index 46e79311..00000000 --- a/python/src/numerous/collection/collection.py +++ /dev/null @@ -1,18 +0,0 @@ -"""Get or create a collection by name.""" - -from typing import Optional - -from numerous._client._get_client import get_client -from numerous.collection.numerous_collection import NumerousCollection - -from ._client import Client - - -def collection( - collection_key: str, _client: Optional[Client] = None -) -> NumerousCollection: - """Get or create a collection by name.""" - if _client is None: - _client = get_client() - collection_ref = _client.get_collection_reference(collection_key) - return NumerousCollection(collection_ref, _client) diff --git a/python/src/numerous/collection/__init__.py b/python/src/numerous/collections/__init__.py similarity index 100% rename from python/src/numerous/collection/__init__.py rename to python/src/numerous/collections/__init__.py diff --git a/python/src/numerous/collection/_client.py b/python/src/numerous/collections/_client.py similarity index 100% rename from python/src/numerous/collection/_client.py rename to python/src/numerous/collections/_client.py diff --git a/python/src/numerous/collections/collection.py b/python/src/numerous/collections/collection.py new file mode 100644 index 00000000..ff3e6f3c --- /dev/null +++ b/python/src/numerous/collections/collection.py @@ -0,0 +1,27 @@ +"""Collection API.""" + +from typing import Optional + +from numerous._client._get_client import get_client +from numerous.collections.numerous_collection import NumerousCollection + +from ._client import Client + + +def collection( + collection_key: str, _client: Optional[Client] = None +) -> NumerousCollection: + """Get or create a collection by name. + + Args: + collection_key: The unique identifier for the collection. + _client: Optional client instance. If not provided, the default client will be used. + + Returns: + NumerousCollection: An instance of NumerousCollection representing the requested collection. + + """ + if _client is None: + _client = get_client() + collection_ref = _client.get_collection_reference(collection_key) + return NumerousCollection(collection_ref, _client) diff --git a/python/src/numerous/collection/exceptions.py b/python/src/numerous/collections/exceptions.py similarity index 100% rename from python/src/numerous/collection/exceptions.py rename to python/src/numerous/collections/exceptions.py diff --git a/python/src/numerous/collection/numerous_collection.py b/python/src/numerous/collections/numerous_collection.py similarity index 72% rename from python/src/numerous/collection/numerous_collection.py rename to python/src/numerous/collections/numerous_collection.py index ff29c05c..e030f94f 100644 --- a/python/src/numerous/collection/numerous_collection.py +++ b/python/src/numerous/collections/numerous_collection.py @@ -1,21 +1,36 @@ -"""Class for working with numerous collections.""" +"""Module for working with numerous collections.""" from typing import Iterator, Optional -from numerous.collection._client import Client -from numerous.collection.numerous_document import NumerousDocument +from numerous.collections._client import Client +from numerous.collections.numerous_document import NumerousDocument from numerous.generated.graphql.fragments import CollectionReference from numerous.generated.graphql.input_types import TagInput class NumerousCollection: + """This class represents a collection in Numerous. + + Args: + collection_ref: The collection reference. + _client: The client instance. + """ def __init__(self, collection_ref: CollectionReference, _client: Client) -> None: + self.key = collection_ref.key self.id = collection_ref.id self._client = _client def collection(self, collection_key: str) -> Optional["NumerousCollection"]: - """Get or create a collection by name.""" + """Get or create a nested collection by name. + + Args: + collection_key: The unique identifier for the collection. + + Returns: + NumerousCollection: An instance of NumerousCollection representing the requested collection. + + """ collection_ref = self._client.get_collection_reference( collection_key=collection_key, parent_collection_id=self.id ) @@ -28,9 +43,11 @@ def document(self, key: str) -> NumerousDocument: """ Get or create a document by key. - Attributes - ---------- - key (str): The key of the document. + Args: + key: The unique identifier for the document. + + Returns: + NumerousDocument: An instance of NumerousDocument representing the requested document. """ numerous_doc_ref = self._client.get_collection_document(self.key, key) @@ -52,17 +69,12 @@ def documents( """ Retrieve documents from the collection, filtered by a tag key and value. - Parameters - ---------- - tag_key : Optional[str] - The key of the tag used to filter documents (optional). - tag_value : Optional[str] - The value of the tag used to filter documents (optional). + Args: + tag_key: The key of the tag used to filter documents (optional). + tag_value: The value of the tag used to filter documents (optional). - Yields - ------ - NumerousDocument - Yields NumerousDocument objects from the collection. + Yields: + NumerousDocument: Yields NumerousDocument objects from the collection. """ end_cursor = "" @@ -93,10 +105,8 @@ def collections(self) -> Iterator["NumerousCollection"]: """ Retrieve nested collections from the collection. - Yields - ------ - NumerousCollection - Yields NumerousCollection objects. + Yields: + NumerousCollection: Yields NumerousCollection objects. """ end_cursor = "" diff --git a/python/src/numerous/collection/numerous_document.py b/python/src/numerous/collections/numerous_document.py similarity index 86% rename from python/src/numerous/collection/numerous_document.py rename to python/src/numerous/collections/numerous_document.py index f4d54f64..681e8e63 100644 --- a/python/src/numerous/collection/numerous_document.py +++ b/python/src/numerous/collections/numerous_document.py @@ -2,7 +2,7 @@ from typing import Any, Optional -from numerous.collection._client import Client +from numerous.collections._client import Client from numerous.generated.graphql.fragments import CollectionDocumentReference from numerous.generated.graphql.input_types import TagInput from numerous.jsonbase64 import base64_to_dict, dict_to_base64 @@ -12,10 +12,9 @@ class NumerousDocument: """ Represents a document in a Numerous collection. - Attributes - ---------- - key (str): The key of the document. - collection_info tuple[str, str]: The id + Args: + key: The key of the document. + collection_info: The id and key of collection document belongs to. and key of collection document belongs to. data (Optional[dict[str, Any]]): The data of the document. id (Optional[str]): The unique identifier of the document. @@ -48,12 +47,20 @@ def __init__( @property def exists(self) -> bool: - """Check if the document exists.""" + """Check if the document exists. + + Returns: + bool: True if the document exists, False otherwise. + """ return self.document_id is not None @property def tags(self) -> dict[str, str]: - """Get the tags for the document.""" + """Get the tags for the document. + + Returns: + dict[str, str]: The tags for the document. + """ if self.document_id is not None: return self._tags @@ -65,11 +72,9 @@ def set(self, data: dict[str, Any]) -> None: Set the data for the document. Args: - ---- - data (dict[str, Any]): The data to set for the document. + data: The data to set for the document. Raises: - ------ ValueError: If the document data setting fails. """ @@ -88,12 +93,10 @@ def get(self) -> Optional[dict[str, Any]]: """ Get the data of the document. - Returns - ------- + Returns: dict[str, Any]: The data of the document. - Raises - ------ + Raises: ValueError: If the document does not exist. """ @@ -107,8 +110,7 @@ def delete(self) -> None: """ Delete the document. - Raises - ------ + Raises: ValueError: If the document does not exist or deletion failed. """ @@ -131,12 +133,10 @@ def tag(self, key: str, value: str) -> None: Add a tag to the document. Args: - ---- - key (str): The tag key. - value (str): The tag value. + key: The tag key. + value: The tag value. Raises: - ------ ValueError: If the document does not exist. """ @@ -156,11 +156,9 @@ def tag_delete(self, tag_key: str) -> None: Delete a tag from the document. Args: - ---- - tag_key (str): The key of the tag to delete. + tag_key: The key of the tag to delete. Raises: - ------ ValueError: If the document does not exist. """ @@ -176,7 +174,11 @@ def tag_delete(self, tag_key: str) -> None: self._tags = {tag.key: tag.value for tag in tagged_document.tags} def _fetch_data(self, document_key: str) -> None: - """Fetch the data from the server.""" + """Fetch the data from the server. + + Args: + document_key: The key of the document to fetch. + """ if self.document_id is not None: document = self._client.get_collection_document( self.collection_key, document_key diff --git a/python/src/numerous/updates.py b/python/src/numerous/updates.py deleted file mode 100644 index 43d20fbc..00000000 --- a/python/src/numerous/updates.py +++ /dev/null @@ -1,180 +0,0 @@ -"""Handling of updates sent to the tool session instance.""" - -import inspect -import logging -from types import MethodType -from typing import Any, Callable, Optional, Union - -from numerous.generated.graphql import Updates -from numerous.generated.graphql.updates import ( - UpdatesToolSessionEventToolSessionActionTriggered, - UpdatesToolSessionEventToolSessionElementUpdated, - UpdatesToolSessionEventToolSessionElementUpdatedElementHTMLElement, - UpdatesToolSessionEventToolSessionElementUpdatedElementNumberField, - UpdatesToolSessionEventToolSessionElementUpdatedElementSliderElement, - UpdatesToolSessionEventToolSessionElementUpdatedElementTextField, -) - - -log = logging.getLogger(__name__) - - -class ElementUpdateError(Exception): - def __init__(self, element_name: str, instance_element_name: str) -> None: - super().__init__( - f"expected name {element_name} but instead found {instance_element_name}", - ) - - -class UpdateHandler: - """Updates app instances according to events sent to the app session.""" - - def __init__(self, instance: object) -> None: - self._instance = instance - self._update_handlers = _find_instance_update_handlers(instance) - self._actions = self._find_actions(instance) - - def _find_actions(self, instance: object) -> dict[str, Callable[[], Any]]: - methods = inspect.getmembers(instance, predicate=inspect.ismethod) - return { - name: method - for name, method in methods - if getattr(method, "__action__", True) - } - - def handle_update(self, updates: Updates) -> None: - """Handle an update for the tool session.""" - event = updates.tool_session_event - if isinstance(event, UpdatesToolSessionEventToolSessionElementUpdated): - self._handle_element_updated(event) - elif isinstance(event, UpdatesToolSessionEventToolSessionActionTriggered): - self._handle_action_triggered(event) - else: - log.info("unhandled event %s", event) - - def _handle_element_updated( - self, - event: UpdatesToolSessionEventToolSessionElementUpdated, - ) -> None: - element = event.element - update_value = self._get_element_update_value(event) - - if update_value is not None: - if self._naive_update_element( - self._instance, - element.id, - element.name, - update_value, - ): - log.debug( - "did not update element %s with value %s", - element, - update_value, - ) - else: - log.debug("unexpected update element %s", element) - - if element.id in self._update_handlers: - log.debug("calling update handler for %s", element.name) - self._update_handlers[element.id]() - else: - log.debug("no associated update handler for %s", element.name) - - def _get_element_update_value( - self, - event: UpdatesToolSessionEventToolSessionElementUpdated, - ) -> Union[str, float, None]: - element = event.element - if isinstance( - element, - UpdatesToolSessionEventToolSessionElementUpdatedElementTextField, - ): - return element.text_value - - if isinstance( - element, - UpdatesToolSessionEventToolSessionElementUpdatedElementNumberField, - ): - return element.number_value - - if isinstance( - element, - UpdatesToolSessionEventToolSessionElementUpdatedElementHTMLElement, - ): - return element.html - - if isinstance( - element, - UpdatesToolSessionEventToolSessionElementUpdatedElementSliderElement, - ): - return element.slider_value - - return None - - def _handle_action_triggered( - self, - event: UpdatesToolSessionEventToolSessionActionTriggered, - ) -> None: - action = event.element - if action.name in self._actions: - self._actions[action.name]() - else: - log.warning("no action found for %r", action.name) - - def _naive_update_element( - self, - app_or_container: Any, # noqa: ANN401 - element_id: str, - element_name: str, - value: Any, # noqa: ANN401 - ) -> bool: - element_ids_to_names: dict[str, str] = getattr( - app_or_container, - "__element_ids_to_names__", - None, - ) # type: ignore[assignment] - if element_ids_to_names is None: - return False - - if element_id in element_ids_to_names: - instance_element_name = element_ids_to_names[element_id] - if instance_element_name != element_name: - raise ElementUpdateError(element_name, instance_element_name) - app_or_container.__dict__[element_name] = value - return True - - for child_name in element_ids_to_names.values(): - child = getattr(app_or_container, child_name) - if self._naive_update_element(child, element_id, element_name, value): - return True - - return False - - -def _find_instance_update_handlers(instance: object) -> dict[str, MethodType]: - element_names_to_ids: Optional[dict[str, str]] = getattr( - instance, - "__element_names_to_ids__", - None, - ) - if element_names_to_ids is None: - return {} - - methods = inspect.getmembers(instance, predicate=inspect.ismethod) - element_names_to_handlers = { - name.removesuffix("_updated"): method - for name, method in methods - if name.endswith("_updated") - } - - element_ids_to_handlers = { - element_names_to_ids[element_name]: handler - for element_name, handler in element_names_to_handlers.items() - if element_name in element_names_to_ids - } - - for element_name in element_names_to_ids: - child = getattr(instance, element_name, None) - element_ids_to_handlers.update(_find_instance_update_handlers(child)) - - return element_ids_to_handlers diff --git a/python/tests/test_collections.py b/python/tests/test_collections.py index 0aee4dd8..79b9d4f9 100644 --- a/python/tests/test_collections.py +++ b/python/tests/test_collections.py @@ -4,8 +4,8 @@ from numerous import collection from numerous._client._graphql_client import COLLECTED_OBJECTS_NUMBER, GraphQLClient -from numerous.collection.exceptions import ParentCollectionNotFoundError -from numerous.collection.numerous_document import NumerousDocument +from numerous.collections.exceptions import ParentCollectionNotFoundError +from numerous.collections.numerous_document import NumerousDocument from numerous.generated.graphql.client import Client as GQLClient from numerous.generated.graphql.collection_collections import CollectionCollections from numerous.generated.graphql.collection_create import CollectionCreate diff --git a/src/numerous/collection/collection.py b/src/numerous/collection/collection.py new file mode 100644 index 00000000..0519ecba --- /dev/null +++ b/src/numerous/collection/collection.py @@ -0,0 +1 @@ + \ No newline at end of file