Skip to content
This repository has been archived by the owner on Sep 26, 2024. It is now read-only.

Commit

Permalink
Merge pull request #1241 from interval/async-metadata-props
Browse files Browse the repository at this point in the history
Allow all `io.display.metadata` `data` item props to be async (except `label`)
  • Loading branch information
jacobmischka authored May 2, 2023
2 parents 03d9cc6 + 48965c1 commit 9869650
Show file tree
Hide file tree
Showing 9 changed files with 210 additions and 30 deletions.
20 changes: 20 additions & 0 deletions src/demos/basic.py
Original file line number Diff line number Diff line change
Expand Up @@ -599,13 +599,33 @@ async def io_display_metadata(io: IO):
default_value="list",
)

def sync_fn():
return "Called it"

async def async_fn():
await asyncio.sleep(1.5)
return "Done!"

async def another():
await asyncio.sleep(2)
return "Slept!"

async def raises_error():
raise Exception("Oops!")

await io.display.metadata(
"User info",
layout=layout,
data=[
{"label": "Name", "value": "Alex"},
{"label": "Email", "value": "[email protected]"},
{"label": "Not defined"},
{"label": "Friends", "value": 24},
{"label": "None", "value": None},
{"label": "Function", "value": sync_fn},
{"label": "Async function", "value": async_fn},
{"label": "Raises an error", "value": raises_error},
{"label": "Task", "value": another()},
],
)

