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.
Files changed (231) hide show
  1. bead/__init__.py +11 -0
  2. bead/__main__.py +11 -0
  3. bead/active_learning/__init__.py +15 -0
  4. bead/active_learning/config.py +231 -0
  5. bead/active_learning/loop.py +566 -0
  6. bead/active_learning/models/__init__.py +24 -0
  7. bead/active_learning/models/base.py +852 -0
  8. bead/active_learning/models/binary.py +910 -0
  9. bead/active_learning/models/categorical.py +943 -0
  10. bead/active_learning/models/cloze.py +862 -0
  11. bead/active_learning/models/forced_choice.py +956 -0
  12. bead/active_learning/models/free_text.py +773 -0
  13. bead/active_learning/models/lora.py +365 -0
  14. bead/active_learning/models/magnitude.py +835 -0
  15. bead/active_learning/models/multi_select.py +795 -0
  16. bead/active_learning/models/ordinal_scale.py +811 -0
  17. bead/active_learning/models/peft_adapter.py +155 -0
  18. bead/active_learning/models/random_effects.py +639 -0
  19. bead/active_learning/selection.py +354 -0
  20. bead/active_learning/strategies.py +391 -0
  21. bead/active_learning/trainers/__init__.py +26 -0
  22. bead/active_learning/trainers/base.py +210 -0
  23. bead/active_learning/trainers/data_collator.py +172 -0
  24. bead/active_learning/trainers/dataset_utils.py +261 -0
  25. bead/active_learning/trainers/huggingface.py +304 -0
  26. bead/active_learning/trainers/lightning.py +324 -0
  27. bead/active_learning/trainers/metrics.py +424 -0
  28. bead/active_learning/trainers/mixed_effects.py +551 -0
  29. bead/active_learning/trainers/model_wrapper.py +509 -0
  30. bead/active_learning/trainers/registry.py +104 -0
  31. bead/adapters/__init__.py +11 -0
  32. bead/adapters/huggingface.py +61 -0
  33. bead/behavioral/__init__.py +116 -0
  34. bead/behavioral/analytics.py +646 -0
  35. bead/behavioral/extraction.py +343 -0
  36. bead/behavioral/merging.py +343 -0
  37. bead/cli/__init__.py +11 -0
  38. bead/cli/active_learning.py +513 -0
  39. bead/cli/active_learning_commands.py +779 -0
  40. bead/cli/completion.py +359 -0
  41. bead/cli/config.py +624 -0
  42. bead/cli/constraint_builders.py +286 -0
  43. bead/cli/deployment.py +859 -0
  44. bead/cli/deployment_trials.py +493 -0
  45. bead/cli/deployment_ui.py +332 -0
  46. bead/cli/display.py +378 -0
  47. bead/cli/items.py +960 -0
  48. bead/cli/items_factories.py +776 -0
  49. bead/cli/list_constraints.py +714 -0
  50. bead/cli/lists.py +490 -0
  51. bead/cli/main.py +430 -0
  52. bead/cli/models.py +877 -0
  53. bead/cli/resource_loaders.py +621 -0
  54. bead/cli/resources.py +1036 -0
  55. bead/cli/shell.py +356 -0
  56. bead/cli/simulate.py +840 -0
  57. bead/cli/templates.py +1158 -0
  58. bead/cli/training.py +1080 -0
  59. bead/cli/utils.py +614 -0
  60. bead/cli/workflow.py +1273 -0
  61. bead/config/__init__.py +68 -0
  62. bead/config/active_learning.py +1009 -0
  63. bead/config/config.py +192 -0
  64. bead/config/defaults.py +118 -0
  65. bead/config/deployment.py +217 -0
  66. bead/config/env.py +147 -0
  67. bead/config/item.py +45 -0
  68. bead/config/list.py +193 -0
  69. bead/config/loader.py +149 -0
  70. bead/config/logging.py +42 -0
  71. bead/config/model.py +49 -0
  72. bead/config/paths.py +46 -0
  73. bead/config/profiles.py +320 -0
  74. bead/config/resources.py +47 -0
  75. bead/config/serialization.py +210 -0
  76. bead/config/simulation.py +206 -0
  77. bead/config/template.py +238 -0
  78. bead/config/validation.py +267 -0
  79. bead/data/__init__.py +65 -0
  80. bead/data/base.py +87 -0
  81. bead/data/identifiers.py +97 -0
  82. bead/data/language_codes.py +61 -0
  83. bead/data/metadata.py +270 -0
  84. bead/data/range.py +123 -0
  85. bead/data/repository.py +358 -0
  86. bead/data/serialization.py +249 -0
  87. bead/data/timestamps.py +89 -0
  88. bead/data/validation.py +349 -0
  89. bead/data_collection/__init__.py +11 -0
  90. bead/data_collection/jatos.py +223 -0
  91. bead/data_collection/merger.py +154 -0
  92. bead/data_collection/prolific.py +198 -0
  93. bead/deployment/__init__.py +5 -0
  94. bead/deployment/distribution.py +402 -0
  95. bead/deployment/jatos/__init__.py +1 -0
  96. bead/deployment/jatos/api.py +200 -0
  97. bead/deployment/jatos/exporter.py +210 -0
  98. bead/deployment/jspsych/__init__.py +9 -0
  99. bead/deployment/jspsych/biome.json +44 -0
  100. bead/deployment/jspsych/config.py +411 -0
  101. bead/deployment/jspsych/generator.py +598 -0
  102. bead/deployment/jspsych/package.json +51 -0
  103. bead/deployment/jspsych/pnpm-lock.yaml +2141 -0
  104. bead/deployment/jspsych/randomizer.py +299 -0
  105. bead/deployment/jspsych/src/lib/list-distributor.test.ts +327 -0
  106. bead/deployment/jspsych/src/lib/list-distributor.ts +1282 -0
  107. bead/deployment/jspsych/src/lib/randomizer.test.ts +232 -0
  108. bead/deployment/jspsych/src/lib/randomizer.ts +367 -0
  109. bead/deployment/jspsych/src/plugins/cloze-dropdown.ts +252 -0
  110. bead/deployment/jspsych/src/plugins/forced-choice.ts +265 -0
  111. bead/deployment/jspsych/src/plugins/plugins.test.ts +141 -0
  112. bead/deployment/jspsych/src/plugins/rating.ts +248 -0
  113. bead/deployment/jspsych/src/slopit/index.ts +9 -0
  114. bead/deployment/jspsych/src/types/jatos.d.ts +256 -0
  115. bead/deployment/jspsych/src/types/jspsych.d.ts +228 -0
  116. bead/deployment/jspsych/templates/experiment.css +1 -0
  117. bead/deployment/jspsych/templates/experiment.js.template +289 -0
  118. bead/deployment/jspsych/templates/index.html +51 -0
  119. bead/deployment/jspsych/templates/randomizer.js +241 -0
  120. bead/deployment/jspsych/templates/randomizer.js.template +313 -0
  121. bead/deployment/jspsych/trials.py +723 -0
  122. bead/deployment/jspsych/tsconfig.json +23 -0
  123. bead/deployment/jspsych/tsup.config.ts +30 -0
  124. bead/deployment/jspsych/ui/__init__.py +1 -0
  125. bead/deployment/jspsych/ui/components.py +383 -0
  126. bead/deployment/jspsych/ui/styles.py +411 -0
  127. bead/dsl/__init__.py +80 -0
  128. bead/dsl/ast.py +168 -0
  129. bead/dsl/context.py +178 -0
  130. bead/dsl/errors.py +71 -0
  131. bead/dsl/evaluator.py +570 -0
  132. bead/dsl/grammar.lark +81 -0
  133. bead/dsl/parser.py +231 -0
  134. bead/dsl/stdlib.py +929 -0
  135. bead/evaluation/__init__.py +13 -0
  136. bead/evaluation/convergence.py +485 -0
  137. bead/evaluation/interannotator.py +398 -0
  138. bead/items/__init__.py +40 -0
  139. bead/items/adapters/__init__.py +70 -0
  140. bead/items/adapters/anthropic.py +224 -0
  141. bead/items/adapters/api_utils.py +167 -0
  142. bead/items/adapters/base.py +216 -0
  143. bead/items/adapters/google.py +259 -0
  144. bead/items/adapters/huggingface.py +1074 -0
  145. bead/items/adapters/openai.py +323 -0
  146. bead/items/adapters/registry.py +202 -0
  147. bead/items/adapters/sentence_transformers.py +224 -0
  148. bead/items/adapters/togetherai.py +309 -0
  149. bead/items/binary.py +515 -0
  150. bead/items/cache.py +558 -0
  151. bead/items/categorical.py +593 -0
  152. bead/items/cloze.py +757 -0
  153. bead/items/constructor.py +784 -0
  154. bead/items/forced_choice.py +413 -0
  155. bead/items/free_text.py +681 -0
  156. bead/items/generation.py +432 -0
  157. bead/items/item.py +396 -0
  158. bead/items/item_template.py +787 -0
  159. bead/items/magnitude.py +573 -0
  160. bead/items/multi_select.py +621 -0
  161. bead/items/ordinal_scale.py +569 -0
  162. bead/items/scoring.py +448 -0
  163. bead/items/validation.py +723 -0
  164. bead/lists/__init__.py +30 -0
  165. bead/lists/balancer.py +263 -0
  166. bead/lists/constraints.py +1067 -0
  167. bead/lists/experiment_list.py +286 -0
  168. bead/lists/list_collection.py +378 -0
  169. bead/lists/partitioner.py +1141 -0
  170. bead/lists/stratification.py +254 -0
  171. bead/participants/__init__.py +73 -0
  172. bead/participants/collection.py +699 -0
  173. bead/participants/merging.py +312 -0
  174. bead/participants/metadata_spec.py +491 -0
  175. bead/participants/models.py +276 -0
  176. bead/resources/__init__.py +29 -0
  177. bead/resources/adapters/__init__.py +19 -0
  178. bead/resources/adapters/base.py +104 -0
  179. bead/resources/adapters/cache.py +128 -0
  180. bead/resources/adapters/glazing.py +508 -0
  181. bead/resources/adapters/registry.py +117 -0
  182. bead/resources/adapters/unimorph.py +796 -0
  183. bead/resources/classification.py +856 -0
  184. bead/resources/constraint_builders.py +329 -0
  185. bead/resources/constraints.py +165 -0
  186. bead/resources/lexical_item.py +223 -0
  187. bead/resources/lexicon.py +744 -0
  188. bead/resources/loaders.py +209 -0
  189. bead/resources/template.py +441 -0
  190. bead/resources/template_collection.py +707 -0
  191. bead/resources/template_generation.py +349 -0
  192. bead/simulation/__init__.py +29 -0
  193. bead/simulation/annotators/__init__.py +15 -0
  194. bead/simulation/annotators/base.py +175 -0
  195. bead/simulation/annotators/distance_based.py +135 -0
  196. bead/simulation/annotators/lm_based.py +114 -0
  197. bead/simulation/annotators/oracle.py +182 -0
  198. bead/simulation/annotators/random.py +181 -0
  199. bead/simulation/dsl_extension/__init__.py +3 -0
  200. bead/simulation/noise_models/__init__.py +13 -0
  201. bead/simulation/noise_models/base.py +42 -0
  202. bead/simulation/noise_models/random_noise.py +82 -0
  203. bead/simulation/noise_models/systematic.py +132 -0
  204. bead/simulation/noise_models/temperature.py +86 -0
  205. bead/simulation/runner.py +144 -0
  206. bead/simulation/strategies/__init__.py +23 -0
  207. bead/simulation/strategies/base.py +123 -0
  208. bead/simulation/strategies/binary.py +103 -0
  209. bead/simulation/strategies/categorical.py +123 -0
  210. bead/simulation/strategies/cloze.py +224 -0
  211. bead/simulation/strategies/forced_choice.py +127 -0
  212. bead/simulation/strategies/free_text.py +105 -0
  213. bead/simulation/strategies/magnitude.py +116 -0
  214. bead/simulation/strategies/multi_select.py +129 -0
  215. bead/simulation/strategies/ordinal_scale.py +131 -0
  216. bead/templates/__init__.py +27 -0
  217. bead/templates/adapters/__init__.py +17 -0
  218. bead/templates/adapters/base.py +128 -0
  219. bead/templates/adapters/cache.py +178 -0
  220. bead/templates/adapters/huggingface.py +312 -0
  221. bead/templates/combinatorics.py +103 -0
  222. bead/templates/filler.py +605 -0
  223. bead/templates/renderers.py +177 -0
  224. bead/templates/resolver.py +178 -0
  225. bead/templates/strategies.py +1806 -0
  226. bead/templates/streaming.py +195 -0
  227. bead-0.1.0.dist-info/METADATA +212 -0
  228. bead-0.1.0.dist-info/RECORD +231 -0
  229. bead-0.1.0.dist-info/WHEEL +4 -0
  230. bead-0.1.0.dist-info/entry_points.txt +2 -0
  231. bead-0.1.0.dist-info/licenses/LICENSE +21 -0
