sql-dialect-master 1.1.1__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 (86) hide show
  1. backend/__init__.py +1 -0
  2. backend/api/__init__.py +1 -0
  3. backend/api/main.py +732 -0
  4. backend/api/middleware.py +274 -0
  5. backend/api/rate_limit_store.py +105 -0
  6. backend/api/readiness.py +178 -0
  7. backend/core/__init__.py +57 -0
  8. backend/core/cache.py +261 -0
  9. backend/core/column_hint_validation.py +22 -0
  10. backend/core/config.py +546 -0
  11. backend/core/constants.py +84 -0
  12. backend/core/exceptions.py +124 -0
  13. backend/core/function_call_scanner.py +64 -0
  14. backend/core/function_semantics.py +148 -0
  15. backend/core/functions_db.json +601 -0
  16. backend/core/functions_lookup.py +332 -0
  17. backend/core/metadata.py +71 -0
  18. backend/core/nl2sql.py +85 -0
  19. backend/core/nl2sql_components/__init__.py +72 -0
  20. backend/core/nl2sql_components/aggregations.py +35 -0
  21. backend/core/nl2sql_components/boolean_conditions.py +279 -0
  22. backend/core/nl2sql_components/core_extract.py +106 -0
  23. backend/core/nl2sql_components/evidence.py +122 -0
  24. backend/core/nl2sql_components/mappings.py +185 -0
  25. backend/core/nl2sql_components/relations.py +180 -0
  26. backend/core/nl2sql_components/templates.py +130 -0
  27. backend/core/nl2sql_components/tokenizer.py +187 -0
  28. backend/core/nl2sql_legacy.py +22 -0
  29. backend/core/nl2sql_pipeline/__init__.py +13 -0
  30. backend/core/nl2sql_pipeline/aggregations.py +26 -0
  31. backend/core/nl2sql_pipeline/clause_extract.py +97 -0
  32. backend/core/nl2sql_pipeline/dialect.py +139 -0
  33. backend/core/nl2sql_pipeline/intent.py +85 -0
  34. backend/core/nl2sql_pipeline/ir.py +70 -0
  35. backend/core/nl2sql_pipeline/models.py +30 -0
  36. backend/core/nl2sql_pipeline/orchestrator.py +576 -0
  37. backend/core/nl2sql_pipeline/predicates.py +182 -0
  38. backend/core/nl2sql_pipeline/renderer.py +248 -0
  39. backend/core/nl2sql_pipeline/resolution.py +69 -0
  40. backend/core/nl2sql_pipeline/suggestions.py +30 -0
  41. backend/core/nl2sql_pipeline/templates.py +371 -0
  42. backend/core/nl2sql_pipeline/validation.py +28 -0
  43. backend/core/p1_sql_scanner.py +196 -0
  44. backend/core/p2_data_validation.py +72 -0
  45. backend/core/parser.py +280 -0
  46. backend/core/post_processor.py +129 -0
  47. backend/core/rules.py +808 -0
  48. backend/core/semantic_diff.py +385 -0
  49. backend/core/transpiler.py +791 -0
  50. backend/core/type_mapping.json +593 -0
  51. backend/core/type_mapping.py +765 -0
  52. backend/utils/__init__.py +2 -0
  53. backend/utils/validators.py +407 -0
  54. frontend/__init__.py +10 -0
  55. frontend/app_context_v2.py +201 -0
  56. frontend/core/__init__.py +33 -0
  57. frontend/core/design_tokens.py +127 -0
  58. frontend/core/escaping.py +13 -0
  59. frontend/core/navigation.py +135 -0
  60. frontend/core/state.py +612 -0
  61. frontend/core/styles.py +523 -0
  62. frontend/core/themes.py +69 -0
  63. frontend/core/viewmodels.py +499 -0
  64. frontend/pages/__init__.py +0 -0
  65. frontend/pages/convert.py +283 -0
  66. frontend/pages/diff.py +157 -0
  67. frontend/pages/functions.py +100 -0
  68. frontend/pages/history.py +144 -0
  69. frontend/pages/lineage.py +168 -0
  70. frontend/pages/nl2sql.py +305 -0
  71. frontend/pages/query_analysis.py +120 -0
  72. frontend/pages/runtime.py +66 -0
  73. frontend/pages/settings.py +60 -0
  74. frontend/pages/templates.py +67 -0
  75. frontend/pages/types.py +110 -0
  76. frontend/templates_v2.py +53 -0
  77. frontend/ui/__init__.py +0 -0
  78. frontend/ui/command_palette.py +153 -0
  79. frontend/ui/diff.py +275 -0
  80. frontend/ui/editor.py +102 -0
  81. frontend/ui/status.py +255 -0
  82. sql_dialect_master-1.1.1.dist-info/METADATA +360 -0
  83. sql_dialect_master-1.1.1.dist-info/RECORD +86 -0
  84. sql_dialect_master-1.1.1.dist-info/WHEEL +5 -0
  85. sql_dialect_master-1.1.1.dist-info/licenses/LICENSE +21 -0
  86. sql_dialect_master-1.1.1.dist-info/top_level.txt +2 -0
