From ccf82b026f7cb57b5e81c2188a83439d75a068cf Mon Sep 17 00:00:00 2001 From: Iliyas Date: Tue, 16 Jun 2026 17:34:19 +0500 Subject: [PATCH] =?UTF-8?q?feat:=20add=20=D0=9C=D0=B5=D1=82=D1=80=D0=B8?= =?UTF-8?q?=D0=BA=D0=B8=20=D0=9C=D0=9F=20page=20+=20app-metrics=20updater?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - new page app_stats/index.html (login-gated, same style/nav) - app_stats/app_metrics.json data (year-over-year comparison, NEW badges) - updater/update_app_metrics.py: adaptive SQL (Jan 1 -> yesterday vs prev year) - run both updaters from run_update.bat; refactor shared git push --- app_stats/app_metrics.json | 236 ++++++++ app_stats/index.html | 560 +++++++++++++++++++ app_stats/Результаты по метрикам 202606.xlsx | Bin 0 -> 12266 bytes index.html | 1 + updater/run_update.bat | 12 +- updater/update_app_metrics.py | 236 ++++++++ updater/update_kpi.py | 22 +- 7 files changed, 1057 insertions(+), 10 deletions(-) create mode 100644 app_stats/app_metrics.json create mode 100644 app_stats/index.html create mode 100644 app_stats/Результаты по метрикам 202606.xlsx create mode 100644 updater/update_app_metrics.py diff --git a/app_stats/app_metrics.json b/app_stats/app_metrics.json new file mode 100644 index 0000000..07ce86a --- /dev/null +++ b/app_stats/app_metrics.json @@ -0,0 +1,236 @@ +{ + "generated_at": "2026-06-16T17:30:19", + "cur_year": 2026, + "prev_year": 2025, + "period_label": "с 1 января по 15 июня", + "range": { + "start": "2026-01-01", + "end": "2026-06-15" + }, + "metrics": [ + { + "key": "my_services", + "label": "Мои услуги", + "prev": 1459571, + "cur": 1769470, + "growth": 0.21232197680003234, + "is_new": false + }, + { + "key": "traffic", + "label": "Детализация трафика", + "prev": 1079736, + "cur": 1271563, + "growth": 0.17766102084213178, + "is_new": false + }, + { + "key": "payments", + "label": "Платежи", + "prev": 553808, + "cur": 730185, + "growth": 0.3184804119839367, + "is_new": false + }, + { + "key": "orders", + "label": "Заявки", + "prev": 635621, + "cur": 826255, + "growth": 0.2999177182629271, + "is_new": false + }, + { + "key": "loyalty", + "label": "Лояльность", + "prev": 464365, + "cur": 470969, + "growth": 0.014221571393192856, + "is_new": false + }, + { + "key": "pay", + "label": "Оплата", + "prev": 302510, + "cur": 337103, + "growth": 0.11435324452084229, + "is_new": false + }, + { + "key": "billing_detail", + "label": "Детали счета", + "prev": 358290, + "cur": 496915, + "growth": 0.3869072539004717, + "is_new": false + }, + { + "key": "viktorina", + "label": "Викторина KT Club", + "prev": 298475, + "cur": 213879, + "growth": -0.28342742273222216, + "is_new": false + }, + { + "key": "partners", + "label": "Акции партнеров", + "prev": 94639, + "cur": 197009, + "growth": 1.08168936696288, + "is_new": false + }, + { + "key": "tv_plus", + "label": "TV+", + "prev": 95647, + "cur": 64104, + "growth": -0.32978556567377965, + "is_new": false + }, + { + "key": "boosters", + "label": "Бустеры", + "prev": 53649, + "cur": 121065, + "growth": 1.2566124252082984, + "is_new": false + }, + { + "key": "roaming", + "label": "Роуминг", + "prev": 39200, + "cur": 22160, + "growth": -0.4346938775510204, + "is_new": false + }, + { + "key": "pereoform", + "label": "Переоформление", + "prev": 23537, + "cur": 34570, + "growth": 0.46875132769681777, + "is_new": false + }, + { + "key": "aitu_music", + "label": "Aitu Music", + "prev": 0, + "cur": 8651, + "growth": null, + "is_new": true + }, + { + "key": "online_booking", + "label": "Онлайн очередь", + "prev": 5421, + "cur": 22144, + "growth": 3.084855192768862, + "is_new": false + }, + { + "key": "my_docs", + "label": "Мои документы", + "prev": 0, + "cur": 53376, + "growth": null, + "is_new": true + }, + { + "key": "dz_statement", + "label": "Справка о ДЗ", + "prev": 0, + "cur": 132795, + "growth": null, + "is_new": true + }, + { + "key": "new_boosters_roaming_kcell", + "label": "Новая линейка бустеров и роумингов Кселл", + "prev": 0, + "cur": 28626, + "growth": null, + "is_new": true + }, + { + "key": "adsl", + "label": "ADSL отключение услуги", + "prev": 0, + "cur": 69, + "growth": null, + "is_new": true + }, + { + "key": "law_and_order", + "label": "Закон и порядок", + "prev": 0, + "cur": 1555, + "growth": null, + "is_new": true + }, + { + "key": "acs", + "label": "ACS", + "prev": 0, + "cur": 9154, + "growth": null, + "is_new": true + }, + { + "key": "kaspi_freedom_pay", + "label": "Прием платежей через Freedom и Kaspi", + "prev": 0, + "cur": 61481, + "growth": null, + "is_new": true + }, + { + "key": "csat", + "label": "CSAT", + "prev": 0, + "cur": 2486, + "growth": null, + "is_new": true + }, + { + "key": "multicustomer", + "label": "Мультикастомер", + "prev": 0, + "cur": 164, + "growth": null, + "is_new": true + }, + { + "key": "tv_plus_setup", + "label": "Настройка TV+", + "prev": 0, + "cur": 4545, + "growth": null, + "is_new": true + }, + { + "key": "static_ip", + "label": "Статический IP", + "prev": 0, + "cur": 108, + "growth": null, + "is_new": true + }, + { + "key": "turbo_button", + "label": "Turbo кнопка", + "prev": 0, + "cur": 4312, + "growth": null, + "is_new": true + }, + { + "key": "real_estate_docs", + "label": "Справка о недвижимости", + "prev": 0, + "cur": 78, + "growth": null, + "is_new": true + } + ] +} diff --git a/app_stats/index.html b/app_stats/index.html new file mode 100644 index 0000000..984935e --- /dev/null +++ b/app_stats/index.html @@ -0,0 +1,560 @@ + + + + + +Метрики МП — Казахтелеком 2026 + + + + + + + +
+ +
+ + +
+
+
+ +
+

