nodekit 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- nodekit/.DS_Store +0 -0
- nodekit/__init__.py +53 -0
- nodekit/_internal/__init__.py +0 -0
- nodekit/_internal/ops/__init__.py +0 -0
- nodekit/_internal/ops/build_site/__init__.py +117 -0
- nodekit/_internal/ops/build_site/harness.j2 +158 -0
- nodekit/_internal/ops/concat.py +51 -0
- nodekit/_internal/ops/open_asset_save_asset.py +125 -0
- nodekit/_internal/ops/play/__init__.py +4 -0
- nodekit/_internal/ops/play/local_runner/__init__.py +0 -0
- nodekit/_internal/ops/play/local_runner/main.py +234 -0
- nodekit/_internal/ops/play/local_runner/site-template.j2 +38 -0
- nodekit/_internal/ops/save_graph_load_graph.py +131 -0
- nodekit/_internal/ops/topological_sorting.py +122 -0
- nodekit/_internal/types/__init__.py +0 -0
- nodekit/_internal/types/actions/__init__.py +0 -0
- nodekit/_internal/types/actions/actions.py +89 -0
- nodekit/_internal/types/assets/__init__.py +151 -0
- nodekit/_internal/types/cards/__init__.py +85 -0
- nodekit/_internal/types/events/__init__.py +0 -0
- nodekit/_internal/types/events/events.py +145 -0
- nodekit/_internal/types/expressions/__init__.py +0 -0
- nodekit/_internal/types/expressions/expressions.py +242 -0
- nodekit/_internal/types/graph.py +42 -0
- nodekit/_internal/types/node.py +21 -0
- nodekit/_internal/types/regions/__init__.py +13 -0
- nodekit/_internal/types/sensors/__init__.py +0 -0
- nodekit/_internal/types/sensors/sensors.py +156 -0
- nodekit/_internal/types/trace.py +17 -0
- nodekit/_internal/types/transition.py +68 -0
- nodekit/_internal/types/value.py +145 -0
- nodekit/_internal/utils/__init__.py +0 -0
- nodekit/_internal/utils/get_browser_bundle.py +35 -0
- nodekit/_internal/utils/get_extension_from_media_type.py +15 -0
- nodekit/_internal/utils/hashing.py +46 -0
- nodekit/_internal/utils/iter_assets.py +61 -0
- nodekit/_internal/version.py +1 -0
- nodekit/_static/nodekit.css +10 -0
- nodekit/_static/nodekit.js +59 -0
- nodekit/actions/__init__.py +25 -0
- nodekit/assets/__init__.py +7 -0
- nodekit/cards/__init__.py +15 -0
- nodekit/events/__init__.py +30 -0
- nodekit/experimental/.DS_Store +0 -0
- nodekit/experimental/__init__.py +0 -0
- nodekit/experimental/recruitment_services/__init__.py +0 -0
- nodekit/experimental/recruitment_services/base.py +77 -0
- nodekit/experimental/recruitment_services/mechanical_turk/__init__.py +0 -0
- nodekit/experimental/recruitment_services/mechanical_turk/client.py +359 -0
- nodekit/experimental/recruitment_services/mechanical_turk/models.py +116 -0
- nodekit/experimental/s3.py +219 -0
- nodekit/experimental/turk_helper.py +223 -0
- nodekit/experimental/visualization/.DS_Store +0 -0
- nodekit/experimental/visualization/__init__.py +0 -0
- nodekit/experimental/visualization/pointer.py +443 -0
- nodekit/expressions/__init__.py +55 -0
- nodekit/sensors/__init__.py +25 -0
- nodekit/transitions/__init__.py +15 -0
- nodekit/values/__init__.py +63 -0
- nodekit-0.2.0.dist-info/METADATA +221 -0
- nodekit-0.2.0.dist-info/RECORD +62 -0
- nodekit-0.2.0.dist-info/WHEEL +4 -0
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import enum
|
|
2
|
+
import mimetypes
|
|
3
|
+
from abc import ABC
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Literal, Annotated, Union, Self, cast
|
|
6
|
+
|
|
7
|
+
import pydantic
|
|
8
|
+
|
|
9
|
+
from nodekit._internal.utils.hashing import (
|
|
10
|
+
hash_file,
|
|
11
|
+
)
|
|
12
|
+
from nodekit._internal.types.value import (
|
|
13
|
+
SHA256,
|
|
14
|
+
MediaType,
|
|
15
|
+
ImageMediaType,
|
|
16
|
+
VideoMediaType,
|
|
17
|
+
)
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
# %%
|
|
21
|
+
class LocatorTypeEnum(str, enum.Enum):
|
|
22
|
+
FileSystemPath = "FileSystemPath"
|
|
23
|
+
ZipArchiveInnerPath = "ZipArchiveInnerPath"
|
|
24
|
+
RelativePath = "RelativePath"
|
|
25
|
+
URL = "URL"
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class BaseLocator(pydantic.BaseModel, ABC):
|
|
29
|
+
locator_type: LocatorTypeEnum
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class FileSystemPath(BaseLocator):
|
|
33
|
+
"""
|
|
34
|
+
A locator which points to an absolute filepath on the viewer's local file system.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
locator_type: Literal[LocatorTypeEnum.FileSystemPath] = (
|
|
38
|
+
LocatorTypeEnum.FileSystemPath
|
|
39
|
+
)
|
|
40
|
+
path: pydantic.FilePath = pydantic.Field(
|
|
41
|
+
description="The absolute path to the asset file in the local filesystem."
|
|
42
|
+
)
|
|
43
|
+
|
|
44
|
+
@pydantic.field_validator("path", mode="after")
|
|
45
|
+
def ensure_path_absolute(cls, path: Path) -> Path:
|
|
46
|
+
return path.resolve()
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ZipArchiveInnerPath(BaseLocator):
|
|
50
|
+
locator_type: Literal[LocatorTypeEnum.ZipArchiveInnerPath] = (
|
|
51
|
+
LocatorTypeEnum.ZipArchiveInnerPath
|
|
52
|
+
)
|
|
53
|
+
zip_archive_path: pydantic.FilePath = pydantic.Field(
|
|
54
|
+
description="The path to the zip archive file on the local filesystem"
|
|
55
|
+
)
|
|
56
|
+
inner_path: Path = pydantic.Field(
|
|
57
|
+
description="The internal path within the zip archive to the asset file."
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
@pydantic.field_validator("zip_archive_path", mode="after")
|
|
61
|
+
def ensure_zip_path_absolute(cls, path: Path) -> Path:
|
|
62
|
+
return path.resolve()
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class RelativePath(BaseLocator):
|
|
66
|
+
"""
|
|
67
|
+
A locator which points to a relative path on the viewer's local file system.
|
|
68
|
+
This is useful for assets that are bundled alongside a graph file, e.g., in a zip archive.
|
|
69
|
+
The viewer must resolve the relative path against a known base path.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
locator_type: Literal[LocatorTypeEnum.RelativePath] = LocatorTypeEnum.RelativePath
|
|
73
|
+
relative_path: Path = pydantic.Field(
|
|
74
|
+
description="The relative path to the asset file in the local filesystem."
|
|
75
|
+
)
|
|
76
|
+
|
|
77
|
+
@pydantic.field_validator("relative_path", mode="after")
|
|
78
|
+
def ensure_path_not_absolute(cls, path: Path) -> Path:
|
|
79
|
+
if path.is_absolute():
|
|
80
|
+
raise ValueError("RelativePath must be a relative path, got absolute path.")
|
|
81
|
+
return path
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
class URL(BaseLocator):
|
|
85
|
+
locator_type: Literal[LocatorTypeEnum.URL] = LocatorTypeEnum.URL
|
|
86
|
+
url: str = pydantic.Field(
|
|
87
|
+
description="The URL to the asset file. May be a relative or absolute URL."
|
|
88
|
+
)
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
type AssetLocator = Annotated[
|
|
92
|
+
Union[FileSystemPath, ZipArchiveInnerPath, RelativePath, URL],
|
|
93
|
+
pydantic.Field(discriminator="locator_type"),
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# %%
|
|
98
|
+
class BaseAsset(pydantic.BaseModel):
|
|
99
|
+
"""
|
|
100
|
+
An Asset is:
|
|
101
|
+
- An identifier for an asset file (its SHA-256 hash and media type)
|
|
102
|
+
- A locator of bytes that are claimed to hash to the identifier.
|
|
103
|
+
"""
|
|
104
|
+
|
|
105
|
+
sha256: SHA256 = pydantic.Field(
|
|
106
|
+
description="The SHA-256 hash of the asset file, as a hex string."
|
|
107
|
+
)
|
|
108
|
+
media_type: MediaType = pydantic.Field(
|
|
109
|
+
description="The IANA media (MIME) type of the asset."
|
|
110
|
+
)
|
|
111
|
+
locator: AssetLocator = pydantic.Field(
|
|
112
|
+
description="A location which is a claimed source of valid bytes for this Asset.",
|
|
113
|
+
)
|
|
114
|
+
|
|
115
|
+
@classmethod
|
|
116
|
+
def from_path(cls, path: Path | str) -> Self:
|
|
117
|
+
"""
|
|
118
|
+
A public convenience method to create an Asset from a file path on the user's local file system.
|
|
119
|
+
This is I/O bound, as it computes the SHA-256 hash of the file.
|
|
120
|
+
"""
|
|
121
|
+
path = Path(path)
|
|
122
|
+
sha256 = hash_file(path)
|
|
123
|
+
guessed_media_type, _ = mimetypes.guess_type(path, strict=True)
|
|
124
|
+
if not guessed_media_type:
|
|
125
|
+
raise ValueError(
|
|
126
|
+
f"Could not determine MIME type for file at {path}\n Does it have a valid file extension?"
|
|
127
|
+
)
|
|
128
|
+
|
|
129
|
+
return cls(
|
|
130
|
+
sha256=sha256,
|
|
131
|
+
media_type=cast(MediaType, guessed_media_type),
|
|
132
|
+
locator=FileSystemPath(path=path),
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class Image(BaseAsset):
|
|
137
|
+
media_type: ImageMediaType = pydantic.Field(
|
|
138
|
+
description="The IANA media (MIME) type of the image file."
|
|
139
|
+
)
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
class Video(BaseAsset):
|
|
143
|
+
media_type: VideoMediaType = pydantic.Field(
|
|
144
|
+
description="The IANA media (MIME) type of the video file."
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
type Asset = Annotated[
|
|
149
|
+
Union[Image, Video],
|
|
150
|
+
pydantic.Field(discriminator="media_type"),
|
|
151
|
+
]
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Literal, Annotated, Union, Dict
|
|
3
|
+
|
|
4
|
+
import pydantic
|
|
5
|
+
|
|
6
|
+
from nodekit._internal.types.assets import Image, Video
|
|
7
|
+
from nodekit._internal.types.value import (
|
|
8
|
+
ColorHexString,
|
|
9
|
+
MarkdownString,
|
|
10
|
+
SpatialSize,
|
|
11
|
+
)
|
|
12
|
+
from nodekit._internal.types.regions import Region
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# %%
|
|
16
|
+
class BaseCard(pydantic.BaseModel, ABC):
|
|
17
|
+
"""
|
|
18
|
+
Cards are visual elements which are placed on the Board.
|
|
19
|
+
They are defined by their type, position, size, and the time range during which they are visible.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
# Identifiers
|
|
23
|
+
card_type: str
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
# %%
|
|
27
|
+
class BaseLeafCard(BaseCard):
|
|
28
|
+
region: Region = pydantic.Field(
|
|
29
|
+
description="The Board region where the card is rendered.",
|
|
30
|
+
default=Region(x=0, y=0, w=0.5, h=0.5),
|
|
31
|
+
)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
# %%
|
|
35
|
+
class ImageCard(BaseLeafCard):
|
|
36
|
+
card_type: Literal["ImageCard"] = "ImageCard"
|
|
37
|
+
image: Image
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# %%
|
|
41
|
+
class VideoCard(BaseLeafCard):
|
|
42
|
+
"""Video stimulus placed on the Board.
|
|
43
|
+
|
|
44
|
+
Attributes:
|
|
45
|
+
video: The video asset to render.
|
|
46
|
+
loop: Whether to loop the video when it ends.
|
|
47
|
+
region: The Board region where the video is rendered.
|
|
48
|
+
"""
|
|
49
|
+
|
|
50
|
+
card_type: Literal["VideoCard"] = "VideoCard"
|
|
51
|
+
video: Video
|
|
52
|
+
loop: bool = pydantic.Field(
|
|
53
|
+
description="Whether to loop the video when it ends.", default=False
|
|
54
|
+
)
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
# %%
|
|
58
|
+
class TextCard(BaseLeafCard):
|
|
59
|
+
card_type: Literal["TextCard"] = "TextCard"
|
|
60
|
+
text: MarkdownString
|
|
61
|
+
font_size: SpatialSize = pydantic.Field(
|
|
62
|
+
default=0.02, description="The height of the em-box, in Board units."
|
|
63
|
+
)
|
|
64
|
+
justification_horizontal: Literal["left", "center", "right"] = "center"
|
|
65
|
+
justification_vertical: Literal["top", "center", "bottom"] = "center"
|
|
66
|
+
text_color: ColorHexString = pydantic.Field(
|
|
67
|
+
default="#000000", validate_default=True
|
|
68
|
+
)
|
|
69
|
+
background_color: ColorHexString = pydantic.Field(
|
|
70
|
+
default="#E6E6E600", # Transparent by default
|
|
71
|
+
description="The background color of the TextCard in hexadecimal format.",
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
# %%
|
|
76
|
+
class CompositeCard(BaseCard):
|
|
77
|
+
card_type: Literal["CompositeCard"] = "CompositeCard"
|
|
78
|
+
children: Dict[str, "Card"]
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
# %%
|
|
82
|
+
type Card = Annotated[
|
|
83
|
+
Union[ImageCard, VideoCard, TextCard, CompositeCard],
|
|
84
|
+
pydantic.Field(discriminator="card_type"),
|
|
85
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
# %%
|
|
2
|
+
import enum
|
|
3
|
+
from typing import Literal, Annotated, Union
|
|
4
|
+
|
|
5
|
+
import pydantic
|
|
6
|
+
|
|
7
|
+
from nodekit._internal.types.value import (
|
|
8
|
+
TimeElapsedMsec,
|
|
9
|
+
NodeId,
|
|
10
|
+
SpatialPoint,
|
|
11
|
+
)
|
|
12
|
+
from nodekit._internal.types.actions.actions import Action
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
# %%
|
|
16
|
+
class EventTypeEnum(str, enum.Enum):
|
|
17
|
+
TraceStartedEvent = "TraceStartedEvent"
|
|
18
|
+
TraceEndedEvent = "TraceEndedEvent"
|
|
19
|
+
|
|
20
|
+
NodeStartedEvent = "NodeStartedEvent"
|
|
21
|
+
ActionTakenEvent = "ActionTakenEvent"
|
|
22
|
+
NodeEndedEvent = "NodeEndedEvent"
|
|
23
|
+
|
|
24
|
+
PointerSampledEvent = "PointerSampledEvent"
|
|
25
|
+
KeySampledEvent = "KeySampledEvent"
|
|
26
|
+
|
|
27
|
+
BrowserContextSampledEvent = "BrowserContextSampledEvent"
|
|
28
|
+
PageSuspendedEvent = "PageSuspendedEvent"
|
|
29
|
+
PageResumedEvent = "PageResumedEvent"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# %%
|
|
33
|
+
class BaseEvent(pydantic.BaseModel):
|
|
34
|
+
event_type: EventTypeEnum
|
|
35
|
+
t: TimeElapsedMsec = pydantic.Field(
|
|
36
|
+
description="The number of elapsed milliseconds since StartedEvent."
|
|
37
|
+
)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# %% System events
|
|
41
|
+
class TraceStartedEvent(BaseEvent):
|
|
42
|
+
event_type: Literal[EventTypeEnum.TraceStartedEvent] = (
|
|
43
|
+
EventTypeEnum.TraceStartedEvent
|
|
44
|
+
)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
class TraceEndedEvent(BaseEvent):
|
|
48
|
+
event_type: Literal[EventTypeEnum.TraceEndedEvent] = EventTypeEnum.TraceEndedEvent
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
class PageSuspendedEvent(BaseEvent):
|
|
52
|
+
"""
|
|
53
|
+
Emitted when a Participant suspends the page (e.g., closes the tab or navigates away).
|
|
54
|
+
"""
|
|
55
|
+
|
|
56
|
+
event_type: Literal[EventTypeEnum.PageSuspendedEvent] = (
|
|
57
|
+
EventTypeEnum.PageSuspendedEvent
|
|
58
|
+
)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
class PageResumedEvent(BaseEvent):
|
|
62
|
+
"""
|
|
63
|
+
Emitted when a Participant returns to the page (e.g., reopens the tab or navigates back).
|
|
64
|
+
"""
|
|
65
|
+
|
|
66
|
+
event_type: Literal[EventTypeEnum.PageResumedEvent] = EventTypeEnum.PageResumedEvent
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class RegionSizePx(pydantic.BaseModel):
|
|
70
|
+
width_px: int
|
|
71
|
+
height_px: int
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
class BrowserContextSampledEvent(BaseEvent):
|
|
75
|
+
event_type: Literal[EventTypeEnum.BrowserContextSampledEvent] = (
|
|
76
|
+
EventTypeEnum.BrowserContextSampledEvent
|
|
77
|
+
)
|
|
78
|
+
user_agent: str = pydantic.Field(
|
|
79
|
+
description="The user agent string of the browser."
|
|
80
|
+
)
|
|
81
|
+
timestamp_client: str = pydantic.Field(
|
|
82
|
+
description="The ISO8601-formatted timestamp that the Participant's browser disclosed at the time of this event."
|
|
83
|
+
)
|
|
84
|
+
device_pixel_ratio: float = pydantic.Field(
|
|
85
|
+
description="The ratio between physical pixels and logical CSS pixels on the device."
|
|
86
|
+
)
|
|
87
|
+
display: RegionSizePx = pydantic.Field(
|
|
88
|
+
description="The size of the Participant's display in physical pixels."
|
|
89
|
+
)
|
|
90
|
+
viewport: RegionSizePx
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
# %%
|
|
94
|
+
class PointerSampledEvent(BaseEvent):
|
|
95
|
+
event_type: Literal[EventTypeEnum.PointerSampledEvent] = (
|
|
96
|
+
EventTypeEnum.PointerSampledEvent
|
|
97
|
+
)
|
|
98
|
+
x: SpatialPoint
|
|
99
|
+
y: SpatialPoint
|
|
100
|
+
kind: Literal["move", "down", "up"]
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
class KeySampledEvent(BaseEvent):
|
|
104
|
+
event_type: Literal[EventTypeEnum.KeySampledEvent] = EventTypeEnum.KeySampledEvent
|
|
105
|
+
key: str
|
|
106
|
+
kind: Literal["down", "up"]
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
# %%
|
|
110
|
+
class BaseNodeEvent(BaseEvent):
|
|
111
|
+
node_id: NodeId
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
class NodeStartedEvent(BaseNodeEvent):
|
|
115
|
+
event_type: Literal[EventTypeEnum.NodeStartedEvent] = EventTypeEnum.NodeStartedEvent
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
class ActionTakenEvent(BaseNodeEvent):
|
|
119
|
+
event_type: Literal[EventTypeEnum.ActionTakenEvent] = EventTypeEnum.ActionTakenEvent
|
|
120
|
+
action: Action
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
class NodeEndedEvent(BaseNodeEvent):
|
|
124
|
+
event_type: Literal[EventTypeEnum.NodeEndedEvent] = EventTypeEnum.NodeEndedEvent
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
# %%
|
|
128
|
+
type Event = Annotated[
|
|
129
|
+
Union[
|
|
130
|
+
# System events flow:
|
|
131
|
+
TraceStartedEvent,
|
|
132
|
+
TraceEndedEvent,
|
|
133
|
+
PageSuspendedEvent,
|
|
134
|
+
PageResumedEvent,
|
|
135
|
+
BrowserContextSampledEvent,
|
|
136
|
+
# Node events:
|
|
137
|
+
NodeStartedEvent,
|
|
138
|
+
ActionTakenEvent,
|
|
139
|
+
NodeEndedEvent,
|
|
140
|
+
# Agent inputs:
|
|
141
|
+
PointerSampledEvent,
|
|
142
|
+
KeySampledEvent,
|
|
143
|
+
],
|
|
144
|
+
pydantic.Field(discriminator="event_type"),
|
|
145
|
+
]
|
|
File without changes
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
from abc import ABC
|
|
2
|
+
from typing import Annotated, Literal, Optional
|
|
3
|
+
|
|
4
|
+
import pydantic
|
|
5
|
+
|
|
6
|
+
from nodekit._internal.types.value import Value, RegisterId
|
|
7
|
+
|
|
8
|
+
# %% Expression
|
|
9
|
+
type LocalVariableName = str
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
class BaseExpression(pydantic.BaseModel, ABC):
|
|
13
|
+
op: str
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class Reg(BaseExpression):
|
|
17
|
+
op: Literal["reg"] = "reg"
|
|
18
|
+
id: RegisterId
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class Local(BaseExpression):
|
|
22
|
+
op: Literal["local"] = "local"
|
|
23
|
+
name: LocalVariableName
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class LastAction(BaseExpression):
|
|
27
|
+
"""
|
|
28
|
+
Evaluates to the last completed Node's Action.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
op: Literal["la"] = "la"
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class GetListItem(BaseExpression):
|
|
35
|
+
"""
|
|
36
|
+
Get an element from a container (Array or Struct).
|
|
37
|
+
`container` must evaluate to an array- or struct-valued result.
|
|
38
|
+
"""
|
|
39
|
+
|
|
40
|
+
op: Literal["gli"] = "gli"
|
|
41
|
+
list: "Expression"
|
|
42
|
+
index: "Expression"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class GetDictValue(BaseExpression):
|
|
46
|
+
"""
|
|
47
|
+
Get a value from a dictionary by key.
|
|
48
|
+
`dict` must evaluate to a dict-valued result.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
op: Literal["gdv"] = "gdv"
|
|
52
|
+
d: "Expression" = pydantic.Field(description="Evaluates to a Dict.")
|
|
53
|
+
key: "Expression"
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class Lit(BaseExpression):
|
|
57
|
+
"""
|
|
58
|
+
Literal value.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
op: Literal["lit"] = "lit"
|
|
62
|
+
value: Value
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
# %% Conditional
|
|
66
|
+
class If(BaseExpression):
|
|
67
|
+
op: Literal["if"] = "if"
|
|
68
|
+
cond: "Expression"
|
|
69
|
+
then: "Expression"
|
|
70
|
+
otherwise: "Expression"
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
# %% Boolean logic
|
|
74
|
+
class Not(BaseExpression):
|
|
75
|
+
op: Literal["not"] = "not"
|
|
76
|
+
operand: "Expression"
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
class Or(BaseExpression):
|
|
80
|
+
op: Literal["or"] = "or"
|
|
81
|
+
# variadic
|
|
82
|
+
args: list["Expression"]
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
class And(BaseExpression):
|
|
86
|
+
op: Literal["and"] = "and"
|
|
87
|
+
# variadic
|
|
88
|
+
args: list["Expression"]
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
# %% Binary comparators
|
|
92
|
+
class BaseCmp(BaseExpression, ABC):
|
|
93
|
+
lhs: "Expression"
|
|
94
|
+
rhs: "Expression"
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class Eq(BaseCmp):
|
|
98
|
+
op: Literal["eq"] = "eq"
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
class Ne(BaseCmp):
|
|
102
|
+
op: Literal["ne"] = "ne"
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
class Gt(BaseCmp):
|
|
106
|
+
op: Literal["gt"] = "gt"
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
class Ge(BaseCmp):
|
|
110
|
+
op: Literal["ge"] = "ge"
|
|
111
|
+
|
|
112
|
+
|
|
113
|
+
class Lt(BaseCmp):
|
|
114
|
+
op: Literal["lt"] = "lt"
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
class Le(BaseCmp):
|
|
118
|
+
op: Literal["le"] = "le"
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
# %% Arithmetic
|
|
122
|
+
class BaseArithmeticOperation(BaseExpression, ABC):
|
|
123
|
+
lhs: "Expression"
|
|
124
|
+
rhs: "Expression"
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
class Add(BaseArithmeticOperation):
|
|
128
|
+
op: Literal["add"] = "add"
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
class Sub(BaseArithmeticOperation):
|
|
132
|
+
op: Literal["sub"] = "sub"
|
|
133
|
+
|
|
134
|
+
|
|
135
|
+
class Mul(BaseArithmeticOperation):
|
|
136
|
+
op: Literal["mul"] = "mul"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
class Div(BaseArithmeticOperation):
|
|
140
|
+
op: Literal["div"] = "div"
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
# %% Array operations
|
|
144
|
+
class ListOp(BaseExpression, ABC):
|
|
145
|
+
# Expression must be array-valued at runtime
|
|
146
|
+
array: "Expression"
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
class Slice(ListOp):
|
|
150
|
+
op: Literal["slice"] = "slice"
|
|
151
|
+
start: "Expression"
|
|
152
|
+
end: Optional["Expression"] = None
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Map(ListOp):
|
|
156
|
+
op: Literal["map"] = "map"
|
|
157
|
+
# The variable name of the current array element.
|
|
158
|
+
cur: LocalVariableName
|
|
159
|
+
# Expression that will be applied to each element of the array.
|
|
160
|
+
func: "Expression"
|
|
161
|
+
|
|
162
|
+
|
|
163
|
+
class Filter(ListOp):
|
|
164
|
+
op: Literal["filter"] = "filter"
|
|
165
|
+
# The variable name of the current array element.
|
|
166
|
+
cur: LocalVariableName
|
|
167
|
+
# Expression that will be applied to each element of the array
|
|
168
|
+
# and interpreted as a predicate.
|
|
169
|
+
predicate: "Expression"
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
class Fold(ListOp):
|
|
173
|
+
op: Literal["fold"] = "fold"
|
|
174
|
+
init: "Expression"
|
|
175
|
+
# The ID of the current cumulant.
|
|
176
|
+
acc: LocalVariableName
|
|
177
|
+
# The variable name of the current array element.
|
|
178
|
+
cur: LocalVariableName
|
|
179
|
+
func: "Expression"
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# =====================
|
|
183
|
+
# Discriminated union
|
|
184
|
+
# =====================
|
|
185
|
+
|
|
186
|
+
type Expression = Annotated[
|
|
187
|
+
Reg
|
|
188
|
+
| Local
|
|
189
|
+
| LastAction
|
|
190
|
+
| GetListItem
|
|
191
|
+
| GetDictValue
|
|
192
|
+
| Lit
|
|
193
|
+
| If
|
|
194
|
+
| Not
|
|
195
|
+
| Or
|
|
196
|
+
| And
|
|
197
|
+
| Eq
|
|
198
|
+
| Ne
|
|
199
|
+
| Gt
|
|
200
|
+
| Ge
|
|
201
|
+
| Lt
|
|
202
|
+
| Le
|
|
203
|
+
| Add
|
|
204
|
+
| Sub
|
|
205
|
+
| Mul
|
|
206
|
+
| Div
|
|
207
|
+
| Slice
|
|
208
|
+
| Map
|
|
209
|
+
| Filter
|
|
210
|
+
| Fold,
|
|
211
|
+
pydantic.Field(discriminator="op"),
|
|
212
|
+
]
|
|
213
|
+
|
|
214
|
+
|
|
215
|
+
# Ensure forward refs are resolved (Pydantic v2)
|
|
216
|
+
for _model in (
|
|
217
|
+
Reg,
|
|
218
|
+
Local,
|
|
219
|
+
LastAction,
|
|
220
|
+
GetListItem,
|
|
221
|
+
GetDictValue,
|
|
222
|
+
Lit,
|
|
223
|
+
If,
|
|
224
|
+
Not,
|
|
225
|
+
Or,
|
|
226
|
+
And,
|
|
227
|
+
Eq,
|
|
228
|
+
Ne,
|
|
229
|
+
Gt,
|
|
230
|
+
Ge,
|
|
231
|
+
Lt,
|
|
232
|
+
Le,
|
|
233
|
+
Add,
|
|
234
|
+
Sub,
|
|
235
|
+
Mul,
|
|
236
|
+
Div,
|
|
237
|
+
Slice,
|
|
238
|
+
Map,
|
|
239
|
+
Filter,
|
|
240
|
+
Fold,
|
|
241
|
+
):
|
|
242
|
+
_model.model_rebuild()
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
from typing import Dict, Literal, Self, Union
|
|
2
|
+
|
|
3
|
+
import pydantic
|
|
4
|
+
|
|
5
|
+
from nodekit import VERSION, Node
|
|
6
|
+
from nodekit._internal.types.transition import Transition
|
|
7
|
+
from nodekit._internal.types.value import NodeId, RegisterId, Value
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# %%
|
|
11
|
+
class Graph(pydantic.BaseModel):
|
|
12
|
+
type: Literal["Graph"] = "Graph"
|
|
13
|
+
nodekit_version: Literal["0.2.0"] = pydantic.Field(
|
|
14
|
+
default=VERSION, validate_default=True
|
|
15
|
+
)
|
|
16
|
+
nodes: Dict[NodeId, Union[Node, "Graph"]]
|
|
17
|
+
transitions: Dict[NodeId, Transition]
|
|
18
|
+
start: NodeId
|
|
19
|
+
registers: Dict[RegisterId, Value] = pydantic.Field(default_factory=dict)
|
|
20
|
+
|
|
21
|
+
@pydantic.model_validator(mode="after")
|
|
22
|
+
def check_graph_is_valid(
|
|
23
|
+
self,
|
|
24
|
+
) -> Self:
|
|
25
|
+
if self.start not in self.nodes:
|
|
26
|
+
raise ValueError(f"Graph start node {self.start} not in nodes.")
|
|
27
|
+
|
|
28
|
+
num_nodes = len(self.nodes)
|
|
29
|
+
if num_nodes == 0:
|
|
30
|
+
raise ValueError("Graph must have at least one node.")
|
|
31
|
+
|
|
32
|
+
# Check the start Node exists:
|
|
33
|
+
if self.start not in self.nodes:
|
|
34
|
+
raise ValueError(f"Start Node {self.start} does not exist in nodes.")
|
|
35
|
+
|
|
36
|
+
# Todo: Each Node must be reachable from the start Node (i.e., no disconnected components)
|
|
37
|
+
|
|
38
|
+
# Todo: Each Go transition must point to an existing Node
|
|
39
|
+
|
|
40
|
+
# Todo: check each Nodes has a path to an End transition
|
|
41
|
+
|
|
42
|
+
return self
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
from typing import Literal
|
|
2
|
+
|
|
3
|
+
import pydantic
|
|
4
|
+
|
|
5
|
+
from nodekit._internal.types.cards import Card
|
|
6
|
+
from nodekit._internal.types.sensors.sensors import Sensor
|
|
7
|
+
from nodekit._internal.types.value import ColorHexString
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
# %%
|
|
11
|
+
class Node(pydantic.BaseModel):
|
|
12
|
+
type: Literal["Node"] = "Node"
|
|
13
|
+
stimulus: Card | None
|
|
14
|
+
sensor: Sensor
|
|
15
|
+
|
|
16
|
+
board_color: ColorHexString = pydantic.Field(
|
|
17
|
+
description='The color of the Board during this Node (the "background color").',
|
|
18
|
+
default="#808080ff",
|
|
19
|
+
validate_default=True,
|
|
20
|
+
)
|
|
21
|
+
hide_pointer: bool = False
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import pydantic
|
|
2
|
+
|
|
3
|
+
from nodekit._internal.types.value import SpatialPoint, SpatialSize, Mask
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
# %%
|
|
7
|
+
class Region(pydantic.BaseModel):
|
|
8
|
+
x: SpatialPoint
|
|
9
|
+
y: SpatialPoint
|
|
10
|
+
w: SpatialSize
|
|
11
|
+
h: SpatialSize
|
|
12
|
+
z_index: int | None = None
|
|
13
|
+
mask: Mask = "rectangle"
|
|
File without changes
|