Compare commits
4 Commits
Author | SHA1 | Date |
---|---|---|
ScuroNeko | 341a311eab | |
ScuroNeko | fb5c53a6b7 | |
ScuroNeko | 4f152233ca | |
ScuroNeko | 5a2c1d9bc7 |
|
@ -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
|
||||||
|
|
|
@ -0,0 +1,4 @@
|
||||||
|
{
|
||||||
|
"python.analysis.typeCheckingMode": "basic",
|
||||||
|
"python.analysis.autoImportCompletions": true
|
||||||
|
}
|
33
main.py
33
main.py
|
@ -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=["*"],
|
||||||
|
)
|
||||||
|
|
|
@ -1,2 +1,6 @@
|
||||||
fastapi
|
fastapi
|
||||||
uvicorn
|
uvicorn
|
||||||
|
pydantic
|
||||||
|
|
||||||
|
python-memcached
|
||||||
|
python-dotenv
|
|
@ -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'
|
||||||
|
|
|
@ -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
|
|
@ -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()
|
|
@ -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',
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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)
|
||||||
|
|
|
@ -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):
|
||||||
|
|
|
@ -7,4 +7,4 @@ class Element(BaseModel):
|
||||||
color: str
|
color: str
|
||||||
region: str
|
region: str
|
||||||
archon: str
|
archon: str
|
||||||
theme: str
|
theme: str
|
||||||
|
|
|
@ -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
|
||||||
|
|
48
src/utils.py
48
src/utils.py
|
@ -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()):
|
||||||
|
|
Loading…
Reference in New Issue