diff --git a/.coveragerc b/.coveragerc new file mode 100644 index 0000000..be9958c --- /dev/null +++ b/.coveragerc @@ -0,0 +1,2 @@ +[run] +omit = *tests*,python/* \ No newline at end of file diff --git a/.gitignore b/.gitignore index ef5fa94..911a918 100644 --- a/.gitignore +++ b/.gitignore @@ -1,6 +1,7 @@ *.pyc .cache .coverage +htmlcov/* .pytest_cache .mypy_cache/ __pycache__ @@ -24,7 +25,7 @@ webdriver/* .idea/* venv/* tests/data/mbplugin/* -BalanceHistory.sqlite +*.sqlite balance.html plugin/debug.log mbplugin*.zip diff --git a/build.bat b/build.bat index f671656..d28f2a0 100644 --- a/build.bat +++ b/build.bat @@ -2,10 +2,23 @@ %~d0 @REM clean: @REM git clean -fXd +set "ptime= " +where ptime +if %errorlevel%==0 set ptime=ptime if NOT "%1"=="" goto %1 ECHO RUN build clean/test/build/fixup +goto :EOF +@REM @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ test +:coverage +%~d0 +cd "%~dp0" +call python\python -m coverage run -m pytest tests %2 %3 %4 %5 %6 %7 %8 %9 +echo coverage html +call python\python -m coverage html +@rem call start htmlcov\index.html +echo %errorlevel% goto :EOF @REM @@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ test :test diff --git a/changelist.md b/changelist.md index e26aa8b..e06d318 100644 --- a/changelist.md +++ b/changelist.md @@ -429,3 +429,7 @@ FIX: Tele2 исправлена работа через Chrome (напомню FIX: Beeline подправил расчет интернета FIX: Sipnet в процессе получения ругается на неправильный сертификат, пришлось отключить проверку сертификатов. FIX: Похоже поменялась страница входа в beeline_uz поправил, но проверить не могу, т.к. не имею телефонов + +## mbplugin v1.00.38 (24.10.22) refactoring и консольные команды +REFACTORING: Переработано большое количество кода, с целью большей управляемости, но без изменения функционала, pytest+coverage +ADD: Диагностические сообщения и коды возврата для консольной команды mbp get-balance и check-plugin diff --git a/docker/requirements_pytest.txt b/docker/requirements_pytest.txt index a644c34..bd94f68 100644 --- a/docker/requirements_pytest.txt +++ b/docker/requirements_pytest.txt @@ -1,5 +1,6 @@ atomicwrites==1.4.0 attrs==21.4.0 +coverage==6.5.0 iniconfig==1.1.1 packaging==21.3 pluggy==1.0.0 diff --git a/plugin/beeline_uz.py b/plugin/beeline_uz.py index 557b7e9..d162a04 100644 --- a/plugin/beeline_uz.py +++ b/plugin/beeline_uz.py @@ -26,7 +26,7 @@ def data_collector(self): {'name': 'TarifPlan', 'url_tag': ['/dashboard$'], 'jsformula': "data.subUsers[0].pricePlan.ru"}, {'name': 'UserName', 'url_tag': ['/dashboard$'], 'jsformula': "data.subUsers[0].fio"}, {'name': 'Internet', 'url_tag': ['/dashboard$'], 'jsformula': "parseFloat(data.usages.filter(el => el.belongsTo=='GPRS_PACK_2')[0].leftover/1024/1024).toFixed(2)"}, - {'name': 'SMS', 'url_tag': ['/dashboard$'], 'jsformula': "parseFloat(data.usages.filter(el => el.belongsTo=='SMS_ACTIVE')[0].leftover).toFixed(0)"}, + {'name': 'SMS', 'url_tag': ['/dashboard$'], 'jsformula': "parseFloat(data.usages.filter(el => el.belongsTo.startsWith('SMS'))[0].leftover).toFixed(0)"}, {'name': 'Min', 'url_tag': ['/dashboard$'], 'jsformula': "parseFloat(data.usages.filter(el => el.belongsTo=='CBM_ON_MINUTE_BALANCE')[0].leftover/60).toFixed(0)"}, {'name': 'LicSchet', 'url_tag': ['/dashboard$'], 'jsformula': "data.subUsers[0].subAccount"}, {'name': 'BlockStatus', 'url_tag': ['/dashboard$'], 'jsformula': "data.state"}, diff --git a/plugin/dbengine.py b/plugin/dbengine.py index 4344186..78b08c2 100644 --- a/plugin/dbengine.py +++ b/plugin/dbengine.py @@ -135,12 +135,17 @@ class Dbengine(): - def __init__(self, dbname=None, updatescheme=True, fast=False): + 'Класс для работы с базой, внутри одного экземпляра используется только один курсор, для нескольких курсоров используйте несколько экземпляров' + + def __init__(self, dbname=None, updatescheme=True, fast=False, row_factory=None, make_headers=True): 'fast - быстрее, но менее безопасно' if dbname is None: dbname = store.abspath_join(settings.mbplugin_ini_path, 'BalanceHistory.sqlite') self.dbname = dbname + logging.debug(f'Db open {self.dbname}') self.conn = sqlite3.connect(self.dbname) # detect_types=sqlite3.PARSE_DECLTYPES + if row_factory is not None: + self.conn.row_factory = row_factory self.cur = self.conn.cursor() cache_size = int(store.options('sqlite_cache_size')) if cache_size != 0: @@ -150,17 +155,33 @@ def __init__(self, dbname=None, updatescheme=True, fast=False): self.conn.commit() if updatescheme: self.check_and_add_addition() - rows = self.cur.execute('SELECT * FROM phones limit 1;') - self.phoneheader = list(zip(*rows.description))[0] + if make_headers: + self.cur.execute('SELECT * FROM phones limit 1;') + self.phoneheader = list(zip(*self.cur.description))[0] + + def __enter__(self): + logging.debug(f'Db __enter__ {self.dbname}') + return self + + def __exit__(self, type, value, traceback): + logging.info(f'Db __exit__ close {self.dbname}') + self.conn.close() def cur_execute(self, query, *args, **kwargs): - 'Обертка для cur.execute c логированием и таймингом' + 'Обертка для cur.execute(...).fetchall() c логированием и таймингом сразу возврящает list с результатом' t_start = time.process_time() - res = self.cur.execute(query, *args, **kwargs) + res = self.cur.execute(query, *args, **kwargs).fetchall() logging.debug(f'{query} {args} {kwargs}') logging.debug(f'Execution time {time.process_time()-t_start:.6f}') return res + def cur_execute_00(self, query, *args, **kwargs): + 'cur.execute(...).fetchall()[0][0] ' + res = self.cur_execute(query, *args, **kwargs) + if len(res) != 0: + return res[0][0] + return None + def write_result(self, plugin, login, result, commit=True): 'Записывает результат в базу' # Делаем копию, чтобы не трогать оригинал @@ -182,12 +203,13 @@ def write_result(self, plugin, login, result, commit=True): line['Operator'] = plugin line['PhoneNumber'] = login # PhoneNumber=PhoneNum line['QueryDateTime'] = datetime.datetime.now().replace(microsecond=0) # no microsecond - self.cur.execute(f"select cast(julianday('now')-julianday(max(QueryDateTime)) as integer) from phones where phonenumber='{login}' and operator='{plugin}' and abs(balance-({result['Balance']}))>0.02") - line['NoChangeDays'] = self.cur.fetchall()[0][0] # Дней без изм. + # Дней без изм. + query = f"select cast(julianday('now')-julianday(max(QueryDateTime)) as integer) from phones where phonenumber='{login}' and operator='{plugin}' and abs(balance-({result['Balance']}))>0.02" + line['NoChangeDays'] = self.cur_execute(query)[0][0] if line['NoChangeDays'] is None: # Если баланс не менялся ни разу - то ориентируемся на самый первый запрос баланса - self.cur.execute(f"select cast(julianday('now')-julianday(min(QueryDateTime)) as integer) from phones where phonenumber='{login}' and operator='{plugin}' ") - line['NoChangeDays'] = self.cur.fetchall()[0][0] + query = f"select cast(julianday('now')-julianday(min(QueryDateTime)) as integer) from phones where phonenumber='{login}' and operator='{plugin}' " + line['NoChangeDays'] = self.cur_execute(query)[0][0] # Если записей по этому номеру совсем нет (первый запрос), тогда просто ставим 0 line['NoChangeDays'] = line['NoChangeDays'] if line['NoChangeDays'] is not None else 0 try: @@ -195,16 +217,13 @@ def write_result(self, plugin, login, result, commit=True): average_days = int(options_ini['Additional']['AverageDays']) except Exception: average_days = int(store.options('average_days')) - self.cur.execute(f"select {line['Balance']}-balance from phones where phonenumber='{login}' and operator='{plugin}' and QueryDateTime>date('now','-{average_days} day') and strftime('%Y%m%d', QueryDateTime)<>strftime('%Y%m%d', date('now')) order by QueryDateTime desc limit 1") - qres = self.cur.fetchall() + qres = self.cur_execute(f"select {line['Balance']}-balance from phones where phonenumber='{login}' and operator='{plugin}' and QueryDateTime>date('now','-{average_days} day') and strftime('%Y%m%d', QueryDateTime)<>strftime('%Y%m%d', date('now')) order by QueryDateTime desc limit 1") if qres != []: line['BalDelta'] = round(qres[0][0], 2) # Delta (день) - self.cur.execute(f"select {line['Balance']}-balance from phones where phonenumber='{login}' and operator='{plugin}' order by QueryDateTime desc limit 1") - qres = self.cur.fetchall() + qres = self.cur_execute(f"select {line['Balance']}-balance from phones where phonenumber='{login}' and operator='{plugin}' order by QueryDateTime desc limit 1") if qres != []: line['BalDeltaQuery'] = round(qres[0][0], 2) # Delta (запрос) - self.cur.execute(f"select avg(b) from (select min(BalDelta) b from phones where phonenumber='{login}' and operator='{plugin}' and QueryDateTime>date('now','-{average_days} day') group by strftime('%Y%m%d', QueryDateTime))") - qres = self.cur.fetchall() + qres = self.cur_execute(f"select avg(b) from (select min(BalDelta) b from phones where phonenumber='{login}' and operator='{plugin}' and QueryDateTime>date('now','-{average_days} day') group by strftime('%Y%m%d', QueryDateTime))") if qres != [] and qres[0][0] is not None: line['RealAverage'] = round(qres[0][0], 2) # $/День(Р) if line.get('RealAverage', 0.0) < 0: @@ -218,9 +237,8 @@ def write_result(self, plugin, login, result, commit=True): def report(self): ''' Генерирует отчет по последнему состоянию телефонов''' reportsql = f'''select * from phones where NN in (select NN from (SELECT NN,max(QueryDateTime) FROM Phones GROUP BY PhoneNumber,Operator)) order by PhoneNumber,Operator''' - cur = self.cur_execute(reportsql) - dbheaders = list(zip(*cur.description))[0] - dbdata = cur.fetchall() + dbdata = self.cur_execute(reportsql) + dbheaders = list(zip(*self.cur.description))[0] phones = store.ini('phones.ini').phones() dbdata.sort(key=lambda line: (phones.get(line[0:2], {}).get('NN', 999))) # округляем float до 2х знаков @@ -244,9 +262,8 @@ def history(self, phone_number, operator, days=7, lastonly=1, pkey=None): if days == 0: return [] historysql = f'''select * from phones where phonenumber=? and operator=? and QueryDateTime>date('now','-'|| ? ||' day') order by QueryDateTime desc''' - cur = self.cur_execute(historysql, [phone_number, operator, days]) - dbheaders = list(zip(*cur.description))[0] - dbdata = cur.fetchall() + dbdata = self.cur_execute(historysql, [phone_number, operator, days]) + dbheaders = list(zip(*self.cur.description))[0] dbdata_sets = [set(el) for el in zip(*dbdata)] # составляем список уникальных значений по каждой колонке dbdata_sets = [{i for i in el if str(i).strip() not in ['', 'None', '0.0', '0']} for el in dbdata_sets] # подправляем косяки if len(dbdata_sets) == 0: # Истории нет - возвращаем пустой @@ -268,8 +285,7 @@ def check_and_add_addition(self): 'Создаем таблицы, добавляем новые поля, и нужные индексы если их нет' [self.cur.execute(query) for query in DB_SCHEMA] for k, v in addition_phone_fields.items(): - self.cur.execute("SELECT COUNT(*) AS CNTREC FROM pragma_table_info('phones') WHERE name=?", [k]) - if self.cur.fetchall()[0][0] == 0: + if self.cur_execute("SELECT COUNT(*) AS CNTREC FROM pragma_table_info('phones') WHERE name=?", [k])[0][0] == 0: self.cur.execute(f"ALTER TABLE phones ADD COLUMN {k} {v}") for idx in addition_indexes: self.cur.execute(f"CREATE INDEX IF NOT EXISTS {idx}") @@ -278,27 +294,29 @@ def check_and_add_addition(self): def copy_data(self, path: str): 'Копирует данные по запросам из другой БД sqlite, может быть полезно когда нужно объединить данные с нескольких источников' try: - src_conn = sqlite3.connect(path) - src_conn.row_factory = sqlite3.Row - src_cur = src_conn.cursor() - table = 'Phones' - current_data = set(self.cur.execute('select distinct PhoneNumber,Operator,QueryDateTime from phones').fetchall()) - print(len(current_data)) - print("Copying Phones %s => %s" % (path, self.dbname)) - sc = src_cur.execute('SELECT * FROM %s' % table) - ins = None - dc = self.cur - cnt_write, cnt_skip = 0, 0 - for row in sc.fetchall(): - if not ins: - cols = tuple([k for k in row.keys() if k != 'id']) - ins = 'INSERT OR REPLACE INTO Phones %s VALUES (%s)' % (cols, ','.join(['?'] * len(cols))) - if (row['PhoneNumber'], row['Operator'], row['QueryDateTime']) in current_data: - cnt_skip += 1 - else: - c = [row[c] for c in cols] - res = dc.execute(ins, c) - cnt_write += res.rowcount + logging.debug(f'Open db {path}') + if not os.path.exists(path): + raise FileNotFoundError + with sqlite3.connect(path) as src_conn: + src_conn.row_factory = sqlite3.Row + src_cur = src_conn.cursor() + table = 'Phones' + current_data = set(self.cur_execute('select distinct PhoneNumber,Operator,QueryDateTime from phones')) + print(len(current_data)) + print("Copying Phones %s => %s" % (path, self.dbname)) + src_cur.execute('SELECT * FROM %s' % table) + ins = None + cnt_write, cnt_skip = 0, 0 + for row in src_cur.fetchall(): + if not ins: + cols = tuple([k for k in row.keys() if k != 'id']) + ins = 'INSERT OR REPLACE INTO Phones %s VALUES (%s)' % (cols, ','.join(['?'] * len(cols))) + if (row['PhoneNumber'], row['Operator'], row['QueryDateTime']) in current_data: + cnt_skip += 1 + else: + c = [row[c] for c in cols] + res = self.cur.execute(ins, c) + cnt_write += res.rowcount print(f'Update {cnt_write} row, skip {cnt_skip} row') self.conn.commit() except Exception: @@ -315,13 +333,19 @@ def __init__(self, dbname=None): DRV = '{Microsoft Access Driver (*.mdb)}' self.conn = pyodbc.connect(f'DRIVER={DRV};DBQ={dbname}') self.cur = self.conn.cursor() - rows = self.cur.execute('SELECT top 1 * FROM phones') - self.phoneheader = list(zip(*rows.description))[0] + self.cur.execute('SELECT top 1 * FROM phones') + self.phoneheader = list(zip(*self.cur.description))[0] phones_ini = store.ini('phones.ini').read() # phones - словарь key=MBphonenumber values=[phonenumber,region] self.phones = {v['Number']: (re.sub(r' #\d+', '', v['Number']), v['Region']) for k, v in phones_ini.items() if k.isnumeric() and 'Monitor' in v} + def __enter__(self): + return self + + def __exit__(self, type, value, traceback): + self.conn.close() + def to_sqlite(self, line): '''конвертирует строчку для sqlite: Убираем последовательный номер NN @@ -350,85 +374,83 @@ def update_sqlite_from_mdb_core(dbname=None, deep=None) -> bool: if deep is None: deep = int(store.options('updatefrommdbdeep')) # читаем sqlite БД - db = Dbengine(fast=True) - mdb = Mdbengine(dbname) # BalanceHistory.mdb - # Дата согласно указанному deep от которой сверяем данные - dd = datetime.datetime.now() - datetime.timedelta(days=deep) - logging.debug(f'Read from sqlite QueryDateTime>{dd}') - db.cur.execute("SELECT * FROM phones where QueryDateTime>?", [dd]) - sqldata = db.cur.fetchall() - dsqlite = {datetime.datetime.strptime(i[db.phoneheader.index('QueryDateTime')].split('.')[0], '%Y-%m-%d %H:%M:%S').timestamp(): i for i in sqldata} - # теперь все то же самое из базы MDB - logging.debug(f'Read from mdb QueryDateTime>{dd}') - mdb.cur.execute("SELECT * FROM phones where QueryDateTime>?", [dd]) - mdbdata = mdb.cur.fetchall() - dmdb = {i[mdb.phoneheader.index('QueryDateTime')].timestamp(): i for i in mdbdata} - logging.debug('calculate') - # Строим общий список timestamp всех данных - allt = sorted(set(list(dsqlite) + list(dmdb))) - # обрабатываем и составляем пары данных которые затем будем подправлять - pairs = [] # mdb timestamp, sqlite timestamp - while allt: - # берем одну строчку из общего списка - c = allt.pop(0) - # Если для этого timestamp есть строчка добавляем из - pair = [c if c in dmdb else None, c if c in dsqlite else None] - if allt == [] or allt[0] in dmdb and pair[0] is not None or allt[0] in dsqlite and pair[1] is not None: - # Это следующая строка или была последняя - pairs.append(pair) - elif allt[0] in dmdb and pair[0] is None and allt[0] - c < 10: - # следующий timestamp это пара MDB к записи sqlite ? - pair[0] = allt.pop(0) - pairs.append(pair) - elif allt[0] in dsqlite and pair[1] is None and allt[0] - c < 10: - # следующий timestamp это пара sqlite к записи mdb ? - pair[1] = allt.pop(0) - pairs.append(pair) - logging.debug('Before:') - logging.debug(f'Difference time:{len([1 for a,b in pairs if a!=b and a is not None and b is not None])}') - logging.debug(f'Only mdb:{len([1 for a,b in pairs if b is None])}') - logging.debug(f'Only sqlite:{len([1 for a,b in pairs if a is None])}') - update_param = [] - insert_param, insert_header = [], [] - for num, [mdb_ts, sqlite_ts] in enumerate(pairs): - if mdb_ts != sqlite_ts and mdb_ts is not None and sqlite_ts is not None: - # исправляем время в sqlite чтобы совпадало с mdb - update_param.append([datetime.datetime.fromtimestamp(mdb_ts), datetime.datetime.fromtimestamp(sqlite_ts)]) - pairs[num][1] = mdb_ts - elif mdb_ts is not None and sqlite_ts is None: - # Копируем несуществующие записи в sqlite из mdb - header, line = mdb.to_sqlite(dmdb[mdb_ts]) - insert_param.append(line) - insert_header = header - pairs[num][1] = mdb_ts - - # есть что вставить ? - logging.debug(f'Insert {len(insert_param)}') - if insert_param: - db.cur.executemany(f'insert into phones ({",".join(insert_header)}) VALUES ({",".join(list("?"*len(insert_header)))})', insert_param) - db.conn.commit() + with Dbengine(fast=True) as db, Mdbengine(dbname) as mdb: # BalanceHistory.mdb + # Дата согласно указанному deep от которой сверяем данные + dd = datetime.datetime.now() - datetime.timedelta(days=deep) + logging.debug(f'Read from sqlite QueryDateTime>{dd}') + sqldata = db.cur_execute("SELECT * FROM phones where QueryDateTime>?", [dd]) + dsqlite = {datetime.datetime.strptime(i[db.phoneheader.index('QueryDateTime')].split('.')[0], '%Y-%m-%d %H:%M:%S').timestamp(): i for i in sqldata} + # теперь все то же самое из базы MDB + logging.debug(f'Read from mdb QueryDateTime>{dd}') + mdb.cur.execute("SELECT * FROM phones where QueryDateTime>?", [dd]) + mdbdata = mdb.cur.fetchall() + dmdb = {i[mdb.phoneheader.index('QueryDateTime')].timestamp(): i for i in mdbdata} + logging.debug('calculate') + # Строим общий список timestamp всех данных + allt = sorted(set(list(dsqlite) + list(dmdb))) + # обрабатываем и составляем пары данных которые затем будем подправлять + pairs = [] # mdb timestamp, sqlite timestamp + while allt: + # берем одну строчку из общего списка + c = allt.pop(0) + # Если для этого timestamp есть строчка добавляем из + pair = [c if c in dmdb else None, c if c in dsqlite else None] + if allt == [] or allt[0] in dmdb and pair[0] is not None or allt[0] in dsqlite and pair[1] is not None: + # Это следующая строка или была последняя + pairs.append(pair) + elif allt[0] in dmdb and pair[0] is None and allt[0] - c < 10: + # следующий timestamp это пара MDB к записи sqlite ? + pair[0] = allt.pop(0) + pairs.append(pair) + elif allt[0] in dsqlite and pair[1] is None and allt[0] - c < 10: + # следующий timestamp это пара sqlite к записи mdb ? + pair[1] = allt.pop(0) + pairs.append(pair) + logging.debug('Before:') + logging.debug(f'Difference time:{len([1 for a,b in pairs if a!=b and a is not None and b is not None])}') + logging.debug(f'Only mdb:{len([1 for a,b in pairs if b is None])}') + logging.debug(f'Only sqlite:{len([1 for a,b in pairs if a is None])}') + update_param = [] + insert_param, insert_header = [], [] + for num, [mdb_ts, sqlite_ts] in enumerate(pairs): + if mdb_ts != sqlite_ts and mdb_ts is not None and sqlite_ts is not None: + # исправляем время в sqlite чтобы совпадало с mdb + update_param.append([datetime.datetime.fromtimestamp(mdb_ts), datetime.datetime.fromtimestamp(sqlite_ts)]) + pairs[num][1] = mdb_ts + elif mdb_ts is not None and sqlite_ts is None: + # Копируем несуществующие записи в sqlite из mdb + header, line = mdb.to_sqlite(dmdb[mdb_ts]) + insert_param.append(line) + insert_header = header + pairs[num][1] = mdb_ts - # есть что проапдейтить ? - logging.debug(f'Update {len(update_param)}') - if update_param: - db.cur.executemany('update phones set QueryDateTime=? where QueryDateTime=?', update_param) - db.conn.commit() + # есть что вставить ? + logging.debug(f'Insert {len(insert_param)}') + if insert_param: + db.cur.executemany(f'insert into phones ({",".join(insert_header)}) VALUES ({",".join(list("?"*len(insert_header)))})', insert_param) + db.conn.commit() + + # есть что проапдейтить ? + logging.debug(f'Update {len(update_param)}') + if update_param: + db.cur.executemany('update phones set QueryDateTime=? where QueryDateTime=?', update_param) + db.conn.commit() - # дополнительные фиксы (у меня в mdb мусор оказался, чтобы не трогать mdb чистим здесь) - for sql in addition_queries: - db.cur.execute(sql) + # дополнительные фиксы (у меня в mdb мусор оказался, чтобы не трогать mdb чистим здесь) + for sql in addition_queries: + db.cur.execute(sql) + db.conn.commit() + # прописываем колонку mbnumber + update_mbnumber = [[MBphonenumber, phonenumber, region] for MBphonenumber, (phonenumber, region) in mdb.phones.items()] + db.cur.executemany(f'update phones set MBPhonenumber=? where MBPhonenumber is null and Phonenumber=? and operator=?', update_mbnumber) + logging.debug(f'Update empty MBPhonenumber {db.cur.rowcount}:') db.conn.commit() - # прописываем колонку mbnumber - update_mbnumber = [[MBphonenumber, phonenumber, region] for MBphonenumber, (phonenumber, region) in mdb.phones.items()] - db.cur.executemany(f'update phones set MBPhonenumber=? where MBPhonenumber is null and Phonenumber=? and operator=?', update_mbnumber) - logging.debug(f'Update empty MBPhonenumber {db.cur.rowcount}:') - db.conn.commit() - - logging.debug(f'After:') - logging.debug(f'Difference time:{len([1 for a,b in pairs if a!=b and a is not None and b is not None])}') - logging.debug(f'Only mdb:{len([1 for a,b in pairs if b is None])}') - logging.debug(f'Only sqlite:{len([1 for a,b in pairs if a is None])}') - logging.debug(f'Update complete') + + logging.debug(f'After:') + logging.debug(f'Difference time:{len([1 for a,b in pairs if a!=b and a is not None and b is not None])}') + logging.debug(f'Only mdb:{len([1 for a,b in pairs if b is None])}') + logging.debug(f'Only sqlite:{len([1 for a,b in pairs if a is None])}') + logging.debug(f'Update complete') return True @@ -445,9 +467,9 @@ def write_result_to_db(plugin, login, result): 'пишем в базу если в ini установлен sqlitestore=1' try: if store.options('sqlitestore') == '1': - db = Dbengine() - logging.info(f'Пишем в базу {db.dbname}') - db.write_result(plugin, login, result) + with Dbengine() as db: + logging.info(f'Пишем в базу {db.dbname}') + db.write_result(plugin, login, result) except AttributeError: logging.info(f'Отсутствуют параметры {store.exception_text()} дополнительные действия не производятся') except Exception: @@ -465,30 +487,28 @@ def flags(cmd, key=None, value=None): try: if store.options('sqlitestore') == '1': logging.debug(f'Flag:{cmd}') - db = Dbengine() - if cmd.lower() == 'set': - db.cur.execute('REPLACE INTO flags(key,value) VALUES(?,?)', [key, value]) - db.conn.commit() - if cmd.lower() == 'setunic': - db.cur.execute('delete from flags where value=?', [value]) - db.conn.commit() - db.cur.execute('REPLACE INTO flags(key,value) VALUES(?,?)', [key, value]) - db.conn.commit() - elif cmd.lower() == 'get': - db.cur.execute('select value from flags where key=?', [key]) - qres = db.cur.fetchall() - if len(qres) > 0: - return qres[0][0] - elif cmd.lower() == 'getall': - db.cur.execute('select * from flags') - qres = db.cur.fetchall() - return {k: v for k, v in qres} - elif cmd.lower() == 'deleteall': - db.cur.execute('delete from flags') - db.conn.commit() - elif cmd.lower() == 'delete': - db.cur.execute('delete from flags where key=?', [key]) - db.conn.commit() + with Dbengine() as db: + if cmd.lower() == 'set': + db.cur.execute('REPLACE INTO flags(key,value) VALUES(?,?)', [key, value]) + db.conn.commit() + if cmd.lower() == 'setunic': + db.cur.execute('delete from flags where value=?', [value]) + db.conn.commit() + db.cur.execute('REPLACE INTO flags(key,value) VALUES(?,?)', [key, value]) + db.conn.commit() + elif cmd.lower() == 'get': + qres = db.cur_execute('select value from flags where key=?', [key]) + if len(qres) > 0: + return qres[0][0] + elif cmd.lower() == 'getall': + qres = db.cur_execute('select * from flags') + return {k: v for k, v in qres} + elif cmd.lower() == 'deleteall': + db.cur.execute('delete from flags') + db.conn.commit() + elif cmd.lower() == 'delete': + db.cur.execute('delete from flags where key=?', [key]) + db.conn.commit() else: if cmd.lower() == 'getall': return {} @@ -502,8 +522,7 @@ def responses() -> typing.Dict[str, str]: if store.options('sqlitestore') == '1': logging.debug(f'Responses from sqlite') db = Dbengine() - db.cur.execute('select key,value from responses') - qres = db.cur.fetchall() + qres = db.cur_execute('select key,value from responses') return {k: v for k, v in qres} else: return {} diff --git a/plugin/httpserver_mobile.py b/plugin/httpserver_mobile.py index 6a3ad97..5a368ef 100644 --- a/plugin/httpserver_mobile.py +++ b/plugin/httpserver_mobile.py @@ -40,7 +40,7 @@ После включения, запустите mbplugin\\setup_and_check.bat ''' -# TODO в командах для traymeny используется os.system(f'start ... это будет работать только в windows, но пока пофигу, т.к. сам pystrayработает только в windows +# TODO в командах для traymeny используется os.system(f'start ... это будет работать только в windows, но пока пофигу, т.к. сам pystray работает только в windows TRAY_MENU = ( {'text': "Main page", 'cmd': lambda: os.system(f'start http://localhost:{store.options("port", section="HttpServer")}/main'), 'show': True}, {'text': "View report", 'cmd': lambda: os.system(f'start http://localhost:{store.options("port", section="HttpServer")}/report'), 'show': True}, @@ -58,7 +58,7 @@ ) -def getbalance_standalone_one_pass(filter: list = [], only_failed: bool = False) -> None: +def getbalance_standalone_one_pass(filter: list = [], only_failed: bool = False): ''' Получаем балансы самостоятельно без mobilebalance ОДИН ПРОХОД Если filter пустой то по всем номерам из phones.ini Если не пустой - то логин/алиас/оператор или его часть @@ -68,6 +68,7 @@ def getbalance_standalone_one_pass(filter: list = [], only_failed: bool = False) для Standalone версии в файле phones_add.ini only_failed=True - делать запросы только по тем номерам, по которым прошлый запрос был неудачный ''' + result: typing.Dict = {} phones = store.ini('phones.ini').phones() queue_balance = [] # Очередь телефонов на получение баланса for val in phones.values(): @@ -84,7 +85,8 @@ def getbalance_standalone_one_pass(filter: list = [], only_failed: bool = False) dbengine.flags('set', f'{keypair}', 'queue') # выставляем флаг о постановке в очередь store.feedback.text(f'Queued {len(queue_balance)} numbers') for val in queue_balance: - # TODO пока дергаем метод от вебсервера там уже все есть, потом может вынесем отдельно + # TODO пока дергаем метод от веб сервера там уже все есть, потом может вынесем отдельно + keypair = f"{val['Region']}_{val['Number']}" try: # проверяем на сигнал Q_CMD_CANCEL, все остальное - кладем обратно if Q_CMD_CANCEL in cmdqueue.queue: @@ -92,14 +94,17 @@ def getbalance_standalone_one_pass(filter: list = [], only_failed: bool = False) [cmdqueue.put(el) for el in qu if el != Q_CMD_CANCEL] # type: ignore logging.info(f'Receive cancel signal to query') store.feedback.text(f"Receive cancel signal") - return + return result store.feedback.text(f"Receive {val['Alias']}:{val['Region']}_{val['Number']}") - getbalance_plugin('get', {'plugin': [val['Region']], 'login': [val['Number']], 'password': [val['Password2']], 'date': ['date']}) + r1 = getbalance_plugin('get', {'plugin': [val['Region']], 'login': [val['Number']], 'password': [val['Password2']], 'date': ['date']}) + result[keypair] = 'Balance' in repr(r1) except Exception: + result[keypair] = False logging.error(f"Unsuccessful check {val['Region']} {val['Number']} {store.exception_text()}") + return result -def getbalance_standalone(filter: list = [], only_failed: bool = False, retry: int = -1, params=None) -> None: +def getbalance_standalone(filter: list = [], only_failed: bool = False, retry: int = -1, params=None): ''' Получаем балансы делая несколько проходов по неудачным retry=N количество повторов по неудачным попыткам, после запроса по всем (повторы только при only_failed=False) params добавлен чтобы унифицировать вызовы @@ -108,12 +113,14 @@ def getbalance_standalone(filter: list = [], only_failed: bool = False, retry: i logging.info(f'getbalance_standalone: filter={filter}') if retry == -1: retry = int(store.options('retry_failed', flush=True)) + result = {} if only_failed: - getbalance_standalone_one_pass(filter=filter, only_failed=True) + result.update(getbalance_standalone_one_pass(filter=filter, only_failed=True)) else: - getbalance_standalone_one_pass(filter=filter, only_failed=False) + result.update(getbalance_standalone_one_pass(filter=filter, only_failed=False)) for i in range(retry): - getbalance_standalone_one_pass(filter=filter, only_failed=True) + result.update(getbalance_standalone_one_pass(filter=filter, only_failed=True)) + return result def get_full_info_one_number(keypair: str, check: bool = False) -> str: @@ -246,9 +253,9 @@ def view_log(param): def prepare_loglist_personal(): 'Делает список пар по которым есть скриншоты' ss = glob.glob(store.abspath_join(store.options('loggingfolder'), '*.png')) - allmatch = [re.search(r'(.*)_\d+\.png', os.path.split(fn)[-1]) for fn in ss] - allgroups = sorted(set([m.groups()[0] for m in allmatch if m])) - return allgroups + all_match = [re.search(r'(.*)_\d+\.png', os.path.split(fn)[-1]) for fn in ss] + all_groups = sorted(set([m.groups()[0] for m in all_match if m])) + return all_groups def prepare_log_personal(prefix): 'Готовит html лог со скриншотами начинающимися на prefix' @@ -293,7 +300,7 @@ def pp_field(pkey, he, el, hover, unwanted=False): store.options('logginglevel', flush=True) # Запускаем, чтобы сбросить кэш и перечитать ini template_page = settings.table_template['page'] template_history = settings.table_template['history'] - temlate_style = settings.table_template['style'] + template_style = settings.table_template['style'] html_script = settings.table_template['script'] db = dbengine.Dbengine() flags = dbengine.flags('getall') # берем все флаги словарем @@ -353,8 +360,8 @@ def pp_field(pkey, he, el, hover, unwanted=False): if flags.get(f"{line['Operator']}_{line['PhoneNumber']}", '').startswith('queue'): classflag = 'n_us' html_table.append(f'{"".join(html_line)}') - temlate_style = temlate_style.replace('{HoverCss}', store.options('HoverCss')) # HoverCss общий на всю страницу, поэтому берем без pkey - res = template_page.format(style=temlate_style, html_header=html_header, html_table='\n'.join(html_table), title=store.version(), html_script=html_script) + template_style = template_style.replace('{HoverCss}', store.options('HoverCss')) # HoverCss общий на всю страницу, поэтому берем без pkey + res = template_page.format(style=template_style, html_header=html_header, html_table='\n'.join(html_table), title=store.version(), html_script=html_script) return 'text/html', [res] @@ -532,7 +539,7 @@ def restart_program(reason='', exit_only=False, delay=0): if pid_from_file == os.getpid(): os.remove(filename_pid) if not exit_only: - subprocess.Popen(cmd) # Crossplatform run process + subprocess.Popen(cmd) # Cross platform run process psutil.Process().kill() if Q_CMD_EXIT not in cmdqueue.queue: # Если есть то второй раз не кладем cmdqueue.put(Q_CMD_EXIT) # Если kill не сработал (для pid=1 не сработает) - шлем сигнал @@ -556,7 +563,7 @@ def send_http_signal(cmd, force=True): except Exception: pass # То что дальше - это вышибание процесса если web сервер не остановился - if not(cmd == 'exit' and force): + if not (cmd == 'exit' and force): return for i in range(50): # Подождем пока сервер остановится if os.path.exists(filename_pid): @@ -778,7 +785,7 @@ def stop(self): def auth_decorator(errmsg=None, nonauth: typing.Callable = None): - 'Если хотим незалогиненому выдать сообщение об ошибке - указываем его в errmsg, если без авторизации хотим вызвать другой метод - указываем его в nonauth' + 'Если хотим не залогиненому выдать сообщение об ошибке - указываем его в errmsg, если без авторизации хотим вызвать другой метод - указываем его в nonauth' def decorator(func): # pylint: disable=no-self-argument def wrapper(self, update: telegram.update.Update, context): # update.message.chat_id отсутствует у CallbackQueryHandler пробуем через update.effective_chat.id: @@ -998,7 +1005,7 @@ def get_one(self, update, context: callbackcontext.CallbackContext): """Receive one balance with inline keyboard/args, only auth user. /checkone - получаем баланс /getone - показываем""" - # Заданы агрументы? Тогда спросим по ним. + # Заданы аргументы? Тогда спросим по ним. args = ' '.join(context.args if context.args is not None else []).lower() if args != '' and update is not None: # context.args cmd = (update.effective_message.text[1:]).split(' ')[0] @@ -1047,7 +1054,7 @@ def reply(edit_text, message, keypair): self.put_text(edit_text, 'This is log') res = prepare_log_personal(keypair) message.reply_document(filename=f'{keypair}_log.htm', document=io.BytesIO(res.strip().encode('cp1251'))) - # Заданы агрументы? Тогда спросим по ним. + # Заданы аргументы? Тогда спросим по ним. args = ' '.join(context.args if context.args is not None else []).lower() # запрашиваем по заданному аргументу if args != '' and update is not None: # context.args diff --git a/plugin/mbplugin.py b/plugin/mbplugin.py index 334f534..a3b6d35 100644 --- a/plugin/mbplugin.py +++ b/plugin/mbplugin.py @@ -28,10 +28,10 @@ def main(): logging.error(exception_text) sys.stdout.write(exception_text) return -1 - if len(sys.argv) == 4: # plugin login password + if len(sys.argv) == 4: # plugin login password login = sys.argv[2] password = sys.argv[3] - else: # request указан в переменной RequestVariable ? + else: # request указан в переменной RequestVariable ? try: RequestVariable = os.environ['RequestVariable'].strip(' "') root = etree.fromstring(RequestVariable) @@ -46,7 +46,7 @@ def main(): # Запуск плагина logging.info(f'Start {lang} {plugin} {login}') - dbengine.flags('setunic',f'{lang}_{plugin}_{login}','start') # выставляем флаг о начале запроса + dbengine.flags('setunic', f'{lang}_{plugin}_{login}', 'start') # выставляем флаг о начале запроса try: storename = re.sub(r'\W', '_', f'{lang}_{plugin}_{login}') pkey = (login, f'{lang}_{plugin}') # Пара (номер, оператор) @@ -57,7 +57,7 @@ def main(): exception_text = f'Ошибка при вызове модуля \n{plugin}: {store.exception_text()}' logging.error(exception_text) sys.stdout.write(exception_text) - dbengine.flags('set',f'{lang}_{plugin}_{login}','error call') # выставляем флаг о ошибке вызова + dbengine.flags('set', f'{lang}_{plugin}_{login}', 'error call') # выставляем флаг о ошибке вызова return -1 # Готовим результат try: @@ -66,9 +66,9 @@ def main(): exception_text = f'Ошибка при подготовке результата: {store.exception_text()}' logging.error(exception_text) sys.stdout.write(exception_text) - dbengine.flags('set',f'{lang}_{plugin}_{login}','error result') # выставляем флаг о плохом результате] + dbengine.flags('set', f'{lang}_{plugin}_{login}', 'error result') # выставляем флаг о плохом результате] return -1 - dbengine.flags('delete',f'{lang}_{plugin}_{login}','start') # запрос завершился успешно - сбрасываем флаг + dbengine.flags('delete', f'{lang}_{plugin}_{login}', 'start') # запрос завершился успешно - сбрасываем флаг try: # пишем в базу dbengine.write_result_to_db(f'{lang}_{plugin}', login, result) diff --git a/plugin/test1.py b/plugin/test1.py index 21eb196..3902b5e 100644 --- a/plugin/test1.py +++ b/plugin/test1.py @@ -4,40 +4,43 @@ import requests import store -def get_balance(login, password, storename=None, **kwargs): +def get_balance(login, password, storename=None, wait=True, **kwargs): ''' На вход логин и пароль, на выходе словарь с результатами ''' result = {} session = store.Session(storename) - result = {'Balance': 124.45 + random.randint(1,5), # double - 'Balance2': 22, # double - 'Balance3': 33, # double - 'LicSchet': 'Лицевой счет', - 'UserName': 'ФИО', - 'TarifPlan': 'Тарифный план', - 'BlockStatus': 'Статус блокировки', - 'AnyString': 'Любая строка', - 'SpendBalance': 12, # double Потрачено средств - 'KreditLimit': 23, # double Кредитный лимит - 'Currenc': 'Валюта', - 'Average': 5, # double Средний расход в день - 'TurnOff': 20, # дней до отключения - 'TurnOffStr': 'Ожидаемая дата отключения', - 'Recomend': 54, # double Рекомендовано оплатить - 'SMS': 43, # !!! integer Кол-во оставшихся/потраченных СМС - 'Min': 222, # !!! integer Кол-во оставшихся минут - 'SpendMin': 32, # double Кол-во потраченных минут (с секундами) - 'Expired': 'Дата истечения баланса/платежа', # BalExpired->BeeExpired, Expired->BeeExpired - 'ObPlat': 14, # double Сумма обещанного платежа - 'Internet': 1234.45, # double Кол-во оставшегося/потраченного трафика - # 'ErrorMsg': 'Сообщение об ошибке', # Если оо есть в Reponce то это ошибка - 'UslugiOn': '2/8', - 'UslugiList': 'Услуга1\t10р\nУслуга2\t20р\nУслуга3\t30р\nУслуга4\t40р' # Это будет показано в hover, если включено - } + result = { + 'Balance': 124.45 + random.randint(1, 5), # double + 'Balance2': 22, # double + 'Balance3': 33, # double + 'LicSchet': 'Лицевой счет', + 'UserName': 'ФИО', + 'TarifPlan': 'Тарифный план', + 'BlockStatus': 'Статус блокировки', + 'AnyString': 'Любая строка', + 'SpendBalance': 12, # double Потрачено средств + 'KreditLimit': 23, # double Кредитный лимит + 'Currenc': 'Валюта', + 'Average': 5, # double Средний расход в день + 'TurnOff': 20, # дней до отключения + 'TurnOffStr': 'Ожидаемая дата отключения', + 'Recomend': 54, # double Рекомендовано оплатить + 'SMS': 43, # !!! integer Кол-во оставшихся/потраченных СМС + 'Min': 222, # !!! integer Кол-во оставшихся минут + 'SpendMin': 32, # double Кол-во потраченных минут (с секундами) + # BalExpired->BeeExpired, Expired->BeeExpired + 'Expired': 'Дата истечения баланса/платежа', + 'ObPlat': 14, # double Сумма обещанного платежа + 'Internet': 1234.45, # double Кол-во оставшегося/потраченного трафика + # 'ErrorMsg': 'Сообщение об ошибке', # Если оо есть в Reponce то это ошибка + 'UslugiOn': '2/8', + # Это будет показано в hover, если включено + 'UslugiList': 'Услуга1\t10р\nУслуга2\t20р\nУслуга3\t30р\nУслуга4\t40р' + } session.save_session() # В реальном случае мы получаем баланс гораздо больше чем за секунду # Если мы получаем несколько результатов в секунду, то у нас ломаются отчеты. # Так что чтобы не чинить что не сломано просто чуть подождем - time.sleep(1) + time.sleep(1 if wait else 0) return result diff --git a/plugin/util.py b/plugin/util.py index 4e47b12..1b40884 100644 --- a/plugin/util.py +++ b/plugin/util.py @@ -402,8 +402,14 @@ def full_get_balance(ctx, only_failed, filter): # !!! нельзя пользо store.turn_logging() import httpserver_mobile # breakpoint() - httpserver_mobile.getbalance_standalone(filter=filter, only_failed=only_failed) - echo(f'OK {name}') + result = httpserver_mobile.getbalance_standalone(filter=filter, only_failed=only_failed) + for k, v in result.items(): + echo(f"{k} {'OK' if v else 'BAD'}") + state = 'ALL_OK' if all(result.values()) else 'ANY_OK' if any(result.values()) else 'NOONE_OK' + state_code = 0 if all(result.values()) else 1 if any(result.values()) else 2 + counters = f'OK:{list(result.values()).count(True)}/BAD:{list(result.values()).count(False)}/ALL:{len(result)}' + echo(f'{name} {state} {counters}') + sys.exit(state_code) @cli.command() @@ -487,7 +493,7 @@ def check_ini(ctx): mbplugin_ini = store.ini() mbplugin_ini.read() mbplugin_ini_mess = [] - if'Telegram' in mbplugin_ini.ini: + if 'Telegram' in mbplugin_ini.ini: if len([i for i in mbplugin_ini.ini['Telegram'].keys() if i.startswith('subscrib' + 'tion')]): msg = f'Warning {name} mbplugin.ini - subsri_B_tion key found in ini' mbplugin_ini_mess.append(msg) @@ -558,6 +564,7 @@ def check_plugin(ctx, bpoint, params, plugin, login, password): else: res = httpserver_mobile.getbalance_plugin('url', [plugin, login, password, '123']) echo(f'{name}:\n{res}') + sys.exit(0 if 'Balance' in repr(res) else 1) @cli.command() diff --git a/tests/conftest.py b/tests/conftest.py index bf8e419..1a42cd9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -5,7 +5,7 @@ import settings # pylint: disable=import-error # noqa data_path = os.path.abspath(os.path.join('tests', 'data')) -logging.basicConfig(filename=os.path.abspath(os.path.join('log', 'pytest.log')), level=logging.INFO) +logging.basicConfig(filename=os.path.abspath(os.path.join('log', 'pytest.log')), level=logging.DEBUG) settings.mbplugin_root_path = data_path settings.mbplugin_ini_path = data_path settings.ini_codepage = 'cp1251' diff --git a/tests/data/BalanceHistory_test.mdb b/tests/data/BalanceHistory_test.mdb new file mode 100644 index 0000000..83f6800 Binary files /dev/null and b/tests/data/BalanceHistory_test.mdb differ diff --git a/tests/debug_test.py b/tests/debug_test.py new file mode 100644 index 0000000..f18f8e5 --- /dev/null +++ b/tests/debug_test.py @@ -0,0 +1,24 @@ +import sys, os, pdb, logging + +if __name__ == '__main__': + # ??? import pdb, rlcompleter;pdb.Pdb.complete = rlcompleter.Completer(locals()).complete + # import sys,os;sys.path.insert(0, os.path.abspath('plugin')); sys.path.insert(0, os.path.abspath('tests')); from test_dbengine import *;test_create_db() + + sys.path.insert(0, os.path.abspath('plugin')) + sys.path.insert(0, os.path.abspath('tests')) + + import test_dbengine # noqa: E402 + logging.getLogger().setLevel(logging.DEBUG) + print(f'{logging.getLogger().getEffectiveLevel()=}') + # test_create_db() + # ??? python -m pdb -c "b test_dbengine.py:18" -c c tests/debug_test.py + # python\python -i tests\debug_test.py + print('use\nb test_dbengine.py:41') + # pdb.set_trace() # b test_dbengine.py:18 + # test_create_db() + tst = test_dbengine.Test() + tst.setup_method() + tst.test_create_db() + tst.test_mdb_import() + tst.test_flags() + tst.teardown_method() diff --git a/tests/test_dbengine.py b/tests/test_dbengine.py new file mode 100644 index 0000000..ab7d28f --- /dev/null +++ b/tests/test_dbengine.py @@ -0,0 +1,136 @@ +import pytest +import os, sys, shutil, logging, threading +import conftest # type: ignore # ignore import error +import dbengine, store, settings # pylint: disable=import-error +import test1 + +class Test: + + def setup_method(self, test_method=None): + # configure self.attribute + # assert test_method.__name__ == 'test_create_db', test_method + self.ini_path = os.path.join(conftest.data_path, 'mbplugin.ini') + # logginglevel for putest see conftest.py + logging.info(f'inipath={self.ini_path}') + self.change_ini('updatefrommdb', '1') + self.change_ini('sqlitestore', '1') + shutil.copyfile(self.ini_path + '.ori', self.ini_path) + self.db = dbengine.Dbengine() + self.dbname_copy = self.db.dbname + '.cp.sqlite' + + def teardown_method(self, test_method=None): + # tear down self.attribute + self.db.conn.close() + if 'mbplugin\\tests\\data' in self.db.dbname: + os.remove(self.db.dbname) + if os.path.exists(self.dbname_copy): + os.remove(self.dbname_copy) + os.remove(self.ini_path) + + def change_ini(self, option, value): + ini = store.ini() + # ini.fn = 'mbplugin.ini' + logging.info(f'inipath={ini.inipath}') + ini.read() + ini.ini['Options'][option] = value + ini.write() + ini.inipath = self.ini_path + store.options('updatefrommdb', flush=True) + + def test_create_db(self): + phone_number = '9161234567' + operator = 'p_test1' + assert len(self.db.phoneheader) == 47 + assert self.db.cur_execute_00('select count(*) from phones') >= 0 + result1 = test1.get_balance(operator, phone_number, wait=False) + self.db.write_result(operator, phone_number, result1) + result2 = test1.get_balance(operator, phone_number, wait=False) + result2.update({'Balance': '123', 'Currency': 'rub', 'Minutes': '23', 'BalExpired': '22.10.2022'}) + dbengine.write_result_to_db(operator, phone_number, result1) + assert phone_number in self.db.cur_execute('select * from phones where PhoneNumber=? limit 1', [phone_number])[0], 'Отсутствует запись' + with pytest.raises(KeyError) as e_info: + self.db.write_result(operator, phone_number, {}) + dbengine.write_result_to_db(operator, phone_number, {}) + report = self.db.report() + assert len(report) > 0 + history = self.db.history(phone_number, operator) + assert len(history) > 0 + shutil.copyfile(self.db.dbname, self.dbname_copy) + self.db.cur_execute_00('delete from phones where PhoneNumber=?', [phone_number]) + assert self.db.copy_data(os.path.join(conftest.data_path, 'aaabbb.sqlite')) is False, 'return False as error' + self.db.copy_data(self.dbname_copy) + assert self.db.cur_execute_00('select count(*) from phones') >= 0 + self.db.cur_execute_00('delete from phones where PhoneNumber=?', [phone_number]) + self.db.conn.commit() + assert self.db.cur.rowcount > 0 + assert self.db.cur_execute_00('select count(*) from phones') == 0 + assert len(dbengine.responses()) > 0 + self.change_ini('sqlitestore', '0') + assert len(dbengine.responses()) == 0 + self.change_ini('sqlitestore', '1') + + def test_mdb_import(self): + if sys.platform == 'win32': + with pytest.raises(Exception) as e_info: + dbengine.update_sqlite_from_mdb_core(os.path.join(conftest.data_path, 'aaabbb.mdb')) + assert dbengine.update_sqlite_from_mdb(os.path.join(conftest.data_path, 'aaabbb.mdb')) is False + dbengine.update_sqlite_from_mdb(os.path.join(conftest.data_path, 'BalanceHistory_test.mdb')) + assert self.db.cur_execute_00('select count(*) from phones') == 9 + + def test_flags(self): + dbengine.flags('set', 'key1', 'val1') + dbengine.flags('set', 'key2', 'val2') + dbengine.flags('set', 'key3', 'val1') + getall = dbengine.flags('getall') + assert len(getall) == 3, f'{getall=}' + dbengine.flags('get', 'key1') + # ??? assert dbengine.flags('get', 'key1') == 'val1' + dbengine.flags('setunic', 'key1', 'val1') + assert dbengine.flags('get', 'key3') is None + assert dbengine.flags('get', 'key1') == 'val1' + assert dbengine.flags('getall') == {'key1': 'val1', 'key2': 'val2'} + dbengine.flags('delete', 'key1') + assert len(dbengine.flags('getall')) == 1 + self.change_ini('sqlitestore', '0') + assert len(dbengine.flags('getall')) == 0 + self.change_ini('sqlitestore', '1') + assert len(dbengine.flags('getall')) == 1 + dbengine.flags('deleteall') + assert len(dbengine.flags('getall')) == 0 + + def thread(self): + name = threading.current_thread().name + for task in range(10): + for i in range(10): + if name == 'thr_0': + dbengine.flags('set', 'key1', 'val1') + elif name == 'thr_1': + dbengine.flags('setunic', 'key1', 'val1') + elif name == 'thr_2': + dbengine.flags('getall') + print(f'{threading.currentThread().name} done task {task}') + + def test_multithread(self): + for t in range(3): + threading.Thread(target=self.thread, name=f'thr_{t}', daemon=True).start() + [t.join() for t in threading.enumerate() if t.name.startswith('thr_')] + + +def old_test_ini_class_phones_ini_write(): + ini = store.ini('phones.ini') + phones = ini.phones() + print(f'inipath={ini.inipath}') + print(f'mbplugin_root_path={settings.mbplugin_root_path}') + expected_result1 = [ + ('region', 'p_test1'), ('monitor', 'TRUE'), ('alias', 'Иваныч'), ('number', '9161112233'), ('balancenotchangedmorethen', '40'), + ('balancechangedlessthen', '1'), ('balancelessthen', '100.0'), ('turnofflessthen', '1')] + # expected_result2 = {'nn': 1, 'Alias': 'Иваныч', 'region': 'p_test1', 'number': '9161112233', 'phonedescription': '', 'monitor': 'TRUE', + # 'balancelessthen': '100.0', 'turnofflessthen': '1', 'balancenotchangedmorethen': '40', 'balancechangedlessthen': '1', 'password2': '123password'} + # expected_result2 = {'NN': 1, 'Alias': 'Иваныч', 'Region': 'p_test1', 'Number': '9161112233', 'PhoneDescription': '', 'Monitor': 'TRUE', + # 'BalanceLessThen': 100.0, 'TurnOffLessThen': 1, 'BalanceNotChangedMoreThen': 40, 'BalanceChangedLessThen': 1, 'Password2': '123password'} + expected_result2 = { + 'NN': 1, 'Alias': 'Иваныч', 'Region': 'p_test1', 'Number': '9161112233', 'Monitor': 'TRUE', 'Password2': '123password', + 'nn': 1, 'alias': 'Иваныч', 'region': 'p_test1', 'number': '9161112233', 'monitor': 'TRUE', + 'balancelessthen': '100.0', 'turnofflessthen': '1', 'balancenotchangedmorethen': '40', 'balancechangedlessthen': '1', 'password2': '123password'} + assert list(ini.ini['1'].items()) == expected_result1 + assert phones[('9161112233', 'p_test1')] == expected_result2 diff --git a/tests/test_store.py b/tests/test_store.py index acaea23..07cb0ca 100644 --- a/tests/test_store.py +++ b/tests/test_store.py @@ -4,19 +4,19 @@ import store, settings # pylint: disable=import-error def test_ini_class_mbplugin_ini_write(): - ini_path = os.path.join(conftest.data_path,'mbplugin.ini') - shutil.copyfile(ini_path+'.ori', ini_path) - ini=store.ini() + ini_path = os.path.join(conftest.data_path, 'mbplugin.ini') + shutil.copyfile(ini_path + '.ori', ini_path) + ini = store.ini() ini.fn = 'mbplugin.ini' ini.inipath = ini_path print(f'inipath={ini.inipath}') ini.read() ini.ini['Options']['show_chrome'] = '0' ini.write() - assert not conftest.ini_compare(ini_path+'.ori', ini_path) # Проверяем что файл изменился + assert not conftest.ini_compare(ini_path + '.ori', ini_path) # Проверяем что файл изменился ini.ini['Options']['show_chrome'] = '1' ini.write() - assert conftest.ini_compare(ini_path+'.ori', ini_path) # Проверяем идентичность первоначального и сохраненного файла + assert conftest.ini_compare(ini_path + '.ori', ini_path) # Проверяем идентичность первоначального и сохраненного файла def test_ini_class_phones_ini_write(): @@ -24,15 +24,17 @@ def test_ini_class_phones_ini_write(): phones = ini.phones() print(f'inipath={ini.inipath}') print(f'mbplugin_root_path={settings.mbplugin_root_path}') - expected_result1 = [('region', 'p_test1'), ('monitor', 'TRUE'), ('alias', 'Иваныч'), ('number', '9161112233'), ('balancenotchangedmorethen', '40'), - ('balancechangedlessthen', '1'), ('balancelessthen', '100.0'), ('turnofflessthen', '1')] + expected_result1 = [ + ('region', 'p_test1'), ('monitor', 'TRUE'), ('alias', 'Иваныч'), ('number', '9161112233'), ('balancenotchangedmorethen', '40'), + ('balancechangedlessthen', '1'), ('balancelessthen', '100.0'), ('turnofflessthen', '1')] # expected_result2 = {'nn': 1, 'Alias': 'Иваныч', 'region': 'p_test1', 'number': '9161112233', 'phonedescription': '', 'monitor': 'TRUE', # 'balancelessthen': '100.0', 'turnofflessthen': '1', 'balancenotchangedmorethen': '40', 'balancechangedlessthen': '1', 'password2': '123password'} # expected_result2 = {'NN': 1, 'Alias': 'Иваныч', 'Region': 'p_test1', 'Number': '9161112233', 'PhoneDescription': '', 'Monitor': 'TRUE', # 'BalanceLessThen': 100.0, 'TurnOffLessThen': 1, 'BalanceNotChangedMoreThen': 40, 'BalanceChangedLessThen': 1, 'Password2': '123password'} - expected_result2 = { 'NN': 1, 'Alias': 'Иваныч', 'Region': 'p_test1', 'Number': '9161112233', 'Monitor': 'TRUE', 'Password2': '123password', - 'nn': 1, 'alias': 'Иваныч', 'region': 'p_test1', 'number': '9161112233', 'monitor': 'TRUE', - 'balancelessthen': '100.0', 'turnofflessthen': '1', 'balancenotchangedmorethen': '40', 'balancechangedlessthen': '1', 'password2': '123password'} + expected_result2 = { + 'NN': 1, 'Alias': 'Иваныч', 'Region': 'p_test1', 'Number': '9161112233', 'Monitor': 'TRUE', 'Password2': '123password', + 'nn': 1, 'alias': 'Иваныч', 'region': 'p_test1', 'number': '9161112233', 'monitor': 'TRUE', + 'balancelessthen': '100.0', 'turnofflessthen': '1', 'balancenotchangedmorethen': '40', 'balancechangedlessthen': '1', 'password2': '123password'} assert list(ini.ini['1'].items()) == expected_result1 assert phones[('9161112233', 'p_test1')] == expected_result2 @@ -51,7 +53,7 @@ def test_result_to_xml(param_test_result_to_xml): assert result == expected_result @pytest.fixture(scope="function", params=[ -({'Balance': 124.45, 'SMS': 43, 'Min': 222}, '

{"Balance": 124.45, "SMS": 43, "Min": 222}

'), + ({'Balance': 124.45, 'SMS': 43, 'Min': 222}, '

{"Balance": 124.45, "SMS": 43, "Min": 222}

'), ({'Balance': 124.45, 'SMS': '43', 'Min': '222'}, '

{"Balance": 124.45, "SMS": 43, "Min": 222}

'), ]) def param_test_result_to_html(request):