From 7722666498b7ddad2f7cc69f9114c19ed1a4455e Mon Sep 17 00:00:00 2001 From: firedotguy Date: Tue, 30 Dec 2025 23:28:21 +0300 Subject: [PATCH 01/34] feat(customer): add planning disconnect date --- main.py | 3 +- routers/customer.py | 83 +++++++++------------------------------ tariff.py | 96 +++++++++++++++++++++++++++++++++++++++++++++ 3 files changed, 117 insertions(+), 65 deletions(-) create mode 100644 tariff.py diff --git a/main.py b/main.py index 06b3e96..5d179ee 100644 --- a/main.py +++ b/main.py @@ -15,12 +15,13 @@ from routers import attach from routers import inventory from api import api_call +from tariff import Tariff from config import API_KEY as APIKEY app = FastAPI(title='SmartLinkAPI') app.state.tariffs = { - tariff['billing_uuid']: unescape(tariff['name']) + tariff['billing_uuid']: Tariff(unescape(tariff['name'])) for tariff in api_call('tariff', 'get')['data'].values() } app.state.customer_groups = { diff --git a/routers/customer.py b/routers/customer.py index 17e2ffa..3f89edb 100644 --- a/routers/customer.py +++ b/routers/customer.py @@ -1,5 +1,6 @@ from html import unescape from ipaddress import IPv4Address +from datetime import datetime from fastapi import APIRouter from fastapi.requests import Request @@ -8,6 +9,7 @@ from api import api_call from utils import list_to_str, to_2gis_link, to_neo_link, normalize_items, extract_sn, remove_sn,\ parse_agreement, status_to_str, format_mac +from tariff import calc_disconnect router = APIRouter(prefix='/customer') PHONE_LENGTH = 9 @@ -62,7 +64,7 @@ def api_get_customer(request: Request, id: int): return JSONResponse({'status': 'fail', 'detail': 'customer not found'}, 404) tariffs = [ - {'id': int(tariff['id']), 'name': request.app.state.tariffs[tariff['id']]} + {'id': int(tariff['id']), 'name': request.app.state.tariffs[tariff['id']].content} for tariff in customer['tariff']['current'] if tariff['id'] ] @@ -95,66 +97,13 @@ def api_get_customer(request: Request, id: int): else: olt_id = olt['finish']['object_id'] + will_disconnect = calc_disconnect( + [request.app.state.tariffs[tariff['id']] for tariff in customer['tariff']['current'] if tariff['id']], + customer['balance'], datetime.strptime(customer['date_connect'], '%Y-%m-%d') + ) if customer.get('date_connect') else None + if will_disconnect: + will_disconnect = will_disconnect.strftime('%Y-%m-%d') - # INVENTORY - # items = api_call('inventory', 'get_inventory_amount', f'location=customer&object_id={id}')\ - # .get('data', {}) - # if isinstance(items, dict): - # items = items.values() - - # item_names = [ - # { - # 'id': str(item['id']), - # 'name': unescape(item['name']), - # 'catalog': item['inventory_section_catalog_id'] - # } - # for item in api_call('inventory', 'get_inventory_catalog', - # f'id={list_to_str([str(i["inventory_type_id"]) for i in items])}')['data'].values() - # ] - # inventory = [] - # for item in items: - # item_name = [i for i in item_names if i['id'] == str(item['inventory_type_id'])][0] - # inventory.append({ - # 'id': item['id'], - # 'catalog_id': item['inventory_type_id'], - # 'name': item_name['name'], - # 'amount': item['amount'], - # 'category_id': item_name['catalog'], - # 'sn': item['serial_number'] - # }) - - - # TASK - # tasks_id = str_to_list(api_call('task', 'get_list', f'customer_id={id}')['list']) - # if tasks_id: - # tasks_data = normalize_items(api_call('task', 'show', f'id={list_to_str(tasks_id)}')) - # tasks = [] - # for task in tasks_data: - # dates = {} - # if 'create' in task['date']: - # dates['create'] = task['date']['create'] - # if 'update' in task['date']: - # dates['update'] = task['date']['update'] - # if 'complete' in task['date']: - # dates['complete'] = task['date']['complete'] - # if task['type']['name'] != 'Обращение абонента' and \ - # task['type']['name'] != 'Регистрация звонка': - # tasks.append({ - # 'id': task['id'], - # 'customer_id': task['customer'][0], - # 'employee_id': list(task['staff']['employee'].values())[0] - # if 'staff' in task and 'employee' in task['staff'] else None, - # 'name': task['type']['name'], - # 'status': { - # 'id': task['state']['id'], - # 'name': task['state']['name'], - # 'system_id': task['state']['system_role'] - # }, - # 'address': task['address']['text'], - # 'dates': dates - # }) - # else: - # tasks = [] return { 'status': 'success', 'data': { @@ -175,9 +124,6 @@ def api_get_customer(request: Request, id: int): 'is_disabled': bool(customer.get('is_disable', False)), 'is_potential': bool(customer.get('is_potential', False)), - # 'inventory': inventory, - # 'tasks': tasks, - # ONT 'olt_id': olt_id, 'sn': extract_sn(customer['full_name']), @@ -188,7 +134,7 @@ def api_get_customer(request: Request, id: int): # billing 'has_billing': bool(customer.get('is_in_billing', False)), 'billing': { - 'id': int(customer['billing_id']) if 'billing_id' in customer and customer['billing_id'] else None, + 'id': int(customer['billing_id']) if customer.get('billing_id') else None, 'crc': customer.get('crc_billing') }, 'balance': customer['balance'], @@ -205,6 +151,15 @@ def api_get_customer(request: Request, id: int): 'geodata': geodata, # timestamps + 'created_at': customer.get('date_create'), + 'connected_at': customer.get('date_connect'), + 'agreed_at': datetime.strptime(customer['agreement'][0]['date'], '%d.%m.%Y').strftime('%Y-%m-%d'), + 'positive_balance_at': customer.get('date_positive_balance'), + 'last_active_at': customer.get('date_activity'), + 'last_inet_active_at': customer.get('date_activity_inet'), + 'will_disconnect_at': will_disconnect, + + # OLD (for backward compitability) 'timestamps': { 'created_at': customer.get('date_create'), 'connected_at': customer.get('date_connect'), diff --git a/tariff.py b/tariff.py new file mode 100644 index 0000000..758fe0a --- /dev/null +++ b/tariff.py @@ -0,0 +1,96 @@ +from re import search, escape +from enum import Enum +from datetime import datetime, timedelta +from functools import reduce + +class TariffType(Enum): + BASE = 'base' # priced + PROMO = 'promo' # free + SALE = 'sale' # % sale + NONE = 'none' # no price/sale + +class Tariff: + def __init__(self, content: str): + # self.id = id + self.content = content + self.price = 0 + self.free_days = 0 + self.sale = 0 + self.sale_days = 0 + + content = content.lower() + + if search(r'\(\d+\s*сом\)', content): + self.type = TariffType.BASE + match = search(r'\((\d+)\s*сом\)', content) + if match: + self.price = int(match.group(1)) + + elif 'бесплатно' in content: + self.type = TariffType.PROMO + match = search(r'(\S+)\s+бесплатно', content) + if match: + word = match.group(1) + + multiplier_match = search(rf'(\d+)\s+{escape(match.group(1))}', content) + multiplier = multiplier_match.group(1) if multiplier_match else 1 + + if 'месяц' in word: + self.free_days = 30 * int(multiplier) + + elif 'день' in word or 'дн' in word: + self.free_days = int(multiplier) + + elif '%' in content: + self.type = TariffType.SALE + match = search(r'(\d+)%', content) + if match: + sale = match.group(1) + self.sale = int(sale) + + word_match = search(fr'{sale}%\s+на\s+(\w+)', content) + if word_match: + word = word_match.group(1) + + if 'год' in word: + self.sale_days = 365 + else: + self.type = TariffType.NONE + + +def calc_disconnect(tariffs: list[Tariff], balance: float, connected_at: datetime) -> datetime | None: + base_sum = sum([t.price for t in tariffs]) / 30 # sum per day + free = sum([t.free_days for t in tariffs]) + sale = reduce(lambda a, b: a * b, [t.sale / 100 for t in tariffs if t.type == TariffType.SALE], 0) + + saled_sum = (base_sum - base_sum * sale) if sale else base_sum + sale_tariff = next((t for t in tariffs if t.type == TariffType.SALE), None) # first sale tariff + now = datetime.now() + days_since_connect = (now - connected_at).days + + if sale_tariff and sale_tariff.sale_days > 0: + sale_remaining = max(0, sale_tariff.sale_days - days_since_connect) + sale_multiplier = 1 - sale_tariff.sale / 100 + else: + sale_remaining = 0 + sale_multiplier = 1 if not sale_tariff else 1 - sale_tariff.sale / 100 + + saled_sum = base_sum * sale_multiplier + + # spend balance + if sale_remaining > 0: + # sale period cost + sale_period_cost = sale_remaining * saled_sum + + if balance >= sale_period_cost: + # balance enough for whole sale period + balance -= sale_period_cost + days = free + sale_remaining + balance / base_sum + else: + # disconnect while sale period + days = free + balance / saled_sum + else: + # sale expired or no sale + days = free + balance / (saled_sum if sale_tariff and sale_tariff.sale_days == 0 else base_sum) + + return now + timedelta(days=int(days)) From 14312e564a02a891aa4005de51720a12728cfacb Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sun, 4 Jan 2026 22:52:37 +0300 Subject: [PATCH 02/34] chore: remove old commented code in customer --- routers/customer.py | 63 --------------------------------------------- 1 file changed, 63 deletions(-) diff --git a/routers/customer.py b/routers/customer.py index 17e2ffa..fdaf298 100644 --- a/routers/customer.py +++ b/routers/customer.py @@ -95,66 +95,6 @@ def api_get_customer(request: Request, id: int): else: olt_id = olt['finish']['object_id'] - - # INVENTORY - # items = api_call('inventory', 'get_inventory_amount', f'location=customer&object_id={id}')\ - # .get('data', {}) - # if isinstance(items, dict): - # items = items.values() - - # item_names = [ - # { - # 'id': str(item['id']), - # 'name': unescape(item['name']), - # 'catalog': item['inventory_section_catalog_id'] - # } - # for item in api_call('inventory', 'get_inventory_catalog', - # f'id={list_to_str([str(i["inventory_type_id"]) for i in items])}')['data'].values() - # ] - # inventory = [] - # for item in items: - # item_name = [i for i in item_names if i['id'] == str(item['inventory_type_id'])][0] - # inventory.append({ - # 'id': item['id'], - # 'catalog_id': item['inventory_type_id'], - # 'name': item_name['name'], - # 'amount': item['amount'], - # 'category_id': item_name['catalog'], - # 'sn': item['serial_number'] - # }) - - - # TASK - # tasks_id = str_to_list(api_call('task', 'get_list', f'customer_id={id}')['list']) - # if tasks_id: - # tasks_data = normalize_items(api_call('task', 'show', f'id={list_to_str(tasks_id)}')) - # tasks = [] - # for task in tasks_data: - # dates = {} - # if 'create' in task['date']: - # dates['create'] = task['date']['create'] - # if 'update' in task['date']: - # dates['update'] = task['date']['update'] - # if 'complete' in task['date']: - # dates['complete'] = task['date']['complete'] - # if task['type']['name'] != 'Обращение абонента' and \ - # task['type']['name'] != 'Регистрация звонка': - # tasks.append({ - # 'id': task['id'], - # 'customer_id': task['customer'][0], - # 'employee_id': list(task['staff']['employee'].values())[0] - # if 'staff' in task and 'employee' in task['staff'] else None, - # 'name': task['type']['name'], - # 'status': { - # 'id': task['state']['id'], - # 'name': task['state']['name'], - # 'system_id': task['state']['system_role'] - # }, - # 'address': task['address']['text'], - # 'dates': dates - # }) - # else: - # tasks = [] return { 'status': 'success', 'data': { @@ -175,9 +115,6 @@ def api_get_customer(request: Request, id: int): 'is_disabled': bool(customer.get('is_disable', False)), 'is_potential': bool(customer.get('is_potential', False)), - # 'inventory': inventory, - # 'tasks': tasks, - # ONT 'olt_id': olt_id, 'sn': extract_sn(customer['full_name']), From 7a3570011318f09516add394e9193844d60422d2 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Mon, 5 Jan 2026 12:25:01 +0300 Subject: [PATCH 03/34] feat(customer): add get_list endpoint; refactor(customer): move process customer to function (#45) --- routers/customer.py | 171 ++++++++++++++++++++++++++++---------------- 1 file changed, 108 insertions(+), 63 deletions(-) diff --git a/routers/customer.py b/routers/customer.py index fdaf298..5918514 100644 --- a/routers/customer.py +++ b/routers/customer.py @@ -1,5 +1,7 @@ from html import unescape from ipaddress import IPv4Address +from json import loads +from json.decoder import JSONDecodeError from fastapi import APIRouter from fastapi.requests import Request @@ -54,15 +56,9 @@ def api_get_customer_search(query: str): }, 404) -# TODO: divide api calls -@router.get('/{id}') -def api_get_customer(request: Request, id: int): - customer = api_call('customer', 'get_data', f'id={id}').get('data') - if customer is None: - return JSONResponse({'status': 'fail', 'detail': 'customer not found'}, 404) - +def _process_customer(request_tariffs: list, request_groups: list, customer: dict): tariffs = [ - {'id': int(tariff['id']), 'name': request.app.state.tariffs[tariff['id']]} + {'id': int(tariff['id']), 'name': request_tariffs[tariff['id']]} for tariff in customer['tariff']['current'] if tariff['id'] ] @@ -81,8 +77,7 @@ def api_get_customer(request: Request, id: int): geodata['2gis_link'] = to_2gis_link(geodata['coord'][0], geodata['coord'][1]) - olt = api_call('commutation', 'get_data', - f'object_type=customer&object_id={id}&is_finish_data=1')['data'] + olt = api_call('commutation', 'get_data', f'object_type=customer&object_id={customer["id"]}&is_finish_data=1')['data'] if 'finish' not in olt or olt['finish'].get('object_type') != 'switch' and extract_sn(customer['full_name']) is not None: ont = api_call('device', 'get_ont_data', f'id={extract_sn(customer["full_name"])}')['data'] @@ -96,63 +91,72 @@ def api_get_customer(request: Request, id: int): olt_id = olt['finish']['object_id'] return { - 'status': 'success', - 'data': { - # main data - 'id': customer['id'], - 'name': remove_sn(customer['full_name']), - 'agreement': parse_agreement(customer['agreement'][0]['number']), - 'status': status_to_str(customer['state_id']), - 'group': { - 'id': list(customer['group'].values())[0]['id'], - 'name': request.app.state.customer_groups[list(customer['group'].values())[0]['id']] - } if 'group' in customer else None, - 'phones': [phone['number'] for phone in customer['phone'] if phone['number']], - 'tariffs': tariffs, - 'manager_id': customer.get('manager_id'), - - 'is_corporate': bool(customer.get('flag_corporate', False)), - 'is_disabled': bool(customer.get('is_disable', False)), - 'is_potential': bool(customer.get('is_potential', False)), - - # ONT - 'olt_id': olt_id, - 'sn': extract_sn(customer['full_name']), - 'ip': str(IPv4Address(int(list(customer['ip_mac'].values())[0]['ip']))) if list(customer.get('ip_mac', {'': {}}).values())[0].get('ip') else None, - 'mac': format_mac(list(customer.get('ip_mac', {'': {}}).values())[0].get('mac')), - # 'onu_level': get_ont_data(extract_sn(customer['full_name'])), - - # billing - 'has_billing': bool(customer.get('is_in_billing', False)), - 'billing': { - 'id': int(customer['billing_id']) if 'billing_id' in customer and customer['billing_id'] else None, - 'crc': customer.get('crc_billing') - }, - 'balance': customer['balance'], - - # geodata - 'address': { - 'house_id': customer['address'][0].get('house_id') if customer.get('address', [{}])[0].get('house_id') else None, - 'entrance': customer['address'][0].get('entrance') if customer.get('address', [{}])[0].get('entrance') else None, - 'floor': int(customer['address'][0]['floor']) if customer.get('address', [{}])[0].get('floor') else None, - 'apartment': unescape(customer['address'][0]['apartment']['number']) - if customer.get('address', [{}])[0].get('apartment', {}).get('number') else None - }, - 'box_id': customer['address'][0]['house_id'] if customer['address'][0]['house_id'] != 0 else None, - 'geodata': geodata, - - # timestamps - 'timestamps': { - 'created_at': customer.get('date_create'), - 'connected_at': customer.get('date_connect'), - 'positive_balance_at': customer.get('date_positive_balance'), - 'last_active_at': customer.get('date_activity'), - 'last_inet_active_at': customer.get('date_activity_inet') - } + # main data + 'id': customer['id'], + 'name': remove_sn(customer['full_name']), + 'agreement': parse_agreement(customer['agreement'][0]['number']), + 'status': status_to_str(customer['state_id']), + 'group': { + 'id': list(customer['group'].values())[0]['id'], + 'name': request_groups[list(customer['group'].values())[0]['id']] + } if 'group' in customer else None, + 'phones': [phone['number'] for phone in customer['phone'] if phone['number']], + 'tariffs': tariffs, + 'manager_id': customer.get('manager_id'), + + 'is_corporate': bool(customer.get('flag_corporate', False)), + 'is_disabled': bool(customer.get('is_disable', False)), + 'is_potential': bool(customer.get('is_potential', False)), + + # ONT + 'olt_id': olt_id, + 'sn': extract_sn(customer['full_name']), + 'ip': str(IPv4Address(int(list(customer['ip_mac'].values())[0]['ip']))) if list(customer.get('ip_mac', {'': {}}).values())[0].get('ip') else None, + 'mac': format_mac(list(customer.get('ip_mac', {'': {}}).values())[0].get('mac')), + # 'onu_level': get_ont_data(extract_sn(customer['full_name'])), + + # billing + 'has_billing': bool(customer.get('is_in_billing', False)), + 'billing': { + 'id': int(customer['billing_id']) if 'billing_id' in customer and customer['billing_id'] else None, + 'crc': customer.get('crc_billing') + }, + 'balance': customer['balance'], + + # geodata + 'address': { + 'house_id': customer['address'][0].get('house_id') if customer.get('address', [{}])[0].get('house_id') else None, + 'entrance': customer['address'][0].get('entrance') if customer.get('address', [{}])[0].get('entrance') else None, + 'floor': int(customer['address'][0]['floor']) if customer.get('address', [{}])[0].get('floor') else None, + 'apartment': unescape(customer['address'][0]['apartment']['number']) + if customer.get('address', [{}])[0].get('apartment', {}).get('number') else None + }, + 'box_id': customer['address'][0]['house_id'] if customer['address'][0]['house_id'] != 0 else None, + 'geodata': geodata, + + # timestamps + 'timestamps': { + 'created_at': customer.get('date_create'), + 'connected_at': customer.get('date_connect'), + 'positive_balance_at': customer.get('date_positive_balance'), + 'last_active_at': customer.get('date_activity'), + 'last_inet_active_at': customer.get('date_activity_inet') } } +@router.get('/{id}') +def api_get_customer(request: Request, id: int): + customer = api_call('customer', 'get_data', f'id={id}').get('data') + if customer is None: + return JSONResponse({'status': 'fail', 'detail': 'customer not found'}, 404) + + return { + 'status': 'success', + 'data': _process_customer(request.app.state.tariffs, request.app.state.customer_groups, customer) + } + + @router.get('/{id}/name') def api_get_customer_name(id: int): @@ -165,3 +169,44 @@ def api_get_customer_name(id: int): 'id': id, 'name': remove_sn(customer['full_name']) } + + +@router.get('') +def api_get_customers( + request: Request, + ids: str | None = None, + get_data: bool = True, + limit: int | None = None, + skip: int | None = None +): + customers = [] + if ids is not None: + try: + customers: list[int] = loads(ids) + if not (isinstance(customers, list) and all(isinstance(customer, int) for customer in customers)): + return JSONResponse({'status': 'fail', 'detail': 'incorrect type of ids param'}, 422) + except JSONDecodeError: + return JSONResponse({'status': 'fail', 'detail': 'unable to parse ids param'}, 422) + else: + return JSONResponse({'status': 'fail', 'detail': 'no filters provided'}, 422) + + count = len(customers) + if skip: + customers = customers[skip:] + if limit: + customers = customers[:limit] + + customers_data = [] + if get_data: + for customer_id in customers: + customer = api_call('customer', 'get_data', f'id={customer_id}').get('data') + if customer is None: + return JSONResponse({'status': 'fail', 'detail': f'customer {customer_id} not found'}, 404) + + customers_data.append(_process_customer(request.app.state.tariffs, request.app.state.customer_groups, customer)) + + return { + 'status': 'success', + 'data': customers_data or customers, + 'count': count # total count without limit/skip + } \ No newline at end of file From 2b9ab2804c082d91a1bc8b87561da5dc16cf3c09 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Mon, 5 Jan 2026 13:20:13 +0300 Subject: [PATCH 04/34] fix(box): fix excluding customers --- routers/box.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/routers/box.py b/routers/box.py index 2abe1d3..cb29d68 100644 --- a/routers/box.py +++ b/routers/box.py @@ -1,3 +1,6 @@ +from json import loads +from json.decoder import JSONDecodeError + from fastapi import APIRouter from fastapi.responses import JSONResponse @@ -12,7 +15,7 @@ def api_get_box( get_onu_level: bool = False, get_tasks: bool = False, limit: int | None = None, - exclude_customer_ids: list[int] = [] + exclude_customer_ids: str = '[]' ): def _get_onu_level(name) -> float | None: if extract_sn(name) is None: @@ -40,13 +43,22 @@ def _build_customer(customer: dict) -> dict | None: 'tasks': _get_tasks('customer', customer['id']) if get_tasks else None } + exclude_ids = [] + if exclude_customer_ids: + try: + exclude_ids: list[int] = loads(exclude_customer_ids) + if not (isinstance(exclude_ids, list) and all(isinstance(customer, int) for customer in exclude_ids)): + return JSONResponse({'status': 'fail', 'detail': 'incorrect type of exclude_customer_ids param'}, 422) + except JSONDecodeError: + return JSONResponse({'status': 'fail', 'detail': 'unable to parse exclude_customer_ids param'}, 422) + house_data = api_call('address', 'get_house', f'building_id={id}').get('data') if not house_data: return JSONResponse({'status': 'fail', 'detail': 'box not found'}, 404) house = list(house_data.values())[0] customer_ids: list = api_call('customer', 'get_customers_id', f'house_id={id}').get('data', []) - for customer in exclude_customer_ids: + for customer in exclude_ids: if customer in customer_ids: customer_ids.remove(customer) customers_count = len(customer_ids) From 70fb161baaf7b110b5a8799d520c717774774b7a Mon Sep 17 00:00:00 2001 From: firedotguy Date: Mon, 5 Jan 2026 13:26:37 +0300 Subject: [PATCH 05/34] fix(box): fix 500 Internal Server Error when get box without coordinates --- utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/utils.py b/utils.py index 82a2883..d27efb1 100644 --- a/utils.py +++ b/utils.py @@ -185,7 +185,7 @@ def format_mac(mac: str | None) -> str | None: return ':'.join(mac.replace('-', '')[i:i + 2] for i in range(0, len(mac.replace('-', '')), 2)) def get_coordinates(polygon: list[list[float]] | None) -> list[float] | None: - if polygon is None: + if not polygon: return None points = polygon[:-1] lats = [p[0] for p in points] From 81756299fcc34cb3302b8f7647e9946a6477ae42 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Mon, 5 Jan 2026 23:25:43 +0300 Subject: [PATCH 06/34] fix(box): add onu level; use process_customer func in box for neighbours --- routers/box.py | 32 ++++++++++++++------------------ routers/customer.py | 30 +++++++++++++++++------------- 2 files changed, 31 insertions(+), 31 deletions(-) diff --git a/routers/box.py b/routers/box.py index cb29d68..beaa90e 100644 --- a/routers/box.py +++ b/routers/box.py @@ -3,28 +3,23 @@ from fastapi import APIRouter from fastapi.responses import JSONResponse +from fastapi.requests import Request from api import api_call +from routers.customer import _process_customer from utils import extract_sn, normalize_items, remove_sn, status_to_str, list_to_str, str_to_list, get_coordinates, get_box_map_link router = APIRouter(prefix='/box') @router.get('/{id}') def api_get_box( + request: Request, id: int, - get_onu_level: bool = False, + get_olt_data: bool = False, get_tasks: bool = False, limit: int | None = None, exclude_customer_ids: str = '[]' ): - def _get_onu_level(name) -> float | None: - if extract_sn(name) is None: - return - data = api_call('device', 'get_ont_data', f'id={extract_sn(name)}').get('data') - if not isinstance(data, dict): - return - return data.get('level_onu_rx') - def _get_tasks(entity: str, entity_id: int) -> list[int]: res = api_call('task', 'get_list', f'{entity}_id={entity_id}&state_id=18,3,17,11,1,16,19') return list(map(int, str_to_list(res.get('list', '')))) @@ -33,15 +28,16 @@ def _build_customer(customer: dict) -> dict | None: name = customer.get('full_name') if name is None: return None - return { - 'id': customer['id'], - 'name': remove_sn(name), - 'last_activity': customer.get('date_activity'), - 'status': status_to_str(customer['state_id']), - 'sn': extract_sn(name), - 'onu_level': _get_onu_level(name) if get_onu_level else None, - 'tasks': _get_tasks('customer', customer['id']) if get_tasks else None - } + return _process_customer(request.app.state.tariffs, request.app.state.customer_groups, customer, get_olt_data) + # { + # 'id': customer['id'], + # 'name': remove_sn(name), + # 'last_activity': customer.get('date_activity'), + # 'status': status_to_str(customer['state_id']), + # 'sn': extract_sn(name), + # 'onu_level': _get_onu_level(name) if get_onu_level else None, + # 'tasks': _get_tasks('customer', customer['id']) if get_tasks else None + # } exclude_ids = [] if exclude_customer_ids: diff --git a/routers/customer.py b/routers/customer.py index 5918514..b95c63a 100644 --- a/routers/customer.py +++ b/routers/customer.py @@ -56,7 +56,7 @@ def api_get_customer_search(query: str): }, 404) -def _process_customer(request_tariffs: list, request_groups: list, customer: dict): +def _process_customer(request_tariffs: list, request_groups: list, customer: dict, get_olt_data: bool = True): tariffs = [ {'id': int(tariff['id']), 'name': request_tariffs[tariff['id']]} for tariff in customer['tariff']['current'] if tariff['id'] @@ -77,18 +77,17 @@ def _process_customer(request_tariffs: list, request_groups: list, customer: dic geodata['2gis_link'] = to_2gis_link(geodata['coord'][0], geodata['coord'][1]) - olt = api_call('commutation', 'get_data', f'object_type=customer&object_id={customer["id"]}&is_finish_data=1')['data'] - - if 'finish' not in olt or olt['finish'].get('object_type') != 'switch' and extract_sn(customer['full_name']) is not None: + olt_id = None + onu_level = None + if get_olt_data and extract_sn(customer['full_name']) is not None: ont = api_call('device', 'get_ont_data', f'id={extract_sn(customer["full_name"])}')['data'] if isinstance(ont, dict): olt_id = ont.get('device_id') + onu_level = ont.get('level_onu_rx') else: - olt_id = None - elif extract_sn(customer['full_name']) is None: - olt_id = None - else: - olt_id = olt['finish']['object_id'] + olt = api_call('commutation', 'get_data', f'object_type=customer&object_id={customer["id"]}&is_finish_data=1')['data'] + if olt.get('finish', {}).get('object_type') != 'switch': + olt_id = olt['finish']['object_id'] return { # main data @@ -113,7 +112,7 @@ def _process_customer(request_tariffs: list, request_groups: list, customer: dic 'sn': extract_sn(customer['full_name']), 'ip': str(IPv4Address(int(list(customer['ip_mac'].values())[0]['ip']))) if list(customer.get('ip_mac', {'': {}}).values())[0].get('ip') else None, 'mac': format_mac(list(customer.get('ip_mac', {'': {}}).values())[0].get('mac')), - # 'onu_level': get_ont_data(extract_sn(customer['full_name'])), + 'onu_level': onu_level, # billing 'has_billing': bool(customer.get('is_in_billing', False)), @@ -146,14 +145,18 @@ def _process_customer(request_tariffs: list, request_groups: list, customer: dic @router.get('/{id}') -def api_get_customer(request: Request, id: int): +def api_get_customer( + request: Request, + id: int, + get_olt_data: bool = False +): customer = api_call('customer', 'get_data', f'id={id}').get('data') if customer is None: return JSONResponse({'status': 'fail', 'detail': 'customer not found'}, 404) return { 'status': 'success', - 'data': _process_customer(request.app.state.tariffs, request.app.state.customer_groups, customer) + 'data': _process_customer(request.app.state.tariffs, request.app.state.customer_groups, customer, get_olt_data) } @@ -176,6 +179,7 @@ def api_get_customers( request: Request, ids: str | None = None, get_data: bool = True, + get_olt_data: bool = False, limit: int | None = None, skip: int | None = None ): @@ -203,7 +207,7 @@ def api_get_customers( if customer is None: return JSONResponse({'status': 'fail', 'detail': f'customer {customer_id} not found'}, 404) - customers_data.append(_process_customer(request.app.state.tariffs, request.app.state.customer_groups, customer)) + customers_data.append(_process_customer(request.app.state.tariffs, request.app.state.customer_groups, customer, get_olt_data)) return { 'status': 'success', From 38e9e1872f939c4245de2e0eb8964e2489c1b2b9 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Mon, 5 Jan 2026 23:27:00 +0300 Subject: [PATCH 07/34] fix(customer/task): remove 'timestamps' sub-dict (move timestamps to root dict) --- routers/customer.py | 12 +++++------- routers/task.py | 12 +++++------- 2 files changed, 10 insertions(+), 14 deletions(-) diff --git a/routers/customer.py b/routers/customer.py index b95c63a..875a6d1 100644 --- a/routers/customer.py +++ b/routers/customer.py @@ -134,13 +134,11 @@ def _process_customer(request_tariffs: list, request_groups: list, customer: dic 'geodata': geodata, # timestamps - 'timestamps': { - 'created_at': customer.get('date_create'), - 'connected_at': customer.get('date_connect'), - 'positive_balance_at': customer.get('date_positive_balance'), - 'last_active_at': customer.get('date_activity'), - 'last_inet_active_at': customer.get('date_activity_inet') - } + 'created_at': customer.get('date_create'), + 'connected_at': customer.get('date_connect'), + 'positive_balance_at': customer.get('date_positive_balance'), + 'last_active_at': customer.get('date_activity'), + 'last_inet_active_at': customer.get('date_activity_inet') } diff --git a/routers/task.py b/routers/task.py index de68570..3737250 100644 --- a/routers/task.py +++ b/routers/task.py @@ -220,13 +220,11 @@ def api_get_tasks( 'content': unescape(comment['comment']) } for comment in task.get('comments', {}).values() ], - 'timestamps': { - 'created_at': task['date'].get('create'), - 'planned_at': task['date'].get('todo'), - 'updated_at': task['date'].get('update'), - 'completed_at': task['date'].get('complete'), - 'deadline': task['date'].get('runtime_individual_hour') - }, + 'created_at': task['date'].get('create'), + 'planned_at': task['date'].get('todo'), + 'updated_at': task['date'].get('update'), + 'completed_at': task['date'].get('complete'), + 'deadline': task['date'].get('runtime_individual_hour'), 'addata': { 'reason': task['additional_data'].get('30', {}).get('value'), 'solve': task['additional_data'].get('36', {}).get('value'), From 1d5eb729c4b050fd51997640fd16cb6bee7aedd3 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Mon, 5 Jan 2026 23:36:52 +0300 Subject: [PATCH 08/34] chore: stylize old code; remove unused imports --- routers/attach.py | 6 ++--- routers/box.py | 2 +- routers/customer.py | 3 +-- routers/employee.py | 3 +-- routers/neomobile.py | 36 +++++++++++------------------- routers/task.py | 20 ++++++++--------- utils.py | 53 ++++++++++++++++++++++++-------------------- 7 files changed, 56 insertions(+), 67 deletions(-) diff --git a/routers/attach.py b/routers/attach.py index b0b8961..b586d31 100644 --- a/routers/attach.py +++ b/routers/attach.py @@ -11,8 +11,7 @@ def api_get_attachs(id: int, include_task: bool = False): if include_task: tasks = api_call('task', 'get_list', f'customer_id={id}')['list'].split(',') for task in tasks: - task_attachs = normalize_items(api_call('attach', 'get', - f'object_id={task}&object_type=task')) + task_attachs = normalize_items(api_call('attach', 'get', f'object_id={task}&object_type=task')) if isinstance(task_attachs, dict): for attach in task_attachs: attach['source'] = 'task' attachs.extend(task_attachs) @@ -23,8 +22,7 @@ def api_get_attachs(id: int, include_task: bool = False): # 'url': api_call('attach', 'get_file_temporary_link', f'uuid={attach["id"]}'), 'url': get_attach_url(attach['id']), 'name': attach['internal_filepath'], - 'extension': attach['internal_filepath'].split('.')[1].lower() - if '.' in attach['internal_filepath'] else None, + 'extension': attach['internal_filepath'].split('.')[1].lower() if '.' in attach['internal_filepath'] else None, 'created_at': attach['date_add'], 'source': attach.get('source', 'customer'), 'source_id': attach.get('object_id'), diff --git a/routers/box.py b/routers/box.py index beaa90e..15f8ae3 100644 --- a/routers/box.py +++ b/routers/box.py @@ -7,7 +7,7 @@ from api import api_call from routers.customer import _process_customer -from utils import extract_sn, normalize_items, remove_sn, status_to_str, list_to_str, str_to_list, get_coordinates, get_box_map_link +from utils import normalize_items, list_to_str, str_to_list, get_coordinates, get_box_map_link router = APIRouter(prefix='/box') diff --git a/routers/customer.py b/routers/customer.py index 875a6d1..f2da1ca 100644 --- a/routers/customer.py +++ b/routers/customer.py @@ -8,8 +8,7 @@ from fastapi.responses import JSONResponse from api import api_call -from utils import list_to_str, to_2gis_link, to_neo_link, normalize_items, extract_sn, remove_sn,\ - parse_agreement, status_to_str, format_mac +from utils import list_to_str, to_2gis_link, to_neo_link, normalize_items, extract_sn, remove_sn, parse_agreement, status_to_str, format_mac router = APIRouter(prefix='/customer') PHONE_LENGTH = 9 diff --git a/routers/employee.py b/routers/employee.py index 76b5246..86a2be3 100644 --- a/routers/employee.py +++ b/routers/employee.py @@ -12,8 +12,7 @@ def api_get_employee_login(login: str, password: str): return { 'status': 'success', 'correct': result, - 'id': api_call('employee', 'get_employee_id', f'data_typer=login&data_value={login}').get('id') - if result else None + 'id': api_call('employee', 'get_employee_id', f'data_typer=login&data_value={login}').get('id') if result else None } @router.get('/name/{id}') diff --git a/routers/neomobile.py b/routers/neomobile.py index bec6fea..54a4872 100644 --- a/routers/neomobile.py +++ b/routers/neomobile.py @@ -78,9 +78,10 @@ def neomobile_api_get_customer(request: Request, id: int): @router.post('/task') def neomobile_api_post_task(customer_id: int, phone: str, reason: str, comment: str): - id = api_call('task', 'add', f'work_typer=37&work_datedo={get_current_time()}&customer_id=\ -{customer_id}&author_employee_id=184&opis={comment}&deadline_hour=72&employee_id=184&\ -division_id=81')['Id'] + id = api_call('task', 'add', + f'work_typer=37&work_datedo={get_current_time()}&customer_id={customer_id}&author_employee_id=184&' + f'opis={comment}&deadline_hour=72&employee_id=184&division_id=81' + )['Id'] set_additional_data(17, 28, id, 'Приложение') #TODO: make own appeal type set_additional_data(17, 29, id, phone) set_additional_data(17, 30, id, reason) @@ -119,10 +120,8 @@ def neomobile_api_get_task(id: int): 'status': {'id': data['state']['id'], 'name': data['state']['name']}, 'address': {'id': data['address']['addressId'], 'text': data['address']['text']}, 'customer': data['customer'][0], - 'reason': data['additional_data']['30']['value'] if '30' in data['additional_data'] - else None, - 'phone': data['additional_data']['29']['value'] if '29' in data['additional_data'] - else None, + 'reason': data['additional_data']['30']['value'] if '30' in data['additional_data'] else None, + 'phone': data['additional_data']['29']['value'] if '29' in data['additional_data'] else None, 'comments': [{ 'id': comment['comment_id'], 'content': comment['text'], @@ -153,13 +152,9 @@ def neomobile_api_get_inventory(request: Request, id: int): 'data': [ { 'id': inventory['id'], - 'name': unescape([ - name for name in names if name['id'] == inventory['catalog_id'] - ][0]['name']), + 'name': unescape([name for name in names if name['id'] == inventory['catalog_id']][0]['name']), 'type': { - 'id': [ - name for name in names if name['id'] == inventory['catalog_id'] - ][0]['inventory_section_catalog_id'], + 'id': [name for name in names if name['id'] == inventory['catalog_id']][0]['inventory_section_catalog_id'], 'name': [ category['name'] for category in request.app.state.tmc_categories if category['id'] == [ @@ -177,13 +172,11 @@ def neomobile_api_get_inventory(request: Request, id: int): @router.get('/documents') def neomobile_api_get_documents(id: int): - attachs = list(api_call('attach', 'get', f'object_id={id}&object_type=customer') - ['data'].values()) + attachs = list(api_call('attach', 'get', f'object_id={id}&object_type=customer')['data'].values()) tasks = api_call('task', 'get_list', f'customer_id={id}')['list'].split(',') for task in tasks: try: - attachs.extend(api_call('attach', 'get', f'object_id={task}&object_type=task') - ['data'].values()) + attachs.extend(api_call('attach', 'get', f'object_id={task}&object_type=task')['data'].values()) except AttributeError: continue return { @@ -193,12 +186,9 @@ def neomobile_api_get_documents(id: int): 'attachs': [ { 'id': attach['id'], - 'url': api_call('attach', 'get_file_temporary_link', - f'uuid={attach["id"]}')['data'], - 'name': attach['internal_filepath'] if '.' in attach['internal_filepath'] else - attach['internal_filepath'] + '.png', - 'extension': attach['internal_filepath'].split('.')[1].lower() - if '.' in attach['internal_filepath'] else 'png', + 'url': api_call('attach', 'get_file_temporary_link', f'uuid={attach["id"]}')['data'], + 'name': attach['internal_filepath'] if '.' in attach['internal_filepath'] else attach['internal_filepath'] + '.png', + 'extension': attach['internal_filepath'].split('.')[1].lower() if '.' in attach['internal_filepath'] else 'png', 'created_at': attach['date_add'] } for attach in attachs ] diff --git a/routers/task.py b/routers/task.py index 3737250..2c95543 100644 --- a/routers/task.py +++ b/routers/task.py @@ -193,7 +193,9 @@ def api_get_tasks( ): tasks = [] if customer_id is not None: - tasks = list(map(int, str_to_list(api_call('task', 'get_list', f'customer_id={customer_id}&order_by=date_add&{f"&limit={limit}" if limit else ""}{f"&offset={skip}" if skip else ""}')['list']))) + tasks = list(map(int, str_to_list( + api_call('task', 'get_list', f'customer_id={customer_id}&order_by=date_add&{f"&limit={limit}" if limit else ""}{f"&offset={skip}" if skip else ""}')['list'] + ))) if (limit or skip) and get_count: tasks_count = api_call('task', 'get_list', f'customer_id={customer_id}')['count'] else: @@ -213,9 +215,8 @@ def api_get_tasks( 'created_at': comment['dateAdd'], 'author': { 'id': comment['employee_id'], - 'name': (api_call('employee', 'get_data', f'id={comment["employee_id"]}') - .get('data', {}).get(str(comment['employee_id']), {}).get('name') - if get_employee_names else None) + 'name': api_call('employee', 'get_data', f'id={comment["employee_id"]}').get('data', {}).get(str(comment['employee_id']), {}).get('name') + if get_employee_names else None } if comment.get('employee_id') else None, 'content': unescape(comment['comment']) } for comment in task.get('comments', {}).values() @@ -242,8 +243,7 @@ def api_get_tasks( 'type': task['additional_data'].get('28', {}).get('value') }, } if task['type']['id'] == 38 else { - 'coord': list(map(float, task['additional_data']['7']['value'].split(','))) - if '7' in task['additional_data'] else None, + 'coord': list(map(float, task['additional_data']['7']['value'].split(','))) if '7' in task['additional_data'] else None, 'tariff': task['additional_data'].get('25', {}).get('value'), 'connect_type': task['additional_data'].get('27', {}).get('value') } if task['type']['id'] == 28 else None, @@ -253,9 +253,8 @@ def api_get_tasks( }, 'author': { 'id': task['author_employee_id'], - 'name': (api_call('employee', 'get_data', f'id={task["author_employee_id"]}') - .get('data', {}).get(str(task['author_employee_id']), {}).get('name') - if get_employee_names else None) + 'name': api_call('employee', 'get_data', f'id={task["author_employee_id"]}').get('data', {}).get(str(task['author_employee_id']), {}).get('name') + if get_employee_names else None }, 'status': { 'id': task['state']['id'], @@ -265,8 +264,7 @@ def api_get_tasks( 'address': { 'id': task['address'].get('addressId'), 'name': task['address'].get('text'), - 'apartment': unescape(task['address']['apartment']) - if task['address'].get('apartment') else None + 'apartment': unescape(task['address']['apartment']) if task['address'].get('apartment') else None }, 'customer': { 'id': customer['id'], diff --git a/utils.py b/utils.py index d27efb1..dc4062d 100644 --- a/utils.py +++ b/utils.py @@ -1,6 +1,5 @@ """Simple utils like parse agreement or build 2 gis link""" from datetime import datetime as dt -from functools import reduce from urllib.parse import urljoin @@ -8,14 +7,13 @@ def parse_agreement(agreement: str | None) -> int | None: - """ - Parse agreement string into an integer if it contains only digits. + """Parse agreement string into an integer if it contains only digits. Args: agreement (str | None): Agreement value as a string. Returns: - int|None: Parsed integer if valid, otherwise None. + int | None: Parsed integer if valid, otherwise None. """ if agreement: if agreement.isdigit(): @@ -23,8 +21,7 @@ def parse_agreement(agreement: str | None) -> int | None: return None def remove_sn(data: str) -> str: - """ - Extract the name part from a string formatted like 'name (sn)'. + """Extract the name part from a string formatted like 'name (sn)'. If parentheses are present, the substring before them is returned. Otherwise, the original string is returned. @@ -40,8 +37,7 @@ def remove_sn(data: str) -> str: return data def extract_sn(data: str) -> None | str: - """ - Extract serial number from a string in the format 'name(sn)'. + """Extract serial number from a string in the format 'name(sn)'. If the string ends with '()', it is treated as empty and None is returned. Otherwise, the substring inside parentheses is returned. @@ -58,8 +54,7 @@ def extract_sn(data: str) -> None | str: return data.rsplit('(', maxsplit=1)[-1].rstrip().rstrip(')') def status_to_str(status: int) -> str: - """ - Convert numeric status code to human-readable text. + """Convert numeric status code to human-readable text. Args: status (int): Status code (0 = off, 1 = pause, 2 = active). @@ -78,8 +73,7 @@ def status_to_str(status: int) -> str: return 'Неизвестен' def list_to_str(data: list) -> str: - """ - Join a list of strings into a single comma-separated string. + """Join a list of strings into a single comma-separated string. Args: data (list): List of string elements. @@ -90,8 +84,7 @@ def list_to_str(data: list) -> str: return ','.join(map(str, data)) def str_to_list(data: str) -> list: - """ - Convert a comma-separated string into a list of trimmed strings. + """Convert a comma-separated string into a list of trimmed strings. Args: data (str): Input string with items separated by commas. @@ -102,8 +95,7 @@ def str_to_list(data: str) -> list: return [item.strip() for item in data.split(",") if item.strip()] def to_neo_link(lat: float, lon: float) -> str: - """ - Build a NeoTelecom map link from latitude and longitude. + """Build a NeoTelecom map link from latitude and longitude. Args: lat (float): Latitude coordinate. @@ -116,8 +108,7 @@ def to_neo_link(lat: float, lon: float) -> str: @{lat},{lon},18z' def to_2gis_link(lat: float, lon: float) -> str: - """ - Build a 2GIS map link from latitude and longitude. + """Build a 2GIS map link from latitude and longitude. Args: lat (float): Latitude coordinate. @@ -150,8 +141,7 @@ def normalize_items(raw: dict) -> list: return [data] def get_attach_url(path: str) -> str: - """ - Build full attachment URL by joining base and relative path. + """Build full attachment URL by joining base and relative path. Args: path (str): Relative path to the attachment. @@ -162,8 +152,7 @@ def get_attach_url(path: str) -> str: return urljoin(ATTACH_URL, path) def get_current_time() -> str: - """ - Get the current local time formatted as 'YYYY.MM.DD HH:MM:SS'. + """Get the current local time formatted as 'YYYY.MM.DD HH:MM:SS'. Returns: str: Current time string. @@ -171,8 +160,7 @@ def get_current_time() -> str: return dt.now().strftime("%Y.%m.%d %H:%M:%S") def format_mac(mac: str | None) -> str | None: - """ - Format MAC address (insert ":" between every 2 symbols) + """Format MAC address (insert ":" between every 2 symbols) Args: mac (str | None): MAC address without ":" @@ -185,6 +173,14 @@ def format_mac(mac: str | None) -> str | None: return ':'.join(mac.replace('-', '')[i:i + 2] for i in range(0, len(mac.replace('-', '')), 2)) def get_coordinates(polygon: list[list[float]] | None) -> list[float] | None: + """Get coorinates from polyfon + + Args: + polygon (list[list[float]] | None): Polygon + + Returns: + list[float] | None: Coordinates as [lat, lon] + """ if not polygon: return None points = polygon[:-1] @@ -193,6 +189,15 @@ def get_coordinates(polygon: list[list[float]] | None) -> list[float] | None: return [sum(lats) / len(lats), sum(lons) / len(lons)] def get_box_map_link(coords: list[float] | None, box_id: int) -> str | None: + """Get link to box in map + + Args: + coords (list[float] | None): Coordinates as [lat, lon] + box_id (int): Box id + + Returns: + str | None: Link + """ if coords is None: return None return f'https://us.neotelecom.kg/map/show?opt_wh=1&by_building={box_id}&is_show_center_marker=1@{coords[0]},{coords[1]},18z' \ No newline at end of file From 9d6f96fe1f88d094b8199b738ce296a00ed29ff0 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Wed, 7 Jan 2026 17:42:44 +0300 Subject: [PATCH 09/34] fix(customer/box): fix fetch olt data if fetch by device fail --- routers/customer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routers/customer.py b/routers/customer.py index f2da1ca..5956628 100644 --- a/routers/customer.py +++ b/routers/customer.py @@ -85,7 +85,7 @@ def _process_customer(request_tariffs: list, request_groups: list, customer: dic onu_level = ont.get('level_onu_rx') else: olt = api_call('commutation', 'get_data', f'object_type=customer&object_id={customer["id"]}&is_finish_data=1')['data'] - if olt.get('finish', {}).get('object_type') != 'switch': + if isinstance(olt, dict) and olt.get('finish', {}).get('object_type') != 'switch': olt_id = olt['finish']['object_id'] return { From cccf31e19535dfcb1afb7aaf4ab98bcfd4af4447 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Wed, 7 Jan 2026 17:56:54 +0300 Subject: [PATCH 10/34] fix(tariff): fix logic in calc disconnect --- tariff.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/tariff.py b/tariff.py index 758fe0a..a2838cb 100644 --- a/tariff.py +++ b/tariff.py @@ -60,20 +60,21 @@ def __init__(self, content: str): def calc_disconnect(tariffs: list[Tariff], balance: float, connected_at: datetime) -> datetime | None: base_sum = sum([t.price for t in tariffs]) / 30 # sum per day - free = sum([t.free_days for t in tariffs]) - sale = reduce(lambda a, b: a * b, [t.sale / 100 for t in tariffs if t.type == TariffType.SALE], 0) + if base_sum == 0: + return None - saled_sum = (base_sum - base_sum * sale) if sale else base_sum sale_tariff = next((t for t in tariffs if t.type == TariffType.SALE), None) # first sale tariff now = datetime.now() days_since_connect = (now - connected_at).days + free_days = sum(t.free_days for t in tariffs) + free = max(0, free_days - days_since_connect) - if sale_tariff and sale_tariff.sale_days > 0: - sale_remaining = max(0, sale_tariff.sale_days - days_since_connect) + if sale_tariff: sale_multiplier = 1 - sale_tariff.sale / 100 + sale_remaining = max(0, sale_tariff.sale_days - days_since_connect) if sale_tariff.sale_days > 0 else 0 else: + sale_multiplier = 1 sale_remaining = 0 - sale_multiplier = 1 if not sale_tariff else 1 - sale_tariff.sale / 100 saled_sum = base_sum * sale_multiplier From dbab903912a0a90d0031842272b3ba902cc03cc2 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Wed, 7 Jan 2026 20:59:24 +0300 Subject: [PATCH 11/34] fix(tariff/customer/box): fix tariff name --- main.py | 3 +-- routers/customer.py | 6 +++--- tariff.py | 9 +++++++++ 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/main.py b/main.py index 5d179ee..cb79235 100644 --- a/main.py +++ b/main.py @@ -48,8 +48,7 @@ 'host': olt['host'], 'online': bool(olt['is_online']), 'location': unescape(olt['location']) - } for olt in api_call('device', 'get_data', 'object_type=olt&is_hide_ifaces_data=1')['data'] - .values() + } for olt in api_call('device', 'get_data', 'object_type=olt&is_hide_ifaces_data=1')['data'].values() ] app.state.divisions = [ { diff --git a/routers/customer.py b/routers/customer.py index f399d93..cbb6181 100644 --- a/routers/customer.py +++ b/routers/customer.py @@ -10,7 +10,7 @@ from api import api_call from utils import list_to_str, to_2gis_link, to_neo_link, normalize_items, extract_sn, remove_sn, parse_agreement, status_to_str, format_mac -from tariff import calc_disconnect +from tariff import calc_disconnect, Tariff router = APIRouter(prefix='/customer') PHONE_LENGTH = 9 @@ -57,9 +57,9 @@ def api_get_customer_search(query: str): }, 404) -def _process_customer(request_tariffs: list, request_groups: list, customer: dict, get_olt_data: bool = True): +def _process_customer(request_tariffs: dict[int, Tariff], request_groups: dict, customer: dict, get_olt_data: bool = True): tariffs = [ - {'id': int(tariff['id']), 'name': request_tariffs[tariff['id']]} + {'id': int(tariff['id']), 'name': request_tariffs[tariff['id']].content, 'prices': request_tariffs[tariff['id']].to_dict()} for tariff in customer['tariff']['current'] if tariff['id'] ] diff --git a/tariff.py b/tariff.py index a2838cb..2449bdc 100644 --- a/tariff.py +++ b/tariff.py @@ -57,6 +57,15 @@ def __init__(self, content: str): else: self.type = TariffType.NONE + def to_dict(self) -> dict: + return { + 'price': self.price, + 'free_days': self.free_days, + 'sale': self.sale, + 'sale_days': self.sale_days, + 'type': self.type.value + } + def calc_disconnect(tariffs: list[Tariff], balance: float, connected_at: datetime) -> datetime | None: base_sum = sum([t.price for t in tariffs]) / 30 # sum per day From 5fb12c6c4277400050690c333a2156651c29cc9d Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 13:22:20 +0300 Subject: [PATCH 12/34] fix(ont): add debug log --- ont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ont.py b/ont.py index d4d8b05..1ecb0eb 100644 --- a/ont.py +++ b/ont.py @@ -219,6 +219,7 @@ def _read_output(channel: Channel, force: bool = True): print('read output takes more than 20 sceonds') print(output) sleep(0.01) + print(output) return '\n'.join(output.splitlines()[1:]) if output.count('\n') > 1 else output def _parse_output(raw: str) -> tuple[dict, list[list[dict]]]: From 3d5b6a51d9b3821b21c750d9a9bd9d53706f737a Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 13:26:27 +0300 Subject: [PATCH 13/34] fix(ont): double tim limit if no new data; remove debug log --- ont.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/ont.py b/ont.py index 1ecb0eb..e416c8e 100644 --- a/ont.py +++ b/ont.py @@ -208,8 +208,8 @@ def _read_output(channel: Channel, force: bool = True): if time() - last_data_time > 1.5 and len(output.strip().strip('\n').splitlines()) > 5: print('no new data more than 1.5 seconds') break - if time() - last_data_time > 10 and len(output.strip().strip('\n').splitlines()) <= 5: - print('no new data more than 10 seconds') + if time() - last_data_time > 20 and len(output.strip().strip('\n').splitlines()) <= 5: + print('no new data more than 20 seconds') print(output) break if time() - start_time > 5 and not force: @@ -219,7 +219,6 @@ def _read_output(channel: Channel, force: bool = True): print('read output takes more than 20 sceonds') print(output) sleep(0.01) - print(output) return '\n'.join(output.splitlines()[1:]) if output.count('\n') > 1 else output def _parse_output(raw: str) -> tuple[dict, list[list[dict]]]: From b4e4aeac1ce6f7a2d966a4c4386ccb8944eedf0a Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 13:44:01 +0300 Subject: [PATCH 14/34] fix(ont): return old time limit; add break if read takes 20+ secs fix(ont): add one more newline in display ont command fix(ont): add one more newline in display ont port attr catv command fix(ont): make temperture not required --- ont.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/ont.py b/ont.py index e416c8e..01a4cdc 100644 --- a/ont.py +++ b/ont.py @@ -50,7 +50,7 @@ def search_ont(sn: str, host: str) -> tuple[dict, str | None] | None: try: channel, ssh, olt_name = _connect_ssh(host) - channel.send(bytes(f"display ont info by-sn {sn}\n", 'utf-8')) + channel.send(bytes(f"display ont info by-sn {sn}\n\n", 'utf-8')) parsed_ont_info = _parse_basic_info(_read_output(channel)) if 'error' in parsed_ont_info: @@ -69,7 +69,7 @@ def search_ont(sn: str, host: str) -> tuple[dict, str | None] | None: catv_results = [] for port_num in range(1, (ont_info['_catv_ports'] or 2) + 1): sleep(0.07) - channel.send(bytes(f"display ont port attribute {ont_info['interface']['port']} {ont_info['ont_id']} catv {port_num}\n", 'utf-8')) + channel.send(bytes(f"display ont port attribute {ont_info['interface']['port']} {ont_info['ont_id']} catv {port_num}\n\n", 'utf-8')) catv = _parse_port_status(_read_output(channel)) catv_results.append(catv) @@ -205,19 +205,20 @@ def _read_output(channel: Channel, force: bool = True): break sleep(0.05) - if time() - last_data_time > 1.5 and len(output.strip().strip('\n').splitlines()) > 5: - print('no new data more than 1.5 seconds') + if time() - last_data_time > 2 and len(output.strip().strip('\n').splitlines()) > 5: + print('no new data more than 2 seconds') break - if time() - last_data_time > 20 and len(output.strip().strip('\n').splitlines()) <= 5: - print('no new data more than 20 seconds') + if time() - last_data_time > 10 and len(output.strip().strip('\n').splitlines()) <= 5: + print('no new data more than 10 seconds') print(output) break if time() - start_time > 5 and not force: print('read output takes more than 5 seconds') break if time() - start_time > 20: - print('read output takes more than 20 sceonds') + print('read output takes more than 20 seconds') print(output) + break sleep(0.01) return '\n'.join(output.splitlines()[1:]) if output.count('\n') > 1 else output @@ -355,7 +356,7 @@ def _parse_basic_info(raw: str) -> dict: 'online': data.get('Run state', False), 'mem_load': data.get('Memory occupation'), 'cpu_load': data.get('CPU occupation'), - 'temp': data['Temperature'], + 'temp': data.get('Temperature'), 'ip': data['ONT IP 0 address/mask'].split('/')[0] if data.get('ONT IP 0 address/mask') else None, 'last_down_cause': data.get('Last down cause'), 'last_down': data.get('Last down time'), From 9d7e808058e738a1a6b8bc35e9a2d5e198a789f0 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 14:39:37 +0300 Subject: [PATCH 15/34] fix(ont): increase sleep before get port attribute --- ont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ont.py b/ont.py index 01a4cdc..a174975 100644 --- a/ont.py +++ b/ont.py @@ -68,7 +68,7 @@ def search_ont(sn: str, host: str) -> tuple[dict, str | None] | None: catv_results = [] for port_num in range(1, (ont_info['_catv_ports'] or 2) + 1): - sleep(0.07) + sleep(0.1) channel.send(bytes(f"display ont port attribute {ont_info['interface']['port']} {ont_info['ont_id']} catv {port_num}\n\n", 'utf-8')) catv = _parse_port_status(_read_output(channel)) catv_results.append(catv) From ac731de6678ce04201a600a674f0f2776a26cf2a Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 15:24:04 +0300 Subject: [PATCH 16/34] fix(ont): add one more newline in display ont optical info command --- ont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ont.py b/ont.py index a174975..0b584a5 100644 --- a/ont.py +++ b/ont.py @@ -62,7 +62,7 @@ def search_ont(sn: str, host: str) -> tuple[dict, str | None] | None: _clear_buffer(channel) if ont_info.get('online'): - channel.send(bytes(f"display ont optical-info {ont_info['interface']['port']} {ont_info['ont_id']}\n", 'utf-8')) + channel.send(bytes(f"display ont optical-info {ont_info['interface']['port']} {ont_info['ont_id']}\n\n", 'utf-8')) optical_info = _parse_optical_info(_read_output(channel)) ont_info['optical'] = optical_info From b33db97caaefcbee49d18f3bc6df767a3b2bf845 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 15:25:19 +0300 Subject: [PATCH 17/34] fix(ont): remove try/except --- ont.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/ont.py b/ont.py index 0b584a5..aad3aaa 100644 --- a/ont.py +++ b/ont.py @@ -47,7 +47,7 @@ def search_ont(sn: str, host: str) -> tuple[dict, str | None] | None: """Search ONT by serial number and return its basic, optical and catv data""" ont_info: dict = {} olt_name = None - try: + if True: #try: channel, ssh, olt_name = _connect_ssh(host) channel.send(bytes(f"display ont info by-sn {sn}\n\n", 'utf-8')) @@ -101,9 +101,9 @@ def search_ont(sn: str, host: str) -> tuple[dict, str | None] | None: ping_result = _ping(ont_info['ip']) if 'ip' in ont_info else None ont_info['ping'] = float(ping_result.split(' ', maxsplit=1)[0]) if ping_result else None return ont_info, olt_name - except Exception as e: - print(f'error search ont: {e.__class__.__name__}: {e}') - return {'online': False, 'detail': str(e)}, olt_name + # except Exception as e: + # print(f'error search ont: {e.__class__.__name__}: {e}') + # return {'online': False, 'detail': str(e)}, olt_name def reset_ont(host: str, id: int, interface: dict) -> dict: From 87549900f885e72b47b6d46015f3862383ad3ed2 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 15:28:29 +0300 Subject: [PATCH 18/34] fix(ont): add debug log in parse output --- ont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ont.py b/ont.py index aad3aaa..ca1a449 100644 --- a/ont.py +++ b/ont.py @@ -223,6 +223,7 @@ def _read_output(channel: Channel, force: bool = True): return '\n'.join(output.splitlines()[1:]) if output.count('\n') > 1 else output def _parse_output(raw: str) -> tuple[dict, list[list[dict]]]: + print(raw) def _parse_value(value: str) -> str | float | int | bool | None: value = value.strip().rstrip('/') value = split(r"\+06:00|%|\(\w*\)$", value, maxsplit=1)[0] # remove "+06:00", "%", and units From 5ab742490d5bfa35b6d3d248153c40ea4cdc89da Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 15:30:44 +0300 Subject: [PATCH 19/34] fix(ont): add delay before interface --- ont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ont.py b/ont.py index ca1a449..ab933ef 100644 --- a/ont.py +++ b/ont.py @@ -111,6 +111,7 @@ def reset_ont(host: str, id: int, interface: dict) -> dict: try: channel, ssh, _ = _connect_ssh(host) + sleep(0.1) channel.send(bytes(f"interface gpon {interface['fibre']}/{interface['service']}\n", 'utf-8')) sleep(0.1) _clear_buffer(channel) From cb9788623c3718a897adcde60d0a2cad43cbf2fc Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 15:37:24 +0300 Subject: [PATCH 20/34] fix(ont): add one more newline in display service port command --- ont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ont.py b/ont.py index ab933ef..3a0985a 100644 --- a/ont.py +++ b/ont.py @@ -92,7 +92,7 @@ def search_ont(sn: str, host: str) -> tuple[dict, str | None] | None: ont_info['service_port'] = _parse_service_port(_read_output(channel), ont_info['interface']) if ont_info['service_port']: sleep(0.07) - channel.send(bytes(f'display mac-address service-port {ont_info["service_port"]}\n', 'utf-8')) + channel.send(bytes(f'display mac-address service-port {ont_info["service_port"]}\n\n', 'utf-8')) ont_info['mac'] = _parse_mac(_read_output(channel)) channel.close() From 5611deaab29295d5dc1f25b42ab330ef589ff96a Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 17:27:40 +0300 Subject: [PATCH 21/34] fix(ont): add debug log in parse mac --- ont.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/ont.py b/ont.py index 3a0985a..8347ad8 100644 --- a/ont.py +++ b/ont.py @@ -418,9 +418,11 @@ def _parse_service_port(raw: str, interface: dict) -> int | None: return _parse_output(raw)[1][0][0].get('INDEX') def _parse_mac(raw: str) -> str | None: + print(raw) if 'Failure: There is not any MAC address record' in raw: return raw = raw.replace('MAC TYPE', 'MAC-TYPE') # avoid extra spaces for better parsing (prefer "-") + print(_parse_output(raw)[1][0][0].get('MAC')) return format_mac(_parse_output(raw)[1][0][0].get('MAC')) def _parse_onts_info(output: str) -> tuple[int, int, list[dict]] | tuple[dict, None, None]: From 27d8824b6bbe1db13d508c1bbb1af20b86f121aa Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 17:29:38 +0300 Subject: [PATCH 22/34] fix(ont): remove raw debug log --- ont.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/ont.py b/ont.py index 8347ad8..e5893c2 100644 --- a/ont.py +++ b/ont.py @@ -224,7 +224,6 @@ def _read_output(channel: Channel, force: bool = True): return '\n'.join(output.splitlines()[1:]) if output.count('\n') > 1 else output def _parse_output(raw: str) -> tuple[dict, list[list[dict]]]: - print(raw) def _parse_value(value: str) -> str | float | int | bool | None: value = value.strip().rstrip('/') value = split(r"\+06:00|%|\(\w*\)$", value, maxsplit=1)[0] # remove "+06:00", "%", and units @@ -418,11 +417,10 @@ def _parse_service_port(raw: str, interface: dict) -> int | None: return _parse_output(raw)[1][0][0].get('INDEX') def _parse_mac(raw: str) -> str | None: - print(raw) if 'Failure: There is not any MAC address record' in raw: return raw = raw.replace('MAC TYPE', 'MAC-TYPE') # avoid extra spaces for better parsing (prefer "-") - print(_parse_output(raw)[1][0][0].get('MAC')) + print(_parse_output(raw)[1][0][0]) return format_mac(_parse_output(raw)[1][0][0].get('MAC')) def _parse_onts_info(output: str) -> tuple[int, int, list[dict]] | tuple[dict, None, None]: From db9bc8de073925525b4bab25cafec8807f6ca3ce Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 17:32:57 +0300 Subject: [PATCH 23/34] fix(ont): remove extra text near mac table --- ont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ont.py b/ont.py index e5893c2..1fc7434 100644 --- a/ont.py +++ b/ont.py @@ -420,6 +420,7 @@ def _parse_mac(raw: str) -> str | None: if 'Failure: There is not any MAC address record' in raw: return raw = raw.replace('MAC TYPE', 'MAC-TYPE') # avoid extra spaces for better parsing (prefer "-") + raw = raw.replace('It will take some time, please wait...', '') # remove extra text because it is near to table and can perceived as heading print(_parse_output(raw)[1][0][0]) return format_mac(_parse_output(raw)[1][0][0].get('MAC')) From 1711b8b538f8775d23cb5acebf93a772bf595850 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 17:33:56 +0300 Subject: [PATCH 24/34] fix(ont): return raw debug log --- ont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ont.py b/ont.py index 1fc7434..5492edb 100644 --- a/ont.py +++ b/ont.py @@ -421,7 +421,7 @@ def _parse_mac(raw: str) -> str | None: return raw = raw.replace('MAC TYPE', 'MAC-TYPE') # avoid extra spaces for better parsing (prefer "-") raw = raw.replace('It will take some time, please wait...', '') # remove extra text because it is near to table and can perceived as heading - print(_parse_output(raw)[1][0][0]) + print(raw) return format_mac(_parse_output(raw)[1][0][0].get('MAC')) def _parse_onts_info(output: str) -> tuple[int, int, list[dict]] | tuple[dict, None, None]: From 47b2eb1ed430e14dd530abd8dc5476004bf89a63 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 17:40:19 +0300 Subject: [PATCH 25/34] fix(ont): add parsed data debug log --- ont.py | 1 + 1 file changed, 1 insertion(+) diff --git a/ont.py b/ont.py index 5492edb..49f85fc 100644 --- a/ont.py +++ b/ont.py @@ -422,6 +422,7 @@ def _parse_mac(raw: str) -> str | None: raw = raw.replace('MAC TYPE', 'MAC-TYPE') # avoid extra spaces for better parsing (prefer "-") raw = raw.replace('It will take some time, please wait...', '') # remove extra text because it is near to table and can perceived as heading print(raw) + print(_parse_output(raw)) return format_mac(_parse_output(raw)[1][0][0].get('MAC')) def _parse_onts_info(output: str) -> tuple[int, int, list[dict]] | tuple[dict, None, None]: From a28911fdd92fa767f815c4f94acd12d87ac6c0f0 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 17:43:16 +0300 Subject: [PATCH 26/34] fix(ont): add logs in parse output --- ont.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/ont.py b/ont.py index 49f85fc..6dd43ad 100644 --- a/ont.py +++ b/ont.py @@ -307,26 +307,26 @@ def _find_all(string: str, finding: str) -> list[int]: if is_table_heading: # table next heading line line = line[len(table_heading_raw) - len(table_heading_raw.lstrip()):] full_line = line - # print('begin table parse; fields:', table_fields, 'appendixes line:', line) + print('begin table parse; fields:', table_fields, 'appendixes line:', line) for i, field in enumerate(table_fields): raw_index = _find_all(table_heading_raw.lstrip(), field)[table_fields[:i].count(field)] - # print('found fields:', _find_all(table_heading_raw.lstrip(), field)) + print('found fields:', _find_all(table_heading_raw.lstrip(), field)) if search(r'\w', full_line[raw_index:raw_index + len(field)]): - # print('found non space appendix:', full_line[raw_index:raw_index + len(field)] + '... for', field) + print('found non space appendix:', full_line[raw_index:raw_index + len(field)] + '... for', field) appendix = line.lstrip().split(' ', maxsplit=1)[0] - # print('cleaned appendix:', appendix) + print('cleaned appendix:', appendix) table_fields[i] += '-' + appendix - # print('invoked to field:', table_fields[i]) + print('invoked to field:', table_fields[i]) line = line[line.index(appendix) + len(appendix):] - # print('line truncated:', line) + print('line truncated:', line) else: - # print('found space appendix for', field) + print('found space appendix for', field) # spaces += len(table_heading_raw[:raw_index]) - len(table_heading_raw[:raw_index].rstrip()) - 1 line = line[len(field):] - # print('line truncated:', line) + print('line truncated:', line) return fields, [table for table in tables if table] From d2665432d0c03938740dd7185a932f4dda77dd4c Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 17:52:52 +0300 Subject: [PATCH 27/34] fix(ont): fix F /S/P interfaces in mac table --- ont.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/ont.py b/ont.py index 6dd43ad..3a1416b 100644 --- a/ont.py +++ b/ont.py @@ -93,7 +93,7 @@ def search_ont(sn: str, host: str) -> tuple[dict, str | None] | None: if ont_info['service_port']: sleep(0.07) channel.send(bytes(f'display mac-address service-port {ont_info["service_port"]}\n\n', 'utf-8')) - ont_info['mac'] = _parse_mac(_read_output(channel)) + ont_info['mac'] = _parse_mac(_read_output(channel), ont_info['interface']) channel.close() ssh.close() @@ -410,17 +410,21 @@ def _parse_service_port(raw: str, interface: dict) -> int | None: raw = raw.replace( f"{interface['fibre']}/{interface['service']} /{interface['port']}", f"{interface['fibre']}/ {interface['service']}/ {interface['port']}" - ) # change F/S /P -> F/ S/ P/ + ) # change F/S /P -> F/ S/ P raw = raw.replace(' Switch-Oriented Flow List\n', '') # remove extra text if 'Failure: No service virtual port can be operated' in raw: return return _parse_output(raw)[1][0][0].get('INDEX') -def _parse_mac(raw: str) -> str | None: +def _parse_mac(raw: str, interface: dict) -> str | None: if 'Failure: There is not any MAC address record' in raw: return raw = raw.replace('MAC TYPE', 'MAC-TYPE') # avoid extra spaces for better parsing (prefer "-") raw = raw.replace('It will take some time, please wait...', '') # remove extra text because it is near to table and can perceived as heading + raw = raw.replace( + f"{interface['fibre']}/{interface['service']} /{interface['port']}", + f"{interface['fibre']}/ {interface['service']}/ {interface['port']}" + ) # change F /S/P -> F /S /P print(raw) print(_parse_output(raw)) return format_mac(_parse_output(raw)[1][0][0].get('MAC')) From f45496679bd36dce16d5bffe5859b461363c055c Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 17:55:18 +0300 Subject: [PATCH 28/34] fix(ont): fix F /S/P interfaces pattern --- ont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ont.py b/ont.py index 3a1416b..8ea5289 100644 --- a/ont.py +++ b/ont.py @@ -422,7 +422,7 @@ def _parse_mac(raw: str, interface: dict) -> str | None: raw = raw.replace('MAC TYPE', 'MAC-TYPE') # avoid extra spaces for better parsing (prefer "-") raw = raw.replace('It will take some time, please wait...', '') # remove extra text because it is near to table and can perceived as heading raw = raw.replace( - f"{interface['fibre']}/{interface['service']} /{interface['port']}", + f"{interface['fibre']} /{interface['service']}/{interface['port']}", f"{interface['fibre']}/ {interface['service']}/ {interface['port']}" ) # change F /S/P -> F /S /P print(raw) From 9553142beb7845fc26bfdaf8aa63dfde0d6d1857 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 17:56:13 +0300 Subject: [PATCH 29/34] fix(ont): fix F /S/P interfaces replacing --- ont.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ont.py b/ont.py index 8ea5289..daa95d2 100644 --- a/ont.py +++ b/ont.py @@ -423,7 +423,7 @@ def _parse_mac(raw: str, interface: dict) -> str | None: raw = raw.replace('It will take some time, please wait...', '') # remove extra text because it is near to table and can perceived as heading raw = raw.replace( f"{interface['fibre']} /{interface['service']}/{interface['port']}", - f"{interface['fibre']}/ {interface['service']}/ {interface['port']}" + f"{interface['fibre']} /{interface['service']} /{interface['port']}" ) # change F /S/P -> F /S /P print(raw) print(_parse_output(raw)) From 0588d36f7d4bb8d00242b20499b08e937dff4246 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 18:00:36 +0300 Subject: [PATCH 30/34] fix(ont): remove deubg logs in parse output --- ont.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/ont.py b/ont.py index daa95d2..2108201 100644 --- a/ont.py +++ b/ont.py @@ -263,6 +263,7 @@ def _find_all(string: str, finding: str) -> list[int]: if "Command:" in raw: raw = raw.split("Command:", 1)[1] raw = "\n".join(raw.splitlines()[2:]) + print(raw) for line in raw.splitlines(): if '#' in line: # prompt lines continue @@ -307,26 +308,26 @@ def _find_all(string: str, finding: str) -> list[int]: if is_table_heading: # table next heading line line = line[len(table_heading_raw) - len(table_heading_raw.lstrip()):] full_line = line - print('begin table parse; fields:', table_fields, 'appendixes line:', line) + # print('begin table parse; fields:', table_fields, 'appendixes line:', line) for i, field in enumerate(table_fields): raw_index = _find_all(table_heading_raw.lstrip(), field)[table_fields[:i].count(field)] - print('found fields:', _find_all(table_heading_raw.lstrip(), field)) + # print('found fields:', _find_all(table_heading_raw.lstrip(), field)) if search(r'\w', full_line[raw_index:raw_index + len(field)]): - print('found non space appendix:', full_line[raw_index:raw_index + len(field)] + '... for', field) + # print('found non space appendix:', full_line[raw_index:raw_index + len(field)] + '... for', field) appendix = line.lstrip().split(' ', maxsplit=1)[0] - print('cleaned appendix:', appendix) + # print('cleaned appendix:', appendix) table_fields[i] += '-' + appendix - print('invoked to field:', table_fields[i]) + # print('invoked to field:', table_fields[i]) line = line[line.index(appendix) + len(appendix):] - print('line truncated:', line) + # print('line truncated:', line) else: - print('found space appendix for', field) + # print('found space appendix for', field) # spaces += len(table_heading_raw[:raw_index]) - len(table_heading_raw[:raw_index].rstrip()) - 1 line = line[len(field):] - print('line truncated:', line) + # print('line truncated:', line) return fields, [table for table in tables if table] @@ -425,7 +426,7 @@ def _parse_mac(raw: str, interface: dict) -> str | None: f"{interface['fibre']} /{interface['service']}/{interface['port']}", f"{interface['fibre']} /{interface['service']} /{interface['port']}" ) # change F /S/P -> F /S /P - print(raw) + raw = raw.replace('VLAN ID', 'VLAN-ID') print(_parse_output(raw)) return format_mac(_parse_output(raw)[1][0][0].get('MAC')) From 22a8903a35b3797a27c10ff7c9351dd3a36e4658 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sat, 17 Jan 2026 19:03:14 +0300 Subject: [PATCH 31/34] fix(ont): add min space count condition for table heading --- ont.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ont.py b/ont.py index 2108201..29c273d 100644 --- a/ont.py +++ b/ont.py @@ -297,7 +297,7 @@ def _find_all(string: str, finding: str) -> list[int]: tables[-1].append({key: _parse_value(value.strip()) for key, value in zip(table_fields, split(r'\s+', line.strip()))}) continue - if not is_table and len(split(r'\s+', line)) > 1: # table start heading line + if not is_table and len(split(r'\s+', line)) > 1 and search(r'\s{3,}', line): # table start heading line is_table = True is_table_heading = True table_heading_raw = line @@ -483,4 +483,4 @@ def _ping(ip: str) -> None | str: return f"{time_match.group(1)} ms" if time_match else "-" return None except Exception: - return None \ No newline at end of file + return None From b67191084825c8bd35a9dadc791405fbadf259f7 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sun, 1 Mar 2026 17:50:40 +0300 Subject: [PATCH 32/34] fix(ont): fix parsing on tables with invalid fisrt line [ai] --- ont.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/ont.py b/ont.py index d4d8b05..46fd0d2 100644 --- a/ont.py +++ b/ont.py @@ -256,6 +256,7 @@ def _find_all(string: str, finding: str) -> list[int]: table_heading_raw = '' is_notes = False table_fields = [] + col_positions = [] raw = raw.replace(PAGINATION, '').replace('\x1b[37D', '').replace('x1b[37D', '') # remove stupid pagination if "Command:" in raw: @@ -291,7 +292,21 @@ def _find_all(string: str, finding: str) -> list[int]: continue if is_table and not is_table_heading: # table field line - tables[-1].append({key: _parse_value(value.strip()) for key, value in zip(table_fields, split(r'\s+', line.strip()))}) + row_data = {} + line_stripped = line.rstrip() + for i, field in enumerate(table_fields): + if i < len(col_positions) - 1: + start = col_positions[i] + end = col_positions[i + 1] + value = line_stripped[start:end].strip() if start < len(line_stripped) else '' + else: + start = col_positions[i] if i < len(col_positions) else 0 + value = line_stripped[start:].strip() if start < len(line_stripped) else '' + if value: + row_data[field] = _parse_value(value) + else: + row_data[field] = None + tables[-1].append(row_data) continue if not is_table and len(split(r'\s+', line)) > 1: # table start heading line @@ -300,6 +315,12 @@ def _find_all(string: str, finding: str) -> list[int]: table_heading_raw = line table_fields = [c for c in split(r'\s+', line.strip()) if c] tables.append([]) + # calculate column pos by heading + col_positions = [] + for field in table_fields: + pos = table_heading_raw.find(field) + if pos != -1: + col_positions.append(pos) continue if is_table_heading: # table next heading line From 608c2193eaea1844732434569079cbaaf638f845 Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sun, 1 Mar 2026 19:17:12 +0300 Subject: [PATCH 33/34] fix(ont): refix parsing [ai] --- ont.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/ont.py b/ont.py index 793cdb1..1a6b154 100644 --- a/ont.py +++ b/ont.py @@ -295,18 +295,26 @@ def _find_all(string: str, finding: str) -> list[int]: continue if is_table and not is_table_heading: # table field line + first_value_match = search(r'\S+', line) + first_value_pos = first_value_match.start() if first_value_match else 0 + + start_col = 0 + for i, col_pos in enumerate(col_positions): + if first_value_pos >= col_pos: + start_col = i + else: + break + row_data = {} line_stripped = line.rstrip() for i, field in enumerate(table_fields): - if i < len(col_positions) - 1: + if i < start_col: + row_data[field] = None + elif i < len(col_positions): start = col_positions[i] - end = col_positions[i + 1] + end = col_positions[i + 1] if i + 1 < len(col_positions) else len(line_stripped) value = line_stripped[start:end].strip() if start < len(line_stripped) else '' - else: - start = col_positions[i] if i < len(col_positions) else 0 - value = line_stripped[start:].strip() if start < len(line_stripped) else '' - if value: - row_data[field] = _parse_value(value) + row_data[field] = _parse_value(value) if value else None else: row_data[field] = None tables[-1].append(row_data) From ace902e21809047f754ff9a1e7bd8bfb11f5feda Mon Sep 17 00:00:00 2001 From: firedotguy Date: Sun, 1 Mar 2026 19:28:37 +0300 Subject: [PATCH 34/34] =?UTF-8?q?fix:=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D0=BF=D1=80=D0=BE=D0=B1=D0=BB=D0=B5=D0=BC=D0=BD?= =?UTF-8?q?=D1=8B=D0=B9=20=D0=B1=D0=BB=D0=BE=D0=BA=20is=5Ftable=5Fheading?= =?UTF-8?q?=20=D0=B2=20=5Fparse=5Foutput?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ont.py | 35 ----------------------------------- 1 file changed, 35 deletions(-) diff --git a/ont.py b/ont.py index 1a6b154..fbf5ec3 100644 --- a/ont.py +++ b/ont.py @@ -240,17 +240,6 @@ def _parse_value(value: str) -> str | float | int | bool | None: return False return value - def _find_all(string: str, finding: str) -> list[int]: - result = [] - for i, _ in enumerate(string): - if string[i:i + len(finding)] == finding: - if len(string) > i + len(finding): - if string[i + len(finding)] == ' ': - result.append(i) - else: - result.append(i) - return result - fields = {} tables = [] is_table = False @@ -334,30 +323,6 @@ def _find_all(string: str, finding: str) -> list[int]: col_positions.append(pos) continue - if is_table_heading: # table next heading line - line = line[len(table_heading_raw) - len(table_heading_raw.lstrip()):] - full_line = line - # print('begin table parse; fields:', table_fields, 'appendixes line:', line) - - for i, field in enumerate(table_fields): - raw_index = _find_all(table_heading_raw.lstrip(), field)[table_fields[:i].count(field)] - # print('found fields:', _find_all(table_heading_raw.lstrip(), field)) - - if search(r'\w', full_line[raw_index:raw_index + len(field)]): - # print('found non space appendix:', full_line[raw_index:raw_index + len(field)] + '... for', field) - appendix = line.lstrip().split(' ', maxsplit=1)[0] - # print('cleaned appendix:', appendix) - table_fields[i] += '-' + appendix - # print('invoked to field:', table_fields[i]) - line = line[line.index(appendix) + len(appendix):] - # print('line truncated:', line) - - else: - # print('found space appendix for', field) - # spaces += len(table_heading_raw[:raw_index]) - len(table_heading_raw[:raw_index].rstrip()) - 1 - line = line[len(field):] - # print('line truncated:', line) - return fields, [table for table in tables if table] def _parse_basic_info(raw: str) -> dict: