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.
Files changed (62) hide show
  1. nodekit/.DS_Store +0 -0
  2. nodekit/__init__.py +53 -0
  3. nodekit/_internal/__init__.py +0 -0
  4. nodekit/_internal/ops/__init__.py +0 -0
  5. nodekit/_internal/ops/build_site/__init__.py +117 -0
  6. nodekit/_internal/ops/build_site/harness.j2 +158 -0
  7. nodekit/_internal/ops/concat.py +51 -0
  8. nodekit/_internal/ops/open_asset_save_asset.py +125 -0
  9. nodekit/_internal/ops/play/__init__.py +4 -0
  10. nodekit/_internal/ops/play/local_runner/__init__.py +0 -0
  11. nodekit/_internal/ops/play/local_runner/main.py +234 -0
  12. nodekit/_internal/ops/play/local_runner/site-template.j2 +38 -0
  13. nodekit/_internal/ops/save_graph_load_graph.py +131 -0
  14. nodekit/_internal/ops/topological_sorting.py +122 -0
  15. nodekit/_internal/types/__init__.py +0 -0
  16. nodekit/_internal/types/actions/__init__.py +0 -0
  17. nodekit/_internal/types/actions/actions.py +89 -0
  18. nodekit/_internal/types/assets/__init__.py +151 -0
  19. nodekit/_internal/types/cards/__init__.py +85 -0
  20. nodekit/_internal/types/events/__init__.py +0 -0
  21. nodekit/_internal/types/events/events.py +145 -0
  22. nodekit/_internal/types/expressions/__init__.py +0 -0
  23. nodekit/_internal/types/expressions/expressions.py +242 -0
  24. nodekit/_internal/types/graph.py +42 -0
  25. nodekit/_internal/types/node.py +21 -0
  26. nodekit/_internal/types/regions/__init__.py +13 -0
  27. nodekit/_internal/types/sensors/__init__.py +0 -0
  28. nodekit/_internal/types/sensors/sensors.py +156 -0
  29. nodekit/_internal/types/trace.py +17 -0
  30. nodekit/_internal/types/transition.py +68 -0
  31. nodekit/_internal/types/value.py +145 -0
  32. nodekit/_internal/utils/__init__.py +0 -0
  33. nodekit/_internal/utils/get_browser_bundle.py +35 -0
  34. nodekit/_internal/utils/get_extension_from_media_type.py +15 -0
  35. nodekit/_internal/utils/hashing.py +46 -0
  36. nodekit/_internal/utils/iter_assets.py +61 -0
  37. nodekit/_internal/version.py +1 -0
  38. nodekit/_static/nodekit.css +10 -0
  39. nodekit/_static/nodekit.js +59 -0
  40. nodekit/actions/__init__.py +25 -0
  41. nodekit/assets/__init__.py +7 -0
  42. nodekit/cards/__init__.py +15 -0
  43. nodekit/events/__init__.py +30 -0
  44. nodekit/experimental/.DS_Store +0 -0
  45. nodekit/experimental/__init__.py +0 -0
  46. nodekit/experimental/recruitment_services/__init__.py +0 -0
  47. nodekit/experimental/recruitment_services/base.py +77 -0
  48. nodekit/experimental/recruitment_services/mechanical_turk/__init__.py +0 -0
  49. nodekit/experimental/recruitment_services/mechanical_turk/client.py +359 -0
  50. nodekit/experimental/recruitment_services/mechanical_turk/models.py +116 -0
  51. nodekit/experimental/s3.py +219 -0
  52. nodekit/experimental/turk_helper.py +223 -0
  53. nodekit/experimental/visualization/.DS_Store +0 -0
  54. nodekit/experimental/visualization/__init__.py +0 -0
  55. nodekit/experimental/visualization/pointer.py +443 -0
  56. nodekit/expressions/__init__.py +55 -0
  57. nodekit/sensors/__init__.py +25 -0
  58. nodekit/transitions/__init__.py +15 -0
  59. nodekit/values/__init__.py +63 -0
  60. nodekit-0.2.0.dist-info/METADATA +221 -0
  61. nodekit-0.2.0.dist-info/RECORD +62 -0
  62. 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