complete swagger

This commit is contained in:
Evgenii Alekseev 2019-09-15 01:07:15 +03:00
parent c83f36f40b
commit 80ea82904b
14 changed files with 515 additions and 114 deletions

104
README.md
View File

@ -2,104 +2,30 @@
Service which allows to manage savage loot distribution easy.
## REST API
## Installation and usage
### Party API
This service requires python >= 3.7. For other dependencies see `setup.py`.
* `GET /api/v1/party`
In general installation process looks like:
Get party list. Parameters:
```bash
python setup.py build
python setup.py test # if you want to run tests
```
* `nick`: player full nickname to filter, string, optional.
Service can be run from `src` directory by using command:
* `POST /api/v1/party`
```bash
python -m service.application.application
```
Add or remove party member. Parameters:
To see all available options type `--help`.
* `action`: action to do, string, required. One of `add`, `remove`.
* `job`: player job name, string, required.
* `nick`: player nickname, string, required.
* `link`: link to ariyala set to parse BiS, string, optional.
## Web service
### BiS API
* `GET /api/v1/party/bis`
Get party/player BiS. Parameters:
* `nick`: player full nickname to filter, string, optional.
* `POST /api/v1/party/bis`
Add or remove item to/from BiS. Parameters:
* `action`: action to do, string, required. One of `add`, `remove`.
* `job`: player job name, string, required.
* `nick`: player nickname, string, required.
* `is_tome`: is item tome gear or not, bool, required.
* `piece`: item name, string, required.
* `PUT /api/v1/party/bis`
Create BiS from ariyala link. Parameters:
* `job`: player job name, string, required.
* `nick`: player nickname, string, required.
* `link`: link to ariyala set to parse BiS, string, required.
### Loot API
* `GET /api/v1/party/loot`
Get party/player loot. Parameters:
* `nick`: player full nickname to filter, string, optional.
* `POST /api/v1/party/loot`
Add or remove item to/from loot list. Parameters:
* `action`: action to do, string, required. One of `add`, `remove`.
* `job`: player job name, string, required.
* `nick`: player nickname, string, required.
* `is_tome`: is item tome gear or not, bool, required.
* `piece`: item name, string, required.
* `PUT /api/v1/party/loot`
Suggest players to get loot. Parameters:
* `is_tome`: is item tome gear or not, bool, required.
* `piece`: item name, string, required.
### Users API
* `DELETE /api/v1/login/{username}`
Delete user with specified username. Parameters:
* `username`: username to remove, required.
* `POST /api/v1/login`
Login with credentials. Parameters:
* `username`: username to login, string, required.
* `password`: password to login, string, required.
* `PUT /api/v1/login`
Create new user. Parameters:
* `username`: username to login, string, required.
* `password`: password to login, string,
* `permission`: user permission, one of `get`, `post`, optional, default `get`.
* `POST /api/v1/logout`
Logout.
REST API documentation is available at `http://0.0.0.0:8000/api-docs`. HTML representation is available at `http://0.0.0.0:8000`.
*Note*: host and port depend on configuration settings.
## Configuration

View File