Expand Down
26 changes: 16 additions & 10 deletions src/interval_sdk/classes/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -137,26 +137,32 @@ async def set_state(self, value: Any):
try:
parsed = parse_obj_as(self.schema.state, dict_keys_to_snake(value))
if self._handle_state_change:
self.instance.props = await self._handle_state_change(
parsed,
parse_obj_as(
self.schema.props,
dict_keys_to_snake(self.instance.props),
),
await self.set_props(
await self._handle_state_change(
parsed,
parse_obj_as(
self.schema.props,
dict_keys_to_snake(self.instance.props),
),
)
)
elif parsed is not None:
print(
"[Interval] Received state, but no method was defined to handle.",
file=sys.stderr,
)

if self.on_state_change is not None:
# This is definitely callable?
# pylint: disable-next=not-callable
await self.on_state_change()
except ValidationError as err:
print("[Interval] Received invalid state:", value, err, file=sys.stderr)

async def set_props(self, value: Any):
self.instance.props = value

if self.on_state_change is not None:
# This is definitely callable?
# pylint: disable-next=not-callable
await self.on_state_change()

def parse_return_value(self, value: Any):
return_schema = self.schema.returns
if self.instance.is_multiple:
Expand Down
82 changes: 77 additions & 5 deletions src/interval_sdk/classes/io.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import asyncio
import base64
from inspect import isawaitable
import sys
from dataclasses import dataclass
from datetime import date, datetime, time
Expand All @@ -18,6 +19,11 @@
from typing_extensions import Never
from urllib.parse import ParseResult, urlparse

from pydantic.fields import Undefined

from interval_sdk.classes.logger import Logger
from interval_sdk.superjson.transformer import UNDEFINED

from ..io_schema import (
ButtonItem,
ButtonItemModel,
Expand Down Expand Up @@ -144,6 +150,7 @@ class IO:
@dataclass
class Input:
_renderer: ComponentRenderer
_logger: Logger
_display_resolves_immediately: Optional[bool]

def text(
Expand Down Expand Up @@ -536,6 +543,7 @@ def get_value(val: InnerFileModel) -> IntervalFile:
@dataclass
class Select:
_renderer: ComponentRenderer
_logger: Logger
_display_resolves_immediately: Optional[bool]

def table(
Expand Down Expand Up @@ -863,6 +871,7 @@ def get_value(
@dataclass
class Display:
_renderer: ComponentRenderer
_logger: Logger
_display_resolves_immediately: Optional[bool]

def code(
Expand Down Expand Up @@ -946,15 +955,66 @@ def metadata(
data: Iterable[MetaItemDefinition],
layout: MetadataLayout = "grid",
) -> DisplayIOPromise[Literal["DISPLAY_METADATA"], None]:
eventual_props = ["value", "url", "image", "route", "params"]

new_data: list[MetaItemDefinition] = []
model_data: list[MetaItemDefinitionModel] = []

for item in data:
new_item: MetaItemDefinition = {"label": item["label"]}
model_item = MetaItemDefinitionModel(label=item["label"])

for prop in eventual_props:
if prop in item:
prop_val = item[prop]
if callable(prop_val):
prop_val = prop_val()

new_item[prop] = prop_val

if isawaitable(prop_val):
model_item.__setattr__(prop, UNDEFINED)
elif prop_val is not None:
model_item.__setattr__(prop, prop_val)

new_data.append(new_item)
model_data.append(model_item)

c = Component(
method_name="DISPLAY_METADATA",
label=label,
initial_props=DisplayMetadataProps(
layout=layout,
data=[MetaItemDefinitionModel.parse_obj(item) for item in data],
data=model_data,
),
display_resolves_immediately=self._display_resolves_immediately,
)

loop = asyncio.get_running_loop()
for i, item in enumerate(new_data):
for prop in eventual_props:
if prop in item and isawaitable(item[prop]):

async def handle_wait(
item: MetaItemDefinition,
prop: str,
i: int,
):
try:
value = await item[prop]
item[prop] = value
model_data[i].__setattr__(prop, value)
await c.set_props(
DisplayMetadataProps(layout=layout, data=model_data)
)
except Exception as err:
self._logger.error(
f'Error updating metadata field "{prop}" with result from async task:',
err,
)

_task = loop.create_task(handle_wait(item, prop, i))

return DisplayIOPromise(c, renderer=self._renderer)

def object(
Expand Down Expand Up @@ -1289,6 +1349,7 @@ def video(
@dataclass
class Experimental:
_renderer: ComponentRenderer
_logger: Logger
_display_resolves_immediately: Optional[bool]

def spreadsheet(
Expand All @@ -1314,6 +1375,7 @@ def spreadsheet(
)
return InputIOPromise(c, renderer=self._renderer)

_logger: Logger
_renderer: ComponentRenderer
_display_resolves_immediately: Optional[bool]
input: Input
Expand All @@ -1325,21 +1387,31 @@ def __init__(
self,
renderer: ComponentRenderer,
*,
logger: Logger,
display_resolves_immediately: Optional[bool] = None,
):
self._logger = logger
self._renderer = renderer
self._display_resolves_immediately = display_resolves_immediately
self.input = self.Input(
renderer, _display_resolves_immediately=display_resolves_immediately
renderer,
_logger=logger,
_display_resolves_immediately=display_resolves_immediately,
)
self.select = self.Select(
renderer, _display_resolves_immediately=display_resolves_immediately
renderer,
_logger=logger,
_display_resolves_immediately=display_resolves_immediately,
)
self.display = self.Display(
renderer, _display_resolves_immediately=display_resolves_immediately
renderer,
_logger=logger,
_display_resolves_immediately=display_resolves_immediately,
)
self.experimental = self.Experimental(
renderer, _display_resolves_immediately=display_resolves_immediately
renderer,
_logger=logger,
_display_resolves_immediately=display_resolves_immediately,
)

def confirm(
Expand Down
1 change: 1 addition & 0 deletions src/interval_sdk/classes/io_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ def __init__(

self.io = IO(
self.render_components,
logger=logger,
display_resolves_immediately=display_resolves_immediately,
)

Expand Down
9 changes: 4 additions & 5 deletions src/interval_sdk/classes/layout.py
Original file line number Diff line number Diff line change
@@ -1,15 +1,14 @@
import json
from dataclasses import dataclass
from typing import Any, Callable, Optional, Union
from typing import Optional

from typing_extensions import Literal, TypeAlias, Awaitable
from typing_extensions import Literal, TypeAlias

from pydantic import Field

from ..classes.io_promise import DisplayIOPromise
from ..types import BaseModel
from ..io_schema import ButtonItem, ButtonItemModel, IORender
from ..util import dump_snake_obj, json_loads_camel, snake_to_camel
from ..util import Eventual

PageLayoutKey = Literal["title", "description", "children", "menuItems"]

Expand All @@ -22,7 +21,7 @@ class PageError(BaseModel):
stack: Optional[str] = None


EventualStr: TypeAlias = Union[str, Awaitable[str], Callable[[], Awaitable[str]]]
EventualStr: TypeAlias = Eventual[str]


@dataclass
Expand Down
11 changes: 6 additions & 5 deletions src/interval_sdk/io_schema.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@
GenericModel,
)
from .util import (
Eventual,
ObjectLiteral,
json_dumps_strip_none,
json_loads_strip_none,
Expand Down Expand Up @@ -676,11 +677,11 @@ class DisplayLinkProps(BaseModel):

class MetaItemDefinition(TypedDict):
label: str
value: NotRequired[Optional[ObjectLiteral]]
url: NotRequired[str]
image: NotRequired[ImageDefinition]
route: NotRequired[str]
params: NotRequired[SerializableRecord]
value: NotRequired[Optional[Eventual[ObjectLiteral]]]
url: NotRequired[Eventual[str]]
image: NotRequired[Eventual[ImageDefinition]]
route: NotRequired[Eventual[str]]
params: NotRequired[Eventual[SerializableRecord]]


class MetaItemDefinitionModel(BaseModel):
Expand Down
13 changes: 12 additions & 1 deletion src/interval_sdk/superjson/transformer.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@
from typing import Any, Callable, Union
from typing_extensions import TypeAlias, Literal

LeafTypeAnnotation: TypeAlias = Literal["number", "Date", "regexp", "set", "map"]
LeafTypeAnnotation: TypeAlias = Literal[
"number", "Date", "regexp", "set", "map", "undefined"
]

CustomTypeAnnotation: TypeAlias = tuple[Literal["custom"], str]

Expand All @@ -19,7 +21,16 @@
}


class Undefined:
pass


UNDEFINED = Undefined()


def transform_value(value: Any) -> Union[tuple[Any, TypeAnnotation], None]:
if isinstance(value, Undefined):
return (None, "undefined")
if isinstance(value, float):
if math.isinf(value):
return ("Infinity", "number")
Expand Down
16 changes: 15 additions & 1 deletion src/interval_sdk/util.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
import json, re
from typing import Any, Iterable, Mapping, Optional, Tuple, Callable, Union, cast
from typing import (
Any,
Awaitable,
Iterable,
Mapping,
Optional,
Tuple,
Callable,
Union,
cast,
)
from datetime import date, time, datetime
from typing_extensions import TypeAlias, TypeVar
from time import time_ns
Expand Down Expand Up @@ -174,6 +184,10 @@ def json_loads_strip_none(*args, **kwargs) -> Any:
return dict_strip_none(obj)


Eventual: TypeAlias = Union[
T, Awaitable[T], Callable[[], T], Callable[[], Awaitable[T]]
]

Deserializable: TypeAlias = Union[int, float, bool, None, str]
DeserializableRecord: TypeAlias = Mapping[str, Deserializable]
Serializable: TypeAlias = Union[bool, int, float, datetime, date, time, str, None]
Expand Down
Loading

0 comments on commit 9869650

Please sign in to comment.