Compare commits

...

4 Commits
main ... dev

Author SHA1 Message Date
ScuroNeko 341a311eab added caching 2023-11-18 15:40:34 +03:00
ScuroNeko fb5c53a6b7 added languages route 2023-11-12 21:43:27 +03:00
ScuroNeko 4f152233ca added version route; fix images 2023-11-12 19:22:49 +03:00
ScuroNeko 5a2c1d9bc7 weapon img, stats, url 2023-11-12 05:24:04 +03:00
14 changed files with 266 additions and 49 deletions

3
.gitignore vendored
View File

@ -1,4 +1,7 @@
.env
.idea/ .idea/
.vscode/
.idea/**/workspace.xml .idea/**/workspace.xml
.idea/**/tasks.xml .idea/**/tasks.xml
.idea/**/usage.statistics.xml .idea/**/usage.statistics.xml

4
.vscode/settings.json vendored Normal file
View File

@ -0,0 +1,4 @@
{
"python.analysis.typeCheckingMode": "basic",
"python.analysis.autoImportCompletions": true
}

33
main.py
View File

@ -2,29 +2,14 @@ from json import loads
from fastapi import FastAPI from fastapi import FastAPI
from fastapi.openapi.utils import get_openapi from fastapi.openapi.utils import get_openapi
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse from fastapi.responses import JSONResponse
from pydantic import ValidationError from pydantic import ValidationError
from src.errors import Response, NOT_FOUND from src.errors import Response, NOT_FOUND
from src.routes.achievements import achievements from src.routes import *
from src.routes.adventureranks import adventure_ranks
from src.routes.animals import animals from src.routes.api import api
from src.routes.artifacts import artifacts
from src.routes.characters import characters
from src.routes.crafts import crafts
from src.routes.domains import domains
from src.routes.elements import elements
from src.routes.enemies import enemies
from src.routes.foods import foods
from src.routes.geographies import geographies
from src.routes.glider import gliders
from src.routes.materials import materials
from src.routes.namecards import namecards
from src.routes.outfits import outfits
from src.routes.talentmaterialtypes import talent_material_types
from src.routes.tcg import tcg
from src.routes.weaponmaterialtypes import weapon_material_types
from src.routes.weapons import weapons
app = FastAPI(title='Genshin Impact DB') app = FastAPI(title='Genshin Impact DB')
@ -47,7 +32,7 @@ async def validation_error(_, e: ValidationError):
routers = [achievements, adventure_ranks, animals, artifacts, characters, crafts, domains, elements, enemies, foods, routers = [achievements, adventure_ranks, animals, artifacts, characters, crafts, domains, elements, enemies, foods,
geographies, materials, namecards, outfits, talent_material_types, weapon_material_types, weapons, gliders, geographies, materials, namecards, outfits, talent_material_types, weapon_material_types, weapons, gliders,
tcg] tcg, api]
for router in routers: for router in routers:
app.include_router(router) app.include_router(router)
@ -72,3 +57,11 @@ def custom_openapi():
app.openapi = custom_openapi app.openapi = custom_openapi
app.add_middleware(
CORSMiddleware,
allow_origins=['*'],
allow_credentials=True,
allow_methods=["*"],
allow_headers=["*"],
)

View File

@ -1,2 +1,6 @@
fastapi fastapi
uvicorn uvicorn
pydantic
python-memcached
python-dotenv

View File

@ -1,6 +1,10 @@
version = '1.0' SERVER_VERSION = '1.1'
DEFAULT_QUERY = ['English'] GAME_VERSION = '4.2.0'
DEFAULT_QUERY = ['English', 'Russian']
DEFAULT_RESULT = 'Russian' DEFAULT_RESULT = 'Russian'
LANGUAGES = ['English', 'Russian', 'Japanese'] LANGUAGES = ['English', 'French', 'German', 'Indonesian', 'Italian',
'Japanese', 'Korean', 'Portuguese', 'Russian', 'Spanish',
'Thai', 'Turkish', 'Vietnamese']
DATA_FOLDER = 'data' DATA_FOLDER = 'data'

