queryview 0.0.2__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 (54) hide show
  1. queryview/__init__.py +0 -0
  2. queryview/conftest.py +86 -0
  3. queryview/connect.py +428 -0
  4. queryview/dashboard_queries.py +75 -0
  5. queryview/dashboards.py +156 -0
  6. queryview/drivers/__init__.py +10 -0
  7. queryview/drivers/base.py +165 -0
  8. queryview/drivers/clickhouse.py +138 -0
  9. queryview/drivers/duckdb.py +153 -0
  10. queryview/drivers/postgres.py +166 -0
  11. queryview/drivers/test_base.py +49 -0
  12. queryview/drivers/test_clickhouse.py +53 -0
  13. queryview/drivers/test_contract.py +77 -0
  14. queryview/drivers/test_duckdb.py +80 -0
  15. queryview/drivers/test_postgres.py +80 -0
  16. queryview/gitsync.py +374 -0
  17. queryview/main.py +740 -0
  18. queryview/mcp_server.py +294 -0
  19. queryview/migrations/env.py +39 -0
  20. queryview/migrations/script.py.mako +29 -0
  21. queryview/migrations/versions/9a536b7c0328_initial_schema.py +89 -0
  22. queryview/migrations/versions/a1b2c3d4e5f6_connection_config_blob.py +59 -0
  23. queryview/migrations/versions/b2c3d4e5f6a7_predefined_presentation.py +32 -0
  24. queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py +98 -0
  25. queryview/queries.py +159 -0
  26. queryview/remote.py +141 -0
  27. queryview/static/assets/index-CvnC_D68.js +47 -0
  28. queryview/static/assets/index-Qe7bhycG.css +2 -0
  29. queryview/static/favicon.svg +1 -0
  30. queryview/static/index.html +14 -0
  31. queryview/test_api_db.py +51 -0
  32. queryview/test_api_export_import.py +85 -0
  33. queryview/test_api_gitsync.py +83 -0
  34. queryview/test_api_workspaces.py +44 -0
  35. queryview/test_connect_flow.py +123 -0
  36. queryview/test_connect_store.py +34 -0
  37. queryview/test_dashboards.py +216 -0
  38. queryview/test_gitsync.py +346 -0
  39. queryview/test_main.py +18 -0
  40. queryview/test_mcp_gitsync.py +72 -0
  41. queryview/test_migrations.py +99 -0
  42. queryview/test_queries.py +170 -0
  43. queryview/test_remote.py +260 -0
  44. queryview/test_validation.py +87 -0
  45. queryview/test_workspaces.py +109 -0
  46. queryview/test_yamlio.py +198 -0
  47. queryview/validation.py +111 -0
  48. queryview/workspaces.py +167 -0
  49. queryview/yamlio.py +245 -0
  50. queryview-0.0.2.dist-info/METADATA +183 -0
  51. queryview-0.0.2.dist-info/RECORD +54 -0
  52. queryview-0.0.2.dist-info/WHEEL +4 -0
  53. queryview-0.0.2.dist-info/entry_points.txt +3 -0
  54. queryview-0.0.2.dist-info/licenses/LICENSE +21 -0
