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
@@ -0,0 +1,320 @@
1
+ """Configuration profiles for the bead package.
2
+
3
+ This module provides pre-configured profiles for different environments
4
+ (development, production, testing) with optimized settings for each use case.
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ from pathlib import Path
10
+ from tempfile import gettempdir
11
+
12
+ from bead.config.active_learning import (
13
+ ActiveLearningConfig,
14
+ ForcedChoiceModelConfig,
15
+ TrainerConfig,
16
+ )
17
+ from bead.config.config import BeadConfig
18
+ from bead.config.deployment import DeploymentConfig
19
+ from bead.config.item import ItemConfig
20
+ from bead.config.list import ListConfig
21
+ from bead.config.logging import LoggingConfig
22
+ from bead.config.model import ModelConfig
23
+ from bead.config.paths import PathsConfig
24
+ from bead.config.resources import ResourceConfig
25
+ from bead.config.template import TemplateConfig
26
+
27
+ # development profile: verbose logging, small batches, relative paths
28
+ DEV_CONFIG = BeadConfig(
29
+ profile="dev",
30
+ paths=PathsConfig(
31
+ data_dir=Path("data"),
32
+ output_dir=Path("output"),
33
+ cache_dir=Path(".cache"),
34
+ temp_dir=Path(gettempdir()) / "bead_dev",
35
+ create_dirs=True,
36
+ ),
37
+ resources=ResourceConfig(
38
+ cache_external=False, # disable caching for development
39
+ ),
40
+ templates=TemplateConfig(
41
+ filling_strategy="exhaustive",
42
+ batch_size=100, # small batch size for quick iteration
43
+ stream_mode=False,
44
+ ),
45
+ items=ItemConfig(
46
+ model=ModelConfig(
47
+ provider="huggingface",
48
+ model_name="gpt2",
49
+ batch_size=4, # small batch for development
50
+ device="cpu",
51
+ ),
52
+ parallel_processing=False, # simpler debugging without parallelism
53
+ ),
54
+ lists=ListConfig(
55
+ num_lists=1,
56
+ ),
57
+ deployment=DeploymentConfig(),
58
+ active_learning=ActiveLearningConfig(
59
+ forced_choice_model=ForcedChoiceModelConfig(
60
+ num_epochs=1, # quick training for testing
61
+ batch_size=8,
62
+ learning_rate=2e-5,
63
+ ),
64
+ trainer=TrainerConfig(epochs=1),
65
+ ),
66
+ logging=LoggingConfig(
67
+ level="DEBUG", # verbose logging for development
68
+ console=True,
69
+ ),
70
+ )
71
+ """Development configuration profile.
72
+
73
+ Optimized for:
74
+ - Quick iteration and debugging
75
+ - Verbose logging (DEBUG level)
76
+ - Small batch sizes for fast feedback
77
+ - No caching for fresh data
78
+ - Simple single-threaded processing
79
+ - Temporary directories for easy cleanup
80
+
81
+ Examples
82
+ --------
83
+ >>> from bead.config.profiles import DEV_CONFIG
84
+ >>> DEV_CONFIG.logging.level
85
+ 'DEBUG'
86
+ >>> DEV_CONFIG.templates.batch_size
87
+ 100
88
+ """
89
+
90
+ # production profile: optimized settings, large batches, absolute paths
91
+ PROD_CONFIG = BeadConfig(
92
+ profile="prod",
93
+ paths=PathsConfig(
94
+ data_dir=Path("/var/bead/data").absolute(),
95
+ output_dir=Path("/var/bead/output").absolute(),
96
+ cache_dir=Path("/var/bead/cache").absolute(),
97
+ temp_dir=Path("/var/bead/temp").absolute(),
98
+ create_dirs=True,
99
+ ),
100
+ resources=ResourceConfig(
101
+ cache_external=True, # enable caching for performance
102
+ ),
103
+ templates=TemplateConfig(
104
+ filling_strategy="exhaustive",
105
+ batch_size=10000, # large batches for efficiency
106
+ stream_mode=True, # handle large templates efficiently
107
+ ),
108
+ items=ItemConfig(
109
+ model=ModelConfig(
110
+ provider="huggingface",
111
+ model_name="gpt2",
112
+ batch_size=32, # large batch for production
113
+ device="cuda", # use GPU if available
114
+ ),
115
+ parallel_processing=True, # enable parallelism
116
+ num_workers=8, # multiple workers
117
+ ),
118
+ lists=ListConfig(
119
+ num_lists=1,
120
+ ),
121
+ deployment=DeploymentConfig(
122
+ apply_material_design=True,
123
+ include_demographics=True,
124
+ include_attention_checks=True,
125
+ ),
126
+ active_learning=ActiveLearningConfig(
127
+ forced_choice_model=ForcedChoiceModelConfig(
128
+ num_epochs=10, # full training
129
+ batch_size=32,
130
+ learning_rate=2e-5,
131
+ ),
132
+ trainer=TrainerConfig(epochs=10, use_wandb=True),
133
+ ),
134
+ logging=LoggingConfig(
135
+ level="WARNING", # minimal logging for production
136
+ console=False, # log to file only
137
+ file=Path("/var/log/bead/app.log"),
138
+ ),
139
+ )
140
+ """Production configuration profile.
141
+
142
+ Optimized for:
143
+ - Maximum performance and throughput
144
+ - Large batch sizes for efficiency
145
+ - GPU acceleration (when available)
146
+ - Parallel processing
147
+ - External caching enabled
148
+ - Minimal logging (WARNING level)
149
+ - Absolute paths to production directories
150
+ - Metrics tracking with W&B
151
+
152
+ Examples
153
+ --------
154
+ >>> from bead.config.profiles import PROD_CONFIG
155
+ >>> PROD_CONFIG.logging.level
156
+ 'WARNING'
157
+ >>> PROD_CONFIG.templates.batch_size
158
+ 10000
159
+ >>> PROD_CONFIG.items.parallel_processing
160
+ True
161
+ """
162
+
163
+ # test profile: minimal logging, tiny batches, temp directories
164
+ TEST_CONFIG = BeadConfig(
165
+ profile="test",
166
+ paths=PathsConfig(
167
+ data_dir=Path(gettempdir()) / "bead_test" / "data",
168
+ output_dir=Path(gettempdir()) / "bead_test" / "output",
169
+ cache_dir=Path(gettempdir()) / "bead_test" / "cache",
170
+ temp_dir=Path(gettempdir()) / "bead_test" / "temp",
171
+ create_dirs=True,
172
+ ),
173
+ resources=ResourceConfig(
174
+ cache_external=False, # no caching for tests
175
+ ),
176
+ templates=TemplateConfig(
177
+ filling_strategy="exhaustive",
178
+ batch_size=10, # tiny batches for fast tests
179
+ max_combinations=100, # limit for tests
180
+ random_seed=42, # reproducible tests
181
+ ),
182
+ items=ItemConfig(
183
+ model=ModelConfig(
184
+ provider="huggingface",
185
+ model_name="gpt2",
186
+ batch_size=1, # minimal batch for tests
187
+ device="cpu", # CPU only for tests
188
+ ),
189
+ parallel_processing=False, # simpler test execution
190
+ num_workers=1,
191
+ ),
192
+ lists=ListConfig(
193
+ num_lists=1,
194
+ random_seed=42, # reproducible tests
195
+ ),
196
+ deployment=DeploymentConfig(
197
+ apply_material_design=False, # minimal for tests
198
+ include_demographics=False,
199
+ include_attention_checks=False,
200
+ ),
201
+ active_learning=ActiveLearningConfig(
202
+ forced_choice_model=ForcedChoiceModelConfig(
203
+ num_epochs=1, # minimal training
204
+ batch_size=2,
205
+ learning_rate=2e-5,
206
+ ),
207
+ trainer=TrainerConfig(epochs=1, use_wandb=False),
208
+ ),
209
+ logging=LoggingConfig(
210
+ level="CRITICAL", # minimal logging for tests
211
+ console=False, # quiet tests
212
+ ),
213
+ )
214
+ """Test configuration profile.
215
+
216
+ Optimized for:
217
+ - Fast test execution
218
+ - Reproducibility (fixed random seeds)
219
+ - Minimal resource usage
220
+ - Tiny batch sizes
221
+ - Temporary directories for isolation
222
+ - Minimal logging (CRITICAL level)
223
+ - No external dependencies
224
+ - CPU-only execution
225
+
226
+ Examples
227
+ --------
228
+ >>> from bead.config.profiles import TEST_CONFIG
229
+ >>> TEST_CONFIG.logging.level
230
+ 'CRITICAL'
231
+ >>> TEST_CONFIG.templates.batch_size
232
+ 10
233
+ >>> TEST_CONFIG.templates.random_seed
234
+ 42
235
+ """
236
+
237
+ # profile registry
238
+ PROFILES: dict[str, BeadConfig] = {
239
+ "default": BeadConfig(), # default from models
240
+ "dev": DEV_CONFIG,
241
+ "prod": PROD_CONFIG,
242
+ "test": TEST_CONFIG,
243
+ }
244
+ """Registry of all available configuration profiles.
245
+
246
+ Maps profile names to their corresponding BeadConfig instances.
247
+
248
+ Examples
249
+ --------
250
+ >>> from bead.config.profiles import PROFILES
251
+ >>> list(PROFILES.keys())
252
+ ['default', 'dev', 'prod', 'test']
253
+ >>> PROFILES["dev"].logging.level
254
+ 'DEBUG'
255
+ """
256
+
257
+
258
+ def get_profile(name: str) -> BeadConfig:
259
+ """Get configuration profile by name.
260
+
261
+ Parameters
262
+ ----------
263
+ name : str
264
+ Profile name. Must be one of: 'default', 'dev', 'prod', 'test'.
265
+
266
+ Returns
267
+ -------
268
+ BeadConfig
269
+ Configuration for the specified profile.
270
+
271
+ Raises
272
+ ------
273
+ ValueError
274
+ If profile name is not found in the registry.
275
+
276
+ Examples
277
+ --------
278
+ >>> from bead.config.profiles import get_profile
279
+ >>> config = get_profile("dev")
280
+ >>> config.profile
281
+ 'dev'
282
+ >>> config.logging.level
283
+ 'DEBUG'
284
+
285
+ >>> try:
286
+ ... get_profile("invalid")
287
+ ... except ValueError as e:
288
+ ... print(str(e))
289
+ Profile 'invalid' not found. Available profiles: default, dev, prod, test
290
+ """
291
+ if name not in PROFILES:
292
+ available = ", ".join(sorted(PROFILES.keys()))
293
+ msg = f"Profile {name!r} not found. Available profiles: {available}"
294
+ raise ValueError(msg)
295
+
296
+ return PROFILES[name].model_copy(deep=True)
297
+
298
+
299
+ def list_profiles() -> list[str]:
300
+ """Return list of available profile names.
301
+
302
+ Returns
303
+ -------
304
+ list[str]
305
+ List of available profile names, sorted alphabetically.
306
+
307
+ Examples
308
+ --------
309
+ >>> from bead.config.profiles import list_profiles
310
+ >>> profiles = list_profiles()
311
+ >>> "default" in profiles
312
+ True
313
+ >>> "dev" in profiles
314
+ True
315
+ >>> "prod" in profiles
316
+ True
317
+ >>> "test" in profiles
318
+ True
319
+ """
320
+ return sorted(PROFILES.keys())
@@ -0,0 +1,47 @@
1
+ """Resource configuration models for the bead package."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from pathlib import Path
6
+
7
+ from pydantic import BaseModel, Field
8
+
9
+
10
+ class ResourceConfig(BaseModel):
11
+ """Configuration for external resources.
12
+
13
+ Parameters
14
+ ----------
15
+ lexicon_path : Path | None
16
+ Path to lexicon file.
17
+ templates_path : Path | None
18
+ Path to templates file.
19
+ constraints_path : Path | None
20
+ Path to constraints file.
21
+ external_adapters : list[str]
22
+ List of external adapters to enable.
23
+ cache_external : bool
24
+ Whether to cache external resource lookups.
25
+
26
+ Examples
27
+ --------
28
+ >>> config = ResourceConfig()
29
+ >>> config.cache_external
30
+ True
31
+ >>> config.external_adapters
32
+ []
33
+ """
34
+
35
+ lexicon_path: Path | None = Field(default=None, description="Path to lexicon file")
36
+ templates_path: Path | None = Field(
37
+ default=None, description="Path to templates file"
38
+ )
39
+ constraints_path: Path | None = Field(
40
+ default=None, description="Path to constraints file"
41
+ )
42
+ external_adapters: list[str] = Field(
43
+ default_factory=list, description="External adapters to enable"
44
+ )
45
+ cache_external: bool = Field(
46
+ default=True, description="Cache external resource lookups"
47
+ )
@@ -0,0 +1,210 @@
1
+ """Configuration serialization to YAML format.
2
+
3
+ This module provides functionality for serializing BeadConfig objects to YAML format,
4
+ including conversion to dictionaries and saving to files.
5
+ """
6
+
7
+ from pathlib import Path
8
+ from typing import Any
9
+
10
+ import yaml
11
+
12
+ from bead.config.config import BeadConfig
13
+ from bead.config.defaults import get_default_config
14
+
15
+
16
+ def config_to_dict(
17
+ config: BeadConfig, include_defaults: bool = False
18
+ ) -> dict[str, Any]:
19
+ """Convert BeadConfig to dictionary for YAML serialization.
20
+
21
+ Parameters
22
+ ----------
23
+ config : BeadConfig
24
+ Configuration to convert.
25
+ include_defaults : bool
26
+ Whether to include default values.
27
+
28
+ Returns
29
+ -------
30
+ dict[str, Any]
31
+ Dictionary representation suitable for YAML.
32
+
33
+ Examples
34
+ --------
35
+ >>> from bead.config import get_default_config
36
+ >>> config = get_default_config()
37
+ >>> config_dict = config_to_dict(config)
38
+ >>> 'profile' in config_dict
39
+ True
40
+ """
41
+ # get dictionary with all values, excluding unset fields
42
+ config_dict: dict[str, Any] = config.model_dump(
43
+ mode="json", exclude_unset=not include_defaults
44
+ )
45
+
46
+ if not include_defaults:
47
+ # get default config to compare against
48
+ default_config = get_default_config()
49
+ default_dict: dict[str, Any] = default_config.model_dump(mode="json")
50
+ # remove values that match defaults
51
+ config_dict = _remove_defaults(config_dict, default_dict) # type: ignore[arg-type]
52
+
53
+ # convert Path objects to strings
54
+ config_dict = _convert_paths_to_strings(config_dict) # type: ignore[arg-type]
55
+
56
+ return config_dict
57
+
58
+
59
+ def _remove_defaults(
60
+ config_dict: dict[str, Any], default_dict: dict[str, Any]
61
+ ) -> dict[str, Any]:
62
+ """Remove values that match defaults from config dictionary.
63
+
64
+ Parameters
65
+ ----------
66
+ config_dict : dict[str, Any]
67
+ Configuration dictionary.
68
+ default_dict : dict[str, Any]
69
+ Default configuration dictionary.
70
+
71
+ Returns
72
+ -------
73
+ dict[str, Any]
74
+ Configuration dictionary with defaults removed.
75
+ """
76
+ result: dict[str, Any] = {}
77
+ for key, value in config_dict.items():
78
+ if key not in default_dict:
79
+ # keep values not in defaults
80
+ result[key] = value
81
+ elif isinstance(value, dict) and isinstance(default_dict[key], dict):
82
+ # recursively remove defaults from nested dicts
83
+ nested_result = _remove_defaults(value, default_dict[key]) # type: ignore[arg-type]
84
+ if nested_result: # only include if not empty after removing defaults
85
+ result[key] = nested_result
86
+ elif value != default_dict[key]:
87
+ # keep values that differ from defaults
88
+ result[key] = value
89
+ return result
90
+
91
+
92
+ def _convert_paths_to_strings(data: dict[str, Any]) -> dict[str, Any]:
93
+ """Convert Path objects to strings in dictionary.
94
+
95
+ Parameters
96
+ ----------
97
+ data : dict[str, Any]
98
+ Dictionary potentially containing Path objects.
99
+
100
+ Returns
101
+ -------
102
+ dict[str, Any]
103
+ Dictionary with Path objects converted to strings.
104
+ """
105
+ result: dict[str, Any] = {}
106
+ for key, value in data.items():
107
+ if isinstance(value, dict):
108
+ result[key] = _convert_paths_to_strings(value) # type: ignore[arg-type]
109
+ elif isinstance(value, Path):
110
+ result[key] = str(value)
111
+ elif isinstance(value, list):
112
+ converted_list: list[Any] = [
113
+ str(item) if isinstance(item, Path) else item
114
+ for item in value # type: ignore[misc]
115
+ ]
116
+ result[key] = converted_list
117
+ else:
118
+ result[key] = value
119
+ return result
120
+
121
+
122
+ def to_yaml(config: BeadConfig, include_defaults: bool = False) -> str:
123
+ """Serialize configuration to YAML string.
124
+
125
+ Parameters
126
+ ----------
127
+ config : BeadConfig
128
+ Configuration to serialize.
129
+ include_defaults : bool
130
+ If True, include all fields even if they have default values.
131
+ If False, only include non-default values.
132
+
133
+ Returns
134
+ -------
135
+ str
136
+ YAML representation of configuration.
137
+
138
+ Examples
139
+ --------
140
+ >>> from bead.config import get_default_config
141
+ >>> config = get_default_config()
142
+ >>> yaml_str = to_yaml(config)
143
+ >>> 'profile: default' in yaml_str
144
+ True
145
+ """
146
+ config_dict = config_to_dict(config, include_defaults=include_defaults)
147
+
148
+ # configure YAML dumper for clean output
149
+ return yaml.dump(
150
+ config_dict,
151
+ default_flow_style=False,
152
+ sort_keys=True,
153
+ allow_unicode=True,
154
+ indent=2,
155
+ )
156
+
157
+
158
+ def save_yaml(
159
+ config: BeadConfig,
160
+ path: Path | str,
161
+ include_defaults: bool = False,
162
+ create_dirs: bool = True,
163
+ ) -> None:
164
+ """Save configuration to YAML file.
165
+
166
+ Parameters
167
+ ----------
168
+ config : BeadConfig
169
+ Configuration to save.
170
+ path : Path | str
171
+ Path where YAML file should be saved.
172
+ include_defaults : bool
173
+ If True, include all fields even if they have default values.
174
+ create_dirs : bool
175
+ If True, create parent directories if they don't exist.
176
+
177
+ Raises
178
+ ------
179
+ IOError
180
+ If file cannot be written.
181
+ FileNotFoundError
182
+ If create_dirs is False and parent directory doesn't exist.
183
+
184
+ Examples
185
+ --------
186
+ >>> from pathlib import Path
187
+ >>> from bead.config import get_default_config
188
+ >>> config = get_default_config()
189
+ >>> save_yaml(config, Path("config.yaml"))
190
+ """
191
+ path = Path(path) if isinstance(path, str) else path
192
+
193
+ # ensure parent directory exists
194
+ if create_dirs:
195
+ path.parent.mkdir(parents=True, exist_ok=True)
196
+ elif not path.parent.exists():
197
+ raise FileNotFoundError(
198
+ f"Parent directory does not exist: {path.parent}. "
199
+ f"Set create_dirs=True to create it automatically."
200
+ )
201
+
202
+ # get YAML string
203
+ yaml_str = to_yaml(config, include_defaults=include_defaults)
204
+
205
+ # write to file
206
+ try:
207
+ with open(path, "w") as f:
208
+ f.write(yaml_str)
209
+ except OSError as e:
210
+ raise OSError(f"Failed to write YAML file {path}: {e}") from e