def test_combo_box(self): def update(control: ctrl.Select, parent: ctrl.Select): selected = parent.items[parent.selected] control.items = [f"{selected} 1", f"{selected} 2"] app = ds.app() cb = app.select(["Hello", "World"]) self.assertTrue(isinstance(cb._derive_model(), ctrl.DefaultListModel)) c1 = app.select(handler=update, depends=cb) controller = Controller(app.controls, []) views = controller.list() v = ty.cast(ctrl.SelectView, self.get_by_id(cb.get_id(), views)) v1 = ty.cast(ctrl.SelectView, self.get_by_id(c1.get_id(), views)) self.assertEqual(0, v.selected) self.assertEqual(["Hello 1", "Hello 2"], v1.titles) v.selected = 1 print(views) views = controller.list(views) print(views) v = ty.cast(ctrl.SelectView, self.get_by_id(cb.get_id(), views)) v1 = ty.cast(ctrl.SelectView, self.get_by_id(c1.get_id(), views)) self.assertEqual(1, v.selected) self.assertEqual(["World 1", "World 2"], v1.titles)
def test_simple_check_box(self): app = ds.app() c1 = app.checkbox() controller = Controller(app.controls, []) views = controller.list() v1 = ty.cast(ctrl.CheckboxView, self.get_by_id(c1.get_id(), views)) self.assertEqual(False, v1.selected)
def test_handler_called_only_once(self): count = 0 def update_c2(control: ctrl.Control, text_area: ctrl.Input): nonlocal count if count == 0: count += 1 else: print(count) raise RuntimeError() control.text = str(int(text_area.text) * 2) def update_c3_c4(control: ctrl.Control, text_area: ctrl.Input): control.text = str(int(text_area.text) * 2) app = ds.app() c1 = app.input(text="10") c2 = app.input(handler=update_c2, depends=c1) c3 = app.input(handler=update_c3_c4, depends=c2) c4 = app.input(handler=update_c3_c4, depends=c2) controller = Controller(app.controls, []) views = controller.list() self.assertEqual(1, count) v2 = ty.cast(ctrl.InputView, self.get_by_id(c2.get_id(), views)) v3 = ty.cast(ctrl.InputView, self.get_by_id(c3.get_id(), views)) v4 = ty.cast(ctrl.InputView, self.get_by_id(c4.get_id(), views)) self.assertEqual("20", v2.text) self.assertEqual("40", v3.text) self.assertEqual("40", v4.text)
def test_pack(self): app = ds.app() c1 = app.input(label="my text", optional=True) v1 = c1.view() p1 = v1.pack() self.assertEqual(c1.get_id(), p1["id"]) self.assertEqual(None, p1["data"]) self.assertTrue(p1["enabled"]) self.assertTrue(p1["optional"]) self.assertEqual("my text", p1["label"]) self.assertEqual(v1.__class__.__name__, p1["type"]) c2 = app.input(text="10") p2 = c2.view().pack() self.assertFalse(p2["optional"]) self.assertEqual("10", p2["data"]) c3 = app.select(items=["Hello", "World"]) p3 = c3.view().pack() self.assertEqual(0, p3["selected"]) self.assertFalse(p3["optional"]) self.assertEqual(["Hello", "World"], p3["titles"]) c5 = app.slider(values=range(0, 10), selected=3) p5 = c5.view().pack() self.assertEqual(list(range(0, 10)), p5["data"]) self.assertEqual(3, p5["selected"])
def test_markdown(self): app = ds.app() app.markdown(text="Hello, **this** is Markdown") controller = Controller(app.controls, []) views = controller.list() self.assertTrue(isinstance(views[0].data, _Markdown)) self.assertEquals("Hello, **this** is Markdown", views[0].data.text) app = ds.app() t1 = app.input(text="Markdown") def m_handler(self: ctrl.Markdown, t1: ctrl.Input): self.text = "Hello, **this** is " + t1.text app.markdown(handler=m_handler, depends=[t1]) controller = Controller(app.controls, []) views = controller.list() self.assertTrue(isinstance(views[1].data, _Markdown)) self.assertEquals("Hello, **this** is Markdown", views[1].data.text)
def test_first_example(self): encoder = AppEncoder() def update(control: ctrl.Input, text_field: ctrl.Input): control.text = str(int(text_field.text) * 2) my_app = app(requirements="tests/application/test_requirements.txt", depends=[ "deprecation", "PyYAML==5.3.1", "tests.application.test_package" ]) c1 = my_app.input(text="10") c2 = my_app.input(handler=update, depends=c1) _ = my_app.output(handler=test_app, depends=[c1, c2]) # def my_test_app(x: ctrl.TextField, y: ctrl.TextField): # foo() # print(f"My local app: x={x.value()} y={y.value()}") frame_data = encoder.encode(my_app, None, None) base_dir = gettempdir() / Path("stage_simple") app_dir = self._save_data(frame_data, filename=base_dir / "application") env = Env(base_dir / "venv") env.install_dstack() env.pip_install(app_dir / "requirements.txt") test_script = f""" from inspect import signature # to be sure that all dependencies are installed import deprecation import yaml import cloudpickle with open("controller.pickle", "rb") as f: controller = cloudpickle.load(f) views = controller.list() print(views[2].data) """ test_file = Path(app_dir) / "test_script.py" test_file.write_text(dedent(test_script).lstrip()) output, error = env.run_script(["test_script.py"], app_dir) self.assertEqual("", error) self.assertEqual( f"Here is bar!{os.linesep}Here is foo!{os.linesep}My app: x=10 y=20{os.linesep}30{os.linesep}", output) env.dispose() shutil.rmtree(base_dir)
def test_immutability_of_controller(self): app = ds.app() app.input(text="Some initial data") controller = Controller(app.controls, []) views = controller.list() self.assertEqual(views[0].text, "Some initial data") views[0].text = "Some other data" updated_views = controller.list(views) self.assertEqual(updated_views[0].text, "Some other data") views = controller.list() self.assertEqual(views[0].text, "Some initial data")
def test_combo_box_model(self): class City: def __init__(self, id, title): self.id = id self.title = title def __repr__(self): return self.title class Country: def __init__(self, code, title): self.code = code self.title = title def __repr__(self): return self.title data = {"US": [City(0, "New York"), City(1, "San Francisco"), City(2, "Boston")], "DE": [City(10, "Munich"), City(11, "Berlin"), City(12, "Hamburg")]} def list_cities_from_db_by_code(country: Country) -> ty.List[City]: return data[country.code] def list_countries_from_db(self): self.items = [Country("US", "United States"), Country("DE", "German")] def update_cities(control: ctrl.Select, parent: ctrl.Select): country = ty.cast(Country, parent.get_model().element(parent.selected)) control.items = list_cities_from_db_by_code(country) app = ds.app() countries = app.select(handler=list_countries_from_db) cities = app.select(handler=update_cities, depends=countries) controller = Controller(app.controls, []) views = controller.list() v1 = ty.cast(ctrl.SelectView, self.get_by_id(countries.get_id(), views)) v2 = ty.cast(ctrl.SelectView, self.get_by_id(cities.get_id(), views)) self.assertEqual(["United States", "German"], v1.titles) self.assertEqual(0, v1.selected) self.assertEqual(["New York", "San Francisco", "Boston"], v2.titles) v1.selected = 1 views = controller.list(views) v1 = ty.cast(ctrl.SelectView, self.get_by_id(countries.get_id(), views)) v2 = ty.cast(ctrl.SelectView, self.get_by_id(cities.get_id(), views)) self.assertEqual(1, v1.selected) self.assertEqual(["Munich", "Berlin", "Hamburg"], v2.titles)
def test_jupyter_like_env(self): def update(control, text_field): control.text = str(int(text_field.text) * 2) def baz(): print("baz") my_app = app(depends=["tests.application.test_package"]) _ = my_app.input(text="0") c1 = my_app.input(text="10") c2 = my_app.input(handler=update, depends=c1) def output_handler(self: ctrl.Output, x: ctrl.Input, y: ctrl.Input): foo() baz() self.data = int(x.text) + int(y.text) _ = my_app.output(handler=output_handler, depends=[c1, c2]) encoder = AppEncoder() frame_data = encoder.encode(my_app, None, None) base_dir = gettempdir() / Path("stage_jupyter_like") app_dir = self._save_data(frame_data, filename=base_dir / "application") env = Env(base_dir / "venv") env.install_dstack() test_script = f""" from inspect import signature import cloudpickle with open("controller.pickle", "rb") as f: controller = cloudpickle.load(f) views = controller.list() print(views[3].data) """ test_file = Path(app_dir) / "test_script.py" test_file.write_text(dedent(test_script).lstrip()) output, error = env.run_script(["test_script.py"], app_dir) self.assertEqual("", error) self.assertEqual( f"Here is bar!{os.linesep}Here is foo!{os.linesep}baz{os.linesep}30{os.linesep}", output) env.dispose() shutil.rmtree(base_dir)
def test_title_override(self): class Item: def __init__(self, id, title): self.id = id self.title = title def __repr__(self): return self.title app = ds.app() app.select([Item(1, "hello"), Item(2, "world")], title=lambda x: x.title.upper()) controller = Controller(app.controls, []) views = controller.list() items_view = ty.cast(ctrl.SelectView, views[0]) self.assertEqual(["HELLO", "WORLD"], items_view.titles)
def test_update_error(self): def update(control: ctrl.Control, text_area: ctrl.Input): raise ValueError() app = ds.app() c1 = app.input("10") c2 = app.input(handler=update, depends=c1) controller = Controller(app.controls, []) try: controller.list(controller.list()) self.fail() except ctrl.UpdateError as e: self.assertEqual(c2.get_id(), e.id)
def test_controller_apply(self): def update(control, text_field): control.text = str(int(text_field.text) * 2) app = ds.app() c1 = app.input(text="10") c2 = app.input(handler=update, depends=c1) def test(): return 30 o1 = app.output(data=test()) controller = Controller(app.controls, []) views = controller.list() self.assertEqual(30, views[2].data)
def test_dependant_check_box(self): def get_selected(self: ctrl.Checkbox, c2: ctrl.Input): self.selected = int(c2.value()) > 5 app = ds.app() c1 = app.input(text="10") c2 = app.checkbox(handler=get_selected, depends=[c1]) controller = Controller(app.controls, []) views = controller.list() v1 = ty.cast(ctrl.InputView, self.get_by_id(c1.get_id(), views)) v2 = ty.cast(ctrl.CheckboxView, self.get_by_id(c2.get_id(), views)) self.assertEqual("10", v1.text) self.assertEqual(True, v2.selected) v1.text = "5" views = controller.list(views) v1 = ty.cast(ctrl.InputView, self.get_by_id(c1.get_id(), views)) v2 = ty.cast(ctrl.CheckboxView, self.get_by_id(c2.get_id(), views)) self.assertEqual("5", v1.text) self.assertEqual(False, v2.selected)
def test_simple(self): def update(control: Input, text_field: Input): control.data = str(int(text_field.data) * 2) my_app = app(requirements="tests/application/test_requirements.txt", depends=[ "deprecation", "PyYAML==5.3.1", "tests.application.test_package" ]) c1 = my_app.input("10") c2 = my_app.input(handler=update, depends=c1) _ = my_app.output(handler=test_app, depends=[c1, c2]) deps = my_app.deps() print(deps) stage_dir = self._get_temp_dir("stage1") print(stage_dir) _stage_deps(deps, stage_dir) self.tree_view(stage_dir)
def test_simple_update(self): def update(control: ctrl.Control, text_field: ctrl.Input): control.text = str(int(text_field.text) * 2) app = ds.app() c1 = app.input(text="10") c2 = app.input(handler=update, depends=c1) controller = Controller(app.controls, []) views = controller.list() self.assertEqual(2, len(views)) ids = [v.id for v in views] self.assertIn(c1.get_id(), ids) self.assertIn(c2.get_id(), ids) v1 = self.get_by_id(c1.get_id(), views) v2 = self.get_by_id(c2.get_id(), views) if isinstance(v1, ctrl.InputView): self.assertEqual("10", v1.text) else: self.fail() if isinstance(v2, ctrl.InputView): self.assertEqual("20", v2.text) else: self.fail() views = controller.list(views) v1 = self.get_by_id(c1.get_id(), views) v2 = self.get_by_id(c2.get_id(), views) if isinstance(v1, ctrl.InputView): self.assertEqual("10", v1.text) else: self.fail() if isinstance(v2, ctrl.InputView): self.assertEqual("20", v2.text)
def test_controller_event(self): app = ds.app() def i1_handler(self): print("i1_handler") if self.text is None: self.text = "Andrey" i1 = app.input(handler=i1_handler) def i2_handler(self, i1): print("i2_handler") self.text = i1.text.upper() i2 = app.input(handler=i2_handler, depends=[i1]) cb1 = app.checkbox() def m1_handler(self, i2, cb1): print("m1_handler") self.text = i2.text.upper() if cb1.selected else i2.text.lower() app.markdown(handler=m1_handler, depends=[i2, cb1], require_apply=True) controller = Controller(app.controls, []) print("Event: empty") views = controller.list() for view in views: print(str(view.pack())) print("Event: apply") views = controller.list(views, event=ctrl.Event(type="apply", source=None)) for view in views: print(str(view.pack())) print("Event: change i1") views[0].text = 'Sergey' views = controller.list(views, event=ctrl.Event(type="change", source=i1.get_id())) for view in views: print(str(view.pack()))
def test_multiple_combo_box(self): def update(control: ctrl.Select, parent: ctrl.Select): selected = [parent.items[s] for s in parent.selected] control.items = [f"{selected} 1", f"{selected} 2"] app = ds.app() cb = app.select(["Hello", "World"], multiple=True) self.assertTrue(isinstance(cb._derive_model(), ctrl.DefaultListModel)) c1 = app.select(handler=update, depends=cb) controller = Controller(app.controls, []) views = controller.list() v = ty.cast(ctrl.SelectView, self.get_by_id(cb.get_id(), views)) v1 = ty.cast(ctrl.SelectView, self.get_by_id(c1.get_id(), views)) self.assertEqual([], v.selected) self.assertEqual(["[] 1", "[] 2"], v1.titles) v.selected = [1] print(views) views = controller.list(views) print(views) v = ty.cast(ctrl.SelectView, self.get_by_id(cb.get_id(), views)) v1 = ty.cast(ctrl.SelectView, self.get_by_id(c1.get_id(), views)) self.assertEqual([1], v.selected) self.assertEqual(["['World'] 1", "['World'] 2"], v1.titles) v.selected = [0, 1] print(views) views = controller.list(views) print(views) v = ty.cast(ctrl.SelectView, self.get_by_id(cb.get_id(), views)) v1 = ty.cast(ctrl.SelectView, self.get_by_id(c1.get_id(), views)) self.assertEqual([0, 1], v.selected) self.assertEqual(["['Hello', 'World'] 1", "['Hello', 'World'] 2"], v1.titles)
def test_tqdm(self): tqdm_state = {} class Handler(TqdmHandler): def close(self, tqdm: tqdm): tqdm_state[tqdm.n] = {"desc": tqdm.desc, "n": tqdm.n, "total": tqdm.total, "elapsed": tqdm.format_dict["elapsed"]} def display(self, tqdm: tqdm): tqdm_state[tqdm.n] = {"desc": tqdm.desc, "n": tqdm.n, "total": tqdm.total, "elapsed": tqdm.format_dict["elapsed"]} set_tqdm_handler(Handler()) def output_handler(output): for _ in trange(3, desc="Calculating data"): time.sleep(0.25) output.data = "success" app = ds.app() app.output(handler=output_handler) controller = Controller(app.controls, []) views = controller.list() def apply(): controller.list(views) apply() for i in range(4): self.assertEqual(i, tqdm_state[i]["n"]) self.assertEqual(3, tqdm_state[i]["total"]) self.assertEqual("Calculating data", tqdm_state[i]["desc"]) set_tqdm_handler(None)
def test_file_uploader(self): app = ds.app() file_uploader = app.uploader() self.assertTrue(isinstance(file_uploader.uploads, list)) controller = Controller(app.controls, []) views = controller.list() file_uploader_view = ty.cast(ctrl.UploaderView, self.get_by_id(file_uploader.get_id(), views)) self.assertTrue(isinstance(file_uploader_view.uploads, list)) self.assertEqual({"uploads": []}, file_uploader_view._pack()) today = date.today() file_uploader_view.uploads.append(ctrl.Upload("some_file_id", "some_file_name", 123, today)) packed_view = {"id": file_uploader._id, "colspan": 2, "container": "main", "enabled": True, "label": None, "optional": False, "visible": True, "rowspan": 1, "type": "UploaderView", "uploads": [ {"id": "some_file_id", "file_name": "some_file_name", "length": 123, "created_date": today.strftime("%Y-%m-%d")}]} self.assertEqual(packed_view, file_uploader_view.pack()) file_uploader._apply(file_uploader_view) # TODO: Clean me value = file_uploader.value() self.assertTrue(isinstance(value, list)) self.assertEqual(len(value), 1) self.assertEqual(value[0].id, "some_file_id") self.assertEqual(value[0].file_name, "some_file_name") self.assertEqual(value[0].length, 123) self.assertEqual(value[0].created_date, today) unpacked_view = ctrl.unpack_view(packed_view) self.assertTrue(isinstance(unpacked_view, ctrl.UploaderView)) self.assertTrue(isinstance(unpacked_view.uploads, list)) self.assertEqual(len(unpacked_view.uploads), 1) unpacked_upload = unpacked_view.uploads[0] self.assertEqual(unpacked_upload.id, "some_file_id") self.assertEqual(unpacked_upload.file_name, "some_file_name") self.assertEqual(unpacked_upload.length, 123) self.assertEqual(unpacked_upload.created_date, today)
regions.value()]["Country"].unique().tolist() regions = ctrl.ComboBox(items=get_regions, label="Region") countries = ctrl.ComboBox(handler=countries_handler, label="Country", depends=[regions]) def country_output_handler(self: ctrl.Output, countries: ctrl.ComboBox): df = get_data() self.data = df[df["Country"] == countries.value()] data_by_country_app = ds.app( controls=[regions, countries], outputs=[ctrl.Output(handler=country_output_handler, depends=[countries])]) def get_companies(): df = get_data() return df["Company"].unique().tolist() companies = ctrl.ComboBox(items=get_companies, label="Company") def company_output_handler(self: ctrl.Output, companies: ctrl.ComboBox): df = get_data() row = df[df["Company"] == companies.value()].filter( ["y2015", "y2016", "y2017", "y2018", "y2019"], axis=1)
import dstack.controls as ctrl import dstack as ds import plotly.express as px @ds.cache() def get_data(): return px.data.gapminder() def output_handler(self: ctrl.Output, year: ctrl.Slider): year = year.values[year.selected] self.data = px.scatter(get_data().query("year==" + str(year)), x="gdpPercap", y="lifeExp", size="pop", color="country", hover_name="country", log_x=True, size_max=60) app = ds.app(controls=[ ctrl.Slider(values=get_data()["year"].unique().tolist(), require_apply=False) ], outputs=[ctrl.Output(handler=output_handler)]) result = ds.push('controls/slider', app) print(result.url)
regions_ctrl = ctrl.ComboBox(x1["Region"].unique().tolist(), label="Region") months_ctrl = ctrl.ComboBox(['Oct', 'Nov', 'Dec'], label="Month") churn_ctrl = ctrl.CheckBox(label="Churn", selected=True, require_apply=False) def app_handler(self: ctrl.Output, regions_ctrl: ctrl.ComboBox, months_ctrl: ctrl.ComboBox, churn_ctrl: ctrl.CheckBox): x1, x1a = get_data() y1_pred = get_model().predict(x1a) data = x1.copy() data["Predicted Churn"] = y1_pred data["Predicted Churn"] = data["Predicted Churn"].apply( lambda x: "Yes" if x == 1.0 else "No") data["RenewalMonth"] = data["RenewalMonth"].apply(lambda x: months[x - 1]) data = data.drop(["y2015", "y2016", "y2017", "y2018", "y2019", "Churn"], axis=1) data = data[(data["Predicted Churn"] == ("Yes" if churn_ctrl.selected else "No"))] data = data[(data["Region"] == regions_ctrl.value())] data = data[(data["RenewalMonth"] == months_ctrl.value())] self.data = data app = ds.app(controls=[regions_ctrl, months_ctrl, churn_ctrl], outputs=[ctrl.Output(handler=app_handler)]) url = ds.push("sklearn", app) print(url)
import dstack.controls as ctrl import dstack as ds import plotly.express as px @ds.cache() def get_data(): return px.data.stocks() def output_handler(self, ticker): self.data = px.line(get_data(), x='date', y=ticker.value()) app = ds.app(controls=[(ctrl.ComboBox(items=get_data().columns[1:].tolist()))], outputs=[(ctrl.Output(handler=output_handler))]) result = ds.push("stocks", app) print(result.url)
import dstack.controls as ctrl import dstack as ds import plotly.express as px @ds.cache() def get_data(): return px.data.stocks() def symbols_handler(self: ctrl.ComboBox): print("Calling symbols_handler") self.items = get_data().columns[1:].tolist() def output_handler(self, ticker): print("Calling output_handler") self.data = px.line(get_data(), x='date', y=ticker.value()) app = ds.app(controls=[(ctrl.ComboBox(handler=symbols_handler))], outputs=[(ctrl.Output(handler=output_handler))]) result = ds.push("logs", app) print(result.url)
import dstack as ds import dstack.controls as ctrl import pandas as pd def app_handler(self: ctrl.Output, uploader: ctrl.FileUploader): if len(uploader.uploads) > 0: with uploader.uploads[0].open() as f: self.data = pd.read_csv(f).head(100) else: self.data = ds.md("No file selected") app = ds.app(controls=[ctrl.FileUploader(label="Select a CSV file")], outputs=[ctrl.Output(handler=app_handler)]) url = ds.push("controls/file_uploader", app) print(url)
import dstack.controls as ctrl import dstack as ds import plotly.express as px @ds.cache() def get_data(): return px.data.stocks() def output_handler(self, ticker): self.data = px.line(get_data(), x='date', y=ticker.value()) app = ds.app( controls=[(ctrl.ComboBox(items=get_data().columns[1:].tolist()))], outputs=[ ctrl.Output(data=ds.md( "Here's a simple application with **Markdown** and a chart.")), ctrl.Output(handler=output_handler) ]) result = ds.push("markdown", app) print(result.url)
from datetime import datetime, timedelta import dstack.controls as ctrl import dstack as ds import plotly.express as px import pandas_datareader as pdr def ticker_handler(self: ctrl.ComboBox): self.items = ['FB', 'AMZN', 'AAPL', 'NFLX', 'GOOG'] def output_handler(self: ctrl.Output, ticker: ctrl.ComboBox): if ticker.selected > -1: start = datetime.today() - timedelta(days=30) end = datetime.today() df = pdr.data.DataReader(ticker.items[ticker.selected], 'yahoo', start, end) self.data = px.line(df, x=df.index, y=df['High']) else: self.data = ds.md("No ticker selected") app = ds.app( controls=[ctrl.ComboBox(label="Select ticker", handler=ticker_handler)], outputs=[ctrl.Output(handler=output_handler)]) result = ds.push('controls/combo_box', app) print(result.url)
import dstack as ds import dstack.controls as ctrl from handlers import fake_handler app = ds.app(outputs=[ctrl.Output(handler=fake_handler)], depends=["handlers", "utils"], requirements="requirements.txt") # An equal alternative to this is the following: # ds.app(outputs=[ctrl.Output(handler=fake_handler)], depends=["numpy", "pandas", "faker==5.5.0", "handlers", "utils"]) url = ds.push("depends", app) print(url)
from datetime import datetime, timedelta import dstack.controls as ctrl import dstack as ds import plotly.express as px import pandas_datareader as pdr def output_handler(self: ctrl.Output, ticker: ctrl.TextField): if len(ticker.text) > 0: start = datetime.today() - timedelta(days=30) end = datetime.today() df = pdr.data.DataReader(ticker.text, 'yahoo', start, end) self.data = px.line(df, x=df.index, y=df['High']) else: self.data = ds.md("No ticker selected") app = ds.app(controls=[ctrl.TextField(label="Select ticker")], outputs=[ctrl.Output(handler=output_handler)]) result = ds.push('controls/text_field', app) print(result.url)
def get_regions(): df = get_data() return df["Region"].unique().tolist() def countries_handler(self: ctrl.ComboBox, regions: ctrl.ComboBox): df = get_data() self.items = df[df["Region"] == regions.value()]["Country"].unique().tolist() regions = ctrl.ComboBox(items=get_regions, label="Region") countries = ctrl.ComboBox(handler=countries_handler, label="Country", multiple=True, depends=[regions]) def output_handler(self: ctrl.Output, countries: ctrl.ComboBox): df = get_data() self.data = df[df["Country"].isin(countries.value())] app = ds.app(controls=[regions, countries], outputs=[ds.Output(handler=output_handler, depends=[countries])]) result = ds.push('combo_box', app) print(result.url)