@weirdfingers/baseboards 0.2.0

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 (139) hide show
  1. package/README.md +191 -0
  2. package/dist/index.d.ts +1 -0
  3. package/dist/index.js +887 -0
  4. package/dist/index.js.map +1 -0
  5. package/package.json +64 -0
  6. package/templates/README.md +120 -0
  7. package/templates/api/.env.example +62 -0
  8. package/templates/api/Dockerfile +32 -0
  9. package/templates/api/README.md +132 -0
  10. package/templates/api/alembic/env.py +106 -0
  11. package/templates/api/alembic/script.py.mako +28 -0
  12. package/templates/api/alembic/versions/20250101_000000_initial_schema.py +448 -0
  13. package/templates/api/alembic/versions/20251022_174729_remove_provider_name_from_generations.py +71 -0
  14. package/templates/api/alembic/versions/20251023_165852_switch_to_declarative_base_and_mapping.py +411 -0
  15. package/templates/api/alembic/versions/2025925_62735_add_seed_data_for_default_tenant.py +85 -0
  16. package/templates/api/alembic.ini +36 -0
  17. package/templates/api/config/generators.yaml +25 -0
  18. package/templates/api/config/storage_config.yaml +26 -0
  19. package/templates/api/docs/ADDING_GENERATORS.md +409 -0
  20. package/templates/api/docs/GENERATORS_API.md +502 -0
  21. package/templates/api/docs/MIGRATIONS.md +472 -0
  22. package/templates/api/docs/storage_providers.md +337 -0
  23. package/templates/api/pyproject.toml +165 -0
  24. package/templates/api/src/boards/__init__.py +10 -0
  25. package/templates/api/src/boards/api/app.py +171 -0
  26. package/templates/api/src/boards/api/auth.py +75 -0
  27. package/templates/api/src/boards/api/endpoints/__init__.py +3 -0
  28. package/templates/api/src/boards/api/endpoints/jobs.py +76 -0
  29. package/templates/api/src/boards/api/endpoints/setup.py +505 -0
  30. package/templates/api/src/boards/api/endpoints/sse.py +129 -0
  31. package/templates/api/src/boards/api/endpoints/storage.py +74 -0
  32. package/templates/api/src/boards/api/endpoints/tenant_registration.py +296 -0
  33. package/templates/api/src/boards/api/endpoints/webhooks.py +13 -0
  34. package/templates/api/src/boards/auth/__init__.py +15 -0
  35. package/templates/api/src/boards/auth/adapters/__init__.py +20 -0
  36. package/templates/api/src/boards/auth/adapters/auth0.py +220 -0
  37. package/templates/api/src/boards/auth/adapters/base.py +73 -0
  38. package/templates/api/src/boards/auth/adapters/clerk.py +172 -0
  39. package/templates/api/src/boards/auth/adapters/jwt.py +122 -0
  40. package/templates/api/src/boards/auth/adapters/none.py +102 -0
  41. package/templates/api/src/boards/auth/adapters/oidc.py +284 -0
  42. package/templates/api/src/boards/auth/adapters/supabase.py +110 -0
  43. package/templates/api/src/boards/auth/context.py +35 -0
  44. package/templates/api/src/boards/auth/factory.py +115 -0
  45. package/templates/api/src/boards/auth/middleware.py +221 -0
  46. package/templates/api/src/boards/auth/provisioning.py +129 -0
  47. package/templates/api/src/boards/auth/tenant_extraction.py +278 -0
  48. package/templates/api/src/boards/cli.py +354 -0
  49. package/templates/api/src/boards/config.py +116 -0
  50. package/templates/api/src/boards/database/__init__.py +7 -0
  51. package/templates/api/src/boards/database/cli.py +110 -0
  52. package/templates/api/src/boards/database/connection.py +252 -0
  53. package/templates/api/src/boards/database/models.py +19 -0
  54. package/templates/api/src/boards/database/seed_data.py +182 -0
  55. package/templates/api/src/boards/dbmodels/__init__.py +455 -0
  56. package/templates/api/src/boards/generators/__init__.py +57 -0
  57. package/templates/api/src/boards/generators/artifacts.py +53 -0
  58. package/templates/api/src/boards/generators/base.py +140 -0
  59. package/templates/api/src/boards/generators/implementations/__init__.py +12 -0
  60. package/templates/api/src/boards/generators/implementations/audio/__init__.py +3 -0
  61. package/templates/api/src/boards/generators/implementations/audio/whisper.py +66 -0
  62. package/templates/api/src/boards/generators/implementations/image/__init__.py +3 -0
  63. package/templates/api/src/boards/generators/implementations/image/dalle3.py +93 -0
  64. package/templates/api/src/boards/generators/implementations/image/flux_pro.py +85 -0
  65. package/templates/api/src/boards/generators/implementations/video/__init__.py +3 -0
  66. package/templates/api/src/boards/generators/implementations/video/lipsync.py +70 -0
  67. package/templates/api/src/boards/generators/loader.py +253 -0
  68. package/templates/api/src/boards/generators/registry.py +114 -0
  69. package/templates/api/src/boards/generators/resolution.py +515 -0
  70. package/templates/api/src/boards/generators/testmods/class_gen.py +34 -0
  71. package/templates/api/src/boards/generators/testmods/import_side_effect.py +35 -0
  72. package/templates/api/src/boards/graphql/__init__.py +7 -0
  73. package/templates/api/src/boards/graphql/access_control.py +136 -0
  74. package/templates/api/src/boards/graphql/mutations/root.py +136 -0
  75. package/templates/api/src/boards/graphql/queries/root.py +116 -0
  76. package/templates/api/src/boards/graphql/resolvers/__init__.py +8 -0
  77. package/templates/api/src/boards/graphql/resolvers/auth.py +12 -0
  78. package/templates/api/src/boards/graphql/resolvers/board.py +1055 -0
  79. package/templates/api/src/boards/graphql/resolvers/generation.py +889 -0
  80. package/templates/api/src/boards/graphql/resolvers/generator.py +50 -0
  81. package/templates/api/src/boards/graphql/resolvers/user.py +25 -0
  82. package/templates/api/src/boards/graphql/schema.py +81 -0
  83. package/templates/api/src/boards/graphql/types/board.py +102 -0
  84. package/templates/api/src/boards/graphql/types/generation.py +130 -0
  85. package/templates/api/src/boards/graphql/types/generator.py +17 -0
  86. package/templates/api/src/boards/graphql/types/user.py +47 -0
  87. package/templates/api/src/boards/jobs/repository.py +104 -0
  88. package/templates/api/src/boards/logging.py +195 -0
  89. package/templates/api/src/boards/middleware.py +339 -0
  90. package/templates/api/src/boards/progress/__init__.py +4 -0
  91. package/templates/api/src/boards/progress/models.py +25 -0
  92. package/templates/api/src/boards/progress/publisher.py +64 -0
  93. package/templates/api/src/boards/py.typed +0 -0
  94. package/templates/api/src/boards/redis_pool.py +118 -0
  95. package/templates/api/src/boards/storage/__init__.py +52 -0
  96. package/templates/api/src/boards/storage/base.py +363 -0
  97. package/templates/api/src/boards/storage/config.py +187 -0
  98. package/templates/api/src/boards/storage/factory.py +278 -0
  99. package/templates/api/src/boards/storage/implementations/__init__.py +27 -0
  100. package/templates/api/src/boards/storage/implementations/gcs.py +340 -0
  101. package/templates/api/src/boards/storage/implementations/local.py +201 -0
  102. package/templates/api/src/boards/storage/implementations/s3.py +294 -0
  103. package/templates/api/src/boards/storage/implementations/supabase.py +218 -0
  104. package/templates/api/src/boards/tenant_isolation.py +446 -0
  105. package/templates/api/src/boards/validation.py +262 -0
  106. package/templates/api/src/boards/workers/__init__.py +1 -0
  107. package/templates/api/src/boards/workers/actors.py +201 -0
  108. package/templates/api/src/boards/workers/cli.py +125 -0
  109. package/templates/api/src/boards/workers/context.py +188 -0
  110. package/templates/api/src/boards/workers/middleware.py +58 -0
  111. package/templates/api/src/py.typed +0 -0
  112. package/templates/compose.dev.yaml +39 -0
  113. package/templates/compose.yaml +109 -0
  114. package/templates/docker/env.example +23 -0
  115. package/templates/web/.env.example +28 -0
  116. package/templates/web/Dockerfile +51 -0
  117. package/templates/web/components.json +22 -0
  118. package/templates/web/imageLoader.js +18 -0
  119. package/templates/web/next-env.d.ts +5 -0
  120. package/templates/web/next.config.js +36 -0
  121. package/templates/web/package.json +37 -0
  122. package/templates/web/postcss.config.mjs +7 -0
  123. package/templates/web/public/favicon.ico +0 -0
  124. package/templates/web/src/app/boards/[boardId]/page.tsx +232 -0
  125. package/templates/web/src/app/globals.css +120 -0
  126. package/templates/web/src/app/layout.tsx +21 -0
  127. package/templates/web/src/app/page.tsx +35 -0
  128. package/templates/web/src/app/providers.tsx +18 -0
  129. package/templates/web/src/components/boards/ArtifactInputSlots.tsx +142 -0
  130. package/templates/web/src/components/boards/ArtifactPreview.tsx +125 -0
  131. package/templates/web/src/components/boards/GenerationGrid.tsx +45 -0
  132. package/templates/web/src/components/boards/GenerationInput.tsx +251 -0
  133. package/templates/web/src/components/boards/GeneratorSelector.tsx +89 -0
  134. package/templates/web/src/components/header.tsx +30 -0
  135. package/templates/web/src/components/ui/button.tsx +58 -0
  136. package/templates/web/src/components/ui/card.tsx +92 -0
  137. package/templates/web/src/components/ui/navigation-menu.tsx +168 -0
  138. package/templates/web/src/lib/utils.ts +6 -0
  139. package/templates/web/tsconfig.json +47 -0
