Files
DemonEditor/app/ui/satellites_dialog.py

588 lines
23 KiB
Python
Raw Normal View History

import re
import time
import concurrent.futures
from math import fabs
2018-04-30 18:37:02 +03:00
from app.commons import run_idle, run_task
2017-11-09 19:01:09 +03:00
from app.eparser import get_satellites, write_satellites, Satellite, Transponder
from app.tools.satellites import SatellitesParser
2018-04-16 19:42:48 +03:00
from .uicommons import Gtk, Gdk, UI_RESOURCES_PATH, TEXT_DOMAIN, MOVE_KEYS
2018-02-18 17:14:02 +03:00
from .dialogs import show_dialog, DialogType, WaitDialog
2018-05-01 21:05:18 +03:00
from .main_helper import move_items, scroll_to, append_text_to_tview
2017-10-14 13:23:34 +03:00
def show_satellites_dialog(transient, options):
SatellitesDialog(transient, options).show()
2017-10-14 13:23:34 +03:00
2017-10-30 16:20:19 +03:00
class SatellitesDialog:
2017-11-09 19:01:09 +03:00
_aggr = [None for x in range(9)] # aggregate
def __init__(self, transient, options):
2017-12-30 22:58:47 +03:00
self._data_path = options.get("data_dir_path") + "satellites.xml"
self._options = options
2017-10-30 16:20:19 +03:00
2017-11-03 23:39:15 +03:00
handlers = {"on_open": self.on_open,
2017-10-30 16:20:19 +03:00
"on_remove": self.on_remove,
"on_save": self.on_save,
"on_update": self.on_update,
2017-12-23 22:25:29 +03:00
"on_up": self.on_up,
"on_down": self.on_down,
2017-10-30 16:20:19 +03:00
"on_popup_menu": self.on_popup_menu,
2017-11-11 00:08:40 +03:00
"on_satellite_add": self.on_satellite_add,
"on_transponder_add": self.on_transponder_add,
"on_edit": self.on_edit,
"on_key_release": self.on_key_release,
2017-11-03 23:39:15 +03:00
"on_row_activated": self.on_row_activated,
2017-11-10 13:38:03 +03:00
"on_resize": self.on_resize,
"on_quit": self.on_quit}
2017-10-30 16:20:19 +03:00
builder = Gtk.Builder()
2018-03-02 17:06:53 +03:00
builder.set_translation_domain(TEXT_DOMAIN)
2017-12-25 19:50:35 +03:00
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellites_editor_dialog", "satellites_tree_store", "popup_menu",
2018-04-30 11:11:55 +03:00
"add_popup_menu", "add_menu_icon", "receive_menu_icon"))
2017-10-30 16:20:19 +03:00
builder.connect_signals(handlers)
# Adding custom image for add_menu_tool_button
add_menu_tool_button = builder.get_object("add_menu_tool_button")
add_menu_tool_button.set_image(builder.get_object("add_menu_icon"))
2017-10-30 16:20:19 +03:00
self._dialog = builder.get_object("satellites_editor_dialog")
self._dialog.set_transient_for(transient)
2017-11-09 19:01:09 +03:00
self._dialog.get_content_area().set_border_width(0) # The width of the border around the app dialog area!
2017-11-03 23:39:15 +03:00
self._sat_view = builder.get_object("satellites_editor_tree_view")
2018-02-18 17:14:02 +03:00
self._wait_dialog = WaitDialog(self._dialog)
# Setting the last size of the dialog window if it was saved
window_size = self._options.get("sat_editor_window_size", None)
if window_size:
self._dialog.resize(*window_size)
2017-10-30 16:20:19 +03:00
self._stores = {3: builder.get_object("pol_store"),
4: builder.get_object("fec_store"),
5: builder.get_object("system_store"),
6: builder.get_object("mod_store")}
2017-11-03 23:39:15 +03:00
self.on_satellites_list_load(self._sat_view.get_model())
2017-10-30 16:20:19 +03:00
@run_idle
def show(self):
2017-10-30 16:20:19 +03:00
self._dialog.run()
self._dialog.destroy()
def on_resize(self, window):
""" Stores new size properties for dialog window after resize """
if self._options:
self._options["sat_editor_window_size"] = window.get_size()
2017-11-10 13:38:03 +03:00
def on_quit(self, item):
self._dialog.destroy()
2017-11-10 13:38:03 +03:00
2017-11-03 23:39:15 +03:00
def on_open(self, model):
file_filter = Gtk.FileFilter()
file_filter.add_pattern("satellites.xml")
file_filter.set_name("satellites.xml")
response = show_dialog(dialog_type=DialogType.CHOOSER,
transient=self._dialog,
options=self._options,
action_type=Gtk.FileChooserAction.OPEN,
file_filter=file_filter)
if response == Gtk.ResponseType.CANCEL:
return
if not str(response).endswith("satellites.xml"):
show_dialog(DialogType.ERROR, self._dialog, text="No satellites.xml file is selected!")
return
self._data_path = response
self.on_satellites_list_load(model)
2017-11-03 23:39:15 +03:00
@staticmethod
def on_row_activated(view, path, column):
if view.row_expanded(path):
view.collapse_row(path)
else:
view.expand_row(path, column)
2017-12-23 22:25:29 +03:00
def on_up(self, item):
move_items(Gdk.KEY_Up, self._sat_view)
def on_down(self, item):
move_items(Gdk.KEY_Down, self._sat_view)
def on_key_release(self, view, event):
""" Handling keystrokes """
key = event.keyval
ctrl = event.state & Gdk.ModifierType.CONTROL_MASK
if key == Gdk.KEY_Delete:
self.on_remove(view)
elif key == Gdk.KEY_Insert:
2017-11-11 00:08:40 +03:00
pass
2017-11-09 19:01:09 +03:00
elif ctrl and key == Gdk.KEY_E or key == Gdk.KEY_e:
self.on_edit(view)
elif ctrl and key == Gdk.KEY_s or key == Gdk.KEY_S:
2017-11-03 23:39:15 +03:00
self.on_satellite()
elif ctrl and key == Gdk.KEY_t or key == Gdk.KEY_T:
2017-11-03 23:39:15 +03:00
self.on_transponder()
elif key == Gdk.KEY_space:
pass
2018-04-16 19:42:48 +03:00
elif ctrl and key in MOVE_KEYS:
2017-12-23 22:25:29 +03:00
move_items(key, self._sat_view)
2018-04-14 00:05:08 +03:00
elif key == Gdk.KEY_Left or key == Gdk.KEY_Right:
view.do_unselect_all(view)
2018-01-05 14:32:14 +03:00
@run_idle
2017-10-30 16:20:19 +03:00
def on_satellites_list_load(self, model):
""" Load satellites data into model """
2017-11-09 19:01:09 +03:00
try:
2018-02-18 17:14:02 +03:00
self._wait_dialog.show()
2017-11-09 19:01:09 +03:00
satellites = get_satellites(self._data_path)
except FileNotFoundError as e:
2017-12-09 16:25:54 +03:00
show_dialog(DialogType.ERROR, self._dialog, getattr(e, "message", str(e)) +
2017-11-09 19:01:09 +03:00
"\n\nPlease, download files from receiver or setup your path for read data!")
else:
model.clear()
2017-11-14 19:20:16 +03:00
self.append_data(model, satellites)
2018-02-18 17:14:02 +03:00
finally:
self._wait_dialog.hide()
2017-11-14 19:20:16 +03:00
@run_idle
def append_data(self, model, satellites):
for sat in satellites:
append_satellite(model, sat)
2017-10-30 16:20:19 +03:00
def on_add(self, view):
2017-11-09 19:01:09 +03:00
""" Common adding """
self.on_edit(view, force=True)
2017-11-11 00:08:40 +03:00
def on_satellite_add(self, item):
self.on_satellite(None)
def on_transponder_add(self, item):
self.on_transponder(None)
def on_edit(self, view, force=False):
2017-11-09 19:01:09 +03:00
""" Common edit """
2017-11-11 00:08:40 +03:00
paths = self.check_selection(view, "Please, select only one item!")
2017-11-09 19:01:09 +03:00
if not paths:
return
2017-10-30 16:20:19 +03:00
2017-11-09 19:01:09 +03:00
model = view.get_model()
itr = model.get_iter(paths[0])
row = model.get(itr, *[x for x in range(view.get_n_columns())])
2017-11-11 00:08:40 +03:00
if row[-1]: # satellite
2017-11-09 19:01:09 +03:00
self.on_satellite(None if force else Satellite(row[0], None, row[-1], None), itr)
2017-10-30 16:20:19 +03:00
else:
2017-11-26 20:40:22 +03:00
self.on_transponder(None if force else Transponder(*row[1:-2]), itr)
2017-11-09 19:01:09 +03:00
def on_satellite(self, satellite=None, edited_itr=None):
""" Create or edit satellite"""
sat_dialog = SatelliteDialog(self._dialog, satellite)
sat = sat_dialog.run()
sat_dialog.destroy()
2017-11-09 19:01:09 +03:00
if sat:
view = self._sat_view
model = view.get_model()
if satellite and edited_itr:
model.set(edited_itr, {0: sat.name, 10: sat.flags, 11: sat.position})
else:
index = self.get_sat_position_index(sat.position, model)
model.insert(None, index, [sat.name, *self._aggr, sat.flags, sat.position])
2017-12-25 19:50:35 +03:00
scroll_to(index, view)
2017-11-09 19:01:09 +03:00
def on_transponder(self, transponder=None, edited_itr=None):
""" Create or edit transponder """
paths = self.check_selection(self._sat_view, "Please, select only one satellite!")
if paths is None:
return
elif len(paths) == 0:
2017-12-09 16:25:54 +03:00
show_dialog(DialogType.ERROR, self._dialog, "No satellite is selected!")
2017-11-11 00:08:40 +03:00
return
dialog = TransponderDialog(self._dialog, transponder)
tr = dialog.run()
dialog.destroy()
2017-10-30 16:20:19 +03:00
2017-11-09 19:01:09 +03:00
if tr:
view = self._sat_view
model = view.get_model()
if transponder and edited_itr:
model.set(edited_itr, {1: tr.frequency, 2: tr.symbol_rate, 3: tr.polarization,
4: tr.fec_inner, 5: tr.system, 6: tr.modulation,
7: tr.pls_mode, 8: tr.pls_code, 9: tr.is_id})
else:
2017-11-11 00:08:40 +03:00
row = ["Transponder:", *tr, None, None]
model, paths = view.get_selection().get_selected_rows()
itr = model.get_iter(paths[0])
view.expand_row(paths[0], 0)
# Get parent iter if selected transponder
parent_itr = model.iter_parent(itr)
if parent_itr:
itr = parent_itr
2017-11-17 15:20:17 +03:00
freq = int(tr.frequency if tr.frequency else 0)
2017-11-11 00:08:40 +03:00
tr_itr = model.iter_children(itr)
# Inserting according to frequency value.
while tr_itr:
cur_freq = int(model.get_value(tr_itr, 1))
if freq <= cur_freq:
path = model.get_path(tr_itr)
index = path.get_indices()[1]
model.insert(model.iter_parent(tr_itr), index, row)
2017-12-25 19:50:35 +03:00
scroll_to(path, view)
2017-11-11 00:08:40 +03:00
break
else:
tr_itr = model.iter_next(tr_itr)
else:
itr = model.append(itr, row)
scroll_to(model.get_path(itr), view)
2017-11-09 19:01:09 +03:00
def get_sat_position_index(self, pos, model):
""" Search and returns index after given position """
pos = int(pos)
row = next(filter(lambda r: int(r[-1]) >= pos, model), None)
return row.path[0] if row else len(model)
2017-11-11 00:08:40 +03:00
def check_selection(self, view, message):
2017-11-09 19:01:09 +03:00
""" Checks if any row is selected. Shows error dialog if selected more than one.
returns selected path or None
"""
model, paths = view.get_selection().get_selected_rows()
paths_count = len(paths)
if paths_count > 1:
2017-12-09 16:25:54 +03:00
show_dialog(DialogType.ERROR, self._dialog, message)
2017-11-09 19:01:09 +03:00
return
return paths
2017-10-30 16:20:19 +03:00
@staticmethod
def on_remove(view):
selection = view.get_selection()
model, paths = selection.get_selected_rows()
itrs = [model.get_iter(path) for path in paths]
2017-10-30 16:20:19 +03:00
for itr in itrs:
model.remove(itr)
def on_save(self, view):
2017-12-09 16:25:54 +03:00
if show_dialog(DialogType.QUESTION, self._dialog) == Gtk.ResponseType.CANCEL:
2017-11-09 19:01:09 +03:00
return
2017-10-30 16:20:19 +03:00
model = view.get_model()
satellites = []
model.foreach(self.parse_data, satellites)
2017-11-12 23:39:27 +03:00
write_satellites(satellites, self._data_path)
2017-10-30 16:20:19 +03:00
2018-04-30 18:37:02 +03:00
def on_update(self, item):
dialog = SatellitesUpdateDialog(self._dialog, self._sat_view.get_model())
dialog.run()
2018-04-30 18:37:02 +03:00
dialog.destroy()
2017-10-30 16:20:19 +03:00
@staticmethod
def parse_data(model, path, itr, sats):
if model.iter_has_child(itr):
num_of_children = model.iter_n_children(itr)
transponders = []
num_columns = model.get_n_columns()
for num in range(num_of_children):
transponder_itr = model.iter_nth_child(itr, num)
transponder = model.get(transponder_itr, *[item for item in range(num_columns)])
transponders.append(Transponder(*transponder[1:-2]))
sat = model.get(itr, *[item for item in range(num_columns)])
satellite = Satellite(sat[0], sat[-2], sat[-1], transponders)
sats.append(satellite)
@staticmethod
def on_popup_menu(menu, event):
if event.get_event_type() == Gdk.EventType.BUTTON_PRESS and event.button == Gdk.BUTTON_SECONDARY:
menu.popup(None, None, None, None, event.button, event.time)
2017-10-20 14:45:51 +03:00
2018-04-30 18:37:02 +03:00
# ***************** Transponder dialog *******************#
class TransponderDialog:
""" Shows dialog for adding or edit transponder """
def __init__(self, transient, transponder: Transponder = None):
handlers = {"on_entry_changed": self.on_entry_changed}
builder = Gtk.Builder()
2018-03-02 17:06:53 +03:00
builder.set_translation_domain(TEXT_DOMAIN)
2017-12-25 19:50:35 +03:00
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
2018-04-30 18:37:02 +03:00
("transponder_dialog", "pol_store", "fec_store", "mod_store", "system_store",
"pls_mode_store"))
builder.connect_signals(handlers)
self._dialog = builder.get_object("transponder_dialog")
self._dialog.set_transient_for(transient)
self._freq_entry = builder.get_object("freq_entry")
self._rate_entry = builder.get_object("rate_entry")
self._pol_box = builder.get_object("pol_box")
self._fec_box = builder.get_object("fec_box")
self._sys_box = builder.get_object("sys_box")
self._mod_box = builder.get_object("mod_box")
self._pls_mode_box = builder.get_object("pls_mode_box")
self._pls_code_entry = builder.get_object("pls_code_entry")
self._is_id_entry = builder.get_object("is_id_entry")
# pattern for frequency and rate entries (only digits)
self._pattern = re.compile("\D")
# style
self._style_provider = Gtk.CssProvider()
2017-12-25 19:50:35 +03:00
self._style_provider.load_from_path(UI_RESOURCES_PATH + "style.css")
self._freq_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
self._rate_entry.get_style_context().add_provider_for_screen(Gdk.Screen.get_default(), self._style_provider,
Gtk.STYLE_PROVIDER_PRIORITY_USER)
if transponder:
self.init_transponder(transponder)
def run(self):
while self._dialog.run() != Gtk.ResponseType.CANCEL:
tr = self.to_transponder()
if self.is_accept(tr):
return tr
2017-12-09 16:25:54 +03:00
show_dialog(DialogType.ERROR, self._dialog, "Please check your parameters and try again.")
def destroy(self):
self._dialog.destroy()
def init_transponder(self, transponder):
self._freq_entry.set_text(transponder.frequency)
self._rate_entry.set_text(transponder.symbol_rate)
self._pol_box.set_active_id(transponder.polarization)
self._fec_box.set_active_id(transponder.fec_inner)
self._sys_box.set_active_id(transponder.system)
self._mod_box.set_active_id(transponder.modulation)
self._pls_mode_box.set_active_id(transponder.pls_mode)
self._is_id_entry.set_text(transponder.is_id if transponder.is_id else "")
self._pls_code_entry.set_text(transponder.pls_code if transponder.pls_code else "")
def to_transponder(self):
return Transponder(frequency=self._freq_entry.get_text(),
symbol_rate=self._rate_entry.get_text(),
polarization=self._pol_box.get_active_id(),
fec_inner=self._fec_box.get_active_id(),
system=self._sys_box.get_active_id(),
modulation=self._mod_box.get_active_id(),
pls_mode=self._pls_mode_box.get_active_id(),
pls_code=self._pls_code_entry.get_text(),
is_id=self._is_id_entry.get_text())
def on_entry_changed(self, entry):
entry.set_name("digit-entry" if self._pattern.search(entry.get_text()) else "GtkEntry")
def is_accept(self, tr):
if self._pattern.search(tr.frequency) or not tr.frequency:
return False
elif self._pattern.search(tr.symbol_rate) or not tr.symbol_rate:
return False
elif None in (tr.polarization, tr.fec_inner, tr.system, tr.modulation):
return False
elif self._pattern.search(tr.pls_code) or self._pattern.search(tr.is_id):
return False
return True
2018-04-30 18:37:02 +03:00
# ***************** Satellite dialog *******************#
class SatelliteDialog:
""" Shows dialog for adding or edit satellite """
def __init__(self, transient, satellite: Satellite = None):
builder = Gtk.Builder()
2018-03-02 17:06:53 +03:00
builder.set_translation_domain(TEXT_DOMAIN)
2017-12-25 19:50:35 +03:00
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellite_dialog", "side_store", "pos_adjustment"))
self._dialog = builder.get_object("satellite_dialog")
self._dialog.set_transient_for(transient)
self._sat_name = builder.get_object("sat_name_entry")
self._sat_position = builder.get_object("sat_position_button")
self._side = builder.get_object("side_box")
if satellite:
self._sat_name.set_text(satellite.name[0:satellite.name.find("(")].strip())
pos = satellite.position
pos = float("{}.{}".format(pos[:-1], pos[-1:]))
self._sat_position.set_value(fabs(pos))
2017-11-17 15:20:17 +03:00
self._side.set_active(0 if pos >= 0 else 1) # E or W
def run(self):
if self._dialog.run() == Gtk.ResponseType.CANCEL:
return
return self.to_satellite()
def destroy(self):
self._dialog.destroy()
def to_satellite(self):
name = self._sat_name.get_text()
pos = round(self._sat_position.get_value(), 1)
side = self._side.get_active()
name = "{} ({}{})".format(name, pos, self._side.get_active_id())
pos = "{}{}{}".format("-" if side == 1 else "", *str(pos).split("."))
2017-11-09 19:01:09 +03:00
return Satellite(name=name, flags="0", position=pos, transponders=None)
2018-04-30 18:37:02 +03:00
# ***************** Satellite update dialog *******************#
class SatellitesUpdateDialog:
""" Dialog for update satellites over internet """
2018-04-30 18:37:02 +03:00
def __init__(self, transient, main_model):
handlers = {"on_update_satellites_list": self.on_update_satellites_list,
"on_receive_satellites_list": self.on_receive_satellites_list,
"on_cancel_receive": self.on_cancel_receive,
"on_selected_toggled": self.on_selected_toggled,
"on_info_bar_close": self.on_info_bar_close,
2018-04-30 18:37:02 +03:00
"on_quit": self.on_quit}
builder = Gtk.Builder()
builder.set_translation_domain(TEXT_DOMAIN)
builder.add_objects_from_file(UI_RESOURCES_PATH + "satellites_dialog.glade",
("satellites_update_dialog", "update_source_store", "update_sat_list_store"))
builder.connect_signals(handlers)
self._dialog = builder.get_object("satellites_update_dialog")
self._dialog.set_transient_for(transient)
self._main_model = main_model
# self._dialog.get_content_area().set_border_width(0)
self._sat_view = builder.get_object("sat_update_tree_view")
2018-05-01 21:05:18 +03:00
self._sat_update_expander = builder.get_object("sat_update_expander")
self._text_view = builder.get_object("text_view")
2018-04-30 18:37:02 +03:00
self._receive_sat_list_tool_button = builder.get_object("receive_sat_list_tool_button")
self._sat_update_info_bar = builder.get_object("sat_update_info_bar")
self._info_bar_message_label = builder.get_object("info_bar_message_label")
2018-04-30 18:37:02 +03:00
self._download_task = False
self._parser = None
def run(self):
if self._dialog.run() == Gtk.ResponseType.CANCEL:
self._download_task = False
return
def destroy(self):
self._dialog.destroy()
@run_idle
def on_update_satellites_list(self, item):
if self._download_task:
show_dialog(DialogType.ERROR, self._dialog, "The task is already running!")
return
model = self._sat_view.get_model()
model.clear()
self._download_task = True
if not self._parser:
self._parser = SatellitesParser(url="https://www.flysat.com/satlist.php")
sats = self._parser.get_satellites_list()
if sats:
for sat in sats:
model = self._sat_view.get_model()
model.append((sat[1], sat[2], sat[3], sat[0], False))
self._download_task = False
@run_task
2018-04-30 18:37:02 +03:00
def on_receive_satellites_list(self, item):
if self._download_task:
show_dialog(DialogType.ERROR, self._dialog, "The task is already running!")
return
self.receive_satellites()
@run_task
def receive_satellites(self):
self._download_task = True
self._sat_update_expander.set_expanded(True)
self._text_view.get_buffer().set_text("", 0)
2018-04-30 18:37:02 +03:00
model = self._sat_view.get_model()
start = time.time()
with concurrent.futures.ProcessPoolExecutor(max_workers=4) as executor:
text = "Processing: {}\n"
sats = []
futures = {executor.submit(self._parser.get_satellite, sat[:-1]): sat for sat in [r for r in model if r[4]]}
for future in concurrent.futures.as_completed(futures):
if not self._download_task:
executor.shutdown()
return
data = future.result()
self.append_output(text.format(data[0]))
sats.append(data)
self.append_output("-" * 75 + "\n")
self.append_output("Consumed : {:0.0f}s, {} satellites received.".format(start - time.time(), len(sats)))
# self.show_info_message(message, Gtk.MessageType.INFO)
sats = {s[2]: s for s in sats} # key = position, v = satellite
for row in self._main_model:
pos = row[-1]
if pos in sats:
sat = sats.pop(pos)
itr = row.iter
self.update_satellite(itr, row, sat)
for sat in sats.values():
append_satellite(self._main_model, sat)
self._download_task = False
2018-04-30 18:37:02 +03:00
@run_idle
def update_satellite(self, itr, row, sat):
if self._main_model.iter_has_child(itr):
children = row.iterchildren()
for ch in children:
self._main_model.remove(ch.iter)
for tr in sat[3]:
self._main_model.append(itr, ["Transponder:", *tr, None, None])
2018-05-01 21:05:18 +03:00
@run_idle
def append_output(self, text):
append_text_to_tview(text, self._text_view)
2018-04-30 18:37:02 +03:00
@run_idle
def on_cancel_receive(self, item=None):
self._download_task = False
def on_selected_toggled(self, toggle, path):
model = self._sat_view.get_model()
model.set_value(model.get_iter(path), 4, not toggle.get_active())
self.update_receive_button_state(model)
@run_idle
def update_receive_button_state(self, model):
self._receive_sat_list_tool_button.set_sensitive((any(r[4] for r in model)))
@run_idle
def show_info_message(self, text, message_type):
self._sat_update_info_bar.set_visible(True)
self._sat_update_info_bar.set_message_type(message_type)
self._info_bar_message_label.set_text(text)
def on_info_bar_close(self, bar=None, resp=None):
self._sat_update_info_bar.set_visible(False)
2018-04-30 18:37:02 +03:00
def on_quit(self):
self._download_task = False
# ***************** Commons *******************#
@run_idle
def append_satellite(model, sat):
""" Common function for append satellite to the model """
name, flags, pos, transponders = sat
parent = model.append(None, [name, *(None,) * 9, flags, pos])
for transponder in transponders:
model.append(parent, ["Transponder:", *transponder, None, None])
2017-10-14 12:24:59 +03:00
if __name__ == "__main__":
pass