21
src/routes/__init__.py Normal file
View File

@ -0,0 +1,21 @@
from .api import api
from .achievements import achievements
from .adventureranks import adventure_ranks
from .animals import animals
from .artifacts import artifacts
from .characters import characters
from .crafts import crafts
from .domains import domains
from .elements import elements
from .enemies import enemies
from .foods import foods
from .geographies import geographies
from .glider import gliders
from .materials import materials
from .namecards import namecards
from .outfits import outfits
from .talentmaterialtypes import talent_material_types
from .tcg import tcg
from .weaponmaterialtypes import weapon_material_types
from .weapons import weapons

58
src/routes/api.py Normal file
View File

@ -0,0 +1,58 @@
from fastapi import APIRouter
from subprocess import check_output
from src.constants import GAME_VERSION, SERVER_VERSION, LANGUAGES
from src.errors import Response
api = APIRouter(prefix='/api', tags=['Service'])
@api.get('/version')
async def get_versions():
git_server = git_info('.')
git_data = git_info('./data')
response = {'server_version': SERVER_VERSION,
'game_version': GAME_VERSION,
'git_server': git_server,
'git_data': git_data}
return Response(response=response)
@api.get('/languages')
async def get_languages():
return Response(response=LANGUAGES)
def git_info(path: str):
data_hash = git_revision_hash(path)
git = {
'commit': git_revision_short_hash(path),
'commit_hash': data_hash,
'branch': git_branch(path),
'release': git_text(data_hash, path)
}
return git
def git_revision_short_hash(cwd=None) -> str:
return check_out(['git', 'rev-parse', '--short', 'HEAD'], cwd)
def git_revision_hash(cwd=None) -> str:
return check_out(['git', 'rev-parse', 'HEAD'], cwd)
def git_branch(cwd=None) -> str:
return check_out(['git', 'branch', '--show-current'], cwd)
def git_text(commit, cwd=None) -> str:
return check_out(['git', 'log', '--format=%B', '-n', '1', commit], cwd)
def check_out(args, cwd=None):
if not cwd:
cwd = './'
return check_output(args, cwd=cwd).decode('ascii').strip()

View File

