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/
.vscode/
.idea/**/workspace.xml
.idea/**/tasks.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.openapi.utils import get_openapi
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import JSONResponse
from pydantic import ValidationError
from src.errors import Response, NOT_FOUND
from src.routes.achievements import achievements
from src.routes.adventureranks import adventure_ranks
from src.routes.animals import animals
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
from src.routes import *
from src.routes.api import api
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,
geographies, materials, namecards, outfits, talent_material_types, weapon_material_types, weapons, gliders,
tcg]
tcg, api]
for router in routers:
app.include_router(router)
@ -72,3 +57,11 @@ def 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
uvicorn
uvicorn
pydantic
python-memcached
python-dotenv

View File

@ -1,6 +1,10 @@
version = '1.0'
DEFAULT_QUERY = ['English']
SERVER_VERSION = '1.1'
GAME_VERSION = '4.2.0'
DEFAULT_QUERY = ['English', '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'

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.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)
@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}')
async def get_character(
name: str, query_languages: str = 'eng', result_language: str = 'ru',

View File

@ -10,15 +10,17 @@ geographies = APIRouter(prefix='/geographies', tags=['Geographies'])
@geographies.get('/')
async def get_geographies(query_field: str = 'names', query_languages: str = 'eng') -> Response:
query_langs = parse_query_langs(query_languages)
response: list[dict[str, list[str]]] = []
response: list = []
for query_lang in query_langs:
chars = load_index(query_lang, 'geographies')
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)
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)
result_lang = parse_result_lang(result_language)
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.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'])
@ -18,8 +18,26 @@ async def get_weapons(query_field: str = 'names', result_language: str = 'eng')
@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)
result_lang = parse_result_lang(result_language)
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):
card: str
portrait: str
icon: str
sideicon: str
cover1: str
cover2: str
hoyolab_avatar: str = Field(..., alias='hoyolab-avatar')
nameicon: str
nameiconcard: str
namegachasplash: str
namegachaslice: str
namesideicon: str
card: Optional[str] = None
portrait: Optional[str] = None
icon: Optional[str] = None
sideicon: Optional[str] = None
cover1: Optional[str] = None
cover2: Optional[str] = None
hoyolab_avatar: Optional[str] = Field(None, alias='hoyolab-avatar')
nameicon: Optional[str] = None
nameiconcard: Optional[str] = None
namegachasplash: Optional[str] = None
namegachaslice: Optional[str] = None
namesideicon: Optional[str] = None
class Base(BaseModel):

View File

@ -7,4 +7,4 @@ class Element(BaseModel):
color: str
region: str
archon: str
theme: str
theme: str

View File

@ -1,4 +1,4 @@
from typing import List
from typing import List, Optional
from pydantic import BaseModel
@ -17,6 +17,40 @@ class Costs(BaseModel):
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):
name: str
description: str
@ -35,3 +69,7 @@ class Weapon(BaseModel):
r5: List[str]
weaponmaterialtype: str
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 dotenv import load_dotenv
from fastapi import HTTPException
from memcache import Client
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):
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:
return load(f)
json = f.read()
save_cache(cache_key, json)
return loads(json)
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:
return load(f)
json = f.read()
save_cache(cache_key, json)
return loads(json)
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:
return load(f)
json = f.read()
save_cache(cache_key, json)
return loads(json)
def get_file_name(query, category, langs) -> str:
for lang in langs:
index: dict[str, str] = load_index(lang, category)['names']
for key, value in index.items():
for k in key.lower().split(' '):
if k.startswith(query.lower()):