bead/config/config.py ADDED
@@ -0,0 +1,192 @@
1
+ """Main configuration model for the bead package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from typing import Any
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+ from bead.config.active_learning import ActiveLearningConfig
10
+ from bead.config.deployment import DeploymentConfig
11
+ from bead.config.item import ItemConfig
12
+ from bead.config.list import ListConfig
13
+ from bead.config.logging import LoggingConfig
14
+ from bead.config.paths import PathsConfig
15
+ from bead.config.resources import ResourceConfig
16
+ from bead.config.template import TemplateConfig
17
+
18
+
19
+ class BeadConfig(BaseModel):
20
+ """Main configuration for the bead package.
21
+
22
+ Reflects the actual bead/ module structure:
23
+ - active_learning: Active learning (models, trainers, loop, selection)
24
+ - data_collection: Human data collection (JATOS, Prolific)
25
+ - deployment: Experiment deployment (jsPsych, JATOS)
26
+ - evaluation: Model evaluation and metrics
27
+ - items: Item generation and management
28
+ - lists: List construction and balancing
29
+ - resources: Linguistic resources (VerbNet, PropBank, UniMorph)
30
+ - templates: Template management
31
+
32
+ Parameters
33
+ ----------
34
+ profile : str
35
+ Configuration profile name.
36
+ paths : PathsConfig
37
+ Paths configuration.
38
+ resources : ResourceConfig
39
+ Resources configuration.
40
+ templates : TemplateConfig
41
+ Templates configuration.
42
+ items : ItemConfig
43
+ Items configuration.
44
+ lists : ListConfig
45
+ Lists configuration.
46
+ deployment : DeploymentConfig
47
+ Deployment configuration.
48
+ active_learning : ActiveLearningConfig
49
+ Active learning configuration (models, trainers, loop, selection).
50
+ logging : LoggingConfig
51
+ Logging configuration.
52
+
53
+ Examples
54
+ --------
55
+ >>> config = BeadConfig()
56
+ >>> config.profile
57
+ 'default'
58
+ >>> config.paths.data_dir
59
+ PosixPath('data')
60
+ >>> config.active_learning.forced_choice_model.model_name
61
+ 'bert-base-uncased'
62
+ >>> config.active_learning.trainer.trainer_type
63
+ 'huggingface'
64
+ >>> config.active_learning.loop.max_iterations
65
+ 10
66
+ """
67
+
68
+ profile: str = Field(default="default", description="Configuration profile name")
69
+ paths: PathsConfig = Field(
70
+ default_factory=PathsConfig, description="Paths configuration"
71
+ )
72
+ resources: ResourceConfig = Field(
73
+ default_factory=ResourceConfig, description="Resources configuration"
74
+ )
75
+ templates: TemplateConfig = Field(
76
+ default_factory=TemplateConfig, description="Templates configuration"
77
+ )
78
+ items: ItemConfig = Field(
79
+ default_factory=ItemConfig, description="Items configuration"
80
+ )
81
+ lists: ListConfig = Field(
82
+ default_factory=ListConfig, description="Lists configuration"
83
+ )
84
+ deployment: DeploymentConfig = Field(
85
+ default_factory=DeploymentConfig, description="Deployment configuration"
86
+ )
87
+ active_learning: ActiveLearningConfig = Field(
88
+ default_factory=ActiveLearningConfig,
89
+ description="Active learning configuration",
90
+ )
91
+ logging: LoggingConfig = Field(
92
+ default_factory=LoggingConfig, description="Logging configuration"
93
+ )
94
+
95
+ def to_dict(self) -> dict[str, Any]:
96
+ """Convert configuration to dictionary.
97
+
98
+ Returns
99
+ -------
100
+ dict[str, Any]
101
+ Configuration as a dictionary.
102
+
103
+ Examples
104
+ --------
105
+ >>> config = BeadConfig()
106
+ >>> d = config.to_dict()
107
+ >>> d["profile"]
108
+ 'default'
109
+ """
110
+ return self.model_dump()
111
+
112
+ def to_yaml(self) -> str:
113
+ """Convert configuration to YAML string.
114
+
115
+ Returns
116
+ -------
117
+ str
118
+ Configuration as YAML string.
119
+
120
+ Examples
121
+ --------
122
+ >>> config = BeadConfig()
123
+ >>> yaml_str = config.to_yaml()
124
+ >>> 'profile: default' in yaml_str
125
+ True
126
+ """
127
+ from bead.config.serialization import to_yaml # noqa: PLC0415
128
+
129
+ return to_yaml(self, include_defaults=False)
130
+
131
+ def validate_paths(self) -> list[str]:
132
+ """Validate all path fields exist.
133
+
134
+ Returns
135
+ -------
136
+ list[str]
137
+ List of validation errors. Empty if all paths are valid.
138
+
139
+ Examples
140
+ --------
141
+ >>> config = BeadConfig()
142
+ >>> errors = config.validate_paths()
143
+ >>> len(errors)
144
+ 0
145
+ """
146
+ errors: list[str] = []
147
+
148
+ # check paths config
149
+ if not self.paths.data_dir.exists() and self.paths.data_dir.is_absolute():
150
+ errors.append(f"data_dir does not exist: {self.paths.data_dir}")
151
+ if not self.paths.output_dir.exists() and self.paths.output_dir.is_absolute():
152
+ errors.append(f"output_dir does not exist: {self.paths.output_dir}")
153
+ if not self.paths.cache_dir.exists() and self.paths.cache_dir.is_absolute():
154
+ errors.append(f"cache_dir does not exist: {self.paths.cache_dir}")
155
+ if self.paths.temp_dir is not None and not self.paths.temp_dir.exists():
156
+ errors.append(f"temp_dir does not exist: {self.paths.temp_dir}")
157
+
158
+ # check resource paths
159
+ if (
160
+ self.resources.lexicon_path is not None
161
+ and not self.resources.lexicon_path.exists()
162
+ ):
163
+ errors.append(f"lexicon_path does not exist: {self.resources.lexicon_path}")
164
+ if (
165
+ self.resources.templates_path is not None
166
+ and not self.resources.templates_path.exists()
167
+ ):
168
+ errors.append(
169
+ f"templates_path does not exist: {self.resources.templates_path}"
170
+ )
171
+ if (
172
+ self.resources.constraints_path is not None
173
+ and not self.resources.constraints_path.exists()
174
+ ):
175
+ errors.append(
176
+ f"constraints_path does not exist: {self.resources.constraints_path}"
177
+ )
178
+
179
+ # check training logging dir
180
+ if (
181
+ not self.active_learning.trainer.logging_dir.exists()
182
+ and self.active_learning.trainer.logging_dir.is_absolute()
183
+ ):
184
+ log_dir = self.active_learning.trainer.logging_dir
185
+ errors.append(f"logging_dir does not exist: {log_dir}")
186
+
187
+ # check logging file
188
+ if self.logging.file is not None and not self.logging.file.parent.exists():
189
+ parent_dir = self.logging.file.parent
190
+ errors.append(f"logging file parent directory does not exist: {parent_dir}")
191
+
192
+ return errors
@@ -0,0 +1,118 @@
1
+ """Default configurations for the bead package.
2
+
3
+ This module provides default configuration instances and helper functions
4
+ for retrieving default configurations.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pydantic import BaseModel
10
+
11
+ from bead.config.active_learning import ActiveLearningConfig
12
+ from bead.config.config import BeadConfig
13
+ from bead.config.deployment import DeploymentConfig
14
+ from bead.config.item import ItemConfig
15
+ from bead.config.list import ListConfig
16
+ from bead.config.logging import LoggingConfig
17
+ from bead.config.paths import PathsConfig
18
+ from bead.config.resources import ResourceConfig
19
+ from bead.config.template import TemplateConfig
20
+
21
+ DEFAULT_CONFIG = BeadConfig(
22
+ profile="default",
23
+ paths=PathsConfig(),
24
+ resources=ResourceConfig(),
25
+ templates=TemplateConfig(),
26
+ items=ItemConfig(),
27
+ lists=ListConfig(),
28
+ deployment=DeploymentConfig(),
29
+ active_learning=ActiveLearningConfig(),
30
+ logging=LoggingConfig(),
31
+ )
32
+ """Default configuration instance.
33
+
34
+ This configuration uses all default values from each config model.
35
+ It's the base configuration used when no config file is provided.
36
+
37
+ Examples
38
+ --------
39
+ >>> from bead.config.defaults import DEFAULT_CONFIG
40
+ >>> DEFAULT_CONFIG.profile
41
+ 'default'
42
+ >>> DEFAULT_CONFIG.paths.data_dir
43
+ PosixPath('data')
44
+ """
45
+
46
+
47
+ def get_default_config() -> BeadConfig:
48
+ """Get a copy of the default configuration.
49
+
50
+ Returns
51
+ -------
52
+ BeadConfig
53
+ A deep copy of the default configuration.
54
+
55
+ Examples
56
+ --------
57
+ >>> from bead.config.defaults import get_default_config
58
+ >>> config = get_default_config()
59
+ >>> config.profile
60
+ 'default'
61
+ >>> config.templates.batch_size
62
+ 1000
63
+
64
+ Notes
65
+ -----
66
+ Returns a deep copy to ensure modifications don't affect the original
67
+ DEFAULT_CONFIG instance.
68
+ """
69
+ return DEFAULT_CONFIG.model_copy(deep=True)
70
+
71
+
72
+ def get_default_for_model[T: BaseModel](model_type: type[T]) -> T:
73
+ """Get default instance of any config model.
74
+
75
+ Parameters
76
+ ----------
77
+ model_type : type[BaseModel]
78
+ The configuration model type to instantiate.
79
+
80
+ Returns
81
+ -------
82
+ T
83
+ Default instance of the specified model type.
84
+
85
+ Examples
86
+ --------
87
+ >>> from bead.config.defaults import get_default_for_model
88
+ >>> from bead.config.paths import PathsConfig
89
+ >>> paths = get_default_for_model(PathsConfig)
90
+ >>> paths.data_dir
91
+ PosixPath('data')
92
+
93
+ Raises
94
+ ------
95
+ TypeError
96
+ If model_type is not a valid Pydantic model class.
97
+
98
+ Notes
99
+ -----
100
+ This function provides runtime validation to ensure the input is a valid
101
+ Pydantic model class, even though the type system constrains it.
102
+ """
103
+ # runtime validation for cases where type checking is bypassed
104
+ try:
105
+ if not isinstance(model_type, type): # type: ignore[reportUnnecessaryIsInstance]
106
+ msg = f"model_type must be a Pydantic BaseModel class, got {model_type}"
107
+ raise TypeError(msg)
108
+ if not issubclass(model_type, BaseModel): # type: ignore[reportUnnecessaryIsInstance]
109
+ msg = f"model_type must be a Pydantic BaseModel class, got {model_type}"
110
+ raise TypeError(msg)
111
+ except TypeError as e:
112
+ # re-raise TypeError with our custom message
113
+ if "must be a Pydantic BaseModel class" in str(e):
114
+ raise
115
+ msg = f"model_type must be a Pydantic BaseModel class, got {model_type}"
116
+ raise TypeError(msg) from e
117
+
118
+ return model_type()
@@ -0,0 +1,217 @@
1
+ """Deployment configuration models for the bead package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
8
+
9
+ from bead.deployment.distribution import (
10
+ DistributionStrategyType,
11
+ ListDistributionStrategy,
12
+ )
13
+
14
+
15
+ class SlopitKeystrokeConfig(BaseModel):
16
+ """Configuration for slopit keystroke capture.
17
+
18
+ Attributes
19
+ ----------
20
+ enabled
21
+ Whether to capture keystroke events.
22
+ capture_key_up
23
+ Whether to capture keyup events in addition to keydown.
24
+ include_modifiers
25
+ Whether to record modifier key states.
26
+ """
27
+
28
+ model_config = ConfigDict(extra="forbid")
29
+
30
+ enabled: bool = Field(default=True, description="Capture keystroke events")
31
+ capture_key_up: bool = Field(default=True, description="Capture keyup events")
32
+ include_modifiers: bool = Field(default=True, description="Record modifier states")
33
+
34
+
35
+ class SlopitFocusConfig(BaseModel):
36
+ """Configuration for slopit focus/blur capture.
37
+
38
+ Attributes
39
+ ----------
40
+ enabled
41
+ Whether to capture focus events.
42
+ use_visibility_api
43
+ Whether to use the Page Visibility API.
44
+ use_blur_focus
45
+ Whether to track blur and focus events.
46
+ """
47
+
48
+ model_config = ConfigDict(extra="forbid")
49
+
50
+ enabled: bool = Field(default=True, description="Capture focus events")
51
+ use_visibility_api: bool = Field(
52
+ default=True, description="Use Page Visibility API"
53
+ )
54
+ use_blur_focus: bool = Field(default=True, description="Track blur/focus events")
55
+
56
+
57
+ class SlopitPasteConfig(BaseModel):
58
+ """Configuration for slopit paste event capture.
59
+
60
+ Attributes
61
+ ----------
62
+ enabled
63
+ Whether to capture paste events.
64
+ prevent
65
+ Whether to block paste actions.
66
+ capture_preview
67
+ Whether to capture preview of pasted text.
68
+ preview_length
69
+ Number of characters to include in preview.
70
+ """
71
+
72
+ model_config = ConfigDict(extra="forbid")
73
+
74
+ enabled: bool = Field(default=True, description="Capture paste events")
75
+ prevent: bool = Field(default=False, description="Block paste actions")
76
+ capture_preview: bool = Field(default=True, description="Capture text preview")
77
+ preview_length: int = Field(
78
+ default=100, ge=0, description="Preview character limit"
79
+ )
80
+
81
+
82
+ class SlopitIntegrationConfig(BaseModel):
83
+ """Configuration for slopit behavioral capture integration.
84
+
85
+ Slopit captures behavioral signals during experiment trials,
86
+ including keystroke dynamics, focus patterns, and paste events.
87
+ These signals can be used to detect AI-assisted responses.
88
+
89
+ Attributes
90
+ ----------
91
+ enabled
92
+ Whether to enable slopit behavioral capture. Disabled by default.
93
+ keystroke
94
+ Keystroke capture configuration.
95
+ focus
96
+ Focus/blur capture configuration.
97
+ paste
98
+ Paste event capture configuration.
99
+ target_selectors
100
+ CSS selectors for capture targets by task type.
101
+
102
+ Examples
103
+ --------
104
+ >>> config = SlopitIntegrationConfig(enabled=True)
105
+ >>> config.keystroke.enabled
106
+ True
107
+
108
+ >>> config = SlopitIntegrationConfig(
109
+ ... enabled=True,
110
+ ... paste=SlopitPasteConfig(prevent=True),
111
+ ... )
112
+ >>> config.paste.prevent
113
+ True
114
+ """
115
+
116
+ model_config = ConfigDict(extra="forbid")
117
+
118
+ enabled: bool = Field(
119
+ default=False,
120
+ description="Enable slopit behavioral capture (opt-in)",
121
+ )
122
+ keystroke: SlopitKeystrokeConfig = Field(
123
+ default_factory=SlopitKeystrokeConfig,
124
+ description="Keystroke capture settings",
125
+ )
126
+ focus: SlopitFocusConfig = Field(
127
+ default_factory=SlopitFocusConfig,
128
+ description="Focus/blur capture settings",
129
+ )
130
+ paste: SlopitPasteConfig = Field(
131
+ default_factory=SlopitPasteConfig,
132
+ description="Paste event capture settings",
133
+ )
134
+ target_selectors: dict[str, str] = Field(
135
+ default_factory=lambda: {
136
+ "likert_rating": ".bead-rating-button",
137
+ "slider_rating": ".bead-slider",
138
+ "forced_choice": ".bead-choice-button",
139
+ "cloze": ".bead-cloze-field",
140
+ },
141
+ description="CSS selectors for capture targets by task type",
142
+ )
143
+
144
+ @model_validator(mode="after")
145
+ def validate_slopit_bundle_exists(self) -> SlopitIntegrationConfig:
146
+ """Validate that slopit bundle exists when enabled.
147
+
148
+ Raises
149
+ ------
150
+ ValueError
151
+ If slopit is enabled but the compiled bundle is not found.
152
+ """
153
+ if self.enabled:
154
+ bundle_path = (
155
+ Path(__file__).parent.parent
156
+ / "deployment"
157
+ / "jspsych"
158
+ / "dist"
159
+ / "slopit-bundle.js"
160
+ )
161
+ if not bundle_path.exists():
162
+ msg = (
163
+ f"Slopit bundle not found at {bundle_path}. "
164
+ "Run 'pnpm build' in bead/deployment/jspsych to compile TypeScript."
165
+ )
166
+ raise ValueError(msg)
167
+ return self
168
+
169
+
170
+ class DeploymentConfig(BaseModel):
171
+ """Configuration for experiment deployment.
172
+
173
+ Parameters
174
+ ----------
175
+ platform : str
176
+ Deployment platform.
177
+ jspsych_version : str
178
+ jsPsych version to use.
179
+ apply_material_design : bool
180
+ Whether to use Material Design.
181
+ include_demographics : bool
182
+ Whether to include demographics survey.
183
+ include_attention_checks : bool
184
+ Whether to include attention checks.
185
+ jatos_export : bool
186
+ Whether to export to JATOS.
187
+ distribution_strategy : ListDistributionStrategy
188
+ List distribution strategy for batch experiments.
189
+ Defaults to balanced assignment.
190
+
191
+ Examples
192
+ --------
193
+ >>> config = DeploymentConfig()
194
+ >>> config.platform
195
+ 'jspsych'
196
+ >>> config.jspsych_version
197
+ '7.3.0'
198
+ >>> config.distribution_strategy.strategy_type
199
+ <DistributionStrategyType.BALANCED: 'balanced'>
200
+ """
201
+
202
+ platform: str = Field(default="jspsych", description="Deployment platform")
203
+ jspsych_version: str = Field(default="7.3.0", description="jsPsych version")
204
+ apply_material_design: bool = Field(default=True, description="Use Material Design")
205
+ include_demographics: bool = Field(
206
+ default=True, description="Include demographics survey"
207
+ )
208
+ include_attention_checks: bool = Field(
209
+ default=True, description="Include attention checks"
210
+ )
211
+ jatos_export: bool = Field(default=False, description="Export to JATOS")
212
+ distribution_strategy: ListDistributionStrategy = Field(
213
+ default_factory=lambda: ListDistributionStrategy(
214
+ strategy_type=DistributionStrategyType.BALANCED
215
+ ),
216
+ description="List distribution strategy for batch experiments",
217
+ )
bead/config/env.py ADDED
@@ -0,0 +1,147 @@
1
+ """Environment variable support for configuration.
2
+
3
+ This module provides functionality for loading configuration values from
4
+ environment variables, with support for nested configuration paths and
5
+ automatic type parsing.
6
+ """
7
+
8
+ import os
9
+ from pathlib import Path
10
+ from typing import Any
11
+
12
+
13
+ def parse_env_value(value: str) -> Any:
14
+ """Parse environment variable value to appropriate Python type.
15
+
16
+ Handles: bool, int, float, Path, list (comma-separated), string
17
+
18
+ Parameters
19
+ ----------
20
+ value : str
21
+ Raw environment variable value.
22
+
23
+ Returns
24
+ -------
25
+ Any
26
+ Parsed value with appropriate type.
27
+
28
+ Examples
29
+ --------
30
+ >>> parse_env_value("true")
31
+ True
32
+ >>> parse_env_value("42")
33
+ 42
34
+ >>> parse_env_value("/path/to/file")
35
+ PosixPath('/path/to/file')
36
+ >>> parse_env_value("a,b,c")
37
+ ['a', 'b', 'c']
38
+ """
39
+ # handle boolean values
40
+ if value.lower() in ("true", "1", "yes", "on"):
41
+ return True
42
+ if value.lower() in ("false", "0", "no", "off"):
43
+ return False
44
+
45
+ # handle numeric values; try int first
46
+ try:
47
+ return int(value)
48
+ except ValueError:
49
+ pass
50
+
51
+ # try float
52
+ try:
53
+ return float(value)
54
+ except ValueError:
55
+ pass
56
+
57
+ # handle path-like strings
58
+ if value.startswith(("/", "./", "~/", "../")):
59
+ return Path(value).expanduser()
60
+
61
+ # handle comma-separated lists
62
+ if "," in value:
63
+ return [item.strip() for item in value.split(",")]
64
+
65
+ # default to string
66
+ return value
67
+
68
+
69
+ def env_to_nested_dict(env_vars: dict[str, str], prefix: str) -> dict[str, Any]:
70
+ """Convert flat environment variables to nested dictionary.
71
+
72
+ Parameters
73
+ ----------
74
+ env_vars : dict[str, str]
75
+ Environment variables to convert.
76
+ prefix : str
77
+ Prefix to strip from variable names.
78
+
79
+ Returns
80
+ -------
81
+ dict[str, Any]
82
+ Nested configuration dictionary.
83
+
84
+ Examples
85
+ --------
86
+ >>> env_vars = {"BEAD_LOGGING__LEVEL": "DEBUG"}
87
+ >>> env_to_nested_dict(env_vars, "BEAD_")
88
+ {'logging': {'level': 'DEBUG'}}
89
+ """
90
+ result: dict[str, Any] = {}
91
+
92
+ for key, value in env_vars.items():
93
+ if not key.startswith(prefix):
94
+ continue
95
+
96
+ # remove prefix
97
+ key_without_prefix = key[len(prefix) :]
98
+
99
+ # split on double underscore for nesting
100
+ parts = key_without_prefix.split("__")
101
+
102
+ # convert to lowercase for config keys
103
+ parts = [part.lower() for part in parts]
104
+
105
+ # parse the value
106
+ parsed_value = parse_env_value(value)
107
+
108
+ # navigate/create nested structure
109
+ current = result
110
+ for part in parts[:-1]:
111
+ if part not in current:
112
+ current[part] = {}
113
+ current = current[part]
114
+
115
+ # set the final value
116
+ current[parts[-1]] = parsed_value
117
+
118
+ return result
119
+
120
+
121
+ def load_from_env(prefix: str = "BEAD_") -> dict[str, Any]:
122
+ """Load configuration values from environment variables.
123
+
124
+ Converts environment variables with the given prefix to a nested
125
+ configuration dictionary.
126
+
127
+ Parameters
128
+ ----------
129
+ prefix : str
130
+ Environment variable prefix to filter on.
131
+
132
+ Returns
133
+ -------
134
+ dict[str, Any]
135
+ Nested configuration dictionary from environment.
136
+
137
+ Examples
138
+ --------
139
+ >>> # With env var: BEAD_LOGGING__LEVEL=DEBUG
140
+ >>> load_from_env()
141
+ {'logging': {'level': 'DEBUG'}}
142
+ """
143
+ # get all environment variables with the prefix
144
+ env_vars = {k: v for k, v in os.environ.items() if k.startswith(prefix)}
145
+
146
+ # convert to nested dict
147
+ return env_to_nested_dict(env_vars, prefix)