From ffc006ea057de27d86c8acf5a593abce2a572d11 Mon Sep 17 00:00:00 2001 From: Jylpah Date: Thu, 12 Jan 2023 20:51:05 +0200 Subject: [PATCH 1/8] update 9.6 --- tanks.json | 264 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 258 insertions(+), 6 deletions(-) diff --git a/tanks.json b/tanks.json index bb0276a..1017588 100644 --- a/tanks.json +++ b/tanks.json @@ -1,7 +1,7 @@ { "status": "ok", "meta": { - "count": 564 + "count": 580 }, "data": { "1": { @@ -2172,6 +2172,14 @@ "tier": 6, "type": "lightTank" }, + "9777": { + "is_premium": true, + "name": "WZ-114", + "nation": "china", + "tank_id": 9777, + "tier": 9, + "type": "heavyTank" + }, "9793": { "is_premium": false, "name": "Somua SAu 40", @@ -2284,6 +2292,14 @@ "tier": 4, "type": "AT-SPG" }, + "10289": { + "is_premium": true, + "name": "WZ-132-1", + "nation": "china", + "tank_id": 10289, + "tier": 10, + "type": "lightTank" + }, "10353": { "is_premium": true, "name": "Pharaoh", @@ -2293,7 +2309,7 @@ "type": "heavyTank" }, "10369": { - "is_premium": true, + "is_premium": false, "name": "Controcarro 3 Minotauro", "nation": "european", "tank_id": 10369, @@ -2324,6 +2340,22 @@ "tier": 5, "type": "AT-SPG" }, + "10545": { + "is_premium": true, + "name": "Wind", + "nation": "china", + "tank_id": 10545, + "tier": 6, + "type": "lightTank" + }, + "10625": { + "is_premium": false, + "name": "Controcarro 1 Mk. 2", + "nation": "european", + "tank_id": 10625, + "tier": 9, + "type": "AT-SPG" + }, "10753": { "is_premium": false, "name": "ST-I", @@ -2356,6 +2388,14 @@ "tier": 7, "type": "AT-SPG" }, + "10881": { + "is_premium": false, + "name": "Semovente Controcarro mod. 1964", + "nation": "european", + "tank_id": 10881, + "tier": 8, + "type": "AT-SPG" + }, "11009": { "is_premium": false, "name": "KV-4", @@ -2388,6 +2428,14 @@ "tier": 9, "type": "AT-SPG" }, + "11137": { + "is_premium": false, + "name": "Semovente Controcarro mod. 1956", + "nation": "european", + "tank_id": 11137, + "tier": 7, + "type": "AT-SPG" + }, "11265": { "is_premium": true, "name": "T-150", @@ -2412,6 +2460,14 @@ "tier": 8, "type": "AT-SPG" }, + "11393": { + "is_premium": false, + "name": "Semovente M43 Bassotto", + "nation": "european", + "tank_id": 11393, + "tier": 6, + "type": "AT-SPG" + }, "11521": { "is_premium": false, "name": "IS-8", @@ -2444,6 +2500,14 @@ "tier": 6, "type": "AT-SPG" }, + "11649": { + "is_premium": false, + "name": "Semovente M41", + "nation": "european", + "tank_id": 11649, + "tier": 5, + "type": "AT-SPG" + }, "11777": { "is_premium": false, "name": "KV-1", @@ -2532,6 +2596,14 @@ "tier": 4, "type": "mediumTank" }, + "12417": { + "is_premium": true, + "name": "Bisonte C45", + "nation": "european", + "tank_id": 12417, + "tier": 8, + "type": "heavyTank" + }, "12545": { "is_premium": true, "name": "K-91", @@ -2540,6 +2612,14 @@ "tier": 9, "type": "heavyTank" }, + "12673": { + "is_premium": true, + "name": "Bofors Tornvagn", + "nation": "european", + "tank_id": 12673, + "tier": 8, + "type": "heavyTank" + }, "13073": { "is_premium": true, "name": "Pz.Kpfw. II Ausf. G", @@ -3140,6 +3220,14 @@ "tier": 8, "type": "AT-SPG" }, + "19521": { + "is_premium": true, + "name": "Bat-Chat-25CL", + "nation": "france", + "tank_id": 19521, + "tier": 9, + "type": "lightTank" + }, "19537": { "is_premium": false, "name": "Vickers Light 105", @@ -3750,7 +3838,7 @@ }, "25361": { "is_premium": true, - "name": "Waffenträger Camelot", + "name": "Waffenträger Ritter", "nation": "germany", "tank_id": 25361, "tier": 9, @@ -3780,6 +3868,14 @@ "tier": 8, "type": "heavyTank" }, + "25857": { + "is_premium": true, + "name": "Object 777 Version II", + "nation": "ussr", + "tank_id": 25857, + "tier": 10, + "type": "heavyTank" + }, "25889": { "is_premium": true, "name": "Ranger", @@ -3812,6 +3908,38 @@ "tier": 8, "type": "mediumTank" }, + "26913": { + "is_premium": true, + "name": "Frosty", + "nation": "usa", + "tank_id": 26913, + "tier": 6, + "type": "lightTank" + }, + "27169": { + "is_premium": true, + "name": "Cyborg", + "nation": "usa", + "tank_id": 27169, + "tier": 6, + "type": "heavyTank" + }, + "27425": { + "is_premium": false, + "name": "TL-7-120", + "nation": "usa", + "tank_id": 27425, + "tier": 9, + "type": "heavyTank" + }, + "27681": { + "is_premium": true, + "name": "Pawlack Tank", + "nation": "usa", + "tank_id": 27681, + "tier": 6, + "type": "heavyTank" + }, "51201": { "is_premium": true, "name": "KV-220 Beta-Test", @@ -4520,6 +4648,7 @@ "userStr": { "1_test": "#germany_vehicles:1_test", "1_test_short": "#germany_vehicles:1_test_short", + "85mm_D5T-85BML": "85 mm D-5T-85BML", "8V135-2": "8V135-2", "A-20": "A-20", "A-20_mod_1938": "A-20 mod. 1938", @@ -4583,12 +4712,20 @@ "A132_T77_Custom_short": "T77 Cannonball", "A132_T77_short": "T77", "A140_ASTRON_REX_105": "ASTRON Rex 105 mm", + "A140_ASTRON_REX_105_Custom": "ASTRON Rex Ayame", + "A140_ASTRON_REX_105_Custom_short": "ASTRON Rex Ayame", "A140_ASTRON_REX_105_short": "ASTRON Rex", "A141_M-4-Y": "M-IV-Y", "A141_M-4-Y_Custom": "M-IV-Y Stalker", "A141_M-4-Y_Custom_short": "M-IV-Y Stalker", "A141_M-4-Y_short": "M-IV-Y", + "A142_Pawlack_Tank": "Pawlack Tank", + "A142_Pawlack_Tank_short": "Pawlack Tank", + "A146_TL_7_120": "TL-7-120", + "A146_TL_7_120_short": "TL-7-120", "A68_T28D": "T28 Defender", + "A68_T28D_Custom": "Defender D-Gunner", + "A68_T28D_Custom_short": "Defender D-Gunner", "A68_T28D_short": "T28 Defender", "A77_T6M": "T6E1 Grizzly", "A77_T6M_short": "T6E1 Grizzly", @@ -4647,6 +4784,7 @@ "AMX_dracula_short": "Dracula", "AOI-1490": "Continental AOI-1490", "AOI-1490A": "Continental AOI-1490-A", + "AOI_695": "AOI-695", "AOSI_1490-1": "Continental AOSI-1490-1", "AOS_895": "AOS-895", "APCNR": "Pzgr. 41 HK", @@ -4772,6 +4910,10 @@ "Ch12_111_1_2_3_short": "WZ-111 1-4", "Ch14_T34_3": "T-34-3", "Ch14_T34_3_short": "T-34-3", + "Ch15_59_16_BP": "Wind", + "Ch15_59_16_BP_Custom": "Free Wind", + "Ch15_59_16_BP_Custom_short": "Free Wind", + "Ch15_59_16_BP_short": "Wind", "Ch17_WZ131_1_WZ132": "WZ-132", "Ch18_WZ-120": "WZ-120", "Ch19_121": "WZ-121", @@ -4782,6 +4924,8 @@ "Ch22_113": "WZ-113", "Ch22_113_Custom": "WZ-113 Bronze Warrior", "Ch22_113_Custom_short": "WZ-113 Bronze Warrior", + "Ch22_113_Custom_short_v2": "WZ-113 Nine-Colors", + "Ch22_113_Custom_v2": "WZ-113 Nine-Colors", "Ch23_112": "112 Glacial", "Ch24_Type64": "Type 64", "Ch24_Type64_short": "Type 64", @@ -4791,6 +4935,8 @@ "Ch26_59_Patton_Custom": "59-Patton Aratinga", "Ch26_59_Patton_Custom_short": "59-Patton Aratinga", "Ch26_59_Patton_short": "59-Patton", + "Ch29_Type_62C_prot": "WZ-132-1", + "Ch29_Type_62C_prot_short": "WZ-132-1", "Ch34_WZ-131G_FT": "WZ-131G FT", "Ch34_WZ-131G_FT_short": "WZ-131G FT", "Ch35_T-34-2G_FT": "T-34-2G FT", @@ -4815,6 +4961,10 @@ "Ch42_M41D_short": "M41D", "Ch43_WZ-122_2": "WZ-122 TM", "Ch43_WZ-122_2_short": "WZ-122 TM", + "Ch45_WZ_114": "WZ-114", + "Ch45_WZ_114_Custom": "WZ-114 Zhulong", + "Ch45_WZ_114_Custom_short": "WZ-114 Zhulong", + "Ch45_WZ_114_short": "WZ-114", "Ch_LT_vz38": "LT vz. 38", "Ch_LT_vz38_short": "LT vz. 38", "Ch_WZ-112v2": "WZ-112-2", @@ -4823,18 +4973,52 @@ "Ch_WZ_112v2_Custom_short": "WZ-112-2 Celestial", "Chassis1_A140_ASTRON_REX_105": "Rex", "Chassis1_A140_ASTRON_REX_105_short": "Rex", + "Chassis1_Ch29_Type_62C_prot": "WZ-132-1", + "Chassis1_Ch29_Type_62C_prot_short": "WZ-132-1", + "Chassis1_Ch45_WZ_114": "WZ-114", + "Chassis1_Ch45_WZ_114_short": "WZ-114", "Chassis1_F117_Alt_Proto_AMX_30": "AltProto AMX 30", "Chassis1_F117_Alt_Proto_AMX_30_short": "A.P. AMX 30", "Chassis1_GB09_Churchill_VII_BP": "Churchill W", "Chassis1_GB09_Churchill_VII_BP_short": "Churchill W", "Chassis1_It10_P43_bis_BP": "Shadowhunter", "Chassis1_It10_P43_bis_BP_short": "Shadowhunter", + "Chassis1_It15_Semovente_M41": "Semovente M41", + "Chassis1_It15_Semovente_M41_short": "Semovente M41", + "Chassis1_It16_Bassoto": "Semovente M43", + "Chassis1_It16_Bassoto_short": "Semovente M43", + "Chassis1_It17_Semovente_mod_56": "CC mod. 56", + "Chassis1_It17_Semovente_mod_56_short": "CC mod. 56", + "Chassis1_It18_Semovente_mod_64": "CC-64 tipo A", + "Chassis1_It18_Semovente_mod_64_short": "CC-64 tipo A", + "Chassis1_It19_Controcarro_1MK2_Ciclope": "Controcarro 1 Mk. 2 tipo A", + "Chassis1_It19_Controcarro_1MK2_Ciclope_short": "CC 1 Mk. 2 tipo A", + "Chassis1_It22_Progetto_C45_mod_71": "Bisonte C45", + "Chassis1_It22_Progetto_C45_mod_71_short": "Bisonte C45", + "Chassis1_M24_Chaffee_BP": "Frosty", + "Chassis1_M24_Chaffee_BP_short": "Frosty", + "Chassis1_M6E2V2_BP": "Cyborg", + "Chassis1_M6E2V2_BP_short": "Cyborg", "Chassis1_Oth34_PanzerMummy": "Royal Tread", "Chassis1_Oth34_PanzerMummy_short": "Royal Tread", "Chassis1_Oth35_PanzerMejai": "Desert Wanderer", "Chassis1_Oth35_PanzerMejai_short": "Desert Wanderer", + "Chassis1_R119_Object_777": "Object 777 Version II", + "Chassis1_R119_Object_777_short": "Obj. 777 II", "Chassis1_S31_Strv_K": "Stridsvagn KRV", "Chassis1_S31_Strv_K_short": "Stridsvagn KRV", + "Chassis1_S32_Bofors_Tornvagn": "Bofors Tornvagn", + "Chassis1_S32_Bofors_Tornvagn_short": "Tornvagn", + "Chassis2_It15_Semovente_M41": "Semovente M42", + "Chassis2_It15_Semovente_M41_short": "Semovente M42", + "Chassis2_It16_Bassoto": "Semovente M44", + "Chassis2_It16_Bassoto_short": "Semovente M44", + "Chassis2_It17_Semovente_mod_56": "CC mod. 57", + "Chassis2_It17_Semovente_mod_56_short": "CC mod. 57", + "Chassis2_It18_Semovente_mod_64": "CC-64 tipo B", + "Chassis2_It18_Semovente_mod_64_short": "CC-64 tipo B", + "Chassis2_It19_Controcarro_1MK2_Ciclope": "Controcarro 1 Mk. 2 tipo B", + "Chassis2_It19_Controcarro_1MK2_Ciclope_short": "CC 1 Mk. 2 tipo B", "Chi_Ha": "Type 97 Chi-Ha", "Chi_Ha_short": "Chi-Ha", "Chi_He": "Type 1 Chi-He", @@ -4866,6 +5050,8 @@ "Continental_AOI-628-2": "Continental AOI-628-2", "Continental_AOI-628-2A": "Continental AOI-628-2A", "Continental_AOI-628-2_short": "Continental\\nAOI-628-2", + "Continental_AOI_1490": "Continental AOI-1490", + "Continental_AOI_1490M": "Continental AOI-1490-M", "Continental_AOI_1490_1": "Continental AOI-1490-1", "Continental_AOS-895-1": "Continental AOS-895-1", "Continental_AOS-895-1_short": "Continental\\nAOS-895-1", @@ -5006,7 +5192,9 @@ "E50_Ausf_M_Custom": "E 50 M Roaring", "E50_Ausf_M_Custom_short": "E 50 M Roaring", "E50_Ausf_M_Custom_short_v2": "E 50 M Monster", + "E50_Ausf_M_Custom_short_v3": "E 50 M Monster Evo.", "E50_Ausf_M_Custom_v2": "E 50 Ausf. M Monster", + "E50_Ausf_M_Custom_v3": "E 50 M Monster Evo.", "E50_Ausf_M_short": "E 50 M", "ELC_AMX": "AMX ELC bis", "ELC_AMX_short": "AMX ELC bis", @@ -5021,6 +5209,8 @@ "F116_Bat_Chat_Bourrasque_Custom_short": "Bourrasque Noir", "F116_Bat_Chat_Bourrasque_short": "B-C Bourrasque", "F117_Alt_Proto_AMX_30": "AltProto AMX 30", + "F117_Alt_Proto_AMX_30_Custom": "A.P. AMX 30 Disruptor", + "F117_Alt_Proto_AMX_30_Custom_short": "A.P. AMX 30 Disruptor", "F117_Alt_Proto_AMX_30_short": "A.P. AMX 30", "F19_Lorraine40t": "Lorraine 40 t", "F19_Lorraine40t_Com": "Lorraine 40 t Fearless", @@ -5078,6 +5268,7 @@ "Ferdinandstandardketten": "Ferdinand", "Ferdinandturm": "Ferdinand", "Ferdinandvetstaerktketten": "Elefant", + "Fiat_V10": "Fiat V-10", "Firefly_GuP": "Firefly Saunders SP", "Firefly_GuP_short": "Firefly Saunders SP", "First_april": "#other_vehicles:First_april", @@ -5138,6 +5329,8 @@ "G121_Grille_15_L63": "Grille 15", "G121_Grille_15_L63_Custom": "Grille 15 RA1DER", "G121_Grille_15_L63_Custom_short": "Grille 15 RA1DER", + "G121_Grille_15_L63_Custom_short_v3": "Grille 15 Förster", + "G121_Grille_15_L63_Custom_v3": "Grille 15 Förster", "G121_Grille_15_L63_short": "Grille 15", "G136_Tiger_131": "Tiger 131", "G136_Tiger_131_short": "Tiger 131", @@ -5150,6 +5343,8 @@ "G144_Kpz_50t": "Kampfpanzer 50 t", "G144_Kpz_50t_Custom": "Kpz 50 t König", "G144_Kpz_50t_Custom_short": "Kpz 50 t König", + "G144_Kpz_50t_Custom_short_v2": "Kpz 50 t Jäger", + "G144_Kpz_50t_Custom_v2": "Kpz 50 t Jäger", "G144_Kpz_50t_short": "Kpz 50 t", "G147_mPanzer_64": "Kunze Panzer", "G147_mPanzer_64_Custom": "Kunze Pz. Daredevil", @@ -5347,12 +5542,15 @@ "GMC_Allison_V1710-E32_short": "GMC Allison\\nV1710-E32", "GMS-6046B": "GMS 6046B", "GM_6-71_M": "GM 6062 6-71M", + "GM_DIESEL_P_V2": "GM Diesel P V2", "GM_X-11": "GM X-11", "GM_X-12": "GM X-12", "GTD": "GTD", "GT_101": "GT 101", "G_E": "G.W. E 100", "G_E-75RT": "Keiler", + "G_E-75RT_Custom": "Keiler Exterminator", + "G_E-75RT_Custom_short": "Keiler Exterminator", "G_E-75RT_short": "Keiler", "G_E-75TS": "E 75 TS", "G_E-75TS_short": "E 75 TS", @@ -5459,6 +5657,8 @@ "IS_4_Custom_short": "IS-4 Skilled", "IS_6_Custom": "IS-6 Legend", "IS_6_Custom_short": "IS-6 Legend", + "IS_Custom": "IS Steppe", + "IS_Custom_short": "IS Steppe", "Indien_Panzer": "Indien-Panzer", "Indien_Panzer_short": "Indien-Pz.", "It08_Progetto_M40_mod65": "Progetto M40 mod. 65", @@ -5485,12 +5685,28 @@ "It14_P44_Pantera_Custom": "P.44 Pantera Furious", "It14_P44_Pantera_Custom_short": "P.44 Pantera Furious", "It14_P44_Pantera_short": "P.44 Pantera", + "It15_Semovente_M41": "Semovente M41", + "It15_Semovente_M41_short": "Semovente M41", + "It16_Bassoto": "Semovente M43 Bassotto", + "It16_Bassoto_short": "Bassotto", + "It17_Semovente_mod_56": "Semovente Controcarro mod. 1956", + "It17_Semovente_mod_56_short": "SMV CC-56", + "It18_Semovente_mod_64": "Semovente Controcarro mod. 1964", + "It18_Semovente_mod_64_short": "SMV CC-64", + "It19_Controcarro_1MK2_Ciclope": "Controcarro 1 Mk. 2", + "It19_Controcarro_1MK2_Ciclope_short": "CC 1 Mk. 2", "It20_Car_Comb_45t": "Carro da Combattimento 45t", "It20_Car_Comb_45t_Custom": "Carro 45t Glitch", "It20_Car_Comb_45t_Custom_short": "Carro 45t Glitch", "It20_Car_Comb_45t_short": "Carro 45t", "It21_Controcarro3_Minotauro": "Controcarro 3 Minotauro", + "It21_Controcarro3_Minotauro_Custom": "Minotauro Moderno", + "It21_Controcarro3_Minotauro_Custom_short": "Minotauro Moderno", "It21_Controcarro3_Minotauro_short": "Minotauro", + "It22_Progetto_C45_mod_71": "Bisonte C45", + "It22_Progetto_C45_mod_71_Custom": "Bisonte Unbreakable", + "It22_Progetto_C45_mod_71_Custom_short": "Bisonte Unbreakable", + "It22_Progetto_C45_mod_71_short": "Bisonte C45", "J12_Shinobi": "Shinobi", "J12_Shinobi_short": "Shinobi", "J20_Ho_Ri_type3": "Ho-Ri Type III", @@ -5572,6 +5788,8 @@ "KV1": "KV-1", "KV1S_mod_42": "KV-1S mod. 1942", "KV1S_mod_43": "KV-85", + "KV1_Custom": "KV-1 Ghost", + "KV1_Custom_short": "KV-1 Ghost", "KV1s_bear": "KV-1S Football", "KV1s_bear_short": "KV-1S Football", "KV2": "KV-2", @@ -5665,6 +5883,10 @@ "M22_Locust_short": "Locust", "M24A1": "M24A1", "M24_Chaffee": "M24 Chaffee", + "M24_Chaffee_BP": "Frosty", + "M24_Chaffee_BP_Custom": "Frosty Jinglebell", + "M24_Chaffee_BP_Custom_short": "Frosty Jinglebell", + "M24_Chaffee_BP_short": "Frosty", "M24_Chaffee_short": "Chaffee", "M2D27810": "M2D27810", "M2D27812": "M2D27812", @@ -5727,6 +5949,10 @@ "M6D2": "M6D2", "M6E2BP": "Magnus", "M6E2BP_short": "Magnus", + "M6E2V2_BP": "Cyborg", + "M6E2V2_BP_Custom": "Cyborg T-800", + "M6E2V2_BP_Custom_short": "Cyborg T-800", + "M6E2V2_BP_short": "Cyborg", "M7": "M5A1", "M7D1": "M7D1", "M7_Priest": "M7 Priest", @@ -5765,6 +5991,10 @@ "MTLS-1G14": "MTLS-1G14", "MTLS_D1": "MTLS D1", "MTLS_T1": "MTLS T1", + "MTU_870": "MTU 870", + "MTU_870M": "MTU 870M", + "MTU_970": "MTU 970", + "MTU_970M": "MTU 970M", "MTU_MB-838_Ca_M-500": "MTU MB 838 CaM 500B", "MT_50_T": "MT-50T", "M_40": "M-40", @@ -6201,6 +6431,8 @@ "PzVI_Porsche_Turm_short": "Pz.Kpfw. Tiger\\nAusf. E", "PzVI_Standardketten": "Pz.Kpfw. VI H Ausf. H1", "PzVI_Tiger_P": "Tiger (P)", + "PzVI_Tiger_P_Custom": "Tiger (P) Sandsturm", + "PzVI_Tiger_P_Custom_short": "Tiger (P) Sandsturm", "PzVI_Tiger_P_short": "Tiger (P)", "PzVI_Vetstaerktketten": "Pz.Kpfw. Tiger Ausf. E", "PzVI_skin_gamescom": "Tiger I Gamescom", @@ -6267,6 +6499,8 @@ "R115_IS-3_auto": "IS-3 Defender", "R115_IS-3_auto_short": "IS-3 Defender", "R116_ISU122C_Berlin": "ISU-122S", + "R119_Object_777": "Object 777 Version II", + "R119_Object_777_short": "Obj. 777 II", "R120_T22SR": "T-22 medium", "R120_T22SR_Custom": "T-22 Uncatchable", "R120_T22SR_Custom_short": "T-22 Uncatchable", @@ -6290,6 +6524,8 @@ "R146_STG_Custom_short": "STG Guard", "R146_STG_short": "STG", "R149_Object_268_4": "Object 268 Version 4", + "R149_Object_268_4_Custom": "Object 268/4 Chronos", + "R149_Object_268_4_Custom_short": "Object 268/4 Chronos", "R149_Object_268_4_short": "Object 268/4", "R154_T-34E-43": "T-34 shielded", "R154_T-34E-43_short": "T-34 shielded", @@ -6416,7 +6652,11 @@ "S26_Lancen_C_Custom_short": "Lansen Moose", "S26_Lancen_C_short": "Lansen C", "S31_Strv_K": "Strv K", + "S31_Strv_K_Custom": "Strv K Konung", + "S31_Strv_K_Custom_short": "Strv K Konung", "S31_Strv_K_short": "Strv K", + "S32_Bofors_Tornvagn": "Bofors Tornvagn", + "S32_Bofors_Tornvagn_short": "Tornvagn", "S35_captured": "Pz.Kpfw. S35 739 (f)", "S35_captured_short": "Pz. S35", "SA35_4": "SA35 mle. 4", @@ -6430,9 +6670,13 @@ "SOFAM_12_58": "SOFAM 12 58", "SOFAM_8Gxb": "SOFAM 8 Gxb", "SOFAM_8Gxb_A": "SOFAM 8 Gxb", + "SPA_15TB": "SPA 15TB", + "SPA_17TB": "SPA 17TB", "SPA_242": "SPA 242", "SPA_343": "SPA 343", "SPA_344": "SPA 344", + "SPA_mod_15": "SPA mod. 15", + "SPA_mod_17": "SPA mod. 17", "STA_1": "STA-1", "STA_1_Rin": "STA-1 Rin", "STA_1_short": "STA-1", @@ -6452,7 +6696,7 @@ "SU-152": "SU-152", "SU-152_01": "SU-152", "SU-152_Custom": "SU-152 Taiga", - "SU-152_Custom_short": "SU-152 Тaiga", + "SU-152_Custom_short": "SU-152 Taiga", "SU-152_mod_1943": "SU-152", "SU-152_mod_1944": "SU-152M", "SU-18": "SU-18", @@ -6677,6 +6921,8 @@ "T57_58": "T57 Heavy Tank", "T57_58_Custom": "T57 Heavy Tireless", "T57_58_Custom_short": "T57 Heavy Tireless", + "T57_58_Custom_short_v2": "T57 Heavy Ammoel", + "T57_58_Custom_v2": "T57 Heavy Ammoel", "T57_58_short": "T57 Heavy", "T62A": "T-62A", "T62A_Custom": "T-62A Fearless", @@ -6773,6 +7019,7 @@ "V-2-54IS": "V-2-54IS", "V-2-54IS_2": "V-2-54-IS/2", "V-2-54IS_GuP": "V-2-54 GuP", + "V-2-54SC": "V-2-54SC", "V-2IS": "V-2IS", "V-2IS_B": "V-2IS", "V-2IS_B_2": "V-2IS-2", @@ -6794,6 +7041,7 @@ "V-54-6": "V-54-6", "V-54I": "V-54I", "V-54K": "V-54K", + "V-54KML": "V-54K-ML", "V-54K_TU": "V-54K", "V-55": "V-55", "V-55U": "V-14", @@ -6917,8 +7165,10 @@ "W_12_5_B": "W12-5B", "W_12_5_P": "W12-5P", "Waffentrager_IV": "Waffenträger auf Pz. IV", - "Waffentrager_IV_CL": "Waffenträger Camelot", - "Waffentrager_IV_CL_short": "WT Camelot", + "Waffentrager_IV_CL": "Waffenträger Ritter", + "Waffentrager_IV_CL_short": "WT Ritter", + "Waffentrager_IV_Custom": "WT auf Pz. IV Darkwing", + "Waffentrager_IV_Custom_short": "WT auf Pz. IV Darkwing", "Waffentrager_IV_short": "WT auf Pz. IV", "Waterspit_1": "Waterspit", "Wespe": "Wespe", @@ -6927,6 +7177,7 @@ "Wespeketten_ausf_B": "Wespe verstärkteketten", "Wespeturm": "Wespe", "Wright_Continental_R-975C1": "Wright Continental R-975C1", + "Wright_Continental_R-975C1_BP": "Wright Continental R-975C1 B", "Wright_Continental_R-975C1_GuP": "Wright Continental R-975C1 GuP", "Wright_Continental_R-975C1_short": "Wright C. R-975C1", "Wright_Continental_R-975C2": "Wright Continental R-975C2", @@ -6940,6 +7191,7 @@ "Wright_Continental_R-975EC2_short": "Wright C. R-975EC2", "Wright_Continental_R_975_C4_858": "Wright Continental\\nR-975-C4 (858)", "Wright_G200_m_781C9GC1": "Wright G200 M781C9GC1", + "Wright_G200_m_781C9GC1V2_BP": "Wright G200 M781C9GC1", "Wright_G200_m_781C9GC5": "Wright G200 M781C9GC5", "Wright_G200_m_781C9GC5_short": "Wright G200\\nM781C9GC5", "Wright_G200_m_795C9GC1": "Wright G200 M795C9GC1", From e4f746ca38e0857a5a1c1cedd4cd09a84386952a Mon Sep 17 00:00:00 2001 From: Jylpah Date: Wed, 22 Mar 2023 22:52:41 +0200 Subject: [PATCH 2/8] Update 9.7 --- maps.json | 4 +-- tanks.json | 100 +++++++++++++++++++++++++++++++++++++++++++++++++++-- 2 files changed, 100 insertions(+), 4 deletions(-) diff --git a/maps.json b/maps.json index 046e9ac..cb404c2 100644 --- a/maps.json +++ b/maps.json @@ -26,7 +26,7 @@ "karelia": "Rockfield", "karieri": "Copperfield", "lake": "Mirage", - "lumber": "Alpenstadt", + "lumber": "Horrorstadt", "lumber_01": "Alpenstadt - Town", "malinovka": "Winter Malinovka", "medvedkovo": "Dead Rail", @@ -57,4 +57,4 @@ "skit": "Naval Frontier", "test": "World of Ducks", "tutorial": "Proving Grounds" -} +} \ No newline at end of file diff --git a/tanks.json b/tanks.json index 1017588..27866ae 100644 --- a/tanks.json +++ b/tanks.json @@ -1,7 +1,7 @@ { "status": "ok", "meta": { - "count": 580 + "count": 592 }, "data": { "1": { @@ -788,6 +788,14 @@ "tier": 5, "type": "heavyTank" }, + "3377": { + "is_premium": false, + "name": "Ch16_WZ_131", + "nation": "china", + "tank_id": 3377, + "tier": 7, + "type": "lightTank" + }, "3425": { "is_premium": false, "name": "Type 61", @@ -876,6 +884,14 @@ "tier": 7, "type": "heavyTank" }, + "3889": { + "is_premium": false, + "name": "WZ-132", + "nation": "china", + "tank_id": 3889, + "tier": 8, + "type": "lightTank" + }, "3905": { "is_premium": false, "name": "AMX 50 120", @@ -1092,6 +1108,14 @@ "tier": 3, "type": "mediumTank" }, + "4913": { + "is_premium": false, + "name": "Ch15_59_16", + "nation": "china", + "tank_id": 4913, + "tier": 6, + "type": "lightTank" + }, "4929": { "is_premium": false, "name": "AMX 13 90", @@ -2236,6 +2260,14 @@ "tier": 6, "type": "mediumTank" }, + "10033": { + "is_premium": false, + "name": "Ch28_WZ_132A", + "nation": "china", + "tank_id": 10033, + "tier": 9, + "type": "lightTank" + }, "10049": { "is_premium": false, "name": "S35 CA", @@ -2293,7 +2325,7 @@ "type": "AT-SPG" }, "10289": { - "is_premium": true, + "is_premium": false, "name": "WZ-132-1", "nation": "china", "tank_id": 10289, @@ -2380,6 +2412,14 @@ "tier": 10, "type": "heavyTank" }, + "10801": { + "is_premium": true, + "name": "Ch34_WZ-131G_FT_BP", + "nation": "china", + "tank_id": 10801, + "tier": 6, + "type": "AT-SPG" + }, "10817": { "is_premium": false, "name": "AMX AC mle. 46", @@ -2636,6 +2676,14 @@ "tier": 10, "type": "AT-SPG" }, + "13185": { + "is_premium": true, + "name": "Cz17_Vz_55", + "nation": "european", + "tank_id": 13185, + "tier": 10, + "type": "heavyTank" + }, "13329": { "is_premium": true, "name": "Durchbruchswagen 2", @@ -3300,6 +3348,14 @@ "tier": 9, "type": "lightTank" }, + "20033": { + "is_premium": true, + "name": "F114_Projet_4_1", + "nation": "france", + "tank_id": 20033, + "tier": 9, + "type": "mediumTank" + }, "20049": { "is_premium": false, "name": "FV301", @@ -3860,6 +3916,14 @@ "tier": 7, "type": "heavyTank" }, + "25617": { + "is_premium": true, + "name": "G140_HWK_30", + "nation": "germany", + "tank_id": 25617, + "tier": 8, + "type": "lightTank" + }, "25633": { "is_premium": true, "name": "M-IV-Y", @@ -3884,6 +3948,22 @@ "tier": 6, "type": "lightTank" }, + "26113": { + "is_premium": true, + "name": "R181_Object_452K", + "nation": "ussr", + "tank_id": 26113, + "tier": 9, + "type": "heavyTank" + }, + "26129": { + "is_premium": true, + "name": "Nashorn_BP", + "nation": "germany", + "tank_id": 26129, + "tier": 6, + "type": "AT-SPG" + }, "26145": { "is_premium": true, "name": "High Score", @@ -3940,6 +4020,22 @@ "tier": 6, "type": "heavyTank" }, + "27937": { + "is_premium": true, + "name": "T49_Fearless", + "nation": "usa", + "tank_id": 27937, + "tier": 8, + "type": "lightTank" + }, + "28449": { + "is_premium": true, + "name": "A136_T42", + "nation": "usa", + "tank_id": 28449, + "tier": 7, + "type": "mediumTank" + }, "51201": { "is_premium": true, "name": "KV-220 Beta-Test", From 3f4e9c42d16021b3e3029dca029b856ae2707779 Mon Sep 17 00:00:00 2001 From: Jylpah Date: Wed, 22 Mar 2023 22:53:43 +0200 Subject: [PATCH 3/8] FIX Alpenstadt --- maps.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/maps.json b/maps.json index cb404c2..146ad11 100644 --- a/maps.json +++ b/maps.json @@ -26,7 +26,7 @@ "karelia": "Rockfield", "karieri": "Copperfield", "lake": "Mirage", - "lumber": "Horrorstadt", + "lumber": "Alpenstadt", "lumber_01": "Alpenstadt - Town", "malinovka": "Winter Malinovka", "medvedkovo": "Dead Rail", From d4a35e6e8d72aded5528d5e9b639e93716c4adc7 Mon Sep 17 00:00:00 2001 From: Jylpah Date: Wed, 22 Mar 2023 22:54:10 +0200 Subject: [PATCH 4/8] Whitespace --- blitzutils.py | 3424 +++++++++++++++++++++-------------------- extract_tankopedia.py | 388 ++--- 2 files changed, 1917 insertions(+), 1895 deletions(-) diff --git a/blitzutils.py b/blitzutils.py index 39bd482..729c3c8 100755 --- a/blitzutils.py +++ b/blitzutils.py @@ -52,195 +52,195 @@ import logging import re class ThrottledClientSession(aiohttp.ClientSession): - """Rate-throttled client session class inherited from aiohttp.ClientSession)""" + """Rate-throttled client session class inherited from aiohttp.ClientSession)""" - def __init__(self, rate_limit: float = 0, filters: list[str] = list() , - limit_filtered: bool = False, re_filter: bool = False, *args,**kwargs) -> None: - assert isinstance(rate_limit, (int, float)), "rate_limit has to be float" - assert isinstance(filters, list), "filters has to be list" - assert isinstance(limit_filtered, bool),"limit_filtered has to be bool" - assert isinstance(re_filter, bool), "re_filter has to be bool" + def __init__(self, rate_limit: float = 0, filters: list[str] = list() , + limit_filtered: bool = False, re_filter: bool = False, *args,**kwargs) -> None: + assert isinstance(rate_limit, (int, float)), "rate_limit has to be float" + assert isinstance(filters, list), "filters has to be list" + assert isinstance(limit_filtered, bool),"limit_filtered has to be bool" + assert isinstance(re_filter, bool), "re_filter has to be bool" - super().__init__(*args,**kwargs) - - self.rate_limit : float - self._fillerTask : Optional[asyncio.Task] = None - self._queue : Optional[asyncio.Queue] = None - self._start_time : float = time.time() - self._count : int = 0 - self._limit_filtered: bool = limit_filtered - self._re_filter : bool = re_filter - self._filters : list[Union[str, re.Pattern]] = list() + super().__init__(*args,**kwargs) + + self.rate_limit : float + self._fillerTask : Optional[asyncio.Task] = None + self._queue : Optional[asyncio.Queue] = None + self._start_time : float = time.time() + self._count : int = 0 + self._limit_filtered: bool = limit_filtered + self._re_filter : bool = re_filter + self._filters : list[Union[str, re.Pattern]] = list() - if re_filter: - for filter in filters: - self._filters.append(re.compile(filter)) - else: - for filter in filters: - self._filters.append(filter) - self.set_rate_limit(rate_limit) + if re_filter: + for filter in filters: + self._filters.append(re.compile(filter)) + else: + for filter in filters: + self._filters.append(filter) + self.set_rate_limit(rate_limit) - def _get_sleep(self) -> float: - if self.rate_limit > 0: - return 1/self.rate_limit - return 0 + def _get_sleep(self) -> float: + if self.rate_limit > 0: + return 1/self.rate_limit + return 0 - def get_rate(self) -> float: - """Return rate of requests""" - return self._count / (time.time() - self._start_time) + def get_rate(self) -> float: + """Return rate of requests""" + return self._count / (time.time() - self._start_time) - def get_stats(self) -> dict[str, float]: - """Get session statistics""" - res = {'rate' : self.get_rate(), 'rate_limit': self.rate_limit, 'count' : self._count } - return res - + def get_stats(self) -> dict[str, float]: + """Get session statistics""" + res = {'rate' : self.get_rate(), 'rate_limit': self.rate_limit, 'count' : self._count } + return res + - def get_stats_str(self) -> str: - """Print session statistics""" - return f"rate limit: {str(self.rate_limit if self.rate_limit != None else '-')} \ - rate: {0:.1f}.format(self.get_rate()) requests: {str(self._count)}" + def get_stats_str(self) -> str: + """Print session statistics""" + return f"rate limit: {str(self.rate_limit if self.rate_limit is not None else '-')} \ + rate: {0:.1f}.format(self.get_rate()) requests: {str(self._count)}" - def reset_counters(self) -> dict[str, float]: - """Reset rate counters and return current results""" - res = self.get_stats() - self._start_time = time.time() - self._count = 0 - return res + def reset_counters(self) -> dict[str, float]: + """Reset rate counters and return current results""" + res = self.get_stats() + self._start_time = time.time() + self._count = 0 + return res - def set_rate_limit(self, rate_limit: float = 0) -> float: - assert rate_limit is not None, "rate_limit must not be None" - assert isinstance(rate_limit, (int,float)) and rate_limit >= 0, "rate_limit has to be type of 'float' >= 0" - - self.rate_limit = rate_limit - if rate_limit > 0: - self._queue = asyncio.Queue(int(rate_limit)+1) - if self._fillerTask is not None: - self._fillerTask.cancel() - self._fillerTask = asyncio.create_task(self._filler()) - return self.rate_limit - + def set_rate_limit(self, rate_limit: float = 0) -> float: + assert rate_limit is not None, "rate_limit must not be None" + assert isinstance(rate_limit, (int,float)) and rate_limit >= 0, "rate_limit has to be type of 'float' >= 0" + + self.rate_limit = rate_limit + if rate_limit > 0: + self._queue = asyncio.Queue(int(rate_limit)+1) + if self._fillerTask is not None: + self._fillerTask.cancel() + self._fillerTask = asyncio.create_task(self._filler()) + return self.rate_limit + - async def close(self) -> None: - """Close rate-limiter's "bucket filler" task""" - # DEBUG - logging.debug(self.get_stats_str()) - try: - if self._fillerTask is not None: - self._fillerTask.cancel() - await asyncio.wait_for(self._fillerTask, timeout=0.5) - except asyncio.TimeoutError as err: - logging.error(str(err)) - await super().close() + async def close(self) -> None: + """Close rate-limiter's "bucket filler" task""" + # DEBUG + logging.debug(self.get_stats_str()) + try: + if self._fillerTask is not None: + self._fillerTask.cancel() + await asyncio.wait_for(self._fillerTask, timeout=0.5) + except asyncio.TimeoutError as err: + logging.error(str(err)) + await super().close() - - async def _filler(self) -> None: - """Filler task to fill the leaky bucket algo""" - try: - if self._queue is None: - return None - logging.debug('SLEEP: ' + str(self._get_sleep())) - updated_at = time.monotonic() - extra_increment : float = 0 - for i in range(0, self._queue.maxsize): - await self._queue.put(i) - while True: - if not self._queue.full(): - now = time.monotonic() - increment = self.rate_limit * (now - updated_at) - items_2_add = int(min(self._queue.maxsize - self._queue.qsize(), int(increment + extra_increment))) - extra_increment = (increment + extra_increment) % 1 - for i in range(0,items_2_add): - self._queue.put_nowait(i) - updated_at = now - await asyncio.sleep(self._get_sleep()) - except asyncio.CancelledError: - logging.debug('Cancelled') - except Exception as err: - logging.error(str(err)) - return None + + async def _filler(self) -> None: + """Filler task to fill the leaky bucket algo""" + try: + if self._queue is None: + return None + logging.debug('SLEEP: ' + str(self._get_sleep())) + updated_at = time.monotonic() + extra_increment : float = 0 + for i in range(0, self._queue.maxsize): + await self._queue.put(i) + while True: + if not self._queue.full(): + now = time.monotonic() + increment = self.rate_limit * (now - updated_at) + items_2_add = int(min(self._queue.maxsize - self._queue.qsize(), int(increment + extra_increment))) + extra_increment = (increment + extra_increment) % 1 + for i in range(0,items_2_add): + self._queue.put_nowait(i) + updated_at = now + await asyncio.sleep(self._get_sleep()) + except asyncio.CancelledError: + logging.debug('Cancelled') + except Exception as err: + logging.error(str(err)) + return None - async def _request(self, *args,**kwargs) -> aiohttp.ClientResponse: - """Throttled _request()""" - if self._queue is not None and self.is_limited(*args): - await self._queue.get() - self._queue.task_done() - self._count += 1 - return await super()._request(*args,**kwargs) + async def _request(self, *args,**kwargs) -> aiohttp.ClientResponse: + """Throttled _request()""" + if self._queue is not None and self.is_limited(*args): + await self._queue.get() + self._queue.task_done() + self._count += 1 + return await super()._request(*args,**kwargs) - def is_limited(self, *args: str) -> bool: - """Check wether the rate limit should be applied""" - try: - url: str = args[1] - for filter in self._filters: - if isinstance(filter, re.Pattern) and filter.match(url) is not None: - return self._limit_filtered - elif isinstance(filter, str) and url.startswith(filter): - return self._limit_filtered - - return not self._limit_filtered - except Exception as err: - logging.error(str(err)) - return True + def is_limited(self, *args: str) -> bool: + """Check wether the rate limit should be applied""" + try: + url: str = args[1] + for filter in self._filters: + if isinstance(filter, re.Pattern) and filter.match(url) is not None: + return self._limit_filtered + elif isinstance(filter, str) and url.startswith(filter): + return self._limit_filtered + + return not self._limit_filtered + except Exception as err: + logging.error(str(err)) + return True # ----------------------------------------------------------- # Class AsyncLogger() # ----------------------------------------------------------- class AsyncLogger(): - """Async file logger""" + """Async file logger""" - def __init__(self) -> None: - self._queue = asyncio.Queue() - self._task = None - self._file = None + def __init__(self) -> None: + self._queue = asyncio.Queue() + self._task = None + self._file = None - async def open(self, logfn: str = None): - """Set logging to file""" - if logfn == None: - logfn = 'LOG_' + _randomword(6) + '.log' - try: - self._file = await aiofiles.open(logfn, mode='a') - self._task = asyncio.create_task(self.logger()) - return True - except Exception as err: - error('Error opening file: ' + logfn, err) - self._file = None - return False + async def open(self, logfn: str = None): + """Set logging to file""" + if logfn is None: + logfn = 'LOG_' + _randomword(6) + '.log' + try: + self._file = await aiofiles.open(logfn, mode='a') + self._task = asyncio.create_task(self.logger()) + return True + except Exception as err: + error('Error opening file: ' + logfn, err) + self._file = None + return False - async def logger(self): - """Async file logger""" - if self._file == None: - error('No log file defined') - return False - while True: - try: - msg = await self._queue.get() - await self._file.write(msg + '\n') - self._queue.task_done() - except asyncio.CancelledError as err: - return None - except Exception as err: - error(exception=err) + async def logger(self): + """Async file logger""" + if self._file is None: + error('No log file defined') + return False + while True: + try: + msg = await self._queue.get() + await self._file.write(msg + '\n') + self._queue.task_done() + except asyncio.CancelledError as err: + return None + except Exception as err: + error(exception=err) - def log(self, msg: str = ''): - self._queue.put_nowait(msg) + def log(self, msg: str = ''): + self._queue.put_nowait(msg) - async def close(self): - try: - # empty queue & close - await self._queue.join() - self._task.cancel() - self._file.close() - except Exception as err: - error('Error closing log file', err) - return None + async def close(self): + try: + # empty queue & close + await self._queue.join() + self._task.cancel() + self._file.close() + except Exception as err: + error('Error closing log file', err) + return None # ----------------------------------------------------------- # Utils @@ -248,86 +248,86 @@ class AsyncLogger(): def set_debug(debug: bool): - global _log_level - _log_level = DEBUG + global _log_level + _log_level = DEBUG def set_verbose(verbose: bool): - global _log_level - _log_level = VERBOSE + global _log_level + _log_level = VERBOSE def set_silent(silent: bool): - global _log_level - _log_level = SILENT + global _log_level + _log_level = SILENT def set_log_level_normal(): - global _log_level - _log_level = NORMAL + global _log_level + _log_level = NORMAL def is_debug() -> bool: - return _log_level == DEBUG + return _log_level == DEBUG def is_verbose() -> bool: - return _log_level == VERBOSE + return _log_level == VERBOSE def is_normal() -> bool: - return _log_level == NORMAL + return _log_level == NORMAL def is_silent() -> bool: - return _log_level == SILENT + return _log_level == SILENT def set_log_level(silent: bool, verbose: bool, debug: bool): - global _log_level - _log_level = NORMAL - if silent: _log_level = SILENT - if verbose: _log_level = VERBOSE - if debug: _log_level = DEBUG + global _log_level + _log_level = NORMAL + if silent: _log_level = SILENT + if verbose: _log_level = VERBOSE + if debug: _log_level = DEBUG def get_log_level() -> int: - return _log_level + return _log_level def get_log_level_str() -> str: - for log_level in LOG_LEVELS: - if _log_level == LOG_LEVELS[log_level]: - return log_level - error('Unknown log level: ' + str(_log_level)) + for log_level in LOG_LEVELS: + if _log_level == LOG_LEVELS[log_level]: + return log_level + error('Unknown log level: ' + str(_log_level)) async def set_file_logging(logfn=None, add_timestamp=False): - """Set logging to file""" - global LOG, LOGGER - LOG = True - if logfn == None: - logfn = 'LOG_' + _randomword(6) - else: - dateTimeObj = datetime.now() - timestampStr = dateTimeObj.strftime("%y%m%d%H%M%S") - logfn = logfn + '_' + timestampStr - logfn = logfn + '.log' - try: - LOGGER = AsyncLogger() - await LOGGER.open(logfn) - except Exception as err: - error('Error starting logger: ' + logfn, err) - LOG = False - LOGGER = None - return LOG + """Set logging to file""" + global LOG, LOGGER + LOG = True + if logfn is None: + logfn = 'LOG_' + _randomword(6) + else: + dateTimeObj = datetime.now() + timestampStr = dateTimeObj.strftime("%y%m%d%H%M%S") + logfn = logfn + '_' + timestampStr + logfn = logfn + '.log' + try: + LOGGER = AsyncLogger() + await LOGGER.open(logfn) + except Exception as err: + error('Error starting logger: ' + logfn, err) + LOG = False + LOGGER = None + return LOG async def close_file_logging(): - global LOG, LOGGER - LOG = False - await LOGGER.close() - LOGGER = None + global LOG, LOGGER + LOG = False + await LOGGER.close() + LOGGER = None def _randomword(length): @@ -336,349 +336,355 @@ def _randomword(length): def verbose(msg="", id=None) -> bool: - """Print a message""" - return _print_log_msg('', msg, exception=None, id=id, print_msg=(_log_level >= VERBOSE)) + """Print a message""" + return _print_log_msg('', msg, exception=None, id=id, print_msg=(_log_level >= VERBOSE)) def verbose_std(msg="", id=None) -> bool: - """Print a message""" - return _print_log_msg('', msg, exception=None, id=id, print_msg=(_log_level >= NORMAL)) + """Print a message""" + return _print_log_msg('', msg, exception=None, id=id, print_msg=(_log_level >= NORMAL)) def warning(msg="", id=None, force: bool = False) -> bool: - """Print a warning message""" - return _print_log_msg('', 'Warning: ' + msg, None, id, print_msg=(force or (_log_level >= NORMAL))) + """Print a warning message""" + return _print_log_msg('', 'Warning: ' + msg, None, id, print_msg=(force or (_log_level >= NORMAL))) def debug(msg="", id=None, exception=None, force: bool = False) -> bool: - """print a conditional debug message""" - if (_log_level >= DEBUG) or force: - return _print_log_msg('DEBUG', msg, exception, id) - return False + """print a conditional debug message""" + if (_log_level >= DEBUG) or force: + return _print_log_msg('DEBUG', msg, exception, id) + return False def error(msg="", exception=None, id=None) -> bool: - """Print an error message""" - return _print_log_msg('ERROR', msg, exception, id) + """Print an error message""" + return _print_log_msg('ERROR', msg, exception, id) def log(msg="", id=None, exception=None) -> bool: - """print a conditional debug message""" - return _print_log_msg('LOG', msg=msg, exception=exception, id=id, print_msg=(_log_level >= DEBUG)) + """print a conditional debug message""" + return _print_log_msg('LOG', msg=msg, exception=exception, id=id, print_msg=(_log_level >= DEBUG)) ## Copy with pride: https://stackoverflow.com/questions/2203424/python-how-to-retrieve-class-information-from-a-frame-object def get_class_from_frame(fr): - args, _, _, value_dict = inspect.getargvalues(fr) - # we check the first parameter for the frame function is - # named 'self' - if len(args) and args[0] == 'self': - # in that case, 'self' will be referenced in value_dict - instance = value_dict.get('self', None) - if instance: - # return its class - return getattr(instance, '__class__', None) - # return None otherwise - return None + args, _, _, value_dict = inspect.getargvalues(fr) + # we check the first parameter for the frame function is + # named 'self' + if len(args) and args[0] == 'self': + # in that case, 'self' will be referenced in value_dict + instance = value_dict.get('self', None) + if instance: + # return its class + return getattr(instance, '__class__', None) + # return None otherwise + return None -def _print_log_msg(prefix = 'LOG', msg = '', exception = None, id = None, print_msg : bool = True): - # Use empty prefix to determine standard verbose messages - if not (print_msg or LOG): - return False - retval = False - if prefix != '': - curframe = inspect.currentframe() - calframe = inspect.getouterframes(curframe) - class_name = get_class_from_frame(curframe) - caller = calframe[2].function - if class_name != None: - prefix = prefix + ': ' + class_name + '.' + caller - else: - prefix = prefix + ': ' + caller - prefix = prefix + '(): ' - - if id != None: - prefix = prefix + '[' + str(id) + ']: ' +def _print_log_msg(prefix: str = 'LOG', msg:str = '', + exception: Exception | None = None, + id: int|None = None, print_msg : bool = True): + # Use empty prefix to determine standard verbose messages + if not (print_msg or LOG): + return False + retval = False + if prefix != '': + curframe = inspect.currentframe() + calframe = inspect.getouterframes(curframe) + class_name = get_class_from_frame(curframe) + caller = calframe[2].function + if class_name is not None: + prefix = f'{prefix}: {class_name}.{caller}' + else: + prefix = prefix + ': ' + caller + prefix = prefix + '(): ' + + if id is not None: + prefix = f'{prefix} [ {id}]: ' - exception_msg = '' - if (exception != None) and isinstance(exception, Exception): - exception_msg = ' : Exception: ' + str(type(exception)) + ' : ' + str(exception) + exception_msg = '' + if (exception is not None) and isinstance(exception, Exception): + exception_msg = f' : Exception: {type(exception)} : {exception}' - msg = prefix + msg + exception_msg - if print_msg: - print(msg) - retval = True - if _log_msg(msg): - retval = True - return retval + msg = f'{prefix}{msg}{exception_msg}' + if print_msg: + print(msg) + retval = True + if _log_msg(msg): + retval = True + return retval -def _log_msg(msg =''): - if LOG and (LOGGER != None): - LOGGER.log(msg) - return True - return False +def _log_msg(msg :str=''): + if LOG and (LOGGER is not None): + LOGGER.log(msg) + return True + return False def set_progress_step(n: int): - """Set the frequency of the progress dots. The bigger 'n', the fewer dots""" - global _progress_N, _progress_i - if n > 0: - _progress_N = n - _progress_i = 0 - return + """Set the frequency of the progress dots. The bigger 'n', the fewer dots""" + global _progress_N, _progress_i + if n > 0: + _progress_N = n + _progress_i = 0 + return def get_progress_step(): - """Get the frequency of the progress dots. The bigger 'n', the fewer dots""" - return _progress_N + """Get the frequency of the progress dots. The bigger 'n', the fewer dots""" + return _progress_N def set_progress_bar(heading: str, max_value: int, step: int = None, slow: bool = False, id: str = None): - global _progress_obj, _progress_N, _progress_i, _progress_id - _progress_id = id - if step == None: - _progress_N = int(max_value / 1000) if (max_value > 1000) else 2 - else: - _progress_N = step - if _progress_obj != None: - finish_progress_bar() - if slow: - _progress_obj = SlowBar(heading, max=max_value) - else: - _progress_obj = IncrementalBar(heading, max=max_value, suffix='%(index)d/%(max)d %(percent)d%%') - _progress_i = 0 + global _progress_obj, _progress_N, _progress_i, _progress_id + _progress_id = id + if step is None: + _progress_N = int(max_value / 1000) if (max_value > 1000) else 2 + else: + _progress_N = step + if _progress_obj is not None: + finish_progress_bar() + if slow: + _progress_obj = SlowBar(heading, max=max_value) + else: + _progress_obj = IncrementalBar(heading, max=max_value, suffix='%(index)d/%(max)d %(percent)d%%') + _progress_i = 0 - _log_msg(heading + str(max_value)) - return + _log_msg(heading + str(max_value)) + return def set_counter(heading: str, step = None): - global _progress_obj, _progress_i - _progress_i = 0 - if _progress_obj != None: - finish_progress_bar() - _progress_obj = Counter(heading + ': ') - if step != None: - set_progress_step(step) - return + global _progress_obj, _progress_i + _progress_i = 0 + if _progress_obj is not None: + finish_progress_bar() + _progress_obj = Counter(heading + ': ') + if step is not None: + set_progress_step(step) + return def print_progress(force = False, id : str = None) -> bool: - """Print progress bar/dots. Returns True if the dot is being printed.""" - global _progress_i - - _progress_i += 1 - if ((_progress_i % _progress_N) == 0): - if (_log_level > SILENT) and ( force or (_log_level < DEBUG ) ): - if (_progress_obj != None): - if (_progress_id == id): - _progress_obj.next(_progress_N) - return True - else: - return False - else: - print('.', end='', flush=True) - return True - return False + """Print progress bar/dots. Returns True if the dot is being printed.""" + global _progress_i + + _progress_i += 1 + if ((_progress_i % _progress_N) == 0): + if (_log_level > SILENT) and ( force or (_log_level < DEBUG ) ): + if (_progress_obj is not None): + if (_progress_id == id): + _progress_obj.next(_progress_N) + return True + else: + return False + else: + print('.', end='', flush=True) + return True + return False def finish_progress_bar(): - """Finish and close progress bar object""" - global _progress_obj + """Finish and close progress bar object""" + global _progress_obj - # print_nl = True - if _progress_obj != None: - # if isinstance(_progress_obj, Counter): - # print_nl = False - _progress_obj.finish() - # if print_nl: - print_new_line() - _progress_obj = None - return None + # print_nl = True + if _progress_obj is not None: + # if isinstance(_progress_obj, Counter): + # print_nl = False + _progress_obj.finish() + # if print_nl: + print_new_line() + _progress_obj = None + return None def wait(sec : int): - for i in range(0, sec): - i=i ## to get rid of the warning... - time.sleep(1) - print_progress(True) - print('', flush=True) + for i in range(0, sec): + i=i ## to get rid of the warning... + time.sleep(1) + print_progress(True) + print('', flush=True) def print_new_line(force = False): - if (_log_level > SILENT) and ( force or (_log_level < DEBUG ) ): - print('', flush=True) - _log_msg('') + if (_log_level > SILENT) and ( force or (_log_level < DEBUG ) ): + print('', flush=True) + _log_msg('') def NOW() -> int: - return int(time.time()) + return int(time.time()) def get_date_str(timestamp:int = NOW(), date_format = '%Y%m%d_%H%M%S'): - """Return YYYYMMDD_HHmm date string""" - try: - ts = datetime.fromtimestamp(timestamp) - return ts.strftime(date_format) - except Exception as err: - error(exception=err) - return None + """Return YYYYMMDD_HHmm date string""" + try: + ts = datetime.fromtimestamp(timestamp) + return ts.strftime(date_format) + except Exception as err: + error(exception=err) + return None def rebase_file_args(current_dir : str, files: list[str]) -> list[str]: - """Rebase file command line params after moving working dir to the script's dir""" - assert isinstance(files, list), "files has to be a list" + """Rebase file command line params after moving working dir to the script's dir""" + assert isinstance(files, list), "files has to be a list" - if (files[0] == '-') or (files[0] == 'db:'): - return [ files[0] ] - else: - return [ os.path.join(current_dir, fn) for fn in files ] + if (files[0] == '-') or (files[0] == 'db:'): + return [ files[0] ] + else: + return [ os.path.join(current_dir, fn) for fn in files ] async def read_int_list(filename: str) -> list[int]: - """Read file to a list and return list of integers in the input file""" - - input_list = [] - try: - async with aiofiles.open(filename) as fp: - async for line in fp: - try: - input_list.append(int(line)) - except (ValueError, TypeError) as err: - pass - except Exception as err: - error('Unexpected error when reading file: ' + filename, err) - return input_list + """Read file to a list and return list of integers in the input file""" + + input_list = [] + try: + async with aiofiles.open(filename) as fp: + async for line in fp: + try: + input_list.append(int(line)) + except (ValueError, TypeError) as err: + pass + except Exception as err: + error('Unexpected error when reading file: ' + filename, err) + return input_list async def save_JSON(filename: str, json_data: dict, sort_keys = False, pretty = True) -> bool: - """Save JSON data into file""" - try: - dirname = os.path.dirname(filename) - if (dirname != '') and not os.path.isdir(dirname): - os.makedirs(dirname, 0o770-UMASK) - async with aiofiles.open(filename,'w', encoding="utf8") as outfile: - if pretty: - await outfile.write(json.dumps(json_data, ensure_ascii=False, indent=4, sort_keys=sort_keys)) - else: - await outfile.write(json.dumps(json_data, ensure_ascii=False, sort_keys=sort_keys)) - return True - except Exception as err: - error('Error saving JSON', err) - return False + """Save JSON data into file""" + try: + dirname = os.path.dirname(filename) + if (dirname != '') and not os.path.isdir(dirname): + os.makedirs(dirname, 0o770-UMASK) + async with aiofiles.open(filename,'w', encoding="utf8") as outfile: + if pretty: + await outfile.write(json.dumps(json_data, ensure_ascii=False, indent=4, sort_keys=sort_keys)) + else: + await outfile.write(json.dumps(json_data, ensure_ascii=False, sort_keys=sort_keys)) + return True + except Exception as err: + error('Error saving JSON', err) + return False async def open_JSON(filename: str, chk_JSON_func = None) -> dict: - try: - async with aiofiles.open(filename) as fp: - json_data = json.loads(await fp.read()) - if (chk_JSON_func == None): - debug("JSON file content not checked: " + filename) - return json_data - elif chk_JSON_func(json_data): - debug("JSON File is valid: " + filename) - return json_data - else: - debug('JSON File has invalid content: ' + filename) - except Exception as err: - error('Unexpected error when reading file: ' + filename, err) - return None + try: + async with aiofiles.open(filename) as fp: + json_data = json.loads(await fp.read()) + if (chk_JSON_func is None): + debug("JSON file content not checked: " + filename) + return json_data + elif chk_JSON_func(json_data): + debug("JSON File is valid: " + filename) + return json_data + else: + debug('JSON File has invalid content: ' + filename) + except Exception as err: + error('Unexpected error when reading file: ' + filename, err) + return None async def get_url_JSON(session: aiohttp.ClientSession, url: str, chk_JSON_func = None, max_tries = MAX_RETRIES) -> dict: - """Retrieve (GET) an URL and return JSON object""" - if session == None: - error('Session must be initialized first') - sys.exit(1) - if url == None: - return None - - # To avoid excessive use of servers - for retry in range(1,max_tries+1): - try: - async with session.get(url) as resp: - if resp.status == 200: - debug('HTTP request OK') - json_resp = await resp.json() - if (chk_JSON_func == None) or chk_JSON_func(json_resp): - # debug("Received valid JSON: " + str(json_resp)) - return json_resp - # Sometimes WG API returns JSON error even a retry gives valid JSON - elif resp.status == 407: - json_resp_err = await resp.json() - error('WG API returned 407: ' + json_resp_err['error']['message']) - if retry == max_tries: - break - debug('Retrying URL [' + str(retry) + '/' + str(max_tries) + ']: ' + url ) - await asyncio.sleep(SLEEP) + """Retrieve (GET) an URL and return JSON object""" + if session is None: + error('Session must be initialized first') + sys.exit(1) + if url is None: + return None + + # To avoid excessive use of servers + for retry in range(1,max_tries+1): + try: + async with session.get(url) as resp: + if resp.status == 200: + debug('HTTP request OK') + json_resp = await resp.json() + if (chk_JSON_func is None) or chk_JSON_func(json_resp): + # debug("Received valid JSON: " + str(json_resp)) + return json_resp + # Sometimes WG API returns JSON error even a retry gives valid JSON + elif resp.status == 407: + json_resp_err = await resp.json() + error('WG API returned 407: ' + json_resp_err['error']['message']) + if retry == max_tries: + break + debug('Retrying URL [' + str(retry) + '/' + str(max_tries) + ']: ' + url ) + await asyncio.sleep(SLEEP) - except aiohttp.ClientError as err: - debug("Could not retrieve URL: " + url, exception=err) - except asyncio.CancelledError as err: - debug('Queue gets cancelled while still working.', exception=err) - except Exception as err: - debug('Unexpected Exception', exception=err) - debug("Could not retrieve URL: " + url) - return None + except aiohttp.ClientError as err: + debug("Could not retrieve URL: " + url, exception=err) + except asyncio.CancelledError as err: + debug('Queue gets cancelled while still working.', exception=err) + except Exception as err: + debug('Unexpected Exception', exception=err) + debug("Could not retrieve URL: " + url) + return None def bld_dict_hierarcy(d : dict, key : str, value) -> dict: - """Build hierarcical dict based on multi-level key separated with """ - try: - key_hier = key.split('.') - sub_key = key_hier.pop(0) - if len(key_hier) == 0: - d[sub_key] = value - elif sub_key not in d: - d[sub_key] = bld_dict_hierarcy({}, '.'.join(key_hier), value) - else: - d[sub_key] = bld_dict_hierarcy(d[sub_key], '.'.join(key_hier), value) + """Build hierarcical dict based on multi-level key separated with """ + try: + key_hier = key.split('.') + sub_key = key_hier.pop(0) + if len(key_hier) == 0: + d[sub_key] = value + elif sub_key not in d: + d[sub_key] = bld_dict_hierarcy({}, '.'.join(key_hier), value) + else: + d[sub_key] = bld_dict_hierarcy(d[sub_key], '.'.join(key_hier), value) - return d - except KeyError as err: - error('Key not found', err) - except Exception as err: - error('Unexpected Exception', err) - return None + return d + except KeyError as err: + error('Key not found', err) + except Exception as err: + error('Unexpected Exception', err) + return None def get_JSON_keypath(keypath: str, key: str): - if keypath == None: - return key - else: - return '.'.join([keypath, key]) + if keypath is None: + return key + else: + return '.'.join([keypath, key]) -def get_JSON_value(json, key : str = None, keys : list = None, keypath = None): - if (keys == None) and (key != None): - keys = key.split('.') - if len(keys) == 0: - return json - key = keys.pop(0) - if type(json) == dict: - if key in json: - return get_JSON_value(json[key], keys=keys, keypath=get_JSON_keypath(keypath, key)) - else: - raise KeyError('Key: '+ get_JSON_keypath(keypath, key) + ' not found') - if type(json) == list: - p = re.compile(r'^\[(\d+)\]$') - m = p.match(key) - if len(m.groups()) != 1: - raise KeyError('Invalid key given: ' + get_JSON_keypath(keypath, key)) - ndx = m.group(1) - try: - return get_JSON_value(json[ndx], keys=keys, keypath=get_JSON_keypath(keypath, key)) - except IndexError: - raise KeyError('JSON array index out of range: ' + get_JSON_keypath(keypath, key)) - raise KeyError('Key not found: ' + get_JSON_keypath(keypath, keys[0])) +def get_JSON_value(json, key : str | None = None, keys : list | None = None, + keypath : str | None = None): + if (keys is None): + if key is not None: + keys = key.split('.') + else: + return json + if len(keys) == 0: + return json + key = keys.pop(0) + if type(json) == dict: + if key in json: + return get_JSON_value(json[key], keys=keys, keypath=get_JSON_keypath(keypath, key)) + else: + raise KeyError('Key: '+ get_JSON_keypath(keypath, key) + ' not found') + if type(json) == list: + p = re.compile(r'^\[(\d+)\]$') + m = p.match(key) + if len(m.groups()) != 1: + raise KeyError('Invalid key given: ' + get_JSON_keypath(keypath, key)) + ndx = m.group(1) + try: + return get_JSON_value(json[ndx], keys=keys, keypath=get_JSON_keypath(keypath, key)) + except IndexError: + raise KeyError('JSON array index out of range: ' + get_JSON_keypath(keypath, key)) + raise KeyError('Key not found: ' + get_JSON_keypath(keypath, keys[0])) def sort_dict(d: dict, number: bool = False) -> dict: - """Sort a dict by keys""" - if number: - return dict(sorted(d.items(), key=lambda item: int(item[0]))) - else: - return dict(sorted(d.items(), key=lambda item: item[0])) + """Sort a dict by keys""" + if number: + return dict(sorted(d.items(), key=lambda item: int(item[0]))) + else: + return dict(sorted(d.items(), key=lambda item: item[0])) # ----------------------------------------------------------- @@ -686,14 +692,14 @@ def sort_dict(d: dict, number: bool = False) -> dict: # ----------------------------------------------------------- class SlowBar(IncrementalBar): - suffix = '%(index)d/%(max)d %(percent)d%% ETA %(remaining_hours).0f h %(remaining_mins).0f mins' - @property - def remaining_hours(self): - return self.eta // 3600 + suffix = '%(index)d/%(max)d %(percent)d%% ETA %(remaining_hours).0f h %(remaining_mins).0f mins' + @property + def remaining_hours(self): + return self.eta // 3600 - @property - def remaining_mins(self): - return (self.eta - (self.eta // 3600)*3600) // 60 + @property + def remaining_mins(self): + return (self.eta - (self.eta // 3600)*3600) // 60 # ----------------------------------------------------------- @@ -701,7 +707,7 @@ class SlowBar(IncrementalBar): # ----------------------------------------------------------- class StatsNotFound(Exception): - pass + pass # ----------------------------------------------------------- @@ -710,1082 +716,1082 @@ class StatsNotFound(Exception): class WG: - URL_WG_CLAN_INFO = 'clans/info/?application_id=' - # URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=tank_id%2Clast_battle_time&application_id=' - # URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=account_id%2Ctank_id%2Clast_battle_time%2Cbattle_life_time%2Call&application_id=' - URL_WG_PLAYER_TANK_STATS = 'tanks/stats/?application_id=' - URL_WG_ACCOUNT_ID = 'account/list/?fields=account_id%2Cnickname&application_id=' - URL_WG_PLAYER_STATS = 'account/info/?application_id=' - URL_WG_PLAYER_ACHIEVEMENTS = 'account/achievements/?application_id=' - CACHE_DB_FILE = '.blitzutils_cache.sqlite3' - CACHE_GRACE_TIME = 30*24*3600 # 30 days cache + URL_WG_CLAN_INFO = 'clans/info/?application_id=' + # URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=tank_id%2Clast_battle_time&application_id=' + # URL_WG_PLAYER_TANK_LIST = 'tanks/stats/?fields=account_id%2Ctank_id%2Clast_battle_time%2Cbattle_life_time%2Call&application_id=' + URL_WG_PLAYER_TANK_STATS = 'tanks/stats/?application_id=' + URL_WG_ACCOUNT_ID = 'account/list/?fields=account_id%2Cnickname&application_id=' + URL_WG_PLAYER_STATS = 'account/info/?application_id=' + URL_WG_PLAYER_ACHIEVEMENTS = 'account/achievements/?application_id=' + CACHE_DB_FILE = '.blitzutils_cache.sqlite3' + CACHE_GRACE_TIME = 30*24*3600 # 30 days cache - # sql_create_player_stats_tbl = """CREATE TABLE IF NOT EXISTS player_stats ( - # account_id INTEGER NOT NULL, - # date INTEGER NOT NULL, - # stat TEXT, - # value FLOAT - # ); """ - - # sql_create_player_tank_stats_tbl = """CREATE TABLE IF NOT EXISTS player_tank_stats ( - # account_id INTEGER NOT NULL, - # tank_id INTEGER DEFAULT NULL, - # date INTEGER NOT NULL, - # stat TEXT, - # value FLOAT - # ); """ - - # sql_select_player_stats = """SELECT value FROM player_stats ORDERBY date ASC - # WHERE account_id = {} AND stat = {} - # AND date >= {} LIMIT 1;""" - - # TANK_STATS - SQL_TANK_STATS_TBL = 'tank_stats' + # sql_create_player_stats_tbl = """CREATE TABLE IF NOT EXISTS player_stats ( + # account_id INTEGER NOT NULL, + # date INTEGER NOT NULL, + # stat TEXT, + # value FLOAT + # ); """ + + # sql_create_player_tank_stats_tbl = """CREATE TABLE IF NOT EXISTS player_tank_stats ( + # account_id INTEGER NOT NULL, + # tank_id INTEGER DEFAULT NULL, + # date INTEGER NOT NULL, + # stat TEXT, + # value FLOAT + # ); """ + + # sql_select_player_stats = """SELECT value FROM player_stats ORDERBY date ASC + # WHERE account_id = {} AND stat = {} + # AND date >= {} LIMIT 1;""" + + # TANK_STATS + SQL_TANK_STATS_TBL = 'tank_stats' - SQL_TANK_STATS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_TANK_STATS_TBL + \ - """ ( account_id INTEGER NOT NULL, - tank_id INTEGER NOT NULL, - update_time INTEGER NOT NULL, - stats TEXT, - PRIMARY KEY (account_id, tank_id) )""" + SQL_TANK_STATS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_TANK_STATS_TBL + \ + """ ( account_id INTEGER NOT NULL, + tank_id INTEGER NOT NULL, + update_time INTEGER NOT NULL, + stats TEXT, + PRIMARY KEY (account_id, tank_id) )""" - SQL_TANK_STATS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_TANK_STATS_TBL + SQL_TANK_STATS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_TANK_STATS_TBL - SQL_TANK_STATS_UPDATE = 'REPLACE INTO ' + SQL_TANK_STATS_TBL + '(account_id, tank_id, update_time, stats) VALUES(?,?,?,?)' + SQL_TANK_STATS_UPDATE = 'REPLACE INTO ' + SQL_TANK_STATS_TBL + '(account_id, tank_id, update_time, stats) VALUES(?,?,?,?)' - # PLAYER_STATS - SQL_PLAYER_STATS_TBL = 'player_stats' + # PLAYER_STATS + SQL_PLAYER_STATS_TBL = 'player_stats' - SQL_PLAYER_STATS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_PLAYER_STATS_TBL + \ - """ ( account_id INTEGER PRIMARY KEY, - update_time INTEGER NOT NULL, - stats TEXT)""" + SQL_PLAYER_STATS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_PLAYER_STATS_TBL + \ + """ ( account_id INTEGER PRIMARY KEY, + update_time INTEGER NOT NULL, + stats TEXT)""" - SQL_PLAYER_STATS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_PLAYER_STATS_TBL + SQL_PLAYER_STATS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_PLAYER_STATS_TBL - SQL_PLAYER_STATS_UPDATE = 'REPLACE INTO ' + SQL_PLAYER_STATS_TBL + '(account_id, update_time, stats) VALUES(?,?,?)' + SQL_PLAYER_STATS_UPDATE = 'REPLACE INTO ' + SQL_PLAYER_STATS_TBL + '(account_id, update_time, stats) VALUES(?,?,?)' - SQL_PLAYER_STATS_CACHED = 'SELECT * FROM ' + SQL_PLAYER_STATS_TBL + ' WHERE account_id = ? AND update_time > ?' + SQL_PLAYER_STATS_CACHED = 'SELECT * FROM ' + SQL_PLAYER_STATS_TBL + ' WHERE account_id = ? AND update_time > ?' - # PLAYER_ACHIEVEMENTS - SQL_PLAYER_ACHIEVEMENTS_TBL = 'player_achievements' + # PLAYER_ACHIEVEMENTS + SQL_PLAYER_ACHIEVEMENTS_TBL = 'player_achievements' - SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_PLAYER_ACHIEVEMENTS_TBL + \ - """ ( account_id INTEGER PRIMARY KEY, - update_time INTEGER NOT NULL, - stats TEXT)""" + SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL = 'CREATE TABLE IF NOT EXISTS ' + SQL_PLAYER_ACHIEVEMENTS_TBL + \ + """ ( account_id INTEGER PRIMARY KEY, + update_time INTEGER NOT NULL, + stats TEXT)""" - SQL_PLAYER_ACHIEVEMENTS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_PLAYER_ACHIEVEMENTS_TBL - - SQL_PLAYER_ACHIEVEMENTS_CACHED = 'SELECT * FROM ' + SQL_PLAYER_ACHIEVEMENTS_TBL + ' WHERE account_id = ? AND update_time > ?' + SQL_PLAYER_ACHIEVEMENTS_COUNT = 'SELECT COUNT(*) FROM ' + SQL_PLAYER_ACHIEVEMENTS_TBL + + SQL_PLAYER_ACHIEVEMENTS_CACHED = 'SELECT * FROM ' + SQL_PLAYER_ACHIEVEMENTS_TBL + ' WHERE account_id = ? AND update_time > ?' - SQL_PLAYER_ACHIEVEMENTS_UPDATE = 'REPLACE INTO ' + SQL_PLAYER_ACHIEVEMENTS_TBL + '(account_id, update_time, stats) VALUES(?,?,?)' + SQL_PLAYER_ACHIEVEMENTS_UPDATE = 'REPLACE INTO ' + SQL_PLAYER_ACHIEVEMENTS_TBL + '(account_id, update_time, stats) VALUES(?,?,?)' - - SQL_TABLES = [ SQL_PLAYER_STATS_TBL, SQL_TANK_STATS_TBL, SQL_PLAYER_ACHIEVEMENTS_TBL ] + + SQL_TABLES = [ SQL_PLAYER_STATS_TBL, SQL_TANK_STATS_TBL, SQL_PLAYER_ACHIEVEMENTS_TBL ] - SQL_CHECK_TABLE_EXITS = """SELECT name FROM sqlite_master WHERE type='table' AND name=?""" + SQL_CHECK_TABLE_EXITS = """SELECT name FROM sqlite_master WHERE type='table' AND name=?""" - SQL_PRUNE_CACHE = """DELETE from {} WHERE update_time < {}""" + SQL_PRUNE_CACHE = """DELETE from {} WHERE update_time < {}""" # Default data. Please use the latest maps.json - maps = { - "Random": "Random map", - "amigosville": "Falls Creek", - "asia": "Lost Temple", - "canal": "Canal", - "canyon": "Canyon", - "desert_train": "Desert Sands", - "erlenberg": "Middleburg", - "faust": "Faust", - "fort": "Macragge", - "grossberg": "Dynasty's Pearl", - "himmelsdorf": "Himmelsdorf", - "italy": "Vineyards", - "karelia": "Rockfield", - "karieri": "Copperfield", - "lake": "Mirage", - "lumber": "Alpenstadt", - "malinovka": "Winter Malinovka", - "medvedkovo": "Dead Rail", - "milbase": "Yamato Harbor", - "mountain": "Black Goldville", - "north": "North", - "ordeal": "Trial by Fire", - "pliego": "Castilla", - "port": "Port Bay", - "rock": "Mayan Ruins", - "rudniki": "Mines", - "savanna": "Oasis Palms", - "skit": "Naval Frontier", - "test": "World of Ducks", - "tutorial": "Proving Grounds" - } + maps = { + "Random": "Random map", + "amigosville": "Falls Creek", + "asia": "Lost Temple", + "canal": "Canal", + "canyon": "Canyon", + "desert_train": "Desert Sands", + "erlenberg": "Middleburg", + "faust": "Faust", + "fort": "Macragge", + "grossberg": "Dynasty's Pearl", + "himmelsdorf": "Himmelsdorf", + "italy": "Vineyards", + "karelia": "Rockfield", + "karieri": "Copperfield", + "lake": "Mirage", + "lumber": "Alpenstadt", + "malinovka": "Winter Malinovka", + "medvedkovo": "Dead Rail", + "milbase": "Yamato Harbor", + "mountain": "Black Goldville", + "north": "North", + "ordeal": "Trial by Fire", + "pliego": "Castilla", + "port": "Port Bay", + "rock": "Mayan Ruins", + "rudniki": "Mines", + "savanna": "Oasis Palms", + "skit": "Naval Frontier", + "test": "World of Ducks", + "tutorial": "Proving Grounds" + } - tanks = None - tanks_by_tier = None + tanks = None + tanks_by_tier = None - NATION = [ 'ussr', 'germany', 'usa', 'china', 'france', 'uk', 'japan', 'other', 'european'] - NATION_STR = [ 'Soviet', 'Germany', 'USA', 'China', 'France', 'UK', 'Japan', 'Other', 'European'] - NATION_ID = { - 'ussr' : 0, - 'germany' : 1, - 'usa' : 2, - 'china' : 3, - 'france' : 4, - 'uk' : 5, - 'japan' : 6, - 'other' : 7, - 'european' : 8 - } + NATION = [ 'ussr', 'germany', 'usa', 'china', 'france', 'uk', 'japan', 'other', 'european'] + NATION_STR = [ 'Soviet', 'Germany', 'USA', 'China', 'France', 'UK', 'Japan', 'Other', 'European'] + NATION_ID = { + 'ussr' : 0, + 'germany' : 1, + 'usa' : 2, + 'china' : 3, + 'france' : 4, + 'uk' : 5, + 'japan' : 6, + 'other' : 7, + 'european' : 8 + } - TANK_TYPE = [ 'lightTank', 'mediumTank', 'heavyTank', 'AT-SPG' ] - TANK_TYPE_STR = [ 'Light Tank', 'Medium Tank', 'Heavy Tank', 'Tank Destroyer' ] - - TANK_TYPE_ID = { - 'lightTank' : 0, - 'mediumTank' : 1, - 'heavyTank' : 2, - 'AT-SPG' : 3 - } + TANK_TYPE = [ 'lightTank', 'mediumTank', 'heavyTank', 'AT-SPG' ] + TANK_TYPE_STR = [ 'Light Tank', 'Medium Tank', 'Heavy Tank', 'Tank Destroyer' ] + + TANK_TYPE_ID = { + 'lightTank' : 0, + 'mediumTank' : 1, + 'heavyTank' : 2, + 'AT-SPG' : 3 + } - URL_WG_SERVER = { - 'eu' : 'https://api.wotblitz.eu/wotb/', - 'ru' : 'https://api.wotblitz.ru/wotb/', - 'na' : 'https://api.wotblitz.com/wotb/', - 'asia' : 'https://api.wotblitz.asia/wotb/', - 'china' : None - } + URL_WG_SERVER = { + 'eu' : 'https://api.wotblitz.eu/wotb/', + 'ru' : 'https://api.wotblitz.ru/wotb/', + 'na' : 'https://api.wotblitz.com/wotb/', + 'asia' : 'https://api.wotblitz.asia/wotb/', + 'china' : None + } - ACCOUNT_ID_SERVER= { - 'ru' : range(0, int(5e8)), - 'eu' : range(int(5e8), int(10e8)), - 'na' : range(int(1e9),int(2e9)), - 'asia' : range(int(2e9),int(31e8)), - 'china' : range(int(31e8),int(4e9)) - } + ACCOUNT_ID_SERVER= { + 'ru' : range(0, int(5e8)), + 'eu' : range(int(5e8), int(10e8)), + 'na' : range(int(1e9),int(2e9)), + 'asia' : range(int(2e9),int(31e8)), + 'china' : range(int(31e8),int(4e9)) + } - def __init__(self, WG_app_id : str = None, tankopedia_fn : str = None, maps_fn : str = None, - stats_cache: bool = False, rate_limit: int = 10, global_rate_limit = True): - - self.WG_app_id = WG_app_id - self.load_tanks(tankopedia_fn) - WG.tanks = self.tanks - self.global_rate_limit = global_rate_limit - - if (maps_fn != None): - if os.path.exists(maps_fn) and os.path.isfile(maps_fn): - try: - with open(maps_fn, 'rt', encoding='utf8') as f: - WG.maps = json.loads(f.read()) - except Exception as err: - error('Could not read maps file: ' + maps_fn + '\n' + str(err)) - else: - verbose('Could not find maps file: ' + maps_fn) - if self.WG_app_id != None: - headers = {'Accept-Encoding': 'gzip, deflate'} - if self.global_rate_limit: - self.session = ThrottledClientSession(rate_limit=rate_limit, headers=headers) - else: - self.session = dict() - for server in list(self.URL_WG_SERVER)[:4]: # China (5th) server is unknown, thus excluded - self.session[server] = ThrottledClientSession(rate_limit=rate_limit, headers=headers) - debug('WG aiohttp session initiated') - else: - self.session = None - debug('WG aiohttp session NOT initiated') - - # cache - self.cache = None - self.statsQ = None - self.stat_saver_task = None - if stats_cache: - try: - self.statsQ = asyncio.Queue() - self.stat_saver_task = asyncio.create_task(self.stat_saver()) - except Exception as err: - error(exception=err) - sys.exit(1) - + def __init__(self, WG_app_id : str = None, tankopedia_fn : str = None, maps_fn : str = None, + stats_cache: bool = False, rate_limit: int = 10, global_rate_limit = True): + + self.WG_app_id = WG_app_id + self.load_tanks(tankopedia_fn) + WG.tanks = self.tanks + self.global_rate_limit = global_rate_limit + + if (maps_fn is not None): + if os.path.exists(maps_fn) and os.path.isfile(maps_fn): + try: + with open(maps_fn, 'rt', encoding='utf8') as f: + WG.maps = json.loads(f.read()) + except Exception as err: + error('Could not read maps file: ' + maps_fn + '\n' + str(err)) + else: + verbose('Could not find maps file: ' + maps_fn) + if self.WG_app_id is not None: + headers = {'Accept-Encoding': 'gzip, deflate'} + if self.global_rate_limit: + self.session = ThrottledClientSession(rate_limit=rate_limit, headers=headers) + else: + self.session = dict() + for server in list(self.URL_WG_SERVER)[:4]: # China (5th) server is unknown, thus excluded + self.session[server] = ThrottledClientSession(rate_limit=rate_limit, headers=headers) + debug('WG aiohttp session initiated') + else: + self.session = None + debug('WG aiohttp session NOT initiated') + + # cache + self.cache = None + self.statsQ = None + self.stat_saver_task = None + if stats_cache: + try: + self.statsQ = asyncio.Queue() + self.stat_saver_task = asyncio.create_task(self.stat_saver()) + except Exception as err: + error(exception=err) + sys.exit(1) + - async def close(self): - # close stats queue - try: - if self.statsQ != None: - debug('WG.close(): Waiting for statsQ to finish') - await self.statsQ.join() - debug('WG.close(): statsQ finished') - self.stat_saver_task.cancel() - debug('statsCacheTask cancelled') - await self.stat_saver_task - - # close cacheDB - if self.cache != None: - # prune old cache records - await self.cleanup_cache() - await self.cache.commit() - await self.cache.close() - - if self.session != None: - if self.global_rate_limit: - await self.session.close() - else: - for server in self.session: - await self.session[server].close() - except Exception as err: - error(exception=err) + async def close(self): + # close stats queue + try: + if self.statsQ is not None: + debug('WG.close(): Waiting for statsQ to finish') + await self.statsQ.join() + debug('WG.close(): statsQ finished') + self.stat_saver_task.cancel() + debug('statsCacheTask cancelled') + await self.stat_saver_task + + # close cacheDB + if self.cache is not None: + # prune old cache records + await self.cleanup_cache() + await self.cache.commit() + await self.cache.close() + + if self.session is not None: + if self.global_rate_limit: + await self.session.close() + else: + for server in self.session: + await self.session[server].close() + except Exception as err: + error(exception=err) - - # Class methods ---------------------------------------------------------- + + # Class methods ---------------------------------------------------------- - @classmethod - def get_server(cls, account_id: int) -> str: - """Get Realm/server of an account based on account ID""" - if account_id >= 1e9: - if account_id >= 31e8: - debug('Chinese account/server: no stats available') - return None - if account_id >= 2e9: - return 'asia' - return 'na' - else: - if account_id < 5e8: - return 'ru' - return 'eu' - return None + @classmethod + def get_server(cls, account_id: int) -> str: + """Get Realm/server of an account based on account ID""" + if account_id >= 1e9: + if account_id >= 31e8: + debug('Chinese account/server: no stats available') + return None + if account_id >= 2e9: + return 'asia' + return 'na' + else: + if account_id < 5e8: + return 'ru' + return 'eu' + return None - @classmethod - def update_maps(cls, map_data: dict): - """Update maps data""" - cls.maps = map_data - return None + @classmethod + def update_maps(cls, map_data: dict): + """Update maps data""" + cls.maps = map_data + return None - @classmethod - def get_map(self, map_str: str) -> str: - """Return map name from short map string in replays""" - try: - return self.maps[map_str] - except: - error('Map ' + map_str + ' not found') - return None - + @classmethod + def get_map(self, map_str: str) -> str: + """Return map name from short map string in replays""" + try: + return self.maps[map_str] + except: + error('Map ' + map_str + ' not found') + return None + - @classmethod - def get_tank_user_strs(self) -> str: - return self.tanks["userStr"].keys() + @classmethod + def get_tank_user_strs(self) -> str: + return self.tanks["userStr"].keys() - @classmethod - def chk_JSON(cls, json_obj, check = None) -> bool: - try: - if (check == None): - # nothing to check - return True - if (check == 'tank_stats'): - if cls.chk_JSON_tank_stats(json_obj): - return True - else: - debug('Checking tank list JSON failed.') - return False - elif (check == 'player_stats'): - if cls.chk_JSON_player_stats(json_obj): - return True - else: - debug('Checking player JSON failed.') - return False - elif (check == 'tankopedia'): - if cls.chk_JSON_tankopedia(json_obj): - return True - else: - debug('Checking tank JSON failed.') - return False - elif (check == 'account_id'): - if cls.chk_JSON_get_account_id(json_obj): - return True - else: - debug('Checking account_id JSON failed.') - return False - except (TypeError, ValueError) as err: - debug(str(err)) - return False + @classmethod + def chk_JSON(cls, json_obj, check = None) -> bool: + try: + if (check is None): + # nothing to check + return True + if (check == 'tank_stats'): + if cls.chk_JSON_tank_stats(json_obj): + return True + else: + debug('Checking tank list JSON failed.') + return False + elif (check == 'player_stats'): + if cls.chk_JSON_player_stats(json_obj): + return True + else: + debug('Checking player JSON failed.') + return False + elif (check == 'tankopedia'): + if cls.chk_JSON_tankopedia(json_obj): + return True + else: + debug('Checking tank JSON failed.') + return False + elif (check == 'account_id'): + if cls.chk_JSON_get_account_id(json_obj): + return True + else: + debug('Checking account_id JSON failed.') + return False + except (TypeError, ValueError) as err: + debug(str(err)) + return False - @classmethod - def chk_JSON_status(cls, json_resp: dict) -> bool: - try: - if (json_resp == None) or ('status' not in json_resp) or (json_resp['status'] == None): - return False - elif (json_resp['status'] == 'ok') and ('data' in json_resp): - return True - elif json_resp['status'] == 'error': - if ('error' in json_resp): - error_msg = 'Received an error' - if ('message' in json_resp['error']) and (json_resp['error']['message'] != None): - error_msg = error_msg + ': ' + json_resp['error']['message'] - if ('value' in json_resp['error']) and (json_resp['error']['value'] != None): - error_msg = error_msg + ' Value: ' + json_resp['error']['value'] - debug(error_msg) - return False - else: - error('Unknown status-code: ' + json_resp['status']) - return False - except KeyError as err: - error('No field found in JSON data', err) - except Exception as err: - error("JSON format error", err) - return False + @classmethod + def chk_JSON_status(cls, json_resp: dict) -> bool: + try: + if (json_resp is None) or ('status' not in json_resp) or (json_resp['status'] is None): + return False + elif (json_resp['status'] == 'ok') and ('data' in json_resp): + return True + elif json_resp['status'] == 'error': + if ('error' in json_resp): + error_msg = 'Received an error' + if ('message' in json_resp['error']) and (json_resp['error']['message'] is not None): + error_msg = error_msg + ': ' + json_resp['error']['message'] + if ('value' in json_resp['error']) and (json_resp['error']['value'] is not None): + error_msg = error_msg + ' Value: ' + json_resp['error']['value'] + debug(error_msg) + return False + else: + error('Unknown status-code: ' + json_resp['status']) + return False + except KeyError as err: + error('No field found in JSON data', err) + except Exception as err: + error("JSON format error", err) + return False - @classmethod - def chk_JSON_get_account_id(cls, json_resp: dict) -> bool: - try: - if cls.chk_JSON_status(json_resp): - if (json_resp['meta']['count'] > 0): - return True - except KeyError as err: - error('Key not found', err) - except Exception as err: - error(exception=err) - return False + @classmethod + def chk_JSON_get_account_id(cls, json_resp: dict) -> bool: + try: + if cls.chk_JSON_status(json_resp): + if (json_resp['meta']['count'] > 0): + return True + except KeyError as err: + error('Key not found', err) + except Exception as err: + error(exception=err) + return False - # @classmethod - # def chkJSONplayer(cls, json_resp: dict) -> bool: - # """"Check String for being a valid Player JSON file""" - # try: - # if cls.chk_JSON_status(json_resp): - # if int(json_resp[0]['account_id']) > 0: - # return True - # except KeyError as err: - # error('Key not found', err) - # except: - # debug("JSON check failed") - # return False - - @classmethod - def chk_JSON_tankopedia(cls, json_resp: dict) -> bool: - """"Check String for being a valid Tankopedia JSON file""" - try: - if cls.chk_JSON_status(json_resp): - if int(json_resp[0]['tank_id']) > 0: - return True - except KeyError as err: - error('Key not found', err) - except: - debug("JSON check failed") - return False - + # @classmethod + # def chkJSONplayer(cls, json_resp: dict) -> bool: + # """"Check String for being a valid Player JSON file""" + # try: + # if cls.chk_JSON_status(json_resp): + # if int(json_resp[0]['account_id']) > 0: + # return True + # except KeyError as err: + # error('Key not found', err) + # except: + # debug("JSON check failed") + # return False + + @classmethod + def chk_JSON_tankopedia(cls, json_resp: dict) -> bool: + """"Check String for being a valid Tankopedia JSON file""" + try: + if cls.chk_JSON_status(json_resp): + if int(json_resp[0]['tank_id']) > 0: + return True + except KeyError as err: + error('Key not found', err) + except: + debug("JSON check failed") + return False + - @classmethod - def chk_JSON_player_stats(cls, json_resp: dict) -> bool: - """"Check String for being a valid Tank JSON file""" - try: - if cls.chk_JSON_status(json_resp): - for acc in json_resp['data']: - if json_resp['data'][acc] != None: - return True - except KeyError as err: - error('Key not found', err) - except: - debug("JSON check failed") - return False + @classmethod + def chk_JSON_player_stats(cls, json_resp: dict) -> bool: + """"Check String for being a valid Tank JSON file""" + try: + if cls.chk_JSON_status(json_resp): + for acc in json_resp['data']: + if json_resp['data'][acc] is not None: + return True + except KeyError as err: + error('Key not found', err) + except: + debug("JSON check failed") + return False - @classmethod - def chk_JSON_tank_stats(cls, json_resp: dict) -> bool: - """"Check String for being a valid Tank list JSON file""" - try: - if cls.chk_JSON_status(json_resp): - if ('data' in json_resp) and (len(json_resp['data']) > 0): - debug('JSON tank list check OK') - return True - except Exception as err: - error('JSON check FAILED: ' + str(json_resp) ) - error(exception=err) - return False + @classmethod + def chk_JSON_tank_stats(cls, json_resp: dict) -> bool: + """"Check String for being a valid Tank list JSON file""" + try: + if cls.chk_JSON_status(json_resp): + if ('data' in json_resp) and (len(json_resp['data']) > 0): + debug('JSON tank list check OK') + return True + except Exception as err: + error('JSON check FAILED: ' + str(json_resp) ) + error(exception=err) + return False - # Methods -------------------------------------------------- - def load_tanks(self, tankopedia_fn: str): - """Load tanks from tankopedia JSON""" - if tankopedia_fn == None: - return False - try: - with open(tankopedia_fn, 'rt', encoding='utf8') as f: - self.tanks = json.loads(f.read()) - self.tanks['tankStr'] = dict() - p = re.compile('.+_short$') - for usr_str in self.tanks['userStr']: - if p.match(usr_str) == None: - self.tanks['tankStr'][self.tanks['userStr'][usr_str]] = usr_str - self.tanks_by_tier = dict() - for tier in range(1,11): - self.tanks_by_tier[str(tier)] = list() - for tank in self.tanks['data'].values(): - self.tanks_by_tier[str(tank['tier'])].append(tank['tank_id']) - return True - except Exception as err: - error('Could not read tankopedia: ' + tankopedia_fn, err) - return False - + # Methods -------------------------------------------------- + def load_tanks(self, tankopedia_fn: str): + """Load tanks from tankopedia JSON""" + if tankopedia_fn is None: + return False + try: + with open(tankopedia_fn, 'rt', encoding='utf8') as f: + self.tanks = json.loads(f.read()) + self.tanks['tankStr'] = dict() + p = re.compile('.+_short$') + for usr_str in self.tanks['userStr']: + if p.match(usr_str) is None: + self.tanks['tankStr'][self.tanks['userStr'][usr_str]] = usr_str + self.tanks_by_tier = dict() + for tier in range(1,11): + self.tanks_by_tier[str(tier)] = list() + for tank in self.tanks['data'].values(): + self.tanks_by_tier[str(tank['tier'])].append(tank['tank_id']) + return True + except Exception as err: + error('Could not read tankopedia: ' + tankopedia_fn, err) + return False + - def get_replay_filename(self, replay: dict): - try: - summary = replay['data']['summary'] - timestamp = int(summary['battle_start_timestamp']) - timestamp = get_date_str(timestamp, date_format='%Y%m%d_%H%M') - player = summary['player_name'] - arena_id = int(summary['arena_unique_id']) - vehicle = self.name2tank_str(summary['vehicle']) - vehicle.replace(' ','_') + def get_replay_filename(self, replay: dict): + try: + summary = replay['data']['summary'] + timestamp = int(summary['battle_start_timestamp']) + timestamp = get_date_str(timestamp, date_format='%Y%m%d_%H%M') + player = summary['player_name'] + arena_id = int(summary['arena_unique_id']) + vehicle = self.name2tank_str(summary['vehicle']) + vehicle.replace(' ','_') - return '{:s}_{:s}_{:s}_{:d}.wotbreplay.json'.format(timestamp, player, vehicle, arena_id) - except Exception as err: - error('Unexpected Exception', err) - return None + return '{:s}_{:s}_{:s}_{:d}.wotbreplay.json'.format(timestamp, player, vehicle, arena_id) + except Exception as err: + error('Unexpected Exception', err) + return None - def get_map_user_strs(self) -> str: - return self.maps.keys() + def get_map_user_strs(self) -> str: + return self.maps.keys() - def tank_str2name(self, tank_str: str) -> str: - """Return tank name from short tank string in replays""" - try: - return self.tanks["userStr"][tank_str] - except: - error('Tank ' + tank_str + ' not found') - return tank_str + def tank_str2name(self, tank_str: str) -> str: + """Return tank name from short tank string in replays""" + try: + return self.tanks["userStr"][tank_str] + except: + error('Tank ' + tank_str + ' not found') + return tank_str - - def name2tank_str(self, name: str) -> str: - """Return shorttank string from tank string in replays""" - try: - return self.tanks["tankStr"][name] - except: - debug('Tank ' + name + ' not found') - return name + + def name2tank_str(self, name: str) -> str: + """Return shorttank string from tank string in replays""" + try: + return self.tanks["tankStr"][name] + except: + debug('Tank ' + name + ' not found') + return name - def get_tanks_by_tier(self, tier: int) -> list(): - """Returns tank_ids by tier""" - try: - return self.tanks_by_tier[str(tier)] - except KeyError as err: - error('Invalid tier', err) - return None - + def get_tanks_by_tier(self, tier: int) -> list(): + """Returns tank_ids by tier""" + try: + return self.tanks_by_tier[str(tier)] + except KeyError as err: + error('Invalid tier', err) + return None + - def get_url_clan_info(self, server: str, clan_id: int) -> str: - try: - if server == None: - return None - return self.URL_WG_SERVER[server] + self.URL_WG_CLAN_INFO + self.WG_app_id + '&clan_id=' + str(clan_id) - except Exception as err: - if (server == None) or (server.lower() not in WG.ACCOUNT_ID_SERVER.keys()): - error('No server name or invalid server name given: ' + server if (server != None) else '') - error('Available servers: ' + ', '.join(WG.ACCOUNT_ID_SERVER.keys())) - error(exception=err) - return None + def get_url_clan_info(self, server: str, clan_id: int) -> str: + try: + if server is None: + return None + return self.URL_WG_SERVER[server] + self.URL_WG_CLAN_INFO + self.WG_app_id + '&clan_id=' + str(clan_id) + except Exception as err: + if (server is None) or (server.lower() not in WG.ACCOUNT_ID_SERVER.keys()): + error('No server name or invalid server name given: ' + server if (server != None) else '') + error('Available servers: ' + ', '.join(WG.ACCOUNT_ID_SERVER.keys())) + error(exception=err) + return None - def get_url_player_tank_list(self, account_id: int) -> str: - return self.get_url_player_tanks_stats(account_id, fields='tank_id') + def get_url_player_tank_list(self, account_id: int) -> str: + return self.get_url_player_tanks_stats(account_id, fields='tank_id') - def get_url_player_tanks_stats(self, account_id: int, tank_ids: list = [], fields: list = []) -> str: - server = self.get_server(account_id) - if server == None: - return None - if (tank_ids != None) and (len(tank_ids) > 0): - tank_id_str= '&tank_id=' + '%2C'.join([ str(x) for x in tank_ids]) - else: - # emtpy tank-id list returns all the player's tanks - tank_id_str = '' + def get_url_player_tanks_stats(self, account_id: int, tank_ids: list = [], fields: list = []) -> str: + server = self.get_server(account_id) + if server is None: + return None + if (tank_ids is not None) and (len(tank_ids) > 0): + tank_id_str= '&tank_id=' + '%2C'.join([ str(x) for x in tank_ids]) + else: + # emtpy tank-id list returns all the player's tanks + tank_id_str = '' - if (fields != None) and (len(fields) > 0): - field_str = '&fields=' + '%2C'.join(fields) - else: - # return all the fields - field_str = '' + if (fields is not None) and (len(fields) > 0): + field_str = '&fields=' + '%2C'.join(fields) + else: + # return all the fields + field_str = '' - return self.URL_WG_SERVER[server] + self.URL_WG_PLAYER_TANK_STATS + self.WG_app_id + '&account_id=' + str(account_id) + tank_id_str + field_str - + return self.URL_WG_SERVER[server] + self.URL_WG_PLAYER_TANK_STATS + self.WG_app_id + '&account_id=' + str(account_id) + tank_id_str + field_str + - def get_url_player_stats(self, account_id, fields) -> str: - try: - server = self.get_server(account_id) - if server == None: - return None - if (fields != None) and (len(fields) > 0): - field_str = '&fields=' + '%2C'.join(fields) - else: - # return all the fields - field_str = '' + def get_url_player_stats(self, account_id, fields) -> str: + try: + server = self.get_server(account_id) + if server is None: + return None + if (fields is not None) and (len(fields) > 0): + field_str = '&fields=' + '%2C'.join(fields) + else: + # return all the fields + field_str = '' - return self.URL_WG_SERVER[server] + self.URL_WG_PLAYER_STATS + self.WG_app_id + '&account_id=' + str(account_id) + field_str - except Exception as err: - if (server == None): - error('Invalid account_id') - error(exception=err) - return None + return self.URL_WG_SERVER[server] + self.URL_WG_PLAYER_STATS + self.WG_app_id + '&account_id=' + str(account_id) + field_str + except Exception as err: + if (server is None): + error('Invalid account_id') + error(exception=err) + return None - def get_url_player_achievements(self, account_ids: list, fields : str = 'max_series') -> str: - try: - # assumming that all account_ids are from the same server. This has to be taken care. - server = self.get_server(account_ids[0]) + def get_url_player_achievements(self, account_ids: list, fields : str = 'max_series') -> str: + try: + # assumming that all account_ids are from the same server. This has to be taken care. + server = self.get_server(account_ids[0]) - if server == None: - return None - account_ids_str = '%2C'.join(str(id) for id in account_ids) - if (fields != None) and (len(fields) > 0): - field_str = '&fields=' + '%2C'.join(fields) - else: - # return all the fields - field_str = '' + if server is None: + return None + account_ids_str = '%2C'.join(str(id) for id in account_ids) + if (fields is not None) and (len(fields) > 0): + field_str = '&fields=' + '%2C'.join(fields) + else: + # return all the fields + field_str = '' - return self.URL_WG_SERVER[server] + self.URL_WG_PLAYER_ACHIEVEMENTS + self.WG_app_id + '&account_id=' + account_ids_str + field_str - except Exception as err: - if (server == None): - error('Invalid account_id') - error(exception=err) - return None + return self.URL_WG_SERVER[server] + self.URL_WG_PLAYER_ACHIEVEMENTS + self.WG_app_id + '&account_id=' + account_ids_str + field_str + except Exception as err: + if (server is None): + error('Invalid account_id') + error(exception=err) + return None - def get_url_account_id(self, nickname, server) -> int: - try: - return self.URL_WG_SERVER[server] + self.URL_WG_ACCOUNT_ID + self.WG_app_id + '&search=' + urllib.parse.quote(nickname) - except Exception as err: - if nickname == None or len(nickname) == 0: - error('No nickname given') - if (server == None) or (server.lower() not in WG.ACCOUNT_ID_SERVER.keys()): - error('No server name or invalid server name given: ' + server if (server != None) else '') - error('Available servers: ' + ', '.join(WG.ACCOUNT_ID_SERVER.keys())) - error(exception=err) - return None + def get_url_account_id(self, nickname, server) -> int: + try: + return self.URL_WG_SERVER[server] + self.URL_WG_ACCOUNT_ID + self.WG_app_id + '&search=' + urllib.parse.quote(nickname) + except Exception as err: + if nickname is None or len(nickname) == 0: + error('No nickname given') + if (server is None) or (server.lower() not in WG.ACCOUNT_ID_SERVER.keys()): + error('No server name or invalid server name given: ' + server if (server != None) else '') + error('Available servers: ' + ', '.join(WG.ACCOUNT_ID_SERVER.keys())) + error(exception=err) + return None - def url_get_server(self, url: str) -> str: - """Decode WG server from the URL""" - try: - for server in self.session: - if url.startswith(self.URL_WG_SERVER[server]): - return server - except Exception as err: - error(exception=err) - return 'eu' # default + def url_get_server(self, url: str) -> str: + """Decode WG server from the URL""" + try: + for server in self.session: + if url.startswith(self.URL_WG_SERVER[server]): + return server + except Exception as err: + error(exception=err) + return 'eu' # default - def print_request_stats(self): - """Print session statics""" - if self.global_rate_limit: - verbose_std('Globar rate limit: ' + self.session.get_stats_str()) - else: - for server in self.session: - verbose_std('Per server rate limits: ' + server + ': '+ self.session[server].get_stats_str()) + def print_request_stats(self): + """Print session statics""" + if self.global_rate_limit: + verbose_std('Globar rate limit: ' + self.session.get_stats_str()) + else: + for server in self.session: + verbose_std('Per server rate limits: ' + server + ': '+ self.session[server].get_stats_str()) - async def get_url_JSON(self, url: str, chk_JSON_func = None, max_tries = MAX_RETRIES) -> dict: - """Class WG get_url_JSON() for load balancing between WG servers - that have individial rate limits""" - - if self.global_rate_limit: - session = self.session - else: - server = self.url_get_server(url) - session = self.session[server] - debug('server:' + server) - return await get_url_JSON(session, url, chk_JSON_func, max_tries) + async def get_url_JSON(self, url: str, chk_JSON_func = None, max_tries = MAX_RETRIES) -> dict: + """Class WG get_url_JSON() for load balancing between WG servers + that have individial rate limits""" + + if self.global_rate_limit: + session = self.session + else: + server = self.url_get_server(url) + session = self.session[server] + debug('server:' + server) + return await get_url_JSON(session, url, chk_JSON_func, max_tries) - async def get_account_id(self, nickname: str) -> int: - """Get WG account_id for a nickname""" - try: - nick = None - server = None - nick, server = nickname.split('@') - debug(nick + ' @ '+ server) - server = server.lower() - if nick == None or server == None: - raise ValueError('Invalid nickname given: ' + nickname) - url = self.get_url_account_id(nick, server) + async def get_account_id(self, nickname: str) -> int: + """Get WG account_id for a nickname""" + try: + nick = None + server = None + nick, server = nickname.split('@') + debug(nick + ' @ '+ server) + server = server.lower() + if nick is None or server is None: + raise ValueError('Invalid nickname given: ' + nickname) + url = self.get_url_account_id(nick, server) - json_data = await self.get_url_JSON(url, self.chk_JSON_status) - for res in json_data['data']: - if res['nickname'].lower() == nick.lower(): - return res['account_id'] - error('No WG account_id found: ' + nickname) - - except Exception as err: - error(exception=err) - return None - + json_data = await self.get_url_JSON(url, self.chk_JSON_status) + for res in json_data['data']: + if res['nickname'].lower() == nick.lower(): + return res['account_id'] + error('No WG account_id found: ' + nickname) + + except Exception as err: + error(exception=err) + return None + - async def get_player_tank_stats(self, account_id: int, tank_ids = [], fields = [], cache=True, cache_only = False) -> dict: - """Get player's stats (WR, # of battles) in a tank or all tanks (empty tank_ids[])""" - try: - stats = None + async def get_player_tank_stats(self, account_id: int, tank_ids = [], fields = [], cache=True, cache_only = False) -> dict: + """Get player's stats (WR, # of battles) in a tank or all tanks (empty tank_ids[])""" + try: + stats = None - # try cached stats first: - if cache: - stats = await self.get_cached_tank_stats(account_id, tank_ids, fields) - if stats != None: - return stats - if cache_only: - return None + # try cached stats first: + if cache: + stats = await self.get_cached_tank_stats(account_id, tank_ids, fields) + if stats is not None: + return stats + if cache_only: + return None - # Cached stats not found, fetching new ones - url = self.get_url_player_tanks_stats(account_id, tank_ids, fields) - json_data = await self.get_url_JSON(url, self.chk_JSON_status) - if json_data != None: - # debug('JSON Response received: ' + str(json_data)) - stats = json_data['data'][str(account_id)] - if cache: - await self.save_tank_stats(account_id, tank_ids, stats) - return stats - except Exception as err: - error(exception=err) - return None + # Cached stats not found, fetching new ones + url = self.get_url_player_tanks_stats(account_id, tank_ids, fields) + json_data = await self.get_url_JSON(url, self.chk_JSON_status) + if json_data is not None: + # debug('JSON Response received: ' + str(json_data)) + stats = json_data['data'][str(account_id)] + if cache: + await self.save_tank_stats(account_id, tank_ids, stats) + return stats + except Exception as err: + error(exception=err) + return None - async def get_player_stats(self, account_id: int, fields = [], cache=True, cache_only = False) -> dict: - """Get player's global stats """ - try: - # debug('account_id: ' + str(account_id) ) - stats = None + async def get_player_stats(self, account_id: int, fields = [], cache=True, cache_only = False) -> dict: + """Get player's global stats """ + try: + # debug('account_id: ' + str(account_id) ) + stats = None - # try cached stats first: - if cache: - stats = await self.get_cached_player_stats(account_id,fields) - # stats found unless StatsNotFound exception is raised - return stats + # try cached stats first: + if cache: + stats = await self.get_cached_player_stats(account_id,fields) + # stats found unless StatsNotFound exception is raised + return stats - except StatsNotFound as err: - debug('No Cached stats for account_id=' + str(account_id)) - if cache_only: - return None - # No cached stats found, need to retrieve - - try: - # Cached stats not found, fetching new ones - url = self.get_url_player_stats(account_id, fields) - json_data = await self.get_url_JSON(url, self.chk_JSON_status) - if json_data != None: - # debug('JSON Response received: ' + str(json_data)) - stats = json_data['data'][str(account_id)] - if cache: - await self.save_player_stats(account_id, stats) - return stats - except Exception as err: - error(exception=err) - return None + except StatsNotFound as err: + debug('No Cached stats for account_id=' + str(account_id)) + if cache_only: + return None + # No cached stats found, need to retrieve + + try: + # Cached stats not found, fetching new ones + url = self.get_url_player_stats(account_id, fields) + json_data = await self.get_url_JSON(url, self.chk_JSON_status) + if json_data is not None: + # debug('JSON Response received: ' + str(json_data)) + stats = json_data['data'][str(account_id)] + if cache: + await self.save_player_stats(account_id, stats) + return stats + except Exception as err: + error(exception=err) + return None - async def get_player_achievements(self, account_ids: list, fields = [], cache=True) -> dict: - """Get player's achievements stats """ - try: - account_ids = set(account_ids) - stats = dict() - if len(account_ids) == 0: - debug('Zero account_ids given') - return None + async def get_player_achievements(self, account_ids: list, fields = [], cache=True) -> dict: + """Get player's achievements stats """ + try: + account_ids = set(account_ids) + stats = dict() + if len(account_ids) == 0: + debug('Zero account_ids given') + return None - # try cached stats first: - if cache: - debug('Checking for cached stats') - account_ids_cached = set() - for account_id in account_ids: - try: - stats[str(account_id)] = await self.get_cached_player_achievements(account_id,fields) - account_ids_cached.add(account_id) - except StatsNotFound as err: - # No cached stats found, need to retrieve - debug(exception=err) - account_ids = account_ids.difference(account_ids_cached) - if len(account_ids) == 0: - return stats - debug('fetching new stats') - # Cached stats not found, fetching new ones - url = self.get_url_player_achievements(list(account_ids), fields) - json_data = await self.get_url_JSON(url, self.chk_JSON_status) - if (json_data != None) and ('data' in json_data): - # debug('JSON Response received: ' + str(json_data)) - for account_id in json_data['data'].keys(): - stats[account_id] = json_data['data'][account_id] - if cache: - await self.save_player_achievements(int(account_id), json_data['data'][account_id]) - return stats - except Exception as err: - error(exception=err) - return None + # try cached stats first: + if cache: + debug('Checking for cached stats') + account_ids_cached = set() + for account_id in account_ids: + try: + stats[str(account_id)] = await self.get_cached_player_achievements(account_id,fields) + account_ids_cached.add(account_id) + except StatsNotFound as err: + # No cached stats found, need to retrieve + debug(exception=err) + account_ids = account_ids.difference(account_ids_cached) + if len(account_ids) == 0: + return stats + debug('fetching new stats') + # Cached stats not found, fetching new ones + url = self.get_url_player_achievements(list(account_ids), fields) + json_data = await self.get_url_JSON(url, self.chk_JSON_status) + if (json_data is not None) and ('data' in json_data): + # debug('JSON Response received: ' + str(json_data)) + for account_id in json_data['data'].keys(): + stats[account_id] = json_data['data'][account_id] + if cache: + await self.save_player_achievements(int(account_id), json_data['data'][account_id]) + return stats + except Exception as err: + error(exception=err) + return None - def merge_player_stats(self, stats1: dict, stats2: dict) -> dict: - try: - if stats2 == None: return stats1 - for keyA in stats2: - if keyA not in stats1: - stats1[keyA] = stats2[keyA] - else: - for keyB in stats2[keyA]: - stats1[keyA][keyB] = stats2[keyA][keyB] - return stats1 - except KeyError as err: - error('Key not found', err) - return None + def merge_player_stats(self, stats1: dict, stats2: dict) -> dict: + try: + if stats2 is None: return stats1 + for keyA in stats2: + if keyA not in stats1: + stats1[keyA] = stats2[keyA] + else: + for keyB in stats2[keyA]: + stats1[keyA][keyB] = stats2[keyA][keyB] + return stats1 + except KeyError as err: + error('Key not found', err) + return None - def get_tank_data(self, tank_id: int, field: str): - if self.tanks == None: - return None - try: - return self.tanks['data'][str(tank_id)][field] - except KeyError as err: - error('Key not found', err) - return None + def get_tank_data(self, tank_id: int, field: str): + if self.tanks is None: + return None + try: + return self.tanks['data'][str(tank_id)][field] + except KeyError as err: + error('Key not found', err) + return None - def get_tank_tier(self, tank_id: int) -> str: - try: - return self.get_tank_data(tank_id, 'tier') - except Exception as err: - error(exception=err) - return None + def get_tank_tier(self, tank_id: int) -> str: + try: + return self.get_tank_data(tank_id, 'tier') + except Exception as err: + error(exception=err) + return None - - def get_tank_name(self, tank_id: int) -> str: - try: - return self.get_tank_data(tank_id, 'name') - except Exception as err: - error(exception=err) - return None + + def get_tank_name(self, tank_id: int) -> str: + try: + return self.get_tank_data(tank_id, 'name') + except Exception as err: + error(exception=err) + return None - def get_tank_type(self, tank_id: int) -> str: - try: - return self.get_tank_data(tank_id, 'type') - except Exception as err: - error(exception=err) - return None + def get_tank_type(self, tank_id: int) -> str: + try: + return self.get_tank_data(tank_id, 'type') + except Exception as err: + error(exception=err) + return None - def get_tank_nation(self, tank_id: int) -> str: - try: - return self.get_tank_data(tank_id, 'nation') - except Exception as err: - error(exception=err) - return None + def get_tank_nation(self, tank_id: int) -> str: + try: + return self.get_tank_data(tank_id, 'nation') + except Exception as err: + error(exception=err) + return None - def get_tank_type_str(self, tank_id: int) -> str: - try: - tank_type = self.get_tank_data(tank_id, 'type') - return self.TANK_TYPE_STR[self.TANK_TYPE_ID[tank_type]] - except Exception as err: - error(exception=err) - return None + def get_tank_type_str(self, tank_id: int) -> str: + try: + tank_type = self.get_tank_data(tank_id, 'type') + return self.TANK_TYPE_STR[self.TANK_TYPE_ID[tank_type]] + except Exception as err: + error(exception=err) + return None - def get_tank_nation_str(self, tank_id: int) -> str: - try: - nation = self.get_tank_data(tank_id, 'nation') - return self.NATION_STR[self.NATION_ID[nation]] - except Exception as err: - error(exception=err) - return None + def get_tank_nation_str(self, tank_id: int) -> str: + try: + nation = self.get_tank_data(tank_id, 'nation') + return self.NATION_STR[self.NATION_ID[nation]] + except Exception as err: + error(exception=err) + return None - def get_tank_type_id(self, tank_id: int) -> str: - try: - tank_type = self.get_tank_data(tank_id, 'type') - return self.TANK_TYPE_ID[tank_type] - except Exception as err: - error(exception=err) - return None + def get_tank_type_id(self, tank_id: int) -> str: + try: + tank_type = self.get_tank_data(tank_id, 'type') + return self.TANK_TYPE_ID[tank_type] + except Exception as err: + error(exception=err) + return None - def get_tank_nation_id(self, tank_id: int) -> str: - try: - nation = self.get_tank_data(tank_id, 'nation') - return self.NATION_ID[nation] - except Exception as err: - error(exception=err) - return None + def get_tank_nation_id(self, tank_id: int) -> str: + try: + nation = self.get_tank_data(tank_id, 'nation') + return self.NATION_ID[nation] + except Exception as err: + error(exception=err) + return None - def is_premium(self, tank_id: int) -> bool: - try: - return self.get_tank_data(tank_id, 'is_premium') - except Exception as err: - error(exception=err) - return None + def is_premium(self, tank_id: int) -> bool: + try: + return self.get_tank_data(tank_id, 'is_premium') + except Exception as err: + error(exception=err) + return None - async def save_stats(self, statsType: str, key: list, stats: list): - """Save stats to a async queue to be saved by the stat_saver -task""" - if self.statsQ == None: - return False - else: - await self.statsQ.put([ statsType, key, stats, NOW() ]) - return True + async def save_stats(self, statsType: str, key: list, stats: list): + """Save stats to a async queue to be saved by the stat_saver -task""" + if self.statsQ is None: + return False + else: + await self.statsQ.put([ statsType, key, stats, NOW() ]) + return True - async def save_player_stats(self, account_id: int, stats: dict): - try: - await self.save_stats(MODE_PLAYER_STATS, [int(account_id)], stats) - except Exception as err: - error(exception=err) - return None + async def save_player_stats(self, account_id: int, stats: dict): + try: + await self.save_stats(MODE_PLAYER_STATS, [int(account_id)], stats) + except Exception as err: + error(exception=err) + return None - async def save_player_achievements(self, account_id: int, stats: dict): - try: - await self.save_stats(MODE_PLAYER_ACHIEVEMENTS, [int(account_id)], stats) - except Exception as err: - error(exception=err) - return None + async def save_player_achievements(self, account_id: int, stats: dict): + try: + await self.save_stats(MODE_PLAYER_ACHIEVEMENTS, [int(account_id)], stats) + except Exception as err: + error(exception=err) + return None - async def save_tank_stats(self, account_id: int, tank_ids: list, stats: dict): - try: - await self.save_stats(MODE_TANK_STATS, [int(account_id), tank_ids], stats) - except Exception as err: - error(exception=err) - return None + async def save_tank_stats(self, account_id: int, tank_ids: list, stats: dict): + try: + await self.save_stats(MODE_TANK_STATS, [int(account_id), tank_ids], stats) + except Exception as err: + error(exception=err) + return None - async def stat_saver(self): - """Async task for saving stats into cache in background""" + async def stat_saver(self): + """Async task for saving stats into cache in background""" - if self.statsQ == None: - error('No statsQ defined') - return None - try: - self.cache = await aiosqlite.connect(WG.CACHE_DB_FILE) - # Create cache tables table - await self.cache.execute(WG.SQL_TANK_STATS_CREATE_TBL) - await self.cache.execute(WG.SQL_PLAYER_STATS_CREATE_TBL) - await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL) + if self.statsQ is None: + error('No statsQ defined') + return None + try: + self.cache = await aiosqlite.connect(WG.CACHE_DB_FILE) + # Create cache tables table + await self.cache.execute(WG.SQL_TANK_STATS_CREATE_TBL) + await self.cache.execute(WG.SQL_PLAYER_STATS_CREATE_TBL) + await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CREATE_TBL) - await self.cache.commit() - - if is_debug(): - async with self.cache.execute(WG.SQL_PLAYER_STATS_COUNT) as cursor: - debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player stat records' ) - async with self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_COUNT) as cursor: - debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player achievement records' ) - async with self.cache.execute(WG.SQL_TANK_STATS_COUNT) as cursor: - debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player tank stat records' ) - except Exception as err: - error(exception=err) - sys.exit(1) + await self.cache.commit() + + if is_debug(): + async with self.cache.execute(WG.SQL_PLAYER_STATS_COUNT) as cursor: + debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player stat records' ) + async with self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_COUNT) as cursor: + debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player achievement records' ) + async with self.cache.execute(WG.SQL_TANK_STATS_COUNT) as cursor: + debug('Cache contains: ' + str((await cursor.fetchone())[0]) + ' player tank stat records' ) + except Exception as err: + error(exception=err) + sys.exit(1) - while True: - try: - stats = await self.statsQ.get() - - stat_type = stats[0] - key = stats[1] - stats_data = stats[2] - update_time = stats[3] + while True: + try: + stats = await self.statsQ.get() + + stat_type = stats[0] + key = stats[1] + stats_data = stats[2] + update_time = stats[3] - #debug('stat_type: ' + stat_type + ', key: ' + str(key)) - if stat_type == MODE_TANK_STATS: - await self.store_tank_stats(key, stats_data, update_time) - elif stat_type == MODE_PLAYER_STATS: - await self.store_player_stats(key, stats_data, update_time) - elif stat_type == MODE_PLAYER_ACHIEVEMENTS: - await self.store_player_achievements(key, stats_data, update_time) - else: - error('Function to saves stats type \'' + stat_type + '\' is not implemented yet') - - except asyncio.CancelledError: - # this is an eternal loop that will wait until cancelled - return None + #debug('stat_type: ' + stat_type + ', key: ' + str(key)) + if stat_type == MODE_TANK_STATS: + await self.store_tank_stats(key, stats_data, update_time) + elif stat_type == MODE_PLAYER_STATS: + await self.store_player_stats(key, stats_data, update_time) + elif stat_type == MODE_PLAYER_ACHIEVEMENTS: + await self.store_player_achievements(key, stats_data, update_time) + else: + error('Function to saves stats type \'' + stat_type + '\' is not implemented yet') + + except asyncio.CancelledError: + # this is an eternal loop that will wait until cancelled + return None - except Exception as err: - error(exception=err) - self.statsQ.task_done() - return None + except Exception as err: + error(exception=err) + self.statsQ.task_done() + return None - async def cleanup_cache(self, grace_time = CACHE_GRACE_TIME): - """Clean old cache records""" - if self.cache == None: - debug('No active cache') - return None - for table in WG.SQL_TABLES: - async with self.cache.execute(WG.SQL_CHECK_TABLE_EXITS, (table,)) as cursor: - if (await cursor.fetchone()) != None: - debug('Pruning cache table: ' + table) - await self.cache.execute(WG.SQL_PRUNE_CACHE.format(table, NOW() - grace_time)) - await self.cache.commit() - return None + async def cleanup_cache(self, grace_time = CACHE_GRACE_TIME): + """Clean old cache records""" + if self.cache is None: + debug('No active cache') + return None + for table in WG.SQL_TABLES: + async with self.cache.execute(WG.SQL_CHECK_TABLE_EXITS, (table,)) as cursor: + if (await cursor.fetchone()) is not None: + debug('Pruning cache table: ' + table) + await self.cache.execute(WG.SQL_PRUNE_CACHE.format(table, NOW() - grace_time)) + await self.cache.commit() + return None - async def store_tank_stats(self, key: list , stats_data: list, update_time: int): - """Save tank stats into cache""" - assert self.cache is not None, "cache must be initiated" - - try: - account_id = key[0] - tank_ids = set(key[1]) - if stats_data != None: - for stat in stats_data: - tank_id = stat['tank_id'] - await self.cache.execute(WG.SQL_TANK_STATS_UPDATE, (account_id, tank_id, update_time, json.dumps(stat))) - tank_ids.discard(tank_id) - # no stats found => Add None to mark that - for tank_id in tank_ids: - await self.cache.execute(WG.SQL_TANK_STATS_UPDATE, (account_id, tank_id, update_time, None)) - await self.cache.commit() - debug('Cached tank stats saved for account_id: ' + str(account_id) ) - return True - except Exception as err: - error(exception=err) - return False + async def store_tank_stats(self, key: list , stats_data: list, update_time: int): + """Save tank stats into cache""" + assert self.cache is not None, "cache must be initiated" + + try: + account_id = key[0] + tank_ids = set(key[1]) + if stats_data is not None: + for stat in stats_data: + tank_id = stat['tank_id'] + await self.cache.execute(WG.SQL_TANK_STATS_UPDATE, (account_id, tank_id, update_time, json.dumps(stat))) + tank_ids.discard(tank_id) + # no stats found => Add None to mark that + for tank_id in tank_ids: + await self.cache.execute(WG.SQL_TANK_STATS_UPDATE, (account_id, tank_id, update_time, None)) + await self.cache.commit() + debug('Cached tank stats saved for account_id: ' + str(account_id) ) + return True + except Exception as err: + error(exception=err) + return False - async def store_player_stats(self, key: list , stats_data: list, update_time: int): - """Save player stats into cache""" - assert self.cache is not None, "cache must be initiated" - - try: - account_id = key[0] - if stats_data != None: - await self.cache.execute(WG.SQL_PLAYER_STATS_UPDATE, (account_id, update_time, json.dumps(stats_data))) - else: - await self.cache.execute(WG.SQL_PLAYER_STATS_UPDATE, (account_id, update_time, None)) - await self.cache.commit() - debug('Cached player stats saved for account_id: ' + str(account_id) ) - return True - except Exception as err: - error(exception=err) - return False + async def store_player_stats(self, key: list , stats_data: list, update_time: int): + """Save player stats into cache""" + assert self.cache is not None, "cache must be initiated" + + try: + account_id = key[0] + if stats_data is not None: + await self.cache.execute(WG.SQL_PLAYER_STATS_UPDATE, (account_id, update_time, json.dumps(stats_data))) + else: + await self.cache.execute(WG.SQL_PLAYER_STATS_UPDATE, (account_id, update_time, None)) + await self.cache.commit() + debug('Cached player stats saved for account_id: ' + str(account_id) ) + return True + except Exception as err: + error(exception=err) + return False - async def store_player_achievements(self, key: list , stats_data: list, update_time: int): - """Save player stats into cache""" - assert self.cache is not None, "cache must be initiated" - - try: - account_id = key[0] - if stats_data != None: - await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_UPDATE, (account_id, update_time, json.dumps(stats_data))) - else: - await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_UPDATE, (account_id, update_time, None)) - await self.cache.commit() - debug('Cached player achievements saved for account_id: ' + str(account_id) ) - return True - except Exception as err: - error(exception=err) - return False + async def store_player_achievements(self, key: list , stats_data: list, update_time: int): + """Save player stats into cache""" + assert self.cache is not None, "cache must be initiated" + + try: + account_id = key[0] + if stats_data is not None: + await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_UPDATE, (account_id, update_time, json.dumps(stats_data))) + else: + await self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_UPDATE, (account_id, update_time, None)) + await self.cache.commit() + debug('Cached player achievements saved for account_id: ' + str(account_id) ) + return True + except Exception as err: + error(exception=err) + return False - async def get_cached_tank_stats(self, account_id: int, tank_ids: list, fields: list ): - assert self.cache is not None, "cache must be initiated" + async def get_cached_tank_stats(self, account_id: int, tank_ids: list, fields: list ): + assert self.cache is not None, "cache must be initiated" - try: - # test for cacheDB existence - debug('Trying cached stats first') - if self.cache == None: - debug('No cache DB') - return None - - stats = [] - if len(tank_ids) > 0: - sql_query = 'SELECT * FROM ' + WG.SQL_TANK_STATS_TBL + ' WHERE account_id = ? AND update_time > ? AND tank_id IN (' + ','.join([str(x) for x in tank_ids]) + ')' - else: - sql_query = 'SELECT * FROM ' + WG.SQL_TANK_STATS_TBL + ' WHERE account_id = ? AND update_time > ?' + try: + # test for cacheDB existence + debug('Trying cached stats first') + if self.cache is None: + debug('No cache DB') + return None + + stats = [] + if len(tank_ids) > 0: + sql_query = 'SELECT * FROM ' + WG.SQL_TANK_STATS_TBL + ' WHERE account_id = ? AND update_time > ? AND tank_id IN (' + ','.join([str(x) for x in tank_ids]) + ')' + else: + sql_query = 'SELECT * FROM ' + WG.SQL_TANK_STATS_TBL + ' WHERE account_id = ? AND update_time > ?' - async with self.cache.execute(sql_query, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: - tank_ids = set(tank_ids) - async for row in cursor: - # debug('account_id: ' + str(account_id) + ': 1') - if row[3] == None: - # None/null stats found in cache - # i.e. stats have been requested, but not returned from WG API - tank_ids.remove(row[1]) - continue - # debug('account_id: ' + str(account_id) + ': 2') - stats.append(json.loads(row[3])) - # debug('account_id: ' + str(account_id) + ': 3') - tank_ids.remove(row[1]) - - # return stats ONLY if ALL the requested stats were found in cache - if tank_ids == set(): - debug('Cached stats found: ' + str(account_id)) - return stats - - except Exception as err: - error(exception=err) - debug('No cached stats found') - return None + async with self.cache.execute(sql_query, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: + tank_ids = set(tank_ids) + async for row in cursor: + # debug('account_id: ' + str(account_id) + ': 1') + if row[3] is None: + # None/null stats found in cache + # i.e. stats have been requested, but not returned from WG API + tank_ids.remove(row[1]) + continue + # debug('account_id: ' + str(account_id) + ': 2') + stats.append(json.loads(row[3])) + # debug('account_id: ' + str(account_id) + ': 3') + tank_ids.remove(row[1]) + + # return stats ONLY if ALL the requested stats were found in cache + if tank_ids == set(): + debug('Cached stats found: ' + str(account_id)) + return stats + + except Exception as err: + error(exception=err) + debug('No cached stats found') + return None - async def get_cached_player_stats(self, account_id, fields): - try: - # test for cacheDB existence - debug('Trying cached stats first') - if self.cache == None: - # debug('No cache DB') - raise StatsNotFound('No cache DB in use') - - async with self.cache.execute(WG.SQL_PLAYER_STATS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: - row = await cursor.fetchone() - # debug('account_id: ' + str(account_id) + ': 1') - if row == None: - # no cached stats found, marked with an empty array - # debug('No cached stats found') - raise StatsNotFound('No cached stats found') - - debug('Cached stats found') - if row[2] == None: - # None/null stats found in cache - # i.e. stats have been requested, but not returned from WG API - return None - else: - # Return proper stats - return json.loads(row[2]) - except StatsNotFound as err: - debug(exception=err) - raise - except Exception as err: - error('Error trying to look for cached stats', exception=err) - return None + async def get_cached_player_stats(self, account_id, fields): + try: + # test for cacheDB existence + debug('Trying cached stats first') + if self.cache is None: + # debug('No cache DB') + raise StatsNotFound('No cache DB in use') + + async with self.cache.execute(WG.SQL_PLAYER_STATS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: + row = await cursor.fetchone() + # debug('account_id: ' + str(account_id) + ': 1') + if row is None: + # no cached stats found, marked with an empty array + # debug('No cached stats found') + raise StatsNotFound('No cached stats found') + + debug('Cached stats found') + if row[2] is None: + # None/null stats found in cache + # i.e. stats have been requested, but not returned from WG API + return None + else: + # Return proper stats + return json.loads(row[2]) + except StatsNotFound as err: + debug(exception=err) + raise + except Exception as err: + error('Error trying to look for cached stats', exception=err) + return None - async def get_cached_player_achievements(self, account_id, fields): - try: - # test for cacheDB existence - debug('Trying cached stats first') - if self.cache == None: - # debug('No cache DB') - raise StatsNotFound('No cache DB in use') - - async with self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: - row = await cursor.fetchone() - # debug('account_id: ' + str(account_id) + ': 1') - if row == None: - # no cached stats found, marked with an empty array - # debug('No cached stats found') - raise StatsNotFound('No cached stats found') - - debug('Cached stats found') - if row[2] == None: - # None/null stats found in cache - # i.e. stats have been requested, but not returned from WG API - return None - else: - # Return proper stats - return json.loads(row[2]) - except StatsNotFound as err: - debug(exception=err) - raise - except Exception as err: - error('Error trying to look for cached stats', exception=err) - return None + async def get_cached_player_achievements(self, account_id, fields): + try: + # test for cacheDB existence + debug('Trying cached stats first') + if self.cache is None: + # debug('No cache DB') + raise StatsNotFound('No cache DB in use') + + async with self.cache.execute(WG.SQL_PLAYER_ACHIEVEMENTS_CACHED, [account_id, NOW() - WG.CACHE_GRACE_TIME] ) as cursor: + row = await cursor.fetchone() + # debug('account_id: ' + str(account_id) + ': 1') + if row is None: + # no cached stats found, marked with an empty array + # debug('No cached stats found') + raise StatsNotFound('No cached stats found') + + debug('Cached stats found') + if row[2] is None: + # None/null stats found in cache + # i.e. stats have been requested, but not returned from WG API + return None + else: + # Return proper stats + return json.loads(row[2]) + except StatsNotFound as err: + debug(exception=err) + raise + except Exception as err: + error('Error trying to look for cached stats', exception=err) + return None # ----------------------------------------------------------- @@ -1793,202 +1799,202 @@ class WG: # ----------------------------------------------------------- class WoTinspector: - URL_WI = 'https://replays.wotinspector.com' - URL_REPLAY_LIST = URL_WI + '/en/sort/ut/page/' - URL_REPLAY_DL = URL_WI + '/en/download/' - URL_REPLAY_VIEW = URL_WI +'/en/view/' - URL_REPLAY_UL = 'https://api.wotinspector.com/replay/upload?' - URL_REPLAY_INFO = 'https://api.wotinspector.com/replay/upload?details=full&key=' - URL_TANK_DB = "https://wotinspector.com/static/armorinspector/tank_db_blitz.js" + URL_WI = 'https://replays.wotinspector.com' + URL_REPLAY_LIST = URL_WI + '/en/sort/ut/page/' + URL_REPLAY_DL = URL_WI + '/en/download/' + URL_REPLAY_VIEW = URL_WI +'/en/view/' + URL_REPLAY_UL = 'https://api.wotinspector.com/replay/upload?' + URL_REPLAY_INFO = 'https://api.wotinspector.com/replay/upload?details=full&key=' + URL_TANK_DB = "https://wotinspector.com/static/armorinspector/tank_db_blitz.js" - REPLAY_N = 1 - DEFAULT_RATE_LIMIT : float = 10/60 # 10 requests / min - DEFAULT_TOKEN : str = 'a5428af49bf7495986577d59b0e5bcff' + REPLAY_N = 1 + DEFAULT_RATE_LIMIT : float = 10/60 # 10 requests / min + DEFAULT_TOKEN : str = 'a5428af49bf7495986577d59b0e5bcff' - def __init__(self, rate_limit: float = DEFAULT_RATE_LIMIT, auth_token: Optional[str] = None): + def __init__(self, rate_limit: float = DEFAULT_RATE_LIMIT, auth_token: Optional[str] = None): - headers : Optional[dict[str, str]] = None + headers : Optional[dict[str, str]] = None - if auth_token is None: - auth_token = self.DEFAULT_TOKEN - headers = dict() - headers['Authorization'] = 'Token ' + auth_token - self.session = ThrottledClientSession(rate_limit=rate_limit, - filters=[self.URL_REPLAY_LIST, self.URL_REPLAY_INFO], - re_filter=False, limit_filtered=True, headers = headers) - + if auth_token is None: + auth_token = self.DEFAULT_TOKEN + headers = dict() + headers['Authorization'] = 'Token ' + auth_token + self.session = ThrottledClientSession(rate_limit=rate_limit, + filters=[self.URL_REPLAY_LIST, self.URL_REPLAY_INFO], + re_filter=False, limit_filtered=True, headers = headers) + - async def close(self) -> None: - if self.session is not None: - debug('Closing aiohttp session') - await self.session.close() - + async def close(self) -> None: + if self.session is not None: + debug('Closing aiohttp session') + await self.session.close() + - async def get_tankopedia(self, filename = 'tanks.json'): - """Retrieve Tankpedia from WoTinspector.com""" - - async with self.session.get(self.URL_TANK_DB) as r: - if r.status == 200: - WI_tank_db=await r.text() - WI_tank_db = WI_tank_db.split("\n") - else: - print('Error: Could not get valid HTTPS response. HTTP: ' + str(r.status) ) - sys.exit(1) - tanks = {} - n = 0 - p = re.compile('\\s*(\\d+):\\s{"en":"([^"]+)",.*?"tier":(\\d+), "type":(\\d), "premium":(\\d).*') - for line in WI_tank_db[1:-1]: - try: - m = p.match(line) - tank = {} - tank['tank_id'] = int(m.group(1)) - tank['name'] = m.group(2) - tank['tier'] = int(m.group(3)) - tank['type'] = WG.TANK_TYPE[int(m.group(4))] - tank['is_premium'] = (int(m.group(5)) == 1) - tanks[str(m.group(1))] = tank - n += 1 - except Exception as err: - error(exception=err) - - tankopedia = {} - tankopedia['status'] = "ok" - tankopedia['meta'] = {"count" : n} - tankopedia['data'] = tanks - - verbose_std("Tankopedia has " + str(n) + " tanks in: " + filename) - with open(filename,'w') as outfile: - outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False)) - return None + async def get_tankopedia(self, filename = 'tanks.json'): + """Retrieve Tankpedia from WoTinspector.com""" + + async with self.session.get(self.URL_TANK_DB) as r: + if r.status == 200: + WI_tank_db=await r.text() + WI_tank_db = WI_tank_db.split("\n") + else: + print('Error: Could not get valid HTTPS response. HTTP: ' + str(r.status) ) + sys.exit(1) + tanks = {} + n = 0 + p = re.compile('\\s*(\\d+):\\s{"en":"([^"]+)",.*?"tier":(\\d+), "type":(\\d), "premium":(\\d).*') + for line in WI_tank_db[1:-1]: + try: + m = p.match(line) + tank = {} + tank['tank_id'] = int(m.group(1)) + tank['name'] = m.group(2) + tank['tier'] = int(m.group(3)) + tank['type'] = WG.TANK_TYPE[int(m.group(4))] + tank['is_premium'] = (int(m.group(5)) == 1) + tanks[str(m.group(1))] = tank + n += 1 + except Exception as err: + error(exception=err) + + tankopedia = {} + tankopedia['status'] = "ok" + tankopedia['meta'] = {"count" : n} + tankopedia['data'] = tanks + + verbose_std("Tankopedia has " + str(n) + " tanks in: " + filename) + with open(filename,'w') as outfile: + outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False)) + return None - async def get_replay_JSON(self, replay_id: str): - json_resp = await get_url_JSON(self.session, self.URL_REPLAY_INFO + replay_id, chk_JSON_func=None) - try: - if self.chk_JSON_replay(json_resp): - return json_resp - else: - return None - except Exception as err: - error('Unexpected Exception', err) - return None + async def get_replay_JSON(self, replay_id: str): + json_resp = await get_url_JSON(self.session, self.URL_REPLAY_INFO + replay_id, chk_JSON_func=None) + try: + if self.chk_JSON_replay(json_resp): + return json_resp + else: + return None + except Exception as err: + error('Unexpected Exception', err) + return None - async def post_replay(self, data, filename = 'Replay', account_id = 0, title = 'Replay', priv = False, N = None): - try: - N = N if N != None else self.REPLAY_N - self.REPLAY_N += 1 + async def post_replay(self, data, filename = 'Replay', account_id = 0, title = 'Replay', priv = False, N = None): + try: + N = N if N is not None else self.REPLAY_N + self.REPLAY_N += 1 - hash = hashlib.md5() - hash.update(data) - replay_id = hash.hexdigest() + hash = hashlib.md5() + hash.update(data) + replay_id = hash.hexdigest() - ## Testing if the replay has already been posted - json_resp = await self.get_replay_JSON(replay_id) - if json_resp != None: - debug('Already uploaded: ' + title, id=N) - return json_resp + ## Testing if the replay has already been posted + json_resp = await self.get_replay_JSON(replay_id) + if json_resp is not None: + debug('Already uploaded: ' + title, id=N) + return json_resp - params = { - 'title' : title, - 'private' : (1 if priv else 0), - 'uploaded_by' : account_id, - 'details' : 'full', - 'key' : replay_id - } + params = { + 'title' : title, + 'private' : (1 if priv else 0), + 'uploaded_by' : account_id, + 'details' : 'full', + 'key' : replay_id + } - url = self.URL_REPLAY_UL + urllib.parse.urlencode(params, quote_via=urllib.parse.quote) - #debug('URL: ' + url) - headers ={'Content-type': 'application/x-www-form-urlencoded'} - payload = { 'file' : (filename, base64.b64encode(data)) } - except Exception as err: - error('Unexpected Exception', exception=err, id=N) - return None + url = self.URL_REPLAY_UL + urllib.parse.urlencode(params, quote_via=urllib.parse.quote) + #debug('URL: ' + url) + headers ={'Content-type': 'application/x-www-form-urlencoded'} + payload = { 'file' : (filename, base64.b64encode(data)) } + except Exception as err: + error('Unexpected Exception', exception=err, id=N) + return None - json_resp = None - for retry in range(MAX_RETRIES): - debug('Posting: ' + title + ' Try #: ' + str(retry + 1) + '/' + str(MAX_RETRIES), id=N ) - try: - async with self.session.post(url, headers=headers, data=payload) as resp: - debug('HTTP response: '+ str(resp.status), id=N) - if resp.status == 200: - debug('HTTP POST 200 = Success. Reading response data', id=N) - json_resp = await resp.json() - if self.chk_JSON_replay(json_resp): - debug('Response data read. Status OK', id=N) - return json_resp - debug(title + ' : Receive invalid JSON', id=N) - else: - debug('Got HTTP/' + str(resp.status), id=N) - except Exception as err: - debug(exception=err, id=N) - await asyncio.sleep(SLEEP) - - debug(' Could not post replay: ' + title, id=N) - return json_resp + json_resp = None + for retry in range(MAX_RETRIES): + debug('Posting: ' + title + ' Try #: ' + str(retry + 1) + '/' + str(MAX_RETRIES), id=N ) + try: + async with self.session.post(url, headers=headers, data=payload) as resp: + debug('HTTP response: '+ str(resp.status), id=N) + if resp.status == 200: + debug('HTTP POST 200 = Success. Reading response data', id=N) + json_resp = await resp.json() + if self.chk_JSON_replay(json_resp): + debug('Response data read. Status OK', id=N) + return json_resp + debug(title + ' : Receive invalid JSON', id=N) + else: + debug('Got HTTP/' + str(resp.status), id=N) + except Exception as err: + debug(exception=err, id=N) + await asyncio.sleep(SLEEP) + + debug(' Could not post replay: ' + title, id=N) + return json_resp - async def get_replay_listing(self, page: int = 0) -> aiohttp.ClientResponse: - url = self.get_url_replay_listing(page) - return await self.session.get(url) + async def get_replay_listing(self, page: int = 0) -> aiohttp.ClientResponse: + url = self.get_url_replay_listing(page) + return await self.session.get(url) - @classmethod - def get_url_replay_listing(cls, page : int): - return cls.URL_REPLAY_LIST + str(page) + '?vt=#filters' + @classmethod + def get_url_replay_listing(cls, page : int): + return cls.URL_REPLAY_LIST + str(page) + '?vt=#filters' - @classmethod - def get_url_replay_view(cls, replay_id): - return cls.URL_REPLAY_VIEW + replay_id + @classmethod + def get_url_replay_view(cls, replay_id): + return cls.URL_REPLAY_VIEW + replay_id - @classmethod - def get_replay_links(cls, doc: str): - """Get replay download links from WoTinspector.com replay listing page""" - try: - soup = BeautifulSoup(doc, 'lxml') - links = soup.find_all('a') - replay_links = set() - for tag in links: - link = tag.get('href',None) - if (link is not None) and link.startswith(cls.URL_REPLAY_DL): - replay_links.add(link) - debug('Adding replay link:' + link) - except Exception as err: - error(exception=err) - return replay_links - + @classmethod + def get_replay_links(cls, doc: str): + """Get replay download links from WoTinspector.com replay listing page""" + try: + soup = BeautifulSoup(doc, 'lxml') + links = soup.find_all('a') + replay_links = set() + for tag in links: + link = tag.get('href',None) + if (link is not None) and link.startswith(cls.URL_REPLAY_DL): + replay_links.add(link) + debug('Adding replay link:' + link) + except Exception as err: + error(exception=err) + return replay_links + - @classmethod - def read_replay_id(cls, json_replay): - """Read replay_id from replay JSON file""" - try: - url = json_replay['data']['view_url'] - return cls.get_replay_id(url) - except Exception as err: - error(exception=err) - return None + @classmethod + def read_replay_id(cls, json_replay): + """Read replay_id from replay JSON file""" + try: + url = json_replay['data']['view_url'] + return cls.get_replay_id(url) + except Exception as err: + error(exception=err) + return None - @classmethod - def get_replay_id(cls, url): - return url.rsplit('/', 1)[-1] + @classmethod + def get_replay_id(cls, url): + return url.rsplit('/', 1)[-1] - @classmethod - def chk_JSON_replay(cls, json_resp) -> bool: - """"Check String for being a valid JSON file""" - try: - if ('status' in json_resp) and json_resp['status'] == 'ok' and \ - (get_JSON_value(json_resp, key='data.summary.exp_base') != None) : - debug("JSON check OK") - return True - except KeyError as err: - debug('Replay JSON check failed', exception=err) - except: - debug("Replay JSON check failed: " + str(json_resp)) - return False + @classmethod + def chk_JSON_replay(cls, json_resp) -> bool: + """"Check String for being a valid JSON file""" + try: + if ('status' in json_resp) and json_resp['status'] == 'ok' and \ + (get_JSON_value(json_resp, key='data.summary.exp_base') is not None) : + debug("JSON check OK") + return True + except KeyError as err: + debug('Replay JSON check failed', exception=err) + except: + debug("Replay JSON check failed: " + str(json_resp)) + return False # ----------------------------------------------------------- # Class BlitzStars @@ -1996,164 +2002,164 @@ class WoTinspector: class BlitzStars: - URL_BLITZSTARS = 'https://www.blitzstars.com' - URL_PLAYER_STATS = URL_BLITZSTARS + '/api/playerstats' - URL_PLAYER_TANK_STATS = URL_BLITZSTARS + '/api/tanks' - URL_TANK_AVERAGES = URL_BLITZSTARS + '/tankaverages.json' - URL_ACTIVE_PLAYERS = URL_BLITZSTARS + '/api/playerstats/activeinlast30days' + URL_BLITZSTARS = 'https://www.blitzstars.com' + URL_PLAYER_STATS = URL_BLITZSTARS + '/api/playerstats' + URL_PLAYER_TANK_STATS = URL_BLITZSTARS + '/api/tanks' + URL_TANK_AVERAGES = URL_BLITZSTARS + '/tankaverages.json' + URL_ACTIVE_PLAYERS = URL_BLITZSTARS + '/api/playerstats/activeinlast30days' - def __init__(self, rate_limit=30): - headers = {'Accept-Encoding': 'gzip, deflate'} - self.session = ThrottledClientSession(rate_limit=rate_limit, headers=headers) + def __init__(self, rate_limit=30): + headers = {'Accept-Encoding': 'gzip, deflate'} + self.session = ThrottledClientSession(rate_limit=rate_limit, headers=headers) - async def close(self): - if self.session != None: - await self.session.close() + async def close(self): + if self.session is not None: + await self.session.close() - @classmethod - def get_url_tank_averages(cls): - return cls.URL_TANK_AVERAGES + @classmethod + def get_url_tank_averages(cls): + return cls.URL_TANK_AVERAGES - @classmethod - def get_url_player_stats(cls, account_id: int): - return cls.URL_PLAYER_STATS + '/' + str(account_id) + @classmethod + def get_url_player_stats(cls, account_id: int): + return cls.URL_PLAYER_STATS + '/' + str(account_id) - @classmethod - def get_url_player_tank_stats(cls, account_id: int, tank_id: int): - return cls.URL_PLAYER_TANK_STATS + '/' + str(account_id) + '/' + str(tank_id) + @classmethod + def get_url_player_tank_stats(cls, account_id: int, tank_id: int): + return cls.URL_PLAYER_TANK_STATS + '/' + str(account_id) + '/' + str(tank_id) - @classmethod - def get_url_player_tanks_stats(cls, account_id: int): - return cls.URL_PLAYER_TANK_STATS + '/' + str(account_id) + @classmethod + def get_url_player_tanks_stats(cls, account_id: int): + return cls.URL_PLAYER_TANK_STATS + '/' + str(account_id) - @classmethod - def get_url_active_players(cls): - return cls.URL_ACTIVE_PLAYERS + @classmethod + def get_url_active_players(cls): + return cls.URL_ACTIVE_PLAYERS - @classmethod - def chk_JSON_tank_stats(cls, json_resp): - """Check BlitzStars player tank stats""" - try: - if (len(json_resp) > 0) and ('tank_id' in json_resp[0]): - debug('JSON check OK') - return True - except Exception as err: - error('JSON check FAILED: ' + str(json_resp) ) - error(exception=err) - return False + @classmethod + def chk_JSON_tank_stats(cls, json_resp): + """Check BlitzStars player tank stats""" + try: + if (len(json_resp) > 0) and ('tank_id' in json_resp[0]): + debug('JSON check OK') + return True + except Exception as err: + error('JSON check FAILED: ' + str(json_resp) ) + error(exception=err) + return False - @classmethod - def chk_JSON_player_stats(cls, json_resp : list): - """Check BlitzStars player stats""" - try: - if (len(json_resp) > 0) and ('account_id' in json_resp[0]): - debug('JSON check OK') - return True - except Exception as err: - error('JSON check FAILED: ' + str(json_resp) ) - error(exception=err) - return False + @classmethod + def chk_JSON_player_stats(cls, json_resp : list): + """Check BlitzStars player stats""" + try: + if (len(json_resp) > 0) and ('account_id' in json_resp[0]): + debug('JSON check OK') + return True + except Exception as err: + error('JSON check FAILED: ' + str(json_resp) ) + error(exception=err) + return False - @classmethod - async def tank_stats2WG(cls, BS_tank_stats: list) -> list: - """Convert BlitzStars player tank stats to WG API format""" - try: - if BS_tank_stats == None: - return None - WG_tank_stats = [] - for stat in BS_tank_stats: - try: - tmp = {} - for field in ['all', 'last_battle_time', 'tank_id', 'battle_life_time', 'account_id']: - tmp[field] = stat[field] - WG_tank_stats.append(tmp) - except KeyError as err: - error(exception=err) - if len(WG_tank_stats) > 0: - return WG_tank_stats - - except Exception as err: - error(exception=err) - pass - return None + @classmethod + async def tank_stats2WG(cls, BS_tank_stats: list) -> list: + """Convert BlitzStars player tank stats to WG API format""" + try: + if BS_tank_stats is None: + return None + WG_tank_stats = [] + for stat in BS_tank_stats: + try: + tmp = {} + for field in ['all', 'last_battle_time', 'tank_id', 'battle_life_time', 'account_id']: + tmp[field] = stat[field] + WG_tank_stats.append(tmp) + except KeyError as err: + error(exception=err) + if len(WG_tank_stats) > 0: + return WG_tank_stats + + except Exception as err: + error(exception=err) + pass + return None - async def get_player_stats(self, account_id: int, cache=True): - """Get player stats from BlitzStars""" - try: - # debug('account_id: ' + str(account_id) ) - stats = None + async def get_player_stats(self, account_id: int, cache=True): + """Get player stats from BlitzStars""" + try: + # debug('account_id: ' + str(account_id) ) + stats = None - if cache: - error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') - sys.exit(1) - # stats = await self.get_cached_player_stats(account_id,fields) - # stats found unless StatsNotFound exception is raised - return stats + if cache: + error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') + sys.exit(1) + # stats = await self.get_cached_player_stats(account_id,fields) + # stats found unless StatsNotFound exception is raised + return stats - except StatsNotFound as err: - # No cached stats found, need to retrieve - debug(exception=err) - pass - - try: - # Cached stats not found, fetching new ones - url = self.get_url_player_stats(account_id) - stats = await get_url_JSON(self.session, url, self.chk_JSON_player_stats) - if stats != None: - # debug('JSON Response received: ' + str(json_data)) - if cache: - error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') - sys.exit(1) - # await self.save_stats('player_stats', [account_id], stats) - return stats - except Exception as err: - error(exception=err) - return None + except StatsNotFound as err: + # No cached stats found, need to retrieve + debug(exception=err) + pass + + try: + # Cached stats not found, fetching new ones + url = self.get_url_player_stats(account_id) + stats = await get_url_JSON(self.session, url, self.chk_JSON_player_stats) + if stats is not None: + # debug('JSON Response received: ' + str(json_data)) + if cache: + error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') + sys.exit(1) + # await self.save_stats('player_stats', [account_id], stats) + return stats + except Exception as err: + error(exception=err) + return None - async def get_player_tank_stats(self, account_id: int, tank_id = None, cache=True): - """Get player stats for all his tanks from BlitzStars""" - try: - # debug('account_id: ' + str(account_id) ) - stats = None + async def get_player_tank_stats(self, account_id: int, tank_id = None, cache=True): + """Get player stats for all his tanks from BlitzStars""" + try: + # debug('account_id: ' + str(account_id) ) + stats = None - if cache: - error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') - sys.exit(1) - # stats = await self.get_cached_player_stats(account_id,fields) - # stats found unless StatsNotFound exception is raised - return stats + if cache: + error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') + sys.exit(1) + # stats = await self.get_cached_player_stats(account_id,fields) + # stats found unless StatsNotFound exception is raised + return stats - except StatsNotFound as err: - # No cached stats found, need to retrieve - debug(exception=err) - pass - - try: - # Cached stats not found, fetching new ones - if tank_id == None: - # get stats for all the player's tanks - url = self.get_url_player_tanks_stats(account_id) - else: - url = self.get_url_player_tank_stats(account_id, tank_id) - stats = await get_url_JSON(self.session, url, self.chk_JSON_tank_stats) - if stats != None: - # debug('JSON Response received: ' + str(json_data)) - if cache: - error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') - sys.exit(1) - # await self.save_stats('player_stats', [account_id], stats) - return stats - except Exception as err: - error(exception=err) - return None + except StatsNotFound as err: + # No cached stats found, need to retrieve + debug(exception=err) + pass + + try: + # Cached stats not found, fetching new ones + if tank_id is None: + # get stats for all the player's tanks + url = self.get_url_player_tanks_stats(account_id) + else: + url = self.get_url_player_tank_stats(account_id, tank_id) + stats = await get_url_JSON(self.session, url, self.chk_JSON_tank_stats) + if stats is not None: + # debug('JSON Response received: ' + str(json_data)) + if cache: + error('CACHE NOT IMPLEMENTED YET FOR BlitzStars()') + sys.exit(1) + # await self.save_stats('player_stats', [account_id], stats) + return stats + except Exception as err: + error(exception=err) + return None diff --git a/extract_tankopedia.py b/extract_tankopedia.py index 60217be..c5057d3 100755 --- a/extract_tankopedia.py +++ b/extract_tankopedia.py @@ -4,7 +4,11 @@ # Extract tankopedia data in WG API JSON format from Blitz app files (Android APK unzipped) from os import path -import sys, argparse, json, os, inspect, aiohttp, asyncio, aiofiles, re, logging, time, xmltodict, collections, configparser +import aiofiles # type: ignore +import xmltodict # type: ignore +from asyncio import set_event_loop_policy, run, create_task, get_event_loop_policy +import sys, argparse, json, os, inspect, asyncio, re, logging, time, configparser +from collections import OrderedDict import blitzutils as bu from blitzutils import WG @@ -18,216 +22,228 @@ BLITZAPP_STRINGS='Data/Strings/en.yaml' BLITZAPP_VEHICLES_DIR='Data/XML/item_defs/vehicles/' BLITZAPP_VEHICLE_FILE='list.xml' -wg = None +wg : WG | None = None ## main() ------------------------------------------------------------- -async def main(argv): - global wg - # set the directory for the script - os.chdir(os.path.dirname(sys.argv[0])) +async def main(argv: list[str]) -> None: + global wg + # set the directory for the script + os.chdir(os.path.dirname(sys.argv[0])) - ## Read config - BLITZAPP_FOLDER = '.' - try: - if os.path.isfile(FILE_CONFIG): - config = configparser.ConfigParser() - config.read(FILE_CONFIG) + ## Read config + BLITZAPP_FOLDER = '.' + try: + if os.path.isfile(FILE_CONFIG): + config = configparser.ConfigParser() + config.read(FILE_CONFIG) - configOptions = config['EXTRACT_TANKOPEDIA'] - BLITZAPP_FOLDER = configOptions.get('blitz_app_dir', BLITZAPP_FOLDER) - except: - pass - - parser = argparse.ArgumentParser(description='Extract Tankopedia data from Blitz game files') - parser.add_argument('blitz_app_base', type=str, nargs='?', metavar="BLITZAPP_FOLDER", default=BLITZAPP_FOLDER, help='Base dir of the Blitz App files') - parser.add_argument('tanks', type=str, default='tanks.json', nargs='?', metavar="TANKS_FILE", help='File to write Tankopedia') - parser.add_argument('maps', type=str, default='maps.json', nargs='?', metavar='MAPS_FILE', help='File to write map names') - arggroup = parser.add_mutually_exclusive_group() - arggroup.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode') - arggroup.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode') - arggroup.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode') - - args = parser.parse_args() - bu.set_log_level(args.silent, args.verbose, args.debug) + configOptions = config['EXTRACT_TANKOPEDIA'] + BLITZAPP_FOLDER = configOptions.get('blitz_app_dir', BLITZAPP_FOLDER) + except: + pass + + parser = argparse.ArgumentParser(description='Extract Tankopedia data from Blitz game files') + parser.add_argument('blitz_app_base', type=str, nargs='?', metavar="BLITZAPP_FOLDER", default=BLITZAPP_FOLDER, help='Base dir of the Blitz App files') + parser.add_argument('tanks', type=str, default='tanks.json', nargs='?', metavar="TANKS_FILE", help='File to write Tankopedia') + parser.add_argument('maps', type=str, default='maps.json', nargs='?', metavar='MAPS_FILE', help='File to write map names') + arggroup = parser.add_mutually_exclusive_group() + arggroup.add_argument('-d', '--debug', action='store_true', default=False, help='Debug mode') + arggroup.add_argument('-v', '--verbose', action='store_true', default=False, help='Verbose mode') + arggroup.add_argument('-s', '--silent', action='store_true', default=False, help='Silent mode') + + args = parser.parse_args() + bu.set_log_level(args.silent, args.verbose, args.debug) - wg = WG() - - tasks = [] - for nation in wg.NATION: - tasks.append(asyncio.create_task(extract_tanks(args.blitz_app_base, nation))) + wg = WG() + + tasks = [] + for nation in wg.NATION: + tasks.append(asyncio.create_task(extract_tanks(args.blitz_app_base, nation))) - tanklist = [] - for tanklist_tmp in await asyncio.gather(*tasks): - tanklist.extend(tanklist_tmp) - - tank_strs, map_strs = await read_user_strs(args.blitz_app_base) + tanklist = [] + for tanklist_tmp in await asyncio.gather(*tasks): + tanklist.extend(tanklist_tmp) + + tank_strs, map_strs = await read_user_strs(args.blitz_app_base) - json_data = None - userStrs = {} - tanks = {} - if os.path.exists(args.tanks): - try: - async with aiofiles.open(args.tanks) as infile: - json_data = json.loads(await infile.read()) - userStrs = json_data['userStr'] - tanks = json_data['data'] - except Exception as err: - bu.error('Unexpected error when reading file: ' + args.tanks + ' : ' + str(err)) + json_data = None + userStrs = {} + tanks = {} + if os.path.exists(args.tanks): + try: + async with aiofiles.open(args.tanks) as infile: + json_data = json.loads(await infile.read()) + userStrs = json_data['userStr'] + tanks = json_data['data'] + except Exception as err: + bu.error('Unexpected error when reading file: ' + args.tanks + ' : ' + str(err)) - async with aiofiles.open(args.tanks, 'w', encoding="utf8") as outfile: - new_tanks, new_userStrs = await convert_tank_names(tanklist, tank_strs) - # merge old and new tankopedia - tanks.update(new_tanks) - userStrs.update(new_userStrs) - tankopedia = collections.OrderedDict() - tankopedia['status'] = 'ok' - tankopedia['meta'] = { "count": len(tanks) } - tankopedia['data'] = bu.sort_dict(tanks, number=True) - tankopedia['userStr'] = bu.sort_dict(userStrs) - bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(tanks)) + ' tanks') - bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(userStrs)) + ' tank strings') - await outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False)) - - if args.maps != None: - maps = {} - if os.path.exists(args.maps): - try: - async with aiofiles.open(args.maps) as infile: - maps = json.loads(await infile.read()) - except Exception as err: - bu.error('Unexpected error when reading file: ' + args.maps + ' : ' + str(err)) - # merge old and new map data - maps.update(map_strs) - async with aiofiles.open(args.maps, 'w', encoding="utf8") as outfile: - bu.verbose_std('New maps file \'' + args.maps + '\' contains ' + str(len(maps)) + ' maps') - await outfile.write(json.dumps(maps, ensure_ascii=False, indent=4, sort_keys=True)) + async with aiofiles.open(args.tanks, 'w', encoding="utf8") as outfile: + new_tanks, new_userStrs = await convert_tank_names(tanklist, tank_strs) + # merge old and new tankopedia + tanks.update(new_tanks) + userStrs.update(new_userStrs) + tankopedia : OrderedDict[str, str| int | dict ]= OrderedDict() + tankopedia['status'] = 'ok' + tankopedia['meta'] = { "count": len(tanks) } + tankopedia['data'] = bu.sort_dict(tanks, number=True) + tankopedia['userStr'] = bu.sort_dict(userStrs) + bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(tanks)) + ' tanks') + bu.verbose_std('New tankopedia \'' + args.tanks + '\' contains ' + str(len(userStrs)) + ' tank strings') + await outfile.write(json.dumps(tankopedia, ensure_ascii=False, indent=4, sort_keys=False)) + + if args.maps is not None: + maps = {} + if os.path.exists(args.maps): + try: + async with aiofiles.open(args.maps) as infile: + maps = json.loads(await infile.read()) + except Exception as err: + bu.error('Unexpected error when reading file: ' + args.maps + ' : ' + str(err)) + # merge old and new map data + maps.update(map_strs) + async with aiofiles.open(args.maps, 'w', encoding="utf8") as outfile: + bu.verbose_std('New maps file \'' + args.maps + '\' contains ' + str(len(maps)) + ' maps') + await outfile.write(json.dumps(maps, ensure_ascii=False, indent=4, sort_keys=True)) - return None - + return None + async def extract_tanks(blitz_app_base : str, nation: str): - tanks = [] - # WG has changed the location of Data directory - at least in steam client - if path.isdir(path.join(blitz_app_base, 'assets')): - blitz_app_base = path.join(blitz_app_base, 'assets') - list_xml_file = path.join(blitz_app_base, BLITZAPP_VEHICLES_DIR ,nation ,BLITZAPP_VEHICLE_FILE) - if not path.isfile(list_xml_file): - print('ERROR: cannot open ' + list_xml_file) - return None - bu.debug('Opening file: ' + list_xml_file + ' (Nation: ' + nation + ')') - async with aiofiles.open(list_xml_file, 'r', encoding="utf8") as f: - try: - tankList = xmltodict.parse(await f.read()) - for data in tankList['root'].keys(): - tank_xml = tankList['root'][data] - tank = dict() - tank['is_premium'] = issubclass(type(tank_xml['price']), dict) - tank['nation'] = nation - tank['tank_id'] = get_tank_id(nation, int(tank_xml['id'])) - tank['tier'] = int(tank_xml['level']) - tank['type'] = get_tank_type(tank_xml['tags']) - tank['userStr'] = tank_xml['userString'] - #bu.debug('Reading tank string: ' + tank['userStr'], force=True) - tanks.append(tank) - except Exception as err: - bu.error(err) - sys.exit(2) - return tanks + tanks = [] + # WG has changed the location of Data directory - at least in steam client + if path.isdir(path.join(blitz_app_base, 'assets')): + blitz_app_base = path.join(blitz_app_base, 'assets') + list_xml_file = path.join(blitz_app_base, BLITZAPP_VEHICLES_DIR ,nation ,BLITZAPP_VEHICLE_FILE) + if not path.isfile(list_xml_file): + print('ERROR: cannot open ' + list_xml_file) + return None + bu.debug('Opening file: ' + list_xml_file + ' (Nation: ' + nation + ')') + async with aiofiles.open(list_xml_file, 'r', encoding="utf8") as f: + try: + tankList = xmltodict.parse(await f.read()) + for data in tankList['root'].keys(): + tank_xml = tankList['root'][data] + tank : dict[str, bool | int | str]= dict() + tank['is_premium'] = issubclass(type(tank_xml['price']), dict) + tank['nation'] = nation + tank['tank_id'] = get_tank_id(nation, int(tank_xml['id'])) + tank['tier'] = int(tank_xml['level']) + tank['type'] = get_tank_type(tank_xml['tags']) + tank['userStr'] = tank_xml['userString'] + #bu.debug('Reading tank string: ' + tank['userStr'], force=True) + tanks.append(tank) + except Exception as err: + bu.error(err) + sys.exit(2) + return tanks -async def read_user_strs(blitz_app_base : str) -> dict: - """Read user strings to convert map and tank names""" - tank_strs = {} - map_strs = {} - filename = path.join(blitz_app_base, BLITZAPP_STRINGS) - bu.debug('Opening file: ' + filename + ' for reading UserStrings') - try: - async with aiofiles.open(filename, 'r', encoding="utf8") as f: - p_tank = re.compile('^"(#\\w+?_vehicles:.+?)": "(.+)"$') - p_map = re.compile('^"#maps:(.+?):.+?: "(.+?)"$') - - async for l in f: - m = p_tank.match(l) - if m != None: - tank_strs[m.group(1)] = m.group(2) - - m = p_map.match(l) - if m != None and m.group(2) != 'Macragge': - map_strs[m.group(1)] = m.group(2) - - except Exception as err: - bu.error(err) - sys.exit(1) +async def read_user_strs(blitz_app_base : str) -> tuple[dict, dict]: + """Read user strings to convert map and tank names""" + tank_strs = {} + map_strs = {} + filename = path.join(blitz_app_base, BLITZAPP_STRINGS) + bu.debug('Opening file: ' + filename + ' for reading UserStrings') + try: + async with aiofiles.open(filename, 'r', encoding="utf8") as f: + p_tank = re.compile('^"(#\\w+?_vehicles:.+?)": "(.+)"$') + p_map = re.compile('^"#maps:(.+?):.+?: "(.+?)"$') + + async for l in f: + m = p_tank.match(l) + if m is not None: + tank_strs[m.group(1)] = m.group(2) + + m = p_map.match(l) + if m is not None and m.group(2) != 'Macragge': + map_strs[m.group(1)] = m.group(2) + + except Exception as err: + bu.error(err) + sys.exit(1) - return tank_strs, map_strs - -async def convert_tank_names(tanklist : list, tank_strs: dict) -> dict: - """Convert tank names for Tankopedia""" - tankopedia = {} - userStrs = {} + return tank_strs, map_strs + +async def convert_tank_names(tanklist : list, tank_strs: dict) -> tuple[dict, dict]: + """Convert tank names for Tankopedia""" + tankopedia = {} + userStrs = {} - try: - for tank in tanklist: - tank['name'] = tank_strs[tank['userStr']] - #userStrs[tank['userStr'].split(':')[1]] = tank['name'] - tank.pop('userStr', None) - tank_tmp = collections.OrderedDict() - for key in sorted(tank.keys()): - tank_tmp[key] = tank[key] - tankopedia[str(tank['tank_id'])] = tank_tmp + bu.debug(f'tank_strs:') + for key, value in tank_strs.items(): + bu.debug(f'{key}: {value}') + bu.debug('---------') + try: + for tank in tanklist: + try: + bu.debug(f'tank: {tank}') + if tank['userStr'] in tank_strs: + tank['name'] = tank_strs[tank['userStr']] + else: + tank['name'] = tank['userStr'].split(':')[1] + tank.pop('userStr', None) + tank_tmp = OrderedDict() + for key in sorted(tank.keys()): + tank_tmp[key] = tank[key] + tankopedia[str(tank['tank_id'])] = tank_tmp + except: + bu.error(f'Could not process tank: {tank}') - for tank_str in tank_strs: - skip = False - key = tank_str.split(':')[1] - bu.debug('Tank string: ' + key + ' = ' + tank_strs[tank_str]) - re_strs = [r'^Chassis_', r'^Turret_', r'^_', r'_short$' ] - for re_str in re_strs: - p = re.compile(re_str) - if p.match(key): - skip = True - break - if skip: - continue - - userStrs[key] = tank_strs[tank_str] + for tank_str in tank_strs: + skip = False + key = tank_str.split(':')[1] + bu.debug('Tank string: ' + key + ' = ' + tank_strs[tank_str]) + re_strs = [r'^Chassis_', r'^Turret_', r'^_', r'_short$' ] + for re_str in re_strs: + p = re.compile(re_str) + if p.match(key): + skip = True + break + if skip: + continue + + userStrs[key] = tank_strs[tank_str] - # sorting - tankopedia_sorted = collections.OrderedDict() - for tank_id in sorted(tankopedia.keys(), key=int): - tankopedia_sorted[str(tank_id)] = tankopedia[str(tank_id)] + # sorting + tankopedia_sorted = OrderedDict() + for tank_id in sorted(tankopedia.keys(), key=int): + tankopedia_sorted[str(tank_id)] = tankopedia[str(tank_id)] - userStrs_sorted = collections.OrderedDict() - for userStr in sorted(userStrs.keys()): - userStrs_sorted[userStr] = userStrs[userStr] - # bu.debug('Tank strings: ' + str(len(userStrs_sorted))) + userStrs_sorted = OrderedDict() + for userStr in sorted(userStrs.keys()): + userStrs_sorted[userStr] = userStrs[userStr] + # bu.debug('Tank strings: ' + str(len(userStrs_sorted))) - except Exception as err: - bu.error(err) - sys.exit(1) + except Exception as err: + bu.error(err) + sys.exit(1) - return tankopedia_sorted, userStrs_sorted + return tankopedia_sorted, userStrs_sorted def get_tank_id(nation: str, tank_id : int) -> int: - return (tank_id << 8) + (wg.NATION_ID[nation] << 4) + 1 + assert wg is not None, "wg has to be set" + return (tank_id << 8) + (wg.NATION_ID[nation] << 4) + 1 def get_tank_type(tagstr : str): - tags = tagstr.split(' ') - for t_type in wg.TANK_TYPE: - if tags[0] == t_type: - return t_type - return None + assert wg is not None, "wg has to be set" + tags = tagstr.split(' ') + for t_type in wg.TANK_TYPE: + if tags[0] == t_type: + return t_type + return None ### main() if __name__ == "__main__": - # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp - if sys.platform.startswith("win") and sys.version_info >= (3, 8): - try: - from asyncio import WindowsSelectorEventLoopPolicy - except ImportError: - pass - else: - if not isinstance(asyncio.get_event_loop_policy(), WindowsSelectorEventLoopPolicy): - asyncio.set_event_loop_policy(WindowsSelectorEventLoopPolicy()) - #asyncio.run(main(sys.argv[1:]), debug=True) - asyncio.run(main(sys.argv[1:])) + # To avoid 'Event loop is closed' RuntimeError due to compatibility issue with aiohttp + if sys.platform.startswith("win") and sys.version_info >= (3, 8): + try: + from asyncio import WindowsSelectorEventLoopPolicy + except ImportError: + pass + else: + if not isinstance(get_event_loop_policy(), WindowsSelectorEventLoopPolicy): + set_event_loop_policy(WindowsSelectorEventLoopPolicy()) + #asyncio.run(main(sys.argv[1:]), debug=True) + run(main(sys.argv[1:])) From b273fcd9f341aaab57a04304c6743849c5f3ca20 Mon Sep 17 00:00:00 2001 From: jylpah Date: Sun, 10 Sep 2023 17:36:52 +0300 Subject: [PATCH 5/8] sort_dict(): force input to str --- blitzutils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/blitzutils.py b/blitzutils.py index 729c3c8..5590dc5 100755 --- a/blitzutils.py +++ b/blitzutils.py @@ -684,7 +684,7 @@ def sort_dict(d: dict, number: bool = False) -> dict: if number: return dict(sorted(d.items(), key=lambda item: int(item[0]))) else: - return dict(sorted(d.items(), key=lambda item: item[0])) + return dict(sorted(d.items(), key=lambda item: str(item[0]))) # ----------------------------------------------------------- From b5621991c101f55e3edf37cc72172af4f58d5351 Mon Sep 17 00:00:00 2001 From: jylpah Date: Sun, 10 Sep 2023 17:37:01 +0300 Subject: [PATCH 6/8] ignore .venv --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 74f3f89..ab72423 100755 --- a/.gitignore +++ b/.gitignore @@ -13,3 +13,4 @@ export* *.csv !/stats/*.csv !*.py +/.venv From 859c7aa2931a5e549180fbf2f4e370e1f5704d8f Mon Sep 17 00:00:00 2001 From: jylpah Date: Sun, 10 Sep 2023 17:37:40 +0300 Subject: [PATCH 7/8] update tanks.json --- tanks.json | 96 +++++++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 84 insertions(+), 12 deletions(-) diff --git a/tanks.json b/tanks.json index 27866ae..1964f1d 100644 --- a/tanks.json +++ b/tanks.json @@ -790,7 +790,7 @@ }, "3377": { "is_premium": false, - "name": "Ch16_WZ_131", + "name": "WZ-131", "nation": "china", "tank_id": 3377, "tier": 7, @@ -1110,7 +1110,7 @@ }, "4913": { "is_premium": false, - "name": "Ch15_59_16", + "name": "59-16", "nation": "china", "tank_id": 4913, "tier": 6, @@ -2262,7 +2262,7 @@ }, "10033": { "is_premium": false, - "name": "Ch28_WZ_132A", + "name": "WZ-132A", "nation": "china", "tank_id": 10033, "tier": 9, @@ -2414,7 +2414,7 @@ }, "10801": { "is_premium": true, - "name": "Ch34_WZ-131G_FT_BP", + "name": "Panlong", "nation": "china", "tank_id": 10801, "tier": 6, @@ -2678,7 +2678,7 @@ }, "13185": { "is_premium": true, - "name": "Cz17_Vz_55", + "name": "Vz. 55", "nation": "european", "tank_id": 13185, "tier": 10, @@ -3350,7 +3350,7 @@ }, "20033": { "is_premium": true, - "name": "F114_Projet_4_1", + "name": "Char Futur 4", "nation": "france", "tank_id": 20033, "tier": 9, @@ -3918,7 +3918,7 @@ }, "25617": { "is_premium": true, - "name": "G140_HWK_30", + "name": "HWK 30", "nation": "germany", "tank_id": 25617, "tier": 8, @@ -3950,7 +3950,7 @@ }, "26113": { "is_premium": true, - "name": "R181_Object_452K", + "name": "Object 452K", "nation": "ussr", "tank_id": 26113, "tier": 9, @@ -3958,7 +3958,7 @@ }, "26129": { "is_premium": true, - "name": "Nashorn_BP", + "name": "Epsilon", "nation": "germany", "tank_id": 26129, "tier": 6, @@ -4022,7 +4022,7 @@ }, "27937": { "is_premium": true, - "name": "T49_Fearless", + "name": "T49 Fearless", "nation": "usa", "tank_id": 27937, "tier": 8, @@ -4030,7 +4030,7 @@ }, "28449": { "is_premium": true, - "name": "A136_T42", + "name": "T42", "nation": "usa", "tank_id": 28449, "tier": 7, @@ -4757,6 +4757,8 @@ "A100_T49": "T49", "A100_T49_Custom": "T49 Hornet", "A100_T49_Custom_short": "T49 Hornet", + "A100_T49_Custom_short_v2": "T49 Guanlong", + "A100_T49_Custom_v2": "T49 Guanlong", "A100_T49_short": "T49", "A102_T28_concept": "T28 Concept", "A102_T28_concept_Custom": "T28 HTC Independence", @@ -4807,6 +4809,8 @@ "A132_T77_Custom": "T77 Cannonball", "A132_T77_Custom_short": "T77 Cannonball", "A132_T77_short": "T77", + "A136_T42": "T42", + "A136_T42_short": "T42", "A140_ASTRON_REX_105": "ASTRON Rex 105 mm", "A140_ASTRON_REX_105_Custom": "ASTRON Rex Ayame", "A140_ASTRON_REX_105_Custom_short": "ASTRON Rex Ayame", @@ -4818,6 +4822,8 @@ "A142_Pawlack_Tank": "Pawlack Tank", "A142_Pawlack_Tank_short": "Pawlack Tank", "A146_TL_7_120": "TL-7-120", + "A146_TL_7_120_Custom": "TL-7-120 Arachnid", + "A146_TL_7_120_Custom_short": "TL-7-120 Arachnid", "A146_TL_7_120_short": "TL-7-120", "A68_T28D": "T28 Defender", "A68_T28D_Custom": "Defender D-Gunner", @@ -4952,6 +4958,8 @@ "Bat_Chatillon25t": "Bat.-Châtillon 25 t", "Bat_Chatillon25t_Custom": "Bat.-Châtillon 25 t Musketeer", "Bat_Chatillon25t_Custom_short": "B-C 25 t Musketeer", + "Bat_Chatillon25t_Custom_short_v2": "B-C 25 t Runner", + "Bat_Chatillon25t_Custom_v2": "B-C 25 t Runner", "Bat_Chatillon25t_short": "B-C 25 t", "Bedford_Twin-Six": "Bedford Twin-Six", "Berliet_MDP": "Berliet MDP", @@ -4965,6 +4973,7 @@ "CH38_WZ113G_FT_Custom": "WZ-113G FT Lucky", "CH38_WZ113G_FT_Custom_short": "WZ-113G FT Lucky", "CKD_AXK": "ČKD AXK", + "CKD_AXL": "ČKD AXL", "Cadillac_V8": "3xCadillacV8", "Cannone_da_105_51_M68": "Cannone da 105/51 M68", "Cannone_da_105_51_M68_short": "Cannone da 105/51 M68", @@ -5006,10 +5015,12 @@ "Ch12_111_1_2_3_short": "WZ-111 1-4", "Ch14_T34_3": "T-34-3", "Ch14_T34_3_short": "T-34-3", + "Ch15_59_16": "59-16", "Ch15_59_16_BP": "Wind", "Ch15_59_16_BP_Custom": "Free Wind", "Ch15_59_16_BP_Custom_short": "Free Wind", "Ch15_59_16_BP_short": "Wind", + "Ch16_WZ_131": "WZ-131", "Ch17_WZ131_1_WZ132": "WZ-132", "Ch18_WZ-120": "WZ-120", "Ch19_121": "WZ-121", @@ -5031,9 +5042,17 @@ "Ch26_59_Patton_Custom": "59-Patton Aratinga", "Ch26_59_Patton_Custom_short": "59-Patton Aratinga", "Ch26_59_Patton_short": "59-Patton", + "Ch28_WZ_132A": "WZ-132A", + "Ch28_WZ_132A_short": "WZ-132A", "Ch29_Type_62C_prot": "WZ-132-1", + "Ch29_Type_62C_prot_Custom": "WZ-132-1 Futuro", + "Ch29_Type_62C_prot_Custom_short": "WZ-132-1 Futuro", "Ch29_Type_62C_prot_short": "WZ-132-1", "Ch34_WZ-131G_FT": "WZ-131G FT", + "Ch34_WZ-131G_FT_BP": "Panlong", + "Ch34_WZ-131G_FT_BP_Custom": "Panlong Immortal", + "Ch34_WZ-131G_FT_BP_Custom_short": "Panlong Immortal", + "Ch34_WZ-131G_FT_BP_short": "Panlong", "Ch34_WZ-131G_FT_short": "WZ-131G FT", "Ch35_T-34-2G_FT": "T-34-2G FT", "Ch35_T-34-2G_FT_short": "T-34-2G FT", @@ -5069,6 +5088,12 @@ "Ch_WZ_112v2_Custom_short": "WZ-112-2 Celestial", "Chassis1_A140_ASTRON_REX_105": "Rex", "Chassis1_A140_ASTRON_REX_105_short": "Rex", + "Chassis1_Ch16_WZ_131": "WZ-131", + "Chassis1_Ch16_WZ_131_short": "WZ-131", + "Chassis1_Ch17_WZ131_1_WZ132": "WZ-131-1", + "Chassis1_Ch17_WZ131_1_WZ132_short": "WZ-131-1", + "Chassis1_Ch28_WZ_132A": "WZ-132A", + "Chassis1_Ch28_WZ_132A_short": "WZ-132A", "Chassis1_Ch29_Type_62C_prot": "WZ-132-1", "Chassis1_Ch29_Type_62C_prot_short": "WZ-132-1", "Chassis1_Ch45_WZ_114": "WZ-114", @@ -5105,6 +5130,12 @@ "Chassis1_S31_Strv_K_short": "Stridsvagn KRV", "Chassis1_S32_Bofors_Tornvagn": "Bofors Tornvagn", "Chassis1_S32_Bofors_Tornvagn_short": "Tornvagn", + "Chassis2_Ch16_WZ_131": "WZ-131 model 2", + "Chassis2_Ch16_WZ_131_short": "WZ-131 model 2", + "Chassis2_Ch17_WZ131_1_WZ132": "WZ-132", + "Chassis2_Ch17_WZ131_1_WZ132_short": "WZ-132", + "Chassis2_Ch28_WZ_132A": "WZ-132B", + "Chassis2_Ch28_WZ_132A_short": "WZ-132B", "Chassis2_It15_Semovente_M41": "Semovente M42", "Chassis2_It15_Semovente_M41_short": "Semovente M42", "Chassis2_It16_Bassoto": "Semovente M44", @@ -5152,12 +5183,14 @@ "Continental_AOS-895-1": "Continental AOS-895-1", "Continental_AOS-895-1_short": "Continental\\nAOS-895-1", "Continental_AOS-895-3": "AN/GRC-8", + "Continental_AOS-895-3M": "Continental AOS-895-3M", "Continental_AOS-895-3V2": "Continental AOS-895-3-2", "Continental_AOS-895-3_short": "AN/GRC-8", "Continental_AOSI-1195-1": "Continental AOSI 1195-1", "Continental_AOSI-1195-1A": "Continental AOSI-1195-1A", "Continental_AOSI-1195-5": "Continental AOSI-1195-5", "Continental_AOSI-895-5": "Continental AOSI-895-5", + "Continental_AOSI-895-5FR": "Continental AOSI-895-5", "Continental_AOSI-895-5_short": "Continental\\nAOSI-895-5", "Continental_AOSI_1195_5A": "Continental AOSI-1195-5 Y IV", "Continental_AOSI_895_5_Prem": "Continental AOSI-895-5", @@ -5238,6 +5271,8 @@ "Cz15_Skoda_T-45_Custom": "Škoda T 45 Skvělý", "Cz15_Skoda_T-45_Custom_short": "Škoda T 45 Skvělý", "Cz15_Skoda_T-45_short": "Škoda T 45", + "Cz17_Vz_55": "Vz. 55", + "Cz17_Vz_55_short": "Vz. 55", "D-744": "D-744", "D1": "D1", "D1_short": "D1", @@ -5294,12 +5329,15 @@ "E50_Ausf_M_short": "E 50 M", "ELC_AMX": "AMX ELC bis", "ELC_AMX_short": "AMX ELC bis", + "E_9": "E-9", "Engine_1_test": "#germany_vehicles:Engine_1_test", "Engine_1_test_short": "#germany_vehicles:Engine_1_test_short", "F01_Vickers_MkF": "Vickers Mk. F", "F01_Vickers_MkF_short": "Vickers Mk. F", "F113_PzV_FR": "Bretagne Panther", "F113_PzV_FR_short": "Bretagne Panther", + "F114_Projet_4_1": "Char Futur 4", + "F114_Projet_4_1_short": "Char Futur 4", "F116_Bat_Chat_Bourrasque": "Bat.-Châtillon Bourrasque", "F116_Bat_Chat_Bourrasque_Custom": "Bourrasque Noir", "F116_Bat_Chat_Bourrasque_Custom_short": "Bourrasque Noir", @@ -5394,6 +5432,8 @@ "G100_Gtraktor_Krupp": "Großtraktor - Krupp", "G100_Gtraktor_Krupp_short": "Großtraktor", "G103_RU_251": "Spähpanzer Ru 251", + "G103_RU_251_Custom": "Ru 251 Breakneck", + "G103_RU_251_Custom_short": "Ru 251 Breakneck", "G103_RU_251_short": "Ru 251", "G106_PzKpfwPanther_AusfF": "Panther mit 8,8 cm L/71", "G106_PzKpfwPanther_AusfF_short": "Panther 8,8", @@ -5432,6 +5472,8 @@ "G136_Tiger_131_short": "Tiger 131", "G138_VK16801P": "VK 168.01 (P)", "G138_VK16801P_short": "VK 168.01 (P)", + "G140_HWK_30": "HWK 30", + "G140_HWK_30_short": "HWK 30", "G142_M48RPz": "M48A2 Räumpanzer", "G142_M48RPz_Custom": "Räumpanzer Rhino", "G142_M48RPz_Custom_short": "Räumpanzer Rhino", @@ -5921,6 +5963,8 @@ "Leopard1": "Leopard 1", "Leopard1_Custom": "Leopard 1 Wild", "Leopard1_Custom_short": "Leopard 1 Wild", + "Leopard1_Custom_short_v2": "Leopard 1 Irbis", + "Leopard1_Custom_v2": "Leopard 1 Irbis", "Leopard1_short": "Leopard 1", "Leopardketten_Ausf_A": "VK 16.02 Leopard", "Leopardketten_Ausf_B": "VK 16.02 Leopard verstärkteketten", @@ -5932,7 +5976,9 @@ "Licoming_0-435t": "Lycoming O-435T", "Lowe": "Löwe", "Lowe_Custom": "Löwe Reliant", + "Lowe_Custom_2": "Löwe Colossus", "Lowe_Custom_short": "Löwe Reliant", + "Lowe_Custom_short_2": "Löwe Colossus", "Ltraktor": "Leichttraktor", "Ltraktor_short": "L.Tr.", "Luchsketten_Ausf_A": "Luchs", @@ -5958,9 +6004,13 @@ "M-5-Y_short": "M-V-Yoh", "M-50T": "M-50T", "M-50TD": "M-50TD", + "M-50TV2": "M-50TV2", + "M-50TV2K": "M-50TV2K", "M-6-Y": "M-VI-Yoh", "M-6-Y_Custom": "M-VI-Yoh Pride", "M-6-Y_Custom_short": "M-VI-Yoh Pride", + "M-6-Y_Custom_short_v2": "M-VI-Yoh Ankylosaurus", + "M-6-Y_Custom_v2": "M-VI-Yoh Ankylosaurus", "M-6-Y_short": "M-VI-Yoh", "M-7-Y": "M-VII-Yoh", "M-7-Y_short": "M-VII-Yoh", @@ -6065,6 +6115,7 @@ "MB-837-Ea-600": "MB 837 Ea 600", "MB-837A": "MTU MB 837 A", "MB-837A_short": "MB 837 A", + "MB-837M": "MB 837 M", "MB-838_CAM-500": "MTU MB 838 CaM 500", "MB-838_CAM-500_1": "MTU MB 838 CaM 500A", "MB-838_CAM-500_1_short": "MB 838 CaM 500A", @@ -6132,6 +6183,7 @@ "Maybach_HL_120_TRM_BT": "Maybach HL 120 TRM", "Maybach_HL_120_TRM_C": "Maybach HL 120 TRM V12", "Maybach_HL_120_TRM_R": "Maybach HL 120 TRM R", + "Maybach_HL_120_TRM_RBP": "Maybach HL 120 TRM R", "Maybach_HL_120_TR_A": "Maybach HL 120 TRM 112", "Maybach_HL_120_TR_R": "Maybach HL 120 TR R", "Maybach_HL_120_WH": "Dark Soul", @@ -6153,6 +6205,7 @@ "Maybach_HL_210_TRM_P30": "Maybach HL 210 TRM P30", "Maybach_HL_210_TRM_P30_A": "Maybach HL 210 TRM P30", "Maybach_HL_210_TRM_P30_short": "Maybach\\nHL 210 TRM P30", + "Maybach_HL_210_TRM_P30v2": "Maybach HL 210 TRM P30 V2", "Maybach_HL_210_TRM_P45": "Maybach HL 210 TRM P45", "Maybach_HL_210_TRM_P45TU": "Maybach HL 210 TRM P45", "Maybach_HL_210_TRM_P45_BP": "Maybach HL 210 TRM P45 B", @@ -6167,8 +6220,10 @@ "Maybach_HL_230P45_short": "Maybach HL 230\\nP45F", "Maybach_HL_230_F": "Maybach HL 230 F", "Maybach_HL_230_TRM_P30": "Maybach HL 230 TRM P30 pl.", + "Maybach_HL_230_TRM_P30V2": "Maybach HL 230 TRM P30 V2", "Maybach_HL_230_TRM_P30_FR": "Maybach HL 230 TRM P30 fr.", "Maybach_HL_230_TRM_P30_short": "Maybach\\nHL 230 TRM P30", + "Maybach_HL_230_TRM_P30v3": "Maybach HL 230 TRM P30 V3", "Maybach_HL_230_TRM_P45": "Maybach HL 230 TRM P45", "Maybach_HL_230_TRM_P45A": "Maybach HL 230 TRM P45 A", "Maybach_HL_230_TRM_P45_short": "Maybach HL 230\\nTRM P45", @@ -6218,6 +6273,10 @@ "NVSS_T66": "HVSS T66", "NVSS_T80": "HVSS T80", "Nashorn": "Nashorn", + "Nashorn_BP": "Epsilon", + "Nashorn_BP_Custom": "Epsilon Sagittarius", + "Nashorn_BP_Custom_short": "Epsilon Sagittarius", + "Nashorn_BP_short": "Epsilon", "Nashorn_short": "Nashorn", "Nuffield_liberty_MKII": "Nuffield Liberty Mk. II", "Nuffield_liberty_MKIV": "Nuffield Liberty Mk. III", @@ -6272,7 +6331,6 @@ "Oe_mle._1932A": "HE Shell", "Ordnance_QF_2pdr_AT_Gun_Mk.X": "QF Mk. IX", "Ordnance_QF_2pdr_AT_Gun_Mk.X_short": "QF Mk. IX", - "Oth06_Sega_Lupus": "Lupus", "Oth08_WH_Vindicator": "Vindicator Ultramarines", "Oth08_WH_Vindicator_Dark_Angels_Custom": "Vindicator Dark Angels", "Oth08_WH_Vindicator_Dark_Angels_Custom_short": "Vindicator DA", @@ -6645,6 +6703,8 @@ "R180_Object_274A_Custom": "Object 274a Leshy", "R180_Object_274A_Custom_short": "Object 274a Leshy", "R180_Object_274A_short": "Object 274a", + "R181_Object_452K": "Object 452K", + "R181_Object_452K_short": "Object 452K", "R43_T-70_57": "T-70/57", "R43_T-70_57_short": "T-70/57", "R63_ST_IBD": "T-2020", @@ -6689,6 +6749,8 @@ "Renault_T14": "Renault T14", "Renault_V4": "Renault V4", "RhB_Waffentrager": "Rhm.-Borsig Waffenträger", + "RhB_Waffentrager_Custom": "Rhm.-B. WT Tarnkappe", + "RhB_Waffentrager_Custom_short": "Rhm.-B. WT Tarnkappe", "RhB_Waffentrager_short": "Rhm.-B. WT", "Rolls_Royce_Griffon": "Rolls-Royce Griffon", "Rolls_Royce_Griffon-Mk2": "Rolls-Royce Griffon A", @@ -6752,6 +6814,8 @@ "S31_Strv_K_Custom_short": "Strv K Konung", "S31_Strv_K_short": "Strv K", "S32_Bofors_Tornvagn": "Bofors Tornvagn", + "S32_Bofors_Tornvagn_Custom": "Tornvagn Stegosaurus", + "S32_Bofors_Tornvagn_Custom_short": "Tornvagn Stegosaurus", "S32_Bofors_Tornvagn_short": "Tornvagn", "S35_captured": "Pz.Kpfw. S35 739 (f)", "S35_captured_short": "Pz. S35", @@ -6779,6 +6843,8 @@ "ST_B1": "STB-1", "ST_B1_Custom": "STB-1 Adamant", "ST_B1_Custom_short": "STB-1 Adamant", + "ST_B1_Custom_short_v2": "STB-1 Spinosaurus", + "ST_B1_Custom_v2": "STB-1 Spinosaurus", "ST_B1_short": "STB-1", "ST_I": "ST-I", "SU-100": "SU-100", @@ -7000,6 +7066,8 @@ "T34_hvy": "T34", "T34_hvy_Custom": "T34 Falcon", "T34_hvy_Custom_short": "T34 Falcon", + "T34_hvy_Custom_short_v2": "T34 H-Combat", + "T34_hvy_Custom_v2": "T34 H-Combat", "T34_hvy_Freedom": "T34 Independence", "T34_hvy_Freedom_short": "T34 (1776)", "T37": "T37", @@ -7009,6 +7077,8 @@ "T42": "T42", "T48": "T48", "T49": "T67", + "T49_Fearless": "T49 Fearless", + "T49_Fearless_short": "T49 Fearless", "T49_Rocket": "T49 ATM", "T49_Rocket_short": "T49 A", "T51D1": "T51D1", @@ -7045,6 +7115,8 @@ "T95": "T95", "T95E2": "T95E2", "T95E2_short": "T95E2", + "T95_Custom": "T95 Turtle", + "T95_Custom_short": "T95 Turtle", "T99E2": "T99E2", "TD-12": "TD-12", "TD-12S": "TD-12S", From b5efc4bc9d8b1a70fd83658898136586da57520b Mon Sep 17 00:00:00 2001 From: jylpah Date: Sun, 10 Sep 2023 17:47:08 +0300 Subject: [PATCH 8/8] commit mix fixes --- extract_tankopedia.py | 5 ++--- maps.json | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/extract_tankopedia.py b/extract_tankopedia.py index c5057d3..01330b3 100755 --- a/extract_tankopedia.py +++ b/extract_tankopedia.py @@ -25,7 +25,7 @@ BLITZAPP_VEHICLE_FILE='list.xml' wg : WG | None = None ## main() ------------------------------------------------------------- -async def main(argv: list[str]) -> None: +async def main() -> None: global wg # set the directory for the script os.chdir(os.path.dirname(sys.argv[0])) @@ -245,5 +245,4 @@ if __name__ == "__main__": else: if not isinstance(get_event_loop_policy(), WindowsSelectorEventLoopPolicy): set_event_loop_policy(WindowsSelectorEventLoopPolicy()) - #asyncio.run(main(sys.argv[1:]), debug=True) - run(main(sys.argv[1:])) + run(main()) diff --git a/maps.json b/maps.json index 146ad11..046e9ac 100644 --- a/maps.json +++ b/maps.json @@ -57,4 +57,4 @@ "skit": "Naval Frontier", "test": "World of Ducks", "tutorial": "Proving Grounds" -} \ No newline at end of file +}