mirror of
https://github.com/arcan1s/ffxivbis.git
synced 2025-04-25 01:37:17 +00:00
initial swagger impl, small refactoring and move create/delete user under admin perms
This commit is contained in:
parent
f63b64e77c
commit
c83f36f40b
1
setup.py
1
setup.py
@ -28,6 +28,7 @@ setup(
|
|||||||
'aiohttp',
|
'aiohttp',
|
||||||
'aiohttp_jinja2',
|
'aiohttp_jinja2',
|
||||||
'aiohttp_security',
|
'aiohttp_security',
|
||||||
|
'apispec',
|
||||||
'Jinja2',
|
'Jinja2',
|
||||||
'passlib',
|
'passlib',
|
||||||
'requests',
|
'requests',
|
||||||
|
@ -35,6 +35,7 @@ class AuthorizationPolicy(AbstractAuthorizationPolicy):
|
|||||||
|
|
||||||
def authorize_factory() -> Callable:
|
def authorize_factory() -> Callable:
|
||||||
allowed_paths = {'/', '/favicon.ico', '/api/v1/login', '/api/v1/logout'}
|
allowed_paths = {'/', '/favicon.ico', '/api/v1/login', '/api/v1/logout'}
|
||||||
|
allowed_paths_groups = {'/api-docs', '/static'}
|
||||||
|
|
||||||
@middleware
|
@middleware
|
||||||
async def authorize(request: Request, handler: Callable) -> Response:
|
async def authorize(request: Request, handler: Callable) -> Response:
|
||||||
@ -42,7 +43,8 @@ def authorize_factory() -> Callable:
|
|||||||
permission = 'admin'
|
permission = 'admin'
|
||||||
else:
|
else:
|
||||||
permission = 'get' if request.method in ('GET', 'HEAD') else 'post'
|
permission = 'get' if request.method in ('GET', 'HEAD') else 'post'
|
||||||
if request.path not in allowed_paths:
|
if request.path not in allowed_paths \
|
||||||
|
and not any(request.path.startswith(path) for path in allowed_paths_groups):
|
||||||
await check_permission(request, permission)
|
await check_permission(request, permission)
|
||||||
|
|
||||||
return await handler(request)
|
return await handler(request)
|
||||||
|
@ -13,6 +13,7 @@ from .views.api.login import LoginView
|
|||||||
from .views.api.logout import LogoutView
|
from .views.api.logout import LogoutView
|
||||||
from .views.api.loot import LootView
|
from .views.api.loot import LootView
|
||||||
from .views.api.player import PlayerView
|
from .views.api.player import PlayerView
|
||||||
|
from .views.html.api import ApiDocVIew, ApiHtmlView
|
||||||
from .views.html.bis import BiSHtmlView
|
from .views.html.bis import BiSHtmlView
|
||||||
from .views.html.index import IndexHtmlView
|
from .views.html.index import IndexHtmlView
|
||||||
from .views.html.loot import LootHtmlView
|
from .views.html.loot import LootHtmlView
|
||||||
@ -24,10 +25,10 @@ from .views.html.users import UsersHtmlView
|
|||||||
|
|
||||||
def setup_routes(app: Application) -> None:
|
def setup_routes(app: Application) -> None:
|
||||||
# api routes
|
# api routes
|
||||||
app.router.add_delete('/api/v1/login/{username}', LoginView)
|
app.router.add_delete('/admin/api/v1/login/{username}', LoginView)
|
||||||
app.router.add_post('/api/v1/login', LoginView)
|
app.router.add_post('/api/v1/login', LoginView)
|
||||||
app.router.add_post('/api/v1/logout', LogoutView)
|
app.router.add_post('/api/v1/logout', LogoutView)
|
||||||
app.router.add_put('/api/v1/login', LoginView)
|
app.router.add_put('/admin/api/v1/login', LoginView)
|
||||||
|
|
||||||
app.router.add_get('/api/v1/party', PlayerView)
|
app.router.add_get('/api/v1/party', PlayerView)
|
||||||
app.router.add_post('/api/v1/party', PlayerView)
|
app.router.add_post('/api/v1/party', PlayerView)
|
||||||
@ -44,6 +45,9 @@ def setup_routes(app: Application) -> None:
|
|||||||
app.router.add_get('/', IndexHtmlView)
|
app.router.add_get('/', IndexHtmlView)
|
||||||
app.router.add_get('/static/{resource_id}', StaticHtmlView)
|
app.router.add_get('/static/{resource_id}', StaticHtmlView)
|
||||||
|
|
||||||
|
app.router.add_get('/api-docs', ApiHtmlView)
|
||||||
|
app.router.add_get('/api-docs/swagger.json', ApiDocVIew)
|
||||||
|
|
||||||
app.router.add_get('/party', PlayerHtmlView)
|
app.router.add_get('/party', PlayerHtmlView)
|
||||||
app.router.add_post('/party', PlayerHtmlView)
|
app.router.add_post('/party', PlayerHtmlView)
|
||||||
|
|
||||||
|
72
src/service/api/spec.py
Normal file
72
src/service/api/spec.py
Normal file
@ -0,0 +1,72 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from aiohttp.web import Application
|
||||||
|
from apispec import APISpec
|
||||||
|
|
||||||
|
from service.core.version import __version__
|
||||||
|
from service.models.action import Action
|
||||||
|
from service.models.bis import BiS
|
||||||
|
from service.models.error import Error
|
||||||
|
from service.models.job import Job
|
||||||
|
from service.models.piece import Piece
|
||||||
|
from service.models.player import Player, PlayerId
|
||||||
|
from service.models.player_edit import PlayerEdit
|
||||||
|
from service.models.upgrade import Upgrade
|
||||||
|
|
||||||
|
|
||||||
|
def get_spec(app: Application) -> APISpec:
|
||||||
|
spec = APISpec(
|
||||||
|
title='FFXIV loot helper',
|
||||||
|
version=__version__,
|
||||||
|
openapi_version='3.0.2',
|
||||||
|
info=dict(description='Loot manager for FFXIV statics'),
|
||||||
|
)
|
||||||
|
|
||||||
|
# routes
|
||||||
|
for route in app.router.routes():
|
||||||
|
path = route.get_info().get('path') or route.get_info().get('formatter')
|
||||||
|
method = route.method.lower()
|
||||||
|
|
||||||
|
spec_method = f'endpoint_{method}_spec'
|
||||||
|
if not hasattr(route.handler, spec_method):
|
||||||
|
continue
|
||||||
|
operations = getattr(route.handler, spec_method)()
|
||||||
|
if not operations:
|
||||||
|
continue
|
||||||
|
|
||||||
|
spec.path(path, operations={method: operations})
|
||||||
|
|
||||||
|
# components
|
||||||
|
spec.components.schema(Action.model_name(), Action.model_spec())
|
||||||
|
spec.components.schema(BiS.model_name(), BiS.model_spec())
|
||||||
|
spec.components.schema(Error.model_name(), Error.model_spec())
|
||||||
|
spec.components.schema(Job.model_name(), Job.model_spec())
|
||||||
|
spec.components.schema(Piece.model_name(), Piece.model_spec())
|
||||||
|
spec.components.schema(Player.model_name(), Player.model_spec())
|
||||||
|
spec.components.schema(PlayerEdit.model_name(), PlayerEdit.model_spec())
|
||||||
|
spec.components.schema(PlayerId.model_name(), PlayerId.model_spec())
|
||||||
|
spec.components.schema(Upgrade.model_name(), Upgrade.model_spec())
|
||||||
|
|
||||||
|
# default responses
|
||||||
|
spec.components.response('BadRequest', dict(
|
||||||
|
description='Bad parameters applied or bad request was formed',
|
||||||
|
content={'application/json': {'schema': {'$ref': Error.model_ref('Error')}}}
|
||||||
|
))
|
||||||
|
spec.components.response('Forbidden', dict(
|
||||||
|
description='User permissions do not allow this action'
|
||||||
|
))
|
||||||
|
spec.components.response('ServerError', dict(
|
||||||
|
description='Server was unable to process request',
|
||||||
|
content={'application/json': {'schema': {'$ref': Error.model_ref('Error')}}}
|
||||||
|
))
|
||||||
|
spec.components.response('Unauthorized', dict(
|
||||||
|
description='User was not authorized'
|
||||||
|
))
|
||||||
|
|
||||||
|
return spec
|
@ -14,27 +14,29 @@ from typing import Any, Mapping, List
|
|||||||
from .json import HttpEncoder
|
from .json import HttpEncoder
|
||||||
|
|
||||||
|
|
||||||
def make_json(response: Any, args: Mapping[str, Any], code: int = 200) -> str:
|
def make_json(response: Any) -> str:
|
||||||
return json.dumps({
|
return json.dumps(response, cls=HttpEncoder, sort_keys=True)
|
||||||
'arguments': dict(args),
|
|
||||||
'response': response,
|
|
||||||
'status': code
|
|
||||||
}, cls=HttpEncoder, sort_keys=True)
|
|
||||||
|
|
||||||
|
|
||||||
def wrap_exception(exception: Exception, args: Mapping[str, Any], code: int = 500) -> Response:
|
def wrap_exception(exception: Exception, args: Mapping[str, Any], code: int = 500) -> Response:
|
||||||
if isinstance(exception, HTTPException):
|
if isinstance(exception, HTTPException):
|
||||||
raise exception # reraise return
|
raise exception # reraise return
|
||||||
return wrap_json({'message': repr(exception)}, args, code)
|
return wrap_json({
|
||||||
|
'message': repr(exception),
|
||||||
|
'arguments': dict(args)
|
||||||
|
}, code)
|
||||||
|
|
||||||
|
|
||||||
def wrap_invalid_param(params: List[str], args: Mapping[str, Any], code: int = 400) -> Response:
|
def wrap_invalid_param(params: List[str], args: Mapping[str, Any], code: int = 400) -> Response:
|
||||||
return wrap_json({'message': 'invalid or missing parameters: `{}`'.format(params)}, args, code)
|
return wrap_json({
|
||||||
|
'message': f'invalid or missing parameters: `{params}`',
|
||||||
|
'arguments': dict(args)
|
||||||
|
}, code)
|
||||||
|
|
||||||
|
|
||||||
def wrap_json(response: Any, args: Mapping[str, Any], code: int = 200) -> Response:
|
def wrap_json(response: Any, code: int = 200) -> Response:
|
||||||
return Response(
|
return Response(
|
||||||
text=make_json(response, args, code),
|
text=make_json(response),
|
||||||
status=code,
|
status=code,
|
||||||
content_type='application/json'
|
content_type='application/json'
|
||||||
)
|
)
|
||||||
|
@ -26,7 +26,7 @@ class BiSView(BiSBaseView):
|
|||||||
self.request.app.logger.exception('could not get bis')
|
self.request.app.logger.exception('could not get bis')
|
||||||
return wrap_exception(e, self.request.query)
|
return wrap_exception(e, self.request.query)
|
||||||
|
|
||||||
return wrap_json(loot, self.request.query)
|
return wrap_json(loot)
|
||||||
|
|
||||||
async def post(self) -> Response:
|
async def post(self) -> Response:
|
||||||
try:
|
try:
|
||||||
@ -51,7 +51,7 @@ class BiSView(BiSBaseView):
|
|||||||
self.request.app.logger.exception('could not add bis')
|
self.request.app.logger.exception('could not add bis')
|
||||||
return wrap_exception(e, data)
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
return wrap_json({'piece': piece, 'player_id': player_id}, data)
|
return wrap_json({'piece': piece, 'player_id': player_id})
|
||||||
|
|
||||||
async def put(self) -> Response:
|
async def put(self) -> Response:
|
||||||
try:
|
try:
|
||||||
@ -71,4 +71,4 @@ class BiSView(BiSBaseView):
|
|||||||
self.request.app.logger.exception('could not parse bis')
|
self.request.app.logger.exception('could not parse bis')
|
||||||
return wrap_exception(e, data)
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
return wrap_json({'link': link}, data)
|
return wrap_json({'link': link})
|
@ -23,7 +23,7 @@ class LoginView(LoginBaseView):
|
|||||||
self.request.app.logger.exception('cannot remove user')
|
self.request.app.logger.exception('cannot remove user')
|
||||||
return wrap_exception(e, {'username': username})
|
return wrap_exception(e, {'username': username})
|
||||||
|
|
||||||
return wrap_json({}, {'username': username})
|
return wrap_json({})
|
||||||
|
|
||||||
async def post(self) -> Response:
|
async def post(self) -> Response:
|
||||||
try:
|
try:
|
||||||
@ -41,7 +41,7 @@ class LoginView(LoginBaseView):
|
|||||||
self.request.app.logger.exception('cannot login user')
|
self.request.app.logger.exception('cannot login user')
|
||||||
return wrap_exception(e, data)
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
return wrap_json({}, data)
|
return wrap_json({})
|
||||||
|
|
||||||
async def put(self) -> Response:
|
async def put(self) -> Response:
|
||||||
try:
|
try:
|
||||||
@ -56,7 +56,7 @@ class LoginView(LoginBaseView):
|
|||||||
try:
|
try:
|
||||||
await self.create_user(data['username'], data['password'], data.get('permission', 'get'))
|
await self.create_user(data['username'], data['password'], data.get('permission', 'get'))
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.request.app.logger.exception('cannot login user')
|
self.request.app.logger.exception('cannot create user')
|
||||||
return wrap_exception(e, data)
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
return wrap_json({}, data)
|
return wrap_json({})
|
@ -18,4 +18,4 @@ class LogoutView(LoginBaseView):
|
|||||||
self.request.app.logger.exception('cannot logout user')
|
self.request.app.logger.exception('cannot logout user')
|
||||||
return wrap_exception(e, {})
|
return wrap_exception(e, {})
|
||||||
|
|
||||||
return wrap_json({}, {})
|
return wrap_json({})
|
@ -26,7 +26,7 @@ class LootView(LootBaseView):
|
|||||||
self.request.app.logger.exception('could not get loot')
|
self.request.app.logger.exception('could not get loot')
|
||||||
return wrap_exception(e, self.request.query)
|
return wrap_exception(e, self.request.query)
|
||||||
|
|
||||||
return wrap_json(loot, self.request.query)
|
return wrap_json(loot)
|
||||||
|
|
||||||
async def post(self) -> Response:
|
async def post(self) -> Response:
|
||||||
try:
|
try:
|
||||||
@ -51,7 +51,7 @@ class LootView(LootBaseView):
|
|||||||
self.request.app.logger.exception('could not add loot')
|
self.request.app.logger.exception('could not add loot')
|
||||||
return wrap_exception(e, data)
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
return wrap_json({'piece': piece, 'player_id': player_id}, data)
|
return wrap_json({'piece': piece, 'player_id': player_id})
|
||||||
|
|
||||||
async def put(self) -> Response:
|
async def put(self) -> Response:
|
||||||
try:
|
try:
|
||||||
@ -71,4 +71,4 @@ class LootView(LootBaseView):
|
|||||||
self.request.app.logger.exception('could not suggest loot')
|
self.request.app.logger.exception('could not suggest loot')
|
||||||
return wrap_exception(e, data)
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
return wrap_json(players, data)
|
return wrap_json(players)
|
115
src/service/api/views/api/openapi.py
Normal file
115
src/service/api/views/api/openapi.py
Normal file
@ -0,0 +1,115 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Optional, Type
|
||||||
|
|
||||||
|
from service.models.serializable import Serializable
|
||||||
|
|
||||||
|
|
||||||
|
class OpenApi(Serializable):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_delete_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_description(cls: Type[OpenApi]) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_summary(cls: Type[OpenApi]) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_tags(cls: Type[OpenApi]) -> List[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
|
||||||
|
description = cls.endpoint_get_description()
|
||||||
|
if description is None:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
'description': description,
|
||||||
|
'parameters': cls.endpoint_get_parameters(),
|
||||||
|
'responses': cls.endpoint_with_default_responses(cls.endpoint_get_responses()),
|
||||||
|
'summary': cls.endpoint_get_summary(),
|
||||||
|
'tags': cls.endpoint_get_tags()
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_consumes(cls: Type[OpenApi]) -> List[str]:
|
||||||
|
return ['application/json']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> str:
|
||||||
|
return ''
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
|
||||||
|
return None
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
|
||||||
|
description = cls.endpoint_post_description()
|
||||||
|
if description is None:
|
||||||
|
return {}
|
||||||
|
return {
|
||||||
|
'consumes': cls.endpoint_post_consumes(),
|
||||||
|
'description': description,
|
||||||
|
'requestBody': {
|
||||||
|
'content': {
|
||||||
|
content_type: {
|
||||||
|
'schema': {'$ref': cls.model_ref(cls.endpoint_post_request_body(content_type))}
|
||||||
|
}
|
||||||
|
for content_type in cls.endpoint_post_consumes()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'responses': cls.endpoint_with_default_responses(cls.endpoint_post_responses()),
|
||||||
|
'summary': cls.endpoint_post_summary(),
|
||||||
|
'tags': cls.endpoint_post_tags()
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_spec(cls: Type[OpenApi], operations: List[str]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
operation.lower(): getattr(cls, f'endpoint_{operation.lower()}_spec')
|
||||||
|
for operation in operations
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_with_default_responses(cls: Type[OpenApi], responses: Dict[str, Any]) -> Dict[str, Any]:
|
||||||
|
responses.update({
|
||||||
|
'400': {'$ref': cls.model_ref('BadRequest', 'responses')},
|
||||||
|
'401': {'$ref': cls.model_ref('Unauthorized', 'responses')},
|
||||||
|
'403': {'$ref': cls.model_ref('Forbidden', 'responses')},
|
||||||
|
'500': {'$ref': cls.model_ref('ServerError', 'responses')}
|
||||||
|
})
|
||||||
|
return responses
|
@ -7,14 +7,69 @@
|
|||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
#
|
#
|
||||||
from aiohttp.web import Response
|
from aiohttp.web import Response
|
||||||
|
from typing import Any, Dict, List, Optional, Type
|
||||||
|
|
||||||
from service.models.job import Job
|
from service.models.job import Job
|
||||||
|
|
||||||
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json
|
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json
|
||||||
from service.api.views.common.player_base import PlayerBaseView
|
from service.api.views.common.player_base import PlayerBaseView
|
||||||
|
|
||||||
|
from .openapi import OpenApi
|
||||||
|
|
||||||
class PlayerView(PlayerBaseView):
|
|
||||||
|
class PlayerView(PlayerBaseView, OpenApi):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_description(cls: Type[OpenApi]) -> Optional[str]:
|
||||||
|
return 'Get party players with optional nick filter'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
'name': 'nick',
|
||||||
|
'in': 'query',
|
||||||
|
'description': 'player nick name to filter',
|
||||||
|
'required': False,
|
||||||
|
'type': 'string'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('Player')}}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_summary(cls: Type[OpenApi]) -> Optional[str]:
|
||||||
|
return 'get party players'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_get_tags(cls: Type[OpenApi]) -> List[str]:
|
||||||
|
return ['party']
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
|
||||||
|
return 'Create new party player or remove existing'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> str:
|
||||||
|
return 'PlayerEdit'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('PlayerId')}}}}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
|
||||||
|
return 'add or remove player'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
|
||||||
|
return ['party']
|
||||||
|
|
||||||
async def get(self) -> Response:
|
async def get(self) -> Response:
|
||||||
try:
|
try:
|
||||||
@ -24,7 +79,7 @@ class PlayerView(PlayerBaseView):
|
|||||||
self.request.app.logger.exception('could not get party')
|
self.request.app.logger.exception('could not get party')
|
||||||
return wrap_exception(e, self.request.query)
|
return wrap_exception(e, self.request.query)
|
||||||
|
|
||||||
return wrap_json(party, self.request.query)
|
return wrap_json(party)
|
||||||
|
|
||||||
async def post(self) -> Response:
|
async def post(self) -> Response:
|
||||||
try:
|
try:
|
||||||
@ -49,4 +104,4 @@ class PlayerView(PlayerBaseView):
|
|||||||
self.request.app.logger.exception('could not add loot')
|
self.request.app.logger.exception('could not add loot')
|
||||||
return wrap_exception(e, data)
|
return wrap_exception(e, data)
|
||||||
|
|
||||||
return wrap_json(player_id, data)
|
return wrap_json(player_id)
|
29
src/service/api/views/html/api.py
Normal file
29
src/service/api/views/html/api.py
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
import json
|
||||||
|
|
||||||
|
from aiohttp.web import Response, View
|
||||||
|
from aiohttp_jinja2 import template
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
|
||||||
|
class ApiDocVIew(View):
|
||||||
|
|
||||||
|
async def get(self) -> Response:
|
||||||
|
return Response(
|
||||||
|
text=json.dumps(self.request.app['spec'].to_dict()),
|
||||||
|
status=200,
|
||||||
|
content_type='application/json'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class ApiHtmlView(View):
|
||||||
|
|
||||||
|
@template('api.jinja2')
|
||||||
|
async def get(self) -> Dict[str, Any]:
|
||||||
|
return {}
|
@ -21,6 +21,7 @@ from service.core.party import Party
|
|||||||
|
|
||||||
from .auth import AuthorizationPolicy, authorize_factory
|
from .auth import AuthorizationPolicy, authorize_factory
|
||||||
from .routes import setup_routes
|
from .routes import setup_routes
|
||||||
|
from .spec import get_spec
|
||||||
|
|
||||||
|
|
||||||
async def on_shutdown(app: web.Application) -> None:
|
async def on_shutdown(app: web.Application) -> None:
|
||||||
@ -53,6 +54,7 @@ def setup_service(config: Configuration, database: Database, loot: LootSelector,
|
|||||||
templates_root = app['templates_root'] = config.get('web', 'templates')
|
templates_root = app['templates_root'] = config.get('web', 'templates')
|
||||||
app['static_root_url'] = '/static'
|
app['static_root_url'] = '/static'
|
||||||
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(templates_root))
|
aiohttp_jinja2.setup(app, loader=jinja2.FileSystemLoader(templates_root))
|
||||||
|
app['spec'] = get_spec(app)
|
||||||
|
|
||||||
app.logger.info('setup configuration')
|
app.logger.info('setup configuration')
|
||||||
app['config'] = config
|
app['config'] = config
|
||||||
|
@ -47,7 +47,7 @@ class AriyalaParser:
|
|||||||
def get_ids(self, url: str, job: str) -> Dict[str, int]:
|
def get_ids(self, url: str, job: str) -> Dict[str, int]:
|
||||||
norm_path = os.path.normpath(url)
|
norm_path = os.path.normpath(url)
|
||||||
set_id = os.path.basename(norm_path)
|
set_id = os.path.basename(norm_path)
|
||||||
response = requests.get('{}/store.app'.format(self.ariyala_url), params={'identifier': set_id})
|
response = requests.get(f'{self.ariyala_url}/store.app', params={'identifier': set_id})
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
@ -72,8 +72,7 @@ class AriyalaParser:
|
|||||||
if self.xivapi_key is not None:
|
if self.xivapi_key is not None:
|
||||||
params['private_key'] = self.xivapi_key
|
params['private_key'] = self.xivapi_key
|
||||||
|
|
||||||
response = requests.get('{}/item/{}'.format(self.xivapi_url, item_id),
|
response = requests.get(f'{self.xivapi_url}/item/{item_id}', params=params, timeout=self.request_timeout)
|
||||||
params=params, timeout=self.request_timeout)
|
|
||||||
response.raise_for_status()
|
response.raise_for_status()
|
||||||
data = response.json()
|
data = response.json()
|
||||||
|
|
||||||
|
@ -96,7 +96,7 @@ class Database:
|
|||||||
raise NotImplementedError
|
raise NotImplementedError
|
||||||
|
|
||||||
def migration(self) -> None:
|
def migration(self) -> None:
|
||||||
self.logger.info('perform migrations at {}'.format(self.connection))
|
self.logger.info('perform migrations')
|
||||||
backend = get_backend(self.connection)
|
backend = get_backend(self.connection)
|
||||||
migrations = read_migrations(self.migrations_path)
|
migrations = read_migrations(self.migrations_path)
|
||||||
with backend.lock():
|
with backend.lock():
|
||||||
|
@ -12,16 +12,16 @@ from typing import Any, Mapping
|
|||||||
class InvalidDatabase(Exception):
|
class InvalidDatabase(Exception):
|
||||||
|
|
||||||
def __init__(self, database_type: str) -> None:
|
def __init__(self, database_type: str) -> None:
|
||||||
Exception.__init__(self, 'Unsupported database {}'.format(database_type))
|
Exception.__init__(self, f'Unsupported database {database_type}')
|
||||||
|
|
||||||
|
|
||||||
class InvalidDataRow(Exception):
|
class InvalidDataRow(Exception):
|
||||||
|
|
||||||
def __init__(self, data: Mapping[str, Any]) -> None:
|
def __init__(self, data: Mapping[str, Any]) -> None:
|
||||||
Exception.__init__(self, 'Invalid data row `{}`'.format(data))
|
Exception.__init__(self, f'Invalid data row `{data}`')
|
||||||
|
|
||||||
|
|
||||||
class MissingConfiguration(Exception):
|
class MissingConfiguration(Exception):
|
||||||
|
|
||||||
def __init__(self, section: str) -> None:
|
def __init__(self, section: str) -> None:
|
||||||
Exception.__init__(self, 'Missing configuration section {}'.format(section))
|
Exception.__init__(self, f'Missing configuration section {section}')
|
@ -36,9 +36,7 @@ class PostgresDatabase(Database):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def connection(self) -> str:
|
def connection(self) -> str:
|
||||||
return 'postgresql://{username}:{password}@{host}:{port}/{database}'.format(
|
return f'postgresql://{self.username}:{self.password}@{self.host}:{self.port}/{self.database}'
|
||||||
username=self.username, password=self.password, host=self.host, port=self.port, database=self.database
|
|
||||||
)
|
|
||||||
|
|
||||||
async def init(self) -> None:
|
async def init(self) -> None:
|
||||||
self.pool = await asyncpg.create_pool(host=self.host, port=self.port, username=self.username,
|
self.pool = await asyncpg.create_pool(host=self.host, port=self.port, username=self.username,
|
||||||
|
@ -29,7 +29,7 @@ class SQLiteDatabase(Database):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def connection(self) -> str:
|
def connection(self) -> str:
|
||||||
return 'sqlite:///{path}'.format(path=self.database_path)
|
return f'sqlite:///{self.database_path}'
|
||||||
|
|
||||||
async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
async def delete_piece(self, player_id: PlayerId, piece: Union[Piece, Upgrade]) -> None:
|
||||||
player = await self.get_player(player_id)
|
player = await self.get_player(player_id)
|
||||||
|
16
src/service/models/action.py
Normal file
16
src/service/models/action.py
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from enum import auto
|
||||||
|
|
||||||
|
from .serializable import SerializableEnum
|
||||||
|
|
||||||
|
|
||||||
|
class Action(SerializableEnum):
|
||||||
|
add = auto()
|
||||||
|
remove = auto()
|
@ -9,14 +9,15 @@
|
|||||||
import itertools
|
import itertools
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Dict, List, Optional, Union
|
from typing import Any, Dict, List, Optional, Type, Union
|
||||||
|
|
||||||
from .piece import Piece
|
from .piece import Piece
|
||||||
|
from .serializable import Serializable
|
||||||
from .upgrade import Upgrade
|
from .upgrade import Upgrade
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class BiS:
|
class BiS(Serializable):
|
||||||
weapon: Optional[Piece] = None
|
weapon: Optional[Piece] = None
|
||||||
head: Optional[Piece] = None
|
head: Optional[Piece] = None
|
||||||
body: Optional[Piece] = None
|
body: Optional[Piece] = None
|
||||||
@ -30,13 +31,6 @@ class BiS:
|
|||||||
left_ring: Optional[Piece] = None
|
left_ring: Optional[Piece] = None
|
||||||
right_ring: Optional[Piece] = None
|
right_ring: Optional[Piece] = None
|
||||||
|
|
||||||
def has_piece(self, piece: Union[Piece, Upgrade]) -> bool:
|
|
||||||
if isinstance(piece, Piece):
|
|
||||||
return piece in self.pieces
|
|
||||||
elif isinstance(piece, Upgrade):
|
|
||||||
return self.upgrades_required.get(piece) is not None
|
|
||||||
return False
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pieces(self) -> List[Piece]:
|
def pieces(self) -> List[Piece]:
|
||||||
return [piece for piece in self.__dict__.values() if isinstance(piece, Piece)]
|
return [piece for piece in self.__dict__.values() if isinstance(piece, Piece)]
|
||||||
@ -48,6 +42,66 @@ class BiS:
|
|||||||
for upgrade, pieces in itertools.groupby(self.pieces, lambda piece: piece.upgrade)
|
for upgrade, pieces in itertools.groupby(self.pieces, lambda piece: piece.upgrade)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'weapon': {
|
||||||
|
'description': 'weapon part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'head': {
|
||||||
|
'description': 'head part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'body': {
|
||||||
|
'description': 'body part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'hands': {
|
||||||
|
'description': 'hands part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'waist': {
|
||||||
|
'description': 'waist part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'legs': {
|
||||||
|
'description': 'legs part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'feet': {
|
||||||
|
'description': 'feet part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'ears': {
|
||||||
|
'description': 'ears part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'neck': {
|
||||||
|
'description': 'neck part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'wrist': {
|
||||||
|
'description': 'wrist part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'left_ring': {
|
||||||
|
'description': 'left_ring part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
},
|
||||||
|
'right_ring': {
|
||||||
|
'description': 'right_ring part of BiS',
|
||||||
|
'$ref': cls.model_ref('Piece')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
def has_piece(self, piece: Union[Piece, Upgrade]) -> bool:
|
||||||
|
if isinstance(piece, Piece):
|
||||||
|
return piece in self.pieces
|
||||||
|
elif isinstance(piece, Upgrade):
|
||||||
|
return self.upgrades_required.get(piece) is not None
|
||||||
|
return False
|
||||||
|
|
||||||
def set_item(self, piece: Union[Piece, Upgrade]) -> None:
|
def set_item(self, piece: Union[Piece, Upgrade]) -> None:
|
||||||
setattr(self, piece.name, piece)
|
setattr(self, piece.name, piece)
|
||||||
|
|
||||||
|
36
src/service/models/error.py
Normal file
36
src/service/models/error.py
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from dataclasses import dataclass
|
||||||
|
from typing import Any, Dict, List, Type
|
||||||
|
|
||||||
|
from .serializable import Serializable
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class Error(Serializable):
|
||||||
|
message: str
|
||||||
|
arguments: Dict[str, Any]
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'arguments': {
|
||||||
|
'description': 'arguments passed to request',
|
||||||
|
'type': 'object',
|
||||||
|
'additionalProperties': True
|
||||||
|
},
|
||||||
|
'message': {
|
||||||
|
'description': 'error message',
|
||||||
|
'type': 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_required(cls: Type[Serializable]) -> List[str]:
|
||||||
|
return ['arguments', 'message']
|
@ -8,13 +8,14 @@
|
|||||||
#
|
#
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from enum import Enum, auto
|
from enum import auto
|
||||||
from typing import Tuple
|
from typing import Tuple
|
||||||
|
|
||||||
from .piece import Piece, PieceAccessory, Weapon
|
from .piece import Piece, PieceAccessory, Weapon
|
||||||
|
from .serializable import SerializableEnum
|
||||||
|
|
||||||
|
|
||||||
class Job(Enum):
|
class Job(SerializableEnum):
|
||||||
PLD = auto()
|
PLD = auto()
|
||||||
WAR = auto()
|
WAR = auto()
|
||||||
DRK = auto()
|
DRK = auto()
|
||||||
|
@ -9,15 +9,17 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import Any, List, Mapping, Type, Union
|
from typing import Any, Dict, List, Mapping, Type, Union
|
||||||
|
|
||||||
from .upgrade import Upgrade
|
|
||||||
|
|
||||||
from service.core.exceptions import InvalidDataRow
|
from service.core.exceptions import InvalidDataRow
|
||||||
|
|
||||||
|
from .serializable import Serializable
|
||||||
|
from .upgrade import Upgrade
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class Piece:
|
class Piece(Serializable):
|
||||||
is_tome: bool
|
is_tome: bool
|
||||||
name: str
|
name: str
|
||||||
|
|
||||||
@ -75,6 +77,24 @@ class Piece:
|
|||||||
else:
|
else:
|
||||||
raise InvalidDataRow(data)
|
raise InvalidDataRow(data)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'is_tome': {
|
||||||
|
'description': 'is this piece tome gear or not',
|
||||||
|
'type': 'boolean'
|
||||||
|
},
|
||||||
|
'name': {
|
||||||
|
'description': 'piece name',
|
||||||
|
'required': True,
|
||||||
|
'type': 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_required(cls: Type[Serializable]) -> List[str]:
|
||||||
|
return ['is_tome', 'name']
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PieceAccessory(Piece):
|
class PieceAccessory(Piece):
|
||||||
|
@ -11,22 +11,23 @@ from __future__ import annotations
|
|||||||
import re
|
import re
|
||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from typing import List, Optional, Type, Union
|
from typing import Any, Dict, List, Optional, Type, Union
|
||||||
|
|
||||||
from .bis import BiS
|
from .bis import BiS
|
||||||
from .job import Job
|
from .job import Job
|
||||||
from .piece import Piece
|
from .piece import Piece
|
||||||
|
from .serializable import Serializable
|
||||||
from .upgrade import Upgrade
|
from .upgrade import Upgrade
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlayerId:
|
class PlayerId(Serializable):
|
||||||
job: Job
|
job: Job
|
||||||
nick: str
|
nick: str
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def pretty_name(self) -> str:
|
def pretty_name(self) -> str:
|
||||||
return '{} ({})'.format(self.nick, self.job.name)
|
return f'{self.nick} ({self.job.name})'
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
def from_pretty_name(cls: Type[PlayerId], value: str) -> Optional[PlayerId]:
|
def from_pretty_name(cls: Type[PlayerId], value: str) -> Optional[PlayerId]:
|
||||||
@ -35,6 +36,23 @@ class PlayerId:
|
|||||||
return None
|
return None
|
||||||
return PlayerId(Job[matches.group('job')], matches.group('nick'))
|
return PlayerId(Job[matches.group('job')], matches.group('nick'))
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'job': {
|
||||||
|
'description': 'player job name',
|
||||||
|
'$ref': cls.model_ref('Job')
|
||||||
|
},
|
||||||
|
'nick': {
|
||||||
|
'description': 'player nick name',
|
||||||
|
'type': 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_required(cls: Type[Serializable]) -> List[str]:
|
||||||
|
return ['job', 'nick']
|
||||||
|
|
||||||
def __hash__(self) -> int:
|
def __hash__(self) -> int:
|
||||||
return hash(str(self))
|
return hash(str(self))
|
||||||
|
|
||||||
@ -50,13 +68,7 @@ class PlayerIdWithCounters(PlayerId):
|
|||||||
|
|
||||||
|
|
||||||
@dataclass
|
@dataclass
|
||||||
class PlayerIdFull:
|
class Player(Serializable):
|
||||||
jobs: List[Job]
|
|
||||||
nick: str
|
|
||||||
|
|
||||||
|
|
||||||
@dataclass
|
|
||||||
class Player:
|
|
||||||
job: Job
|
job: Job
|
||||||
nick: str
|
nick: str
|
||||||
bis: BiS
|
bis: BiS
|
||||||
@ -68,6 +80,45 @@ class Player:
|
|||||||
def player_id(self) -> PlayerId:
|
def player_id(self) -> PlayerId:
|
||||||
return PlayerId(self.job, self.nick)
|
return PlayerId(self.job, self.nick)
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'bis': {
|
||||||
|
'description': 'player BiS',
|
||||||
|
'$ref': cls.model_ref('BiS')
|
||||||
|
},
|
||||||
|
'job': {
|
||||||
|
'description': 'player job name',
|
||||||
|
'$ref': cls.model_ref('Job')
|
||||||
|
},
|
||||||
|
'link': {
|
||||||
|
'description': 'link to player BiS',
|
||||||
|
'type': 'string'
|
||||||
|
},
|
||||||
|
'loot': {
|
||||||
|
'description': 'player looted items',
|
||||||
|
'type': 'array',
|
||||||
|
'items': {
|
||||||
|
'anyOf': [
|
||||||
|
{'$ref': cls.model_ref('Piece')},
|
||||||
|
{'$ref': cls.model_ref('Upgrade')}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
'nick': {
|
||||||
|
'description': 'player nick name',
|
||||||
|
'type': 'string'
|
||||||
|
},
|
||||||
|
'priority': {
|
||||||
|
'description': 'player loot priority',
|
||||||
|
'type': 'integer'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_required(cls: Type[Serializable]) -> List[str]:
|
||||||
|
return ['bis', 'job', 'loot', 'nick', 'priority']
|
||||||
|
|
||||||
def player_id_with_counters(self, piece: Union[Piece, Upgrade, None]) -> PlayerIdWithCounters:
|
def player_id_with_counters(self, piece: Union[Piece, Upgrade, None]) -> PlayerIdWithCounters:
|
||||||
return PlayerIdWithCounters(self.job, self.nick, self.is_required(piece), self.priority,
|
return PlayerIdWithCounters(self.job, self.nick, self.is_required(piece), self.priority,
|
||||||
abs(self.loot_count(piece)), abs(self.loot_count_bis(piece)),
|
abs(self.loot_count(piece)), abs(self.loot_count_bis(piece)),
|
||||||
|
35
src/service/models/player_edit.py
Normal file
35
src/service/models/player_edit.py
Normal file
@ -0,0 +1,35 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from typing import Any, Dict, List, Type
|
||||||
|
|
||||||
|
from .serializable import Serializable
|
||||||
|
|
||||||
|
|
||||||
|
class PlayerEdit(Serializable):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'action': {
|
||||||
|
'description': 'action to perform',
|
||||||
|
'$ref': cls.model_ref('Action')
|
||||||
|
},
|
||||||
|
'job': {
|
||||||
|
'description': 'player job name to edit',
|
||||||
|
'$ref': cls.model_ref('Job')
|
||||||
|
},
|
||||||
|
'nick': {
|
||||||
|
'description': 'player nick name to edit',
|
||||||
|
'type': 'string'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_required(cls: Type[Serializable]) -> List[str]:
|
||||||
|
return ['action', 'nick', 'job']
|
57
src/service/models/serializable.py
Normal file
57
src/service/models/serializable.py
Normal file
@ -0,0 +1,57 @@
|
|||||||
|
#
|
||||||
|
# Copyright (c) 2019 Evgeniy Alekseev.
|
||||||
|
#
|
||||||
|
# This file is part of ffxivbis
|
||||||
|
# (see https://github.com/arcan1s/ffxivbis).
|
||||||
|
#
|
||||||
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
|
#
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from enum import Enum
|
||||||
|
from typing import Any, Dict, List, Type
|
||||||
|
|
||||||
|
|
||||||
|
class Serializable:
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_name(cls: Type[Serializable]) -> str:
|
||||||
|
return cls.__name__
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
|
||||||
|
raise NotImplementedError
|
||||||
|
|
||||||
|
@staticmethod
|
||||||
|
def model_ref(model_name: str, model_group: str = 'schemas') -> str:
|
||||||
|
return f'#/components/{model_group}/{model_name}'
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_required(cls: Type[Serializable]) -> List[str]:
|
||||||
|
return []
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_spec(cls: Type[Serializable]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'type': cls.model_type(),
|
||||||
|
'properties': cls.model_properties(),
|
||||||
|
'required': cls.model_required()
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_type(cls: Type[Serializable]) -> str:
|
||||||
|
return 'object'
|
||||||
|
|
||||||
|
|
||||||
|
class SerializableEnum(Serializable, Enum):
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_spec(cls: Type[SerializableEnum]) -> Dict[str, Any]:
|
||||||
|
return {
|
||||||
|
'type': cls.model_type(),
|
||||||
|
'enum': [item.name for item in cls]
|
||||||
|
}
|
||||||
|
|
||||||
|
@classmethod
|
||||||
|
def model_type(cls: Type[Serializable]) -> str:
|
||||||
|
return 'string'
|
@ -6,11 +6,13 @@
|
|||||||
#
|
#
|
||||||
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
|
||||||
#
|
#
|
||||||
from enum import Enum, auto
|
from enum import auto
|
||||||
from typing import List
|
from typing import List
|
||||||
|
|
||||||
|
from .serializable import SerializableEnum
|
||||||
|
|
||||||
class Upgrade(Enum):
|
|
||||||
|
class Upgrade(SerializableEnum):
|
||||||
NoUpgrade = auto()
|
NoUpgrade = auto()
|
||||||
AccessoryUpgrade = auto()
|
AccessoryUpgrade = auto()
|
||||||
GearUpgrade = auto()
|
GearUpgrade = auto()
|
||||||
|
24
templates/api.jinja2
Normal file
24
templates/api.jinja2
Normal file
@ -0,0 +1,24 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
<head>
|
||||||
|
<title>ReDoc</title>
|
||||||
|
<!-- needed for adaptive design -->
|
||||||
|
<meta charset="utf-8"/>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<link href="https://fonts.googleapis.com/css?family=Montserrat:300,400,700|Roboto:300,400,700" rel="stylesheet">
|
||||||
|
|
||||||
|
<!--
|
||||||
|
ReDoc doesn't change outer page styles
|
||||||
|
-->
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<redoc spec-url='/api-docs/swagger.json'></redoc>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/redoc@next/bundles/redoc.standalone.js"> </script>
|
||||||
|
</body>
|
||||||
|
</html>
|
@ -14,7 +14,7 @@ async def test_bis_get(server: Any, party: Party, player: Player, player2: Playe
|
|||||||
|
|
||||||
response = await server.get('/api/v1/party/bis')
|
response = await server.get('/api/v1/party/bis')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([weapon, weapon, head_with_upgrade], {}, 200)
|
assert await response.text() == make_json([weapon, weapon, head_with_upgrade])
|
||||||
|
|
||||||
|
|
||||||
async def test_bis_get_with_filter(server: Any, party: Party, player: Player, player2: Player,
|
async def test_bis_get_with_filter(server: Any, party: Party, player: Player, player2: Player,
|
||||||
@ -25,11 +25,11 @@ async def test_bis_get_with_filter(server: Any, party: Party, player: Player, pl
|
|||||||
|
|
||||||
response = await server.get('/api/v1/party/bis', params={'nick': player.nick})
|
response = await server.get('/api/v1/party/bis', params={'nick': player.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([weapon], {'nick': player.nick}, 200)
|
assert await response.text() == make_json([weapon])
|
||||||
|
|
||||||
response = await server.get('/api/v1/party/bis', params={'nick': player2.nick})
|
response = await server.get('/api/v1/party/bis', params={'nick': player2.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([weapon, head_with_upgrade], {'nick': player2.nick}, 200)
|
assert await response.text() == make_json([weapon, head_with_upgrade])
|
||||||
|
|
||||||
|
|
||||||
async def test_bis_post_add(server: Any, player: Player, head_with_upgrade: Piece) -> None:
|
async def test_bis_post_add(server: Any, player: Player, head_with_upgrade: Piece) -> None:
|
||||||
|
@ -12,7 +12,7 @@ async def test_loot_get(server: Any, party: Party, player: Player, player2: Play
|
|||||||
|
|
||||||
response = await server.get('/api/v1/party/loot')
|
response = await server.get('/api/v1/party/loot')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([weapon, weapon], {}, 200)
|
assert await response.text() == make_json([weapon, weapon])
|
||||||
|
|
||||||
|
|
||||||
async def test_loot_get_with_filter(server: Any, party: Party, player: Player, player2: Player, weapon: Piece) -> None:
|
async def test_loot_get_with_filter(server: Any, party: Party, player: Player, player2: Player, weapon: Piece) -> None:
|
||||||
@ -21,17 +21,17 @@ async def test_loot_get_with_filter(server: Any, party: Party, player: Player, p
|
|||||||
|
|
||||||
response = await server.get('/api/v1/party/loot', params={'nick': player.nick})
|
response = await server.get('/api/v1/party/loot', params={'nick': player.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([weapon], {'nick': player.nick}, 200)
|
assert await response.text() == make_json([weapon])
|
||||||
|
|
||||||
response = await server.get('/api/v1/party/loot', params={'nick': player2.nick})
|
response = await server.get('/api/v1/party/loot', params={'nick': player2.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([weapon], {'nick': player2.nick}, 200)
|
assert await response.text() == make_json([weapon])
|
||||||
|
|
||||||
|
|
||||||
async def test_loot_post_add(server: Any, player: Player, weapon: Piece) -> None:
|
async def test_loot_post_add(server: Any, player: Player, weapon: Piece) -> None:
|
||||||
response = await server.get('/api/v1/party/loot')
|
response = await server.get('/api/v1/party/loot')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([], {}, 200)
|
assert await response.text() == make_json([])
|
||||||
assert weapon not in player.loot
|
assert weapon not in player.loot
|
||||||
|
|
||||||
response = await server.post('/api/v1/party/loot', json={
|
response = await server.post('/api/v1/party/loot', json={
|
||||||
@ -82,7 +82,5 @@ async def test_loot_put(server: Any, player: Player, player2: Player, head_with_
|
|||||||
})
|
})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json(
|
assert await response.text() == make_json(
|
||||||
[player2.player_id_with_counters(head_with_upgrade), player.player_id_with_counters(head_with_upgrade)],
|
[player2.player_id_with_counters(head_with_upgrade), player.player_id_with_counters(head_with_upgrade)]
|
||||||
{'is_tome': head_with_upgrade.is_tome, 'piece': head_with_upgrade.name},
|
|
||||||
200
|
|
||||||
)
|
)
|
||||||
|
@ -11,7 +11,7 @@ async def test_players_get(server: Any, party: Party, player: Player) -> None:
|
|||||||
|
|
||||||
response = await server.get('/api/v1/party')
|
response = await server.get('/api/v1/party')
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json(party.party, {}, 200)
|
assert await response.text() == make_json(party.party)
|
||||||
|
|
||||||
|
|
||||||
async def test_players_get_with_filter(server: Any, party: Party, player: Player, player2: Player) -> None:
|
async def test_players_get_with_filter(server: Any, party: Party, player: Player, player2: Player) -> None:
|
||||||
@ -19,11 +19,11 @@ async def test_players_get_with_filter(server: Any, party: Party, player: Player
|
|||||||
|
|
||||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([player], {'nick': player.nick}, 200)
|
assert await response.text() == make_json([player])
|
||||||
|
|
||||||
response = await server.get('/api/v1/party', params={'nick': player2.nick})
|
response = await server.get('/api/v1/party', params={'nick': player2.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([player2], {'nick': player2.nick}, 200)
|
assert await response.text() == make_json([player2])
|
||||||
|
|
||||||
|
|
||||||
async def test_players_post_add(server: Any, party: Party, player: Player) -> None:
|
async def test_players_post_add(server: Any, party: Party, player: Player) -> None:
|
||||||
@ -31,7 +31,7 @@ async def test_players_post_add(server: Any, party: Party, player: Player) -> No
|
|||||||
|
|
||||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([], {'nick': player.nick}, 200)
|
assert await response.text() == make_json([])
|
||||||
|
|
||||||
response = await server.post('/api/v1/party', json={
|
response = await server.post('/api/v1/party', json={
|
||||||
'action': 'add',
|
'action': 'add',
|
||||||
@ -46,7 +46,7 @@ async def test_players_post_add(server: Any, party: Party, player: Player) -> No
|
|||||||
async def test_players_post_remove(server: Any, party: Party, player: Player) -> None:
|
async def test_players_post_remove(server: Any, party: Party, player: Player) -> None:
|
||||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([player], {'nick': player.nick}, 200)
|
assert await response.text() == make_json([player])
|
||||||
|
|
||||||
response = await server.post('/api/v1/party', json={
|
response = await server.post('/api/v1/party', json={
|
||||||
'action': 'remove',
|
'action': 'remove',
|
||||||
@ -57,7 +57,7 @@ async def test_players_post_remove(server: Any, party: Party, player: Player) ->
|
|||||||
|
|
||||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([], {'nick': player.nick}, 200)
|
assert await response.text() == make_json([])
|
||||||
|
|
||||||
assert player.player_id not in party.players
|
assert player.player_id not in party.players
|
||||||
|
|
||||||
@ -68,7 +68,7 @@ async def test_players_post_add_with_link(server: Any, party: Party, player: Pla
|
|||||||
|
|
||||||
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
response = await server.get('/api/v1/party', params={'nick': player.nick})
|
||||||
assert response.status == 200
|
assert response.status == 200
|
||||||
assert await response.text() == make_json([], {'nick': player.nick}, 200)
|
assert await response.text() == make_json([])
|
||||||
|
|
||||||
response = await server.post('/api/v1/party', json={
|
response = await server.post('/api/v1/party', json={
|
||||||
'action': 'add',
|
'action': 'add',
|
||||||
|
Loading…
Reference in New Issue
Block a user