From 12df873ead698454477e70da759a43b24e24f7c4 Mon Sep 17 00:00:00 2001 From: Katboi01 Date: Thu, 24 Aug 2023 22:02:10 +0200 Subject: [PATCH] Initial API (Kingdom + KF3) --- .gitignore | 2 + app.py | 30 ++++ gunicorn_config.py | 10 ++ loaders/KF3/charaData.py | 133 +++++++++++++++++ loaders/KF3/kf3db.py | 276 +++++++++++++++++++++++++++++++++++ loaders/Kingdom/kemono.py | 136 +++++++++++++++++ loaders/Kingdom/kingdomdb.py | 276 +++++++++++++++++++++++++++++++++++ loaders/Kingdom/numeric.py | 21 +++ loaders/Nexon/nexondb.py | 7 + resources/KF3/endpoints.py | 0 resources/KF3/friend.py | 27 ++++ resources/KF3/friends.py | 21 +++ resources/Kingdom/friend.py | 25 ++++ resources/Kingdom/friends.py | 21 +++ resources/Kingdom/item.py | 16 ++ resources/Kingdom/items.py | 23 +++ resources/average.py | 36 +++++ resources/difference_last.py | 34 +++++ resources/minmax_last.py | 38 +++++ resources/shared.py | 13 ++ wsgi.py | 5 + 21 files changed, 1150 insertions(+) create mode 100644 .gitignore create mode 100644 app.py create mode 100644 gunicorn_config.py create mode 100644 loaders/KF3/charaData.py create mode 100644 loaders/KF3/kf3db.py create mode 100644 loaders/Kingdom/kemono.py create mode 100644 loaders/Kingdom/kingdomdb.py create mode 100644 loaders/Kingdom/numeric.py create mode 100644 loaders/Nexon/nexondb.py create mode 100644 resources/KF3/endpoints.py create mode 100644 resources/KF3/friend.py create mode 100644 resources/KF3/friends.py create mode 100644 resources/Kingdom/friend.py create mode 100644 resources/Kingdom/friends.py create mode 100644 resources/Kingdom/item.py create mode 100644 resources/Kingdom/items.py create mode 100644 resources/average.py create mode 100644 resources/difference_last.py create mode 100644 resources/minmax_last.py create mode 100644 resources/shared.py create mode 100644 wsgi.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..54e6782 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +__pycache__/ +data/ \ No newline at end of file diff --git a/app.py b/app.py new file mode 100644 index 0000000..0c6dfb3 --- /dev/null +++ b/app.py @@ -0,0 +1,30 @@ +from flask import Flask +from flask_restful import Api + +from resources.KF3.friend import KF3_Friend +from resources.KF3.friends import KF3_Friends +from resources.Kingdom.friend import Kingdom_Friend +from resources.Kingdom.friends import Kingdom_Friends +from resources.Kingdom.item import Kingdom_Item +from resources.Kingdom.items import Kingdom_Items +from loaders.KF3.kf3db import KF3DB +from loaders.Kingdom.kingdomdb import KingdomDB + +app = Flask(__name__) +app.config['JSON_AS_ASCII'] = False +app.databases = {} + +KF3DB(app) +KingdomDB(app) + +api = Api(app) + +api.add_resource(KF3_Friend, "/KF3/Friend/") +api.add_resource(KF3_Friends, "/KF3/Friends") +api.add_resource(Kingdom_Friend, "/Kingdom/Friend/") +api.add_resource(Kingdom_Friends, "/Kingdom/Friends") +api.add_resource(Kingdom_Item, "/Kingdom/Item/") +api.add_resource(Kingdom_Items, "/Kingdom/Items") + +if __name__ == '__main__': + app.run(host='127.0.0.1', port=8080, debug=True) \ No newline at end of file diff --git a/gunicorn_config.py b/gunicorn_config.py new file mode 100644 index 0000000..d82469d --- /dev/null +++ b/gunicorn_config.py @@ -0,0 +1,10 @@ +import multiprocessing + +workers = multiprocessing.cpu_count() # * 2 + 1 +bind = 'unix:flaskrest.sock' +umask = 0o007 +reload = False + +#logging +accesslog = '-' +errorlog = '-' \ No newline at end of file diff --git a/loaders/KF3/charaData.py b/loaders/KF3/charaData.py new file mode 100644 index 0000000..fa24453 --- /dev/null +++ b/loaders/KF3/charaData.py @@ -0,0 +1,133 @@ +import json +import math +from collections import defaultdict + +def GetName(charaData): + name = "no name" + if charaData is not None: + if charaData["nameEn"] != "": + name = charaData["nameEn"] + elif charaData["name"] != "": + name = charaData["name"] + if charaData["nickname"] != "": + name += " " + charaData["nickname"] + return name + +def GetAttackBase(alphaBase, level): + if level <= alphaBase["atkLvMiddleNum"]: + t = (level - 1) / (alphaBase["atkLvMiddleNum"]-1) + return lerp(alphaBase["atkParamLv1"], alphaBase["atkParamLvMiddle"], t) + else: + t = (level - alphaBase["atkLvMiddleNum"]) / (99 - alphaBase["atkLvMiddleNum"]) + return lerp(alphaBase["atkParamLvMiddle"], alphaBase["atkParamLv99"], t) + +def GetDefenseBase(alphaBase, level): + if level <= alphaBase["defLvMiddleNum"]: + t = (level - 1) / (alphaBase["defLvMiddleNum"]-1) + return lerp(alphaBase["defParamLv1"], alphaBase["defParamLvMiddle"], t) + else: + t = (level - alphaBase["defLvMiddleNum"]) / (99 - alphaBase["defLvMiddleNum"]) + return lerp(alphaBase["defParamLvMiddle"], alphaBase["defParamLv99"], t) + +def GetHealthBase(alphaBase, level): + if level <= alphaBase["hpLvMiddleNum"]: + t = (level - 1) / (alphaBase["hpLvMiddleNum"]-1) + return lerp(alphaBase["hpParamLv1"], alphaBase["hpParamLvMiddle"], t) + else: + t = (level - alphaBase["hpLvMiddleNum"]) / (99 - alphaBase["hpLvMiddleNum"]) + return lerp(alphaBase["hpParamLvMiddle"], alphaBase["hpParamLv99"], t) + +def get_all_stats(chara, alphaBase, max_level: bool, rising_status_pattern): + level = 99 if max_level else 1 + + hp = GetHealthBase(alphaBase, level) + atk = GetAttackBase(alphaBase, level) + defe = GetDefenseBase(alphaBase, level) + + if max_level: + starBoost = 1 + (chara["rankHigh"] - 1) * 0.02 + hp = int(math.ceil((hp + chara["promoteBonus"]["hp"]) * starBoost) + chara["costumeBonus"]["hp"]) + atk = int(math.ceil((atk + chara["promoteBonus"]["atk"]) * starBoost) + chara["costumeBonus"]["atk"]) + defe = int(math.ceil((defe + chara["promoteBonus"]["def"]) * starBoost) + chara["costumeBonus"]["def"]) + evd = 10 * alphaBase["avoidRatio"] + chara["promoteBonus"]["evd"] + beatBonus = chara["promoteBonus"]["beat"] / 10 + actBonus = chara["promoteBonus"]["act"] / 10 + tryBonus = chara["promoteBonus"]["try"] / 10 + + if rising_status_pattern is not None: + for i in range(51): + hp += rising_status_pattern["hp"] + atk += rising_status_pattern["atk"] + defe += rising_status_pattern["def"] + else: + starBoost = 1 + (chara["rankLow"] - 1) * 0.02 + hp = int(math.ceil(hp * starBoost)) + atk = int(math.ceil(atk * starBoost)) + defe = int(math.ceil(defe * starBoost)) + evd = 10 * alphaBase["avoidRatio"] + beatBonus = 0 + actBonus = 0 + tryBonus = 0 + + + status = hp * 8 / 10 + if hp * 8 % 10 > 0: + status += 1 + status += atk * 3 + defe * 2 + + result = { + "level" : level, + "status" : int(status), + "wr" : chara["rankHigh"] if max_level else chara["rankLow"], + "hp" : hp, + "atk" : atk, + "def" : defe, + "evd" : evd, + "beat" : beatBonus, + "act" : actBonus, + "try" : tryBonus, + } + + return result + +def fill_miracle_numbers(chara): + output = [] + damages = {} + buffs = defaultdict(list) + + for i, damage_param in enumerate(chara["arts"]["damageList"]): + damages[f"[DAMAGE{i}]"] = damage_param + + for i, buff_param in enumerate(chara["arts"]["buffList"]): + buffs[f"[BUFF{i}]"].append(buff_param) + buffs[f"[HEAL{i}]"].append(buff_param) + buffs[f"[INCREMENT{i}]"].append(buff_param) + + for i in range(1, 7): + base_text = chara["arts"]["actionEffect"] + for damage_key, damage_value in damages.items(): + new_value = str(int(damage_value["damageRate"] * (1 + damage_value["growthRate"] * (i - 1)) * 100 + 0.01)) + base_text = base_text.replace(damage_key, new_value) + + for buff_key, buff_value_list in buffs.items(): + buff_value = buff_value_list[0] + if buff_key[1] == 'B': + new_value = str(int(abs(buff_value["coefficient"] * (1 + buff_value["growthRate"] * (i - 1)) - 1) * 100 + 0.01)) + base_text = base_text.replace(buff_key, new_value) + elif buff_key[1] == 'H': + new_value = str(int(buff_value["coefficient"] * (1 + buff_value["growthRate"] * (i - 1)) * 100 + 0.01)) + base_text = base_text.replace(buff_key, new_value) + elif buff_key[1] == 'I': + new_value = str(int(abs(buff_value["increment"]) * (1 + buff_value["growthRate"] * (i - 1)) + 0.01)) + base_text = base_text.replace(buff_key, new_value) + + output.append(base_text) + + return output + +def toJSON(chara): + return json.dumps(chara, default=lambda o: o.__dict__, + sort_keys=True, indent=1, ensure_ascii=False) + +def lerp(a, b, t): + return (1 - t) * a + t * b \ No newline at end of file diff --git a/loaders/KF3/kf3db.py b/loaders/KF3/kf3db.py new file mode 100644 index 0000000..87d1cc8 --- /dev/null +++ b/loaders/KF3/kf3db.py @@ -0,0 +1,276 @@ +import gzip +import json +import UnityPy +from loaders.KF3.charaData import get_all_stats, fill_miracle_numbers + +class KF3DB: + processed_friends = {} + paramAbilities = {} + paramAbilities1 = {} + paramAbilities2 = {} + paramAlphaBases = {} + paramArts = {} + paramSpecialAttacks = {} + paramWaitActions = {} + charaData = {} + promoteData = {} + promotePresetData = {} + charaClothesData = {} + limitlevel_rising_status = {} + + def __init__(self, app) -> None: + if "KF3" in app.databases: + del app.databases["KF3"] + + self.parse_parameter() + + with gzip.open("data/KF3/CHARA_DATA.d", mode="rt", encoding="utf-8") as file: + for entry in json.loads(file.read()): + self.charaData[entry["id"]] = entry + + with gzip.open("data/KF3/CHARA_PROMOTE_DATA.d", mode="rt", encoding="utf-8") as file: + for entry in json.loads(file.read()): + self.promoteData[entry["promoteId"]] = entry + + with gzip.open("data/KF3/CHARA_PROMOTE_PRESET_DATA.d", mode="rt", encoding="utf-8") as file: + for entry in json.loads(file.read()): + if entry["promotePresetId"] not in self.promotePresetData: + self.promotePresetData[entry["promotePresetId"]] = [] + self.promotePresetData[entry["promotePresetId"]].append(entry) + + with gzip.open("data/KF3/CHARA_CLOTHES_DATA.d", mode="rt", encoding="utf-8") as file: + for entry in json.loads(file.read()): + if entry["clothesPresetId"] not in self.charaClothesData: + self.charaClothesData[entry["clothesPresetId"]] = [] + self.charaClothesData[entry["clothesPresetId"]].append(entry) + + with gzip.open("data/KF3/LIMITLEVEL_RISING_STATUS.d", mode="rt", encoding="utf-8") as file: + for entry in json.loads(file.read()): + self.limitlevel_rising_status[entry["patternId"]] = entry + + self.process_friends() + app.databases["KF3"] = self + + def parse_parameter(self): + paramAsset = UnityPy.load("data/KF3/assets/parameter.asset") + for obj in paramAsset.objects: + data = obj.read() + if data.name.split('_')[0] == "ParamAbility": + id = int(data.name.split('_')[1]) + if data.name.endswith("_1"): + self.paramAbilities1[id] = obj.read_typetree() + elif data.name.endswith("_2"): + self.paramAbilities2[id] = obj.read_typetree() + else: + self.paramAbilities[id] = obj.read_typetree() + elif data.name.split('_')[0] == "ParamAlphaBase": + id = int(data.name.split('_')[1]) + self.paramAlphaBases[id] = obj.read_typetree() + elif data.name.split('_')[0] == "ParamArts": + id = int(data.name.split('_')[1]) + self.paramArts[id] = obj.read_typetree() + elif data.name.split('_')[0] == "ParamSpecialAttack": + id = int(data.name.split('_')[1]) + self.paramSpecialAttacks[id] = obj.read_typetree() + elif data.name.split('_')[0] == "ParamWaitAction": + id = int(data.name.split('_')[1]) + self.paramWaitActions[id] = obj.read_typetree() + + def process_friends(self): + self.processed_friends = {} + for charaID in self.charaData: + self.processed_friends[charaID] = self.process_chara(charaID) + + def process_chara(self, id : int): + chara = {} + + charaData = self.charaData[id] + alphaBase = self.paramAlphaBases[id] + + wrLocked = False + promoIds = [] + promotePresetId = charaData["promotePresetId"] + promos = self.promotePresetData[promotePresetId] if promotePresetId in self.promotePresetData else [] + for promo in promos: + if promo["promoteStepDatetime"] == 1917860400000: + wrLocked = True + continue + else: + promoIds.append([promo["promoteId00"], promo["promoteId01"], promo["promoteId02"], promo["promoteId03"], promo["promoteId04"], promo["promoteId05"]]) + + promoteDatas = [[self.promoteData[id] for id in promo] for promo in promoIds] + promote_bonus = {"atk" : 0, "def" : 0, "hp" : 0, "evd" : 0, "beat" : 0, "act" : 0, "try" : 0} + for promoTier in promoteDatas: + for promoteStep in promoTier: + promote_bonus["atk"] += promoteStep["promoteAtk"] + promote_bonus["def"] += promoteStep["promoteDef"] + promote_bonus["hp"] += promoteStep["promoteHp"] + promote_bonus["evd"] += promoteStep["promoteAvoid"] + promote_bonus["act"] += promoteStep["promoteActionDamageRatio"] + promote_bonus["beat"] += promoteStep["promoteBeatDamageRatio"] + promote_bonus["try"] += promoteStep["promoteTryDamageRatio"] + + clothesPresetId = charaData["clothesPresetId"] + clothesDatas = self.charaClothesData[clothesPresetId] if clothesPresetId in self.charaClothesData else [] + costume_bonus = {"atk" : 0, "def" : 0, "hp" : 0, "evd" : 0, "beat" : 0, "act" : 0, "try" : 0} + for clothesData in clothesDatas: + costume_bonus["atk"] += clothesData["atkBonus"] + costume_bonus["def"] += clothesData["defBonus"] + costume_bonus["hp"] += clothesData["hpBonus"] + + chara["id"] = id + chara["promoteBonus"] = promote_bonus + chara["costumeBonus"] = costume_bonus + chara["attribute"] = charaData["attribute"] + chara["nameEn"] = charaData["nameEn"] + chara["rankLow"] = charaData["rankLow"] + chara["rankHigh"] = charaData["rankHigh"] + chara["castName"] = charaData["castName"] + + chara["wr"] = 4 if wrLocked else 5 + chara["is_wr5"] = chara["wr"] == 5 + chara["has_party_dress"] = False + for clothesData in clothesDatas: + if clothesData["clothesId"] == 8: + chara["has_party_dress"] = True + break + chara["has_rainbow_trait"] = id in self.paramAbilities2 + + chara["risingStatusPatternId"] = patternId = charaData["risingStatusPatternId"] + level_curve = self.limitlevel_rising_status[patternId] if patternId != 0 else None + + chara["stats_min"] = get_all_stats(chara, alphaBase, max_level = False, rising_status_pattern=level_curve) + chara["stats_max"] = get_all_stats(chara, alphaBase, max_level = True, rising_status_pattern=level_curve) + chara["plasmPoint"] = alphaBase["plasmPoint"] + chara["cards"] = [ + {"type":alphaBase["orderCardType00"], "value":alphaBase["orderCardValue00"]}, + {"type":alphaBase["orderCardType01"], "value":alphaBase["orderCardValue01"]}, + {"type":alphaBase["orderCardType02"], "value":alphaBase["orderCardValue02"]}, + {"type":alphaBase["orderCardType03"], "value":alphaBase["orderCardValue03"]}, + {"type":alphaBase["orderCardType04"], "value":alphaBase["orderCardValue04"]} + ] + chara["synergy_flag"] = self.paramArts[id]["authParam"]["SynergyFlag"] if id in self.paramArts else 0 + + chara["arts"] = self.paramArts[id] if id in self.paramArts else None + chara["ability"] = self.paramAbilities[id] if id in self.paramAbilities else None + chara["ability1"] = self.paramAbilities1[id] if id in self.paramAbilities1 else None + chara["ability2"] = self.paramAbilities2[id] if id in self.paramAbilities2 else None + chara["wait_action"] = self.paramWaitActions[id] if id in self.paramWaitActions else None + chara["special_attack"] = self.paramSpecialAttacks[id] if id in self.paramSpecialAttacks else None + + return chara + + def get_chara(self, id : int): + if id in self.processed_friends: + return self.processed_friends[id] + else: + return None + + def get_chara_wiki(self, id : int): + chara = self.get_chara(id) + + lines = [] + + stars_word = chara["rankLow"] + stars_word = "Two" if stars_word == 2 else "Three" if stars_word == 3 else "Four" if stars_word == 4 else "Five" if stars_word == 5 else "Unknown" + + attribute_word = chara["attribute"] + attribute_word = "Funny" if attribute_word == 1 else "Friendly" if attribute_word == 2 else "Relaxed" if attribute_word == 3 else "Lovely" if attribute_word == 4 else "Active" if attribute_word == 5 else "Carefree" if attribute_word == 6 else "Unknown" + + categories = [ + f"[[Category:{attribute_word} KF3 Friends]]", + f"[[Category:{stars_word} Star KF3 Friends]]", + f"[[Category:Missing Content]]", + f"[[Category:Needs Audio]]", + f"{{{{#vardefine:id|{str(chara['id'])}}}}}" + ] + + if chara['has_party_dress']: + categories.append("[[Category:Party Dress KF3 Friends]]") + if chara['is_wr5']: + categories.append("[[Category:Wild Release 5 KF3 Friends]]") + if chara['has_rainbow_trait']: + categories.append("[[Category:Rainbow Trait KF3 Friends]]") + + lines.append(" ".join(categories)) + lines.append("") + lines.append("{{FriendBox/KF3") + lines.append(f"|name={chara['nameEn'].replace('_', ' ')}") + lines.append(f"|apppic={chara['nameEn'].replace('_', ' ')}KF3.png") + lines.append(f"|apprarity={{{{KF3{chara['rankLow']}Star}}}}") + lines.append(f"|seiyuu={chara['castName']}") + lines.append(f"|attribute={attribute_word} {{{{KF3{attribute_word}}}}}") + lines.append(f"|implemented={0}") + lines.append(f"|id={chara['id']}") + lines.append(f"|wr5={'Yes' if chara['is_wr5'] else 'No'}") + lines.append(f"|rainbowtrait={'Yes' if chara['has_rainbow_trait'] else 'No'}") + lines.append(f"|partydress={'Yes' if chara['has_party_dress'] else 'No'}") + lines.append("}}") + lines.append("") + lines.append("{{FriendBuilder/KF3") + lines.append(f"|introduction = '''{chara['nameEn'].replace('_', ' ')}''' is a Friend that appears in the app version of [[Kemono Friends 3]].") + lines.append(f"|status={chara['stats_min']['status']}") + lines.append(f"|hp={chara['stats_min']['hp']}") + lines.append(f"|atk={chara['stats_min']['atk']}") + lines.append(f"|def={chara['stats_min']['def']}") + lines.append(f"|evd={round(chara['stats_min']['evd']/10,1)}%") + lines.append(f"|beat={chara['stats_min']['beat']}%") + lines.append(f"|action={chara['stats_min']['act']}%") + lines.append(f"|try={chara['stats_min']['try']}%") + lines.append(f"|plasm={chara['plasmPoint']}") + lines.append(f"|maxstatus={chara['stats_max']['status']}") + lines.append(f"|maxhp={chara['stats_max']['hp']}") + lines.append(f"|maxatk={chara['stats_max']['atk']}") + lines.append(f"|maxdef={chara['stats_max']['def']}") + lines.append(f"|maxevd={round(chara['stats_max']['evd']/10,1)}%") + lines.append(f"|maxbeat={chara['stats_max']['beat']}%") + lines.append(f"|maxaction={chara['stats_max']['act']}%") + lines.append(f"|maxtry={chara['stats_max']['try']}%") + cards_str = "|flags=" + for card in chara["cards"]: + cards_str += " {{" + if card["type"] == 1: + cards_str += "Beat}}" + elif card["type"] == 2: + cards_str += f"Action{card['value']}}}}}" + elif card["type"] == 3: + if card["value"] == 20: + cards_str += "TryLow}}" + if card["value"] == 30: + cards_str += "TryMiddle}}" + if card["value"] == 40: + cards_str += "TryHigh}}" + lines.append(cards_str) + cardType = "Beat" if chara["synergy_flag"] == 1 else "Action20" if chara["synergy_flag"] == 2 else "TryHigh" if chara["synergy_flag"] == 3 else "" + lines.append(f"|miracleplus={{{{{cardType}}}}}") + + if chara["arts"] is not None: + miracles = fill_miracle_numbers(chara) + lines.append(f"|miracle={chara['arts']['actionName']}") + for idx,miracle in enumerate(miracles): + lines.append("|miracle" + str(idx + 1) + "=" + miracle.replace("\r\n", "\n").replace("\\n", "\n").replace("\\", " ")) + + if chara["special_attack"] is not None: + lines.append(f"|beatname={chara['special_attack']['actionName']}") + lines.append(f"|beatskill={chara['special_attack']['actionEffect']}") + if chara["wait_action"] is not None: + lines.append(f"|standby={chara['wait_action']['skillName']}") + lines.append(f"|standbyskill={chara['wait_action']['skillEffect']}") + if chara["ability"] is not None: + lines.append(f"|unique={chara['ability']['abilityName']}") + lines.append(f"|uniqueskill={chara['ability']['abilityEffect']}") + if chara["ability1"] is not None: + lines.append(f"|miracletrait={chara['ability1']['abilityName']}") + lines.append(f"|miracletraitskill={chara['ability1']['abilityEffect']}") + else: + lines.append(f"|miracletrait=N/A") + lines.append(f"|miracletraitskill=Not Implemented.") + if chara["ability2"] is not None: + lines.append(f"|rainbowtrait={chara['ability2']['abilityName']}") + lines.append(f"|rainbowtraitskill={chara['ability2']['abilityEffect']}") + else: + lines.append(f"|rainbowtrait=N/A") + lines.append(f"|rainbowtraitskill=Not Implemented.") + lines.append("}}") + + return "\n".join(lines) \ No newline at end of file diff --git a/loaders/Kingdom/kemono.py b/loaders/Kingdom/kemono.py new file mode 100644 index 0000000..30da50f --- /dev/null +++ b/loaders/Kingdom/kemono.py @@ -0,0 +1,136 @@ +import re +import math + +def get_skill_materials(friend, kfk_kemono_skill, kfk_en_item): + items_total = [] + kfk_skill = sorted(filter(lambda s: s["kemonosn"] == friend["sn"] and s["type"] == 2, kfk_kemono_skill.indexed_data.values()), key = lambda s: s["sn"]) + for idx, s in enumerate(kfk_skill): + if len(s["item"]) == 0: + continue + items_total.append( + { + "lvl": s["skilllevel"], + "awakenReq": s["wakenlevel"], + "lvlReq": s["kemonolevel"], + "items": list([{"count": num, "name": kfk_en_item.get(item)["name"]} for item, num in zip(s["item"], s["itemnum"])]) + } + ) + return items_total + +def get_awaken_materials(friend, kfk_kemono_waken, kfk_en_item): + items_total = [] + kfk_waken = sorted(filter(lambda s: s["kemonosn"] == friend["sn"], kfk_kemono_waken.indexed_data.values()), key = lambda s: s["sn"]) + for idx, s in enumerate(kfk_waken): + if len(s["item"]) == 0: + continue + items_total.append( + { + "lvl": idx+1, + "items": list([{"count": num, "name": kfk_en_item.get(item)["name"]} for item, num in zip(s["item"], s["itemnum"])]) + } + ) + return items_total + +def calculate_stats(friend, awaken : int, level : int, trust_percent = 0, trustAttri = None): + stats_array = friend["attrScriptParam"] + max_levels = friend["maxLvl"] + max_awaken = friend["maxAwaken"] + + awaken_step = max_awaken + 2 + + atk_ptr = 1 * awaken_step + satk_ptr = 2 * awaken_step + pdef_ptr = 3 * awaken_step + sdef_ptr = 4 * awaken_step + hp_ptr = 5 * awaken_step + speed_ptr = 6 * awaken_step + + stats = {"patk":stats_array[atk_ptr], "satk":stats_array[satk_ptr], "pdef":stats_array[pdef_ptr], "sdef":stats_array[sdef_ptr], "hp":stats_array[hp_ptr], "speed":stats_array[speed_ptr]} + for i in range(awaken + 1): + if i == awaken: + max_level = level + else: + max_level = max_levels[i] + + stats["patk"] += stats_array[atk_ptr + 1 + i] * (max_level-1) + stats["satk"] += stats_array[satk_ptr + 1 + i] * (max_level-1) + stats["pdef"] += stats_array[pdef_ptr + 1 + i] * (max_level-1) + stats["sdef"] += stats_array[sdef_ptr + 1 + i] * (max_level-1) + stats["hp"] += stats_array[hp_ptr + 1 + i] * (max_level-1) + stats["speed"] += stats_array[speed_ptr + 1 + i] * (max_level-1) + + if trust_percent > 0 and trustAttri != None: + if trustAttri["attribute1"] == 1001: + stats["hp"] += trustAttri["attributeValue1"][trust_percent] + elif trustAttri["attribute1"] == 1021: + stats["patk"] += trustAttri["attributeValue1"][trust_percent] + elif trustAttri["attribute1"] == 1031: + stats["satk"] += trustAttri["attributeValue1"][trust_percent] + else: + print(trustAttri["attribute1"]) + + if trustAttri["attribute2"] == 1001: + stats["hp"] += trustAttri["attributeValue2"][trust_percent] + elif trustAttri["attribute2"] == 1021: + stats["patk"] += trustAttri["attributeValue2"][trust_percent] + elif trustAttri["attribute2"] == 1031: + stats["satk"] += trustAttri["attributeValue2"][trust_percent] + else: + print(trustAttri["attribute2"]) + + stats["patk"] = int(round(stats["patk"])) + stats["satk"] = int(round(stats["satk"])) + stats["pdef"] = int(round(stats["pdef"])) + stats["sdef"] = int(round(stats["sdef"])) + stats["hp"] = int(round(stats["hp"])) + stats["speed"] = int(round(stats["speed"])) + + return stats + +def clean_skill_string(input_string): + if input_string is None: + return None + + color_tags_pattern = r"(.*?)<\/color>" + output_string = re.sub(color_tags_pattern, r"\1", input_string) + + link_tags_pattern = r"(.*?)<\/link>" + output_string = re.sub(link_tags_pattern, r"\1", output_string) + + return output_string + +def fill_miracle(input_string, miracle_level = 1, kemono_level = 0): + if input_string is None: + return "" + + pattern = r"\{\{(.*?)\}\}" + def replace_match(match): + key : str = match.group(1).strip() + + if key.startswith("skill | SkillDesc:"): + values = key.replace("skill | SkillDesc:", "").split(',') + values = [float(value) for value in values] + level_up_values = [ + values[1], + values[1], + values[1], + values[2], + values[2], + values[2], + values[3], + values[3], + values[3], + values[4], + values[4], + values[4], + ] + base_value = values[0] + level_up_value = sum(level_up_values[0:miracle_level-1]) + values[5] + level_bonus = (kemono_level - values[7]) * values[6] + return str(math.floor(base_value + level_up_value + level_bonus)) + + return "{{" + key + "}}" + + output_string = re.sub(pattern, replace_match, input_string) + + return clean_skill_string(output_string) \ No newline at end of file diff --git a/loaders/Kingdom/kingdomdb.py b/loaders/Kingdom/kingdomdb.py new file mode 100644 index 0000000..30f5051 --- /dev/null +++ b/loaders/Kingdom/kingdomdb.py @@ -0,0 +1,276 @@ +import json +import platform +from loaders.Kingdom.numeric import numeric +from loaders.Kingdom.kemono import clean_skill_string, calculate_stats, fill_miracle, get_awaken_materials, get_skill_materials + +#element 1-orange 2-blue 3-green +#showSkillType 1-control 2-guard 3-heal 4-support 5-assault 6-aoe + +class KingdomDB: + processed_friends = {} + item_stages = {} + + def __init__(self, app) -> None: + if "Kingdom" in app.databases: + del app.databases["Kingdom"] + + if platform.system() == "Windows": + NUMERICPATH = "H:\\Apache\\Katworks\\KF\\assets\\Kingdom1\\NumericData" + else: + NUMERICPATH = "/media/USB2/Apache/Katworks/KF/assets/Kingdom1/NumericData" + + self.kfk_drop = numeric(NUMERICPATH +"/Drop.num", 0) + self.kfk_combo = numeric(NUMERICPATH +"/Combo.num", 0) + self.kfk_skill = numeric(NUMERICPATH +"/Skill.num", 0) + self.kfk_equip = numeric(NUMERICPATH +"/Equip.num", 0) + self.kfk_kemono = numeric(NUMERICPATH +"/Kemono.num", 0) + self.kfk_stages = numeric(NUMERICPATH +"/Stage.num", 0) + self.kfk_drop_group = numeric(NUMERICPATH +"/DropGroup.num", 0) + self.kfk_trust_attri = numeric(NUMERICPATH +"/TrustAttri.num", 0) + self.kfk_kemono_skill = numeric(NUMERICPATH +"/KemonoSkill.num", 0) + self.kfk_kemono_power = numeric(NUMERICPATH +"/KemonoPower.num", 0) + self.kfk_kemono_waken = numeric(NUMERICPATH +"/KemonoWaken.num", 0) + self.kfk_en_item = numeric(NUMERICPATH + "/en/Item.ntxt", 1) + self.kfk_en_equip = numeric(NUMERICPATH + "/en/Equip.ntxt", 1) + self.kfk_en_skill = numeric(NUMERICPATH + "/en/Skill.ntxt", 1) + self.kfk_en_combo = numeric(NUMERICPATH + "/en/Combo.ntxt", 1) + self.kfk_en_kemono = numeric(NUMERICPATH + "/en/Kemono.ntxt", 1) + self.kfk_en_kemono_WkPaDc = numeric(NUMERICPATH + "/en/KemonoWkPaDc.ntxt", 1) + self.kfk_en_kemono_power = numeric(NUMERICPATH + "/en/KemonoPower.ntxt", 1) + self.kfk_en_kemono_waken = numeric(NUMERICPATH + "/en/KemonoWaken.ntxt", 1) + self.kfk_en_kemono_garden = numeric(NUMERICPATH + "/en/KemonoGarden.ntxt", 1) + self.kfk_en_str = numeric(NUMERICPATH + "/en/Str.ntxt", 1) + + self.process_friends() + self.process_stages() + + app.databases["Kingdom"] = self + + def process_friends(self): + self.processed_friends = {} + for friend_id in self.kfk_kemono.indexed_data: + if friend_id > 19000: + continue + + friend = self.kfk_kemono.get(friend_id) + + friend["comboSkill"] = "" + combo_friends = [] + for entry in friend["combo"]: + combo = self.kfk_combo.get(entry) + if combo is not None: + combo_en = self.kfk_en_combo.get(combo["comboSkill"]) + friend["comboSkill"] = {"name":combo_en["comboName"], "desc":combo_en["desc"]} + for combo_friend_id in combo["comboKemono"]: + combo_friend = self.kfk_en_kemono.get(combo_friend_id) + if combo_friend is not None and "name" in combo_friend: + combo_friends.append(combo_friend["name"]) + else: + combo_friends.append(combo_friend_id) + + friend["combo"] = combo_friends + + limit_breaks = [] + for bk_value in self.kfk_kemono_power.indexed_data.values(): + if len(limit_breaks) == 5: + break + if(bk_value["kemonosn"] == friend_id): + if bk_value["effectType1"] == 0: + pass + if bk_value["effectType1"] == 1 or bk_value["effectType1"] == 3: + lb = self.kfk_en_kemono_power.get(friend_id * 100 + bk_value["powerlevel"]) + if "name" in lb: + limit_breaks.append({"name": lb["name"], "desc": lb["describe"]}) + else: + limit_breaks.append({"name": None, "desc": lb["describe"]}) + if bk_value["effectType1"] == 2: + raise Exception() + if bk_value["effectType1"] == 4: + lb = self.kfk_en_kemono_WkPaDc.get(bk_value['effectParam1'][0] * 100) + limit_breaks.append({"name": lb["name"], "desc": clean_skill_string(lb["describe"])}) + + habits = [] + for habit in friend["habitSn"]: + hbt = self.kfk_en_kemono_WkPaDc.get(habit*100) + habits.append({"name":hbt["name"], "desc":hbt["describe"]}) + for i in range (3): + if len(habits) < i+1: + habits.append({"name":None, "desc":None}) + + awakens = [] + max_levels = [] + for i in range(3): + awaken_id = friend_id * 100 + i + awaken = self.kfk_kemono_waken.get(awaken_id) + awaken1 = self.kfk_en_kemono_waken.get(awaken_id) + if awaken != None and awaken1 != None: + awaken["en"] = awaken1 + awakens.append(awaken) + max_levels.append(awaken["levelmax"]) + + def validateValue(obj, valName): + if obj[valName] is None: + obj[valName] = {"name": "", "desc": ""} + if "desc" not in obj[valName]: + obj[valName]["desc"] = None + + friend["name"] = self.kfk_en_kemono.get(friend_id)["name"] + ele = friend["element"] + friend["color"] = "Orange" if ele == 1 else "Green" if ele == 2 else "Blue" if ele == 3 else "Error" + ele = friend["showSkillType"] + friend["role"] = "Control" if ele == 1 else "Guard" if ele == 2 else "Healer" if ele == 3 else "Support" if ele == 4 else "Assault" if ele == 5 else "Assault (AOE)" if ele == 6 else "Error" + friend["maxLvl"] = max_levels + friend["awakenings"] = awakens + friend["maxAwaken"] = max_awaken = len(awakens)-1 + friend["base_stats"] = calculate_stats(friend, 0, 1) + friend["max_stats"] = calculate_stats(friend, max_awaken, friend["maxLvl"][max_awaken], trust_percent=100, trustAttri= next((e for e in self.kfk_trust_attri.indexed_data.values() if e["kemonoSn"] == friend_id), None)) + friend["max_miracle"] = 13 if friend["star"] == 6 or friend["star"] == 5 else 12 if friend["star"] == 4 else 11 + friend["collisionSkill"] = self.kfk_en_skill.get(friend["collisionSkill"]) + friend["gardenSpec1"] = self.kfk_en_kemono_garden.get(friend["gardenSpec1"]) + friend["gardenSpec2"] = self.kfk_en_kemono_garden.get(friend["gardenSpec2"]) + friend["gardenSpec3"] = self.kfk_en_kemono_garden.get(friend["gardenSpec3"]) + friend["describe"] = self.kfk_en_str.get(friend["describe"]) + + friend["attack"] = self.kfk_en_skill.get(friend["attack"]) + + skill_id = friend["skill"] + friend["skill"] = self.kfk_en_skill.get(skill_id) + friend["skill1"] = self.kfk_en_skill.get(skill_id+1) if max_awaken >= 1 else None + friend["skill2"] = self.kfk_en_skill.get(skill_id+2) if max_awaken >= 2 else None + friend["spAttack"] = self.kfk_en_skill.get(friend["spAttack"]) + friend["habitSn"] = habits + friend["limitBreaks"] = limit_breaks + + validateValue(friend, "collisionSkill") + validateValue(friend, "attack") + validateValue(friend, "skill") + validateValue(friend, "skill1") + validateValue(friend, "skill2") + validateValue(friend, "spAttack") + + self.processed_friends[friend_id] = friend + + def process_stages(self): + self.item_stages = {} + for stage in self.kfk_stages.indexed_data.values(): + merged_array = stage["fastReward2_star3"] + stage["fastReward3_star3"] + if len(merged_array) > 0: + for drop_sn in merged_array: + drop = self.kfk_drop.get(drop_sn) + for drop_group_sn in drop["groupSn"]: + drop_group = self.kfk_drop_group.get(drop_group_sn) + if drop_group["type"] == 2 or drop_group["type"] == 1: + #materials + item_id = drop_group["content"] + item = self.kfk_en_item.get(item_id) + if item_id not in self.item_stages: + self.item_stages[item_id] = [] + if stage not in self.item_stages[item_id]: + self.item_stages[item_id].append(stage) + + def get_chara(self, id : int): + #return next(lambda f: f["sn"] == id, None) + if id not in self.processed_friends: + return None + else: + friend = self.processed_friends[id] + data = { + "id": friend["sn"], + "name": friend["name"], + "element": friend["color"], + "role": friend["role"], + "star": friend["star"], + "combo": friend["combo"], + "comboSkill": friend["comboSkill"], + "describe": friend["describe"], + "awakenings": friend["awakenings"], + "max_miracle": friend["max_miracle"], + "stats": { + "base":friend["base_stats"], + "max":friend["max_stats"] + }, + "collision": {"name": friend["collisionSkill"]["name"], "desc": clean_skill_string(friend["collisionSkill"]["desc"])}, + "attack": {"name": friend["attack"]["name"], "desc": clean_skill_string(friend["attack"]["desc"])}, + "skills":[ + {"name": friend["skill"]["name"], "desc": fill_miracle(friend["skill"]["desc"], miracle_level=friend["max_miracle"], kemono_level=friend["maxLvl"][-1])}, + {"name": friend["skill1"]["name"], "desc": fill_miracle(friend["skill1"]["desc"], miracle_level=friend["max_miracle"], kemono_level=friend["maxLvl"][-1])}, + {"name": friend["skill2"]["name"], "desc": fill_miracle(friend["skill2"]["desc"], miracle_level=friend["max_miracle"], kemono_level=friend["maxLvl"][-1])}, + ], + "garden":[ + friend["gardenSpec1"], + friend["gardenSpec2"], + friend["gardenSpec3"], + ], + "habits":[ + {"name": friend["habitSn"][0]["name"], "desc": clean_skill_string(friend["habitSn"][0]["desc"])}, + {"name": friend["habitSn"][1]["name"], "desc": clean_skill_string(friend["habitSn"][1]["desc"])}, + {"name": friend["habitSn"][2]["name"], "desc": clean_skill_string(friend["habitSn"][2]["desc"])} + ], + "limitBreaks":[ + friend["limitBreaks"][0], + friend["limitBreaks"][1], + friend["limitBreaks"][2], + friend["limitBreaks"][3], + friend["limitBreaks"][4] + ], + "awakenMaterials": get_awaken_materials(friend, self.kfk_kemono_waken, self.kfk_en_item), + "skillMaterials": get_skill_materials(friend, self.kfk_kemono_skill, self.kfk_en_item) + } + + return json.dumps(data, sort_keys=False, indent=1, ensure_ascii=False) + + def get_chara_wiki(self, id : int): + friend = self.processed_friends[id] + lines = [] + lines.append("|name=" + friend["name"]) + + #stats + lines.append("|maxLvls=" + ",".join([str(lvl) for lvl in friend["maxLvl"]])) + lines.append("|hp=" + str(friend["base_stats"]["hp"])) + lines.append("|maxhp=" + str(friend["max_stats"]["hp"])) + lines.append("|atk=" + str(friend["base_stats"]["patk"])) + lines.append("|maxatk=" + str(friend["max_stats"]["patk"])) + lines.append("|satk=" + str(friend["base_stats"]["satk"])) + lines.append("|maxsatk=" + str(friend["max_stats"]["satk"])) + lines.append("|pdef=" + str(friend["base_stats"]["pdef"])) + lines.append("|maxpdef=" + str(friend["max_stats"]["pdef"])) + lines.append("|sdef=" + str(friend["base_stats"]["sdef"])) + lines.append("|maxsdef=" + str(friend["max_stats"]["sdef"])) + lines.append("|speed=" + str(friend["base_stats"]["speed"])) + lines.append("|maxspeed=" + str(friend["max_stats"]["speed"])) + + #skills + lines.append("|collisionSkill=" + friend["collisionSkill"]["name"]) + lines.append("|collisionSkillEffect=" + clean_skill_string(friend["collisionSkill"]["desc"])) + lines.append("|attack=" + friend["attack"]["name"]) + lines.append("|attackEffect=" + clean_skill_string(friend["attack"]["desc"])) + lines.append("|skill1=" + friend["skill"]["name"]) + lines.append("|skill1Effect=" + clean_skill_string(friend["skill"]["desc"])) + lines.append("|skill2=" + friend["skill1"]["name"]) + lines.append("|skill2Effect=" + clean_skill_string(friend["skill1"]["desc"])) + lines.append("|skill3=" + friend["skill2"]["name"]) + lines.append("|skill3Effect=" + clean_skill_string(friend["skill2"]["desc"])) + lines.append("|spAttack=" + friend["spAttack"]["name"]) + lines.append("|spAttackEffect=" + clean_skill_string(friend["spAttack"]["desc"])) + + for i in range(0,5): + lb = friend['limitBreaks'][i] + if lb["name"] is None: + lines.append(f"|b{i+1}={friend['limitBreaks'][i]['desc']}") + else: + lines.append(f"|b{i+1}='''{friend['limitBreaks'][i]['name']}'''\n{friend['limitBreaks'][i]['desc']}") + + return "\n".join(lines) + + def get_item(self, id): + result = {"item": None, "stages": []} + item = self.kfk_en_item.get(id) + if item is None: + return result + + result["item"] = item + + if id in self.item_stages: + result["stages"] = self.item_stages[id] + + return json.dumps(result, sort_keys=False, indent=1, ensure_ascii=False) diff --git a/loaders/Kingdom/numeric.py b/loaders/Kingdom/numeric.py new file mode 100644 index 0000000..194b5e8 --- /dev/null +++ b/loaders/Kingdom/numeric.py @@ -0,0 +1,21 @@ +import json + + +class numeric: + def __init__(self, file_path:str, type:int): + self.indexed_data = {} + with open(file_path, "rt", encoding="utf-8") as f: + if type == 0: + values = json.load(f)["Data"] + for value in values: + self.indexed_data[value["sn"]] = value + if type == 1: + values = json.load(f) + for i in range(0, len(values["IDs"])): + id = values["IDs"][i] + self.indexed_data[id] = values["Data"][i] + + def get(self, id): + if id not in self.indexed_data: + return None + return self.indexed_data[id] \ No newline at end of file diff --git a/loaders/Nexon/nexondb.py b/loaders/Nexon/nexondb.py new file mode 100644 index 0000000..2a73830 --- /dev/null +++ b/loaders/Nexon/nexondb.py @@ -0,0 +1,7 @@ +class KingdomDB: + + def __init__(self, app) -> None: + if "Nexon" in app.databases: + del app.databases["Nexon"] + + app.databases["Nexon"] = self \ No newline at end of file diff --git a/resources/KF3/endpoints.py b/resources/KF3/endpoints.py new file mode 100644 index 0000000..e69de29 diff --git a/resources/KF3/friend.py b/resources/KF3/friend.py new file mode 100644 index 0000000..26e2ec1 --- /dev/null +++ b/resources/KF3/friend.py @@ -0,0 +1,27 @@ +from flask_restful import Resource +from flask import current_app as app +from flask import request +import json + +class KF3_Friend(Resource): + def get(self, id:int): + if "wiki" in request.args: + result = app.databases["KF3"].get_chara_wiki(id) + + response = app.response_class( + response=result, + status=200, + mimetype='text/plain' + ) + else: + result = app.databases["KF3"].get_chara(id) + result = json.dumps(result) + + response = app.response_class( + response=result, + status=200, + mimetype='application/json' + ) + + response.headers.add("Access-Control-Allow-Origin", "*") + return response \ No newline at end of file diff --git a/resources/KF3/friends.py b/resources/KF3/friends.py new file mode 100644 index 0000000..345f8e2 --- /dev/null +++ b/resources/KF3/friends.py @@ -0,0 +1,21 @@ +import json +from flask_restful import Resource +from flask import current_app as app + +class KF3_Friends(Resource): + def get(self): + db = app.databases["KF3"] + result = [] + for value in db.charaData.values(): + result.append({"id": value["id"], "name": value["nameEn"]}) + + result = sorted(result, key=lambda f: f["id"]) + + response = app.response_class( + response=json.dumps(result, ensure_ascii=False, indent=1), + status=200, + mimetype='application/json' + ) + response.headers.add("Access-Control-Allow-Origin", "*") + + return response \ No newline at end of file diff --git a/resources/Kingdom/friend.py b/resources/Kingdom/friend.py new file mode 100644 index 0000000..ae88204 --- /dev/null +++ b/resources/Kingdom/friend.py @@ -0,0 +1,25 @@ +from flask_restful import Resource +from flask import current_app as app +from flask import request + +class Kingdom_Friend(Resource): + def get(self, id:int): + if "wiki" in request.args: + result = app.databases["Kingdom"].get_chara_wiki(id) + + response = app.response_class( + response=result, + status=200, + mimetype='text/plain' + ) + else: + result = app.databases["Kingdom"].get_chara(id) + + response = app.response_class( + response=result, + status=200, + mimetype='application/json' + ) + + response.headers.add("Access-Control-Allow-Origin", "*") + return response \ No newline at end of file diff --git a/resources/Kingdom/friends.py b/resources/Kingdom/friends.py new file mode 100644 index 0000000..7e7e033 --- /dev/null +++ b/resources/Kingdom/friends.py @@ -0,0 +1,21 @@ +import json +from flask_restful import Resource +from flask import current_app as app, jsonify + +class Kingdom_Friends(Resource): + def get(self): + db = app.databases["Kingdom"] + result = [] + for value in db.processed_friends.values(): + result.append({"id": value["sn"], "name": value["name"], "describe": value["describe"]["content"] if "content" in value["describe"] else ""}) + + result = sorted(result, key=lambda f: f["id"]) + + response = app.response_class( + response=json.dumps(result, ensure_ascii=False, indent=1), + status=200, + mimetype='application/json' + ) + response.headers.add("Access-Control-Allow-Origin", "*") + + return response \ No newline at end of file diff --git a/resources/Kingdom/item.py b/resources/Kingdom/item.py new file mode 100644 index 0000000..13c18ee --- /dev/null +++ b/resources/Kingdom/item.py @@ -0,0 +1,16 @@ +from flask_restful import Resource +from flask import current_app as app +from flask import request + +class Kingdom_Item(Resource): + def get(self, id:int): + result = app.databases["Kingdom"].get_item(id) + + response = app.response_class( + response=result, + status=200, + mimetype='application/json' + ) + + response.headers.add("Access-Control-Allow-Origin", "*") + return response \ No newline at end of file diff --git a/resources/Kingdom/items.py b/resources/Kingdom/items.py new file mode 100644 index 0000000..73e41c8 --- /dev/null +++ b/resources/Kingdom/items.py @@ -0,0 +1,23 @@ +import json +from flask_restful import Resource +from flask import current_app as app, jsonify + +class Kingdom_Items(Resource): + def get(self): + db = app.databases["Kingdom"] + result = [] + for key in db.kfk_en_item.indexed_data.keys(): + new_value = db.kfk_en_item.indexed_data[key] + new_value["id"] = key + result.append(new_value) + + result = sorted(result, key=lambda f: f["id"]) + + response = app.response_class( + response=json.dumps(result, ensure_ascii=False, indent=1), + status=200, + mimetype='application/json' + ) + response.headers.add("Access-Control-Allow-Origin", "*") + + return response \ No newline at end of file diff --git a/resources/average.py b/resources/average.py new file mode 100644 index 0000000..024f4c4 --- /dev/null +++ b/resources/average.py @@ -0,0 +1,36 @@ +import requests +from datetime import datetime +from flask_restful import Resource +from flask import jsonify, make_response +from resources.shared import API_URL, VALID_CURRENCIES_A, ERROR_CURRENCY_CODE, ERROR_DATE_FORMAT, ERROR_NO_DATA + +class AverageRate(Resource): + def get(self, date : str, currency : str): + """ + Returns average exchange rate for the specified currency and date. + + Weekends and holidays return code 404. + """ + if not currency.upper() in VALID_CURRENCIES_A: + return make_response(jsonify({"error": ERROR_CURRENCY_CODE}), 400) + + try: + date_temp = datetime.strptime(date, "%Y-%m-%d") + + if date_temp.weekday() in [5,6]: + return make_response(jsonify({"error": ERROR_NO_DATA}), 404) + + #ensure correct date string format + date = date_temp.strftime("%Y-%m-%d") + except: + return make_response(jsonify({"error": ERROR_DATE_FORMAT}), 400) + + url = f"{API_URL}/exchangerates/rates/a/{currency}/{date}/" + response = requests.get(url) + if response.status_code == 200: + data = response.json() + return make_response(jsonify({"mid": data["rates"][0]["mid"]}), 200) + elif response.status_code == 404: + return make_response(jsonify({"error": ERROR_NO_DATA}), 404) + else: + return make_response(jsonify({"error": response.text}), 400) \ No newline at end of file diff --git a/resources/difference_last.py b/resources/difference_last.py new file mode 100644 index 0000000..82befd5 --- /dev/null +++ b/resources/difference_last.py @@ -0,0 +1,34 @@ +import requests +from flask_restful import Resource +from flask import jsonify, make_response +from resources.shared import API_URL, ERROR_CURRENCY_CODE, ERROR_DATE_RANGE, VALID_CURRENCIES_C + +class DifferenceLast(Resource): + def get(self, currency : str, num_days : int): + """ + Returns the highest difference between "ask" and "bid" values, and the day it occured. + + Weekends and holidays are skipped and do not count towards the 'num_days' limit." + """ + if not currency.upper() in VALID_CURRENCIES_C: + return make_response(jsonify({"error": ERROR_CURRENCY_CODE}), 400) + + if not (num_days > 0 and num_days < 256): + return make_response(jsonify({"error": ERROR_DATE_RANGE}), 400) + + url = f"{API_URL}/exchangerates/rates/c/{currency}/last/{num_days}/" + response = requests.get(url) + if response.status_code == 200: + data = response.json() + result = {"diff_max": 0, "date":""} + for rate in data["rates"]: + #ensure correct precision + difference = round(rate["ask"] - rate["bid"],4) + #spread can be negative on rare occasions + if abs(difference) > abs(result["diff_max"]): + result["diff_max"] = difference + result["date"] = rate["effectiveDate"] + + return make_response(jsonify(result), 200) + else: + return make_response(jsonify({"error": response.text}), 400) \ No newline at end of file diff --git a/resources/minmax_last.py b/resources/minmax_last.py new file mode 100644 index 0000000..30edbb3 --- /dev/null +++ b/resources/minmax_last.py @@ -0,0 +1,38 @@ +import requests +from flask_restful import Resource +from flask import jsonify, make_response +from resources.shared import API_URL, ERROR_CURRENCY_CODE, ERROR_DATE_RANGE, VALID_CURRENCIES_A + +class MinMaxLast(Resource): + def get(self, currency : str, num_days : int): + """ + Returns minimum and maximum average exchange rate values, and the days they occured. + + Weekends and holidays are skipped and do not count towards the 'num_days' limit." + """ + if not currency.upper() in VALID_CURRENCIES_A: + return make_response(jsonify({"error": ERROR_CURRENCY_CODE}), 400) + + if not (num_days > 0 and num_days < 256): + return make_response(jsonify({"error": ERROR_DATE_RANGE}), 400) + + url = f"{API_URL}/exchangerates/rates/a/{currency}/last/{num_days}/" + response = requests.get(url) + if response.status_code == 200: + data = response.json() + result = {"mid_min": data["rates"][0]["mid"], "date_min":"", "mid_max": data["rates"][0]["mid"], "date_max":""} + for rate in data["rates"]: + #if rates are equal, return the more recent one + #recent values are at the end of the list + + if rate["mid"] >= result["mid_max"]: + result["mid_max"] = rate["mid"] + result["date_max"] = rate["effectiveDate"] + + if rate["mid"] <= result["mid_min"]: + result["mid_min"] = rate["mid"] + result["date_min"] = rate["effectiveDate"] + + return make_response(jsonify(result), 200) + else: + return make_response(jsonify({"error": response.text}), 400) diff --git a/resources/shared.py b/resources/shared.py new file mode 100644 index 0000000..e4475d9 --- /dev/null +++ b/resources/shared.py @@ -0,0 +1,13 @@ +# Root url for the NBP API +API_URL = "https://api.nbp.pl/api/" + +# List of currencies accepted by NBP as of 2023-04-21 +# https://nbp.pl/en/statistic-and-financial-reporting/rates/table-a/ +# https://nbp.pl/en/statistic-and-financial-reporting/rates/table-c/ +VALID_CURRENCIES_A = ["AUD","THB","BRL","BGN","CAD","CLP","CZK","DKK","EUR","HUF","HKD","UAH","ISK","INR","MYR","MXN","ILS","NZD","NOK","PHP","GBP","ZAR","RON","IDR","SGD","SEK","CHF","TRY","USD","KRW","JPY","CNY","XDR" ] +VALID_CURRENCIES_C = ["AUD","CAD","CZK","DKK","EUR","HUF","NOK","GBP","SEK","CHF","USD","JPY","XDR"] + +ERROR_CURRENCY_CODE = "Invalid currency code." +ERROR_DATE_FORMAT = "Incorrect date format, expected YYYY-MM-DD" +ERROR_DATE_RANGE = "Invalid currency code." +ERROR_NO_DATA = "Exchange rate data is not available for this date." \ No newline at end of file diff --git a/wsgi.py b/wsgi.py new file mode 100644 index 0000000..e24c109 --- /dev/null +++ b/wsgi.py @@ -0,0 +1,5 @@ +from app import app + + +if __name__ == '__main__': + app.run() \ No newline at end of file