From 605d4a3e43e2ddb0338aaf70bba9afbeb7d46a70 Mon Sep 17 00:00:00 2001 From: norohind <60548839+norohind@users.noreply.github.com> Date: Sun, 10 Apr 2022 14:32:48 +0300 Subject: [PATCH] Basic functional implemented: can update squadron with specified id --- DB.py | 53 ++++++++++++++++++++++ FAPI/BearerManager.py | 39 ++++++++++++++++ FAPI/Converters.py | 3 ++ FAPI/Exceptions.py | 6 +++ FAPI/Mappings.py | 45 +++++++++++++++++++ FAPI/Queries.py | 88 +++++++++++++++++++++++++++++++++++++ FAPI/Requester.py | 30 +++++++++++++ FAPI/__init__.py | 1 + SQLRequests.py | 17 +++++++ main.py | 27 +++++++++++- sql/create_operation_id.sql | 1 + sql/delete_squadron.sql | 1 + sql/insert_info.sql | 43 ++++++++++++++++++ sql/insert_news.sql | 22 ++++++++++ sql/squad_deleted.sql | 1 + 15 files changed, 376 insertions(+), 1 deletion(-) create mode 100644 DB.py create mode 100644 FAPI/BearerManager.py create mode 100644 FAPI/Converters.py create mode 100644 FAPI/Exceptions.py create mode 100644 FAPI/Mappings.py create mode 100644 FAPI/Queries.py create mode 100644 FAPI/Requester.py create mode 100644 FAPI/__init__.py create mode 100644 SQLRequests.py create mode 100644 sql/create_operation_id.sql create mode 100644 sql/delete_squadron.sql create mode 100644 sql/insert_info.sql create mode 100644 sql/insert_news.sql create mode 100644 sql/squad_deleted.sql diff --git a/DB.py b/DB.py new file mode 100644 index 0000000..12c3fa3 --- /dev/null +++ b/DB.py @@ -0,0 +1,53 @@ +import sqlite3 +from SQLRequests import SQLRequests + +db = sqlite3.connect('jubilant-system.sqlite') +db.row_factory = lambda c, r: dict(zip([col[0] for col in c.description], r)) + + +def allocate_operation_id(squad_id: int) -> int: + return db.execute(SQLRequests.create_operation_id, {'squad_id': squad_id}).fetchone()['operation_id'] + + +def insert_info_news(news_dict: dict | None, info_dict: dict) -> int: + """ + Saved both news and info endpoint's data + + :param news_dict: + :param info_dict: + :return: + """ + + with db: + operation_id = allocate_operation_id(info_dict['squad_id']) + info_dict['operation_id'] = operation_id + + db.execute(SQLRequests.insert_info, info_dict) + + if news_dict is not None: + news_dict['type_of_news'] = 'public_statements' + news_dict['operation_id'] = operation_id + db.execute(SQLRequests.insert_news, news_dict) + + return operation_id + + +def delete_squadron(squad_id: int, suppress_deleted=True) -> None: + """ + A function to make record in squadrons_deleted table + + :param squad_id: squad_id to mark as deleted + :param suppress_deleted: if IntegrityError exception due to this squad already + exists in squadrons_deleted table should be suppressed + + :return: + """ + + try: + with db: + operation_id = allocate_operation_id(squad_id) + db.execute(SQLRequests.delete_squadron, {'squad_id': squad_id, 'operation_id': operation_id}) + + except sqlite3.IntegrityError as e: + if not suppress_deleted: + raise e diff --git a/FAPI/BearerManager.py b/FAPI/BearerManager.py new file mode 100644 index 0000000..b0b193d --- /dev/null +++ b/FAPI/BearerManager.py @@ -0,0 +1,39 @@ +from enum import Enum +from loguru import logger +import os +import requests + + +class BearerManager: + class Endpoints(Enum): + RANDOM = '/random_token' + + def __init__(self, demb_capi_auth: str, base_address: str): + + self.base_address = base_address + self.demb_capi_auth = demb_capi_auth + + def get_random_bearer(self) -> str: + """Gets bearer token from capi.demb.design (companion-api project) + + :return: bearer token as str + """ + + bearer_request: requests.Response = self._request(self.Endpoints.RANDOM) + + try: + bearer: str = bearer_request.json()['access_token'] + + except Exception as e: + logger.exception(f'Unable to parse capi.demb.design answer\nrequested: {bearer_request.url!r}\n' + f'code: {bearer_request.status_code!r}\nresponse: {bearer_request.content!r}', exc_info=e) + raise e + + return bearer + + def _request(self, _endpoint: Endpoints) -> requests.Response: + endpoint = self.base_address + _endpoint.value + return requests.get(url=endpoint, headers={'auth': self.demb_capi_auth}) + + +bearer_manager = BearerManager(os.environ['DEMB_CAPI_AUTH'], 'https://capi.demb.design') diff --git a/FAPI/Converters.py b/FAPI/Converters.py new file mode 100644 index 0000000..bc913a0 --- /dev/null +++ b/FAPI/Converters.py @@ -0,0 +1,3 @@ +def dehexify(hex_str: str) -> str: + """Converts string with hex chars to string""" + return bytes.fromhex(hex_str).decode('utf-8') diff --git a/FAPI/Exceptions.py b/FAPI/Exceptions.py new file mode 100644 index 0000000..715ce1b --- /dev/null +++ b/FAPI/Exceptions.py @@ -0,0 +1,6 @@ +class FAPIDownForMaintenance(Exception): + pass + + +class FAPIUnknownStatusCode(Exception): + pass diff --git a/FAPI/Mappings.py b/FAPI/Mappings.py new file mode 100644 index 0000000..d2cb11c --- /dev/null +++ b/FAPI/Mappings.py @@ -0,0 +1,45 @@ +info_request_mapping = { + "id": "squad_id", + "name": "name", + "tag": "tag", + "ownerId": "owner_id", + "ownerName": "owner_name", + "platform": "platform", + "created": "created", + "created_ts": "created_ts", + "acceptingNewMembers": "accepting_new_members", + "powerId": "power_id", + "powerName": "power_name", + "superpowerId": "superpower_id", + "superpowerName": "superpower_name", + "factionId": "faction_id", + "factionName": "faction_name", + "deleteAfter": "delete_after", + "userTags": "user_tags", + "memberCount": "member_count", + "onlineCount": "online_count", + "pendingCount": "pending_count", + "publicComms": "public_comms", + "publicCommsOverride": "public_comms_override", + "publicCommsAvailable": "public_comms_available" +} + +news_request_mapping = { + 'id': 'news_id' +} + + +def perform_mapping(mapping: dict, dict_to_map: dict) -> dict: + for key in (list(dict_to_map.keys())): + if key in mapping: + dict_to_map[mapping[key]] = dict_to_map.pop(key) + + return dict_to_map + + +def perform_info_mapping(info_data: dict) -> dict: + return perform_mapping(info_request_mapping, info_data) + + +def perform_news_mapping(news_data: dict) -> dict: + return perform_mapping(news_request_mapping, news_data) diff --git a/FAPI/Queries.py b/FAPI/Queries.py new file mode 100644 index 0000000..bca9fe5 --- /dev/null +++ b/FAPI/Queries.py @@ -0,0 +1,88 @@ +from .Mappings import perform_info_mapping, perform_news_mapping +from .Exceptions import * +from .Requester import * +from loguru import logger +from . import Converters +import json + + +def get_squad_news(squad_id) -> dict | None: + """ + Returns news of squadron with specified id or None if squadron doesn't exists or there is no news + + :param squad_id: + :return: + """ + + news_request: requests.Response = request(BASE_URL + NEWS_ENDPOINT, params={'squadronId': squad_id}) + if news_request.status_code != 200: # must not happen + logger.warning(f'Got not 200 status code on requesting news, content: {news_request.content}, ' + f'code: {news_request.status_code}') + + squad_news: dict = news_request.json()['squadron'] + + if isinstance(squad_news, list): # check squadron 2517 for example 0_0 + # squadron have no public statements + return None + + elif 'id' not in squad_news.keys(): # squadron doesn't FDEV + return None + + else: + if 'public_statements' in squad_news.keys() and len(squad_news['public_statements']) > 0: + return perform_news_mapping(squad_news['public_statements'][0]) + + +def get_squad_info(squad_id: int) -> dict | None: + """Returns information about squadron with specified id or None if squadrons doesn't exists on FDEV side + + :param squad_id: id of squad to update/insert + :return: dict with state of squadron or None if squad not found + """ + + """ + How it should works? + Request squad's info + + if squad exists FDEV + return squadron state + + if squad doesn't exists FDEV + return None + """ + """ + if DB.db.execute(SQLRequests.squad_deleted, {'squad_id': squad_id}) == 1: + # we have it deleted in our DB + logger.debug(f'squad {squad_id} is marked as deleted in our DB, returning False') + return None + """ + + squad_request: requests.Response = request(BASE_URL + INFO_ENDPOINT, params={'squadronId': squad_id}) + + if squad_request.status_code == 200: # squad exists FDEV + squad_request_json: dict = squad_request.json()['squadron'] + squad_request_json['ownerName'] = Converters.dehexify(squad_request_json['ownerName']) # normalize value + squad_request_json['userTags'] = json.dumps(squad_request_json['userTags']) + squad_request_json = perform_info_mapping(squad_request_json) + + """ + with DB.db: + operation_id = DB.allocate_operation_id(squad_id) + squad_request_json['operation_id'] = operation_id + DB.db.execute(SQLRequests.insert_info, squad_request_json) + """ + + return squad_request_json + + elif squad_request.status_code == 404: # squad doesn't exists FDEV + """ + if not suppress_absence: + with DB.db: + operation_id = DB.allocate_operation_id(squad_id) + DB.db.execute(SQLRequests.delete_squadron, {'operation_id': operation_id, 'squad_id': squad_id}) + """ + return None + + else: # any other codes (except 418, that one handles in authed_request), never should happen + logger.warning(f'Unknown squad info status_code: {squad_request.status_code}, content: {squad_request.content}') + raise FAPIUnknownStatusCode(f'Status code: {squad_request.status_code}, content: {squad_request.content}') \ No newline at end of file diff --git a/FAPI/Requester.py b/FAPI/Requester.py new file mode 100644 index 0000000..ac07ae2 --- /dev/null +++ b/FAPI/Requester.py @@ -0,0 +1,30 @@ +import requests +from .BearerManager import bearer_manager +from loguru import logger +from . import Exceptions + + +""" +Functions to perform queries to FDEV +""" + + +BASE_URL = 'https://api.orerve.net/2.0/website/squadron/' +INFO_ENDPOINT = 'info' +NEWS_ENDPOINT = 'news/list' + + +def request(url: str, method: str = 'get', **kwargs) -> requests.Response: + _request: requests.Response = requests.request( + method=method, + url=url, + headers={'Authorization': f'Bearer {bearer_manager.get_random_bearer()}'}, + **kwargs + ) + + if _request.status_code == 418: # FAPI is on maintenance + logger.warning(f'{method.upper()} {_request.url} returned 418, content dump:\n{_request.content}') + + raise Exceptions.FAPIDownForMaintenance + + return _request diff --git a/FAPI/__init__.py b/FAPI/__init__.py new file mode 100644 index 0000000..cae44b2 --- /dev/null +++ b/FAPI/__init__.py @@ -0,0 +1 @@ +from . import Queries diff --git a/SQLRequests.py b/SQLRequests.py new file mode 100644 index 0000000..94bca1c --- /dev/null +++ b/SQLRequests.py @@ -0,0 +1,17 @@ +from os import path + + +def read_request(name: str) -> str: + req_path = path.join('sql', name+'.sql') + with open(req_path, mode='r') as file: + return ''.join(file.readlines()) + + +class SQLRequests: + build_squadrons_current_data = read_request('build_squadrons_current_data') + schema = read_request('schema') + squad_deleted = read_request('squad_deleted') + create_operation_id = read_request('create_operation_id') + insert_info = read_request('insert_info') + delete_squadron = read_request('delete_squadron') + insert_news = read_request('insert_news') diff --git a/main.py b/main.py index 771d483..e3d4d4b 100644 --- a/main.py +++ b/main.py @@ -22,4 +22,29 @@ Workflow to update existing squadron: else: insert squad_id, operations_id to squadrons_deleted -""" \ No newline at end of file +""" + +import FAPI +import DB + + +def update_squad(squad_id: int, suppress_absence=False) -> None: + squad_info = FAPI.Queries.get_squad_info(squad_id) + if squad_info is None: + # Squad not found FDEV + if not suppress_absence: + DB.delete_squadron(squad_id) + + else: + # Then we got valid squad_info dict + news_info = FAPI.Queries.get_squad_news(squad_id) + print(DB.insert_info_news(news_info, squad_info)) + + +def main(): + update_squad(2530) + update_squad(47999) + + +if __name__ == '__main__': + main() diff --git a/sql/create_operation_id.sql b/sql/create_operation_id.sql new file mode 100644 index 0000000..35ced7f --- /dev/null +++ b/sql/create_operation_id.sql @@ -0,0 +1 @@ +insert into operations_info (squad_id) values (:squad_id) RETURNING operation_id; \ No newline at end of file diff --git a/sql/delete_squadron.sql b/sql/delete_squadron.sql new file mode 100644 index 0000000..a5b8433 --- /dev/null +++ b/sql/delete_squadron.sql @@ -0,0 +1 @@ +insert into squadrons_deleted (operation_id, squad_id) VALUES (:operation_id, :squad_id); \ No newline at end of file diff --git a/sql/insert_info.sql b/sql/insert_info.sql new file mode 100644 index 0000000..b233256 --- /dev/null +++ b/sql/insert_info.sql @@ -0,0 +1,43 @@ +INSERT INTO squadrons_historical_data values ( +:operation_id, +:name, +:tag, +:owner_id, +:owner_name, +:platform, +:created, +:created_ts, +:accepting_new_members, +:power_id, +:power_name, +:superpower_id, +:superpower_name, +:faction_id, +:faction_name, +:delete_after, +:credits_balance, +:credits_in, +:credits_out, +:user_tags, +:member_count, +:online_count, +:pending_count, +:full, +:public_comms, +:public_comms_override, +:public_comms_available, +:current_season_trade_score, +:previous_season_trade_score, +:current_season_combat_score, +:previous_season_combat_score, +:current_season_exploration_score, +:previous_season_exploration_score, +:current_season_cqc_score, +:previous_season_cqc_score, +:current_season_bgs_score, +:previous_season_bgs_score, +:current_season_powerplay_score, +:previous_season_powerplay_score, +:current_season_aegis_score, +:previous_season_aegis_score +); \ No newline at end of file diff --git a/sql/insert_news.sql b/sql/insert_news.sql new file mode 100644 index 0000000..abf27db --- /dev/null +++ b/sql/insert_news.sql @@ -0,0 +1,22 @@ +insert into squadrons_news_historical ( + operation_id, + type_of_news, + news_id, + "date", + category, + motd, + author, + cmdr_id, + user_id +) values +( + :operation_id, + :type_of_news, + :news_id, + :date, + :category, + :motd, + :author, + :cmdr_id, + :user_id +); \ No newline at end of file diff --git a/sql/squad_deleted.sql b/sql/squad_deleted.sql new file mode 100644 index 0000000..722673f --- /dev/null +++ b/sql/squad_deleted.sql @@ -0,0 +1 @@ +select count(*) from squadrons_deleted where squad_id = :squad_id; \ No newline at end of file