@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,252 @@
1
+ """
2
+ Database connection management
3
+ """
4
+
5
+ import os
6
+ import threading
7
+ from collections.abc import AsyncGenerator, Generator
8
+ from contextlib import asynccontextmanager, contextmanager
9
+
10
+ from sqlalchemy import create_engine, text
11
+ from sqlalchemy.ext.asyncio import AsyncSession, async_sessionmaker, create_async_engine
12
+ from sqlalchemy.orm import Session, sessionmaker
13
+
14
+ from ..config import settings
15
+ from ..logging import get_logger
16
+
17
+ logger = get_logger(__name__)
18
+
19
+ # Global shared connection pools (proper FastAPI pattern)
20
+ _engine = None
21
+ _async_engine = None
22
+ _session_local = None
23
+ _async_session_local = None
24
+ _initialized = False
25
+ _init_lock = threading.Lock() # Protect initialization from race conditions
26
+
27
+
28
+ def get_database_url() -> str:
29
+ """Get database URL, checking environment variables first for test compatibility."""
30
+ # Always check environment first (for tests), then fall back to settings
31
+ db_url = os.getenv("BOARDS_DATABASE_URL")
32
+ if not db_url:
33
+ # For tests that set env vars after settings are loaded
34
+ if "BOARDS_DATABASE_URL" in os.environ:
35
+ db_url = os.environ["BOARDS_DATABASE_URL"]
36
+ else:
37
+ db_url = settings.database_url
38
+ return db_url
39
+
40
+
41
+ def reset_database():
42
+ """Reset database connections (for tests)."""
43
+ global _engine, _async_engine, _session_local, _async_session_local, _initialized
44
+ _engine = None
45
+ _async_engine = None
46
+ _session_local = None
47
+ _async_session_local = None
48
+ _initialized = False
49
+
50
+
51
+ async def test_database_connection() -> tuple[bool, str | None]:
52
+ """
53
+ Test the database connection and return helpful error messages.
54
+
55
+ Returns:
56
+ tuple: (success: bool, error_message: str | None)
57
+ """
58
+ if _async_engine is None:
59
+ return False, "Database engine not initialized"
60
+
61
+ try:
62
+ async with _async_engine.connect() as conn:
63
+ await conn.execute(text("SELECT 1"))
64
+ return True, None
65
+ except Exception as e:
66
+ error_str = str(e)
67
+ error_type = type(e).__name__
68
+
69
+ # Provide helpful error messages based on the error type
70
+ if "does not exist" in error_str and "role" in error_str:
71
+ # This is the confusing error - could be database server not running
72
+ db_url = get_database_url()
73
+ # Extract database name from URL
74
+ db_name = db_url.split("/")[-1].split("?")[0]
75
+ return False, (
76
+ f"Cannot connect to database: {error_str}\n"
77
+ f"This usually means:\n"
78
+ f" 1. The database server is not running\n"
79
+ f" 2. The database '{db_name}' doesn't exist\n"
80
+ f" 3. The database user/role doesn't exist\n"
81
+ f"Please check your database connection and run migrations if needed."
82
+ )
83
+ elif "Connection refused" in error_str or "could not connect" in error_str:
84
+ return False, (
85
+ f"Cannot connect to database server: {error_str}\n"
86
+ f"The database server appears to be down or unreachable.\n"
87
+ f"Please check that PostgreSQL is running and accessible."
88
+ )
89
+ elif "password authentication failed" in error_str:
90
+ return False, (
91
+ f"Database authentication failed: {error_str}\n"
92
+ f"Please check your database credentials."
93
+ )
94
+ else:
95
+ return False, f"Database connection error ({error_type}): {error_str}"
96
+
97
+
98
+ def init_database(database_url: str | None = None, force_reinit: bool = False):
99
+ """Initialize shared database connection pools.
100
+
101
+ Thread-safe initialization using a lock to prevent race conditions
102
+ when multiple threads attempt to initialize simultaneously.
103
+ """
104
+ global _engine, _async_engine, _session_local, _async_session_local, _initialized
105
+
106
+ # Fast path: already initialized, no lock needed
107
+ if _initialized and not force_reinit and database_url is None:
108
+ return
109
+
110
+ # Slow path: acquire lock for initialization
111
+ with _init_lock:
112
+ # Double-check after acquiring lock (another thread may have initialized)
113
+ if _initialized and not force_reinit and database_url is None:
114
+ return
115
+
116
+ # Get the database URL
117
+ db_url = database_url or get_database_url()
118
+
119
+ # Create sync engine
120
+ _engine = create_engine(
121
+ db_url,
122
+ pool_size=settings.database_pool_size,
123
+ max_overflow=settings.database_max_overflow,
124
+ echo=settings.sql_echo,
125
+ )
126
+ _session_local = sessionmaker(autocommit=False, autoflush=False, bind=_engine)
127
+
128
+ # Create async engine (if PostgreSQL)
129
+ if db_url.startswith("postgresql://"):
130
+ async_db_url = db_url.replace("postgresql://", "postgresql+asyncpg://")
131
+ _async_engine = create_async_engine(
132
+ async_db_url,
133
+ pool_size=settings.database_pool_size,
134
+ max_overflow=settings.database_max_overflow,
135
+ echo=settings.sql_echo,
136
+ )
137
+ _async_session_local = async_sessionmaker(
138
+ _async_engine,
139
+ class_=AsyncSession,
140
+ autocommit=False,
141
+ autoflush=False,
142
+ )
143
+
144
+ _initialized = True
145
+ logger.info("Database initialized", database_url=db_url)
146
+
147
+
148
+ def get_engine():
149
+ """Get the shared SQLAlchemy engine."""
150
+ if _engine is None:
151
+ init_database()
152
+ return _engine
153
+
154
+
155
+ def get_async_engine():
156
+ """Get the shared async SQLAlchemy engine."""
157
+ if _async_engine is None:
158
+ init_database()
159
+ return _async_engine
160
+
161
+
162
+ @contextmanager
163
+ def get_session() -> Generator[Session, None, None]:
164
+ """Get a database session (sync) from shared pool."""
165
+ if _session_local is None:
166
+ init_database()
167
+
168
+ if _session_local is None:
169
+ raise RuntimeError("Database not initialized")
170
+
171
+ session = _session_local()
172
+ try:
173
+ yield session
174
+ session.commit()
175
+ except Exception:
176
+ session.rollback()
177
+ raise
178
+ finally:
179
+ session.close()
180
+
181
+
182
+ @asynccontextmanager
183
+ async def get_async_session() -> AsyncGenerator[AsyncSession, None]:
184
+ """Get a database session (async) from shared pool."""
185
+ if _async_session_local is None:
186
+ init_database()
187
+
188
+ if _async_session_local is None:
189
+ raise RuntimeError("Async database not available (PostgreSQL required)")
190
+
191
+ async with _async_session_local() as session:
192
+ try:
193
+ yield session
194
+ await session.commit()
195
+ except Exception:
196
+ await session.rollback()
197
+ raise
198
+ finally:
199
+ await session.close()
200
+
201
+
202
+ # Dependency for FastAPI
203
+ async def get_db_session() -> AsyncGenerator[AsyncSession, None]:
204
+ """FastAPI dependency for database sessions."""
205
+ async with get_async_session() as session:
206
+ yield session
207
+
208
+
209
+ # Test-specific database session context
210
+ @asynccontextmanager
211
+ async def get_test_db_session(database_url: str) -> AsyncGenerator[AsyncSession, None]:
212
+ """Get a database session for testing with explicit database URL."""
213
+ from sqlalchemy.ext.asyncio import (
214
+ AsyncSession,
215
+ async_sessionmaker,
216
+ create_async_engine,
217
+ )
218
+
219
+ from ..config import settings
220
+
221
+ # Convert to async URL for PostgreSQL
222
+ if database_url.startswith("postgresql://"):
223
+ async_url = database_url.replace("postgresql://", "postgresql+asyncpg://")
224
+ else:
225
+ async_url = database_url
226
+
227
+ # Create isolated engine for this session
228
+ engine = create_async_engine(
229
+ async_url,
230
+ pool_size=settings.database_pool_size,
231
+ max_overflow=settings.database_max_overflow,
232
+ echo=settings.debug,
233
+ isolation_level="AUTOCOMMIT", # Ensure we can see committed schema changes
234
+ )
235
+
236
+ session_local = async_sessionmaker(
237
+ engine,
238
+ class_=AsyncSession,
239
+ autocommit=False,
240
+ autoflush=False,
241
+ )
242
+
243
+ async with session_local() as session:
244
+ try:
245
+ yield session
246
+ await session.commit()
247
+ except Exception:
248
+ await session.rollback()
249
+ raise
250
+ finally:
251
+ await session.close()
252
+ await engine.dispose()
@@ -0,0 +1,19 @@
1
+ # type: ignore[reportMissingImports]
2
+
3
+ """
4
+ Compatibility shim: re-export models from boards.dbmodels
5
+ This file remains to avoid breaking existing imports like `from boards.database.models import ...`.
6
+ """
7
+
8
+ from boards.dbmodels import ( # noqa: F401
9
+ Base,
10
+ BoardMembers,
11
+ Boards,
12
+ CreditTransactions,
13
+ Generations,
14
+ LoraModels,
15
+ ProviderConfigs,
16
+ Tenants,
17
+ Users,
18
+ target_metadata,
19
+ )
@@ -0,0 +1,182 @@
1
+ """
2
+ Reusable seed data functions for database initialization.
3
+
4
+ This module provides functions to seed initial data into the database,
5
+ including tenants, users, and other setup-time functionality.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from typing import Any
12
+ from uuid import UUID
13
+
14
+ from sqlalchemy import select
15
+ from sqlalchemy.ext.asyncio import AsyncSession
16
+
17
+ from ..config import settings
18
+ from ..dbmodels import Tenants
19
+ from ..logging import get_logger
20
+
21
+ logger = get_logger(__name__)
22
+
23
+
24
+ async def ensure_tenant(
25
+ db: AsyncSession,
26
+ *,
27
+ tenant_id: UUID | None = None,
28
+ name: str | None = None,
29
+ slug: str | None = None,
30
+ settings_dict: dict[str, Any] | None = None,
31
+ ) -> UUID:
32
+ """
33
+ Ensure a tenant exists in the database.
34
+
35
+ This function creates a tenant if it doesn't exist, or returns the
36
+ existing tenant's ID if it does.
37
+
38
+ Args:
39
+ db: Database session
40
+ tenant_id: UUID for the tenant (if None, auto-generated)
41
+ name: Tenant name (defaults to env var or "Default Tenant")
42
+ slug: Tenant slug (defaults to env var or "default")
43
+ settings_dict: Optional tenant settings/metadata
44
+
45
+ Returns:
46
+ UUID of the tenant (existing or newly created)
47
+ """
48
+ # Use environment variables with fallbacks
49
+ if name is None:
50
+ name = os.getenv("BOARDS_TENANT_NAME", "Default Tenant")
51
+
52
+ if slug is None:
53
+ slug = os.getenv("BOARDS_TENANT_SLUG", settings.default_tenant_slug)
54
+
55
+ if settings_dict is None:
56
+ settings_dict = {}
57
+
58
+ # Check if tenant already exists by slug
59
+ stmt = select(Tenants).where(Tenants.slug == slug)
60
+ result = await db.execute(stmt)
61
+ existing_tenant = result.scalar_one_or_none()
62
+
63
+ if existing_tenant:
64
+ logger.debug(
65
+ "Tenant already exists",
66
+ tenant_id=str(existing_tenant.id),
67
+ slug=slug,
68
+ name=existing_tenant.name,
69
+ )
70
+ return existing_tenant.id
71
+
72
+ # Create new tenant
73
+ new_tenant = Tenants(
74
+ name=name,
75
+ slug=slug,
76
+ settings=settings_dict,
77
+ )
78
+
79
+ # Set specific ID if provided
80
+ if tenant_id:
81
+ new_tenant.id = tenant_id
82
+
83
+ db.add(new_tenant)
84
+ await db.commit()
85
+ await db.refresh(new_tenant)
86
+
87
+ logger.info(
88
+ "Created new tenant",
89
+ tenant_id=str(new_tenant.id),
90
+ slug=slug,
91
+ name=name,
92
+ )
93
+
94
+ return new_tenant.id
95
+
96
+
97
+ async def ensure_default_tenant(db: AsyncSession) -> UUID:
98
+ """
99
+ Ensure the default tenant exists for single-tenant or no-auth mode.
100
+
101
+ This is a convenience function that uses environment variables
102
+ or defaults to create/get the default tenant.
103
+
104
+ Args:
105
+ db: Database session
106
+
107
+ Returns:
108
+ UUID of the default tenant
109
+ """
110
+ return await ensure_tenant(db)
111
+
112
+
113
+ async def seed_initial_data(db: AsyncSession) -> None:
114
+ """
115
+ Seed all initial data required for the application.
116
+
117
+ This function can be extended to seed additional data like:
118
+ - Default provider configurations
119
+ - Initial admin users
120
+ - Sample boards or generations
121
+ - Default credit allocations
122
+
123
+ Args:
124
+ db: Database session
125
+ """
126
+ logger.info("Starting database seeding")
127
+
128
+ # Always ensure default tenant exists in non-multi-tenant mode
129
+ if not settings.multi_tenant_mode:
130
+ tenant_id = await ensure_default_tenant(db)
131
+ logger.info("Ensured default tenant exists", tenant_id=str(tenant_id))
132
+
133
+ # Add more seed operations here as needed
134
+ # For example:
135
+ # await ensure_default_providers(db, tenant_id)
136
+ # await ensure_admin_user(db, tenant_id)
137
+
138
+ logger.info("Database seeding completed")
139
+
140
+
141
+ async def seed_tenant_with_data(
142
+ db: AsyncSession,
143
+ *,
144
+ tenant_name: str,
145
+ tenant_slug: str,
146
+ tenant_settings: dict[str, Any] | None = None,
147
+ include_sample_data: bool = False,
148
+ ) -> UUID:
149
+ """
150
+ Create a new tenant with optional sample data.
151
+
152
+ This function is useful for:
153
+ - Multi-tenant setup
154
+ - Creating demo tenants
155
+ - Testing different tenant configurations
156
+
157
+ Args:
158
+ db: Database session
159
+ tenant_name: Display name for the tenant
160
+ tenant_slug: Unique slug for the tenant
161
+ tenant_settings: Optional tenant-specific settings
162
+ include_sample_data: Whether to create sample boards/generations
163
+
164
+ Returns:
165
+ UUID of the created tenant
166
+ """
167
+ tenant_id = await ensure_tenant(
168
+ db,
169
+ name=tenant_name,
170
+ slug=tenant_slug,
171
+ settings_dict=tenant_settings,
172
+ )
173
+
174
+ if include_sample_data:
175
+ # Future: Add sample boards, users, generations
176
+ logger.info(
177
+ "Would create sample data for tenant",
178
+ tenant_id=str(tenant_id),
179
+ note="Sample data creation not yet implemented",
180
+ )
181
+
182
+ return tenant_id