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.
- queryview/__init__.py +0 -0
- queryview/conftest.py +86 -0
- queryview/connect.py +428 -0
- queryview/dashboard_queries.py +75 -0
- queryview/dashboards.py +156 -0
- queryview/drivers/__init__.py +10 -0
- queryview/drivers/base.py +165 -0
- queryview/drivers/clickhouse.py +138 -0
- queryview/drivers/duckdb.py +153 -0
- queryview/drivers/postgres.py +166 -0
- queryview/drivers/test_base.py +49 -0
- queryview/drivers/test_clickhouse.py +53 -0
- queryview/drivers/test_contract.py +77 -0
- queryview/drivers/test_duckdb.py +80 -0
- queryview/drivers/test_postgres.py +80 -0
- queryview/gitsync.py +374 -0
- queryview/main.py +740 -0
- queryview/mcp_server.py +294 -0
- queryview/migrations/env.py +39 -0
- queryview/migrations/script.py.mako +29 -0
- queryview/migrations/versions/9a536b7c0328_initial_schema.py +89 -0
- queryview/migrations/versions/a1b2c3d4e5f6_connection_config_blob.py +59 -0
- queryview/migrations/versions/b2c3d4e5f6a7_predefined_presentation.py +32 -0
- queryview/migrations/versions/c7d8e9f0a1b2_workspaces.py +98 -0
- queryview/queries.py +159 -0
- queryview/remote.py +141 -0
- queryview/static/assets/index-CvnC_D68.js +47 -0
- queryview/static/assets/index-Qe7bhycG.css +2 -0
- queryview/static/favicon.svg +1 -0
- queryview/static/index.html +14 -0
- queryview/test_api_db.py +51 -0
- queryview/test_api_export_import.py +85 -0
- queryview/test_api_gitsync.py +83 -0
- queryview/test_api_workspaces.py +44 -0
- queryview/test_connect_flow.py +123 -0
- queryview/test_connect_store.py +34 -0
- queryview/test_dashboards.py +216 -0
- queryview/test_gitsync.py +346 -0
- queryview/test_main.py +18 -0
- queryview/test_mcp_gitsync.py +72 -0
- queryview/test_migrations.py +99 -0
- queryview/test_queries.py +170 -0
- queryview/test_remote.py +260 -0
- queryview/test_validation.py +87 -0
- queryview/test_workspaces.py +109 -0
- queryview/test_yamlio.py +198 -0
- queryview/validation.py +111 -0
- queryview/workspaces.py +167 -0
- queryview/yamlio.py +245 -0
- queryview-0.0.2.dist-info/METADATA +183 -0
- queryview-0.0.2.dist-info/RECORD +54 -0
- queryview-0.0.2.dist-info/WHEEL +4 -0
- queryview-0.0.2.dist-info/entry_points.txt +3 -0
- 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>
|
queryview/test_api_db.py
ADDED
|
@@ -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
|