feat: add support for multiselect in python

This commit is contained in:
Ruben Fiszel
2024-01-09 14:39:27 +01:00
parent 224f18915d
commit 45a794b1b3
20 changed files with 226 additions and 84 deletions
+1 -1
View File
@@ -101,7 +101,7 @@ jobs:
platforms: linux/amd64,linux/arm64
push: true
build-args: |
features=enterprise,enterprise_saml
features=enterprise,enterprise_saml,stripe
nsjail=true
tags: |
${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}-ee:dev
+1
View File
@@ -9533,6 +9533,7 @@ version = "1.237.0"
dependencies = [
"anyhow",
"convert_case 0.6.0",
"lazy_static",
"regex",
"serde-wasm-bindgen",
"serde_json",
@@ -13,4 +13,4 @@ windmill-parser.workspace = true
rustpython-parser.workspace = true
itertools.workspace = true
serde_json.workspace = true
anyhow.workspace = true
anyhow.workspace = true
+93 -16
View File
@@ -93,22 +93,7 @@ pub fn parse_python_signature(code: &str) -> anyhow::Result<MainArgSignature> {
.as_arg()
.annotation
.as_ref()
.map_or(Typ::Unknown, |e| match e.as_ref() {
Expr::Name(ExprName { id, .. }) => match id.as_ref() {
"str" => Typ::Str(None),
"float" => Typ::Float,
"int" => Typ::Int,
"bool" => Typ::Bool,
"dict" => Typ::Object(vec![]),
"list" => Typ::List(Box::new(Typ::Str(None))),
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
"Sql" | "sql" => Typ::Sql,
_ => Typ::Resource(id.to_string()),
},
_ => Typ::Unknown,
});
.map_or(Typ::Unknown, |e| parse_expr(e));
if typ == Typ::Unknown
&& default.is_some()
@@ -134,6 +119,58 @@ pub fn parse_python_signature(code: &str) -> anyhow::Result<MainArgSignature> {
}
}
fn parse_expr(e: &Box<Expr>) -> Typ {
match e.as_ref() {
Expr::Name(ExprName { id, .. }) => parse_typ(id.as_ref()),
Expr::Subscript(x) => match x.value.as_ref() {
Expr::Name(ExprName { id, .. }) => match id.as_str() {
"Literal" => {
let values = match x.slice.as_ref() {
Expr::Tuple(elts) => {
let v: Vec<String> = elts
.elts
.iter()
.map(|x| match x {
Expr::Constant(c) => c.value.as_str().map(|x| x.to_string()),
_ => None,
})
.filter_map(|x| x)
.collect();
if v.is_empty() {
None
} else {
Some(v)
}
}
_ => None,
};
Typ::Str(values)
}
"List" => Typ::List(Box::new(parse_expr(&x.slice))),
_ => Typ::Unknown,
},
_ => Typ::Unknown,
},
_ => Typ::Unknown,
}
}
fn parse_typ(id: &str) -> Typ {
match id {
"str" => Typ::Str(None),
"float" => Typ::Float,
"int" => Typ::Int,
"bool" => Typ::Bool,
"dict" => Typ::Object(vec![]),
"list" => Typ::List(Box::new(Typ::Str(None))),
"bytes" => Typ::Bytes,
"datetime" => Typ::Datetime,
"datetime.datetime" => Typ::Datetime,
"Sql" | "sql" => Typ::Sql,
_ => Typ::Resource(id.to_string()),
}
}
fn to_value<R>(et: &Expr<R>) -> Option<serde_json::Value> {
match et {
Expr::Constant(ExprConstant { value, .. }) => Some(constant_to_value(value)),
@@ -366,4 +403,44 @@ def main(test1: str,
Ok(())
}
#[test]
fn test_parse_python_sig_4() -> anyhow::Result<()> {
let code = r#"
import os
def main(test1: Literal["foo", "bar"], test2: List[Literal["foo", "bar"]]): return
"#;
//println!("{}", serde_json::to_string()?);
assert_eq!(
parse_python_signature(code)?,
MainArgSignature {
star_args: false,
star_kwargs: false,
args: vec![
Arg {
otyp: None,
name: "test1".to_string(),
typ: Typ::Str(Some(vec!["foo".to_string(), "bar".to_string()])),
default: None,
has_default: false
},
Arg {
otyp: None,
name: "test2".to_string(),
typ: Typ::List(Box::new(Typ::Str(Some(vec![
"foo".to_string(),
"bar".to_string()
])))),
default: None,
has_default: false
}
]
}
);
Ok(())
}
}
@@ -22,4 +22,5 @@ swc_ecma_visit.workspace = true
serde_json.workspace = true
anyhow.workspace = true
convert_case.workspace = true
regex.workspace = true
regex.workspace = true
lazy_static.workspace = true
@@ -284,12 +284,15 @@ fn binding_ident_to_arg(BindingIdent { id, type_ann }: &BindingIdent) -> (String
(id.sym.to_string(), typ, nullable)
}
lazy_static::lazy_static! {
static ref RE_SNK_CASE: Regex = Regex::new(r"_(\d)").unwrap();
}
fn to_snake_case(s: &str) -> String {
let r = s.to_case(Case::Snake);
// s_3 => s3
let re = Regex::new(r"_(\d)").unwrap();
re.replace_all(&r, "$1").to_string()
RE_SNK_CASE.replace_all(&r, "$1").to_string()
}
fn tstype_to_typ(ts_type: &TsType) -> (Typ, bool) {
@@ -3,7 +3,7 @@
"collaborators": [
"Ruben Fiszel <ruben@windmill.dev>"
],
"version": "1.226.9",
"version": "1.237.0",
"files": [
"windmill_parser_wasm_bg.wasm",
"windmill_parser_wasm.js",
@@ -14,4 +14,4 @@
"sideEffects": [
"./snippets/*"
]
}
}
@@ -538,6 +538,10 @@ async function __wbg_load(module, imports) {
function __wbg_get_imports() {
const imports = {};
imports.wbg = {};
imports.wbg.__wbg_eval_ff4183ac1495b791 = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_object_drop_ref = function(arg0) {
takeObject(arg0);
};
@@ -589,10 +593,6 @@ function __wbg_get_imports() {
const ret = getObject(arg0) in getObject(arg1);
return ret;
};
imports.wbg.__wbg_eval_596393dc5ae50a1b = function(arg0, arg1) {
const ret = eval(getStringFromWasm0(arg0, arg1));
return addHeapObject(ret);
};
imports.wbg.__wbindgen_jsval_loose_eq = function(arg0, arg1) {
const ret = getObject(arg0) == getObject(arg1);
return ret;
+2 -1
View File
@@ -9,7 +9,8 @@ name = "windmill_api"
path = "src/lib.rs"
[features]
enterprise = ["windmill-queue/enterprise", "async-stripe", "windmill-audit/enterprise", "windmill-git-sync/enterprise"]
enterprise = ["windmill-queue/enterprise", "windmill-audit/enterprise", "windmill-git-sync/enterprise"]
stripe = ["async-stripe"]
enterprise_saml = ["samael"]
benchmark = []
+11 -10
View File
@@ -6,7 +6,7 @@
* LICENSE-AGPL for a copy of the license.
*/
#[cfg(feature = "enterprise")]
#[cfg(feature = "stripe")]
use std::str::FromStr;
use crate::db::ApiAuthed;
@@ -21,7 +21,7 @@ use crate::{
variables::build_crypt,
webhook_util::{InstanceEvent, WebhookShared},
};
#[cfg(feature = "enterprise")]
#[cfg(feature = "stripe")]
use axum::response::Redirect;
use axum::{
body::StreamBody,
@@ -32,10 +32,10 @@ use axum::{
Json, Router,
};
use chrono::Utc;
#[cfg(feature = "enterprise")]
#[cfg(feature = "stripe")]
use chrono::{Datelike, TimeZone, Timelike};
use magic_crypt::MagicCryptTrait;
#[cfg(feature = "enterprise")]
#[cfg(feature = "stripe")]
use stripe::CustomerId;
use uuid::Uuid;
use windmill_audit::{audit_log, ActionKind};
@@ -83,7 +83,6 @@ pub fn workspaced_service() -> Router {
.route("/edit_deploy_to", post(edit_deploy_to))
.route("/tarball", get(tarball_workspace))
.route("/is_premium", get(is_premium))
.route("/premium_info", get(premium_info))
.route("/edit_copilot_config", post(edit_copilot_config))
.route("/get_copilot_info", get(get_copilot_info))
.route("/edit_error_handler", post(edit_error_handler))
@@ -94,7 +93,7 @@ pub fn workspaced_service() -> Router {
.route("/edit_git_sync_config", post(edit_git_sync_config))
.route("/leave", post(leave_workspace));
#[cfg(feature = "enterprise")]
#[cfg(feature = "stripe")]
{
if STRIPE_KEY.is_none() {
return router;
@@ -102,12 +101,13 @@ pub fn workspaced_service() -> Router {
tracing::info!("stripe enabled");
return router
.route("/premium_info", get(premium_info))
.route("/checkout", get(stripe_checkout))
.route("/billing_portal", get(stripe_portal));
}
}
#[cfg(not(feature = "enterprise"))]
#[cfg(not(feature = "stripe"))]
router
}
pub fn global_service() -> Router {
@@ -324,6 +324,7 @@ pub struct PremiumWorkspaceInfo {
pub usage: Option<i32>,
pub seats: Option<i32>,
}
#[cfg(feature = "stripe")]
async fn premium_info(
authed: ApiAuthed,
Extension(db): Extension<DB>,
@@ -395,14 +396,14 @@ async fn premium_info(
Ok(Json(result))
}
#[cfg(feature = "enterprise")]
#[cfg(feature = "stripe")]
#[derive(Deserialize)]
struct PlanQuery {
plan: String,
seats: Option<i32>,
}
#[cfg(feature = "enterprise")]
#[cfg(feature = "stripe")]
async fn stripe_checkout(
authed: ApiAuthed,
Path(w_id): Path<String>,
@@ -491,7 +492,7 @@ async fn stripe_checkout(
}
}
#[cfg(feature = "enterprise")]
#[cfg(feature = "stripe")]
async fn stripe_portal(
authed: ApiAuthed,
Path(w_id): Path<String>,
+4 -4
View File
@@ -48,7 +48,7 @@
"vscode-languageclient": "~9.0.1",
"vscode-uri": "~3.0.8",
"vscode-ws-jsonrpc": "~3.1.0",
"windmill-parser-wasm": "^1.226.9",
"windmill-parser-wasm": "^1.237.0",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.0",
"yaml": "^2.3.4",
@@ -9585,9 +9585,9 @@
}
},
"node_modules/windmill-parser-wasm": {
"version": "1.226.9",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.226.9.tgz",
"integrity": "sha512-yOlLjUF4NlRutZhHrRa4fhxuxLslMaVXBojY3QyIVLlSq+7SGIUTYpEb04XrwpcCNyrgMIL1cNYaq4qzGDKsxA=="
"version": "1.237.0",
"resolved": "https://registry.npmjs.org/windmill-parser-wasm/-/windmill-parser-wasm-1.237.0.tgz",
"integrity": "sha512-MGO9rkSnkFMOkkkEr/whFzkbyeHX4E6JwYLazlDgyprNNibpEi7GXcOeuz+QgbzDNrTLo/sYN6bYqcHt4Jk6dQ=="
},
"node_modules/wordwrap": {
"version": "1.0.0",
+1 -1
View File
@@ -131,7 +131,7 @@
"vscode-languageclient": "~9.0.1",
"vscode-uri": "~3.0.8",
"vscode-ws-jsonrpc": "~3.1.0",
"windmill-parser-wasm": "^1.226.9",
"windmill-parser-wasm": "^1.237.0",
"y-monaco": "^0.1.4",
"y-websocket": "^1.5.0",
"yaml": "^2.3.4",
+2 -10
View File
@@ -27,7 +27,6 @@
import DateTimeInput from './DateTimeInput.svelte'
import S3FilePicker from './S3FilePicker.svelte'
import CurrencyInput from './apps/components/inputs/currency/CurrencyInput.svelte'
import Label from './Label.svelte'
export let label: string = ''
export let value: any
@@ -234,13 +233,6 @@
{#if type == 'array'}
<ArrayTypeNarrowing bind:itemsType />
<Label label="Display using multiselect (require enum)">
<Toggle
size="xs"
disabled={itemsType?.enum == undefined}
bind:checked={extra.multiselect}
/>
</Label>
{:else if type == 'string' || ['number', 'integer', 'object'].includes(type ?? '')}
<div class="p-2 my-1 text-xs border-solid border border-gray-200 rounded-lg">
<div class="w-min">
@@ -356,7 +348,7 @@
{/if}
{:else if inputCat == 'list'}
<div class="w-full">
{#if Array.isArray(itemsType?.multiselect)}
{#if Array.isArray(itemsType?.multiselect) && Array.isArray(value)}
<div class="items-start">
<Multiselect
ulOptionsClass={'p-2 !bg-surface-secondary'}
@@ -366,7 +358,7 @@
selectedOptionsDraggable={true}
/>
</div>
{:else if extra.multiselect && itemsType?.enum != undefined}
{:else if itemsType?.enum != undefined && Array.isArray(itemsType?.enum) && Array.isArray(value)}
<div class="items-start">
<Multiselect
ulOptionsClass={'p-2 !bg-surface-secondary'}
@@ -1,5 +1,7 @@
<script lang="ts">
import { Plus, X } from 'lucide-svelte'
import { Button } from './common'
import { fade } from 'svelte/transition'
export let itemsType:
| {
@@ -48,22 +50,27 @@
Enums
<div class="flex flex-col gap-1">
{#each itemsType?.enum || [] as e}
<div class="flex flex-row max-w-md">
<div class="flex flex-row max-w-md gap-1 items-center">
<input id="input" type="text" bind:value={e} />
<Button
size="sm"
btnClasses="ml-6"
on:click={() => {
if (itemsType?.enum) {
itemsType.enum = (itemsType.enum || []).filter((el) => el !== e)
}
}}>-</Button
>
<div>
<button
transition:fade|local={{ duration: 100 }}
class="rounded-full p-1 bg-surface-secondary duration-200 hover:bg-surface-hover ml-2"
on:click={() => {
if (itemsType?.enum) {
itemsType.enum = (itemsType.enum || []).filter((el) => el !== e)
}
}}
>
<X size={14} />
</button>
</div>
</div>
{/each}
</div>
<div class="flex flex-row my-1">
<div class="flex flex-row mb-1 mt-2">
<Button
variant="border"
size="sm"
on:click={() => {
if (itemsType?.enum) {
@@ -71,8 +78,10 @@
let choice = `choice ${enum_?.length ? enum_?.length + 1 : 1}`
itemsType.enum = itemsType.enum ? itemsType.enum.concat(choice) : [choice]
}
}}>+</Button
}}
>
<Plus size={14} />
</Button>
<Button
variant="border"
size="sm"
@@ -228,7 +228,7 @@
{/if}
{:else if inputCat == 'list'}
<div class="w-full">
{#if Array.isArray(itemsType?.multiselect)}
{#if Array.isArray(itemsType?.multiselect) && Array.isArray(value)}
<div class="items-start">
<Multiselect
bind:selected={value}
@@ -236,7 +236,7 @@
selectedOptionsDraggable={true}
/>
</div>
{:else if extra.multiselect}
{:else if Array.isArray(itemsType?.enum) && Array.isArray(value)}
<div class="items-start">
<Multiselect
bind:selected={value}
@@ -65,7 +65,6 @@
import ArrayTypeNarrowing from './ArrayTypeNarrowing.svelte'
import LightweightSchemaForm from './LightweightSchemaForm.svelte'
import NumberTypeNarrowing from './NumberTypeNarrowing.svelte'
import Label from './Label.svelte'
import SimpleEditor from './SimpleEditor.svelte'
export let error = ''
@@ -248,13 +247,6 @@
/>
{:else if property.selectedType == 'array'}
<ArrayTypeNarrowing bind:itemsType={property.items} />
<div class="mt-4" />
<Label label="Display using multiselect (require enum)">
<Toggle
disabled={property?.items?.enum == undefined}
bind:checked={property.multiselect}
/>
</Label>
{:else if property.selectedType == 'number' || property.selectedType == 'integer'}
<NumberTypeNarrowing
bind:min={property.min}
@@ -107,6 +107,7 @@
try {
await inferArgs(nlang ?? lang, code, nschema)
validCode = true
schema = nschema
} catch (e) {
+6 -9
View File
@@ -1,7 +1,7 @@
import { ScriptService, type MainArgSignature, FlowService, Script } from '$lib/gen'
import { get, writable } from 'svelte/store'
import type { Schema, SchemaProperty, SupportedLanguage } from './common.js'
import { emptySchema, sortObject } from './utils.js'
import { emptySchema, sendUserToast, sortObject } from './utils.js'
import { tick } from 'svelte'
import init, {
parse_deno,
@@ -207,15 +207,12 @@ function argSigToJsonSchemaType(
delete oldS.items
}
let sameItems = oldS.items?.type == 'string' && newS.items?.type == 'string'
let savedItems: any = undefined
if (sameItems) {
savedItems = JSON.parse(JSON.stringify(oldS.items))
}
Object.assign(oldS, newS)
if (sameItems) {
oldS.items = savedItems
}
// if (sameItems && savedItems != undefined && savedItems.enum != undefined) {
// sendUserToast(JSON.stringify(savedItems))
// oldS.items = savedItems
// }
if (oldS.format?.startsWith('resource-') && newS.type != 'object') {
oldS.format = undefined
@@ -0,0 +1,67 @@
// vite.config.js
import { sveltekit } from "file:///git/windmill/frontend/node_modules/@sveltejs/kit/src/exports/vite/index.js";
import { readFileSync } from "fs";
import { fileURLToPath } from "url";
import monacoEditorPlugin from "file:///git/windmill/frontend/node_modules/vite-plugin-monaco-editor/dist/index.js";
import circleDependency from "file:///git/windmill/frontend/node_modules/vite-plugin-circular-dependency/dist/index.mjs";
var __vite_injected_original_import_meta_url = "file:///git/windmill/frontend/vite.config.js";
var file = fileURLToPath(new URL("package.json", __vite_injected_original_import_meta_url));
var json = readFileSync(file, "utf8");
var version = JSON.parse(json);
var config = {
server: {
port: 3e3,
proxy: {
"^/api/.*": {
target: process.env.REMOTE ?? "https://app.windmill.dev/",
changeOrigin: true,
cookieDomainRewrite: "localhost"
},
"^/ws/.*": {
target: process.env.REMOTE_LSP ?? "https://app.windmill.dev",
changeOrigin: true,
ws: true
},
"^/ws_mp/.*": {
target: process.env.REMOTE_MP ?? "https://app.windmill.dev",
changeOrigin: true,
ws: true
}
}
},
preview: {
port: 3e3
},
plugins: [
sveltekit(),
monacoEditorPlugin.default({
publicPath: "workers",
languageWorkers: [],
customWorkers: [
{
label: "graphql",
entry: "monaco-graphql/esm/graphql.worker"
}
]
}),
circleDependency({ circleImportThrowErr: false })
],
define: {
__pkg__: version
},
optimizeDeps: {
include: ["highlight.js", "highlight.js/lib/core"]
},
resolve: {
alias: {
path: "path-browserify"
},
dedupe: ["monaco-editor", "vscode"]
},
assetsInclude: ["**/*.wasm"]
};
var vite_config_default = config;
export {
vite_config_default as default
};
//# sourceMappingURL=data:application/json;base64,ewogICJ2ZXJzaW9uIjogMywKICAic291cmNlcyI6IFsidml0ZS5jb25maWcuanMiXSwKICAic291cmNlc0NvbnRlbnQiOiBbImNvbnN0IF9fdml0ZV9pbmplY3RlZF9vcmlnaW5hbF9kaXJuYW1lID0gXCIvZ2l0L3dpbmRtaWxsL2Zyb250ZW5kXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ZpbGVuYW1lID0gXCIvZ2l0L3dpbmRtaWxsL2Zyb250ZW5kL3ZpdGUuY29uZmlnLmpzXCI7Y29uc3QgX192aXRlX2luamVjdGVkX29yaWdpbmFsX2ltcG9ydF9tZXRhX3VybCA9IFwiZmlsZTovLy9naXQvd2luZG1pbGwvZnJvbnRlbmQvdml0ZS5jb25maWcuanNcIjtpbXBvcnQgeyBzdmVsdGVraXQgfSBmcm9tICdAc3ZlbHRlanMva2l0L3ZpdGUnXG5pbXBvcnQgeyByZWFkRmlsZVN5bmMgfSBmcm9tICdmcydcbmltcG9ydCB7IGZpbGVVUkxUb1BhdGggfSBmcm9tICd1cmwnXG5pbXBvcnQgbW9uYWNvRWRpdG9yUGx1Z2luIGZyb20gJ3ZpdGUtcGx1Z2luLW1vbmFjby1lZGl0b3InXG5pbXBvcnQgY2lyY2xlRGVwZW5kZW5jeSBmcm9tICd2aXRlLXBsdWdpbi1jaXJjdWxhci1kZXBlbmRlbmN5J1xuXG5jb25zdCBmaWxlID0gZmlsZVVSTFRvUGF0aChuZXcgVVJMKCdwYWNrYWdlLmpzb24nLCBpbXBvcnQubWV0YS51cmwpKVxuY29uc3QganNvbiA9IHJlYWRGaWxlU3luYyhmaWxlLCAndXRmOCcpXG5jb25zdCB2ZXJzaW9uID0gSlNPTi5wYXJzZShqc29uKVxuXG4vKiogQHR5cGUge2ltcG9ydCgndml0ZScpLlVzZXJDb25maWd9ICovXG5jb25zdCBjb25maWcgPSB7XG5cdHNlcnZlcjoge1xuXHRcdHBvcnQ6IDMwMDAsXG5cdFx0cHJveHk6IHtcblx0XHRcdCdeL2FwaS8uKic6IHtcblx0XHRcdFx0dGFyZ2V0OiBwcm9jZXNzLmVudi5SRU1PVEUgPz8gJ2h0dHBzOi8vYXBwLndpbmRtaWxsLmRldi8nLFxuXHRcdFx0XHRjaGFuZ2VPcmlnaW46IHRydWUsXG5cdFx0XHRcdGNvb2tpZURvbWFpblJld3JpdGU6ICdsb2NhbGhvc3QnXG5cdFx0XHR9LFxuXHRcdFx0J14vd3MvLionOiB7XG5cdFx0XHRcdHRhcmdldDogcHJvY2Vzcy5lbnYuUkVNT1RFX0xTUCA/PyAnaHR0cHM6Ly9hcHAud2luZG1pbGwuZGV2Jyxcblx0XHRcdFx0Y2hhbmdlT3JpZ2luOiB0cnVlLFxuXHRcdFx0XHR3czogdHJ1ZVxuXHRcdFx0fSxcblx0XHRcdCdeL3dzX21wLy4qJzoge1xuXHRcdFx0XHR0YXJnZXQ6IHByb2Nlc3MuZW52LlJFTU9URV9NUCA/PyAnaHR0cHM6Ly9hcHAud2luZG1pbGwuZGV2Jyxcblx0XHRcdFx0Y2hhbmdlT3JpZ2luOiB0cnVlLFxuXHRcdFx0XHR3czogdHJ1ZVxuXHRcdFx0fVxuXHRcdH1cblx0fSxcblx0cHJldmlldzoge1xuXHRcdHBvcnQ6IDMwMDBcblx0fSxcblx0cGx1Z2luczogW1xuXHRcdHN2ZWx0ZWtpdCgpLFxuXHRcdG1vbmFjb0VkaXRvclBsdWdpbi5kZWZhdWx0KHtcblx0XHRcdHB1YmxpY1BhdGg6ICd3b3JrZXJzJyxcblx0XHRcdGxhbmd1YWdlV29ya2VyczogW10sXG5cdFx0XHRjdXN0b21Xb3JrZXJzOiBbXG5cdFx0XHRcdHtcblx0XHRcdFx0XHRsYWJlbDogJ2dyYXBocWwnLFxuXHRcdFx0XHRcdGVudHJ5OiAnbW9uYWNvLWdyYXBocWwvZXNtL2dyYXBocWwud29ya2VyJ1xuXHRcdFx0XHR9XG5cdFx0XHRdXG5cdFx0fSksXG5cdFx0Y2lyY2xlRGVwZW5kZW5jeSh7IGNpcmNsZUltcG9ydFRocm93RXJyOiBmYWxzZSB9KVxuXHRdLFxuXHRkZWZpbmU6IHtcblx0XHRfX3BrZ19fOiB2ZXJzaW9uXG5cdH0sXG5cdG9wdGltaXplRGVwczoge1xuXHRcdGluY2x1ZGU6IFsnaGlnaGxpZ2h0LmpzJywgJ2hpZ2hsaWdodC5qcy9saWIvY29yZSddXG5cdH0sXG5cdHJlc29sdmU6IHtcblx0XHRhbGlhczoge1xuXHRcdFx0cGF0aDogJ3BhdGgtYnJvd3NlcmlmeSdcblx0XHR9LFxuXHRcdGRlZHVwZTogWydtb25hY28tZWRpdG9yJywgJ3ZzY29kZSddXG5cdH0sXG5cdGFzc2V0c0luY2x1ZGU6IFsnKiovKi53YXNtJ11cbn1cblxuZXhwb3J0IGRlZmF1bHQgY29uZmlnXG4iXSwKICAibWFwcGluZ3MiOiAiO0FBQW9QLFNBQVMsaUJBQWlCO0FBQzlRLFNBQVMsb0JBQW9CO0FBQzdCLFNBQVMscUJBQXFCO0FBQzlCLE9BQU8sd0JBQXdCO0FBQy9CLE9BQU8sc0JBQXNCO0FBSnVILElBQU0sMkNBQTJDO0FBTXJNLElBQU0sT0FBTyxjQUFjLElBQUksSUFBSSxnQkFBZ0Isd0NBQWUsQ0FBQztBQUNuRSxJQUFNLE9BQU8sYUFBYSxNQUFNLE1BQU07QUFDdEMsSUFBTSxVQUFVLEtBQUssTUFBTSxJQUFJO0FBRy9CLElBQU0sU0FBUztBQUFBLEVBQ2QsUUFBUTtBQUFBLElBQ1AsTUFBTTtBQUFBLElBQ04sT0FBTztBQUFBLE1BQ04sWUFBWTtBQUFBLFFBQ1gsUUFBUSxRQUFRLElBQUksVUFBVTtBQUFBLFFBQzlCLGNBQWM7QUFBQSxRQUNkLHFCQUFxQjtBQUFBLE1BQ3RCO0FBQUEsTUFDQSxXQUFXO0FBQUEsUUFDVixRQUFRLFFBQVEsSUFBSSxjQUFjO0FBQUEsUUFDbEMsY0FBYztBQUFBLFFBQ2QsSUFBSTtBQUFBLE1BQ0w7QUFBQSxNQUNBLGNBQWM7QUFBQSxRQUNiLFFBQVEsUUFBUSxJQUFJLGFBQWE7QUFBQSxRQUNqQyxjQUFjO0FBQUEsUUFDZCxJQUFJO0FBQUEsTUFDTDtBQUFBLElBQ0Q7QUFBQSxFQUNEO0FBQUEsRUFDQSxTQUFTO0FBQUEsSUFDUixNQUFNO0FBQUEsRUFDUDtBQUFBLEVBQ0EsU0FBUztBQUFBLElBQ1IsVUFBVTtBQUFBLElBQ1YsbUJBQW1CLFFBQVE7QUFBQSxNQUMxQixZQUFZO0FBQUEsTUFDWixpQkFBaUIsQ0FBQztBQUFBLE1BQ2xCLGVBQWU7QUFBQSxRQUNkO0FBQUEsVUFDQyxPQUFPO0FBQUEsVUFDUCxPQUFPO0FBQUEsUUFDUjtBQUFBLE1BQ0Q7QUFBQSxJQUNELENBQUM7QUFBQSxJQUNELGlCQUFpQixFQUFFLHNCQUFzQixNQUFNLENBQUM7QUFBQSxFQUNqRDtBQUFBLEVBQ0EsUUFBUTtBQUFBLElBQ1AsU0FBUztBQUFBLEVBQ1Y7QUFBQSxFQUNBLGNBQWM7QUFBQSxJQUNiLFNBQVMsQ0FBQyxnQkFBZ0IsdUJBQXVCO0FBQUEsRUFDbEQ7QUFBQSxFQUNBLFNBQVM7QUFBQSxJQUNSLE9BQU87QUFBQSxNQUNOLE1BQU07QUFBQSxJQUNQO0FBQUEsSUFDQSxRQUFRLENBQUMsaUJBQWlCLFFBQVE7QUFBQSxFQUNuQztBQUFBLEVBQ0EsZUFBZSxDQUFDLFdBQVc7QUFDNUI7QUFFQSxJQUFPLHNCQUFROyIsCiAgIm5hbWVzIjogW10KfQo=