bead 0.1.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.
- bead/__init__.py +11 -0
- bead/__main__.py +11 -0
- bead/active_learning/__init__.py +15 -0
- bead/active_learning/config.py +231 -0
- bead/active_learning/loop.py +566 -0
- bead/active_learning/models/__init__.py +24 -0
- bead/active_learning/models/base.py +852 -0
- bead/active_learning/models/binary.py +910 -0
- bead/active_learning/models/categorical.py +943 -0
- bead/active_learning/models/cloze.py +862 -0
- bead/active_learning/models/forced_choice.py +956 -0
- bead/active_learning/models/free_text.py +773 -0
- bead/active_learning/models/lora.py +365 -0
- bead/active_learning/models/magnitude.py +835 -0
- bead/active_learning/models/multi_select.py +795 -0
- bead/active_learning/models/ordinal_scale.py +811 -0
- bead/active_learning/models/peft_adapter.py +155 -0
- bead/active_learning/models/random_effects.py +639 -0
- bead/active_learning/selection.py +354 -0
- bead/active_learning/strategies.py +391 -0
- bead/active_learning/trainers/__init__.py +26 -0
- bead/active_learning/trainers/base.py +210 -0
- bead/active_learning/trainers/data_collator.py +172 -0
- bead/active_learning/trainers/dataset_utils.py +261 -0
- bead/active_learning/trainers/huggingface.py +304 -0
- bead/active_learning/trainers/lightning.py +324 -0
- bead/active_learning/trainers/metrics.py +424 -0
- bead/active_learning/trainers/mixed_effects.py +551 -0
- bead/active_learning/trainers/model_wrapper.py +509 -0
- bead/active_learning/trainers/registry.py +104 -0
- bead/adapters/__init__.py +11 -0
- bead/adapters/huggingface.py +61 -0
- bead/behavioral/__init__.py +116 -0
- bead/behavioral/analytics.py +646 -0
- bead/behavioral/extraction.py +343 -0
- bead/behavioral/merging.py +343 -0
- bead/cli/__init__.py +11 -0
- bead/cli/active_learning.py +513 -0
- bead/cli/active_learning_commands.py +779 -0
- bead/cli/completion.py +359 -0
- bead/cli/config.py +624 -0
- bead/cli/constraint_builders.py +286 -0
- bead/cli/deployment.py +859 -0
- bead/cli/deployment_trials.py +493 -0
- bead/cli/deployment_ui.py +332 -0
- bead/cli/display.py +378 -0
- bead/cli/items.py +960 -0
- bead/cli/items_factories.py +776 -0
- bead/cli/list_constraints.py +714 -0
- bead/cli/lists.py +490 -0
- bead/cli/main.py +430 -0
- bead/cli/models.py +877 -0
- bead/cli/resource_loaders.py +621 -0
- bead/cli/resources.py +1036 -0
- bead/cli/shell.py +356 -0
- bead/cli/simulate.py +840 -0
- bead/cli/templates.py +1158 -0
- bead/cli/training.py +1080 -0
- bead/cli/utils.py +614 -0
- bead/cli/workflow.py +1273 -0
- bead/config/__init__.py +68 -0
- bead/config/active_learning.py +1009 -0
- bead/config/config.py +192 -0
- bead/config/defaults.py +118 -0
- bead/config/deployment.py +217 -0
- bead/config/env.py +147 -0
- bead/config/item.py +45 -0
- bead/config/list.py +193 -0
- bead/config/loader.py +149 -0
- bead/config/logging.py +42 -0
- bead/config/model.py +49 -0
- bead/config/paths.py +46 -0
- bead/config/profiles.py +320 -0
- bead/config/resources.py +47 -0
- bead/config/serialization.py +210 -0
- bead/config/simulation.py +206 -0
- bead/config/template.py +238 -0
- bead/config/validation.py +267 -0
- bead/data/__init__.py +65 -0
- bead/data/base.py +87 -0
- bead/data/identifiers.py +97 -0
- bead/data/language_codes.py +61 -0
- bead/data/metadata.py +270 -0
- bead/data/range.py +123 -0
- bead/data/repository.py +358 -0
- bead/data/serialization.py +249 -0
- bead/data/timestamps.py +89 -0
- bead/data/validation.py +349 -0
- bead/data_collection/__init__.py +11 -0
- bead/data_collection/jatos.py +223 -0
- bead/data_collection/merger.py +154 -0
- bead/data_collection/prolific.py +198 -0
- bead/deployment/__init__.py +5 -0
- bead/deployment/distribution.py +402 -0
- bead/deployment/jatos/__init__.py +1 -0
- bead/deployment/jatos/api.py +200 -0
- bead/deployment/jatos/exporter.py +210 -0
- bead/deployment/jspsych/__init__.py +9 -0
- bead/deployment/jspsych/biome.json +44 -0
- bead/deployment/jspsych/config.py +411 -0
- bead/deployment/jspsych/generator.py +598 -0
- bead/deployment/jspsych/package.json +51 -0
- bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
- bead/deployment/jspsych/randomizer.py +299 -0
- bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
- bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
- bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
- bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
- bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
- bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
- bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
- bead/deployment/jspsych/src/plugins/rating.ts +248 -0
- bead/deployment/jspsych/src/slopit/index.ts +9 -0
- bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
- bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
- bead/deployment/jspsych/templates/experiment.css +1 -0
- bead/deployment/jspsych/templates/experiment.js.template +289 -0
- bead/deployment/jspsych/templates/index.html +51 -0
- bead/deployment/jspsych/templates/randomizer.js +241 -0
- bead/deployment/jspsych/templates/randomizer.js.template +313 -0
- bead/deployment/jspsych/trials.py +723 -0
- bead/deployment/jspsych/tsconfig.json +23 -0
- bead/deployment/jspsych/tsup.config.ts +30 -0
- bead/deployment/jspsych/ui/__init__.py +1 -0
- bead/deployment/jspsych/ui/components.py +383 -0
- bead/deployment/jspsych/ui/styles.py +411 -0
- bead/dsl/__init__.py +80 -0
- bead/dsl/ast.py +168 -0
- bead/dsl/context.py +178 -0
- bead/dsl/errors.py +71 -0
- bead/dsl/evaluator.py +570 -0
- bead/dsl/grammar.lark +81 -0
- bead/dsl/parser.py +231 -0
- bead/dsl/stdlib.py +929 -0
- bead/evaluation/__init__.py +13 -0
- bead/evaluation/convergence.py +485 -0
- bead/evaluation/interannotator.py +398 -0
- bead/items/__init__.py +40 -0
- bead/items/adapters/__init__.py +70 -0
- bead/items/adapters/anthropic.py +224 -0
- bead/items/adapters/api_utils.py +167 -0
- bead/items/adapters/base.py +216 -0
- bead/items/adapters/google.py +259 -0
- bead/items/adapters/huggingface.py +1074 -0
- bead/items/adapters/openai.py +323 -0
- bead/items/adapters/registry.py +202 -0
- bead/items/adapters/sentence_transformers.py +224 -0
- bead/items/adapters/togetherai.py +309 -0
- bead/items/binary.py +515 -0
- bead/items/cache.py +558 -0
- bead/items/categorical.py +593 -0
- bead/items/cloze.py +757 -0
- bead/items/constructor.py +784 -0
- bead/items/forced_choice.py +413 -0
- bead/items/free_text.py +681 -0
- bead/items/generation.py +432 -0
- bead/items/item.py +396 -0
- bead/items/item_template.py +787 -0
- bead/items/magnitude.py +573 -0
- bead/items/multi_select.py +621 -0
- bead/items/ordinal_scale.py +569 -0
- bead/items/scoring.py +448 -0
- bead/items/validation.py +723 -0
- bead/lists/__init__.py +30 -0
- bead/lists/balancer.py +263 -0
- bead/lists/constraints.py +1067 -0
- bead/lists/experiment_list.py +286 -0
- bead/lists/list_collection.py +378 -0
- bead/lists/partitioner.py +1141 -0
- bead/lists/stratification.py +254 -0
- bead/participants/__init__.py +73 -0
- bead/participants/collection.py +699 -0
- bead/participants/merging.py +312 -0
- bead/participants/metadata_spec.py +491 -0
- bead/participants/models.py +276 -0
- bead/resources/__init__.py +29 -0
- bead/resources/adapters/__init__.py +19 -0
- bead/resources/adapters/base.py +104 -0
- bead/resources/adapters/cache.py +128 -0
- bead/resources/adapters/glazing.py +508 -0
- bead/resources/adapters/registry.py +117 -0
- bead/resources/adapters/unimorph.py +796 -0
- bead/resources/classification.py +856 -0
- bead/resources/constraint_builders.py +329 -0
- bead/resources/constraints.py +165 -0
- bead/resources/lexical_item.py +223 -0
- bead/resources/lexicon.py +744 -0
- bead/resources/loaders.py +209 -0
- bead/resources/template.py +441 -0
- bead/resources/template_collection.py +707 -0
- bead/resources/template_generation.py +349 -0
- bead/simulation/__init__.py +29 -0
- bead/simulation/annotators/__init__.py +15 -0
- bead/simulation/annotators/base.py +175 -0
- bead/simulation/annotators/distance_based.py +135 -0
- bead/simulation/annotators/lm_based.py +114 -0
- bead/simulation/annotators/oracle.py +182 -0
- bead/simulation/annotators/random.py +181 -0
- bead/simulation/dsl_extension/__init__.py +3 -0
- bead/simulation/noise_models/__init__.py +13 -0
- bead/simulation/noise_models/base.py +42 -0
- bead/simulation/noise_models/random_noise.py +82 -0
- bead/simulation/noise_models/systematic.py +132 -0
- bead/simulation/noise_models/temperature.py +86 -0
- bead/simulation/runner.py +144 -0
- bead/simulation/strategies/__init__.py +23 -0
- bead/simulation/strategies/base.py +123 -0
- bead/simulation/strategies/binary.py +103 -0
- bead/simulation/strategies/categorical.py +123 -0
- bead/simulation/strategies/cloze.py +224 -0
- bead/simulation/strategies/forced_choice.py +127 -0
- bead/simulation/strategies/free_text.py +105 -0
- bead/simulation/strategies/magnitude.py +116 -0
- bead/simulation/strategies/multi_select.py +129 -0
- bead/simulation/strategies/ordinal_scale.py +131 -0
- bead/templates/__init__.py +27 -0
- bead/templates/adapters/__init__.py +17 -0
- bead/templates/adapters/base.py +128 -0
- bead/templates/adapters/cache.py +178 -0
- bead/templates/adapters/huggingface.py +312 -0
- bead/templates/combinatorics.py +103 -0
- bead/templates/filler.py +605 -0
- bead/templates/renderers.py +177 -0
- bead/templates/resolver.py +178 -0
- bead/templates/strategies.py +1806 -0
- bead/templates/streaming.py +195 -0
- bead-0.1.0.dist-info/METADATA +212 -0
- bead-0.1.0.dist-info/RECORD +231 -0
- bead-0.1.0.dist-info/WHEEL +4 -0
- bead-0.1.0.dist-info/entry_points.txt +2 -0
- bead-0.1.0.dist-info/licenses/LICENSE +21 -0
|
@@ -0,0 +1,787 @@
|
|
|
1
|
+
"""Data models for experimental item templates."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
from typing import Literal
|
|
6
|
+
from uuid import UUID
|
|
7
|
+
|
|
8
|
+
from pydantic import Field, ValidationInfo, field_validator
|
|
9
|
+
|
|
10
|
+
from bead.data.base import BeadBaseModel
|
|
11
|
+
|
|
12
|
+
# Type aliases for JSON-serializable metadata values
|
|
13
|
+
type MetadataValue = (
|
|
14
|
+
str | int | float | bool | None | dict[str, MetadataValue] | list[MetadataValue]
|
|
15
|
+
)
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# Factory functions for default values with explicit types
|
|
19
|
+
def _empty_item_element_list() -> list[ItemElement]:
|
|
20
|
+
"""Return empty ItemElement list."""
|
|
21
|
+
return []
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def _empty_item_template_list() -> list[ItemTemplate]:
|
|
25
|
+
"""Return empty ItemTemplate list."""
|
|
26
|
+
return []
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def _empty_metadata_dict() -> dict[str, MetadataValue]:
|
|
30
|
+
"""Return empty metadata dict."""
|
|
31
|
+
return {}
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _empty_display_format_dict() -> dict[str, str | int | float | bool]:
|
|
35
|
+
"""Return empty display format dict."""
|
|
36
|
+
return {}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def _empty_uuid_list() -> list[UUID]:
|
|
40
|
+
"""Return empty UUID list."""
|
|
41
|
+
return []
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# Type aliases for judgment and task types
|
|
45
|
+
JudgmentType = Literal[
|
|
46
|
+
"acceptability", # Linguistic acceptability/grammaticality/naturalness
|
|
47
|
+
"inference", # Semantic relationship (NLI: entailment/neutral/contradiction)
|
|
48
|
+
"similarity", # Semantic similarity/distance/relatedness
|
|
49
|
+
"plausibility", # Likelihood/plausibility of events or statements
|
|
50
|
+
"comprehension", # Understanding/recall of content
|
|
51
|
+
"preference", # Subjective preference between alternatives
|
|
52
|
+
]
|
|
53
|
+
|
|
54
|
+
TaskType = Literal[
|
|
55
|
+
"forced_choice", # Pick exactly one option (UI: radio buttons)
|
|
56
|
+
"multi_select", # Pick one or more options (UI: checkboxes)
|
|
57
|
+
"ordinal_scale", # Value on ordered discrete scale (UI: Likert, slider)
|
|
58
|
+
"magnitude", # Unbounded numeric value (UI: number input)
|
|
59
|
+
"binary", # Yes/no, true/false (UI: toggle, buttons)
|
|
60
|
+
"categorical", # Pick from unordered categories (UI: dropdown, radio)
|
|
61
|
+
"free_text", # Open-ended text (UI: text input, textarea)
|
|
62
|
+
"cloze", # Fill-in-the-blank with unfilled slots (UI: inferred)
|
|
63
|
+
]
|
|
64
|
+
|
|
65
|
+
ElementRefType = Literal["text", "filled_template_ref"]
|
|
66
|
+
|
|
67
|
+
PresentationMode = Literal["static", "self_paced", "timed_sequence"]
|
|
68
|
+
|
|
69
|
+
ChunkingUnit = Literal[
|
|
70
|
+
"character",
|
|
71
|
+
"word",
|
|
72
|
+
"sentence",
|
|
73
|
+
"constituent",
|
|
74
|
+
"custom",
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
ParseType = Literal["constituency", "dependency"]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class ChunkingSpec(BeadBaseModel):
|
|
81
|
+
"""Specification for text segmentation in incremental presentation.
|
|
82
|
+
|
|
83
|
+
Defines how to segment text for self-paced reading or timed sequence
|
|
84
|
+
presentation. Supports character-level, word-level, sentence-level,
|
|
85
|
+
constituent-based (with parsing), or custom boundary segmentation.
|
|
86
|
+
|
|
87
|
+
Attributes
|
|
88
|
+
----------
|
|
89
|
+
unit : ChunkingUnit
|
|
90
|
+
Segmentation unit type. Defaults to "word".
|
|
91
|
+
parse_type : ParseType | None
|
|
92
|
+
Type of parsing for constituent chunking ("constituency" or "dependency").
|
|
93
|
+
constituent_labels : list[str] | None
|
|
94
|
+
Labels for constituent chunking. For constituency parsing, these are
|
|
95
|
+
constituent types (e.g., ["NP", "VP", "S"]). For dependency parsing,
|
|
96
|
+
these are dependency relations (e.g., ["nsubj", "dobj", "root"]).
|
|
97
|
+
parser : Literal["stanza", "spacy"] | None
|
|
98
|
+
Parser library to use for constituent chunking.
|
|
99
|
+
parse_language : str | None
|
|
100
|
+
ISO 639 language code for parser (e.g., "en", "es", "zh").
|
|
101
|
+
custom_boundaries : list[int] | None
|
|
102
|
+
Token indices for custom chunking boundaries.
|
|
103
|
+
|
|
104
|
+
Examples
|
|
105
|
+
--------
|
|
106
|
+
>>> # Word-by-word chunking
|
|
107
|
+
>>> ChunkingSpec(unit="word")
|
|
108
|
+
>>> # Chunk by noun phrases (constituency)
|
|
109
|
+
>>> ChunkingSpec(
|
|
110
|
+
... unit="constituent",
|
|
111
|
+
... parse_type="constituency",
|
|
112
|
+
... constituent_labels=["NP"],
|
|
113
|
+
... parser="stanza",
|
|
114
|
+
... parse_language="en"
|
|
115
|
+
... )
|
|
116
|
+
>>> # Chunk by subjects and objects (dependency)
|
|
117
|
+
>>> ChunkingSpec(
|
|
118
|
+
... unit="constituent",
|
|
119
|
+
... parse_type="dependency",
|
|
120
|
+
... constituent_labels=["nsubj", "dobj"],
|
|
121
|
+
... parser="spacy",
|
|
122
|
+
... parse_language="en"
|
|
123
|
+
... )
|
|
124
|
+
>>> # Custom boundaries at specific token positions
|
|
125
|
+
>>> ChunkingSpec(unit="custom", custom_boundaries=[0, 3, 7, 10])
|
|
126
|
+
"""
|
|
127
|
+
|
|
128
|
+
unit: ChunkingUnit = Field(default="word", description="Segmentation unit type")
|
|
129
|
+
parse_type: ParseType | None = Field(
|
|
130
|
+
default=None, description="Parsing type for constituent chunking"
|
|
131
|
+
)
|
|
132
|
+
constituent_labels: list[str] | None = Field(
|
|
133
|
+
default=None,
|
|
134
|
+
description="Constituent or dependency labels for chunking",
|
|
135
|
+
)
|
|
136
|
+
parser: Literal["stanza", "spacy"] | None = Field(
|
|
137
|
+
default=None, description="Parser library"
|
|
138
|
+
)
|
|
139
|
+
parse_language: str | None = Field(
|
|
140
|
+
default=None, description="ISO 639 language code"
|
|
141
|
+
)
|
|
142
|
+
custom_boundaries: list[int] | None = Field(
|
|
143
|
+
default=None, description="Custom token boundary indices"
|
|
144
|
+
)
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class TimingParams(BeadBaseModel):
|
|
148
|
+
"""Timing parameters for stimulus presentation.
|
|
149
|
+
|
|
150
|
+
Defines timing constraints for timed sequence presentations,
|
|
151
|
+
including per-chunk duration, inter-stimulus intervals, and
|
|
152
|
+
response timeouts.
|
|
153
|
+
|
|
154
|
+
Attributes
|
|
155
|
+
----------
|
|
156
|
+
duration_ms : int | None
|
|
157
|
+
Duration in milliseconds to display each chunk (for timed sequences).
|
|
158
|
+
isi_ms : int | None
|
|
159
|
+
Inter-stimulus interval in milliseconds between chunks.
|
|
160
|
+
timeout_ms : int | None
|
|
161
|
+
Maximum time in milliseconds to wait for response.
|
|
162
|
+
mask_char : str | None
|
|
163
|
+
Character to use for masking non-current chunks (e.g., "_").
|
|
164
|
+
cumulative : bool
|
|
165
|
+
If True, show all previous chunks; if False, show only current chunk.
|
|
166
|
+
|
|
167
|
+
Examples
|
|
168
|
+
--------
|
|
169
|
+
>>> # RSVP (Rapid Serial Visual Presentation)
|
|
170
|
+
>>> TimingParams(
|
|
171
|
+
... duration_ms=250,
|
|
172
|
+
... isi_ms=50,
|
|
173
|
+
... cumulative=False,
|
|
174
|
+
... mask_char="_"
|
|
175
|
+
... )
|
|
176
|
+
>>> # Self-paced with timeout
|
|
177
|
+
>>> TimingParams(timeout_ms=5000, cumulative=True)
|
|
178
|
+
"""
|
|
179
|
+
|
|
180
|
+
duration_ms: int | None = Field(
|
|
181
|
+
default=None, description="Per-chunk display duration (ms)"
|
|
182
|
+
)
|
|
183
|
+
isi_ms: int | None = Field(default=None, description="Inter-stimulus interval (ms)")
|
|
184
|
+
timeout_ms: int | None = Field(default=None, description="Response timeout (ms)")
|
|
185
|
+
mask_char: str | None = Field(default=None, description="Masking character")
|
|
186
|
+
cumulative: bool = Field(
|
|
187
|
+
default=True, description="Show all previous chunks or only current"
|
|
188
|
+
)
|
|
189
|
+
|
|
190
|
+
|
|
191
|
+
class TaskSpec(BeadBaseModel):
|
|
192
|
+
"""Parameters for the response collection task.
|
|
193
|
+
|
|
194
|
+
Specifies task-specific parameters like prompts, options, scale bounds,
|
|
195
|
+
validation rules, etc. The appropriate parameters depend on the task_type
|
|
196
|
+
specified in ItemTemplate. The task_type itself is not included here since
|
|
197
|
+
it's part of the ItemTemplate structure.
|
|
198
|
+
|
|
199
|
+
Attributes
|
|
200
|
+
----------
|
|
201
|
+
prompt : str
|
|
202
|
+
Question or instruction shown to participants.
|
|
203
|
+
scale_bounds : tuple[int, int] | None
|
|
204
|
+
Min and max values for ordinal_scale task.
|
|
205
|
+
scale_labels : dict[int, str] | None
|
|
206
|
+
Optional labels for specific scale points (ordinal_scale).
|
|
207
|
+
options : list[str] | None
|
|
208
|
+
Available options for forced_choice, multi_select, or categorical tasks.
|
|
209
|
+
For forced_choice/multi_select: element names to choose from.
|
|
210
|
+
For categorical: category labels.
|
|
211
|
+
min_selections : int | None
|
|
212
|
+
Minimum number of selections required (multi_select only).
|
|
213
|
+
max_selections : int | None
|
|
214
|
+
Maximum number of selections allowed (multi_select only).
|
|
215
|
+
text_validation_pattern : str | None
|
|
216
|
+
Regular expression pattern for validating free_text responses.
|
|
217
|
+
max_length : int | None
|
|
218
|
+
Maximum character length for free_text responses.
|
|
219
|
+
|
|
220
|
+
Examples
|
|
221
|
+
--------
|
|
222
|
+
>>> # Ordinal scale task (e.g., acceptability rating)
|
|
223
|
+
>>> TaskSpec(
|
|
224
|
+
... prompt="How natural does this sentence sound?",
|
|
225
|
+
... scale_bounds=(1, 7),
|
|
226
|
+
... scale_labels={1: "Very unnatural", 7: "Very natural"}
|
|
227
|
+
... )
|
|
228
|
+
>>> # Categorical task (e.g., NLI)
|
|
229
|
+
>>> TaskSpec(
|
|
230
|
+
... prompt="What is the relationship?",
|
|
231
|
+
... options=["Entailment", "Neutral", "Contradiction"]
|
|
232
|
+
... )
|
|
233
|
+
>>> # Binary task
|
|
234
|
+
>>> TaskSpec(
|
|
235
|
+
... prompt="Is this sentence grammatical?"
|
|
236
|
+
... )
|
|
237
|
+
>>> # Forced choice task (e.g., minimal pair)
|
|
238
|
+
>>> TaskSpec(
|
|
239
|
+
... prompt="Which sounds more natural?",
|
|
240
|
+
... options=["sentence_a", "sentence_b"]
|
|
241
|
+
... )
|
|
242
|
+
>>> # Multi-select task (e.g., select all grammatical)
|
|
243
|
+
>>> TaskSpec(
|
|
244
|
+
... prompt="Select all grammatical sentences:",
|
|
245
|
+
... options=["sent_a", "sent_b", "sent_c"],
|
|
246
|
+
... min_selections=1
|
|
247
|
+
... )
|
|
248
|
+
>>> # Free text task
|
|
249
|
+
>>> TaskSpec(
|
|
250
|
+
... prompt="Who performed the action?",
|
|
251
|
+
... max_length=50
|
|
252
|
+
... )
|
|
253
|
+
"""
|
|
254
|
+
|
|
255
|
+
prompt: str = Field(..., description="Participant prompt/question")
|
|
256
|
+
scale_bounds: tuple[int, int] | None = Field(
|
|
257
|
+
default=None, description="Scale bounds for ordinal_scale task"
|
|
258
|
+
)
|
|
259
|
+
scale_labels: dict[int, str] | None = Field(
|
|
260
|
+
default=None, description="Labels for scale points"
|
|
261
|
+
)
|
|
262
|
+
options: list[str] | None = Field(
|
|
263
|
+
default=None,
|
|
264
|
+
description="Options for forced_choice/multi_select/categorical tasks",
|
|
265
|
+
)
|
|
266
|
+
min_selections: int | None = Field(
|
|
267
|
+
default=None, description="Minimum selections for multi_select task"
|
|
268
|
+
)
|
|
269
|
+
max_selections: int | None = Field(
|
|
270
|
+
default=None, description="Maximum selections for multi_select task"
|
|
271
|
+
)
|
|
272
|
+
text_validation_pattern: str | None = Field(
|
|
273
|
+
default=None, description="Regex pattern for text validation"
|
|
274
|
+
)
|
|
275
|
+
max_length: int | None = Field(default=None, description="Maximum text length")
|
|
276
|
+
|
|
277
|
+
@field_validator("prompt")
|
|
278
|
+
@classmethod
|
|
279
|
+
def validate_prompt(cls, v: str) -> str:
|
|
280
|
+
"""Validate prompt is not empty.
|
|
281
|
+
|
|
282
|
+
Parameters
|
|
283
|
+
----------
|
|
284
|
+
v : str
|
|
285
|
+
Prompt to validate.
|
|
286
|
+
|
|
287
|
+
Returns
|
|
288
|
+
-------
|
|
289
|
+
str
|
|
290
|
+
Validated prompt.
|
|
291
|
+
|
|
292
|
+
Raises
|
|
293
|
+
------
|
|
294
|
+
ValueError
|
|
295
|
+
If prompt is empty or contains only whitespace.
|
|
296
|
+
"""
|
|
297
|
+
if not v or not v.strip():
|
|
298
|
+
raise ValueError("Prompt cannot be empty")
|
|
299
|
+
return v.strip()
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
class PresentationSpec(BeadBaseModel):
|
|
303
|
+
"""Specification of stimulus presentation method.
|
|
304
|
+
|
|
305
|
+
Defines how stimuli are displayed to participants (static, self-paced,
|
|
306
|
+
or timed sequence), including segmentation and timing parameters.
|
|
307
|
+
Separate from judgment specification to maintain clean separation
|
|
308
|
+
of concerns.
|
|
309
|
+
|
|
310
|
+
Attributes
|
|
311
|
+
----------
|
|
312
|
+
mode : PresentationMode
|
|
313
|
+
Presentation mode (static, self_paced, or timed_sequence). Defaults to
|
|
314
|
+
"static".
|
|
315
|
+
chunking : ChunkingSpec
|
|
316
|
+
Chunking specification for incremental presentations. Defaults to
|
|
317
|
+
word-level chunking.
|
|
318
|
+
timing : TimingParams
|
|
319
|
+
Timing parameters for timed presentations. Defaults to cumulative
|
|
320
|
+
display with no fixed durations.
|
|
321
|
+
display_format : dict[str, str | int | float | bool]
|
|
322
|
+
Additional display formatting options.
|
|
323
|
+
|
|
324
|
+
Examples
|
|
325
|
+
--------
|
|
326
|
+
>>> # Static presentation (default)
|
|
327
|
+
>>> PresentationSpec()
|
|
328
|
+
>>> # Self-paced word-by-word reading
|
|
329
|
+
>>> PresentationSpec(
|
|
330
|
+
... mode="self_paced",
|
|
331
|
+
... chunking=ChunkingSpec(unit="word")
|
|
332
|
+
... )
|
|
333
|
+
>>> # Self-paced by noun phrases
|
|
334
|
+
>>> PresentationSpec(
|
|
335
|
+
... mode="self_paced",
|
|
336
|
+
... chunking=ChunkingSpec(
|
|
337
|
+
... unit="constituent",
|
|
338
|
+
... parse_type="constituency",
|
|
339
|
+
... constituent_labels=["NP"],
|
|
340
|
+
... parser="stanza",
|
|
341
|
+
... parse_language="en"
|
|
342
|
+
... )
|
|
343
|
+
... )
|
|
344
|
+
>>> # RSVP (timed sequence)
|
|
345
|
+
>>> PresentationSpec(
|
|
346
|
+
... mode="timed_sequence",
|
|
347
|
+
... chunking=ChunkingSpec(unit="word"),
|
|
348
|
+
... timing=TimingParams(duration_ms=250, isi_ms=50, cumulative=False)
|
|
349
|
+
... )
|
|
350
|
+
"""
|
|
351
|
+
|
|
352
|
+
mode: PresentationMode = Field(default="static", description="Presentation mode")
|
|
353
|
+
chunking: ChunkingSpec = Field(
|
|
354
|
+
default_factory=ChunkingSpec, description="Chunking specification"
|
|
355
|
+
)
|
|
356
|
+
timing: TimingParams = Field(
|
|
357
|
+
default_factory=TimingParams, description="Timing parameters"
|
|
358
|
+
)
|
|
359
|
+
display_format: dict[str, str | int | float | bool] = Field(
|
|
360
|
+
default_factory=_empty_display_format_dict,
|
|
361
|
+
description="Display formatting options",
|
|
362
|
+
)
|
|
363
|
+
|
|
364
|
+
|
|
365
|
+
class ItemElement(BeadBaseModel):
|
|
366
|
+
"""A structured element within an item template.
|
|
367
|
+
|
|
368
|
+
ItemElements represent distinct parts of a complex item,
|
|
369
|
+
such as context, target sentence, question, or response options.
|
|
370
|
+
Elements can be static text or references to filled templates.
|
|
371
|
+
|
|
372
|
+
Attributes
|
|
373
|
+
----------
|
|
374
|
+
element_type : ElementRefType
|
|
375
|
+
Type of element ("text" or "filled_template_ref").
|
|
376
|
+
element_name : str
|
|
377
|
+
Unique name for this element within the item.
|
|
378
|
+
content : str | None
|
|
379
|
+
Static text content (for text elements).
|
|
380
|
+
filled_template_ref_id : UUID | None
|
|
381
|
+
UUID of filled template (for reference elements).
|
|
382
|
+
element_metadata : dict[str, MetadataValue]
|
|
383
|
+
Additional element-specific metadata.
|
|
384
|
+
order : int | None
|
|
385
|
+
Display order for this element (optional).
|
|
386
|
+
|
|
387
|
+
Examples
|
|
388
|
+
--------
|
|
389
|
+
>>> # Text element
|
|
390
|
+
>>> context = ItemElement(
|
|
391
|
+
... element_type="text",
|
|
392
|
+
... element_name="context",
|
|
393
|
+
... content="Mary loves books.",
|
|
394
|
+
... order=1
|
|
395
|
+
... )
|
|
396
|
+
>>> # Template reference element
|
|
397
|
+
>>> target = ItemElement(
|
|
398
|
+
... element_type="filled_template_ref",
|
|
399
|
+
... element_name="target",
|
|
400
|
+
... filled_template_ref_id=UUID("..."),
|
|
401
|
+
... order=2
|
|
402
|
+
... )
|
|
403
|
+
"""
|
|
404
|
+
|
|
405
|
+
element_type: ElementRefType = Field(..., description="Type of element")
|
|
406
|
+
element_name: str = Field(..., description="Unique element name within item")
|
|
407
|
+
content: str | None = Field(default=None, description="Static text content")
|
|
408
|
+
filled_template_ref_id: UUID | None = Field(
|
|
409
|
+
default=None, description="Filled template reference"
|
|
410
|
+
)
|
|
411
|
+
element_metadata: dict[str, MetadataValue] = Field(
|
|
412
|
+
default_factory=_empty_metadata_dict, description="Element-specific metadata"
|
|
413
|
+
)
|
|
414
|
+
order: int | None = Field(
|
|
415
|
+
default=None, description="Display order for this element"
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
@field_validator("element_name")
|
|
419
|
+
@classmethod
|
|
420
|
+
def validate_element_name(cls, v: str) -> str:
|
|
421
|
+
"""Validate element name is not empty.
|
|
422
|
+
|
|
423
|
+
Parameters
|
|
424
|
+
----------
|
|
425
|
+
v : str
|
|
426
|
+
Element name to validate.
|
|
427
|
+
|
|
428
|
+
Returns
|
|
429
|
+
-------
|
|
430
|
+
str
|
|
431
|
+
Validated element name.
|
|
432
|
+
|
|
433
|
+
Raises
|
|
434
|
+
------
|
|
435
|
+
ValueError
|
|
436
|
+
If name is empty or contains only whitespace.
|
|
437
|
+
"""
|
|
438
|
+
if not v or not v.strip():
|
|
439
|
+
raise ValueError("Element name cannot be empty")
|
|
440
|
+
return v.strip()
|
|
441
|
+
|
|
442
|
+
@property
|
|
443
|
+
def is_text(self) -> bool:
|
|
444
|
+
"""Check if this is a text element.
|
|
445
|
+
|
|
446
|
+
Returns
|
|
447
|
+
-------
|
|
448
|
+
bool
|
|
449
|
+
True if element_type is "text".
|
|
450
|
+
"""
|
|
451
|
+
return self.element_type == "text"
|
|
452
|
+
|
|
453
|
+
@property
|
|
454
|
+
def is_template_ref(self) -> bool:
|
|
455
|
+
"""Check if this references a filled template.
|
|
456
|
+
|
|
457
|
+
Returns
|
|
458
|
+
-------
|
|
459
|
+
bool
|
|
460
|
+
True if element_type is "filled_template_ref".
|
|
461
|
+
"""
|
|
462
|
+
return self.element_type == "filled_template_ref"
|
|
463
|
+
|
|
464
|
+
|
|
465
|
+
class ItemTemplate(BeadBaseModel):
|
|
466
|
+
"""Template specification for constructing experimental items.
|
|
467
|
+
|
|
468
|
+
ItemTemplate defines how to construct an experimental item with three
|
|
469
|
+
orthogonal dimensions: what semantic property to measure (judgment_type),
|
|
470
|
+
how to collect the response (task_type), and how to present the stimulus
|
|
471
|
+
(presentation_spec).
|
|
472
|
+
|
|
473
|
+
This is distinct from Template (in bead.resources.structures), which defines
|
|
474
|
+
linguistic structure. ItemTemplate defines experimental structure.
|
|
475
|
+
|
|
476
|
+
Attributes
|
|
477
|
+
----------
|
|
478
|
+
name : str
|
|
479
|
+
Template name (e.g., "acceptability_rating").
|
|
480
|
+
description : str | None
|
|
481
|
+
Human-readable description of this item template.
|
|
482
|
+
judgment_type : JudgmentType
|
|
483
|
+
Semantic property being measured (acceptability, inference, etc.).
|
|
484
|
+
task_type : TaskType
|
|
485
|
+
Response collection method (forced_choice, ordinal_scale, etc.).
|
|
486
|
+
elements : list[ItemElement]
|
|
487
|
+
Elements that compose this item.
|
|
488
|
+
constraints : list[UUID]
|
|
489
|
+
UUIDs of constraints on items (typically model-based).
|
|
490
|
+
task_spec : TaskSpec
|
|
491
|
+
Task-specific parameters (prompt, options, scale bounds, etc.).
|
|
492
|
+
presentation_spec : PresentationSpec
|
|
493
|
+
Specification of how to present stimuli.
|
|
494
|
+
presentation_order : list[str] | None
|
|
495
|
+
Order to present elements (by element_name).
|
|
496
|
+
template_metadata : dict[str, MetadataValue]
|
|
497
|
+
Additional template metadata.
|
|
498
|
+
|
|
499
|
+
Examples
|
|
500
|
+
--------
|
|
501
|
+
>>> # Acceptability judgment with ordinal scale task
|
|
502
|
+
>>> template = ItemTemplate(
|
|
503
|
+
... name="acceptability_rating",
|
|
504
|
+
... judgment_type="acceptability",
|
|
505
|
+
... task_type="ordinal_scale",
|
|
506
|
+
... task_spec=TaskSpec(
|
|
507
|
+
... prompt="How natural is this sentence?",
|
|
508
|
+
... scale_bounds=(1, 7),
|
|
509
|
+
... scale_labels={1: "Very unnatural", 7: "Very natural"}
|
|
510
|
+
... ),
|
|
511
|
+
... presentation_spec=PresentationSpec(mode="static"),
|
|
512
|
+
... elements=[
|
|
513
|
+
... ItemElement(
|
|
514
|
+
... element_type="filled_template_ref",
|
|
515
|
+
... element_name="sentence",
|
|
516
|
+
... filled_template_ref_id=UUID("...")
|
|
517
|
+
... )
|
|
518
|
+
... ]
|
|
519
|
+
... )
|
|
520
|
+
>>> # Minimal pair: acceptability judgment with forced choice task
|
|
521
|
+
>>> minimal_pair = ItemTemplate(
|
|
522
|
+
... name="minimal_pair",
|
|
523
|
+
... judgment_type="acceptability",
|
|
524
|
+
... task_type="forced_choice",
|
|
525
|
+
... elements=[
|
|
526
|
+
... ItemElement(
|
|
527
|
+
... element_type="text", element_name="sent_a", content="Who..."
|
|
528
|
+
... ),
|
|
529
|
+
... ItemElement(
|
|
530
|
+
... element_type="text", element_name="sent_b", content="Whom..."
|
|
531
|
+
... )
|
|
532
|
+
... ],
|
|
533
|
+
... task_spec=TaskSpec(
|
|
534
|
+
... prompt="Which sounds more natural?",
|
|
535
|
+
... options=["sent_a", "sent_b"]
|
|
536
|
+
... ),
|
|
537
|
+
... presentation_spec=PresentationSpec(mode="static")
|
|
538
|
+
... )
|
|
539
|
+
>>> # Odd-man-out: similarity judgment with forced choice task
|
|
540
|
+
>>> odd_man_out = ItemTemplate(
|
|
541
|
+
... name="odd_man_out",
|
|
542
|
+
... judgment_type="similarity",
|
|
543
|
+
... task_type="forced_choice",
|
|
544
|
+
... elements=[...], # 4 elements
|
|
545
|
+
... task_spec=TaskSpec(
|
|
546
|
+
... prompt="Which is most different?",
|
|
547
|
+
... options=["opt_a", "opt_b", "opt_c", "opt_d"]
|
|
548
|
+
... ),
|
|
549
|
+
... presentation_spec=PresentationSpec(mode="static")
|
|
550
|
+
... )
|
|
551
|
+
"""
|
|
552
|
+
|
|
553
|
+
name: str = Field(..., description="Template name")
|
|
554
|
+
description: str | None = Field(default=None, description="Template description")
|
|
555
|
+
judgment_type: JudgmentType = Field(
|
|
556
|
+
..., description="Semantic property being measured"
|
|
557
|
+
)
|
|
558
|
+
task_type: TaskType = Field(..., description="Response collection method")
|
|
559
|
+
elements: list[ItemElement] = Field(
|
|
560
|
+
default_factory=_empty_item_element_list, description="Item elements"
|
|
561
|
+
)
|
|
562
|
+
constraints: list[UUID] = Field(
|
|
563
|
+
default_factory=_empty_uuid_list, description="Constraint UUIDs"
|
|
564
|
+
)
|
|
565
|
+
task_spec: TaskSpec = Field(..., description="Task-specific parameters")
|
|
566
|
+
presentation_spec: PresentationSpec = Field(
|
|
567
|
+
..., description="Presentation specification"
|
|
568
|
+
)
|
|
569
|
+
presentation_order: list[str] | None = Field(
|
|
570
|
+
default=None, description="Element presentation order"
|
|
571
|
+
)
|
|
572
|
+
template_metadata: dict[str, MetadataValue] = Field(
|
|
573
|
+
default_factory=_empty_metadata_dict, description="Additional metadata"
|
|
574
|
+
)
|
|
575
|
+
|
|
576
|
+
@field_validator("name")
|
|
577
|
+
@classmethod
|
|
578
|
+
def validate_name(cls, v: str) -> str:
|
|
579
|
+
"""Validate template name is not empty.
|
|
580
|
+
|
|
581
|
+
Parameters
|
|
582
|
+
----------
|
|
583
|
+
v : str
|
|
584
|
+
Template name to validate.
|
|
585
|
+
|
|
586
|
+
Returns
|
|
587
|
+
-------
|
|
588
|
+
str
|
|
589
|
+
Validated template name.
|
|
590
|
+
|
|
591
|
+
Raises
|
|
592
|
+
------
|
|
593
|
+
ValueError
|
|
594
|
+
If name is empty or contains only whitespace.
|
|
595
|
+
"""
|
|
596
|
+
if not v or not v.strip():
|
|
597
|
+
raise ValueError("Template name cannot be empty")
|
|
598
|
+
return v.strip()
|
|
599
|
+
|
|
600
|
+
@field_validator("elements")
|
|
601
|
+
@classmethod
|
|
602
|
+
def validate_unique_element_names(cls, v: list[ItemElement]) -> list[ItemElement]:
|
|
603
|
+
"""Validate all element names are unique within template.
|
|
604
|
+
|
|
605
|
+
Parameters
|
|
606
|
+
----------
|
|
607
|
+
v : list[ItemElement]
|
|
608
|
+
List of elements to validate.
|
|
609
|
+
|
|
610
|
+
Returns
|
|
611
|
+
-------
|
|
612
|
+
list[ItemElement]
|
|
613
|
+
Validated elements.
|
|
614
|
+
|
|
615
|
+
Raises
|
|
616
|
+
------
|
|
617
|
+
ValueError
|
|
618
|
+
If duplicate element names found.
|
|
619
|
+
"""
|
|
620
|
+
if not v:
|
|
621
|
+
return v
|
|
622
|
+
|
|
623
|
+
names = [elem.element_name for elem in v]
|
|
624
|
+
if len(names) != len(set(names)):
|
|
625
|
+
duplicates = [name for name in names if names.count(name) > 1]
|
|
626
|
+
raise ValueError(f"Duplicate element names: {set(duplicates)}")
|
|
627
|
+
|
|
628
|
+
return v
|
|
629
|
+
|
|
630
|
+
@field_validator("presentation_order", mode="after")
|
|
631
|
+
@classmethod
|
|
632
|
+
def validate_presentation_order(
|
|
633
|
+
cls, v: list[str] | None, info: ValidationInfo
|
|
634
|
+
) -> list[str] | None:
|
|
635
|
+
"""Validate presentation_order matches element names.
|
|
636
|
+
|
|
637
|
+
Parameters
|
|
638
|
+
----------
|
|
639
|
+
v : list[str] | None
|
|
640
|
+
Presentation order list to validate.
|
|
641
|
+
info : ValidationInfo
|
|
642
|
+
Pydantic validation info containing other field values.
|
|
643
|
+
|
|
644
|
+
Returns
|
|
645
|
+
-------
|
|
646
|
+
list[str] | None
|
|
647
|
+
Validated presentation order.
|
|
648
|
+
|
|
649
|
+
Raises
|
|
650
|
+
------
|
|
651
|
+
ValueError
|
|
652
|
+
If presentation_order contains names not in elements,
|
|
653
|
+
or is missing names from elements.
|
|
654
|
+
"""
|
|
655
|
+
if v is None:
|
|
656
|
+
return v
|
|
657
|
+
|
|
658
|
+
# Get elements from validation info
|
|
659
|
+
elements = info.data.get("elements", [])
|
|
660
|
+
if not elements:
|
|
661
|
+
return v
|
|
662
|
+
|
|
663
|
+
element_names = {e.element_name for e in elements}
|
|
664
|
+
order_names = set(v)
|
|
665
|
+
|
|
666
|
+
# Check for names in order that aren't in elements
|
|
667
|
+
extra = order_names - element_names
|
|
668
|
+
if extra:
|
|
669
|
+
raise ValueError(
|
|
670
|
+
f"presentation_order contains element names not in elements: {extra}"
|
|
671
|
+
)
|
|
672
|
+
|
|
673
|
+
# Check for names in elements that aren't in order
|
|
674
|
+
missing = element_names - order_names
|
|
675
|
+
if missing:
|
|
676
|
+
raise ValueError(
|
|
677
|
+
f"presentation_order missing element names from elements: {missing}"
|
|
678
|
+
)
|
|
679
|
+
|
|
680
|
+
return v
|
|
681
|
+
|
|
682
|
+
def get_element_by_name(self, name: str) -> ItemElement | None:
|
|
683
|
+
"""Get an element by its name.
|
|
684
|
+
|
|
685
|
+
Parameters
|
|
686
|
+
----------
|
|
687
|
+
name : str
|
|
688
|
+
Element name to search for.
|
|
689
|
+
|
|
690
|
+
Returns
|
|
691
|
+
-------
|
|
692
|
+
ItemElement | None
|
|
693
|
+
Element with matching name, or None if not found.
|
|
694
|
+
|
|
695
|
+
Examples
|
|
696
|
+
--------
|
|
697
|
+
>>> elem = template.get_element_by_name("sentence")
|
|
698
|
+
>>> if elem:
|
|
699
|
+
... print(elem.element_type)
|
|
700
|
+
"""
|
|
701
|
+
for elem in self.elements:
|
|
702
|
+
if elem.element_name == name:
|
|
703
|
+
return elem
|
|
704
|
+
return None
|
|
705
|
+
|
|
706
|
+
def get_template_ref_elements(self) -> list[ItemElement]:
|
|
707
|
+
"""Get all elements that reference filled templates.
|
|
708
|
+
|
|
709
|
+
Returns
|
|
710
|
+
-------
|
|
711
|
+
list[ItemElement]
|
|
712
|
+
Elements with element_type="filled_template_ref".
|
|
713
|
+
|
|
714
|
+
Examples
|
|
715
|
+
--------
|
|
716
|
+
>>> refs = template.get_template_ref_elements()
|
|
717
|
+
>>> print(f"Found {len(refs)} template references")
|
|
718
|
+
"""
|
|
719
|
+
return [elem for elem in self.elements if elem.is_template_ref]
|
|
720
|
+
|
|
721
|
+
|
|
722
|
+
class ItemTemplateCollection(BeadBaseModel):
|
|
723
|
+
"""A collection of item templates.
|
|
724
|
+
|
|
725
|
+
Attributes
|
|
726
|
+
----------
|
|
727
|
+
name : str
|
|
728
|
+
Name of this collection.
|
|
729
|
+
description : str | None
|
|
730
|
+
Description of this collection.
|
|
731
|
+
templates : list[ItemTemplate]
|
|
732
|
+
Item templates in this collection.
|
|
733
|
+
|
|
734
|
+
Examples
|
|
735
|
+
--------
|
|
736
|
+
>>> collection = ItemTemplateCollection(
|
|
737
|
+
... name="acceptability_study",
|
|
738
|
+
... description="Templates for acceptability judgments"
|
|
739
|
+
... )
|
|
740
|
+
>>> collection.add_template(template)
|
|
741
|
+
"""
|
|
742
|
+
|
|
743
|
+
name: str = Field(..., description="Collection name")
|
|
744
|
+
description: str | None = Field(default=None, description="Collection description")
|
|
745
|
+
templates: list[ItemTemplate] = Field(
|
|
746
|
+
default_factory=_empty_item_template_list, description="Item templates"
|
|
747
|
+
)
|
|
748
|
+
|
|
749
|
+
@field_validator("name")
|
|
750
|
+
@classmethod
|
|
751
|
+
def validate_name(cls, v: str) -> str:
|
|
752
|
+
"""Validate collection name is not empty.
|
|
753
|
+
|
|
754
|
+
Parameters
|
|
755
|
+
----------
|
|
756
|
+
v : str
|
|
757
|
+
Collection name to validate.
|
|
758
|
+
|
|
759
|
+
Returns
|
|
760
|
+
-------
|
|
761
|
+
str
|
|
762
|
+
Validated collection name.
|
|
763
|
+
|
|
764
|
+
Raises
|
|
765
|
+
------
|
|
766
|
+
ValueError
|
|
767
|
+
If name is empty or contains only whitespace.
|
|
768
|
+
"""
|
|
769
|
+
if not v or not v.strip():
|
|
770
|
+
raise ValueError("Collection name cannot be empty")
|
|
771
|
+
return v.strip()
|
|
772
|
+
|
|
773
|
+
def add_template(self, template: ItemTemplate) -> None:
|
|
774
|
+
"""Add a template to the collection.
|
|
775
|
+
|
|
776
|
+
Parameters
|
|
777
|
+
----------
|
|
778
|
+
template : ItemTemplate
|
|
779
|
+
Template to add.
|
|
780
|
+
|
|
781
|
+
Examples
|
|
782
|
+
--------
|
|
783
|
+
>>> collection.add_template(my_template)
|
|
784
|
+
>>> print(f"Collection now has {len(collection.templates)} templates")
|
|
785
|
+
"""
|
|
786
|
+
self.templates.append(template)
|
|
787
|
+
self.update_modified_time()
|