@ -1,4 +1,6 @@
from fastapi import APIRouter, HTTPException from os import walk
from typing import List
from fastapi import APIRouter
from src.errors import Response from src.errors import Response
from src.types.characters import Character, Constellations, Talents from src.types.characters import Character, Constellations, Talents
@ -17,6 +19,36 @@ async def get_characters(query_field: str = 'names', query_languages: str = 'eng
return Response(error=False, response=response) return Response(error=False, response=response)
@characters.get('/all', response_model_exclude_none=True)
async def get_all_characters(
result_language: str = 'ru',
images: bool = False, stats: bool = False, url: bool = False
) -> Response[List[Character]]:
result_lang = parse_result_lang(result_language)
chars = []
version_file = load_file('version', 'characters')
images_file = load_file('image', 'characters')
stats_file = load_file('stats', 'characters')
url_file = load_file('url', 'characters')
for _, _, files in walk(f'./data/{result_lang}/characters'):
for filename in files:
character_name = filename[:-5]
char = load_category(result_lang, 'characters', character_name)
if images:
char.update({'images': images_file[character_name]})
if stats:
char.update({'stats': stats_file[character_name]})
if url:
if character_name in url_file:
char.update({'url': url_file[character_name]})
char.update({'version': version_file[character_name]})
chars.append(char)
return Response(response=chars)
@characters.get('/{name}') @characters.get('/{name}')
async def get_character( async def get_character(
name: str, query_languages: str = 'eng', result_language: str = 'ru', name: str, query_languages: str = 'eng', result_language: str = 'ru',

View File

@ -10,15 +10,17 @@ geographies = APIRouter(prefix='/geographies', tags=['Geographies'])
@geographies.get('/') @geographies.get('/')
async def get_geographies(query_field: str = 'names', query_languages: str = 'eng') -> Response: async def get_geographies(query_field: str = 'names', query_languages: str = 'eng') -> Response:
query_langs = parse_query_langs(query_languages) query_langs = parse_query_langs(query_languages)
response: list[dict[str, list[str]]] = [] response: list = []
for query_lang in query_langs: for query_lang in query_langs:
chars = load_index(query_lang, 'geographies') chars = load_index(query_lang, 'geographies')
response.append({query_lang: list(chars[query_field].keys())}) response.append({query_lang: list(chars[query_field].keys())})
return Response[list[dict[str, list[str]]]](error=False, response=response) return Response(error=False, response=response)
@geographies.get('/{query}', response_model_exclude_none=True) @geographies.get('/{query}', response_model_exclude_none=True)
async def get_geography(query: str, query_languages: str = 'eng', result_language: str = 'ru') -> Response[Geography]: async def get_geography(
query: str, query_languages: str = 'eng', result_language: str = 'ru'
) -> Response[Geography]:
query_langs = parse_query_langs(query_languages) query_langs = parse_query_langs(query_languages)
result_lang = parse_result_lang(result_language) result_lang = parse_result_lang(result_language)
filename = get_file_name(query, 'geographies', query_langs) filename = get_file_name(query, 'geographies', query_langs)

View File

@ -2,7 +2,7 @@ from fastapi import APIRouter
from src.errors import Response from src.errors import Response
from src.types.weapons import Weapon from src.types.weapons import Weapon
from src.utils import load_index, parse_query_langs, parse_result_lang, load_category, get_file_name from src.utils import load_file, load_index, parse_query_langs, parse_result_lang, load_category, get_file_name
weapons = APIRouter(prefix='/weapons', tags=['Weapons']) weapons = APIRouter(prefix='/weapons', tags=['Weapons'])
@ -18,8 +18,26 @@ async def get_weapons(query_field: str = 'names', result_language: str = 'eng')
@weapons.get('/{query}', response_model_exclude_none=True) @weapons.get('/{query}', response_model_exclude_none=True)
async def get_weapon(query: str, query_languages: str = 'eng', result_language: str = 'ru') -> Response[Weapon]: async def get_weapon(
query: str, query_languages: str = 'eng', result_language: str = 'ru',
images: bool = False, stats: bool = False, url: bool = False
) -> Response[Weapon]:
query_langs = parse_query_langs(query_languages) query_langs = parse_query_langs(query_languages)
result_lang = parse_result_lang(result_language) result_lang = parse_result_lang(result_language)
filename = get_file_name(query, 'weapons', query_langs) filename = get_file_name(query, 'weapons', query_langs)
return Response[Weapon](response=load_category(result_lang, 'weapons', filename))
response = load_category(result_lang, 'weapons', filename)
if images:
images_file = load_file('image', 'weapons')
response.update({'images': images_file[filename]})
if stats:
stats_file = load_file('stats', 'weapons')
response.update({'stats': stats_file[filename]})
if url:
url_file = load_file('url', 'weapons')
response.update({'url': url_file[filename]})
version_file = load_file('version', 'weapons')
response.update({'version': version_file[filename]})
return Response[Weapon](response=response)

View File

@ -25,18 +25,18 @@ class Costs(BaseModel):
class Images(BaseModel): class Images(BaseModel):
card: str card: Optional[str] = None
portrait: str portrait: Optional[str] = None
icon: str icon: Optional[str] = None
sideicon: str sideicon: Optional[str] = None
cover1: str cover1: Optional[str] = None
cover2: str cover2: Optional[str] = None
hoyolab_avatar: str = Field(..., alias='hoyolab-avatar') hoyolab_avatar: Optional[str] = Field(None, alias='hoyolab-avatar')
nameicon: str nameicon: Optional[str] = None
nameiconcard: str nameiconcard: Optional[str] = None
namegachasplash: str namegachasplash: Optional[str] = None
namegachaslice: str namegachaslice: Optional[str] = None
namesideicon: str namesideicon: Optional[str] = None
class Base(BaseModel): class Base(BaseModel):

View File

@ -1,4 +1,4 @@
from typing import List from typing import List, Optional
from pydantic import BaseModel from pydantic import BaseModel
@ -17,6 +17,40 @@ class Costs(BaseModel):
ascend6: List[AscendItem] ascend6: List[AscendItem]
class URL(BaseModel):
fandom: str
class Images(BaseModel):
nameicon: str
namegacha: str
icon: str
nameawakenicon: str
awakenicon: Optional[str] = None
class Base(BaseModel):
attack: float
specialized: float
class Curve(BaseModel):
attack: str
specialized: str
class PromotionItem(BaseModel):
maxlevel: int
attack: float
class Stats(BaseModel):
base: Base
curve: Curve
specialized: str
promotion: List[PromotionItem]
class Weapon(BaseModel): class Weapon(BaseModel):
name: str name: str
description: str description: str
@ -35,3 +69,7 @@ class Weapon(BaseModel):
r5: List[str] r5: List[str]
weaponmaterialtype: str weaponmaterialtype: str
costs: Costs costs: Costs
url: Optional[URL] = None
images: Optional[Images] = None
stats: Optional[Stats] = None
version: str

View File

@ -1,29 +1,69 @@
from json import load from json import load, loads, dumps
from os import environ
from os.path import join from os.path import join
from dotenv import load_dotenv
from fastapi import HTTPException from fastapi import HTTPException
from memcache import Client
from src.constants import LANGUAGES, DEFAULT_RESULT, DEFAULT_QUERY, DATA_FOLDER from src.constants import LANGUAGES, DEFAULT_RESULT, DEFAULT_QUERY, DATA_FOLDER
load_dotenv()
memcache_ip = environ.get('MEMCACHE_IP')
memcache_port = environ.get('MEMCACHE_PORT') or 11211
mc = Client([f'{memcache_ip}:{memcache_port}'], debug=1)
def load_cached(key):
value = mc.get(key)
if not value:
return None
print(f'loaded {key} from cache')
return loads(value)
def save_cache(key, value: str | dict):
if type(value) is dict:
mc.set(key, dumps(value), 86400)
else:
mc.set(key, value, 86400)
def load_index(language: str, category: str): def load_index(language: str, category: str):
cache_key = join('index', language, category)
if cache := load_cached(cache_key):
return cache
with open(join(DATA_FOLDER, 'index', language, f'{category}.json'), 'r', encoding='utf-8') as f: with open(join(DATA_FOLDER, 'index', language, f'{category}.json'), 'r', encoding='utf-8') as f:
return load(f) json = f.read()
save_cache(cache_key, json)
return loads(json)
def load_category(lang: str, category: str, name: str) -> dict: def load_category(lang: str, category: str, name: str) -> dict:
cache_key = join(lang, category, name)
if cache := load_cached(cache_key):
return cache
with open(join(DATA_FOLDER, lang, category, f'{name}.json'), 'r', encoding='utf-8') as f: with open(join(DATA_FOLDER, lang, category, f'{name}.json'), 'r', encoding='utf-8') as f:
return load(f) json = f.read()
save_cache(cache_key, json)
return loads(json)
def load_file(folder: str, name: str) -> dict: def load_file(folder: str, name: str) -> dict:
cache_key = join(folder, name)
if cache := load_cached(cache_key):
return cache
with open(join(DATA_FOLDER, folder, f'{name}.json'), 'r', encoding='utf-8') as f: with open(join(DATA_FOLDER, folder, f'{name}.json'), 'r', encoding='utf-8') as f:
return load(f) json = f.read()
save_cache(cache_key, json)
return loads(json)
def get_file_name(query, category, langs) -> str: def get_file_name(query, category, langs) -> str:
for lang in langs: for lang in langs:
index: dict[str, str] = load_index(lang, category)['names'] index: dict[str, str] = load_index(lang, category)['names']
for key, value in index.items(): for key, value in index.items():
for k in key.lower().split(' '): for k in key.lower().split(' '):
if k.startswith(query.lower()): if k.startswith(query.lower()):