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
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
|
bead/config/defaults.py
ADDED
|
@@ -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)
|