@@ -0,0 +1,2 @@
1
+ /*! tailwindcss v4.3.0 | MIT License | https://tailwindcss.com */
2
+ @layer properties{@supports (((-webkit-hyphens:none)) and (not (margin-trim:inline))) or ((-moz-orient:inline) and (not (color:rgb(from red r g b)))){*,:before,:after,::backdrop{--tw-translate-x:0;--tw-translate-y:0;--tw-translate-z:0;--tw-rotate-x:initial;--tw-rotate-y:initial;--tw-rotate-z:initial;--tw-skew-x:initial;--tw-skew-y:initial;--tw-space-y-reverse:0;--tw-border-style:solid;--tw-font-weight:initial;--tw-tracking:initial;--tw-shadow:0 0 #0000;--tw-shadow-color:initial;--tw-shadow-alpha:100%;--tw-inset-shadow:0 0 #0000;--tw-inset-shadow-color:initial;--tw-inset-shadow-alpha:100%;--tw-ring-color:initial;--tw-ring-shadow:0 0 #0000;--tw-inset-ring-color:initial;--tw-inset-ring-shadow:0 0 #0000;--tw-ring-inset:initial;--tw-ring-offset-width:0px;--tw-ring-offset-color:#fff;--tw-ring-offset-shadow:0 0 #0000;--tw-blur:initial;--tw-brightness:initial;--tw-contrast:initial;--tw-grayscale:initial;--tw-hue-rotate:initial;--tw-invert:initial;--tw-opacity:initial;--tw-saturate:initial;--tw-sepia:initial;--tw-drop-shadow:initial;--tw-drop-shadow-color:initial;--tw-drop-shadow-alpha:100%;--tw-drop-shadow-size:initial;--tw-backdrop-blur:initial;--tw-backdrop-brightness:initial;--tw-backdrop-contrast:initial;--tw-backdrop-grayscale:initial;--tw-backdrop-hue-rotate:initial;--tw-backdrop-invert:initial;--tw-backdrop-opacity:initial;--tw-backdrop-saturate:initial;--tw-backdrop-sepia:initial}}}@layer theme{:root,:host{--font-sans:ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";--font-mono:ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace;--color-red-200:oklch(88.5% .062 18.334);--color-red-300:oklch(80.8% .114 19.571);--color-red-400:oklch(70.4% .191 22.216);--color-red-500:oklch(63.7% .237 25.331);--color-emerald-300:oklch(84.5% .143 164.978);--color-emerald-500:oklch(69.6% .17 162.48);--color-indigo-200:oklch(87% .065 274.039);--color-indigo-300:oklch(78.5% .115 274.713);--color-indigo-400:oklch(67.3% .182 276.935);--color-indigo-500:oklch(58.5% .233 277.117);--color-slate-100:oklch(96.8% .007 247.896);--color-slate-200:oklch(92.9% .013 255.508);--color-slate-300:oklch(86.9% .022 252.894);--color-slate-400:oklch(70.4% .04 256.788);--color-slate-500:oklch(55.4% .046 257.417);--color-slate-800:oklch(27.9% .041 260.031);--color-slate-900:oklch(20.8% .042 265.755);--color-black:#000;--color-white:#fff;--spacing:.25rem;--container-md:28rem;--container-2xl:42rem;--text-xs:.75rem;--text-xs--line-height:calc(1 / .75);--text-sm:.875rem;--text-sm--line-height:calc(1.25 / .875);--text-base:1rem;--text-base--line-height:calc(1.5 / 1);--text-lg:1.125rem;--text-lg--line-height:calc(1.75 / 1.125);--text-2xl:1.5rem;--text-2xl--line-height:calc(2 / 1.5);--text-3xl:1.875rem;--text-3xl--line-height:calc(2.25 / 1.875);--font-weight-normal:400;--font-weight-medium:500;--font-weight-semibold:600;--font-weight-bold:700;--tracking-tight:-.025em;--radius-md:.375rem;--radius-lg:.5rem;--radius-xl:.75rem;--blur-sm:8px;--blur-lg:16px;--default-transition-duration:.15s;--default-transition-timing-function:cubic-bezier(.4, 0, .2, 1);--default-font-family:var(--font-sans);--default-mono-font-family:var(--font-mono)}}@layer base{*,:after,:before,::backdrop{box-sizing:border-box;border:0 solid;margin:0;padding:0}::file-selector-button{box-sizing:border-box;border:0 solid;margin:0;padding:0}html,:host{-webkit-text-size-adjust:100%;tab-size:4;line-height:1.5;font-family:var(--default-font-family,ui-sans-serif, system-ui, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji");font-feature-settings:var(--default-font-feature-settings,normal);font-variation-settings:var(--default-font-variation-settings,normal);-webkit-tap-highlight-color:transparent}hr{height:0;color:inherit;border-top-width:1px}abbr:where([title]){-webkit-text-decoration:underline dotted;text-decoration:underline dotted}h1,h2,h3,h4,h5,h6{font-size:inherit;font-weight:inherit}a{color:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;-webkit-text-decoration:inherit;text-decoration:inherit}b,strong{font-weight:bolder}code,kbd,samp,pre{font-family:var(--default-mono-font-family,ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace);font-feature-settings:var(--default-mono-font-feature-settings,normal);font-variation-settings:var(--default-mono-font-variation-settings,normal);font-size:1em}small{font-size:80%}sub,sup{vertical-align:baseline;font-size:75%;line-height:0;position:relative}sub{bottom:-.25em}sup{top:-.5em}table{text-indent:0;border-color:inherit;border-collapse:collapse}:-moz-focusring{outline:auto}progress{vertical-align:baseline}summary{display:list-item}ol,ul,menu{list-style:none}img,svg,video,canvas,audio,iframe,embed,object{vertical-align:middle;display:block}img,video{max-width:100%;height:auto}button,input,select,optgroup,textarea{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}::file-selector-button{font:inherit;font-feature-settings:inherit;font-variation-settings:inherit;letter-spacing:inherit;color:inherit;opacity:1;background-color:#0000;border-radius:0}:where(select:is([multiple],[size])) optgroup{font-weight:bolder}:where(select:is([multiple],[size])) optgroup option{padding-inline-start:20px}::file-selector-button{margin-inline-end:4px}::placeholder{opacity:1}@supports (not ((-webkit-appearance:-apple-pay-button))) or (contain-intrinsic-size:1px){::placeholder{color:currentColor}@supports (color:color-mix(in lab, red, red)){::placeholder{color:color-mix(in oklab, currentcolor 50%, transparent)}}}textarea{resize:vertical}::-webkit-search-decoration{-webkit-appearance:none}::-webkit-date-and-time-value{min-height:1lh;text-align:inherit}::-webkit-datetime-edit{display:inline-flex}::-webkit-datetime-edit-fields-wrapper{padding:0}::-webkit-datetime-edit{padding-block:0}::-webkit-datetime-edit-year-field{padding-block:0}::-webkit-datetime-edit-month-field{padding-block:0}::-webkit-datetime-edit-day-field{padding-block:0}::-webkit-datetime-edit-hour-field{padding-block:0}::-webkit-datetime-edit-minute-field{padding-block:0}::-webkit-datetime-edit-second-field{padding-block:0}::-webkit-datetime-edit-millisecond-field{padding-block:0}::-webkit-datetime-edit-meridiem-field{padding-block:0}::-webkit-calendar-picker-indicator{line-height:1}:-moz-ui-invalid{box-shadow:none}button,input:where([type=button],[type=reset],[type=submit]){appearance:button}::file-selector-button{appearance:button}::-webkit-inner-spin-button{height:auto}::-webkit-outer-spin-button{height:auto}[hidden]:where(:not([hidden=until-found])){display:none!important}html{min-height:100%}body{--lightningcss-light: ;--lightningcss-dark:initial;color-scheme:dark;color:#e2e8f0;background-color:#0000;background-image:radial-gradient(820px 620px at 12% 6%,#6366f14d,#0000 62%),radial-gradient(760px 580px at 88% 14%,#a855f73d,#0000 60%),radial-gradient(900px 720px at 78% 104%,#2dd4bf29,#0000 60%),linear-gradient(160deg,#070a14 0%,#0c1124 52%,#160f2b 100%);background-position:0 0,0 0,0 0,0 0;background-repeat:repeat,repeat,repeat,repeat;background-size:auto,auto,auto,auto;background-attachment:fixed;background-origin:padding-box,padding-box,padding-box,padding-box;background-clip:border-box,border-box,border-box,border-box;min-height:100vh}}@layer components{.glass-panel{-webkit-backdrop-filter:blur(22px)saturate(170%);background:#ffffff06;border:1px solid #ffffff1f;border-radius:1rem;box-shadow:0 16px 40px -16px #000000b3,inset 0 1px #ffffff29}.glass-popover{-webkit-backdrop-filter:blur(26px)saturate(160%);background:#121626f0;border:1px solid #ffffff24;border-radius:.9rem;box-shadow:0 20px 50px -20px #000c,inset 0 1px #ffffff29}.glass-chip{-webkit-backdrop-filter:blur(16px)saturate(140%);color:#e2e8f0;background:#ffffff0f;border:1px solid #ffffff1f;border-radius:9999px;box-shadow:inset 0 1px #ffffff1f}.glass-input{color:#e2e8f0;background:#ffffff0a;border:1px solid #ffffff1a;border-radius:.75rem;outline:none;transition:border-color .15s,background-color .15s,box-shadow .15s}.glass-input::placeholder{color:#94a3b8b3}.glass-input:focus{background:#ffffff12;border-color:#818cf8b3;box-shadow:0 0 0 3px #6366f14d}.glass-btn{color:#e2e8f0;background:#ffffff0d;border:1px solid #ffffff1f;border-radius:.75rem;transition:background-color .15s,border-color .15s,transform .1s}.glass-btn:hover:not(:disabled){background:#ffffff1a;border-color:#ffffff38}.glass-btn:active:not(:disabled){transform:scale(.98)}.glass-btn:disabled{opacity:.4;cursor:not-allowed}.glass-btn-primary{color:#fff;background-image:linear-gradient(#818cf8f2,#4f46e5f2);border:1px solid #818cf880;border-radius:.75rem;transition:filter .15s,transform .1s;box-shadow:0 10px 26px -10px #4f46e5bf,inset 0 1px #ffffff47}.glass-btn-primary:hover:not(:disabled){filter:brightness(1.1)}.glass-btn-primary:active:not(:disabled){transform:scale(.98)}.glass-btn-primary:disabled{opacity:.45;cursor:not-allowed}.glass-toggle{color:#cbd5e1;background:#ffffff0a;border:1px solid #ffffff1f;border-radius:.6rem;transition:background-color .15s,border-color .15s,color .15s}.glass-toggle:hover:not(:disabled){color:#e2e8f0;background:#ffffff17;border-color:#ffffff38}.glass-toggle:disabled{opacity:.4;cursor:not-allowed}.glass-toggle.is-active{color:#fff;background-image:linear-gradient(#818cf8e6,#4f46e5e6);border-color:#818cf899;box-shadow:0 8px 20px -10px #4f46e5b3,inset 0 1px #ffffff40}.glass-toggle.is-active-soft{color:#c7d2fe;background:#6366f129;border-color:#818cf88c}}@layer utilities{.collapse{visibility:collapse}.visible{visibility:visible}.absolute{position:absolute}.fixed{position:fixed}.relative{position:relative}.static{position:static}.sticky{position:sticky}.inset-0{inset:calc(var(--spacing) * 0)}.top-0{top:calc(var(--spacing) * 0)}.top-4{top:calc(var(--spacing) * 4)}.top-full{top:100%}.right-0{right:calc(var(--spacing) * 0)}.right-4{right:calc(var(--spacing) * 4)}.bottom-4{bottom:calc(var(--spacing) * 4)}.left-0{left:calc(var(--spacing) * 0)}.left-1\/2{left:50%}.left-4{left:calc(var(--spacing) * 4)}.z-10{z-index:10}.z-20{z-index:20}.z-30{z-index:30}.mt-1{margin-top:calc(var(--spacing) * 1)}.mt-2{margin-top:calc(var(--spacing) * 2)}.mt-3{margin-top:calc(var(--spacing) * 3)}.mt-4{margin-top:calc(var(--spacing) * 4)}.mt-6{margin-top:calc(var(--spacing) * 6)}.mt-10{margin-top:calc(var(--spacing) * 10)}.mr-2{margin-right:calc(var(--spacing) * 2)}.mr-auto{margin-right:auto}.mb-2{margin-bottom:calc(var(--spacing) * 2)}.mb-3{margin-bottom:calc(var(--spacing) * 3)}.mb-4{margin-bottom:calc(var(--spacing) * 4)}.mb-6{margin-bottom:calc(var(--spacing) * 6)}.ml-1{margin-left:calc(var(--spacing) * 1)}.ml-3{margin-left:calc(var(--spacing) * 3)}.block{display:block}.contents{display:contents}.flex{display:flex}.grid{display:grid}.hidden{display:none}.inline{display:inline}.inline-block{display:inline-block}.table{display:table}.h-0{height:calc(var(--spacing) * 0)}.h-2\.5{height:calc(var(--spacing) * 2.5)}.h-4{height:calc(var(--spacing) * 4)}.h-8{height:calc(var(--spacing) * 8)}.h-\[78vh\]{height:78vh}.max-h-72{max-height:calc(var(--spacing) * 72)}.max-h-\[70vh\]{max-height:70vh}.min-h-0{min-height:calc(var(--spacing) * 0)}.min-h-screen{min-height:100vh}.w-2\.5{width:calc(var(--spacing) * 2.5)}.w-4{width:calc(var(--spacing) * 4)}.w-8{width:calc(var(--spacing) * 8)}.w-20{width:calc(var(--spacing) * 20)}.w-64{width:calc(var(--spacing) * 64)}.w-72{width:calc(var(--spacing) * 72)}.w-80{width:calc(var(--spacing) * 80)}.w-96{width:calc(var(--spacing) * 96)}.w-full{width:100%}.max-w-2xl{max-width:var(--container-2xl)}.max-w-\[80vw\]{max-width:80vw}.max-w-\[85vw\]{max-width:85vw}.max-w-md{max-width:var(--container-md)}.min-w-0{min-width:calc(var(--spacing) * 0)}.min-w-48{min-width:calc(var(--spacing) * 48)}.min-w-\[4\.5rem\]{min-width:4.5rem}.min-w-\[5rem\]{min-width:5rem}.min-w-full{min-width:100%}.flex-1{flex:1}.shrink-0{flex-shrink:0}.border-collapse{border-collapse:collapse}.-translate-x-1\/2{--tw-translate-x:calc(calc(1 / 2 * 100%) * -1);translate:var(--tw-translate-x) var(--tw-translate-y)}.transform{transform:var(--tw-rotate-x,) var(--tw-rotate-y,) var(--tw-rotate-z,) var(--tw-skew-x,) var(--tw-skew-y,)}.cursor-pointer{cursor:pointer}.flex-col{flex-direction:column}.flex-wrap{flex-wrap:wrap}.items-baseline{align-items:baseline}.items-center{align-items:center}.items-start{align-items:flex-start}.justify-between{justify-content:space-between}.justify-center{justify-content:center}.justify-end{justify-content:flex-end}.gap-0\.5{gap:calc(var(--spacing) * .5)}.gap-1{gap:calc(var(--spacing) * 1)}.gap-1\.5{gap:calc(var(--spacing) * 1.5)}.gap-2{gap:calc(var(--spacing) * 2)}.gap-3{gap:calc(var(--spacing) * 3)}.gap-4{gap:calc(var(--spacing) * 4)}:where(.space-y-1>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 1) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 1) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-2>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 2) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 2) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-3>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 3) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 3) * calc(1 - var(--tw-space-y-reverse)))}:where(.space-y-4>:not(:last-child)){--tw-space-y-reverse:0;margin-block-start:calc(calc(var(--spacing) * 4) * var(--tw-space-y-reverse));margin-block-end:calc(calc(var(--spacing) * 4) * calc(1 - var(--tw-space-y-reverse)))}.gap-x-4{column-gap:calc(var(--spacing) * 4)}.gap-y-2{row-gap:calc(var(--spacing) * 2)}.self-start{align-self:flex-start}.truncate{text-overflow:ellipsis;white-space:nowrap;overflow:hidden}.overflow-auto{overflow:auto}.overflow-hidden{overflow:hidden}.overflow-y-auto{overflow-y:auto}.rounded{border-radius:.25rem}.rounded-full{border-radius:3.40282e38px}.rounded-lg{border-radius:var(--radius-lg)}.rounded-md{border-radius:var(--radius-md)}.rounded-xl{border-radius:var(--radius-xl)}.border{border-style:var(--tw-border-style);border-width:1px}.border-t{border-top-style:var(--tw-border-style);border-top-width:1px}.border-b{border-bottom-style:var(--tw-border-style);border-bottom-width:1px}.border-l{border-left-style:var(--tw-border-style);border-left-width:1px}.border-indigo-400\/40{border-color:#7d87ff66}@supports (color:color-mix(in lab, red, red)){.border-indigo-400\/40{border-color:color-mix(in oklab, var(--color-indigo-400) 40%, transparent)}}.border-red-400\/40{border-color:#ff656866}@supports (color:color-mix(in lab, red, red)){.border-red-400\/40{border-color:color-mix(in oklab, var(--color-red-400) 40%, transparent)}}.border-transparent{border-color:#0000}.border-white\/5{border-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.border-white\/5{border-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.border-white\/10{border-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.border-white\/10{border-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.border-white\/15{border-color:#ffffff26}@supports (color:color-mix(in lab, red, red)){.border-white\/15{border-color:color-mix(in oklab, var(--color-white) 15%, transparent)}}.bg-\[rgba\(16\,20\,36\,0\.62\)\]{background-color:#1014249e}.bg-black\/50{background-color:#00000080}@supports (color:color-mix(in lab, red, red)){.bg-black\/50{background-color:color-mix(in oklab, var(--color-black) 50%, transparent)}}.bg-emerald-500{background-color:var(--color-emerald-500)}.bg-indigo-500\/30{background-color:#625fff4d}@supports (color:color-mix(in lab, red, red)){.bg-indigo-500\/30{background-color:color-mix(in oklab, var(--color-indigo-500) 30%, transparent)}}.bg-red-500\/10{background-color:#fb2c361a}@supports (color:color-mix(in lab, red, red)){.bg-red-500\/10{background-color:color-mix(in oklab, var(--color-red-500) 10%, transparent)}}.bg-slate-800\/90{background-color:#1d293de6}@supports (color:color-mix(in lab, red, red)){.bg-slate-800\/90{background-color:color-mix(in oklab, var(--color-slate-800) 90%, transparent)}}.bg-slate-900\/95{background-color:#0f172bf2}@supports (color:color-mix(in lab, red, red)){.bg-slate-900\/95{background-color:color-mix(in oklab, var(--color-slate-900) 95%, transparent)}}.bg-white{background-color:var(--color-white)}.bg-white\/10{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.bg-white\/10{background-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.bg-white\/\[0\.03\]{background-color:#ffffff08}@supports (color:color-mix(in lab, red, red)){.bg-white\/\[0\.03\]{background-color:color-mix(in oklab, var(--color-white) 3%, transparent)}}.bg-white\/\[0\.06\]{background-color:#ffffff0f}@supports (color:color-mix(in lab, red, red)){.bg-white\/\[0\.06\]{background-color:color-mix(in oklab, var(--color-white) 6%, transparent)}}.p-1{padding:calc(var(--spacing) * 1)}.p-2{padding:calc(var(--spacing) * 2)}.p-3{padding:calc(var(--spacing) * 3)}.p-4{padding:calc(var(--spacing) * 4)}.p-5{padding:calc(var(--spacing) * 5)}.p-6{padding:calc(var(--spacing) * 6)}.px-1\.5{padding-inline:calc(var(--spacing) * 1.5)}.px-2{padding-inline:calc(var(--spacing) * 2)}.px-2\.5{padding-inline:calc(var(--spacing) * 2.5)}.px-3{padding-inline:calc(var(--spacing) * 3)}.px-4{padding-inline:calc(var(--spacing) * 4)}.px-6{padding-inline:calc(var(--spacing) * 6)}.py-0{padding-block:calc(var(--spacing) * 0)}.py-0\.5{padding-block:calc(var(--spacing) * .5)}.py-1{padding-block:calc(var(--spacing) * 1)}.py-1\.5{padding-block:calc(var(--spacing) * 1.5)}.py-2{padding-block:calc(var(--spacing) * 2)}.py-3{padding-block:calc(var(--spacing) * 3)}.py-10{padding-block:calc(var(--spacing) * 10)}.pt-1{padding-top:calc(var(--spacing) * 1)}.pl-2{padding-left:calc(var(--spacing) * 2)}.text-center{text-align:center}.text-left{text-align:left}.font-mono{font-family:var(--font-mono)}.text-2xl{font-size:var(--text-2xl);line-height:var(--tw-leading,var(--text-2xl--line-height))}.text-3xl{font-size:var(--text-3xl);line-height:var(--tw-leading,var(--text-3xl--line-height))}.text-base{font-size:var(--text-base);line-height:var(--tw-leading,var(--text-base--line-height))}.text-lg{font-size:var(--text-lg);line-height:var(--tw-leading,var(--text-lg--line-height))}.text-sm{font-size:var(--text-sm);line-height:var(--tw-leading,var(--text-sm--line-height))}.text-xs{font-size:var(--text-xs);line-height:var(--tw-leading,var(--text-xs--line-height))}.font-bold{--tw-font-weight:var(--font-weight-bold);font-weight:var(--font-weight-bold)}.font-medium{--tw-font-weight:var(--font-weight-medium);font-weight:var(--font-weight-medium)}.font-normal{--tw-font-weight:var(--font-weight-normal);font-weight:var(--font-weight-normal)}.font-semibold{--tw-font-weight:var(--font-weight-semibold);font-weight:var(--font-weight-semibold)}.tracking-tight{--tw-tracking:var(--tracking-tight);letter-spacing:var(--tracking-tight)}.whitespace-nowrap{white-space:nowrap}.whitespace-pre{white-space:pre}.text-emerald-300{color:var(--color-emerald-300)}.text-indigo-200{color:var(--color-indigo-200)}.text-indigo-300{color:var(--color-indigo-300)}.text-red-200{color:var(--color-red-200)}.text-red-300{color:var(--color-red-300)}.text-slate-100{color:var(--color-slate-100)}.text-slate-200{color:var(--color-slate-200)}.text-slate-300{color:var(--color-slate-300)}.text-slate-400{color:var(--color-slate-400)}.text-slate-500{color:var(--color-slate-500)}.text-white{color:var(--color-white)}.uppercase{text-transform:uppercase}.underline{text-decoration-line:underline}.shadow-lg{--tw-shadow:0 10px 15px -3px var(--tw-shadow-color,#0000001a), 0 4px 6px -4px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.shadow-xl{--tw-shadow:0 20px 25px -5px var(--tw-shadow-color,#0000001a), 0 8px 10px -6px var(--tw-shadow-color,#0000001a);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-1{--tw-ring-shadow:var(--tw-ring-inset,) 0 0 0 calc(1px + var(--tw-ring-offset-width)) var(--tw-ring-color,currentcolor);box-shadow:var(--tw-inset-shadow), var(--tw-inset-ring-shadow), var(--tw-ring-offset-shadow), var(--tw-ring-shadow), var(--tw-shadow)}.ring-white\/10{--tw-ring-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.ring-white\/10{--tw-ring-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.blur{--tw-blur:blur(8px);filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.filter{filter:var(--tw-blur,) var(--tw-brightness,) var(--tw-contrast,) var(--tw-grayscale,) var(--tw-hue-rotate,) var(--tw-invert,) var(--tw-saturate,) var(--tw-sepia,) var(--tw-drop-shadow,)}.backdrop-blur{--tw-backdrop-blur:blur(8px);-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-lg{--tw-backdrop-blur:blur(var(--blur-lg));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.backdrop-blur-sm{--tw-backdrop-blur:blur(var(--blur-sm));-webkit-backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,);backdrop-filter:var(--tw-backdrop-blur,) var(--tw-backdrop-brightness,) var(--tw-backdrop-contrast,) var(--tw-backdrop-grayscale,) var(--tw-backdrop-hue-rotate,) var(--tw-backdrop-invert,) var(--tw-backdrop-opacity,) var(--tw-backdrop-saturate,) var(--tw-backdrop-sepia,)}.transition{transition-property:color,background-color,border-color,outline-color,text-decoration-color,fill,stroke,--tw-gradient-from,--tw-gradient-via,--tw-gradient-to,opacity,box-shadow,transform,translate,scale,rotate,filter,-webkit-backdrop-filter,backdrop-filter,display,content-visibility,overlay,pointer-events;transition-timing-function:var(--tw-ease,var(--default-transition-timing-function));transition-duration:var(--tw-duration,var(--default-transition-duration))}.\[text-shadow\:0_2px_30px_rgba\(129\,140\,248\,0\.45\)\]{text-shadow:0 2px 30px #818cf873}.last\:border-b-0:last-child{border-bottom-style:var(--tw-border-style);border-bottom-width:0}.odd\:bg-transparent:nth-child(odd){background-color:#0000}.even\:bg-white\/\[0\.03\]:nth-child(2n){background-color:#ffffff08}@supports (color:color-mix(in lab, red, red)){.even\:bg-white\/\[0\.03\]:nth-child(2n){background-color:color-mix(in oklab, var(--color-white) 3%, transparent)}}@media (hover:hover){.hover\:bg-white\/5:hover{background-color:#ffffff0d}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/5:hover{background-color:color-mix(in oklab, var(--color-white) 5%, transparent)}}.hover\:bg-white\/10:hover{background-color:#ffffff1a}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/10:hover{background-color:color-mix(in oklab, var(--color-white) 10%, transparent)}}.hover\:bg-white\/20:hover{background-color:#fff3}@supports (color:color-mix(in lab, red, red)){.hover\:bg-white\/20:hover{background-color:color-mix(in oklab, var(--color-white) 20%, transparent)}}.hover\:text-indigo-200:hover{color:var(--color-indigo-200)}.hover\:text-red-400:hover{color:var(--color-red-400)}.hover\:text-slate-200:hover{color:var(--color-slate-200)}}}@property --tw-translate-x{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-y{syntax:"*";inherits:false;initial-value:0}@property --tw-translate-z{syntax:"*";inherits:false;initial-value:0}@property --tw-rotate-x{syntax:"*";inherits:false}@property --tw-rotate-y{syntax:"*";inherits:false}@property --tw-rotate-z{syntax:"*";inherits:false}@property --tw-skew-x{syntax:"*";inherits:false}@property --tw-skew-y{syntax:"*";inherits:false}@property --tw-space-y-reverse{syntax:"*";inherits:false;initial-value:0}@property --tw-border-style{syntax:"*";inherits:false;initial-value:solid}@property --tw-font-weight{syntax:"*";inherits:false}@property --tw-tracking{syntax:"*";inherits:false}@property --tw-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-shadow-color{syntax:"*";inherits:false}@property --tw-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-inset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-shadow-color{syntax:"*";inherits:false}@property --tw-inset-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-ring-color{syntax:"*";inherits:false}@property --tw-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-inset-ring-color{syntax:"*";inherits:false}@property --tw-inset-ring-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-ring-inset{syntax:"*";inherits:false}@property --tw-ring-offset-width{syntax:"<length>";inherits:false;initial-value:0}@property --tw-ring-offset-color{syntax:"*";inherits:false;initial-value:#fff}@property --tw-ring-offset-shadow{syntax:"*";inherits:false;initial-value:0 0 #0000}@property --tw-blur{syntax:"*";inherits:false}@property --tw-brightness{syntax:"*";inherits:false}@property --tw-contrast{syntax:"*";inherits:false}@property --tw-grayscale{syntax:"*";inherits:false}@property --tw-hue-rotate{syntax:"*";inherits:false}@property --tw-invert{syntax:"*";inherits:false}@property --tw-opacity{syntax:"*";inherits:false}@property --tw-saturate{syntax:"*";inherits:false}@property --tw-sepia{syntax:"*";inherits:false}@property --tw-drop-shadow{syntax:"*";inherits:false}@property --tw-drop-shadow-color{syntax:"*";inherits:false}@property --tw-drop-shadow-alpha{syntax:"<percentage>";inherits:false;initial-value:100%}@property --tw-drop-shadow-size{syntax:"*";inherits:false}@property --tw-backdrop-blur{syntax:"*";inherits:false}@property --tw-backdrop-brightness{syntax:"*";inherits:false}@property --tw-backdrop-contrast{syntax:"*";inherits:false}@property --tw-backdrop-grayscale{syntax:"*";inherits:false}@property --tw-backdrop-hue-rotate{syntax:"*";inherits:false}@property --tw-backdrop-invert{syntax:"*";inherits:false}@property --tw-backdrop-opacity{syntax:"*";inherits:false}@property --tw-backdrop-saturate{syntax:"*";inherits:false}@property --tw-backdrop-sepia{syntax:"*";inherits:false}
@@ -0,0 +1 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="48" height="46" fill="none" viewBox="0 0 48 46"><path fill="#863bff" d="M25.946 44.938c-.664.845-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.287c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.497 0-3.578-1.842-3.578H1.237c-.92 0-1.456-1.04-.92-1.788L10.013.474c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.579 1.842 3.579h11.377c.943 0 1.473 1.088.89 1.83L25.947 44.94z" style="fill:#863bff;fill:color(display-p3 .5252 .23 1);fill-opacity:1"/><mask id="a" width="48" height="46" x="0" y="0" maskUnits="userSpaceOnUse" style="mask-type:alpha"><path fill="#000" d="M25.842 44.938c-.664.844-2.021.375-2.021-.698V33.937a2.26 2.26 0 0 0-2.262-2.262H10.183c-.92 0-1.456-1.04-.92-1.788l7.48-10.471c1.07-1.498 0-3.579-1.842-3.579H1.133c-.92 0-1.456-1.04-.92-1.787L9.91.473c.214-.297.556-.474.92-.474h28.894c.92 0 1.456 1.04.92 1.788l-7.48 10.471c-1.07 1.498 0 3.578 1.842 3.578h11.377c.943 0 1.473 1.088.89 1.832L25.843 44.94z" style="fill:#000;fill-opacity:1"/></mask><g mask="url(#a)"><g filter="url(#b)"><ellipse cx="5.508" cy="14.704" fill="#ede6ff" rx="5.508" ry="14.704" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -4.47 31.516)"/></g><g filter="url(#c)"><ellipse cx="10.399" cy="29.851" fill="#ede6ff" rx="10.399" ry="29.851" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -39.328 7.883)"/></g><g filter="url(#d)"><ellipse cx="5.508" cy="30.487" fill="#7e14ff" rx="5.508" ry="30.487" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -25.913 -14.639)scale(1 -1)"/></g><g filter="url(#e)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.814 -32.644 -3.334)scale(1 -1)"/></g><g filter="url(#f)"><ellipse cx="5.508" cy="30.599" fill="#7e14ff" rx="5.508" ry="30.599" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="matrix(.00324 1 1 -.00324 -34.34 30.47)"/></g><g filter="url(#g)"><ellipse cx="14.072" cy="22.078" fill="#ede6ff" rx="14.072" ry="22.078" style="fill:#ede6ff;fill:color(display-p3 .9275 .9033 1);fill-opacity:1" transform="rotate(93.35 24.506 48.493)scale(-1 1)"/></g><g filter="url(#h)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#i)"><ellipse cx="3.47" cy="21.501" fill="#7e14ff" rx="3.47" ry="21.501" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(89.009 28.708 47.59)scale(-1 1)"/></g><g filter="url(#j)"><ellipse cx=".387" cy="8.972" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(39.51 .387 8.972)"/></g><g filter="url(#k)"><ellipse cx="47.523" cy="-6.092" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 47.523 -6.092)"/></g><g filter="url(#l)"><ellipse cx="41.412" cy="6.333" fill="#47bfff" rx="5.971" ry="9.665" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 41.412 6.333)"/></g><g filter="url(#m)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#n)"><ellipse cx="-1.879" cy="38.332" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 -1.88 38.332)"/></g><g filter="url(#o)"><ellipse cx="35.651" cy="29.907" fill="#7e14ff" rx="4.407" ry="29.108" style="fill:#7e14ff;fill:color(display-p3 .4922 .0767 1);fill-opacity:1" transform="rotate(37.892 35.651 29.907)"/></g><g filter="url(#p)"><ellipse cx="38.418" cy="32.4" fill="#47bfff" rx="5.971" ry="15.297" style="fill:#47bfff;fill:color(display-p3 .2799 .748 1);fill-opacity:1" transform="rotate(37.892 38.418 32.4)"/></g></g><defs><filter id="b" width="60.045" height="41.654" x="-19.77" y="16.149" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="c" width="90.34" height="51.437" x="-54.613" y="-7.533" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="d" width="79.355" height="29.4" x="-49.64" y="2.03" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="e" width="79.579" height="29.4" x="-45.045" y="20.029" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="f" width="79.579" height="29.4" x="-43.513" y="21.178" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="g" width="74.749" height="58.852" x="15.756" y="-17.901" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="7.659"/></filter><filter id="h" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="i" width="61.377" height="25.362" x="23.548" y="2.284" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="j" width="56.045" height="63.649" x="-27.636" y="-22.853" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="k" width="54.814" height="64.646" x="20.116" y="-38.415" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="l" width="33.541" height="35.313" x="24.641" y="-11.323" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="m" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="n" width="54.814" height="64.646" x="-29.286" y="6.009" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="o" width="54.814" height="64.646" x="8.244" y="-2.416" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter><filter id="p" width="39.409" height="43.623" x="18.713" y="10.588" color-interpolation-filters="sRGB" filterUnits="userSpaceOnUse"><feFlood flood-opacity="0" result="BackgroundImageFix"/><feBlend in="SourceGraphic" in2="BackgroundImageFix" result="shape"/><feGaussianBlur result="effect1_foregroundBlur_2002_17158" stdDeviation="4.596"/></filter></defs></svg>
@@ -0,0 +1,14 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" type="image/svg+xml" href="/favicon.svg" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>QueryView</title>
8
+ <script type="module" crossorigin src="/assets/index-CvnC_D68.js"></script>
9
+ <link rel="stylesheet" crossorigin href="/assets/index-Qe7bhycG.css">
10
+ </head>
11
+ <body>
12
+ <div id="root"></div>
13
+ </body>
14
+ </html>
@@ -0,0 +1,51 @@
1
+ """The /api/db surface: unknown type is a 400; validation errors are 400;
2
+ the old /api/clickhouse paths are gone (404)."""
3
+
4
+ from __future__ import annotations
5
+
6
+ from fastapi.testclient import TestClient
7
+
8
+ from queryview.main import app
9
+
10
+
11
+ def test_connect_unknown_type_is_400():
12
+ c = TestClient(app)
13
+ r = c.post("/api/db/connect", json={"type": "nope", "name": "x", "host": "h", "port": 1})
14
+ assert r.status_code == 400
15
+ assert "unknown" in r.json()["message"].lower()
16
+
17
+
18
+ def test_connect_validation_error_is_400():
19
+ c = TestClient(app)
20
+ r = c.post("/api/db/connect", json={"type": "clickhouse", "name": "x"}) # no host
21
+ assert r.status_code == 400
22
+
23
+
24
+ def test_predefined_query_save_rejects_malformed_cell_view():
25
+ c = TestClient(app)
26
+ body = {"query_name": "cv bad", "type": "clickhouse", "query": "SELECT 1"}
27
+ r = c.post("/api/predefined-queries", json={**body, "cell_view": "col: [unclosed"})
28
+ assert r.status_code == 400 and "invalid cell_view" in r.json()["message"]
29
+ # A non-string cell_view is rejected, not silently dropped.
30
+ r = c.post("/api/predefined-queries", json={**body, "cell_view": {"col": {"type": "link"}}})
31
+ assert r.status_code == 400
32
+ # The valid shape still saves.
33
+ ok = c.post(
34
+ "/api/predefined-queries", json={**body, "cell_view": "col:\n type: link\n value: https://x/{cell}\n"}
35
+ )
36
+ assert ok.json() == {"ok": True}
37
+
38
+
39
+ def test_old_clickhouse_path_is_gone():
40
+ c = TestClient(app)
41
+ assert c.post("/api/clickhouse/connect", json={}).status_code == 404
42
+
43
+
44
+ def test_tables_without_session_is_409():
45
+ c = TestClient(app)
46
+ # Disconnect first: it marks this cookie disconnected, so the auto-reconnect
47
+ # of a connection saved by an earlier test can't turn this into a 200.
48
+ c.post("/api/db/disconnect")
49
+ r = c.get("/api/db/tables")
50
+ assert r.status_code == 409
51
+ assert r.json()["ok"] is False
@@ -0,0 +1,85 @@
1
+ """The /api/export and /api/import surface: validation 400s, 404s for unknown
2
+ entities/workspaces, download headers, and an export -> import round trip."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+
8
+ import yaml
9
+ from fastapi.testclient import TestClient
10
+
11
+ from queryview.main import app
12
+
13
+
14
+ def _run(coro):
15
+ return asyncio.run(coro)
16
+
17
+
18
+ def test_export_validation_400():
19
+ c = TestClient(app)
20
+ assert c.get("/api/export").status_code == 400
21
+ assert c.get("/api/export", params={"kind": "nope", "name": "x"}).status_code == 400
22
+ assert c.get("/api/export", params={"kind": "dashboard"}).status_code == 400 # name required
23
+ # queries additionally require conn_type
24
+ assert c.get("/api/export", params={"kind": "query", "name": "x"}).status_code == 400
25
+
26
+
27
+ def test_export_unknown_entity_404():
28
+ c = TestClient(app)
29
+ r = c.get("/api/export", params={"kind": "dashboard", "name": "never saved"})
30
+ assert r.status_code == 404
31
+
32
+
33
+ def test_export_unknown_workspace_404():
34
+ c = TestClient(app)
35
+ r = c.get("/api/export", params={"kind": "workspace", "workspace": "no such ws"})
36
+ assert r.status_code == 404
37
+
38
+
39
+ def test_import_requires_body_and_valid_yaml():
40
+ c = TestClient(app)
41
+ assert c.post("/api/import", content=b"").status_code == 400
42
+ assert c.post("/api/import", content=b"kind: nope\n").status_code == 400
43
+
44
+
45
+ def test_query_export_headers_and_round_trip(default_ws_id):
46
+ from queryview.queries import list_predefined_queries, save_predefined_query
47
+
48
+ c = TestClient(app)
49
+ _run(save_predefined_query("api yio", "clickhouse", "SELECT 1", workspace_id=default_ws_id))
50
+ r = c.get("/api/export", params={"kind": "query", "name": "api yio", "conn_type": "clickhouse"})
51
+ assert r.status_code == 200
52
+ assert r.headers["content-disposition"] == 'attachment; filename="api yio.query.yaml"'
53
+ assert "yaml" in r.headers["content-type"]
54
+ doc = yaml.safe_load(r.text)
55
+ assert doc == {"kind": "query", "type": "clickhouse", "query_name": "api yio", "query": "SELECT 1"}
56
+
57
+ # Drift, then import the exported text back over it.
58
+ _run(save_predefined_query("api yio", "clickhouse", "SELECT 2", workspace_id=default_ws_id))
59
+ imp = c.post("/api/import", content=r.text.encode())
60
+ assert imp.json() == {"ok": True, "kind": "query", "queries": 1, "dashboards": 0}
61
+ rows = _run(list_predefined_queries("clickhouse", default_ws_id))
62
+ assert next(x for x in rows if x["query_name"] == "api yio")["query"] == "SELECT 1"
63
+
64
+
65
+ def test_workspace_export_import_between_workspaces(default_ws_id, wipe_workspace_entities):
66
+ from queryview.dashboards import upsert_dashboard
67
+
68
+ c = TestClient(app)
69
+ _run(upsert_dashboard("api ws dash", "prod", "<p>v1</p>", {"q": "SELECT 1"}, workspace_id=default_ws_id))
70
+ r = c.get("/api/export", params={"kind": "workspace"})
71
+ assert r.status_code == 200
72
+ assert r.headers["content-disposition"] == 'attachment; filename="default.workspace.yaml"'
73
+
74
+ assert c.post("/api/workspaces", json={"name": "api yio target"}).json() == {"ok": True}
75
+ try:
76
+ imp = c.post("/api/import", params={"workspace": "api yio target"}, content=r.text.encode()).json()
77
+ assert imp["ok"] is True and imp["kind"] == "workspace" and imp["dashboards"] >= 1
78
+ d = c.get("/api/dashboards/api ws dash", params={"workspace": "api yio target"}).json()
79
+ assert d["queries"] == {"q": "SELECT 1"}
80
+ finally:
81
+ # Leave no imported entities behind: wipe the target then delete it.
82
+ from queryview.workspaces import resolve
83
+
84
+ wipe_workspace_entities(_run(resolve("api yio target")).id)
85
+ assert c.delete("/api/workspaces/api yio target").json() == {"ok": True}
@@ -0,0 +1,83 @@
1
+ """The /api/git surface: validation 400s, unconfigured 409, and a store ->
2
+ history -> restore round trip against a local bare repo."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+
8
+ from fastapi.testclient import TestClient
9
+
10
+ from queryview.main import app
11
+
12
+ # The git_env fixture (bare repo + GIT_SYNC_* env vars) lives in conftest.py.
13
+
14
+
15
+ def _run(coro):
16
+ return asyncio.run(coro)
17
+
18
+
19
+ def _default_ws_id() -> int:
20
+ from queryview.workspaces import DEFAULT_WORKSPACE, resolve
21
+
22
+ return _run(resolve(DEFAULT_WORKSPACE)).id
23
+
24
+
25
+ def test_status_reports_unconfigured():
26
+ # Outside git_env the default workspace has no remote configured.
27
+ c = TestClient(app)
28
+ assert c.get("/api/git/status").json() == {"configured": False}
29
+
30
+
31
+ def test_status_reports_configured(git_env):
32
+ c = TestClient(app)
33
+ assert c.get("/api/git/status").json() == {"configured": True}
34
+
35
+
36
+ def test_store_validation_400():
37
+ c = TestClient(app)
38
+ assert c.post("/api/git/store", json={}).status_code == 400
39
+ assert c.post("/api/git/store", json={"kind": "nope", "name": "x"}).status_code == 400
40
+ # queries require conn_type
41
+ assert c.post("/api/git/store", json={"kind": "query", "name": "x"}).status_code == 400
42
+
43
+
44
+ def test_store_history_restore_round_trip(git_env):
45
+ from queryview.queries import list_predefined_queries, save_predefined_query
46
+
47
+ c = TestClient(app)
48
+ _run(save_predefined_query("api rt", "clickhouse", "SELECT 1", workspace_id=_default_ws_id()))
49
+ r1 = c.post(
50
+ "/api/git/store",
51
+ json={"kind": "query", "name": "api rt", "conn_type": "clickhouse"},
52
+ ).json()
53
+ assert r1["ok"] is True and r1["committed"] is True
54
+ _run(save_predefined_query("api rt", "clickhouse", "SELECT 2", workspace_id=_default_ws_id()))
55
+ c.post(
56
+ "/api/git/store",
57
+ json={"kind": "query", "name": "api rt", "conn_type": "clickhouse"},
58
+ )
59
+
60
+ h = c.get(
61
+ "/api/git/history",
62
+ params={"kind": "query", "name": "api rt", "conn_type": "clickhouse"},
63
+ ).json()
64
+ assert h["ok"] is True and len(h["revisions"]) == 2 and h["has_more"] is False
65
+ oldest = h["revisions"][-1]["sha"]
66
+
67
+ r2 = c.post(
68
+ "/api/git/restore",
69
+ json={"kind": "query", "name": "api rt", "conn_type": "clickhouse", "ref": oldest},
70
+ ).json()
71
+ assert r2["ok"] is True
72
+ rows = _run(list_predefined_queries("clickhouse", _default_ws_id()))
73
+ row = next(x for x in rows if x["query_name"] == "api rt")
74
+ assert row["query"] == "SELECT 1"
75
+
76
+
77
+ def test_restore_unknown_entity_404(git_env):
78
+ c = TestClient(app)
79
+ r = c.post(
80
+ "/api/git/restore",
81
+ json={"kind": "query", "name": "never stored api", "conn_type": "clickhouse"},
82
+ )
83
+ assert r.status_code == 404
@@ -0,0 +1,44 @@
1
+ """The /api/workspaces surface: CRUD round trip and no-secret listing.
2
+ Store-level rules (delete-non-empty 409, name validation details) are covered
3
+ in test_workspaces.py."""
4
+
5
+ from __future__ import annotations
6
+
7
+ from fastapi.testclient import TestClient
8
+
9
+ from queryview.main import app
10
+
11
+
12
+ def test_list_includes_default():
13
+ c = TestClient(app)
14
+ names = [w["name"] for w in c.get("/api/workspaces").json()["workspaces"]]
15
+ assert "default" in names
16
+
17
+
18
+ def test_create_list_update_delete_round_trip():
19
+ c = TestClient(app)
20
+ r = c.post(
21
+ "/api/workspaces",
22
+ json={"name": "t6-rt", "remote": "https://u:tok@example.test/r.git", "branch": "dev"},
23
+ )
24
+ assert r.json() == {"ok": True}
25
+
26
+ listed = {w["name"]: w for w in c.get("/api/workspaces").json()["workspaces"]}
27
+ assert listed["t6-rt"] == {"name": "t6-rt", "branch": "dev", "configured": True}
28
+ assert "tok" not in r.text and "remote" not in listed["t6-rt"]
29
+
30
+ # Rename + clear the remote (present-but-null clears; absent keeps).
31
+ assert c.patch("/api/workspaces/t6-rt", json={"name": "t6-rt2", "remote": None}).json() == {"ok": True}
32
+ listed = {w["name"]: w for w in c.get("/api/workspaces").json()["workspaces"]}
33
+ assert "t6-rt" not in listed
34
+ assert listed["t6-rt2"]["configured"] is False
35
+
36
+ assert c.delete("/api/workspaces/t6-rt2").json() == {"ok": True}
37
+ assert c.delete("/api/workspaces/t6-rt2").status_code == 404
38
+
39
+
40
+ def test_create_validation_and_conflict():
41
+ c = TestClient(app)
42
+ assert c.post("/api/workspaces", json={}).status_code == 400
43
+ c.post("/api/workspaces", json={"name": "t6-dup"})
44
+ assert c.post("/api/workspaces", json={"name": "t6-dup"}).status_code == 409
@@ -0,0 +1,123 @@
1
+ """Flow dispatch: connect_new persists with the given type; run_query routes to
2
+ the driver; the database gate is skipped when the driver exposes no databases."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+
8
+ import queryview.connect as connect
9
+ from queryview.drivers import DRIVERS
10
+ from queryview.drivers.base import QueryResult
11
+
12
+
13
+ def _run(coro):
14
+ return asyncio.run(coro)
15
+
16
+
17
+ class _FakeDriver:
18
+ type = "fake"
19
+ requires_database = False # no picker
20
+ ident_quote = '"'
21
+
22
+ def parse_config(self, body):
23
+ return {"v": 1}, None
24
+
25
+ def config_to_dict(self, c):
26
+ return c
27
+
28
+ def config_from_dict(self, d):
29
+ return d
30
+
31
+ async def test(self, c):
32
+ return {"ok": True, "message": "ok"}
33
+
34
+ async def list_databases(self, c):
35
+ return True, [] # no picker
36
+
37
+ async def run_query(self, c, sql, database, limit, offset, order_by, fmt):
38
+ return QueryResult(True, f"ran:{sql}:db={database}")
39
+
40
+ async def describe_query(self, c, sql, database):
41
+ return True, [{"name": "x", "type": "int"}]
42
+
43
+ async def list_tables(self, c, database):
44
+ return True, [{"name": f"t-of-{database}", "rows": 1, "bytes": None}]
45
+
46
+
47
+ def test_run_query_skips_db_gate_when_no_databases(monkeypatch):
48
+ monkeypatch.setitem(DRIVERS, "fake", _FakeDriver())
49
+ sid = "s-fake"
50
+ _run(connect.connect_new(sid, "f", {"v": 1}, "fake"))
51
+ out = _run(connect.run_query(sid, "SELECT 1", 10, 0, "tsv", None))
52
+ assert out["ok"] and out["output"] == "ran:SELECT 1:db=None"
53
+
54
+
55
+ def test_disconnect_clears_session_and_suppresses_reconnect(monkeypatch):
56
+ monkeypatch.setitem(DRIVERS, "fake", _FakeDriver())
57
+ sid = "s-disc"
58
+ _run(connect.connect_new(sid, "f", {"v": 1}, "fake"))
59
+ assert _run(connect.get_session(sid))["connected"] is True
60
+ # Disconnect drops the active session...
61
+ assert _run(connect.disconnect(sid)) == {"ok": True}
62
+ # ...and stays disconnected even though a saved active connection exists,
63
+ # i.e. get_session must not silently auto-reconnect it.
64
+ assert _run(connect.get_session(sid))["connected"] is False
65
+ # Connecting again clears the suppression.
66
+ _run(connect.connect_new(sid, "f", {"v": 1}, "fake"))
67
+ assert _run(connect.get_session(sid))["connected"] is True
68
+
69
+
70
+ def test_list_connection_names_orders_by_recency(monkeypatch):
71
+ monkeypatch.setitem(DRIVERS, "fake", _FakeDriver())
72
+ _run(connect.connect_new("s-a", "alpha", {"v": 1}, "fake"))
73
+ _run(connect.connect_new("s-b", "beta", {"v": 1}, "fake"))
74
+ names = _run(connect.list_connection_names())
75
+ # Most-recently-activated first; both saved connections are present.
76
+ assert names[:2] == ["beta", "alpha"]
77
+
78
+
79
+ def test_list_tables_routes_to_driver_and_skips_db_gate(monkeypatch):
80
+ monkeypatch.setitem(DRIVERS, "fake", _FakeDriver())
81
+ sid = "s-tables"
82
+ _run(connect.connect_new(sid, "f", {"v": 1}, "fake"))
83
+ out = _run(connect.list_tables(sid))
84
+ # The browse SELECT is attached here, quoted with the driver's ident_quote.
85
+ assert out["ok"] and out["tables"] == [
86
+ {"name": "t-of-None", "rows": 1, "bytes": None, "query": 'SELECT * FROM "t-of-None"'},
87
+ ]
88
+
89
+
90
+ def test_run_query_requires_database_when_picker_present(monkeypatch):
91
+ class _WithDbs(_FakeDriver):
92
+ type = "fakedb"
93
+ requires_database = True
94
+
95
+ async def list_databases(self, c):
96
+ return True, ["a", "b"]
97
+
98
+ monkeypatch.setitem(DRIVERS, "fakedb", _WithDbs())
99
+ sid = "s-fakedb"
100
+ _run(connect.connect_new(sid, "g", {"v": 1}, "fakedb"))
101
+ out = _run(connect.run_query(sid, "SELECT 1", 10, 0, "tsv", None))
102
+ assert out["ok"] is False and out["reason"] == "no-database"
103
+
104
+
105
+ def test_list_tables_requires_database_when_picker_present(monkeypatch):
106
+ class _WithDbs(_FakeDriver):
107
+ type = "fakedb2"
108
+ requires_database = True
109
+
110
+ async def list_databases(self, c):
111
+ return True, ["a", "b"]
112
+
113
+ monkeypatch.setitem(DRIVERS, "fakedb2", _WithDbs())
114
+ sid = "s-fakedb2"
115
+ _run(connect.connect_new(sid, "g2", {"v": 1}, "fakedb2"))
116
+ out = _run(connect.list_tables(sid))
117
+ assert out["ok"] is False and out["reason"] == "no-database"
118
+ # Selecting a database opens the gate; the driver sees the selection.
119
+ _run(connect.select_database(sid, "a"))
120
+ out = _run(connect.list_tables(sid))
121
+ assert out["ok"] and out["tables"] == [
122
+ {"name": "t-of-a", "rows": 1, "bytes": None, "query": 'SELECT * FROM "t-of-a"'},
123
+ ]
@@ -0,0 +1,34 @@
1
+ """The connection store round-trips a driver config through the encrypted JSON
2
+ blob, keyed by type, and never persists secrets in plaintext."""
3
+
4
+ from __future__ import annotations
5
+
6
+ import asyncio
7
+ import os
8
+ import sqlite3
9
+
10
+ from queryview.connect import _connection_by_name, _save_active_connection
11
+ from queryview.drivers.clickhouse import ChConfig
12
+
13
+
14
+ def _run(coro):
15
+ return asyncio.run(coro)
16
+
17
+
18
+ def test_save_then_load_round_trips_config_and_type():
19
+ cfg = ChConfig("h", 8123, "u", "s3cret")
20
+ _run(_save_active_connection("ch1", cfg, "clickhouse"))
21
+ stored = _run(_connection_by_name("ch1"))
22
+ assert stored is not None
23
+ assert stored.type == "clickhouse"
24
+ assert stored.config == cfg
25
+
26
+
27
+ def test_password_is_not_stored_in_plaintext():
28
+ _run(_save_active_connection("ch2", ChConfig("h", 8123, "u", "TOPSECRET"), "clickhouse"))
29
+ con = sqlite3.connect(os.environ["DB_PATH"])
30
+ try:
31
+ blob = con.execute("SELECT config FROM connections WHERE name='ch2'").fetchone()[0]
32
+ finally:
33
+ con.close()
34
+ assert "TOPSECRET" not in blob