@@ -0,0 +1,253 @@
1
+ """Configuration-driven generator loader.
2
+
3
+ Loads and registers generators based on configuration file.
4
+ File path is specified via settings.generators_config_path.
5
+
6
+ Supports three declaration forms: import, class, entrypoint.
7
+ Strict mode is enabled by default and will fail startup on errors.
8
+ """
9
+
10
+ from __future__ import annotations
11
+
12
+ from collections.abc import Iterable
13
+ from dataclasses import dataclass
14
+ from importlib import import_module
15
+ from importlib import metadata as importlib_metadata
16
+ from pathlib import Path
17
+ from typing import Any
18
+
19
+ import yaml
20
+
21
+ from boards.config import settings
22
+ from boards.logging import get_logger
23
+
24
+ from .base import BaseGenerator
25
+ from .registry import registry
26
+
27
+ logger = get_logger(__name__)
28
+
29
+
30
+ ENTRYPOINT_GROUP = "boards.generators"
31
+
32
+ VALID_ARTIFACT_TYPES: set[str] = {"image", "video", "audio", "text", "lora"}
33
+
34
+
35
+ @dataclass
36
+ class LoaderConfig:
37
+ strict_mode: bool = True
38
+ allow_unlisted: bool = False
39
+ declarations: list[dict[str, Any]] | None = None
40
+
41
+
42
+ def _load_file_config(path: str) -> LoaderConfig | None:
43
+ try:
44
+ with open(path, encoding="utf-8") as f:
45
+ data: dict[str, Any] = yaml.safe_load(f) or {}
46
+ except FileNotFoundError:
47
+ return None
48
+
49
+ strict_mode = bool(data.get("strict_mode", True))
50
+ allow_unlisted = bool(data.get("allow_unlisted", False))
51
+ declarations = list(data.get("generators", []) or [])
52
+
53
+ return LoaderConfig(
54
+ strict_mode=strict_mode,
55
+ allow_unlisted=allow_unlisted,
56
+ declarations=declarations,
57
+ )
58
+
59
+
60
+ def _discover_config() -> LoaderConfig | None:
61
+ """Discover config from settings.generators_config_path."""
62
+ if not settings.generators_config_path:
63
+ return None
64
+
65
+ path = Path(settings.generators_config_path)
66
+ if not path.exists():
67
+ logger.warning("Generators config path set but not found", path=str(path))
68
+ return None
69
+
70
+ cfg = _load_file_config(str(path))
71
+ if cfg:
72
+ logger.info("Loaded generators config from settings", path=str(path))
73
+ return cfg
74
+
75
+
76
+ def _resolve_class(qualified_name: str) -> type[BaseGenerator]:
77
+ if ":" in qualified_name:
78
+ module_name, class_name = qualified_name.split(":", 1)
79
+ else:
80
+ # Split on last dot for module path
81
+ module_name, class_name = qualified_name.rsplit(".", 1)
82
+
83
+ module = import_module(module_name)
84
+ cls = getattr(module, class_name)
85
+ if not isinstance(cls, type) or not issubclass(cls, BaseGenerator):
86
+ raise TypeError(f"Resolved object is not a BaseGenerator subclass: {qualified_name}")
87
+ return cls
88
+
89
+
90
+ def _resolve_entrypoint(name: str) -> type[BaseGenerator]:
91
+ try:
92
+ eps = importlib_metadata.entry_points()
93
+ # Python 3.12 returns a Selection object with .select
94
+ group_filtered: Iterable[Any]
95
+ if hasattr(eps, "select"):
96
+ group_filtered = eps.select(group=ENTRYPOINT_GROUP)
97
+ else:
98
+ group_filtered = eps.get(ENTRYPOINT_GROUP, []) # type: ignore[attr-defined]
99
+ except Exception as e: # pragma: no cover - edge cases
100
+ raise RuntimeError(f"Failed to read entry points: {e}") from e
101
+
102
+ for ep in group_filtered:
103
+ if getattr(ep, "name", None) == name:
104
+ obj = ep.load()
105
+ if not isinstance(obj, type) or not issubclass(obj, BaseGenerator):
106
+ raise TypeError(f"Entry point '{name}' is not a BaseGenerator class")
107
+ return obj
108
+
109
+ raise LookupError(f"Entry point not found: {name}")
110
+
111
+
112
+ def _validate_artifact_type(instance: BaseGenerator) -> None:
113
+ artifact_type = getattr(instance, "artifact_type", None)
114
+ if not artifact_type or artifact_type not in VALID_ARTIFACT_TYPES:
115
+ raise ValueError(f"Invalid artifact_type: {artifact_type}")
116
+
117
+
118
+ def _register_instance(instance: BaseGenerator, name_override: str | None) -> None:
119
+ if name_override:
120
+ # Override instance name if provided
121
+ try:
122
+ instance.name = name_override
123
+ except Exception as e:
124
+ raise ValueError(f"Failed to set generator name override: {e}") from e
125
+
126
+ _validate_artifact_type(instance)
127
+ registry.register(instance)
128
+
129
+
130
+ def _enforce_unlisted_policy(
131
+ allowed_names: set[str], strict_mode: bool, allow_unlisted: bool
132
+ ) -> None:
133
+ registered = set(registry.list_names())
134
+ extras = registered - allowed_names
135
+ if not extras:
136
+ return
137
+
138
+ msg = f"Found unlisted generator registrations: {sorted(extras)}"
139
+ if allow_unlisted:
140
+ logger.warning(msg)
141
+ return
142
+
143
+ # Not allowed. Attempt to unregister extras then decide based on strict_mode.
144
+ for name in extras:
145
+ try:
146
+ registry.unregister(name)
147
+ except Exception: # pragma: no cover - defensive
148
+ pass
149
+
150
+ if strict_mode:
151
+ raise RuntimeError(msg)
152
+ else:
153
+ logger.error(msg)
154
+
155
+
156
+ def load_generators_from_config(config_path: str | None = None) -> None:
157
+ """Load and register generators according to configuration.
158
+
159
+ Raises on errors when strict mode is enabled (default).
160
+ """
161
+
162
+ # Discover configuration
163
+ cfg: LoaderConfig | None
164
+ if config_path:
165
+ cfg = _load_file_config(config_path)
166
+ if cfg:
167
+ logger.info("Loaded generators config from explicit path", path=config_path)
168
+ else:
169
+ cfg = _discover_config()
170
+
171
+ # If nothing configured, do nothing (no implicit imports)
172
+ if not cfg or not cfg.declarations:
173
+ logger.info("No generators configuration found; skipping generator loading")
174
+ return
175
+
176
+ strict_mode = cfg.strict_mode
177
+ allow_unlisted = cfg.allow_unlisted
178
+
179
+ requested_names: set[str] = set()
180
+
181
+ # Process declarations in order
182
+ for decl in cfg.declarations:
183
+ if not isinstance(decl, dict):
184
+ msg = f"Invalid generator declaration type: {type(decl)}"
185
+ if strict_mode:
186
+ raise ValueError(msg)
187
+ logger.error(msg)
188
+ continue
189
+
190
+ if decl.get("enabled") is False:
191
+ continue
192
+
193
+ name_override = decl.get("name")
194
+
195
+ try:
196
+ if "import" in decl:
197
+ import_path = decl["import"]
198
+ import_module(import_path)
199
+ # Back-compat path relies on module side-effect registration.
200
+ # We cannot infer the name here reliably; collect after loop.
201
+ logger.info("Imported generator module", import_path=import_path)
202
+ elif "class" in decl:
203
+ qualified = decl["class"]
204
+ options = decl.get("options", {}) or {}
205
+ cls = _resolve_class(qualified)
206
+ instance = cls(**options) if options else cls()
207
+ _register_instance(instance, name_override)
208
+ requested_names.add(instance.name)
209
+ logger.info(
210
+ "Registered generator via class",
211
+ class_path=qualified,
212
+ name=instance.name,
213
+ )
214
+ elif "entrypoint" in decl:
215
+ ep_name = decl["entrypoint"]
216
+ options = decl.get("options", {}) or {}
217
+ cls = _resolve_entrypoint(ep_name)
218
+ instance = cls(**options) if options else cls()
219
+ _register_instance(instance, name_override)
220
+ requested_names.add(instance.name)
221
+ logger.info(
222
+ "Registered generator via entrypoint",
223
+ entrypoint=ep_name,
224
+ name=instance.name,
225
+ )
226
+ else:
227
+ raise ValueError(
228
+ "Generator declaration must include one of: import, class, entrypoint"
229
+ )
230
+
231
+ except Exception as e:
232
+ msg = f"Failed to load generator declaration: {e}"
233
+ if strict_mode:
234
+ raise RuntimeError(msg) from e
235
+ logger.error(msg)
236
+ continue
237
+
238
+ # After imports, collect names to enforce allow_unlisted policy
239
+ # requested_names may be incomplete for `import` declarations; fill with registry state
240
+ for name in registry.list_names():
241
+ requested_names.add(name)
242
+
243
+ _enforce_unlisted_policy(requested_names, strict_mode, allow_unlisted)
244
+
245
+ # Final summary
246
+ logger.info(
247
+ "Generators loading complete",
248
+ requested=len(cfg.declarations or []),
249
+ registered=len(registry),
250
+ names=registry.list_names(),
251
+ strict_mode=strict_mode,
252
+ allow_unlisted=allow_unlisted,
253
+ )
@@ -0,0 +1,114 @@
1
+ """
2
+ Generator registry system for discovering and managing generators.
3
+ """
4
+
5
+ from boards.logging import get_logger
6
+
7
+ from .base import BaseGenerator
8
+
9
+ logger = get_logger(__name__)
10
+
11
+
12
+ class GeneratorRegistry:
13
+ """
14
+ Central registry for generator discovery and management.
15
+
16
+ Provides methods to register generators, look them up by name,
17
+ and list available generators by various criteria.
18
+ """
19
+
20
+ def __init__(self):
21
+ self._generators: dict[str, BaseGenerator] = {}
22
+
23
+ def register(self, generator: BaseGenerator) -> None:
24
+ """
25
+ Register a generator instance with the registry.
26
+
27
+ Args:
28
+ generator: Generator instance to register
29
+
30
+ Raises:
31
+ ValueError: If a generator with the same name is already registered
32
+ """
33
+ logger.info("Registering generator", name=generator.name)
34
+ if generator.name in self._generators:
35
+ raise ValueError(f"Generator '{generator.name}' is already registered")
36
+
37
+ self._generators[generator.name] = generator
38
+
39
+ def get(self, name: str) -> BaseGenerator | None:
40
+ """
41
+ Get a generator by name.
42
+
43
+ Args:
44
+ name: Name of the generator to retrieve
45
+
46
+ Returns:
47
+ BaseGenerator instance or None if not found
48
+ """
49
+ return self._generators.get(name)
50
+
51
+ def list_all(self) -> list[BaseGenerator]:
52
+ """
53
+ List all registered generators.
54
+
55
+ Returns:
56
+ List of all generator instances
57
+ """
58
+ return list(self._generators.values())
59
+
60
+ def list_by_artifact_type(self, artifact_type: str) -> list[BaseGenerator]:
61
+ """
62
+ List generators that produce a specific artifact type.
63
+
64
+ Args:
65
+ artifact_type: Type of artifact (image, video, audio, text, lora)
66
+
67
+ Returns:
68
+ List of generators that produce the specified artifact type
69
+ """
70
+ return [
71
+ generator
72
+ for generator in self._generators.values()
73
+ if generator.artifact_type == artifact_type
74
+ ]
75
+
76
+ def list_names(self) -> list[str]:
77
+ """
78
+ List all registered generator names.
79
+
80
+ Returns:
81
+ List of generator names
82
+ """
83
+ return list(self._generators.keys())
84
+
85
+ def unregister(self, name: str) -> bool:
86
+ """
87
+ Unregister a generator by name.
88
+
89
+ Args:
90
+ name: Name of the generator to unregister
91
+
92
+ Returns:
93
+ True if the generator was found and removed, False otherwise
94
+ """
95
+ if name in self._generators:
96
+ del self._generators[name]
97
+ return True
98
+ return False
99
+
100
+ def clear(self) -> None:
101
+ """Clear all registered generators."""
102
+ self._generators.clear()
103
+
104
+ def __len__(self) -> int:
105
+ """Return the number of registered generators."""
106
+ return len(self._generators)
107
+
108
+ def __contains__(self, name: str) -> bool:
109
+ """Check if a generator with the given name is registered."""
110
+ return name in self._generators
111
+
112
+
113
+ # Global registry instance
114
+ registry = GeneratorRegistry()