Compare commits

..

No commits in common. "dev" and "main" have entirely different histories.
dev ... main

14 changed files with 49 additions and 266 deletions

3
.gitignore vendored
View File

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

View File

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

33
main.py
View File

@ -2,14 +2,29 @@ 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 import * from src.routes.achievements import achievements
from src.routes.adventureranks import adventure_ranks
from src.routes.api import api 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
app = FastAPI(title='Genshin Impact DB') app = FastAPI(title='Genshin Impact DB')
@ -32,7 +47,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, api] tcg]
for router in routers: for router in routers:
app.include_router(router) app.include_router(router)
@ -57,11 +72,3 @@ 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,6 +1,2 @@
fastapi fastapi
uvicorn uvicorn
pydantic
python-memcached
python-dotenv

View File

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

View File

@ -1,21 +0,0 @@
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

View File

@ -1,58 +0,0 @@
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,6 +1,4 @@
from os import walk from fastapi import APIRouter, HTTPException
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
@ -19,36 +17,6 @@ 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,17 +10,15 @@ 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 = [] response: list[dict[str, list[str]]] = []
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(error=False, response=response) return Response[list[dict[str, list[str]]]](error=False, response=response)
@geographies.get('/{query}', response_model_exclude_none=True) @geographies.get('/{query}', response_model_exclude_none=True)
async def get_geography( async def get_geography(query: str, query_languages: str = 'eng', result_language: str = 'ru') -> Response[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_file, load_index, parse_query_langs, parse_result_lang, load_category, get_file_name from src.utils import 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,26 +18,8 @@ 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( async def get_weapon(query: str, query_languages: str = 'eng', result_language: str = 'ru') -> Response[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: Optional[str] = None card: str
portrait: Optional[str] = None portrait: str
icon: Optional[str] = None icon: str
sideicon: Optional[str] = None sideicon: str
cover1: Optional[str] = None cover1: str
cover2: Optional[str] = None cover2: str
hoyolab_avatar: Optional[str] = Field(None, alias='hoyolab-avatar') hoyolab_avatar: str = Field(..., alias='hoyolab-avatar')
nameicon: Optional[str] = None nameicon: str
nameiconcard: Optional[str] = None nameiconcard: str
namegachasplash: Optional[str] = None namegachasplash: str
namegachaslice: Optional[str] = None namegachaslice: str
namesideicon: Optional[str] = None namesideicon: str
class Base(BaseModel): class Base(BaseModel):

View File

@ -1,4 +1,4 @@
from typing import List, Optional from typing import List
from pydantic import BaseModel from pydantic import BaseModel
@ -17,40 +17,6 @@ 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
@ -69,7 +35,3 @@ 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,69 +1,29 @@
from json import load, loads, dumps from json import load
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:
json = f.read() return load(f)
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:
json = f.read() return load(f)
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:
json = f.read() return load(f)
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()):