@ -11,13 +11,15 @@ 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.bis import BiS, BiSLink
from service.models.error import Error
from service.models.job import Job
from service.models.loot import Loot
from service.models.piece import Piece
from service.models.player import Player, PlayerId
from service.models.player import Player, PlayerId, PlayerIdWithCounters
from service.models.player_edit import PlayerEdit
from service.models.upgrade import Upgrade
from service.models.user import User
def get_spec(app: Application) -> APISpec:
@ -45,13 +47,17 @@ def get_spec(app: Application) -> APISpec:
# components
spec.components.schema(Action.model_name(), Action.model_spec())
spec.components.schema(BiS.model_name(), BiS.model_spec())
spec.components.schema(BiSLink.model_name(), BiSLink.model_spec())
spec.components.schema(Error.model_name(), Error.model_spec())
spec.components.schema(Job.model_name(), Job.model_spec())
spec.components.schema(Loot.model_name(), Loot.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(PlayerIdWithCounters.model_name(), PlayerIdWithCounters.model_spec())
spec.components.schema(Upgrade.model_name(), Upgrade.model_spec())
spec.components.schema(User.model_name(), User.model_spec())
# default responses
spec.components.response('BadRequest', dict(

View File

@ -7,6 +7,7 @@
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
from aiohttp.web import Response
from typing import Any, Dict, List, Optional, Type
from service.models.job import Job
from service.models.piece import Piece
@ -15,8 +16,92 @@ from service.models.player import PlayerId
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json
from service.api.views.common.bis_base import BiSBaseView
from .openapi import OpenApi
class BiSView(BiSBaseView):
class BiSView(BiSBaseView, OpenApi):
@classmethod
def endpoint_get_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Get party players BiS items'
@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': {
'type': 'array',
'items': {
'allOf': [{'$ref': cls.model_ref('Piece')}]
}}}}}
}
@classmethod
def endpoint_get_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'get party BiS items'
@classmethod
def endpoint_get_tags(cls: Type[OpenApi]) -> List[str]:
return ['BiS']
@classmethod
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Add new item to player BiS or remove existing'
@classmethod
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return ['Piece', 'PlayerEdit']
@classmethod
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('Loot')}}}}
}
@classmethod
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'edit BiS'
@classmethod
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
return ['BiS']
@classmethod
def endpoint_put_consumes(cls: Type[OpenApi]) -> List[str]:
return ['application/json']
@classmethod
def endpoint_put_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Generate new BiS set'
@classmethod
def endpoint_put_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return ['BiSLink']
@classmethod
def endpoint_put_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('BiS')}}}}
}
@classmethod
def endpoint_put_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'update BiS'
@classmethod
def endpoint_put_tags(cls: Type[OpenApi]) -> List[str]:
return ['BiS']
async def get(self) -> Response:
try:
@ -34,7 +119,7 @@ class BiSView(BiSBaseView):
except Exception:
data = dict(await self.request.post())
required = ['action', 'is_tome', 'job', 'nick', 'piece']
required = ['action', 'is_tome', 'job', 'name', 'nick']
if any(param not in data for param in required):
return wrap_invalid_param(required, data)
@ -65,10 +150,10 @@ class BiSView(BiSBaseView):
try:
player_id = PlayerId(Job[data['job']], data['nick'])
link = await self.bis_put(player_id, data['link'])
bis = await self.bis_put(player_id, data['link'])
except Exception as e:
self.request.app.logger.exception('could not parse bis')
return wrap_exception(e, data)
return wrap_json({'link': link})
return wrap_json(bis)

View File

@ -7,12 +7,89 @@
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
from aiohttp.web import Response
from typing import Any, Dict, List, Optional, Type
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json
from service.api.views.common.login_base import LoginBaseView
from .openapi import OpenApi
class LoginView(LoginBaseView):
class LoginView(LoginBaseView, OpenApi):
@classmethod
def endpoint_delete_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Delete registered user'
@classmethod
def endpoint_delete_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
return [
{
'name': 'username',
'in': 'path',
'description': 'username to remove',
'required': True,
'type': 'string'
}
]
@classmethod
def endpoint_delete_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {
'200': {'content': {'application/json': {'type': 'object'}}}
}
@classmethod
def endpoint_delete_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'delete user'
@classmethod
def endpoint_delete_tags(cls: Type[OpenApi]) -> List[str]:
return ['users']
@classmethod
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Login as user'
@classmethod
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return ['User']
@classmethod
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {
'200': {'content': {'application/json': {'type': 'object'}}}
}
@classmethod
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'login'
@classmethod
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
return ['users']
@classmethod
def endpoint_put_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Create new user'
@classmethod
def endpoint_put_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return ['User']
@classmethod
def endpoint_put_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {
'200': {'content': {'application/json': {'type': 'object'}}}
}
@classmethod
def endpoint_put_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'create user'
@classmethod
def endpoint_put_tags(cls: Type[OpenApi]) -> List[str]:
return ['users']
async def delete(self) -> Response:
username = self.request.match_info['username']

View File

@ -4,12 +4,37 @@
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
from aiohttp.web import Response
from typing import Any, Dict, List, Optional, Type
from service.api.utils import wrap_exception, wrap_json
from service.api.views.common.login_base import LoginBaseView
from .openapi import OpenApi
class LogoutView(LoginBaseView):
class LogoutView(LoginBaseView, OpenApi):
@classmethod
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Logout'
@classmethod
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return []
@classmethod
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {
'200': {'content': {'application/json': {'type': 'object'}}}
}
@classmethod
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'logout'
@classmethod
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
return ['users']
async def post(self) -> Response:
try:

View File