📱 Метрики МП

+

Использование функций мобильного приложения

+
+
+
+ + ← KPI Dashboard +
+
+ +
+
Загрузка данных…
+ + +
+
+ + + + diff --git a/app_stats/Результаты по метрикам 202606.xlsx b/app_stats/Результаты по метрикам 202606.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..bafefe4a2e489201ccf54608c05c9f0687c5a8d7 GIT binary patch literal 12266 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ÐCw}AQ$^D*r>Noq-Fd3kEh4_3t~yVOVQP&_ zMi8d4abAPzfhsCa=4Z>Z6|-m?w{1&juNbb3dNY63`4rCcYswZ^`0<-l2-mAkZd>Vs z$((?9mQmSXC1fZRmVH!qeMij%8!z_L>z;xOst6pYb-&1(1w8Z1B8dkIFIHeJ>#EY; z_Sh9K6m$DU3JKeCNXp#7*yQtJFL87b>Tn%LCl|3M3)9dsc?Rgr1|8S_u7IY7B4j|nY3@5FAk-&EVK=7 zx+lpUsuWGYJk2WhRjwp`58>kis`0$;)P?w)~%@2 zk26^{A)vMwDH>v-i)Kfm+bHz$-u0+nt0*Bv3T+RO`~)xg#PsF))~!HO!Gm=`m6O;n zF_bIWZtD7(DQyQlalpeDH+@$VDB7(&`eaAKkeU=$ob3gKr0w_+&?LKNM^qNMqiTV)XdKkla=FDYZt!t%+83IH{+d|_ z97AV0O#~e~c^2ewWE(Q_IFEdF2an>b3i319cX6Munw|+YuuMrL$af^(VtlE7!?C|R z#cZ2~w7@8adqEj+*!?)j@o@K*f*sip;j}CHQN!_JOGSc?$YtN&4L+H!RfdjxVS}PB zKe+r3@4Uf+cDuq)<_5~Yjfeu{Y5CBL{$TWmKpq8Z(sTIkT~Z0@XKUDy=DE|vi3|xr zUvw&5+#*eOba-s=TuMaD#Z{cw;&1&vn{!aXYT?`E-|veiOoFK^0+}(T7QDwaP(KS> z=1)DGILaghp7uKH6v6n3QmiSP2=93vR^-VwD~wNbP{0jd!7cZyz`yc z1!^F+D=(f(HGTpgw{#Oo^dq@;2OgkrzaF%xXpf`cbnY&Ir1^SV2sy?spg6Z2l&75_ z%lT>gU0|TjuDj=#;qnTAqSAWlje>+tzBZn#5yZvqC&@g#iZgXO7+Nfc@*|BARxa79 z6{NbImS+KRqq0uc9*nLxh)}(d8?Z5WgWp>$ zV%6vMOKvOkDt2hLH9xewd0v-1K&)ds`f|p<$%(77Y$9a%;c{K$W`C`9jbG)}GJ%X% zwaD_Cu}!AZ2BZ++6Ic@Jiyu++2F|JkS^)s)I8zE?`YWjD~b zANU@y4t@)$ugxLsWErVqkq*``pGt*~vGIRyS9Z*3A7=n0-i$sUyKqb<7xu)zWbo|0 z2~aEfQkF@-0uTNvb^8@YbemGN{4;OS}4^7^y(At{dz0Gq#j7=a-5H?_UYi?~Z7&I$zue z^hOahlmPt05t-=P85$@!*qK@x|MEv3idyCva;TozPbfV+lzMD(z*s5qP+&ehf8Obe zfhAVlTBEbdRk>KWXBD#-5MI6f8OA(q@sq<$2g5Cqs}X7W6&=rNaGUe~Xv#a|c{Pt2 z(;0Jmc`qErvJ3v9*YqXVqqTZi{DBG^IT_Oqm0CoU02)@8)- zEv#Bg`Wmx5atzbkwqkegxipV%eNuyHk>&|X8Q0OG^dsj^yqFVz;gT;l99y%ZoPsW4 zb!abCKC1&OKGCtJNsnRs(8RS6KU~w?20pw*HSFIR?RkGiS6t4lWh3S}6XfK(>=eI- zZZv`otn=_-Y5cLB6#F8_91k7)uJqN- z!<+RT&%6t^DfzcjP0qyhqMbCoy!3tXIF0dk3Ob^!9nat@Q88RBZr*hdf~kiPRwd2_ z#Z4YB&P*1;0V%Mt(FDsqX09#!5e72=_qRV9ub9-c5s+j|IWAg_7SUQURUADb`W3|u zaQKs}Eue+7+*eoq-O$SeA)6P$87P*kN!29MZKXexuRuCQXJI0~#;n}m&y>j?oPxpO zn3o0=Xr7$l!gkX`Z2s zD*l}B<<)+Ci}EDmw#w@m7rmGW5aWpX$dfC|F(~x?!bgwq*!d>Y+!GnH&~kL-^5!2i zeWyKTxLVNanK1zX@_+4s4G63bhIR^u4h}z2OaI$5Bb}b9)whHZ@tzOBpciKC5;{dx zK^qdf0w{?cLsL`ra^^zja)&oSe3EQ2G1A;^e$7Yl?Vc+%4}c1tW+=+0@JlGBoMKob z+IYab1195fNuD76t{ysm$gi?DsJh6Q4}?#PQ8-Fy7(yN_bzq)fu_7tom-F?&R`f?0 zgwM0%g+e-#=Gc#S-Pzrq`l(x|O~ZvvPrN#QbI`+RWLnA-jh}xtySp}Ca5-b@f4(iL zd5ls?RqzAN_lxt~M56VlUDbN2L@r-C>FN-r_KVk!LH5X|z^VuuebcZ|OfS=Y16=w^ z^?d8e3E6`~!)hR?@})}4ZqyXfd>UvaqLyg1f<$oID>Uvy;8sDzLP~!oS6y0fR-q^` z_Z{*v*fyeYO<5WG{v$7JdG=nr`o@cz8Tultspd06fuMf&i?@x7L0@#GQbD*FApDhA+{uq{zJ)jlHJbc6fOdt=$ROqsZ3M( z6bYmRUB zVsi&3%&elH!if*m4xap$+MG^>A&H&2sgH+j_!td(-Xo>b|0Bo>>bAKP zKsE;i^~q8HYIC|aHvcm@(3bsirN?zyVK9PvyrQi=!?*ouepEnLtsLMiM2M)EJ5g(G zXy8J_w?nEeXca@CJrHa#a5DYnkgmC>gTFKTl=fy&#qQAKqU)@1x?XmVRO6H21knddOp; z+?|v?x3Zq3u6w$;Lkx|JUfDV;tH5`?!h9lco*$fETwc6CWaE%_Y$%G>O)Yhf?>}yC zuP(ww5YwcSTG4QfW~E}1J;0oMh;uVUv>rt4R4epu&3h~oosUckpTS^B&%qpb?6x$* zv33i?&(5GtIVgO&Pi-86mi%otMe%2>kJUIv`VVpi+CGG$g~7LV5p` zacvDclYeiT@?~d#?7ohTqxppBh9! z3oOX=Tu&hOHHrwM<`0su@P qdi)=x;dk-Bd;GtO7gGL3{15*xD**+v;Gb1HB!B@31U583pZ*VGAdpu8 literal 0 HcmV?d00001 diff --git a/index.html b/index.html index 4d7af66..1984ebc 100644 --- a/index.html +++ b/index.html @@ -280,6 +280,7 @@ body { font-family: var(--font-base); background: var(--color-bg); color: var(--
+ 📱 Метрики МП
diff --git a/updater/run_update.bat b/updater/run_update.bat index 2ac40f1..5b2cc0e 100644 --- a/updater/run_update.bat +++ b/updater/run_update.bat @@ -13,8 +13,16 @@ if exist "venv\Scripts\python.exe" ( set "PYEXE=python" ) +REM 1) KPI dashboard (drb_iliyas_kpi_2026.csv) "%PYEXE%" "%~dp0update_kpi.py" -set "RC=%ERRORLEVEL%" +set "RC_KPI=%ERRORLEVEL%" -echo Exit code: %RC% +REM 2) Метрики МП (app_stats/app_metrics.json) +"%PYEXE%" "%~dp0update_app_metrics.py" +set "RC_APP=%ERRORLEVEL%" + +echo KPI exit code: %RC_KPI% App-metrics exit code: %RC_APP% + +REM Ненулевой код, если упал хотя бы один +set /a RC=%RC_KPI%+%RC_APP% exit /b %RC% diff --git a/updater/update_app_metrics.py b/updater/update_app_metrics.py new file mode 100644 index 0000000..ec5462a --- /dev/null +++ b/updater/update_app_metrics.py @@ -0,0 +1,236 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Ежедневное обновление статистики «Метрики МП» из Impala. + +Сравнивает использование функций мобильного приложения за текущий год +(с 1 января по ВЧЕРАШНИЙ день) с аналогичным периодом прошлого года. +Результат пишется в ../app_stats/app_metrics.json и пушится в ветку pages — +его читает страница app_stats/index.html. + +Подключение к Impala, конфиг и git-push переиспользуются из update_kpi.py. +""" + +import sys +import json +import datetime as dt +from pathlib import Path + +import update_kpi as base # общие функции: load_config, connect_impala, git_commit_push, ... + +log = base.log +REPO_DIR = base.REPO_DIR +OUT_PATH = REPO_DIR / "app_stats" / "app_metrics.json" +OUT_REL = "app_stats/app_metrics.json" + +# ─── Метрики: ключ в SQL → человекочитаемое название (в порядке SELECT) ─── +METRICS = [ + ("my_services", "Мои услуги"), + ("traffic", "Детализация трафика"), + ("payments", "Платежи"), + ("orders", "Заявки"), + ("loyalty", "Лояльность"), + ("pay", "Оплата"), + ("billing_detail", "Детали счета"), + ("viktorina", "Викторина KT Club"), + ("partners", "Акции партнеров"), + ("tv_plus", "TV+"), + ("boosters", "Бустеры"), + ("roaming", "Роуминг"), + ("pereoform", "Переоформление"), + ("aitu_music", "Aitu Music"), + ("online_booking", "Онлайн очередь"), + ("my_docs", "Мои документы"), + ("dz_statement", "Справка о ДЗ"), + ("new_boosters_roaming_kcell", "Новая линейка бустеров и роумингов Кселл"), + ("adsl", "ADSL отключение услуги"), + ("law_and_order", "Закон и порядок"), + ("acs", "ACS"), + ("kaspi_freedom_pay", "Прием платежей через Freedom и Kaspi"), + ("csat", "CSAT"), + ("multicustomer", "Мультикастомер"), + ("tv_plus_setup", "Настройка TV+"), + ("static_ip", "Статический IP"), + ("turbo_button", "Turbo кнопка"), + ("real_estate_docs", "Справка о недвижимости"), +] + +_MONTHS_RU = ["", "января", "февраля", "марта", "апреля", "мая", "июня", + "июля", "августа", "сентября", "октября", "ноября", "декабря"] + + +def date_range(today: dt.date | None = None): + """Возвращает (cur_year, prev_year, start_cur, end_cur, start_prev, end_prev) как date.""" + today = today or dt.date.today() + end_cur = today - dt.timedelta(days=1) # вчера + cur_year = today.year + prev_year = cur_year - 1 + start_cur = dt.date(cur_year, 1, 1) + start_prev = dt.date(prev_year, 1, 1) + # та же дата прошлого года; подстраховка на 29 февраля + try: + end_prev = dt.date(prev_year, end_cur.month, end_cur.day) + except ValueError: + end_prev = dt.date(prev_year, end_cur.month, 28) + return cur_year, prev_year, start_cur, end_cur, start_prev, end_prev + + +def build_sql(start_cur, end_cur, start_prev, end_prev) -> str: + return f""" +with t as ( + select round(report_period_id/100) as report_year, + count(case when event_type = 'OPENWSCREENMYSERVICES' then 1 end) as my_services, + count(case when event_type = 'OPENWINDOWDETALIZTION' then 1 end) as traffic, + count(case when event_type = 'OPENWINDOWPAYMENT' then 1 end) as payments, + count(case when event_type = 'OPENSCREENAPPEALS' then 1 end) as orders, + count(case when event_type in ('banner_auth', 'banner_unauth', 'loyalty_banner_slider_auth', 'loyalty_banner_slider_unauth', 'get_bonus_opened', 'bonus_opened','promo_partners_opened','company_promo_opened') then 1 end) as loyalty, + count(case when event_type = 'WINDOWPAYMENT' then 1 end) as pay, + count(case when event_type = 'OPENWINDOWBILLING' then 1 end) as billing_detail, + count(case when event_type = 'game_page' then 1 end) as viktorina, + count(case when event_type = 'promo_partners_opened' then 1 end) as partners, + count(case when event_type = 'OPENWINDOWTVPLUS' then 1 end) as tv_plus, + count(case when event_type = 'MOBCONNECTIONOPENWINDOWADDITIONALTRAFFIC' then 1 end) as boosters, + count(case when event_type = 'OPENWINDOWROAMING' then 1 end) as roaming, + count(case when event_type = 'reregistration_comm_start' then 1 end) as pereoform, + count(case when event_type = 'aitu_music_banner_clicked' then 1 end) as aitu_music, + count(case when event_type = 'ONLINE_BOOKING_SERVICES' then 1 end) as online_booking, + count(case when event_type in ('EMPTYLISTDOCS', 'HASLISTDOCS') then 1 end) as my_docs, + count(case when event_type = 'PDFSTATEMENT' then 1 end) as dz_statement, + count(case when event_type in ('booster_success_screen_kcell', 'ROAMINGPACKAGEMOBILEKCELL') then 1 end) as new_boosters_roaming_kcell, + count(case when event_type = 'law_and_order_service_clicked' then 1 end) as law_and_order, + count(case when event_type = 'ACS_DEVICE_SELECTION_OPEN' then 1 end) as acs, + count(case when event_type in ('PAYMENTWASSUCCESSFULFREEDOM', 'PAYWITHKASPI') then 1 end) as kaspi_freedom_pay, + count(case when event_type = 'csat_screen_sent' then 1 end) as csat, + count(case when event_type = 'multicustomer_completed_screen_viewed' then 1 end) as multicustomer, + count(case when event_type = 'tv_plus_setup_success_viewed' then 1 end) as tv_plus_setup, + count(case when event_type = 'static_ip_connect_success_viewed' then 1 end) as static_ip, + count(case when event_type = 'turbo_activation_success_viewed' then 1 end) as turbo_button, + count(case when event_type = 'real_estate_docs_screen_shown' then 1 end) as real_estate_docs + from drb.drb_iliyas_amplitude_metrics_full + where entry_date between '{start_cur:%Y-%m-%d}' and '{end_cur:%Y-%m-%d}' + or entry_date between '{start_prev:%Y-%m-%d}' and '{end_prev:%Y-%m-%d}' + group by 1 +) +, a as ( + select year(created_at) as report_year, count(order_id) as adsl + from telecomkz.telecomkz_retention_service_prod_tariff_change_validations + group by 1 +) +select t.report_year, my_services, traffic, payments, orders, loyalty, pay, billing_detail, viktorina, partners, tv_plus, boosters, roaming, pereoform, aitu_music, online_booking, my_docs, dz_statement, new_boosters_roaming_kcell, adsl, law_and_order, acs, kaspi_freedom_pay, csat, multicustomer, +tv_plus_setup, static_ip, turbo_button, real_estate_docs +from t +left join a on t.report_year = a.report_year +order by t.report_year +""".strip() + + +def fetch_by_year(conn, sql): + cur = conn.cursor() + log.info("Выполнение запроса метрик МП...") + cur.execute(sql) + rows = cur.fetchall() + names = [d[0].lower() for d in cur.description] + cur.close() + idx = {n: i for i, n in enumerate(names)} + by_year = {} + for r in rows: + year = int(round(float(r[idx["report_year"]]))) + by_year[year] = {name: r[idx[name]] for name in idx} + log.info("Получено годовых строк: %s", sorted(by_year)) + return by_year, idx + + +def _num(v): + return int(v) if v is not None else 0 + + +def build_payload(by_year, cur_year, prev_year, start_cur, end_cur): + cur_row = by_year.get(cur_year, {}) + prev_row = by_year.get(prev_year, {}) + + metrics = [] + for key, label in METRICS: + cur_v = _num(cur_row.get(key)) + prev_v = _num(prev_row.get(key)) + is_new = prev_v == 0 + growth = None if is_new else (cur_v - prev_v) / prev_v + metrics.append({ + "key": key, "label": label, + "prev": prev_v, "cur": cur_v, + "growth": growth, "is_new": is_new, + }) + + period_label = (f"с 1 января по {end_cur.day} {_MONTHS_RU[end_cur.month]}") + return { + "generated_at": dt.datetime.now().isoformat(timespec="seconds"), + "cur_year": cur_year, + "prev_year": prev_year, + "period_label": period_label, + "range": {"start": f"{start_cur:%Y-%m-%d}", "end": f"{end_cur:%Y-%m-%d}"}, + "metrics": metrics, + } + + +def write_if_changed(payload) -> bool: + text = json.dumps(payload, ensure_ascii=False, indent=2) + "\n" + old = "" + if OUT_PATH.exists(): + old = OUT_PATH.read_text(encoding="utf-8") + + # Сравниваем без учёта generated_at (чтобы не коммитить, если данные те же) + def strip_ts(s): + try: + d = json.loads(s) + d.pop("generated_at", None) + return json.dumps(d, ensure_ascii=False, sort_keys=True) + except Exception: + return s + + if strip_ts(old) == strip_ts(text): + log.info("Метрики МП не изменились — коммит не требуется.") + return False + + OUT_PATH.parent.mkdir(parents=True, exist_ok=True) + OUT_PATH.write_text(text, encoding="utf-8") + log.info("app_metrics.json обновлён (%d метрик).", len(payload["metrics"])) + return True + + +def main() -> int: + log.info("=" * 60) + log.info("Старт обновления Метрик МП") + cfg = base.load_config() + + cur_year, prev_year, start_cur, end_cur, start_prev, end_prev = date_range() + log.info("Период: %s..%s (тек.) и %s..%s (пред.)", + start_cur, end_cur, start_prev, end_prev) + sql = build_sql(start_cur, end_cur, start_prev, end_prev) + + base.patch_thrift_ssl() + conn = base.connect_impala(cfg) + try: + by_year, _ = fetch_by_year(conn, sql) + finally: + try: + conn.close() + except Exception: + pass + + if cur_year not in by_year: + log.error("В ответе нет данных за %s — JSON НЕ перезаписан.", cur_year) + return 1 + + payload = build_payload(by_year, cur_year, prev_year, start_cur, end_cur) + if write_if_changed(payload): + base.git_commit_push(cfg, [OUT_REL], + f"data: update app metrics {dt.date.today():%Y-%m-%d}") + log.info("Готово (Метрики МП).") + return 0 + + +if __name__ == "__main__": + try: + sys.exit(main()) + except Exception as e: # noqa: BLE001 + log.exception("ОШИБКА (Метрики МП): %s", e) + sys.exit(1) diff --git a/updater/update_kpi.py b/updater/update_kpi.py index ab468ed..4e06139 100644 --- a/updater/update_kpi.py +++ b/updater/update_kpi.py @@ -241,26 +241,27 @@ def push_url(cfg: dict, mask_token: str): return url -def commit_and_push(cfg: dict): +def git_commit_push(cfg: dict, rel_paths, message: str): + """Коммитит указанные файлы (пути относительно корня репо) и пушит в ветку. + + Переиспользуется и для KPI, и для метрик МП. Если изменений нет — выходит молча. + """ git = cfg.get("git", {}) or {} branch = git.get("branch", "pages") token = (git.get("token") or "") url = push_url(cfg, token) - # Коммитим только CSV — daily-диффы остаются чистыми. - _run_git(["add", "--", CSV_PATH.name]) + _run_git(["add", "--", *rel_paths]) - rc, out = _run_git(["status", "--porcelain", "--", CSV_PATH.name], check=True) + rc, out = _run_git(["status", "--porcelain", "--", *rel_paths], check=True) if not out.strip(): - log.info("Нет изменений CSV для коммита.") + log.info("Нет изменений (%s) для коммита.", ", ".join(rel_paths)) return - msg = f"data: update KPI {datetime.now():%Y-%m-%d}" - _run_git(["commit", "-m", msg]) + _run_git(["commit", "-m", message]) # Пуш с автоматическим rebase при гонке (веб-приложение тоже пушит ai-cache.json через API) for attempt in range(1, 4): - # подтянуть свежий remote и переставить наш коммит сверху _run_git(["fetch", url, branch], check=True, mask=token or None) # --autostash: не падать, если в дереве есть посторонние незакоммиченные правки _run_git(["rebase", "--autostash", "FETCH_HEAD"], check=True) @@ -272,6 +273,11 @@ def commit_and_push(cfg: dict): raise RuntimeError("Не удалось запушить изменения после 3 попыток.") +def commit_and_push(cfg: dict): + # Коммитим только CSV — daily-диффы остаются чистыми. + git_commit_push(cfg, [CSV_PATH.name], f"data: update KPI {datetime.now():%Y-%m-%d}") + + # ═══════════════════════ main ════════════════════════════════ def main() -> int: log.info("=" * 60)