From 6536f5eb0cdb687d2cb245ac41188642927b812c Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Sun, 29 Sep 2024 08:57:38 -0400 Subject: [PATCH 1/4] Refactor: Add support for different HTTP methods in update_out_api --- api_output.py | 84 +++++++++++++++++++++++++++++++++++++++--------- mainwindow.py | 8 +++++ mainwindow.ui | 30 +++++++++++------ ui_mainwindow.py | 19 ++++++----- 4 files changed, 108 insertions(+), 33 deletions(-) diff --git a/api_output.py b/api_output.py index abc2fe4..7ac0370 100644 --- a/api_output.py +++ b/api_output.py @@ -13,6 +13,7 @@ out_api_url = fetch_data("scoresight.json", "out_api_url", None) out_api_encoding = fetch_data("scoresight.json", "out_api_encoding", "JSON") +out_api_method = fetch_data("scoresight.json", "out_api_method", "POST") def is_valid_url_urllib(url): @@ -33,13 +34,21 @@ def setup_out_api_encoding(encoding): out_api_encoding = encoding +def setup_out_api_method(method): + global out_api_method + out_api_method = method + + subscribe_to_data("scoresight.json", "out_api_url", setup_out_api_url) subscribe_to_data("scoresight.json", "out_api_encoding", setup_out_api_encoding) +subscribe_to_data("scoresight.json", "out_api_method", setup_out_api_method) def update_out_api(data: list[TextDetectionTargetWithResult]): - if out_api_url is None or out_api_encoding is None: - logger.error(f"Output API not set up: {out_api_url}, {out_api_encoding}") + if out_api_url is None or out_api_encoding is None or out_api_method is None: + logger.error( + f"Output API not set up: {out_api_url}, {out_api_encoding}, {out_api_method}" + ) return # validate the URL @@ -51,14 +60,20 @@ def update_out_api(data: list[TextDetectionTargetWithResult]): def send_data(): try: - if out_api_encoding == "JSON": - response = send_json(data) - elif out_api_encoding == "XML": - response = send_xml(data) - elif out_api_encoding == "CSV": - response = send_csv(data) + if out_api_method == "GET": + response = send_get(data) else: - logger.error("Invalid encoding: %s", out_api_encoding) + if out_api_encoding == "JSON": + response = send_json(data) + elif out_api_encoding == "XML": + response = send_xml(data) + elif out_api_encoding == "CSV": + response = send_csv(data) + else: + logger.error("Invalid encoding: %s", out_api_encoding) + return + + if response is None: return if response.status_code != 200: @@ -72,13 +87,38 @@ def send_data(): thread.start() +def send_get(data: list[TextDetectionTargetWithResult]): + out_api_url_copy = out_api_url + # add the data to the URL as query parameters + # check if the URL already has query parameters + if "?" in out_api_url_copy: + out_api_url_copy += "&" + else: + out_api_url_copy += "?" + for i, result in enumerate(data): + out_api_url_copy += f"{urlencode(result.name)}={urlencode(result.result)}&" + response = requests.get(out_api_url_copy) + return response + + def send_json(data: list[TextDetectionTargetWithResult]): headers = {"Content-Type": "application/json"} - response = requests.post( - out_api_url, - headers=headers, - data=json.dumps([result.to_dict() for result in data]), - ) + json_data_dump = json.dumps([result.to_dict() for result in data]) + if out_api_method == "POST": + response = requests.post( + out_api_url, + headers=headers, + data=json_data_dump, + ) + elif out_api_method == "PUT": + response = requests.put( + out_api_url, + headers=headers, + data=json_data_dump, + ) + else: + logger.error(f"Invalid method: {out_api_method}") + return None return response @@ -95,7 +135,13 @@ def send_xml(data: list[TextDetectionTargetWithResult]): resultEl.set("width", str(targetWithResult.width())) resultEl.set("height", str(targetWithResult.height())) xml_data = ET.tostring(root, encoding="utf-8") - response = requests.post(out_api_url, headers=headers, data=xml_data) + if out_api_method == "POST": + response = requests.post(out_api_url, headers=headers, data=xml_data) + elif out_api_method == "PUT": + response = requests.put(out_api_url, headers=headers, data=xml_data) + else: + logger.error(f"Invalid method: {out_api_method}") + return None return response @@ -116,5 +162,11 @@ def send_csv(data: list[TextDetectionTargetWithResult]): result.height(), ] ) - response = requests.post(out_api_url, headers=headers, data=output.getvalue()) + if out_api_method == "POST": + response = requests.post(out_api_url, headers=headers, data=output.getvalue()) + elif out_api_method == "PUT": + response = requests.put(out_api_url, headers=headers, data=output.getvalue()) + else: + logger.error(f"Invalid method: {out_api_method}") + return None return response diff --git a/mainwindow.py b/mainwindow.py index 89092bc..a0691db 100644 --- a/mainwindow.py +++ b/mainwindow.py @@ -227,6 +227,14 @@ def __init__(self, translator: QTranslator, parent: QObject): fetch_data("scoresight.json", "out_api_encoding", "JSON") ) ) + self.ui.comboBox_outApiMethod.currentTextChanged.connect( + partial(self.globalSettingsChanged, "out_api_method") + ) + self.ui.comboBox_outApiMethod.setCurrentIndex( + self.ui.comboBox_outApiMethod.findText( + fetch_data("scoresight.json", "out_api_method", "POST") + ) + ) self.obs_websocket_client = None diff --git a/mainwindow.ui b/mainwindow.ui index 647e46c..1c66a64 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -1511,16 +1511,28 @@ - - - false - - - Not implemented yet. - - - Websocket + + + + 50 + 16777215 + + + + POST + + + + + PUT + + + + + GET + + diff --git a/ui_mainwindow.py b/ui_mainwindow.py index ae9f8c8..af17fbe 100644 --- a/ui_mainwindow.py +++ b/ui_mainwindow.py @@ -793,11 +793,14 @@ def setupUi(self, MainWindow): self.horizontalLayout_27.addWidget(self.lineEdit_api_url) - self.checkBox_is_websocket = QCheckBox(self.widget_24) - self.checkBox_is_websocket.setObjectName(u"checkBox_is_websocket") - self.checkBox_is_websocket.setEnabled(False) + self.comboBox_outApiMethod = QComboBox(self.widget_24) + self.comboBox_outApiMethod.addItem("") + self.comboBox_outApiMethod.addItem("") + self.comboBox_outApiMethod.addItem("") + self.comboBox_outApiMethod.setObjectName(u"comboBox_outApiMethod") + self.comboBox_outApiMethod.setMaximumSize(QSize(50, 16777215)) - self.horizontalLayout_27.addWidget(self.checkBox_is_websocket) + self.horizontalLayout_27.addWidget(self.comboBox_outApiMethod) self.formLayout_3.setWidget(1, QFormLayout.FieldRole, self.widget_24) @@ -1228,10 +1231,10 @@ def retranslateUi(self, MainWindow): self.comboBox_api_encode.setItemText(2, QCoreApplication.translate("MainWindow", u"CSV", None)) self.lineEdit_api_url.setPlaceholderText(QCoreApplication.translate("MainWindow", u"http://", None)) -#if QT_CONFIG(tooltip) - self.checkBox_is_websocket.setToolTip(QCoreApplication.translate("MainWindow", u"Not implemented yet.", None)) -#endif // QT_CONFIG(tooltip) - self.checkBox_is_websocket.setText(QCoreApplication.translate("MainWindow", u"Websocket", None)) + self.comboBox_outApiMethod.setItemText(0, QCoreApplication.translate("MainWindow", u"POST", None)) + self.comboBox_outApiMethod.setItemText(1, QCoreApplication.translate("MainWindow", u"PUT", None)) + self.comboBox_outApiMethod.setItemText(2, QCoreApplication.translate("MainWindow", u"GET", None)) + self.label_20.setText(QCoreApplication.translate("MainWindow", u"URL", None)) self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_api), QCoreApplication.translate("MainWindow", u"API", None)) self.pushButton_stopUpdates.setText(QCoreApplication.translate("MainWindow", u"Stop Updates", None)) From 9b1af82deaa53f1bfcbedf6c4cc34cf206ad58b9 Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Sun, 29 Sep 2024 09:11:20 -0400 Subject: [PATCH 2/4] Refactor: Update logging setup and error handling --- sc_logging.py | 125 ++++++++++++++++++++++++++++---------------------- 1 file changed, 69 insertions(+), 56 deletions(-) diff --git a/sc_logging.py b/sc_logging.py index 09930c0..532267b 100644 --- a/sc_logging.py +++ b/sc_logging.py @@ -4,59 +4,72 @@ from datetime import datetime from dotenv import load_dotenv -# Load the environment variables from the .env file -load_dotenv(os.path.abspath(os.path.join(os.path.dirname(__file__), ".env"))) - -# Create a logger -logger = logging.getLogger(__name__) -logger.setLevel(logging.DEBUG) - -# get the user data directory -data_dir = user_log_dir("scoresight") -if not os.path.exists(data_dir): - os.makedirs(data_dir) - -current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") - -# basic config - send all logs to a file -logging.basicConfig( - filename=os.path.join(data_dir, f"scoresight_std_{current_time}.log"), - level=logging.INFO, -) - -# prepend the user data directory -log_file_path = os.path.join(data_dir, f"scoresight_{current_time}.log") - -# Create a file handler -file_handler = logging.FileHandler(log_file_path) -file_handler.setLevel(logging.DEBUG) - -# Create a formatter -formatter = logging.Formatter("%(asctime)s - %(levelname)s - %(module)s - %(message)s") -file_handler.setFormatter(formatter) - -# Add the file handler to the logger -logger.addHandler(file_handler) - -# if the .env file has a debug flag, set the logger to output to console -if os.getenv("SCORESIGHT_DEBUG"): - console_handler = logging.StreamHandler() - console_handler.setLevel(logging.DEBUG) - console_handler.setFormatter(formatter) - logger.addHandler(console_handler) - logger.debug("Debug mode enabled") - -# check to see if there are more log files, and only keep the most recent 10 -log_files = [ - f - for f in os.listdir(data_dir) - if f.startswith("scoresight_") and f.endswith(".log") -] -# sort log files by date -log_files.sort() -if len(log_files) > 10: - for f in log_files[:-10]: - try: - os.remove(os.path.join(data_dir, f)) - except PermissionError as e: - logger.error(f"Failed to remove log file: {f}") + +def setup_logging(): + # Load the environment variables from the .env file + load_dotenv(os.path.abspath(os.path.join(os.path.dirname(__file__), ".env"))) + + # Create a logger + logger = logging.getLogger(__name__) + logger.setLevel(logging.DEBUG) + + # get the user data directory + data_dir = user_log_dir("scoresight") + if not os.path.exists(data_dir): + os.makedirs(data_dir) + + current_time = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") + + # basic config - send all logs to a file + logging.basicConfig( + filename=os.path.join(data_dir, f"scoresight_std_{current_time}.log"), + level=logging.INFO, + ) + + # prepend the user data directory + log_file_path = os.path.join(data_dir, f"scoresight_{current_time}.log") + + # Create a file handler + file_handler = logging.FileHandler(log_file_path) + file_handler.setLevel(logging.DEBUG) + + # Create a formatter + formatter = logging.Formatter( + "%(asctime)s - %(levelname)s - %(module)s - %(message)s" + ) + file_handler.setFormatter(formatter) + + # Add the file handler to the logger + logger.addHandler(file_handler) + + # if the .env file has a debug flag, set the logger to output to console + if os.getenv("SCORESIGHT_DEBUG"): + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + logger.debug("Debug mode enabled") + + # check to see if there are more log files, and only keep the most recent 10 + log_files = [ + f + for f in os.listdir(data_dir) + if f.startswith("scoresight_") and f.endswith(".log") + ] + # sort log files by date + log_files.sort() + if len(log_files) > 10: + for f in log_files[:-10]: + try: + os.remove(os.path.join(data_dir, f)) + except PermissionError as e: + logger.error(f"Failed to remove log file: {f}") + + return logger, file_handler, log_file_path + + +try: + # Create a logger + logger, file_handler, log_file_path = setup_logging() +except Exception as e: + print(f"Error setting up logging: {e}") From df81ecaaa02c9ef27abdeadbd27127da8c3ea525 Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Sun, 29 Sep 2024 09:18:16 -0400 Subject: [PATCH 3/4] Refactor: Add support for different HTTP methods in out_api.md --- docs/image-29.png | Bin 0 -> 14404 bytes docs/image-30.png | Bin 0 -> 16504 bytes docs/out_api.md | 52 ++++++++++++++++++++++++++++++++-------------- 3 files changed, 36 insertions(+), 16 deletions(-) create mode 100644 docs/image-29.png create mode 100644 docs/image-30.png diff --git a/docs/image-29.png b/docs/image-29.png new file mode 100644 index 0000000000000000000000000000000000000000..79fbe0672d429ce21a7b1c282cb1412b1eeecde2 GIT binary patch literal 14404 zcmeIZXHXQu+wMJN5CIhh$sh_UNx%ik0wN$uML}RmlEZ?M^P&VrP(<>QBnT3hoM91A zvSgODEJ)5dZaTyNskh#$x6V`N)O)_34_i~SGt=AKclUJvuIuiIr@HD47uYWV0KlNB z@#r}KkOu<*nF=j6_>Co%GZ}awbAPU`0+jsaTm?@k?UZ$t0iZ0J9&bqnp3}Y2FmeX~ z#RSYXFd*(0ru)!pCeAqh$|u2qKY?!dNj3U-G@!P|Y8Ie5iF^Q?&}0)A3tN z?+V{cqEvT+bXu@KqGxxVj3lWvi8F9he$HeNDSy$(9&t~1QY5Y`&?~at%6n5tK+?iFzL3(-;#WvAi9hdg5$~*ivq=h>&n;9+n{?8SCuv0qa&8;_iMo7O7X;~+bHk;A=;docwvBkJAzqo zBTSkNIP{L@Rw(eew@RaM+_?LPBR}vZdc97wEn_U;W!hNint1WD)fZD|^|@*u1>UOM zk8;xltsaA(2Y!1$&I?C85zBgJTw$O|+N8OS~@p$I~8u-i^*b=ypX5-+UBNziQ&*=0rpgxDPrAnqlTae4(F%y5%}4ON3oUsINl5=Q1SstF$Y+1+-M{K-Ha5d^*gvkt*KQ*gvdgVy zeGq8WGyWwd#i=!Sn#t`;V%2eMp0$TZWfjykw#=8Avf?@Kz1gFiz>HsmK$Wpm@=2}b znAF02#K~D1(-?;NtSbXw->h+8F(IPUUklaYd1|4JZW5`%voAOW$X}lNvK?rUs3ml* zFmT)s%4-s%W!}sxG-V7^QB2S(ADXKbYSybAsBl}=@)g_jF`cf<32-57Na7BsqbGTQ zY-U$YOaNiSZnZ+2P#l5(im@g?lUv3gzg_2vnt+{n+2~sEyID30q!b!1EFn6_dsB^u ze=Hg$8&BWYOYbkDqog)kLaOA%9gTbFE%)up{5!2JLpwEY`Jx$3cJE)^ zRjy6pXRupV6z1OBKZ=OV2)MnxAjwP|k~U#~{e;cJh0q&tV;-#1NcZRNb`vS0rUVJX zZmT*f*42Edxgmo7c$%|)56VoJ`mONpeQgbU4wVyH^8a(#w>zj@Lmr?&J@U(`7L)=B4Ko`7D9 zCAHstXcUWxI*tRj>3rF3UVH+Q*4CNV^+wj*;|h#u2{s42ME~L+PmP)9%#h{;i+Y}& zm^J!jeaN@1^%rSGx2+0Xf6A11{wPyH-A!ETk2Bq>TDQ*eTs92Vr+Bq`9%wy00Uia< z=nKrWKO6_%_I+bGJrPVRm>F9PiFVfRVv@O5-Mp92aK)Fegy2O?rvJJ;)1@SxWcAPl z&XG~l$r|+X#duAd==d~RA7?#|>IvXRTumgoL@p`C0>599o2@jn`7Bhz60pwGo=OzA z#COP^ibV+}-W*ceQ8+CsAw;Oq4lLcO_#2Suc-Qavo1Ep}Al~(Lp3YY}(O+Q&<;WV= zz@urpgEsfcs6}i?d&kg&V@umwog+`giI37uv-#EH)XoA-X|o;q^ar-UL}v}$(`M#% zC6=>PHPNT*LVNiv1n*{!DRl%Ky>Z7?=Vs9S%vV+k<}Oai`JXW>*ZNg`OCpq!WXMs6%i8`r+wub0HZ`L zna!51fRp5agLk#YBaD?Yn2q|?rw%DGeI}KAd6C2h=dLOW z%_~T%xL*%x9yU>o($Cv%e^%4IU$X#X&~nqAaY&Evl~vGJ!?lDqV94Y(P4VrNq4kV@ z5uthvKlA%%5drKxq?;_C#*};-8U<&x*-I4T++_y@@)&lDy+ykcy9P?C{9I@>FJ2!? zA>UN86%D87d-`E^C!yVKX2p9^&epZu^rx;mM@FE&6)jI#cPpIHJtok#ciQ8m%55Fy zJs)Evx1Z3+%n<)$z2!(j238#zaH_VxUtK{AyV?-gg%vdZ)joaCW~?M39;;sAsG+)Y z=5Y%ym*W1h(4z0cS1y-Qw`o5U*OKhap}0u7vTkPwJr#ZlX=Vo^gR|rt@4!=!`otfr zh$7Rpuj>m+alHUd%bN~dS&p$hKF!T}WU4sh(HU!U?8y7Nsm*20?lIXj+xn#;EoO}e zm*H`YZ=3mj$|hsnDj=`>HdiRh#zxMt9iu;pHuZ2LoIsh!EIs7rAVLwx{#kF4KhXK0 zm5m(RB9VtetI`YFB?v{skBzqWf(FM53l`iSTQZD-e*+Wx7T&St05WG_yL0fgd@0PKo(ZFw>WbY(aVZ}s$ zzayr;IKGuND$$9&;)QSeK_dc+@ihj$VM@#Hxp_^r{5Mc>Vs70Qn9ZBT1B*@>#*n}* zNTtDILeox=dNT*{ri=h6iBk}fH(C68pDVfBT6x+Db6 z8;`N}$KO+_;g>$TE@_G;2j=!tS&pUVuO0qU(){ft*P2zuO(aW5QEW5AA|PLFwtO&; zbLay24#_yanOsJ@-2b`lI_9Re;o&s*&(T#PptFLk_!>)_eb*ROjgL_!^>sB?82jI+ zXM;r%DG9pHu?;~RQX`_5%1b`tQwZE0u>pKa-|cteFi2Z zd{G@mU&| zqj}ibN%-&Ik!_s9%ghW5|4fdDK|60^g9II!Hjlg^fk&N1~v{g zkpm0UNW3vi+T)Ds1OBKN%9XkGiN83SvFY3#te9r}gnE_r$Mh zIAq&LQ#tlpv<%t1D{QmuEpM(kOCn!BTEF?D>izDQo@ZM(nm!-`yZq!MSh&@UR7|B_ z)-i7T{^GZrj4CDQp`65u22qRedu2`s8;rGHes?u17k&*9yY{ca%$m#fnF0f^#wIiC zB4L_YC6WIdO;TaL6z-p369ijt{K(xoV8!KALx7)_{mb3hfDg(7JPGA=t$l<qsQLEfh~C@_@MsqZarVbE*;1Y)P(^_hykA$Gv<*+4JVof zO8VQ`r_@mECyA}aIWe=BvAXqKRZ2FmWm$Tpjnd`ps;&ODuC&d`>^Hpltd&`AucKc1 zjlK5GGsOuz3g4{O5#b36@k;sWn;W}#(qua)1a+r~6-FIjGTz%M-LLqNLWg1Xzwm}g zJ=w_~Zx%I?oB8sG=WCIxM>K<-(^0kfL^Pfw)OL1jnUb>Gx&;Ry=pBm>qg&PGM&Y+D zAEnflmaJ&rW&0FhXU||fNbkDx%0<^=tLhu;H`DLi%7Q{J_Oww!CD(k{hda9_LtpOy z;zuztUp~Hf=F^fx{VPeLav$&MyUVCkw!3NFicQ?soVkg$Ecko?T*MYR=TP6dDnc*E z%cpHH<5$E;KFH>yW#$a4BgQOMNJkf!hVK2WVmSVNM5iFQ^4aoi+`({ZarnZU{FIpY zP?+g*zSUms>62ly@(K0bHa7;1sai!#8b9r;Nu{p7V+ErjD>$-NRV{kacQ;Kla?9Tq)Ju0xu=~DvwzsI|d$N;h z9-GeKZVkT>mY*>)c#ur3!|`byUKTjv^UbUxZu~QE=8a9x+GvW*$g*%sj$YCN` z#TEuKRlT@xY={}>8dn?t8Dth&G&3{yl7%D8Ztsy>BpqKq^(aA=rhS_d#}}cP+{v?~ z>lr*VwErIFDXn{rodVg*xp#;9kyFYwH10QEH(b!8EKXN~yMzR9h5C_Yo?5{N=o7~* zLL(>6*$6%RD}?4qXwgiRuXO!wVeEBviYH0qCed~x$4`$mhOJa<(p);EXMFe#rt&)4 z(B^QFb6D$8D?hsMd!pvaZvAC^&D-(g)-w8=tG*hnP09oFxtPQHH^4~9 z{&<WMn1l0oHf)Tt(-E?_$nx$T6MQ05xK|i_J>g5+jqqYpiXC#rw#p1Ny zPT*bFIgOR`u6a(n<(We++Kv`^UkuJ%(hK29(**&HI>TTj?1|i_)YpyHAt@l7$b#Ud zgM&{kX7!<-q=aTqQn%de)}Y`8zv@;?39Y(qz;9^@Z+YWgC zRM$wRWS$qqHef^SzcBv)gL2@NYpVZkulrv-l9UPdUzgKuecg2VJ8f-kZX{wLiaO+< z3HB@@fP}*)y25c&ZfhfVWCBh~Njm-}CMJ8}066V)4dO?gjUpEkQTQK?KD(_X7O;ZS z>R&ixvel#i>~WHnT2})7X>x)fvL}EtLr>cWro}>Jb2*mv2XgnbYd~AVW~`vz!m<*n zE@1aJE3pOuER^_wgy|I5<~vHn-Dp^yeIEve??H8vh})z|<0cn}&vQJaU7j9u{BQGd zG~u^ckR>aKyC`d_mO0m;{S;CLNl{I;^&JJx!bF8#5W~?YmKk%>p?S~le8*y*5+NVI zuSBYVb#IP>V{QRXT!vc*2AT$uj^XCS1wCSWb2{}Cusk@Yyw8B>pnM1K*Ka4_6YeJZ z$4sOhrs^Z4c_rP+HgRiv;k-%oGA7O|5!;ZP zJWc#39dNul8$0_q9{em#kO%;bBWZ(geWy?yK@p}0>eLxB?tiG2&wu?g zpf!6Ow0*pRCY>Qk<>ub+skMqNM<)LeMlZ_Wp|gM*ar?z6peZWko{BgVbzWx_B*xUmzwoWK`30375@6kf(*Wb} z^QZK8f4VH4t*hCaF~X4*my=Dze_1w*M(Sr*XS{Z=t6$;?xy{;Zkb`!X_YKmvcGs73 zns>{bcXH0{UR;|@>P_L*2(x$f61bgMnc!!wYtV~Zdh9AD+*8Co%#5~aLz3020gw70 zFO_t8=&1e)h3ch;CD_^AsJdde1|gY>38?L3C;NoT>y2VpdoVQNixYQ@rmwQ4)~+of2lux%B_+L zFY*1cfAb;mQ<$9BE74WT@ldnJp7D?{?Hu{49B$dr?3&;FQDMyO$4s_SIbwMd^6nOV z89RkfB0h~>y+x`IlY1N88TOXB7QNM3e(C^%v-8pQe^OfzymwQCREQLgT|h})+9u|7 zJj!;^rP}*Nf&4yVmVk{CC2lwL*WDbh?(TtPtHZY!l$27oRm$#fIu89=O6k{95UKj? z=j5kz$ekQ5Qo(J=4h`uyYcIv8PH zjmIvb`u9_fU*#g@$kXylJy*pUd6z%M{X&K{u-5g{<5+j1227|TXU2u+3w<01*(C*& z=4=AK_jjrN$3fioBjiUzS zWG~W}$GT1-^SSr8>NEUpyWP8PWnFJzu(HYQ`^%0PVoeH)Hz~=&r8eCLn_%yO$rcgK zffnrTFFBo#dkWI@a>E3^cd&$4^kZMNT6KJouEH0$ znSHhj?1^@=L=eohr0W{V8;>1BKS_TeAlBoNUB%^@EWu;%9K^v_YCpk(K_j$FC8Axq zl}RKn9HMHMo^7hblk~MJWpztRT(n3;N7{JGG|@U^Ly?R1wlq1fpPkC*bZFmbEA)}< z^tCliZaSt|PabV3j&5cuq8SAn(~GL!?o_=(^jL3U~B=AxNw(42EKDyw(h{`(-k z@rLIHm7boTK>Icvfux@enbw)*qQ?FbjTSGmZS%1azyjw66Q?Hm@vvs z8%};9{ZOFJ} zB?y_Jw96K`2CZ&N!PG9&4X@=sjN*TSwZa4YC$3yNA)G2} zRm-d?^z}q7h2PKX@wCN4W2ANBWaAe(nJq={W7q1L0Jo_ydJ{8XK;Qv3hb5sIf~jO9 zaMe@ZOeiA04-~3_QXR{{WDlh8^=(e~U2|>dcjcGUkP&^sdX>%kifK8MlDocLij&jx z^{iFt0?31#^(TQ+`F)=ar4b=K30VXm(_!2G3yCoA<(iG}3D4;5C_Kj)458kE{@$y# zEf4vDtnEsp2Nm$YOIg}#Z5prH4qR>pa>x(H52UhekIz6ix?~1xT#$=Rh7H!eY<02q z6U%r8?IJBz))3)p0-oDL@|wO=>Ep3)MRO@}VV9_Tmsb`MJRIY(P_NhI^D#1YwnH{2 zseuX^SMN+IreXh?%00IJTfOd5)9=#i!p(a96=lm6AJ9-C_|3NbWkC-Nd?m0G%}CbL zl85SMvEK}RzL}C_VP_|crp``Au&05Dn>8|L63D<_X^L$fYPbai5ZpK9t9lO>*^O?9Du_4$PQs0*9Ra`P@!=j0~>;IpqW*r%LLxJOnKcT$O^moophj z%n)ntKw=K0ywqGJR&%UV#htH#Gubw?n4zvj0qc&n`WX>$uLt=rqz!Ijl#v1~p$ky@R{59@6qatk{AF`QIGIQsxU`EazZI}m)j=?XXDR`9c?u;ZJb%)9O6BE7j> zaVOI7hIes~%Ov$CYaIJ6XVC{u+KwE~P}D!iJ4^H1zjXtjVSAf3Y$6&vd}0&-7DL3W zCY`UiayW!1t^5|(u8a9GZd%VOyDj*bBoKDFqN$f|+_kt|!GT%G^nf86n{Fg=ajKu9dkNRp5@+oD|fGRm~*5o&a?3``ZU+P0x7Nj zQ>rGL>Jd(c$$D3A&*?bi^^duj*@8ll8#(^u*6L) z3zCe|D+=j3N_GqC3|N>P$ihL^f0dq1U3!yvXigesnKmabns*u4nGGse(7&;}*; z#C)CudlD{U!VsrKI_+PK6l>l=QBjlWv;YHQMmjB&E#J<@YMO0Q-g_==fvoQQ+3d1x zi>mq=aY-5#V@_Bz-0LwXJ~wOhnS;|`yZ{=-2lW5t2wtZI?Lpu_&hhB~+CBW=K~6R> z4jTz1#YK?gCFJFWkGHYMPNj*J!eGPMq6$DCx&Z(Nm{e6&y`G7gHGTz^m^Wz?G+#Jn zZm36de_oU!x6lMZ?CH60%hU^c-Cg_3{fmd|=yOs$=l~(l*(Suy`#^RdjAzl@P{{)0KX%x^TCPM1=u#r}X&0D&CkXp?>VWVahG4&CHIx$2;xf#xz~Gfu9(;6dB)EbV0=3DS z(x}O!w%tS{`F!(is2;A_kpV5$_8J%G^qi_`;in!P4m@L} z&O_f)Ek0Qz?GNcX5~XPYanLC7@Q^C`z^Oc{w>YVzsB`+GdFJCKYa7#ylrmv!)a1f@ zVlx3-fOrCn?V@Uc7qxrkG%a`Ymcdz9`fBw%c@ELx>D($t=HxPmAej%{wm03@$IGNpGCmXy$T6prmDQMWztN%lXiP zStyS9LZ|v5of5=$VJ=>$g#&G)qE#v-k|+~>oPlEQY5smD*NCYP{sR5Fi;#KYY9{^R zZGxtZQm!~py)gbeBQm-9o?FYp_h)J;ua8_L3t_ytA+}%SR5lMPM>1PZqEUSUdjHPkIhGC@V=lg1lK)Oq5#D#e zth17`i1=y=KOk$Iy2NmDIQ(1m@k(oQeNt~cY@u?5P}5+!w^!*a=Hw>0tigRwyA^^( zGk?E)SjqOnsePl1lw_~$?UM0xF-vc9d{QAWjQ$59i>2Ssn?2q##H`umjqzg=jL710H zlB}jU5BCQ{rpzOyIJ`0aw?mbAZ%61>fnfgYB~!QO9@b*lQasSDS?Kc%@*c6}&r^j< z?dGw&^mE_lxP_d&hj8tGU`7z6f|p`8%bQ^|itvUl=SXF*|zzw48RPbtbw!_94FfkHra$;S}alcO>|GAZD)ThX~yE9_8JlUja2pc)w8Z4a? z80|VKD4O6+x%?^MRn(!RWY0*JK$OLEQ9WsAZ2Xdwm;2s;?NIyrdUbT7=148n#QfV? z-`ITB&&mwwTK~jx#ug{o7Pok|-~W65ucA`Xse#a1(p@SnUCHW|r8x${x^%zJ8F;Ka zr6D*WBGWwfJ!rDAC&8nX^4A-nTI;H|R+d)|5}5eQ65dxR4P&G;P39T=3g{iGuQ zb?$zbhTi=yME)X2iFU47o~lE!EroNvUfQX9nQgzO(E!3YkQCw3EYG20b@L!xZub$h zGrppFp5paulkq(Wyf-ZsBcYU=)-?ZXH>#Lr<+}kZ$0^&4%{1gM`#A)l@9#+A{8%0?wlkPUfZ}jH#~1jK-~{^tS%4(koZr5bfbg zd~ty*HO=uD${Gx*Gn#FwK*r<7qmHk&+HG~tA3^oikO9fo-Ru`*pQ*BJRl3iR%5g2l zxp7?o9+RPM-ga{})N6~3NjmbrRQ>dl@{w$4M{eAJ?F!p|W=+K=*+Cd}-c!r$NF}C? z7f&z5)0M>BD2&gh*;KIrGms`1f$^>PV@-=j(y0FNfQ$lvp==CQwJ3o@I}IJj`u^EF z)M%NPUWys)(4y$iofPD?N#~@WaUv5nIf1d(2|ng>a+4$N!~croGMUm$dVPO)j_bk{ zRqwYqv`j4#aF-AIjN3HK5zT$Y4!U2@WX4MVIN=T+r4_srzHv5j*j@yw0mUUpml`$) zZJkc83Jlps*@Aqq5k{>(wJW>R*U037Gn#up&|E^rfL8eJi9Q9C;#Cd{?+DPI!dwRT zD8f`gpu0iVTxv92H*ew+Y6ZH;aar4fNvR)4AnI4Y3tqwDee(sK>S~JnaK{uv#!!+j z&O0U3QBF{g`6KLof5|Tf$X7`s)7572k92*1EhC^7lT^7&9lM<1V#gr64=-;gyH{I`KJ+WzE(2SZ52eO=zMv*8WDzJ zi1huVEhf#{OkH6})b*B=Z6e*fox3U3dv_U}m$YjucoK_Pk_ObvWNzo{=s#Uy9H?s~ zG`Ho3Rl;_CIl&foxhWB;J!Y zVY)TiIPgQ(zdr5Gk+;XHA=P4_`BCL$q`#F=bx;VO1q5WWCLK z$2_u~d_g4xzgYJra{Fq_2>sYndw6ZTqNGI4>~YmSzX!56Fri)LMC!-fF%e*WzRwu0S!-40P=`~y-ab?x>Fs?q8O5n2Rn>bq z&6&|Qe7vsHY`)*@@la6vPd!YwFzrwW3=7H~C&P67s9s=%|zz${@VgvNKK`$aP zt=;s%lUQ}2JaU0D;+ZjDWcFw)xop6suOX-G7;ss*5Mo9dE$z9UPW)LU^FY?ngGR%a@?|=y1j5 zzo!dUg!$+DLIoNpny{LvW2B|@DSKHtqgywhJBk)ANJxG9M}!S$tiMNClCNHOi=y3D zvVv0%xSi}+%6M(Oa$$M!W;Qrg*7FHyAc}(gv+FddIqq`4!0(9W!Bxr4gC?rmIaje= z0kpnnkiDt7dcnSK`nS1yo2_|g;K1NuHMEw@Lf!L1@ZE(l&^hb|y&Al6r91xQt&GRY zS1>75!>l=A6}=Tz1UzUNZuwqtM%*Tn170tFo(_MnCFIT$^i-?Dl^BBLxQKg~0P#1| z+FLM3Mi|e~0ZQ&7u!6erPS=T$t{CX(L{=rW1G5oIOd_E&q*yvP`fIA{s>tRZ; zr{Ey*noU`gfCbR#o${% z$ewz$Y??e{0Zq5tN?bp`8$AqpGU)VGQN8Ukt(^U!Q=P%mhF2IP$O)~OL<6Lf|4}cJ z3b3$C1BrxMrnq=$umCf3@2A$NgUC-&e@Id+@anlA%@3N)1S-BuK<_*ov@5%+yK8aWLz=n-f`O?u^yHb@iJF9?6KTVh&47mWLu5Sb8daOb=+!8?gn%L2S1R=s4t*KnD6mIeAp*c{vL=`FWMk1u*g9x76QvuXfmbSs z1qh|%PIeb75bQRn7!3B>Qk~ZN4JuHAS`B8}RC?IQ1Dd!82dnzHmnk72M1dw}_bXP0 zV9>Wst)$XJE>7mbL5Qlc0*$aSw2h5RJ96B2@}SWEj0*UUKXSZKV@fIn6KTs`RP>L^ zd1HxhJ6XsrOcaxHYqme-lsnE?u?A>ri?+l6LTX6`+@&}N++1Xt- z1_=cIfk<#oQ(Aprv@m~D?woThXBOTodKt4DGDoKw-@mmWf{lR?WWAaLOF8Oz=OO!^ zn~XNJKud5ooey?kM_SCv$x!EG%GY&{AkX1&XvW8)4CvQW@mRF>Ns5j@_2+eoL1>+@ zHOf^(&oUtXDxEPB*kQHcXKA>NmFzb z(VHOQYm}=)G;XL@)mh=Fu^GM9vSDk1#W&RXge;i@4^P9Kd^(8Rbc)E0rP%>9h5!bO zz(178eeZvSVaMU}xXUf&xJpycz;VR1Mc9wka41aikFX)y+F7x%v~B*@nCn|dPiR8# zlj7qigIp~NnWra(PLO0XM;;0PQ5~3B>0AF%c~z`_j4}s@+tjV|v@P|L8)_7nGMA@ZtAYnTTU6onYHGyJH8F zgo(~iU17J9OICT~9>FH|iWuGrQ2kMv?3{*>PJB@HpVyfMMoxLM4oZGC#f&qS%CA#f zM)i68l-V%04}x_zxg1Gj>NaVv$NDz)tUk?NCC@e>7hnq2tScIgPAOC&QUDtq7%-_i zOsPtpY+y9J3Zahq_R^-l&d=mqirc(d8y+QaD7j_FpzJOw-~kiu{#*_F8tXPAuRZ2s zd-2;n{71Aw=kDBH!D-=!!OP;AQqJOsj{`)EC2Eg)Y*p)j0v`7d!UumD7Zxmjt%+TR zQ0S#gUzTV{1&2=64ZhOQIlawiKyNy=fKwY}@`cl=-iMo>kJqZNY?$FLU1@#dVzO%L z(5|BU0o^oJTJ>6F(`kO_m*=IG6$=cpIUnl1*IS1#PkAQ@%gQ}v1xod*=-=I9fIH5n z1kV;YWOz@4XPYMdW@zyJib8ilq^C#vtUmw7G~GYb6f33=d-sacxO1hN;9= zX?TX}A{NQcRe8Pa&3GXj`mgNKD{26+5%)OSm=GZac6Y@Jo^KJ^!lJg(&KcBEx6-6V z)!n(3cseCW#gkH8Vijge>u+TpDyh>F4xn%m_H+;8LsGV-`Th}=(Jr%M-WDa^KguMw zFnbDzmSyC8z?-L=_U&BI1LaM^y6WCk-|E*Do0?&M)= z4vdTE^pH~x@059QUlXFGsPyT<7w_z_B7E{wUa@aO7*HF%nie+mazGG#wg1X~=ZZ95 zqXp*=;9A@Ld*I<*tpi>#{Ff^GUl;y=C-?mKJAwXBXZ!zKvHz2#tN$Apa3%LY+o1Np z*DZ5l!1Uzg9+nJT?V;>B0CQ9j48{H6p&B1pEE@w86K>mhQlKGG%zP60fsw1+9K7)< zH)u4tf!o>6H@}(Bdt3}QruPAtp^jwW#V1QEgV|D)ig4mI^4Sd%Q{}6Tz%{$OufX95 OXsYQxDp9d`|33iyJg)x$ literal 0 HcmV?d00001 diff --git a/docs/image-30.png b/docs/image-30.png new file mode 100644 index 0000000000000000000000000000000000000000..3296c814e8f606e44b1e24bc25bd4926a5cf4b3b GIT binary patch literal 16504 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 z ztMI){uppdk@&VEE3@B$Xfei$hpN@2?>eHpTyUJU2$?fAU&JVudS7x;Ut0uqHaqzHz z!#dlayT8vWl~f?~X;gv}RenPhYSIlP+EHKy2)ApoT2KRkx$;qz-863#@qYFvQJTsR zK!lcHK2npgAT^;U-*1RweJ{qZgWjhBiy%(wA*0ZXd_iuus@#$wlu5Y~C-RifM%XB- zO7sE`taX+f@X}$KE$zvYiA_PipYZ*&O=7C-;bcnVs>L?q{URWJKE8?ul*bjX5=dqc z=NVz4`t{n%p7iV+{XKajzPC0~oAivxOF|p*2OtOyfT-8lb;(9~01B!$KzeopHEyXT_ss^ophxzcDEp3en|joWum07EPo zlrd%*&#FGf+VUlYy(bn4obHuyXl;fKL;ar3Us0TVzUOReHvi=~GTA^xbcvh-t9wcV ze3mPwZNWONrk1(u|M<0Cza0bdnDX=Pv}Exd0QpKx z|ECKDu&o33fCOIu(>|Bg0fsAIdkSFVub*5&rC45h(K~2OCw?z~rBMwomVqLL9cR`E zK(3rsgCe4V@}J7}c;SZ|gTR8UuLEY?&^Dg@c z4-XFhLmY5dMz1fNVVZu_Yp+kPN@*ekfX}{Q5=dI)wyv4+{~+1!|2o0bBAh=4c8Xu& zZEfN4b6*O+ye+18A3g;hE*c5mhioY=xj)=7&;PulK4AT*f80Bb$OG?I=?0M%f9k(44$R>XOHJrSI u0XO~MPj&yNoS6aeum5)!>h7Fg5*ft|wr%?Sx%!U+ASDI0SEX|A1O5-c;02KY literal 0 HcmV?d00001 diff --git a/docs/out_api.md b/docs/out_api.md index b6392dd..66524fe 100644 --- a/docs/out_api.md +++ b/docs/out_api.md @@ -1,4 +1,4 @@ -# ScoreSight Outboud API Integration Tutorial +# ScoreSight Outbound API Integration Tutorial ScoreSight now offers the ability to send OCR-extracted scoreboard data to external APIs. This tutorial will guide you through setting up the feature and provide a simple Python script to receive the data. @@ -6,13 +6,15 @@ ScoreSight now offers the ability to send OCR-extracted scoreboard data to exter 1. Open ScoreSight and navigate to the "API" tab in the bottom left corner. -![alt text](image-21.png) +![alt text](image-29.png) 2. Check the box labeled "Send out API requests to external services". 3. Enter the URL where you want to send the data in the provided box. 4. Select the encoding format for the data: JSON, XML, or CSV. -![alt text](image-22.png) +![alt text](image-30.png) + +5. Choose the HTTP method for the API request: POST, PUT, or GET. ## Troubleshooting @@ -35,13 +37,17 @@ If you encounter issues with the API integration, follow these steps to troubles 3. Encoding Format: - Verify that the encoding format selected in ScoreSight (JSON, XML, or CSV) matches the format your receiving script or API expects. -4. Server Availability and Authentication: +4. HTTP Method: + - Ensure that the selected HTTP method (POST, PUT, or GET) is supported by your receiving API. + - Verify that your API is configured to handle the chosen method correctly. + +5. Server Availability and Authentication: - If using the provided Python script, make sure it's running before attempting to send data from ScoreSight. - For external APIs, check if the service is up and accessible. - If your external API requires an API key or authentication, ensure these details are correctly included in the URL or headers. - If testing locally, check that your firewall isn't blocking the connection. -5. Test with a Simple Server: +6. Test with a Simple Server: - Use the provided Python script below as a test server to isolate whether the issue is with ScoreSight or the receiving end. If problems persist after trying these steps, consider reaching out to ScoreSight support for further assistance. @@ -56,16 +62,30 @@ import json class RequestHandler(BaseHTTPRequestHandler): def do_POST(self): - content_length = int(self.headers['Content-Length']) - post_data = self.rfile.read(content_length) - - print("Received data:") - try: - # Assuming JSON format, adjust if using XML or CSV - data = json.loads(post_data.decode('utf-8')) - print(json.dumps(data, indent=2)) - except json.JSONDecodeError: - print(post_data.decode('utf-8')) + self.handle_request() + + def do_PUT(self): + self.handle_request() + + def do_GET(self): + self.handle_request() + + def handle_request(self): + if self.command in ['POST', 'PUT']: + content_length = int(self.headers['Content-Length']) + post_data = self.rfile.read(content_length) + + print(f"Received {self.command} data:") + try: + # Assuming JSON format, adjust if using XML or CSV + data = json.loads(post_data.decode('utf-8')) + print(json.dumps(data, indent=2)) + except json.JSONDecodeError: + print(post_data.decode('utf-8')) + elif self.command == 'GET': + print("Received GET request") + print(f"Path: {self.path}") + print(f"Headers: {self.headers}") self.send_response(200) self.end_headers() @@ -100,7 +120,7 @@ You may see in the console an output similar to: ``` 127.0.0.1 - - [nn/nn/nnnn 10:57:03] "POST / HTTP/1.1" 200 - -Received data: Name,Text,State,X,Y,Width,Height +Received POST data: Name,Text,State,X,Y,Width,Height Time,0:52,SameNoChange,843.656319861826,663.0215827338131,359.13669064748206,207.19424460431662 Home Score,35,SameNoChange,525.969716884406,638.4370727628325,214.62426933453253,175.27648662320166 ``` From 031a38cc071f8d202e1ea44853b9692710bc0b81 Mon Sep 17 00:00:00 2001 From: Roy Shilkrot Date: Mon, 30 Sep 2024 08:47:11 -0400 Subject: [PATCH 4/4] Refactor: Update API output encoding options --- api_output.py | 19 +++++++++++-------- docs/out_api.md | 2 +- mainwindow.ui | 7 ++++++- ui_mainwindow.py | 8 +++++--- 4 files changed, 23 insertions(+), 13 deletions(-) diff --git a/api_output.py b/api_output.py index 7ac0370..d17724b 100644 --- a/api_output.py +++ b/api_output.py @@ -4,7 +4,7 @@ import io from functools import partial import xml.etree.ElementTree as ET -from urllib.parse import urlparse +from urllib.parse import urlparse, urlencode import threading from sc_logging import logger @@ -12,7 +12,7 @@ from storage import fetch_data, subscribe_to_data out_api_url = fetch_data("scoresight.json", "out_api_url", None) -out_api_encoding = fetch_data("scoresight.json", "out_api_encoding", "JSON") +out_api_encoding = fetch_data("scoresight.json", "out_api_encoding", "JSON (Full)") out_api_method = fetch_data("scoresight.json", "out_api_method", "POST") @@ -63,8 +63,8 @@ def send_data(): if out_api_method == "GET": response = send_get(data) else: - if out_api_encoding == "JSON": - response = send_json(data) + if out_api_encoding.startswith("JSON"): + response = send_json(data, out_api_encoding) elif out_api_encoding == "XML": response = send_xml(data) elif out_api_encoding == "CSV": @@ -95,15 +95,18 @@ def send_get(data: list[TextDetectionTargetWithResult]): out_api_url_copy += "&" else: out_api_url_copy += "?" - for i, result in enumerate(data): - out_api_url_copy += f"{urlencode(result.name)}={urlencode(result.result)}&" + out_api_url_copy += urlencode({result.name: result.result for result in data}) + logger.debug(f"GET URL: {out_api_url_copy}") response = requests.get(out_api_url_copy) return response -def send_json(data: list[TextDetectionTargetWithResult]): +def send_json(data: list[TextDetectionTargetWithResult], encoding: str): headers = {"Content-Type": "application/json"} - json_data_dump = json.dumps([result.to_dict() for result in data]) + if encoding == "JSON (Full)": + json_data_dump = json.dumps([result.to_dict() for result in data]) + elif encoding == "JSON (Simple key-value)": + json_data_dump = json.dumps({result.name: result.result for result in data}) if out_api_method == "POST": response = requests.post( out_api_url, diff --git a/docs/out_api.md b/docs/out_api.md index 66524fe..c642c1c 100644 --- a/docs/out_api.md +++ b/docs/out_api.md @@ -10,7 +10,7 @@ ScoreSight now offers the ability to send OCR-extracted scoreboard data to exter 2. Check the box labeled "Send out API requests to external services". 3. Enter the URL where you want to send the data in the provided box. -4. Select the encoding format for the data: JSON, XML, or CSV. +4. Select the encoding format for the data: JSON (Full), Json (Simple key-value) XML, or CSV. ![alt text](image-30.png) diff --git a/mainwindow.ui b/mainwindow.ui index 1c66a64..63c3592 100644 --- a/mainwindow.ui +++ b/mainwindow.ui @@ -1457,7 +1457,12 @@ - JSON + JSON (Full) + + + + + JSON (Simple key-value) diff --git a/ui_mainwindow.py b/ui_mainwindow.py index af17fbe..ced6658 100644 --- a/ui_mainwindow.py +++ b/ui_mainwindow.py @@ -774,6 +774,7 @@ def setupUi(self, MainWindow): self.comboBox_api_encode.addItem("") self.comboBox_api_encode.addItem("") self.comboBox_api_encode.addItem("") + self.comboBox_api_encode.addItem("") self.comboBox_api_encode.setObjectName(u"comboBox_api_encode") self.formLayout_3.setWidget(4, QFormLayout.FieldRole, self.comboBox_api_encode) @@ -1226,9 +1227,10 @@ def retranslateUi(self, MainWindow): self.tabWidget_outputs.setTabText(self.tabWidget_outputs.indexOf(self.tab_vmix), QCoreApplication.translate("MainWindow", u"VMix", None)) self.checkBox_enableOutAPI.setText(QCoreApplication.translate("MainWindow", u"Send out API requests to external services.", None)) self.label_21.setText(QCoreApplication.translate("MainWindow", u"Encode", None)) - self.comboBox_api_encode.setItemText(0, QCoreApplication.translate("MainWindow", u"JSON", None)) - self.comboBox_api_encode.setItemText(1, QCoreApplication.translate("MainWindow", u"XML", None)) - self.comboBox_api_encode.setItemText(2, QCoreApplication.translate("MainWindow", u"CSV", None)) + self.comboBox_api_encode.setItemText(0, QCoreApplication.translate("MainWindow", u"JSON (Full)", None)) + self.comboBox_api_encode.setItemText(1, QCoreApplication.translate("MainWindow", u"JSON (Simple key-value)", None)) + self.comboBox_api_encode.setItemText(2, QCoreApplication.translate("MainWindow", u"XML", None)) + self.comboBox_api_encode.setItemText(3, QCoreApplication.translate("MainWindow", u"CSV", None)) self.lineEdit_api_url.setPlaceholderText(QCoreApplication.translate("MainWindow", u"http://", None)) self.comboBox_outApiMethod.setItemText(0, QCoreApplication.translate("MainWindow", u"POST", None))