@@ -0,0 +1,523 @@
1
+ """Centralized CSS generation for SQL Dialect Master v2.
2
+
3
+ Generates a complete stylesheet using CSS custom properties derived from
4
+ design tokens. All UI components consume these variables — no inline
5
+ style hex values leak into templates.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from .design_tokens import ColorTokens
10
+
11
+
12
+ def generate_css(tokens: ColorTokens) -> str:
13
+ """Generate the full application stylesheet.
14
+
15
+ Args:
16
+ tokens: ColorTokens from the active theme
17
+
18
+ Returns:
19
+ CSS string for injection via st.markdown(..., unsafe_allow_html=True)
20
+ """
21
+ return f"""
22
+ <style>
23
+ /* ── Theme Variables ─────────────────────────────────────────────── */
24
+ :root {{
25
+ --sdm-bg: {tokens.bg};
26
+ --sdm-surface: {tokens.surface};
27
+ --sdm-surface-elev: {tokens.surface_elevated};
28
+ --sdm-border: {tokens.border};
29
+ --sdm-border-subtle: {tokens.border_subtle};
30
+ --sdm-text-primary: {tokens.text_primary};
31
+ --sdm-text-secondary: {tokens.text_secondary};
32
+ --sdm-text-muted: {tokens.text_muted};
33
+ --sdm-text-accent: {tokens.text_on_accent};
34
+ --sdm-accent: {tokens.accent};
35
+ --sdm-success: {tokens.success};
36
+ --sdm-warning: {tokens.warning};
37
+ --sdm-danger: {tokens.danger};
38
+ --sdm-info: {tokens.info};
39
+ --sdm-input-bg: {tokens.input_bg};
40
+ --sdm-input-border: {tokens.input_border};
41
+ --sdm-input-focus: {tokens.input_border_focus};
42
+ --sdm-hover: {tokens.hover_bg};
43
+ --sdm-selected: {tokens.selected_bg};
44
+ --sdm-font-ui: 'Inter', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
45
+ --sdm-font-mono: 'JetBrains Mono', 'SFMono-Regular', Menlo, Monaco, Consolas, monospace;
46
+ --sdm-radius-sm: 4px;
47
+ --sdm-radius-md: 6px;
48
+ --sdm-radius-lg: 8px;
49
+ }}
50
+
51
+ /* ── Reset & Base ─────────────────────────────────────────────────── */
52
+ html, body {{
53
+ font-family: var(--sdm-font-ui);
54
+ color: var(--sdm-text-primary);
55
+ background: var(--sdm-bg);
56
+ font-size: 14px;
57
+ line-height: 1.5;
58
+ }}
59
+
60
+ /* ── App Shell ────────────────────────────────────────────────────── */
61
+ .sdm-app {{
62
+ display: flex;
63
+ flex-direction: column;
64
+ min-height: 100vh;
65
+ }}
66
+
67
+ .sdm-topbar {{
68
+ display: flex;
69
+ align-items: center;
70
+ justify-content: space-between;
71
+ padding: 0 24px;
72
+ height: 48px;
73
+ background: var(--sdm-surface);
74
+ border-bottom: 1px solid var(--sdm-border);
75
+ position: sticky;
76
+ top: 0;
77
+ z-index: 100;
78
+ }}
79
+
80
+ .sdm-topbar-title {{
81
+ font-size: 13px;
82
+ font-weight: 600;
83
+ letter-spacing: 0.05em;
84
+ text-transform: uppercase;
85
+ color: var(--sdm-text-muted);
86
+ margin: 0;
87
+ }}
88
+
89
+ .sdm-topbar-version {{
90
+ font-size: 11px;
91
+ color: var(--sdm-text-muted);
92
+ font-family: var(--sdm-font-mono);
93
+ }}
94
+
95
+ /* ── Sidebar ──────────────────────────────────────────────────────── */
96
+ .sdm-sidebar {{
97
+ width: 220px;
98
+ min-width: 220px;
99
+ background: var(--sdm-surface);
100
+ border-right: 1px solid var(--sdm-border);
101
+ display: flex;
102
+ flex-direction: column;
103
+ padding: 16px 0;
104
+ overflow-y: auto;
105
+ }}
106
+
107
+ .sdm-nav-group {{
108
+ margin-bottom: 20px;
109
+ }}
110
+
111
+ .sdm-nav-group-label {{
112
+ font-size: 10px;
113
+ font-weight: 600;
114
+ letter-spacing: 0.08em;
115
+ text-transform: uppercase;
116
+ color: var(--sdm-text-muted);
117
+ padding: 0 16px;
118
+ margin-bottom: 4px;
119
+ }}
120
+
121
+ .sdm-nav-item {{
122
+ display: flex;
123
+ align-items: center;
124
+ gap: 10px;
125
+ padding: 7px 16px;
126
+ font-size: 13px;
127
+ font-weight: 500;
128
+ color: var(--sdm-text-secondary);
129
+ cursor: pointer;
130
+ border-left: 2px solid transparent;
131
+ text-decoration: none;
132
+ transition: color 0.15s, border-color 0.15s, background 0.15s;
133
+ }}
134
+
135
+ .sdm-nav-item:hover {{
136
+ color: var(--sdm-text-primary);
137
+ background: var(--sdm-hover);
138
+ }}
139
+
140
+ .sdm-nav-item.active {{
141
+ color: var(--sdm-accent);
142
+ border-left-color: var(--sdm-accent);
143
+ background: var(--sdm-selected);
144
+ }}
145
+
146
+ .sdm-nav-icon {{
147
+ font-size: 14px;
148
+ width: 18px;
149
+ text-align: center;
150
+ flex-shrink: 0;
151
+ }}
152
+
153
+ /* ── Main Workspace ───────────────────────────────────────────────── */
154
+ .sdm-main {{
155
+ flex: 1;
156
+ display: flex;
157
+ flex-direction: column;
158
+ overflow: hidden;
159
+ }}
160
+
161
+ .sdm-workspace {{
162
+ display: flex;
163
+ flex: 1;
164
+ overflow: hidden;
165
+ }}
166
+
167
+ .sdm-content {{
168
+ flex: 1;
169
+ overflow-y: auto;
170
+ padding: 24px;
171
+ background: var(--sdm-bg);
172
+ }}
173
+
174
+ .sdm-inspector {{
175
+ width: 320px;
176
+ min-width: 320px;
177
+ background: var(--sdm-surface);
178
+ border-left: 1px solid var(--sdm-border);
179
+ overflow-y: auto;
180
+ padding: 20px;
181
+ display: none;
182
+ }}
183
+
184
+ .sdm-inspector.visible {{
185
+ display: block;
186
+ }}
187
+
188
+ /* ── Editor ───────────────────────────────────────────────────────── */
189
+ .sdm-editor-wrap {{
190
+ border: 1px solid var(--sdm-border);
191
+ border-radius: var(--sdm-radius-md);
192
+ overflow: hidden;
193
+ background: var(--sdm-input-bg);
194
+ }}
195
+
196
+ .sdm-editor-wrap:focus-within {{
197
+ border-color: var(--sdm-input-focus);
198
+ box-shadow: 0 0 0 3px {tokens.accent}20;
199
+ }}
200
+
201
+ .sdm-editor-label {{
202
+ display: flex;
203
+ align-items: center;
204
+ justify-content: space-between;
205
+ padding: 6px 10px;
206
+ background: var(--sdm-surface-elev);
207
+ border-bottom: 1px solid var(--sdm-border);
208
+ font-size: 11px;
209
+ font-weight: 600;
210
+ letter-spacing: 0.06em;
211
+ text-transform: uppercase;
212
+ color: var(--sdm-text-muted);
213
+ }}
214
+
215
+ .sdm-editor-label-dialect {{
216
+ font-family: var(--sdm-font-mono);
217
+ color: var(--sdm-accent);
218
+ font-size: 11px;
219
+ }}
220
+
221
+ .sdm-editor {{
222
+ width: 100%;
223
+ min-height: 240px;
224
+ resize: vertical;
225
+ border: none;
226
+ outline: none;
227
+ background: transparent;
228
+ color: var(--sdm-text-primary);
229
+ font-family: var(--sdm-font-mono);
230
+ font-size: 13px;
231
+ line-height: 1.6;
232
+ padding: 12px;
233
+ tab-size: 4;
234
+ white-space: pre;
235
+ overflow: auto;
236
+ }}
237
+
238
+ .sdm-editor::placeholder {{
239
+ color: var(--sdm-text-muted);
240
+ }}
241
+
242
+ /* ── Toolbar ──────────────────────────────────────────────────────── */
243
+ .sdm-toolbar {{
244
+ display: flex;
245
+ align-items: center;
246
+ gap: 8px;
247
+ padding: 10px 0;
248
+ margin-bottom: 12px;
249
+ border-bottom: 1px solid var(--sdm-border-subtle);
250
+ }}
251
+
252
+ .sdm-btn {{
253
+ display: inline-flex;
254
+ align-items: center;
255
+ gap: 6px;
256
+ padding: 6px 12px;
257
+ font-size: 12px;
258
+ font-weight: 500;
259
+ font-family: var(--sdm-font-ui);
260
+ border: 1px solid var(--sdm-border);
261
+ border-radius: var(--sdm-radius-sm);
262
+ background: var(--sdm-surface);
263
+ color: var(--sdm-text-secondary);
264
+ cursor: pointer;
265
+ transition: all 0.15s;
266
+ white-space: nowrap;
267
+ }}
268
+
269
+ .sdm-btn:hover {{
270
+ background: var(--sdm-hover);
271
+ color: var(--sdm-text-primary);
272
+ border-color: var(--sdm-text-muted);
273
+ }}
274
+
275
+ .sdm-btn-primary {{
276
+ background: var(--sdm-accent);
277
+ border-color: var(--sdm-accent);
278
+ color: var(--sdm-text-accent);
279
+ }}
280
+
281
+ .sdm-btn-primary:hover {{
282
+ filter: brightness(1.1);
283
+ background: var(--sdm-accent);
284
+ color: var(--sdm-text-accent);
285
+ }}
286
+
287
+ .sdm-btn-sm {{
288
+ padding: 4px 8px;
289
+ font-size: 11px;
290
+ }}
291
+
292
+ .sdm-separator {{
293
+ width: 1px;
294
+ height: 20px;
295
+ background: var(--sdm-border);
296
+ margin: 0 4px;
297
+ }}
298
+
299
+ /* ── Panels (source / target) ─────────────────────────────────────── */
300
+ .sdm-panels {{
301
+ display: grid;
302
+ grid-template-columns: 1fr auto 1fr;
303
+ gap: 0;
304
+ align-items: stretch;
305
+ }}
306
+
307
+ .sdm-panel {{
308
+ display: flex;
309
+ flex-direction: column;
310
+ border: 1px solid var(--sdm-border);
311
+ border-radius: var(--sdm-radius-md);
312
+ overflow: hidden;
313
+ background: var(--sdm-surface);
314
+ }}
315
+
316
+ .sdm-panel + .sdm-panel {{
317
+ margin-left: 0;
318
+ }}
319
+
320
+ .sdm-panel-divider {{
321
+ display: flex;
322
+ align-items: center;
323
+ justify-content: center;
324
+ padding: 0 12px;
325
+ color: var(--sdm-text-muted);
326
+ font-size: 18px;
327
+ flex-shrink: 0;
328
+ }}
329
+
330
+ /* ── Status badges ────────────────────────────────────────────────── */
331
+ .sdm-badge {{
332
+ display: inline-flex;
333
+ align-items: center;
334
+ gap: 5px;
335
+ padding: 3px 8px;
336
+ font-size: 11px;
337
+ font-weight: 600;
338
+ letter-spacing: 0.04em;
339
+ border-radius: var(--sdm-radius-sm);
340
+ border: 1px solid transparent;
341
+ }}
342
+
343
+ .sdm-badge-success {{
344
+ background: {tokens.success}15;
345
+ color: var(--sdm-success);
346
+ border-color: {tokens.success}40;
347
+ }}
348
+
349
+ .sdm-badge-warning {{
350
+ background: {tokens.warning}15;
351
+ color: var(--sdm-warning);
352
+ border-color: {tokens.warning}40;
353
+ }}
354
+
355
+ .sdm-badge-error {{
356
+ background: {tokens.danger}15;
357
+ color: var(--sdm-danger);
358
+ border-color: {tokens.danger}40;
359
+ }}
360
+
361
+ .sdm-badge-info {{
362
+ background: {tokens.info}15;
363
+ color: var(--sdm-info);
364
+ border-color: {tokens.info}40;
365
+ }}
366
+
367
+ .sdm-badge-neutral {{
368
+ background: var(--sdm-hover);
369
+ color: var(--sdm-text-muted);
370
+ border-color: var(--sdm-border);
371
+ }}
372
+
373
+ /* ── Result card ──────────────────────────────────────────────────── */
374
+ .sdm-result-card {{
375
+ background: var(--sdm-surface);
376
+ border: 1px solid var(--sdm-border);
377
+ border-radius: var(--sdm-radius-lg);
378
+ overflow: hidden;
379
+ margin-bottom: 16px;
380
+ }}
381
+
382
+ .sdm-result-header {{
383
+ display: flex;
384
+ align-items: center;
385
+ justify-content: space-between;
386
+ padding: 10px 14px;
387
+ border-bottom: 1px solid var(--sdm-border);
388
+ background: var(--sdm-surface-elev);
389
+ }}
390
+
391
+ .sdm-result-header-title {{
392
+ font-size: 12px;
393
+ font-weight: 600;
394
+ letter-spacing: 0.05em;
395
+ text-transform: uppercase;
396
+ color: var(--sdm-text-muted);
397
+ }}
398
+
399
+ .sdm-result-body {{
400
+ padding: 14px;
401
+ }}
402
+
403
+ /* ── Status matrix ────────────────────────────────────────────────── */
404
+ .sdm-status-grid {{
405
+ display: grid;
406
+ grid-template-columns: repeat(2, 1fr);
407
+ gap: 8px;
408
+ margin-bottom: 14px;
409
+ }}
410
+
411
+ .sdm-status-item {{
412
+ display: flex;
413
+ align-items: center;
414
+ gap: 8px;
415
+ padding: 8px 10px;
416
+ background: var(--sdm-input-bg);
417
+ border: 1px solid var(--sdm-border);
418
+ border-radius: var(--sdm-radius-md);
419
+ }}
420
+
421
+ .sdm-status-dot {{
422
+ width: 8px;
423
+ height: 8px;
424
+ border-radius: 50%;
425
+ flex-shrink: 0;
426
+ }}
427
+
428
+ .sdm-status-dot.green {{ background: var(--sdm-success); }}
429
+ .sdm-status-dot.yellow {{ background: var(--sdm-warning); }}
430
+ .sdm-status-dot.red {{ background: var(--sdm-danger); }}
431
+ .sdm-status-dot.gray {{ background: var(--sdm-text-muted); }}
432
+
433
+ .sdm-status-label {{
434
+ font-size: 11px;
435
+ font-weight: 600;
436
+ text-transform: uppercase;
437
+ letter-spacing: 0.05em;
438
+ color: var(--sdm-text-muted);
439
+ }}
440
+
441
+ .sdm-status-value {{
442
+ font-size: 12px;
443
+ font-weight: 500;
444
+ color: var(--sdm-text-primary);
445
+ }}
446
+
447
+ /* ── Inspector ────────────────────────────────────────────────────── */
448
+ .sdm-inspector-section {{
449
+ margin-bottom: 20px;
450
+ }}
451
+
452
+ .sdm-inspector-title {{
453
+ font-size: 10px;
454
+ font-weight: 600;
455
+ letter-spacing: 0.08em;
456
+ text-transform: uppercase;
457
+ color: var(--sdm-text-muted);
458
+ margin-bottom: 8px;
459
+ padding-bottom: 4px;
460
+ border-bottom: 1px solid var(--sdm-border-subtle);
461
+ }}
462
+
463
+ .sdm-inspector-row {{
464
+ display: flex;
465
+ justify-content: space-between;
466
+ padding: 5px 0;
467
+ font-size: 12px;
468
+ }}
469
+
470
+ .sdm-inspector-key {{
471
+ color: var(--sdm-text-muted);
472
+ }}
473
+
474
+ .sdm-inspector-val {{
475
+ color: var(--sdm-text-primary);
476
+ font-family: var(--sdm-font-mono);
477
+ font-size: 11px;
478
+ text-align: right;
479
+ max-width: 60%;
480
+ overflow: hidden;
481
+ text-overflow: ellipsis;
482
+ white-space: nowrap;
483
+ }}
484
+
485
+ /* ── Dialect select wrapper ───────────────────────────────────────── */
486
+ .sdm-dialect-select {{
487
+ font-family: var(--sdm-font-mono);
488
+ font-size: 12px;
489
+ color: var(--sdm-text-primary);
490
+ background: var(--sdm-input-bg);
491
+ border: 1px solid var(--sdm-border);
492
+ border-radius: var(--sdm-radius-sm);
493
+ padding: 4px 8px;
494
+ }}
495
+
496
+ /* ── Scrollbar ────────────────────────────────────────────────────── */
497
+ ::-webkit-scrollbar {{ width: 6px; height: 6px; }}
498
+ ::-webkit-scrollbar-track {{ background: transparent; }}
499
+ ::-webkit-scrollbar-thumb {{ background: var(--sdm-border); border-radius: 3px; }}
500
+ ::-webkit-scrollbar-thumb:hover {{ background: var(--sdm-text-muted); }}
501
+
502
+ /* ── Utility ──────────────────────────────────────────────────────── */
503
+ .sdm-mono {{ font-family: var(--sdm-font-mono); }}
504
+ .sdm-text-muted {{ color: var(--sdm-text-muted); }}
505
+ .sdm-text-secondary {{ color: var(--sdm-text-secondary); }}
506
+ .sdm-mb-8 {{ margin-bottom: 8px; }}
507
+ .sdm-mb-12 {{ margin-bottom: 12px; }}
508
+ .sdm-mb-16 {{ margin-bottom: 16px; }}
509
+ .sdm-mb-20 {{ margin-bottom: 20px; }}
510
+ .sdm-mt-8 {{ margin-top: 8px; }}
511
+ .sdm-flex {{ display: flex; }}
512
+ .sdm-gap-8 {{ gap: 8px; }}
513
+ .sdm-gap-12 {{ gap: 12px; }}
514
+ .sdm-items-center {{ align-items: center; }}
515
+ .sdm-truncate {{ overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }}
516
+ .sdm-w-full {{ width: 100%; }}
517
+ </style>
518
+ """.strip()
519
+
520
+
521
+ def _vars(tokens: ColorTokens) -> ColorTokens:
522
+ """Identity helper — tokens are injected directly via f-string."""
523
+ return tokens
@@ -0,0 +1,69 @@
1
+ """Theme management for SQL Dialect Master v2.
2
+
3
+ Provides theme switching (Dark / Light) and CSS variable generation.
4
+ Custom themes from the old system are no longer supported.
5
+ """
6
+ import json
7
+
8
+ from .design_tokens import THEMES, ColorTokens, ThemeName
9
+
10
+ DEFAULT_THEME: ThemeName = "dark"
11
+
12
+
13
+ def get_theme(theme_name: str) -> ColorTokens:
14
+ """Get theme by name with fallback to default.
15
+
16
+ Args:
17
+ theme_name: Theme name — 'dark' or 'light'
18
+
19
+ Returns:
20
+ ColorTokens for the requested theme
21
+ """
22
+ return THEMES.get(theme_name, THEMES[DEFAULT_THEME])
23
+
24
+
25
+ def list_themes() -> list[str]:
26
+ """Return list of available theme keys."""
27
+ return list(THEMES.keys())
28
+
29
+
30
+ def export_theme(theme: ColorTokens) -> str:
31
+ """Export theme tokens to JSON string.
32
+
33
+ Args:
34
+ theme: ColorTokens instance
35
+
36
+ Returns:
37
+ JSON representation
38
+ """
39
+ return theme.model_dump_json(indent=2)
40
+
41
+
42
+ def validate_theme_json(json_str: str) -> dict:
43
+ """Validate a theme JSON string without applying it.
44
+
45
+ Args:
46
+ json_str: JSON string to validate
47
+
48
+ Returns:
49
+ Parsed dictionary if valid
50
+
51
+ Raises:
52
+ ValueError: If JSON is invalid or missing required keys
53
+ """
54
+ try:
55
+ data = json.loads(json_str)
56
+ except json.JSONDecodeError as exc:
57
+ raise ValueError(f"Invalid JSON: {exc}") from exc
58
+
59
+ required = {
60
+ "bg", "surface", "surface_elevated", "border", "border_subtle",
61
+ "text_primary", "text_secondary", "text_muted", "text_on_accent",
62
+ "accent", "success", "warning", "danger", "info",
63
+ "input_bg", "input_border", "input_border_focus",
64
+ "hover_bg", "selected_bg",
65
+ }
66
+ missing = required - set(data.keys())
67
+ if missing:
68
+ raise ValueError(f"Missing required theme keys: {', '.join(sorted(missing))}")
69
+ return data