reformat code, and use global variable

master
CHUN YU YAO 2022-01-13 01:14:27 +08:00
parent 1820d5e817
commit 4f9d29d9e0
2 changed files with 408 additions and 315 deletions

View File

@ -27,13 +27,6 @@ from selenium.webdriver.common.desired_capabilities import DesiredCapabilities
# for selenium 4 # for selenium 4
from selenium.webdriver.chrome.service import Service from selenium.webdriver.chrome.service import Service
# method 5: uc
#import undetected_chromedriver as uc
# method 6: Selenium Stealth
from selenium_stealth import stealth
# for wait #1 # for wait #1
import time import time
@ -54,12 +47,11 @@ warnings.simplefilter('ignore',InsecureRequestWarning)
import ssl import ssl
ssl._create_default_https_context = ssl._create_unverified_context ssl._create_default_https_context = ssl._create_unverified_context
#執行方式python chrome_tixcraft.py 或 python3 chrome_tixcraft.py #執行方式python chrome_tixcraft.py 或 python3 chrome_tixcraft.py
#附註1沒有寫的很好很多地方應該可以模組化。 #附註1沒有寫的很好很多地方應該可以模組化。
#附註2 #附註2
CONST_APP_VERSION = u"MaxBot (2022.01.10)" CONST_APP_VERSION = u"MaxBot (2022.01.12)"
CONST_FROM_TOP_TO_BOTTOM = u"from top to bottom" CONST_FROM_TOP_TO_BOTTOM = u"from top to bottom"
CONST_FROM_BOTTOM_TO_TOP = u"from bottom to top" CONST_FROM_BOTTOM_TO_TOP = u"from bottom to top"
@ -74,20 +66,6 @@ CONT_STRING_1_SEATS_REMAINING = [u'1 seat(s) remaining',u'剩餘 1',u'1 席残
# 說明:初始化 webdriver # 說明:初始化 webdriver
driver = None driver = None
# 讀取檔案裡的參數值
basis = ""
if hasattr(sys, 'frozen'):
basis = sys.executable
else:
basis = sys.argv[0]
app_root = os.path.dirname(basis)
config_filepath = os.path.join(app_root, 'settings.json')
config_dict = None
if os.path.isfile(config_filepath):
with open(config_filepath) as json_data:
config_dict = json.load(json_data)
homepage = None homepage = None
browser = None browser = None
ticket_number = None ticket_number = None
@ -120,7 +98,58 @@ auto_guess_options = False
debugMode = False debugMode = False
if not config_dict is None: def get_app_root():
# 讀取檔案裡的參數值
basis = ""
if hasattr(sys, 'frozen'):
basis = sys.executable
else:
basis = sys.argv[0]
app_root = os.path.dirname(basis)
return app_root
def get_config_dict():
config_json_filename = 'settings.json'
app_root = get_app_root()
config_filepath = os.path.join(app_root, config_json_filename)
config_dict = None
if os.path.isfile(config_filepath):
with open(config_filepath) as json_data:
config_dict = json.load(json_data)
return config_dict
def load_config_from_local(driver):
config_dict = get_config_dict()
global homepage
global browser
global debugMode
global ticket_number
global facebook_account
global auto_press_next_step_button
global auto_fill_ticket_number
global kktix_area_auto_select_mode
global kktix_area_keyword
global kktix_answer_dictionary
global kktix_answer_dictionary_list
global auto_guess_options
global pass_1_seat_remaining_enable
global area_keyword_1
global area_keyword_2
global date_auto_select_enable
global date_auto_select_mode
global date_keyword
global area_auto_select_enable
global area_auto_select_mode
global debugMode
if not config_dict is None:
# read config. # read config.
if 'homepage' in config_dict: if 'homepage' in config_dict:
homepage = config_dict["homepage"] homepage = config_dict["homepage"]
@ -249,6 +278,12 @@ if not config_dict is None:
Root_Dir = "" Root_Dir = ""
if browser == "chrome": if browser == "chrome":
# method 5: uc
#import undetected_chromedriver as uc
# method 6: Selenium Stealth
from selenium_stealth import stealth
DEFAULT_ARGS = [ DEFAULT_ARGS = [
'--disable-audio-output', '--disable-audio-output',
'--disable-background-networking', '--disable-background-networking',
@ -376,6 +411,7 @@ if not config_dict is None:
chromedriver_path =Root_Dir+ "webdriver/geckodriver" chromedriver_path =Root_Dir+ "webdriver/geckodriver"
if platform.system()=="windows": if platform.system()=="windows":
chromedriver_path =Root_Dir+ "webdriver/geckodriver.exe" chromedriver_path =Root_Dir+ "webdriver/geckodriver.exe"
firefox_service = Service(chromedriver_path) firefox_service = Service(chromedriver_path)
driver = webdriver.Firefox(service=firefox_service) driver = webdriver.Firefox(service=firefox_service)
@ -389,9 +425,12 @@ if not config_dict is None:
except Exception as excSwithFail: except Exception as excSwithFail:
pass pass
driver.get(homepage) driver.get(homepage)
else:
else:
print("Config error!") print("Config error!")
return driver
# common functions. # common functions.
def find_between( s, first, last ): def find_between( s, first, last ):
try: try:
@ -737,7 +776,7 @@ def get_answer_list_by_question(captcha_text_div_text):
return return_list, my_answer_delimitor return return_list, my_answer_delimitor
# from detail to game # from detail to game
def tixcraft_redirect(url): def tixcraft_redirect(driver, url):
game_name = "" game_name = ""
# get game_name from url # get game_name from url
@ -752,7 +791,10 @@ def tixcraft_redirect(url):
#entry_url = "tixcraft.com/activity/game/%s" % (game_name,) #entry_url = "tixcraft.com/activity/game/%s" % (game_name,)
driver.get(entry_url) driver.get(entry_url)
def date_auto_select(url): def date_auto_select(driver, url, date_auto_select_mode, date_keyword):
debug_date_select = True # debug.
debug_date_select = False # online
game_name = "" game_name = ""
if "/activity/game/" in url: if "/activity/game/" in url:
@ -760,9 +802,18 @@ def date_auto_select(url):
if len(url_split) >= 6: if len(url_split) >= 6:
game_name = url_split[5] game_name = url_split[5]
if debug_date_select:
print('get date game_name:', game_name)
print("date_auto_select_mode:", date_auto_select_mode)
print("date_keyword:", date_keyword)
# choose date # choose date
if "/activity/game/%s" % (game_name,) in url: if "/activity/game/%s" % (game_name,) in url:
if len(date_keyword) == 0: if len(date_keyword) == 0:
if debug_date_select:
print("date keyword is empty.")
el = None el = None
if date_auto_select_mode == CONST_FROM_TOP_TO_BOTTOM: if date_auto_select_mode == CONST_FROM_TOP_TO_BOTTOM:
@ -781,6 +832,9 @@ def date_auto_select(url):
pass pass
#print("find a tag fail") #print("find a tag fail")
if debug_date_select:
print("date keyword is empty.")
if el is not None: if el is not None:
# first date. # first date.
try: try:
@ -788,6 +842,9 @@ def date_auto_select(url):
except Exception as exc: except Exception as exc:
print("try to click .btn-next fail") print("try to click .btn-next fail")
else: else:
if debug_date_select:
print("date keyword:", date_keyword)
# match keyword. # match keyword.
date_list = None date_list = None
try: try:
@ -930,7 +987,7 @@ def get_tixcraft_target_area(el, area_keyword):
# PS: auto refresh condition 1: no keyword + no hyperlink. # PS: auto refresh condition 1: no keyword + no hyperlink.
# PS: auto refresh condition 2: with keyword + no hyperlink. # PS: auto refresh condition 2: with keyword + no hyperlink.
def area_auto_select(url): def area_auto_select(driver, url, area_keyword_1, area_keyword_2):
if '/ticket/area/' in url: if '/ticket/area/' in url:
#driver.switch_to.default_content() #driver.switch_to.default_content()
@ -1117,7 +1174,7 @@ def ticket_number_auto_fill(url, form_select):
except Exception as exc: except Exception as exc:
print("find form_verifyCode fail") print("find form_verifyCode fail")
def tixcraft_verify(url): def tixcraft_verify(driver, url):
ret = False ret = False
captcha_password_string = None captcha_password_string = None
@ -1248,7 +1305,7 @@ def tixcraft_verify(url):
return ret return ret
def tixcraft_ticket_main(url, is_verifyCode_editing): def tixcraft_ticket_main(driver, url, is_verifyCode_editing):
form_select = None form_select = None
try: try:
#form_select = driver.find_element(By.TAG_NAME, 'select') #form_select = driver.find_element(By.TAG_NAME, 'select')
@ -1292,7 +1349,7 @@ def tixcraft_ticket_main(url, is_verifyCode_editing):
# : 1: /events/xxx # : 1: /events/xxx
# : 2: /events/xxx/registrations/new # : 2: /events/xxx/registrations/new
# : This is for case-1. # : This is for case-1.
def kktix_events_press_next_button(): def kktix_events_press_next_button(driver):
ret = False ret = False
# let javascript to enable button. # let javascript to enable button.
@ -1309,7 +1366,7 @@ def kktix_events_press_next_button():
except Exception as exc: except Exception as exc:
print("wait form-actions div wait to be clickable Exception:") print("wait form-actions div wait to be clickable Exception:")
print(exc) #print(exc)
pass pass
# retry once # retry once
@ -1328,7 +1385,7 @@ def kktix_events_press_next_button():
return ret return ret
# : This is for case-2 next button. # : This is for case-2 next button.
def kktix_press_next_button(): def kktix_press_next_button(driver):
ret = False ret = False
# let javascript to enable button. # let javascript to enable button.
@ -1424,7 +1481,7 @@ def kktix_input_captcha_text(captcha_inner_div, captcha_password_string, force_o
return ret return ret
def kktix_assign_ticket_number(): def kktix_assign_ticket_number(driver, ticket_number, kktix_area_keyword):
ret = False ret = False
areas = None areas = None
@ -1598,7 +1655,7 @@ def kktix_get_web_datetime(url, registrationsNewApp_div):
return web_datetime return web_datetime
def kktix_check_agree_checkbox(): def kktix_check_agree_checkbox(driver):
is_need_refresh = False is_need_refresh = False
is_finish_checkbox_click = False is_finish_checkbox_click = False
@ -1679,14 +1736,14 @@ def kktix_check_register_status(url):
#print("registerStatus:", registerStatus) #print("registerStatus:", registerStatus)
return registerStatus return registerStatus
def kktix_reg_new_main(url, answer_index, registrationsNewApp_div, is_finish_checkbox_click): def kktix_reg_new_main(url, answer_index, registrationsNewApp_div, is_finish_checkbox_click, auto_fill_ticket_number, ticket_number, kktix_area_keyword):
#--------------------------- #---------------------------
# part 2: ticket number # part 2: ticket number
#--------------------------- #---------------------------
is_assign_ticket_number = False is_assign_ticket_number = False
if auto_fill_ticket_number: if auto_fill_ticket_number:
for retry_index in range(10): for retry_index in range(10):
is_assign_ticket_number = kktix_assign_ticket_number() is_assign_ticket_number = kktix_assign_ticket_number(driver, ticket_number, kktix_area_keyword)
if is_assign_ticket_number: if is_assign_ticket_number:
break break
#print('is_assign_ticket_number:', is_assign_ticket_number) #print('is_assign_ticket_number:', is_assign_ticket_number)
@ -2065,7 +2122,7 @@ def kktix_reg_new_main(url, answer_index, registrationsNewApp_div, is_finish_che
if not is_finish_checkbox_click: if not is_finish_checkbox_click:
for retry_i in range(10): for retry_i in range(10):
# retry again. # retry again.
is_need_refresh, is_finish_checkbox_click = kktix_check_agree_checkbox() is_need_refresh, is_finish_checkbox_click = kktix_check_agree_checkbox(driver)
time.sleep(0.1) time.sleep(0.1)
if is_finish_checkbox_click: if is_finish_checkbox_click:
break break
@ -2075,7 +2132,7 @@ def kktix_reg_new_main(url, answer_index, registrationsNewApp_div, is_finish_che
# normal mode. # normal mode.
#print("# normal mode.") #print("# normal mode.")
if is_finish_checkbox_click: if is_finish_checkbox_click:
kktix_press_next_button() kktix_press_next_button(driver)
else: else:
print("unable to assign checkbox value") print("unable to assign checkbox value")
else: else:
@ -2083,7 +2140,7 @@ def kktix_reg_new_main(url, answer_index, registrationsNewApp_div, is_finish_che
# for easy guest mode, we can fill the password correct. # for easy guest mode, we can fill the password correct.
#print("for easy guest mode, we can fill the password correct.") #print("for easy guest mode, we can fill the password correct.")
if is_finish_checkbox_click: if is_finish_checkbox_click:
kktix_press_next_button() kktix_press_next_button(driver)
else: else:
print("unable to assign checkbox value") print("unable to assign checkbox value")
else: else:
@ -2119,7 +2176,7 @@ def kktix_reg_new_main(url, answer_index, registrationsNewApp_div, is_finish_che
print("send ans:" + answer) print("send ans:" + answer)
captcha_password_string = answer captcha_password_string = answer
if kktix_input_captcha_text(captcha_inner_div, captcha_password_string): if kktix_input_captcha_text(captcha_inner_div, captcha_password_string):
kktix_press_next_button() kktix_press_next_button(driver)
else: else:
# exceed index, do nothing. # exceed index, do nothing.
pass pass
@ -2130,7 +2187,7 @@ def kktix_reg_new_main(url, answer_index, registrationsNewApp_div, is_finish_che
return answer_index return answer_index
def kktix_reg_new(url, answer_index, kktix_register_status_last): def kktix_reg_new(driver, url, answer_index, kktix_register_status_last):
registerStatus = kktix_register_status_last registerStatus = kktix_register_status_last
#--------------------------- #---------------------------
@ -2164,11 +2221,11 @@ def kktix_reg_new(url, answer_index, kktix_register_status_last):
is_need_refresh = True is_need_refresh = True
if not is_need_refresh: if not is_need_refresh:
is_need_refresh, is_finish_checkbox_click = kktix_check_agree_checkbox() is_need_refresh, is_finish_checkbox_click = kktix_check_agree_checkbox(driver)
if not is_finish_checkbox_click: if not is_finish_checkbox_click:
# retry again. # retry again.
is_need_refresh, is_finish_checkbox_click = kktix_check_agree_checkbox() is_need_refresh, is_finish_checkbox_click = kktix_check_agree_checkbox(driver)
#print('check agree_terms_checkbox, is_need_refresh:',is_need_refresh) #print('check agree_terms_checkbox, is_need_refresh:',is_need_refresh)
# check is able to buy. # check is able to buy.
@ -2215,7 +2272,7 @@ def kktix_reg_new(url, answer_index, kktix_register_status_last):
''' '''
except Exception as exc: except Exception as exc:
pass pass
print("find input fail:", exc) #print("find input fail:", exc)
if is_need_refresh: if is_need_refresh:
try: try:
@ -2229,7 +2286,10 @@ def kktix_reg_new(url, answer_index, kktix_register_status_last):
answer_index = -1 answer_index = -1
registerStatus = None registerStatus = None
else: else:
answer_index = kktix_reg_new_main(url, answer_index, registrationsNewApp_div, is_finish_checkbox_click) global auto_fill_ticket_number
global ticket_number
global kktix_area_keyword
answer_index = kktix_reg_new_main(url, answer_index, registrationsNewApp_div, is_finish_checkbox_click, auto_fill_ticket_number, ticket_number, kktix_area_keyword)
return answer_index, registerStatus return answer_index, registerStatus
@ -2342,7 +2402,7 @@ def get_fami_target_area(date_keyword, area_keyword_1, area_keyword_2):
return areas return areas
def fami_activity(url): def fami_activity(driver, url):
#print("fami_activity bingo") #print("fami_activity bingo")
#--------------------------- #---------------------------
@ -2356,13 +2416,23 @@ def fami_activity(url):
fami_start_to_buy_button.click() fami_start_to_buy_button.click()
time.sleep(0.5) time.sleep(0.5)
except Exception as exc: except Exception as exc:
print("click buyWaiting button fail") pass
print(exc) print("click buyWaiting button fail...")
#print(exc)
def fami_home(url): def fami_home(driver, url):
print("fami_home bingo") print("fami_home bingo")
global is_assign_ticket_number
global ticket_number
global date_keyword
global area_keyword_1
global area_keyword_2
global area_auto_select_mode
is_select_box_visible = False is_select_box_visible = False
#--------------------------- #---------------------------
@ -2467,7 +2537,7 @@ def fami_home(url):
pass pass
def urbtix_ticket_number_auto_select(url): def urbtix_ticket_number_auto_select(driver, url, ticket_number):
ret = False ret = False
is_assign_ticket_number = False is_assign_ticket_number = False
@ -2516,7 +2586,7 @@ def urbtix_ticket_number_auto_select(url):
# True: area block appear. # True: area block appear.
# False: area block not appear. # False: area block not appear.
# ps: return value for date auto select. # ps: return value for date auto select.
def urbtix_area_auto_select(url): def urbtix_area_auto_select(driver, url, kktix_area_keyword):
ret = False ret = False
areas = None areas = None
@ -2601,7 +2671,7 @@ def urbtix_area_auto_select(url):
return ret return ret
def urbtix_next_button_press(url): def urbtix_next_button_press(driver, url):
ret = False ret = False
try: try:
el = driver.find_element(By.CSS_SELECTOR, '#express-purchase-btn > div > span') el = driver.find_element(By.CSS_SELECTOR, '#express-purchase-btn > div > span')
@ -2618,20 +2688,24 @@ def urbtix_next_button_press(url):
return ret return ret
def urbtix_performance(url): def urbtix_performance(driver, url):
#print("urbtix performance bingo") #print("urbtix performance bingo")
if auto_fill_ticket_number: global auto_fill_ticket_number
area_div_exist = False global ticket_number
if len(kktix_area_keyword) > 0:
area_div_exist = urbtix_area_auto_select(url)
ticket_number_select_exist, is_assign_ticket_number = urbtix_ticket_number_auto_select(url) global kktix_area_keyword
global auto_press_next_step_button
if auto_fill_ticket_number:
area_div_exist = urbtix_area_auto_select(driver, url, kktix_area_keyword)
ticket_number_select_exist, is_assign_ticket_number = urbtix_ticket_number_auto_select(driver, url, ticket_number)
# todo. # todo.
if auto_press_next_step_button: if auto_press_next_step_button:
if is_assign_ticket_number: if is_assign_ticket_number:
urbtix_next_button_press(url) urbtix_next_button_press(driver, url)
# purpose: area auto select # purpose: area auto select
# return: # return:
@ -2850,7 +2924,7 @@ def cityline_next_button_press(url):
return ret return ret
def cityline_event(url): def cityline_event(driver, url):
ret = False ret = False
is_non_member_displayed = False is_non_member_displayed = False
@ -2925,11 +2999,16 @@ def cityline_captcha_auto_focus(url):
return ret return ret
def cityline_performance(url): def cityline_performance(driver, url):
#print("cityline bingo") #print("cityline bingo")
if "performance.do;" in url: if "performance.do;" in url:
cityline_captcha_auto_focus(url) cityline_captcha_auto_focus(url)
global auto_fill_ticket_number
global kktix_area_keyword
global auto_press_next_step_button
global is_assign_ticket_number
if "?cid=" in url: if "?cid=" in url:
if auto_fill_ticket_number: if auto_fill_ticket_number:
area_div_exist = False area_div_exist = False
@ -2951,7 +3030,7 @@ def cityline_performance(url):
break break
def facebook_login(url): def facebook_login(driver, url):
ret = False ret = False
try: try:
el = driver.find_element(By.CSS_SELECTOR, '#email') el = driver.find_element(By.CSS_SELECTOR, '#email')
@ -2971,6 +3050,9 @@ def facebook_login(url):
def main(): def main():
global driver
driver = load_config_from_local(driver)
# internal variable. 說明:這是一個內部變數,請略過。 # internal variable. 說明:這是一個內部變數,請略過。
url = "" url = ""
last_url = "" last_url = ""
@ -2981,6 +3063,7 @@ def main():
answer_index = -1 answer_index = -1
kktix_register_status_last = None kktix_register_status_last = None
global debugMode
if debugMode: if debugMode:
print("Start to looping, detect browser url...") print("Start to looping, detect browser url...")
@ -3110,7 +3193,7 @@ def main():
if len(str_exc)==0: if len(str_exc)==0:
str_exc = repr(exc) str_exc = repr(exc)
exit_bot_error_strings = [u'Max retries exceeded with url', u'chrome not reachable'] exit_bot_error_strings = [u'Max retries exceeded with url', u'chrome not reachable', u'without establishing a connection']
for str_chrome_not_reachable in exit_bot_error_strings: for str_chrome_not_reachable in exit_bot_error_strings:
# for python2 # for python2
try: try:
@ -3177,7 +3260,7 @@ def main():
# for Max's test. # for Max's test.
if '/Downloads/varify.html' in url: if '/Downloads/varify.html' in url:
tixcraft_verify(url) tixcraft_verify(driver, url)
tixcraft_family = False tixcraft_family = False
if 'tixcraft.com' in url: if 'tixcraft.com' in url:
@ -3187,6 +3270,7 @@ def main():
tixcraft_family = True tixcraft_family = True
if tixcraft_family: if tixcraft_family:
#print("tixcraft_family entry.")
if '/ticket/order' in url: if '/ticket/order' in url:
# do nothing. # do nothing.
continue continue
@ -3195,30 +3279,39 @@ def main():
# do nothing. # do nothing.
continue continue
tixcraft_redirect(url) tixcraft_redirect(driver, url)
global date_auto_select_enable
global date_auto_select_mode
global date_keyword
if date_auto_select_enable: if date_auto_select_enable:
date_auto_select(url) date_auto_select(driver, url, date_auto_select_mode, date_keyword)
# choose area # choose area
if area_auto_select_enable: global area_auto_select_enable
area_auto_select(url) global area_keyword_1
global area_keyword_2
if area_auto_select_enable:
area_auto_select(driver, url, area_keyword_1, area_keyword_2)
if '/ticket/verify/' in url: if '/ticket/verify/' in url:
tixcraft_verify(url) tixcraft_verify(driver, url)
# main app, to select ticket number. # main app, to select ticket number.
if '/ticket/ticket/' in url: if '/ticket/ticket/' in url:
is_verifyCode_editing = tixcraft_ticket_main(url, is_verifyCode_editing) is_verifyCode_editing = tixcraft_ticket_main(driver, url, is_verifyCode_editing)
else: else:
# not is input verify code, reset flag. # not is input verify code, reset flag.
is_verifyCode_editing = False is_verifyCode_editing = False
global auto_press_next_step_button
# for kktix.cc and kktix.com # for kktix.cc and kktix.com
if 'kktix.c' in url: if 'kktix.c' in url:
if '/registrations/new' in url: if '/registrations/new' in url:
answer_index, kktix_register_status_last = kktix_reg_new(url, answer_index, kktix_register_status_last) answer_index, kktix_register_status_last = kktix_reg_new(driver, url, answer_index, kktix_register_status_last)
else: else:
is_event_page = False is_event_page = False
if '/events/' in url: if '/events/' in url:
@ -3229,7 +3322,7 @@ def main():
if auto_press_next_step_button: if auto_press_next_step_button:
# pass switch check. # pass switch check.
#print("should press next here.") #print("should press next here.")
kktix_events_press_next_button() kktix_events_press_next_button(driver)
answer_index = -1 answer_index = -1
kktix_register_status_last = None kktix_register_status_last = None
@ -3237,9 +3330,9 @@ def main():
# for famiticket # for famiticket
if 'famiticket.com' in url: if 'famiticket.com' in url:
if '/Home/Activity/Info/' in url: if '/Home/Activity/Info/' in url:
fami_activity(url) fami_activity(driver, url)
if '/Sales/Home/Index/' in url: if '/Sales/Home/Index/' in url:
fami_home(url) fami_home(driver, url)
# for urbtix # for urbtix
@ -3260,11 +3353,11 @@ def main():
pass pass
if '/performanceDetail/' in url: if '/performanceDetail/' in url:
urbtix_performance(url) urbtix_performance(driver, url)
if 'cityline.com' in url: if 'cityline.com' in url:
if '/event.do' in url: if '/event.do' in url:
cityline_event(url) cityline_event(driver, url)
#pass #pass
if '/Events.do' in url: if '/Events.do' in url:
@ -3275,7 +3368,7 @@ def main():
pass pass
if '/performance.do' in url: if '/performance.do' in url:
cityline_performance(url) cityline_performance(driver, url)
if __name__ == "__main__": if __name__ == "__main__":
main() main()

View File

@ -17,7 +17,7 @@ import sys
import platform import platform
import json import json
CONST_APP_VERSION = u"MaxBot (2022.01.10)" CONST_APP_VERSION = u"MaxBot (2022.01.12)"
CONST_FROM_TOP_TO_BOTTOM = u"from top to bottom" CONST_FROM_TOP_TO_BOTTOM = u"from top to bottom"
CONST_FROM_BOTTOM_TO_TOP = u"from bottom to top" CONST_FROM_BOTTOM_TO_TOP = u"from bottom to top"