@ -7,6 +7,7 @@
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
from aiohttp.web import Response
from typing import Any, Dict, List, Optional, Type
from service.models.job import Job
from service.models.piece import Piece
@ -15,8 +16,92 @@ from service.models.player import PlayerId
from service.api.utils import wrap_exception, wrap_invalid_param, wrap_json
from service.api.views.common.loot_base import LootBaseView
from .openapi import OpenApi
class LootView(LootBaseView):
class LootView(LootBaseView, OpenApi):
@classmethod
def endpoint_get_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Get party players loot'
@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': {
'type': 'array',
'items': {
'allOf': [{'$ref': cls.model_ref('Piece')}]
}}}}}
}
@classmethod
def endpoint_get_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'get party loot'
@classmethod
def endpoint_get_tags(cls: Type[OpenApi]) -> List[str]:
return ['loot']
@classmethod
def endpoint_post_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Add new loot item or remove existing'
@classmethod
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return ['Piece', 'PlayerEdit']
@classmethod
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {
'200': {'content': {'application/json': {'schema': {'$ref': cls.model_ref('Loot')}}}}
}
@classmethod
def endpoint_post_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'edit loot'
@classmethod
def endpoint_post_tags(cls: Type[OpenApi]) -> List[str]:
return ['loot']
@classmethod
def endpoint_put_description(cls: Type[OpenApi]) -> Optional[str]:
return 'Suggest loot to party member'
@classmethod
def endpoint_put_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return ['Piece']
@classmethod
def endpoint_put_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {
'200': {'content': {'application/json': {'schema': {
'type': 'array',
'items': {
'allOf': [{'$ref': cls.model_ref('PlayerIdWithCounters')}]
}}}}}
}
@classmethod
def endpoint_put_summary(cls: Type[OpenApi]) -> Optional[str]:
return 'suggest loot'
@classmethod
def endpoint_put_tags(cls: Type[OpenApi]) -> List[str]:
return ['loot']
async def get(self) -> Response:
try:
@ -34,7 +119,7 @@ class LootView(LootBaseView):
except Exception:
data = dict(await self.request.post())
required = ['action', 'is_tome', 'job', 'nick', 'piece']
required = ['action', 'is_tome', 'job', 'name', 'nick']
if any(param not in data for param in required):
return wrap_invalid_param(required, data)
@ -59,7 +144,7 @@ class LootView(LootBaseView):
except Exception:
data = dict(await self.request.post())
required = ['is_tome', 'piece']
required = ['is_tome', 'name']
if any(param not in data for param in required):
return wrap_invalid_param(required, data)

View File

