From 1096d86196dd754e21858b1716e287d0f4bdc479 Mon Sep 17 00:00:00 2001 From: yumoqing Date: Mon, 23 Sep 2024 14:16:07 +0800 Subject: [PATCH] no history --- README.md | 3 + appPublic.egg-info/PKG-INFO | 16 + appPublic.egg-info/SOURCES.txt | 87 +++++ appPublic.egg-info/dependency_links.txt | 1 + appPublic.egg-info/requires.txt | 19 + appPublic.egg-info/top_level.txt | 1 + appPublic/CSVData.py | 65 ++++ appPublic/Config.py | 37 ++ appPublic/ExecFile.py | 107 ++++++ appPublic/FiniteStateMachine.py | 53 +++ appPublic/MiniI18N.py | 150 ++++++++ appPublic/ObjectCache.py | 53 +++ appPublic/RSAutils.py | 89 +++++ appPublic/SQLite3Utils.py | 220 ++++++++++++ appPublic/Singleton.py | 42 +++ appPublic/__init__.py | 1 + .../__pycache__/__init__.cpython-312.pyc | Bin 0 -> 188 bytes appPublic/__pycache__/version.cpython-312.pyc | Bin 0 -> 168 bytes appPublic/across_nat.bak.py | 106 ++++++ appPublic/across_nat.py | 118 ++++++ appPublic/app_logger.py | 88 +++++ appPublic/argsConvert.py | 182 ++++++++++ appPublic/asynciorun.py | 15 + appPublic/audioplayer.py | 157 ++++++++ appPublic/background.py | 11 + appPublic/country_cn_en.py | 247 +++++++++++++ appPublic/csv_Data.py | 54 +++ appPublic/dataencoder.py | 170 +++++++++ appPublic/datamapping.py | 50 +++ appPublic/dictExt.py | 38 ++ appPublic/dictObject.old.py | 161 +++++++++ appPublic/dictObject.py | 130 +++++++ appPublic/easyExcel.py | 71 ++++ appPublic/exceldata.py | 281 +++++++++++++++ appPublic/excelwriter.py | 144 ++++++++ appPublic/find_player.py | 60 ++++ appPublic/folderUtils.py | 195 ++++++++++ appPublic/genetic.py | 44 +++ appPublic/hf.py | 20 ++ appPublic/http_client.py | 131 +++++++ appPublic/httpclient.py | 117 ++++++ appPublic/i18n.py | 161 +++++++++ appPublic/ipgetter.py | 238 +++++++++++++ appPublic/iplocation.py | 75 ++++ appPublic/jsonConfig.py | 74 ++++ appPublic/jsonIO.py | 38 ++ appPublic/localefunc.py | 43 +++ appPublic/log.py | 95 +++++ appPublic/macAddress.py | 38 ++ appPublic/myImport.py | 6 + appPublic/myTE.py | 82 +++++ appPublic/myjson.py | 22 ++ appPublic/mylog.py | 89 +++++ appPublic/oauth_client.py | 188 ++++++++++ appPublic/objectAction.py | 62 ++++ appPublic/outip.py | 93 +++++ appPublic/pickleUtils.py | 16 + appPublic/port_forward.py | 162 +++++++++ appPublic/process_workers.py | 36 ++ appPublic/proxy.py | 12 + appPublic/rc4.py | 215 +++++++++++ appPublic/receiveMail.py | 137 +++++++ appPublic/registerfunction.py | 89 +++++ appPublic/restrictedEnv.py | 34 ++ appPublic/rsaPeer.py | 128 +++++++ appPublic/rsawrap.py | 108 ++++++ appPublic/set_fgcolor.py | 33 ++ appPublic/sockPackage.py | 136 +++++++ appPublic/sshx.py | 335 ++++++++++++++++++ appPublic/strUtils.py | 20 ++ appPublic/t | 1 + appPublic/t.py | 8 + appPublic/testdict.py | 6 + appPublic/thread_workers.py | 41 +++ appPublic/timeUtils.py | 253 +++++++++++++ appPublic/timecost.py | 42 +++ appPublic/tworkers.py | 62 ++++ appPublic/udp_comm.py | 103 ++++++ appPublic/uni_outip.py | 70 ++++ appPublic/unicoding.py | 39 ++ appPublic/uniqueID.py | 36 ++ appPublic/version.py | 1 + appPublic/wcag_checker.py | 27 ++ appPublic/worker.py | 74 ++++ appPublic/zmq_reqrep.py | 135 +++++++ appPublic/zmq_topic.py | 123 +++++++ appPublic/zmqapi.py | 176 +++++++++ build/lib/appPublic/CSVData.py | 65 ++++ build/lib/appPublic/Config.py | 37 ++ build/lib/appPublic/ExecFile.py | 107 ++++++ build/lib/appPublic/FiniteStateMachine.py | 53 +++ build/lib/appPublic/MiniI18N.py | 150 ++++++++ build/lib/appPublic/ObjectCache.py | 53 +++ build/lib/appPublic/RSAutils.py | 89 +++++ build/lib/appPublic/SQLite3Utils.py | 220 ++++++++++++ build/lib/appPublic/Singleton.py | 42 +++ build/lib/appPublic/__init__.py | 1 + build/lib/appPublic/across_nat.bak.py | 106 ++++++ build/lib/appPublic/across_nat.py | 118 ++++++ build/lib/appPublic/app_logger.py | 88 +++++ build/lib/appPublic/argsConvert.py | 182 ++++++++++ build/lib/appPublic/asynciorun.py | 15 + build/lib/appPublic/audioplayer.py | 157 ++++++++ build/lib/appPublic/background.py | 11 + build/lib/appPublic/country_cn_en.py | 247 +++++++++++++ build/lib/appPublic/csv_Data.py | 54 +++ build/lib/appPublic/dataencoder.py | 170 +++++++++ build/lib/appPublic/datamapping.py | 50 +++ build/lib/appPublic/dictExt.py | 38 ++ build/lib/appPublic/dictObject.old.py | 161 +++++++++ build/lib/appPublic/dictObject.py | 130 +++++++ build/lib/appPublic/easyExcel.py | 71 ++++ build/lib/appPublic/exceldata.py | 281 +++++++++++++++ build/lib/appPublic/excelwriter.py | 144 ++++++++ build/lib/appPublic/find_player.py | 60 ++++ build/lib/appPublic/folderUtils.py | 195 ++++++++++ build/lib/appPublic/genetic.py | 44 +++ build/lib/appPublic/hf.py | 20 ++ build/lib/appPublic/http_client.py | 131 +++++++ build/lib/appPublic/httpclient.py | 117 ++++++ build/lib/appPublic/i18n.py | 161 +++++++++ build/lib/appPublic/ipgetter.py | 238 +++++++++++++ build/lib/appPublic/iplocation.py | 75 ++++ build/lib/appPublic/jsonConfig.py | 74 ++++ build/lib/appPublic/jsonIO.py | 38 ++ build/lib/appPublic/localefunc.py | 43 +++ build/lib/appPublic/log.py | 95 +++++ build/lib/appPublic/macAddress.py | 38 ++ build/lib/appPublic/myImport.py | 6 + build/lib/appPublic/myTE.py | 82 +++++ build/lib/appPublic/myjson.py | 22 ++ build/lib/appPublic/mylog.py | 89 +++++ build/lib/appPublic/oauth_client.py | 188 ++++++++++ build/lib/appPublic/objectAction.py | 62 ++++ build/lib/appPublic/outip.py | 93 +++++ build/lib/appPublic/pickleUtils.py | 16 + build/lib/appPublic/port_forward.py | 162 +++++++++ build/lib/appPublic/process_workers.py | 36 ++ build/lib/appPublic/proxy.py | 12 + build/lib/appPublic/rc4.py | 215 +++++++++++ build/lib/appPublic/receiveMail.py | 137 +++++++ build/lib/appPublic/registerfunction.py | 89 +++++ build/lib/appPublic/restrictedEnv.py | 34 ++ build/lib/appPublic/rsaPeer.py | 128 +++++++ build/lib/appPublic/rsawrap.py | 108 ++++++ build/lib/appPublic/set_fgcolor.py | 33 ++ build/lib/appPublic/sockPackage.py | 136 +++++++ build/lib/appPublic/sshx.py | 335 ++++++++++++++++++ build/lib/appPublic/strUtils.py | 20 ++ build/lib/appPublic/t.py | 8 + build/lib/appPublic/testdict.py | 6 + build/lib/appPublic/thread_workers.py | 41 +++ build/lib/appPublic/timeUtils.py | 253 +++++++++++++ build/lib/appPublic/timecost.py | 42 +++ build/lib/appPublic/tworkers.py | 62 ++++ build/lib/appPublic/udp_comm.py | 103 ++++++ build/lib/appPublic/uni_outip.py | 70 ++++ build/lib/appPublic/unicoding.py | 39 ++ build/lib/appPublic/uniqueID.py | 36 ++ build/lib/appPublic/version.py | 1 + build/lib/appPublic/wcag_checker.py | 27 ++ build/lib/appPublic/worker.py | 74 ++++ build/lib/appPublic/zmq_reqrep.py | 135 +++++++ build/lib/appPublic/zmq_topic.py | 123 +++++++ build/lib/appPublic/zmqapi.py | 176 +++++++++ dist/appPublic-5.1.26-py3-none-any.whl | Bin 0 -> 64587 bytes dist/appPublic-5.1.26.tar.gz | Bin 0 -> 49237 bytes dist/appPublic-5.1.27.tar.gz | Bin 0 -> 53926 bytes doc/zmq_topic.md | 60 ++++ requirements.txt | 19 + setup.py | 52 +++ test/brotli_test.py | 14 + test/create_private_file.py | 14 + test/dataencoder_test.py | 36 ++ test/prikey1.rsa | 27 ++ test/prikey2.rsa | 27 ++ test/test_across_nat.py | 17 + test/test_aioupnp.py | 31 ++ test/time_test.py | 13 + test/zmqreply.py | 15 + 180 files changed, 15189 insertions(+) create mode 100755 README.md create mode 100644 appPublic.egg-info/PKG-INFO create mode 100755 appPublic.egg-info/SOURCES.txt create mode 100755 appPublic.egg-info/dependency_links.txt create mode 100644 appPublic.egg-info/requires.txt create mode 100755 appPublic.egg-info/top_level.txt create mode 100755 appPublic/CSVData.py create mode 100755 appPublic/Config.py create mode 100755 appPublic/ExecFile.py create mode 100755 appPublic/FiniteStateMachine.py create mode 100755 appPublic/MiniI18N.py create mode 100755 appPublic/ObjectCache.py create mode 100755 appPublic/RSAutils.py create mode 100755 appPublic/SQLite3Utils.py create mode 100755 appPublic/Singleton.py create mode 100755 appPublic/__init__.py create mode 100644 appPublic/__pycache__/__init__.cpython-312.pyc create mode 100644 appPublic/__pycache__/version.cpython-312.pyc create mode 100755 appPublic/across_nat.bak.py create mode 100755 appPublic/across_nat.py create mode 100755 appPublic/app_logger.py create mode 100755 appPublic/argsConvert.py create mode 100755 appPublic/asynciorun.py create mode 100755 appPublic/audioplayer.py create mode 100755 appPublic/background.py create mode 100755 appPublic/country_cn_en.py create mode 100755 appPublic/csv_Data.py create mode 100755 appPublic/dataencoder.py create mode 100755 appPublic/datamapping.py create mode 100755 appPublic/dictExt.py create mode 100755 appPublic/dictObject.old.py create mode 100755 appPublic/dictObject.py create mode 100755 appPublic/easyExcel.py create mode 100755 appPublic/exceldata.py create mode 100755 appPublic/excelwriter.py create mode 100755 appPublic/find_player.py create mode 100755 appPublic/folderUtils.py create mode 100755 appPublic/genetic.py create mode 100644 appPublic/hf.py create mode 100755 appPublic/http_client.py create mode 100755 appPublic/httpclient.py create mode 100755 appPublic/i18n.py create mode 100755 appPublic/ipgetter.py create mode 100755 appPublic/iplocation.py create mode 100755 appPublic/jsonConfig.py create mode 100755 appPublic/jsonIO.py create mode 100755 appPublic/localefunc.py create mode 100644 appPublic/log.py create mode 100755 appPublic/macAddress.py create mode 100755 appPublic/myImport.py create mode 100755 appPublic/myTE.py create mode 100755 appPublic/myjson.py create mode 100755 appPublic/mylog.py create mode 100644 appPublic/oauth_client.py create mode 100755 appPublic/objectAction.py create mode 100755 appPublic/outip.py create mode 100755 appPublic/pickleUtils.py create mode 100755 appPublic/port_forward.py create mode 100755 appPublic/process_workers.py create mode 100755 appPublic/proxy.py create mode 100755 appPublic/rc4.py create mode 100755 appPublic/receiveMail.py create mode 100755 appPublic/registerfunction.py create mode 100755 appPublic/restrictedEnv.py create mode 100755 appPublic/rsaPeer.py create mode 100755 appPublic/rsawrap.py create mode 100755 appPublic/set_fgcolor.py create mode 100755 appPublic/sockPackage.py create mode 100644 appPublic/sshx.py create mode 100755 appPublic/strUtils.py create mode 100644 appPublic/t create mode 100644 appPublic/t.py create mode 100755 appPublic/testdict.py create mode 100755 appPublic/thread_workers.py create mode 100755 appPublic/timeUtils.py create mode 100755 appPublic/timecost.py create mode 100755 appPublic/tworkers.py create mode 100755 appPublic/udp_comm.py create mode 100755 appPublic/uni_outip.py create mode 100755 appPublic/unicoding.py create mode 100755 appPublic/uniqueID.py create mode 100755 appPublic/version.py create mode 100755 appPublic/wcag_checker.py create mode 100755 appPublic/worker.py create mode 100755 appPublic/zmq_reqrep.py create mode 100755 appPublic/zmq_topic.py create mode 100755 appPublic/zmqapi.py create mode 100644 build/lib/appPublic/CSVData.py create mode 100644 build/lib/appPublic/Config.py create mode 100644 build/lib/appPublic/ExecFile.py create mode 100644 build/lib/appPublic/FiniteStateMachine.py create mode 100644 build/lib/appPublic/MiniI18N.py create mode 100644 build/lib/appPublic/ObjectCache.py create mode 100644 build/lib/appPublic/RSAutils.py create mode 100644 build/lib/appPublic/SQLite3Utils.py create mode 100644 build/lib/appPublic/Singleton.py create mode 100644 build/lib/appPublic/__init__.py create mode 100644 build/lib/appPublic/across_nat.bak.py create mode 100644 build/lib/appPublic/across_nat.py create mode 100644 build/lib/appPublic/app_logger.py create mode 100644 build/lib/appPublic/argsConvert.py create mode 100644 build/lib/appPublic/asynciorun.py create mode 100644 build/lib/appPublic/audioplayer.py create mode 100644 build/lib/appPublic/background.py create mode 100644 build/lib/appPublic/country_cn_en.py create mode 100644 build/lib/appPublic/csv_Data.py create mode 100644 build/lib/appPublic/dataencoder.py create mode 100644 build/lib/appPublic/datamapping.py create mode 100644 build/lib/appPublic/dictExt.py create mode 100644 build/lib/appPublic/dictObject.old.py create mode 100644 build/lib/appPublic/dictObject.py create mode 100644 build/lib/appPublic/easyExcel.py create mode 100644 build/lib/appPublic/exceldata.py create mode 100644 build/lib/appPublic/excelwriter.py create mode 100644 build/lib/appPublic/find_player.py create mode 100644 build/lib/appPublic/folderUtils.py create mode 100644 build/lib/appPublic/genetic.py create mode 100644 build/lib/appPublic/hf.py create mode 100644 build/lib/appPublic/http_client.py create mode 100644 build/lib/appPublic/httpclient.py create mode 100644 build/lib/appPublic/i18n.py create mode 100644 build/lib/appPublic/ipgetter.py create mode 100644 build/lib/appPublic/iplocation.py create mode 100644 build/lib/appPublic/jsonConfig.py create mode 100644 build/lib/appPublic/jsonIO.py create mode 100644 build/lib/appPublic/localefunc.py create mode 100644 build/lib/appPublic/log.py create mode 100644 build/lib/appPublic/macAddress.py create mode 100644 build/lib/appPublic/myImport.py create mode 100644 build/lib/appPublic/myTE.py create mode 100644 build/lib/appPublic/myjson.py create mode 100644 build/lib/appPublic/mylog.py create mode 100644 build/lib/appPublic/oauth_client.py create mode 100644 build/lib/appPublic/objectAction.py create mode 100644 build/lib/appPublic/outip.py create mode 100644 build/lib/appPublic/pickleUtils.py create mode 100644 build/lib/appPublic/port_forward.py create mode 100644 build/lib/appPublic/process_workers.py create mode 100644 build/lib/appPublic/proxy.py create mode 100644 build/lib/appPublic/rc4.py create mode 100644 build/lib/appPublic/receiveMail.py create mode 100644 build/lib/appPublic/registerfunction.py create mode 100644 build/lib/appPublic/restrictedEnv.py create mode 100644 build/lib/appPublic/rsaPeer.py create mode 100644 build/lib/appPublic/rsawrap.py create mode 100644 build/lib/appPublic/set_fgcolor.py create mode 100644 build/lib/appPublic/sockPackage.py create mode 100644 build/lib/appPublic/sshx.py create mode 100644 build/lib/appPublic/strUtils.py create mode 100644 build/lib/appPublic/t.py create mode 100644 build/lib/appPublic/testdict.py create mode 100644 build/lib/appPublic/thread_workers.py create mode 100644 build/lib/appPublic/timeUtils.py create mode 100644 build/lib/appPublic/timecost.py create mode 100644 build/lib/appPublic/tworkers.py create mode 100644 build/lib/appPublic/udp_comm.py create mode 100644 build/lib/appPublic/uni_outip.py create mode 100644 build/lib/appPublic/unicoding.py create mode 100644 build/lib/appPublic/uniqueID.py create mode 100644 build/lib/appPublic/version.py create mode 100644 build/lib/appPublic/wcag_checker.py create mode 100644 build/lib/appPublic/worker.py create mode 100644 build/lib/appPublic/zmq_reqrep.py create mode 100644 build/lib/appPublic/zmq_topic.py create mode 100644 build/lib/appPublic/zmqapi.py create mode 100755 dist/appPublic-5.1.26-py3-none-any.whl create mode 100755 dist/appPublic-5.1.26.tar.gz create mode 100644 dist/appPublic-5.1.27.tar.gz create mode 100755 doc/zmq_topic.md create mode 100755 requirements.txt create mode 100755 setup.py create mode 100755 test/brotli_test.py create mode 100755 test/create_private_file.py create mode 100755 test/dataencoder_test.py create mode 100755 test/prikey1.rsa create mode 100755 test/prikey2.rsa create mode 100755 test/test_across_nat.py create mode 100755 test/test_aioupnp.py create mode 100755 test/time_test.py create mode 100755 test/zmqreply.py diff --git a/README.md b/README.md new file mode 100755 index 0000000..cf78d89 --- /dev/null +++ b/README.md @@ -0,0 +1,3 @@ +# appPublic +a set of icommon modules for python development + diff --git a/appPublic.egg-info/PKG-INFO b/appPublic.egg-info/PKG-INFO new file mode 100644 index 0000000..b8cdc91 --- /dev/null +++ b/appPublic.egg-info/PKG-INFO @@ -0,0 +1,16 @@ +Metadata-Version: 2.1 +Name: appPublic +Version: 5.1.27 +Summary: appPublic +Home-page: https://github.com/yumoqing/appPublic +Author: yumoqing +Author-email: yumoqing@gmail.com +Platform: any +Classifier: Operating System :: OS Independent +Classifier: Programming Language :: Python :: 3 +Classifier: License :: OSI Approved :: MIT License +Description-Content-Type: text/markdown + +# appPublic +a set of icommon modules for python development + diff --git a/appPublic.egg-info/SOURCES.txt b/appPublic.egg-info/SOURCES.txt new file mode 100755 index 0000000..e7eac6e --- /dev/null +++ b/appPublic.egg-info/SOURCES.txt @@ -0,0 +1,87 @@ +README.md +setup.py +appPublic/CSVData.py +appPublic/Config.py +appPublic/ExecFile.py +appPublic/FiniteStateMachine.py +appPublic/MiniI18N.py +appPublic/ObjectCache.py +appPublic/RSAutils.py +appPublic/SQLite3Utils.py +appPublic/Singleton.py +appPublic/__init__.py +appPublic/across_nat.bak.py +appPublic/across_nat.py +appPublic/app_logger.py +appPublic/argsConvert.py +appPublic/asynciorun.py +appPublic/audioplayer.py +appPublic/background.py +appPublic/country_cn_en.py +appPublic/csv_Data.py +appPublic/dataencoder.py +appPublic/datamapping.py +appPublic/dictExt.py +appPublic/dictObject.old.py +appPublic/dictObject.py +appPublic/easyExcel.py +appPublic/exceldata.py +appPublic/excelwriter.py +appPublic/find_player.py +appPublic/folderUtils.py +appPublic/genetic.py +appPublic/hf.py +appPublic/http_client.py +appPublic/httpclient.py +appPublic/i18n.py +appPublic/ipgetter.py +appPublic/iplocation.py +appPublic/jsonConfig.py +appPublic/jsonIO.py +appPublic/localefunc.py +appPublic/log.py +appPublic/macAddress.py +appPublic/myImport.py +appPublic/myTE.py +appPublic/myjson.py +appPublic/mylog.py +appPublic/oauth_client.py +appPublic/objectAction.py +appPublic/outip.py +appPublic/pickleUtils.py +appPublic/port_forward.py +appPublic/process_workers.py +appPublic/proxy.py +appPublic/rc4.py +appPublic/receiveMail.py +appPublic/registerfunction.py +appPublic/restrictedEnv.py +appPublic/rsaPeer.py +appPublic/rsawrap.py +appPublic/set_fgcolor.py +appPublic/sockPackage.py +appPublic/sshx.py +appPublic/strUtils.py +appPublic/t.py +appPublic/testdict.py +appPublic/thread_workers.py +appPublic/timeUtils.py +appPublic/timecost.py +appPublic/tworkers.py +appPublic/udp_comm.py +appPublic/uni_outip.py +appPublic/unicoding.py +appPublic/uniqueID.py +appPublic/version.py +appPublic/wcag_checker.py +appPublic/worker.py +appPublic/zmq_reqrep.py +appPublic/zmq_topic.py +appPublic/zmqapi.py +appPublic.egg-info/PKG-INFO +appPublic.egg-info/SOURCES.txt +appPublic.egg-info/dependency_links.txt +appPublic.egg-info/requires.txt +appPublic.egg-info/top_level.txt +test/test_across_nat.py +test/test_aioupnp.py \ No newline at end of file diff --git a/appPublic.egg-info/dependency_links.txt b/appPublic.egg-info/dependency_links.txt new file mode 100755 index 0000000..8b13789 --- /dev/null +++ b/appPublic.egg-info/dependency_links.txt @@ -0,0 +1 @@ + diff --git a/appPublic.egg-info/requires.txt b/appPublic.egg-info/requires.txt new file mode 100644 index 0000000..8d9605e --- /dev/null +++ b/appPublic.egg-info/requires.txt @@ -0,0 +1,19 @@ +xlrd +bs4 +ffpyplayer +xlwt +nanoid +psutil +ujson +numpy +rsa +brotli +aiohttp +upnpclient +py-natpmp +asyncio +requests +jinja2 +pyzmq +cryptography +asyncssh==2.13.2 diff --git a/appPublic.egg-info/top_level.txt b/appPublic.egg-info/top_level.txt new file mode 100755 index 0000000..663b209 --- /dev/null +++ b/appPublic.egg-info/top_level.txt @@ -0,0 +1 @@ +appPublic diff --git a/appPublic/CSVData.py b/appPublic/CSVData.py new file mode 100755 index 0000000..989b89d --- /dev/null +++ b/appPublic/CSVData.py @@ -0,0 +1,65 @@ +import csv + +class CSVData: + def __init__(self,csvfile,names = None,headline = 0,dataline = 1): + self.csvfile = csvfile + self.names = names + self.headline = headline + self.dataline = dataline + + def read(self): + f = open(self.csvfile,'rb') + reader = csv.reader(f) + fields = None + if self.names is not None: + fields = self.names + data = [] + lno = 0 + for l in reader: + if fields is None and lno == self.headline: + fields = [f for f in l] + if lno >= self.dataline: + rec = {} + for i in range(len(fields)): + rec[fields[i]] = l[i] + data.append(rec) + lno += 1 + f.close() + return data + + def iterRead(self): + self.fd = open(self.csvfile,'r') + try: + reader = csv.reader(self.fd) + fields = None + if self.names is not None: + fields = self.names + lno = 0 + self.onBegin() + for l in reader: + if fields is None and lno == self.headline: + fields = [f for f in l] + if lno >= self.dataline: + rec = {} + for i in range(len(fields)): + rec[fields[i]] = l[i] + self.onRecord(rec) + lno += 1 + self.fd.close() + self.onFinish() + except exception as e: + fd.close() + raise e + + def onReadBegin(self): + pass + def onRecord(self,rec): + print(rec) + + def onFinish(self): + print("onFinish() called") + +if __name__ == '__main__': + import sys + cd = CSVData(sys.argv[1],names = ['st_date','open_price','max_price','min_price','close_price','volume','adj_price']) + cd.iterRead() diff --git a/appPublic/Config.py b/appPublic/Config.py new file mode 100755 index 0000000..26c4774 --- /dev/null +++ b/appPublic/Config.py @@ -0,0 +1,37 @@ +# Config.py +# Copyright (c) 2009 longtop Co. +# See LICENSE for details. +# author: yumoqing@gmail.com +# created date: 2009-02-01 +# last modified date: 2009-02-05 + +import os,sys +from appPublic.ExecFile import ExecFile +from appPublic.dictObject import DictObject +from appPublic.Singleton import Singleton +from zope.interface import implements +CONFIG_FILE = 'conf/config.ini' +from folderUtils import ProgramPath +class Node(object) : + pass + +class Config: + + __metaclass = Singleton + def __init__(self,configpath=None): + if configpath is None: + ps = CONFIG_FILE.split('/') + configpath = os.path.join(ProgramPath(),*ps) + self.configfile = configpath + self.__execfile = ExecFile(self,path=configpath) + self.__execfile.set('Node',Node) + self.__execfile.set('DictObject',DictObject) + self.__execfile.set('dict',DictObject) + r,msg = self.__execfile.run() + if not r: + print(r,msg) + +def getConfig(path=None): + conf = Config(path) + return conf + diff --git a/appPublic/ExecFile.py b/appPublic/ExecFile.py new file mode 100755 index 0000000..ae64638 --- /dev/null +++ b/appPublic/ExecFile.py @@ -0,0 +1,107 @@ +# ExecFile.py +# usage : +# r = ExecFile() +# r.set('a','bbbb') +# r.run('test/cards.ini') +# r.cards +# +import os,sys + +class DictConfig(dict): + def __init__(self,dic=None,path=None,str=None,namespace={}): + dict.__init__(self) + self.namespace=namespace + if dic is not None and type(dic) == dict: + self.__dict__.update(dic) + self.__subConfig() + if path is not None: + self.__path = path + self.__load(path) + if str is not None: + self.__confstr = str + try: + exec(str,self.namespace,self.__dict__) + self.__subConfig() + except: + pass + def keys(self): + return self.__dict__.keys() + + def __getitem__(self,n): + return self.__dict__[n] + + def __getattr__(self,name): + if self.__dict__.has_key(name): + return self.__dict__[name] + raise AttributeError(name) + + def __subConfig(self): + for n in self.__dict__.keys(): + if type(self.__dict__[n]) == dict: + self.__dict__[n] = DictConfig(dic=self.__dict__[n]) + elif type(self.__dict__[n]) == type([]): + a = [] + for i in self.__dict__[n]: + if type(i) == dict: + a.append(DictConfig(dic=i)) + else: + a.append(i) + self.__dict__[n] = a + elif type(self.__dict__[n]) == type(()): + a = [] + for i in self.__dict__[n]: + if type(i) == dict: + a.append(DictConfig(dic=i)) + else: + a.append(i) + self.__dict__[n] = tuple(a) + + def __load(self,path): + d = {} + c = {} + f = open(path,'r') + buf = f.read() + f.close() + try: + exec(buf,self.namespace,namespace) + #print d + #print "c=",c + self.__dict__.update(c) + #print self.__dict__ + self.__subConfig() + return True + except Exception as e: + print(self.__path,e) + return False + +class ExecFile(object) : + def __init__(self,obj=None,path=None,namespace={}): + self.namespace = namespace + if obj == None: + obj = self + self.__object = obj + #self.namespace.update(self.__object.__dict__) + self.__file = path + + def set(self,name,v) : + setattr(self.__object,name,v) + + def get(self,name,default=None) : + return getattr(self.__object,name,default) + + def run(self,path=None) : + if path!=None: + self.__file = path + if self.__file is None: + raise Exception('exec file is none') + f = open(self.__file,'r') + buf = f.read() + f.close() + try : + exec(buf,globals(),self.__object.__dict__) + except Exception as e: + print("ExecFile()",e,self.__file) + return (False,e) + return (True,'') + + diff --git a/appPublic/FiniteStateMachine.py b/appPublic/FiniteStateMachine.py new file mode 100755 index 0000000..e906a6a --- /dev/null +++ b/appPublic/FiniteStateMachine.py @@ -0,0 +1,53 @@ +# FiniteStateMachine.py +## a virtual State object of FSM +# +class BaseFSM(object): + def enterState(self, obj): + raise NotImplementedError() + + def execState(self, obj): + raise NotImplementedError() + + def exitState(self, obj): + raise NotImplementedError() + +## a FMS Manager +# only need one Manager for a FSM +class FSMManager(object): + def __init__(self): + self._fsms = {} + + def addState(self,state,fsm): + self._fsms[state] = fsm + + def delState(self,state): + del self._fsms[state] + + def getFSM(self, state): + return self._fsms[state] + + def frame(self, objs, state): + for obj in objs: + if state == obj.curr_state: + obj.keepState() + else: + obj.changeState(state, self._fsms[state]) + +## the object with has a Finite State Machine +# +class FSMObject(object): + def attachFSM(self,state,fsm): + self.fsm_state_object = fsm + self.fsm_cur_state = state + + def changeState(self,new_state,newfsm): + self.fsm_cur_state = new_state + self.fsm_state_object.exitState(self) + self.fsm_state_object = new_fsm + self.fsm_state_object.enterState(self) + self.fsm_state_object.execState(self) + + def keepState(self): + self.fsm_state_object.execState(self) + + \ No newline at end of file diff --git a/appPublic/MiniI18N.py b/appPublic/MiniI18N.py new file mode 100755 index 0000000..81c2afe --- /dev/null +++ b/appPublic/MiniI18N.py @@ -0,0 +1,150 @@ +import os,re,sys +import codecs +from appPublic.folderUtils import _mkdir +from appPublic.Singleton import SingletonDecorator +from appPublic.folderUtils import ProgramPath +import threading +import time + +import locale + +comment_re = re.compile(r'\s*#.*') +msg_re = re.compile(r'\s*([^:]*)\s*:\s*([^\s].*)') + +def dictModify(d, md) : + for i in md.keys() : + if md[i]!=None : + d[i] = md[i] + return d + +convert_pairs = {':':'\\x3A', + '\n':'\\x0A', + '\r':'\\x0D', +} + +def charEncode(s) : + r = '' + v = s.split('\\') + s = '\\\\'.join(v) + for i in convert_pairs.keys() : + v = s.split(i) + s = convert_pairs[i].join(v) + # print 'i=',i,'iv=',convert_pairs[i],'s=',s + return s + +def charDecode(s) : + for i in convert_pairs.items() : + v = s.split(i[1]) + s = i[0].join(v) + v = s.split('\\\\') + s = '\\'.join(v) + return s + +def getTextDictFromLines(lines) : + d = {} + for l in lines : + l = ''.join(l.split('\r')) + if comment_re.match(l) : + continue + m = msg_re.match(l) + if m : + grp = m.groups() + d[charDecode(grp[0])] = charDecode(grp[1]) + return d + +def getFirstLang(lang) : + s = lang.split(',') + return s[0] + +@SingletonDecorator +class MiniI18N: + """ + """ + def __init__(self,path,lang=None,coding='utf8') : + self.path = path + l = locale.getdefaultlocale() + self.curLang = l[0] + self.coding = coding + self.id = 'i18n' + self.langTextDict = {} + self.setupMiniI18N() + self.missed_pt = None + self.translated_pt = None + self.header_pt = None + self.footer_pt = None + self.show_pt=None + self.clientLangs = {} + self.languageMapping = {} + self.timeout = 600 + + def __call__(self,msg,lang=None) : + """ + """ + if type(msg) == type(b''): + msg = msg.decode(self.coding) + return self.getLangText(msg,lang) + + def setLangMapping(self,lang,path): + self.languageMapping[lang] = path + + def getLangMapping(self,lang): + return self.languageMapping.get(lang,lang) + + def setTimeout(self,timeout=600): + self.timeout = timeout + + def delClientLangs(self): + t = threading.currentThread() + tim = time.time() - self.timeout + [ self.clientLangs.pop(k,None) for k in self.clientLangs.keys() if self.clientLangs[k]['timestamp'] < tim ] + + def getLangDict(self,lang): + lang = self.getLangMapping(lang) + return self.langTextDict.get(lang,{}) + + def getLangText(self,msg,lang=None) : + """ + """ + if lang==None : + lang = self.getCurrentLang() + textMapping = self.getLangDict(lang) + return textMapping.get(msg,msg) + + def setupMiniI18N(self) : + """ + """ + + p = os.path.join(self.path,'i18n') + langs = [] + + for f in os.listdir(p) : + if os.path.isdir(os.path.join(p,f)) : + langs.append(f) + for dir in langs : + p1 = os.path.join(p,dir,'msg.txt') + if os.path.exists(p1) : + f = codecs.open(p1,'r',self.coding) + textDict = getTextDictFromLines(f.readlines()) + f.close() + self.langTextDict[dir] = textDict + + self._p_changed = 1 + + def setCurrentLang(self,lang): + lang = self.getLangMapping(lang) + t = time.time() + threadid = threading.currentThread() + a = dict(timestamp=t,lang=lang) + self.clientLangs[threadid] = a + + def getCurrentLang(self) : + """ + """ + threadid = threading.currentThread() + return self.clientLangs[threadid]['lang'] + +def getI18N(coding='utf8'): + path = ProgramPath() + i18n = MiniI18N(path,coding) + return i18n + diff --git a/appPublic/ObjectCache.py b/appPublic/ObjectCache.py new file mode 100755 index 0000000..381b18a --- /dev/null +++ b/appPublic/ObjectCache.py @@ -0,0 +1,53 @@ +# !/usr/bin/env python +# +# ObjectsCache is a Objects cache +# the Object has to have a method "get_size" to tell +# the cacher counting the objects size + +class ObjectCache(dict) : + def __init__(self,maxsize=10000000,*args) : + super(ObjectsCache,self).__init__(*args) + self.maxsize = maxsize + self.size = 0 + self._shadow = {} + + def __setitem__(self,key,item) : + try : + size = item.get_size() + self.size += size + except : + return + if self.size >= self.maxsize : + tmp = [(t,key) for key,(t,size) in self._shadow.iteritems() ] + tmp.sort() + for i in xrange(len(tmp)//2) : + del self[tmp[i][i]] + del tmp + super(ObjectCache,self).__setitem__(key,item) + self._shadow[key] = [time.time(),size] + + def __getitem__(self,key) : + try : + item = super(ObjectCache,self).__getitem__(key) + except : + raise + else : + self._shadow[key][0] = time.time() + return item + + def get(self,key,default=None) : + if self.has_key(key) : + return self[key] + else : + return default + + def __delitem__(self,key) : + try : + super(ObjectCache,self).__delitem__(key) + except : + raise + else : + self.size -= self._shadow[key][1] + del self._shadow[key] + + diff --git a/appPublic/RSAutils.py b/appPublic/RSAutils.py new file mode 100755 index 0000000..3eb4908 --- /dev/null +++ b/appPublic/RSAutils.py @@ -0,0 +1,89 @@ +import codecs + +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_OAEP +from Crypto.Cipher import KPCS1_V1_5 as V1_5 +from Crypto.Signature import PKCS1_v1_5 +from Crypto.Hash import SHA512, SHA384, SHA256, SHA, MD5 +from Crypto import Random +from base64 import b64encode, b64decode +hash = "SHA-256" + +def readPublickey(fname): + with codecs.open(fname,'r','utf8') as f: + b = f.read() + k = RSA.importKey(b) + return k + return None + +def readPrivatekey(fname,pwd): + with codecs.open(fname,'r','utf8') as f: + b = f.read() + k = RSA.importKey(b,pwd) + return k + return None + +def newkeys(keysize): + random_generator = Random.new().read + key = RSA.generate(keysize, random_generator) + private, public = key, key.publickey() + return public, private + +def importKey(externKey): + return RSA.importKey(externKey) + +def getpublickey(priv_key): + return priv_key.publickey() + +def encrypt(message, pub_key): + cipher = PKCS1_OAEP.new(pub_key) + return cipher.encrypt(message) + +def decrypt(ciphertext, priv_key): + try: + cipher = PKCS1_OAEP.new(priv_key) + return cipher.decrypt(ciphertext) + except Exception as e: + print('e=',e) + cipher = V1_5.new(priv_key) + return cipher.decrypt(ciphertext) + +def sign(message, priv_key, hashAlg = "SHA-256"): + global hash + hash = hashAlg + signer = PKCS1_v1_5.new(priv_key) + + if (hash == "SHA-512"): + digest = SHA512.new() + elif (hash == "SHA-384"): + digest = SHA384.new() + elif (hash == "SHA-256"): + digest = SHA256.new() + elif (hash == "SHA-1"): + digest = SHA.new() + else: + digest = MD5.new() + digest.update(message) + return signer.sign(digest) + +def verify(message, signature, pub_key): + signer = PKCS1_v1_5.new(pub_key) + if (hash == "SHA-512"): + digest = SHA512.new() + elif (hash == "SHA-384"): + digest = SHA384.new() + elif (hash == "SHA-256"): + digest = SHA256.new() + elif (hash == "SHA-1"): + digest = SHA.new() + else: + digest = MD5.new() + digest.update(message) + return signer.verify(digest, signature) + +if __name__ == '__main__': + cipher="""WaMlLEYnhBk+kTDyN/4OJmQf4ccNdk6USgtKpb7eHsYsotq4iyXi3N5hB1E/PqrPSmca1AMDLUcumwIrLeGLT9it3eTBQgl1YQAsmPxa6lF/rDOZoLbwD5sJ6ab/0/fuM4GbotqN5/d0MeuOSELoo8cFWw+7XpRxn9EMYnw5SzsjDQRWxXjZptoaGa/8pBBkDmgLqINif9EWV+8899xqTd0e9w1Gqb7wbt/elRNVBpgsSuSZb+dtBlvNUjuTms8BETSRai5vhXetK26Ms8hrayiy38n7wwEKE8fZ9iFzLtwa6xbhD5KudWbKJFFOZAfpzWttGMwWlISbGQigcW4+Bg==""" + key = readPrivatekey('d:/dev/mecp/conf/RSA.private.key','ymq123') + t = decrypt(cipher,key) + print('t=',t) + \ No newline at end of file diff --git a/appPublic/SQLite3Utils.py b/appPublic/SQLite3Utils.py new file mode 100755 index 0000000..9a4038d --- /dev/null +++ b/appPublic/SQLite3Utils.py @@ -0,0 +1,220 @@ +import os,sys +import thread +from sqlite3 import dbapi2 as sqlite +import time +from localefunc import * +from folderUtils import mkdir +from PublicData import public_data +from mylog import mylog + +def logit(s) : + mylog('%s:%s' % (__file__,s)) + +class Record : + def __init__(self,data,localize=False) : + + for i in data.keys() : + d = data[i] + if localize and type(d)==type('') : + d = localeString(d) + setattr(self,i.lower(),d) + + def __getattr__(self,name) : + name = name.lower() + try : + return getattr(self,name) + except : + raise AttributeError(name) + + def __str__(self) : + a = self.__dict__ + f = [] + for i in a.keys() : + f.append("%s : %s" % (i,str(a[i]))) + return '[%s]' % '\n'.join(f) + +def str2unicode(s) : + if type(s) == type('') : + try : + ret = unicode(s,local_encoding) + return ret + except : + try : + ret = unicode(s,'utf8') + return ret + except : + return buffer(s) + return s + +def unicode2str(s) : + t = type(s) + if t == type(5) : + return long(s) + if t == type(buffer('')) : + return str(s) + if t == type(u"w") : + return s.encode('utf8') + return s + +def argConvert(args) : + if args==None : + return None + t = type(args) + if t==type(()) or t==type([]) : + return [str2unicode(i) for i in args] + if t==type({}) : + for i in args.keys() : + args[i] = str2unicode(args[i]) + return args + return args + +class SQLite3 : + + def __init__(self,dbpath,localize=False) : + self.__dict__['threadMap'] = {} + self.__dict__['localize'] = localize + self.__dict__['dbpath'] = dbpath + self.results = None + self.con = None + self.cursor = None + self.sqlcmd = '' + self._connection(dbpath) + + def _connection(self,dbpath=None) : + if dbpath!=None : + self.dbpath = dbpath + self.con = sqlite.connect(self.dbpath) + self.cursor = self.con.cursor() + self.result = None + self.sqlcmd = '' + + def __setattr__(self, name, value): + id = thread.get_ident() + if not self.__dict__['threadMap'].has_key(id): + self.__dict__['threadMap'][id] = {} + self.threadMap[id][name] = value + + def __getattr__(self, name): + id = thread.get_ident() + + if not self.__dict__['threadMap'].has_key(id) : + self.__dict__['threadMap'][id] = {} + if self.__dict__['threadMap'][id].has_key(name) : + return self.__dict__['threadMap'][id][name] + raise AttributeError(name) + + def tables(self) : + self.SQL("select * from sqlite_master where type='table'") + r = self.FETCH() + ts = [] + while r : + ts.append(r.name) + r = self.FETCH() + return ts + + def columns(self,tablenmae) : + self.SQL('select * from %s' % tablename) + self.desc = self.results.getdescription() + return desc + + def FETCHALL(self) : + all=[] + r = True + r = self.cursor.fetchall() + return r + + def _eatCursorNext(self) : + if self.cursor==None : + return None + r = 1 + while r : + try : + r = self.cursor.next() + except : + return + + def SQL(self,cmd,args=(),retry=0) : + if self.con==None : + print("self.con==None",cmd) + self._connection() + return self.SQL(cmd,args,retry) + return -1 + self._eatCursorNext() + args = argConvert(args) + self.lastSQL = cmd + self.desc = None + try : + if len(cmd.split(';'))>1 : + self.results = self.cursor.executescript(cmd) + else : + self.results = self.cursor.execute(cmd,args) + return True + except Exception as e: + print('execute:',cmd,'error',e) + self.results = None + raise + return True + + def FETCH(self) : + if self.results == None : + return None + if self.desc == None : + try : + self.desc = self.results.description + + except Exception as e: + print("fetch error",self.lastSQL,e) + raise + try : + desc = self.desc + d = self.results.next() + data = {} + for i in range(len(d)) : + data[desc[i][0]] = unicode2str(d[i]) + return Record(data,self.localize) + except StopIteration : + return None + except Exception as e: + print("error happen",e,self,lastSQL) + raise + + def COMMIT(self) : + self.SQL('PRAGMA case_sensitive_like = 1') + try : + self.cursor.fetchall() + except : + pass + + def ROLLBACK(self) : + self.SQL('ROLLBACK') + + def BEGIN(self) : + # self.SQL('BEGIN') + return + + def CLOSE(self) : + self.con = None + self.cursor = None + + +def getDataBase(name) : + a_name='db_%s' % name + db = public_data.get(a_name,None) + if db==None : + dbpath = public_data.get('dbpath_%s' % name,None) + if dbpath==None : + p = public_data.get('ProgramPath',None) + if p==None: + raise Exception('public_data must has a "ProgramPath" variable') + p1 = os.path.join(p,'var') + mkdir(p1) + dbpath = os.path.join(p1,'%s.db3' % name) + public_data.set('dbpath_%s' % name,dbpath) + db = SQLite3(dbpath) + public_data.set(a_name,db) + try : + con = db.con + except : + dbpath = public_data.get('dbpath_%s' % name,None) + db._connection(dbpath) + return db diff --git a/appPublic/Singleton.py b/appPublic/Singleton.py new file mode 100755 index 0000000..9157c98 --- /dev/null +++ b/appPublic/Singleton.py @@ -0,0 +1,42 @@ +# +from appPublic.dictObject import DictObject + +class SingletonDecorator: + def __init__(self,klass): + self.klass = klass + self.instance = None + def __call__(self,*args,**kwds): + if self.instance == None: + self.instance = self.klass(*args,**kwds) + return self.instance + +@SingletonDecorator +class GlobalEnv(DictObject): + pass + +if __name__ == '__main__': + @SingletonDecorator + class Child(object): + def __init__(self,name): + print("clild.init") + self.name = name + def __str__(self): + return 'HAHA:' + self.name + def __expr__(self): + print(self.name) + + @SingletonDecorator + class Handle(object): + def __init__(self,name): + self.name = name + def __expr__(self): + print(self.name) + + c = Child('me') + d = Child('he') + + print(str(c),str(d)) + e = Handle('hammer') + f = Handle('nail'); + print(str(e),str(f)) + diff --git a/appPublic/__init__.py b/appPublic/__init__.py new file mode 100755 index 0000000..58f3ace --- /dev/null +++ b/appPublic/__init__.py @@ -0,0 +1 @@ +from .version import __version__ diff --git a/appPublic/__pycache__/__init__.cpython-312.pyc b/appPublic/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..4ddcaad4085e7de9c58a9abb002f93b2c57747a8 GIT binary patch literal 188 zcmX@j%ge<81olxAlNEvVV-N=hn4pZ$0zk%eh7^Vr#vF!R#wbQchDs()=9i2>VNJ$c z-0|^csYS(^`FZj2ews|T*rB{4W}y5^hR+~Ve(CCm76TROSLPP#7gXve78DeeCgo%% zgIEC&R(yPBUS>&ryk0@&FAkgB{FKt1RJ$S$pl*;w#URE9W=2NFdkhLiY(NeGP|7Y% literal 0 HcmV?d00001 diff --git a/appPublic/__pycache__/version.cpython-312.pyc b/appPublic/__pycache__/version.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..abbab40275f6c1dc4e2b8dabb42d88d24cf89de8 GIT binary patch literal 168 zcmX@j%ge<81iQapN)ZRrk3k$5V1zP0a{w9B8B!Rc7%CYxnX1@K^$hil%>6VOZ*j-R zm!%dJXXfX{$FF4g4ASsRM?bU}D5hVTTc}@9sh?O-P*9qblbH-+1wdF(J$eO|zc_4i i^HWN5QtgV^fSN&87K2>$ftit!@dlq*BX 0: + return x[0].external_port + + occupied_ports = [ i.external_port for i in all_mappings if i.protocol == protocol ] + external_port = from_port + while external_port < 52333: + if external_port not in occupied_ports: + break + external_port += 1 + + if external_port < 52333: + await self.upnp.add_port_mapping(external_port, + protocol, + inner_port, + ip, + desc or 'user added') + return external_port + return None + + async def is_port_mapped(self, external_port, protocol='TCP'): + if self.upnp is None: + await self.init_upnp() + protocol = protocol.upper() + if self.upnp_supported: + x = await self.upnp.get_specific_port_mapping(external_port, + protocol) + if len(x) == 0: + return True + return False + raise Exception('not implemented') + + async def port_unmap(self, external_port, protocol='TCP'): + if self.upnp is None: + await self.init_upnp() + protocol = protocol.upper() + if self.upnp_supported: + await self.upnp.delete_port_mapping(external_port, protocol) + raise Exception('not implemented') + + def pmp_map_port(self, inner_port, protocol='TCP', from_port=40003): + if protocol.upper() == 'TCP': + x = pmp.map_tcp_port(from_port, inner_port, + lifetime=999999999) + return x.public_port + x = pmp.map_udp_port(from_port, inner_port, + lifetime=999999999) + return x.public_port + + async def map_port(self, inner_port, protocol='tcp', from_port=40003, lan_ip=None, desc=None): + if self.pmp_supported: + return self.pmp_map_port(inner_port, protocol=protocol) + + return await self.upnp_map_port( inner_port, protocol=protocol, ip=lan_ip, desc=desc) + diff --git a/appPublic/across_nat.py b/appPublic/across_nat.py new file mode 100755 index 0000000..b045ec6 --- /dev/null +++ b/appPublic/across_nat.py @@ -0,0 +1,118 @@ + +from traceback import print_exc +from natpmp import NATPMP as pmp +import upnpclient +from requests import get +from .background import Background + +class AcrossNat(object): + def __init__(self): + self.external_ip = None + self.upnp = None + self.pmp_supported = True + self.upnp_supported = True + self.init_pmp() + self.init_upnp() + + def init_upnp(self): + try: + igd = upnpclient.discover()[0] + s_names = [ n for n in igd.service_map.keys() if 'WAN' in n and 'Conn' in n] + self.upnp = igd.service_map[s_names[0]] + except Exception as e: + print(e) + print_exc() + self.upnp_supported = False + + def init_pmp(self): + try: + self.external_ip = pmp.get_public_address() + except pmp.NATPMPUnsupportedError: + self.pmp_supported = False + + def get_external_ip(self): + if self.pmp_supported: + try: + self.external_ip = pmp.get_public_address() + return self.external_ip + except: + self.pmp_supported = False + + if self.upnp_supported: + try: + x = self.upnp.GetExternalIPAddress() + return x['NewExternalIPAddress'] + except: + self.upnp_supported = False + try: + return get('https://api.ipify.org').text + except: + pass + try: + return get('https://ipapi.co/ip/').text + except: + return None + + def upnp_check_external_port(self, eport, protocol='TCP'): + try: + self.upnp.GetSpecificPortMappingEntry(NewExternalPort=eport, + NewProtocol=protocol, + NewRemoteHost='') + return True + except: + return False + + def upnp_map_port(self, inner_port, + protocol='TCP', from_port=40003, + ip=None, desc='test'): + + protocol = protocol.upper() + external_port = from_port + while external_port < 52333: + if self.upnp_check_external_port(external_port, + protocol=protocol): + external_port += 1 + continue + try: + self.upnp.AddPortMapping(NewRemoteHost='', + NewExternalPort=external_port, + NewProtocol=protocol, + NewInternalPort=inner_port, + NewInternalClient=ip, + NewEnabled='1', + NewPortMappingDescription=desc, + NewLeaseDuration=0 + ) + return external_port + except: + return None + return None + + def is_port_mapped(self, external_port, protocol='TCP'): + protocol = protocol.upper() + if self.upnp_supported: + return self.upnp_check_external_port(external_port, + protocol=protocol) + raise Exception('not implemented') + + def port_unmap(self, external_port, protocol='TCP'): + protocol = protocol.upper() + if self.upnp_supported: + self.upnp.delete_port_mapping(external_port, protocol) + raise Exception('not implemented') + + def pmp_map_port(self, inner_port, protocol='TCP', from_port=40003): + if protocol.upper() == 'TCP': + x = pmp.map_tcp_port(from_port, inner_port, + lifetime=999999999) + return x.public_port + x = pmp.map_udp_port(from_port, inner_port, + lifetime=999999999) + return x.public_port + + def map_port(self, inner_port, protocol='tcp', from_port=40003, lan_ip=None, desc=None): + if self.pmp_supported: + return self.pmp_map_port(inner_port, protocol=protocol) + + return self.upnp_map_port( inner_port, protocol=protocol, ip=lan_ip, desc=desc) + diff --git a/appPublic/app_logger.py b/appPublic/app_logger.py new file mode 100755 index 0000000..8872cd0 --- /dev/null +++ b/appPublic/app_logger.py @@ -0,0 +1,88 @@ +import os +import sys +import logging +from functools import partial +from appPublic.timeUtils import timestampstr +levels={ + "debug":logging.DEBUG, + "info":logging.INFO, + "warning":logging.WARNING, + "error":logging.error, + "critical":logging.CRITICAL +} +defaultfmt = '%(asctime)s[%(name)s][%(levelname)s][%(filename)s:%(lineno)s]%(message)s' +logfile = -1 +logger = None +g_levelname='info' +level = levels.get('info') + +def create_logger(name, formater=defaultfmt, levelname=None, file=None): + global logger, logfile, level, g_levelname + if logfile == -1: + logfile = file + if logger: + return logger + logger = logging.getLogger(name) + if levelname: + g_levelname = levelname + else: + levelname = g_levelname + level = levels.get(levelname, levels.get('info')) + logger.setLevel(level) + format = logging.Formatter(formater) + file_handler = None + if logfile is not None: + file_handler = logging.FileHandler(logfile) + else: + file_handler = logging.StreamHandler() + + file_handler.setFormatter(format) + logger.addHandler(file_handler) + return logger + + +def info(*args, **kw): + global logger + if logger is None: + return + logger.info(*args, **kw) + +def debug(*args, **kw): + global logger + if logger is None: + return + logger.debug(*args, **kw) + +def warning(*args, **kw): + global logger + if logger is None: + return + logger.warning(*aegs, **kw) + +def error(*args, **kw): + global logger + if logger is None: + return + logger.error(*args, **kw) + +def critical(*args, **kw): + global logger + if logger is None: + return + logger.critical(*args, **kw) + +def exception(*args, **kw): + global logger + if logger is None: + return + logger.exception(**args, **kw) + +class AppLogger: + def __init__(self): + self.logger = create_logger(self.__class__.__name__) + self.debug = self.logger.debug + self.info = self.logger.info + self.warning = self.logger.warning + self.error = self.logger.error + self.critical = self.logger.critical + self.exception = self.logger.exception diff --git a/appPublic/argsConvert.py b/appPublic/argsConvert.py new file mode 100755 index 0000000..d459260 --- /dev/null +++ b/appPublic/argsConvert.py @@ -0,0 +1,182 @@ +# -*- coding:utf8 -*- +import re +from appPublic.dictObject import DictObject +class ConvertException(Exception): + pass + +class ArgsConvert(object): + def __init__(self,preString,subfixString,coding='utf-8'): + self.preString = preString + self.subfixString = subfixString + self.coding=coding + sl1 = [ u'\\' + c for c in self.preString ] + sl2 = [ u'\\' + c for c in self.subfixString ] + ps = u''.join(sl1) + ss = u''.join(sl2) + re1 = ps + r"[_a-zA-Z_\u4e00-\u9fa5][a-zA-Z_0-9\u4e00-\u9fa5\,\.\'\{\}\[\]\(\)\-\+\*\/]*" + ss + self.re1 = re1 + # print( self.re1,len(self.re1),len(re1),type(self.re1)) + + def convert(self,obj,namespace,default=''): + """ obj can be a string,[],or dictionary """ + if isinstance(obj, str): + return self.convertString(obj,namespace,default) + if isinstance(obj, list): + ret = [] + for o in obj: + ret.append(self.convert(o,namespace,default)) + return ret + if isinstance(obj, dict): + ret = DictObject() + ret.update({k:self.convert(v,namespace,default) for k,v in obj.items()}) + return ret + return obj + + def findAllVariables(self,src): + r = [] + for ph in re.findall(self.re1,src): + dl = self.getVarName(ph) + r.append(dl) + return r + + def getVarName(self,vs): + return vs[len(self.preString):-len(self.subfixString)] + + def getVarValue(self,var,namespace,default): + v = default + try: + v = eval(var,namespace) + except Exception as e: + v = namespace.get(var, None) + if v: + return v + if callable(default): + return default(var) + return default + return v + + def convertString(self,s,namespace,default): + args = re.findall(self.re1,s) + for arg in args: + dl = s.split(arg) + var = self.getVarName(arg) + v = self.getVarValue(var,namespace,default) + if not isinstance(v, str): + if len(args) == 1 and s.startswith(self.preString) and s.endswith(self.subfixString): + return v + v = str(v) + s = v.join(dl) + return s + +class ConditionConvert(object): + def __init__(self,pString = u'$<',sString=u'>$',coding='utf-8'): + self.coding = coding + self.pString = pString + self.sString = sString + pS = ''.join([u'\\'+i for i in self.pString ]) + sS = ''.join([u'\\'+i for i in self.sString ]) + self.re1 = re.compile(u'(' + pS + '/?' + u'[_a-zA-Z_\u4e00-\u9fa5][a-zA-Z_0-9\u4e00-\u9fa5\,\.\'\{\}\[\]\(\)\-\+\*\/]*' + sS + u')') + self.buffer1 = [] + def convert(self,obj,namespace): + """ obj can be a string,[],or dictionary """ + if type(obj) == type(u''): + return self.convertUnicode(obj,namespace) + if type(obj) == type(''): + return self.convertString(obj,namespace) + if type(obj) == type([]): + ret = [] + for o in obj: + ret.append(self.convert(o,namespace)) + return ret + if type(obj) == type({}): + ret = {} + for k in obj.keys(): + ret.update({k:self.convert(obj.get(k),namespace)}) + return ret + # print( type(obj),"not converted") + return obj + + def getVarName(self,vs): + return vs[len(self.pString):-len(self.sString)] + + def getVarValue(self,var,namespace): + v = None + try: + v = eval(var,namespace) + except Exception as e: + v = namespace.get(var,None) + return v + + def convertList(self,alist,namespace): + ret = [] + handleList = alist + while len(handleList) > 0: + i = handleList[0] + handleList = handleList[1:] + if len(self.re1.findall(i)) < 1: + ret.append(i) + else: + name = self.getVarName(i) + + if name[0] == u'/': + name = name[1:] + if len(self.buffer1) < 1: + raise ConvertException('name(%s) not match' % name) + if self.buffer1[-1] != name: + raise ConvertException('name(%s) not match(%s)' % (self.buffer1[-1],name)) + val = self.getVarValue(name,namespace) + self.buffer1 = self.buffer1[:-1] + if val is not None: + return u''.join(ret),handleList + else: + return u'',handleList + else: + self.buffer1.append(name) + subStr,handleList = self.convertList(handleList,namespace) + ret.append(subStr) + if len(self.buffer1)>0: + raise ConvertException('name(s)(%s) not closed' % ','.join(self.buffer1)) + return u''.join(ret),[] + + def convertUnicode(self,s,namespace): + ret = [] + parts = self.re1.split(s) + s,b = self.convertList(parts,namespace) + return s + + def convertString(self,s,namespace): + ret = self.convertUnicode(s,namespace) + return ret + + +if __name__ == '__main__': + """ + ns = { + 'a':12, + 'b':'of', + 'c':'abc', + '是':'is', + 'd':{ + 'a':'doc', + 'b':'gg', + } + } + AC = ArgsConvert('%{','}%') + s1 = u"%{a}% is a number,%{d['b']}% is %{是}% undefined,%{c}% is %{d['a']+'(rr)'}% string" + arglist=['this is a descrciption %{b}% selling book',123,'ereg%{a}%,%{c}%'] + argdict={ + 'my':arglist, + 'b':s1 + } + print(f(s1,'<=>',AC.convert(s1,ns))) + print(f(argdict,'<=>',AC.convert(argdict,ns))) + """ + cc = ConditionConvert() + s2 = u"Begin $$this is $$ba = 100 $$condition out$$ end" + s3 = """select * from RPT_BONDRATINGS +where 1=1 +$$and ratingtype=${rtype}$$$ +$$and bond_id = ${bond}$$$""" + print(f("result=",cc.convert(s2,{'ba':23}))) + print(f("result = ",cc.convert(s3,{'bond':'943','rtype':'1'}))) + diff --git a/appPublic/asynciorun.py b/appPublic/asynciorun.py new file mode 100755 index 0000000..4e3d054 --- /dev/null +++ b/appPublic/asynciorun.py @@ -0,0 +1,15 @@ +import asyncio +import sys +from sqlor.dbpools import DBPools +from appPublic.jsonConfig import getConfig + +def run(coro): + p = '.' + if len(sys.argv) > 1: + p = sys.argv[1] + config = getConfig(p, {'woridir':p}) + DBPools(config.databases) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(coro()) + diff --git a/appPublic/audioplayer.py b/appPublic/audioplayer.py new file mode 100755 index 0000000..c89ea89 --- /dev/null +++ b/appPublic/audioplayer.py @@ -0,0 +1,157 @@ +import time +from ffpyplayer.player import MediaPlayer +from ffpyplayer.tools import set_log_callback, get_log_callback, formats_in + +class AudioPlayer: + def __init__(self, source=None, autoplay=False, + loop=False, + on_stop=None): + self.volume = 1 + self.state = None + self.source = None + self.quitted = False + self.loop = loop + self.autoplay = autoplay + self.player = None + self.on_stop = on_stop + self.cmds = [] + if source: + self.set_source(source) + + def set_source(self, source): + self.source = source + self.load() + + def player_callback(self, selector, value): + print(f'player_callback(): {selector}, {value}') + if self.player is None: + print(f'player_callback():self.player is None, {selector}, {value}') + return + if selector == 'quit': + def close(*args): + self.quitted = True + self.unload() + close() + + elif selector == 'eof': + self._do_eos() + + def load(self): + if self.source is None: + return + source = self.source + self.unload() + ff_opts = {'vn':True, 'sn':True} + self.player = MediaPlayer(source, + callback=self.player_callback, + loglevel='info', + ff_opts=ff_opts) + player = self.player + self.state = 'pause' + s = time.perf_counter() + while (player.get_metadata()['duration'] is None and + not self.quitted and + time.perf_counter() - s < 10.): + time.sleep(0.005) + player.toggle_pause() + time.sleep(0.04) + player.set_volume(self.volume) + if self.autoplay: + self.play() + + def unload(self): + self.player = None + self.state = 'stop' + self.quitted = False + + def __del__(self): + self.unload() + + def play(self): + if self.player is None: + self.load() + if self.player is None: + print('play():self.player is None') + return + if self.state == 'play': + return + self.player.toggle_pause() + self.state = 'play' + + def pause(self): + if self.player is None: + self.load() + if self.player is None: + print('pause():self.player is None') + return + if self.state == 'pause': + return + self.player.toggle_pause() + self.state = 'pause' + + def is_busy(self): + if self.player and self.state == 'play': + return True + return False + + def stop(self): + if self.player is None: + return + if self.state == 'play': + self.player.toggle_pause() + self.state = 'stop' + self.seek(0) + if self.on_stop: + self.on_stop() + + def seek(self, pos): + if self.player is None: + print('seek():self.player is None') + return + self.player.seek(pos, relative=False) + + def get_pos(self): + if self.player is None: + return 0 + return self.player.get_pts() + + def _do_eos(self, *args): + print('_do_eos() called ...') + if self.loop: + self.seek(0.) + else: + print('go stop') + self.stop() + + +if __name__ == '__main__': + import sys + p = AudioPlayer(autoplay=True, loop=True) + p.source = sys.argv[1] + p.load() + p.play() + while True: + while p._pump(): + pass + + print(""" +play: play it, +stop: stop play +pause:pause it +quit: exit +""") + x = input() + if x == 'quit': + p.quitted = True + p.stop() + break + if x == 'play': + p.play() + continue + if x == 'stop': + p.stop() + continue + if x == 'pause': + p.pause() + continue + diff --git a/appPublic/background.py b/appPublic/background.py new file mode 100755 index 0000000..94846d1 --- /dev/null +++ b/appPublic/background.py @@ -0,0 +1,11 @@ +from threading import Thread + +class Background(Thread): + def __init__(self,func, *args,**kw): + Thread.__init__(self) + self.__callee = func + self.__args = args + self.__kw = kw + + def run(self): + return self.__callee(*self.__args, **self.__kw) diff --git a/appPublic/country_cn_en.py b/appPublic/country_cn_en.py new file mode 100755 index 0000000..0dbfdd6 --- /dev/null +++ b/appPublic/country_cn_en.py @@ -0,0 +1,247 @@ +# -*- coding = utf-8 -*- +ecc = { +"Afghanistan":"阿富汗", +"Aland Islands":"奥兰群岛", +"Albania":"阿尔巴尼亚", +"Algeria":"阿尔及利亚", +"American Samoa":"美属萨摩亚", +"Andorra":"安道尔", +"Angola":"安哥拉", +"Anguilla":"安圭拉", +"Antigua and Barbuda":"安提瓜和巴布达", +"Argentina":"阿根廷", +"Armenia":"亚美尼亚", +"Aruba":"阿鲁巴", +"Australia":"澳大利亚", +"Austria":"奥地利", +"Azerbaijan":"阿塞拜疆", +"Bangladesh":"孟加拉", +"Bahrain":"巴林", +"Bahamas":"巴哈马", +"Barbados":"巴巴多斯", +"Belarus":"白俄罗斯", +"Belgium":"比利时", +"Belize":"伯利兹", +"Benin":"贝宁", +"Bermuda":"百慕大", +"Bhutan":"不丹", +"Bolivia":"玻利维亚", +"Bosnia and Herzegovina":"波斯尼亚和黑塞哥维那", +"Botswana":"博茨瓦纳", +"Bouvet Island":"布维岛", +"Brazil":"巴西", +"Brunei":"文莱", +"Bulgaria":"保加利亚", +"Burkina Faso":"布基纳法索", +"Burundi":"布隆迪", +"Cambodia":"柬埔寨", +"Cameroon":"喀麦隆", +"Canada":"加拿大", +"Cape Verde":"佛得角", +"Central African Republic":"中非", +"Chad":"乍得", +"Chile":"智利", +"Christmas Islands":"圣诞岛", +"Cocos (keeling) Islands":"科科斯(基林)群岛", +"Colombia":"哥伦比亚", +"Comoros":"科摩罗", +"Congo (Congo-Kinshasa)":"刚果(金)", +"Congo":"刚果", +"Cook Islands":"库克群岛", +"Costa Rica":"哥斯达黎加", +"Cote D’Ivoire":"科特迪瓦", +"China":"中国", +"Croatia":"克罗地亚", +"Cuba":"古巴", +"Czech":"捷克", +"Cyprus":"塞浦路斯", +"Denmark":"丹麦", +"Djibouti":"吉布提", +"Dominica":"多米尼加", +"East Timor":"东帝汶", +"Ecuador":"厄瓜多尔", +"Egypt":"埃及", +"Equatorial Guinea":"赤道几内亚", +"Eritrea":"厄立特里亚", +"Estonia":"爱沙尼亚", +"Ethiopia":"埃塞俄比亚", +"Faroe Islands":"法罗群岛", +"Fiji":"斐济", +"Finland":"Finland", +"France":"法国", +"Franch Metropolitan":"法国大都会", +"Franch Guiana":"法属圭亚那", +"French Polynesia":"法属波利尼西亚", +"Gabon":"加蓬", +"Gambia":"冈比亚", +"Georgia":"格鲁吉亚", +"Germany":"德国", +"Ghana":"加纳", +"Gibraltar":"直布罗陀", +"Greece":"希腊", +"Grenada":"格林纳达", +"Guadeloupe":"瓜德罗普岛", +"Guam":"关岛", +"Guatemala":"危地马拉", +"Guernsey":"根西岛", +"Guinea-Bissau":"几内亚比绍", +"Guinea":"几内亚", +"Guyana":"圭亚那", +"Hong Kong":"香港 (中国)", +"Haiti":"海地", +"Honduras":"洪都拉斯", +"Hungary":"匈牙利", +"Iceland":"冰岛", +"India":"印度", +"Indonesia":"印度尼西亚", +"Iran":"伊朗", +"Iraq":"伊拉克", +"Ireland":"爱尔兰", +"Isle of Man":"马恩岛", +"Israel":"以色列", +"Italy":"意大利", +"Jamaica":"牙买加", +"Japan":"日本", +"Jersey":"泽西岛", +"Jordan":"约旦", +"Kazakhstan":"哈萨克斯坦", +"Kenya":"肯尼亚", +"Kiribati":"基里巴斯", +"Korea (South)":"韩国", +"Korea (North)":"朝鲜", +"Kuwait":"科威特", +"Kyrgyzstan":"吉尔吉斯斯坦", +"Laos":"老挝", +"Latvia":"拉脱维亚", +"Lebanon":"黎巴嫩", +"Lesotho":"莱索托", +"Liberia":"利比里亚", +"Libya":"利比亚", +"Liechtenstein":"列支敦士登", +"Lithuania":"立陶宛", +"Luxembourg":"卢森堡", +"Macau":"澳门(中国)", +"Macedonia":"马其顿", +"Malawi":"马拉维", +"Malaysia":"马来西亚", +"Madagascar":"马达加斯加", +"Maldives":"马尔代夫", +"Mali":"马里", +"Malta":"马耳他", +"Marshall Islands":"马绍尔群岛", +"Martinique":"马提尼克岛", +"Mauritania":"毛里塔尼亚", +"Mauritius":"毛里求斯", +"Mayotte":"马约特", +"Mexico":"墨西哥", +"Micronesia":"密克罗尼西亚", +"Moldova":"摩尔多瓦", +"Monaco":"摩纳哥", +"Mongolia":"蒙古", +"Montenegro":"黑山", +"Montserrat":"蒙特塞拉特", +"Morocco":"摩洛哥", +"Mozambique":"莫桑比克", +"Myanmar":"缅甸", +"Namibia":"纳米比亚", +"Nauru":"瑙鲁", +"Nepal":"尼泊尔", +"Netherlands":"荷兰", +"New Caledonia":"新喀里多尼亚", +"New Zealand":"新西兰", +"Nicaragua":"尼加拉瓜", +"Niger":"尼日尔", +"Nigeria":"尼日利亚", +"Niue":"纽埃", +"Norfolk Island":"诺福克岛", +"Norway":"挪威", +"Oman":"阿曼", +"Pakistan":"巴基斯坦", +"Palau":"帕劳", +"Palestine":"巴勒斯坦", +"Panama":"巴拿马", +"Papua New Guinea":"巴布亚新几内亚", +"Paraguay":"巴拉圭", +"Peru":"秘鲁", +"Philippines":"菲律宾", +"Pitcairn Islands":"皮特凯恩群岛", +"Poland":"波兰", +"Portugal":"葡萄牙", +"Puerto Rico":"波多黎各", +"Qatar":"卡塔尔", +"Reunion":"留尼汪岛", +"Romania":"罗马尼亚", +"Rwanda":"卢旺达", +"Russian Federation":"俄罗斯联邦", +"Saint Helena":"圣赫勒拿", +"Saint Kitts-Nevis":"圣基茨和尼维斯", +"Saint Lucia":"圣卢西亚", +"Saint Vincent and the Grenadines":"圣文森特和格林纳丁斯", +"El Salvador":"萨尔瓦多", +"Samoa":"萨摩亚", +"San Marino":"圣马力诺", +"Sao Tome and Principe":"圣多美和普林西比", +"Saudi Arabia":"沙特阿拉伯", +"Senegal":"塞内加尔", +"Seychelles":"塞舌尔", +"Sierra Leone":"塞拉利昂", +"Singapore":"新加坡", +"Serbia":"塞尔维亚", +"Slovakia":"斯洛伐克", +"Slovenia":"斯洛文尼亚", +"Solomon Islands":"所罗门群岛", +"Somalia":"索马里", +"South Africa":"南非", +"Spain":"西班牙", +"Sri Lanka":"斯里兰卡", +"Sudan":"苏丹", +"Suriname":"苏里南", +"Swaziland":"斯威士兰", +"Sweden":"瑞典", +"Switzerland":"瑞士", +"Syria":"叙利亚", +"Tajikistan":"塔吉克斯坦", +"Tanzania":"坦桑尼亚", +"Taiwan":"台湾 (中国)", +"Thailand":"泰国", +"Trinidad and Tobago":"特立尼达和多巴哥", +"Timor-Leste":"东帝汶", +"Togo":"多哥", +"Tokelau":"托克劳", +"Tonga":"汤加", +"Tunisia":"突尼斯", +"Turkey":"土耳其", +"Turkmenistan":"土库曼斯坦", +"Tuvalu":"图瓦卢", +"Uganda":"乌干达", +"Ukraine":"乌克兰", +"United Arab Emirates":"阿拉伯联合酋长国", +"United Kingdom":"英国", +"United States":"美国", +"Uruguay":"乌拉圭", +"Uzbekistan":"乌兹别克斯坦", +"Vanuatu":"瓦努阿图", +"Vatican City":"梵蒂冈", +"Venezuela":"委内瑞拉", +"Vietnam":"越南", +"Wallis and Futuna":"瓦利斯群岛和富图纳群岛", +"Western Sahara":"西撒哈拉", +"Yemen":"也门", +"Yugoslavia":"南斯拉夫", +"Zambia":"赞比亚", +"Zimbabwe":"津巴布韦" +} + +cec = {v:k for k,v in ecc.items()} + +def get_en_country_name(country): + x = cec.get(country) + if x: + return x + return country + +def get_cn_country_name(country): + x = ecc.get(country) + if x: + return x + return country diff --git a/appPublic/csv_Data.py b/appPublic/csv_Data.py new file mode 100755 index 0000000..4f950d9 --- /dev/null +++ b/appPublic/csv_Data.py @@ -0,0 +1,54 @@ +import codecs +import csv +class Reader: + def __init__(self,f,delimiter): + self.f = f + self.delimiter = delimiter + self.line = 0 + + def __iter__(self): + return self + + def next(self): + l = self.f.readline() + if l == '': + raise StopIteration() + while l[-1] in [ '\n','\r']: + l = l[:-1] + r = [ i if i != '' else None for i in l.split(self.delimiter) ] + self.line = self.line + 1 + return r + +class CSVData: + def __init__(self,filename,coding='utf8',delimiter=','): + self.filename = filename + self.coding = coding + self.f = codecs.open(filename,'rb',self.coding) + self.reader = Reader(self.f,delimiter) + self.fields = self.reader.next() + + def __del__(self): + self.f.close() + + def __iter__(self): + return self + + def next(self): + try: + r = self.reader.next() + if len(r) != len(self.fields): + print("length diff",len(r),len(self.fields),"at line %d" % self.reader.line) + raise StopIteration() + d = {} + [d.update({self.fields[i]:r[i]}) for i in range(len(self.fields))] + return d + except: + raise StopIteration() + +if __name__ == '__main__': + import sys + cd = CSVData(sys.argv[1]) + for r in cd: + print(r) + + diff --git a/appPublic/dataencoder.py b/appPublic/dataencoder.py new file mode 100755 index 0000000..5c8b18e --- /dev/null +++ b/appPublic/dataencoder.py @@ -0,0 +1,170 @@ +try: + import ujson as json +except: + import json +from appPublic.rsawrap import RSA +from appPublic.rc4 import RC4 +from appPublic.uniqueID import getID + +# import brotli +import zlib +import struct + +DATA_TYPE_BYTES = 1 +DATA_TYPE_STR = 2 +DATA_TYPE_JSON = 3 + +class DataEncoder: + """ + security data packing - unpacking object + packs data: + encode data with random key's rc4 crypt algorithm, + encode rc4's key with receiver's public key + sign data with sender's private key + packs data using struct in follows order + 0: data format(18 bytes) + 1. datatype(c) + 2. encoded data(length=len(d)) + 3. encoded_rc4key(length=len(k)) + 4. sign(signs from (0+1+2+3) data) (length=len(s)) + 5. compress data and return compressed dta + return packed data + unpacks data: + 0. decompress data + 1. get 18 bytes fmt data, erase tails b'\x00' + 2. using fmt to unpack data[18:] + 3. verify sign + 4. decode k + 5. decode data usig decoded k with rc4 algorithm + 6. convert data type to origin data type + 7. return converted data + """ + def __init__(self, myid, func_get_peer_pubkey, private_file=None): + self.myid = myid + self.func_get_peer_pubkey = func_get_peer_pubkey + self.public_keys = {} + self.private_file = private_file + self.rsa = RSA() + self.rc4 = RC4() + if self.private_file: + self.private_key = self.rsa.read_privatekey(self.private_file) + else: + self.private_key = self.rsa.create_privatekey() + self.public_key = self.rsa.create_publickey(self.private_key) + + def identify_datatype(self, data): + if isinstance(data, bytes): + return DATA_TYPE_BYTES, data + if isinstance(data, str): + return DATA_TYPE_STR, data.encode('utf-8') + data = json.dumps(data).encode('utf-8') + return DATA_TYPE_JSON, data + + def my_text_publickey(self): + return self.rsa.publickeyText(self.public_key) + + def exist_peer_publickeys(self, peer_id): + return True if self.public_keys.get(peer_id, False) else False + + def set_peer_pubkey(self, peer_id, pubkey): + self.public_keys[peer_id] = pubkey + + def get_peer_text_pubkey(self, peer_id): + pk = self.get_peer_pubkey() + txtpk = self.rsa. publickeyText(pk) + return txtpk + + def set_peer_text_pubkey(self, peer_id, text_pubkey): + pk = self.rsa.publickeyFromText(text_pubkey) + self.set_peer_pubkey(peer_id, pk) + + def get_peer_pubkey(self, peer_id): + pubkey = self.public_keys.get(peer_id) + if not pubkey: + try: + self.func_get_peer_pubkey(peer_id) + except: + raise Exception('Can not get peer public key(%s)') + pubkey = self.public_keys.get(peer_id) + return pubkey + + def pack(self, peer_id, data, uncrypt=False): + t, d = self.identify_datatype(data) + if uncrypt: + return zlib.compress(b'\x00' * 18 + \ + bytes(chr(t),'utf-8') + \ + d) + pk = self.get_peer_pubkey(peer_id) + d, k = self.encode_data(pk, d) + f = 'b%05ds%03ds' % (len(d), len(k)) + f1 = f + '256s' + pd1 = struct.pack('18s', f1.encode('utf-8')) + pd2 = struct.pack(f, t, d, k) + pd = pd1 + pd2 + s = self.sign_data(pd) + pd += s + self.pack_d = [t,d,k,s] + origin_len = len(pd) + pd = zlib.compress(pd) + return pd + + def unpack(self, peer_id, data): + data = zlib.decompress(data) + if data[:18] == b'\x00' * 18: + data = data[18:] + t = ord(chr(data[0])) + d = data[1:] + if t == DATA_TYPE_BYTES: + return d + d = d.decode('utf-8') + if t == DATA_TYPE_STR: + return d + return json.loads(d) + + org_data = data + pk = self.get_peer_pubkey(peer_id) + f = data[:18] + while f[-1] == 0 and len(f) > 0: + f = f[:-1] + f = f.decode('utf-8') + data = data[18:] + t, d, k, s = struct.unpack(f, data) + self.unpack_d = [t,d,k,s] + data1 = org_data[:org_data.index(s)] + if not self.verify_sign(data1, s, pk): + raise Exception('data sign verify failed') + data = self.decode_data(d, k) + if t == DATA_TYPE_BYTES: + return data + if t == DATA_TYPE_STR: + return data.decode('utf-8') + return json.loads(data) + + def encode_data(self, peer_pubkey, data): + key = getID() + if isinstance(key, str): + key = key.encode('utf-8') + ctext = self.rc4.encode_bytes(data, key) + encoded_key = self.rsa.encode_bytes(peer_pubkey, key) + return ctext, encoded_key + + def sign_data(self, data): + return self.rsa.sign_bdata(self.private_key, data) + + def decode_data(self, data, encoded_key): + key = self.rsa.decode_bytes(self.private_key, encoded_key) + return self.rc4.decode_bytes(data, key) + + def verify_sign(self, data, sign, peer_pubkey): + return self.rsa.check_sign_bdata(peer_pubkey, data, sign) + +def quotedstr(s): + def conv(c): + if c == '"': + return '\\"' + if c == '\n': + return '\\n' + return c + x = [ conv(c) for c in s ] + return ''.join(x) + diff --git a/appPublic/datamapping.py b/appPublic/datamapping.py new file mode 100755 index 0000000..3179e03 --- /dev/null +++ b/appPublic/datamapping.py @@ -0,0 +1,50 @@ +#dataMapping +from appPublic.dictObject import DictObject + +def keyMapping(dic,mappingtab,keepmiss=True): + """ + keyMapping mappingtab + { + "a1":"b1", + "a2":'b2", + ... + } + "an" is key in dic + "bn" is key in result dictionary + """ + ret = {} + keys = [ k for k in dic.keys()] + if not keepmiss: + keys = [ k for k in dic.keys() if k in mappingtab.keys() ] + [ ret.update({mappingtab.get(k,k):dic[k]}) for k in keys ] + return ret + + +def valueMapping(dic,mappingtab): + """ + mappingtab format: + { + "field1":{ + "a":"1", + "b":"2", + "__default__":"5" + }, + "field2":{ + "a":"3", + "b":"4" + } + } + field1,field2 is in dic.keys() + """ + ret = {} + for k in dic.keys(): + mt = mappingtab.get(k,None) + if mt is None: + ret[k] = dic[k] + else: + dv = mt.get('__default__',dic[k]) + v = mt.get(dic[k],dv) + ret[k] = v + + return DictObject(**ret) + diff --git a/appPublic/dictExt.py b/appPublic/dictExt.py new file mode 100755 index 0000000..52f247c --- /dev/null +++ b/appPublic/dictExt.py @@ -0,0 +1,38 @@ + +def arrayExtend(s,addon): + ret = [] + s_cnt = len(s) + a_cnt = len(addon) + for i,v in enumerate(addon): + if i < s_cnt: + if type(v)!=type(s[i]): + ret.append(v) + continue + if isinstance(v,dict): + x = dictExtend(v,s[i]) + ret.append(x) + continue + ret.append(v) + if s_cnt < a_cnt: + ret += s[i:] + return ret + +def dictExtend(s,addon): + ret = {} + ret.update(s) + skeys = ret.keys() + for k,v in addon.items(): + if k not in skeys: + ret[k] = v + continue + if type(v)!=type(ret[k]): + ret[k] = v + continue + if type(v)==type({}): + ret[k] = dictExtend(ret[k],v) + continue + if type(v)==type([]): + ret[k] = arrayExtend(ret[k],v) + continue + ret[k] = v + return ret diff --git a/appPublic/dictObject.old.py b/appPublic/dictObject.old.py new file mode 100755 index 0000000..cd7e798 --- /dev/null +++ b/appPublic/dictObject.old.py @@ -0,0 +1,161 @@ +import json +from json import JSONEncoder +from inspect import ismethod, isfunction, isbuiltin, isabstract + +def multiDict2Dict(md): + ns = {} + for k,v in md.items(): + ov = ns.get(k,None) + if ov is None: + ns[k] = v + elif type(ov) == type([]): + ov.append(v) + ns[k] = ov + else: + ns[k] = [ov,v] + return ns + +class DictObject: + def __init__(self,**kw): + self.org_keys__ = [] + self.org_keys__ = [ k for k in self.__dict__.keys()] + for k,v in kw.items(): + self.update({k:self.__DOitem(v)}) + + def __getattr__(self,name): + if name in self._addon().keys(): + return self.__getitem__(name) + return None + + def update(self,kw): + self.__dict__.update(kw) + + def _addon(self): + ks = [ k for k in self.__dict__.keys() if k not in self.org_keys__] + return {k:v for k,v in self.__dict__.items() if k in ks} + + def clear(self): + for k in self._addon().keys(): + self.__dict__.pop(k) + + def get(self,name,default=None): + return self._addon().get(name,default) + + def pop(self,k,default=None): + return self.__dict__.pop(k,default) + + def popitem(self): + return self.__dict__.popitem() + + def items(self): + return self._addon().items() + + def keys(self): + return self._addon().keys() + + def values(self): + return self._addon().values() + + def __delitem__(self,key): + self.pop(key) + + def __getitem__(self,name): + return self._addon().get(name) + + def __setitem__(self,name,value): + self.__dict__[name] = value + + def __str__(self): + return str(self._addon()) + + def __expr__(self): + return self.addon().__expr__() + + def copy(self): + return {k:v for k,v in self._addon().items()} + + def to_dict(self): + d = self._addon() + newd = self.dict_to_dict(d) + return newd + + def dict_to_dict(self,dic): + d = {} + for k,v in dic.items(): + if isinstance(v,DictObject): + d[k] = v.to_dict() + elif isinstance(v,dict): + d[k] = self.dict_to_dict(v) + elif isinstance(v,list): + d[k] = self.array_to_dict(v) + elif k == '__builtins__': + pass + elif isbuiltin(v) or isfunction(v) or ismethod(v) or isabstract(v): + pass + else: + d[k] = v + return d + + def array_to_dict(self,v): + r = [] + for i in v: + if isinstance(i,list): + r.append(self.array_to_dict(i)) + elif isinstance(i,dict): + r.append(self.dict_to_dict(i)) + elif isinstance(i,DictObject): + r.append(i.to_dict()) + elif isbuiltin(i) or isfunction(i) or ismethod(i) or isabstract(i): + pass + else: + r.append(i) + return r + + @classmethod + def isMe(self,name): + return name == 'DictObject' + + def __DOArray(self,a): + b = [ self.__DOitem(i) for i in a ] + return b + + def __DOitem(self, i): + if isinstance(i,DictObject): + return i + if isinstance(i,dict): + i = {k:v for k,v in i.items() if isinstance(k,str)} + try: + d = DictObject(**i) + return d + except Exception as e: + print("****************",i,"*******dictObject.py") + raise e + if type(i) == type([]) or type(i) == type(()) : + return self.__DOArray(i) + return i + +class DictObjectEncoder(JSONEncoder): + def default(self, o): + return o._addon() + + + +def dictObjectFactory(_klassName__,**kwargs): + def findSubclass(_klassName__,klass): + for k in klass.__subclasses__(): + if k.isMe(_klassName__): + return k + k1 = findSubclass(_klassName__,k) + if k1 is not None: + return k1 + return None + try: + if _klassName__=='DictObject': + return DictObject(**kwargs) + k = findSubclass(_klassName__,DictObject) + if k is None: + return DictObject(**kwargs) + return k(**kwargs) + except Exception as e: + print("dictObjectFactory()",e,_klassName__) + raise e diff --git a/appPublic/dictObject.py b/appPublic/dictObject.py new file mode 100755 index 0000000..97be47c --- /dev/null +++ b/appPublic/dictObject.py @@ -0,0 +1,130 @@ +import json +from json import JSONEncoder +from inspect import ismethod, isfunction, isbuiltin, isabstract + +def multiDict2Dict(md): + ns = {} + for k,v in md.items(): + ov = ns.get(k,None) + if ov is None: + ns[k] = v + elif type(ov) == type([]): + ov.append(v) + ns[k] = ov + else: + ns[k] = [ov,v] + return ns + +class DictObjectEncoder(JSONEncoder): + def default(self, o): + return o._addon() + +class DictObject(dict): + def __getattr__(self, attr): + """ + 实现点操作符访问字典中的键值对 + """ + try: + v = self.__DOitem(self[attr]) + self[attr] = v + return v + except KeyError: + return None + + def has(self, key): + try: + v = super().__getitem__(key) + return True + except KeyError: + return False + + def get_data_by_keys(self, keys): + try: + ks = keys.split('.', 1) + if '[' not in ks[0]: + d = getattr(self, ks[0]) + if len(ks) == 1: + return d + if isinstance(d, DictObject): + return d.get_data_by_keys(ks[1]) + return None + ks1 = ks[0].split('[', 1) + k = ks1[0] + idx = int(ks1[1].split(']',1)[0]) + d = getattr(self, k)[idx] + if len(ks) == 1: + return d + if isinstance(d, DictObject): + return d.get_data_by_keys(ks[1]) + return None + except: + return None + + def __getitem__(self, key): + try: + v = self.__DOitem(super().__getitem__(key)) + self[key] = v + return v + except KeyError: + return None + + def __setattr__(self, attr, value): + """ + 实现点操作符设置字典中的键值对 + """ + self[attr] = value + + def get(self, k, defv=None): + if self.has(k): + return self[k] + else: + return defv + + def copy(self): + return self.__DOitem(super().copy()) + + @classmethod + def isMe(self,name): + return name == 'DictObject' + + def to_dict(self): + return self + + def __DOArray(self,a): + b = [ self.__DOitem(i) for i in a ] + return b + + def __DOitem(self, i): + if isinstance(i,DictObject): + return i + if isinstance(i,dict): + i = {k:v for k,v in i.items() if isinstance(k,str)} + try: + d = DictObject(**i) + return d + except Exception as e: + raise e + if type(i) == type([]) or type(i) == type(()) : + return self.__DOArray(i) + return i + +def dictObjectFactory(_klassName__,**kwargs): + def findSubclass(_klassName__,klass): + for k in klass.__subclasses__(): + if k.isMe(_klassName__): + return k + k1 = findSubclass(_klassName__,k) + if k1 is not None: + return k1 + return None + try: + if _klassName__=='DictObject': + return DictObject(**kwargs) + k = findSubclass(_klassName__,DictObject) + if k is None: + return DictObject(**kwargs) + return k(**kwargs) + except Exception as e: + print("dictObjectFactory()",e,_klassName__) + raise e + diff --git a/appPublic/easyExcel.py b/appPublic/easyExcel.py new file mode 100755 index 0000000..051f2f1 --- /dev/null +++ b/appPublic/easyExcel.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from win32com.client import Dispatch +import win32com.client + +class EasyExcel: + """A utility to make it easier to get at Excel. Remembering + to save the data is your problem, as is error handling. + Operates on one workbook at a time.""" + + def __init__(self, filename=None): + self.xlApp = win32com.client.Dispatch('Excel.Application') + if filename: + self.filename = filename + self.xlBook = self.xlApp.Workbooks.Open(filename) + #self.xlBook.Visible = False + else: + self.xlBook = self.xlApp.Workbooks.Add() + self.filename = '' + + def save(self, newfilename=None): + if newfilename: + self.filename = newfilename + self.xlBook.SaveAs(newfilename) + else: + self.xlBook.Save() + + def setSheetName(self,sheet,name): + self.xlBook.Worksheets(sheet).Name = name + + + def newSheet(self,sheetName): + pass + + def close(self): + self.xlBook.Close(SaveChanges=0) + self.xlApp.Quit() + del self.xlApp + + def getCell(self, sheet, row, col): + "Get value of one cell" + sht = self.xlBook.Worksheets(sheet) + return sht.Cells(row, col).Value + + def setCell(self, sheet, row, col, value): + "set value of one cell" + sht = self.xlBook.Worksheets(sheet) + sht.Cells(row, col).Value = value + + def getRange(self, sheet, row1, col1, row2, col2): + "return a 2d array (i.e. tuple of tuples)" + sht = self.xlBook.Worksheets(sheet) + return sht.Range(sht.Cells(row1, col1), sht.Cells(row2, col2)).Value + + def addPicture(self, sheet, pictureName, Left, Top, Width, Height): + "Insert a picture in sheet" + sht = self.xlBook.Worksheets(sheet) + sht.Shapes.AddPicture(pictureName, 1, 1, Left, Top, Width, Height) + + def cpSheet(self, before): + "copy sheet" + shts = self.xlBook.Worksheets + shts(1).Copy(None,shts(1)) + +if __name__ == "__main__": + PNFILE = r'c:\screenshot.bmp' + xls = EasyExcel(r'D:\test.xls') + xls.addPicture('Sheet1', PNFILE, 20,20,1000,1000) + xls.cpSheet('Sheet1') + xls.save() + xls.close() \ No newline at end of file diff --git a/appPublic/exceldata.py b/appPublic/exceldata.py new file mode 100755 index 0000000..76de2c2 --- /dev/null +++ b/appPublic/exceldata.py @@ -0,0 +1,281 @@ +import xlrd +import os +import sys +import datetime + +from appPublic.strUtils import * + +TCS={ + 'int':int, + 'float':float, + 'str':str, +} + +def isEmptyCell(cell): + return cell.ctype == xlrd.XL_CELL_EMPTY + +def isCommentValue(v): + if type(v)==type('') and v[0] == '#': + return True + return False + +def purekey(k): + return k.split(':')[0] + +def castedValue(v,k): + ki = k.split(':') + if len(ki)<2 or v is None: + return v + ki = ki[1:] + if 'list' in ki: + if type(v) == type(''): + v = v.split(',') + elif type(v) != type([]): + v = [v] + + for k,tc in TCS.items(): + if k in ki: + if type(v) == type([]): + return [ tc(i) for i in v ] + else: + return tc(v) + return v + +class ExcelData: + _indictors = { + ':__dict__':'ff', + ':__list__':'ff', + ':__include__':'ff', + } + + def __init__(self,xlsfile,encoding='UTF8',startrow=0,startcol=0): + self._book = xlrd.open_workbook(xlsfile) + self.encoding = encoding + self._filename = xlsfile + self.startrow=0 + self.startcol=0 + self._dataset = self.dataload() + + def __del__(self): + del self._book + del self._dataset + + def cellvalue(self,sheet,x,y): + if sheet.cell_type(x,y)==xlrd.XL_CELL_EMPTY: + return None + if sheet.cell_type(x,y)==xlrd.XL_CELL_DATE: + y,m,d,hh,mm,ss = xlrd.xldate_as_tuple(sheet.cell_value(x,y),self._book.datemode) + return datetime.date(y,m,d) + s = sheet.cell_value(x,y) + return self.trimedValue(s) + + def isCommentCell(self,cell): + if isEmptyCell(cell): + return False + v = self.trimedValue(cell.value) + return isCommentValue(v) + + def dateMode(self): + return self._book.datemode + + def trimedValue(self,v): + if type(v) == type(u' '): + v = v.encode(self.encoding) + if type(v) == type(''): + v = lrtrim(v) + return v + def dataload(self): + dat = {} + for name in self._book.sheet_names(): + sheet = self._book.sheet_by_name(name) + #name = name.encode(self.encoding) + dat[self.trimedValue(name)] = self.loadSheetData(sheet) + return dat + + def findDataRange(self,sheet,pos,maxr): + x,y = pos + j = y + 1 + while j < sheet.ncols: + if isEmptyCell(sheet.cell(x,j)) or self.isCommentCell(sheet.cell(x,y)): + maxy = j + break + j += 1 + i = x + 1 + maxx = maxr + while i < maxr: + if not isEmptyCell(sheet.cell(i,y)): + maxx = i + break + i += 1 + return maxx + + def loadSheetData(self,sheet): + return self.loadSheetDataRange(sheet,(self.startrow,self.startcol),sheet.nrows) + + def include(self,filename,id): + try: + sub = ExcelData(filename,self.encoding) + except Exception as e: + print(e,filename) + return None + if id is None: + return sub.dict() + env = {'data':sub.dict()} + try: + exec("""resutl__ = data%s""" % id,globals(),env) + except Exception as e: + print(e,id) + return None + return env['resutl__'] + + def loadSingleData(self,sheet,pos): + x,y = pos + if sheet.ncols==y: + v = self.cellvalue(sheet,x,y) + if isCommentValue(v): + return None + return v + ret = [] + while y < sheet.ncols: + v = self.cellvalue(sheet,x,y) + if v is None: + break + if isCommentValue(v): + break + ret.append(v) + y += 1 + + if len(ret) < 1: + return None + + if len(ret)<2: + return ret[0] + if ret[0] == '__include__': + if len(ret)<2: + print("include mode error: __include__ filename id") + return None + id = None + if len(ret)>=3: + id = ret[2] + return self.include(ret[1],id) + return ret + + def loadDictData(self,sheet,pos,maxr): + ret = {} + x,y = pos + while x < maxr: + mr = self.findDataRange(sheet,(x,y),maxr) + #print "loadDictData:debug:",x,y,maxr,mr + k = self.cellvalue(sheet,x,y) + if isCommentValue(k): + x = x + 1 + continue + if k is not None: + if 'records' in k.split(':'): + v = self.loadRecords(sheet,(x,y+1),maxr) + else: + v = self.loadSheetDataRange(sheet,(x,y+1),mr) + ret[purekey(k)] = castedValue(v,k) + x = mr + + return ret + + def loadSheetDataRange(self,sheet,pos,maxr): + x,y = pos + #print "debug1:",pos,maxr + if maxr - x < 1 : + #print "debug1-1:",pos,maxr + return None + if isEmptyCell(sheet.cell(x,y)): + #print "debug1-2:",pos,maxr + return None + + cv = self.cellvalue(sheet,x,y) + #print cv + if isCommentValue(cv): + pos = (x+1,y) + return self.loadSheetDataRange(sheet,pos,maxr) + + if cv == '__include__': + return self.include(self.cellvalue(sheet,x,y+1),self.cellvalue(sheet,x,y+2)) + + if cv == '__dict__': + #print "cv==__dict__" + i = x + 1 + vs = [] + while i < maxr: + v = self.cellvalue(sheet,i,y) + if v == '__dict__': + vs.append(self.loadDictData(sheet,(x+1,y),i)) + x = i + i += 1 + vs.append(self.loadDictData(sheet,(x+1,y),i)) + if len(vs) < 1: + return None + if len(vs) < 2: + return vs[0] + return vs + return self.loadDictData(sheet,(x+1,y),maxr) + + if cv == '__list__': + i = x + 1 + vs = [] + while i < maxr: + v = self.loadSingleData(sheet,(i,y)) + vs.append(v) + i += 1 + return vs + + if maxr - x < 2: + v = self.loadSingleData(sheet,(x,y)) + return v + + return self.loadRecords(sheet,pos,maxr) + + def loadRecords(self,sheet,pos,maxr): + x,y = pos + v = self.cellvalue(sheet,x,y) + if v==None or isCommentValue(v): + return self.loadRecords(sheet,(x+1,y),maxr) + + data = [] + i = x + 1 + j = y + keys = [ self.trimedValue(k.value) for k in sheet.row(x)[y:] ] + while i < maxr: + d = {} + j = y + while j < sheet.ncols: + k = self.cellvalue(sheet,x,j) + if k is None or isCommentValue(k): + break + if sheet.cell_type(x,j) == xlrd.XL_CELL_EMPTY: + break + v = self.cellvalue(sheet,i,j) + if sheet.cell_type(x,j) != xlrd.XL_CELL_EMPTY: + d[purekey(k)] = castedValue(v,k) + j += 1 + data.append(d) + i += 1 + return data + + def dict(self): + return self._dataset + +class ExcelDataL(ExcelData): + def dataload(self): + ret = [] + for name in self._book.sheet_names(): + dat = {} + sheet = self._book.sheet_by_name(name) + name = name.encode(self.encoding) + dat[name] = self.loadSheetData(sheet) + ret.append(dat) + return ret + +if __name__ == '__main__': + if len(sys.argv)<2: + print("Usage:\n%s execlfile" % sys.argv[0]) + sys.exit(1) + ed = ExcelData(sys.argv[1]) + print(ed.dict()) diff --git a/appPublic/excelwriter.py b/appPublic/excelwriter.py new file mode 100755 index 0000000..5ba30da --- /dev/null +++ b/appPublic/excelwriter.py @@ -0,0 +1,144 @@ +import xlwt +from appPublic.strUtils import * + +class ExcelWriter: + def __init__(self,encoding='gb2312'): + self.encoding = encoding + + def writeV(self,sheet,x,y,v): + if type(v) == type([]): + return self.writeList(sheet,x,y,v) + + if type(v) == type({}): + return self.writeDict(sheet,x,y,v) + + if type(v) not in (type({}),type([])): + if type(v) == type(' '): + v = lrtrim(v) + sheet.write(x,y,v) + return 1 + + def write(self,excelfile,dictdata): + wb = xlwt.Workbook(encoding=self.encoding) + for tbl in dictdata.keys(): + ws = wb.add_sheet(tbl,cell_overwrite_ok=True) + self.writeV(ws,0,0,dictdata[tbl]) + wb.save(excelfile) + + def createRecordTitle(self,ws,x,y,title,poss,isList=False): + if isList: + poss['__list__'][title] = True + + if title in poss.keys(): + return + + if len(poss.keys()) > 1: + d_ = {} + for k,v in poss.items(): + if k != '__list__': + d_[k] = v + y = max(d_.values()) + 1 + # ws.write(x,y,title) + poss[title] = y + + def writeRecordTitle(self,ws,x,poss): + for k in poss.keys(): + if k == '__list__': + continue + if k in poss['__list__'].keys(): + ws.write(x,poss[k],k+':list') + else: + ws.write(x,poss[k],k) + + def writeRecords(self,ws,x,y,alist): + ox = x + oy = y + poss = {'__list__':{}} + x = ox + 1 + for r in alist: + for k,v in r.items(): + isList = False + if type(v) == type([]): + isList = True + v = ','.join(v) + self.createRecordTitle(ws,ox,oy,k,poss,isList) + ws.write(x,poss[k],v) + x = x + 1 + self.writeRecordTitle(ws,ox,poss) + return x - ox + + def isRecords(self,alist): + records = True + for r in alist: + if type(r) != type({}): + return False + for k,v in r.items(): + if type(v) == type({}): + return False + if type(v) == type([]): + for c in v: + if type(c) in [type([]),type({})]: + return False + return True + + def writeDict(self,ws,x,y,adict): + ox = x + ws.write(x,y,'__dict__') + x = x + 1 + for k in adict.keys(): + ws.write(x,y,k) + cnt = self.writeV(ws,x,y+1,adict[k]) + x = x + cnt + # print "writeV return ",cnt,"handled key=",k,"next row=",x + return x - ox + + def writeList(self,ws,x,y,alist,singlecell=False): + if self.isRecords(alist): + return self.writeRecords(ws,x,y,alist) + + ox = x + if singlecell is True: + s = ','.join([ str(i) for i in alist ]) + ws.write(x,y,s) + return 1 + multiline = False + for d in alist: + if type(d) == type({}): + multiline=True + if multiline is True: + for i in alist: + if type(i) == type({}): + rows = self.writeDict(ws,x,y,i) + elif type(i) == type([]): + rows = self.writeMultiLineList(ws,x,y,i) + else: + ws.write(x,y,i) + rows = 1 + x = x + rows + return x - ox + else: + for i in alist: + if type(i) == type([]): + self.writeList(ws,x,y,i,singlecell=True) + else: + ws.write(x,y,i) + y = y + 1 + return 1 + + def writeMultiLineList(self,ws,x,y,alist): + ox = x + ws.write(x,y,'__list__') + x = x + 1 + for i in alist: + ws.write(x,y,i) + x = x + 1 + return x - os + +if __name__ == '__main__': + data = { + 'my1':['23423','423424','t334t3',2332,'erfverfefew'], + 'my2':[{'aaa':1,'bbb':'bbb'},{'aaa':1,'bbb':'bbb'}], + } + w = ExcelWriter() + w.write('d:\\text.xls',data) + \ No newline at end of file diff --git a/appPublic/find_player.py b/appPublic/find_player.py new file mode 100755 index 0000000..586c4f2 --- /dev/null +++ b/appPublic/find_player.py @@ -0,0 +1,60 @@ +# !/usr/bin/env python +# -*- coding:UTF-8 -*- +from socket import * +import json +from appPublic.sockPackage import get_free_local_addr +from appPublic.background import Background +BUFSIZE = 1024 +class BroadcastServer: + def __init__(self, port, info): + self.info = info + self.port = port + self.udpSerSock = socket(AF_INET, SOCK_DGRAM) + # 设置阻塞 + self.udpSerSock.setblocking(1) + # 设置超时时间 1s + # self.udpSerSock.settimeout(1) + self.udpSerSock.bind(('' ,port)) + self.run_flg = True + self.thread = Background(self.run) + self.thread.start() + + def run(self): + while self.run_flg: + try: + data, addr = self.udpSerSock.recvfrom(BUFSIZE) + ret = json.dumps(self.info).encode('utf-8') + self.udpSerSock.sendto(ret, addr) + except Exception as e: + print('exception happened:',e) + pass + + def stop(self): + self.run_flg = False + self.udpSerSock.close() + +def find_players(port): + # broadcast_addr = '.'.join(host.split('.')[:-1]) + '.255' + host = get_free_local_addr()[0] + udpCliSock = socket(AF_INET, SOCK_DGRAM) + #设置阻塞 + #udpCliSock.setblocking(2) + #设置超时时间 + udpCliSock.settimeout(5) + udpCliSock.bind(('', 0)) + udpCliSock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + udpCliSock.sendto(b'findplayers', ('255.255.255.255',port)) + players = [] + while True: + try: + data,addr = udpCliSock.recvfrom(BUFSIZE) + if addr[0] != host and data: + data = data.decode('utf-8') + d = json.loads(data) + d['ip'] = addr[0] + players.append(d) + except Exception as e: + break + udpCliSock.close() + return players + diff --git a/appPublic/folderUtils.py b/appPublic/folderUtils.py new file mode 100755 index 0000000..38197f5 --- /dev/null +++ b/appPublic/folderUtils.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- + +import os +import sys +import stat +import os.path +import platform +import time +import random +import tempfile +""" +import win32api +""" +import sys + +def temp_file(suffix=None, prefix=None, dir=None, text=False): + x = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir, text=text) + os.close(x[0]) + return x[1] + +def filepoolpath(root): + paths=[191,193,197,199,97] + v = random.randint(0,9999999) + path = os.path.abspath(os.path.join(root, + str(v % paths[0]), + str(v % paths[1]), + str(v % paths[2]), + str(v % paths[3]), + str(v % paths[4]))) + return path + +def startsWith(text,s): + return text[:len(s)] == s + +def endsWith(text,s): + return text[-len(s):] == s + +def ProgramPath(): + filename = sys.argv[0] + if getattr(sys,'frozen',False): + filename = sys.executable + p = os.path.dirname(os.path.abspath(filename)) + return p + +def timestamp2datatiemStr(ts): + t = time.localtime(ts) + return '%04d-%02d-%-02d %02d:%02d:%02d' % (t.tm_year,t.tm_mon,t.tm_mday,t.tm_hour,t.tm_min,t.tm_sec) + +""" +def findAllDrives(): + Drives=[] + # print "Searching for drives..." + drives=win32api.GetLogicalDriveStrings().split(":") + for i in drives: + # print "i=",i,":" + dr=i[-1].lower() + if dr.isalpha(): + dr+=":\\" + inf=None + try: + inf=win32api.GetVolumeInformation(dr) + except: + pass # Removable drive, not ready + # You'll still get the drive letter, but inf will be None + Drives.append([dr,inf]) + return Drives +""" + +## list all folder name under folder named by path +# +def listFolder(path, rescursive=False) : + for name in os.listdir(path) : + full_name = os.path.join(path,name) + if os.path.isdir(full_name): + for f in listFolder(full_name, rescursive=rescursive): + yield f + yield full_name + +def listFile(folder,suffixs=[],rescursive=False): + subffixs = [ i.lower() for i in suffixs ] + for f in os.listdir(folder): + p = os.path.join(folder,f) + if rescursive and os.path.isdir(p): + for p1 in listFile(p,suffixs=suffixs,rescursive=True): + yield p1 + if os.path.isfile(p): + e = p.lower() + if suffixs == [] : + yield p + for s in subffixs: + if e.endswith(s): + yield p + +def folderInfo(root,uri=''): + relpath = uri + if uri[1]=='/': + relpath = uri[1:] + + path = os.path.join(root,*relpath.split('/')) + ret = [] + for name in os.listdir(path): + full_name = os.path.join(path,name) + s = os.stat(full_name) + if stat.S_ISDIR(s.st_mode): + ret.append( { + 'id':relpath + '/' + name, + 'name':name, + 'path':relpath, + 'type':'dir', + 'size':s.st_size, + 'mtime':timestamp2datatiemStr(s.st_mtime), + }) + if stat.S_ISREG(s.st_mode): + ret.append( { + 'id':relpath + '/' + name, + 'name':name, + 'path':relpath, + 'type':'file', + 'size':s.st_size, + 'mtime':timestamp2datatiemStr(s.st_mtime), + }) + return ret + + +def rmdir_recursive(dir): + """Remove a directory, and all its contents if it is not already empty.""" + for name in os.listdir(dir): + full_name = os.path.join(dir, name) + # on Windows, if we don't have write permission we can't remove + # the file/directory either, so turn that on + if not os.access(full_name, os.W_OK): + os.chmod(full_name, 0o600) + if os.path.isdir(full_name): + rmdir_recursive(full_name) + else: + os.remove(full_name) + os.rmdir(dir) + +def _mkdir(newdir) : + """works the way a good mkdir should :) + - already exists, silently complete + - regular file in the way, raise an exception + - parent directory(ies) does not exist, make them as well + """ + if os.path.isdir(newdir): + pass + elif os.path.isfile(newdir): + raise OSError("a file with the same name as the desired " \ + "dir, '%s', already exists." % newdir) + else: + head, tail = os.path.split(newdir) + if head and not os.path.isdir(head): + _mkdir(head) + #print "_mkdir %s" % repr(newdir) + if tail: + os.mkdir(newdir) + +def _copyfile(fp,dir) : + fs = open(fp,'rb') + name = os.path.basename(fp) + newfp = os.path.join(dir,getFileName(name,dir)) + f = open(newfp,'wb') + while True : + data = fs.read(65536) + if not data : + break + f.write(data) + fs.close() + f.close() + return True + +def _copydir(fp,dir,topdistinct) : + name = os.path.basename(fp) + newname = getFileName(name,dir) + debug(newname) + newfp = os.path.join(dir,newname) + _mkdir(newfp) + if fp==topdistinct : + return True + + flist = os.listdir(fp) + for name in flist : + full_name = os.path.join(fp,name) + if os.path.isdir(full_name) : + p = os.path.join(dir,name) + _copydir(full_name,newfp,topdistinct) + else : + if os.path.isfile(full_name) : + _copyfile(full_name,newfp) + return True + +mkdir=_mkdir +copyfile = _copyfile +copydir = _copydir +rmdir = rmdir_recursive diff --git a/appPublic/genetic.py b/appPublic/genetic.py new file mode 100755 index 0000000..35c796a --- /dev/null +++ b/appPublic/genetic.py @@ -0,0 +1,44 @@ +class Genetic: + """ + A Base class for genetical objects, + all the instances can inherite attributes from its parent. + """ + def __init__(self): + self.__parent__ = None + self.__children__ = [] + #print dir(self) + + def __getattr__(self,n): + d = self.__dict__ + if n in d.keys(): + return d[n] + p = self.__parent__ #d['__parent__'] + if p is not None: + return getattr(p,n) + raise AttributeError(n) + + def addChild(self,c): + self.__children__.append(c) + c.__parent__ = self + + def setParent(self,p): + p.addChild(self) + +if __name__ == '__main__': + class A(Genetic): + def __init__(self,a1,a2): + Genetic.__init__(self) + self.a1 = a1 + self.a2 = a2 + + class B(Genetic): + def __init__(self,b): + Genetic.__init__(self) + self.b = b + gp = A(1,2) + p = B(3) + c = A(4,5) + gc = B(6) + gc.setParent(c) + c.setParent(p) + p.setParent(gp) diff --git a/appPublic/hf.py b/appPublic/hf.py new file mode 100644 index 0000000..7410423 --- /dev/null +++ b/appPublic/hf.py @@ -0,0 +1,20 @@ +import requests +from huggingface_hub import configure_http_backend, get_session + +def hf_socks5proxy(proxies={ + "http": "socks5h://127.0.0.1:1086", + "https": "socks5h://127.0.0.1:1086" + }): + # Create a factory function that returns a Session with configured proxies + print(f'proxies={proxies}') + def backend_factory() -> requests.Session: + session = requests.Session() + session.proxies = proxies + print(f'socks5 proxy set {proxies=}') + return session + + # Set it as the default session factory + configure_http_backend(backend_factory=backend_factory) + +if __name__ == '__main__': + hf_socks5proxy() diff --git a/appPublic/http_client.py b/appPublic/http_client.py new file mode 100755 index 0000000..dad7aa5 --- /dev/null +++ b/appPublic/http_client.py @@ -0,0 +1,131 @@ +import requests + +class NeedLogin(Exception): + pass + +class InsufficientPrivilege(Exception): + pass + +class HTTPError(Exception): + def __init__(self,resp_code,url=None): + self.resp_code = resp_code + self.url = url + Exception.__init__(self) + + def __expr__(self): + return f'{self.url}:{self.resp_code}' + + def __str__(self): + return f'{self.url}:{self.resp_code}' + +hostsessions = {} + +class Http_Client: + def __init__(self): + self.s = requests.Session() + self.s.verify = False + self.s.hooks['response'].append(self.response_handler) + + def prepped_handler(self, prepped): + pass + + def response_handler(self, resp, *args, **kw): + return resp + + def url2domain(self,url): + parts = url.split('/')[:3] + pre = '/'.join(parts) + return pre + + def _webcall(self,url,method="GET", + params={}, + files={}, + headers={}, + stream=False): + domain = self.url2domain(url) + sessionid = hostsessions.get(domain,None) + if sessionid: + headers.update({'session':sessionid}) + + if method in ['GET']: + req = requests.Request(method,url, + params=params,headers=headers) + else: + req = requests.Request(method,url, + data=params,files=files,headers=headers) + prepped = self.s.prepare_request(req) + self.prepped_handler(prepped) + resp = self.s.send(prepped) + if resp.status_code == 200: + h = resp.headers.get('Set-Cookie',None) + if h: + sessionid = h.split(';')[0] + hostsessions[domain] = sessionid + + if resp.status_code == 401: + print('NeedLogin:',url) + raise NeedLogin + + if resp.status_code == 403: + raise InsufficientPrivilege + + if resp.status_code != 200: + print('Error', url, method, + params, resp.status_code, + type(resp.status_code)) + raise HTTPError(resp.status_code,url) + return resp + + def webcall(self,url,method="GET", + params={}, + files={}, + headers={}, + stream=False): + resp = self._webcall(url,method=method, + params=params, + files=files, + headers=headers, + stream=stream) + if stream: + return resp + try: + data = resp.json() + if type(data) != type({}): + return data + status = data.get('status',None) + if status is None: + return data + if status == 'OK': + return data.get('data') + return data + except: + return resp.text + + def __call__(self,url,method="GET", + params={}, + headers={}, + files={}, + stream=False): + return self.webcall(url, method=method, + params=params, files=files, + headers=headers, stream=stream) + + def get(self, url, params={}, headers={}, stream=False): + return self.__call__(url,method='GET',params=params, + headers=headers, stream=stream) + def post(self, url, params={}, headers={}, files={}, stream=False): + return self.__call__(url,method='POST',params=params, files=files, + headers=headers, stream=stream) + + def put(self, url, params={}, headers={}, stream=False): + return self.__call__(url,method='PUT',params=params, + headers=headers, stream=stream) + + def delete(self, url, params={}, headers={}, stream=False): + return self.__call__(url,method='DELETE',params=params, + headers=headers, stream=stream) + + def option(self, url, params={}, headers={}, stream=False): + return self.__call__(url,method='OPTION',params=params, + headers=headers, stream=stream) + diff --git a/appPublic/httpclient.py b/appPublic/httpclient.py new file mode 100755 index 0000000..aed9aee --- /dev/null +++ b/appPublic/httpclient.py @@ -0,0 +1,117 @@ +import asyncio +import aiohttp +import re +from appPublic.log import info, debug, warning, error, exception, critical + +RESPONSE_BIN = 0 +RESPONSE_TEXT = 1 +RESPONSE_JSON = 2 +RESPONSE_FILE = 3 +RESPONSE_STREAM = 4 + +class HttpError(Exception): + def __init__(self, code, msg, *args, **kw): + super().__init__(*msg, **kw) + self.code = code + self.msg = msg + def __str__(self): + return f"Error Code:{self.code}, {self.msg}" + + def __expr__(self): + return str(self) + +class HttpClient: + def __init__(self,coding='utf-8'): + self.coding = coding + self.session = None + self.cookies = {} + + async def close(self): + if self.session: + await self.session.close() + self.session = None + + def url2domain(self,url): + parts = url.split('/')[:3] + pre = '/'.join(parts) + return pre + + def setCookie(self,url,cookies): + name = self.url2domain(url) + self.cookies[name] = cookies + + def getCookies(self,url): + name = url2domain(url) + return self.cookies.get(name,None) + + def getsession(self,url): + if self.session is None: + jar = aiohttp.CookieJar(unsafe=True) + self.session = aiohttp.ClientSession(cookie_jar=jar) + return self.session + + async def handleResp(self,url,resp,resp_type, stream_func=None): + if resp.cookies is not None: + self.setCookie(url,resp.cookies) + + if resp_type == RESPONSE_BIN: + return await resp.read() + if resp_type == RESPONSE_JSON: + return await resp.json() + if resp_type == RESPONSE_TEXT: + return await resp.text(self.coding) + async for chunk in resp.content.iter_chunked(1024): + if stream_func: + await stream_func(chunk) + + def grapCookie(self,url): + session = self.getsession(url) + domain = self.url2domain(url) + filtered = session.cookie_jar.filter_cookies(domain) + return filtered + + async def request(self, url, method, + response_type=RESPONSE_TEXT, + params=None, + data=None, + jd=None, + stream_func=None, + headers=None, + **kw): + session = self.getsession(url) + if params == {}: + params = None + if data == {}: + data = None + if jd == {}: + jd = None + if headers == {}: + headers = None + + resp = await session.request(method, url, + params=params, + data=data, + json=jd, + headers=headers, **kw) + if resp.status==200: + return await self.handleResp(url, resp, response_type, stream_func=stream_func) + msg = f'http error({resp.status}, {url=},{params=}, {data=}, {jd=}, {headers=}, {kw=})' + exception(msg) + raise HttpError(resp.status, msg) + + async def get(self,url,**kw): + return self.request(url, 'GET', **kw) + + async def post(self,url, **kw): + return self.request(url, 'POST', **kw) + session = self.getsession(url) + +if __name__ == '__main__': + async def gbaidu(hc): + r = await hc.get('https://www.baidu.com') + print(r) + await hc.close() + loop = asyncio.get_event_loop() + hc = HttpClient() + loop.run_until_complete(gbaidu(hc)) + diff --git a/appPublic/i18n.py b/appPublic/i18n.py new file mode 100755 index 0000000..cb32ca6 --- /dev/null +++ b/appPublic/i18n.py @@ -0,0 +1,161 @@ +import os,re,sys +import codecs +from appPublic.folderUtils import _mkdir +from appPublic.Singleton import SingletonDecorator +from appPublic.folderUtils import ProgramPath +from appPublic.jsonConfig import getConfig +import threading +import time + +import locale + +comment_re = re.compile(r'\s*#.*') +msg_re = re.compile(r'\s*([^:]*)\s*:\s*([^\s].*)') + +def dictModify(d, md) : + for i in md.keys() : + if md[i]!=None : + d[i] = md[i] + return d + +convert_pairs = {':':'\\x3A', + '\n':'\\x0A', + '\r':'\\x0D', +} + +def charEncode(s) : + r = '' + v = s.split('\\') + s = '\\\\'.join(v) + for i in convert_pairs.keys() : + v = s.split(i) + s = convert_pairs[i].join(v) + # print 'i=',i,'iv=',convert_pairs[i],'s=',s + return s + +def charDecode(s) : + for i in convert_pairs.items() : + v = s.split(i[1]) + s = i[0].join(v) + v = s.split('\\\\') + s = '\\'.join(v) + return s + +def getTextDictFromLines(lines) : + d = {} + for l in lines : + l = ''.join(l.split('\r')) + if comment_re.match(l) : + continue + m = msg_re.match(l) + if m : + grp = m.groups() + d[charDecode(grp[0])] = charDecode(grp[1]) + return d + +def getFirstLang(lang) : + s = lang.split(',') + return s[0] + +@SingletonDecorator +class MiniI18N: + """ + """ + def __init__(self,path,lang=None,coding='utf8') : + self.path = path + l = locale.getdefaultlocale() + self.curLang = l[0] + self.coding = coding + self.id = 'i18n' + self.langTextDict = {} + self.messages = {} + self.setupMiniI18N() + self.missed_pt = None + self.translated_pt = None + self.header_pt = None + self.footer_pt = None + self.show_pt=None + self.clientLangs = {} + self.languageMapping = {} + self.timeout = 600 + config = getConfig() + for l1,l in config.langMapping.items(): + self.setLangMapping(l1,l) + + def __call__(self,msg,lang=None) : + """ + """ + if type(msg) == type(b''): + msg = msg.decode(self.coding) + return self.getLangText(msg,lang) + + def setLangMapping(self,lang,path): + self.languageMapping[lang] = path + + def getLangMapping(self,lang): + return self.languageMapping.get(lang,lang) + + def setTimeout(self,timeout=600): + self.timeout = timeout + + def delClientLangs(self): + t = threading.currentThread() + tim = time.time() - self.timeout + [ self.clientLangs.pop(k,None) for k in self.clientLangs.keys() if self.clientLangs[k]['timestamp'] < tim ] + + def getLangDict(self,lang): + lang = self.getLangMapping(lang) + return self.langTextDict.get(lang,{}) + + def getLangText(self,msg,lang=None) : + """ + """ + if lang==None : + lang = self.getCurrentLang() + textMapping = self.getLangDict(lang) + return textMapping.get(msg,msg) + + def setupMiniI18N(self) : + """ + """ + + p = os.path.join(self.path,'i18n') + langs = [] + + for f in os.listdir(p) : + if os.path.isdir(os.path.join(p,f)) : + langs.append(f) + for dir in langs : + p1 = os.path.join(p,dir,'msg.txt') + if os.path.exists(p1) : + f = codecs.open(p1,'r',self.coding) + textDict = getTextDictFromLines(f.readlines()) + f.close() + d = {} + if dir in self.langTextDict : + d = self.langTextDict[dir] + self.langTextDict[dir] = textDict + for i in textDict.keys() : + self.messages[i] = '' + + self._p_changed = 1 + + def setCurrentLang(self,lang): + lang = self.getLangMapping(lang) + t = time.time() + threadid = threading.currentThread() + a = dict(timestamp=t,lang=lang) + self.clientLangs[threadid] = a + + def getCurrentLang(self) : + """ + """ + threadid = threading.currentThread() + return self.clientLangs[threadid]['lang'] + +def getI18N(path=None, coding='utf8'): + if path is None: + path = ProgramPath() + i18n = MiniI18N(path,coding) + return i18n + diff --git a/appPublic/ipgetter.py b/appPublic/ipgetter.py new file mode 100755 index 0000000..0c52b72 --- /dev/null +++ b/appPublic/ipgetter.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python +""" +This module is designed to fetch your external IP address from the internet. +It is used mostly when behind a NAT. +It picks your IP randomly from a serverlist to minimize request +overhead on a single server + +If you want to add or remove your server from the list contact me on github + + +API Usage +========= + + >>> import ipgetter + >>> myip = ipgetter.myip() + >>> myip + '8.8.8.8' + + >>> ipgetter.IPgetter().test() + + Number of servers: 47 + IP's : + 8.8.8.8 = 47 ocurrencies + + +Copyright 2014 phoemur@gmail.com +This work is free. You can redistribute it and/or modify it under the +terms of the Do What The Fuck You Want To Public License, Version 2, +as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. +""" + +import re +import json +import time +import random +import socket +from threading import Timer + +from sys import version_info + +import future.moves.urllib.request +urllib = future.moves.urllib.request +PY3K = version_info >= (3, 0) + +__version__ = "0.6" + + +def myip(): + return IPgetter().get_external_ip() + + +class IPgetter(object): + + """ + This class is designed to fetch your external IP address from the internet. + It is used mostly when behind a NAT. + It picks your IP randomly from a serverlist to minimize request overhead + on a single server + # 'http://ip.dnsexit.com', + # 'http://checkip.dyndns.org/plain', + # 'http://ipogre.com/linux.php', + # 'http://whatismyipaddress.com/', + # 'http://ip.my-proxy.com/', + # 'http://websiteipaddress.com/WhatIsMyIp', + # 'http://www.iplocation.net/', + # 'http://www.howtofindmyipaddress.com/', + # 'http://www.ip-adress.com/', + # 'http://checkmyip.com/', + # 'http://www.tracemyip.org/', + # 'http://checkmyip.net/', + # 'http://www.findmyip.co/', + # 'http://www.dslreports.com/whois', + # 'http://www.mon-ip.com/en/my-ip/', + # 'http://www.myip.ru', + # 'http://www.whatsmyipaddress.net/', + # 'http://formyip.com/', + # 'https://check.torproject.org/', + # 'http://www.displaymyip.com/', + # 'http://www.bobborst.com/tools/whatsmyip/', + # 'https://www.whatsmydns.net/whats-my-ip-address.html', + # 'https://www.privateinternetaccess.com/pages/whats-my-ip/', + # 'http://www.infosniper.net/', + # 'http://ipinfo.io/', + # 'http://myexternalip.com/', + """ + + def __init__(self): + self.server_list = [ + 'http://ifconfig.me/ip', + 'http://ipecho.net/plain', + 'http://getmyipaddress.org/', + 'http://www.my-ip-address.net/', + 'http://www.canyouseeme.org/', + 'http://www.trackip.net/', + 'http://icanhazip.com/', + 'http://www.ipchicken.com/', + 'http://whatsmyip.net/', + 'http://www.lawrencegoetz.com/programs/ipinfo/', + 'http://ip-lookup.net/', + 'http://ipgoat.com/', + 'http://www.myipnumber.com/my-ip-address.asp', + 'http://www.geoiptool.com/', + 'http://checkip.dyndns.com/', + 'http://www.ip-adress.eu/', + 'http://wtfismyip.com/', + 'http://httpbin.org/ip', + ] + + + self.parsers = {} + self.timeout = 1.6 + self.url = None + + def get_external_ip(self): + """ + This function gets your IP from a random server + """ + + random.shuffle(self.server_list) + myip = '' + for server in self.server_list: + myip = self.defaultparser(self.fetch(server)) + if myip != '' and not (myip.startswith('192.') or myip.startswith('10.')) and not myip.startswith('127'): + return myip + else: + continue + return '' + + def add_server(self, server, parser): + self.server_list.append(server) + self.parsers[server] = parser + + def defaultparser(self, content): + p = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(' + p += '25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[' + p += '01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' + try: + m = re.search(p, content) + myip = m.group(0) + if len(myip) > 0: + return myip + else: + return '' + except: + return '' + + def handle_timeout(self, url): + if self.url is not None: + self.url.close() + self.url = None + + def fetch(self, server): + """ + This function gets your IP from a specific server + """ + t = None + socket_default_timeout = socket.getdefaulttimeout() + opener = urllib.build_opener() + opener.addheaders = [('User-agent', + "Mozilla/5.0 (X11; Linux x86_64; rv:24.0)" + " Gecko/20100101 Firefox/24.0")] + + try: + # Close url resource if fetching not finished within timeout. + t = Timer(self.timeout, self.handle_timeout, [self.url]) + t.start() + + # Open URL. + if version_info[0:2] == (2, 5): + # Support for Python 2.5.* using socket hack + # (Changes global socket timeout.) + socket.setdefaulttimeout(self.timeout) + self.url = opener.open(server) + else: + self.url = opener.open(server, timeout=self.timeout) + + # Read response. + content = self.url.read() + + # Didn't want to import chardet. Prefered to stick to stdlib + if PY3K: + try: + content = content.decode('UTF-8') + except UnicodeDecodeError: + content = content.decode('ISO-8859-1') + + parser = self.parsers.get(server, self.defaultparser) + return parser(content) + + except Exception as e: + print(server, e) + return '' + finally: + if self.url is not None: + self.url.close() + self.url = None + if t is not None: + t.cancel() + + # Reset default socket timeout. + if socket.getdefaulttimeout() != socket_default_timeout: + socket.setdefaulttimeout(socket_default_timeout) + + def all_result(self): + x= [] + for s in self.server_list: + x.append([s, self.fetch(s)]) + print(x) + + def test(self): + """ + This functions tests the consistency of the servers + on the list when retrieving your IP. + All results should be the same. + """ + + resultdict = {} + for server in self.server_list: + resultdict.update(**{server: self.fetch(server)}) + + ips = sorted(resultdict.values()) + ips_set = set(ips) + print('\nNumber of servers: {}'.format(len(self.server_list))) + print("IP's :") + for ip, ocorrencia in zip(ips_set, map(lambda x: ips.count(x), ips_set)): + print('{0} = {1} ocurrenc{2}'.format(ip if len(ip) > 0 else 'broken server', ocorrencia, 'y' if ocorrencia == 1 else 'ies')) + print('\n') + print(resultdict) + +if __name__ == '__main__': + def p(content): + d = json.loads(content) + return d['ip'] + + g = IPgetter() + server = 'http://ipinfo.io/json' + g.add_server(server, p) + print(g.get_external_ip()) diff --git a/appPublic/iplocation.py b/appPublic/iplocation.py new file mode 100755 index 0000000..73acca8 --- /dev/null +++ b/appPublic/iplocation.py @@ -0,0 +1,75 @@ +import os +import sys +from requests import get +from bs4 import BeautifulSoup +from appPublic.http_client import Http_Client +from appPublic.sockPackage import get_free_local_addr +public_headers = { + "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36" +} + +def get_outip(): + ip = get('https://api.ipify.org').content.decode('utf8') + return ip + +def ipip(ip=None): + # ipip.net + if ip is None: + ip = get_outip() + api= f"http://freeapi.ipip.net/{ip}" + hc = Http_Client() + r= hc.get(api, headers=public_headers) + return { + 'country':r[0], + 'city':r[2] + } + +def ipapi_co(ip): + url = f'https://ipapi.co/{ip}/json/' + hc = Http_Client() + r = hc.get(url) + r['City'] = r['city'] + r['lat'] = r['latitude'] + r['lon'] = r['longitude'] + return r + +def ip_api_com(ip): + url = f'http://ip-api.com/json/{ip}' + hc = Http_Client() + r = hc.get(url) + r['City'] = r['city'] + return r + +def iplocation(ip=None): + if ip is None: + ip = get_outip() + # apikey come from + # https://app.apiary.io/globaliptv/tests/runs + # using my github accout + apikey='c675f89c4a0e9315437a1a5edca9b92c' + api = f"https://www.iplocate.io/api/lookup/{ip}?apikey={apikey}", + hc = Http_Client() + r= hc.get(api, headers=public_headers) + return r + +def get_ip_location(ip): + funcs = [ + ip_api_com, + ipapi_co, + ipip, + iplocation + ] + hc = Http_Client() + for f in funcs: + try: + r = f(ip) + return r + except: + pass + +if __name__ == '__main__': + print(get_free_local_addr()) + if len(sys.argv) > 1: + info = get_ip_location(sys.argv[1]) + print(info) + diff --git a/appPublic/jsonConfig.py b/appPublic/jsonConfig.py new file mode 100755 index 0000000..789cc13 --- /dev/null +++ b/appPublic/jsonConfig.py @@ -0,0 +1,74 @@ +import os,sys +import json +from pathlib import Path +from appPublic.dictObject import DictObject +from appPublic.Singleton import SingletonDecorator +from appPublic.folderUtils import ProgramPath +from appPublic.argsConvert import ArgsConvert + +def key2ansi(dict): + #print dict + return dict + a = {} + for k,v in dict.items(): + k = k.encode('utf-8') + #if type(v) == type(u" "): + # v = v.encode('utf-8') + a[k] = v + + return a + +class JsonObject(DictObject): + """ + JsonObject class load json from a json file + """ + def __init__(self,jsonholder,keytype='ansi',NS=None): + jhtype = type(jsonholder) + if jhtype == type("") or jhtype == type(u''): + f = open(jsonholder,'r') + else: + f = jsonholder + try: + a = json.load(f) + except Exception as e: + print("exception:",self.__jsonholder__,e) + raise e + finally: + if type(jsonholder) == type(""): + f.close() + + if NS is not None: + ac = ArgsConvert('$[',']$') + a = ac.convert(a,NS) + a['__jsonholder__'] = jsonholder + a['NS'] = NS + DictObject.__init__(self,**a) + +@SingletonDecorator +class JsonConfig(JsonObject): + pass + +def getConfig(path=None,NS=None): + pp = ProgramPath() + if path==None: + path = os.getcwd() + cfname = os.path.abspath(os.path.join(path,"conf","config.json")) + # print __name__,cfname + ns = { + 'home':str(Path.home()), + 'workdir':path, + 'ProgramPath':pp + } + if NS is not None: + ns.update(NS) + a = JsonConfig(cfname,NS=ns) + return a + +if __name__ == '__main__': + conf = JsonConfig(sys.argv[1]) + #print conf.db,conf.sql + #conf1 = JsonConfig(sys.argv[1],keytype='unicode') + conf1 = JsonConfig(sys.argv[1],keytype='ansi') + + print("conf=",dir(conf)) + print("conf1=",dir(conf1) ) diff --git a/appPublic/jsonIO.py b/appPublic/jsonIO.py new file mode 100755 index 0000000..d6f9b66 --- /dev/null +++ b/appPublic/jsonIO.py @@ -0,0 +1,38 @@ +import json + +def uni_str(a, encoding): + if a is None: + return None + if isinstance(a, (list, tuple)): + s = [] + for i, k in enumerate(a): + s.append(uni_str(k, encoding)) + return s + elif isinstance(a, dict): + s = {} + for i, k in enumerate(a.items()): + key, value = k + s[uni_str(key, encoding)] = uni_str(value, encoding) + return s + elif isinstance(a, bool): + return a + elif isinstance(a, unicode): + return a + elif isinstance(a, str) or (hasattr(a, '__str__') and callable(getattr(a, '__str__'))): + if getattr(a, '__str__'): + a = str(a) + return unicode(a, encoding) + else: + return a + +def success(data): + return dict(success=True,data=data) + +def error(errors): + return dict(success=False,errors=errors) + +def jsonEncode(data,encode='utf-8'): + return json.dumps(uni_str(data, encode)) + +def jsonDecode(jsonstring): + return json.loads(jsonstring) diff --git a/appPublic/localefunc.py b/appPublic/localefunc.py new file mode 100755 index 0000000..aab14e4 --- /dev/null +++ b/appPublic/localefunc.py @@ -0,0 +1,43 @@ +# this function will fix a bug for open a file with a not english name. +# + +import sys +import locale + +language, local_encoding = locale.getdefaultlocale() +if sys.platform == 'win32': + import locale, codecs + local_encoding = locale.getdefaultlocale()[1] + if local_encoding.startswith('cp'): # "cp***" ? + try: + codecs.lookup(local_encoding) + except LookupError: + import encodings + encodings._cache[local_encoding] = encodings._unknown + encodings.aliases.aliases[local_encoding] = 'mbcs' + +def locale_open(filename,mode='rb') : + return open(filename.encode(local_encoding),mode) + +def localeString(s) : + try : + return unicode(s,'utf-8').encode(local_encoding) + + except : + return s + +def utf8String(s) : + try : + return unicode(s,local_encoding).encode('utf-8') + except : + return s + +def charsetString(s,charset) : + try : + return unicode(s,local_encoding).encode(charset) + except : + try : + return unicode(s,'utf-8').encode(charset) + except : + return s + diff --git a/appPublic/log.py b/appPublic/log.py new file mode 100644 index 0000000..c4395a7 --- /dev/null +++ b/appPublic/log.py @@ -0,0 +1,95 @@ +import sys +import codecs +from appPublic.timeUtils import timestampstr +from appPublic.Singleton import SingletonDecorator +import inspect + +def my_function(): + frame_info = inspect.currentframe() + caller_frame = frame_info.f_back + file_name = inspect.getframeinfo(caller_frame).filename + line_number = inspect.getframeinfo(caller_frame).lineno + print(f"Called from file: {file_name}, line: {line_number}") + + +@SingletonDecorator +class MyLogger: + levels={ + "debug":5, + "info":6, + "warning":4, + "error":3, + "exception":2, + "critical":1 + } + formater='%(timestamp)s[%(name)s][%(levelname)s][%(filename)s:%(lineno)s]%(message)s\n' + def __init__(self, name, levelname='debug', logfile=None): + self.name = name + self.levelname = levelname + self.level = self.levels.get(levelname) + self.logfile = logfile + + def open_logger(self): + if self.logfile: + self.logger = codecs.open(self.logfile, 'a', 'utf-8') + else: + self.logger = sys.stdout + + def close_logger(self): + if self.logfile: + self.logger.close(); + self.logger = None + self.logger = None + + def log(self, levelname, message, frame_info): + caller_frame = frame_info.f_back + filename = inspect.getframeinfo(caller_frame).filename + lineno = inspect.getframeinfo(caller_frame).lineno + level = self.levels.get(levelname) + if level > self.level: + print(f'{level=},{self.level=}') + return + data = { + 'timestamp':timestampstr(), + 'name':self.name, + 'levelname':levelname, + 'message':message, + 'filename':filename, + 'lineno':lineno + } + self.open_logger() + s = self.formater % data + self.logger.write(s) + self.logger.flush() + self.close_logger() + +def info(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('info', message, frame_info) + +def debug(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('debug', message, frame_info) + +def warning(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('warning', message, frame_info) + +def error(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('error', message, frame_info) + +def critical(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('critical', message, frame_info) + +def exception(message): + frame_info = inspect.currentframe() + logger = MyLogger('exception') + logger.log('exception', message, frame_info) + diff --git a/appPublic/macAddress.py b/appPublic/macAddress.py new file mode 100755 index 0000000..8c3af37 --- /dev/null +++ b/appPublic/macAddress.py @@ -0,0 +1,38 @@ +#! /usr/bin/env python +import locale +import psutil +import socket + + +def getAllAddress(): + iocounts = psutil.net_io_counters(pernic=True) + ns = [ k for k in iocounts.keys() if iocounts[k].bytes_sent>0 and iocounts[k].bytes_recv>0 ] + stats = psutil.net_if_stats() + stat = [ i for i in stats.keys() if i in ns ] + hds = psutil.net_if_addrs() + for n,v in hds.items(): + if n not in stat: + continue + for i in v: + if i.family == socket.AF_INET: + yield n,i.address + +def getAllMacAddress(): + coding = locale.getdefaultlocale()[1] + iocounts = psutil.net_io_counters(pernic=True) + ns = [ k for k in iocounts.keys() if iocounts[k].bytes_sent>0 and iocounts[k].bytes_recv>0 ] + stats = psutil.net_if_stats() + stat = [ i for i in stats.keys() if i in ns ] + hds = psutil.net_if_addrs() + for n,v in hds.items(): + if n not in stat: + continue + for i in v: + if i.family == socket.AF_PACKET: + yield n,i.address + +if __name__ == '__main__': + def test(): + for i in getAllAddress(): + print("mac=",i) + test() diff --git a/appPublic/myImport.py b/appPublic/myImport.py new file mode 100755 index 0000000..ccf4c09 --- /dev/null +++ b/appPublic/myImport.py @@ -0,0 +1,6 @@ +def myImport(modulename): + modules = modulename.split('.') + if len(modules) > 1: + a = __import__(modules[0]) + return eval('a.' + '.'.join(modules[1:])) + return __import__(modulename) \ No newline at end of file diff --git a/appPublic/myTE.py b/appPublic/myTE.py new file mode 100755 index 0000000..a3fcb80 --- /dev/null +++ b/appPublic/myTE.py @@ -0,0 +1,82 @@ +import os +import sys +try: + import ujson as json +except: + import json +from jinja2 import Environment,FileSystemLoader, BaseLoader +import codecs +from appPublic.argsConvert import ArgsConvert +from appPublic.dictObject import DictObject +def isNone(obj): + return obj is None + + +def string_template_render(tmp_string, data): + rtemplate = Environment(loader=BaseLoader()).from_string(tmp_string) + return rtemplate.render(**data) + +class MyTemplateEngine: + def __init__(self,pathList,file_coding='utf-8',out_coding='utf-8'): + self.file_coding = file_coding + self.out_coding = out_coding + loader = FileSystemLoader(pathList, encoding=self.file_coding) + self.env = Environment(loader=loader, enable_async=False) + denv={ + 'json':json, + 'hasattr':hasattr, + 'int':int, + 'float':float, + 'str':str, + 'type':type, + 'isNone':isNone, + 'len':len, + 'render':self.render, + 'renders':self.renders, + 'ArgsConvert':ArgsConvert, + 'renderJsonFile':self.renderJsonFile, + 'ospath':lambda x:os.path.sep.join(x.split(os.altsep)), + 'basename':lambda x:os.path.basename(x), + 'basenameWithoutExt':lambda x:os.path.splitext(os.path.basename(x))[0], + 'extname':lambda x:os.path.splitext(x)[-1], + } + self.env.globals.update(denv) + + def set(self,k,v): + self.env.globals.update({k:v}) + + def _render(self,template,data): + # print('**********template=',template,'**data=',data,'type_data=',type(data),'************') + uRet = template.render(**data) + return uRet + + def renders(self,tmplstring,data): + def getGlobal(): + return data + self.set('global',getGlobal) + template = self.env.from_string(tmplstring) + return self._render(template,data) + + def render(self,tmplfile,data): + def getGlobal(): + return data + self.set('global',getGlobal) + template = self.env.get_template(tmplfile) + return self._render(template,data) + + def renderJsonFile(self,tmplfile,jsonfile): + with codecs.open(jsonfile,"r",self.file_coding) as f: + data = json.load(f) + return self.render(tmplfile,data) + +def tmpTml(f, ns): + te = MyTemplateEngine('.') + with codecs.open(f, 'r', 'utf-8') as fd: + d = fd.read() + b = te.renders(d, ns) + filename = os.path.basename(f) + p = f'/tmp/{filename}' + with codecs.open(p, 'w', 'utf-8') as wf: + wf.write(b) + return p + diff --git a/appPublic/myjson.py b/appPublic/myjson.py new file mode 100755 index 0000000..95f30d4 --- /dev/null +++ b/appPublic/myjson.py @@ -0,0 +1,22 @@ +try: + import ujson as json +except: + import json + +import codecs + +def loadf(fn,coding='utf8'): + f = codecs.open(fn,'r',coding) + d = json.load(f) + f.close() + return d + +def dumpf(obj,fn,coding='utf8'): + f = codecs.open(fn,'w',coding) + json.dump(obj,f) + f.close() + +load = json.load +dump = json.dump +loads = json.loads +dumps = json.dumps diff --git a/appPublic/mylog.py b/appPublic/mylog.py new file mode 100755 index 0000000..0ffdf99 --- /dev/null +++ b/appPublic/mylog.py @@ -0,0 +1,89 @@ +import os +from datetime import datetime +from PublicData import public_data +from folderUtils import mkdir + +myLogPath = '.' + +AllCatelogs=['SYSError', + 'SYSWarn', + 'APPError', + 'APPWarn', + 'APPInfo', + 'DEBUG1', + 'DEBUG2', + 'DEBUG3', + 'DEBUG4', + 'DEBUG5', +] + +class MyLog : + def __init__(self,path) : + self.setLogPath(path) + + def setLogPath(self,path='.') : + self.myLogPath = path + logp=os.path.join(path,'log') + mkdir(logp) + + def __call__(self,msg='') : + p = os.path.join(self.myLogPath,'log','my.log') + f = open(p,'a') + d = datetime.now() + f.write('%04d-%02d-%02d %02d:%02d:%02d %s\n' % ( d.year,d.month,d.day,d.hour,d.minute,d.second,msg)) + f.close() + +class LogMan : + def __init__(self) : + self.logers = {} + self.catelogs = AllCatelogs + + def addCatelog(self,catelog) : + if catelog not in self.catelogs : + self.catelogs.append(catelog) + + def addLoger(self,name,func,catelog) : + if type(catelog)!=type([]) : + catelog = [catelog] + catelog = [ i for i in catelog if i in self.catelogs ] + log = { + 'name':name, + 'func':func, + 'catelog':catelog, + } + self.logers[name] = log + + def delLoger(self,name) : + if name in self.logers.keys() : + del self.logers[name] + + def setCatelog(self,name,catelog) : + if type(catelog)!=type([]) : + catelog = [catelog] + catelog = [ i for i in catelog if i in self.catelogs ] + if name in self.logers.keys() : + log = self.logers[name] + log['catelog'] = catelog + self.logers[name] = log + + def __call__(self,msg='',catelog='APPInfo') : + for name,loger in self.logers.items() : + c = loger['catelog'] + if type(c)!=type([]) : + c = [c] + if catelog in c : + f = loger['func'] + f(msg) + +def mylog(s,catelog='APPInfo') : + logman = public_data.get('mylog',None) + if logman==None : + path = public_data.get('ProgramPath',None) + if path==None : + raise Exception('ProgramPath Not found in "public_data"') + log = MyLog(path) + logman = LogMan() + logman.addLoger('mylog',log,AllCatelogs) + public_data.set('mylog',logman) + return logman(s,catelog) + diff --git a/appPublic/oauth_client.py b/appPublic/oauth_client.py new file mode 100644 index 0000000..79656a4 --- /dev/null +++ b/appPublic/oauth_client.py @@ -0,0 +1,188 @@ +import json +from appPublic.httpclient import HttpClient, RESPONSE_TEXT, RESPONSE_JSON, RESPONSE_BIN,RESPONSE_FILE, RESPONSE_STREAM, HttpError +from appPublic.argsConvert import ArgsConvert +from appPublic.dictObject import DictObject + +class OAuthClient: + """ + OauthClient is a http(s) client for request a api annouce by other + it send https request and get back a json data + we can check the response json data to check if the call is success or failed + desc has the following format + { + "data": predefined data, and if nessory, you can add to new data to it + "method_name":{ + "url" + "method", + "headers", + "params":arguments will appended to url with ?a=a&b=b... + "data":data will send in the request body, json format + "resp":[ + { + "name": + "converter":default none, if not, convert response data first before set the returen data + "keys":resposne json data keys to achieve data + } + ] + "error_if":{ + "keys": + "op", + "value" + } + } + } + return: + if error: + return { + "status":"error", + "code":code returned by server + "message":"message returned by server + } + else: + return { + "status":"ok", + "data":... + } + + """ + def __init__(self, desc, converters={}): + assert desc.get('data') + self.desc = desc + self.data = desc.get('data') + self.converters = converters + self.ac = ArgsConvert('${', '}') + + def setup_req_data(self, data=[], ns={}): + d = {} + if data is None: + return None + for h in data: + d1 = self.setup_req_kv(h, ns) + d.update(d1) + if d == {}: + return None + return d + + def setup_req_kv(self, d, ns): + rd = { + d.name:d.value + } + nd = self.datalize(rd, ns) + if d.converter: + f = self.converters.get(d.converter) + if f: + nd[d.name] = f(nd.get(d.name)) + return nd + + async def __call__(self, host, mapi, params): + if not self.desc.get(mapi): + raise Exception(f'{mapi} not defined') + self.api = self.desc[mapi] + if not self.api: + return { + "status":"error", + "code":'9999', + "message":f"{mapi} not defined" + } + path = self.datalize(self.api.path, params) + url = host + path + method = self.api.get('method', 'GET') + myheaders = self.setup_req_data(self.api.headers, params) + myparams = self.setup_req_data(self.api.params, params) + mydata = self.setup_req_data(self.api.data, params) + response_type = RESPONSE_JSON + hc = HttpClient() + print(f'{url=}, {method=}, {myparams=}, {mydata=}, {myheaders=}') + resp_data = None + try: + resp_data = await hc.request(url, method, response_type=response_type, + params=None if not myparams else myparams, + data=None if not mydata else mydata, + headers=myheaders) + resp_data = DictObject(**resp_data) + print(resp_data) + except HttpError as e: + return { + "status":"error", + "code":e.code, + "message":e.msg + } + + if resp_data is None: + return { + "status":"error", + "code":None, + "message":"https error" + } + err = self.check_if_error(resp_data) + if err: + return err + return self.setup_return_data(resp_data) + + def datalize(self, dic, data={}): + mydata = self.data.copy() + mydata.update(data) + s1 = self.ac.convert(dic, mydata) + return s1 + + def get_resp_data(self, resp, keys, converter=None): + d = resp.get_data_by_keys(keys) + if converter: + f = self.converters.get(converter) + if f: + d = f(d) + return d + + def setup_return_data(self, resp): + data = {} + if not self.api.resp: + return { + 'status':'ok', + 'data':{} + } + + for desc in self.api.resp: + k = desc.name + v = self.get_resp_data(resp, desc.resp_keys, desc.converter) + data[k] = v + return { + "status":"ok", + "data":data + } + + def check_if_error(self, resp): + if not self.api.error_if: + return None + ei = self.api.error_if + v = resp.get_data_by_keys(ei.error_keys) + v1 = ei.value + if ei.converter: + f = self.converters.get(ei.converter) + if f: + v = f(v) + if not ei.op: + ei.op = '==' + print(f'{ei.keys=},{v=}, {v1=}, {ei.op=}{v==v1}, {resp.base_resp.status_code=}') + if (ei.op == '==' and v == v1) or (ei.op == '!=' and v != v1): + print(f'{v=}, {v1=}, {ei.op=}{v==v1}') + code = None + message = None + if ei.code_keys: + code = resp.get_data_by_keys(ei.code_keys) + if ei.msg_keys: + message = resp.get_data_by_keys(ei.msg_keys) + return { + "status":"error", + "code":code, + "message":message + } + print(f'check_if_error ok:{v=}, {v1=}, {ei.op=}{v==v1}') + return None + + def set_data(self, resp_data, data_desc): + for dd in data_desc: + f = dd['field'] + n = dd['name'] + if resp_data.get(f): + self.data[n] = resp_data[f] + diff --git a/appPublic/objectAction.py b/appPublic/objectAction.py new file mode 100755 index 0000000..125fed7 --- /dev/null +++ b/appPublic/objectAction.py @@ -0,0 +1,62 @@ +from appPublic.Singleton import SingletonDecorator + +@SingletonDecorator +class ObjectAction(object): + def __init__(self): + self.actionList = {} + + def init(self,id,action): + idA = self.actionList.get(id,None) + if idA is None: + idA = self.actionList[id] = {} + self.actionList[id][action] = [] + + def add(self,id,action,func): + idA = self.actionList.get(id,None) + if idA is None: + idA = self.actionList[id] = {} + fL = idA.get(action,None) + if fL is None: + fL = self.actionList[id][action] = [] + self.actionList[id][action].append(func) + + def execute(self,id,action,data,callback=None): + if action in ['#','*']: + return data + idA = self.actionList.get(id,None) + if idA is None: + return data + fL = idA.get(action,[]) + fL += idA.get('*',[]) + for f in fL: + data = f(id,action,data) + if len(fL)==0: + for f in idA.get('#',[]): + data = f(id,action,data) + if callback is not None: + callback(data) + return data + +if __name__ == '__main__': + def f(id,act,data): + return data + + def f1(id,act,data): + return data + + def f2(id,act,data): + return data + + def add(): + oa = ObjectAction() + oa.add('test','b',f) + #oa.add('test','*',f1) + oa.add('test','#',f2) + + def exe(): + oa = ObjectAction() + oa.execute('test','a','data1') + + add() + exe() + diff --git a/appPublic/outip.py b/appPublic/outip.py new file mode 100755 index 0000000..c85e40c --- /dev/null +++ b/appPublic/outip.py @@ -0,0 +1,93 @@ +import re +import time +import requests + +class IpGetter: + def __init__(self, url, parser): + self.url = url + self.parser = parser + self.cnt = 0 + self.total_time = 0 + self.avg_time = 0 + + def get(self): + try: + tim1 = time.time() + r = requests.get(self.url) + txt = r.text + ip = self.parser(txt) + tim2 = time.time() + cost_tim = tim2 - tim1 + self.cnt += 1 + self.total_time += cost_tim + self.avg_time = self.total_time / self.cnt + ret = self.check_ip(ip) + if ret: + return ret + self.avg_time = 10000 + print('Error, get=', ip) + return None + except Exception as e: + print(f'{self.url=}. {e=}') + self.avg_time = cost_tim = 10000 + return None + + def check_ip(self, ip): + ret = re.compile(r'(\d+.\d+.\d+.\d+)').search(ip) + if ret: + return ret.group(1) + print('ip format check failed', ip, self.url) + return None + + def get_average_time(self): + return self.avg_time + + def __str__(self): + return f'{self.url=},{self.avg_time=}' + +class OutIP: + def __init__(self): + self.getters = [] + self.set_known_getters() + + + def set_known_getters(self): + g = IpGetter('http://ipinfo.io/ip', lambda x: x) + self.add_getter(g) + g = IpGetter('https://api.ipify.org', lambda x: x) + self.add_getter(g) + g = IpGetter('https://ident.me', lambda x: x) + self.add_getter(g) + # g = IpGetter('https://ipapi.co/ip/', lambda x: x) + # self.add_getter(g) + g = IpGetter('http://myip.dnsomatic.com', lambda x: x) + self.add_getter(g) + g = IpGetter('https://checkip.amazonaws.com', lambda x: x.strip()) + self.add_getter(g) + def f(t): + return re.compile(r'Address: (\d+.\d+.\d+.\d+)').search(t).group(1) + g = IpGetter('http://checkip.dyndns.com', f) + self.add_getter(g) + + def add_getter(self, getter): + self.getters.append(getter) + + def get(self): + gs = self.getters.copy() + gs.sort(key=lambda a: a.get_average_time()) + for g in gs: + # print(*[str(g) for g in self.getters ]) + ip = g.get() + if ip: + return ip + return None + +if __name__ == '__main__': + oi = OutIP() + i = 0 + while i < 100: + ip = oi.get() + print('ip = ', ip) + time.sleep(1) + i += 1 + diff --git a/appPublic/pickleUtils.py b/appPublic/pickleUtils.py new file mode 100755 index 0000000..b764167 --- /dev/null +++ b/appPublic/pickleUtils.py @@ -0,0 +1,16 @@ +import pickle + +def saveData(fn,*args): + f = open(fn,'wb') + a = [ pickle.dump(arg,f) for arg in args ] + f.close() + +def loadData(fn,cnt): + a = [None] * cnt + try: + f = open(fn,'rb') + a = [ pickle.load(f) for i in range(cnt) ] + f.close() + return a + except: + return a diff --git a/appPublic/port_forward.py b/appPublic/port_forward.py new file mode 100755 index 0000000..739f309 --- /dev/null +++ b/appPublic/port_forward.py @@ -0,0 +1,162 @@ +import sys +import select +import paramiko +import socket +from appPublic.background import Background +try: + import SocketServer +except ImportError: + import socketserver as SocketServer + +class ForwardServer(SocketServer.ThreadingTCPServer): + daemon_threads = True + allow_reuse_address = True + server_ready = False + ready_callback = None + def service_actions(self): + super().service_actions() + if not self.server_ready: + self.server_ready = True + if self.ready_callback: + self.ready_callback() + + def shutdown(self): + self.server_ready = False + super().shutdown() + +g_verbose = True +def verbose(s): + if g_verbose: + print(s) + +class Handler(SocketServer.BaseRequestHandler): + def handle(self): + try: + chan = self.ssh_transport.open_channel( + "direct-tcpip", + (self.chain_host, self.chain_port), + self.request.getpeername(), + ) + except Exception as e: + verbose( + "Incoming request to %s:%d failed: %s" + % (self.chain_host, self.chain_port, repr(e)) + ) + return + if chan is None: + verbose( + "Incoming request to %s:%d was rejected by the SSH server." + % (self.chain_host, self.chain_port) + ) + return + + verbose( + "Connected! Tunnel open %r -> %r -> %r" + % ( + self.request.getpeername(), + chan.getpeername(), + (self.chain_host, self.chain_port), + ) + ) + while True: + r, w, x = select.select([self.request, chan], [], []) + if self.request in r: + data = self.request.recv(1024) + if len(data) == 0: + break + chan.send(data) + if chan in r: + data = chan.recv(1024) + if len(data) == 0: + break + self.request.send(data) + + peername = self.request.getpeername() + chan.close() + self.request.close() + verbose("Tunnel closed from %r" % (peername,)) + +def connect_ssh_server(host, port, user, password): + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(host, port=port, username=user, + password=password) + return ssh + +class SSHPortForward: + def __init__(self, local_port, remote_host, remote_port, + ssh_host, ssh_port, ssh_user, ssh_password): + self.local_port = int(local_port) + self.remote_host = remote_host + self.remote_port = int(remote_port) + self.ssh_host = ssh_host + self.ssh_port = int(ssh_port) + self.ssh_user = ssh_user + self.ssh_password = ssh_password + self.running = False + self._ready = False + + def service_ready(self): + print('servie ready .....') + self._ready = True + + def run(self): + if self.running: + return + self.running = True + b = Background(self._run) + b.start() + + def _run(self): + self.ssh = connect_ssh_server(self.ssh_host, + self.ssh_port, + self.ssh_user, + self.ssh_password) + + self.transport = self.ssh.get_transport() + class MyForwardServer(ForwardServer): + ready_callback = self.service_ready + + class SubHandler(Handler): + chain_host = socket.gethostbyname(self.remote_host) + chain_port = self.remote_port + local_port = self.local_port + ssh_transport = self.transport + + self.forward_server = MyForwardServer((socket.gethostbyname('localhost'), self.local_port), SubHandler) + self.forward_server.serve_forever() + print('forward ....') + + def stop(self): + if not self.running: + return + self.running = False + self.forward_server.shutdown() + self.forward_server.server_close() + self.transport.close() + self.ssh.close() + +if __name__ == '__main__': + if len(sys.argv) < 8: + print("""Usage: +{sys.argv[0] local_port remote_host remote_port ssh_host ssh_port ssh_user ssh_password +""") + sys.exit(1) + s = SSHPortForward(*sys.argv[1:]) + while True: + print("""start) start server, +stop) stop server +quit) quit +""") + x = input() + if x == 'start': + s.run() + continue + if x == 'stop': + s.stop() + continue + if x == 'quit': + s.stop() + break + print('error input') + diff --git a/appPublic/process_workers.py b/appPublic/process_workers.py new file mode 100755 index 0000000..0234290 --- /dev/null +++ b/appPublic/process_workers.py @@ -0,0 +1,36 @@ +import time +from multiprocessing import Process +import threading +import random +from appPublic.background import Background + +class ProcessWorkers: + def __init__(self, worker_cnt=10): + self.semaphore = threading.Semaphore(value=worker_cnt) + self.co_worker = 0 + def _do(self, func, *args, **kwargs): + self.semaphore.acquire() + self.co_worker += 1 + p = Process(target=func, args=args, kwargs=kwargs) + p.start() + p.join() + self.co_worker -= 1 + self.semaphore.release() + + def do(self, func, *args, **kwargs): + b = Background(self._do, func, *args, **kwargs) + b.start() + + def get_workers(self): + return self.co_worker + +if __name__ == '__main__': + def k(worker): + t = random.randint(1,4) + print('current workers=',worker.get_workers(), 'sleep=', t) + time.sleep(t) + + w = ProcessWorkers() + for i in range(100): + w.do(k, w) + diff --git a/appPublic/proxy.py b/appPublic/proxy.py new file mode 100755 index 0000000..28828af --- /dev/null +++ b/appPublic/proxy.py @@ -0,0 +1,12 @@ +import socket +import socks +import requests + +original_socket = socket.socket + +def set_socks_proxy(host, port): + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, host, port) + socket.socket = socks.socksocket + +def unset_proxy(): + socket.socket = original_socket diff --git a/appPublic/rc4.py b/appPublic/rc4.py new file mode 100755 index 0000000..4752dab --- /dev/null +++ b/appPublic/rc4.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +import time +import datetime +import random, base64 +from hashlib import sha1 + +class RC4: + def __init__(self,data_coding='utf8'): + self.bcoding = 'iso-8859-1' + self.dcoding = data_coding + self.salt = b'AFUqx9WZuI32lnHk' + + def _crypt(self,data,key): + """RC4 algorithm return bytes""" + x = 0 + box = [i for i in range(256) ] + for i in range(256): + x = (x + box[i] + key[i % len(key)]) % 256 + box[i], box[x] = box[x], box[i] + x = y = 0 + out = [] + for char in data: + x = (x + 1) % 256 + y = (y + box[x]) % 256 + box[x], box[y] = box[y], box[x] + out.append(chr(char ^ box[(box[x] + box[y]) % 256])) + + return ''.join(out).encode(self.bcoding) + + def encode_bytes(self, bdata, key): + a = sha1(key + self.salt) + k = a.digest() + data = self.salt + self._crypt(bdata, k) + return data + + def encode(self,data, key,encode=base64.b64encode, salt_length=16): + """RC4 encryption with random salt and final encoding""" + if type(data)==type(''): + data = data.encode(self.dcoding) + key = key.encode(self.bcoding) + code = self.encode_bytes(data, key) + if encode: + code = encode(code) + return code.decode(self.dcoding) + return code + + def decode_bytes(self, data, key): + salt_length = 16 + salt = data[:salt_length] + a = sha1(key + self.salt) + k = a.digest() #.decode('iso-8859-1') + r = self._crypt(data[salt_length:], k) + return r + + def decode(self,data, key,decode=base64.b64decode, salt_length=16): + """RC4 decryption of encoded data""" + if type(data)==type(''): + data = data.encode(self.dcoding) + key = key.encode(self.bcoding) + if decode: + data = decode(data) + r = self.decode_bytes(data, key) + return r.decode(self.dcoding) + +class KeyChain(object): + def __init__(self, seed_str, crypter=None, keylen=23, period=600, threshold=60, time_delta=0): + self.seed_str = seed_str + if isinstance(self.seed_str, str): + self.seed_str = self.seed_str.encode('utf-8') + self.period = int(period) + self.threshold = int(threshold) + self.crypter = crypter + self.time_delta = time_delta + if crypter is None: + self.crypter = RC4() + self.keylen = keylen + self.keypool = { + } + delta = datetime.timedelta(0) + self.timezone = datetime.timezone(delta, name='gmt') + + def get_timestamp(self): + ts = int(time.time()) - self.time_delta + return ts + + def is_near_bottom(self, indicator=None): + ts = self.get_timestamp() + i = indicator + if i is None: + i = self.get_indicator(ts) + if i + self.threshold > ts: + return True + return False + + def is_near_top(self, indicator=None): + ts = self.get_timestamp() + i = indicator + if i is None: + i = self.get_indicator(ts) + if i + self.period - self.threshold < ts: + return True + return False + + def get_indicator(self, ts=None): + if ts is None: + ts = self.get_timestamp() + return int(ts / self.period) * self.period + + def genKey(self, indicator): + vv = indicator + if self.keypool.get(vv): + return self.keypool[vv] + v = vv + k1 = 0 + k = '' + m = len(self.seed_str) + while k1 < self.keylen: + j = v % self.keylen + v = v - (j + k1) * m + self.keylen + k = k + chr(self.seed_str[j]) + k1 += self.threshold / 2 + key = k.encode('utf-8') + self.keypool[vv] = key + dates = [ d for d in self.keypool.keys() ] + for d in dates: + if d < indicator - self.period: + del self.keypool[d] + return key + + def encode(self, text): + bdata = text.encode('utf-8') + return self.encode_bytes(bdata) + + def encode_bytes(self, bdata): + indicator = self.get_indicator() + key = self.genKey(indicator) + data = key + bdata + return self.crypter.encode_bytes(data, key) + + def _decode(self, data, key): + d = self.crypter.decode_bytes(data, key) + if d[:len(key)] == key: + return d[len(key):] + return None + + def decode_bytes(self, data): + indicator = self.get_indicator() + key = self.genKey(indicator) + d = self._decode(data, key) + if d is not None: + return d + + if self.is_near_bottom(indicator): + indicator -= self.period + key = self.genKey(indicator) + return self._decode(data, key) + + if self.is_near_top(indicator): + indicator += self.period + key = self.genKey(indicator) + return self._decode(data, key) + return None + + def decode(self, data): + d = self.decode_bytes(data) + if d is None: + return None + return d.decode('utf-8') + +pwdkey = 'ytguiojbhvhbnkl' +def password(pwdtxt, key=pwdkey): + rc = RC4() + code = rc.encode(pwdtxt, key) + t = rc.decode(code, key) + if (t == pwdtxt): + return code + else: + return None + +def unpassword(code, key=pwdkey): + rc = RC4() + t = rc.decode(code, key) + return t + +""" +if __name__ == '__main__': + import sys + if len(sys.argv) > 1: + print(password(sys.argv[1])) + sys.exit(0) + ps = [ + '45dr6tcfyvguh', + 'ft7gy8uh9ij0', + 'opiluykhcgjfncm' + ] + for p in ps: + print(password(p)) +""" + +if __name__=='__main__': + # 需要加密的数据长度没有限制 + # 密钥 + + data=b"231r3 feregrenerjk gkht324g8924gnfw k;ejkvwkjerv" + key = b'123456' + rc4 = RC4() + kc = KeyChain('in the heaven, we are equal', rc4) + + print(data) + # 加码 + encoded_data = kc.encode_bytes(data) + print(encoded_data,len(encoded_data) ) + # 解码 + decoded_data = kc.decode_bytes(encoded_data) + print(data, decoded_data, decoded_data==data) diff --git a/appPublic/receiveMail.py b/appPublic/receiveMail.py new file mode 100755 index 0000000..9072c0f --- /dev/null +++ b/appPublic/receiveMail.py @@ -0,0 +1,137 @@ +import poplib,pdb,email,re,time +from email import header +import datetime +import os + +POP_ADDR = r'pop.126.com' +USER = '' +PASS = '' +CONFIG = '' + +def getYear(date): + rslt = re.search(r'\b2\d{3}\b', date) + return int(rslt.group()) + +def getMonth(date): + monthMap = {'Jan':1,'Feb':2,'Mar':3,'Apr':4,'May':5,'Jun':6, + 'Jul':7,'Aug':8,'Sep':9,'Oct':10,'Nov':11,'Dec':12,} + + rslt = re.findall(r'\b\w{3}\b', date) + for i in range(len(rslt)): + month = monthMap.get(rslt[i]) + if None != month: + break + + return month + +def getDay(date): + rslt = re.search(r'\b\d{1,2}\b', date) + return int(rslt.group()) + +def getTime(date): + rslt = re.search(r'\b\d{2}:\d{2}:\d{2}\b', date) + timeList = rslt.group().split(':') + + for i in range(len(timeList)): + timeList[i] = int(timeList[i]) + + return timeList + +def transformDate(date): + rslt = getYear(date) + rslt = rslt * 100 + rslt = rslt + getMonth(date) + rslt = rslt * 100 + rslt = rslt + getDay(date) + + + timeList = getTime(date) + for i in range(len(timeList)): + rslt = rslt * 100 + rslt = rslt + timeList[i] + + return rslt + +def getRecentReadMailTime(): + fp = open(CONFIG, 'r') + rrTime = fp.read() + fp.close() + return rrTime + +def setRecentReadMailTime(): + fp = open(CONFIG, 'w') + fp.write(time.ctime()) + fp.close() + return + +def getTimeEarly(period): + def years(n): + return datetime.timedelta(years=n) + def months(n): + return datetime.timedelta(years=n) + def days(n): + return datetime.timedelta(days=n) + def hours(n): + return datetime.timedelta(hours=n) + def minutes(n): + return datetime.timedelta(minutes=n) + def seconds(n): + return datetime.timedelta(seconds=n) + + funcs={ + 'y':years, + 'm':months, + 'd':days, + 'H':hours, + 'M':minutes, + 'S':seconds, + } + pattern='(\d*)([ymdHMS])' + r=re.compile(pattern) + s = r.findall(period) + t = datetime.datetime.now() + for v,ty in s: + td = funcs[ty](int(v)) + t = t - td + return time.ctime(t.timestamp()) + + +def parseMailContent(msg): + if msg.is_multipart(): + for part in msg.get_payload(): + parseMailContent(part) + else: + bMsgStr = msg.get_payload(decode=True) + charset = msg.get_param('charset') + msgStr = 'Decode Failed' + try: + if None == charset: + msgStr = bMsgStr.decode() + else: + msgStr = bMsgStr.decode(charset) + except: + pass + + print(msgStr) + +def recvEmail(POP_ADDR,USER,PASS,PERIOD,callback): + server = poplib.POP3(POP_ADDR) + server.user(USER) + server.pass_(PASS) + + mailCount,size = server.stat() + mailNoList = list(range(mailCount)) + mailNoList.reverse() + FROMTIME = getTimeEarly(PERIOD) + hisTime = transformDate(FROMTIME) + #pdb.set_trace() + for i in mailNoList: + message = server.retr(i+1)[1] + mail = email.message_from_bytes(b'\n'.join(message)) + + if transformDate(mail.get('Date')) > hisTime: + if not callback(mail): + break + #parseMailContent(mail) + else: + break diff --git a/appPublic/registerfunction.py b/appPublic/registerfunction.py new file mode 100755 index 0000000..63242b6 --- /dev/null +++ b/appPublic/registerfunction.py @@ -0,0 +1,89 @@ +import asyncio +from inspect import isfunction, iscoroutinefunction +from functools import partial +from appPublic.dictObject import DictObject +from appPublic.Singleton import SingletonDecorator +from appPublic.log import info, error + +@SingletonDecorator +class RegisterFunction: + def __init__(self): + self.registKW = {} + + def register(self,name,func): + if not isfunction(func) and not iscoroutinefunction(func): + error(f'RegisterFunction.register({name}, {func}): func is not a function or routine') + return + self.registKW[name] = func + + def get(self,name): + return self.registKW.get(name,None) + + async def exe(self, name, *args, **kw): + f = self.get(name) + if f is None: + error(f'{name=} function not registed') + return None + if iscoroutinefunction(f): + info(f'{name=} is coroutine function'); + return await f(*args, **kw) + return f(*args, **kw) + +@SingletonDecorator +class RegisterCoroutine: + def __init__(self): + self.kw = DictObject() + + def register(self, name, func): + if not isfunction(func) and not iscoroutinefunction(func): + error(f'RegisterFunction.register({name}, {func}): func is not a function or routine') + return + if not self.kw.get(name): + self.kw[name] = [func] + else: + self.kw[name].append(func) + async def exe(self, name, *args, **kw): + fs = self.kw.get(name) + if fs is None: + return + fs = fs.copy() + fs.reverse() + if fs: + for f in fs: + if iscoroutinefunction(f): + await f(*args, **kw) + else: + f(*args, **kw) + return None + +def getRegisterFunctionByName(name): + rf = RegisterFunction() + return rf.get(name) + +def registerFunction(name, func): + rf = RegisterFunction() + rf.register(name, func) + +async def main(): + d = {} + rf = RegisterCoroutine() + rf.register('test', z) + rf.register('test', y) + rf.register('test', x) + nd = await rf.exe('test', d) + print(nd) + +if __name__ == '__main__': + def x(dic): + dic['a'] = 'a' + return dic + + async def y(dic): + dic['b'] = 'b' + return dic + + def z(dic): + dic['c'] = 1 + return dic + + asyncio.get_event_loop().run_until_complete(main()) diff --git a/appPublic/restrictedEnv.py b/appPublic/restrictedEnv.py new file mode 100755 index 0000000..1ef2bcd --- /dev/null +++ b/appPublic/restrictedEnv.py @@ -0,0 +1,34 @@ +import appPublic.timeUtils as tu +import datetime as dt + +class RestrictedEnv: + def __init__(self): + self.reg('today',self.today) + self.reg('date',self.date) + self.reg('datetime',self.datetime) + self.reg('now',dt.datetime.now) + + def reg(self,k,v): + self.__dict__[k] = v + + def run(self,dstr): + dstr = '__tempkey__ = %s' % dstr + exec(dstr,globals(),self.__dict__) + return self.__tempkey__ + + def today(self): + now = dt.datetime.now() + return tu.ymdDate(now.year,now.month,now.day) + + def date(self,dstr): + return tu.str2Date(dstr) + + def datetime(self,dstr): + return tu.str2Datetime(dstr) + +if __name__ == '__main__': + ns = RestrictedEnv() + a = ns.run('today()') + b = ns.run("date('2011-10-31')") + c = ns.run('datetime("2012-03-12 10:22:22")') + d = ns.run('now()') diff --git a/appPublic/rsaPeer.py b/appPublic/rsaPeer.py new file mode 100755 index 0000000..6444daf --- /dev/null +++ b/appPublic/rsaPeer.py @@ -0,0 +1,128 @@ +from appPublic.rsawrap import RSA +from appPublic.rc4 import RC4 +try: + import ujson as json +except: + import json +import random + +class DeliverPacket: + def __init__(self,sender,c,k,s): + self.sender = sender + self.c = c + self.k = k + self.s = s + + def pack(self): + d = { + "sender":self.sender, + "c":self.c, + "k":self.k, + "s":self.s, + } + return json.dumps(d) + + def unpack(self,body): + d = json.loads(body) + self.sender = d.sender + self.c = d['c'] + self.k = d['k'] + self.s = d['s'] + +class RSAPeer: + def __init__(self,myid,myPrikey,pearPubKey=None): + self.myid = myid + self.mypri = myPrikey + self.peerpub = pearPubKey + self.rsa = RSA() + + def getPeerPublicKey(self,id): + pass + + def _genSystematicKey(self): + t = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890~!@#$%^&*' + kl = random.randint(10,15) + ky = [] + klen = len(t) - 1 + for k in range(kl): + i = random.randint(0,klen) + # print(k,klen,i) + ky.append(t[i]) + return ''.join(ky) + + def encode(self,text): + """ + return a json text + json ojbect have three addt: + k:encrypted rc4 key + s:signature + c:ciphertext encrypted by key + """ + d = {"id":self.myid,"data":text} + text = json.dumps(d) + sk = self._genSystematicKey() + rc4 = RC4(sk) + c = rc4.encode(text) + s = self.rsa.sign(self.mypri,sk) + if self.peerpub is None: + return None + k = self.rsa.encode(self.peerpub,sk) + d = { + 'c':c, + 'k':k, + 's':s + } + return json.dumps(d) + + def decode(self,body): + """ + cipher a json text + json ojbect have three addt: + k:encrypted rc4 key + s:signature + c:ciphertext encrypted by key + """ + d = json.loads(body) + signature = d['s'] + sk = self.rsa.decode(self.mypri,d['k']) + # print('sk=',sk,'k=',d['k'],type(d['k'])) + rc4 = RC4(sk) + t = rc4.decode(d['c']) + d = json.loads(t) + ret = d['data'] + if self.peerpub is not None and not self.rsa.check_sign(self.peerpub,sk,signature): + return None + if self.peerpub is None: + peerpub = self.getPeerPublicKey(d['id']) + if peerpub is None: + return None + if not self.rsa.check_sign(peerpub,sk,signature): + return None + return ret + +if __name__ == '__main__': + r = RSA() + mary_pri = r.create_privatekey() + mary_pub = r.create_publickey(mary_pri) + + john_pri = r.create_privatekey() + john_pub = r.create_publickey(john_pri) + + john_rp = RSAPeer(john_pri,mary_pub) + mary_rp = RSAPeer(mary_pri,john_pub) + txt = '''hello python 爱的实打实大师大师大师的发送到发送到而非个人格个二哥而而二哥而个人各位,UDP是一种无连接对等通信协议,没有服务器和客户端概念,通信的任何一方均可通过通信原语直接和其他方通信 + +HOME FAQ DOCS DOWNLOAD + + +index +next | +previous | +Twisted 18.9.0 documentation » Twisted Names (DNS) » Developer Guides » ''' + c = john_rp.encode(txt) + newtxt = mary_rp.decode(c) + print(txt) + print('<===>') + print(c) + print('<===>') + print(newtxt) diff --git a/appPublic/rsawrap.py b/appPublic/rsawrap.py new file mode 100755 index 0000000..c5433ae --- /dev/null +++ b/appPublic/rsawrap.py @@ -0,0 +1,108 @@ +import rsa + +class RSA: + + def __init__(self, keylength=4096, coding='iso8859'): + self.coding = coding + self.keylength = keylength + + def write_privatekey(self,private_key,fname,password=None): + bd = private_key.save_pkcs1() + with open(fname, 'wb') as f: + f.write(bd) + + def publickeyText(self,public_key): + bd = public_key.save_pkcs1() + return bd.decode(self.coding) + + def write_publickey(self,public_key,fname): + bd = public_key.save_pkcs1() + with open(fname, 'wb') as f: + f.write(bd) + + def read_privatekey(self,fname,password=None): + with open(fname, 'rb') as pf: + kd = pf.read() + return rsa.PrivateKey.load_pkcs1(kd) + + def publickeyFromText(self,text): + bd = text.encode(self.coding) + return rsa.PublicKey.load_pkcs1(bd) + + def read_publickey(self,fname): + with open(fname, 'rb') as pf: + kd = pf.read() + return rsa.PublicKey.load_pkcs1(kd) + + def create_privatekey(self, keylength=4096): + _, prik = rsa.newkeys(keylength) + return prik + + def create_publickey(self,private_key): + return rsa.PublicKey(private_key.n, private_key.e) + + def encode_bytes(self, public_key, bdata): + return rsa.encrypt(bdata, public_key) + + def encode(self,public_key,text): + bdata = text.encode(self.coding) + bc = self.encode_bytes(public_key, bdata) + return bc.decode(self.coding) + + def decode_bytes(self, private_key, bdata): + return rsa.decrypt(bdata, private_key) + + def decode(self,private_key,cipher): + bc = cipher.encode(self.coding) + bd = self.decode_bytes(private_key, bc) + return bd.decode(self.coding) + + def sign_bdata(self, private_key, data_to_sign): + return rsa.sign(data_to_sign, private_key, 'SHA-1') + + def sign(self,private_key,message): + bd = message.encode(self.coding) + bs = self.sign_bdata(private_key, bd) + return bs.decode(self.coding) + + def check_sign_bdata(self, public_key, bdata, sign): + try: + r = rsa.verify(bdata, sign, public_key) + if r == 'SHA-1': + return True + print(f'verify()={r}') + return False + except Exception as e: + print(f'check_sign_bdata() raise Exception{e}') + return False + + + def check_sign(self,public_key,plain_text,signature): + bd = plain_text.encode(self.coding) + bs = signature.encode(self.coding) + return self.check_sign_bdata(public_key, bd, bs) + + +if __name__ == '__main__': + import os + prikey1_file = os.path.join(os.path.dirname(__file__),'..','test', 'prikey1.rsa') + r = RSA() + mpri = r.create_privatekey(2048) + mpub = r.create_publickey(mpri) + + zpri = r.create_privatekey(2048) + zpub = r.create_publickey(zpri) + + l = 100 + while True: + text = 'h' * l + cipher = r.encode(mpub,text) + ntext = r.decode(mpri,cipher) + print('textlen=', l, 'encode text=', text, \ + 'decode result=', ntext, + 'cyber size=', len(cipher), + 'check if equal=', text==ntext) + signature = r.sign(zpri,text) + check = r.check_sign(zpub,text,signature) + print('sign and verify=',len(signature),check) + l += 1 diff --git a/appPublic/set_fgcolor.py b/appPublic/set_fgcolor.py new file mode 100755 index 0000000..02d7e68 --- /dev/null +++ b/appPublic/set_fgcolor.py @@ -0,0 +1,33 @@ +# -*- coding=utf-8 -*- + +""" +kivy color: +[ r, g, b, a] +不同的颜色值总能找到一个人眼感知的灰度值,这是著名的心理学公式: +灰度 = 红×0.299 + 绿×0.587 + 蓝×0.114 +当灰度值大于0.5时使用暗色,否则使用明色 +colors 两个颜色,缺省为空,使用函数内置的两个颜色 +""" + +def color_gray_rate(color): + graylevel = 0.299 * color[0] + \ + 0.587 * color[1] + \ + 0.114 * color[2] + return graylevel + +def get_fgcolor_from_bgcolor(bgcolor, colors=None): + dark_fgcolor=[0.11,0.11,0.11,1] + bright_fgcolor=[0.89,0.89,0.89,1] + + graylevel = color_gray_rate(bgcolor) + if colors == None: + if graylevel > 0.5: + return dark_fgcolor + else: + return bright_fgcolor + r1 = color_gray_rate(colors[0]) + r2 = color_gray_rate(colors[1]) + if abs(graylevel - r1) > abs(graylevel - r2): + return colors[0] + return colors[1] + diff --git a/appPublic/sockPackage.py b/appPublic/sockPackage.py new file mode 100755 index 0000000..16ff738 --- /dev/null +++ b/appPublic/sockPackage.py @@ -0,0 +1,136 @@ +import os +import time +import threading +import sys +import socket + +def get_free_local_addr(): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(("8.8.8.8", 80)) + return s.getsockname() + # return (ip,port) + +class background(threading.Thread) : + def __init__(self,func,kw) : + threading.Thread.__init__(self) + self.func = func + self.kw = kw + + def run(self) : + if self.func!=None : + self.func(**self.kw) + return + +def BackgroundCall(func,datas) : + b=background(func,datas) + b.start() + return + +class SocketServerError(Exception) : + pass + +class SocketClientError(Exception) : + pass + +class SocketServer(threading.Thread) : + + def __init__(self,host,port,max_connect=10,callee=None) : + threading.Thread.__init__(self, name = 'SocketServer') + self.setDaemon(False) + self.host = host + self.port = int(port) + self.max_c = max_connect + self.ready = False + self.keep_running = 0 + self.callee = callee + self.setSocketServer() + + def setSocketServer(self) : + try : + self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + self.sock.bind((self.host,self.port)) + self.sock.listen(self.max_c) + self.ready = True + except Exception as e: + print('setSocketServer() Error:%s\nhost=%s,port=%d' % (e,self.host,self.port)) + pass + + def run(self) : + if not self.ready : + raise SocketServerError('not ready') + callee = self.callee + if self.callee!=None : + callee = self.callee + self.keep_running = 1 + while self.keep_running : + conn,addr = self.sock.accept() + BackgroundCall(callee,{'conn':conn,'addr':addr}) + # conn.close() + + def stop(self) : + self.keep_running = 0 + + def callee(self,conn,addr) : + while 1 : + d = conn.recv(1024) + if d==None : + break + conn.send(d) + con.close() + +class SocketClient : + + def __init__(self,host,port) : + self.host = host + self.port = port + self.ready = False + self.connect() + + # if tim ==0 not blocking + def timeout(self,tim) : + if self.ready : + self.sock.setblocking(tim>0) + if tim>0 : + self.sock.settimeout(tim) + + def connect(self) : + try : + self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + self.sock.connect((self.host,self.port)) + self.ready = True + except Exception as e: + self.ready = False + print('Socket connect error,%s\nhost=%s,port=%s' % (e,self.host,self.port)) + raise SocketClientError('connect error') + + def read(self,size) : + try : + data = self.sock.recv(size) + return data + except Exception as e: + print('recv error,%s' % e) + raise SocketClientError('recv error') + + def write(self,data) : + try : + self.sock.send(data) + except Exception as e: + print('recv error,%s' % e) + raise SocketClientError('send error') + + def close(self) : + self.sock.close() + self.ready = False + +if __name__ == '__main__' : + s = SocketServer('localhost',12232) + s.start() + time.sleep(5) + while 1 : + c = SocketClient('localhost',12232) + msg = 'msg1' + print("send:",msg) + c.write(msg) + d = c.read(1024) + print("get:",d) + time.sleep(1) diff --git a/appPublic/sshx.py b/appPublic/sshx.py new file mode 100644 index 0000000..65bcbd2 --- /dev/null +++ b/appPublic/sshx.py @@ -0,0 +1,335 @@ +import os +import sys +import time +import shlex +from functools import partial +from threading import Thread +from appPublic.myTE import tmpTml +import asyncio, asyncssh, sys + +class SSHNode: + def __init__(self, host, + username='root', + port=22, + password=None, + jumpers=[]): + self.server2 = { + "host":host, + "username":username, + "password":password, + "port":port + } + print(self.server2) + self.jumpers = jumpers + self.conn = None + self.jumper_conns = [] + self.batch_cmds = [] + + def info(self): + d = { + "jumpers":self.jumpers, + } + d.update(self.server2) + return d + + def asjumper(self): + a = self.jumpers.copy() + a.append(self.server2) + return a + + def set_jumpers(self, jumpers): + self.jumpers = jumpers + + async def connect(self): + refconn = None + for j in self.jumpers: + host = j['host'] + username = j.get('username', 'root') + port = j.get('port',22) + password= j.get('password', None) + if refconn: + refconn = await refconn.connect_ssh(host, + username=username, + known_hosts=None, + password=password, + port=port) + else: + refconn = await asyncssh.connect(host, + username=username, + known_hosts=None, + password=password, + port=port) + self.jumper_conns.append(refconn) + + host = self.server2['host'] + username = self.server2.get('username', 'root') + port = self.server2.get('port',22) + password = self.server2.get('password', None) + if refconn: + self.conn = await refconn.connect_ssh(host, + username=username, + port=port, + password=password, + known_hosts=None) + else: + self.conn = await asyncssh.connect(host, + username=username, + password=password, + port=port) + + def close(self): + self.conn.close() + cnt = len(self.jumper_conns) + cnt -= 1 + while cnt >= 0: + self.jumper_conns[cnt].close() + cnt -= 1 + self.jumper_conns = [] + self.conn = None + + async def _l2r(self, lf, rf): + x = await asyncssh.scp(lf, (self.conn, rf), + preserve=True, recurse=True) + return x + + async def _process(self, *args, **kw): + a = await self.conn.create_process(*args, **kw) + return a + + async def _r2l(self, rf, lf): + x = await asyncssh.scp((self.conn, rf), lf, + preserve=True, recurse=True) + return x + + async def _cmd(self, cmd, input=None, stdin=None, stdout=None): + return await self.conn.run(cmd, input=input, stdin=stdin, stdout=stdout) + + async def _xcmd(self, cmd, xmsgs=[], ns={}, + show_input=None, + show_stdout=None): + proc = await self._process(cmd, term_type='xterm', + + term_size=(80,24), + encoding='utf-8' + ) + + keyin = False + def feed_data(xmsgs, debug_input): + if len(xmsgs) == 0: + print('#####++##### xmsgs has zero elements') + return + keyin = True + a = xmsgs.pop(0) + while True: + if a[1] is None: + proc.stdin.write_eof() + self.running = False + else: + s = a[1].format(**ns) + proc.stdin.write(s) + + if len(xmsgs) == 0 or xmsgs[0][0]: + break + a = xmsgs.pop(0) + + already_output = False + callee = None + loop = asyncio.get_event_loop() + self.running = True + while self.running: + if keyin: + keyin = False + await proc.stdin.drain() + if proc.stdout.at_eof(): + break + tup = proc.collect_output() + x = tup[0] + if x!='' and show_stdout: + if x is None: + break + if callee: + callee.cancel() + callee = None + show_stdout(x) + else: + if callee is None: + if len(xmsgs) > 0: + f = partial(feed_data, xmsgs, show_input) + t = xmsgs[0][0] or 0 + callee = loop.call_later(t, f) + await asyncio.sleep(0.05) + + print('##########fininshed##########') + + async def _run(self, cmd, input=None, stdin=None, stdout=None): + if cmd.startswith('l2r'): + args = shlex.split(cmd) + if len(args) == 3: + x = await self._l2r(args[1], args[2]) + return x + + if cmd.startswith('r2l'): + args = shlex.split(cmd) + if len(args) == 3: + x = await self._r2l(args[1], args[2]) + return x + + return await self._cmd(cmd, input=input, stdin=stdin, stdout=stdout) + + def show_result(self, x): + if isinstance(x, Exception): + print('Exception:',e) + else: + print('stdout:', x.stdout) + print('stderr:', x.stderr) + + async def run(self, cmd, input=None, stdin=None, stdout=None): + await self.connect() + result = await self._run(cmd, input=input, + stdin=stdin, stdout=stdout) + self.close() + return result + +class SSHNodes: + def __init__(self, nodes, usernmae='root', port=22, jumpers=[]): + self.nodes = [ Node(n, username=username, port=port, jumpers=jumpers) for n in nodes ] + self.batch_cmds = [] + + def append_cmd(self, cmd, stdin=None, stdout=None): + self.batch_cmds.append({ + "cmd":cmd, + "stdin":stdin, + "stdout":stdout}) + + def show_result(self, result, i=0): + if isinstance(result, Exception): + print(f'Task {i} failed:{result}') + elif result.exit_status != 0: + print(f'Task {i} exit {result.exit_status}') + print(result.stderr, end='') + else: + print(f'Task {i} successed:') + print(result.stdout, end='') + + async def run(self, cmd, stdin=None, stdout=None): + tasks = [ n.run(cmd, stdin=stdin, stdout=stdout) for n in self.nodes ] + results = await asyncio.gather(*tasks, return_exceptions=True) + return results + + async def exe_batch(self): + tasks = [ n.exe_batch(self.batch_cmds) for n in self.nodes ] + results = await asyncio.gather(*tasks, return_excetion=True) + return results + for i, result in enumerate(results): + self.show_result(result,i) + +async def main(): + if len(sys.argv) < 3: + print(f'{sys.argv[0]} cmd host1 host2 ....') + sys.exit(1) + + cmd = sys.argv[1] + jumpor = { + "host":"glib.cc", + "username":"ceni", + "port":10022 + } + hosts = sys.argv[2:] + mn = SSHNodes(hosts, jumpers=[jumpor]) + while True: + print('input command:') + cmd = input() + print('input stdin:') + stdin = input() + if stdin == '': + stdin = None + print('input stdout:(default is stdout)') + stdout = input() + if stdout == '': + stdout = None + x = await mn.run(cmd, stdin=stdin, stdout=stdout) + for r in x: + if isinstance(r, Exception): + print(r) + else: + print(r.stdout) + +class SSHBash: + def __init__(self, node, loop=None): + if loop is None: + loop = asyncio.get_event_loop() + self.node = node + self.loop = loop + self.conn = None + self.stdin_need = False + self.subloop = asyncio.new_event_loop() + self.subthread = Thread(target=self.start_thread_loop) + self.subthread.setDaemon(True) + self.subthread.start() + + def start_thread_loop(self): + asyncio.set_event_loop(self.subloop) + self.subloop.run_forever() + + def exit(self): + if self.conn: + self.node.close(self.conn) + self.p_obj.close() + self.subloop.stop() + self.loop.stop() + + async def feed_stdin(self, f): + self.stdin_need = False + x = await f(65535) + if x is None: + self.exit() + self.p_obj.stdin.write(x) + await self.p_obj.stdin.drain() + self.stdin_need = True + + async def run(self, read_co, write_co): + await self.node.connect() + self.p_obj = await self.node._process('bash', + term_type='vt100', + term_size=(80,24), + encoding=None) + if isinstance(self.p_obj, Exception): + print('Excetion:', self.p_obj) + self.exit() + return + if self.p_obj is None: + print('self.p_obj is None') + self.exit() + return + # self.loop.add_reader(sys.stdin.fileno(), self.read_input) + self.stdin_need = True + while True: + if self.stdin_need: + asyncio.run_coroutine_threadsafe(self.feed_stdin(read_co), self.subloop) + + if self.p_obj.stdout.at_eof(): + self.exit() + break + x = await self.p_obj.stdout.read(1024) + await write_co(x) + +if __name__ == '__main__': + async def sysstdin_read(): + return os.read(sys.stdin.fileno(), 65535) + + async def sysstdout_write(x): + sys.stdout.write(x.decode('utf-8')) + + async def test_sshbash(): + jp = { + "host":"glib.cc", + "username":"ceni", + "port":10022 + } + jn = SSHNode('k3', jumpers=[jp]) + bash = SSHBash(jn) + await bash.run(sysstdin_read, sysstdout_write) + + loop = asyncio.get_event_loop() + loop.run_until_complete(test_sshbash()) + diff --git a/appPublic/strUtils.py b/appPublic/strUtils.py new file mode 100755 index 0000000..abaa82f --- /dev/null +++ b/appPublic/strUtils.py @@ -0,0 +1,20 @@ +# strUtils + +def rtrim(ss): + s = ss + if s=='': + return s + while s[-1] == ' ': + s = s[:-1] + return s +def ltrim(ss): + s = ss + if s=='': + return s + while s[0] == ' ': + s = s[1:] + return s +def lrtrim(ss): + s = ltrim(ss) + s = rtrim(s) + return s diff --git a/appPublic/t b/appPublic/t new file mode 100644 index 0000000..a83a5d3 --- /dev/null +++ b/appPublic/t @@ -0,0 +1 @@ +data: {"created":1713348954 diff --git a/appPublic/t.py b/appPublic/t.py new file mode 100644 index 0000000..73ad434 --- /dev/null +++ b/appPublic/t.py @@ -0,0 +1,8 @@ +import json +from dataencoder import quotedstr + +d = { + "gret":"HGREert", + "ynh":"RtghretbertBHER" +} +print(quotedstr(json.dumps(d))) diff --git a/appPublic/testdict.py b/appPublic/testdict.py new file mode 100755 index 0000000..251e953 --- /dev/null +++ b/appPublic/testdict.py @@ -0,0 +1,6 @@ +import ExecFile +c = ExecFile.DictConfig(path='./config.dict') + +print(c.d.b[1].c,c.d.c.a,c.d.c.b,c.d.c.c[3].f) +print(c.d.c.c[1]) +print(c.d.c.d) diff --git a/appPublic/thread_workers.py b/appPublic/thread_workers.py new file mode 100755 index 0000000..45ad8eb --- /dev/null +++ b/appPublic/thread_workers.py @@ -0,0 +1,41 @@ +import time +import threading +import random +from appPublic.background import Background + +class ThreadWorkers: + def __init__(self, max_workers=10): + self.semaphore = threading.Semaphore(value=max_workers) + self.co_worker = 0 + def _do(self, func, *args, **kwargs): + try: + self.semaphore.acquire() + self.co_worker += 1 + func(*args, **kwargs) + finally: + self.co_worker -= 1 + self.semaphore.release() + + + def do(self, func, *args, **kwargs): + b = Background(self._do, func, *args, **kwargs) + b.start() + + def get_workers(self): + return self.co_worker + + def until_done(self): + time.sleep(0.1) + while self.co_worker > 0: + time.sleep(0.01) + +if __name__ == '__main__': + def k(worker): + t = random.randint(1,4) + print('current workers=',worker.get_workers(), 'sleep=', t) + time.sleep(t) + + w = ThreadWorkers(max_workers=30) + for i in range(100000): + w.do(k, w) + diff --git a/appPublic/timeUtils.py b/appPublic/timeUtils.py new file mode 100755 index 0000000..c4e76fe --- /dev/null +++ b/appPublic/timeUtils.py @@ -0,0 +1,253 @@ +import os,sys +import time +from datetime import date, timedelta, datetime + +leapMonthDays = [0,31,29,31,30,31,30,31,31,30,31,30,31] +unleapMonthDays = [0,31,28,31,30,31,30,31,31,30,31,30,31] + +def curDatetime(): + return datetime.now() + +def curDateString(): + d = curDatetime() + return '%04d-%02d-%02d' %(d.year,d.month,d.day) + +def curTimeString(): + d = curDatetime() + return '%02d:%02d:%02d' %(d.hour,d.minute,d.second) + +def timestampstr(): + d = curDatetime() + return '%04d-%02d-%02d %02d:%02d:%02d.%03d' % (d.year, + d.month, + d.day, + d.hour, + d.minute, + d.second, + d.microsecond/1000) + +def isMonthLastDay(d): + dd = timedelta(1) + d1 = d + dd + if d1.month != d.month: + return True + return False + +def isLeapYear(year): + if year % 4 == 0 and year % 100 == 0 and not (year % 400 == 0): + return True + return False + +def timestamp(dt): + return int(time.mktime((dt.year,dt.month,dt.day,dt.hour,dt.minute,dt.second,dt.microsecond,0,0))) + +def timeStampSecond(dt): + return int(time.mktime((dt.year,dt.month,dt.day,dt.hour,dt.minute,dt.second,0,0,0))) + +def addSeconds(dt,s): + ndt = dt + timedelta(0,s) + return ndt + +def monthMaxDay(y,m): + if isLeapYear(y): + return leapMonthDays[m] + return unleapMonthDays[m] + +def date2str(dt=None): + if dt is None: + dt = curDatetime() + return '%04d-%02d-%02d' % (dt.year,dt.month,dt.day) + +def time2str(dt): + return '%02d:%02d:%02d' % (dt.hour,dt,minute,dt.second) + +def str2Date(dstr): + try: + haha = dstr.split(' ') + y,m,d = haha[0].split('-') + H = M = S = 0 + if len(haha) > 1: + H,M,S = haha[1].split(':') + return ymdDate(int(y),int(m),int(d),int(H),int(M),int(S)) + except Exception as e: + print(e) + return None + +def ymdDate(y,m,d,H=0,M=0,S=0): + return datetime(y,m,d,H,M,S) + +def str2Datetime(dstr): + x = dstr.split(' ') + d = x[0] + t = '00:00:00' + if len(x) > 1: + t = x[1] + y,m,d = d.split('-') + H,M,S = t.split(':') + return datetime(int(y),int(m),int(d),int(H),int(M),int(S)) + +def strdate_add(date_str, days=0, months=0, years=0): + dt = str2Datetime(date_str) + dt = dateAdd(dt, days=days, months=months, years=years) + ds = date2str(dt) + return ds + +def addMonths(dt,months): + y = dt.year + m = dt.month + months + d = dt.day + mm = (m - 1) % 12 + 1 + md = int((m - 1) / 12) + y += md + m = mm + maxd = monthMaxDay(y,m) + if d > maxd: + d = maxd + return ymdDate(y,m,d) + +def addYears(dt,years): + y = dt.year + years + m = dt.month + d = dt.day + maxd = monthMaxDay(y,m) + if d > maxd: + d = maxd + return ymdDate(y,m,d) + +def dateAdd(dt,days=0,months=0,years=0): + if days != 0: + dd = timedelta(days) + dt = dt + dd + if months != 0: + dt = addMonths(dt,months) + if years != 0: + dt = addYears(dt,years) + return dt + +def firstSunday(dt): + f = dt.weekday() + if f<6: + return dt + timedelta(7 - f) + return dt + +DTFORMAT = '%Y%m%d %H%M%S' +def getCurrentTimeStamp() : + t = time.localtime() + return TimeStamp(t) + +def TimeStamp(t) : + return time.strftime(DTFORMAT,t) + +def StepedTimestamp(baseTs,ts,step) : + if step<2 : + return ts + offs = int(timestampSub(ts,baseTs)) + step = int(step) + r,m = divmod(offs,step) + if m < step/2 : + return timestampAdd(baseTs,step * r) + else : + return timestampAdd(baseTs,step * (r+1)) + +def timestampAdd(ts1,ts2) : + t1 = time.strptime(ts1,DTFORMAT) + tf = time.mktime(t1) + if type(ts2)=='' : + t2 = time.strptime(ts2,DTFORMAT) + ts2 = time.mktime(t2) + tf += ts2 + t = time.localtime(tf) + return TimeStamp(t) + +def timestampSub(ts1,ts2) : + t1 = time.strptime(ts1,DTFORMAT) + t2 = time.strptime(ts2,DTFORMAT) + ret = time.mktime(t1) - time.mktime(t2) + return int(ret) + +def timestamp2dt(t): + return datetime.fromtimestamp(t) + +def date_weekinyear(date_str): + w = datetime.strptime(date_str, '%Y-%m-%d').strftime('%W') + return date_str[:5] + w + +def date_season(date_str): + m = date_str[5:7] + sl = { + '01':'1', + '02':'1', + '03':'1', + '04':'2', + '05':'2', + '06':'2', + '07':'3', + '08':'3', + '09':'3', + '10':'4', + '11':'4', + '12':'4', + } + s = sl.get(m) + return date_str[:5] + s + +""" +Patterns = + 'D' + 'W[0-6]' + 'M[00-31]' + 'S[0-2]-[00-31]' + 'Y[01-12]-[00-31]' +} +""" + +def str2date(sd): + a = [ int(i) for i in sd.split('-') ] + return date(*a) + +def is_monthend(dt): + if isinstance(dt, str): + dt = str2date(dt) + nxt_day = dt + timedelta(days=1) + if dt.month != nxt_day.month: + return True + return False + +def is_match_pattern(pattern, strdate): + """ +R:代表实时 +D:代表日 +W[0-6]:代表周日到周六 +M[00-31]:代表月末月到某一天 +S[1-3]-[00-31]:代表季度第几个月的第几天 +Y[1-12]-[00-31]:代表一年中的某个月的某一天 + """ + if pattern == 'D': + return True + dt = str2date(strdate) + if pattern.startswith('W'): + w = (int(pattern[1]) + 6) % 7 + if dt.weekday() == w: + return True + return False + if pattern.startswith('M'): + day = int(pattern[1:]) + if day == 0 and is_monthend(dt): + return True + if day == dt.day: + return True + return False + if pattern.startswith('S'): + m,d = [ int(i) for i in pattern[1:].split('-') ] + print(f'{m=}-{d=}, {dt.month=} {dt.day}') + m %= 4 + if m == dt.month % 4 and d == dt.day: + return True + return False + if pattern.startswith('Y'): + m,d = [ int(i) for i in pattern[1:].split('-') ] + print(f'{m=}-{d=}, {dt.month=} {dt.day}') + if m == dt.month and d == dt.day: + return True + return False + diff --git a/appPublic/timecost.py b/appPublic/timecost.py new file mode 100755 index 0000000..190496b --- /dev/null +++ b/appPublic/timecost.py @@ -0,0 +1,42 @@ +import time +import datetime +from .Singleton import SingletonDecorator + +timerecord = {} + +class TimeCost: + def __init__(self,name): + self.name = name + + def __enter__(self): + self.begin_time = time.time() + + def __exit__(self,*args): + self.end_time = time.time() + d = timerecord.get(self.name,[]) + d.append(self.end_time - self.begin_time) + timerecord[self.name] = d + + def clear(self): + timerecord = {} + + @classmethod + def clear_all(self): + timerecord = {} + + @classmethod + def clear(self, name): + timerecord[name] = [] + + @classmethod + def show(self): + def getTimeCost(name): + x = timerecord.get(name,[]) + if len(x) == 0: + return 0,0,0 + return len(x), sum(x), sum(x)/len(x) + + print('TimeCost ....') + for name in timerecord.keys(): + print(name, * getTimeCost(name)) + diff --git a/appPublic/tworkers.py b/appPublic/tworkers.py new file mode 100755 index 0000000..4a393c3 --- /dev/null +++ b/appPublic/tworkers.py @@ -0,0 +1,62 @@ +import sys +import threading +from threading import Thread +from queue import Queue, Empty + +class Worker(Thread): + def __init__(self, rqueue, timeout=1): + Thread.__init__(self) + self.timeout = timeout + self.setDaemon(False) + self.r_queue = rqueue + self.start() + + def run(self): + emptyQueue = False + while True: + try: + callable,args,kw = self.r_queue.get(timeout=self.timeout) + if callable is None: + break + callable(*args,**kw) + + except Empty: + time.sleep(1) + + def resulthandler(self,rez): + pass + +class ThreadWorkers: + def __init__(self,num_workers=20): + self.workQueue = Queue() + self.worker_cnt = num_workers + self.workers = [] + self.__createThreadPool(num_workers) + + def __createThreadPool(self,num): + for i in range(num): + thread = Worker(self.workQueue) + self.workers.append(thread) + + def wait_for_complete(self): + for i in range(self.worker_cnt): + self.add_job(None,None,None) + + while len(self.workers): + thread = self.workers.pop() + if thread.isAlive(): + thread.join() + + def add_job(self,callable,args=[],kw={}): + self.workQueue.put([callable,args,kw]) +if __name__ == '__main__': + import requests + def get(url): + x = requests.get(url) + print(x.status_code) + + tw = ThreadWorkers() + for i in range(10000): + tw.add_job(get,['http://www.baidu.com']) + tw.wait_for_complete() + print('finished') diff --git a/appPublic/udp_comm.py b/appPublic/udp_comm.py new file mode 100755 index 0000000..52e29cf --- /dev/null +++ b/appPublic/udp_comm.py @@ -0,0 +1,103 @@ +# -*- coding:UTF-8 -*- +import time +from traceback import print_exc +from socket import * +from select import select + +import json +from appPublic.sockPackage import get_free_local_addr +from appPublic.background import Background +BUFSIZE = 1024 * 64 +class UdpComm: + def __init__(self, port, callback, timeout=0): + self.buffer = [] + self.callback = callback + self.timeout = timeout + self.host = get_free_local_addr()[0] + self.port = port + self.udpSerSock = socket(AF_INET, SOCK_DGRAM) + # 设置阻塞 + # self.udpSerSock.setblocking(1 if timeout > 0 else 0) + # 设置超时时间 1s + # self.udpSerSock.settimeout(timeout) + self.udpSerSock.bind(('' ,port)) + self.run_flg = True + self.thread = Background(self.run) + self.thread.start() + + def run(self): + sock = self.udpSerSock + while self.run_flg: + outs = [] + if len(self.buffer) > 0: + outs = [sock] + in_s, out_s, exc_s = select([sock], outs, [], 0.1) + if sock in in_s: + b, addr = sock.recvfrom(BUFSIZE) + t = b[0] + b = b[1:] + if t == 'b': + self.callback(b, addr) + else: + try: + txt = b.decode('utf-8') + d = json.loads(txt) + self.callback(d, addr) + except Exception as e: + print('except:',e) + print_exc() + print(t, b) + break + if sock in out_s: + while len(self.buffer) > 0: + d,addr = self.buffer.pop(0) + sock.sendto(d, addr) + time.sleep(0.1) + self.run_flg = False + self.udpSerSock.close() + + def stop(self): + self.run_flg = False + self.udpSerSock.close() + self.thread.join() + + def broadcast(self, data): + broadcast_host = '.'.join(self.host.split('.')[:-1]) + '.255' + udpCliSock = socket(AF_INET, SOCK_DGRAM) + # udpCliSock.settimeout(1) + udpCliSock.bind(('', 0)) + udpCliSock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + b = data + if not isinstance(data, bytes): + b = json.dumps(data).encode('utf-8') + udpCliSock.sendto(b, (broadcast_host,self.port)) + + def send(self,data,addr): + b = data + if not isinstance(data, bytes): + b = b'j' + json.dumps(data).encode('utf-8') + else: + b = b'b' + data + if isinstance(addr,list): + addr = tuple(addr) + self.buffer.append((b, addr)) + + def sends(self,data, addrs): + for a in addrs: + self.send(data, a) + +if __name__ == '__main__': + import sys + def msg_handle(data, addr): + print('addr:', addr, 'data=', data, len(data)) + + port = 50000 + if len(sys.argv)>1: + port = int(sys.argv[1]) + d = UdpComm(port, msg_handle) + x = input() + while x: + port, data = x.split(':') + d.send(data, ('', int(port))) + x = input() + diff --git a/appPublic/uni_outip.py b/appPublic/uni_outip.py new file mode 100755 index 0000000..88f7c25 --- /dev/null +++ b/appPublic/uni_outip.py @@ -0,0 +1,70 @@ +import os +import time +from natpmp import NATPMP as pmp +import upnpclient +from appPublic.ipgetter import IPgetter +from multiprocessing import Process, Pipe + +def pmp_get_external_ip(): + try: + return pmp.get_public_address() + except: + return None + +def upnp_get_external_ip(): + try: + igd = upnpclient.discover()[0] + print(igd.service_map) + + s_names = [ n for n in igd.service_map.keys() if 'WAN' in n and 'Conn' in n] + upnp = igd.service_map[s_names[0]] + x = upnp.GetExternalIPAddress() + return x.get('NewExternalIPAddress', None) + except Exception as e: + print(f'e={e}') + return None + +def ipgetter_get_external_ip(): + getter = IPgetter() + ip = None + while ip is None: + try: + ip = getter.get_external_ip() + except: + ip = None + if ip: + return ip + time.sleep(0.1) + +def get_external_ip(): + ip = pmp_get_external_ip() + if ip: + return ip + ip = upnp_get_external_ip() + if ip: + return ip + return ipgetter_get_external_ip() + +def outip(w): + os.dup2(w.fileno(), 1) + ip = get_external_ip() + print(ip) + +def get_ip(): + r, w = Pipe() + reader = os.fdopen(r.fileno(), 'r') + p = Process(None, outip, 'TESTER', (w, )) + p.start() + ip = reader.readline() + p.join() + return ip.strip() + +def run(): + while True: + ip = get_ip() + if ip: + print(f'{ip=}') + time.sleep(10) + +if __name__ == '__main__': + run() diff --git a/appPublic/unicoding.py b/appPublic/unicoding.py new file mode 100755 index 0000000..9cacba9 --- /dev/null +++ b/appPublic/unicoding.py @@ -0,0 +1,39 @@ +#unidict.py +import locale + +def unicoding(d,coding='utf8'): + if type(d) == type(''): + return d + if type(d) == type(b''): + try: + if coding is not Noene: + return d.decode(coding) + else: + return d.decode(locale.getdefaultlocale()[1]) + except: + try: + return d.decode(locale.getdefaultlocale()[1]) + except: + try: + return d.decode('utf8') + except: + return d + return d + +def uObject(obj,coding='utf8'): + otype = type(obj) + if otype == type(u''): + return obj + if otype == type({}): + return uDict(obj,coding) + if otype == type([]): + return [uObject(i,coding) for i in obj ] + if hasattr(obj,'decode'): + return obj.decode(coding) + return obj + +def uDict(dict,coding='utf8'): + d = {} + for k,v in dict.items(): + d[uObject(k)] = uObject(v) + return d diff --git a/appPublic/uniqueID.py b/appPublic/uniqueID.py new file mode 100755 index 0000000..7387af8 --- /dev/null +++ b/appPublic/uniqueID.py @@ -0,0 +1,36 @@ +import uuid +from nanoid import generate + +def setNode(n='ff001122334455'): + pass + +def getID(size=21): + return generate(size=size) + +def validate_code(id, cnt=6): + b = int(len(id) / cnt) + j = 0 + code = [] + v = 0 + print(f'{b=}, {cnt=}') + for c in id: + if j >= b: + v = v % 10 + code.append(str(v)) + j = 0 + v += ord(c) + j += 1 + if len(code) >= cnt: + break + return ''.join(code) + +def check_code(id, code): + c = validate_code(id) + return c==code + +if __name__ == '__main__': + id = getID() + code = validate_code(id) + b = check_code(id, code) + print(id, code, b) + diff --git a/appPublic/version.py b/appPublic/version.py new file mode 100755 index 0000000..62feff7 --- /dev/null +++ b/appPublic/version.py @@ -0,0 +1 @@ +__version__ = '5.1.27' diff --git a/appPublic/wcag_checker.py b/appPublic/wcag_checker.py new file mode 100755 index 0000000..0452f73 --- /dev/null +++ b/appPublic/wcag_checker.py @@ -0,0 +1,27 @@ + +def calculate_luminence(rgba): + return 0.2126 * color[0] + \ + 0.7152 * color[1] + \ + 0.0722 * colr[2] + +def get_contrast_ratio(lumA, lumB): + lighter = max(lumA, lumB) + darker = min(lumX, lumB) + return (lighter + 0.05) / (darker + 0.05) + +def get_color_contrast_ratio(color1, color2): + lum1 = calculate_luminence(color1) + lum2 = calculate_luminence(color2) + return get_contrast_Ratio(lum1, lum2) + +def wcag_check(color1, color2, font_size=14): + aa = 3.0 + aaa = 4.5 + if font_size < 18: + aa = 4.5 + aaa = 7.0 + ratio = get_color_contrast_ratio(color1, color2) + return ratio >= aa, radio >= aaa + +if __name__ == '__main__': + pass diff --git a/appPublic/worker.py b/appPublic/worker.py new file mode 100755 index 0000000..1179cfd --- /dev/null +++ b/appPublic/worker.py @@ -0,0 +1,74 @@ +import time +import random +import asyncio +import inspect +from functools import wraps +from functools import wraps + +def awaitify(sync_func): + """Wrap a synchronous callable to allow ``await``'ing it""" + @wraps(sync_func) + async def async_func(*args, **kw): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, sync_func, *args, **kw) + return async_func + +def coroutinify(func): + @wraps(func) + async def async_func(*args): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, func, *args) + return async_func + +def to_func(func): + @wraps(func) + def wraped_func(*args,**kw): + if inspect.iscoroutinefunction(func): + task = asyncio.ensure_future(func(*args,**kw)) + ret = asyncio.gather(task) + return ret + return func(*args, **kw) + return wraped_func + +class AsyncWorker: + def __init__(self,maxtask=50): + self.semaphore = asyncio.Semaphore(maxtask) + + async def __call__(self,callee,*args,**kw): + async with self.semaphore: + if inspect.iscoroutinefunction(callee): + return await callee(*args,**kw) + return callee(*args, **kw) + + async def run(self,cmd): + async with self.semaphore: + proc = await asyncio.create_subprocess_shell(cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + + stdout, stderr = await proc.comunicate() + return stdout, stderr + +if __name__ == '__main__': + def hello(cnt,greeting): + t = random.randint(1,10) + print(cnt,'will sleep ',t,'seconds') + time.sleep(t) + print(cnt,'cost ',t,'seconds to',greeting) + + async def ahello(cnt,greeting): + t = random.randint(1,10) + print(cnt,'will sleep ',t,'seconds') + await asyncio.sleep(t) + print(cnt,'cost ',t,'seconds to',greeting) + + async def run(): + w = AsyncWorker() + f = awaitify(hello) + g = [ asyncio.create_task(w(f,i,'hello world')) for i in range(100) ] + await asyncio.wait(g) + print('aaaaaaaaaaaaaaaaaaa') + + loop = asyncio.get_event_loop() + loop.run_until_complete(run()) + diff --git a/appPublic/zmq_reqrep.py b/appPublic/zmq_reqrep.py new file mode 100755 index 0000000..29e0886 --- /dev/null +++ b/appPublic/zmq_reqrep.py @@ -0,0 +1,135 @@ +# zmq_reqresp.py +import asyncio +import zmq +import zmq.asyncio +from .background import Background +from inspect import iscoroutinefunction + +class ZmqRequester(object): + def __init__(self, url, async_mode=False, timeout=0): + super().__init__() + self.async_mode = async_mode + self.url = url + self.timeout = timeout + self._connect() + + def __del__(self): + self._close() + + def _connect(self): + if self.async_mode: + self.ctx = zmq.asyncio.Context() + else: + self.ctx = zmq.Context() + + # Socket to talk to server + self.sock = self.ctx.socket(zmq.REQ) + self.sock.connect(self.url) + if self.timeout > 0: + self.sock.setsockopt(zmq.LINGER, 0) + self.poller = zmq.Poller() + self.poller.register(self.sock, zmq.POLLIN) + + def _close(self): + self.sock.close() + self.ctx.term() + + def send(self, msg): + """ + send s string to responser, and return a string + """ + if self.async_mode: + raise Exception('ZMQ_Requester: in async mode, use asend instead') + b = msg.encode('utf-8') + r = self.send_b(b) + if r is not None: + return r.decode('utf-8') + return None + + def send_b(self, b): + """ + send a bytes and return a bytes + """ + if self.async_mode: + raise Exception('ZMQ_Requester: in async mode, use asend_b instead') + self.sock.send(b) + if self.timeout > 0: + if self.poller.poll(self.timeout * 1000): + return self.sock.recv() + else: + self._close() + self._connect() + return None + else: + return self.sock.recv() + + async def asend_b(self, b): + if not self.async_mode: + raise Exception('ZMQ_Requester: not in async mode, use send_b instead') + await self.sock.send_multipart([b]) + if self.timeout > 0: + if self.poller.poll(self.timeout * 1000): + r = await self.sock.recv_multipart() + return r + else: + self._close() + self._connect() + return None + r = await self.sock.recv_multipart() + return r[0] + + async def asend(self, msg): + if not self.async_mode: + raise Exception('ZMQ_Requester: not in async mode, use send instead') + b = msg.encode('utf-8') + r = await self.asend_b(b) + if r is None: + return None + return r.decode('utf-8') + +class ZmqReplier(object): + def __init__(self, url, handler, async_mode=False): + self.async_mode = async_mode + self.url = url + if not self.async_mode and iscoroutinefunction(handler): + raise('not in async mode, handler can not be a coroutine') + + self.handler = handler + if self.async_mode: + self.ctx = zmq.asyncio.Context() + else: + self.ctx = zmq.Context() + self.sock = self.ctx.socket(zmq.REP) + self.sock.bind(self.url) + self.keep_running = True + + async def async_run(self): + while self.keep_running: + bs = await self.sock.recv_multipart() + b = b[0] + if iscoroutinefunction(self.handler): + rb = await self.handler(b) + else: + rb =self.self.handler(b) + + if isinstance(rb, str): + rb = rb.encode('utf-8') + await self.sock.send_multipart([rb]) + + def run(self): + self.background = Background(self._run) + self.background.daemon = True + self.background.start() + + def _run(self): + while self.keep_running: + b = self.sock.recv() + + rb = self.handler(b) + if isinstance(rb, str): + rb = rb.encode('utf-8') + self.sock.send(rb) + + def stop(self): + self.keep_running = False + self.join() diff --git a/appPublic/zmq_topic.py b/appPublic/zmq_topic.py new file mode 100755 index 0000000..e509acd --- /dev/null +++ b/appPublic/zmq_topic.py @@ -0,0 +1,123 @@ +import sys +import zmq +import time +from zmq import Context +from appPublic.jsonConfig import getConfig + +class TopicServer: + def __init__(self, address='127.0.0.1', pub_port='5566', sub_port='5567'): + # get ZeroMQ version + print("Current libzmq version is %s" % zmq.zmq_version()) + print("Current pyzmq version is %s" % zmq.pyzmq_version()) + + self.context = Context.instance() + # 2 sockets, because we can only bind once to a socket (as opposed to connect) + self.pub_port = "tcp://{}:{}".format(address, pub_port) + self.sub_port = "tcp://{}:{}".format(address, sub_port) + + self.xpub_xsub_proxy() + + # N publishers to 1 sub; proxy 1 sub to 1 pub; publish to M subscribers + def xpub_xsub_proxy(self): + print("Init proxy") + + # Socket subscribing to publishers + frontend_pubs = self.context.socket(zmq.XSUB) + frontend_pubs.bind(self.pub_port) + + # Socket publishing to subscribers + backend_subs = self.context.socket(zmq.XPUB) + backend_subs.bind(self.sub_port) + + print("Try: Proxy... CONNECT!") + zmq.proxy(frontend_pubs, backend_subs) + print("CONNECT successful!") + """ + while True: + time.sleep(1) + """ + +class ConfiguredTopicServer(TopicServer): + """ + in config file has a topicserver key + { + "topicserver":{ + "address":"11.11.1.11", + "pub_port":1234, + "sub_server":1235 + } + } + """ + def __init__(self): + config = getConfig() + params = config.topicserver + if not params: + raise MissTopicServerConfig + super(ConfiguredTopicServer, self).__init__(**params) + +class TopicPublisher: + def __init__(self, topic='en', address='127.0.0.1', port='5566'): + # get ZeroMQ version + print("Current libzmq version is %s" % zmq.zmq_version()) + print("Current pyzmq version is %s" % zmq.pyzmq_version()) + + self.topic = topic + self._topic = topic.encode('utf-8') + self.context = Context.instance() + self.url = "tcp://{}:{}".format(address, port) + self.pub = self.context.socket(zmq.PUB) + self.pub.connect(self.url) + time.sleep(0.5) + + def send(self, message): + self.pub.send_multipart([self._topic, message.encode('utf-8')]) + +class ConfiguredTopicPublisher(TopicPublisher): + def __init__(self, topic=''): + config = getConfig() + params = config.topicserver + if not params: + raise MissTopicServerConfig + super(ConfiguredTopicPublisher, self).__init__(topic=topic, + address = params.address, + port=params.pub_port) + +class TopicSubscriber: + def __init__(self, topic='', address='127.0.0.1', port='5567', callback=None): + # get ZeroMQ version + print("Current libzmq version is %s" % zmq.zmq_version()) + print("Current pyzmq version is %s" % zmq.pyzmq_version()) + + self.callback = callback + self.topic = topic + self.context = Context.instance() + self.url = "tcp://{}:{}".format(address, port) + self.sub = self.context.socket(zmq.SUB) + self.sub.connect(self.url) + # subscribe to topic 'en' or 'jp' + if isinstance(self.topic, list): + for t in self.topic: + self.sub.setsockopt(zmq.SUBSCRIBE, t.encode('utf-8')) + else: + self.sub.setsockopt(zmq.SUBSCRIBE, self.topic.encode('utf-8')) + + + def run(self): + # keep listening to all published message, filtered on topic + print("Sub {}: Going to wait for messages!".format(self.topic)) + while True: + msg_received = self.sub.recv_multipart() + print("sub {}: {}".format(self.topic, msg_received)) + if self.callback: + self.callback(msg_received) + +class ConfiguredTopicSubscriber(TopicSubscriber): + def __init__(self, topic=''): + config = getConfig() + params = config.topicserver + if not params: + raise MissTopicServerConfig + super(ConfiguredTopicSubscriber, self).__init__(topic=topic, + address=params.address, + port=params.sub_port) + diff --git a/appPublic/zmqapi.py b/appPublic/zmqapi.py new file mode 100755 index 0000000..bc643a9 --- /dev/null +++ b/appPublic/zmqapi.py @@ -0,0 +1,176 @@ + +import asyncio +from collections.abc import Coroutine +# from asyncio.coroutines import iscoroutine + +import zmq +import zmq.asyncio +import json + +class Publisher: + def __init__(self,port,coding='utf-8',msgid=1000): + self.port = port + self.socket = None + self.coding = coding + self.msgid = msgid + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.PUB) + self.socket.bind('tcp://*:%d' % self.port) + + async def publish(self,msg,msgtype='text',msgid=-1): + print(msg,msgtype) + if msgid == -1: + msgid = self.msgid + if msgtype != 'text': + msg = json.dumps(msg) + msgtype = 'json' + s = '%d %s %s' % (msgid,msgtype,msg) + print(s,msgtype,msgid) + b = s.encode(self.coding) + await self.socket.send(b) + + def __del__(self): + self.socket.close() + +class Subscriber: + def __init__(self,host,ports,msgid,coding='utf-8'): + self.host = host + self.ports = ports + self.msgid = msgid + self.coding = coding + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.SUB) + f = b'%d' % self.msgid + self.socket.setsockopt(zmq.SUBSCRIBE, f) + for p in self.ports: + self.socket.connect("tcp://%s:%d" % (self.host,p)) + + def addPort(self,port): + self.socket.connect("tcp://%s:%d" % (self.host,port)) + #f = b'%d' % self.msgid + #self.socket.setsockopt(zmq.SUBSCRIBE, f) + + async def subscribe(self): + ret = await self.socket.recv() + ret = ret.decode(self.coding) + msgid, msgtype, body = ret.split(' ',2) + print('msgid=',msgid,'msgtype=',msgtype,'body=',body) + if msgtype == 'json': + return json.loads(body) + return body + + def __del__(self): + self.socket.close() + +class RRServer: + """ + a request / response mode server + """ + def __init__(self,port,handler=None): + self.port = port + self.handler = handler + print(type(self.handler)) + + async def run(self): + running = True + context = zmq.asyncio.Context() + socket = context.socket(zmq.REP) + socket.bind('tcp://*:%s' % self.port) + while running: + rmsg = await socket.recv() + wmsg = rmsg + if self.handler is not None: + wmsg = self.handler(rmsg) + if isinstance(wmsg,Coroutine): + wmsg = await wmsg + await socket.send(wmsg) + socket.close() + +class RRClient: + """ + a request / response mode client + """ + def __init__(self,host,port): + self.host = host + self.port = port + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.REQ) + self.socket.connect('tcp://%s:%d' % (self.host,self.port)) + + async def request(self,msg): + await self.socket.send(msg) + return await self.socket.recv() + + +class PPPusher: + """ + pusher of Push / Pull mode + """ + def __init__(self,host,port): + self.host = host + self.port = port + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.PUSH) + self.socket.bind('tcp://%s:%d' % (self.host,self.port)) + + async def push(self,msg): + await self.socket.send(msg) + +class PPPuller: + """ + puller of Push / Pull mode + """ + def __init__(self,host,port,handler=None): + self.host = host + self.port = port + self.handler = handler + + async def run(self): + self.running = True + context = zmq.asyncio.Context() + socket = context.socket(zmq.PULL) + socket.bind('tcp://%s:%d' % (self.host,self.port)) + while self.running: + msg = await self.socket.recv() + if self.handler is not None: + x = self.handler(msg) + if isinstance(x,Coroutine): + await x + +class PairClient: + """ + client of Pair mode + """ + def __init__(self,host,port): + self.host = host + self.port = port + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.PAIR) + self.socket.bind('tcp://%s:%d' % (self.host,self.port)) + + async def request(self,msg): + await self.socket.send(msg) + return await self.socket.recv() + +class PairServer: + """ + server of Pair mode + """ + def __init__(self,port,handler=None): + self.port = port + self.handler = handler + self.running = True + + async def run(self): + self.running = True + context = zmq.asyncio.Context() + socket = context.socket(zmq.PAIR) + socket.bind('tcp://*:%d' % self.port) + while self.running: + msg = await socket.recv() + ret = msg + if self.handler is not None: + ret = self.handler() + if isinstance(ret,Coroutine): + ret = await ret + await socket.send(ret) diff --git a/build/lib/appPublic/CSVData.py b/build/lib/appPublic/CSVData.py new file mode 100644 index 0000000..989b89d --- /dev/null +++ b/build/lib/appPublic/CSVData.py @@ -0,0 +1,65 @@ +import csv + +class CSVData: + def __init__(self,csvfile,names = None,headline = 0,dataline = 1): + self.csvfile = csvfile + self.names = names + self.headline = headline + self.dataline = dataline + + def read(self): + f = open(self.csvfile,'rb') + reader = csv.reader(f) + fields = None + if self.names is not None: + fields = self.names + data = [] + lno = 0 + for l in reader: + if fields is None and lno == self.headline: + fields = [f for f in l] + if lno >= self.dataline: + rec = {} + for i in range(len(fields)): + rec[fields[i]] = l[i] + data.append(rec) + lno += 1 + f.close() + return data + + def iterRead(self): + self.fd = open(self.csvfile,'r') + try: + reader = csv.reader(self.fd) + fields = None + if self.names is not None: + fields = self.names + lno = 0 + self.onBegin() + for l in reader: + if fields is None and lno == self.headline: + fields = [f for f in l] + if lno >= self.dataline: + rec = {} + for i in range(len(fields)): + rec[fields[i]] = l[i] + self.onRecord(rec) + lno += 1 + self.fd.close() + self.onFinish() + except exception as e: + fd.close() + raise e + + def onReadBegin(self): + pass + def onRecord(self,rec): + print(rec) + + def onFinish(self): + print("onFinish() called") + +if __name__ == '__main__': + import sys + cd = CSVData(sys.argv[1],names = ['st_date','open_price','max_price','min_price','close_price','volume','adj_price']) + cd.iterRead() diff --git a/build/lib/appPublic/Config.py b/build/lib/appPublic/Config.py new file mode 100644 index 0000000..26c4774 --- /dev/null +++ b/build/lib/appPublic/Config.py @@ -0,0 +1,37 @@ +# Config.py +# Copyright (c) 2009 longtop Co. +# See LICENSE for details. +# author: yumoqing@gmail.com +# created date: 2009-02-01 +# last modified date: 2009-02-05 + +import os,sys +from appPublic.ExecFile import ExecFile +from appPublic.dictObject import DictObject +from appPublic.Singleton import Singleton +from zope.interface import implements +CONFIG_FILE = 'conf/config.ini' +from folderUtils import ProgramPath +class Node(object) : + pass + +class Config: + + __metaclass = Singleton + def __init__(self,configpath=None): + if configpath is None: + ps = CONFIG_FILE.split('/') + configpath = os.path.join(ProgramPath(),*ps) + self.configfile = configpath + self.__execfile = ExecFile(self,path=configpath) + self.__execfile.set('Node',Node) + self.__execfile.set('DictObject',DictObject) + self.__execfile.set('dict',DictObject) + r,msg = self.__execfile.run() + if not r: + print(r,msg) + +def getConfig(path=None): + conf = Config(path) + return conf + diff --git a/build/lib/appPublic/ExecFile.py b/build/lib/appPublic/ExecFile.py new file mode 100644 index 0000000..ae64638 --- /dev/null +++ b/build/lib/appPublic/ExecFile.py @@ -0,0 +1,107 @@ +# ExecFile.py +# usage : +# r = ExecFile() +# r.set('a','bbbb') +# r.run('test/cards.ini') +# r.cards +# +import os,sys + +class DictConfig(dict): + def __init__(self,dic=None,path=None,str=None,namespace={}): + dict.__init__(self) + self.namespace=namespace + if dic is not None and type(dic) == dict: + self.__dict__.update(dic) + self.__subConfig() + if path is not None: + self.__path = path + self.__load(path) + if str is not None: + self.__confstr = str + try: + exec(str,self.namespace,self.__dict__) + self.__subConfig() + except: + pass + def keys(self): + return self.__dict__.keys() + + def __getitem__(self,n): + return self.__dict__[n] + + def __getattr__(self,name): + if self.__dict__.has_key(name): + return self.__dict__[name] + raise AttributeError(name) + + def __subConfig(self): + for n in self.__dict__.keys(): + if type(self.__dict__[n]) == dict: + self.__dict__[n] = DictConfig(dic=self.__dict__[n]) + elif type(self.__dict__[n]) == type([]): + a = [] + for i in self.__dict__[n]: + if type(i) == dict: + a.append(DictConfig(dic=i)) + else: + a.append(i) + self.__dict__[n] = a + elif type(self.__dict__[n]) == type(()): + a = [] + for i in self.__dict__[n]: + if type(i) == dict: + a.append(DictConfig(dic=i)) + else: + a.append(i) + self.__dict__[n] = tuple(a) + + def __load(self,path): + d = {} + c = {} + f = open(path,'r') + buf = f.read() + f.close() + try: + exec(buf,self.namespace,namespace) + #print d + #print "c=",c + self.__dict__.update(c) + #print self.__dict__ + self.__subConfig() + return True + except Exception as e: + print(self.__path,e) + return False + +class ExecFile(object) : + def __init__(self,obj=None,path=None,namespace={}): + self.namespace = namespace + if obj == None: + obj = self + self.__object = obj + #self.namespace.update(self.__object.__dict__) + self.__file = path + + def set(self,name,v) : + setattr(self.__object,name,v) + + def get(self,name,default=None) : + return getattr(self.__object,name,default) + + def run(self,path=None) : + if path!=None: + self.__file = path + if self.__file is None: + raise Exception('exec file is none') + f = open(self.__file,'r') + buf = f.read() + f.close() + try : + exec(buf,globals(),self.__object.__dict__) + except Exception as e: + print("ExecFile()",e,self.__file) + return (False,e) + return (True,'') + + diff --git a/build/lib/appPublic/FiniteStateMachine.py b/build/lib/appPublic/FiniteStateMachine.py new file mode 100644 index 0000000..e906a6a --- /dev/null +++ b/build/lib/appPublic/FiniteStateMachine.py @@ -0,0 +1,53 @@ +# FiniteStateMachine.py +## a virtual State object of FSM +# +class BaseFSM(object): + def enterState(self, obj): + raise NotImplementedError() + + def execState(self, obj): + raise NotImplementedError() + + def exitState(self, obj): + raise NotImplementedError() + +## a FMS Manager +# only need one Manager for a FSM +class FSMManager(object): + def __init__(self): + self._fsms = {} + + def addState(self,state,fsm): + self._fsms[state] = fsm + + def delState(self,state): + del self._fsms[state] + + def getFSM(self, state): + return self._fsms[state] + + def frame(self, objs, state): + for obj in objs: + if state == obj.curr_state: + obj.keepState() + else: + obj.changeState(state, self._fsms[state]) + +## the object with has a Finite State Machine +# +class FSMObject(object): + def attachFSM(self,state,fsm): + self.fsm_state_object = fsm + self.fsm_cur_state = state + + def changeState(self,new_state,newfsm): + self.fsm_cur_state = new_state + self.fsm_state_object.exitState(self) + self.fsm_state_object = new_fsm + self.fsm_state_object.enterState(self) + self.fsm_state_object.execState(self) + + def keepState(self): + self.fsm_state_object.execState(self) + + \ No newline at end of file diff --git a/build/lib/appPublic/MiniI18N.py b/build/lib/appPublic/MiniI18N.py new file mode 100644 index 0000000..81c2afe --- /dev/null +++ b/build/lib/appPublic/MiniI18N.py @@ -0,0 +1,150 @@ +import os,re,sys +import codecs +from appPublic.folderUtils import _mkdir +from appPublic.Singleton import SingletonDecorator +from appPublic.folderUtils import ProgramPath +import threading +import time + +import locale + +comment_re = re.compile(r'\s*#.*') +msg_re = re.compile(r'\s*([^:]*)\s*:\s*([^\s].*)') + +def dictModify(d, md) : + for i in md.keys() : + if md[i]!=None : + d[i] = md[i] + return d + +convert_pairs = {':':'\\x3A', + '\n':'\\x0A', + '\r':'\\x0D', +} + +def charEncode(s) : + r = '' + v = s.split('\\') + s = '\\\\'.join(v) + for i in convert_pairs.keys() : + v = s.split(i) + s = convert_pairs[i].join(v) + # print 'i=',i,'iv=',convert_pairs[i],'s=',s + return s + +def charDecode(s) : + for i in convert_pairs.items() : + v = s.split(i[1]) + s = i[0].join(v) + v = s.split('\\\\') + s = '\\'.join(v) + return s + +def getTextDictFromLines(lines) : + d = {} + for l in lines : + l = ''.join(l.split('\r')) + if comment_re.match(l) : + continue + m = msg_re.match(l) + if m : + grp = m.groups() + d[charDecode(grp[0])] = charDecode(grp[1]) + return d + +def getFirstLang(lang) : + s = lang.split(',') + return s[0] + +@SingletonDecorator +class MiniI18N: + """ + """ + def __init__(self,path,lang=None,coding='utf8') : + self.path = path + l = locale.getdefaultlocale() + self.curLang = l[0] + self.coding = coding + self.id = 'i18n' + self.langTextDict = {} + self.setupMiniI18N() + self.missed_pt = None + self.translated_pt = None + self.header_pt = None + self.footer_pt = None + self.show_pt=None + self.clientLangs = {} + self.languageMapping = {} + self.timeout = 600 + + def __call__(self,msg,lang=None) : + """ + """ + if type(msg) == type(b''): + msg = msg.decode(self.coding) + return self.getLangText(msg,lang) + + def setLangMapping(self,lang,path): + self.languageMapping[lang] = path + + def getLangMapping(self,lang): + return self.languageMapping.get(lang,lang) + + def setTimeout(self,timeout=600): + self.timeout = timeout + + def delClientLangs(self): + t = threading.currentThread() + tim = time.time() - self.timeout + [ self.clientLangs.pop(k,None) for k in self.clientLangs.keys() if self.clientLangs[k]['timestamp'] < tim ] + + def getLangDict(self,lang): + lang = self.getLangMapping(lang) + return self.langTextDict.get(lang,{}) + + def getLangText(self,msg,lang=None) : + """ + """ + if lang==None : + lang = self.getCurrentLang() + textMapping = self.getLangDict(lang) + return textMapping.get(msg,msg) + + def setupMiniI18N(self) : + """ + """ + + p = os.path.join(self.path,'i18n') + langs = [] + + for f in os.listdir(p) : + if os.path.isdir(os.path.join(p,f)) : + langs.append(f) + for dir in langs : + p1 = os.path.join(p,dir,'msg.txt') + if os.path.exists(p1) : + f = codecs.open(p1,'r',self.coding) + textDict = getTextDictFromLines(f.readlines()) + f.close() + self.langTextDict[dir] = textDict + + self._p_changed = 1 + + def setCurrentLang(self,lang): + lang = self.getLangMapping(lang) + t = time.time() + threadid = threading.currentThread() + a = dict(timestamp=t,lang=lang) + self.clientLangs[threadid] = a + + def getCurrentLang(self) : + """ + """ + threadid = threading.currentThread() + return self.clientLangs[threadid]['lang'] + +def getI18N(coding='utf8'): + path = ProgramPath() + i18n = MiniI18N(path,coding) + return i18n + diff --git a/build/lib/appPublic/ObjectCache.py b/build/lib/appPublic/ObjectCache.py new file mode 100644 index 0000000..381b18a --- /dev/null +++ b/build/lib/appPublic/ObjectCache.py @@ -0,0 +1,53 @@ +# !/usr/bin/env python +# +# ObjectsCache is a Objects cache +# the Object has to have a method "get_size" to tell +# the cacher counting the objects size + +class ObjectCache(dict) : + def __init__(self,maxsize=10000000,*args) : + super(ObjectsCache,self).__init__(*args) + self.maxsize = maxsize + self.size = 0 + self._shadow = {} + + def __setitem__(self,key,item) : + try : + size = item.get_size() + self.size += size + except : + return + if self.size >= self.maxsize : + tmp = [(t,key) for key,(t,size) in self._shadow.iteritems() ] + tmp.sort() + for i in xrange(len(tmp)//2) : + del self[tmp[i][i]] + del tmp + super(ObjectCache,self).__setitem__(key,item) + self._shadow[key] = [time.time(),size] + + def __getitem__(self,key) : + try : + item = super(ObjectCache,self).__getitem__(key) + except : + raise + else : + self._shadow[key][0] = time.time() + return item + + def get(self,key,default=None) : + if self.has_key(key) : + return self[key] + else : + return default + + def __delitem__(self,key) : + try : + super(ObjectCache,self).__delitem__(key) + except : + raise + else : + self.size -= self._shadow[key][1] + del self._shadow[key] + + diff --git a/build/lib/appPublic/RSAutils.py b/build/lib/appPublic/RSAutils.py new file mode 100644 index 0000000..3eb4908 --- /dev/null +++ b/build/lib/appPublic/RSAutils.py @@ -0,0 +1,89 @@ +import codecs + +from Crypto.PublicKey import RSA +from Crypto.Cipher import PKCS1_OAEP +from Crypto.Cipher import KPCS1_V1_5 as V1_5 +from Crypto.Signature import PKCS1_v1_5 +from Crypto.Hash import SHA512, SHA384, SHA256, SHA, MD5 +from Crypto import Random +from base64 import b64encode, b64decode +hash = "SHA-256" + +def readPublickey(fname): + with codecs.open(fname,'r','utf8') as f: + b = f.read() + k = RSA.importKey(b) + return k + return None + +def readPrivatekey(fname,pwd): + with codecs.open(fname,'r','utf8') as f: + b = f.read() + k = RSA.importKey(b,pwd) + return k + return None + +def newkeys(keysize): + random_generator = Random.new().read + key = RSA.generate(keysize, random_generator) + private, public = key, key.publickey() + return public, private + +def importKey(externKey): + return RSA.importKey(externKey) + +def getpublickey(priv_key): + return priv_key.publickey() + +def encrypt(message, pub_key): + cipher = PKCS1_OAEP.new(pub_key) + return cipher.encrypt(message) + +def decrypt(ciphertext, priv_key): + try: + cipher = PKCS1_OAEP.new(priv_key) + return cipher.decrypt(ciphertext) + except Exception as e: + print('e=',e) + cipher = V1_5.new(priv_key) + return cipher.decrypt(ciphertext) + +def sign(message, priv_key, hashAlg = "SHA-256"): + global hash + hash = hashAlg + signer = PKCS1_v1_5.new(priv_key) + + if (hash == "SHA-512"): + digest = SHA512.new() + elif (hash == "SHA-384"): + digest = SHA384.new() + elif (hash == "SHA-256"): + digest = SHA256.new() + elif (hash == "SHA-1"): + digest = SHA.new() + else: + digest = MD5.new() + digest.update(message) + return signer.sign(digest) + +def verify(message, signature, pub_key): + signer = PKCS1_v1_5.new(pub_key) + if (hash == "SHA-512"): + digest = SHA512.new() + elif (hash == "SHA-384"): + digest = SHA384.new() + elif (hash == "SHA-256"): + digest = SHA256.new() + elif (hash == "SHA-1"): + digest = SHA.new() + else: + digest = MD5.new() + digest.update(message) + return signer.verify(digest, signature) + +if __name__ == '__main__': + cipher="""WaMlLEYnhBk+kTDyN/4OJmQf4ccNdk6USgtKpb7eHsYsotq4iyXi3N5hB1E/PqrPSmca1AMDLUcumwIrLeGLT9it3eTBQgl1YQAsmPxa6lF/rDOZoLbwD5sJ6ab/0/fuM4GbotqN5/d0MeuOSELoo8cFWw+7XpRxn9EMYnw5SzsjDQRWxXjZptoaGa/8pBBkDmgLqINif9EWV+8899xqTd0e9w1Gqb7wbt/elRNVBpgsSuSZb+dtBlvNUjuTms8BETSRai5vhXetK26Ms8hrayiy38n7wwEKE8fZ9iFzLtwa6xbhD5KudWbKJFFOZAfpzWttGMwWlISbGQigcW4+Bg==""" + key = readPrivatekey('d:/dev/mecp/conf/RSA.private.key','ymq123') + t = decrypt(cipher,key) + print('t=',t) + \ No newline at end of file diff --git a/build/lib/appPublic/SQLite3Utils.py b/build/lib/appPublic/SQLite3Utils.py new file mode 100644 index 0000000..9a4038d --- /dev/null +++ b/build/lib/appPublic/SQLite3Utils.py @@ -0,0 +1,220 @@ +import os,sys +import thread +from sqlite3 import dbapi2 as sqlite +import time +from localefunc import * +from folderUtils import mkdir +from PublicData import public_data +from mylog import mylog + +def logit(s) : + mylog('%s:%s' % (__file__,s)) + +class Record : + def __init__(self,data,localize=False) : + + for i in data.keys() : + d = data[i] + if localize and type(d)==type('') : + d = localeString(d) + setattr(self,i.lower(),d) + + def __getattr__(self,name) : + name = name.lower() + try : + return getattr(self,name) + except : + raise AttributeError(name) + + def __str__(self) : + a = self.__dict__ + f = [] + for i in a.keys() : + f.append("%s : %s" % (i,str(a[i]))) + return '[%s]' % '\n'.join(f) + +def str2unicode(s) : + if type(s) == type('') : + try : + ret = unicode(s,local_encoding) + return ret + except : + try : + ret = unicode(s,'utf8') + return ret + except : + return buffer(s) + return s + +def unicode2str(s) : + t = type(s) + if t == type(5) : + return long(s) + if t == type(buffer('')) : + return str(s) + if t == type(u"w") : + return s.encode('utf8') + return s + +def argConvert(args) : + if args==None : + return None + t = type(args) + if t==type(()) or t==type([]) : + return [str2unicode(i) for i in args] + if t==type({}) : + for i in args.keys() : + args[i] = str2unicode(args[i]) + return args + return args + +class SQLite3 : + + def __init__(self,dbpath,localize=False) : + self.__dict__['threadMap'] = {} + self.__dict__['localize'] = localize + self.__dict__['dbpath'] = dbpath + self.results = None + self.con = None + self.cursor = None + self.sqlcmd = '' + self._connection(dbpath) + + def _connection(self,dbpath=None) : + if dbpath!=None : + self.dbpath = dbpath + self.con = sqlite.connect(self.dbpath) + self.cursor = self.con.cursor() + self.result = None + self.sqlcmd = '' + + def __setattr__(self, name, value): + id = thread.get_ident() + if not self.__dict__['threadMap'].has_key(id): + self.__dict__['threadMap'][id] = {} + self.threadMap[id][name] = value + + def __getattr__(self, name): + id = thread.get_ident() + + if not self.__dict__['threadMap'].has_key(id) : + self.__dict__['threadMap'][id] = {} + if self.__dict__['threadMap'][id].has_key(name) : + return self.__dict__['threadMap'][id][name] + raise AttributeError(name) + + def tables(self) : + self.SQL("select * from sqlite_master where type='table'") + r = self.FETCH() + ts = [] + while r : + ts.append(r.name) + r = self.FETCH() + return ts + + def columns(self,tablenmae) : + self.SQL('select * from %s' % tablename) + self.desc = self.results.getdescription() + return desc + + def FETCHALL(self) : + all=[] + r = True + r = self.cursor.fetchall() + return r + + def _eatCursorNext(self) : + if self.cursor==None : + return None + r = 1 + while r : + try : + r = self.cursor.next() + except : + return + + def SQL(self,cmd,args=(),retry=0) : + if self.con==None : + print("self.con==None",cmd) + self._connection() + return self.SQL(cmd,args,retry) + return -1 + self._eatCursorNext() + args = argConvert(args) + self.lastSQL = cmd + self.desc = None + try : + if len(cmd.split(';'))>1 : + self.results = self.cursor.executescript(cmd) + else : + self.results = self.cursor.execute(cmd,args) + return True + except Exception as e: + print('execute:',cmd,'error',e) + self.results = None + raise + return True + + def FETCH(self) : + if self.results == None : + return None + if self.desc == None : + try : + self.desc = self.results.description + + except Exception as e: + print("fetch error",self.lastSQL,e) + raise + try : + desc = self.desc + d = self.results.next() + data = {} + for i in range(len(d)) : + data[desc[i][0]] = unicode2str(d[i]) + return Record(data,self.localize) + except StopIteration : + return None + except Exception as e: + print("error happen",e,self,lastSQL) + raise + + def COMMIT(self) : + self.SQL('PRAGMA case_sensitive_like = 1') + try : + self.cursor.fetchall() + except : + pass + + def ROLLBACK(self) : + self.SQL('ROLLBACK') + + def BEGIN(self) : + # self.SQL('BEGIN') + return + + def CLOSE(self) : + self.con = None + self.cursor = None + + +def getDataBase(name) : + a_name='db_%s' % name + db = public_data.get(a_name,None) + if db==None : + dbpath = public_data.get('dbpath_%s' % name,None) + if dbpath==None : + p = public_data.get('ProgramPath',None) + if p==None: + raise Exception('public_data must has a "ProgramPath" variable') + p1 = os.path.join(p,'var') + mkdir(p1) + dbpath = os.path.join(p1,'%s.db3' % name) + public_data.set('dbpath_%s' % name,dbpath) + db = SQLite3(dbpath) + public_data.set(a_name,db) + try : + con = db.con + except : + dbpath = public_data.get('dbpath_%s' % name,None) + db._connection(dbpath) + return db diff --git a/build/lib/appPublic/Singleton.py b/build/lib/appPublic/Singleton.py new file mode 100644 index 0000000..9157c98 --- /dev/null +++ b/build/lib/appPublic/Singleton.py @@ -0,0 +1,42 @@ +# +from appPublic.dictObject import DictObject + +class SingletonDecorator: + def __init__(self,klass): + self.klass = klass + self.instance = None + def __call__(self,*args,**kwds): + if self.instance == None: + self.instance = self.klass(*args,**kwds) + return self.instance + +@SingletonDecorator +class GlobalEnv(DictObject): + pass + +if __name__ == '__main__': + @SingletonDecorator + class Child(object): + def __init__(self,name): + print("clild.init") + self.name = name + def __str__(self): + return 'HAHA:' + self.name + def __expr__(self): + print(self.name) + + @SingletonDecorator + class Handle(object): + def __init__(self,name): + self.name = name + def __expr__(self): + print(self.name) + + c = Child('me') + d = Child('he') + + print(str(c),str(d)) + e = Handle('hammer') + f = Handle('nail'); + print(str(e),str(f)) + diff --git a/build/lib/appPublic/__init__.py b/build/lib/appPublic/__init__.py new file mode 100644 index 0000000..58f3ace --- /dev/null +++ b/build/lib/appPublic/__init__.py @@ -0,0 +1 @@ +from .version import __version__ diff --git a/build/lib/appPublic/across_nat.bak.py b/build/lib/appPublic/across_nat.bak.py new file mode 100644 index 0000000..2871f73 --- /dev/null +++ b/build/lib/appPublic/across_nat.bak.py @@ -0,0 +1,106 @@ + +from natpmp import NATPMP as pmp +from aioupnp.upnp import UPnP +from requests import get +from .background import Background + +class AcrossNat(object): + def __init__(self): + self.external_ip = None + self.upnp = None + self.pmp_supported = True + self.upnp_supported = True + self.init_pmp() + + async def init_upnp(self): + if self.upnp is None: + self.upnp = await UPnP.discover() + + def init_pmp(self): + try: + self.external_ip = pmp.get_public_address() + except pmp.NATPMPUnsupportedError: + self.pmp_supported = False + + async def get_external_ip(self): + if self.pmp_supported: + self.external_ip = pmp.get_public_address() + return self.external_ip + + if self.upnp_supported: + if self.upnp is None: + await self.init_upnp() + return await self.upnp.get_external_ip() + + try: + return get('https://api.ipify.org').text + except: + return get('https://ipapi.co/ip/').text + + async def upnp_map_port(self, inner_port, + protocol='TCP', from_port=40003, ip=None, desc=None): + + if self.upnp is None: + await self.init_upnp() + protocol = protocol.upper() + if ip is None: + ip = self.upnp.lan_address + + all_mappings = [i for i in await self.upnp.get_redirects()] + x = [ i for i in all_mappings if i.internal_port == inner_port \ + and i.lan_address == ip \ + and i.protocol == protocol ] + if len(x) > 0: + return x[0].external_port + + occupied_ports = [ i.external_port for i in all_mappings if i.protocol == protocol ] + external_port = from_port + while external_port < 52333: + if external_port not in occupied_ports: + break + external_port += 1 + + if external_port < 52333: + await self.upnp.add_port_mapping(external_port, + protocol, + inner_port, + ip, + desc or 'user added') + return external_port + return None + + async def is_port_mapped(self, external_port, protocol='TCP'): + if self.upnp is None: + await self.init_upnp() + protocol = protocol.upper() + if self.upnp_supported: + x = await self.upnp.get_specific_port_mapping(external_port, + protocol) + if len(x) == 0: + return True + return False + raise Exception('not implemented') + + async def port_unmap(self, external_port, protocol='TCP'): + if self.upnp is None: + await self.init_upnp() + protocol = protocol.upper() + if self.upnp_supported: + await self.upnp.delete_port_mapping(external_port, protocol) + raise Exception('not implemented') + + def pmp_map_port(self, inner_port, protocol='TCP', from_port=40003): + if protocol.upper() == 'TCP': + x = pmp.map_tcp_port(from_port, inner_port, + lifetime=999999999) + return x.public_port + x = pmp.map_udp_port(from_port, inner_port, + lifetime=999999999) + return x.public_port + + async def map_port(self, inner_port, protocol='tcp', from_port=40003, lan_ip=None, desc=None): + if self.pmp_supported: + return self.pmp_map_port(inner_port, protocol=protocol) + + return await self.upnp_map_port( inner_port, protocol=protocol, ip=lan_ip, desc=desc) + diff --git a/build/lib/appPublic/across_nat.py b/build/lib/appPublic/across_nat.py new file mode 100644 index 0000000..b045ec6 --- /dev/null +++ b/build/lib/appPublic/across_nat.py @@ -0,0 +1,118 @@ + +from traceback import print_exc +from natpmp import NATPMP as pmp +import upnpclient +from requests import get +from .background import Background + +class AcrossNat(object): + def __init__(self): + self.external_ip = None + self.upnp = None + self.pmp_supported = True + self.upnp_supported = True + self.init_pmp() + self.init_upnp() + + def init_upnp(self): + try: + igd = upnpclient.discover()[0] + s_names = [ n for n in igd.service_map.keys() if 'WAN' in n and 'Conn' in n] + self.upnp = igd.service_map[s_names[0]] + except Exception as e: + print(e) + print_exc() + self.upnp_supported = False + + def init_pmp(self): + try: + self.external_ip = pmp.get_public_address() + except pmp.NATPMPUnsupportedError: + self.pmp_supported = False + + def get_external_ip(self): + if self.pmp_supported: + try: + self.external_ip = pmp.get_public_address() + return self.external_ip + except: + self.pmp_supported = False + + if self.upnp_supported: + try: + x = self.upnp.GetExternalIPAddress() + return x['NewExternalIPAddress'] + except: + self.upnp_supported = False + try: + return get('https://api.ipify.org').text + except: + pass + try: + return get('https://ipapi.co/ip/').text + except: + return None + + def upnp_check_external_port(self, eport, protocol='TCP'): + try: + self.upnp.GetSpecificPortMappingEntry(NewExternalPort=eport, + NewProtocol=protocol, + NewRemoteHost='') + return True + except: + return False + + def upnp_map_port(self, inner_port, + protocol='TCP', from_port=40003, + ip=None, desc='test'): + + protocol = protocol.upper() + external_port = from_port + while external_port < 52333: + if self.upnp_check_external_port(external_port, + protocol=protocol): + external_port += 1 + continue + try: + self.upnp.AddPortMapping(NewRemoteHost='', + NewExternalPort=external_port, + NewProtocol=protocol, + NewInternalPort=inner_port, + NewInternalClient=ip, + NewEnabled='1', + NewPortMappingDescription=desc, + NewLeaseDuration=0 + ) + return external_port + except: + return None + return None + + def is_port_mapped(self, external_port, protocol='TCP'): + protocol = protocol.upper() + if self.upnp_supported: + return self.upnp_check_external_port(external_port, + protocol=protocol) + raise Exception('not implemented') + + def port_unmap(self, external_port, protocol='TCP'): + protocol = protocol.upper() + if self.upnp_supported: + self.upnp.delete_port_mapping(external_port, protocol) + raise Exception('not implemented') + + def pmp_map_port(self, inner_port, protocol='TCP', from_port=40003): + if protocol.upper() == 'TCP': + x = pmp.map_tcp_port(from_port, inner_port, + lifetime=999999999) + return x.public_port + x = pmp.map_udp_port(from_port, inner_port, + lifetime=999999999) + return x.public_port + + def map_port(self, inner_port, protocol='tcp', from_port=40003, lan_ip=None, desc=None): + if self.pmp_supported: + return self.pmp_map_port(inner_port, protocol=protocol) + + return self.upnp_map_port( inner_port, protocol=protocol, ip=lan_ip, desc=desc) + diff --git a/build/lib/appPublic/app_logger.py b/build/lib/appPublic/app_logger.py new file mode 100644 index 0000000..8872cd0 --- /dev/null +++ b/build/lib/appPublic/app_logger.py @@ -0,0 +1,88 @@ +import os +import sys +import logging +from functools import partial +from appPublic.timeUtils import timestampstr +levels={ + "debug":logging.DEBUG, + "info":logging.INFO, + "warning":logging.WARNING, + "error":logging.error, + "critical":logging.CRITICAL +} +defaultfmt = '%(asctime)s[%(name)s][%(levelname)s][%(filename)s:%(lineno)s]%(message)s' +logfile = -1 +logger = None +g_levelname='info' +level = levels.get('info') + +def create_logger(name, formater=defaultfmt, levelname=None, file=None): + global logger, logfile, level, g_levelname + if logfile == -1: + logfile = file + if logger: + return logger + logger = logging.getLogger(name) + if levelname: + g_levelname = levelname + else: + levelname = g_levelname + level = levels.get(levelname, levels.get('info')) + logger.setLevel(level) + format = logging.Formatter(formater) + file_handler = None + if logfile is not None: + file_handler = logging.FileHandler(logfile) + else: + file_handler = logging.StreamHandler() + + file_handler.setFormatter(format) + logger.addHandler(file_handler) + return logger + + +def info(*args, **kw): + global logger + if logger is None: + return + logger.info(*args, **kw) + +def debug(*args, **kw): + global logger + if logger is None: + return + logger.debug(*args, **kw) + +def warning(*args, **kw): + global logger + if logger is None: + return + logger.warning(*aegs, **kw) + +def error(*args, **kw): + global logger + if logger is None: + return + logger.error(*args, **kw) + +def critical(*args, **kw): + global logger + if logger is None: + return + logger.critical(*args, **kw) + +def exception(*args, **kw): + global logger + if logger is None: + return + logger.exception(**args, **kw) + +class AppLogger: + def __init__(self): + self.logger = create_logger(self.__class__.__name__) + self.debug = self.logger.debug + self.info = self.logger.info + self.warning = self.logger.warning + self.error = self.logger.error + self.critical = self.logger.critical + self.exception = self.logger.exception diff --git a/build/lib/appPublic/argsConvert.py b/build/lib/appPublic/argsConvert.py new file mode 100644 index 0000000..d459260 --- /dev/null +++ b/build/lib/appPublic/argsConvert.py @@ -0,0 +1,182 @@ +# -*- coding:utf8 -*- +import re +from appPublic.dictObject import DictObject +class ConvertException(Exception): + pass + +class ArgsConvert(object): + def __init__(self,preString,subfixString,coding='utf-8'): + self.preString = preString + self.subfixString = subfixString + self.coding=coding + sl1 = [ u'\\' + c for c in self.preString ] + sl2 = [ u'\\' + c for c in self.subfixString ] + ps = u''.join(sl1) + ss = u''.join(sl2) + re1 = ps + r"[_a-zA-Z_\u4e00-\u9fa5][a-zA-Z_0-9\u4e00-\u9fa5\,\.\'\{\}\[\]\(\)\-\+\*\/]*" + ss + self.re1 = re1 + # print( self.re1,len(self.re1),len(re1),type(self.re1)) + + def convert(self,obj,namespace,default=''): + """ obj can be a string,[],or dictionary """ + if isinstance(obj, str): + return self.convertString(obj,namespace,default) + if isinstance(obj, list): + ret = [] + for o in obj: + ret.append(self.convert(o,namespace,default)) + return ret + if isinstance(obj, dict): + ret = DictObject() + ret.update({k:self.convert(v,namespace,default) for k,v in obj.items()}) + return ret + return obj + + def findAllVariables(self,src): + r = [] + for ph in re.findall(self.re1,src): + dl = self.getVarName(ph) + r.append(dl) + return r + + def getVarName(self,vs): + return vs[len(self.preString):-len(self.subfixString)] + + def getVarValue(self,var,namespace,default): + v = default + try: + v = eval(var,namespace) + except Exception as e: + v = namespace.get(var, None) + if v: + return v + if callable(default): + return default(var) + return default + return v + + def convertString(self,s,namespace,default): + args = re.findall(self.re1,s) + for arg in args: + dl = s.split(arg) + var = self.getVarName(arg) + v = self.getVarValue(var,namespace,default) + if not isinstance(v, str): + if len(args) == 1 and s.startswith(self.preString) and s.endswith(self.subfixString): + return v + v = str(v) + s = v.join(dl) + return s + +class ConditionConvert(object): + def __init__(self,pString = u'$<',sString=u'>$',coding='utf-8'): + self.coding = coding + self.pString = pString + self.sString = sString + pS = ''.join([u'\\'+i for i in self.pString ]) + sS = ''.join([u'\\'+i for i in self.sString ]) + self.re1 = re.compile(u'(' + pS + '/?' + u'[_a-zA-Z_\u4e00-\u9fa5][a-zA-Z_0-9\u4e00-\u9fa5\,\.\'\{\}\[\]\(\)\-\+\*\/]*' + sS + u')') + self.buffer1 = [] + def convert(self,obj,namespace): + """ obj can be a string,[],or dictionary """ + if type(obj) == type(u''): + return self.convertUnicode(obj,namespace) + if type(obj) == type(''): + return self.convertString(obj,namespace) + if type(obj) == type([]): + ret = [] + for o in obj: + ret.append(self.convert(o,namespace)) + return ret + if type(obj) == type({}): + ret = {} + for k in obj.keys(): + ret.update({k:self.convert(obj.get(k),namespace)}) + return ret + # print( type(obj),"not converted") + return obj + + def getVarName(self,vs): + return vs[len(self.pString):-len(self.sString)] + + def getVarValue(self,var,namespace): + v = None + try: + v = eval(var,namespace) + except Exception as e: + v = namespace.get(var,None) + return v + + def convertList(self,alist,namespace): + ret = [] + handleList = alist + while len(handleList) > 0: + i = handleList[0] + handleList = handleList[1:] + if len(self.re1.findall(i)) < 1: + ret.append(i) + else: + name = self.getVarName(i) + + if name[0] == u'/': + name = name[1:] + if len(self.buffer1) < 1: + raise ConvertException('name(%s) not match' % name) + if self.buffer1[-1] != name: + raise ConvertException('name(%s) not match(%s)' % (self.buffer1[-1],name)) + val = self.getVarValue(name,namespace) + self.buffer1 = self.buffer1[:-1] + if val is not None: + return u''.join(ret),handleList + else: + return u'',handleList + else: + self.buffer1.append(name) + subStr,handleList = self.convertList(handleList,namespace) + ret.append(subStr) + if len(self.buffer1)>0: + raise ConvertException('name(s)(%s) not closed' % ','.join(self.buffer1)) + return u''.join(ret),[] + + def convertUnicode(self,s,namespace): + ret = [] + parts = self.re1.split(s) + s,b = self.convertList(parts,namespace) + return s + + def convertString(self,s,namespace): + ret = self.convertUnicode(s,namespace) + return ret + + +if __name__ == '__main__': + """ + ns = { + 'a':12, + 'b':'of', + 'c':'abc', + '是':'is', + 'd':{ + 'a':'doc', + 'b':'gg', + } + } + AC = ArgsConvert('%{','}%') + s1 = u"%{a}% is a number,%{d['b']}% is %{是}% undefined,%{c}% is %{d['a']+'(rr)'}% string" + arglist=['this is a descrciption %{b}% selling book',123,'ereg%{a}%,%{c}%'] + argdict={ + 'my':arglist, + 'b':s1 + } + print(f(s1,'<=>',AC.convert(s1,ns))) + print(f(argdict,'<=>',AC.convert(argdict,ns))) + """ + cc = ConditionConvert() + s2 = u"Begin $$this is $$ba = 100 $$condition out$$ end" + s3 = """select * from RPT_BONDRATINGS +where 1=1 +$$and ratingtype=${rtype}$$$ +$$and bond_id = ${bond}$$$""" + print(f("result=",cc.convert(s2,{'ba':23}))) + print(f("result = ",cc.convert(s3,{'bond':'943','rtype':'1'}))) + diff --git a/build/lib/appPublic/asynciorun.py b/build/lib/appPublic/asynciorun.py new file mode 100644 index 0000000..4e3d054 --- /dev/null +++ b/build/lib/appPublic/asynciorun.py @@ -0,0 +1,15 @@ +import asyncio +import sys +from sqlor.dbpools import DBPools +from appPublic.jsonConfig import getConfig + +def run(coro): + p = '.' + if len(sys.argv) > 1: + p = sys.argv[1] + config = getConfig(p, {'woridir':p}) + DBPools(config.databases) + loop = asyncio.new_event_loop() + asyncio.set_event_loop(loop) + loop.run_until_complete(coro()) + diff --git a/build/lib/appPublic/audioplayer.py b/build/lib/appPublic/audioplayer.py new file mode 100644 index 0000000..c89ea89 --- /dev/null +++ b/build/lib/appPublic/audioplayer.py @@ -0,0 +1,157 @@ +import time +from ffpyplayer.player import MediaPlayer +from ffpyplayer.tools import set_log_callback, get_log_callback, formats_in + +class AudioPlayer: + def __init__(self, source=None, autoplay=False, + loop=False, + on_stop=None): + self.volume = 1 + self.state = None + self.source = None + self.quitted = False + self.loop = loop + self.autoplay = autoplay + self.player = None + self.on_stop = on_stop + self.cmds = [] + if source: + self.set_source(source) + + def set_source(self, source): + self.source = source + self.load() + + def player_callback(self, selector, value): + print(f'player_callback(): {selector}, {value}') + if self.player is None: + print(f'player_callback():self.player is None, {selector}, {value}') + return + if selector == 'quit': + def close(*args): + self.quitted = True + self.unload() + close() + + elif selector == 'eof': + self._do_eos() + + def load(self): + if self.source is None: + return + source = self.source + self.unload() + ff_opts = {'vn':True, 'sn':True} + self.player = MediaPlayer(source, + callback=self.player_callback, + loglevel='info', + ff_opts=ff_opts) + player = self.player + self.state = 'pause' + s = time.perf_counter() + while (player.get_metadata()['duration'] is None and + not self.quitted and + time.perf_counter() - s < 10.): + time.sleep(0.005) + player.toggle_pause() + time.sleep(0.04) + player.set_volume(self.volume) + if self.autoplay: + self.play() + + def unload(self): + self.player = None + self.state = 'stop' + self.quitted = False + + def __del__(self): + self.unload() + + def play(self): + if self.player is None: + self.load() + if self.player is None: + print('play():self.player is None') + return + if self.state == 'play': + return + self.player.toggle_pause() + self.state = 'play' + + def pause(self): + if self.player is None: + self.load() + if self.player is None: + print('pause():self.player is None') + return + if self.state == 'pause': + return + self.player.toggle_pause() + self.state = 'pause' + + def is_busy(self): + if self.player and self.state == 'play': + return True + return False + + def stop(self): + if self.player is None: + return + if self.state == 'play': + self.player.toggle_pause() + self.state = 'stop' + self.seek(0) + if self.on_stop: + self.on_stop() + + def seek(self, pos): + if self.player is None: + print('seek():self.player is None') + return + self.player.seek(pos, relative=False) + + def get_pos(self): + if self.player is None: + return 0 + return self.player.get_pts() + + def _do_eos(self, *args): + print('_do_eos() called ...') + if self.loop: + self.seek(0.) + else: + print('go stop') + self.stop() + + +if __name__ == '__main__': + import sys + p = AudioPlayer(autoplay=True, loop=True) + p.source = sys.argv[1] + p.load() + p.play() + while True: + while p._pump(): + pass + + print(""" +play: play it, +stop: stop play +pause:pause it +quit: exit +""") + x = input() + if x == 'quit': + p.quitted = True + p.stop() + break + if x == 'play': + p.play() + continue + if x == 'stop': + p.stop() + continue + if x == 'pause': + p.pause() + continue + diff --git a/build/lib/appPublic/background.py b/build/lib/appPublic/background.py new file mode 100644 index 0000000..94846d1 --- /dev/null +++ b/build/lib/appPublic/background.py @@ -0,0 +1,11 @@ +from threading import Thread + +class Background(Thread): + def __init__(self,func, *args,**kw): + Thread.__init__(self) + self.__callee = func + self.__args = args + self.__kw = kw + + def run(self): + return self.__callee(*self.__args, **self.__kw) diff --git a/build/lib/appPublic/country_cn_en.py b/build/lib/appPublic/country_cn_en.py new file mode 100644 index 0000000..0dbfdd6 --- /dev/null +++ b/build/lib/appPublic/country_cn_en.py @@ -0,0 +1,247 @@ +# -*- coding = utf-8 -*- +ecc = { +"Afghanistan":"阿富汗", +"Aland Islands":"奥兰群岛", +"Albania":"阿尔巴尼亚", +"Algeria":"阿尔及利亚", +"American Samoa":"美属萨摩亚", +"Andorra":"安道尔", +"Angola":"安哥拉", +"Anguilla":"安圭拉", +"Antigua and Barbuda":"安提瓜和巴布达", +"Argentina":"阿根廷", +"Armenia":"亚美尼亚", +"Aruba":"阿鲁巴", +"Australia":"澳大利亚", +"Austria":"奥地利", +"Azerbaijan":"阿塞拜疆", +"Bangladesh":"孟加拉", +"Bahrain":"巴林", +"Bahamas":"巴哈马", +"Barbados":"巴巴多斯", +"Belarus":"白俄罗斯", +"Belgium":"比利时", +"Belize":"伯利兹", +"Benin":"贝宁", +"Bermuda":"百慕大", +"Bhutan":"不丹", +"Bolivia":"玻利维亚", +"Bosnia and Herzegovina":"波斯尼亚和黑塞哥维那", +"Botswana":"博茨瓦纳", +"Bouvet Island":"布维岛", +"Brazil":"巴西", +"Brunei":"文莱", +"Bulgaria":"保加利亚", +"Burkina Faso":"布基纳法索", +"Burundi":"布隆迪", +"Cambodia":"柬埔寨", +"Cameroon":"喀麦隆", +"Canada":"加拿大", +"Cape Verde":"佛得角", +"Central African Republic":"中非", +"Chad":"乍得", +"Chile":"智利", +"Christmas Islands":"圣诞岛", +"Cocos (keeling) Islands":"科科斯(基林)群岛", +"Colombia":"哥伦比亚", +"Comoros":"科摩罗", +"Congo (Congo-Kinshasa)":"刚果(金)", +"Congo":"刚果", +"Cook Islands":"库克群岛", +"Costa Rica":"哥斯达黎加", +"Cote D’Ivoire":"科特迪瓦", +"China":"中国", +"Croatia":"克罗地亚", +"Cuba":"古巴", +"Czech":"捷克", +"Cyprus":"塞浦路斯", +"Denmark":"丹麦", +"Djibouti":"吉布提", +"Dominica":"多米尼加", +"East Timor":"东帝汶", +"Ecuador":"厄瓜多尔", +"Egypt":"埃及", +"Equatorial Guinea":"赤道几内亚", +"Eritrea":"厄立特里亚", +"Estonia":"爱沙尼亚", +"Ethiopia":"埃塞俄比亚", +"Faroe Islands":"法罗群岛", +"Fiji":"斐济", +"Finland":"Finland", +"France":"法国", +"Franch Metropolitan":"法国大都会", +"Franch Guiana":"法属圭亚那", +"French Polynesia":"法属波利尼西亚", +"Gabon":"加蓬", +"Gambia":"冈比亚", +"Georgia":"格鲁吉亚", +"Germany":"德国", +"Ghana":"加纳", +"Gibraltar":"直布罗陀", +"Greece":"希腊", +"Grenada":"格林纳达", +"Guadeloupe":"瓜德罗普岛", +"Guam":"关岛", +"Guatemala":"危地马拉", +"Guernsey":"根西岛", +"Guinea-Bissau":"几内亚比绍", +"Guinea":"几内亚", +"Guyana":"圭亚那", +"Hong Kong":"香港 (中国)", +"Haiti":"海地", +"Honduras":"洪都拉斯", +"Hungary":"匈牙利", +"Iceland":"冰岛", +"India":"印度", +"Indonesia":"印度尼西亚", +"Iran":"伊朗", +"Iraq":"伊拉克", +"Ireland":"爱尔兰", +"Isle of Man":"马恩岛", +"Israel":"以色列", +"Italy":"意大利", +"Jamaica":"牙买加", +"Japan":"日本", +"Jersey":"泽西岛", +"Jordan":"约旦", +"Kazakhstan":"哈萨克斯坦", +"Kenya":"肯尼亚", +"Kiribati":"基里巴斯", +"Korea (South)":"韩国", +"Korea (North)":"朝鲜", +"Kuwait":"科威特", +"Kyrgyzstan":"吉尔吉斯斯坦", +"Laos":"老挝", +"Latvia":"拉脱维亚", +"Lebanon":"黎巴嫩", +"Lesotho":"莱索托", +"Liberia":"利比里亚", +"Libya":"利比亚", +"Liechtenstein":"列支敦士登", +"Lithuania":"立陶宛", +"Luxembourg":"卢森堡", +"Macau":"澳门(中国)", +"Macedonia":"马其顿", +"Malawi":"马拉维", +"Malaysia":"马来西亚", +"Madagascar":"马达加斯加", +"Maldives":"马尔代夫", +"Mali":"马里", +"Malta":"马耳他", +"Marshall Islands":"马绍尔群岛", +"Martinique":"马提尼克岛", +"Mauritania":"毛里塔尼亚", +"Mauritius":"毛里求斯", +"Mayotte":"马约特", +"Mexico":"墨西哥", +"Micronesia":"密克罗尼西亚", +"Moldova":"摩尔多瓦", +"Monaco":"摩纳哥", +"Mongolia":"蒙古", +"Montenegro":"黑山", +"Montserrat":"蒙特塞拉特", +"Morocco":"摩洛哥", +"Mozambique":"莫桑比克", +"Myanmar":"缅甸", +"Namibia":"纳米比亚", +"Nauru":"瑙鲁", +"Nepal":"尼泊尔", +"Netherlands":"荷兰", +"New Caledonia":"新喀里多尼亚", +"New Zealand":"新西兰", +"Nicaragua":"尼加拉瓜", +"Niger":"尼日尔", +"Nigeria":"尼日利亚", +"Niue":"纽埃", +"Norfolk Island":"诺福克岛", +"Norway":"挪威", +"Oman":"阿曼", +"Pakistan":"巴基斯坦", +"Palau":"帕劳", +"Palestine":"巴勒斯坦", +"Panama":"巴拿马", +"Papua New Guinea":"巴布亚新几内亚", +"Paraguay":"巴拉圭", +"Peru":"秘鲁", +"Philippines":"菲律宾", +"Pitcairn Islands":"皮特凯恩群岛", +"Poland":"波兰", +"Portugal":"葡萄牙", +"Puerto Rico":"波多黎各", +"Qatar":"卡塔尔", +"Reunion":"留尼汪岛", +"Romania":"罗马尼亚", +"Rwanda":"卢旺达", +"Russian Federation":"俄罗斯联邦", +"Saint Helena":"圣赫勒拿", +"Saint Kitts-Nevis":"圣基茨和尼维斯", +"Saint Lucia":"圣卢西亚", +"Saint Vincent and the Grenadines":"圣文森特和格林纳丁斯", +"El Salvador":"萨尔瓦多", +"Samoa":"萨摩亚", +"San Marino":"圣马力诺", +"Sao Tome and Principe":"圣多美和普林西比", +"Saudi Arabia":"沙特阿拉伯", +"Senegal":"塞内加尔", +"Seychelles":"塞舌尔", +"Sierra Leone":"塞拉利昂", +"Singapore":"新加坡", +"Serbia":"塞尔维亚", +"Slovakia":"斯洛伐克", +"Slovenia":"斯洛文尼亚", +"Solomon Islands":"所罗门群岛", +"Somalia":"索马里", +"South Africa":"南非", +"Spain":"西班牙", +"Sri Lanka":"斯里兰卡", +"Sudan":"苏丹", +"Suriname":"苏里南", +"Swaziland":"斯威士兰", +"Sweden":"瑞典", +"Switzerland":"瑞士", +"Syria":"叙利亚", +"Tajikistan":"塔吉克斯坦", +"Tanzania":"坦桑尼亚", +"Taiwan":"台湾 (中国)", +"Thailand":"泰国", +"Trinidad and Tobago":"特立尼达和多巴哥", +"Timor-Leste":"东帝汶", +"Togo":"多哥", +"Tokelau":"托克劳", +"Tonga":"汤加", +"Tunisia":"突尼斯", +"Turkey":"土耳其", +"Turkmenistan":"土库曼斯坦", +"Tuvalu":"图瓦卢", +"Uganda":"乌干达", +"Ukraine":"乌克兰", +"United Arab Emirates":"阿拉伯联合酋长国", +"United Kingdom":"英国", +"United States":"美国", +"Uruguay":"乌拉圭", +"Uzbekistan":"乌兹别克斯坦", +"Vanuatu":"瓦努阿图", +"Vatican City":"梵蒂冈", +"Venezuela":"委内瑞拉", +"Vietnam":"越南", +"Wallis and Futuna":"瓦利斯群岛和富图纳群岛", +"Western Sahara":"西撒哈拉", +"Yemen":"也门", +"Yugoslavia":"南斯拉夫", +"Zambia":"赞比亚", +"Zimbabwe":"津巴布韦" +} + +cec = {v:k for k,v in ecc.items()} + +def get_en_country_name(country): + x = cec.get(country) + if x: + return x + return country + +def get_cn_country_name(country): + x = ecc.get(country) + if x: + return x + return country diff --git a/build/lib/appPublic/csv_Data.py b/build/lib/appPublic/csv_Data.py new file mode 100644 index 0000000..4f950d9 --- /dev/null +++ b/build/lib/appPublic/csv_Data.py @@ -0,0 +1,54 @@ +import codecs +import csv +class Reader: + def __init__(self,f,delimiter): + self.f = f + self.delimiter = delimiter + self.line = 0 + + def __iter__(self): + return self + + def next(self): + l = self.f.readline() + if l == '': + raise StopIteration() + while l[-1] in [ '\n','\r']: + l = l[:-1] + r = [ i if i != '' else None for i in l.split(self.delimiter) ] + self.line = self.line + 1 + return r + +class CSVData: + def __init__(self,filename,coding='utf8',delimiter=','): + self.filename = filename + self.coding = coding + self.f = codecs.open(filename,'rb',self.coding) + self.reader = Reader(self.f,delimiter) + self.fields = self.reader.next() + + def __del__(self): + self.f.close() + + def __iter__(self): + return self + + def next(self): + try: + r = self.reader.next() + if len(r) != len(self.fields): + print("length diff",len(r),len(self.fields),"at line %d" % self.reader.line) + raise StopIteration() + d = {} + [d.update({self.fields[i]:r[i]}) for i in range(len(self.fields))] + return d + except: + raise StopIteration() + +if __name__ == '__main__': + import sys + cd = CSVData(sys.argv[1]) + for r in cd: + print(r) + + diff --git a/build/lib/appPublic/dataencoder.py b/build/lib/appPublic/dataencoder.py new file mode 100644 index 0000000..5c8b18e --- /dev/null +++ b/build/lib/appPublic/dataencoder.py @@ -0,0 +1,170 @@ +try: + import ujson as json +except: + import json +from appPublic.rsawrap import RSA +from appPublic.rc4 import RC4 +from appPublic.uniqueID import getID + +# import brotli +import zlib +import struct + +DATA_TYPE_BYTES = 1 +DATA_TYPE_STR = 2 +DATA_TYPE_JSON = 3 + +class DataEncoder: + """ + security data packing - unpacking object + packs data: + encode data with random key's rc4 crypt algorithm, + encode rc4's key with receiver's public key + sign data with sender's private key + packs data using struct in follows order + 0: data format(18 bytes) + 1. datatype(c) + 2. encoded data(length=len(d)) + 3. encoded_rc4key(length=len(k)) + 4. sign(signs from (0+1+2+3) data) (length=len(s)) + 5. compress data and return compressed dta + return packed data + unpacks data: + 0. decompress data + 1. get 18 bytes fmt data, erase tails b'\x00' + 2. using fmt to unpack data[18:] + 3. verify sign + 4. decode k + 5. decode data usig decoded k with rc4 algorithm + 6. convert data type to origin data type + 7. return converted data + """ + def __init__(self, myid, func_get_peer_pubkey, private_file=None): + self.myid = myid + self.func_get_peer_pubkey = func_get_peer_pubkey + self.public_keys = {} + self.private_file = private_file + self.rsa = RSA() + self.rc4 = RC4() + if self.private_file: + self.private_key = self.rsa.read_privatekey(self.private_file) + else: + self.private_key = self.rsa.create_privatekey() + self.public_key = self.rsa.create_publickey(self.private_key) + + def identify_datatype(self, data): + if isinstance(data, bytes): + return DATA_TYPE_BYTES, data + if isinstance(data, str): + return DATA_TYPE_STR, data.encode('utf-8') + data = json.dumps(data).encode('utf-8') + return DATA_TYPE_JSON, data + + def my_text_publickey(self): + return self.rsa.publickeyText(self.public_key) + + def exist_peer_publickeys(self, peer_id): + return True if self.public_keys.get(peer_id, False) else False + + def set_peer_pubkey(self, peer_id, pubkey): + self.public_keys[peer_id] = pubkey + + def get_peer_text_pubkey(self, peer_id): + pk = self.get_peer_pubkey() + txtpk = self.rsa. publickeyText(pk) + return txtpk + + def set_peer_text_pubkey(self, peer_id, text_pubkey): + pk = self.rsa.publickeyFromText(text_pubkey) + self.set_peer_pubkey(peer_id, pk) + + def get_peer_pubkey(self, peer_id): + pubkey = self.public_keys.get(peer_id) + if not pubkey: + try: + self.func_get_peer_pubkey(peer_id) + except: + raise Exception('Can not get peer public key(%s)') + pubkey = self.public_keys.get(peer_id) + return pubkey + + def pack(self, peer_id, data, uncrypt=False): + t, d = self.identify_datatype(data) + if uncrypt: + return zlib.compress(b'\x00' * 18 + \ + bytes(chr(t),'utf-8') + \ + d) + pk = self.get_peer_pubkey(peer_id) + d, k = self.encode_data(pk, d) + f = 'b%05ds%03ds' % (len(d), len(k)) + f1 = f + '256s' + pd1 = struct.pack('18s', f1.encode('utf-8')) + pd2 = struct.pack(f, t, d, k) + pd = pd1 + pd2 + s = self.sign_data(pd) + pd += s + self.pack_d = [t,d,k,s] + origin_len = len(pd) + pd = zlib.compress(pd) + return pd + + def unpack(self, peer_id, data): + data = zlib.decompress(data) + if data[:18] == b'\x00' * 18: + data = data[18:] + t = ord(chr(data[0])) + d = data[1:] + if t == DATA_TYPE_BYTES: + return d + d = d.decode('utf-8') + if t == DATA_TYPE_STR: + return d + return json.loads(d) + + org_data = data + pk = self.get_peer_pubkey(peer_id) + f = data[:18] + while f[-1] == 0 and len(f) > 0: + f = f[:-1] + f = f.decode('utf-8') + data = data[18:] + t, d, k, s = struct.unpack(f, data) + self.unpack_d = [t,d,k,s] + data1 = org_data[:org_data.index(s)] + if not self.verify_sign(data1, s, pk): + raise Exception('data sign verify failed') + data = self.decode_data(d, k) + if t == DATA_TYPE_BYTES: + return data + if t == DATA_TYPE_STR: + return data.decode('utf-8') + return json.loads(data) + + def encode_data(self, peer_pubkey, data): + key = getID() + if isinstance(key, str): + key = key.encode('utf-8') + ctext = self.rc4.encode_bytes(data, key) + encoded_key = self.rsa.encode_bytes(peer_pubkey, key) + return ctext, encoded_key + + def sign_data(self, data): + return self.rsa.sign_bdata(self.private_key, data) + + def decode_data(self, data, encoded_key): + key = self.rsa.decode_bytes(self.private_key, encoded_key) + return self.rc4.decode_bytes(data, key) + + def verify_sign(self, data, sign, peer_pubkey): + return self.rsa.check_sign_bdata(peer_pubkey, data, sign) + +def quotedstr(s): + def conv(c): + if c == '"': + return '\\"' + if c == '\n': + return '\\n' + return c + x = [ conv(c) for c in s ] + return ''.join(x) + diff --git a/build/lib/appPublic/datamapping.py b/build/lib/appPublic/datamapping.py new file mode 100644 index 0000000..3179e03 --- /dev/null +++ b/build/lib/appPublic/datamapping.py @@ -0,0 +1,50 @@ +#dataMapping +from appPublic.dictObject import DictObject + +def keyMapping(dic,mappingtab,keepmiss=True): + """ + keyMapping mappingtab + { + "a1":"b1", + "a2":'b2", + ... + } + "an" is key in dic + "bn" is key in result dictionary + """ + ret = {} + keys = [ k for k in dic.keys()] + if not keepmiss: + keys = [ k for k in dic.keys() if k in mappingtab.keys() ] + [ ret.update({mappingtab.get(k,k):dic[k]}) for k in keys ] + return ret + + +def valueMapping(dic,mappingtab): + """ + mappingtab format: + { + "field1":{ + "a":"1", + "b":"2", + "__default__":"5" + }, + "field2":{ + "a":"3", + "b":"4" + } + } + field1,field2 is in dic.keys() + """ + ret = {} + for k in dic.keys(): + mt = mappingtab.get(k,None) + if mt is None: + ret[k] = dic[k] + else: + dv = mt.get('__default__',dic[k]) + v = mt.get(dic[k],dv) + ret[k] = v + + return DictObject(**ret) + diff --git a/build/lib/appPublic/dictExt.py b/build/lib/appPublic/dictExt.py new file mode 100644 index 0000000..52f247c --- /dev/null +++ b/build/lib/appPublic/dictExt.py @@ -0,0 +1,38 @@ + +def arrayExtend(s,addon): + ret = [] + s_cnt = len(s) + a_cnt = len(addon) + for i,v in enumerate(addon): + if i < s_cnt: + if type(v)!=type(s[i]): + ret.append(v) + continue + if isinstance(v,dict): + x = dictExtend(v,s[i]) + ret.append(x) + continue + ret.append(v) + if s_cnt < a_cnt: + ret += s[i:] + return ret + +def dictExtend(s,addon): + ret = {} + ret.update(s) + skeys = ret.keys() + for k,v in addon.items(): + if k not in skeys: + ret[k] = v + continue + if type(v)!=type(ret[k]): + ret[k] = v + continue + if type(v)==type({}): + ret[k] = dictExtend(ret[k],v) + continue + if type(v)==type([]): + ret[k] = arrayExtend(ret[k],v) + continue + ret[k] = v + return ret diff --git a/build/lib/appPublic/dictObject.old.py b/build/lib/appPublic/dictObject.old.py new file mode 100644 index 0000000..cd7e798 --- /dev/null +++ b/build/lib/appPublic/dictObject.old.py @@ -0,0 +1,161 @@ +import json +from json import JSONEncoder +from inspect import ismethod, isfunction, isbuiltin, isabstract + +def multiDict2Dict(md): + ns = {} + for k,v in md.items(): + ov = ns.get(k,None) + if ov is None: + ns[k] = v + elif type(ov) == type([]): + ov.append(v) + ns[k] = ov + else: + ns[k] = [ov,v] + return ns + +class DictObject: + def __init__(self,**kw): + self.org_keys__ = [] + self.org_keys__ = [ k for k in self.__dict__.keys()] + for k,v in kw.items(): + self.update({k:self.__DOitem(v)}) + + def __getattr__(self,name): + if name in self._addon().keys(): + return self.__getitem__(name) + return None + + def update(self,kw): + self.__dict__.update(kw) + + def _addon(self): + ks = [ k for k in self.__dict__.keys() if k not in self.org_keys__] + return {k:v for k,v in self.__dict__.items() if k in ks} + + def clear(self): + for k in self._addon().keys(): + self.__dict__.pop(k) + + def get(self,name,default=None): + return self._addon().get(name,default) + + def pop(self,k,default=None): + return self.__dict__.pop(k,default) + + def popitem(self): + return self.__dict__.popitem() + + def items(self): + return self._addon().items() + + def keys(self): + return self._addon().keys() + + def values(self): + return self._addon().values() + + def __delitem__(self,key): + self.pop(key) + + def __getitem__(self,name): + return self._addon().get(name) + + def __setitem__(self,name,value): + self.__dict__[name] = value + + def __str__(self): + return str(self._addon()) + + def __expr__(self): + return self.addon().__expr__() + + def copy(self): + return {k:v for k,v in self._addon().items()} + + def to_dict(self): + d = self._addon() + newd = self.dict_to_dict(d) + return newd + + def dict_to_dict(self,dic): + d = {} + for k,v in dic.items(): + if isinstance(v,DictObject): + d[k] = v.to_dict() + elif isinstance(v,dict): + d[k] = self.dict_to_dict(v) + elif isinstance(v,list): + d[k] = self.array_to_dict(v) + elif k == '__builtins__': + pass + elif isbuiltin(v) or isfunction(v) or ismethod(v) or isabstract(v): + pass + else: + d[k] = v + return d + + def array_to_dict(self,v): + r = [] + for i in v: + if isinstance(i,list): + r.append(self.array_to_dict(i)) + elif isinstance(i,dict): + r.append(self.dict_to_dict(i)) + elif isinstance(i,DictObject): + r.append(i.to_dict()) + elif isbuiltin(i) or isfunction(i) or ismethod(i) or isabstract(i): + pass + else: + r.append(i) + return r + + @classmethod + def isMe(self,name): + return name == 'DictObject' + + def __DOArray(self,a): + b = [ self.__DOitem(i) for i in a ] + return b + + def __DOitem(self, i): + if isinstance(i,DictObject): + return i + if isinstance(i,dict): + i = {k:v for k,v in i.items() if isinstance(k,str)} + try: + d = DictObject(**i) + return d + except Exception as e: + print("****************",i,"*******dictObject.py") + raise e + if type(i) == type([]) or type(i) == type(()) : + return self.__DOArray(i) + return i + +class DictObjectEncoder(JSONEncoder): + def default(self, o): + return o._addon() + + + +def dictObjectFactory(_klassName__,**kwargs): + def findSubclass(_klassName__,klass): + for k in klass.__subclasses__(): + if k.isMe(_klassName__): + return k + k1 = findSubclass(_klassName__,k) + if k1 is not None: + return k1 + return None + try: + if _klassName__=='DictObject': + return DictObject(**kwargs) + k = findSubclass(_klassName__,DictObject) + if k is None: + return DictObject(**kwargs) + return k(**kwargs) + except Exception as e: + print("dictObjectFactory()",e,_klassName__) + raise e diff --git a/build/lib/appPublic/dictObject.py b/build/lib/appPublic/dictObject.py new file mode 100644 index 0000000..97be47c --- /dev/null +++ b/build/lib/appPublic/dictObject.py @@ -0,0 +1,130 @@ +import json +from json import JSONEncoder +from inspect import ismethod, isfunction, isbuiltin, isabstract + +def multiDict2Dict(md): + ns = {} + for k,v in md.items(): + ov = ns.get(k,None) + if ov is None: + ns[k] = v + elif type(ov) == type([]): + ov.append(v) + ns[k] = ov + else: + ns[k] = [ov,v] + return ns + +class DictObjectEncoder(JSONEncoder): + def default(self, o): + return o._addon() + +class DictObject(dict): + def __getattr__(self, attr): + """ + 实现点操作符访问字典中的键值对 + """ + try: + v = self.__DOitem(self[attr]) + self[attr] = v + return v + except KeyError: + return None + + def has(self, key): + try: + v = super().__getitem__(key) + return True + except KeyError: + return False + + def get_data_by_keys(self, keys): + try: + ks = keys.split('.', 1) + if '[' not in ks[0]: + d = getattr(self, ks[0]) + if len(ks) == 1: + return d + if isinstance(d, DictObject): + return d.get_data_by_keys(ks[1]) + return None + ks1 = ks[0].split('[', 1) + k = ks1[0] + idx = int(ks1[1].split(']',1)[0]) + d = getattr(self, k)[idx] + if len(ks) == 1: + return d + if isinstance(d, DictObject): + return d.get_data_by_keys(ks[1]) + return None + except: + return None + + def __getitem__(self, key): + try: + v = self.__DOitem(super().__getitem__(key)) + self[key] = v + return v + except KeyError: + return None + + def __setattr__(self, attr, value): + """ + 实现点操作符设置字典中的键值对 + """ + self[attr] = value + + def get(self, k, defv=None): + if self.has(k): + return self[k] + else: + return defv + + def copy(self): + return self.__DOitem(super().copy()) + + @classmethod + def isMe(self,name): + return name == 'DictObject' + + def to_dict(self): + return self + + def __DOArray(self,a): + b = [ self.__DOitem(i) for i in a ] + return b + + def __DOitem(self, i): + if isinstance(i,DictObject): + return i + if isinstance(i,dict): + i = {k:v for k,v in i.items() if isinstance(k,str)} + try: + d = DictObject(**i) + return d + except Exception as e: + raise e + if type(i) == type([]) or type(i) == type(()) : + return self.__DOArray(i) + return i + +def dictObjectFactory(_klassName__,**kwargs): + def findSubclass(_klassName__,klass): + for k in klass.__subclasses__(): + if k.isMe(_klassName__): + return k + k1 = findSubclass(_klassName__,k) + if k1 is not None: + return k1 + return None + try: + if _klassName__=='DictObject': + return DictObject(**kwargs) + k = findSubclass(_klassName__,DictObject) + if k is None: + return DictObject(**kwargs) + return k(**kwargs) + except Exception as e: + print("dictObjectFactory()",e,_klassName__) + raise e + diff --git a/build/lib/appPublic/easyExcel.py b/build/lib/appPublic/easyExcel.py new file mode 100644 index 0000000..051f2f1 --- /dev/null +++ b/build/lib/appPublic/easyExcel.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from win32com.client import Dispatch +import win32com.client + +class EasyExcel: + """A utility to make it easier to get at Excel. Remembering + to save the data is your problem, as is error handling. + Operates on one workbook at a time.""" + + def __init__(self, filename=None): + self.xlApp = win32com.client.Dispatch('Excel.Application') + if filename: + self.filename = filename + self.xlBook = self.xlApp.Workbooks.Open(filename) + #self.xlBook.Visible = False + else: + self.xlBook = self.xlApp.Workbooks.Add() + self.filename = '' + + def save(self, newfilename=None): + if newfilename: + self.filename = newfilename + self.xlBook.SaveAs(newfilename) + else: + self.xlBook.Save() + + def setSheetName(self,sheet,name): + self.xlBook.Worksheets(sheet).Name = name + + + def newSheet(self,sheetName): + pass + + def close(self): + self.xlBook.Close(SaveChanges=0) + self.xlApp.Quit() + del self.xlApp + + def getCell(self, sheet, row, col): + "Get value of one cell" + sht = self.xlBook.Worksheets(sheet) + return sht.Cells(row, col).Value + + def setCell(self, sheet, row, col, value): + "set value of one cell" + sht = self.xlBook.Worksheets(sheet) + sht.Cells(row, col).Value = value + + def getRange(self, sheet, row1, col1, row2, col2): + "return a 2d array (i.e. tuple of tuples)" + sht = self.xlBook.Worksheets(sheet) + return sht.Range(sht.Cells(row1, col1), sht.Cells(row2, col2)).Value + + def addPicture(self, sheet, pictureName, Left, Top, Width, Height): + "Insert a picture in sheet" + sht = self.xlBook.Worksheets(sheet) + sht.Shapes.AddPicture(pictureName, 1, 1, Left, Top, Width, Height) + + def cpSheet(self, before): + "copy sheet" + shts = self.xlBook.Worksheets + shts(1).Copy(None,shts(1)) + +if __name__ == "__main__": + PNFILE = r'c:\screenshot.bmp' + xls = EasyExcel(r'D:\test.xls') + xls.addPicture('Sheet1', PNFILE, 20,20,1000,1000) + xls.cpSheet('Sheet1') + xls.save() + xls.close() \ No newline at end of file diff --git a/build/lib/appPublic/exceldata.py b/build/lib/appPublic/exceldata.py new file mode 100644 index 0000000..76de2c2 --- /dev/null +++ b/build/lib/appPublic/exceldata.py @@ -0,0 +1,281 @@ +import xlrd +import os +import sys +import datetime + +from appPublic.strUtils import * + +TCS={ + 'int':int, + 'float':float, + 'str':str, +} + +def isEmptyCell(cell): + return cell.ctype == xlrd.XL_CELL_EMPTY + +def isCommentValue(v): + if type(v)==type('') and v[0] == '#': + return True + return False + +def purekey(k): + return k.split(':')[0] + +def castedValue(v,k): + ki = k.split(':') + if len(ki)<2 or v is None: + return v + ki = ki[1:] + if 'list' in ki: + if type(v) == type(''): + v = v.split(',') + elif type(v) != type([]): + v = [v] + + for k,tc in TCS.items(): + if k in ki: + if type(v) == type([]): + return [ tc(i) for i in v ] + else: + return tc(v) + return v + +class ExcelData: + _indictors = { + ':__dict__':'ff', + ':__list__':'ff', + ':__include__':'ff', + } + + def __init__(self,xlsfile,encoding='UTF8',startrow=0,startcol=0): + self._book = xlrd.open_workbook(xlsfile) + self.encoding = encoding + self._filename = xlsfile + self.startrow=0 + self.startcol=0 + self._dataset = self.dataload() + + def __del__(self): + del self._book + del self._dataset + + def cellvalue(self,sheet,x,y): + if sheet.cell_type(x,y)==xlrd.XL_CELL_EMPTY: + return None + if sheet.cell_type(x,y)==xlrd.XL_CELL_DATE: + y,m,d,hh,mm,ss = xlrd.xldate_as_tuple(sheet.cell_value(x,y),self._book.datemode) + return datetime.date(y,m,d) + s = sheet.cell_value(x,y) + return self.trimedValue(s) + + def isCommentCell(self,cell): + if isEmptyCell(cell): + return False + v = self.trimedValue(cell.value) + return isCommentValue(v) + + def dateMode(self): + return self._book.datemode + + def trimedValue(self,v): + if type(v) == type(u' '): + v = v.encode(self.encoding) + if type(v) == type(''): + v = lrtrim(v) + return v + def dataload(self): + dat = {} + for name in self._book.sheet_names(): + sheet = self._book.sheet_by_name(name) + #name = name.encode(self.encoding) + dat[self.trimedValue(name)] = self.loadSheetData(sheet) + return dat + + def findDataRange(self,sheet,pos,maxr): + x,y = pos + j = y + 1 + while j < sheet.ncols: + if isEmptyCell(sheet.cell(x,j)) or self.isCommentCell(sheet.cell(x,y)): + maxy = j + break + j += 1 + i = x + 1 + maxx = maxr + while i < maxr: + if not isEmptyCell(sheet.cell(i,y)): + maxx = i + break + i += 1 + return maxx + + def loadSheetData(self,sheet): + return self.loadSheetDataRange(sheet,(self.startrow,self.startcol),sheet.nrows) + + def include(self,filename,id): + try: + sub = ExcelData(filename,self.encoding) + except Exception as e: + print(e,filename) + return None + if id is None: + return sub.dict() + env = {'data':sub.dict()} + try: + exec("""resutl__ = data%s""" % id,globals(),env) + except Exception as e: + print(e,id) + return None + return env['resutl__'] + + def loadSingleData(self,sheet,pos): + x,y = pos + if sheet.ncols==y: + v = self.cellvalue(sheet,x,y) + if isCommentValue(v): + return None + return v + ret = [] + while y < sheet.ncols: + v = self.cellvalue(sheet,x,y) + if v is None: + break + if isCommentValue(v): + break + ret.append(v) + y += 1 + + if len(ret) < 1: + return None + + if len(ret)<2: + return ret[0] + if ret[0] == '__include__': + if len(ret)<2: + print("include mode error: __include__ filename id") + return None + id = None + if len(ret)>=3: + id = ret[2] + return self.include(ret[1],id) + return ret + + def loadDictData(self,sheet,pos,maxr): + ret = {} + x,y = pos + while x < maxr: + mr = self.findDataRange(sheet,(x,y),maxr) + #print "loadDictData:debug:",x,y,maxr,mr + k = self.cellvalue(sheet,x,y) + if isCommentValue(k): + x = x + 1 + continue + if k is not None: + if 'records' in k.split(':'): + v = self.loadRecords(sheet,(x,y+1),maxr) + else: + v = self.loadSheetDataRange(sheet,(x,y+1),mr) + ret[purekey(k)] = castedValue(v,k) + x = mr + + return ret + + def loadSheetDataRange(self,sheet,pos,maxr): + x,y = pos + #print "debug1:",pos,maxr + if maxr - x < 1 : + #print "debug1-1:",pos,maxr + return None + if isEmptyCell(sheet.cell(x,y)): + #print "debug1-2:",pos,maxr + return None + + cv = self.cellvalue(sheet,x,y) + #print cv + if isCommentValue(cv): + pos = (x+1,y) + return self.loadSheetDataRange(sheet,pos,maxr) + + if cv == '__include__': + return self.include(self.cellvalue(sheet,x,y+1),self.cellvalue(sheet,x,y+2)) + + if cv == '__dict__': + #print "cv==__dict__" + i = x + 1 + vs = [] + while i < maxr: + v = self.cellvalue(sheet,i,y) + if v == '__dict__': + vs.append(self.loadDictData(sheet,(x+1,y),i)) + x = i + i += 1 + vs.append(self.loadDictData(sheet,(x+1,y),i)) + if len(vs) < 1: + return None + if len(vs) < 2: + return vs[0] + return vs + return self.loadDictData(sheet,(x+1,y),maxr) + + if cv == '__list__': + i = x + 1 + vs = [] + while i < maxr: + v = self.loadSingleData(sheet,(i,y)) + vs.append(v) + i += 1 + return vs + + if maxr - x < 2: + v = self.loadSingleData(sheet,(x,y)) + return v + + return self.loadRecords(sheet,pos,maxr) + + def loadRecords(self,sheet,pos,maxr): + x,y = pos + v = self.cellvalue(sheet,x,y) + if v==None or isCommentValue(v): + return self.loadRecords(sheet,(x+1,y),maxr) + + data = [] + i = x + 1 + j = y + keys = [ self.trimedValue(k.value) for k in sheet.row(x)[y:] ] + while i < maxr: + d = {} + j = y + while j < sheet.ncols: + k = self.cellvalue(sheet,x,j) + if k is None or isCommentValue(k): + break + if sheet.cell_type(x,j) == xlrd.XL_CELL_EMPTY: + break + v = self.cellvalue(sheet,i,j) + if sheet.cell_type(x,j) != xlrd.XL_CELL_EMPTY: + d[purekey(k)] = castedValue(v,k) + j += 1 + data.append(d) + i += 1 + return data + + def dict(self): + return self._dataset + +class ExcelDataL(ExcelData): + def dataload(self): + ret = [] + for name in self._book.sheet_names(): + dat = {} + sheet = self._book.sheet_by_name(name) + name = name.encode(self.encoding) + dat[name] = self.loadSheetData(sheet) + ret.append(dat) + return ret + +if __name__ == '__main__': + if len(sys.argv)<2: + print("Usage:\n%s execlfile" % sys.argv[0]) + sys.exit(1) + ed = ExcelData(sys.argv[1]) + print(ed.dict()) diff --git a/build/lib/appPublic/excelwriter.py b/build/lib/appPublic/excelwriter.py new file mode 100644 index 0000000..5ba30da --- /dev/null +++ b/build/lib/appPublic/excelwriter.py @@ -0,0 +1,144 @@ +import xlwt +from appPublic.strUtils import * + +class ExcelWriter: + def __init__(self,encoding='gb2312'): + self.encoding = encoding + + def writeV(self,sheet,x,y,v): + if type(v) == type([]): + return self.writeList(sheet,x,y,v) + + if type(v) == type({}): + return self.writeDict(sheet,x,y,v) + + if type(v) not in (type({}),type([])): + if type(v) == type(' '): + v = lrtrim(v) + sheet.write(x,y,v) + return 1 + + def write(self,excelfile,dictdata): + wb = xlwt.Workbook(encoding=self.encoding) + for tbl in dictdata.keys(): + ws = wb.add_sheet(tbl,cell_overwrite_ok=True) + self.writeV(ws,0,0,dictdata[tbl]) + wb.save(excelfile) + + def createRecordTitle(self,ws,x,y,title,poss,isList=False): + if isList: + poss['__list__'][title] = True + + if title in poss.keys(): + return + + if len(poss.keys()) > 1: + d_ = {} + for k,v in poss.items(): + if k != '__list__': + d_[k] = v + y = max(d_.values()) + 1 + # ws.write(x,y,title) + poss[title] = y + + def writeRecordTitle(self,ws,x,poss): + for k in poss.keys(): + if k == '__list__': + continue + if k in poss['__list__'].keys(): + ws.write(x,poss[k],k+':list') + else: + ws.write(x,poss[k],k) + + def writeRecords(self,ws,x,y,alist): + ox = x + oy = y + poss = {'__list__':{}} + x = ox + 1 + for r in alist: + for k,v in r.items(): + isList = False + if type(v) == type([]): + isList = True + v = ','.join(v) + self.createRecordTitle(ws,ox,oy,k,poss,isList) + ws.write(x,poss[k],v) + x = x + 1 + self.writeRecordTitle(ws,ox,poss) + return x - ox + + def isRecords(self,alist): + records = True + for r in alist: + if type(r) != type({}): + return False + for k,v in r.items(): + if type(v) == type({}): + return False + if type(v) == type([]): + for c in v: + if type(c) in [type([]),type({})]: + return False + return True + + def writeDict(self,ws,x,y,adict): + ox = x + ws.write(x,y,'__dict__') + x = x + 1 + for k in adict.keys(): + ws.write(x,y,k) + cnt = self.writeV(ws,x,y+1,adict[k]) + x = x + cnt + # print "writeV return ",cnt,"handled key=",k,"next row=",x + return x - ox + + def writeList(self,ws,x,y,alist,singlecell=False): + if self.isRecords(alist): + return self.writeRecords(ws,x,y,alist) + + ox = x + if singlecell is True: + s = ','.join([ str(i) for i in alist ]) + ws.write(x,y,s) + return 1 + multiline = False + for d in alist: + if type(d) == type({}): + multiline=True + if multiline is True: + for i in alist: + if type(i) == type({}): + rows = self.writeDict(ws,x,y,i) + elif type(i) == type([]): + rows = self.writeMultiLineList(ws,x,y,i) + else: + ws.write(x,y,i) + rows = 1 + x = x + rows + return x - ox + else: + for i in alist: + if type(i) == type([]): + self.writeList(ws,x,y,i,singlecell=True) + else: + ws.write(x,y,i) + y = y + 1 + return 1 + + def writeMultiLineList(self,ws,x,y,alist): + ox = x + ws.write(x,y,'__list__') + x = x + 1 + for i in alist: + ws.write(x,y,i) + x = x + 1 + return x - os + +if __name__ == '__main__': + data = { + 'my1':['23423','423424','t334t3',2332,'erfverfefew'], + 'my2':[{'aaa':1,'bbb':'bbb'},{'aaa':1,'bbb':'bbb'}], + } + w = ExcelWriter() + w.write('d:\\text.xls',data) + \ No newline at end of file diff --git a/build/lib/appPublic/find_player.py b/build/lib/appPublic/find_player.py new file mode 100644 index 0000000..586c4f2 --- /dev/null +++ b/build/lib/appPublic/find_player.py @@ -0,0 +1,60 @@ +# !/usr/bin/env python +# -*- coding:UTF-8 -*- +from socket import * +import json +from appPublic.sockPackage import get_free_local_addr +from appPublic.background import Background +BUFSIZE = 1024 +class BroadcastServer: + def __init__(self, port, info): + self.info = info + self.port = port + self.udpSerSock = socket(AF_INET, SOCK_DGRAM) + # 设置阻塞 + self.udpSerSock.setblocking(1) + # 设置超时时间 1s + # self.udpSerSock.settimeout(1) + self.udpSerSock.bind(('' ,port)) + self.run_flg = True + self.thread = Background(self.run) + self.thread.start() + + def run(self): + while self.run_flg: + try: + data, addr = self.udpSerSock.recvfrom(BUFSIZE) + ret = json.dumps(self.info).encode('utf-8') + self.udpSerSock.sendto(ret, addr) + except Exception as e: + print('exception happened:',e) + pass + + def stop(self): + self.run_flg = False + self.udpSerSock.close() + +def find_players(port): + # broadcast_addr = '.'.join(host.split('.')[:-1]) + '.255' + host = get_free_local_addr()[0] + udpCliSock = socket(AF_INET, SOCK_DGRAM) + #设置阻塞 + #udpCliSock.setblocking(2) + #设置超时时间 + udpCliSock.settimeout(5) + udpCliSock.bind(('', 0)) + udpCliSock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + udpCliSock.sendto(b'findplayers', ('255.255.255.255',port)) + players = [] + while True: + try: + data,addr = udpCliSock.recvfrom(BUFSIZE) + if addr[0] != host and data: + data = data.decode('utf-8') + d = json.loads(data) + d['ip'] = addr[0] + players.append(d) + except Exception as e: + break + udpCliSock.close() + return players + diff --git a/build/lib/appPublic/folderUtils.py b/build/lib/appPublic/folderUtils.py new file mode 100644 index 0000000..38197f5 --- /dev/null +++ b/build/lib/appPublic/folderUtils.py @@ -0,0 +1,195 @@ +# -*- coding: utf-8 -*- + +import os +import sys +import stat +import os.path +import platform +import time +import random +import tempfile +""" +import win32api +""" +import sys + +def temp_file(suffix=None, prefix=None, dir=None, text=False): + x = tempfile.mkstemp(suffix=suffix, prefix=prefix, dir=dir, text=text) + os.close(x[0]) + return x[1] + +def filepoolpath(root): + paths=[191,193,197,199,97] + v = random.randint(0,9999999) + path = os.path.abspath(os.path.join(root, + str(v % paths[0]), + str(v % paths[1]), + str(v % paths[2]), + str(v % paths[3]), + str(v % paths[4]))) + return path + +def startsWith(text,s): + return text[:len(s)] == s + +def endsWith(text,s): + return text[-len(s):] == s + +def ProgramPath(): + filename = sys.argv[0] + if getattr(sys,'frozen',False): + filename = sys.executable + p = os.path.dirname(os.path.abspath(filename)) + return p + +def timestamp2datatiemStr(ts): + t = time.localtime(ts) + return '%04d-%02d-%-02d %02d:%02d:%02d' % (t.tm_year,t.tm_mon,t.tm_mday,t.tm_hour,t.tm_min,t.tm_sec) + +""" +def findAllDrives(): + Drives=[] + # print "Searching for drives..." + drives=win32api.GetLogicalDriveStrings().split(":") + for i in drives: + # print "i=",i,":" + dr=i[-1].lower() + if dr.isalpha(): + dr+=":\\" + inf=None + try: + inf=win32api.GetVolumeInformation(dr) + except: + pass # Removable drive, not ready + # You'll still get the drive letter, but inf will be None + Drives.append([dr,inf]) + return Drives +""" + +## list all folder name under folder named by path +# +def listFolder(path, rescursive=False) : + for name in os.listdir(path) : + full_name = os.path.join(path,name) + if os.path.isdir(full_name): + for f in listFolder(full_name, rescursive=rescursive): + yield f + yield full_name + +def listFile(folder,suffixs=[],rescursive=False): + subffixs = [ i.lower() for i in suffixs ] + for f in os.listdir(folder): + p = os.path.join(folder,f) + if rescursive and os.path.isdir(p): + for p1 in listFile(p,suffixs=suffixs,rescursive=True): + yield p1 + if os.path.isfile(p): + e = p.lower() + if suffixs == [] : + yield p + for s in subffixs: + if e.endswith(s): + yield p + +def folderInfo(root,uri=''): + relpath = uri + if uri[1]=='/': + relpath = uri[1:] + + path = os.path.join(root,*relpath.split('/')) + ret = [] + for name in os.listdir(path): + full_name = os.path.join(path,name) + s = os.stat(full_name) + if stat.S_ISDIR(s.st_mode): + ret.append( { + 'id':relpath + '/' + name, + 'name':name, + 'path':relpath, + 'type':'dir', + 'size':s.st_size, + 'mtime':timestamp2datatiemStr(s.st_mtime), + }) + if stat.S_ISREG(s.st_mode): + ret.append( { + 'id':relpath + '/' + name, + 'name':name, + 'path':relpath, + 'type':'file', + 'size':s.st_size, + 'mtime':timestamp2datatiemStr(s.st_mtime), + }) + return ret + + +def rmdir_recursive(dir): + """Remove a directory, and all its contents if it is not already empty.""" + for name in os.listdir(dir): + full_name = os.path.join(dir, name) + # on Windows, if we don't have write permission we can't remove + # the file/directory either, so turn that on + if not os.access(full_name, os.W_OK): + os.chmod(full_name, 0o600) + if os.path.isdir(full_name): + rmdir_recursive(full_name) + else: + os.remove(full_name) + os.rmdir(dir) + +def _mkdir(newdir) : + """works the way a good mkdir should :) + - already exists, silently complete + - regular file in the way, raise an exception + - parent directory(ies) does not exist, make them as well + """ + if os.path.isdir(newdir): + pass + elif os.path.isfile(newdir): + raise OSError("a file with the same name as the desired " \ + "dir, '%s', already exists." % newdir) + else: + head, tail = os.path.split(newdir) + if head and not os.path.isdir(head): + _mkdir(head) + #print "_mkdir %s" % repr(newdir) + if tail: + os.mkdir(newdir) + +def _copyfile(fp,dir) : + fs = open(fp,'rb') + name = os.path.basename(fp) + newfp = os.path.join(dir,getFileName(name,dir)) + f = open(newfp,'wb') + while True : + data = fs.read(65536) + if not data : + break + f.write(data) + fs.close() + f.close() + return True + +def _copydir(fp,dir,topdistinct) : + name = os.path.basename(fp) + newname = getFileName(name,dir) + debug(newname) + newfp = os.path.join(dir,newname) + _mkdir(newfp) + if fp==topdistinct : + return True + + flist = os.listdir(fp) + for name in flist : + full_name = os.path.join(fp,name) + if os.path.isdir(full_name) : + p = os.path.join(dir,name) + _copydir(full_name,newfp,topdistinct) + else : + if os.path.isfile(full_name) : + _copyfile(full_name,newfp) + return True + +mkdir=_mkdir +copyfile = _copyfile +copydir = _copydir +rmdir = rmdir_recursive diff --git a/build/lib/appPublic/genetic.py b/build/lib/appPublic/genetic.py new file mode 100644 index 0000000..35c796a --- /dev/null +++ b/build/lib/appPublic/genetic.py @@ -0,0 +1,44 @@ +class Genetic: + """ + A Base class for genetical objects, + all the instances can inherite attributes from its parent. + """ + def __init__(self): + self.__parent__ = None + self.__children__ = [] + #print dir(self) + + def __getattr__(self,n): + d = self.__dict__ + if n in d.keys(): + return d[n] + p = self.__parent__ #d['__parent__'] + if p is not None: + return getattr(p,n) + raise AttributeError(n) + + def addChild(self,c): + self.__children__.append(c) + c.__parent__ = self + + def setParent(self,p): + p.addChild(self) + +if __name__ == '__main__': + class A(Genetic): + def __init__(self,a1,a2): + Genetic.__init__(self) + self.a1 = a1 + self.a2 = a2 + + class B(Genetic): + def __init__(self,b): + Genetic.__init__(self) + self.b = b + gp = A(1,2) + p = B(3) + c = A(4,5) + gc = B(6) + gc.setParent(c) + c.setParent(p) + p.setParent(gp) diff --git a/build/lib/appPublic/hf.py b/build/lib/appPublic/hf.py new file mode 100644 index 0000000..7410423 --- /dev/null +++ b/build/lib/appPublic/hf.py @@ -0,0 +1,20 @@ +import requests +from huggingface_hub import configure_http_backend, get_session + +def hf_socks5proxy(proxies={ + "http": "socks5h://127.0.0.1:1086", + "https": "socks5h://127.0.0.1:1086" + }): + # Create a factory function that returns a Session with configured proxies + print(f'proxies={proxies}') + def backend_factory() -> requests.Session: + session = requests.Session() + session.proxies = proxies + print(f'socks5 proxy set {proxies=}') + return session + + # Set it as the default session factory + configure_http_backend(backend_factory=backend_factory) + +if __name__ == '__main__': + hf_socks5proxy() diff --git a/build/lib/appPublic/http_client.py b/build/lib/appPublic/http_client.py new file mode 100644 index 0000000..dad7aa5 --- /dev/null +++ b/build/lib/appPublic/http_client.py @@ -0,0 +1,131 @@ +import requests + +class NeedLogin(Exception): + pass + +class InsufficientPrivilege(Exception): + pass + +class HTTPError(Exception): + def __init__(self,resp_code,url=None): + self.resp_code = resp_code + self.url = url + Exception.__init__(self) + + def __expr__(self): + return f'{self.url}:{self.resp_code}' + + def __str__(self): + return f'{self.url}:{self.resp_code}' + +hostsessions = {} + +class Http_Client: + def __init__(self): + self.s = requests.Session() + self.s.verify = False + self.s.hooks['response'].append(self.response_handler) + + def prepped_handler(self, prepped): + pass + + def response_handler(self, resp, *args, **kw): + return resp + + def url2domain(self,url): + parts = url.split('/')[:3] + pre = '/'.join(parts) + return pre + + def _webcall(self,url,method="GET", + params={}, + files={}, + headers={}, + stream=False): + domain = self.url2domain(url) + sessionid = hostsessions.get(domain,None) + if sessionid: + headers.update({'session':sessionid}) + + if method in ['GET']: + req = requests.Request(method,url, + params=params,headers=headers) + else: + req = requests.Request(method,url, + data=params,files=files,headers=headers) + prepped = self.s.prepare_request(req) + self.prepped_handler(prepped) + resp = self.s.send(prepped) + if resp.status_code == 200: + h = resp.headers.get('Set-Cookie',None) + if h: + sessionid = h.split(';')[0] + hostsessions[domain] = sessionid + + if resp.status_code == 401: + print('NeedLogin:',url) + raise NeedLogin + + if resp.status_code == 403: + raise InsufficientPrivilege + + if resp.status_code != 200: + print('Error', url, method, + params, resp.status_code, + type(resp.status_code)) + raise HTTPError(resp.status_code,url) + return resp + + def webcall(self,url,method="GET", + params={}, + files={}, + headers={}, + stream=False): + resp = self._webcall(url,method=method, + params=params, + files=files, + headers=headers, + stream=stream) + if stream: + return resp + try: + data = resp.json() + if type(data) != type({}): + return data + status = data.get('status',None) + if status is None: + return data + if status == 'OK': + return data.get('data') + return data + except: + return resp.text + + def __call__(self,url,method="GET", + params={}, + headers={}, + files={}, + stream=False): + return self.webcall(url, method=method, + params=params, files=files, + headers=headers, stream=stream) + + def get(self, url, params={}, headers={}, stream=False): + return self.__call__(url,method='GET',params=params, + headers=headers, stream=stream) + def post(self, url, params={}, headers={}, files={}, stream=False): + return self.__call__(url,method='POST',params=params, files=files, + headers=headers, stream=stream) + + def put(self, url, params={}, headers={}, stream=False): + return self.__call__(url,method='PUT',params=params, + headers=headers, stream=stream) + + def delete(self, url, params={}, headers={}, stream=False): + return self.__call__(url,method='DELETE',params=params, + headers=headers, stream=stream) + + def option(self, url, params={}, headers={}, stream=False): + return self.__call__(url,method='OPTION',params=params, + headers=headers, stream=stream) + diff --git a/build/lib/appPublic/httpclient.py b/build/lib/appPublic/httpclient.py new file mode 100644 index 0000000..aed9aee --- /dev/null +++ b/build/lib/appPublic/httpclient.py @@ -0,0 +1,117 @@ +import asyncio +import aiohttp +import re +from appPublic.log import info, debug, warning, error, exception, critical + +RESPONSE_BIN = 0 +RESPONSE_TEXT = 1 +RESPONSE_JSON = 2 +RESPONSE_FILE = 3 +RESPONSE_STREAM = 4 + +class HttpError(Exception): + def __init__(self, code, msg, *args, **kw): + super().__init__(*msg, **kw) + self.code = code + self.msg = msg + def __str__(self): + return f"Error Code:{self.code}, {self.msg}" + + def __expr__(self): + return str(self) + +class HttpClient: + def __init__(self,coding='utf-8'): + self.coding = coding + self.session = None + self.cookies = {} + + async def close(self): + if self.session: + await self.session.close() + self.session = None + + def url2domain(self,url): + parts = url.split('/')[:3] + pre = '/'.join(parts) + return pre + + def setCookie(self,url,cookies): + name = self.url2domain(url) + self.cookies[name] = cookies + + def getCookies(self,url): + name = url2domain(url) + return self.cookies.get(name,None) + + def getsession(self,url): + if self.session is None: + jar = aiohttp.CookieJar(unsafe=True) + self.session = aiohttp.ClientSession(cookie_jar=jar) + return self.session + + async def handleResp(self,url,resp,resp_type, stream_func=None): + if resp.cookies is not None: + self.setCookie(url,resp.cookies) + + if resp_type == RESPONSE_BIN: + return await resp.read() + if resp_type == RESPONSE_JSON: + return await resp.json() + if resp_type == RESPONSE_TEXT: + return await resp.text(self.coding) + async for chunk in resp.content.iter_chunked(1024): + if stream_func: + await stream_func(chunk) + + def grapCookie(self,url): + session = self.getsession(url) + domain = self.url2domain(url) + filtered = session.cookie_jar.filter_cookies(domain) + return filtered + + async def request(self, url, method, + response_type=RESPONSE_TEXT, + params=None, + data=None, + jd=None, + stream_func=None, + headers=None, + **kw): + session = self.getsession(url) + if params == {}: + params = None + if data == {}: + data = None + if jd == {}: + jd = None + if headers == {}: + headers = None + + resp = await session.request(method, url, + params=params, + data=data, + json=jd, + headers=headers, **kw) + if resp.status==200: + return await self.handleResp(url, resp, response_type, stream_func=stream_func) + msg = f'http error({resp.status}, {url=},{params=}, {data=}, {jd=}, {headers=}, {kw=})' + exception(msg) + raise HttpError(resp.status, msg) + + async def get(self,url,**kw): + return self.request(url, 'GET', **kw) + + async def post(self,url, **kw): + return self.request(url, 'POST', **kw) + session = self.getsession(url) + +if __name__ == '__main__': + async def gbaidu(hc): + r = await hc.get('https://www.baidu.com') + print(r) + await hc.close() + loop = asyncio.get_event_loop() + hc = HttpClient() + loop.run_until_complete(gbaidu(hc)) + diff --git a/build/lib/appPublic/i18n.py b/build/lib/appPublic/i18n.py new file mode 100644 index 0000000..cb32ca6 --- /dev/null +++ b/build/lib/appPublic/i18n.py @@ -0,0 +1,161 @@ +import os,re,sys +import codecs +from appPublic.folderUtils import _mkdir +from appPublic.Singleton import SingletonDecorator +from appPublic.folderUtils import ProgramPath +from appPublic.jsonConfig import getConfig +import threading +import time + +import locale + +comment_re = re.compile(r'\s*#.*') +msg_re = re.compile(r'\s*([^:]*)\s*:\s*([^\s].*)') + +def dictModify(d, md) : + for i in md.keys() : + if md[i]!=None : + d[i] = md[i] + return d + +convert_pairs = {':':'\\x3A', + '\n':'\\x0A', + '\r':'\\x0D', +} + +def charEncode(s) : + r = '' + v = s.split('\\') + s = '\\\\'.join(v) + for i in convert_pairs.keys() : + v = s.split(i) + s = convert_pairs[i].join(v) + # print 'i=',i,'iv=',convert_pairs[i],'s=',s + return s + +def charDecode(s) : + for i in convert_pairs.items() : + v = s.split(i[1]) + s = i[0].join(v) + v = s.split('\\\\') + s = '\\'.join(v) + return s + +def getTextDictFromLines(lines) : + d = {} + for l in lines : + l = ''.join(l.split('\r')) + if comment_re.match(l) : + continue + m = msg_re.match(l) + if m : + grp = m.groups() + d[charDecode(grp[0])] = charDecode(grp[1]) + return d + +def getFirstLang(lang) : + s = lang.split(',') + return s[0] + +@SingletonDecorator +class MiniI18N: + """ + """ + def __init__(self,path,lang=None,coding='utf8') : + self.path = path + l = locale.getdefaultlocale() + self.curLang = l[0] + self.coding = coding + self.id = 'i18n' + self.langTextDict = {} + self.messages = {} + self.setupMiniI18N() + self.missed_pt = None + self.translated_pt = None + self.header_pt = None + self.footer_pt = None + self.show_pt=None + self.clientLangs = {} + self.languageMapping = {} + self.timeout = 600 + config = getConfig() + for l1,l in config.langMapping.items(): + self.setLangMapping(l1,l) + + def __call__(self,msg,lang=None) : + """ + """ + if type(msg) == type(b''): + msg = msg.decode(self.coding) + return self.getLangText(msg,lang) + + def setLangMapping(self,lang,path): + self.languageMapping[lang] = path + + def getLangMapping(self,lang): + return self.languageMapping.get(lang,lang) + + def setTimeout(self,timeout=600): + self.timeout = timeout + + def delClientLangs(self): + t = threading.currentThread() + tim = time.time() - self.timeout + [ self.clientLangs.pop(k,None) for k in self.clientLangs.keys() if self.clientLangs[k]['timestamp'] < tim ] + + def getLangDict(self,lang): + lang = self.getLangMapping(lang) + return self.langTextDict.get(lang,{}) + + def getLangText(self,msg,lang=None) : + """ + """ + if lang==None : + lang = self.getCurrentLang() + textMapping = self.getLangDict(lang) + return textMapping.get(msg,msg) + + def setupMiniI18N(self) : + """ + """ + + p = os.path.join(self.path,'i18n') + langs = [] + + for f in os.listdir(p) : + if os.path.isdir(os.path.join(p,f)) : + langs.append(f) + for dir in langs : + p1 = os.path.join(p,dir,'msg.txt') + if os.path.exists(p1) : + f = codecs.open(p1,'r',self.coding) + textDict = getTextDictFromLines(f.readlines()) + f.close() + d = {} + if dir in self.langTextDict : + d = self.langTextDict[dir] + self.langTextDict[dir] = textDict + for i in textDict.keys() : + self.messages[i] = '' + + self._p_changed = 1 + + def setCurrentLang(self,lang): + lang = self.getLangMapping(lang) + t = time.time() + threadid = threading.currentThread() + a = dict(timestamp=t,lang=lang) + self.clientLangs[threadid] = a + + def getCurrentLang(self) : + """ + """ + threadid = threading.currentThread() + return self.clientLangs[threadid]['lang'] + +def getI18N(path=None, coding='utf8'): + if path is None: + path = ProgramPath() + i18n = MiniI18N(path,coding) + return i18n + diff --git a/build/lib/appPublic/ipgetter.py b/build/lib/appPublic/ipgetter.py new file mode 100644 index 0000000..0c52b72 --- /dev/null +++ b/build/lib/appPublic/ipgetter.py @@ -0,0 +1,238 @@ +#!/usr/bin/env python +""" +This module is designed to fetch your external IP address from the internet. +It is used mostly when behind a NAT. +It picks your IP randomly from a serverlist to minimize request +overhead on a single server + +If you want to add or remove your server from the list contact me on github + + +API Usage +========= + + >>> import ipgetter + >>> myip = ipgetter.myip() + >>> myip + '8.8.8.8' + + >>> ipgetter.IPgetter().test() + + Number of servers: 47 + IP's : + 8.8.8.8 = 47 ocurrencies + + +Copyright 2014 phoemur@gmail.com +This work is free. You can redistribute it and/or modify it under the +terms of the Do What The Fuck You Want To Public License, Version 2, +as published by Sam Hocevar. See http://www.wtfpl.net/ for more details. +""" + +import re +import json +import time +import random +import socket +from threading import Timer + +from sys import version_info + +import future.moves.urllib.request +urllib = future.moves.urllib.request +PY3K = version_info >= (3, 0) + +__version__ = "0.6" + + +def myip(): + return IPgetter().get_external_ip() + + +class IPgetter(object): + + """ + This class is designed to fetch your external IP address from the internet. + It is used mostly when behind a NAT. + It picks your IP randomly from a serverlist to minimize request overhead + on a single server + # 'http://ip.dnsexit.com', + # 'http://checkip.dyndns.org/plain', + # 'http://ipogre.com/linux.php', + # 'http://whatismyipaddress.com/', + # 'http://ip.my-proxy.com/', + # 'http://websiteipaddress.com/WhatIsMyIp', + # 'http://www.iplocation.net/', + # 'http://www.howtofindmyipaddress.com/', + # 'http://www.ip-adress.com/', + # 'http://checkmyip.com/', + # 'http://www.tracemyip.org/', + # 'http://checkmyip.net/', + # 'http://www.findmyip.co/', + # 'http://www.dslreports.com/whois', + # 'http://www.mon-ip.com/en/my-ip/', + # 'http://www.myip.ru', + # 'http://www.whatsmyipaddress.net/', + # 'http://formyip.com/', + # 'https://check.torproject.org/', + # 'http://www.displaymyip.com/', + # 'http://www.bobborst.com/tools/whatsmyip/', + # 'https://www.whatsmydns.net/whats-my-ip-address.html', + # 'https://www.privateinternetaccess.com/pages/whats-my-ip/', + # 'http://www.infosniper.net/', + # 'http://ipinfo.io/', + # 'http://myexternalip.com/', + """ + + def __init__(self): + self.server_list = [ + 'http://ifconfig.me/ip', + 'http://ipecho.net/plain', + 'http://getmyipaddress.org/', + 'http://www.my-ip-address.net/', + 'http://www.canyouseeme.org/', + 'http://www.trackip.net/', + 'http://icanhazip.com/', + 'http://www.ipchicken.com/', + 'http://whatsmyip.net/', + 'http://www.lawrencegoetz.com/programs/ipinfo/', + 'http://ip-lookup.net/', + 'http://ipgoat.com/', + 'http://www.myipnumber.com/my-ip-address.asp', + 'http://www.geoiptool.com/', + 'http://checkip.dyndns.com/', + 'http://www.ip-adress.eu/', + 'http://wtfismyip.com/', + 'http://httpbin.org/ip', + ] + + + self.parsers = {} + self.timeout = 1.6 + self.url = None + + def get_external_ip(self): + """ + This function gets your IP from a random server + """ + + random.shuffle(self.server_list) + myip = '' + for server in self.server_list: + myip = self.defaultparser(self.fetch(server)) + if myip != '' and not (myip.startswith('192.') or myip.startswith('10.')) and not myip.startswith('127'): + return myip + else: + continue + return '' + + def add_server(self, server, parser): + self.server_list.append(server) + self.parsers[server] = parser + + def defaultparser(self, content): + p = '(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(' + p += '25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[' + p += '01]?[0-9][0-9]?)\.(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)' + try: + m = re.search(p, content) + myip = m.group(0) + if len(myip) > 0: + return myip + else: + return '' + except: + return '' + + def handle_timeout(self, url): + if self.url is not None: + self.url.close() + self.url = None + + def fetch(self, server): + """ + This function gets your IP from a specific server + """ + t = None + socket_default_timeout = socket.getdefaulttimeout() + opener = urllib.build_opener() + opener.addheaders = [('User-agent', + "Mozilla/5.0 (X11; Linux x86_64; rv:24.0)" + " Gecko/20100101 Firefox/24.0")] + + try: + # Close url resource if fetching not finished within timeout. + t = Timer(self.timeout, self.handle_timeout, [self.url]) + t.start() + + # Open URL. + if version_info[0:2] == (2, 5): + # Support for Python 2.5.* using socket hack + # (Changes global socket timeout.) + socket.setdefaulttimeout(self.timeout) + self.url = opener.open(server) + else: + self.url = opener.open(server, timeout=self.timeout) + + # Read response. + content = self.url.read() + + # Didn't want to import chardet. Prefered to stick to stdlib + if PY3K: + try: + content = content.decode('UTF-8') + except UnicodeDecodeError: + content = content.decode('ISO-8859-1') + + parser = self.parsers.get(server, self.defaultparser) + return parser(content) + + except Exception as e: + print(server, e) + return '' + finally: + if self.url is not None: + self.url.close() + self.url = None + if t is not None: + t.cancel() + + # Reset default socket timeout. + if socket.getdefaulttimeout() != socket_default_timeout: + socket.setdefaulttimeout(socket_default_timeout) + + def all_result(self): + x= [] + for s in self.server_list: + x.append([s, self.fetch(s)]) + print(x) + + def test(self): + """ + This functions tests the consistency of the servers + on the list when retrieving your IP. + All results should be the same. + """ + + resultdict = {} + for server in self.server_list: + resultdict.update(**{server: self.fetch(server)}) + + ips = sorted(resultdict.values()) + ips_set = set(ips) + print('\nNumber of servers: {}'.format(len(self.server_list))) + print("IP's :") + for ip, ocorrencia in zip(ips_set, map(lambda x: ips.count(x), ips_set)): + print('{0} = {1} ocurrenc{2}'.format(ip if len(ip) > 0 else 'broken server', ocorrencia, 'y' if ocorrencia == 1 else 'ies')) + print('\n') + print(resultdict) + +if __name__ == '__main__': + def p(content): + d = json.loads(content) + return d['ip'] + + g = IPgetter() + server = 'http://ipinfo.io/json' + g.add_server(server, p) + print(g.get_external_ip()) diff --git a/build/lib/appPublic/iplocation.py b/build/lib/appPublic/iplocation.py new file mode 100644 index 0000000..73acca8 --- /dev/null +++ b/build/lib/appPublic/iplocation.py @@ -0,0 +1,75 @@ +import os +import sys +from requests import get +from bs4 import BeautifulSoup +from appPublic.http_client import Http_Client +from appPublic.sockPackage import get_free_local_addr +public_headers = { + "User-Agent":"Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/60.0.3100.0 Safari/537.36" +} + +def get_outip(): + ip = get('https://api.ipify.org').content.decode('utf8') + return ip + +def ipip(ip=None): + # ipip.net + if ip is None: + ip = get_outip() + api= f"http://freeapi.ipip.net/{ip}" + hc = Http_Client() + r= hc.get(api, headers=public_headers) + return { + 'country':r[0], + 'city':r[2] + } + +def ipapi_co(ip): + url = f'https://ipapi.co/{ip}/json/' + hc = Http_Client() + r = hc.get(url) + r['City'] = r['city'] + r['lat'] = r['latitude'] + r['lon'] = r['longitude'] + return r + +def ip_api_com(ip): + url = f'http://ip-api.com/json/{ip}' + hc = Http_Client() + r = hc.get(url) + r['City'] = r['city'] + return r + +def iplocation(ip=None): + if ip is None: + ip = get_outip() + # apikey come from + # https://app.apiary.io/globaliptv/tests/runs + # using my github accout + apikey='c675f89c4a0e9315437a1a5edca9b92c' + api = f"https://www.iplocate.io/api/lookup/{ip}?apikey={apikey}", + hc = Http_Client() + r= hc.get(api, headers=public_headers) + return r + +def get_ip_location(ip): + funcs = [ + ip_api_com, + ipapi_co, + ipip, + iplocation + ] + hc = Http_Client() + for f in funcs: + try: + r = f(ip) + return r + except: + pass + +if __name__ == '__main__': + print(get_free_local_addr()) + if len(sys.argv) > 1: + info = get_ip_location(sys.argv[1]) + print(info) + diff --git a/build/lib/appPublic/jsonConfig.py b/build/lib/appPublic/jsonConfig.py new file mode 100644 index 0000000..789cc13 --- /dev/null +++ b/build/lib/appPublic/jsonConfig.py @@ -0,0 +1,74 @@ +import os,sys +import json +from pathlib import Path +from appPublic.dictObject import DictObject +from appPublic.Singleton import SingletonDecorator +from appPublic.folderUtils import ProgramPath +from appPublic.argsConvert import ArgsConvert + +def key2ansi(dict): + #print dict + return dict + a = {} + for k,v in dict.items(): + k = k.encode('utf-8') + #if type(v) == type(u" "): + # v = v.encode('utf-8') + a[k] = v + + return a + +class JsonObject(DictObject): + """ + JsonObject class load json from a json file + """ + def __init__(self,jsonholder,keytype='ansi',NS=None): + jhtype = type(jsonholder) + if jhtype == type("") or jhtype == type(u''): + f = open(jsonholder,'r') + else: + f = jsonholder + try: + a = json.load(f) + except Exception as e: + print("exception:",self.__jsonholder__,e) + raise e + finally: + if type(jsonholder) == type(""): + f.close() + + if NS is not None: + ac = ArgsConvert('$[',']$') + a = ac.convert(a,NS) + a['__jsonholder__'] = jsonholder + a['NS'] = NS + DictObject.__init__(self,**a) + +@SingletonDecorator +class JsonConfig(JsonObject): + pass + +def getConfig(path=None,NS=None): + pp = ProgramPath() + if path==None: + path = os.getcwd() + cfname = os.path.abspath(os.path.join(path,"conf","config.json")) + # print __name__,cfname + ns = { + 'home':str(Path.home()), + 'workdir':path, + 'ProgramPath':pp + } + if NS is not None: + ns.update(NS) + a = JsonConfig(cfname,NS=ns) + return a + +if __name__ == '__main__': + conf = JsonConfig(sys.argv[1]) + #print conf.db,conf.sql + #conf1 = JsonConfig(sys.argv[1],keytype='unicode') + conf1 = JsonConfig(sys.argv[1],keytype='ansi') + + print("conf=",dir(conf)) + print("conf1=",dir(conf1) ) diff --git a/build/lib/appPublic/jsonIO.py b/build/lib/appPublic/jsonIO.py new file mode 100644 index 0000000..d6f9b66 --- /dev/null +++ b/build/lib/appPublic/jsonIO.py @@ -0,0 +1,38 @@ +import json + +def uni_str(a, encoding): + if a is None: + return None + if isinstance(a, (list, tuple)): + s = [] + for i, k in enumerate(a): + s.append(uni_str(k, encoding)) + return s + elif isinstance(a, dict): + s = {} + for i, k in enumerate(a.items()): + key, value = k + s[uni_str(key, encoding)] = uni_str(value, encoding) + return s + elif isinstance(a, bool): + return a + elif isinstance(a, unicode): + return a + elif isinstance(a, str) or (hasattr(a, '__str__') and callable(getattr(a, '__str__'))): + if getattr(a, '__str__'): + a = str(a) + return unicode(a, encoding) + else: + return a + +def success(data): + return dict(success=True,data=data) + +def error(errors): + return dict(success=False,errors=errors) + +def jsonEncode(data,encode='utf-8'): + return json.dumps(uni_str(data, encode)) + +def jsonDecode(jsonstring): + return json.loads(jsonstring) diff --git a/build/lib/appPublic/localefunc.py b/build/lib/appPublic/localefunc.py new file mode 100644 index 0000000..aab14e4 --- /dev/null +++ b/build/lib/appPublic/localefunc.py @@ -0,0 +1,43 @@ +# this function will fix a bug for open a file with a not english name. +# + +import sys +import locale + +language, local_encoding = locale.getdefaultlocale() +if sys.platform == 'win32': + import locale, codecs + local_encoding = locale.getdefaultlocale()[1] + if local_encoding.startswith('cp'): # "cp***" ? + try: + codecs.lookup(local_encoding) + except LookupError: + import encodings + encodings._cache[local_encoding] = encodings._unknown + encodings.aliases.aliases[local_encoding] = 'mbcs' + +def locale_open(filename,mode='rb') : + return open(filename.encode(local_encoding),mode) + +def localeString(s) : + try : + return unicode(s,'utf-8').encode(local_encoding) + + except : + return s + +def utf8String(s) : + try : + return unicode(s,local_encoding).encode('utf-8') + except : + return s + +def charsetString(s,charset) : + try : + return unicode(s,local_encoding).encode(charset) + except : + try : + return unicode(s,'utf-8').encode(charset) + except : + return s + diff --git a/build/lib/appPublic/log.py b/build/lib/appPublic/log.py new file mode 100644 index 0000000..c4395a7 --- /dev/null +++ b/build/lib/appPublic/log.py @@ -0,0 +1,95 @@ +import sys +import codecs +from appPublic.timeUtils import timestampstr +from appPublic.Singleton import SingletonDecorator +import inspect + +def my_function(): + frame_info = inspect.currentframe() + caller_frame = frame_info.f_back + file_name = inspect.getframeinfo(caller_frame).filename + line_number = inspect.getframeinfo(caller_frame).lineno + print(f"Called from file: {file_name}, line: {line_number}") + + +@SingletonDecorator +class MyLogger: + levels={ + "debug":5, + "info":6, + "warning":4, + "error":3, + "exception":2, + "critical":1 + } + formater='%(timestamp)s[%(name)s][%(levelname)s][%(filename)s:%(lineno)s]%(message)s\n' + def __init__(self, name, levelname='debug', logfile=None): + self.name = name + self.levelname = levelname + self.level = self.levels.get(levelname) + self.logfile = logfile + + def open_logger(self): + if self.logfile: + self.logger = codecs.open(self.logfile, 'a', 'utf-8') + else: + self.logger = sys.stdout + + def close_logger(self): + if self.logfile: + self.logger.close(); + self.logger = None + self.logger = None + + def log(self, levelname, message, frame_info): + caller_frame = frame_info.f_back + filename = inspect.getframeinfo(caller_frame).filename + lineno = inspect.getframeinfo(caller_frame).lineno + level = self.levels.get(levelname) + if level > self.level: + print(f'{level=},{self.level=}') + return + data = { + 'timestamp':timestampstr(), + 'name':self.name, + 'levelname':levelname, + 'message':message, + 'filename':filename, + 'lineno':lineno + } + self.open_logger() + s = self.formater % data + self.logger.write(s) + self.logger.flush() + self.close_logger() + +def info(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('info', message, frame_info) + +def debug(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('debug', message, frame_info) + +def warning(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('warning', message, frame_info) + +def error(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('error', message, frame_info) + +def critical(message): + frame_info = inspect.currentframe() + logger = MyLogger('Test') + logger.log('critical', message, frame_info) + +def exception(message): + frame_info = inspect.currentframe() + logger = MyLogger('exception') + logger.log('exception', message, frame_info) + diff --git a/build/lib/appPublic/macAddress.py b/build/lib/appPublic/macAddress.py new file mode 100644 index 0000000..8c3af37 --- /dev/null +++ b/build/lib/appPublic/macAddress.py @@ -0,0 +1,38 @@ +#! /usr/bin/env python +import locale +import psutil +import socket + + +def getAllAddress(): + iocounts = psutil.net_io_counters(pernic=True) + ns = [ k for k in iocounts.keys() if iocounts[k].bytes_sent>0 and iocounts[k].bytes_recv>0 ] + stats = psutil.net_if_stats() + stat = [ i for i in stats.keys() if i in ns ] + hds = psutil.net_if_addrs() + for n,v in hds.items(): + if n not in stat: + continue + for i in v: + if i.family == socket.AF_INET: + yield n,i.address + +def getAllMacAddress(): + coding = locale.getdefaultlocale()[1] + iocounts = psutil.net_io_counters(pernic=True) + ns = [ k for k in iocounts.keys() if iocounts[k].bytes_sent>0 and iocounts[k].bytes_recv>0 ] + stats = psutil.net_if_stats() + stat = [ i for i in stats.keys() if i in ns ] + hds = psutil.net_if_addrs() + for n,v in hds.items(): + if n not in stat: + continue + for i in v: + if i.family == socket.AF_PACKET: + yield n,i.address + +if __name__ == '__main__': + def test(): + for i in getAllAddress(): + print("mac=",i) + test() diff --git a/build/lib/appPublic/myImport.py b/build/lib/appPublic/myImport.py new file mode 100644 index 0000000..ccf4c09 --- /dev/null +++ b/build/lib/appPublic/myImport.py @@ -0,0 +1,6 @@ +def myImport(modulename): + modules = modulename.split('.') + if len(modules) > 1: + a = __import__(modules[0]) + return eval('a.' + '.'.join(modules[1:])) + return __import__(modulename) \ No newline at end of file diff --git a/build/lib/appPublic/myTE.py b/build/lib/appPublic/myTE.py new file mode 100644 index 0000000..a3fcb80 --- /dev/null +++ b/build/lib/appPublic/myTE.py @@ -0,0 +1,82 @@ +import os +import sys +try: + import ujson as json +except: + import json +from jinja2 import Environment,FileSystemLoader, BaseLoader +import codecs +from appPublic.argsConvert import ArgsConvert +from appPublic.dictObject import DictObject +def isNone(obj): + return obj is None + + +def string_template_render(tmp_string, data): + rtemplate = Environment(loader=BaseLoader()).from_string(tmp_string) + return rtemplate.render(**data) + +class MyTemplateEngine: + def __init__(self,pathList,file_coding='utf-8',out_coding='utf-8'): + self.file_coding = file_coding + self.out_coding = out_coding + loader = FileSystemLoader(pathList, encoding=self.file_coding) + self.env = Environment(loader=loader, enable_async=False) + denv={ + 'json':json, + 'hasattr':hasattr, + 'int':int, + 'float':float, + 'str':str, + 'type':type, + 'isNone':isNone, + 'len':len, + 'render':self.render, + 'renders':self.renders, + 'ArgsConvert':ArgsConvert, + 'renderJsonFile':self.renderJsonFile, + 'ospath':lambda x:os.path.sep.join(x.split(os.altsep)), + 'basename':lambda x:os.path.basename(x), + 'basenameWithoutExt':lambda x:os.path.splitext(os.path.basename(x))[0], + 'extname':lambda x:os.path.splitext(x)[-1], + } + self.env.globals.update(denv) + + def set(self,k,v): + self.env.globals.update({k:v}) + + def _render(self,template,data): + # print('**********template=',template,'**data=',data,'type_data=',type(data),'************') + uRet = template.render(**data) + return uRet + + def renders(self,tmplstring,data): + def getGlobal(): + return data + self.set('global',getGlobal) + template = self.env.from_string(tmplstring) + return self._render(template,data) + + def render(self,tmplfile,data): + def getGlobal(): + return data + self.set('global',getGlobal) + template = self.env.get_template(tmplfile) + return self._render(template,data) + + def renderJsonFile(self,tmplfile,jsonfile): + with codecs.open(jsonfile,"r",self.file_coding) as f: + data = json.load(f) + return self.render(tmplfile,data) + +def tmpTml(f, ns): + te = MyTemplateEngine('.') + with codecs.open(f, 'r', 'utf-8') as fd: + d = fd.read() + b = te.renders(d, ns) + filename = os.path.basename(f) + p = f'/tmp/{filename}' + with codecs.open(p, 'w', 'utf-8') as wf: + wf.write(b) + return p + diff --git a/build/lib/appPublic/myjson.py b/build/lib/appPublic/myjson.py new file mode 100644 index 0000000..95f30d4 --- /dev/null +++ b/build/lib/appPublic/myjson.py @@ -0,0 +1,22 @@ +try: + import ujson as json +except: + import json + +import codecs + +def loadf(fn,coding='utf8'): + f = codecs.open(fn,'r',coding) + d = json.load(f) + f.close() + return d + +def dumpf(obj,fn,coding='utf8'): + f = codecs.open(fn,'w',coding) + json.dump(obj,f) + f.close() + +load = json.load +dump = json.dump +loads = json.loads +dumps = json.dumps diff --git a/build/lib/appPublic/mylog.py b/build/lib/appPublic/mylog.py new file mode 100644 index 0000000..0ffdf99 --- /dev/null +++ b/build/lib/appPublic/mylog.py @@ -0,0 +1,89 @@ +import os +from datetime import datetime +from PublicData import public_data +from folderUtils import mkdir + +myLogPath = '.' + +AllCatelogs=['SYSError', + 'SYSWarn', + 'APPError', + 'APPWarn', + 'APPInfo', + 'DEBUG1', + 'DEBUG2', + 'DEBUG3', + 'DEBUG4', + 'DEBUG5', +] + +class MyLog : + def __init__(self,path) : + self.setLogPath(path) + + def setLogPath(self,path='.') : + self.myLogPath = path + logp=os.path.join(path,'log') + mkdir(logp) + + def __call__(self,msg='') : + p = os.path.join(self.myLogPath,'log','my.log') + f = open(p,'a') + d = datetime.now() + f.write('%04d-%02d-%02d %02d:%02d:%02d %s\n' % ( d.year,d.month,d.day,d.hour,d.minute,d.second,msg)) + f.close() + +class LogMan : + def __init__(self) : + self.logers = {} + self.catelogs = AllCatelogs + + def addCatelog(self,catelog) : + if catelog not in self.catelogs : + self.catelogs.append(catelog) + + def addLoger(self,name,func,catelog) : + if type(catelog)!=type([]) : + catelog = [catelog] + catelog = [ i for i in catelog if i in self.catelogs ] + log = { + 'name':name, + 'func':func, + 'catelog':catelog, + } + self.logers[name] = log + + def delLoger(self,name) : + if name in self.logers.keys() : + del self.logers[name] + + def setCatelog(self,name,catelog) : + if type(catelog)!=type([]) : + catelog = [catelog] + catelog = [ i for i in catelog if i in self.catelogs ] + if name in self.logers.keys() : + log = self.logers[name] + log['catelog'] = catelog + self.logers[name] = log + + def __call__(self,msg='',catelog='APPInfo') : + for name,loger in self.logers.items() : + c = loger['catelog'] + if type(c)!=type([]) : + c = [c] + if catelog in c : + f = loger['func'] + f(msg) + +def mylog(s,catelog='APPInfo') : + logman = public_data.get('mylog',None) + if logman==None : + path = public_data.get('ProgramPath',None) + if path==None : + raise Exception('ProgramPath Not found in "public_data"') + log = MyLog(path) + logman = LogMan() + logman.addLoger('mylog',log,AllCatelogs) + public_data.set('mylog',logman) + return logman(s,catelog) + diff --git a/build/lib/appPublic/oauth_client.py b/build/lib/appPublic/oauth_client.py new file mode 100644 index 0000000..79656a4 --- /dev/null +++ b/build/lib/appPublic/oauth_client.py @@ -0,0 +1,188 @@ +import json +from appPublic.httpclient import HttpClient, RESPONSE_TEXT, RESPONSE_JSON, RESPONSE_BIN,RESPONSE_FILE, RESPONSE_STREAM, HttpError +from appPublic.argsConvert import ArgsConvert +from appPublic.dictObject import DictObject + +class OAuthClient: + """ + OauthClient is a http(s) client for request a api annouce by other + it send https request and get back a json data + we can check the response json data to check if the call is success or failed + desc has the following format + { + "data": predefined data, and if nessory, you can add to new data to it + "method_name":{ + "url" + "method", + "headers", + "params":arguments will appended to url with ?a=a&b=b... + "data":data will send in the request body, json format + "resp":[ + { + "name": + "converter":default none, if not, convert response data first before set the returen data + "keys":resposne json data keys to achieve data + } + ] + "error_if":{ + "keys": + "op", + "value" + } + } + } + return: + if error: + return { + "status":"error", + "code":code returned by server + "message":"message returned by server + } + else: + return { + "status":"ok", + "data":... + } + + """ + def __init__(self, desc, converters={}): + assert desc.get('data') + self.desc = desc + self.data = desc.get('data') + self.converters = converters + self.ac = ArgsConvert('${', '}') + + def setup_req_data(self, data=[], ns={}): + d = {} + if data is None: + return None + for h in data: + d1 = self.setup_req_kv(h, ns) + d.update(d1) + if d == {}: + return None + return d + + def setup_req_kv(self, d, ns): + rd = { + d.name:d.value + } + nd = self.datalize(rd, ns) + if d.converter: + f = self.converters.get(d.converter) + if f: + nd[d.name] = f(nd.get(d.name)) + return nd + + async def __call__(self, host, mapi, params): + if not self.desc.get(mapi): + raise Exception(f'{mapi} not defined') + self.api = self.desc[mapi] + if not self.api: + return { + "status":"error", + "code":'9999', + "message":f"{mapi} not defined" + } + path = self.datalize(self.api.path, params) + url = host + path + method = self.api.get('method', 'GET') + myheaders = self.setup_req_data(self.api.headers, params) + myparams = self.setup_req_data(self.api.params, params) + mydata = self.setup_req_data(self.api.data, params) + response_type = RESPONSE_JSON + hc = HttpClient() + print(f'{url=}, {method=}, {myparams=}, {mydata=}, {myheaders=}') + resp_data = None + try: + resp_data = await hc.request(url, method, response_type=response_type, + params=None if not myparams else myparams, + data=None if not mydata else mydata, + headers=myheaders) + resp_data = DictObject(**resp_data) + print(resp_data) + except HttpError as e: + return { + "status":"error", + "code":e.code, + "message":e.msg + } + + if resp_data is None: + return { + "status":"error", + "code":None, + "message":"https error" + } + err = self.check_if_error(resp_data) + if err: + return err + return self.setup_return_data(resp_data) + + def datalize(self, dic, data={}): + mydata = self.data.copy() + mydata.update(data) + s1 = self.ac.convert(dic, mydata) + return s1 + + def get_resp_data(self, resp, keys, converter=None): + d = resp.get_data_by_keys(keys) + if converter: + f = self.converters.get(converter) + if f: + d = f(d) + return d + + def setup_return_data(self, resp): + data = {} + if not self.api.resp: + return { + 'status':'ok', + 'data':{} + } + + for desc in self.api.resp: + k = desc.name + v = self.get_resp_data(resp, desc.resp_keys, desc.converter) + data[k] = v + return { + "status":"ok", + "data":data + } + + def check_if_error(self, resp): + if not self.api.error_if: + return None + ei = self.api.error_if + v = resp.get_data_by_keys(ei.error_keys) + v1 = ei.value + if ei.converter: + f = self.converters.get(ei.converter) + if f: + v = f(v) + if not ei.op: + ei.op = '==' + print(f'{ei.keys=},{v=}, {v1=}, {ei.op=}{v==v1}, {resp.base_resp.status_code=}') + if (ei.op == '==' and v == v1) or (ei.op == '!=' and v != v1): + print(f'{v=}, {v1=}, {ei.op=}{v==v1}') + code = None + message = None + if ei.code_keys: + code = resp.get_data_by_keys(ei.code_keys) + if ei.msg_keys: + message = resp.get_data_by_keys(ei.msg_keys) + return { + "status":"error", + "code":code, + "message":message + } + print(f'check_if_error ok:{v=}, {v1=}, {ei.op=}{v==v1}') + return None + + def set_data(self, resp_data, data_desc): + for dd in data_desc: + f = dd['field'] + n = dd['name'] + if resp_data.get(f): + self.data[n] = resp_data[f] + diff --git a/build/lib/appPublic/objectAction.py b/build/lib/appPublic/objectAction.py new file mode 100644 index 0000000..125fed7 --- /dev/null +++ b/build/lib/appPublic/objectAction.py @@ -0,0 +1,62 @@ +from appPublic.Singleton import SingletonDecorator + +@SingletonDecorator +class ObjectAction(object): + def __init__(self): + self.actionList = {} + + def init(self,id,action): + idA = self.actionList.get(id,None) + if idA is None: + idA = self.actionList[id] = {} + self.actionList[id][action] = [] + + def add(self,id,action,func): + idA = self.actionList.get(id,None) + if idA is None: + idA = self.actionList[id] = {} + fL = idA.get(action,None) + if fL is None: + fL = self.actionList[id][action] = [] + self.actionList[id][action].append(func) + + def execute(self,id,action,data,callback=None): + if action in ['#','*']: + return data + idA = self.actionList.get(id,None) + if idA is None: + return data + fL = idA.get(action,[]) + fL += idA.get('*',[]) + for f in fL: + data = f(id,action,data) + if len(fL)==0: + for f in idA.get('#',[]): + data = f(id,action,data) + if callback is not None: + callback(data) + return data + +if __name__ == '__main__': + def f(id,act,data): + return data + + def f1(id,act,data): + return data + + def f2(id,act,data): + return data + + def add(): + oa = ObjectAction() + oa.add('test','b',f) + #oa.add('test','*',f1) + oa.add('test','#',f2) + + def exe(): + oa = ObjectAction() + oa.execute('test','a','data1') + + add() + exe() + diff --git a/build/lib/appPublic/outip.py b/build/lib/appPublic/outip.py new file mode 100644 index 0000000..c85e40c --- /dev/null +++ b/build/lib/appPublic/outip.py @@ -0,0 +1,93 @@ +import re +import time +import requests + +class IpGetter: + def __init__(self, url, parser): + self.url = url + self.parser = parser + self.cnt = 0 + self.total_time = 0 + self.avg_time = 0 + + def get(self): + try: + tim1 = time.time() + r = requests.get(self.url) + txt = r.text + ip = self.parser(txt) + tim2 = time.time() + cost_tim = tim2 - tim1 + self.cnt += 1 + self.total_time += cost_tim + self.avg_time = self.total_time / self.cnt + ret = self.check_ip(ip) + if ret: + return ret + self.avg_time = 10000 + print('Error, get=', ip) + return None + except Exception as e: + print(f'{self.url=}. {e=}') + self.avg_time = cost_tim = 10000 + return None + + def check_ip(self, ip): + ret = re.compile(r'(\d+.\d+.\d+.\d+)').search(ip) + if ret: + return ret.group(1) + print('ip format check failed', ip, self.url) + return None + + def get_average_time(self): + return self.avg_time + + def __str__(self): + return f'{self.url=},{self.avg_time=}' + +class OutIP: + def __init__(self): + self.getters = [] + self.set_known_getters() + + + def set_known_getters(self): + g = IpGetter('http://ipinfo.io/ip', lambda x: x) + self.add_getter(g) + g = IpGetter('https://api.ipify.org', lambda x: x) + self.add_getter(g) + g = IpGetter('https://ident.me', lambda x: x) + self.add_getter(g) + # g = IpGetter('https://ipapi.co/ip/', lambda x: x) + # self.add_getter(g) + g = IpGetter('http://myip.dnsomatic.com', lambda x: x) + self.add_getter(g) + g = IpGetter('https://checkip.amazonaws.com', lambda x: x.strip()) + self.add_getter(g) + def f(t): + return re.compile(r'Address: (\d+.\d+.\d+.\d+)').search(t).group(1) + g = IpGetter('http://checkip.dyndns.com', f) + self.add_getter(g) + + def add_getter(self, getter): + self.getters.append(getter) + + def get(self): + gs = self.getters.copy() + gs.sort(key=lambda a: a.get_average_time()) + for g in gs: + # print(*[str(g) for g in self.getters ]) + ip = g.get() + if ip: + return ip + return None + +if __name__ == '__main__': + oi = OutIP() + i = 0 + while i < 100: + ip = oi.get() + print('ip = ', ip) + time.sleep(1) + i += 1 + diff --git a/build/lib/appPublic/pickleUtils.py b/build/lib/appPublic/pickleUtils.py new file mode 100644 index 0000000..b764167 --- /dev/null +++ b/build/lib/appPublic/pickleUtils.py @@ -0,0 +1,16 @@ +import pickle + +def saveData(fn,*args): + f = open(fn,'wb') + a = [ pickle.dump(arg,f) for arg in args ] + f.close() + +def loadData(fn,cnt): + a = [None] * cnt + try: + f = open(fn,'rb') + a = [ pickle.load(f) for i in range(cnt) ] + f.close() + return a + except: + return a diff --git a/build/lib/appPublic/port_forward.py b/build/lib/appPublic/port_forward.py new file mode 100644 index 0000000..739f309 --- /dev/null +++ b/build/lib/appPublic/port_forward.py @@ -0,0 +1,162 @@ +import sys +import select +import paramiko +import socket +from appPublic.background import Background +try: + import SocketServer +except ImportError: + import socketserver as SocketServer + +class ForwardServer(SocketServer.ThreadingTCPServer): + daemon_threads = True + allow_reuse_address = True + server_ready = False + ready_callback = None + def service_actions(self): + super().service_actions() + if not self.server_ready: + self.server_ready = True + if self.ready_callback: + self.ready_callback() + + def shutdown(self): + self.server_ready = False + super().shutdown() + +g_verbose = True +def verbose(s): + if g_verbose: + print(s) + +class Handler(SocketServer.BaseRequestHandler): + def handle(self): + try: + chan = self.ssh_transport.open_channel( + "direct-tcpip", + (self.chain_host, self.chain_port), + self.request.getpeername(), + ) + except Exception as e: + verbose( + "Incoming request to %s:%d failed: %s" + % (self.chain_host, self.chain_port, repr(e)) + ) + return + if chan is None: + verbose( + "Incoming request to %s:%d was rejected by the SSH server." + % (self.chain_host, self.chain_port) + ) + return + + verbose( + "Connected! Tunnel open %r -> %r -> %r" + % ( + self.request.getpeername(), + chan.getpeername(), + (self.chain_host, self.chain_port), + ) + ) + while True: + r, w, x = select.select([self.request, chan], [], []) + if self.request in r: + data = self.request.recv(1024) + if len(data) == 0: + break + chan.send(data) + if chan in r: + data = chan.recv(1024) + if len(data) == 0: + break + self.request.send(data) + + peername = self.request.getpeername() + chan.close() + self.request.close() + verbose("Tunnel closed from %r" % (peername,)) + +def connect_ssh_server(host, port, user, password): + ssh = paramiko.SSHClient() + ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy()) + ssh.connect(host, port=port, username=user, + password=password) + return ssh + +class SSHPortForward: + def __init__(self, local_port, remote_host, remote_port, + ssh_host, ssh_port, ssh_user, ssh_password): + self.local_port = int(local_port) + self.remote_host = remote_host + self.remote_port = int(remote_port) + self.ssh_host = ssh_host + self.ssh_port = int(ssh_port) + self.ssh_user = ssh_user + self.ssh_password = ssh_password + self.running = False + self._ready = False + + def service_ready(self): + print('servie ready .....') + self._ready = True + + def run(self): + if self.running: + return + self.running = True + b = Background(self._run) + b.start() + + def _run(self): + self.ssh = connect_ssh_server(self.ssh_host, + self.ssh_port, + self.ssh_user, + self.ssh_password) + + self.transport = self.ssh.get_transport() + class MyForwardServer(ForwardServer): + ready_callback = self.service_ready + + class SubHandler(Handler): + chain_host = socket.gethostbyname(self.remote_host) + chain_port = self.remote_port + local_port = self.local_port + ssh_transport = self.transport + + self.forward_server = MyForwardServer((socket.gethostbyname('localhost'), self.local_port), SubHandler) + self.forward_server.serve_forever() + print('forward ....') + + def stop(self): + if not self.running: + return + self.running = False + self.forward_server.shutdown() + self.forward_server.server_close() + self.transport.close() + self.ssh.close() + +if __name__ == '__main__': + if len(sys.argv) < 8: + print("""Usage: +{sys.argv[0] local_port remote_host remote_port ssh_host ssh_port ssh_user ssh_password +""") + sys.exit(1) + s = SSHPortForward(*sys.argv[1:]) + while True: + print("""start) start server, +stop) stop server +quit) quit +""") + x = input() + if x == 'start': + s.run() + continue + if x == 'stop': + s.stop() + continue + if x == 'quit': + s.stop() + break + print('error input') + diff --git a/build/lib/appPublic/process_workers.py b/build/lib/appPublic/process_workers.py new file mode 100644 index 0000000..0234290 --- /dev/null +++ b/build/lib/appPublic/process_workers.py @@ -0,0 +1,36 @@ +import time +from multiprocessing import Process +import threading +import random +from appPublic.background import Background + +class ProcessWorkers: + def __init__(self, worker_cnt=10): + self.semaphore = threading.Semaphore(value=worker_cnt) + self.co_worker = 0 + def _do(self, func, *args, **kwargs): + self.semaphore.acquire() + self.co_worker += 1 + p = Process(target=func, args=args, kwargs=kwargs) + p.start() + p.join() + self.co_worker -= 1 + self.semaphore.release() + + def do(self, func, *args, **kwargs): + b = Background(self._do, func, *args, **kwargs) + b.start() + + def get_workers(self): + return self.co_worker + +if __name__ == '__main__': + def k(worker): + t = random.randint(1,4) + print('current workers=',worker.get_workers(), 'sleep=', t) + time.sleep(t) + + w = ProcessWorkers() + for i in range(100): + w.do(k, w) + diff --git a/build/lib/appPublic/proxy.py b/build/lib/appPublic/proxy.py new file mode 100644 index 0000000..28828af --- /dev/null +++ b/build/lib/appPublic/proxy.py @@ -0,0 +1,12 @@ +import socket +import socks +import requests + +original_socket = socket.socket + +def set_socks_proxy(host, port): + socks.setdefaultproxy(socks.PROXY_TYPE_SOCKS5, host, port) + socket.socket = socks.socksocket + +def unset_proxy(): + socket.socket = original_socket diff --git a/build/lib/appPublic/rc4.py b/build/lib/appPublic/rc4.py new file mode 100644 index 0000000..4752dab --- /dev/null +++ b/build/lib/appPublic/rc4.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +import time +import datetime +import random, base64 +from hashlib import sha1 + +class RC4: + def __init__(self,data_coding='utf8'): + self.bcoding = 'iso-8859-1' + self.dcoding = data_coding + self.salt = b'AFUqx9WZuI32lnHk' + + def _crypt(self,data,key): + """RC4 algorithm return bytes""" + x = 0 + box = [i for i in range(256) ] + for i in range(256): + x = (x + box[i] + key[i % len(key)]) % 256 + box[i], box[x] = box[x], box[i] + x = y = 0 + out = [] + for char in data: + x = (x + 1) % 256 + y = (y + box[x]) % 256 + box[x], box[y] = box[y], box[x] + out.append(chr(char ^ box[(box[x] + box[y]) % 256])) + + return ''.join(out).encode(self.bcoding) + + def encode_bytes(self, bdata, key): + a = sha1(key + self.salt) + k = a.digest() + data = self.salt + self._crypt(bdata, k) + return data + + def encode(self,data, key,encode=base64.b64encode, salt_length=16): + """RC4 encryption with random salt and final encoding""" + if type(data)==type(''): + data = data.encode(self.dcoding) + key = key.encode(self.bcoding) + code = self.encode_bytes(data, key) + if encode: + code = encode(code) + return code.decode(self.dcoding) + return code + + def decode_bytes(self, data, key): + salt_length = 16 + salt = data[:salt_length] + a = sha1(key + self.salt) + k = a.digest() #.decode('iso-8859-1') + r = self._crypt(data[salt_length:], k) + return r + + def decode(self,data, key,decode=base64.b64decode, salt_length=16): + """RC4 decryption of encoded data""" + if type(data)==type(''): + data = data.encode(self.dcoding) + key = key.encode(self.bcoding) + if decode: + data = decode(data) + r = self.decode_bytes(data, key) + return r.decode(self.dcoding) + +class KeyChain(object): + def __init__(self, seed_str, crypter=None, keylen=23, period=600, threshold=60, time_delta=0): + self.seed_str = seed_str + if isinstance(self.seed_str, str): + self.seed_str = self.seed_str.encode('utf-8') + self.period = int(period) + self.threshold = int(threshold) + self.crypter = crypter + self.time_delta = time_delta + if crypter is None: + self.crypter = RC4() + self.keylen = keylen + self.keypool = { + } + delta = datetime.timedelta(0) + self.timezone = datetime.timezone(delta, name='gmt') + + def get_timestamp(self): + ts = int(time.time()) - self.time_delta + return ts + + def is_near_bottom(self, indicator=None): + ts = self.get_timestamp() + i = indicator + if i is None: + i = self.get_indicator(ts) + if i + self.threshold > ts: + return True + return False + + def is_near_top(self, indicator=None): + ts = self.get_timestamp() + i = indicator + if i is None: + i = self.get_indicator(ts) + if i + self.period - self.threshold < ts: + return True + return False + + def get_indicator(self, ts=None): + if ts is None: + ts = self.get_timestamp() + return int(ts / self.period) * self.period + + def genKey(self, indicator): + vv = indicator + if self.keypool.get(vv): + return self.keypool[vv] + v = vv + k1 = 0 + k = '' + m = len(self.seed_str) + while k1 < self.keylen: + j = v % self.keylen + v = v - (j + k1) * m + self.keylen + k = k + chr(self.seed_str[j]) + k1 += self.threshold / 2 + key = k.encode('utf-8') + self.keypool[vv] = key + dates = [ d for d in self.keypool.keys() ] + for d in dates: + if d < indicator - self.period: + del self.keypool[d] + return key + + def encode(self, text): + bdata = text.encode('utf-8') + return self.encode_bytes(bdata) + + def encode_bytes(self, bdata): + indicator = self.get_indicator() + key = self.genKey(indicator) + data = key + bdata + return self.crypter.encode_bytes(data, key) + + def _decode(self, data, key): + d = self.crypter.decode_bytes(data, key) + if d[:len(key)] == key: + return d[len(key):] + return None + + def decode_bytes(self, data): + indicator = self.get_indicator() + key = self.genKey(indicator) + d = self._decode(data, key) + if d is not None: + return d + + if self.is_near_bottom(indicator): + indicator -= self.period + key = self.genKey(indicator) + return self._decode(data, key) + + if self.is_near_top(indicator): + indicator += self.period + key = self.genKey(indicator) + return self._decode(data, key) + return None + + def decode(self, data): + d = self.decode_bytes(data) + if d is None: + return None + return d.decode('utf-8') + +pwdkey = 'ytguiojbhvhbnkl' +def password(pwdtxt, key=pwdkey): + rc = RC4() + code = rc.encode(pwdtxt, key) + t = rc.decode(code, key) + if (t == pwdtxt): + return code + else: + return None + +def unpassword(code, key=pwdkey): + rc = RC4() + t = rc.decode(code, key) + return t + +""" +if __name__ == '__main__': + import sys + if len(sys.argv) > 1: + print(password(sys.argv[1])) + sys.exit(0) + ps = [ + '45dr6tcfyvguh', + 'ft7gy8uh9ij0', + 'opiluykhcgjfncm' + ] + for p in ps: + print(password(p)) +""" + +if __name__=='__main__': + # 需要加密的数据长度没有限制 + # 密钥 + + data=b"231r3 feregrenerjk gkht324g8924gnfw k;ejkvwkjerv" + key = b'123456' + rc4 = RC4() + kc = KeyChain('in the heaven, we are equal', rc4) + + print(data) + # 加码 + encoded_data = kc.encode_bytes(data) + print(encoded_data,len(encoded_data) ) + # 解码 + decoded_data = kc.decode_bytes(encoded_data) + print(data, decoded_data, decoded_data==data) diff --git a/build/lib/appPublic/receiveMail.py b/build/lib/appPublic/receiveMail.py new file mode 100644 index 0000000..9072c0f --- /dev/null +++ b/build/lib/appPublic/receiveMail.py @@ -0,0 +1,137 @@ +import poplib,pdb,email,re,time +from email import header +import datetime +import os + +POP_ADDR = r'pop.126.com' +USER = '' +PASS = '' +CONFIG = '' + +def getYear(date): + rslt = re.search(r'\b2\d{3}\b', date) + return int(rslt.group()) + +def getMonth(date): + monthMap = {'Jan':1,'Feb':2,'Mar':3,'Apr':4,'May':5,'Jun':6, + 'Jul':7,'Aug':8,'Sep':9,'Oct':10,'Nov':11,'Dec':12,} + + rslt = re.findall(r'\b\w{3}\b', date) + for i in range(len(rslt)): + month = monthMap.get(rslt[i]) + if None != month: + break + + return month + +def getDay(date): + rslt = re.search(r'\b\d{1,2}\b', date) + return int(rslt.group()) + +def getTime(date): + rslt = re.search(r'\b\d{2}:\d{2}:\d{2}\b', date) + timeList = rslt.group().split(':') + + for i in range(len(timeList)): + timeList[i] = int(timeList[i]) + + return timeList + +def transformDate(date): + rslt = getYear(date) + rslt = rslt * 100 + rslt = rslt + getMonth(date) + rslt = rslt * 100 + rslt = rslt + getDay(date) + + + timeList = getTime(date) + for i in range(len(timeList)): + rslt = rslt * 100 + rslt = rslt + timeList[i] + + return rslt + +def getRecentReadMailTime(): + fp = open(CONFIG, 'r') + rrTime = fp.read() + fp.close() + return rrTime + +def setRecentReadMailTime(): + fp = open(CONFIG, 'w') + fp.write(time.ctime()) + fp.close() + return + +def getTimeEarly(period): + def years(n): + return datetime.timedelta(years=n) + def months(n): + return datetime.timedelta(years=n) + def days(n): + return datetime.timedelta(days=n) + def hours(n): + return datetime.timedelta(hours=n) + def minutes(n): + return datetime.timedelta(minutes=n) + def seconds(n): + return datetime.timedelta(seconds=n) + + funcs={ + 'y':years, + 'm':months, + 'd':days, + 'H':hours, + 'M':minutes, + 'S':seconds, + } + pattern='(\d*)([ymdHMS])' + r=re.compile(pattern) + s = r.findall(period) + t = datetime.datetime.now() + for v,ty in s: + td = funcs[ty](int(v)) + t = t - td + return time.ctime(t.timestamp()) + + +def parseMailContent(msg): + if msg.is_multipart(): + for part in msg.get_payload(): + parseMailContent(part) + else: + bMsgStr = msg.get_payload(decode=True) + charset = msg.get_param('charset') + msgStr = 'Decode Failed' + try: + if None == charset: + msgStr = bMsgStr.decode() + else: + msgStr = bMsgStr.decode(charset) + except: + pass + + print(msgStr) + +def recvEmail(POP_ADDR,USER,PASS,PERIOD,callback): + server = poplib.POP3(POP_ADDR) + server.user(USER) + server.pass_(PASS) + + mailCount,size = server.stat() + mailNoList = list(range(mailCount)) + mailNoList.reverse() + FROMTIME = getTimeEarly(PERIOD) + hisTime = transformDate(FROMTIME) + #pdb.set_trace() + for i in mailNoList: + message = server.retr(i+1)[1] + mail = email.message_from_bytes(b'\n'.join(message)) + + if transformDate(mail.get('Date')) > hisTime: + if not callback(mail): + break + #parseMailContent(mail) + else: + break diff --git a/build/lib/appPublic/registerfunction.py b/build/lib/appPublic/registerfunction.py new file mode 100644 index 0000000..63242b6 --- /dev/null +++ b/build/lib/appPublic/registerfunction.py @@ -0,0 +1,89 @@ +import asyncio +from inspect import isfunction, iscoroutinefunction +from functools import partial +from appPublic.dictObject import DictObject +from appPublic.Singleton import SingletonDecorator +from appPublic.log import info, error + +@SingletonDecorator +class RegisterFunction: + def __init__(self): + self.registKW = {} + + def register(self,name,func): + if not isfunction(func) and not iscoroutinefunction(func): + error(f'RegisterFunction.register({name}, {func}): func is not a function or routine') + return + self.registKW[name] = func + + def get(self,name): + return self.registKW.get(name,None) + + async def exe(self, name, *args, **kw): + f = self.get(name) + if f is None: + error(f'{name=} function not registed') + return None + if iscoroutinefunction(f): + info(f'{name=} is coroutine function'); + return await f(*args, **kw) + return f(*args, **kw) + +@SingletonDecorator +class RegisterCoroutine: + def __init__(self): + self.kw = DictObject() + + def register(self, name, func): + if not isfunction(func) and not iscoroutinefunction(func): + error(f'RegisterFunction.register({name}, {func}): func is not a function or routine') + return + if not self.kw.get(name): + self.kw[name] = [func] + else: + self.kw[name].append(func) + async def exe(self, name, *args, **kw): + fs = self.kw.get(name) + if fs is None: + return + fs = fs.copy() + fs.reverse() + if fs: + for f in fs: + if iscoroutinefunction(f): + await f(*args, **kw) + else: + f(*args, **kw) + return None + +def getRegisterFunctionByName(name): + rf = RegisterFunction() + return rf.get(name) + +def registerFunction(name, func): + rf = RegisterFunction() + rf.register(name, func) + +async def main(): + d = {} + rf = RegisterCoroutine() + rf.register('test', z) + rf.register('test', y) + rf.register('test', x) + nd = await rf.exe('test', d) + print(nd) + +if __name__ == '__main__': + def x(dic): + dic['a'] = 'a' + return dic + + async def y(dic): + dic['b'] = 'b' + return dic + + def z(dic): + dic['c'] = 1 + return dic + + asyncio.get_event_loop().run_until_complete(main()) diff --git a/build/lib/appPublic/restrictedEnv.py b/build/lib/appPublic/restrictedEnv.py new file mode 100644 index 0000000..1ef2bcd --- /dev/null +++ b/build/lib/appPublic/restrictedEnv.py @@ -0,0 +1,34 @@ +import appPublic.timeUtils as tu +import datetime as dt + +class RestrictedEnv: + def __init__(self): + self.reg('today',self.today) + self.reg('date',self.date) + self.reg('datetime',self.datetime) + self.reg('now',dt.datetime.now) + + def reg(self,k,v): + self.__dict__[k] = v + + def run(self,dstr): + dstr = '__tempkey__ = %s' % dstr + exec(dstr,globals(),self.__dict__) + return self.__tempkey__ + + def today(self): + now = dt.datetime.now() + return tu.ymdDate(now.year,now.month,now.day) + + def date(self,dstr): + return tu.str2Date(dstr) + + def datetime(self,dstr): + return tu.str2Datetime(dstr) + +if __name__ == '__main__': + ns = RestrictedEnv() + a = ns.run('today()') + b = ns.run("date('2011-10-31')") + c = ns.run('datetime("2012-03-12 10:22:22")') + d = ns.run('now()') diff --git a/build/lib/appPublic/rsaPeer.py b/build/lib/appPublic/rsaPeer.py new file mode 100644 index 0000000..6444daf --- /dev/null +++ b/build/lib/appPublic/rsaPeer.py @@ -0,0 +1,128 @@ +from appPublic.rsawrap import RSA +from appPublic.rc4 import RC4 +try: + import ujson as json +except: + import json +import random + +class DeliverPacket: + def __init__(self,sender,c,k,s): + self.sender = sender + self.c = c + self.k = k + self.s = s + + def pack(self): + d = { + "sender":self.sender, + "c":self.c, + "k":self.k, + "s":self.s, + } + return json.dumps(d) + + def unpack(self,body): + d = json.loads(body) + self.sender = d.sender + self.c = d['c'] + self.k = d['k'] + self.s = d['s'] + +class RSAPeer: + def __init__(self,myid,myPrikey,pearPubKey=None): + self.myid = myid + self.mypri = myPrikey + self.peerpub = pearPubKey + self.rsa = RSA() + + def getPeerPublicKey(self,id): + pass + + def _genSystematicKey(self): + t = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890~!@#$%^&*' + kl = random.randint(10,15) + ky = [] + klen = len(t) - 1 + for k in range(kl): + i = random.randint(0,klen) + # print(k,klen,i) + ky.append(t[i]) + return ''.join(ky) + + def encode(self,text): + """ + return a json text + json ojbect have three addt: + k:encrypted rc4 key + s:signature + c:ciphertext encrypted by key + """ + d = {"id":self.myid,"data":text} + text = json.dumps(d) + sk = self._genSystematicKey() + rc4 = RC4(sk) + c = rc4.encode(text) + s = self.rsa.sign(self.mypri,sk) + if self.peerpub is None: + return None + k = self.rsa.encode(self.peerpub,sk) + d = { + 'c':c, + 'k':k, + 's':s + } + return json.dumps(d) + + def decode(self,body): + """ + cipher a json text + json ojbect have three addt: + k:encrypted rc4 key + s:signature + c:ciphertext encrypted by key + """ + d = json.loads(body) + signature = d['s'] + sk = self.rsa.decode(self.mypri,d['k']) + # print('sk=',sk,'k=',d['k'],type(d['k'])) + rc4 = RC4(sk) + t = rc4.decode(d['c']) + d = json.loads(t) + ret = d['data'] + if self.peerpub is not None and not self.rsa.check_sign(self.peerpub,sk,signature): + return None + if self.peerpub is None: + peerpub = self.getPeerPublicKey(d['id']) + if peerpub is None: + return None + if not self.rsa.check_sign(peerpub,sk,signature): + return None + return ret + +if __name__ == '__main__': + r = RSA() + mary_pri = r.create_privatekey() + mary_pub = r.create_publickey(mary_pri) + + john_pri = r.create_privatekey() + john_pub = r.create_publickey(john_pri) + + john_rp = RSAPeer(john_pri,mary_pub) + mary_rp = RSAPeer(mary_pri,john_pub) + txt = '''hello python 爱的实打实大师大师大师的发送到发送到而非个人格个二哥而而二哥而个人各位,UDP是一种无连接对等通信协议,没有服务器和客户端概念,通信的任何一方均可通过通信原语直接和其他方通信 + +HOME FAQ DOCS DOWNLOAD + + +index +next | +previous | +Twisted 18.9.0 documentation » Twisted Names (DNS) » Developer Guides » ''' + c = john_rp.encode(txt) + newtxt = mary_rp.decode(c) + print(txt) + print('<===>') + print(c) + print('<===>') + print(newtxt) diff --git a/build/lib/appPublic/rsawrap.py b/build/lib/appPublic/rsawrap.py new file mode 100644 index 0000000..c5433ae --- /dev/null +++ b/build/lib/appPublic/rsawrap.py @@ -0,0 +1,108 @@ +import rsa + +class RSA: + + def __init__(self, keylength=4096, coding='iso8859'): + self.coding = coding + self.keylength = keylength + + def write_privatekey(self,private_key,fname,password=None): + bd = private_key.save_pkcs1() + with open(fname, 'wb') as f: + f.write(bd) + + def publickeyText(self,public_key): + bd = public_key.save_pkcs1() + return bd.decode(self.coding) + + def write_publickey(self,public_key,fname): + bd = public_key.save_pkcs1() + with open(fname, 'wb') as f: + f.write(bd) + + def read_privatekey(self,fname,password=None): + with open(fname, 'rb') as pf: + kd = pf.read() + return rsa.PrivateKey.load_pkcs1(kd) + + def publickeyFromText(self,text): + bd = text.encode(self.coding) + return rsa.PublicKey.load_pkcs1(bd) + + def read_publickey(self,fname): + with open(fname, 'rb') as pf: + kd = pf.read() + return rsa.PublicKey.load_pkcs1(kd) + + def create_privatekey(self, keylength=4096): + _, prik = rsa.newkeys(keylength) + return prik + + def create_publickey(self,private_key): + return rsa.PublicKey(private_key.n, private_key.e) + + def encode_bytes(self, public_key, bdata): + return rsa.encrypt(bdata, public_key) + + def encode(self,public_key,text): + bdata = text.encode(self.coding) + bc = self.encode_bytes(public_key, bdata) + return bc.decode(self.coding) + + def decode_bytes(self, private_key, bdata): + return rsa.decrypt(bdata, private_key) + + def decode(self,private_key,cipher): + bc = cipher.encode(self.coding) + bd = self.decode_bytes(private_key, bc) + return bd.decode(self.coding) + + def sign_bdata(self, private_key, data_to_sign): + return rsa.sign(data_to_sign, private_key, 'SHA-1') + + def sign(self,private_key,message): + bd = message.encode(self.coding) + bs = self.sign_bdata(private_key, bd) + return bs.decode(self.coding) + + def check_sign_bdata(self, public_key, bdata, sign): + try: + r = rsa.verify(bdata, sign, public_key) + if r == 'SHA-1': + return True + print(f'verify()={r}') + return False + except Exception as e: + print(f'check_sign_bdata() raise Exception{e}') + return False + + + def check_sign(self,public_key,plain_text,signature): + bd = plain_text.encode(self.coding) + bs = signature.encode(self.coding) + return self.check_sign_bdata(public_key, bd, bs) + + +if __name__ == '__main__': + import os + prikey1_file = os.path.join(os.path.dirname(__file__),'..','test', 'prikey1.rsa') + r = RSA() + mpri = r.create_privatekey(2048) + mpub = r.create_publickey(mpri) + + zpri = r.create_privatekey(2048) + zpub = r.create_publickey(zpri) + + l = 100 + while True: + text = 'h' * l + cipher = r.encode(mpub,text) + ntext = r.decode(mpri,cipher) + print('textlen=', l, 'encode text=', text, \ + 'decode result=', ntext, + 'cyber size=', len(cipher), + 'check if equal=', text==ntext) + signature = r.sign(zpri,text) + check = r.check_sign(zpub,text,signature) + print('sign and verify=',len(signature),check) + l += 1 diff --git a/build/lib/appPublic/set_fgcolor.py b/build/lib/appPublic/set_fgcolor.py new file mode 100644 index 0000000..02d7e68 --- /dev/null +++ b/build/lib/appPublic/set_fgcolor.py @@ -0,0 +1,33 @@ +# -*- coding=utf-8 -*- + +""" +kivy color: +[ r, g, b, a] +不同的颜色值总能找到一个人眼感知的灰度值,这是著名的心理学公式: +灰度 = 红×0.299 + 绿×0.587 + 蓝×0.114 +当灰度值大于0.5时使用暗色,否则使用明色 +colors 两个颜色,缺省为空,使用函数内置的两个颜色 +""" + +def color_gray_rate(color): + graylevel = 0.299 * color[0] + \ + 0.587 * color[1] + \ + 0.114 * color[2] + return graylevel + +def get_fgcolor_from_bgcolor(bgcolor, colors=None): + dark_fgcolor=[0.11,0.11,0.11,1] + bright_fgcolor=[0.89,0.89,0.89,1] + + graylevel = color_gray_rate(bgcolor) + if colors == None: + if graylevel > 0.5: + return dark_fgcolor + else: + return bright_fgcolor + r1 = color_gray_rate(colors[0]) + r2 = color_gray_rate(colors[1]) + if abs(graylevel - r1) > abs(graylevel - r2): + return colors[0] + return colors[1] + diff --git a/build/lib/appPublic/sockPackage.py b/build/lib/appPublic/sockPackage.py new file mode 100644 index 0000000..16ff738 --- /dev/null +++ b/build/lib/appPublic/sockPackage.py @@ -0,0 +1,136 @@ +import os +import time +import threading +import sys +import socket + +def get_free_local_addr(): + with socket.socket(socket.AF_INET, socket.SOCK_DGRAM) as s: + s.connect(("8.8.8.8", 80)) + return s.getsockname() + # return (ip,port) + +class background(threading.Thread) : + def __init__(self,func,kw) : + threading.Thread.__init__(self) + self.func = func + self.kw = kw + + def run(self) : + if self.func!=None : + self.func(**self.kw) + return + +def BackgroundCall(func,datas) : + b=background(func,datas) + b.start() + return + +class SocketServerError(Exception) : + pass + +class SocketClientError(Exception) : + pass + +class SocketServer(threading.Thread) : + + def __init__(self,host,port,max_connect=10,callee=None) : + threading.Thread.__init__(self, name = 'SocketServer') + self.setDaemon(False) + self.host = host + self.port = int(port) + self.max_c = max_connect + self.ready = False + self.keep_running = 0 + self.callee = callee + self.setSocketServer() + + def setSocketServer(self) : + try : + self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + self.sock.bind((self.host,self.port)) + self.sock.listen(self.max_c) + self.ready = True + except Exception as e: + print('setSocketServer() Error:%s\nhost=%s,port=%d' % (e,self.host,self.port)) + pass + + def run(self) : + if not self.ready : + raise SocketServerError('not ready') + callee = self.callee + if self.callee!=None : + callee = self.callee + self.keep_running = 1 + while self.keep_running : + conn,addr = self.sock.accept() + BackgroundCall(callee,{'conn':conn,'addr':addr}) + # conn.close() + + def stop(self) : + self.keep_running = 0 + + def callee(self,conn,addr) : + while 1 : + d = conn.recv(1024) + if d==None : + break + conn.send(d) + con.close() + +class SocketClient : + + def __init__(self,host,port) : + self.host = host + self.port = port + self.ready = False + self.connect() + + # if tim ==0 not blocking + def timeout(self,tim) : + if self.ready : + self.sock.setblocking(tim>0) + if tim>0 : + self.sock.settimeout(tim) + + def connect(self) : + try : + self.sock = socket.socket(socket.AF_INET,socket.SOCK_STREAM) + self.sock.connect((self.host,self.port)) + self.ready = True + except Exception as e: + self.ready = False + print('Socket connect error,%s\nhost=%s,port=%s' % (e,self.host,self.port)) + raise SocketClientError('connect error') + + def read(self,size) : + try : + data = self.sock.recv(size) + return data + except Exception as e: + print('recv error,%s' % e) + raise SocketClientError('recv error') + + def write(self,data) : + try : + self.sock.send(data) + except Exception as e: + print('recv error,%s' % e) + raise SocketClientError('send error') + + def close(self) : + self.sock.close() + self.ready = False + +if __name__ == '__main__' : + s = SocketServer('localhost',12232) + s.start() + time.sleep(5) + while 1 : + c = SocketClient('localhost',12232) + msg = 'msg1' + print("send:",msg) + c.write(msg) + d = c.read(1024) + print("get:",d) + time.sleep(1) diff --git a/build/lib/appPublic/sshx.py b/build/lib/appPublic/sshx.py new file mode 100644 index 0000000..65bcbd2 --- /dev/null +++ b/build/lib/appPublic/sshx.py @@ -0,0 +1,335 @@ +import os +import sys +import time +import shlex +from functools import partial +from threading import Thread +from appPublic.myTE import tmpTml +import asyncio, asyncssh, sys + +class SSHNode: + def __init__(self, host, + username='root', + port=22, + password=None, + jumpers=[]): + self.server2 = { + "host":host, + "username":username, + "password":password, + "port":port + } + print(self.server2) + self.jumpers = jumpers + self.conn = None + self.jumper_conns = [] + self.batch_cmds = [] + + def info(self): + d = { + "jumpers":self.jumpers, + } + d.update(self.server2) + return d + + def asjumper(self): + a = self.jumpers.copy() + a.append(self.server2) + return a + + def set_jumpers(self, jumpers): + self.jumpers = jumpers + + async def connect(self): + refconn = None + for j in self.jumpers: + host = j['host'] + username = j.get('username', 'root') + port = j.get('port',22) + password= j.get('password', None) + if refconn: + refconn = await refconn.connect_ssh(host, + username=username, + known_hosts=None, + password=password, + port=port) + else: + refconn = await asyncssh.connect(host, + username=username, + known_hosts=None, + password=password, + port=port) + self.jumper_conns.append(refconn) + + host = self.server2['host'] + username = self.server2.get('username', 'root') + port = self.server2.get('port',22) + password = self.server2.get('password', None) + if refconn: + self.conn = await refconn.connect_ssh(host, + username=username, + port=port, + password=password, + known_hosts=None) + else: + self.conn = await asyncssh.connect(host, + username=username, + password=password, + port=port) + + def close(self): + self.conn.close() + cnt = len(self.jumper_conns) + cnt -= 1 + while cnt >= 0: + self.jumper_conns[cnt].close() + cnt -= 1 + self.jumper_conns = [] + self.conn = None + + async def _l2r(self, lf, rf): + x = await asyncssh.scp(lf, (self.conn, rf), + preserve=True, recurse=True) + return x + + async def _process(self, *args, **kw): + a = await self.conn.create_process(*args, **kw) + return a + + async def _r2l(self, rf, lf): + x = await asyncssh.scp((self.conn, rf), lf, + preserve=True, recurse=True) + return x + + async def _cmd(self, cmd, input=None, stdin=None, stdout=None): + return await self.conn.run(cmd, input=input, stdin=stdin, stdout=stdout) + + async def _xcmd(self, cmd, xmsgs=[], ns={}, + show_input=None, + show_stdout=None): + proc = await self._process(cmd, term_type='xterm', + + term_size=(80,24), + encoding='utf-8' + ) + + keyin = False + def feed_data(xmsgs, debug_input): + if len(xmsgs) == 0: + print('#####++##### xmsgs has zero elements') + return + keyin = True + a = xmsgs.pop(0) + while True: + if a[1] is None: + proc.stdin.write_eof() + self.running = False + else: + s = a[1].format(**ns) + proc.stdin.write(s) + + if len(xmsgs) == 0 or xmsgs[0][0]: + break + a = xmsgs.pop(0) + + already_output = False + callee = None + loop = asyncio.get_event_loop() + self.running = True + while self.running: + if keyin: + keyin = False + await proc.stdin.drain() + if proc.stdout.at_eof(): + break + tup = proc.collect_output() + x = tup[0] + if x!='' and show_stdout: + if x is None: + break + if callee: + callee.cancel() + callee = None + show_stdout(x) + else: + if callee is None: + if len(xmsgs) > 0: + f = partial(feed_data, xmsgs, show_input) + t = xmsgs[0][0] or 0 + callee = loop.call_later(t, f) + await asyncio.sleep(0.05) + + print('##########fininshed##########') + + async def _run(self, cmd, input=None, stdin=None, stdout=None): + if cmd.startswith('l2r'): + args = shlex.split(cmd) + if len(args) == 3: + x = await self._l2r(args[1], args[2]) + return x + + if cmd.startswith('r2l'): + args = shlex.split(cmd) + if len(args) == 3: + x = await self._r2l(args[1], args[2]) + return x + + return await self._cmd(cmd, input=input, stdin=stdin, stdout=stdout) + + def show_result(self, x): + if isinstance(x, Exception): + print('Exception:',e) + else: + print('stdout:', x.stdout) + print('stderr:', x.stderr) + + async def run(self, cmd, input=None, stdin=None, stdout=None): + await self.connect() + result = await self._run(cmd, input=input, + stdin=stdin, stdout=stdout) + self.close() + return result + +class SSHNodes: + def __init__(self, nodes, usernmae='root', port=22, jumpers=[]): + self.nodes = [ Node(n, username=username, port=port, jumpers=jumpers) for n in nodes ] + self.batch_cmds = [] + + def append_cmd(self, cmd, stdin=None, stdout=None): + self.batch_cmds.append({ + "cmd":cmd, + "stdin":stdin, + "stdout":stdout}) + + def show_result(self, result, i=0): + if isinstance(result, Exception): + print(f'Task {i} failed:{result}') + elif result.exit_status != 0: + print(f'Task {i} exit {result.exit_status}') + print(result.stderr, end='') + else: + print(f'Task {i} successed:') + print(result.stdout, end='') + + async def run(self, cmd, stdin=None, stdout=None): + tasks = [ n.run(cmd, stdin=stdin, stdout=stdout) for n in self.nodes ] + results = await asyncio.gather(*tasks, return_exceptions=True) + return results + + async def exe_batch(self): + tasks = [ n.exe_batch(self.batch_cmds) for n in self.nodes ] + results = await asyncio.gather(*tasks, return_excetion=True) + return results + for i, result in enumerate(results): + self.show_result(result,i) + +async def main(): + if len(sys.argv) < 3: + print(f'{sys.argv[0]} cmd host1 host2 ....') + sys.exit(1) + + cmd = sys.argv[1] + jumpor = { + "host":"glib.cc", + "username":"ceni", + "port":10022 + } + hosts = sys.argv[2:] + mn = SSHNodes(hosts, jumpers=[jumpor]) + while True: + print('input command:') + cmd = input() + print('input stdin:') + stdin = input() + if stdin == '': + stdin = None + print('input stdout:(default is stdout)') + stdout = input() + if stdout == '': + stdout = None + x = await mn.run(cmd, stdin=stdin, stdout=stdout) + for r in x: + if isinstance(r, Exception): + print(r) + else: + print(r.stdout) + +class SSHBash: + def __init__(self, node, loop=None): + if loop is None: + loop = asyncio.get_event_loop() + self.node = node + self.loop = loop + self.conn = None + self.stdin_need = False + self.subloop = asyncio.new_event_loop() + self.subthread = Thread(target=self.start_thread_loop) + self.subthread.setDaemon(True) + self.subthread.start() + + def start_thread_loop(self): + asyncio.set_event_loop(self.subloop) + self.subloop.run_forever() + + def exit(self): + if self.conn: + self.node.close(self.conn) + self.p_obj.close() + self.subloop.stop() + self.loop.stop() + + async def feed_stdin(self, f): + self.stdin_need = False + x = await f(65535) + if x is None: + self.exit() + self.p_obj.stdin.write(x) + await self.p_obj.stdin.drain() + self.stdin_need = True + + async def run(self, read_co, write_co): + await self.node.connect() + self.p_obj = await self.node._process('bash', + term_type='vt100', + term_size=(80,24), + encoding=None) + if isinstance(self.p_obj, Exception): + print('Excetion:', self.p_obj) + self.exit() + return + if self.p_obj is None: + print('self.p_obj is None') + self.exit() + return + # self.loop.add_reader(sys.stdin.fileno(), self.read_input) + self.stdin_need = True + while True: + if self.stdin_need: + asyncio.run_coroutine_threadsafe(self.feed_stdin(read_co), self.subloop) + + if self.p_obj.stdout.at_eof(): + self.exit() + break + x = await self.p_obj.stdout.read(1024) + await write_co(x) + +if __name__ == '__main__': + async def sysstdin_read(): + return os.read(sys.stdin.fileno(), 65535) + + async def sysstdout_write(x): + sys.stdout.write(x.decode('utf-8')) + + async def test_sshbash(): + jp = { + "host":"glib.cc", + "username":"ceni", + "port":10022 + } + jn = SSHNode('k3', jumpers=[jp]) + bash = SSHBash(jn) + await bash.run(sysstdin_read, sysstdout_write) + + loop = asyncio.get_event_loop() + loop.run_until_complete(test_sshbash()) + diff --git a/build/lib/appPublic/strUtils.py b/build/lib/appPublic/strUtils.py new file mode 100644 index 0000000..abaa82f --- /dev/null +++ b/build/lib/appPublic/strUtils.py @@ -0,0 +1,20 @@ +# strUtils + +def rtrim(ss): + s = ss + if s=='': + return s + while s[-1] == ' ': + s = s[:-1] + return s +def ltrim(ss): + s = ss + if s=='': + return s + while s[0] == ' ': + s = s[1:] + return s +def lrtrim(ss): + s = ltrim(ss) + s = rtrim(s) + return s diff --git a/build/lib/appPublic/t.py b/build/lib/appPublic/t.py new file mode 100644 index 0000000..73ad434 --- /dev/null +++ b/build/lib/appPublic/t.py @@ -0,0 +1,8 @@ +import json +from dataencoder import quotedstr + +d = { + "gret":"HGREert", + "ynh":"RtghretbertBHER" +} +print(quotedstr(json.dumps(d))) diff --git a/build/lib/appPublic/testdict.py b/build/lib/appPublic/testdict.py new file mode 100644 index 0000000..251e953 --- /dev/null +++ b/build/lib/appPublic/testdict.py @@ -0,0 +1,6 @@ +import ExecFile +c = ExecFile.DictConfig(path='./config.dict') + +print(c.d.b[1].c,c.d.c.a,c.d.c.b,c.d.c.c[3].f) +print(c.d.c.c[1]) +print(c.d.c.d) diff --git a/build/lib/appPublic/thread_workers.py b/build/lib/appPublic/thread_workers.py new file mode 100644 index 0000000..45ad8eb --- /dev/null +++ b/build/lib/appPublic/thread_workers.py @@ -0,0 +1,41 @@ +import time +import threading +import random +from appPublic.background import Background + +class ThreadWorkers: + def __init__(self, max_workers=10): + self.semaphore = threading.Semaphore(value=max_workers) + self.co_worker = 0 + def _do(self, func, *args, **kwargs): + try: + self.semaphore.acquire() + self.co_worker += 1 + func(*args, **kwargs) + finally: + self.co_worker -= 1 + self.semaphore.release() + + + def do(self, func, *args, **kwargs): + b = Background(self._do, func, *args, **kwargs) + b.start() + + def get_workers(self): + return self.co_worker + + def until_done(self): + time.sleep(0.1) + while self.co_worker > 0: + time.sleep(0.01) + +if __name__ == '__main__': + def k(worker): + t = random.randint(1,4) + print('current workers=',worker.get_workers(), 'sleep=', t) + time.sleep(t) + + w = ThreadWorkers(max_workers=30) + for i in range(100000): + w.do(k, w) + diff --git a/build/lib/appPublic/timeUtils.py b/build/lib/appPublic/timeUtils.py new file mode 100644 index 0000000..c4e76fe --- /dev/null +++ b/build/lib/appPublic/timeUtils.py @@ -0,0 +1,253 @@ +import os,sys +import time +from datetime import date, timedelta, datetime + +leapMonthDays = [0,31,29,31,30,31,30,31,31,30,31,30,31] +unleapMonthDays = [0,31,28,31,30,31,30,31,31,30,31,30,31] + +def curDatetime(): + return datetime.now() + +def curDateString(): + d = curDatetime() + return '%04d-%02d-%02d' %(d.year,d.month,d.day) + +def curTimeString(): + d = curDatetime() + return '%02d:%02d:%02d' %(d.hour,d.minute,d.second) + +def timestampstr(): + d = curDatetime() + return '%04d-%02d-%02d %02d:%02d:%02d.%03d' % (d.year, + d.month, + d.day, + d.hour, + d.minute, + d.second, + d.microsecond/1000) + +def isMonthLastDay(d): + dd = timedelta(1) + d1 = d + dd + if d1.month != d.month: + return True + return False + +def isLeapYear(year): + if year % 4 == 0 and year % 100 == 0 and not (year % 400 == 0): + return True + return False + +def timestamp(dt): + return int(time.mktime((dt.year,dt.month,dt.day,dt.hour,dt.minute,dt.second,dt.microsecond,0,0))) + +def timeStampSecond(dt): + return int(time.mktime((dt.year,dt.month,dt.day,dt.hour,dt.minute,dt.second,0,0,0))) + +def addSeconds(dt,s): + ndt = dt + timedelta(0,s) + return ndt + +def monthMaxDay(y,m): + if isLeapYear(y): + return leapMonthDays[m] + return unleapMonthDays[m] + +def date2str(dt=None): + if dt is None: + dt = curDatetime() + return '%04d-%02d-%02d' % (dt.year,dt.month,dt.day) + +def time2str(dt): + return '%02d:%02d:%02d' % (dt.hour,dt,minute,dt.second) + +def str2Date(dstr): + try: + haha = dstr.split(' ') + y,m,d = haha[0].split('-') + H = M = S = 0 + if len(haha) > 1: + H,M,S = haha[1].split(':') + return ymdDate(int(y),int(m),int(d),int(H),int(M),int(S)) + except Exception as e: + print(e) + return None + +def ymdDate(y,m,d,H=0,M=0,S=0): + return datetime(y,m,d,H,M,S) + +def str2Datetime(dstr): + x = dstr.split(' ') + d = x[0] + t = '00:00:00' + if len(x) > 1: + t = x[1] + y,m,d = d.split('-') + H,M,S = t.split(':') + return datetime(int(y),int(m),int(d),int(H),int(M),int(S)) + +def strdate_add(date_str, days=0, months=0, years=0): + dt = str2Datetime(date_str) + dt = dateAdd(dt, days=days, months=months, years=years) + ds = date2str(dt) + return ds + +def addMonths(dt,months): + y = dt.year + m = dt.month + months + d = dt.day + mm = (m - 1) % 12 + 1 + md = int((m - 1) / 12) + y += md + m = mm + maxd = monthMaxDay(y,m) + if d > maxd: + d = maxd + return ymdDate(y,m,d) + +def addYears(dt,years): + y = dt.year + years + m = dt.month + d = dt.day + maxd = monthMaxDay(y,m) + if d > maxd: + d = maxd + return ymdDate(y,m,d) + +def dateAdd(dt,days=0,months=0,years=0): + if days != 0: + dd = timedelta(days) + dt = dt + dd + if months != 0: + dt = addMonths(dt,months) + if years != 0: + dt = addYears(dt,years) + return dt + +def firstSunday(dt): + f = dt.weekday() + if f<6: + return dt + timedelta(7 - f) + return dt + +DTFORMAT = '%Y%m%d %H%M%S' +def getCurrentTimeStamp() : + t = time.localtime() + return TimeStamp(t) + +def TimeStamp(t) : + return time.strftime(DTFORMAT,t) + +def StepedTimestamp(baseTs,ts,step) : + if step<2 : + return ts + offs = int(timestampSub(ts,baseTs)) + step = int(step) + r,m = divmod(offs,step) + if m < step/2 : + return timestampAdd(baseTs,step * r) + else : + return timestampAdd(baseTs,step * (r+1)) + +def timestampAdd(ts1,ts2) : + t1 = time.strptime(ts1,DTFORMAT) + tf = time.mktime(t1) + if type(ts2)=='' : + t2 = time.strptime(ts2,DTFORMAT) + ts2 = time.mktime(t2) + tf += ts2 + t = time.localtime(tf) + return TimeStamp(t) + +def timestampSub(ts1,ts2) : + t1 = time.strptime(ts1,DTFORMAT) + t2 = time.strptime(ts2,DTFORMAT) + ret = time.mktime(t1) - time.mktime(t2) + return int(ret) + +def timestamp2dt(t): + return datetime.fromtimestamp(t) + +def date_weekinyear(date_str): + w = datetime.strptime(date_str, '%Y-%m-%d').strftime('%W') + return date_str[:5] + w + +def date_season(date_str): + m = date_str[5:7] + sl = { + '01':'1', + '02':'1', + '03':'1', + '04':'2', + '05':'2', + '06':'2', + '07':'3', + '08':'3', + '09':'3', + '10':'4', + '11':'4', + '12':'4', + } + s = sl.get(m) + return date_str[:5] + s + +""" +Patterns = + 'D' + 'W[0-6]' + 'M[00-31]' + 'S[0-2]-[00-31]' + 'Y[01-12]-[00-31]' +} +""" + +def str2date(sd): + a = [ int(i) for i in sd.split('-') ] + return date(*a) + +def is_monthend(dt): + if isinstance(dt, str): + dt = str2date(dt) + nxt_day = dt + timedelta(days=1) + if dt.month != nxt_day.month: + return True + return False + +def is_match_pattern(pattern, strdate): + """ +R:代表实时 +D:代表日 +W[0-6]:代表周日到周六 +M[00-31]:代表月末月到某一天 +S[1-3]-[00-31]:代表季度第几个月的第几天 +Y[1-12]-[00-31]:代表一年中的某个月的某一天 + """ + if pattern == 'D': + return True + dt = str2date(strdate) + if pattern.startswith('W'): + w = (int(pattern[1]) + 6) % 7 + if dt.weekday() == w: + return True + return False + if pattern.startswith('M'): + day = int(pattern[1:]) + if day == 0 and is_monthend(dt): + return True + if day == dt.day: + return True + return False + if pattern.startswith('S'): + m,d = [ int(i) for i in pattern[1:].split('-') ] + print(f'{m=}-{d=}, {dt.month=} {dt.day}') + m %= 4 + if m == dt.month % 4 and d == dt.day: + return True + return False + if pattern.startswith('Y'): + m,d = [ int(i) for i in pattern[1:].split('-') ] + print(f'{m=}-{d=}, {dt.month=} {dt.day}') + if m == dt.month and d == dt.day: + return True + return False + diff --git a/build/lib/appPublic/timecost.py b/build/lib/appPublic/timecost.py new file mode 100644 index 0000000..190496b --- /dev/null +++ b/build/lib/appPublic/timecost.py @@ -0,0 +1,42 @@ +import time +import datetime +from .Singleton import SingletonDecorator + +timerecord = {} + +class TimeCost: + def __init__(self,name): + self.name = name + + def __enter__(self): + self.begin_time = time.time() + + def __exit__(self,*args): + self.end_time = time.time() + d = timerecord.get(self.name,[]) + d.append(self.end_time - self.begin_time) + timerecord[self.name] = d + + def clear(self): + timerecord = {} + + @classmethod + def clear_all(self): + timerecord = {} + + @classmethod + def clear(self, name): + timerecord[name] = [] + + @classmethod + def show(self): + def getTimeCost(name): + x = timerecord.get(name,[]) + if len(x) == 0: + return 0,0,0 + return len(x), sum(x), sum(x)/len(x) + + print('TimeCost ....') + for name in timerecord.keys(): + print(name, * getTimeCost(name)) + diff --git a/build/lib/appPublic/tworkers.py b/build/lib/appPublic/tworkers.py new file mode 100644 index 0000000..4a393c3 --- /dev/null +++ b/build/lib/appPublic/tworkers.py @@ -0,0 +1,62 @@ +import sys +import threading +from threading import Thread +from queue import Queue, Empty + +class Worker(Thread): + def __init__(self, rqueue, timeout=1): + Thread.__init__(self) + self.timeout = timeout + self.setDaemon(False) + self.r_queue = rqueue + self.start() + + def run(self): + emptyQueue = False + while True: + try: + callable,args,kw = self.r_queue.get(timeout=self.timeout) + if callable is None: + break + callable(*args,**kw) + + except Empty: + time.sleep(1) + + def resulthandler(self,rez): + pass + +class ThreadWorkers: + def __init__(self,num_workers=20): + self.workQueue = Queue() + self.worker_cnt = num_workers + self.workers = [] + self.__createThreadPool(num_workers) + + def __createThreadPool(self,num): + for i in range(num): + thread = Worker(self.workQueue) + self.workers.append(thread) + + def wait_for_complete(self): + for i in range(self.worker_cnt): + self.add_job(None,None,None) + + while len(self.workers): + thread = self.workers.pop() + if thread.isAlive(): + thread.join() + + def add_job(self,callable,args=[],kw={}): + self.workQueue.put([callable,args,kw]) +if __name__ == '__main__': + import requests + def get(url): + x = requests.get(url) + print(x.status_code) + + tw = ThreadWorkers() + for i in range(10000): + tw.add_job(get,['http://www.baidu.com']) + tw.wait_for_complete() + print('finished') diff --git a/build/lib/appPublic/udp_comm.py b/build/lib/appPublic/udp_comm.py new file mode 100644 index 0000000..52e29cf --- /dev/null +++ b/build/lib/appPublic/udp_comm.py @@ -0,0 +1,103 @@ +# -*- coding:UTF-8 -*- +import time +from traceback import print_exc +from socket import * +from select import select + +import json +from appPublic.sockPackage import get_free_local_addr +from appPublic.background import Background +BUFSIZE = 1024 * 64 +class UdpComm: + def __init__(self, port, callback, timeout=0): + self.buffer = [] + self.callback = callback + self.timeout = timeout + self.host = get_free_local_addr()[0] + self.port = port + self.udpSerSock = socket(AF_INET, SOCK_DGRAM) + # 设置阻塞 + # self.udpSerSock.setblocking(1 if timeout > 0 else 0) + # 设置超时时间 1s + # self.udpSerSock.settimeout(timeout) + self.udpSerSock.bind(('' ,port)) + self.run_flg = True + self.thread = Background(self.run) + self.thread.start() + + def run(self): + sock = self.udpSerSock + while self.run_flg: + outs = [] + if len(self.buffer) > 0: + outs = [sock] + in_s, out_s, exc_s = select([sock], outs, [], 0.1) + if sock in in_s: + b, addr = sock.recvfrom(BUFSIZE) + t = b[0] + b = b[1:] + if t == 'b': + self.callback(b, addr) + else: + try: + txt = b.decode('utf-8') + d = json.loads(txt) + self.callback(d, addr) + except Exception as e: + print('except:',e) + print_exc() + print(t, b) + break + if sock in out_s: + while len(self.buffer) > 0: + d,addr = self.buffer.pop(0) + sock.sendto(d, addr) + time.sleep(0.1) + self.run_flg = False + self.udpSerSock.close() + + def stop(self): + self.run_flg = False + self.udpSerSock.close() + self.thread.join() + + def broadcast(self, data): + broadcast_host = '.'.join(self.host.split('.')[:-1]) + '.255' + udpCliSock = socket(AF_INET, SOCK_DGRAM) + # udpCliSock.settimeout(1) + udpCliSock.bind(('', 0)) + udpCliSock.setsockopt(SOL_SOCKET, SO_BROADCAST, 1) + b = data + if not isinstance(data, bytes): + b = json.dumps(data).encode('utf-8') + udpCliSock.sendto(b, (broadcast_host,self.port)) + + def send(self,data,addr): + b = data + if not isinstance(data, bytes): + b = b'j' + json.dumps(data).encode('utf-8') + else: + b = b'b' + data + if isinstance(addr,list): + addr = tuple(addr) + self.buffer.append((b, addr)) + + def sends(self,data, addrs): + for a in addrs: + self.send(data, a) + +if __name__ == '__main__': + import sys + def msg_handle(data, addr): + print('addr:', addr, 'data=', data, len(data)) + + port = 50000 + if len(sys.argv)>1: + port = int(sys.argv[1]) + d = UdpComm(port, msg_handle) + x = input() + while x: + port, data = x.split(':') + d.send(data, ('', int(port))) + x = input() + diff --git a/build/lib/appPublic/uni_outip.py b/build/lib/appPublic/uni_outip.py new file mode 100644 index 0000000..88f7c25 --- /dev/null +++ b/build/lib/appPublic/uni_outip.py @@ -0,0 +1,70 @@ +import os +import time +from natpmp import NATPMP as pmp +import upnpclient +from appPublic.ipgetter import IPgetter +from multiprocessing import Process, Pipe + +def pmp_get_external_ip(): + try: + return pmp.get_public_address() + except: + return None + +def upnp_get_external_ip(): + try: + igd = upnpclient.discover()[0] + print(igd.service_map) + + s_names = [ n for n in igd.service_map.keys() if 'WAN' in n and 'Conn' in n] + upnp = igd.service_map[s_names[0]] + x = upnp.GetExternalIPAddress() + return x.get('NewExternalIPAddress', None) + except Exception as e: + print(f'e={e}') + return None + +def ipgetter_get_external_ip(): + getter = IPgetter() + ip = None + while ip is None: + try: + ip = getter.get_external_ip() + except: + ip = None + if ip: + return ip + time.sleep(0.1) + +def get_external_ip(): + ip = pmp_get_external_ip() + if ip: + return ip + ip = upnp_get_external_ip() + if ip: + return ip + return ipgetter_get_external_ip() + +def outip(w): + os.dup2(w.fileno(), 1) + ip = get_external_ip() + print(ip) + +def get_ip(): + r, w = Pipe() + reader = os.fdopen(r.fileno(), 'r') + p = Process(None, outip, 'TESTER', (w, )) + p.start() + ip = reader.readline() + p.join() + return ip.strip() + +def run(): + while True: + ip = get_ip() + if ip: + print(f'{ip=}') + time.sleep(10) + +if __name__ == '__main__': + run() diff --git a/build/lib/appPublic/unicoding.py b/build/lib/appPublic/unicoding.py new file mode 100644 index 0000000..9cacba9 --- /dev/null +++ b/build/lib/appPublic/unicoding.py @@ -0,0 +1,39 @@ +#unidict.py +import locale + +def unicoding(d,coding='utf8'): + if type(d) == type(''): + return d + if type(d) == type(b''): + try: + if coding is not Noene: + return d.decode(coding) + else: + return d.decode(locale.getdefaultlocale()[1]) + except: + try: + return d.decode(locale.getdefaultlocale()[1]) + except: + try: + return d.decode('utf8') + except: + return d + return d + +def uObject(obj,coding='utf8'): + otype = type(obj) + if otype == type(u''): + return obj + if otype == type({}): + return uDict(obj,coding) + if otype == type([]): + return [uObject(i,coding) for i in obj ] + if hasattr(obj,'decode'): + return obj.decode(coding) + return obj + +def uDict(dict,coding='utf8'): + d = {} + for k,v in dict.items(): + d[uObject(k)] = uObject(v) + return d diff --git a/build/lib/appPublic/uniqueID.py b/build/lib/appPublic/uniqueID.py new file mode 100644 index 0000000..7387af8 --- /dev/null +++ b/build/lib/appPublic/uniqueID.py @@ -0,0 +1,36 @@ +import uuid +from nanoid import generate + +def setNode(n='ff001122334455'): + pass + +def getID(size=21): + return generate(size=size) + +def validate_code(id, cnt=6): + b = int(len(id) / cnt) + j = 0 + code = [] + v = 0 + print(f'{b=}, {cnt=}') + for c in id: + if j >= b: + v = v % 10 + code.append(str(v)) + j = 0 + v += ord(c) + j += 1 + if len(code) >= cnt: + break + return ''.join(code) + +def check_code(id, code): + c = validate_code(id) + return c==code + +if __name__ == '__main__': + id = getID() + code = validate_code(id) + b = check_code(id, code) + print(id, code, b) + diff --git a/build/lib/appPublic/version.py b/build/lib/appPublic/version.py new file mode 100644 index 0000000..62feff7 --- /dev/null +++ b/build/lib/appPublic/version.py @@ -0,0 +1 @@ +__version__ = '5.1.27' diff --git a/build/lib/appPublic/wcag_checker.py b/build/lib/appPublic/wcag_checker.py new file mode 100644 index 0000000..0452f73 --- /dev/null +++ b/build/lib/appPublic/wcag_checker.py @@ -0,0 +1,27 @@ + +def calculate_luminence(rgba): + return 0.2126 * color[0] + \ + 0.7152 * color[1] + \ + 0.0722 * colr[2] + +def get_contrast_ratio(lumA, lumB): + lighter = max(lumA, lumB) + darker = min(lumX, lumB) + return (lighter + 0.05) / (darker + 0.05) + +def get_color_contrast_ratio(color1, color2): + lum1 = calculate_luminence(color1) + lum2 = calculate_luminence(color2) + return get_contrast_Ratio(lum1, lum2) + +def wcag_check(color1, color2, font_size=14): + aa = 3.0 + aaa = 4.5 + if font_size < 18: + aa = 4.5 + aaa = 7.0 + ratio = get_color_contrast_ratio(color1, color2) + return ratio >= aa, radio >= aaa + +if __name__ == '__main__': + pass diff --git a/build/lib/appPublic/worker.py b/build/lib/appPublic/worker.py new file mode 100644 index 0000000..1179cfd --- /dev/null +++ b/build/lib/appPublic/worker.py @@ -0,0 +1,74 @@ +import time +import random +import asyncio +import inspect +from functools import wraps +from functools import wraps + +def awaitify(sync_func): + """Wrap a synchronous callable to allow ``await``'ing it""" + @wraps(sync_func) + async def async_func(*args, **kw): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, sync_func, *args, **kw) + return async_func + +def coroutinify(func): + @wraps(func) + async def async_func(*args): + loop = asyncio.get_event_loop() + return await loop.run_in_executor(None, func, *args) + return async_func + +def to_func(func): + @wraps(func) + def wraped_func(*args,**kw): + if inspect.iscoroutinefunction(func): + task = asyncio.ensure_future(func(*args,**kw)) + ret = asyncio.gather(task) + return ret + return func(*args, **kw) + return wraped_func + +class AsyncWorker: + def __init__(self,maxtask=50): + self.semaphore = asyncio.Semaphore(maxtask) + + async def __call__(self,callee,*args,**kw): + async with self.semaphore: + if inspect.iscoroutinefunction(callee): + return await callee(*args,**kw) + return callee(*args, **kw) + + async def run(self,cmd): + async with self.semaphore: + proc = await asyncio.create_subprocess_shell(cmd, + stdout=asyncio.subprocess.PIPE, + stderr=asyncio.subprocess.PIPE) + + stdout, stderr = await proc.comunicate() + return stdout, stderr + +if __name__ == '__main__': + def hello(cnt,greeting): + t = random.randint(1,10) + print(cnt,'will sleep ',t,'seconds') + time.sleep(t) + print(cnt,'cost ',t,'seconds to',greeting) + + async def ahello(cnt,greeting): + t = random.randint(1,10) + print(cnt,'will sleep ',t,'seconds') + await asyncio.sleep(t) + print(cnt,'cost ',t,'seconds to',greeting) + + async def run(): + w = AsyncWorker() + f = awaitify(hello) + g = [ asyncio.create_task(w(f,i,'hello world')) for i in range(100) ] + await asyncio.wait(g) + print('aaaaaaaaaaaaaaaaaaa') + + loop = asyncio.get_event_loop() + loop.run_until_complete(run()) + diff --git a/build/lib/appPublic/zmq_reqrep.py b/build/lib/appPublic/zmq_reqrep.py new file mode 100644 index 0000000..29e0886 --- /dev/null +++ b/build/lib/appPublic/zmq_reqrep.py @@ -0,0 +1,135 @@ +# zmq_reqresp.py +import asyncio +import zmq +import zmq.asyncio +from .background import Background +from inspect import iscoroutinefunction + +class ZmqRequester(object): + def __init__(self, url, async_mode=False, timeout=0): + super().__init__() + self.async_mode = async_mode + self.url = url + self.timeout = timeout + self._connect() + + def __del__(self): + self._close() + + def _connect(self): + if self.async_mode: + self.ctx = zmq.asyncio.Context() + else: + self.ctx = zmq.Context() + + # Socket to talk to server + self.sock = self.ctx.socket(zmq.REQ) + self.sock.connect(self.url) + if self.timeout > 0: + self.sock.setsockopt(zmq.LINGER, 0) + self.poller = zmq.Poller() + self.poller.register(self.sock, zmq.POLLIN) + + def _close(self): + self.sock.close() + self.ctx.term() + + def send(self, msg): + """ + send s string to responser, and return a string + """ + if self.async_mode: + raise Exception('ZMQ_Requester: in async mode, use asend instead') + b = msg.encode('utf-8') + r = self.send_b(b) + if r is not None: + return r.decode('utf-8') + return None + + def send_b(self, b): + """ + send a bytes and return a bytes + """ + if self.async_mode: + raise Exception('ZMQ_Requester: in async mode, use asend_b instead') + self.sock.send(b) + if self.timeout > 0: + if self.poller.poll(self.timeout * 1000): + return self.sock.recv() + else: + self._close() + self._connect() + return None + else: + return self.sock.recv() + + async def asend_b(self, b): + if not self.async_mode: + raise Exception('ZMQ_Requester: not in async mode, use send_b instead') + await self.sock.send_multipart([b]) + if self.timeout > 0: + if self.poller.poll(self.timeout * 1000): + r = await self.sock.recv_multipart() + return r + else: + self._close() + self._connect() + return None + r = await self.sock.recv_multipart() + return r[0] + + async def asend(self, msg): + if not self.async_mode: + raise Exception('ZMQ_Requester: not in async mode, use send instead') + b = msg.encode('utf-8') + r = await self.asend_b(b) + if r is None: + return None + return r.decode('utf-8') + +class ZmqReplier(object): + def __init__(self, url, handler, async_mode=False): + self.async_mode = async_mode + self.url = url + if not self.async_mode and iscoroutinefunction(handler): + raise('not in async mode, handler can not be a coroutine') + + self.handler = handler + if self.async_mode: + self.ctx = zmq.asyncio.Context() + else: + self.ctx = zmq.Context() + self.sock = self.ctx.socket(zmq.REP) + self.sock.bind(self.url) + self.keep_running = True + + async def async_run(self): + while self.keep_running: + bs = await self.sock.recv_multipart() + b = b[0] + if iscoroutinefunction(self.handler): + rb = await self.handler(b) + else: + rb =self.self.handler(b) + + if isinstance(rb, str): + rb = rb.encode('utf-8') + await self.sock.send_multipart([rb]) + + def run(self): + self.background = Background(self._run) + self.background.daemon = True + self.background.start() + + def _run(self): + while self.keep_running: + b = self.sock.recv() + + rb = self.handler(b) + if isinstance(rb, str): + rb = rb.encode('utf-8') + self.sock.send(rb) + + def stop(self): + self.keep_running = False + self.join() diff --git a/build/lib/appPublic/zmq_topic.py b/build/lib/appPublic/zmq_topic.py new file mode 100644 index 0000000..e509acd --- /dev/null +++ b/build/lib/appPublic/zmq_topic.py @@ -0,0 +1,123 @@ +import sys +import zmq +import time +from zmq import Context +from appPublic.jsonConfig import getConfig + +class TopicServer: + def __init__(self, address='127.0.0.1', pub_port='5566', sub_port='5567'): + # get ZeroMQ version + print("Current libzmq version is %s" % zmq.zmq_version()) + print("Current pyzmq version is %s" % zmq.pyzmq_version()) + + self.context = Context.instance() + # 2 sockets, because we can only bind once to a socket (as opposed to connect) + self.pub_port = "tcp://{}:{}".format(address, pub_port) + self.sub_port = "tcp://{}:{}".format(address, sub_port) + + self.xpub_xsub_proxy() + + # N publishers to 1 sub; proxy 1 sub to 1 pub; publish to M subscribers + def xpub_xsub_proxy(self): + print("Init proxy") + + # Socket subscribing to publishers + frontend_pubs = self.context.socket(zmq.XSUB) + frontend_pubs.bind(self.pub_port) + + # Socket publishing to subscribers + backend_subs = self.context.socket(zmq.XPUB) + backend_subs.bind(self.sub_port) + + print("Try: Proxy... CONNECT!") + zmq.proxy(frontend_pubs, backend_subs) + print("CONNECT successful!") + """ + while True: + time.sleep(1) + """ + +class ConfiguredTopicServer(TopicServer): + """ + in config file has a topicserver key + { + "topicserver":{ + "address":"11.11.1.11", + "pub_port":1234, + "sub_server":1235 + } + } + """ + def __init__(self): + config = getConfig() + params = config.topicserver + if not params: + raise MissTopicServerConfig + super(ConfiguredTopicServer, self).__init__(**params) + +class TopicPublisher: + def __init__(self, topic='en', address='127.0.0.1', port='5566'): + # get ZeroMQ version + print("Current libzmq version is %s" % zmq.zmq_version()) + print("Current pyzmq version is %s" % zmq.pyzmq_version()) + + self.topic = topic + self._topic = topic.encode('utf-8') + self.context = Context.instance() + self.url = "tcp://{}:{}".format(address, port) + self.pub = self.context.socket(zmq.PUB) + self.pub.connect(self.url) + time.sleep(0.5) + + def send(self, message): + self.pub.send_multipart([self._topic, message.encode('utf-8')]) + +class ConfiguredTopicPublisher(TopicPublisher): + def __init__(self, topic=''): + config = getConfig() + params = config.topicserver + if not params: + raise MissTopicServerConfig + super(ConfiguredTopicPublisher, self).__init__(topic=topic, + address = params.address, + port=params.pub_port) + +class TopicSubscriber: + def __init__(self, topic='', address='127.0.0.1', port='5567', callback=None): + # get ZeroMQ version + print("Current libzmq version is %s" % zmq.zmq_version()) + print("Current pyzmq version is %s" % zmq.pyzmq_version()) + + self.callback = callback + self.topic = topic + self.context = Context.instance() + self.url = "tcp://{}:{}".format(address, port) + self.sub = self.context.socket(zmq.SUB) + self.sub.connect(self.url) + # subscribe to topic 'en' or 'jp' + if isinstance(self.topic, list): + for t in self.topic: + self.sub.setsockopt(zmq.SUBSCRIBE, t.encode('utf-8')) + else: + self.sub.setsockopt(zmq.SUBSCRIBE, self.topic.encode('utf-8')) + + + def run(self): + # keep listening to all published message, filtered on topic + print("Sub {}: Going to wait for messages!".format(self.topic)) + while True: + msg_received = self.sub.recv_multipart() + print("sub {}: {}".format(self.topic, msg_received)) + if self.callback: + self.callback(msg_received) + +class ConfiguredTopicSubscriber(TopicSubscriber): + def __init__(self, topic=''): + config = getConfig() + params = config.topicserver + if not params: + raise MissTopicServerConfig + super(ConfiguredTopicSubscriber, self).__init__(topic=topic, + address=params.address, + port=params.sub_port) + diff --git a/build/lib/appPublic/zmqapi.py b/build/lib/appPublic/zmqapi.py new file mode 100644 index 0000000..bc643a9 --- /dev/null +++ b/build/lib/appPublic/zmqapi.py @@ -0,0 +1,176 @@ + +import asyncio +from collections.abc import Coroutine +# from asyncio.coroutines import iscoroutine + +import zmq +import zmq.asyncio +import json + +class Publisher: + def __init__(self,port,coding='utf-8',msgid=1000): + self.port = port + self.socket = None + self.coding = coding + self.msgid = msgid + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.PUB) + self.socket.bind('tcp://*:%d' % self.port) + + async def publish(self,msg,msgtype='text',msgid=-1): + print(msg,msgtype) + if msgid == -1: + msgid = self.msgid + if msgtype != 'text': + msg = json.dumps(msg) + msgtype = 'json' + s = '%d %s %s' % (msgid,msgtype,msg) + print(s,msgtype,msgid) + b = s.encode(self.coding) + await self.socket.send(b) + + def __del__(self): + self.socket.close() + +class Subscriber: + def __init__(self,host,ports,msgid,coding='utf-8'): + self.host = host + self.ports = ports + self.msgid = msgid + self.coding = coding + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.SUB) + f = b'%d' % self.msgid + self.socket.setsockopt(zmq.SUBSCRIBE, f) + for p in self.ports: + self.socket.connect("tcp://%s:%d" % (self.host,p)) + + def addPort(self,port): + self.socket.connect("tcp://%s:%d" % (self.host,port)) + #f = b'%d' % self.msgid + #self.socket.setsockopt(zmq.SUBSCRIBE, f) + + async def subscribe(self): + ret = await self.socket.recv() + ret = ret.decode(self.coding) + msgid, msgtype, body = ret.split(' ',2) + print('msgid=',msgid,'msgtype=',msgtype,'body=',body) + if msgtype == 'json': + return json.loads(body) + return body + + def __del__(self): + self.socket.close() + +class RRServer: + """ + a request / response mode server + """ + def __init__(self,port,handler=None): + self.port = port + self.handler = handler + print(type(self.handler)) + + async def run(self): + running = True + context = zmq.asyncio.Context() + socket = context.socket(zmq.REP) + socket.bind('tcp://*:%s' % self.port) + while running: + rmsg = await socket.recv() + wmsg = rmsg + if self.handler is not None: + wmsg = self.handler(rmsg) + if isinstance(wmsg,Coroutine): + wmsg = await wmsg + await socket.send(wmsg) + socket.close() + +class RRClient: + """ + a request / response mode client + """ + def __init__(self,host,port): + self.host = host + self.port = port + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.REQ) + self.socket.connect('tcp://%s:%d' % (self.host,self.port)) + + async def request(self,msg): + await self.socket.send(msg) + return await self.socket.recv() + + +class PPPusher: + """ + pusher of Push / Pull mode + """ + def __init__(self,host,port): + self.host = host + self.port = port + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.PUSH) + self.socket.bind('tcp://%s:%d' % (self.host,self.port)) + + async def push(self,msg): + await self.socket.send(msg) + +class PPPuller: + """ + puller of Push / Pull mode + """ + def __init__(self,host,port,handler=None): + self.host = host + self.port = port + self.handler = handler + + async def run(self): + self.running = True + context = zmq.asyncio.Context() + socket = context.socket(zmq.PULL) + socket.bind('tcp://%s:%d' % (self.host,self.port)) + while self.running: + msg = await self.socket.recv() + if self.handler is not None: + x = self.handler(msg) + if isinstance(x,Coroutine): + await x + +class PairClient: + """ + client of Pair mode + """ + def __init__(self,host,port): + self.host = host + self.port = port + context = zmq.asyncio.Context() + self.socket = context.socket(zmq.PAIR) + self.socket.bind('tcp://%s:%d' % (self.host,self.port)) + + async def request(self,msg): + await self.socket.send(msg) + return await self.socket.recv() + +class PairServer: + """ + server of Pair mode + """ + def __init__(self,port,handler=None): + self.port = port + self.handler = handler + self.running = True + + async def run(self): + self.running = True + context = zmq.asyncio.Context() + socket = context.socket(zmq.PAIR) + socket.bind('tcp://*:%d' % self.port) + while self.running: + msg = await socket.recv() + ret = msg + if self.handler is not None: + ret = self.handler() + if isinstance(ret,Coroutine): + ret = await ret + await socket.send(ret) diff --git a/dist/appPublic-5.1.26-py3-none-any.whl b/dist/appPublic-5.1.26-py3-none-any.whl new file mode 100755 index 0000000000000000000000000000000000000000..0f34cee439985e142148d6bf1e91ead05d22ad5e GIT binary patch literal 64587 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
    Qf(|M03=*XvUPk^IS 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 literal 0 HcmV?d00001 diff --git a/dist/appPublic-5.1.26.tar.gz b/dist/appPublic-5.1.26.tar.gz new file mode 100755 index 0000000000000000000000000000000000000000..45e00bf6dbc117da3382c264cfac459e1583c660 GIT binary patch literal 49237 zcmV)yK$5>7iwFp$xRGN5|6y=&P<3K#X=5!lE-@}LHZF8wasceT`F9(~kpP@uiN9h( zMQ;FU1Q8%9*;pgrQq*C^x+2o{CKNxs0j2?tIhYyG40u>i?JlQ(uGF zg@+gBPq&+==i8@Sr{`-Qt3LndKXw>ioAf<*aAL99YPL_E{E+7V>BU9<_Z0uzYR_fo zd}jX7FD%S`%vgMo^M7~z^!|i$#;*T=Z~iwc`CrNZqWoVtHQ%b_|39MqzxLRL6BnrFW2|t=_D2&y8>?K~1q^)rHlHbf$sI61W*q88 zV<`-yV2e5U`|`zAnO!@};z8tw2~g_9a^NS>?1|Og5Gazco#Z4?Y|{z0{aWpaMmO7t zSz-i3!-X!6q4ja#Ogt7F&{HGi2svzvc|kbFetf`E`6IFZw@($<|M_bDe<16BTVMZA z&$Z`HH|MIA{~ve#zkY7%?B#RKvGd^8f7l%6*ZC|NiX1i}Q=8EBo(1jQp?mf7Sl4`h3*n|MJQcXYIsphPxl` z{;z#%K5zd$-8xn6{~pN49fv`b7=w7LRvVzn(-6bnsm(ZSX!Lrn?w5c^8THKu>2!ahlYly8ykL* zP#(6CmQKq+G9m_PYwPgc^8+LcLWVuGXSlw>o1^*wU4Q_{2-UEC$Dld_rtCizPup7? z(w=;X@Vs>a3hRAR)RV;F^&&QaN;hwDAW+dIL~MV=>K@RD!)-KpX)M0Rlh)kzb*SmV zU&=zTO^^`Gcj{0IDMnp94x^2IY!192W_3>DWD@x_N;N(}Zo;DLnL#2{ht8CtqLE6X zUBaqt#6`zzd_u86tly4h;iI!AZ72a zvq2E0W2q0O935>;Wv%ld6XFd@VmkvCCI^lh#ciduAc~0WjTRK3WLhbrYIT%+w+(+i%8U5lxH~By&jH^+3SsM*YEXA zs44_uybCxCkR^p2tV2rEjz(K+t#u{$*33BR!RRp4GLdzA(Cz?#jP0HDyPKvHCDgC2 zz?+Qmi|srm64x8pY*WpV#skv9D*eCG|115!(*Mi!|G*!*qxY}=AE^J&wN4fE|M|sA z|9=plBZeC08h#IVBX_is81+HJXwS_(Yu552Ik5 z9$7Wd?Xbal+*ycH@^`+R;|`Lm{ioO{fAVsUNx>Xq{sFZ9k|ymSt91#sT1475OfJYRWyM z0+TJ&>oFK^QAmzF?;dq5t=E`Vu^BVK8_8^1`0oJ)Q>HPk^!ovokoU@qN7gtV$vucx zEt=qtLmLp>yF|G$;My=kXw+&rz$2D$hN$O<6$ybOmgaKkVvmN|^t)R3|KUEB{$J_; zmHuDp|0VjrTv6Zm4&VU&e_=6y7Th|$09%4e|9>F*zc$REjZb3og@$jDu2eNJfvYpN zX_4U_FQ*$o-u)Ib{{0_`Z%XRn99d%fl) z#ElA-(lX;oUv#O#!Ali7V|x~bg<4E%ZJO!@wj(q&Zo^KZX#h}d!|W~=!gOgXpFu^k z4vAK_^H!!0+~8Y^xv=VTV5wPllkLW-sZ}VCS-MaWWuNW!Kqqw*HkRY*Pp!G;qhVUB zW+zFcszDE^rZnoS_BQOe2W{3>i7CJ#8{u;+^b&w}`;&y7i=rUnbyJj6Qly7Tu+#O? z)LzzEd6GkHlkZcOhi3BGFm~B_&@BL<;qdl_PdRJr!tAfkp0p!^d^K@EAfdWBGy#Y_ z<;kJvHaOb=N---Gb{o@rX&<&5^~OiJ8_5JV`E|QdlgvWXRqZ&r7?Ni+>aa$gMPeCw zu+^Vn@{rE5XwvD2XL2UfM23?2iLAa6rblQmXgJ#U*+F;K8cda21HE3R#8hb}C)R2- zVd=Tgx#GDG-ahAieo zF=GDg={$#X*ok3eXU512`oL24hBbXu4r12XRL_~U)G~=k>6}|9*5%wRlW;azrYWS! z%>O=@_-g-O?f zMzJb=WK~cVal|-@(wdpAvsSq#Yh&GE1R7;Z-B<^JoqHE%;mTu;B2Ndw~x8 zR4zAwph`ngMWvFFfgijd>8623C(VVf_C((~4b)yB(fK$f zps^3W2rGwk7E*Q+H}+{#pRoY&~@4 z!#@8zH9wb+|8TlJU)}$CAfNPXE@E^%CocpA4jUBQ?Mj^1dgD#UjSlZacNRVS?Ief} z0b6&76G_Pi+66IOluQ8OrJMx4U|@Uj6XREa^3sb?V~Q9$A%h&MCH{t1DZQp{$so%97K@TzXuA<^5oT_h9r)jk8$0t$rd69UZ}|M@ocI}upJ(Cc zEsh(gmC-pLhprwI26#AVnzflN)Ig<|-i;e*uf#UsEBxRdd0P#w8yRHu9_fJSm?Y-@ zd1-0rRtj+D2=`MnOt))Vu4TGg@Hb!HGGj=M6*Xem&B^W{yQuC=!4FRuOA%aatxFu; zwK)y196d4=(dd!lL;D}Gby{UR37#jN2mZYTn~AuNcXT+0^6*Dp6{0M{%%id3z&+J= zWH!V}Z_2aHv7HPy>K^aW40JYeeL8g+Bj*s~sxpGjc+pW5Vu|J`3MOI9cT>)qMn@U0q|PwpGzh?-)+ey1;EVC&WeAE zr#*CPgl+JX9>5U*cFjpLe8`mNCa{aho}eRr`TTDJ;^N4IC)Cx>1$I){8@dWLx2{sD z0pes5Eh5>)(KOxGL%u1K5GgtS@_3IjVjw^$yO*{%cH@{iy)a2{rcrtl*?#PyOLQ@B zgH-Wi`Y;HRvh;W(*oM@uo;L7Y;3e#8oI%qQ(e5sT+~r+P^H7oo6NG+hZZ16!#|u<) zV1W11ap5CDLn!{02eIgrs!nwKrpb?Vxvwky+jPXFPHErBnIB*TF9}lDWup!KC=GK( zCpc+Bl-Ue6!Pji$gVSVVHr5Jj340yl-(Dkmd{Bxu?Nil%s_gcjd4lg@HF zaOs|zipqtLEGSTBbwizy0T=;{V1p<;VdyPEku@XF7tJsT>zfuINt9Qc>WE!0C4|2` z^V71{HrLloYymW=acHg^k01&LKeNve7iUY3vL2s>`e4f;kYk-EzMOX{F(=f;?kfgW1JhA7CPN{0x3H5#vbxDhRC z)FeMZZRK$cEhuG~IM0)vgddG+?Xn%fDXxbt+0u~9UC=h0+)1K^C&5-uIB@a{v0J(b z^AlrQZ|lOnLq9n#5#VcpD0y{=--2<4;Wg}WD;+KZEnW2Jd~x4Qon$8}rtpdF95Bbx z9|m>Lx|(s_g!8uqU6`K|GIzjH(_y*73}p zklG9~IHamMLTs-XLbQ#AsyK9&{jakBRrbH?^TFEx__^4!J=l1kR{#z?|8M2&e{D1h zR`$OKbN>JFlan|)*?0Yu%-<4WUTa5cM+`pxF);vo@7XfN7+_M3AGnHJwRqN=1n_5z zLBTNtD2_1;OMWkQpJB6@moU#W@NAvx5sU~lzll$Q+tb6_*uX0Co`A-M{Gvd)gXq}a z!CKvxpucs@jz+OC7ED4G)%EU>cG*ZT3y8vM*Uo|AZk2yiTWy{?rPR-O~$x|SgR+9 zh1lf*I`A9IG*X{x-VX{tBCTob0zfkkq9hG~L-x0wH0BJHXq-IRmfF1ZK3IeFHFq83 z&eTLQq~r!8I~FP36v=XQUV{u&X4f16^)UOMk7yq^dqTp z7X?G}kk#;8n_DmF_)1*`z}=7gSB|Z`;GjC9Yu(gd1{-O>t}FbSHR4IX2=@;DwCB2!@54N--chxLywue5qsm(E?=r_f{9u+S5& z-Xh+?!M~ZpEAGg*L4#pgpj-KJkJ|Bu@+^3CX|dI|@bCOX3-q_Wc#8g7#^tk_imE5J z?*wC>)3;-GYC-1oPc1MyLAUTLKd7p0U~^q#7C@c=*t6W8iAGLNBUI3a@(L=sp6Ui| z$|0M!Ji&yn4^dm{XD>Q!!cU-alcNHJulF@G>!vb#(m`K}Lga4Q2~#*&;kNUE<3T{D z;xa?X_-vaFi|`-ZU7-H~(HT@~pV2We0;Z%U6s|X@$=VE55G{&=Ov12?JOn@@*0YjOo_v-kyI+JxxeqBzrhco^?v_yT2K{#ln696nUJ!~v_BYlEmSNX+2%B{_a z87zv{W)PM}fUP20HQa*Lc|8Fc#LTRSG%#W@p5$=Z!A?wfJ7=*fy8~PNc=NZ zAaS26tzw0=H20-fc_0yG9#4~(S`{Oy9VXGJNo40m#XxMa2;HC5P{dMdXBc`ai4{Xv z1SSFta`M6&HG$gu1+3-sqdPl0`=ou@yL9d|{>GWjU^3pm7+qo) zF0DT7Ci86d%%?|Q>ocETipSS>>{H(PlhN6$pA9bcx6dxdpE_mtPtKhjPA)H8=mXp< zizl7A%WQIW<=mwpcxZ6`$?fB(|0TS><3D`v@@M?*#g%8`r_O%*`jb2V^3-QxF}5$* zCm#yWoY_1(9$k9+;uUxJ@VO_SIR4N>4?n!~^r|z*9^P(Uc)EXjyPuq7-t{X_oC!zq z%4FrU{o_t@#@o8`_*0YBas1GkbE_-YZFg~N<6l_vSo_rF_@Rx+-gS59AM#IcZ=ZYY z+(X09KJ1=<=2Eh4pW5kfoLziu;yl@Z>{I8@U;XUTFns38B)M>T`$_NOO8>&A-O=F5 zh2v*N-7X!5^2I5)(3(!?q{FsOj@clj(0-^aH^mBrE)%fEf~p|BKjM z87F(bJ&vD7`~SS$A3J?Jbla%t^NfreK2_i@x5J4)kadntjd-iw9pFrd5%*;#-#+!| z1qmuL-t~f!s)|3jJ_=vlM7W4ka@{-{caFxUanz{uI3B&86*n64Op(7XP=1hz%~{k3 zJWiyL9|*Xnoqo|P7@AW&d3|G|cwGTq3k3izpje~Zr7xzb&dQX>@=6k+lb9pU|I$D% zmfQ4#Z5GuVmeZ*17aogX;x`faM%L3$c}gP1SR(pW@mQxkOqM$2RL%32?KF(73M4d? zUWtlwS+WhKf8p%W*ytEX<5?Uq7el<&k(P~ywmUS}j>hXaqIeO4dm0Yq#xRN66W`S? z3aB_!sT;B+yG{(iPu1gN-Xr6W^f?qw!=D_16p#WGVOo)SY4uEfnbn^Rhxm|*e%FAy zBmlON6ue91ILQ0N`=@%gmYPHBTYl_l zw9Id0)YYlGOUbe3Uz@QQsqRn(j)9mUb-)oAM*dt|&mgm=kG9Ltv^j49p!E!(o40t) zOpy!+VgkR9qeB&`8uMXFE&D@=60t~#o0ojbB#OyWm$_r`v^N+#;x>_J5~})aKw+MEOG-Lu zIT{(Vr`tSDzZu1!yy**X&@A#CO}^wb1^BvFp;5qIRud`dl?+aYefp>Qap+Q(q^f0X z+1^A1BTW5c$%Dgnn4ieVj~K9V`VgyQeb;Feqf2Iru5tfe?V5(n!yF#j5b_DFX$ypr z22h-e^ZlZ2+y~zIu#JU+**K2I^gxdJiie5p$cd8JeUHW3iba57`qpRR8@ezZGc<*# zH@4%1MaDMl))`5vu1O$FdP+!f*!gp-%a8I^RypBsZ(zK$NGuh~i#}?~wYIFfyfzRE zd^Aj7~prL;z8u{KyCVyU63r0AoP|l zUD6kN&+Aezu@52`U^?Gut~G}&8Eim79cm<*liBEj4#lp>s~TC47Y;+emyuaGsIgXo z%ISif!^Ou|8dDZvffJIFzltz+ELycd-+~g+Zg(z+T;S`NiSTjRtXTx3g@x6mVI$Aj z*s^Stx2P96(bDcCWa&m%0wTKjkzak&>rZh4@bP-Z*vYYipk?Y61u84^5fyo@@1HVBx(6E{{pG8dFJD|OTO`bD*Ox9_UNQ!D%s?~n zV>fZPSkH4e>8VRIv(K6;3w061<0{LRuV1}%>CDpdW2J4&OjE_VI&<#AMg7{?kyMCF^6(GR|J0Eu?#waNSGq*obTZKejkCuW9{dSS;c2qAccJ;m=n+7^9MlRLh7~@Hth;UM~8n{_d zyCRpgg2uGVa3&Ptxn%6Xi*Gcq`eo1J;N7>QAPoBRvcDXz)}^?dEU6ky^cUNbdYYVv zAx6YO7X!(-oEe-xaz#yN*7q_%0H$ninnrd_>QGWE7A0>i~Ly z|7UA)vGD%;sngZ_FAwB%i$Cn%TbLlCAIU}BW+;BZdi&0u)Y);=m5u2ZZ2X*j4o@6%N4r@n53W|1^qi?dz zN0%O5`tP^DZyLwbXVO(Awi9M+X%9be%~cX`-;O+7 z>FiFLut5pZDF?T#g9aT%Id}|(4GL7v4SPIhq|^_!RNr3B6t=3M%_gwf65leUmFwS5TgAx0V%sZn}q%~=Y z`0R?E9PSMrejU^HdTy9Ixg_MWNzj2_JPEO73gjrf3RLl_nbb7Y#;cn%cD(Bk$PPux zSTRLPTAes);@mU60w;Uhb~(kGjvEhxEppA}{Zvh*@m|IApC$c;a^$!r4&rRbiC7%# zhs#)kkKp6J>K*w*r7-hk;g53a9mO`ZmW$oZfOrqsX53)3TD2TWbB#=;_6#R+I(Rfn zdP`aX;^ll7aXb|1Q&;J_xsfDc+&KxN(QUe+JKSvs(a3Bx@os7@^H_3OEjPq^g8;sq zl$CT!QzyrE*u!25e?8atG14<6ScZ67awd#|Bp3u<*IZq`W?JO&MMb&`AaCZOQpm$) zlLHrbw(QjQW6-G-lTG0Wimy;R6z=2z&&>j&u}%r=*}fd=8uu8({$UtYa*=a2pWTun ziJ0R?7^$n?KsU1;tYYYOba03ZATI`y1WdQ9k#Ucx#o3es(i_4aH#8_<4 z&(F&Vl+8q!PmC6vL6uLbe#Goe?un$=IF1f@wGvQzJNcmnl2aks$9kr!l@5XIsg|To z2Q4wwevn%eP|}>lEW%gSm}Ba5KRYs6H=>Muk5eR>c-c8y*)p;dasM^oy-%@e0&qvI zCfR8dhiu>uLCEeyWTQyshUWdZL$@X7GB*^&nwWpRx!^5BT$H}8XJ(g8!fE0IRv$%@ z=E>Mz zu>9zAXGa9GkaI_GV&d%E!~rdoHlXv(p{NDjEL#py&X+EU`{^*cQj!_2(xy``s7=!{ zQV7PrAZYpKC<|=ypT>tC6R)!WRQ8|B{!`h1_P77MzZ0N?-hZ2$FUEg9UA_PI0PR0X zWDgkHYSi6p^kIPA@!(+lkVwSEjz0qNk6;thQ-lJl8FR7*B56`^M+h*blV%CxZ&A&} z@RS+QOR} zhqy=~{Lm#=Xo0E=6qB~3kCf^2>fe%Is!k#x}8sSEjVV1{-YlGW#{T>72UR z%y6Zo3B{WvjB0I?!N|Zn`*^mYAkyvX3U_<}eK}FugXc z_59$-jcZ|~%WPJqW1i++^fds*y|Gq;zx~!t4O`f}kJpQwuG!L&(r`bUUL2$?qgLt? zvtxF4!mrPD=O`m13i4L5%=FLQx-Cq$_h>46&sG{;pneeSb6=L5`?p`c=YCdpsE>*r z>VK&P>VJ*xsrpp@UzPo@vj0_|kEZ=E40~QM8nNiZwg0u-?b8MO-(uze{Xjm#0;dIk z#39E=(0RwN zSHm%GM$MyjJ074!Ys70ud9aFj9ljC1=}!y_$$xbq&-IxfK*muOq&qfi&=i&epeI_j zQBMKrn%FUuQvhSJ5hZvy1j-R%2?r6gL4D^_sLrE@4TES5iBUI2+A>neS}RZ%8dn;l zh-NQv5_M`k)TBTY@uXLX72f!#CPEZS!r{BbDMq0EYHiBS@SAld{nM~3_L|3bz~B;> zFpn3;OLEj_4TbZR092?;a$qTp(z!u*>C)hHTKC+TJXNF{K(4p~4axjMUQ`z~wHWPF zDy<|iQe#<-E&=K#u}687MPF^lk=67XX_O0${U}Y^VaN%;zbTuu3kGe17b8udc*soE zB2}F1>EgSJ-m5{Nw|&!}X`DNP%l}}a*P>(giFxA&#CvHq09@>lRYa(^- z#KW148`h2H4fDp$8@Fz(-B`a-ztOmH;>PhC$8MZlKQ@a&JK{9xaf@EDX;G{|ex@%& zKDI1${*^x(^ozdmaN{z)p`s)X#PHHM!WdJ0jySXjOg&_5@}ZlZoyAAJje+eOeS8=^ z=A*o}ZUOx;)C{nY9qk(Q)Q$?<4pq{??GphjW8M}hJT4-3mm<-a25}#NA`t)m!CPBz zXc6TpI4*_C49Ks(|CBmUlaYY+7VAMtOU3B(r48S_r8k_$=-deen|2&CO00 z1t^Lexy?oiF1L!vC=OC6S+lq~6Y!WbtDUMqwv0eIbbV*Z^PUhv6Gd?2I2sU&k^Wpj zxIuA7SrhAE_)NvDviOXnwi$psz`O#e*TW5tt|YbNWnvIZ7S?JI4qI_Ui!r|yuc;}a z1VN*7LM7_+9dD`VK%c-!+ybf{l@Jsg+M*|+_#@rv0X3$wE!(SSDmM1@&)E`@aHuGa zB39N|R`$tAgN^294dO2$j&X7e}^%jqY> zi1?gvYqa)|lgag~MUj~nL6{|CQM%nxvFq<~f{1hru*H251CEg^;W zU+DhlR<{poRr~N)vyZ|m_Cc;*_pxS_tQhyVSg1uJ4b=VtuMjd!@AT#2k{b)ww>|ip zLr!B4ZuP|CcsC8pDo-%%f0~`%nsn(gR+%o`Uo&vD+*W5@yQL;YhFXu@M#Ff-XchR< zRgvg0q3X(Ddw*!Dx zClF}PX0Lgbsa+8KZdmD9^HS-U=@s(Dv{E`6a`-ixg1QFN&&u$ZE;@+Q)7(O*bzw)K z8nto`_~e}Se%6!$iW_Q-23`;|haR=E)$Cikg69bRq9t4jcXzkr#vu=?1an^4rW%}%S0&R%BU?3lrjTqex{e7E}p z`Y|zYfA2N;?Z!OMF*{h3s+vx~3-Ris5&eMW2>x4IhBkE5hI#ZRjM=TDs6NCv6DG4q zZ`!wxBG21~KN;E6{-aE z4isdRs9k*e21*imdMskVoj##%AIme(L#?VG1e>PSYR}_S7;Hp1@pjO`8-Sr&(j{t* zcg>D~E@>9GsJDE59McT)(#3J9G8Q+3QQI7q47csqx2sTHRLd6OTlw4Sezwc#Q%bX8jSp`SXdJlz!_I z0AG}VvVCZo7sYQqdQ|BXH}N|aCE{}D>?t7UQk_iwnb&VQ=&pX&UlI{zs<|Cu;$5PJ6R`*i?2@c!q* zeBu0Ov3mdaLHd8C&-V_8;jSEX`XfERjLXb%?Q4`)D4OvRi&=thD-^OFFMC>a@h+Rf z-Du*T>nr~lWH%1J)C^|C!6X_m;ZAQ)65>t%q^7t--~~aLNeKL24CPY4ds08Q@M%3J zo{{6b^TbYA{z*t`E|>W9#7(mI;DrB55RyYt5XvNpAa04pS7n3abj?B*sN#!2{F^f5 zjUDx}AZ`u`t@Ne{5}T*h`5$@9L(kCYmy$tskbg=0Vms4 zif9doy&$BG-c57MH#>;GWtg$}daJ;S+T<2|L^~l#_^wtuoy9epb0c!}6JC*&CR*=` zzZ9HDHlzW{&ptD>Co!W3X7QC$5J}CDMZ?}8nBWaL>KHdG)rHtWxiV&njqg#_8*8Q` zF1wlQl6rVAnp=P5%cgQrQGKVJl7@^EMvT^+xh7`^DiV8)h4s1S+}vW?BanO}AV7~$ z=kI`K$}VU{aYl04Q`h8>J`<#*(5^GXFU2lmfU^#RrP4%^6eWo{O?pW&>oBk2hp3n- z6$B;srHeCHmm!Ck;wJB4*|aX5!K!P>E>PUeO^*~zab$ALhP7qysLY4bJ>L3z>mN;r z_ufH1o78R1{v@70gg6@xAg_AuwRkrfx$IEnJJe|2M;|ln9W%CBpVP-hETt(_k-(jN z`6|iesul+ELFkJ?rAmhxQ@tlt3EHrXhV`r$;q3BHrlJi-zMBa|f5 z!Y`_-=@dzOmbvP#O42ZN@&L<9YC;Bps%e&%t&9Pc|955otL%T(=O4`em&WP(aPNQ4 z&7V3|y#HDGe?O295C66Sng+Rx2~}*B5~O7=4bN8R8RdRzcvn=Ym-L1r=~cW!Gke)h zYR3FkM@Cl_p>momoDT6%nz*@*Z$@qlr8b)Qsj93i!4hcwm<9~+I;OxhD*0Be|JC|m zt^d_$zxAK2q0w${;P=@3dIS1^`(F#E+lBMLQ*+h&|3E(a{V#ODK`(N;0>;Qw=cZAc zT^f!yY~RI)k7qlx_rCY{z1Lp6{l-hP7Up?qIJ_9+pBQrYe*W{lXJ5bb&d>IK_XAO& z4|Q!(@%3--{pt66ufP57Ti+MCBNpkoU;FajbH7r#W5`73>J@t&V4*wje0A@QAKm@> zFK>S%E#x~v6k+b(Z=b*S`ELOfkvGsVsQ>w2@4oX6FCUE-Lzc`EQ=WP4QQ_B(Ix{p>$3|gdr8Y0xsx&W=e2JG#AkOf|L%*wyz{MJ+~ew(#HMFB-#X2jq`RK@CNpN{1!BS`_1p%`Tf7kBA}DI zJon!BzjXKSzoztMd)$W!P2K$QfA0PG+k3D5Qf9Cy2x#oT`?-5>{Q@e94BzJbj(qZW zN$h1iWX2OLa%jl@_Jh55Ub_1q-{e`q{J^uu(vVNl>x{?FqmlWqdq4aUFSTJKb>98U zSD>m$aXo}~`{lPJCogYAAY6d^bUE_MPw&3=BT3KYU=YMceUmZtacpSi?)=9$;6E72 z|9<;9pbxO~f4}{_CVrL!FBtdv^!e7$-+lWRz>ku`%fUE^0s#di3h=K;2hm~F>CcJB zTtD8hW4nP6p8NjoAH4!C-TSw1Kr?A!l_!#dO}#5`eQWR87cw}3C^fDF**I2!JqVS1 zfBh;DUld4~arXcF+{+iY0yko!o6r9R#ur#nl93n_XubD?zwx9fuoK=>Xzp(>fviwH z`}*_1fFLJ%MgR+?=-kh~f9H)iVUo$7owH-uT)JZn@&mwL+57VkZ@u{p+{?@KurG3t=I@QEFuS1I8i9k)*T@<1$O4AOzXQ1s*Q| zQ}f=x{_Wkjzps}B64*kjeFqjuSk(YeDaOu649i>#yj`EgZdwdPAj;s^-v$vS`*p$Y zbMf%ypWOY{e~KhIfxh&dMu7_~h(;XRPu|AG?CZ~`S<%?`cd^Plf0CWLupvb0mwzIK z=mobAA_UYg9DpC-;tDjp_wwg>X2ci~X7A50-hKAVGEK-SXcOi-0Fuk`1>hd$1(T4b z3$V{Sf5P?X<=@HyfdXTi8*geU2^-s77x>B>FkfKRldI%~35)!g?NVER0ScwHkljw4 zapTyYU`@$yK$|;%{fbsb&lJhKf(DrZdK5&6@mPQ%v1af6;^o_ae$4GmqLX zO|{#9_!HnMtD(CCao_KM4NL*3%NhIV#D|52M&QNg?mYjp6tx!zOfFbodR_MDqR-cg zy|28!Cnh%}1(I2Ka+XalMtlu^`^&dqc}b=`EmB^1UW$~9k!%j;7piy9a@1koFe4Zm zm$~u+RR8?16gF{WGh930{p-)~KL5MD=LG!3_GmD^_%*H{QR=5aY2l&;npON^1@CC|#9mifn@sdp|+uhH7{`QMM z6e$T;l3;-De(?=WNxH;9N2h6zixB8|zxkC&kAq|bS2s{tK5hy8Z3fZhz+&dq4fnotOVA$|M^Tu~h&;eedNz?){bz z(WS`_1BGT1jffln{q|3PyZ4h_;ZctFN5S6+3|qH736~CL2U_0E|?A~JMI>XMPsjj``y3( zbnj=sk$IvuAeKx?L_2ps_vX8Q{jNxjK;7^>ZE1l5AXfmKwz^!#D15&A^n?jazxH)p zWuJW^Eij32-QWZM+7FN$U;Vbmth|6r3qH@k{l*t0w_dh)gCr5KVPZ(eyUcdn0ckt` z{!5%g-}*UEb_Y=^E?)bR&_6N~<8t6R!4_fjjb99mL#4+IHi8K+4=B@)=y`=Z=z0U$%KvrbPJKuO2)%@;4_^?LQe2$K3tKt9QTtMOfK*252Nnfcu6()cD!=QC@%ji~TY~s5l+V6Y^)!W-|{8~1CJs3-I3tATnEX7>cVcW~MgCK2RdQ0k9*C#Q| zao;%49LD`DY2zw3{&zqB?R#I~gRx?}egYeA46!LTbAR{^j@Ju+*YY28lO#TIg>AWF z#|In$yKJ>cFlW)(sql zZ@#FM$9MnydD+N0&sed&EwKf`6(0odw|)UM7L9NVRMz&i0>l7O?D_$31gZGtAHZx7 zxq-17jL8MnxCW&LE@`LOgP)OM2vB+Xw}1-t24sk+G;v&GDYChMflUc+Szyk*@cg@P z3wogZ<09qN9|8NK-9QZY3fmoQFwbLRA9we;7gd&v($ToY0zQFAIN~z>y)TG-P#MF3 zwwpLJ0O${26+k1g0f6R!DyibI;(5 z`}xlyBfj)YZGx--kMbn|)DLa(Bt1&l9*9j}k`{-RklVI^q<6mdUvloOM6Pkk_BRDK zC{3Sz9cBv8oNx{B?h9X&7Nr$f9MM^gr(*H1a5Z#g8!bCh=)MNB4OAF8byv1wV$fXs z#*g-%{j*4OlV`Y~7a2e08o|o0khfoZSrfLa_ET;u6kkQXE~|R3+Ws?Q+Cma6RvJxL zZFiej{Mzfc|ME_87qPlwE9BpNT`Hif&{x;7xkvPB(6`B;0c;0q7%t+V=X{e^AG8ee zMU%GhCqO?WB_X?!Uk$iH1S-hV!6xI<95hH=C4@j-go{k z=_=|x1{2E(#>7f*6-#dCDh3ugE-(xlTiSYVoU5ualJv(2<`f_(nN9h4?ohBvCq2WzTMv@ ziT3-S^L78;kAE?1)Na*k14ic?Tb)gcPqS%lVYm@C7&KvI#&NxI%iv+0>CrI1Cr`uZ z4!`(;z6a>#39u-xhD@fw#XFi~!j1|EEQ+L{2m6CYgzgh`b^c$S|5xY#)#rVm|HoUs zv#_gppWc55zyHuWl{^1$onBZxU7i0wh>vji!#FGh^#lZrym0-yPRl~MyPxGS&mFrM zzF*xM8=@;;8k1h+Q?NtvU0pK;H4D<`)Fm{`)Ni3_K7I1Cj?Z?Iw15g)gn?{q2LZ~P z79L$~FtuPjD@hPuOvJWTyGiI#fF)S@*9`N9Z(8P!$Xus-^z?y@X+*IpF^Js|{`ok< zHt_NbzhbE3+j%mYPNw$_9zs^@bo%``-9c92DP?%Tl_zlGm6KgOT%ia4d&o@3q6>Y{ z#zT}Ak3sN<9rg_nIOHk74b32AzIrIbGNZm}X*IR*eaItFjWY}Hz0MseGR$+d;6qrt zNn@(6W0l`UgeK)J+xN^Z>Z#Ew?W=%J5rvafp`AF=;#RP@90Zvo*nc|ia5&4uxF3&a81X(XWBp9yk{N)|8$gaw(DS3n?CHMzOse*`d5Q zbbV#1P-3v4a+eqKxf43XzIav^8L{Nz*;?(0{N0a&#B=4oYv+1rKC^mmg`)bTX)CMOA*HRQd}`(D6-b(wx;E-A=Qx+kfRZsA;NkeLfiu7e z?ZGB0$|sD8FTe9}-T3mwU^Av-P?ygYUTNC}{Sfpq2YS27cFov8It-%SFfnXz6ae^* zF$O6Tl^_?&KuJ-P4Vb&dB1q$Im6!#Hxg%eLA2Z+KWg>SA10qnN6enX6BeI-QsEZ8) z&kMF=BZwT_h|P6)F&>Ml-g?OB?vP9j$J@u}8w9gq=yhVOv)D8+B6!4N(H{!VD2<&w z12IZ$Wdp^|3nXhZe7w~7%t5DE1|W45Sk91LH}DZb%&`nmhe0(?Y}bp8zIkJ3Zq6h+ zaGGMdBoG`yHP%`WVQgTa1dN+I+@W}$6Jkbyj02d^9q zcYA7b@{y$ZBSMC_z@K3=Wy}oW!5k?g)?Dw+m6L?%{u#&c> zQ-F4YaA;iRN~iNgn8xN)gyQoT5J#m$gc>447-Y$2iUNh^DW{02s?UB@z?yOit&3&R zIEL%zapQ)F*iHhcKG=xrNyCy;UCXClOlME6tI!KojEfFJ6u1-mKw<7M22}s(+@cd7 zott-JignMm63bA!$IK7|bPw?{k@n)L7~@5SPK!Sf1Da8jsAskwicMG!TX`Wx*gEZ8 zaiBC35)kD1I7=vWgak**VmLDU@DAT896lS zlrgv7PaWNDscc zDvSSy$gCV9Jq5@cnQyFwm+wVccTo;wJZ^Yi`Tx$?z}e@L;eoxRG7>S5mi;5JX2_J( z^o^MwDEi)u&R^UB(0B$+d>gC-%GLm*9QsaJKIL}+ov0f1{_kDw@~fW^m@}B<8w=ra zmm>k;O}k90y;ZDWm2w*>EnQ5ZJwA+1pbko>gB=S->^9d)zjlckKfOb-nGNeJ-hB0T z=JnLGP9w&b<*eJ3ZXvyCV{jwL^ER zCsfp!e8Tl4^k1oL3g}K>$+@&GVgHfGon<;NS!Zof`29aR-ja}bUTsaQK}+J^t@o)V zm90+~586*5T2HDN4xNe(ma!iD2`)=xJDcaAMGZ2ox_-9|DRggnhXar16rFdRmd zw+*uUi@BKpIGLX5yx{5W5O-a19k1a=vcU;BmK_GpyxiN-Q$|)r;4p=mMOFb)eVS*< z>2&C%*El%9@tE^qPaM*W!x~HVB9vuWXoVjZR+F+E2s%}p)4fwQ$+YXTx#o842p|oq z4(g{ZR=-^E*p)TzI~D*mU$qN>%306_z$-&5w#UsWynoORfNwiQL~Bx&8dL-X7)vW^Qf1IS;6Y)mN7dL^#wQ?EX--zMu?C8W_3-$0IabNg}YO+Lzh} zLZ{5SKK0xI-pad+B9t{ zR~|e2OBWOY;DNQab!4C!{b825MV$?Bp8S#=vU`M_kk;)Wqsz;)N-_ppHj2tD4VowTaeWTc5p{KIgTjU{0| zV}U4WYN6ZHuc<=Td(6BtjuQpSA{BIdKeKZkAKE{GL``xPgH5;?a?|P}OpRLB$kUhV zX;2_DCh7o@XqIEGm?oMOWrYf{ntL7;2Vo^8QEpv@TB2vM16{)D9mr**5S;g-)IfS} zGZx>}wXY>dq0{{<+ckHzrgb#TOP%x0Hf)f;MikQ2QyN%9(&s)aVq0Y?F=bSZIlnjH z*M4TEQxcaUgf$=Y==0#)>yuQ7pvwuVk&fl4Y=kmrTbQ-lfQ(wr*td<~Wu+ZZy5SO# zdZ=$X9i~l(33AcToERODrH&iwV7ZKqEGIbHR!dvssD)Lj-HdEx^k!owFBaTdtM5zO zbZFHl(|2oA6p>5i!@bL`^CQG-nrH$T2QJEVorsAM6{~F`@z|#=ViCnKba0>%C=#Pm zJM6pEM7%W0M95axP z|K+_|EZ}enz0qBosWUje*7z94H%U_PvrJn6)~jTt6!p_Oot~@$rvASDb>rXSXhN?u zSQMhJK*Z4*=MN-N14n8tw&d|;nAysITi9VzJ`uD)L)|W(&{kRkB1*wy;05;bIl~VK(*17E-hMHu7(JH&0 zjzAqbh9xsczzkskgTtM6H(C+inI~=@(QeHPM}0G`OIqHe(%U;N24P1}Bg;9e$a>-< zf7RhHlt&s2bj|3?$YkIwd1EttDbmB_PHFwbqj3IiwevmLW1(6}c7^k27zNrgLz&S2 zd~lpPdyPEq3|cxKMd-l%z;;IK4Y>G|wM0nIGXK=_%Lo;2jaxfMD%?hlMi#KYC0E+( z0GbbjpBFy?!q~4HQ6E6#)HkQbHlX&(@4r)s6W<#D{pkNVTs{?T1~BvPh~FJ`@8S|c zt^qJG6j~o%1Ql|uHGcjFcii{?YgCbCKyOa|YXkXks2-Js$$KP7p{wv-`)WU?=Q z21%8p=j^1Mz%+HEtu_zD459{F4+=na7QKX#)8a;xm+#;C4!^3{BTfnvV^?VEmI=n3 zpwGwscq18tpE1F9)(SQkGCkxQdl>$|ovC^zi0Fim%js2+uP*N)1Qe0nPjO0|i{8#7 zvP?DmjB-gmFA#3R62>}NVGj=d}oF@Z8poEi25S)o@_XoV3)D=(mYTK9D78yeCi4TK{gQ zJ!RDA3F9Q57Cx2W3&(mmj3Kgm9xb4rxbg@OQ^vbU@N97gUAX|8i(ICG-p$(R%Un87 zwThsiE8@54_oM$cb4J*mOO{Ywd^!)4L_M`z_|{th(D(rSm)WrXO&C)10L%yE(f2`E zhKY#@^8r{)0n7$H0@m+HB<;6ApAd)y?#X)!-619-v<0Lzv&G^lU!?-6JgU`o^z_en zioq9MzJ{*)1Tg^Ik07`xXK_-2@8*!-^|wm;zGQCpcodz*JVoEMs_5AJXv&$OUJ$^K z&|;l$jA>qzrwn=*Ii22iPpl{-PD})Y-Hw*2vX0V$a>I*HthHo zMw}_9H>typQXz&*G$#vtB2Mcma(3ISYLbyI;3nB{*S9Y)a?)U<>87y~ywvUHRXi(9 z)Oy}GR=he4PRsnqfgFJmq-Sr~i#Z;`6@**nG9@C}NwH05_*bX79+5QDPB+LAz(@zEU+?j`Ykl!tn0TYkC!^#m7-i{=?dbC zCXa{_GF`GtL}S*R=TQ-!Oa4%ocH5Gn&ihwG6%`(GqA2 zo>zil;EcqM8VQs@AAg{hs33I$ba0Q#+WcoA4{KKI><~wdWmW=%4r*yR(G<8Rwcc)y z4xCw3$8;N_(d62EJtZ+qvqjQ20=#3p@YjS>mam7WU*8NvO|Gob^p&>BWIYE*_1#*e z33noIn|Wg{{)^?V-s;{x0kI)1IV?&_45%co9eoD}v=5DP*$G=}pD=Y$PMZ9ZDherB1Q$)lj? z21zs=e{tHh0E&Y#&M}B~Fz&3}+=vgX7o$k<Wr7|Wg^++~r0^SUJplbZc{dUyj zj!?%wruI3?`%U_rG+uNg+aZ3i0S02*qcAjLv{ANEIBU7Gsb@Jyt3ol1a*I@c+Qv0@ zI_92u)Kd($0+^#*?@|jp(~qO)#0+uNM<5j%nRlm$WKZT{P;tm{AEk6PUkWxeV&|N~ z%t}<-1qT>RxsZrBmNN=|8sIEowY`GPGvk% z${LZ%MJ^^TzZhX*1Eux^3f9-VJM;EB7)@Iz-Yh2`1yoNXpBfSXkJ)+*`;U(Ktv$jp zLYO*`w5}UgTBXmV*9#q34+@*~BH+0sTfxoS!aawJKxI>WwAQ%gF}I|*u?79AVIxe= z=D1-e&|c7W>6yll5ZGfdt}G>6Tz z1s=FWZ;}}_OrGMs?Y~Z)@{(<>nX6jneq%NABK>A$kBaqzmh1tQOW>0q}qWlGrOqS{A!MnykeU5Xu2hZDqngkE&>oMa~=W_~WSh zV7wHkS8eMV;Swf?@`_{Wbleir2MdDM4^=F3Smf0ve>+opJ*Pc~L$G@L$+P4JqFTQ~ zv;p6$54P^c{Jdiv>s>|Bi^;v;w4s(Zb!u4x=n!wK+)4KjmIhN2H+3|4NvLmn-AG>6d$Zo1Mtvi9a!$4uUoHc})Omw89b7r86 zqj$}g9Q|%UKQ&6@yLg2!_er_>`uGj?jS>fGU9D_N)sRtcaN(I67+|5Y_S(cd*ks%v z+S#+Hxp`fUdl}Xs&!r__A3jKY%>ilYte65L?7CPS{F$ZlSal#^QJL~qA@L@?@ddOa zS(o-wfff@-NC1E8o7{}zoN~lQP3y$_m7P6DSVE^A)Vx_&l4|HV7B48;p{ijZzWg;+ z9kTuWm8Auny76N9r`vC_s_Wjnu7JIjJZwUR)7SHqMTI9V1OMxhZ6^es7{|`cZPZxf z)5A7S^EA|tjUp&9tkaNPHhcJVz2Gbi+Rzp+=6;LE z7j}TzOJCtab_k;T0GH++(&EP*YR(#!x)xg^%pe3~Y2kT0@rX2!NT+CQfIN-En(-=e zc3woHmX?Q?k5Kq1in$W|)A%c!yNCSgx)$hoeJK}oEf*Ic={AfK4>n?4JGzNkfl2!d zMr${^g7>M|(geZ829Ma6)Y^MtVqeSeKdm_Mn(DSJ&nJum6saoD1kSlre>^f~qIWMg z-dSg;(2sy!TXK^8#2y&@&hkcJo(}08udZ2^tl-ULvpAF2E^ ztxIfP;>ij9wKx3Xr5QZ8)W2}!MUN1zf;+t?#Kn1Un)Wa+zn&uO zUlWI|as!#E>)tf^zvV($w!oB;P=uxoM`7G*v(*oSw>ZdSTl4BwwqI7goi7#ex@FUq8fv zSH)Zun!i0UJuO6CPTg0PcS7BL--Xgb)t4p3I;P>b#8K+zh_h%oJdMkoRP>R4~}!NHpF$s|gliK8>=N#_Us60Z<+CDG>5L9+NxzqkZVZcEO6hRc|AIHUr<* zAXJ!>j=9CO?T=S=D%?i5mj?ctI5o>i7Qc|2=b%Fo6Gar$?ZTUo>L z#LCb8k@myS3{Wc&)&wXruwi_>+D2Uc7UV<((5?YCYbL${L;{l0L7;v7Jzn~801U_= z`PAYvkPu3;CQ>jeuW16?4N{89IL!rIiRh=UA6gdHAjib&g=mZtnw+c*kHg_xet%vX8>&THbcI3CE&n`g#N(D(HsLHB4;9x1Q!pBkCEt(%?-1<4b0VPA zZJ5&71mXQqcbMr{qz=6wc19yS9Ei<|lLx#pb;YEE;Rh_I?^p8U|2pPKz#8H+zb7XB z3Dh@Nx!U0j$-$CGSuKY3p+X`Ga$y{%HK z77yCtOlS#g0_w2`3CP?4$g}f`5hHKpJ}`4q6ko}1(L#vYR3|$a(TZP%wz>)Iuvg1@(i&2_p_H^?z@(6;iad)&)-y9 zJ0E)^BJv#`MvMinu>Mn?d2r|jO0|G!+UPKz1k6a+j^DfuzrzcsMHYuuJs(Q+$%6&2 zgIBLHSvvTCldxa=%Jm&P zC|}i+M>Vx$^W|-_#NsSb@Z_bHww4yRmKDz`N*ic8ur-}ZTVmOG`+;V4ze-~=iJNqY zGxm=GX~oyA5Rbk?=*eRifnJ>YvMb!;;CD|nQ(=iezv3u^P1?*Cu|a;~ST7$mi?}qj z%KhrhGwQJ#2VKkXmVZ3XBXc86x1m@XyJGTgK;RJ9tb@3_X)r`tc%I?IB2+o zF--XN^1D@hKEbjqWS2=)m?>_Co{KVerg=XP^|m&^fcJtBHMAg?Ey>X#Z9PGJcO-zZ zhwGUE(1I*yT1v}e!qN9bcSdec(7o?!Oz2F}f7|QrIOquHVKN9Pg}^;?lP+siEnm02 z|H%Ec(3t5z0Z-M1kzja?$rm0iAZa4vC}_k-rV_%Ja|FuK-p}5-UZ|4IdDO2!b=_uP z>jSxNS*DKV!gnbw#og3zGs0x36C3Sp%)#Y!dQtf@YW?ldfW#!0y43k1wkJPwRTu{n zCGu}IXRDd5Cygee(qFxd^c6|CF2sA}!#+WUazoZFfEpZL098(IHi-Lpp!YQyxLe78 z7#@*hl&&HA8%$3)jOaUGDi7Tu;C?^3vx}XwA^+8jLIIyqL|2K|X(&I)2e2E@iBYki(6DTtIT_paJh~BT|zuU5wd<=P(>u@m)2T0jwjjTbTeI zX{cLJj^Ko6Bx`|-aq2~uh}G8A#8t}May0MSW{oO0A&lNu^NC3{2i5P1V1bx3p4$1B zJQ%DF4~kV)lfp^XFft6Rib6*s)P&1CmNA-^ev`(T^u17JQU92gbTgL~ zGL6BJAPT-T0k_$N54x(EY^A3vAgEci-BGj_jQX`E83f`7Ne+?Y*bZC8Pn$0Ijij9( zTR`6X1~7L+jdxY+^C)%smBb5khXYsXbI2;iL;qOgMQRQ;5@a?-8dZ8goK%_PsxVsb zzuJBO4Fp{yvV1~r5nuNZ3)GKZx6ETvtwyD%XS%+IY=gGTMgdRxf+k||ubU@O2~l5B zFnfZt687ZddXSJ-t`XE=LJXGUJdRAUwV=|=pYl){Vkfr8?#Xf}2HGRpj$&}M!&fQn zWku8nARfo}1|v6EUX3v0KokElG;tH#CW2~HCe~A|B*2CqxI<&c&ALan0a|V7AsDhOM*fS$h{GJL{R{RYDwG$&JP#Q~4 z-d=tl!^sS6P#D5S{A7s4gkmlCwcFxuBqb@lho!m_E=&%pwJZ&O7U6>AZdmQV#Y~eN z(F$uKfMAqjp}ig+Ly?P*Y~&a$PMX@Eci-qEnj>P_EW$g`nz|&kq=&(2D1?1VV!d|Y zKndK(yy4xNaXS9J1OpqqVrN&8q}UygsAJDgoqPgipvN7yUwqNXaf349d7&CjC*y7H ztjt3P@rZA-()M$$c1zV<X&HGd1e;i4a0)XZCYeHj#jB|5?U)M1;_wApJFb_ zN;6iBJyW9rrME#|<5Y0@P0{sR#Yf=kArJXM>i}Wr=8_Te9ngHbQTA}tJH-ailxGnf++5#&(g8l0+oIVIUW$+d-{|RaS{1Z^~FM4@AvxHr4{a!ElD)_=AxW#cj`|C#1J(A zzj1Is+Iupe$g@^uYjq10eTYKVljpaIkSFX434j~t*9rAoRR%&RmNY*#8c=hw(pov1 z|9|pKKjlXlyaOZ>gZb~|zQQ~Kr>cMURr>)pcd9=CzwkJh>Hm;UhFFGH(MRxHzHcul zV(?}SUh$#yj|vqjM?jC_Y!W(;y=;d{G5uUzpKvrJS;Zqal!uHBJNt}Q$nMALMLW%i zAI^ZG@AS5t!})k#o`{5-6Eqr3qiiXo7wkeCb`hqtP*!SYrfsuY<J3KW4vNvq^`N*)E4bhVLCjM`>B8J%8GudSq$ z;|46|bpN(@Rc{*=cKw6r?y!T>JI_}0wMB$4Eeph9_}rHDUf}q-OkdU{xrU8{fWzVi zBz_u7VS94XRW6RlsIX|}*w7kvl9d#nd)R3eY1>#dG0)6bJJ~6TY$Gsm$#yI z#sg434T+$`mbn#7y=r^S;l^N|)LBy~j> zM>L!+1@ANrBZ8Wk4Gs^d2T7u=o6kMyrIckxSt67 z=vILe#KgtL%vVs<>wa`CT}7&@*?d#7+^8@ZQidUkKau~~6j9m9*rL8E;kbJ1D80^M zw_UqM{n7bik-Cjl&ft zNbdT&r`#E(_C&_XuSkgmpb&M}&0*+^NTXBj6DB828Dx{dX|a@pObMOG()wS4mQC%6 zw{n7mVvN%d2W*V9_aV~u2(-=BSm@AbY#65rO1OyMA|VH@zi0te`#7KR4QwMI)|Hjx zMo}`Rv3R*ZxkyIiD2jG<7bVlJY#IqXS#)-sM?6)$TapZ?IW?t3g%I5wNrMJVWyiat zrlJ?^Z?m^L@yr})Rdz=9?NxXR=Ey0#2X&{?LHIy#|{GX;KwI`BGpS_s? zjR(NRY1f+{W8jxN4#1&Er3Y{`08^*`0GLStoSHp&piwBFF2JH*`INLlm-Uajx3m)Q zZ}qTxeC>#G;WxSVCGqyY1Bl&t_t!J0i!Tx=1MH)Z`t=3U8Uvy*^%lByMtqUh6}P3;*z9+>S3^NvPp zSY};pe8`fTXkU5XNY?YD6*xH?c^egoIms5mt%QcZEH#j%7*J^%&|o~ifcEza>mmI$ zHcUCkIJghQhhE$1)(@lEHw0&TP~e=sRIrj8V%ZP{|OsQTnir*HSf3vr{YA^gN?Mg z>s4H#A`K4Uf$m2O+A|dow(ke88;mp zd7biy!i7FiE|Hki*c)wBgA5g7Su)fLKEsH{lc8N`cLO8IOYa=m-7|D}zZ8j*IHJ)o z;Y`F*eW?~%jh)s%x4WATgtfb3s8G+F%ziDy7Bm75gQqH?c1<@W?!hkovWjju{Tx_ zP_+ME%-}J#Zt*P9%%X|w3JqLnapA=aTOv8^Su5$z8`|zRPbt-ln1|-6^ zA7&e9gPSgrkv?)ty8?cKBmi}ItAF|~+EPROP3HtNjs&&RPpJ^(=&@~HT$hZLv2YbOeFKQ-D0-G4i*~!#?+feM&%mz`jg09u`bnE zVcQ4+QsWHvZJ3$nk>z6CNkJjql}YLhicoZpV?7nVUFW2A$m6&jBLnx+m71sftBZ9}VEY z|2GW8T_6VMPGh29h~?VLax65&u*dWdpGmRS2?%8Mn8ub#e4RVKmer!^X(kY=W%_`c zTTtb%_MqKPuV@+w8#66f_kSe|;j}_2EVeKWlml3tDP&&NaV;8NQQK5IWDQ?L9VKBQ zCwVH$?W+e(!t{ldACWQ8!&0)Irew8NdM#6#MB}k)1g(%(P^u;J2F-M%Z zO1zMrye$n+CO6u7qDa+x{cs%dRs5&aQxbf4(=+OavpRT)C#tLIegLb+tvE$8n4jMXz@|2b1lsamr zp9pXk@1nNM zZFni2JsE$$SBj6V#6O|Gde;a>#F5d(_ne?aR%F=Li4L?S+mTjE#Yk4)zmE)gfHsJB zNOye7eixX*&7S7!csh3XA?X`@s-)tu)Wi+y$$$FJEXrYY*U60~jyLo;8mR~RIY#Cp z7Z2!Zt9A2o44B{0h(w;?#Y#|ERg=gslA^MS&0@7$2lh~Fs)htJ!4j*=n`UNNFTsp4 zDNWbay!EmJT2BBgpSziWs<&PhK9nSsZP^rnwu6J)#J$0_&CjZVhhsqI$#cXijamoX zH;p7*Bm8k1{jIqtSFZTx&mtD{d7SsbA0E-!E41FQJoS^%)=iSZM#?g0BT=`vGop;% zb&Y`34s6j_XT!R+PArqZHNUs9>qgCUZISp=6>hIRzQMMoHfPx)l?N@BFZEQd?Oy|% z8iVF+#_qeLexKFIfuO#35hfm~%#|7+f3`L!^!wHoUc1-&mhSqrP?E*UqY9F)-?{K* zLp@Ms-WYRf50|a89oLr^_Okr?Zr3zeXm35fZ#pp#^+HB;! z?9jqmRba*SAkEJ;cQhctOhf|Uc&bAwFN5ccYKMh>`3~=kJ;{vzc-r5+Yv4HgP3Jao ziCWoZ&YfWlmAWv{&hJ+P^G6l7$Y@+xO|}GgcShjf1ra?K@yI| z7w|tYR>GF}x>UA1(eOOm;X+oK^4(hW_x#P~l6bL&7~4gII&SvxxE=g(E*J z!n#5Owc}+4?ZbiYkxa2*&6q}W5aG&Ul|mk)AuJT6&FC08nK8Z2(<4AOEdeAAxO z%`m;e>DlWfwS&K|=fTrEu#v35V{W%fYSh}r*6w22d0g*}cv(1`d}XfQae(?AZDZ}6 z#oEePk0(%>sfiVVRO24hib*>$Wue@E$56fhH15a^T}H#?Qm?-vX*g8mh-i%-Vp}jG z#aF-6dR~Ke2dM!QS1~Eu)_Z83K{6cM6Z=G1Ce5Y3I!c!@O;!7HK8fK-Q7@`IUnV9c zNA98?i9JJ3YeFm#y;Owvp;CcT3-R)Q$c#;yTn<)oT^LM z)7~d-z-jieAKX6S^#m^Wh!U472se5n*O-H!*t3vXkPvRa+}%lt*VTvAZZZ)aGL4T61*hInvB<*A7aOhQ~`T!`g-#zQsu?_!@< z(jrt0BYoT)z${4$d~hAjIuTo&=}X<8wQ_=WN*_G;01k}z*3KKzwh zb2h+9SxxrQGm7A8GQFhUWKXL4j=$qF^7Yid*}ba%{?c3xL4+9C=hPvBmJ%d?F0|ZX zjj_Ut;lSO{X{5~qXXGYu0g6NHgrdjC zfo%&QLu{pf-RJPWNKkGHIsRKyU=WHb_PeK?14uHC{%7%9MXC~vkC9n)@_EyNa->7; ztMz7EXQl!!ZIRg*WZ=kRL z945)Y(tsitJ#r(+S5Ll0ajpxKDej!;8JZ{7GCmINq}-CqnjZuRHq4N`EuMs=$2OFoR@zhh%t{*&o|9Z74NHz z@3O1hX!r%2MrJc}*!!nO-VX%M;Omnn8}N_c3mAu}EE~YH%E3ALO~`}0{r>45dn(9t zhb#^W2As(W9=KSzC@p2IGe8?q=71x!1C`7R(G@>dObbOf95KeOCy*r}y!goZ>v3La zz>*=#6XMPmrC3VFlYH?_=1eir)6jpN4%F!7M_A@gq}UesbcBL{dYR~GKY=hdirJVOu>Ta3!G`lM z+OR;wMq0A)7pP5ypePERP@X%uiRd>3pEx!2NX6uml*47~iL8evFTbrhWO5>lFG#QC z$(Lr}>?H?mMwc?Hd5RE%8nE6^$@gU(??rI2qOw`P*Omf)itmgBpk4$`U$LvROoJyGT*uG_Q>>zq+!bUgK9pjddV{iH8HbjErI97>SNWkOwyT zE6O_QYf#?>APTX!qmN5+J=!C{dI}jzp2)pDUuJvrnHE|V6 zQj3)a`m7Y4Tbr=RO$-CMjjjV;SrIRz@1Kk$ngL!j7~+5mPU`Xpc)D~}Pa4PkFIjmi zn(UTGfVGOB0-pE%-BY8Q3XR(xhJ_V=xl|T-d-VwVZeBT0M(eK}x+9mZvnz97&>b&_ z>Yrx%Wv~c%dcZCLQ0|ubeV27>EdowO0KLX~8D{4xMpy>W2ckAS zvoA?)VtrAyRveRqQuA-)XGCMTG6;L-Xc7AZ7l_amAVfAP4+Sm#L~;;lc*Pm4@XGlL z#r+x`xx4iUO%1(t{lD(hgnvX+%|0JugD}R>n~j~Z1cO9r{}~dAJMPhPG2DcQPytnv zKclLTC3T5qB-`bPkUt7}_t$X4uM?gml3nIW7-1e2)$lHe!+GgseUA0QN`d4;3L~2< z`2ow=Fv1b{%aiT??lAYE!k&6#mr+pmfQs)%{LqWoKc4ult`93| zsqWhmi9~Bvj6)>2mIf+hz$DO?wg~#MGtL5jtncr3Cz3<$tU!1*?ykBLhKwKf; zHtw}4*Mj<+Teho@yPkY)wPUev6cK_ZBY6MFK0|Oz5z7O>RD*>vX;;W-SlqO;#vTtTgzE7+S^3QK0@S_$-aR8e07<;3 zMgY$*ZHppXz#9$N@&|yMFXdTKvr__WO3G$zQd8nrMHypxWH;)_jfbA6iYKK2+#;C- zYxBQtCYY8}?Ci1|%C^b2l}mk$+mG?*QbnaxF9LZsmV)?#^ui>#U1rn2d$Ka@FyQACie0}gu}<9(zi(HuBEuZs_v7K5OnEIl1??0Vm8vPYw#3p>WYJP~ zF3~$*u|JK8`RJ6jdhUoIK|3r!;e;byM{KgoXz7ra^sXQJ%Na0eK|I!c9}0E$VGK(3 z!Eq1cl;mT;?KA$G1dppbv(nknah$s%D_V zodmdQACk&$fuDKi-v2Z$npwXlux^*H+Glh3?kT|akL-T%Odh+4DB~vuwuwYnv=jNY z_%J%d<%W5smS$Hjiqll^cg)YvsFf9tib{~wyD?^h*@UmZ-J<4vtX%qh^*3}#CXOk? zpnq`yv>8y3O5~JWqEhCRT=JJSeeTn@!42R)CFR2}`KlWV;M_{PpM^tKzEtI4i(*AV6 zSVJnfyM~X>(y!|%Nwsmaj%U*yln|Rn9hKE*DzFH`_1zjSZes_%nj*F-(9xcyom)l& zYxlRf5I?VrI9KXFye&1dsU~kVo_i&KcQeVDRj*pT9T5CplCZ%;@8Gy;{702gK8~Ej ztCUQ!UZ8-!`=V$&hx(}5Bs76FvilRTlq_{+;~38eCmwu|hq%xxQV=EXYJ$ID$gT5C zS){Z{S%GkoRN`8ON0PNwV`Q*Z%Uyg-S)V_;wBdKAo+i8tLCKI^VR+Xv3Cps>|hxn9m|#^-8(q7D(&bn-%T-ku=Kirn~RNF`jhPZHAGOdt5FY zBZ`-!HI9s=9kliu)WvqB7q;#*)9%8hCrWO(iZ-PQVd=}==a)^ECXMb^i5UkcdDSFjvRfEO~8u8W`d zg0dOn$|wMGwynW&t!_AFz3|@98d9$>+#Hz9DjL+b9*&zZ1lr%T=kD3cJ z`(DA~@Ar29uhbU+sAU6CP_o;&-Z;Q4(kJy*2$3b=8nnlaPU0D5%)Eb@~CCAdt6xk z`!!o$iH<^%E@v?aO)-{D7^4*8{yZD?j05M+(>pb7BYAJe|Q(^RP$YVDX6x)gsD8DtKu`$?Ljf6E*YEDfnN51QI( z2N%ZYEV|4jmj5v(aVGg?8fZs9^+^#zB7M15?hWB1*mT}uc5F&Wr6*q^iRCAI0%g1SuB*i@#lvz=tLPF0Ddk<9@c)2K0-7 zZF_ovU+xb$VpZgGn-}gj7P5qL0jzovw9dQgb%)YrG0d0 zGP*tyT${|{+Nn^_o@xuUMt*%7WavUYo9V#5P$ShEAmIt5WJ4qnvdoyQLT4{X+19n0 zENJn5-D>KK!obU~-W|YC@fFDW5Twx?l<_($`JOxi=;_?((%t}YvzotFAGQGSMKxMT z1wGkha9vAT!I0A97b+2U;kr@X$u|SR7QfMzmngPMKR1QFu!=>xmW(e!2d>U#3JRl9 z3i+&wiwrq;j{C<0VlxV#Oa(*m>7!qAw>A!ljK#eA$s!=R#S6oMHkhodJ(QC1;1>*2 z!9k5;B!`*7K&O}_;W<@#2ctM%@u(CO@|Gco#a+FyV)KcNf0;t`I&+Bg_o;IS7{U_M zN`{^SPzz?HIid%o`3Qxm3@_AnZk|q3IHkh}VkVqQ9*c-8?2 zjJl%H@pJs|DVZ@=SOdt zFP6a>|F=X?$IP$pSK7aP{2 zRoR*Pf3HsR$M3>X4HG^)wJ5!>?C;C?i0T>Ep_J z=LjZ4hZg=xd5~p;K9h~0>aJ&8k6Iqd#R3fY@$td89yT`p`6~pQ#0X}z5J^m;X)kOt z5Rm=RJJ-x&I@(F5v#z1qczlfH6Aj80}nio>N29tnp0IMb5DAx3;y4UYvvh3D;4sG_!nU>D} z4y}xntN>50K42JI=XGs5)C~a0*kq&HuNL#8Rw6$9KdTyj;x}%*XPy>!QONZqQVK$wwjQGtAlLoF28ZINCBkr)Se`XV!(Y{=QrznSfc3-g?gd0=>H zEXm4_(HPgwXuwJO!&T}LJJnjb9(eaF}rBl|M;Ogf{gxvEQ+&@-~IygN;-R} zjsA*_fZlecXTb9L(!1a31Ynqa55(D(vXKf=e3w*>LfO`F4164FhCE0u1gPMcG6{Be z5VvQpjVQLg@U_*W`iVg`*V&T{LBet8rYS33%88zG$Ayqel`WTS-`Z~S*K_^%IBDb57m5|87;+uCDI>)z!V9XRp21 z+H1?L>D0%RDE)X2O~!>jBxj`Flo3}wyQY^rp|<^4=hp6|IqEq@W%0U~nwjtdqOvUW z{}EYuWYhF0ylcU#&SJ63)4TicBP(2$mmSuX6hf%s3i+j==;gVanrk1%&B-Pow(4cE zC=0vgpjn1+plDI5YbeYZW*w5XP+s@!4rn3SY4VH|) zHI3WSp)qoRt*WC#D26t<>BWAL2L1Wdd?fa~p~<`*G$bKDmNSoOcvk|+!Q;Ct(Q5gB z_<|3{Y4`r?pA~?aZ3CC_J96e4aQ<}*c;DD{TX+CEKp?$jMrPiEHKfC`vydiDt<4Yn z*E)O}ip3`~;zyf%FLsA6N>&I5Vu;OOpNDi^9PZb#rOe)^d`;c8EhjqC-^a>HVPn7F zSkAKtVL(%8ELj|raBtOh&(8jLCE5bgJOP~=ZUrWQ*>yly&dfW5pwPzHN=453zV7x)OKE4@Q9weo4(uwZ>!IBpOp9#HnL6E&O~FVky*pd zY}7&6a#=<-dNXmWID*D5TS)Z>caP`k?DVcyIOpYR`3QrswY*a*V%1(9eIT8@U9v_az`f-XKbqW{G!Vyxq1 zZ|w8UzEJJ?_1htSL>>l)6DXoq)7c4zkSU<77Wf-0hk(JK*fLX+@d%e*0)mwWPyC(g z4?(j}gaQZ2YB@|gM&>309FNs?4v!A_txC$W}b)w>P zaKWS`7}P4@5Y@=?Ry~%lyFa-kX(dc%DN%6-+~S1UX`Iav#k3&<#&f;1=qq;@5s~cZ zXaOLdohjG-BD7G!*nn6l8X`FPXT{L`yZ%0WXbRu`mSQPOL-ro^9nC=_58gp}MIb*Iall_uqu2aJd*T=Jf!M?ykGR((3EfXNoe+kt zpnnlU+TfUXsVRr3q=f>JhNaXxLINc*yA$$Hwb1EKYOO7HJpSr@8ANFQD(J@-nr@L( zo|RwXz#F7LL-6gPgCURW(Ga@j&e4H-?*t6XAjDNcau}SMp3xEp+DK8Oi4XN}EKh!f zaT%OArRUvUlDr8jzF>YNWcV)0iI>cN!Hwve#C}07TQ0HMFzFGIHy8h>d;gY}CbM3r zk()o`sS!%ETx$3~6h%11KNW9`$K9b}Cy>}&N!rDW`_hn!u)`V!3c0Rp$heeVExI={ zRV%8t=#%NpMe^Mz^>AtQo_Bdrrn>6nDCH?9s3hRlu#_xg#bwtyQk4it;)c8_54H-m z#bJHB%SFNbYsG5SPh3LVtSOsV5K7EaS4$xc%*@^*)kRY8KvZf}r>HuSBgAl{Jn$_9 zlEVJw*32rjm>fQOQniv-cOuRe2b;m znA%B>tOpNX!r_8B6d88ST&12ZqpuUU>u~jN?;C(8ruFU7YVi5O(tUqv@BX|%1|F?G z1FrM0Kc+$eZ_|W6_ny4K`Ru>%v#z`!(}4F~-H+aB;D_dP*B7`1ytvQTH4qdz^B~`{ zWlj6K;CzWgETEGmG~gi<-11tFSNGcHLc1Fjuy-5hd~@E*1sx&^eGPHCYazJfXy)2> zj+d@#4M9R+Z5DnCB3(#*w-_vp&{+Bee$5!FeNI`^!BXSzYI-s_=AR+7U4tLcgWmHR z0=S>u{$RtTGyC+*c+DRyl@4Ub(kpVYKl=wN9WnOxLI}*KOh>U!);?)A&>4rAYnXw{8At>sf9Qh7W17Qp?iZ^MP#4-^KhDi|C9K&Zx7)s7 zSMz#%nxPfqm@TR+&<5etC2D7KDu`mkdHW(!DU}{%?fHcWv|g|4J{Ytw+h4Z#&RjWb zzx$=EbviG1{R!Ge{K#7GlwSVYLzvSxMihMsz1U(EV_C2FT-NsFN?q&3tNJuc6x6p~ z_DRL>gGcw|!tRw^_FiMu{0$Yq9zkL;wJut?lvouJYt^42O;QT$LMhbbhmDBciFWmE z{9?7zO%ngm@RrzzCQP}p@pMG7k2kYv-##63LowpQRk?6ApLM%TOBj;Gh+vh>@PW1d zckWL`Pr}4-OT4bz?;>@7l7iA(VnG~_Io!ZHissQqyIRB`&@#G;8m zgy0a|YkE&!`3Ny#Q^jwp2rB@lxDIhc1zTV3&;3 z>HZHBk@+TH7i6OqMfNpkiyeM^5XRk%wRV$3ca2NvZAvpj^lyU@ku7vNfxrd29%$S& zK4{7c4Fubf0sTrmg`8L)%^UdMUi6W}G!Mr*+mVgTAQGlOxPPKoR;F(Fo0_GuWDY;} z>QLOT`skq(6R}W>wgzg2A(%KR>KHf1iJF~LE*4gTaelU*K8y~*92tF;k5LoFSK#mc z7B@?wKr=Q!=gfOl7oqj&1?+4Xob#WPyT%&Xll|Hw`A*7{yEdEdUe~kM#x)s2LWL54 z75V8#8msEqjJ@Dl>j`MZpoy`;p~nX^jRZQGA{^P_w+%fhD^jyO$rOBVkS`F}E^(b(9Z$K%9}gD(*LWAj7L?_qL9 z(x!^T;t$5XYy)MSP^9o`oZmPl8cr_h8%Jp|j-HU=#c=Ch1ufwmd~U z)ZXoLVe<-XbWPGP3Ch<+t%(AB3WEk>S|g64i*L(ZW0gFy60;{B!wkQOoo*gLD0#Ax zJ{2w{LopL_yPQgCYqOF#v z;Xkx5lL-=jfV%Dcv>1T`Cro3+zQ8Bybt_SX*DS4K2zvF#+=XtVMkZHmfJ1OfcBZQT zMq;NWKu^M^dR<-r!&e9-H-T$RJQRtlQ<@CKjp8)}zpJ{3tZFpio7T7wljUX&H7ADO z8pJZ_o=@j%>#e~iF`zC>Cb}Pa!fE)T7a(^)Yr+-J zt&%~0mmR|CmgL*q&PS$o)xc2gdO@C6jE{qZQBfXS=Gofm(>mYCd8yM353E-3>x#>; zGY5!-_5a_iJME@^W4Lbu>o`d3_ARxK|5!Fz1?nEJy@}8Po#|!6dZE325&$% z>nFg3YF%(VE%Q${OrKQ6-tOl~7 z_wIdpfB);pu!!;h=JzBx4ouu){9%nyzPw_*ZA?Zi9I||y$h`?FdaE29dgFQ;*<#g1 zAcp27+mA&MAlvNnMmu)-RYh?>6T+)$9$Gf4nDZ<5yc>O@agG2Zd{oX1#K&od1(7Q9 zx25$GQRY-^=zJkn*|{&h}H*lJdyzhcb4CRePqi!k;wB>@(;JR7nA~61z1N?pbiU z9Ov)GO8+3(I{gtv@QHAQdLA_TL2!jPd&&}eL5c3DRG%OJVLGn0L~hijiWqbX%Z6Ah zBG~C)cT?<02g_|P1hLNIy#<;4`k`ka*nUIn<*qI>nQmV5|p~FddvlZJUV!o4$ z9Ld5RxDB4wmf^2at|+I-{AVW6(h+)YoL$y^eui*YFtOXd`$gi4~i z6a*IBV6xiONhl<5AbtGGGtm{(^#$~L$lP{+>g;~ILH>gH;(31ucs=!3rvsRtVt|{| z-B0V^`_AdJ!~JUS{bRtZD&fZs@MA}m_wB~kM}T45?TG>ivU}6*aSSot^~go|@sNI% z-rUUdQGX4%>96*Fh}r(QSqI$TPJf;C^S*_cZUb!zKj#TQJ}kFicT55A7wLeH+i#gJ z1Me%;IiJr9*0$GE$oJ)G;Cwv*7-hQs-j3Y;rOW%)-38or1umQNz5|F6O+j~fUT$M} zUrw_HY6-tWfKL|;0N^SifH05eYxO#U8F*67`}M`^{m>1>s(>GR-6Zt7*$0A9{lS#` z7|Mrst!QWOw%YV@vc&u9c)a<_Pj8+jQX+*AIBG%J@4Mq=FEY2oCa*`($;RJ(%(Qi` zYPM-uOU{loNg$f~TjzYo>ti+Q*5NZrd;HXYNe;D*gcLyeuaD#XDys})%JC_N%{5c95(&4s$Zef|E+qjyo(=%2ap|J zU(;>+@ESJ&HqOS6W%n&0zb;;dUshd|T0_O4i@^-$s>u70@^btUZa*#myjk#|h;e+a zS(H3RPK8KsJLg6$bBy?yeB~aVw_Ted>JNqJ-xz`hp=6zLHA%hdri_2FjR`oh;&-{{ zi1tuv-_6X};}ls01;H|ynp!Y9>Sp{TeZ6Z!&)UtI zXHVFyZW0l0&>6$ZtmHztG5#qhYHAMDi(D03=_oI@IY&SzmbD7mA!yll6G6Bq-l#utOesz~#&?B5g^MRubjY1+@7LwC?w7}P-l-&+o*P*4A+haqL{x!tr>Ay#iy%Q%YxlS{L@P&<<2M2sTQ z>SRV`Efo|j4a$W=6e^k5nBb+ct5&;Zk^rfNWEHYXo2OPLu<{#wGUUXA&5I!?Pmc>- zdDIOb9#i8$@lEl#u zWr-AtJ_M9k^>W!NR^T^&yYP|y_WJNasDwdy9TiV0u3Pq5JashW|KaMIuXzuc+GqL% zJS4rxKyiB8;s=H}M!a<|uhc3muYr3^H+F0eLGykb)!TaS|EF@E23|4nd<6hME-ZOp zZ!CGgUbep;`gvcj|C7DGbC#tS!{e;($3g%0>v_!A8zIr5X!qyzw9kXK*z0%d85-vO zo?-*Mcqat5+w#6IvEM$On*yKmZ9li_0dH6J+n?Rpp9e9(LyB#4JBu6)p10ZUPr$ZK za~r#jTD1n@=Zon}t@|ggm)8dcAdIj^l_l%*L00KI>wOXOzRVKx=OK501|a*q)yI4_ z10Qx>yAT-#8ox85)Q)87pp+R7Arlb;Q8(*4_<_0gv1U!Q*JeBe7exz=N?`U)8O z8EoFtoSiOSwQcuED5YSfg#(^Nxy9(nB9>Cw5MV-uDbRj&sw83f|v)5<>o|JlRg5afu_ zGau`af4Vky*0$;a>z9CB%kSM}<#*4)ef~c;x5agB?knUUu**lISzw^#neX!aj^C+P zVRIsq{J~X3y*4|zTApvF&{T)=`j>kz8MwP%eXq1fz%-X|f7#Ez&qq7I6eZI3-0!+r=LhEi0r(LC?jSB{UC9jn3d-S%aRk4juqIBl39J)*Kc2 zS9{{WOv<&~=Ku1TdqKF!k(~mV}%q3&W(}!>VZ@ zwgmb;0x^x?Ov%OiyZlT^!(@D)W@e_e^R=K|=h>n{t?Rg5D5U+|It4H87{h9RVuCOf zbYcYYXJpgSrD*;EDXZ!j1!amRaor<89&o#GT{GgPhe?fY3T^e=6Vuxk9{FJHtwFr~ z`e?$EieJf12f>Jb6r}%Y87iKGp@M&Uy>R`j))RGSusb?!2jbG_|#0tui# zig;^kYb#DB&Xi(+3WP)`)67z6!OwGYHNg2kuh^-Xe z7g_#ej`o+(xyPI>60y**q!Id+0WGVI-7Csnvp=M^O^YyTrRz7BLxkupg&CLlQZ3eU5>^N^0udER4cmlZR|#%?VHobv5<0Tnja)#QNMp zHG25qvrW%}1H5MCuI;Fvd%lf5L`?+=l zK{*T&^V#N@UTzv^iKt1H6sa;gdtn`ALt2Co~kp!^=LG-lVsXZLz*Qw;x{q1HQvF!{C?f^GZNI*w_bg4dfI9jf-B13cAt-SzeuP?|V(T@R47d5u zz{Jr8tHCA&XQWOu%!jBAX>UAgf3-5dvj;SJ0`UShOP#iq-s!bolr9XVzY&ei9kcEA zj2KsNl84=6&-*mAQi{}Qh`^v)EMbva&U53qMkUSf{vU-+b^+Y&X-+;*o(Rvjk3d3- zaWDQElEgo)M7ITUbolTb)Vp97y?-Gn3nS=qwI`DP>(-y36m0qrcg_|>@)%MAKSb>u;#1N@7L|S`% z%0u2lssy#t%7G+s+tDx!`bx3Wt^8ja=y$@-0~S!k%8r9@27R@=IO~7uwl96(y)OX9 zPp}%bOIyfs&YQ9LGXIMwd|%(fXApN;yYd*m-0JB*x8Aqib!=veNIB^6(U4IoO)ng) zhH)RHasS2eD#UbebxZ|U8JCAA(V6A5)pV;=#m0lS^70>icH;glf4IjO;u<1 zAnUc1*X|?xxtlk8`v+5+ep;L5IBIIj_;$?GExC9ax<$F7!nw3_3am7bm5h*FW8iv* z`c{N73<3cQz-UEnl1Ps4011l^>yRqHz4cs&|F%s0d$Y@yQ*$|bKW^+^UUIrwr_n1L z{ptfr(74loY4UV2heutk+KfENvJ*M~jAn)zI-R1Ubf2cd9XxE94JY~sF-Q%PV6B)a zIR?sP!nqcebIktnkRF+2?#kRKs59z7IC0`QQzBa1Zpb^YDmYP!S`lNALGxu(x-?Uk zBd)#NCEH+U;MjW58~65c8Z%`i-PJG;j@VS~Hs*WdrHO~0fC3Xs$s6<*Q*=Zt7fPm) zOJ>+pA9L;R*|Eez42XQ(IVyQ9itucjC>4CUR&$7dJ3QwL&B?^mCouB)v)~c;8Dl6T zF5@G~XvHVyu;FN;at}UXEx|J?Y*7LnqiSR)h?GwD2KxS)PgRi}QgOC?1K{Ym=zSl) zFbzrm=JI}xf~HmM8_-uxw7K~bC#%6SQ4eH6`xaYHx2Ao(Jiv}#UgQ^3yg{{;aH~6U zTE=iJl>%7sI%mr&N?QRlb5}wm(>Od2Hwl7rC@{-Qc{<^9i7x z7{#TcCh40PW}h7T3{ROdG68R<1^0BnqsRvLJD(>>kl{ob0WQnbmn=*3A$XtFFk@Da zfke>ADqQTND-7}|kMbj5VxLf*>~k>k-w0($myK}|CPG?PgN_|B%v8o^vi_f_Bv`$vk~Ro=$(df`K7A1#u}lnBE)xgRJneSY;K^=O${ds7B_#~f z$R!>I&)0^sAwaF^RV#p>iGIoceRNYahH%5nU-4s3m0p@2)cs+o5IUvFy0peE!d+1I zVCpOuo5NqWFrmiTynV*p@sIE%kz}>{1pnAPQxcJ%z2j`$x;rD{2Ss=?wukx+nWH07 zA8|08z%GRwP-~fa^P*Qxa%f*_0!ld2YC0In=*YnCwWopvB?-p2l|`(-Uwn^CEtrO+ zrn_=l-dHs}X>`th1Qw=rgB2bNtzpn^=s@PpBE*RxoD7|Oq8^E*eZ1vb>;U|zDDY2Y z@`|ASYpoQqSY}5*9g*q}HKehK+GzgfyqYTY6*#Yyh;xTuHD!E8hh`(gl0$;E)*GbL zi%p84CRiASNBuLpW`6uweU>;4Rmh~r0|DsFZ>+iLc;(X|#irQ%1xt96F@Fjo4oiYh zCgI8V4-q9KO^35F>STqeNvr=7@@fH zWYb#PyJV3IT720~0{<(+bA<)ECK@qle^uAdR>pq290ES_u7DWBDfAzj>-rs7kbg2x zg*)I{5c^3V@5roK7`?-@4~{NM+khcEnv8+e)n7@U&TWL4PbfE{o`fJ^f5ou4!;l01 zvN+-^KsjU1SnS1KtEtu(RgDI<+I2J25cE3zHj7d5ZRNV)kn-53Np@PL_WQ*U*IGiL zjL?SlTqxiaD5SJ|)#2NdG>ryF!QYkrB9aVxHp2^G>xUDj+K&#jGHqt|Y|6&Rv0;9F zAmdt+heT~I4CDoOhH@=rm#Y%761Xsvff4 zU#kar!@d-=<39|=IikH>F(<%qZjC?eD!CHOc~T&V;3xj{ji@w2n7z@PqN^Cb5v&!L zUzWm>$dF;6Lw97Fh|&-+mUzpjOX$Y>D~bI3;R~{UYaA!31BOMwN%>E{uH*nQzvq{{ zWZkyOlH@|HRrV?vNz@2)6=lvzmvTCR0xQfPMf4+~n#M+Sh!jVRC|9w1rjtAU?TAk? z>{EyNoxhjz?-O9{w^n&Vgzdo{^ugj*v(-&GeWTF%*jy7Mx{=cb@Ya~R1T~(BP-ZmZ zB*P=CN-9a13gSetb@6IN2$(}_IRZ>P5p*+oT@H|mGR^MTYzix$aj26ah`2#Xs>N0@#9y z2iQh^2$h!Nn(cEP6f#hvkq$gEdmK1-Gy0o7#H#|R6q6aDce!>_>iEskl-c9*S1OSK z^d0fCeYp?fW(o5|?NkP}rgkO?zc(gEV;pf5D@NF9ID7Ys+1{u!OxA2>SV-+)9pH$O zq&9L9xi!+#*4Y5E*SzMb4HwpC+wdQvohhuEEx||*H zx;QheNA3i&*xCq8^bz2Nxk>)~LLNf$SU$YJlZ!S1u>CW!%28=MV}bGk#l(eSjL9Iu zjW7WeUOLsf#o24I$#1?3@gZHhTAld+?kOW@HN{(_Rirv&KI1pjYRI2z3^k9P-l4o0 z;kS{2>m~h`TeAI#v4ChxLQkin4-5Gn?=N*}sgH<_%07Ldjrp3Qv`)5;$Cxrc+sN8T z#;J}QA{^MKJw7B<@-iM~PfRpGd}A=YDdZp{9wI1A$FRv|)g?l*7tG{?(;r=6M}Q6y z+}{uwTrrw=svn7uZv6DDmOR{(bKx$}z0H36&5L{m$&sF?y`P|HdHc})Ki3IP;4pDE zyl&l>t!aK|YYtFz8<;u&O6Z}E{h9#n$mt75C&>!s+khb>5SI+1kXHt!<;A9daIy?s^Nz1;|G$d{>*wOjHpdaZsjz4%{hw+N$T0c^FeF$y{*ST@umMC4V1Z9~ zQM3gXUwzLU_x}V&$y@@DwX%BooPKXrs<9!1wd;WV4-xSm@aYx&49t&fX2-sSCT&Rl zG!ONizXEWovX4Chmn~nX@7;0x)Zff7b1~A)pehmMK1et5JcedFH_u$!R4Z966NSXX zWuA2!w}#3iUCuR3>&rksxz%|8ybU*3-=>rcnL~g~AF6l@UP7QP%7`XtqI2 zSl$aMs_1xjbD@H)e_pR=e7Jdg$qUPb#F?jkqLM>LTqrP;IIN)2=3vUR7<808y z48P{zNnRT*jp_5+rdFB%UGdYs#Ma6l^w%$dcDj=GdHRJcZm;5bm-Ll`(gIBoCKM${9e$y*2tG zl87&5l8Ey~Mgq@yV*;}r(pA9FSJjpX`*#1DD)~P>a1oIDr2Cot8fk84lO@$h0`~^} z?fPu!t&tg6UH>2Fq{CLTQk$uoY=%TLzDLB-iRa{SQ<_|gQ@scl{S0YD=Nm@6Uq28% z&H2boQU|bbu^={|7DwA-R^g=h?)qb^SKH-EOaDrersNVxq4J(2$QbeKe~VFE>#1x; z*0PlD{mI^(bXv3LLc&Af z5Fr}_Whj!LUZzsggPiEc;qY9WflaeKpN!d4us8WYq+AZzPI=;E-Fuf)e{M_ z)0QlNoD8%k4frahY{-iH>lQFN(4XHdz}NGYi*h}w5_-Akkc-7AQHG%o(lY)csuh*X zVWW_9VPl14Eh-P3iOl-dh-UGm!~sm7Uvyq-dfqy`h|=1fTU*Hqb~PJ2sF04E0CymbEw9e1WUeol zWL0S_2=Eyh`(Jl4i!a?tpk^O1!1*_Rrxoxz^T{8WZQ1y-{Y|vNecH}l$C~67$T?Vt z?W?s4M3I)|U*OpIP0Q}!%S;~~V4kifxhR4)ORgAe?A<;eqzv^qSBI(b~AQ7LYu zBnQYbvaFM0PbOVD!f935vWWOfDoNB14#Xe0M3S|dzH`AdQkmXDsyN5l+Z#Ujk0yUQ z?V-}rUgTIK%7g}kFe{0oq9Bcnl{yA7(nq5_;6I6e+>&KJSI`Lwl`qzPb2MLWrYb3_ zD-5VuRA4=Wf_qkehqDj!m_p5nx;64<32RNAomm7i1o|(7%8-%sFo4&y+(iBAVcM!C zEtTW=6XS-0W4QayIQKV#jo3t76S}vf)_>}+YShf)&)|xR@Y~ik%}XbZ7v2x`U+zK5 ztAgd7GOpeM{XabGKwyMtSBejc68O~BvdtP`Pu@J7^Zy})u-N(&jNa0Lb4;r42>qR` zaQ|WQ&kwRzV{ANJqN0^WDbh2tk(ItlUCp@pfU;W047Q_-VBo1vSe2ord*Ym?5xiRe zMs>3SU4TZ4D9hNKEmg3XHtz@wF8?BMl=i4js?|+#j#3?Qlf{}+LWgMK>=GyXzNUb= zxu+*!OkQNb#&PsP$#9s;hfNk=@`=ICgOU-;1O}Wkws$E-=sNeWaaqH!_Jn-%hX`!P z1lk!Ui$Ai}ob{8LfsTSgofTt;UPH$lDG%or~w} zTbTVSHSYf1h!vT8M&6d(6c+BUIdHi+``24Ic6NYP%L2@9z4_<1=OkeelJO( zz>&<(yf#@uu_&5Zcr#>HL=k>BY)VnKl|5!d)px4_#B^%*;Luejld#DmcWpB0@>#Ai zRA=I4=5S$Ym*R)d@lHw$j_Eq;G2>d@R!U5j!3ix4OL~d_MP8bHz8Y@Dk`KL#SW8IElatc@q;RvYgoJT8bBkN*{2AHY7ad zriFwCJndg^?)Nx&v47la*aVAO=%nD>aHX9hNfXwvhhyDv$W4k!cL(j@T%?9G5~Q*z z^j?KKF@L~*GeOiF?^}#wa~rqB{Z3GX#{U^ib(osrjon`^*WR~VSAV3I2sYeN%6Y9& z?g2RpKTySC8v*`VhycZUhtY72?YhRI9b?1KM0q+hnX=A~`5#;}b2X>ArQiWp(%On2oWqHm;lsgND@g{I&{E}^1lycG83=)ey z3Zc`S!8$lZpBk}n=U*yUKU)QJEugG-X*&E03=08sq!xFNo0f>^Fp3g8r8=X~u^1$x ziKuQrmYSDV3gYBjDw+7Ug=yzU%h;dOICLx zdYoqU`(ZbGjD3$7WmQjo4Xrr0&D^PA4xI>}Qz{Q?5@VHk|YP*H&mn+^VTF+%yS~5NAQvkTsPzI|hI`s(U#fQ2TyFc`vZ|hkeXJew>P?+BGlv9iQ6Cx{WCexqh|H~~$5uD=L?Sth z@4e%DGPM3plaIFB6)g6b+dC#-Qs!JtJ>&!3^|!<-E!63cD} z$>_2}A!?s*KiiWYqO%xFg(`q1kK-BtQcpP|R^%`S^4hEa`SNy4a-qh5PhOylsl#qmlGtR&GhKVf-Rd&n}Vj;|hdGl3Q zGb+%r{lo&0Uf67z@~F8lv>5a`GDJpUHZv0Y8jIcyUOGhw8EJ`0)&&P|!r0qJ*Dwli zUjL$o(1!4|Dk2fXW+`AJ3>o~un{E4}VKu;lPawn($yWOWI+G=;qjBbDPM7fsW&|HM zd~YDfxkd?DlRODta4d;pRV%+{Mm?ODIUxV@#dWV=LFvdE^3V^=&kVK;8>W0+uR~PF zN&;o{L=V1*&_WE8^scNo8Jv*=Tq5_KYG!&dmH(`)j0i>qe!p;LjKt&%{nAxdBG-O| z#2CJJ&e}D9?4PM&X-Kt5&~?ZVs%KA>1s8D?-0kfUEN#%x=z)lG1NU6iQ2=Ema-H!h zNCAH!G=+rkr${C{Wz~_0Tk1s7?E~aUJCOSCx4qi`dXc!!y;fTcddGd}zEEcZm$))5 zA78JzvpmWGP5)z=^IzYbgePF1_}Sn+;5NSj=*m`Y{jHa5eFFSFKK}$_kptUnTY#?L zr&>?IbBw$0^G!>3PQ1tP8}$v~E3O^OmS^ofaNrY=F9o!9sdhR01afB)&|)2szM1KmBlSiVhkH8fD77M zSg+P{D3eBBIeJL&4wtiNdI8wN1P#TY4OuIL&A&E14{H{aHWZ-bwaS3-f1&OWTp#C$>bZ$cWFLG{M$ zrvsmGT5l)@2$WHYd1DTUuW@vuhaZULAdhw!=xn_~HovDIT9&jm=scK=#o%Y!SM@7^ z6&UjZ;PcsRk!OI(7JkcyAkqE!0PuW6Vu*>r(np}>mu%^g~vsL5#8~#_k+bez7axhpL!`#CY#`7>EniBLGk{)}TAsu(q*+`Qq3b0}u>^Rs7 z!MFOI5KK4BpVmo4*fYu@X2^gWs5n7I`*U25CgaI^nRAIL$bqR!3+uJKRzAg$Sexm` zm=b2CYsLBHxiuEhr9zvbEo9`)DPdCZ6d~?0AZQ4 zw-67A2;ch+^yNFEiTzKJlEvRq{@ivjNt5^8=H<`8|NbQ3VGXF8D6m2G4lw;(^Z#uA zkISz+y=);6YxVv`bPm+kW>E8Ji}zMw>w_w8gPKWsT=beD#7aB)S&UbTKx%nm{Us{} zTJ+}|ZA}UNt@Z~lC2}k}u6r$lJfwYRMyZsv9!V2s2dK6~KC#t>!pd<4q7#7T?N8?4;Q2M zU-~04k^EH_1TS)C#)HMKMe`BN{B*;EPtT1Xp!n{c^osvJ|GWBuhARG zV~D;=B2lV+*s?Tm?}L#SCw2DjB%Wihcfn9ml%c_dsyoAM80GH4q^G E4?dF~YybcN literal 0 HcmV?d00001 diff --git a/dist/appPublic-5.1.27.tar.gz b/dist/appPublic-5.1.27.tar.gz new file mode 100644 index 0000000000000000000000000000000000000000..afc8b684010bc4b72ed92b970d9538e772f78210 GIT binary patch literal 53926 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 fvVKWEA2r(QdOqRq~O8lVEVG(~!SY&*S0X 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 literal 0 HcmV?d00001 diff --git a/doc/zmq_topic.md b/doc/zmq_topic.md new file mode 100755 index 0000000..814e87b --- /dev/null +++ b/doc/zmq_topic.md @@ -0,0 +1,60 @@ +# zmq_topic +zmq_topic implements a topic message machine, it need a TopicServer with created using a server address, publish_port and subscrible_port, and one or more publisher with can publish topiced message and one or more topic message subscriber which comsumes topiced message. + +If the server gone, subscriber will not received message, and when the server restart, subscriber will received message again, but the message will miss send by publisher when the server is down. + +## Usage examples + +topic server +``` +from appPublic.zmq_topic import TopicServer + +x = TopicServer(address='127.0.0.1', pub_port=5678, sub_port=5679) +``` + +publisher +``` +import time +from appPublic.zmq_topic import TopicPublisher +def test_publish(topic): + p = TopicPublisher(topic=topic, address='127.0.0.1', port=5678) + cnt = 1 + while True: + p.send(f'message {cnt}') + time.sleep(1) + cnt += 1 + +if __name__ == '__main__': + import sys + if len(sys.argv) < 2: + print(f'Usage:\n{sys.argv[0]} topic') + sys.exit(1) + test_publish(sys.argv[1]) + +``` +subscriber +``` +from appPublic.zmq_topic import TopicSubscriber +def test_subscribe(topic): + def print_message(msg): + print(msg) + + s = TopicSubscriber(topic=topic, address='127.0.0.1', + port=5679, callback=print_message) + s.run() + +if __name__ == '__main__': + import sys + if len(sys.argv) < 2: + print(f'Usage:\n{sys.argv[0]} topic') + sys.exit(1) + test_subscribe(sys.argv[1]) + +``` +## TopicServer +## ConfiguredTopicServer +## TopicPublisher +## ConfiguredTopicPublisher +## TopicSubscribler +## ConfiguredTopicSubscribler + diff --git a/requirements.txt b/requirements.txt new file mode 100755 index 0000000..4b4953d --- /dev/null +++ b/requirements.txt @@ -0,0 +1,19 @@ +xlrd +bs4 +ffpyplayer +xlwt +nanoid +psutil +ujson +numpy +rsa +brotli +aiohttp +upnpclient +py-natpmp +asyncio +requests +jinja2 +pyzmq +cryptography +asyncssh==2.13.2 diff --git a/setup.py b/setup.py new file mode 100755 index 0000000..7fb54db --- /dev/null +++ b/setup.py @@ -0,0 +1,52 @@ +# -*- coding: utf-8 -*- + +from appPublic.version import __version__ +try: + from setuptools import setup +except ImportError: + from distutils.core import setup +required = [] +with open('requirements.txt', 'r') as f: + ls = f.read() + required = ls.split('\n') + +with open('appPublic/version.py', 'r') as f: + x = f.read() + y = x[x.index("'")+1:] + z = y[:y.index("'")] + version = z +with open("README.md", "r") as fh: + long_description = fh.read() + +name = "appPublic" +description = "appPublic" +author = "yumoqing" +email = "yumoqing@gmail.com" + +package_data = {} + +setup( + name="appPublic", + version=version, + + # uncomment the following lines if you fill them out in release.py + description=description, + author=author, + author_email=email, + platforms='any', + install_requires=required , + packages=[ + "appPublic" + ], + package_data=package_data, + keywords = [ + ], + url="https://github.com/yumoqing/appPublic", + long_description=long_description, + long_description_content_type="text/markdown", + classifiers = [ + 'Operating System :: OS Independent', + 'Programming Language :: Python :: 3', + 'License :: OSI Approved :: MIT License', + ], +) diff --git a/test/brotli_test.py b/test/brotli_test.py new file mode 100755 index 0000000..579c7b4 --- /dev/null +++ b/test/brotli_test.py @@ -0,0 +1,14 @@ +import sys +from brotli import compress, decompress +import zlib + +def do(fn): + with open(fn, 'rb') as f: + b = f.read() + c = compress(b) + zc = zlib.compress(b) + print(fn,'\t', + float(len(c))/float(len(b)), float(len(zc))/float(len(b))) +fns = sys.argv[1:] +for fn in fns: + do(fn) diff --git a/test/create_private_file.py b/test/create_private_file.py new file mode 100755 index 0000000..9c7a199 --- /dev/null +++ b/test/create_private_file.py @@ -0,0 +1,14 @@ +import sys +from appPublic.rsawrap import RSA + +def gen(filename): + rsa = RSA() + pk = rsa.create_privatekey() + rsa.write_privatekey(pk, filename) + +if __name__ == '__main__': + if len(sys.argv) < 2: + print('Usage\n%s private_file_name' % sys.argv[0]) + sys.exit(1) + + gen(sys.argv[1]) diff --git a/test/dataencoder_test.py b/test/dataencoder_test.py new file mode 100755 index 0000000..ea8cf42 --- /dev/null +++ b/test/dataencoder_test.py @@ -0,0 +1,36 @@ + +from appPublic.rsawrap import RSA +from appPublic.dataencoder import DataEncoder + +Nodes = { +} + +def get_pubkey(id): + node = Nodes.get(id) + return node.public_key + + +Nodes['node1'] = node1 = DataEncoder('node1', get_pubkey, 'prikey1.rsa') +Nodes['node2'] = node2 = DataEncoder('node2', get_pubkey, 'prikey2.rsa') + +node1.set_peer_pubkey('node2', get_pubkey('node2')) +node2.set_peer_pubkey('node1', get_pubkey('node1')) + +data1 = { + 'a':'iy34ti3y42ti23t425g4', + 'b':100, + 'c':'100', + 'd':[1124,'34t342',5445] +} +d = node1.pack('node2', data1) + +try: + data2 = node2.unpack('node1', d) + print(data1,'<===>', data2) +except: + print('ERROR:') + +print('check c', node1.pack_d[0] == node2.unpack_d[0]) +print('check d', node1.pack_d[1] == node2.unpack_d[1]) +print('check k', node1.pack_d[2] == node2.unpack_d[2], len(node1.pack_d[2])) +print('check s', node1.pack_d[3] == node2.unpack_d[3], len(node1.pack_d[3])) diff --git a/test/prikey1.rsa b/test/prikey1.rsa new file mode 100755 index 0000000..2e06dbc --- /dev/null +++ b/test/prikey1.rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpQIBAAKCAQEApavGY/vV0O8RE2+OaVi0slpb19hssNYREQcMrsicS7q23GlU +2NCzU95g8M3fXoOIjTwKBoUtfcvMdMzotibaTlJ+Xj7CFiSZLUYq9hRuujbUyh+a +BoeUtui33hilyksGlumdZwWaPCSrl7uYw2UoAsoFsmNWlNqXig2okfEUGBMsMjhw +GA3W5jZDa/yCGMIiZ11MfsxdIjTgkfEXsQEYfOR77Zebzfh3GaE/R6EmejJHosAQ +HDcDSzsq2ukY1GOdlap0nnINCQkg/wTHkjw/skOLk9NZhJggHnVdpaxJtWUeU9Pv +j6D3tXYopUp44A8iEXqx07uQ5GgwYfsoPVSRmwIDAQABAoIBAQCTg/9z+cKqdqSa +zMNe1EiZYtHSwPgPAZ1SrxdA7ooU+F1opzWQlUyISkPBR180diR5tdMreRxHndcg +zXAM+U17XgL9gh4pv07RPGB2mzz4/Ge+ziLE26a0s0U9Mm81UFa+u6eXGyr5kKFl +yvm8Sm81BJPMAp4OjxFOMNk/kg0VXVXwtpJOxSKBhiG/DMK3k/Cp4W82rj+/soYk +dQqYAAHvMvYIBRcrwRlhEjhnyyK/OycUxBb3uB3JgqYhg9N3cZoQUQdcETWOSuau +4zNyCjmAUlT2RLq/2jFcy66XVLsq8b+6aczPaNGNB5oRG25l78DpdE/NpISb+Dnt +y++GxxKhAoGBANbm2Mr/eWJR1E6DhKH2CGB6++sKRq4iUwnbi/yxZnWuHYsO13zX +V8wUxNBXY3GHFn3oAfMdp4iXDMhnCOaDgSiXb7JvXO9mAzN+ErjwZSJmszdKhCbC +lupXpQ1gdzixT4tgShGtIV/Mi/xtQE3j9Ld5T7PIaVcpuR5UoA+EqOqxAoGBAMVa +sMkjO91A6NT/JYL/15eQV4gWCMJXgFpRAezdfRF56GULkkJPRfsIG/IGS9SWL03c +MTtvqyrfNNbfYeIm9VKxIyvei2TN0NZLFmon6TwJVbkMIoNHELiqXB1r1liHZ0Kk +rBM7wtqqpVP5gqzMEwQqy82pLNSB1q4/x6rmYTwLAoGBAIvYvcucZMluptJRE8ax +7NGcNsPFdCltfMw1auuOJjb+j4smpWRAXooYGdvagoQmfKSCjoI/Ef75DQB8zHX1 +rd9UbQOZJMlFgV/I8zYWejngy0931nfCuaLWVjpWRD9IY5M3nfvSxp0o5w/lqKsH +ZQDgmNVN3f8yz5zFRUPT+HiBAoGAes41ReQb41D50Vi3se2b7V9PaIo1hyFF6G2S +Ih+tLMvMNdxedc64gD4Kja8nTrDp3KxRImgZfgMI8VBtq3mMnJlHhovj96Uu97of +x4zwUICKjCyl6US2fgLccR5ichJHyiuIPCuq08DuQfP4Qah3iNdMnLxR/iaRPTrI +92qGRQMCgYEAoX8nVeH/OJ6SgjlWtu2ReG5sq4l7OlYqVamj2YXsyoRmBRGLWJ+x +ffj/TYoWxykPwZ+Uy+O7EoTz/Ia4pVpQ3dOCrMpq9pksoF7ZCPe9hh9FrY/PBOij +e5nGvNWgropXLP54WzvRvicITv3I3Oe6lF7Up3HBMmfiabEHVAWeWCY= +-----END RSA PRIVATE KEY----- diff --git a/test/prikey2.rsa b/test/prikey2.rsa new file mode 100755 index 0000000..ec1dc03 --- /dev/null +++ b/test/prikey2.rsa @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEpAIBAAKCAQEA0Mpe2xK0ZQdehx5C/nkEbNTRxXHtVJ3oNGytwUxaLikXjfyB +kApEDRFciQLkAUPFQMKNQONLRpeWxwGLK0PD4vxOHg7BxI20JANLImzNY3p819rh +GC1CG+0kZNQt1FGx/0K5Gnm5NlLUZhjqbCif6Lo2DSXhFKj39hOgxbq/IpJhOBOp +SaE10tGS7usXqQ3Efp3xbpw5Q69w9wXWO0y8eDQgy94lkxR4ChJqV4CkSitNpVnr +7uGpqCjzwvfhsAz9FmT3fVZwX+3o6CoFVjFcWMM/AaOJXdBRdQjL35zCkRCd0cqb +zYnh0J896RFazhiJdNybxeSwRyRuRYxLVSOlOQIDAQABAoIBAHrHdq5nOnPquHjG +n+jbK3IAWHc+fqzyytkBS9BO4P0iJvrVES+uCcSRgBQGAiSIQNfLUJtmzh5uCDlw +u/EuQWQasKlWTjYSimtfEHDnAITOQ9n/mbzI6IZZlrxAVc70V7Le/tEF4ZeDb+m2 +HyyJWzfVQPHMY6btkhtAAZwlLD7cO6fMOCY5OdiQwpAI6/DbnfadqSfF9SehfJnn +tSVOZDRI18/S9atUUsdTobFnMJKfD9v/RNvaandYmTtXSvvd7x7sVAtvXb/4vc99 +h8GV2qXq4QFJmoiUjZz/3k2gps055EOiVlOSBwN5lsixRgu3s0q0ExgEgVZKKK61 +0XRuu40CgYEA6dmbgEllN8zRq3K7L940POgAb02D8Ve2hwvA74QM1pCPnG2IV6Kq +1GuvX/q0Y9sDhi3639/weBLdmGGe/bGCpHJNska9i+CNUYWqJWdqmhUxZwWvsxAM +MT9btmsaKzi0h5gXbt1cd/Sc3XZ0VLVTeolEjIkpXW6k7a0DGv+opHMCgYEA5JEe +xU/o/FFSq6LuKd/o5uANTGu5VO/5OyYnaFwaRVdwCiqY6V1U7vEUAaWrDwVfwo6X +0N8uDR+X46/wck4FGhu4tn3RExtKJ22wqBFH4GctY0kW1pn58WwJP4hnsvIxC8ea +LEf73nQ6RBvBCraj1qXgklyqXNZqZh6FWShlUKMCgYAxR9V4VH9LIhOxcM03yjjx ++OZWjlxW2OgzT2vKTeaytOY+zepkpXggqczciiXHwVjdxwTOdw/jDf665R+vUOA6 +LYhoIHZDav/9BREVLtUNQZgWoOEk+CXJnxOBvtJV9248APY3O9kHwnnoRdVAILJF +6XWTtDgOY4nIOKSl5OiyNwKBgQCqne2DSmBXoPj+KFRyK7N2TD+BEVS6I5w+2/9E +tqPhqSB4Zk3YvZO9/XV6uUkUoGVYHQsUntSIdVHzY40wNi82ywS6LPcZtSBIiGf7 +PVMTHtHRsGh871mVLplEtLT77jBQY8IhVeacqtQnrTQPpEBu5Una15NQU7n+2r+d +yzq90wKBgQCfr3RC7mFJ2Ul5UB4PL6W/KyIAANde9FUfR5AUmVJCLCA00Bmy3eBE +ibJxnBNeGHiyA91c//TraHqiRzwm+wBxKA8Kx5b3BfZrtbAyviPflmK9BikB+ScS +CKsroe8pMMQkAnL8wyG5YyrSwmpRGu9nleIyL9Nw36cTGCg8pngmPA== +-----END RSA PRIVATE KEY----- diff --git a/test/test_across_nat.py b/test/test_across_nat.py new file mode 100755 index 0000000..5d6bc62 --- /dev/null +++ b/test/test_across_nat.py @@ -0,0 +1,17 @@ +from appPublic.across_nat import * +import asyncio + +async def run(): + an = AcrossNat() + an.pmp_supported = False + x = await an.get_external_ip() + print('external ip=', x) + print('pmp_supported=', an.pmp_supported, + 'upnp_supported=', an.upnp_supported) + for p in [ 8080, 8081 ]: + x = await an.map_port(p, 'TCP') + print(p, '-->', x) + +loop = asyncio.get_event_loop() +loop.run_until_complete(run()) + diff --git a/test/test_aioupnp.py b/test/test_aioupnp.py new file mode 100755 index 0000000..99ba7cb --- /dev/null +++ b/test/test_aioupnp.py @@ -0,0 +1,31 @@ + +import asyncio +from aioupnp.upnp import UPnP + +async def main(): + upnp = await UPnP.discover() + print(dir(upnp)) + print('gateway=', upnp.gateway, upnp.gateway_address, upnp.lan_address) + print(await upnp.get_external_ip()) + port = 40009 + while port < 41000: + x = await upnp.get_specific_port_mapping(40009, 'TCP') + if len(x) == 0: + print(port, 'port available') + break + else: + print(port, 'port occupied') + port += 1 + + print("adding a port mapping") + + x = await upnp.add_port_mapping(port, 'TCP', 8999, '192.168.1.8', 'test mapping') + print(8999, '-->', port) + print('x=', x, await upnp.get_redirects()) + + # print("deleting the port mapping") + # await upnp.delete_port_mapping(51234, 'TCP') + # print(await upnp.get_redirects()) + + +asyncio.run(main()) diff --git a/test/time_test.py b/test/time_test.py new file mode 100755 index 0000000..4e05fa3 --- /dev/null +++ b/test/time_test.py @@ -0,0 +1,13 @@ +import time + +def f(timeout=2): + t1 = time.time() + t = t1 + t1 += timeout + while t1 > t: + print(t, t1) + time.sleep(1) + t = time.time() + +if __name__ == '__main__': + f() diff --git a/test/zmqreply.py b/test/zmqreply.py new file mode 100755 index 0000000..a34aa5b --- /dev/null +++ b/test/zmqreply.py @@ -0,0 +1,15 @@ +from appPublic.zmq_reqrep import ZmqReplier +import time + +def handler(b): + print(b.decode('utf-8')) + return 'got it' + +x = ZmqReplier('tcp://127.0.0.1:5555', handler) +x.run() +f = 0 +while 1: + time.sleep(0.1) + +x.stop() +