-
Notifications
You must be signed in to change notification settings - Fork 8
/
Copy patheve_db_tools.py
2745 lines (2384 loc) · 146 KB
/
eve_db_tools.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
"""
get_db_connection - подключается к БД
auth_pilot_by_name - подключается к ESI Swagger Interface
actualize_xxx - загружает с серверов CCP xxx-данные, например actualize_character,
и сохраняет полученные данные в БД, если требуется
"""
import sys
import requests
import typing
import datetime
import pytz
import math
import eve_esi_interface as esi
import postgresql_interface as db
from __init__ import __version__
# не используй здесь ни настройки q_industrialist_settings, ни настройки из q_individualist_settings
def is_dicts_equal_by_keys(dict1, dict2, keys):
for key in keys:
if key in dict1:
if not (key in dict2):
return False
else:
if not (key in dict2):
continue
else:
return False
x = dict1[key]
y = dict2[key]
if isinstance(x, float) or isinstance(y, float):
# 5.0 и 4.99 - False (разные), а 5.0 и 4.999 - True (одинаковые)
same: bool = math.isclose(x, y, abs_tol=0.00999)
if not same:
return False
elif x != y:
return False
return True
class QEntity:
def __init__(self, db, esi, obj, at):
""" данные объекта, хранящеося в индексированном справочнике в памяти
:param db: признак того, что данные имеются в БД (известно, что объект с таким id есть в БД)
:param esi: признак того, что данные получены с серверов CCP
:param obj: данные, м.б. None, если не загружены ни с сервера, ни из БД
:param ext: данные, которые опосредовано связаны с объектом (не загружены с сервера CCP)
:param at: дата/время последней актуализации кешированных данных (хранящихся в БД)
"""
self.db: bool = db
self.esi: bool = esi
self.obj: typing.Any = obj
self.ext = None
self.at: datetime.datetime = at
def store(self, db, esi, obj, at):
self.db: bool = db
self.esi: bool = esi
self.obj: typing.Any = obj
self.at: datetime.datetime = at
def store_ext(self, ext: dict):
if self.ext:
self.ext.update(ext)
else:
self.ext = ext
def compare_ext(self, key: str, val) -> bool:
if val is not None:
if not self.ext or (self.ext.get(key) != val):
return False
else:
if self.ext and (self.ext.get(key) is not None):
return False
return True
def is_obj_equal(self, data):
for key in self.obj:
if not (key in data):
return False
elif (data[key] != self.obj[key]):
return False
return True
def is_obj_equal_by_keys(self, data, keys):
return is_dicts_equal_by_keys(self.obj, data, keys)
class QEntityDepth:
def __init__(self):
""" сведения о вложенности данных для отслеживания глубины спуска по зависимостям, с тем, чтобы
не сталкиваться с ситуацией, когда загрузка данных о корпорации приводит к загрузке данных о
домашке корпорации, которая в свою очередь снова может привести к загрузке данных о корпорации
"""
self.urls: typing.List[str] = []
def push(self, url: str):
if url in self.urls:
return False
self.urls.append(url)
return True
def pop(self):
self.urls.pop()
class QDatabaseTools:
character_timedelta = datetime.timedelta(days=3)
corporation_timedelta = datetime.timedelta(days=5)
universe_station_timedelta = datetime.timedelta(days=7)
universe_structure_timedelta = datetime.timedelta(days=3)
corporation_structure_diff = ['corporation_id', 'profile_id']
corporation_asset_diff = ['quantity', 'location_id', 'location_type', 'location_flag', 'is_singleton']
corporation_blueprint_diff = ['type_id', 'location_id', 'location_flag', 'quantity', 'time_efficiency',
'material_efficiency', 'runs']
character_blueprint_diff = ['type_id', 'location_id', 'location_flag', 'quantity', 'time_efficiency',
'material_efficiency', 'runs']
corporation_industry_job_diff = ['status']
character_industry_job_diff = ['status']
market_order_diff = ['price', 'volume_remain']
markets_prices_avg_diff = ['average_price']
markets_prices_adj_diff = ['adjusted_price']
industry_systems_diff = ['manufacturing', 'research_te', 'research_me', 'copying', 'invention', 'reaction']
def __init__(self, module_name, client_scope, database_prms, debug):
""" constructor
:param module_name: name of Q.Industrialist' module
:param debug: debug mode to show SQL queries
"""
self.qidb = db.QIndustrialistDatabase(module_name, debug=debug)
self.qidb.connect(database_prms)
self.dbswagger = db.QSwaggerInterface(self.qidb)
self.esiswagger = None
self.eve_now = datetime.datetime.utcnow().replace(tzinfo=pytz.UTC)
self.__client_scope = client_scope
self.__cached_characters: typing.Dict[int, QEntity] = {}
self.__cached_corporations: typing.Dict[int, QEntity] = {}
self.__cached_stations: typing.Dict[int, QEntity] = {}
self.__cached_structures: typing.Dict[int, QEntity] = {}
self.__cached_corporation_structures: typing.Dict[int, QEntity] = {}
self.__cached_corporation_assets: typing.Dict[int, typing.Dict[int, QEntity]] = {}
self.__cached_corporation_assets_names: typing.Dict[int, typing.Dict[int, str]] = {}
self.__cached_corporation_blueprints: typing.Dict[int, typing.Dict[int, QEntity]] = {}
self.__cached_character_blueprints: typing.Dict[int, typing.Dict[int, QEntity]] = {}
self.__cached_corporation_industry_jobs: typing.Dict[int, typing.Dict[int, QEntity]] = {}
self.__cached_character_industry_jobs: typing.Dict[int, typing.Dict[int, QEntity]] = {}
self.__cached_corporation_orders: typing.Dict[int, typing.Dict[int, QEntity]] = {}
self.__cached_markets_avg_prices: typing.Dict[int, QEntity] = {}
self.__cached_markets_adj_prices: typing.Dict[int, QEntity] = {}
self.__cached_category_ids: typing.Set[int] = set()
self.__cached_group_ids: typing.Set[int] = set()
self.__cached_market_group_ids: typing.Set[int] = set()
self.__cached_type_ids: typing.Set[int] = set()
self.__cached_industry_systems: typing.Dict[int, QEntity] = {}
self.__universe_items_with_names: typing.Set[int] = set()
self.prepare_cache()
self.active_market_hubs: typing.List[db.QSwaggerInterface.MarketHub] = list()
self.prepare_settings()
self.depth = QEntityDepth()
def __del__(self):
""" destructor
"""
del self.depth
del self.active_market_hubs
del self.__universe_items_with_names
del self.__cached_industry_systems
del self.__cached_type_ids
del self.__cached_market_group_ids
del self.__cached_group_ids
del self.__cached_category_ids
del self.__cached_markets_adj_prices
del self.__cached_markets_avg_prices
del self.__cached_corporation_orders
del self.__cached_character_industry_jobs
del self.__cached_corporation_industry_jobs
del self.__cached_character_blueprints
del self.__cached_corporation_blueprints
del self.__cached_corporation_assets_names
del self.__cached_corporation_assets
del self.__cached_corporation_structures
del self.__cached_structures
del self.__cached_stations
del self.__cached_corporations
del self.__cached_characters
del self.esiswagger
del self.dbswagger
self.qidb.commit()
self.qidb.disconnect()
del self.qidb
def auth_pilot_by_name(
self,
pilot_name,
offline_mode: bool,
cache_files_dir: str,
client_id: str,
client_restrict_tls13: bool):
assert client_id is not None
# настройка Eve Online ESI Swagger interface
auth = esi.EveESIAuth(
'{}/auth_cache'.format(cache_files_dir),
debug=True)
client = esi.EveESIClient(
auth,
client_id,
keep_alive=True,
debug=False,
logger=True,
user_agent='Q.Industrialist v{ver}'.format(ver=__version__),
restrict_tls13=client_restrict_tls13)
self.esiswagger = esi.EveOnlineInterface(
client,
self.__client_scope,
cache_dir='{}/esi_cache'.format(cache_files_dir),
offline_mode=offline_mode)
authz = self.esiswagger.authenticate(pilot_name, client_id)
# character_id = authz["character_id"]
# character_name = authz["character_name"]
return authz
# -------------------------------------------------------------------------
# c a c h e
# -------------------------------------------------------------------------
def prepare_cache(self):
rows = self.dbswagger.get_exist_character_ids()
for row in rows:
self.__cached_characters[row[0]] = QEntity(True, False, None, row[1].replace(tzinfo=pytz.UTC))
rows = self.dbswagger.get_exist_corporation_ids()
for row in rows:
self.__cached_corporations[row[0]] = QEntity(True, False, None, row[1].replace(tzinfo=pytz.UTC))
rows = self.dbswagger.get_exist_universe_station_ids()
for row in rows:
self.__cached_stations[row[0]] = QEntity(True, False, None, row[1].replace(tzinfo=pytz.UTC))
rows = self.dbswagger.get_exist_universe_structure_ids()
for row in rows:
self.__cached_structures[row[0]] = QEntity(True, False, None, row[1].replace(tzinfo=pytz.UTC))
rows = self.dbswagger.get_exist_corporation_structures()
for row in rows:
structure_id = row['structure_id']
updated_at = row['ext']['updated_at'].replace(tzinfo=pytz.UTC)
del row['ext']
self.__cached_corporation_structures[structure_id] = QEntity(True, False, row, updated_at)
self.prepare_corp_cache(
self.dbswagger.get_exist_corporation_assets(),
self.__cached_corporation_assets,
'item_id',
None
)
rows = self.dbswagger.get_exist_corporation_assets_names()
if rows:
prev_corporation_id = None
corporation_cache = None
for row in rows:
item_id: int = row[0]
corporation_id: int = row[1]
if prev_corporation_id != corporation_id:
corporation_cache = self.get_corp_cache(self.__cached_corporation_assets_names, corporation_id)
prev_corporation_id = corporation_id
corporation_cache[item_id] = row[2]
rows: typing.Optional[typing.Tuple[typing.List[typing.Any], typing.List[typing.Any]]] = \
self.dbswagger.get_last_known_markets_prices()
if rows is not None:
avg_rows, adj_rows = rows
if avg_rows:
for row in avg_rows:
type_id = row['type_id']
updated_at = row['ext']['updated_at'].replace(tzinfo=pytz.UTC)
del row['ext']
self.__cached_markets_avg_prices[type_id] = QEntity(True, False, row, updated_at)
if adj_rows:
for row in adj_rows:
type_id = row['type_id']
updated_at = row['ext']['updated_at'].replace(tzinfo=pytz.UTC)
del row['ext']
self.__cached_markets_adj_prices[type_id] = QEntity(True, False, row, updated_at)
rows = self.dbswagger.get_industry_systems()
if rows:
for row in rows:
solar_system_id = row['system_id']
updated_at = row['ext']['updated_at'].replace(tzinfo=pytz.UTC)
del row['ext']
self.__cached_industry_systems[solar_system_id] = QEntity(True, False, row, updated_at)
# загрузка из БД type_ids, market_group_ids, group_ids, category_ids (в таблицах в БД таких данных может быть
# больше, чем может выдать ESI, т.к. БД может хранить устаревшие non published товары)
self.prepare_goods_dictionaries()
def prepare_goods_dictionaries(self):
rows = self.dbswagger.get_exist_category_ids()
if rows:
self.__cached_category_ids: typing.Set[int] = set([row[0] for row in rows])
rows = self.dbswagger.get_exist_group_ids()
if rows:
self.__cached_group_ids: typing.Set[int] = set([row[0] for row in rows])
rows = self.dbswagger.get_exist_market_group_ids()
if rows:
self.__cached_market_group_ids: typing.Set[int] = set([row[0] for row in rows])
rows = self.dbswagger.get_exist_type_ids()
if rows:
self.__cached_type_ids: typing.Set[int] = set([row[0] for row in rows])
rows = self.dbswagger.get_universe_items_with_names()
if rows:
self.__universe_items_with_names: typing.Set[int] = set([row[0] for row in rows])
@staticmethod
def get_corp_cache(cache, corporation_id):
corp_cache = cache.get(corporation_id)
if not corp_cache:
cache[corporation_id] = {}
corp_cache = cache.get(corporation_id)
return corp_cache
def prepare_corp_cache(self, rows, cache, row_key, datetime_keys):
for row in rows:
row_id: int = int(row[row_key])
ext = row['ext']
updated_at = ext['updated_at'].replace(tzinfo=pytz.UTC)
corporation_id: int = int(ext['corporation_id'])
del ext['updated_at']
del ext['corporation_id']
del row['ext']
if datetime_keys:
for dtkey in datetime_keys:
if dtkey in row:
row[dtkey].replace(tzinfo=pytz.UTC)
corp_cache = self.get_corp_cache(cache, corporation_id)
corp_cache[row_id] = QEntity(True, False, row, updated_at)
if ext == {}:
del ext
else:
corp_cache[row_id].store_ext(ext)
def get_cache_status(self, cache, data, data_key, filter_val=None, filter_key=None, debug=False):
# список элементов (ассетов или структур) имеющихся у корпорации
ids_from_esi: typing.List[int] = [int(s[data_key]) for s in data]
if not ids_from_esi:
return None, None, None, None
# кешированный, м.б. устаревший список корпоративных элементов
if not cache:
ids_in_cache: typing.List[int] = []
new_ids: typing.List[int] = [id for id in ids_from_esi]
deleted_ids: typing.List[int] = []
else:
if filter_key and filter_val:
ids_in_cache: typing.List[int] = [id for id in cache.keys() if cache[id].obj[filter_key] == filter_val]
else:
ids_in_cache: typing.List[int] = [id for id in cache.keys()]
# список элементов, появившихся у корпорации и отсутствующих в кеше (в базе данных)
new_ids: typing.List[int] = [id for id in ids_from_esi if not cache.get(id)]
# список корпоративных элементов, которых больше нет у корпорации (кеш устарел и база данных устарела)
deleted_ids: typing.List[int] = [id for id in ids_in_cache if not (id in ids_from_esi)]
if debug:
print(' == ids_from_esi', ids_from_esi)
print(' == ids_in_cache', ids_in_cache)
print(' == new_ids ', new_ids)
print(' == deleted_ids ', deleted_ids)
return ids_from_esi, ids_in_cache, new_ids, deleted_ids
@staticmethod
def get_pers_cache(cache, character_id):
pers_cache = cache.get(character_id)
if not pers_cache:
cache[character_id] = {}
pers_cache = cache.get(character_id)
return pers_cache
def prepare_pers_cache(self, rows, cache, row_key, datetime_keys):
for row in rows:
row_id: int = int(row[row_key])
ext = row['ext']
updated_at = ext['updated_at'].replace(tzinfo=pytz.UTC)
character_id: int = int(ext['character_id'])
del ext['updated_at']
del ext['character_id']
del row['ext']
if datetime_keys:
for dtkey in datetime_keys:
if dtkey in row:
row[dtkey].replace(tzinfo=pytz.UTC)
pers_cache = self.get_pers_cache(cache, character_id)
pers_cache[row_id] = QEntity(True, False, row, updated_at)
if ext == {}:
del ext
else:
pers_cache[row_id].store_ext(ext)
# -------------------------------------------------------------------------
# s e t t i n g s
# -------------------------------------------------------------------------
def prepare_settings(self):
self.active_market_hubs = self.dbswagger.get_active_market_hubs()
# -------------------------------------------------------------------------
# e v e s w a g g e r i n t e r f a c e
# -------------------------------------------------------------------------
def load_from_esi(self, url: str, fully_trust_cache=False, body=None):
data = self.esiswagger.get_esi_data(
url,
fully_trust_cache=fully_trust_cache)
updated_at = self.esiswagger.last_modified
is_updated = self.esiswagger.is_last_data_updated
return data, updated_at, is_updated
def load_from_esi_piece_data(self, url: str, body: typing.List[int], fully_trust_cache=False):
data = self.esiswagger.get_esi_piece_data(
url,
body,
fully_trust_cache=fully_trust_cache)
updated_at = self.esiswagger.last_modified
is_updated = self.esiswagger.is_last_data_updated
return data, updated_at, is_updated
def load_from_esi_paged_data(self, url: str, fully_trust_cache=False):
data = self.esiswagger.get_esi_paged_data(
url,
fully_trust_cache=fully_trust_cache)
updated_at = self.esiswagger.last_modified
is_updated = self.esiswagger.is_last_data_updated
return data, updated_at, is_updated
# -------------------------------------------------------------------------
# characters/{character_id}/
# -------------------------------------------------------------------------
@staticmethod
def get_character_url(character_id: int) -> str:
# Public information about a character
return "characters/{character_id}/".format(character_id=character_id)
def actualize_character_details(self, character_id: int, character_data, need_data=False):
# проверяем, возможно ли зацикливание при загрузке сопутствующих данных?
url: str = self.get_character_url(character_id)
if self.depth.push(url):
# сохраняем сопутствующие данные в БД
self.actualize_corporation(character_data['corporation_id'], need_data=need_data)
self.depth.pop()
def actualize_character(self, _character_id, need_data=False):
character_id: int = int(_character_id)
# откидываем "пилотов" типа 'Secure Commerce Commission' = 1000132
# или всякую непись 'Areyara Kogachi' = 3004093
if character_id < 90000000:
return None
in_cache = self.__cached_characters.get(character_id)
# 1. либо данных нет в кеше
# 2. если данные с таким id существуют, но внешнему коду не интересны сами данные, то выход выход
# 3. либо данные в текущую сессию работы программы уже загружались
# 4. данные с таким id существуют, но самих данных нет (видимо хранится только id вместе с at)
# проверяем дату-время последнего обновления информации, и обновляем устаревшие данные
reload_esi: bool = False
if not in_cache:
reload_esi = True
elif not need_data:
return None
elif in_cache.obj:
self.actualize_character_details(character_id, in_cache.obj, need_data=True)
return in_cache.obj
elif (in_cache.at + self.character_timedelta) < self.eve_now:
reload_esi = True
# ---
# загружаем данные с серверов CCP или загружаем данные из БД
if reload_esi:
try:
# Public information about a character
url: str = self.get_character_url(character_id)
data, updated_at, is_updated = self.load_from_esi(url, fully_trust_cache=in_cache is None)
if data:
# сохраняем данные в БД, при этом актуализируем дату последней работы с esi
if updated_at < self.eve_now:
updated_at = self.eve_now
self.dbswagger.insert_or_update_character(character_id, data, updated_at)
else:
# если из кеша (с диска) не удалось в offline режиме считать данные, читаем из БД
data, updated_at = self.dbswagger.select_character(character_id)
if not data:
return None
reload_esi = False
except requests.exceptions.HTTPError as err:
status_code = err.response.status_code
if status_code == 404:
# 404 Client Error: Not Found ('Character has been deleted!')
# это нормально, что часть пилотов со временем могут оказаться Not Found
return None
else:
# print(sys.exc_info())
raise
except:
print(sys.exc_info())
raise
else:
data, updated_at = self.dbswagger.select_character(character_id)
# сохраняем данные в кеше
if not in_cache:
self.__cached_characters[character_id] = QEntity(True, True, data, updated_at)
else:
in_cache.store(True, reload_esi, data, updated_at)
self.actualize_character_details(character_id, data, need_data=need_data)
return data
# -------------------------------------------------------------------------
# corporations/{corporation_id}/
# -------------------------------------------------------------------------
@staticmethod
def get_corporation_url(corporation_id: int) -> str:
# Public information about a corporation
return "corporations/{corporation_id}/".format(corporation_id=corporation_id)
def actualize_corporation_details(self, corporation_id: int, corporation_data, need_data=False):
# проверяем, возможно ли зацикливание при загрузке сопутствующих данных?
url: str = self.get_corporation_url(corporation_id)
if self.depth.push(url):
# сохраняем сопутствующие данные в БД
if corporation_data['ceo_id'] != 1: # EVE System
self.actualize_character(corporation_data['ceo_id'], need_data=need_data)
if corporation_data['creator_id'] != 1: # EVE System
self.actualize_character(corporation_data['creator_id'], need_data=need_data)
if 'home_station_id' in corporation_data:
self.actualize_station_or_structure(
corporation_data['home_station_id'],
need_data=need_data
)
self.depth.pop()
def actualize_corporation(self, _corporation_id, need_data=False):
corporation_id: int = int(_corporation_id)
in_cache = self.__cached_corporations.get(corporation_id)
# 1. либо данных нет в кеше
# 2. если данные с таким id существуют, но внешнему коду не интересны сами данные, то выход выход
# 3. либо данные в текущую сессию работы программы уже загружались
# 4. данные с таким id существуют, но самих данных нет (видимо хранится только id вместе с at)
# проверяем дату-время последнего обновления информации, и обновляем устаревшие данные
reload_esi: bool = False
if not in_cache:
reload_esi = True
elif not need_data:
return None
elif in_cache.obj:
self.actualize_corporation_details(corporation_id, in_cache.obj, need_data=True)
return in_cache.obj
elif (in_cache.at + self.corporation_timedelta) < self.eve_now:
reload_esi = True
# ---
# загружаем данные с серверов CCP или загружаем данные из БД
if reload_esi:
# Public information about a corporation
url: str = self.get_corporation_url(corporation_id)
data, updated_at, is_updated = self.load_from_esi(url, fully_trust_cache=in_cache is None)
if data:
# сохраняем данные в БД, при этом актуализируем дату последней работы с esi
if updated_at < self.eve_now:
updated_at = self.eve_now
self.dbswagger.insert_or_update_corporation(corporation_id, data, updated_at)
else:
# если из кеша (с диска) не удалось в offline режиме считать данные, читаем из БД
data, updated_at = self.dbswagger.select_corporation(corporation_id)
if not data:
return None
reload_esi = False
else:
data, updated_at = self.dbswagger.select_corporation(corporation_id)
# сохраняем данные в кеше
if not in_cache:
self.__cached_corporations[corporation_id] = QEntity(True, True, data, updated_at)
else:
in_cache.store(True, reload_esi, data, updated_at)
self.actualize_corporation_details(corporation_id, data, need_data=need_data)
return data
# -------------------------------------------------------------------------
# universe/stations/{station_id}/
# -------------------------------------------------------------------------
@staticmethod
def get_universe_station_url(station_id: int) -> str:
# Public information about a universe_station
return "universe/stations/{station_id}/".format(station_id=station_id)
def actualize_universe_station_details(self, station_data, need_data=False):
# здесь загружается только корпорация-владелец станции, которой может и не быть,
# чтобы не усложнять проверки и вхолостую не гонять push/pop - сперва проверим owner-а
owner_id = station_data.get('owner', None)
if owner_id:
# проверяем, возможно ли зацикливание при загрузке сопутствующих данных?
url: str = self.get_universe_station_url(station_data['station_id'])
if self.depth.push(url):
# сохраняем сопутствующие данные в БД
self.actualize_corporation(owner_id, need_data=need_data)
self.depth.pop()
def actualize_universe_station(self, _station_id, need_data=False):
station_id: int = int(_station_id)
in_cache = self.__cached_stations.get(station_id)
# 1. либо данных нет в кеше
# 2. если данные с таким id существуют, но внешнему коду не интересны сами данные, то выход выход
# 3. либо данные в текущую сессию работы программы уже загружались
# 4. данные с таким id существуют, но самих данных нет (видимо хранится только id вместе с at)
# проверяем дату-время последнего обновления информации, и обновляем устаревшие данные
reload_esi: bool = False
if not in_cache:
reload_esi = True
elif not need_data:
return None
elif in_cache.obj:
self.actualize_universe_station_details(in_cache.obj, need_data=True)
return in_cache.obj
elif (in_cache.at + self.universe_station_timedelta) < self.eve_now:
reload_esi = True
# ---
# загружаем данные с серверов CCP или загружаем данные из БД
if reload_esi:
# Public information about a universe_station
url: str = self.get_universe_station_url(station_id)
data, updated_at, is_updated = self.load_from_esi(url, fully_trust_cache=in_cache is None)
if data:
# сохраняем данные в БД, при этом актуализируем дату последней работы с esi
if updated_at < self.eve_now:
updated_at = self.eve_now
self.dbswagger.insert_or_update_universe_station(data, updated_at)
else:
# если из кеша (с диска) не удалось в offline режиме считать данные, читаем из БД
data, updated_at = self.dbswagger.select_universe_station(station_id)
if not data:
return None
reload_esi = False
else:
data, updated_at = self.dbswagger.select_universe_station(station_id)
# сохраняем данные в кеше
if not in_cache:
self.__cached_stations[station_id] = QEntity(True, True, data, updated_at)
else:
in_cache.store(True, reload_esi, data, updated_at)
self.actualize_universe_station_details(data, need_data=need_data)
return data
# -------------------------------------------------------------------------
# universe/structures/{structure_id}/
# -------------------------------------------------------------------------
@staticmethod
def get_universe_structure_url(structure_id: int) -> str:
# Requires: access token
return "universe/structures/{structure_id}/".format(structure_id=structure_id)
def load_universe_structure_from_esi(self, structure_id, fully_trust_cache=True):
try:
# Requires: access token
url: str = self.get_universe_structure_url(structure_id)
data, updated_at, is_updated = self.load_from_esi(url, fully_trust_cache=fully_trust_cache)
return data, False, updated_at
except requests.exceptions.HTTPError as err:
status_code = err.response.status_code
if status_code == 403:
# это нормально, что часть структур со временем могут оказаться Forbidden
return None, True, self.eve_now
else:
# print(sys.exc_info())
raise
except:
print(sys.exc_info())
raise
def actualize_universe_structure_details(self, structure_id: int, structure_data, need_data=False):
# проверяем, возможно ли зацикливание при загрузке сопутствующих данных?
url: str = self.get_universe_structure_url(structure_id)
if self.depth.push(url):
# сохраняем сопутствующие данные в БД
self.actualize_corporation(structure_data['owner_id'], need_data=need_data)
self.depth.pop()
def actualize_universe_structure(self, _structure_id, need_data=False):
structure_id: int = int(_structure_id)
in_cache = self.__cached_structures.get(structure_id)
# 1. либо данных нет в кеше
# 2. если данные с таким id существуют, но внешнему коду не интересны сами данные, то выход выход
# 3. либо данные в текущую сессию работы программы уже загружались
# 4. данные с таким id существуют, но самих данных нет (видимо хранится только id вместе с at)
# проверяем дату-время последнего обновления информации, и обновляем устаревшие данные
reload_esi: bool = False
if not in_cache:
reload_esi = True
elif not need_data:
return None
elif in_cache.obj:
self.actualize_universe_structure_details(structure_id, in_cache.obj, need_data=True)
return in_cache.obj
elif in_cache.ext and in_cache.ext.get('forbidden'):
return None
elif (in_cache.at + self.universe_structure_timedelta) < self.eve_now:
reload_esi = True
# ---
# загружаем данные с серверов CCP или загружаем данные из БД
if reload_esi:
data, forbidden, updated_at = self.load_universe_structure_from_esi(
structure_id,
fully_trust_cache=in_cache is None
)
if data:
# сохраняем данные в БД, при этом актуализируем дату последней работы с esi
if updated_at < self.eve_now:
updated_at = self.eve_now
self.dbswagger.insert_or_update_universe_structure(structure_id, data, forbidden, updated_at)
else:
# если из кеша (с диска) не удалось в offline режиме считать данные, читаем из БД
data, forbidden, updated_at = self.dbswagger.select_universe_structure(structure_id)
if not data:
# если и из БД не удалось считать данные, то при загрузке corporation orders (после длительного
# простоя, когда структура стала forbidden), в эту точку для одной и той же станции можем начать
# попадать многократно, что плохо, потому как каждый такой ордер будет сопровождаться бесконечными
# запросами по ESI, - сохраняем в кеше data=None
if not in_cache:
self.__cached_structures[structure_id] = QEntity(True, True, None, self.eve_now)
self.__cached_structures[structure_id].store_ext({'forbidden': True})
else:
in_cache.store_ext({'forbidden': True})
return None
reload_esi = False
else:
data, forbidden, updated_at = self.dbswagger.select_universe_structure(structure_id)
# сохраняем данные в кеше
if not in_cache:
self.__cached_structures[structure_id] = QEntity(True, True, data, updated_at)
if forbidden:
self.__cached_structures[structure_id].store_ext({'forbidden': True})
else:
in_cache.store(True, reload_esi, data, updated_at)
if forbidden:
in_cache.store_ext({'forbidden': True})
if data:
self.actualize_universe_structure_details(structure_id, data, need_data=need_data)
return data
# -------------------------------------------------------------------------
# universe/structures/
# -------------------------------------------------------------------------
def actualize_universe_structures(self):
# Requires: access token
data = self.esiswagger.get_esi_paged_data('universe/structures/')
updated_at = self.esiswagger.is_last_data_updated
if not updated_at:
return None
data_new = self.dbswagger.get_absent_universe_structure_ids(data)
data_new = [id[0] for id in data_new]
# data = [1035620655696, 1035660997658, 1035620697572, ... ]
# data_new = [1035620655696]
for structure_id in data_new:
self.actualize_universe_structure(structure_id, need_data=False)
self.qidb.commit()
data_len: int = len(data)
data_new_len: int = len(data_new)
del data_new
del data
return data_len, data_new_len
# -------------------------------------------------------------------------
# corporations/{corporation_id}/structures/
# -------------------------------------------------------------------------
@staticmethod
def get_corporation_structures_url(corporation_id: int) -> str:
# Requires role(s): Station_Manager
return "corporations/{corporation_id}/structures/".format(corporation_id=corporation_id)
def actualize_corporation_structure_details(self, structure_data, need_data=False):
# проверяем, возможно ли зацикливание при загрузке сопутствующих данных?
structure_id: int = int(structure_data['structure_id'])
# в данном случае объектом self.depth не пользуемся, т.к. он был настроен ранее, при загрузке
# списка всех корпоративных структур
# сохраняем сопутствующие данные в БД
self.actualize_universe_structure(structure_id, need_data=need_data)
self.actualize_corporation(structure_data['corporation_id'], need_data=need_data)
def actualize_corporation_structure(self, structure_data, updated_at):
structure_id: int = int(structure_data['structure_id'])
in_cache = self.__cached_corporation_structures.get(structure_id)
# 1. либо данных нет в кеше
# 2. если данные с таким id существуют, то проверяем изменились ли они в кеше
# если данные изменились, то надо также обновить их в БД
data_equal: bool = False
if not in_cache:
pass
elif in_cache.obj:
data_equal = in_cache.is_obj_equal_by_keys(structure_data, self.corporation_structure_diff)
# ---
# из соображений о том, что корпоративные структуры может читать только пилот с ролью корпорации,
# выполняем обновление сведений как о universe_structure, так и о корпорации (либо актуализируем, либо
# подгружаем из БД)
self.actualize_corporation_structure_details(structure_data, need_data=True)
# данные с серверов CCP уже загружены, в случае необходимости обновляем данные в БД
if data_equal:
return
self.dbswagger.insert_or_update_corporation_structure(structure_data, updated_at)
# сохраняем данные в кеше
if not in_cache:
self.__cached_corporation_structures[structure_id] = QEntity(True, True, structure_data, updated_at)
else:
in_cache.store(True, True, structure_data, updated_at)
def actualize_corporation_structures(self, _corporation_id):
corporation_id: int = int(_corporation_id)
# Requires role(s): Station_Manager
url: str = self.get_corporation_structures_url(corporation_id)
data, updated_at, is_updated = self.load_from_esi_paged_data(url, fully_trust_cache=True)
if not is_updated:
return data, 0
# список структур имеющихся у корпорации, хранящихся в БД, в кеше, а также новых и исчезнувших
ids_from_esi, ids_in_cache, new_ids, deleted_ids = self.get_cache_status(
self.__cached_corporation_structures,
data, 'structure_id',
corporation_id, 'corporation_id'
)
if not ids_from_esi:
return data, 0
# выше были найдены идентификаторы тех структур, которых нет либо в universe_structures, либо
# нет в corporation_structures, теперь добавляем отсутствующие данные в БД
if self.depth.push(url):
for structure_data in data:
self.actualize_corporation_structure(structure_data, updated_at)
self.depth.pop()
# параметр updated_at меняется в случае, если меняются данные корпоративной структуры, т.ч. не
# использует массовое обновление всех корпоративных структур, а лишь удаляем исчезнувшие
self.dbswagger.mark_corporation_structures_updated(corporation_id, deleted_ids, None)
self.qidb.commit()
return data, len(new_ids)
# -------------------------------------------------------------------------
# universe/stations/{station_id}/
# universe/structures/{structure_id}/
# -------------------------------------------------------------------------
def actualize_station_or_structure(self, location_id, need_data=False):
if location_id >= 1000000000:
self.actualize_universe_structure(location_id, need_data=need_data)
else:
self.actualize_universe_station(location_id, need_data=need_data)
def get_system_id_of_station_or_structure(self, location_id):
system_id = None
if location_id >= 1000000000:
structure = self.__cached_structures.get(location_id)
if not structure or not structure.obj or not ('solar_system_id' in structure.obj):
self.actualize_universe_structure(location_id, False)
structure = self.__cached_structures.get(location_id)
if structure and structure.obj:
system_id = structure.obj.get('solar_system_id', None)
del structure
else:
station = self.__cached_stations.get(location_id)
if not station or not station.obj or not ('system_id' in station.obj):
self.actualize_universe_station(location_id, False)
station = self.__cached_stations.get(location_id)
if station and station.obj:
system_id = station.obj.get('system_id', None)
del station
return system_id
# -------------------------------------------------------------------------
# corporations/{corporation_id}/assets/names/
# -------------------------------------------------------------------------
@staticmethod
def get_corporation_assets_names_url(corporation_id: int) -> str:
# Requires role(s): Director
return "corporations/{corporation_id}/assets/names/".format(corporation_id=corporation_id)
def can_assets_item_be_renamed(self, item_data) -> bool:
# пропускаем экземпляры контейнеров, сложенные в стопки (у них нет уник. id и названий тоже не будет)
is_singleton: bool = item_data['is_singleton']
if not is_singleton:
return False
# пропускаем дронов в дронбеях, патроны в карго, корабли в ангарах и т.п.
location_flag: str = item_data['location_flag']
if location_flag[:-1] != 'CorpSAG': # and location_flag != 'Unlocked' and location_flag != 'AutoFit':
return False
type_id: int = item_data['type_id']
return type_id in self.__universe_items_with_names
def load_corporation_assets_names_from_esi(self, corporation_id: int):
# получение названий контейнеров, станций, и т.п. - всё что переименовывается ingame
corp_cache = self.__cached_corporation_assets.get(corporation_id)
# если ассеты не загружены, то нечего и переименовывать
if not corp_cache:
return None
item_ids: typing.List[int] = []
for (item_id, in_cache) in corp_cache.items():
if self.can_assets_item_be_renamed(in_cache.obj):
item_ids.append(item_id)
del corp_cache
# если ничего не переименовывается, то и загружать названия не требуется
if not item_ids:
del item_ids
return None
# Requires role(s): Director
url: str = self.get_corporation_assets_names_url(corporation_id)
data, updated_at, is_updated = self.load_from_esi_piece_data(url, item_ids)
del item_ids
if data is None:
return None
if self.esiswagger.offline_mode:
pass
elif not is_updated:
return None
corp_names_cache = self.get_corp_cache(self.__cached_corporation_assets_names, corporation_id)
corp_names_cache.clear()
for itm in data:
# { "item_id": 1035960770272, "name": "[prod] conveyor 2" },..
item_id: int = itm['item_id']
corp_names_cache[item_id] = itm['name']
del data
# -------------------------------------------------------------------------
# corporations/{corporation_id}/assets/
# -------------------------------------------------------------------------
@staticmethod
def get_corporation_assets_url(corporation_id: int) -> str:
# Requires role(s): Director
return "corporations/{corporation_id}/assets/".format(corporation_id=corporation_id)
def actualize_corporation_asset_item_details(self, item_data, need_data=False):
# добавление в БД возможно отсутствующего типа товара
type_id: int = int(item_data['type_id'])
if type_id not in self.__cached_type_ids:
url: str = self.get_type_id_url(type_id)
if self.depth.push(url):
self.actualize_type_id(type_id)
self.depth.pop()
# актуализация прочей информации об ассетах
if type_id == 27: # Office
location_id: int = int(item_data['location_id'])
self.actualize_station_or_structure(location_id, need_data=need_data)
def actualize_corporation_asset_item(self, corporation_id: int, item_data, updated_at):
item_id: int = int(item_data['item_id'])
corp_cache = self.get_corp_cache(self.__cached_corporation_assets, corporation_id)
in_cache = corp_cache.get(item_id)
# ---
item_possible_be_renamed: bool = self.can_assets_item_be_renamed(item_data)
in_cache_item_name = None
if item_possible_be_renamed:
corp_names_cache = self.get_corp_cache(self.__cached_corporation_assets_names, corporation_id)
in_cache_item_name = corp_names_cache.get(item_id)
# 1. либо данных нет в кеше
# 2. если данные с таким id существуют, то проверяем изменились ли они в кеше
# если данные изменились, то надо также обновить их в БД
data_equal: bool = False
if not in_cache:
pass
elif in_cache.obj:
data_equal = in_cache.is_obj_equal_by_keys(item_data, self.corporation_asset_diff)
# дополнительно: если у item-а присутствует наименование, то сравниванием с тем, который имеется в БД