@ -16,9 +16,38 @@ from service.models.serializable import Serializable
class OpenApi(Serializable):
@classmethod
def endpoint_delete_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
def endpoint_delete_description(cls: Type[OpenApi]) -> Optional[str]:
return None
@classmethod
def endpoint_delete_parameters(cls: Type[OpenApi]) -> List[Dict[str, Any]]:
return []
@classmethod
def endpoint_delete_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {}
@classmethod
def endpoint_delete_summary(cls: Type[OpenApi]) -> Optional[str]:
return None
@classmethod
def endpoint_delete_tags(cls: Type[OpenApi]) -> List[str]:
return []
@classmethod
def endpoint_delete_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
description = cls.endpoint_delete_description()
if description is None:
return {}
return {
'description': description,
'parameters': cls.endpoint_delete_parameters(),
'responses': cls.endpoint_with_default_responses(cls.endpoint_delete_responses()),
'summary': cls.endpoint_delete_summary(),
'tags': cls.endpoint_delete_tags()
}
@classmethod
def endpoint_get_description(cls: Type[OpenApi]) -> Optional[str]:
return None
@ -61,8 +90,8 @@ class OpenApi(Serializable):
return None
@classmethod
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> str:
return ''
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return []
@classmethod
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
@ -87,7 +116,10 @@ class OpenApi(Serializable):
'requestBody': {
'content': {
content_type: {
'schema': {'$ref': cls.model_ref(cls.endpoint_post_request_body(content_type))}
'schema': {'allOf': [
{'$ref': cls.model_ref(ref)}
for ref in cls.endpoint_post_request_body(content_type)
]}
}
for content_type in cls.endpoint_post_consumes()
}
@ -97,6 +129,54 @@ class OpenApi(Serializable):
'tags': cls.endpoint_post_tags()
}
@classmethod
def endpoint_put_consumes(cls: Type[OpenApi]) -> List[str]:
return ['application/json']
@classmethod
def endpoint_put_description(cls: Type[OpenApi]) -> Optional[str]:
return None
@classmethod
def endpoint_put_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return []
@classmethod
def endpoint_put_responses(cls: Type[OpenApi]) -> Dict[str, Any]:
return {}
@classmethod
def endpoint_put_summary(cls: Type[OpenApi]) -> Optional[str]:
return None
@classmethod
def endpoint_put_tags(cls: Type[OpenApi]) -> List[str]:
return []
@classmethod
def endpoint_put_spec(cls: Type[OpenApi]) -> Dict[str, Any]:
description = cls.endpoint_put_description()
if description is None:
return {}
return {
'consumes': cls.endpoint_put_consumes(),
'description': description,
'requestBody': {
'content': {
content_type: {
'schema': {'allOf': [
{'$ref': cls.model_ref(ref)}
for ref in cls.endpoint_put_request_body(content_type)
]}
}
for content_type in cls.endpoint_put_consumes()
}
},
'responses': cls.endpoint_with_default_responses(cls.endpoint_put_responses()),
'summary': cls.endpoint_put_summary(),
'tags': cls.endpoint_put_tags()
}
@classmethod
def endpoint_spec(cls: Type[OpenApi], operations: List[str]) -> Dict[str, Any]:
return {

View File

@ -54,8 +54,8 @@ class PlayerView(PlayerBaseView, OpenApi):
return 'Create new party player or remove existing'
@classmethod
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> str:
return 'PlayerEdit'
def endpoint_post_request_body(cls: Type[OpenApi], content_type: str) -> List[str]:
return ['PlayerEdit']
@classmethod
def endpoint_post_responses(cls: Type[OpenApi]) -> Dict[str, Any]:

View File

@ -10,6 +10,7 @@ from aiohttp.web import View
from typing import List, Optional
from service.core.ariyala_parser import AriyalaParser
from service.models.bis import BiS
from service.models.piece import Piece
from service.models.player import PlayerId
@ -35,13 +36,13 @@ class BiSBaseView(View):
return await self.bis_remove(player_id, piece)
return None
async def bis_put(self, player_id: PlayerId, link: str) -> str:
async def bis_put(self, player_id: PlayerId, link: str) -> BiS:
parser = AriyalaParser(self.request.app['config'])
items = parser.get(link, player_id.job.name)
for piece in items:
await self.request.app['party'].set_item_bis(player_id, piece)
await self.request.app['party'].set_bis_link(player_id, link)
return link
return self.request.app['party'].players[player_id].bis
async def bis_remove(self, player_id: PlayerId, piece: Piece) -> Piece:
await self.request.app['party'].remove_item_bis(player_id, piece)

View File

@ -11,11 +11,42 @@ import itertools
from dataclasses import dataclass
from typing import Any, Dict, List, Optional, Type, Union
from .job import Job
from .piece import Piece
from .serializable import Serializable
from .upgrade import Upgrade
@dataclass
class BiSLink(Serializable):
nick: str
job: Job
link: str
@classmethod
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
return {
'job': {
'description': 'player job name',
'$ref': cls.model_ref('Job')
},
'link': {
'description': 'link to BiS set',
'example': 'https://ffxiv.ariyala.com/19V5R',
'type': 'string'
},
'nick': {
'description': 'player nick name',
'example': 'Siuan Sanche',
'type': 'string'
}
}
@classmethod
def model_required(cls: Type[Serializable]) -> List[str]:
return ['job', 'link', 'nick']
@dataclass
class BiS(Serializable):
weapon: Optional[Piece] = None

View File

@ -7,13 +7,31 @@
# License: 3-clause BSD, see https://opensource.org/licenses/BSD-3-Clause
#
from dataclasses import dataclass
from typing import Union
from typing import Any, Dict, List, Type, Union
from .piece import Piece
from .serializable import Serializable
from .upgrade import Upgrade
@dataclass
class Loot:
class Loot(Serializable):
player_id: int
piece: Union[Piece, Upgrade]
@classmethod
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
return {
'piece': {
'description': 'player piece',
'$ref': cls.model_ref('Piece')
},
'player_id': {
'description': 'player identifier',
'$ref': cls.model_ref('PlayerId')
}
}
@classmethod
def model_required(cls: Type[Serializable]) -> List[str]:
return ['piece', 'player_id']

View File

@ -86,7 +86,6 @@ class Piece(Serializable):
},
'name': {
'description': 'piece name',
'required': True,
'type': 'string'
}
}

View File

@ -66,6 +66,48 @@ class PlayerIdWithCounters(PlayerId):
loot_count_total: int
bis_count_total: int
@classmethod
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
return {
'bis_count_total': {
'description': 'total savage pieces in BiS',
'type': 'integer'
},
'is_required': {
'description': 'is item required by BiS or not',
'type': 'boolean'
},
'job': {
'description': 'player job name',
'$ref': cls.model_ref('Job')
},
'loot_count': {
'description': 'count of this item which was already looted',
'type': 'integer'
},
'loot_count_bis': {
'description': 'count of BiS items which were already looted',
'type': 'integer'
},
'loot_count_total': {
'description': 'total count of items which were looted',
'type': 'integer'
},
'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_count_total', 'is_required', 'job', 'loot_count',
'loot_count_bis', 'loot_count_total', 'nick', 'priority']
@dataclass
class Player(Serializable):

View File

@ -7,10 +7,36 @@
# 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 User:
class User(Serializable):
username: str
password: str
permission: str
@classmethod
def model_properties(cls: Type[Serializable]) -> Dict[str, Any]:
return {
'password': {
'description': 'user password',
'type': 'string'
},
'permission': {
'default': 'get',
'description': 'user action permissions',
'type': 'string',
'enum': ['admin', 'get', 'post']
},
'username': {
'description': 'user name',
'type': 'string'
}
}
@classmethod
def model_required(cls: Type[Serializable]) -> List[str]:
return ['password', 'username']